tangled
alpha
login
or
join now
treethought.xyz
/
obsidian-atmark
8
fork
atom
AT protocol bookmarking platforms in obsidian
8
fork
atom
overview
issues
pulls
pipelines
add cards view
treethought
2 months ago
7c3629c3
f7cfb169
+593
-6
5 changed files
expand all
collapse all
unified
split
src
lib.ts
main.ts
views
cards.ts
collections.ts
styles.css
+20
src/lib.ts
···
26
26
},
27
27
});
28
28
}
29
29
+
30
30
+
export async function getCollectionLinks(client: Client, repo: string) {
31
31
+
return await client.get("com.atproto.repo.listRecords", {
32
32
+
params: {
33
33
+
repo: repo as ActorIdentifier,
34
34
+
collection: "network.cosmik.collectionLink" as Nsid,
35
35
+
limit: 100,
36
36
+
},
37
37
+
});
38
38
+
}
39
39
+
40
40
+
export async function getRecord(client: Client, repo: string, collection: string, rkey: string) {
41
41
+
return await client.get("com.atproto.repo.getRecord", {
42
42
+
params: {
43
43
+
repo: repo as ActorIdentifier,
44
44
+
collection: collection as Nsid,
45
45
+
rkey,
46
46
+
},
47
47
+
});
48
48
+
}
+22
src/main.ts
···
4
4
import { createAuthenticatedClient, createPublicClient } from "./auth";
5
5
import { getCollections } from "./lib";
6
6
import { SembleCollectionsView, VIEW_TYPE_SEMBLE_COLLECTIONS } from "views/collections";
7
7
+
import { SembleCardsView, VIEW_TYPE_SEMBLE_CARDS } from "views/cards";
7
8
8
9
export default class MyPlugin extends Plugin {
9
10
settings: AtProtoSettings = DEFAULT_SETTINGS;
···
17
18
return new SembleCollectionsView(leaf, this);
18
19
});
19
20
21
21
+
this.registerView(VIEW_TYPE_SEMBLE_CARDS, (leaf) => {
22
22
+
return new SembleCardsView(leaf, this);
23
23
+
});
24
24
+
20
25
21
26
this.addCommand({
22
27
id: "list-collections",
···
28
33
id: "view-semble-collections",
29
34
name: "View Semble Collections",
30
35
callback: () => this.activateView(VIEW_TYPE_SEMBLE_COLLECTIONS),
36
36
+
});
37
37
+
38
38
+
this.addCommand({
39
39
+
id: "view-semble-cards",
40
40
+
name: "View Semble Cards",
41
41
+
callback: () => this.activateView(VIEW_TYPE_SEMBLE_CARDS),
31
42
});
32
43
33
44
this.addSettingTab(new SettingTab(this.app, this));
···
99
110
if (leaf) {
100
111
workspace.revealLeaf(leaf);
101
112
}
113
113
+
}
114
114
+
115
115
+
async openCollection(uri: string, name: string) {
116
116
+
const { workspace } = this.app;
117
117
+
const leaf = workspace.getLeaf("tab");
118
118
+
await leaf.setViewState({ type: VIEW_TYPE_SEMBLE_CARDS, active: true });
119
119
+
120
120
+
const view = leaf.view as SembleCardsView;
121
121
+
view.setCollection(uri, name);
122
122
+
123
123
+
workspace.revealLeaf(leaf);
102
124
}
103
125
104
126
async loadSettings() {
+245
src/views/cards.ts
···
1
1
+
import { ItemView, WorkspaceLeaf, setIcon } from "obsidian";
2
2
+
import type MyPlugin from "../main";
3
3
+
import { getCollections, getCollectionLinks, getCards } from "../lib";
4
4
+
import type { Main as Card, NoteContent, UrlContent } from "../lexicons/types/network/cosmik/card";
5
5
+
import type { Main as CollectionLink } from "../lexicons/types/network/cosmik/collectionLink";
6
6
+
import type { Main as Collection } from "../lexicons/types/network/cosmik/collection";
7
7
+
import { VIEW_TYPE_SEMBLE_COLLECTIONS } from "./collections";
8
8
+
9
9
+
export const VIEW_TYPE_SEMBLE_CARDS = "semble-cards-view";
10
10
+
11
11
+
interface CardRecord {
12
12
+
uri: string;
13
13
+
cid: string;
14
14
+
value: Card;
15
15
+
}
16
16
+
17
17
+
interface CollectionLinkRecord {
18
18
+
uri: string;
19
19
+
value: CollectionLink;
20
20
+
}
21
21
+
22
22
+
interface CollectionRecord {
23
23
+
uri: string;
24
24
+
value: Collection;
25
25
+
}
26
26
+
27
27
+
export class SembleCardsView extends ItemView {
28
28
+
plugin: MyPlugin;
29
29
+
collectionUri: string | null = null;
30
30
+
collectionName: string = "All Cards";
31
31
+
32
32
+
constructor(leaf: WorkspaceLeaf, plugin: MyPlugin) {
33
33
+
super(leaf);
34
34
+
this.plugin = plugin;
35
35
+
}
36
36
+
37
37
+
getViewType() {
38
38
+
return VIEW_TYPE_SEMBLE_CARDS;
39
39
+
}
40
40
+
41
41
+
getDisplayText() {
42
42
+
return this.collectionName;
43
43
+
}
44
44
+
45
45
+
getIcon() {
46
46
+
return "layers";
47
47
+
}
48
48
+
49
49
+
setCollection(uri: string | null, name: string) {
50
50
+
this.collectionUri = uri;
51
51
+
this.collectionName = name;
52
52
+
this.render();
53
53
+
}
54
54
+
55
55
+
async onOpen() {
56
56
+
await this.render();
57
57
+
}
58
58
+
59
59
+
async getAllCards() {
60
60
+
if (!this.plugin.client) return [];
61
61
+
62
62
+
const repo = this.plugin.settings.identifier;
63
63
+
const cardsResp = await getCards(this.plugin.client, repo);
64
64
+
if (!cardsResp.ok) return [];
65
65
+
return cardsResp.data.records as unknown as CardRecord[];
66
66
+
}
67
67
+
68
68
+
async getCardsInCollection(collectionUri: string) {
69
69
+
if (!this.plugin.client) return [];
70
70
+
71
71
+
const repo = this.plugin.settings.identifier;
72
72
+
const [linksResp, cardsResp] = await Promise.all([
73
73
+
getCollectionLinks(this.plugin.client, repo),
74
74
+
getCards(this.plugin.client, repo),
75
75
+
]);
76
76
+
77
77
+
if (!linksResp.ok || !cardsResp.ok) return [];
78
78
+
const allLinks = linksResp.data.records as unknown as CollectionLinkRecord[];
79
79
+
const allCards = cardsResp.data.records as unknown as CardRecord[];
80
80
+
81
81
+
// Filter links by collection
82
82
+
const links = allLinks.filter((link) => link.value.collection.uri === collectionUri);
83
83
+
84
84
+
// Get cards in collection
85
85
+
const cardUris = new Set(links.map((link) => link.value.card.uri as string));
86
86
+
const cards = allCards.filter((card) => cardUris.has(card.uri as string));
87
87
+
88
88
+
return cards;
89
89
+
}
90
90
+
91
91
+
async render() {
92
92
+
const container = this.contentEl;
93
93
+
container.empty();
94
94
+
container.addClass("semble-cards-view");
95
95
+
96
96
+
if (!this.plugin.client) {
97
97
+
container.createEl("p", { text: "Not connected." });
98
98
+
return;
99
99
+
}
100
100
+
101
101
+
const loading = container.createEl("p", { text: "Loading..." });
102
102
+
103
103
+
try {
104
104
+
105
105
+
let cards: CardRecord[] = [];
106
106
+
try {
107
107
+
if (this.collectionUri) {
108
108
+
cards = await this.getCardsInCollection(this.collectionUri);
109
109
+
} else {
110
110
+
cards = await this.getAllCards();
111
111
+
}
112
112
+
} catch (e) {
113
113
+
loading.remove();
114
114
+
container.createEl("p", { text: `Failed to load cards: ${e}`, cls: "semble-error" });
115
115
+
return;
116
116
+
}
117
117
+
118
118
+
const collectionsResp = await getCollections(this.plugin.client, this.plugin.settings.identifier);
119
119
+
if (!collectionsResp.ok) {
120
120
+
loading.remove();
121
121
+
container.createEl("p", { text: `Failed to load collections: ${collectionsResp.data?.error}`, cls: "semble-error" });
122
122
+
return;
123
123
+
}
124
124
+
const collections = collectionsResp.data?.records as unknown as CollectionRecord[];
125
125
+
126
126
+
loading.remove();
127
127
+
128
128
+
// Render header with back button and filters
129
129
+
this.renderHeader(container, collections);
130
130
+
131
131
+
if (cards.length === 0) {
132
132
+
container.createEl("p", { text: "No cards found." });
133
133
+
return;
134
134
+
}
135
135
+
136
136
+
const grid = container.createEl("div", { cls: "semble-card-grid" });
137
137
+
for (const record of cards) {
138
138
+
try {
139
139
+
this.renderCard(grid, record.value);
140
140
+
} catch (e) {
141
141
+
console.log(JSON.stringify(record.value, null, 2));
142
142
+
console.error(`Failed to render card ${record.uri}: ${e}`);
143
143
+
}
144
144
+
}
145
145
+
} catch (e) {
146
146
+
loading.remove();
147
147
+
container.createEl("p", { text: `Failed to load: ${e}`, cls: "semble-error" });
148
148
+
}
149
149
+
}
150
150
+
151
151
+
private renderHeader(container: HTMLElement, collections: CollectionRecord[]) {
152
152
+
const header = container.createEl("div", { cls: "semble-page-header" });
153
153
+
154
154
+
const nav = header.createEl("div", { cls: "semble-nav-row" });
155
155
+
156
156
+
// Back button
157
157
+
const backBtn = nav.createEl("button", { cls: "semble-back-btn" });
158
158
+
setIcon(backBtn, "arrow-left");
159
159
+
backBtn.addEventListener("click", () => {
160
160
+
this.plugin.activateView(VIEW_TYPE_SEMBLE_COLLECTIONS);
161
161
+
});
162
162
+
163
163
+
const brand = nav.createEl("span", { text: "Semble", cls: "semble-brand" });
164
164
+
165
165
+
header.createEl("h2", { text: this.collectionName, cls: "semble-page-title" });
166
166
+
167
167
+
// Filter chips
168
168
+
const filters = container.createEl("div", { cls: "semble-filter-chips" });
169
169
+
170
170
+
// All chip
171
171
+
const allChip = filters.createEl("button", {
172
172
+
text: "All",
173
173
+
cls: `semble-chip ${!this.collectionUri ? "semble-chip-active" : ""}`,
174
174
+
});
175
175
+
allChip.addEventListener("click", () => {
176
176
+
this.setCollection(null, "All Cards");
177
177
+
});
178
178
+
179
179
+
// Collection chips
180
180
+
for (const record of collections) {
181
181
+
const chip = filters.createEl("button", {
182
182
+
text: record.value.name,
183
183
+
cls: `semble-chip ${this.collectionUri === record.uri ? "semble-chip-active" : ""}`,
184
184
+
});
185
185
+
chip.addEventListener("click", () => {
186
186
+
this.setCollection(record.uri, record.value.name);
187
187
+
});
188
188
+
}
189
189
+
}
190
190
+
191
191
+
private renderCard(container: HTMLElement, card: Card) {
192
192
+
const el = container.createEl("div", { cls: "semble-card" });
193
193
+
194
194
+
const header = el.createEl("div", { cls: "semble-card-header" });
195
195
+
header.createEl("span", {
196
196
+
text: card.type,
197
197
+
cls: `semble-badge semble-badge-${card.type.toLowerCase()}`,
198
198
+
});
199
199
+
200
200
+
if (card.type === "NOTE") {
201
201
+
const content = card.content as NoteContent;
202
202
+
el.createEl("p", { text: content.text, cls: "semble-card-text" });
203
203
+
} else if (card.type === "URL") {
204
204
+
const content = card.content as UrlContent;
205
205
+
const meta = content.metadata;
206
206
+
207
207
+
if (meta?.title) {
208
208
+
el.createEl("div", { text: meta.title, cls: "semble-card-title" });
209
209
+
}
210
210
+
211
211
+
if (meta?.imageUrl) {
212
212
+
const img = el.createEl("img", { cls: "semble-card-image" });
213
213
+
img.src = meta.imageUrl;
214
214
+
img.alt = meta.title || "Image for " + content.url;
215
215
+
}
216
216
+
217
217
+
if (meta?.description) {
218
218
+
const desc = meta.description.length > 200
219
219
+
? meta.description.slice(0, 200) + "…"
220
220
+
: meta.description;
221
221
+
el.createEl("p", { text: desc, cls: "semble-card-desc" });
222
222
+
}
223
223
+
if (meta?.siteName) {
224
224
+
el.createEl("span", { text: meta.siteName, cls: "semble-card-site" });
225
225
+
}
226
226
+
227
227
+
const link = el.createEl("a", {
228
228
+
text: content.url,
229
229
+
href: content.url,
230
230
+
cls: "semble-card-url",
231
231
+
});
232
232
+
link.setAttr("target", "_blank");
233
233
+
}
234
234
+
235
235
+
const footer = el.createEl("div", { cls: "semble-card-footer" });
236
236
+
if (card.createdAt) {
237
237
+
footer.createEl("span", {
238
238
+
text: new Date(card.createdAt).toLocaleDateString(),
239
239
+
cls: "semble-card-date",
240
240
+
});
241
241
+
}
242
242
+
}
243
243
+
244
244
+
async onClose() { }
245
245
+
}
+83
-1
src/views/collections.ts
···
1
1
-
import { ItemView, WorkspaceLeaf } from "obsidian";
1
1
+
import { ItemView, WorkspaceLeaf, setIcon } from "obsidian";
2
2
import type MyPlugin from "../main";
3
3
import { getCollections } from "../lib";
4
4
import type { Main as Collection } from "../lexicons/types/network/cosmik/collection";
5
5
+
import { SembleCardsView, VIEW_TYPE_SEMBLE_CARDS } from "./cards";
5
6
6
7
export const VIEW_TYPE_SEMBLE_COLLECTIONS = "semble-collections-view";
7
8
···
34
35
await this.render();
35
36
}
36
37
38
38
+
async openCollection(uri: string, name: string) {
39
39
+
const { workspace } = this.app;
40
40
+
const leaf = workspace.getLeaf("tab");
41
41
+
await leaf.setViewState({ type: VIEW_TYPE_SEMBLE_CARDS, active: true });
42
42
+
43
43
+
const view = leaf.view as SembleCardsView;
44
44
+
view.setCollection(uri, name);
45
45
+
46
46
+
workspace.revealLeaf(leaf);
47
47
+
}
48
48
+
37
49
async render() {
38
50
const container = this.contentEl;
39
51
container.empty();
···
41
53
42
54
container.createEl("h4", { text: "Collections" });
43
55
56
56
+
if (!this.plugin.client) {
57
57
+
container.createEl("p", { text: "Not connected. Configure credentials in settings." });
58
58
+
return;
59
59
+
}
60
60
+
61
61
+
const repo = this.plugin.settings.identifier;
62
62
+
if (!repo) {
63
63
+
container.createEl("p", { text: "No identifier configured in settings." });
64
64
+
return;
65
65
+
}
66
66
+
67
67
+
const loading = container.createEl("p", { text: "Loading..." });
68
68
+
69
69
+
try {
70
70
+
const resp = await getCollections(this.plugin.client, repo);
71
71
+
loading.remove();
72
72
+
73
73
+
if (!resp.ok) {
74
74
+
container.createEl("p", { text: `Error: ${resp.data?.error}`, cls: "semble-error" });
75
75
+
return;
76
76
+
}
77
77
+
78
78
+
const records = resp.data.records as unknown as CollectionRecord[];
79
79
+
80
80
+
if (records.length === 0) {
81
81
+
container.createEl("p", { text: "No collections found." });
82
82
+
return;
83
83
+
}
84
84
+
85
85
+
const grid = container.createEl("div", { cls: "semble-card-grid" });
86
86
+
87
87
+
for (const record of records) {
88
88
+
const col = record.value;
89
89
+
const card = grid.createEl("div", { cls: "semble-card" });
90
90
+
91
91
+
card.addEventListener("click", () => {
92
92
+
this.plugin.openCollection(record.uri, col.name);
93
93
+
});
94
94
+
95
95
+
const header = card.createEl("div", { cls: "semble-card-header" });
96
96
+
header.createEl("span", { text: col.name, cls: "semble-card-title" });
97
97
+
98
98
+
const accessIcon = header.createEl("span", {
99
99
+
cls: `semble-access-icon semble-access-${col.accessType.toLowerCase()}`,
100
100
+
attr: { "aria-label": col.accessType },
101
101
+
});
102
102
+
setIcon(accessIcon, col.accessType === "OPEN" ? "globe" : "lock");
103
103
+
104
104
+
if (col.description) {
105
105
+
card.createEl("p", { text: col.description, cls: "semble-card-desc" });
106
106
+
}
107
107
+
108
108
+
const footer = card.createEl("div", { cls: "semble-card-footer" });
109
109
+
if (col.createdAt) {
110
110
+
footer.createEl("span", {
111
111
+
text: new Date(col.createdAt).toLocaleDateString(),
112
112
+
cls: "semble-card-date",
113
113
+
});
114
114
+
}
115
115
+
if (col.collaborators?.length) {
116
116
+
footer.createEl("span", {
117
117
+
text: `${col.collaborators.length} collaborators`,
118
118
+
cls: "semble-card-collabs",
119
119
+
});
120
120
+
}
121
121
+
}
122
122
+
} catch (e) {
123
123
+
loading.remove();
124
124
+
container.createEl("p", { text: `Failed to load: ${e}`, cls: "semble-error" });
125
125
+
}
44
126
}
45
127
46
128
async onClose() {}
+223
-5
styles.css
···
1
1
-
/*
1
1
+
.semble-card-grid {
2
2
+
display: grid;
3
3
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
4
4
+
gap: 16px;
5
5
+
padding: 8px 0;
6
6
+
}
7
7
+
8
8
+
.semble-card {
9
9
+
background: var(--background-secondary);
10
10
+
border: 1px solid var(--background-modifier-border);
11
11
+
border-radius: 8px;
12
12
+
padding: 16px;
13
13
+
display: flex;
14
14
+
flex-direction: column;
15
15
+
gap: 8px;
16
16
+
transition: box-shadow 0.15s ease;
17
17
+
cursor: pointer;
18
18
+
}
19
19
+
20
20
+
.semble-card:hover {
21
21
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
22
22
+
border-color: var(--interactive-accent);
23
23
+
}
24
24
+
25
25
+
.semble-card-header {
26
26
+
display: flex;
27
27
+
justify-content: space-between;
28
28
+
align-items: flex-start;
29
29
+
gap: 8px;
30
30
+
}
2
31
3
3
-
This CSS file will be included with your plugin, and
4
4
-
available in the app when your plugin is enabled.
32
32
+
.semble-card-title {
33
33
+
font-weight: 600;
34
34
+
font-size: 1.1em;
35
35
+
color: var(--text-normal);
36
36
+
}
5
37
6
6
-
If your plugin does not need CSS, delete this file.
38
38
+
.semble-badge {
39
39
+
font-size: 0.7em;
40
40
+
padding: 2px 8px;
41
41
+
border-radius: 12px;
42
42
+
text-transform: uppercase;
43
43
+
font-weight: 500;
44
44
+
flex-shrink: 0;
45
45
+
}
7
46
8
8
-
*/
47
47
+
.semble-badge-open {
48
48
+
background: var(--color-green);
49
49
+
color: var(--text-on-accent);
50
50
+
}
51
51
+
52
52
+
.semble-badge-closed {
53
53
+
background: var(--color-orange);
54
54
+
color: var(--text-on-accent);
55
55
+
}
56
56
+
57
57
+
/* Access type icons */
58
58
+
.semble-access-icon {
59
59
+
display: flex;
60
60
+
align-items: center;
61
61
+
justify-content: center;
62
62
+
width: 24px;
63
63
+
height: 24px;
64
64
+
border-radius: 50%;
65
65
+
flex-shrink: 0;
66
66
+
}
67
67
+
68
68
+
.semble-access-icon svg {
69
69
+
width: 14px;
70
70
+
height: 14px;
71
71
+
}
72
72
+
73
73
+
.semble-access-open {
74
74
+
background: var(--color-green);
75
75
+
color: var(--text-on-accent);
76
76
+
}
77
77
+
78
78
+
.semble-access-closed {
79
79
+
background: var(--color-orange);
80
80
+
color: var(--text-on-accent);
81
81
+
}
82
82
+
83
83
+
.semble-card-desc {
84
84
+
color: var(--text-muted);
85
85
+
font-size: 0.9em;
86
86
+
margin: 0;
87
87
+
flex-grow: 1;
88
88
+
}
89
89
+
90
90
+
.semble-card-footer {
91
91
+
display: flex;
92
92
+
justify-content: space-between;
93
93
+
font-size: 0.8em;
94
94
+
color: var(--text-faint);
95
95
+
margin-top: auto;
96
96
+
padding-top: 8px;
97
97
+
border-top: 1px solid var(--background-modifier-border);
98
98
+
}
99
99
+
100
100
+
.semble-error {
101
101
+
color: var(--text-error);
102
102
+
}
103
103
+
104
104
+
/* Card type badges */
105
105
+
.semble-badge-note {
106
106
+
background: var(--color-blue);
107
107
+
color: var(--text-on-accent);
108
108
+
}
109
109
+
110
110
+
.semble-badge-url {
111
111
+
background: var(--color-purple);
112
112
+
color: var(--text-on-accent);
113
113
+
}
114
114
+
115
115
+
/* Page header */
116
116
+
.semble-page-header {
117
117
+
margin-bottom: 16px;
118
118
+
padding-bottom: 16px;
119
119
+
border-bottom: 1px solid var(--background-modifier-border);
120
120
+
}
121
121
+
122
122
+
.semble-nav-row {
123
123
+
display: flex;
124
124
+
align-items: center;
125
125
+
gap: 12px;
126
126
+
margin-bottom: 8px;
127
127
+
}
128
128
+
129
129
+
.semble-brand {
130
130
+
font-size: 0.85em;
131
131
+
font-weight: 600;
132
132
+
color: var(--text-accent);
133
133
+
text-transform: uppercase;
134
134
+
letter-spacing: 0.5px;
135
135
+
}
136
136
+
137
137
+
.semble-page-title {
138
138
+
margin: 0;
139
139
+
font-size: 1.8em;
140
140
+
font-weight: 700;
141
141
+
color: var(--text-normal);
142
142
+
}
143
143
+
144
144
+
.semble-card-text {
145
145
+
margin: 0;
146
146
+
white-space: pre-wrap;
147
147
+
line-height: 1.5;
148
148
+
}
149
149
+
150
150
+
.semble-card-url {
151
151
+
font-size: 0.85em;
152
152
+
color: var(--text-accent);
153
153
+
text-decoration: none;
154
154
+
word-break: break-all;
155
155
+
}
156
156
+
157
157
+
.semble-card-url:hover {
158
158
+
text-decoration: underline;
159
159
+
}
160
160
+
161
161
+
.semble-card-site {
162
162
+
font-size: 0.8em;
163
163
+
color: var(--text-faint);
164
164
+
}
165
165
+
166
166
+
.semble-card-image {
167
167
+
width: 100%;
168
168
+
max-height: 120px;
169
169
+
object-fit: cover;
170
170
+
border-radius: 4px;
171
171
+
margin: 4px 0;
172
172
+
}
173
173
+
174
174
+
/* Back button */
175
175
+
.semble-back-btn {
176
176
+
display: flex;
177
177
+
align-items: center;
178
178
+
justify-content: center;
179
179
+
width: 32px;
180
180
+
height: 32px;
181
181
+
padding: 0;
182
182
+
background: transparent;
183
183
+
border: 1px solid var(--background-modifier-border);
184
184
+
border-radius: 6px;
185
185
+
cursor: pointer;
186
186
+
color: var(--text-muted);
187
187
+
}
188
188
+
189
189
+
.semble-back-btn:hover {
190
190
+
background: var(--background-modifier-hover);
191
191
+
color: var(--text-normal);
192
192
+
}
193
193
+
194
194
+
/* Filter chips */
195
195
+
.semble-filter-chips {
196
196
+
display: flex;
197
197
+
flex-wrap: wrap;
198
198
+
gap: 8px;
199
199
+
margin-bottom: 16px;
200
200
+
}
201
201
+
202
202
+
.semble-chip {
203
203
+
padding: 6px 14px;
204
204
+
border-radius: 16px;
205
205
+
border: 1px solid var(--background-modifier-border);
206
206
+
background: var(--background-secondary);
207
207
+
color: var(--text-muted);
208
208
+
font-size: 0.85em;
209
209
+
cursor: pointer;
210
210
+
transition: all 0.15s ease;
211
211
+
}
212
212
+
213
213
+
.semble-chip:hover {
214
214
+
background: var(--background-modifier-hover);
215
215
+
color: var(--text-normal);
216
216
+
}
217
217
+
218
218
+
.semble-chip-active {
219
219
+
background: var(--interactive-accent);
220
220
+
color: var(--text-on-accent);
221
221
+
border-color: var(--interactive-accent);
222
222
+
}
223
223
+
224
224
+
.semble-chip-active:hover {
225
225
+
background: var(--interactive-accent-hover);
226
226
+
}