tangled
alpha
login
or
join now
treethought.xyz
/
obsidian-atmosphere
19
fork
atom
Various AT Protocol integrations with obsidian
19
fork
atom
overview
issues
pulls
pipelines
support editing margin tags
treethought
1 month ago
5345b083
2dc05786
+384
-358
15 changed files
expand all
collapse all
unified
split
src
components
cardDetailModal.ts
createCollectionModal.ts
createMarginCollectionModal.ts
createTagModal.ts
editBookmarkModal.ts
editCardModal.ts
editMarginBookmarkModal.ts
profileIcon.ts
lib
margin.ts
lib.ts
sources
bookmark.ts
margin.ts
semble.ts
views
atmark.ts
styles.css
-4
src/components/cardDetailModal.ts
···
21
21
contentEl.empty();
22
22
contentEl.addClass("semble-detail-modal");
23
23
24
24
-
// Header with source badge
25
24
const header = contentEl.createEl("div", { cls: "semble-detail-header" });
26
25
const source = this.item.getSource();
27
26
header.createEl("span", {
···
29
28
cls: `semble-badge semble-badge-source semble-badge-${source}`,
30
29
});
31
30
32
32
-
// Render item detail content
33
31
this.item.renderDetail(contentEl);
34
32
35
33
// Render notes with delete buttons (semble-specific)
···
42
40
this.renderAddNoteForm(contentEl);
43
41
}
44
42
45
45
-
// Footer with date
46
43
const footer = contentEl.createEl("div", { cls: "semble-detail-footer" });
47
44
footer.createEl("span", {
48
45
text: `Created ${new Date(this.item.getCreatedAt()).toLocaleDateString()}`,
···
65
62
setIcon(noteIcon, "message-square");
66
63
noteContent.createEl("p", { text: note.text, cls: "semble-detail-note-text" });
67
64
68
68
-
// Delete button
69
65
const deleteBtn = noteEl.createEl("button", { cls: "semble-note-delete-btn" });
70
66
setIcon(deleteBtn, "trash-2");
71
67
deleteBtn.addEventListener("click", () => {
+9
-13
src/components/createCollectionModal.ts
···
15
15
onOpen() {
16
16
const { contentEl } = this;
17
17
contentEl.empty();
18
18
-
contentEl.addClass("semble-collection-modal");
18
18
+
contentEl.addClass("atmark-modal");
19
19
20
20
contentEl.createEl("h2", { text: "New collection" });
21
21
···
24
24
return;
25
25
}
26
26
27
27
-
const form = contentEl.createEl("form", { cls: "semble-form" });
27
27
+
const form = contentEl.createEl("form", { cls: "atmark-form" });
28
28
29
29
-
// Name field
30
30
-
const nameGroup = form.createEl("div", { cls: "semble-form-group" });
29
29
+
const nameGroup = form.createEl("div", { cls: "atmark-form-group" });
31
30
nameGroup.createEl("label", { text: "Name", attr: { for: "collection-name" } });
32
31
const nameInput = nameGroup.createEl("input", {
33
32
type: "text",
34
34
-
cls: "semble-input",
33
33
+
cls: "atmark-input",
35
34
attr: { id: "collection-name", placeholder: "Collection name", required: "true" },
36
35
});
37
36
38
38
-
// Description field
39
39
-
const descGroup = form.createEl("div", { cls: "semble-form-group" });
37
37
+
const descGroup = form.createEl("div", { cls: "atmark-form-group" });
40
38
descGroup.createEl("label", { text: "Description", attr: { for: "collection-desc" } });
41
39
const descInput = descGroup.createEl("textarea", {
42
42
-
cls: "semble-textarea",
40
40
+
cls: "atmark-textarea",
43
41
attr: { id: "collection-desc", placeholder: "Optional description", rows: "3" },
44
42
});
45
43
46
46
-
// Action buttons
47
47
-
const actions = form.createEl("div", { cls: "semble-modal-actions" });
44
44
+
const actions = form.createEl("div", { cls: "atmark-modal-actions" });
48
45
49
46
const cancelBtn = actions.createEl("button", {
50
47
text: "Cancel",
51
51
-
cls: "semble-btn semble-btn-secondary",
48
48
+
cls: "atmark-btn atmark-btn-secondary",
52
49
type: "button",
53
50
});
54
51
cancelBtn.addEventListener("click", () => this.close());
55
52
56
53
const createBtn = actions.createEl("button", {
57
54
text: "Create",
58
58
-
cls: "semble-btn semble-btn-primary",
55
55
+
cls: "atmark-btn atmark-btn-primary",
59
56
type: "submit",
60
57
});
61
58
···
64
61
void this.handleSubmit(nameInput, descInput, createBtn);
65
62
});
66
63
67
67
-
// Focus name input
68
64
nameInput.focus();
69
65
}
70
66
-5
src/components/createMarginCollectionModal.ts
···
26
26
27
27
const form = contentEl.createEl("form", { cls: "atmark-form" });
28
28
29
29
-
// Name field
30
29
const nameGroup = form.createEl("div", { cls: "atmark-form-group" });
31
30
nameGroup.createEl("label", { text: "Name", attr: { for: "collection-name" } });
32
31
const nameInput = nameGroup.createEl("input", {
···
35
34
attr: { id: "collection-name", placeholder: "Collection name", required: "true" },
36
35
});
37
36
38
38
-
// Icon field
39
37
const iconGroup = form.createEl("div", { cls: "atmark-form-group" });
40
38
iconGroup.createEl("label", { text: "Icon (optional)", attr: { for: "collection-icon" } });
41
39
const iconInput = iconGroup.createEl("input", {
···
44
42
attr: { id: "collection-icon" },
45
43
});
46
44
47
47
-
// Description field
48
45
const descGroup = form.createEl("div", { cls: "atmark-form-group" });
49
46
descGroup.createEl("label", { text: "Description", attr: { for: "collection-desc" } });
50
47
const descInput = descGroup.createEl("textarea", {
···
52
49
attr: { id: "collection-desc", placeholder: "Optional description", rows: "3" },
53
50
});
54
51
55
55
-
// Action buttons
56
52
const actions = form.createEl("div", { cls: "atmark-modal-actions" });
57
53
58
54
const cancelBtn = actions.createEl("button", {
···
73
69
void this.handleSubmit(nameInput, iconInput, descInput, createBtn);
74
70
});
75
71
76
76
-
// Focus name input
77
72
nameInput.focus();
78
73
}
79
74
-3
src/components/createTagModal.ts
···
26
26
27
27
const form = contentEl.createEl("form", { cls: "atmark-form" });
28
28
29
29
-
// Tag value field
30
29
const tagGroup = form.createEl("div", { cls: "atmark-form-group" });
31
30
tagGroup.createEl("label", { text: "Tag", attr: { for: "tag-value" } });
32
31
const tagInput = tagGroup.createEl("input", {
···
35
34
attr: { id: "tag-value", placeholder: "Tag name", required: "true" },
36
35
});
37
36
38
38
-
// Action buttons
39
37
const actions = form.createEl("div", { cls: "atmark-modal-actions" });
40
38
41
39
const cancelBtn = actions.createEl("button", {
···
56
54
void this.handleSubmit(tagInput, createBtn);
57
55
});
58
56
59
59
-
// Focus tag input
60
57
tagInput.focus();
61
58
}
62
59
+75
-48
src/components/editBookmarkModal.ts
···
2
2
import type { Record } from "@atcute/atproto/types/repo/listRecords";
3
3
import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark";
4
4
import type ATmarkPlugin from "../main";
5
5
-
import { putRecord, deleteRecord } from "../lib";
5
5
+
import { putRecord, deleteRecord, getBookmarks } from "../lib";
6
6
7
7
type BookmarkRecord = Record & { value: Bookmark };
8
8
9
9
+
interface TagState {
10
10
+
tag: string;
11
11
+
isSelected: boolean;
12
12
+
}
13
13
+
9
14
export class EditBookmarkModal extends Modal {
10
15
plugin: ATmarkPlugin;
11
16
record: BookmarkRecord;
12
17
onSuccess?: () => void;
13
13
-
tagInputs: HTMLInputElement[] = [];
18
18
+
tagStates: TagState[] = [];
19
19
+
newTagInput: HTMLInputElement | null = null;
14
20
15
21
constructor(plugin: ATmarkPlugin, record: BookmarkRecord, onSuccess?: () => void) {
16
22
super(plugin.app);
···
19
25
this.onSuccess = onSuccess;
20
26
}
21
27
22
22
-
onOpen() {
28
28
+
async onOpen() {
23
29
const { contentEl } = this;
24
30
contentEl.empty();
25
31
contentEl.addClass("atmark-modal");
26
32
27
27
-
contentEl.createEl("h2", { text: "Edit bookmark tags" });
33
33
+
contentEl.createEl("h2", { text: "Edit bookmark" });
28
34
29
35
if (!this.plugin.client) {
30
36
contentEl.createEl("p", { text: "Not connected." });
31
37
return;
32
38
}
33
39
34
34
-
const existingTags = this.record.value.tags || [];
40
40
+
const loading = contentEl.createEl("p", { text: "Loading..." });
41
41
+
42
42
+
try {
43
43
+
const bookmarksResp = await getBookmarks(this.plugin.client, this.plugin.settings.identifier);
44
44
+
loading.remove();
45
45
+
46
46
+
const bookmarks = (bookmarksResp.ok ? bookmarksResp.data.records : []) as unknown as BookmarkRecord[];
47
47
+
48
48
+
const allTags = new Set<string>();
49
49
+
for (const bookmark of bookmarks) {
50
50
+
if (bookmark.value.tags) {
51
51
+
for (const tag of bookmark.value.tags) {
52
52
+
allTags.add(tag);
53
53
+
}
54
54
+
}
55
55
+
}
35
56
57
57
+
const currentTags = new Set(this.record.value.tags || []);
58
58
+
this.tagStates = Array.from(allTags).sort().map(tag => ({
59
59
+
tag,
60
60
+
isSelected: currentTags.has(tag),
61
61
+
}));
62
62
+
63
63
+
this.renderForm(contentEl);
64
64
+
} catch (err) {
65
65
+
loading.remove();
66
66
+
const message = err instanceof Error ? err.message : String(err);
67
67
+
contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmark-error" });
68
68
+
}
69
69
+
}
70
70
+
71
71
+
private renderForm(contentEl: HTMLElement) {
36
72
const form = contentEl.createEl("div", { cls: "atmark-form" });
37
73
38
38
-
// Tags section
39
74
const tagsGroup = form.createEl("div", { cls: "atmark-form-group" });
40
75
tagsGroup.createEl("label", { text: "Tags" });
41
76
42
42
-
const tagsContainer = tagsGroup.createEl("div", { cls: "atmark-tags-container" });
43
43
-
44
44
-
// Render existing tags
45
45
-
for (const tag of existingTags) {
46
46
-
this.addTagInput(tagsContainer, tag);
77
77
+
const tagsList = tagsGroup.createEl("div", { cls: "atmark-tag-list" });
78
78
+
for (const state of this.tagStates) {
79
79
+
this.addTagChip(tagsList, state);
47
80
}
48
81
49
49
-
// Add empty input for new tag
50
50
-
this.addTagInput(tagsContainer, "");
51
51
-
52
52
-
// Add tag button
53
53
-
const addTagBtn = tagsGroup.createEl("button", {
54
54
-
text: "Add tag",
55
55
-
cls: "atmark-btn atmark-btn-secondary"
82
82
+
const newTagRow = tagsGroup.createEl("div", { cls: "atmark-tag-row" });
83
83
+
this.newTagInput = newTagRow.createEl("input", {
84
84
+
type: "text",
85
85
+
cls: "atmark-input",
86
86
+
attr: { placeholder: "Add new tag..." }
87
87
+
});
88
88
+
const addBtn = newTagRow.createEl("button", {
89
89
+
text: "Add",
90
90
+
cls: "atmark-btn atmark-btn-secondary",
91
91
+
attr: { type: "button" }
56
92
});
57
57
-
addTagBtn.addEventListener("click", (e) => {
58
58
-
e.preventDefault();
59
59
-
this.addTagInput(tagsContainer, "");
93
93
+
addBtn.addEventListener("click", () => {
94
94
+
const value = this.newTagInput?.value.trim();
95
95
+
if (value && !this.tagStates.some(s => s.tag === value)) {
96
96
+
const newState = { tag: value, isSelected: true };
97
97
+
this.tagStates.push(newState);
98
98
+
this.addTagChip(tagsList, newState);
99
99
+
if (this.newTagInput) this.newTagInput.value = "";
100
100
+
}
60
101
});
61
102
62
62
-
// Action buttons
63
103
const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" });
64
104
65
105
const deleteBtn = actions.createEl("button", {
···
83
123
saveBtn.addEventListener("click", () => { void this.saveChanges(); });
84
124
}
85
125
86
86
-
private addTagInput(container: HTMLElement, value: string) {
87
87
-
const tagRow = container.createEl("div", { cls: "atmark-tag-row" });
88
88
-
89
89
-
const input = tagRow.createEl("input", {
90
90
-
type: "text",
91
91
-
cls: "atmark-input",
92
92
-
value,
93
93
-
attr: { placeholder: "Enter tag..." }
126
126
+
private addTagChip(container: HTMLElement, state: TagState) {
127
127
+
const item = container.createEl("label", { cls: "atmark-tag-item" });
128
128
+
const checkbox = item.createEl("input", { type: "checkbox" });
129
129
+
checkbox.checked = state.isSelected;
130
130
+
checkbox.addEventListener("change", () => {
131
131
+
state.isSelected = checkbox.checked;
94
132
});
95
95
-
this.tagInputs.push(input);
96
96
-
97
97
-
const removeBtn = tagRow.createEl("button", {
98
98
-
text: "×",
99
99
-
cls: "atmark-btn atmark-btn-secondary atmark-tag-remove-btn"
100
100
-
});
101
101
-
removeBtn.addEventListener("click", (e) => {
102
102
-
e.preventDefault();
103
103
-
tagRow.remove();
104
104
-
this.tagInputs = this.tagInputs.filter(i => i !== input);
105
105
-
});
133
133
+
item.createEl("span", { text: state.tag });
106
134
}
107
135
108
136
private confirmDelete(contentEl: HTMLElement) {
···
167
195
contentEl.createEl("p", { text: "Saving changes..." });
168
196
169
197
try {
170
170
-
// Get non-empty unique tags
171
171
-
const tags = [...new Set(
172
172
-
this.tagInputs
173
173
-
.map(input => input.value.trim())
174
174
-
.filter(tag => tag.length > 0)
175
175
-
)];
198
198
+
const selectedTags = this.tagStates.filter(s => s.isSelected).map(s => s.tag);
199
199
+
const newTag = this.newTagInput?.value.trim();
200
200
+
if (newTag && !selectedTags.includes(newTag)) {
201
201
+
selectedTags.push(newTag);
202
202
+
}
203
203
+
const tags = [...new Set(selectedTags)];
176
204
177
205
const rkey = this.record.uri.split("/").pop();
178
206
if (!rkey) {
···
181
209
return;
182
210
}
183
211
184
184
-
// Update the record with new tags
185
212
const updatedRecord: Bookmark = {
186
213
...this.record.value,
187
214
tags,
+23
-31
src/components/editCardModal.ts
···
40
40
async onOpen() {
41
41
const { contentEl } = this;
42
42
contentEl.empty();
43
43
-
contentEl.addClass("semble-collection-modal");
43
43
+
contentEl.addClass("atmark-modal");
44
44
45
45
contentEl.createEl("h2", { text: "Edit collections" });
46
46
···
52
52
const loading = contentEl.createEl("p", { text: "Loading..." });
53
53
54
54
try {
55
55
-
// Fetch collections and existing links in parallel
56
55
const [collectionsResp, linksResp] = await Promise.all([
57
56
getCollections(this.plugin.client, this.plugin.settings.identifier),
58
57
getCollectionLinks(this.plugin.client, this.plugin.settings.identifier),
···
61
60
loading.remove();
62
61
63
62
if (!collectionsResp.ok) {
64
64
-
contentEl.createEl("p", { text: "Failed to load collections.", cls: "semble-error" });
63
63
+
contentEl.createEl("p", { text: "Failed to load collections.", cls: "atmark-error" });
65
64
return;
66
65
}
67
66
···
73
72
return;
74
73
}
75
74
76
76
-
// Find which collections this card is already in
77
75
const cardLinks = links.filter(link => link.value.card.uri === this.cardUri);
78
76
const linkedCollectionUris = new Map<string, string>();
79
77
for (const link of cardLinks) {
80
78
linkedCollectionUris.set(link.value.collection.uri, link.uri);
81
79
}
82
80
83
83
-
// Build collection states
84
81
this.collectionStates = collections.map(collection => ({
85
82
collection,
86
83
isSelected: linkedCollectionUris.has(collection.uri),
···
92
89
} catch (err) {
93
90
loading.remove();
94
91
const message = err instanceof Error ? err.message : String(err);
95
95
-
contentEl.createEl("p", { text: `Error: ${message}`, cls: "semble-error" });
92
92
+
contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmark-error" });
96
93
}
97
94
}
98
95
99
96
private renderCollectionList(contentEl: HTMLElement) {
100
100
-
const list = contentEl.createEl("div", { cls: "semble-collection-list" });
97
97
+
const list = contentEl.createEl("div", { cls: "atmark-collection-list" });
101
98
102
99
for (const state of this.collectionStates) {
103
103
-
const item = list.createEl("label", { cls: "semble-collection-item" });
100
100
+
const item = list.createEl("label", { cls: "atmark-collection-item" });
104
101
105
105
-
const checkbox = item.createEl("input", { type: "checkbox", cls: "semble-collection-checkbox" });
102
102
+
const checkbox = item.createEl("input", { type: "checkbox", cls: "atmark-collection-checkbox" });
106
103
checkbox.checked = state.isSelected;
107
104
checkbox.addEventListener("change", () => {
108
105
state.isSelected = checkbox.checked;
109
106
this.updateSaveButton();
110
107
});
111
108
112
112
-
const info = item.createEl("div", { cls: "semble-collection-item-info" });
113
113
-
info.createEl("span", { text: state.collection.value.name, cls: "semble-collection-item-name" });
109
109
+
const info = item.createEl("div", { cls: "atmark-collection-item-info" });
110
110
+
info.createEl("span", { text: state.collection.value.name, cls: "atmark-collection-item-name" });
114
111
if (state.collection.value.description) {
115
115
-
info.createEl("span", { text: state.collection.value.description, cls: "semble-collection-item-desc" });
112
112
+
info.createEl("span", { text: state.collection.value.description, cls: "atmark-collection-item-desc" });
116
113
}
117
114
}
118
115
119
119
-
// Action buttons
120
120
-
const actions = contentEl.createEl("div", { cls: "semble-modal-actions" });
116
116
+
const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" });
121
117
122
122
-
const deleteBtn = actions.createEl("button", { text: "Delete", cls: "semble-btn semble-btn-danger" });
118
118
+
const deleteBtn = actions.createEl("button", { text: "Delete", cls: "atmark-btn atmark-btn-danger" });
123
119
deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); });
124
120
125
125
-
actions.createEl("div", { cls: "semble-spacer" });
121
121
+
actions.createEl("div", { cls: "atmark-spacer" });
126
122
127
127
-
const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "semble-btn semble-btn-secondary" });
123
123
+
const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmark-btn atmark-btn-secondary" });
128
124
cancelBtn.addEventListener("click", () => { this.close(); });
129
125
130
130
-
const saveBtn = actions.createEl("button", { text: "Save", cls: "semble-btn semble-btn-primary" });
131
131
-
saveBtn.id = "semble-save-btn";
126
126
+
const saveBtn = actions.createEl("button", { text: "Save", cls: "atmark-btn atmark-btn-primary" });
127
127
+
saveBtn.id = "atmark-save-btn";
132
128
saveBtn.disabled = true;
133
129
saveBtn.addEventListener("click", () => { void this.saveChanges(); });
134
130
}
···
136
132
private confirmDelete(contentEl: HTMLElement) {
137
133
contentEl.empty();
138
134
contentEl.createEl("h2", { text: "Delete card" });
139
139
-
contentEl.createEl("p", { text: "Delete this card?", cls: "semble-warning-text" });
135
135
+
contentEl.createEl("p", { text: "Delete this card?", cls: "atmark-warning-text" });
140
136
141
141
-
const actions = contentEl.createEl("div", { cls: "semble-modal-actions" });
137
137
+
const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" });
142
138
143
143
-
const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "semble-btn semble-btn-secondary" });
139
139
+
const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmark-btn atmark-btn-secondary" });
144
140
cancelBtn.addEventListener("click", () => {
145
145
-
// Re-render the modal
146
141
void this.onOpen();
147
142
});
148
143
149
149
-
const confirmBtn = actions.createEl("button", { text: "Delete", cls: "semble-btn semble-btn-danger" });
144
144
+
const confirmBtn = actions.createEl("button", { text: "Delete", cls: "atmark-btn atmark-btn-danger" });
150
145
confirmBtn.addEventListener("click", () => { void this.deleteCard(); });
151
146
}
152
147
···
161
156
const rkey = this.cardUri.split("/").pop();
162
157
if (!rkey) {
163
158
contentEl.empty();
164
164
-
contentEl.createEl("p", { text: "Invalid card uri.", cls: "semble-error" });
159
159
+
contentEl.createEl("p", { text: "Invalid card uri.", cls: "atmark-error" });
165
160
return;
166
161
}
167
162
···
178
173
} catch (err) {
179
174
contentEl.empty();
180
175
const message = err instanceof Error ? err.message : String(err);
181
181
-
contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "semble-error" });
176
176
+
contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmark-error" });
182
177
}
183
178
}
184
179
185
180
private updateSaveButton() {
186
186
-
const saveBtn = document.getElementById("semble-save-btn") as HTMLButtonElement | null;
181
181
+
const saveBtn = document.getElementById("atmark-save-btn") as HTMLButtonElement | null;
187
182
if (!saveBtn) return;
188
183
189
189
-
// Check if any changes were made
190
184
const hasChanges = this.collectionStates.some(s => s.isSelected !== s.wasSelected);
191
185
saveBtn.disabled = !hasChanges;
192
186
}
···
202
196
const toAdd = this.collectionStates.filter(s => s.isSelected && !s.wasSelected);
203
197
const toRemove = this.collectionStates.filter(s => !s.isSelected && s.wasSelected);
204
198
205
205
-
// Process removals
206
199
for (const state of toRemove) {
207
200
if (state.linkUri) {
208
201
const rkey = state.linkUri.split("/").pop();
···
217
210
}
218
211
}
219
212
220
220
-
// Process additions
221
213
for (const state of toAdd) {
222
214
const collectionRkey = state.collection.uri.split("/").pop();
223
215
if (!collectionRkey) continue;
···
256
248
} catch (err) {
257
249
contentEl.empty();
258
250
const message = err instanceof Error ? err.message : String(err);
259
259
-
contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "semble-error" });
251
251
+
contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmark-error" });
260
252
}
261
253
}
262
254
+168
-48
src/components/editMarginBookmarkModal.ts
···
1
1
import { Modal, Notice } from "obsidian";
2
2
import type { Record } from "@atcute/atproto/types/repo/listRecords";
3
3
import type { Main as MarginBookmark } from "../lexicons/types/at/margin/bookmark";
4
4
+
import type { Main as MarginCollection } from "../lexicons/types/at/margin/collection";
5
5
+
import type { Main as MarginCollectionItem } from "../lexicons/types/at/margin/collectionItem";
4
6
import type ATmarkPlugin from "../main";
5
5
-
import { putRecord, deleteRecord } from "../lib";
7
7
+
import { putRecord, deleteRecord, getMarginCollections, getMarginCollectionItems, createMarginCollectionItem, getMarginBookmarks } from "../lib";
6
8
7
9
type MarginBookmarkRecord = Record & { value: MarginBookmark };
10
10
+
type MarginCollectionRecord = Record & { value: MarginCollection };
11
11
+
type MarginCollectionItemRecord = Record & { value: MarginCollectionItem };
12
12
+
13
13
+
interface CollectionState {
14
14
+
collection: MarginCollectionRecord;
15
15
+
isSelected: boolean;
16
16
+
wasSelected: boolean;
17
17
+
linkUri?: string;
18
18
+
}
19
19
+
20
20
+
interface TagState {
21
21
+
tag: string;
22
22
+
isSelected: boolean;
23
23
+
}
8
24
9
25
export class EditMarginBookmarkModal extends Modal {
10
26
plugin: ATmarkPlugin;
11
27
record: MarginBookmarkRecord;
12
28
onSuccess?: () => void;
13
13
-
tagInputs: HTMLInputElement[] = [];
29
29
+
tagStates: TagState[] = [];
30
30
+
newTagInput: HTMLInputElement | null = null;
31
31
+
collectionStates: CollectionState[] = [];
14
32
15
33
constructor(plugin: ATmarkPlugin, record: MarginBookmarkRecord, onSuccess?: () => void) {
16
34
super(plugin.app);
···
19
37
this.onSuccess = onSuccess;
20
38
}
21
39
22
22
-
onOpen() {
40
40
+
async onOpen() {
23
41
const { contentEl } = this;
24
42
contentEl.empty();
25
43
contentEl.addClass("atmark-modal");
···
31
49
return;
32
50
}
33
51
34
34
-
const existingTags = this.record.value.tags || [];
52
52
+
const loading = contentEl.createEl("p", { text: "Loading..." });
35
53
54
54
+
try {
55
55
+
const [collectionsResp, itemsResp, bookmarksResp] = await Promise.all([
56
56
+
getMarginCollections(this.plugin.client, this.plugin.settings.identifier),
57
57
+
getMarginCollectionItems(this.plugin.client, this.plugin.settings.identifier),
58
58
+
getMarginBookmarks(this.plugin.client, this.plugin.settings.identifier),
59
59
+
]);
60
60
+
61
61
+
loading.remove();
62
62
+
63
63
+
const collections = (collectionsResp.ok ? collectionsResp.data.records : []) as unknown as MarginCollectionRecord[];
64
64
+
const items = (itemsResp.ok ? itemsResp.data.records : []) as unknown as MarginCollectionItemRecord[];
65
65
+
const bookmarks = (bookmarksResp.ok ? bookmarksResp.data.records : []) as unknown as MarginBookmarkRecord[];
66
66
+
67
67
+
const bookmarkLinks = items.filter(item => item.value.annotation === this.record.uri);
68
68
+
const linkedCollectionUris = new Map<string, string>();
69
69
+
for (const link of bookmarkLinks) {
70
70
+
linkedCollectionUris.set(link.value.collection, link.uri);
71
71
+
}
72
72
+
73
73
+
this.collectionStates = collections.map(collection => ({
74
74
+
collection,
75
75
+
isSelected: linkedCollectionUris.has(collection.uri),
76
76
+
wasSelected: linkedCollectionUris.has(collection.uri),
77
77
+
linkUri: linkedCollectionUris.get(collection.uri),
78
78
+
}));
79
79
+
80
80
+
const allTags = new Set<string>();
81
81
+
for (const bookmark of bookmarks) {
82
82
+
if (bookmark.value.tags) {
83
83
+
for (const tag of bookmark.value.tags) {
84
84
+
allTags.add(tag);
85
85
+
}
86
86
+
}
87
87
+
}
88
88
+
89
89
+
const currentTags = new Set(this.record.value.tags || []);
90
90
+
this.tagStates = Array.from(allTags).sort().map(tag => ({
91
91
+
tag,
92
92
+
isSelected: currentTags.has(tag),
93
93
+
}));
94
94
+
95
95
+
this.renderForm(contentEl);
96
96
+
} catch (err) {
97
97
+
loading.remove();
98
98
+
const message = err instanceof Error ? err.message : String(err);
99
99
+
contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmark-error" });
100
100
+
}
101
101
+
}
102
102
+
103
103
+
private renderForm(contentEl: HTMLElement) {
36
104
const form = contentEl.createEl("div", { cls: "atmark-form" });
37
105
38
38
-
// Tags section
39
106
const tagsGroup = form.createEl("div", { cls: "atmark-form-group" });
40
107
tagsGroup.createEl("label", { text: "Tags" });
41
108
42
42
-
const tagsContainer = tagsGroup.createEl("div", { cls: "atmark-tags-container" });
43
43
-
44
44
-
// Render existing tags
45
45
-
for (const tag of existingTags) {
46
46
-
this.addTagInput(tagsContainer, tag);
109
109
+
const tagsList = tagsGroup.createEl("div", { cls: "atmark-tag-list" });
110
110
+
for (const state of this.tagStates) {
111
111
+
this.addTagChip(tagsList, state);
47
112
}
48
113
49
49
-
// Add empty input for new tag
50
50
-
this.addTagInput(tagsContainer, "");
51
51
-
52
52
-
// Add tag button
53
53
-
const addTagBtn = tagsGroup.createEl("button", {
54
54
-
text: "Add tag",
55
55
-
cls: "atmark-btn atmark-btn-secondary"
114
114
+
const newTagRow = tagsGroup.createEl("div", { cls: "atmark-tag-row" });
115
115
+
this.newTagInput = newTagRow.createEl("input", {
116
116
+
type: "text",
117
117
+
cls: "atmark-input",
118
118
+
attr: { placeholder: "Add new tag..." }
56
119
});
57
57
-
addTagBtn.addEventListener("click", (e) => {
58
58
-
e.preventDefault();
59
59
-
this.addTagInput(tagsContainer, "");
120
120
+
const addBtn = newTagRow.createEl("button", {
121
121
+
text: "Add",
122
122
+
cls: "atmark-btn atmark-btn-secondary",
123
123
+
attr: { type: "button" }
60
124
});
125
125
+
addBtn.addEventListener("click", () => {
126
126
+
const value = this.newTagInput?.value.trim();
127
127
+
if (value && !this.tagStates.some(s => s.tag === value)) {
128
128
+
const newState = { tag: value, isSelected: true };
129
129
+
this.tagStates.push(newState);
130
130
+
this.addTagChip(tagsList, newState);
131
131
+
if (this.newTagInput) this.newTagInput.value = "";
132
132
+
}
133
133
+
});
134
134
+
135
135
+
if (this.collectionStates.length > 0) {
136
136
+
const collectionsGroup = form.createEl("div", { cls: "atmark-form-group" });
137
137
+
collectionsGroup.createEl("label", { text: "Collections" });
138
138
+
139
139
+
const collectionsList = collectionsGroup.createEl("div", { cls: "atmark-collection-list" });
140
140
+
141
141
+
for (const state of this.collectionStates) {
142
142
+
const item = collectionsList.createEl("label", { cls: "atmark-collection-item" });
61
143
62
62
-
// Action buttons
144
144
+
const checkbox = item.createEl("input", { type: "checkbox", cls: "atmark-collection-checkbox" });
145
145
+
checkbox.checked = state.isSelected;
146
146
+
checkbox.addEventListener("change", () => {
147
147
+
state.isSelected = checkbox.checked;
148
148
+
});
149
149
+
150
150
+
const info = item.createEl("div", { cls: "atmark-collection-item-info" });
151
151
+
info.createEl("span", { text: state.collection.value.name, cls: "atmark-collection-item-name" });
152
152
+
if (state.collection.value.description) {
153
153
+
info.createEl("span", { text: state.collection.value.description, cls: "atmark-collection-item-desc" });
154
154
+
}
155
155
+
}
156
156
+
}
157
157
+
63
158
const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" });
64
159
65
160
const deleteBtn = actions.createEl("button", {
···
83
178
saveBtn.addEventListener("click", () => { void this.saveChanges(); });
84
179
}
85
180
86
86
-
private addTagInput(container: HTMLElement, value: string) {
87
87
-
const tagRow = container.createEl("div", { cls: "atmark-tag-row" });
88
88
-
89
89
-
const input = tagRow.createEl("input", {
90
90
-
type: "text",
91
91
-
cls: "atmark-input",
92
92
-
value,
93
93
-
attr: { placeholder: "Enter tag..." }
94
94
-
});
95
95
-
this.tagInputs.push(input);
96
96
-
97
97
-
const removeBtn = tagRow.createEl("button", {
98
98
-
text: "×",
99
99
-
cls: "atmark-btn atmark-btn-secondary atmark-tag-remove-btn"
100
100
-
});
101
101
-
removeBtn.addEventListener("click", (e) => {
102
102
-
e.preventDefault();
103
103
-
tagRow.remove();
104
104
-
this.tagInputs = this.tagInputs.filter(i => i !== input);
181
181
+
private addTagChip(container: HTMLElement, state: TagState) {
182
182
+
const item = container.createEl("label", { cls: "atmark-tag-item" });
183
183
+
const checkbox = item.createEl("input", { type: "checkbox" });
184
184
+
checkbox.checked = state.isSelected;
185
185
+
checkbox.addEventListener("change", () => {
186
186
+
state.isSelected = checkbox.checked;
105
187
});
188
188
+
item.createEl("span", { text: state.tag });
106
189
}
107
190
108
191
private confirmDelete(contentEl: HTMLElement) {
···
167
250
contentEl.createEl("p", { text: "Saving changes..." });
168
251
169
252
try {
170
170
-
// Get non-empty unique tags
171
171
-
const tags = [...new Set(
172
172
-
this.tagInputs
173
173
-
.map(input => input.value.trim())
174
174
-
.filter(tag => tag.length > 0)
175
175
-
)];
253
253
+
const selectedTags = this.tagStates.filter(s => s.isSelected).map(s => s.tag);
254
254
+
const newTag = this.newTagInput?.value.trim();
255
255
+
if (newTag && !selectedTags.includes(newTag)) {
256
256
+
selectedTags.push(newTag);
257
257
+
}
258
258
+
const tags = [...new Set(selectedTags)];
176
259
177
260
const rkey = this.record.uri.split("/").pop();
178
261
if (!rkey) {
···
181
264
return;
182
265
}
183
266
184
184
-
// Update the record with new tags
185
267
const updatedRecord: MarginBookmark = {
186
268
...this.record.value,
187
269
tags,
···
195
277
updatedRecord
196
278
);
197
279
198
198
-
new Notice("Tags updated");
280
280
+
const collectionsToAdd = this.collectionStates.filter(s => s.isSelected && !s.wasSelected);
281
281
+
const collectionsToRemove = this.collectionStates.filter(s => !s.isSelected && s.wasSelected);
282
282
+
283
283
+
for (const state of collectionsToRemove) {
284
284
+
if (state.linkUri) {
285
285
+
const linkRkey = state.linkUri.split("/").pop();
286
286
+
if (linkRkey) {
287
287
+
await deleteRecord(
288
288
+
this.plugin.client,
289
289
+
this.plugin.settings.identifier,
290
290
+
"at.margin.collectionItem",
291
291
+
linkRkey
292
292
+
);
293
293
+
}
294
294
+
}
295
295
+
}
296
296
+
297
297
+
for (const state of collectionsToAdd) {
298
298
+
await createMarginCollectionItem(
299
299
+
this.plugin.client,
300
300
+
this.plugin.settings.identifier,
301
301
+
this.record.uri,
302
302
+
state.collection.uri
303
303
+
);
304
304
+
}
305
305
+
306
306
+
const messages: string[] = [];
307
307
+
if (tags.length !== (this.record.value.tags?.length || 0) ||
308
308
+
!tags.every(t => this.record.value.tags?.includes(t))) {
309
309
+
messages.push("Tags updated");
310
310
+
}
311
311
+
if (collectionsToAdd.length > 0) {
312
312
+
messages.push(`Added to ${collectionsToAdd.length} collection${collectionsToAdd.length > 1 ? "s" : ""}`);
313
313
+
}
314
314
+
if (collectionsToRemove.length > 0) {
315
315
+
messages.push(`Removed from ${collectionsToRemove.length} collection${collectionsToRemove.length > 1 ? "s" : ""}`);
316
316
+
}
317
317
+
318
318
+
new Notice(messages.length > 0 ? messages.join(". ") : "Saved");
199
319
this.close();
200
320
this.onSuccess?.();
201
321
} catch (err) {
-2
src/components/profileIcon.ts
···
39
39
return wrapper;
40
40
}
41
41
42
42
-
// Avatar button
43
42
const avatarBtn = wrapper.createEl("button", { cls: "semble-avatar-btn" });
44
43
45
44
if (profile.avatar) {
···
57
56
avatarBtn.createEl("span", { text: initials, cls: "semble-avatar-initials" });
58
57
}
59
58
60
60
-
// User info (display name and handle)
61
59
const info = wrapper.createEl("div", { cls: "semble-profile-info" });
62
60
63
61
if (profile.displayName) {
+1
src/lib.ts
···
18
18
getMarginCollections,
19
19
getMarginCollectionItems,
20
20
createMarginCollection,
21
21
+
createMarginCollectionItem,
21
22
} from "./lib/margin";
+22
src/lib/margin.ts
···
76
76
},
77
77
});
78
78
}
79
79
+
80
80
+
export async function createMarginCollectionItem(
81
81
+
client: Client,
82
82
+
repo: string,
83
83
+
annotationUri: string,
84
84
+
collectionUri: string,
85
85
+
position?: number
86
86
+
) {
87
87
+
return await client.post("com.atproto.repo.createRecord", {
88
88
+
input: {
89
89
+
repo: repo as ActorIdentifier,
90
90
+
collection: "at.margin.collectionItem" as Nsid,
91
91
+
record: {
92
92
+
$type: "at.margin.collectionItem",
93
93
+
annotation: annotationUri,
94
94
+
collection: collectionUri,
95
95
+
position,
96
96
+
createdAt: new Date().toISOString(),
97
97
+
},
98
98
+
},
99
99
+
});
100
100
+
}
-6
src/sources/bookmark.ts
···
52
52
const bookmark = this.record.value;
53
53
const enriched = bookmark.enriched;
54
54
55
55
-
// Display tags
56
55
if (bookmark.tags && bookmark.tags.length > 0) {
57
56
const tagsContainer = el.createEl("div", { cls: "atmark-item-tags" });
58
57
for (const tag of bookmark.tags) {
···
129
128
});
130
129
link.setAttr("target", "_blank");
131
130
132
132
-
// Tags section
133
131
if (bookmark.tags && bookmark.tags.length > 0) {
134
132
const tagsSection = container.createEl("div", { cls: "atmark-item-tags-section" });
135
133
tagsSection.createEl("h3", { text: "Tags", cls: "atmark-detail-section-title" });
···
165
163
166
164
let bookmarks = bookmarksResp.data.records as BookmarkRecord[];
167
165
168
168
-
// Apply tag filter if specified
169
166
const tagFilter = filters.find(f => f.type === "bookmarkTag");
170
167
if (tagFilter && tagFilter.value) {
171
168
bookmarks = bookmarks.filter((record: BookmarkRecord) =>
···
180
177
const bookmarksResp = await getBookmarks(this.client, this.repo);
181
178
if (!bookmarksResp.ok) return [];
182
179
183
183
-
// Extract unique tags
184
180
const tagSet = new Set<string>();
185
181
const records = bookmarksResp.data.records as BookmarkRecord[];
186
182
for (const record of records) {
···
212
208
213
209
const chips = section.createEl("div", { cls: "atmark-filter-chips" });
214
210
215
215
-
// All chip
216
211
const allChip = chips.createEl("button", {
217
212
text: "All",
218
213
cls: `atmark-chip ${!activeFilters.has("bookmarkTag") ? "atmark-chip-active" : ""}`,
···
222
217
onChange();
223
218
});
224
219
225
225
-
// Get tags and render chips
226
220
void this.getAvailableFilters().then(tags => {
227
221
for (const tag of tags) {
228
222
const chip = chips.createEl("button", {
-13
src/sources/margin.ts
···
57
57
const el = container.createEl("div", { cls: "atmark-item-content" });
58
58
const bookmark = this.record.value;
59
59
60
60
-
// Display collections
61
60
if (this.collections.length > 0) {
62
61
const collectionsContainer = el.createEl("div", { cls: "atmark-item-collections" });
63
62
for (const collection of this.collections) {
···
65
64
}
66
65
}
67
66
68
68
-
// Display tags
69
67
if (bookmark.tags && bookmark.tags.length > 0) {
70
68
const tagsContainer = el.createEl("div", { cls: "atmark-item-tags" });
71
69
for (const tag of bookmark.tags) {
···
112
110
});
113
111
link.setAttr("target", "_blank");
114
112
115
115
-
// Collections section
116
113
if (this.collections.length > 0) {
117
114
const collectionsSection = container.createEl("div", { cls: "atmark-item-collections-section" });
118
115
collectionsSection.createEl("h3", { text: "Collections", cls: "atmark-detail-section-title" });
···
122
119
}
123
120
}
124
121
125
125
-
// Tags section
126
122
if (bookmark.tags && bookmark.tags.length > 0) {
127
123
const tagsSection = container.createEl("div", { cls: "atmark-item-tags-section" });
128
124
tagsSection.createEl("h3", { text: "Tags", cls: "atmark-detail-section-title" });
···
184
180
}
185
181
}
186
182
187
187
-
// Apply collection filter if specified
188
183
const collectionFilter = filters.find(f => f.type === "marginCollection");
189
184
if (collectionFilter && collectionFilter.value) {
190
185
if (itemsResp.ok) {
···
197
192
}
198
193
}
199
194
200
200
-
// Apply tag filter if specified
201
195
const tagFilter = filters.find(f => f.type === "marginTag");
202
196
if (tagFilter && tagFilter.value) {
203
197
bookmarks = bookmarks.filter((record: MarginBookmarkRecord) =>
···
213
207
async getAvailableFilters(): Promise<SourceFilter[]> {
214
208
const filters: SourceFilter[] = [];
215
209
216
216
-
// Get collections
217
210
const collectionsResp = await getMarginCollections(this.client, this.repo);
218
211
if (collectionsResp.ok) {
219
212
const collections = collectionsResp.data.records as MarginCollectionRecord[];
···
224
217
})));
225
218
}
226
219
227
227
-
// Get tags
228
220
const bookmarksResp = await getMarginBookmarks(this.client, this.repo);
229
221
if (bookmarksResp.ok) {
230
222
const tagSet = new Set<string>();
···
247
239
}
248
240
249
241
renderFilterUI(container: HTMLElement, activeFilters: Map<string, SourceFilter>, onChange: () => void, plugin: ATmarkPlugin): void {
250
250
-
// Collections section
251
242
const collectionsSection = container.createEl("div", { cls: "atmark-filter-section" });
252
243
253
244
const collectionsTitleRow = collectionsSection.createEl("div", { cls: "atmark-filter-title-row" });
···
261
252
262
253
const collectionsChips = collectionsSection.createEl("div", { cls: "atmark-filter-chips" });
263
254
264
264
-
// All collections chip
265
255
const allCollectionsChip = collectionsChips.createEl("button", {
266
256
text: "All",
267
257
cls: `atmark-chip ${!activeFilters.has("marginCollection") ? "atmark-chip-active" : ""}`,
···
271
261
onChange();
272
262
});
273
263
274
274
-
// Tags section
275
264
const tagsSection = container.createEl("div", { cls: "atmark-filter-section" });
276
265
277
266
const tagsTitleRow = tagsSection.createEl("div", { cls: "atmark-filter-title-row" });
···
279
268
280
269
const tagsChips = tagsSection.createEl("div", { cls: "atmark-filter-chips" });
281
270
282
282
-
// All tags chip
283
271
const allTagsChip = tagsChips.createEl("button", {
284
272
text: "All",
285
273
cls: `atmark-chip ${!activeFilters.has("marginTag") ? "atmark-chip-active" : ""}`,
···
289
277
onChange();
290
278
});
291
279
292
292
-
// Get filters and render chips
293
280
void this.getAvailableFilters().then(filters => {
294
281
for (const filter of filters) {
295
282
if (filter.type === "marginCollection") {
-4
src/sources/semble.ts
···
163
163
164
164
const allSembleCards = cardsResp.data.records as CardRecord[];
165
165
166
166
-
// Build notes map
167
166
const notesMap = new Map<string, Array<{ uri: string; text: string }>>();
168
167
for (const record of allSembleCards) {
169
168
if (record.value.type === "NOTE") {
···
186
185
return true;
187
186
});
188
187
189
189
-
// Apply collection filter if specified
190
188
const collectionFilter = filters.find(f => f.type === "sembleCollection");
191
189
if (collectionFilter && collectionFilter.value) {
192
190
const linksResp = await getCollectionLinks(this.client, this.repo);
···
200
198
}
201
199
}
202
200
203
203
-
// Create SembleItem objects
204
201
return sembleCards.map((record: CardRecord) =>
205
202
new SembleItem(record, notesMap.get(record.uri) || [], plugin)
206
203
);
···
232
229
233
230
const chips = section.createEl("div", { cls: "atmark-filter-chips" });
234
231
235
235
-
// All chip
236
232
const allChip = chips.createEl("button", {
237
233
text: "All",
238
234
cls: `atmark-chip ${!activeFilters.has("sembleCollection") ? "atmark-chip-active" : ""}`,
-3
src/views/atmark.ts
···
24
24
super(leaf);
25
25
this.plugin = plugin;
26
26
27
27
-
// Initialize sources
28
27
if (this.plugin.client) {
29
28
const repo = this.plugin.settings.identifier;
30
29
this.sources.set("semble", {
···
140
139
});
141
140
}
142
141
143
143
-
// Let the active source render its filters
144
142
const filtersContainer = container.createEl("div", { cls: "atmark-filters" });
145
143
const sourceData = this.sources.get(this.activeSource);
146
144
if (sourceData) {
···
173
171
cls: `atmark-badge atmark-badge-${source}`,
174
172
});
175
173
176
176
-
// Add edit button if item supports it
177
174
if (item.canEdit()) {
178
175
const editBtn = header.createEl("button", {
179
176
cls: "atmark-item-edit-btn",
+86
-178
styles.css
···
628
628
flex-shrink: 0;
629
629
}
630
630
631
631
+
632
632
+
.atmark-collection-list {
633
633
+
display: flex;
634
634
+
flex-direction: column;
635
635
+
gap: 8px;
636
636
+
max-height: 200px;
637
637
+
overflow-y: auto;
638
638
+
}
639
639
+
640
640
+
.atmark-collection-item {
641
641
+
display: flex;
642
642
+
align-items: center;
643
643
+
gap: 12px;
644
644
+
padding: 10px 12px;
645
645
+
background: var(--background-secondary);
646
646
+
border: 1px solid var(--background-modifier-border);
647
647
+
border-radius: var(--radius-m);
648
648
+
cursor: pointer;
649
649
+
transition: all 0.15s ease;
650
650
+
}
651
651
+
652
652
+
.atmark-collection-item:hover {
653
653
+
background: var(--background-modifier-hover);
654
654
+
border-color: var(--background-modifier-border-hover);
655
655
+
}
656
656
+
657
657
+
.atmark-collection-checkbox {
658
658
+
width: 18px;
659
659
+
height: 18px;
660
660
+
margin: 0;
661
661
+
cursor: pointer;
662
662
+
accent-color: var(--interactive-accent);
663
663
+
}
664
664
+
665
665
+
.atmark-collection-item-info {
666
666
+
display: flex;
667
667
+
flex-direction: column;
668
668
+
gap: 2px;
669
669
+
flex: 1;
670
670
+
}
671
671
+
672
672
+
.atmark-collection-item-name {
673
673
+
font-weight: var(--font-medium);
674
674
+
color: var(--text-normal);
675
675
+
}
676
676
+
677
677
+
.atmark-collection-item-desc {
678
678
+
font-size: var(--font-small);
679
679
+
color: var(--text-muted);
680
680
+
}
681
681
+
682
682
+
.atmark-tag-list {
683
683
+
display: flex;
684
684
+
flex-wrap: wrap;
685
685
+
gap: 6px;
686
686
+
margin-bottom: 8px;
687
687
+
}
688
688
+
689
689
+
.atmark-tag-item {
690
690
+
display: flex;
691
691
+
align-items: center;
692
692
+
padding: 4px 12px;
693
693
+
background: var(--background-modifier-border);
694
694
+
border-radius: var(--radius-m);
695
695
+
cursor: pointer;
696
696
+
transition: all 0.15s ease;
697
697
+
font-size: var(--font-small);
698
698
+
color: var(--text-muted);
699
699
+
}
700
700
+
701
701
+
.atmark-tag-item:hover {
702
702
+
background: var(--background-modifier-border-hover);
703
703
+
color: var(--text-normal);
704
704
+
}
705
705
+
706
706
+
.atmark-tag-item:has(input:checked) {
707
707
+
background: var(--interactive-accent);
708
708
+
color: var(--text-on-accent);
709
709
+
}
710
710
+
711
711
+
.atmark-tag-item input {
712
712
+
display: none;
713
713
+
}
714
714
+
631
715
/* Semble-specific styles (for NOTE cards and attached notes) */
632
716
.semble-card-note {
633
717
margin: 0;
···
895
979
line-height: 1.2;
896
980
}
897
981
898
898
-
/* Semble-specific Collection UI */
899
899
-
.semble-collection-modal {
900
900
-
padding: 16px;
901
901
-
}
902
902
-
903
903
-
.semble-collection-modal h2 {
904
904
-
margin: 0 0 16px 0;
905
905
-
font-size: var(--h2-size);
906
906
-
font-weight: var(--font-semibold);
907
907
-
color: var(--text-normal);
908
908
-
}
909
909
-
910
910
-
.semble-collection-list {
911
911
-
display: flex;
912
912
-
flex-direction: column;
913
913
-
gap: 8px;
914
914
-
max-height: 300px;
915
915
-
overflow-y: auto;
916
916
-
margin-bottom: 16px;
917
917
-
}
918
918
-
919
919
-
.semble-collection-item {
920
920
-
display: flex;
921
921
-
align-items: center;
922
922
-
gap: 12px;
923
923
-
padding: 12px 16px;
924
924
-
background: var(--background-secondary);
925
925
-
border: 1px solid var(--background-modifier-border);
926
926
-
border-radius: var(--radius-m);
927
927
-
cursor: pointer;
928
928
-
transition: all 0.15s ease;
929
929
-
}
930
930
-
931
931
-
.semble-collection-item:hover {
932
932
-
background: var(--background-modifier-hover);
933
933
-
border-color: var(--background-modifier-border-hover);
934
934
-
}
935
935
-
936
936
-
.semble-collection-checkbox {
937
937
-
width: 18px;
938
938
-
height: 18px;
939
939
-
margin: 0;
940
940
-
cursor: pointer;
941
941
-
accent-color: var(--interactive-accent);
942
942
-
}
943
943
-
944
944
-
.semble-collection-item-info {
945
945
-
display: flex;
946
946
-
flex-direction: column;
947
947
-
gap: 2px;
948
948
-
flex: 1;
949
949
-
}
950
950
-
951
951
-
.semble-collection-item-name {
952
952
-
font-weight: var(--font-semibold);
953
953
-
color: var(--text-normal);
954
954
-
}
955
955
-
956
956
-
.semble-collection-item-desc {
957
957
-
font-size: var(--font-small);
958
958
-
color: var(--text-muted);
959
959
-
}
960
960
-
961
961
-
/* Semble-specific Toolbar */
982
982
+
/* Semble Toolbar */
962
983
.semble-toolbar {
963
984
display: flex;
964
985
align-items: center;
···
1091
1112
height: 14px;
1092
1113
}
1093
1114
1094
1094
-
/* Semble-specific legacy classes that need to be migrated to atmark-* */
1095
1095
-
.semble-modal-actions {
1096
1096
-
display: flex;
1097
1097
-
align-items: center;
1098
1098
-
gap: 8px;
1099
1099
-
padding-top: 16px;
1100
1100
-
border-top: 1px solid var(--background-modifier-border);
1101
1101
-
}
1102
1102
-
1103
1103
-
.semble-spacer {
1104
1104
-
flex: 1;
1105
1105
-
}
1106
1106
-
1107
1107
-
.semble-btn {
1108
1108
-
padding: 8px 16px;
1109
1109
-
border-radius: var(--radius-s);
1110
1110
-
font-size: var(--font-small);
1111
1111
-
font-weight: var(--font-medium);
1112
1112
-
cursor: pointer;
1113
1113
-
transition: all 0.15s ease;
1114
1114
-
}
1115
1115
-
1116
1116
-
.semble-btn:disabled {
1117
1117
-
opacity: 0.5;
1118
1118
-
cursor: not-allowed;
1119
1119
-
}
1120
1120
-
1121
1121
-
.semble-btn-secondary {
1122
1122
-
background: var(--background-secondary);
1123
1123
-
border: 1px solid var(--background-modifier-border);
1124
1124
-
color: var(--text-normal);
1125
1125
-
}
1126
1126
-
1127
1127
-
.semble-btn-secondary:hover:not(:disabled) {
1128
1128
-
background: var(--background-modifier-hover);
1129
1129
-
}
1130
1130
-
1131
1131
-
.semble-btn-primary {
1132
1132
-
background: var(--interactive-accent);
1133
1133
-
border: 1px solid var(--interactive-accent);
1134
1134
-
color: var(--text-on-accent);
1135
1135
-
}
1136
1136
-
1137
1137
-
.semble-btn-primary:hover:not(:disabled) {
1138
1138
-
background: var(--interactive-accent-hover);
1139
1139
-
}
1140
1140
-
1141
1141
-
.semble-btn-danger {
1142
1142
-
background: color-mix(in srgb, var(--color-red) 15%, transparent);
1143
1143
-
border: none;
1144
1144
-
color: var(--color-red);
1145
1145
-
}
1146
1146
-
1147
1147
-
.semble-btn-danger:hover:not(:disabled) {
1148
1148
-
background: color-mix(in srgb, var(--color-red) 25%, transparent);
1149
1149
-
}
1150
1150
-
1151
1151
-
.semble-warning-text {
1152
1152
-
color: var(--text-muted);
1153
1153
-
margin-bottom: 16px;
1154
1154
-
}
1155
1155
-
1156
1156
-
.semble-form {
1157
1157
-
display: flex;
1158
1158
-
flex-direction: column;
1159
1159
-
gap: 16px;
1160
1160
-
}
1161
1161
-
1162
1162
-
.semble-form-group {
1163
1163
-
display: flex;
1164
1164
-
flex-direction: column;
1165
1165
-
gap: 6px;
1166
1166
-
}
1167
1167
-
1168
1168
-
.semble-form-group label {
1169
1169
-
font-size: var(--font-small);
1170
1170
-
font-weight: var(--font-medium);
1171
1171
-
color: var(--text-normal);
1172
1172
-
}
1173
1173
-
1174
1174
-
.semble-input,
1175
1175
-
.semble-textarea {
1176
1176
-
padding: 8px 12px;
1177
1177
-
background: var(--background-primary);
1178
1178
-
border: 1px solid var(--background-modifier-border);
1179
1179
-
border-radius: var(--radius-s);
1180
1180
-
color: var(--text-normal);
1181
1181
-
font-size: var(--font-ui-medium);
1182
1182
-
font-family: inherit;
1183
1183
-
transition: border-color 0.15s ease;
1184
1184
-
}
1185
1185
-
1186
1186
-
.semble-input:focus,
1187
1187
-
.semble-textarea:focus {
1188
1188
-
outline: none;
1189
1189
-
border-color: var(--interactive-accent);
1190
1190
-
box-shadow: 0 0 0 2px var(--background-modifier-border-focus);
1191
1191
-
}
1192
1192
-
1193
1193
-
.semble-input::placeholder,
1194
1194
-
.semble-textarea::placeholder {
1195
1195
-
color: var(--text-faint);
1196
1196
-
}
1197
1197
-
1198
1198
-
.semble-textarea {
1199
1199
-
resize: vertical;
1200
1200
-
min-height: 60px;
1201
1201
-
}
1202
1202
-
1203
1203
-
.semble-error {
1204
1204
-
color: var(--text-error);
1205
1205
-
}
1206
1206
-
1207
1207
-
/* Responsive styles for mobile and narrow views */
1115
1115
+
/* Responsive styles */
1208
1116
@media (max-width: 600px) {
1209
1117
.atmark-view {
1210
1118
padding: 12px;