Various AT Protocol integrations with obsidian

support editing margin tags

+384 -358
-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 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 31 this.item.renderDetail(contentEl); 32 33 // Render notes with delete buttons (semble-specific) ··· 40 this.renderAddNoteForm(contentEl); 41 } 42 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 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" }); 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" }); 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" }); 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 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 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 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 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 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 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 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 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 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 9 export class EditBookmarkModal extends Modal { 10 plugin: ATmarkPlugin; 11 record: BookmarkRecord; 12 onSuccess?: () => void; 13 - tagInputs: HTMLInputElement[] = []; 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 || []; 35 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, ""); 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 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); 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 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; 132 }); 133 + item.createEl("span", { text: state.tag }); 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 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 { 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 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 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" }); 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", () => { 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 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 199 for (const state of toRemove) { 200 if (state.linkUri) { 201 const rkey = state.linkUri.split("/").pop(); ··· 210 } 211 } 212 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"; 4 import type ATmarkPlugin from "../main"; 5 - import { putRecord, deleteRecord } from "../lib"; 6 7 type MarginBookmarkRecord = Record & { value: MarginBookmark }; 8 9 export class EditMarginBookmarkModal extends Modal { 10 plugin: ATmarkPlugin; 11 record: MarginBookmarkRecord; 12 onSuccess?: () => void; 13 - tagInputs: HTMLInputElement[] = []; 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 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, ""); 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: MarginBookmark = { 186 ...this.record.value, 187 tags, ··· 195 updatedRecord 196 ); 197 198 - new Notice("Tags updated"); 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 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); 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..." } 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; 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 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 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 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, 21 } from "./lib/margin";
··· 18 getMarginCollections, 19 getMarginCollectionItems, 20 createMarginCollection, 21 + createMarginCollectionItem, 22 } from "./lib/margin";
+22
src/lib/margin.ts
··· 76 }, 77 }); 78 }
··· 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 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 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 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 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 211 const allChip = chips.createEl("button", { 212 text: "All", 213 cls: `atmark-chip ${!activeFilters.has("bookmarkTag") ? "atmark-chip-active" : ""}`, ··· 217 onChange(); 218 }); 219 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 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 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 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 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 183 const collectionFilter = filters.find(f => f.type === "marginCollection"); 184 if (collectionFilter && collectionFilter.value) { 185 if (itemsResp.ok) { ··· 192 } 193 } 194 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 210 const collectionsResp = await getMarginCollections(this.client, this.repo); 211 if (collectionsResp.ok) { 212 const collections = collectionsResp.data.records as MarginCollectionRecord[]; ··· 217 }))); 218 } 219 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 { 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 255 const allCollectionsChip = collectionsChips.createEl("button", { 256 text: "All", 257 cls: `atmark-chip ${!activeFilters.has("marginCollection") ? "atmark-chip-active" : ""}`, ··· 261 onChange(); 262 }); 263 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 271 const allTagsChip = tagsChips.createEl("button", { 272 text: "All", 273 cls: `atmark-chip ${!activeFilters.has("marginTag") ? "atmark-chip-active" : ""}`, ··· 277 onChange(); 278 }); 279 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 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 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 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 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 27 if (this.plugin.client) { 28 const repo = this.plugin.settings.identifier; 29 this.sources.set("semble", { ··· 139 }); 140 } 141 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 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 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 */ 983 .semble-toolbar { 984 display: flex; 985 align-items: center; ··· 1112 height: 14px; 1113 } 1114 1115 + /* Responsive styles */ 1116 @media (max-width: 600px) { 1117 .atmark-view { 1118 padding: 12px;