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