Various AT Protocol integrations with obsidian

support editing margin tags

+384 -358
-4
src/components/cardDetailModal.ts
··· 21 21 contentEl.empty(); 22 22 contentEl.addClass("semble-detail-modal"); 23 23 24 - // Header with source badge 25 24 const header = contentEl.createEl("div", { cls: "semble-detail-header" }); 26 25 const source = this.item.getSource(); 27 26 header.createEl("span", { ··· 29 28 cls: `semble-badge semble-badge-source semble-badge-${source}`, 30 29 }); 31 30 32 - // Render item detail content 33 31 this.item.renderDetail(contentEl); 34 32 35 33 // Render notes with delete buttons (semble-specific) ··· 42 40 this.renderAddNoteForm(contentEl); 43 41 } 44 42 45 - // Footer with date 46 43 const footer = contentEl.createEl("div", { cls: "semble-detail-footer" }); 47 44 footer.createEl("span", { 48 45 text: `Created ${new Date(this.item.getCreatedAt()).toLocaleDateString()}`, ··· 65 62 setIcon(noteIcon, "message-square"); 66 63 noteContent.createEl("p", { text: note.text, cls: "semble-detail-note-text" }); 67 64 68 - // Delete button 69 65 const deleteBtn = noteEl.createEl("button", { cls: "semble-note-delete-btn" }); 70 66 setIcon(deleteBtn, "trash-2"); 71 67 deleteBtn.addEventListener("click", () => {
+9 -13
src/components/createCollectionModal.ts
··· 15 15 onOpen() { 16 16 const { contentEl } = this; 17 17 contentEl.empty(); 18 - contentEl.addClass("semble-collection-modal"); 18 + contentEl.addClass("atmark-modal"); 19 19 20 20 contentEl.createEl("h2", { text: "New collection" }); 21 21 ··· 24 24 return; 25 25 } 26 26 27 - const form = contentEl.createEl("form", { cls: "semble-form" }); 27 + const form = contentEl.createEl("form", { cls: "atmark-form" }); 28 28 29 - // Name field 30 - const nameGroup = form.createEl("div", { cls: "semble-form-group" }); 29 + const nameGroup = form.createEl("div", { cls: "atmark-form-group" }); 31 30 nameGroup.createEl("label", { text: "Name", attr: { for: "collection-name" } }); 32 31 const nameInput = nameGroup.createEl("input", { 33 32 type: "text", 34 - cls: "semble-input", 33 + cls: "atmark-input", 35 34 attr: { id: "collection-name", placeholder: "Collection name", required: "true" }, 36 35 }); 37 36 38 - // Description field 39 - const descGroup = form.createEl("div", { cls: "semble-form-group" }); 37 + const descGroup = form.createEl("div", { cls: "atmark-form-group" }); 40 38 descGroup.createEl("label", { text: "Description", attr: { for: "collection-desc" } }); 41 39 const descInput = descGroup.createEl("textarea", { 42 - cls: "semble-textarea", 40 + cls: "atmark-textarea", 43 41 attr: { id: "collection-desc", placeholder: "Optional description", rows: "3" }, 44 42 }); 45 43 46 - // Action buttons 47 - const actions = form.createEl("div", { cls: "semble-modal-actions" }); 44 + const actions = form.createEl("div", { cls: "atmark-modal-actions" }); 48 45 49 46 const cancelBtn = actions.createEl("button", { 50 47 text: "Cancel", 51 - cls: "semble-btn semble-btn-secondary", 48 + cls: "atmark-btn atmark-btn-secondary", 52 49 type: "button", 53 50 }); 54 51 cancelBtn.addEventListener("click", () => this.close()); 55 52 56 53 const createBtn = actions.createEl("button", { 57 54 text: "Create", 58 - cls: "semble-btn semble-btn-primary", 55 + cls: "atmark-btn atmark-btn-primary", 59 56 type: "submit", 60 57 }); 61 58 ··· 64 61 void this.handleSubmit(nameInput, descInput, createBtn); 65 62 }); 66 63 67 - // Focus name input 68 64 nameInput.focus(); 69 65 } 70 66
-5
src/components/createMarginCollectionModal.ts
··· 26 26 27 27 const form = contentEl.createEl("form", { cls: "atmark-form" }); 28 28 29 - // Name field 30 29 const nameGroup = form.createEl("div", { cls: "atmark-form-group" }); 31 30 nameGroup.createEl("label", { text: "Name", attr: { for: "collection-name" } }); 32 31 const nameInput = nameGroup.createEl("input", { ··· 35 34 attr: { id: "collection-name", placeholder: "Collection name", required: "true" }, 36 35 }); 37 36 38 - // Icon field 39 37 const iconGroup = form.createEl("div", { cls: "atmark-form-group" }); 40 38 iconGroup.createEl("label", { text: "Icon (optional)", attr: { for: "collection-icon" } }); 41 39 const iconInput = iconGroup.createEl("input", { ··· 44 42 attr: { id: "collection-icon" }, 45 43 }); 46 44 47 - // Description field 48 45 const descGroup = form.createEl("div", { cls: "atmark-form-group" }); 49 46 descGroup.createEl("label", { text: "Description", attr: { for: "collection-desc" } }); 50 47 const descInput = descGroup.createEl("textarea", { ··· 52 49 attr: { id: "collection-desc", placeholder: "Optional description", rows: "3" }, 53 50 }); 54 51 55 - // Action buttons 56 52 const actions = form.createEl("div", { cls: "atmark-modal-actions" }); 57 53 58 54 const cancelBtn = actions.createEl("button", { ··· 73 69 void this.handleSubmit(nameInput, iconInput, descInput, createBtn); 74 70 }); 75 71 76 - // Focus name input 77 72 nameInput.focus(); 78 73 } 79 74
-3
src/components/createTagModal.ts
··· 26 26 27 27 const form = contentEl.createEl("form", { cls: "atmark-form" }); 28 28 29 - // Tag value field 30 29 const tagGroup = form.createEl("div", { cls: "atmark-form-group" }); 31 30 tagGroup.createEl("label", { text: "Tag", attr: { for: "tag-value" } }); 32 31 const tagInput = tagGroup.createEl("input", { ··· 35 34 attr: { id: "tag-value", placeholder: "Tag name", required: "true" }, 36 35 }); 37 36 38 - // Action buttons 39 37 const actions = form.createEl("div", { cls: "atmark-modal-actions" }); 40 38 41 39 const cancelBtn = actions.createEl("button", { ··· 56 54 void this.handleSubmit(tagInput, createBtn); 57 55 }); 58 56 59 - // Focus tag input 60 57 tagInput.focus(); 61 58 } 62 59
+75 -48
src/components/editBookmarkModal.ts
··· 2 2 import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 3 import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 4 4 import type ATmarkPlugin from "../main"; 5 - import { putRecord, deleteRecord } from "../lib"; 5 + import { putRecord, deleteRecord, getBookmarks } from "../lib"; 6 6 7 7 type BookmarkRecord = Record & { value: Bookmark }; 8 8 9 + interface TagState { 10 + tag: string; 11 + isSelected: boolean; 12 + } 13 + 9 14 export class EditBookmarkModal extends Modal { 10 15 plugin: ATmarkPlugin; 11 16 record: BookmarkRecord; 12 17 onSuccess?: () => void; 13 - tagInputs: HTMLInputElement[] = []; 18 + tagStates: TagState[] = []; 19 + newTagInput: HTMLInputElement | null = null; 14 20 15 21 constructor(plugin: ATmarkPlugin, record: BookmarkRecord, onSuccess?: () => void) { 16 22 super(plugin.app); ··· 19 25 this.onSuccess = onSuccess; 20 26 } 21 27 22 - onOpen() { 28 + async onOpen() { 23 29 const { contentEl } = this; 24 30 contentEl.empty(); 25 31 contentEl.addClass("atmark-modal"); 26 32 27 - contentEl.createEl("h2", { text: "Edit bookmark tags" }); 33 + contentEl.createEl("h2", { text: "Edit bookmark" }); 28 34 29 35 if (!this.plugin.client) { 30 36 contentEl.createEl("p", { text: "Not connected." }); 31 37 return; 32 38 } 33 39 34 - const existingTags = this.record.value.tags || []; 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 + } 35 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) { 36 72 const form = contentEl.createEl("div", { cls: "atmark-form" }); 37 73 38 - // Tags section 39 74 const tagsGroup = form.createEl("div", { cls: "atmark-form-group" }); 40 75 tagsGroup.createEl("label", { text: "Tags" }); 41 76 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); 77 + const tagsList = tagsGroup.createEl("div", { cls: "atmark-tag-list" }); 78 + for (const state of this.tagStates) { 79 + this.addTagChip(tagsList, state); 47 80 } 48 81 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" 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" } 56 92 }); 57 - addTagBtn.addEventListener("click", (e) => { 58 - e.preventDefault(); 59 - this.addTagInput(tagsContainer, ""); 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 + } 60 101 }); 61 102 62 - // Action buttons 63 103 const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 64 104 65 105 const deleteBtn = actions.createEl("button", { ··· 83 123 saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 84 124 } 85 125 86 - 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..." } 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; 94 132 }); 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 - }); 133 + item.createEl("span", { text: state.tag }); 106 134 } 107 135 108 136 private confirmDelete(contentEl: HTMLElement) { ··· 167 195 contentEl.createEl("p", { text: "Saving changes..." }); 168 196 169 197 try { 170 - // 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 - )]; 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)]; 176 204 177 205 const rkey = this.record.uri.split("/").pop(); 178 206 if (!rkey) { ··· 181 209 return; 182 210 } 183 211 184 - // Update the record with new tags 185 212 const updatedRecord: Bookmark = { 186 213 ...this.record.value, 187 214 tags,
+23 -31
src/components/editCardModal.ts
··· 40 40 async onOpen() { 41 41 const { contentEl } = this; 42 42 contentEl.empty(); 43 - contentEl.addClass("semble-collection-modal"); 43 + contentEl.addClass("atmark-modal"); 44 44 45 45 contentEl.createEl("h2", { text: "Edit collections" }); 46 46 ··· 52 52 const loading = contentEl.createEl("p", { text: "Loading..." }); 53 53 54 54 try { 55 - // Fetch collections and existing links in parallel 56 55 const [collectionsResp, linksResp] = await Promise.all([ 57 56 getCollections(this.plugin.client, this.plugin.settings.identifier), 58 57 getCollectionLinks(this.plugin.client, this.plugin.settings.identifier), ··· 61 60 loading.remove(); 62 61 63 62 if (!collectionsResp.ok) { 64 - contentEl.createEl("p", { text: "Failed to load collections.", cls: "semble-error" }); 63 + contentEl.createEl("p", { text: "Failed to load collections.", cls: "atmark-error" }); 65 64 return; 66 65 } 67 66 ··· 73 72 return; 74 73 } 75 74 76 - // Find which collections this card is already in 77 75 const cardLinks = links.filter(link => link.value.card.uri === this.cardUri); 78 76 const linkedCollectionUris = new Map<string, string>(); 79 77 for (const link of cardLinks) { 80 78 linkedCollectionUris.set(link.value.collection.uri, link.uri); 81 79 } 82 80 83 - // Build collection states 84 81 this.collectionStates = collections.map(collection => ({ 85 82 collection, 86 83 isSelected: linkedCollectionUris.has(collection.uri), ··· 92 89 } catch (err) { 93 90 loading.remove(); 94 91 const message = err instanceof Error ? err.message : String(err); 95 - contentEl.createEl("p", { text: `Error: ${message}`, cls: "semble-error" }); 92 + contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmark-error" }); 96 93 } 97 94 } 98 95 99 96 private renderCollectionList(contentEl: HTMLElement) { 100 - const list = contentEl.createEl("div", { cls: "semble-collection-list" }); 97 + const list = contentEl.createEl("div", { cls: "atmark-collection-list" }); 101 98 102 99 for (const state of this.collectionStates) { 103 - const item = list.createEl("label", { cls: "semble-collection-item" }); 100 + const item = list.createEl("label", { cls: "atmark-collection-item" }); 104 101 105 - const checkbox = item.createEl("input", { type: "checkbox", cls: "semble-collection-checkbox" }); 102 + const checkbox = item.createEl("input", { type: "checkbox", cls: "atmark-collection-checkbox" }); 106 103 checkbox.checked = state.isSelected; 107 104 checkbox.addEventListener("change", () => { 108 105 state.isSelected = checkbox.checked; 109 106 this.updateSaveButton(); 110 107 }); 111 108 112 - const info = item.createEl("div", { cls: "semble-collection-item-info" }); 113 - info.createEl("span", { text: state.collection.value.name, cls: "semble-collection-item-name" }); 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" }); 114 111 if (state.collection.value.description) { 115 - info.createEl("span", { text: state.collection.value.description, cls: "semble-collection-item-desc" }); 112 + info.createEl("span", { text: state.collection.value.description, cls: "atmark-collection-item-desc" }); 116 113 } 117 114 } 118 115 119 - // Action buttons 120 - const actions = contentEl.createEl("div", { cls: "semble-modal-actions" }); 116 + const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 121 117 122 - const deleteBtn = actions.createEl("button", { text: "Delete", cls: "semble-btn semble-btn-danger" }); 118 + const deleteBtn = actions.createEl("button", { text: "Delete", cls: "atmark-btn atmark-btn-danger" }); 123 119 deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 124 120 125 - actions.createEl("div", { cls: "semble-spacer" }); 121 + actions.createEl("div", { cls: "atmark-spacer" }); 126 122 127 - const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "semble-btn semble-btn-secondary" }); 123 + const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmark-btn atmark-btn-secondary" }); 128 124 cancelBtn.addEventListener("click", () => { this.close(); }); 129 125 130 - const saveBtn = actions.createEl("button", { text: "Save", cls: "semble-btn semble-btn-primary" }); 131 - saveBtn.id = "semble-save-btn"; 126 + const saveBtn = actions.createEl("button", { text: "Save", cls: "atmark-btn atmark-btn-primary" }); 127 + saveBtn.id = "atmark-save-btn"; 132 128 saveBtn.disabled = true; 133 129 saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 134 130 } ··· 136 132 private confirmDelete(contentEl: HTMLElement) { 137 133 contentEl.empty(); 138 134 contentEl.createEl("h2", { text: "Delete card" }); 139 - contentEl.createEl("p", { text: "Delete this card?", cls: "semble-warning-text" }); 135 + contentEl.createEl("p", { text: "Delete this card?", cls: "atmark-warning-text" }); 140 136 141 - const actions = contentEl.createEl("div", { cls: "semble-modal-actions" }); 137 + const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 142 138 143 - const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "semble-btn semble-btn-secondary" }); 139 + const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmark-btn atmark-btn-secondary" }); 144 140 cancelBtn.addEventListener("click", () => { 145 - // Re-render the modal 146 141 void this.onOpen(); 147 142 }); 148 143 149 - const confirmBtn = actions.createEl("button", { text: "Delete", cls: "semble-btn semble-btn-danger" }); 144 + const confirmBtn = actions.createEl("button", { text: "Delete", cls: "atmark-btn atmark-btn-danger" }); 150 145 confirmBtn.addEventListener("click", () => { void this.deleteCard(); }); 151 146 } 152 147 ··· 161 156 const rkey = this.cardUri.split("/").pop(); 162 157 if (!rkey) { 163 158 contentEl.empty(); 164 - contentEl.createEl("p", { text: "Invalid card uri.", cls: "semble-error" }); 159 + contentEl.createEl("p", { text: "Invalid card uri.", cls: "atmark-error" }); 165 160 return; 166 161 } 167 162 ··· 178 173 } catch (err) { 179 174 contentEl.empty(); 180 175 const message = err instanceof Error ? err.message : String(err); 181 - contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "semble-error" }); 176 + contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmark-error" }); 182 177 } 183 178 } 184 179 185 180 private updateSaveButton() { 186 - const saveBtn = document.getElementById("semble-save-btn") as HTMLButtonElement | null; 181 + const saveBtn = document.getElementById("atmark-save-btn") as HTMLButtonElement | null; 187 182 if (!saveBtn) return; 188 183 189 - // Check if any changes were made 190 184 const hasChanges = this.collectionStates.some(s => s.isSelected !== s.wasSelected); 191 185 saveBtn.disabled = !hasChanges; 192 186 } ··· 202 196 const toAdd = this.collectionStates.filter(s => s.isSelected && !s.wasSelected); 203 197 const toRemove = this.collectionStates.filter(s => !s.isSelected && s.wasSelected); 204 198 205 - // Process removals 206 199 for (const state of toRemove) { 207 200 if (state.linkUri) { 208 201 const rkey = state.linkUri.split("/").pop(); ··· 217 210 } 218 211 } 219 212 220 - // Process additions 221 213 for (const state of toAdd) { 222 214 const collectionRkey = state.collection.uri.split("/").pop(); 223 215 if (!collectionRkey) continue; ··· 256 248 } catch (err) { 257 249 contentEl.empty(); 258 250 const message = err instanceof Error ? err.message : String(err); 259 - contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "semble-error" }); 251 + contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmark-error" }); 260 252 } 261 253 } 262 254
+168 -48
src/components/editMarginBookmarkModal.ts
··· 1 1 import { Modal, Notice } from "obsidian"; 2 2 import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 3 import type { Main as MarginBookmark } from "../lexicons/types/at/margin/bookmark"; 4 + import type { Main as MarginCollection } from "../lexicons/types/at/margin/collection"; 5 + import type { Main as MarginCollectionItem } from "../lexicons/types/at/margin/collectionItem"; 4 6 import type ATmarkPlugin from "../main"; 5 - import { putRecord, deleteRecord } from "../lib"; 7 + import { putRecord, deleteRecord, getMarginCollections, getMarginCollectionItems, createMarginCollectionItem, getMarginBookmarks } from "../lib"; 6 8 7 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 + } 8 24 9 25 export class EditMarginBookmarkModal extends Modal { 10 26 plugin: ATmarkPlugin; 11 27 record: MarginBookmarkRecord; 12 28 onSuccess?: () => void; 13 - tagInputs: HTMLInputElement[] = []; 29 + tagStates: TagState[] = []; 30 + newTagInput: HTMLInputElement | null = null; 31 + collectionStates: CollectionState[] = []; 14 32 15 33 constructor(plugin: ATmarkPlugin, record: MarginBookmarkRecord, onSuccess?: () => void) { 16 34 super(plugin.app); ··· 19 37 this.onSuccess = onSuccess; 20 38 } 21 39 22 - onOpen() { 40 + async onOpen() { 23 41 const { contentEl } = this; 24 42 contentEl.empty(); 25 43 contentEl.addClass("atmark-modal"); ··· 31 49 return; 32 50 } 33 51 34 - const existingTags = this.record.value.tags || []; 52 + const loading = contentEl.createEl("p", { text: "Loading..." }); 35 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) { 36 104 const form = contentEl.createEl("div", { cls: "atmark-form" }); 37 105 38 - // Tags section 39 106 const tagsGroup = form.createEl("div", { cls: "atmark-form-group" }); 40 107 tagsGroup.createEl("label", { text: "Tags" }); 41 108 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); 109 + const tagsList = tagsGroup.createEl("div", { cls: "atmark-tag-list" }); 110 + for (const state of this.tagStates) { 111 + this.addTagChip(tagsList, state); 47 112 } 48 113 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" 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..." } 56 119 }); 57 - addTagBtn.addEventListener("click", (e) => { 58 - e.preventDefault(); 59 - this.addTagInput(tagsContainer, ""); 120 + const addBtn = newTagRow.createEl("button", { 121 + text: "Add", 122 + cls: "atmark-btn atmark-btn-secondary", 123 + attr: { type: "button" } 60 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" }); 61 143 62 - // Action buttons 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 + 63 158 const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 64 159 65 160 const deleteBtn = actions.createEl("button", { ··· 83 178 saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 84 179 } 85 180 86 - 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); 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; 105 187 }); 188 + item.createEl("span", { text: state.tag }); 106 189 } 107 190 108 191 private confirmDelete(contentEl: HTMLElement) { ··· 167 250 contentEl.createEl("p", { text: "Saving changes..." }); 168 251 169 252 try { 170 - // 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 - )]; 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)]; 176 259 177 260 const rkey = this.record.uri.split("/").pop(); 178 261 if (!rkey) { ··· 181 264 return; 182 265 } 183 266 184 - // Update the record with new tags 185 267 const updatedRecord: MarginBookmark = { 186 268 ...this.record.value, 187 269 tags, ··· 195 277 updatedRecord 196 278 ); 197 279 198 - new Notice("Tags updated"); 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"); 199 319 this.close(); 200 320 this.onSuccess?.(); 201 321 } catch (err) {
-2
src/components/profileIcon.ts
··· 39 39 return wrapper; 40 40 } 41 41 42 - // Avatar button 43 42 const avatarBtn = wrapper.createEl("button", { cls: "semble-avatar-btn" }); 44 43 45 44 if (profile.avatar) { ··· 57 56 avatarBtn.createEl("span", { text: initials, cls: "semble-avatar-initials" }); 58 57 } 59 58 60 - // User info (display name and handle) 61 59 const info = wrapper.createEl("div", { cls: "semble-profile-info" }); 62 60 63 61 if (profile.displayName) {
+1
src/lib.ts
··· 18 18 getMarginCollections, 19 19 getMarginCollectionItems, 20 20 createMarginCollection, 21 + createMarginCollectionItem, 21 22 } from "./lib/margin";
+22
src/lib/margin.ts
··· 76 76 }, 77 77 }); 78 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 52 const bookmark = this.record.value; 53 53 const enriched = bookmark.enriched; 54 54 55 - // Display tags 56 55 if (bookmark.tags && bookmark.tags.length > 0) { 57 56 const tagsContainer = el.createEl("div", { cls: "atmark-item-tags" }); 58 57 for (const tag of bookmark.tags) { ··· 129 128 }); 130 129 link.setAttr("target", "_blank"); 131 130 132 - // Tags section 133 131 if (bookmark.tags && bookmark.tags.length > 0) { 134 132 const tagsSection = container.createEl("div", { cls: "atmark-item-tags-section" }); 135 133 tagsSection.createEl("h3", { text: "Tags", cls: "atmark-detail-section-title" }); ··· 165 163 166 164 let bookmarks = bookmarksResp.data.records as BookmarkRecord[]; 167 165 168 - // Apply tag filter if specified 169 166 const tagFilter = filters.find(f => f.type === "bookmarkTag"); 170 167 if (tagFilter && tagFilter.value) { 171 168 bookmarks = bookmarks.filter((record: BookmarkRecord) => ··· 180 177 const bookmarksResp = await getBookmarks(this.client, this.repo); 181 178 if (!bookmarksResp.ok) return []; 182 179 183 - // Extract unique tags 184 180 const tagSet = new Set<string>(); 185 181 const records = bookmarksResp.data.records as BookmarkRecord[]; 186 182 for (const record of records) { ··· 212 208 213 209 const chips = section.createEl("div", { cls: "atmark-filter-chips" }); 214 210 215 - // All chip 216 211 const allChip = chips.createEl("button", { 217 212 text: "All", 218 213 cls: `atmark-chip ${!activeFilters.has("bookmarkTag") ? "atmark-chip-active" : ""}`, ··· 222 217 onChange(); 223 218 }); 224 219 225 - // Get tags and render chips 226 220 void this.getAvailableFilters().then(tags => { 227 221 for (const tag of tags) { 228 222 const chip = chips.createEl("button", {
-13
src/sources/margin.ts
··· 57 57 const el = container.createEl("div", { cls: "atmark-item-content" }); 58 58 const bookmark = this.record.value; 59 59 60 - // Display collections 61 60 if (this.collections.length > 0) { 62 61 const collectionsContainer = el.createEl("div", { cls: "atmark-item-collections" }); 63 62 for (const collection of this.collections) { ··· 65 64 } 66 65 } 67 66 68 - // Display tags 69 67 if (bookmark.tags && bookmark.tags.length > 0) { 70 68 const tagsContainer = el.createEl("div", { cls: "atmark-item-tags" }); 71 69 for (const tag of bookmark.tags) { ··· 112 110 }); 113 111 link.setAttr("target", "_blank"); 114 112 115 - // Collections section 116 113 if (this.collections.length > 0) { 117 114 const collectionsSection = container.createEl("div", { cls: "atmark-item-collections-section" }); 118 115 collectionsSection.createEl("h3", { text: "Collections", cls: "atmark-detail-section-title" }); ··· 122 119 } 123 120 } 124 121 125 - // Tags section 126 122 if (bookmark.tags && bookmark.tags.length > 0) { 127 123 const tagsSection = container.createEl("div", { cls: "atmark-item-tags-section" }); 128 124 tagsSection.createEl("h3", { text: "Tags", cls: "atmark-detail-section-title" }); ··· 184 180 } 185 181 } 186 182 187 - // Apply collection filter if specified 188 183 const collectionFilter = filters.find(f => f.type === "marginCollection"); 189 184 if (collectionFilter && collectionFilter.value) { 190 185 if (itemsResp.ok) { ··· 197 192 } 198 193 } 199 194 200 - // Apply tag filter if specified 201 195 const tagFilter = filters.find(f => f.type === "marginTag"); 202 196 if (tagFilter && tagFilter.value) { 203 197 bookmarks = bookmarks.filter((record: MarginBookmarkRecord) => ··· 213 207 async getAvailableFilters(): Promise<SourceFilter[]> { 214 208 const filters: SourceFilter[] = []; 215 209 216 - // Get collections 217 210 const collectionsResp = await getMarginCollections(this.client, this.repo); 218 211 if (collectionsResp.ok) { 219 212 const collections = collectionsResp.data.records as MarginCollectionRecord[]; ··· 224 217 }))); 225 218 } 226 219 227 - // Get tags 228 220 const bookmarksResp = await getMarginBookmarks(this.client, this.repo); 229 221 if (bookmarksResp.ok) { 230 222 const tagSet = new Set<string>(); ··· 247 239 } 248 240 249 241 renderFilterUI(container: HTMLElement, activeFilters: Map<string, SourceFilter>, onChange: () => void, plugin: ATmarkPlugin): void { 250 - // Collections section 251 242 const collectionsSection = container.createEl("div", { cls: "atmark-filter-section" }); 252 243 253 244 const collectionsTitleRow = collectionsSection.createEl("div", { cls: "atmark-filter-title-row" }); ··· 261 252 262 253 const collectionsChips = collectionsSection.createEl("div", { cls: "atmark-filter-chips" }); 263 254 264 - // All collections chip 265 255 const allCollectionsChip = collectionsChips.createEl("button", { 266 256 text: "All", 267 257 cls: `atmark-chip ${!activeFilters.has("marginCollection") ? "atmark-chip-active" : ""}`, ··· 271 261 onChange(); 272 262 }); 273 263 274 - // Tags section 275 264 const tagsSection = container.createEl("div", { cls: "atmark-filter-section" }); 276 265 277 266 const tagsTitleRow = tagsSection.createEl("div", { cls: "atmark-filter-title-row" }); ··· 279 268 280 269 const tagsChips = tagsSection.createEl("div", { cls: "atmark-filter-chips" }); 281 270 282 - // All tags chip 283 271 const allTagsChip = tagsChips.createEl("button", { 284 272 text: "All", 285 273 cls: `atmark-chip ${!activeFilters.has("marginTag") ? "atmark-chip-active" : ""}`, ··· 289 277 onChange(); 290 278 }); 291 279 292 - // Get filters and render chips 293 280 void this.getAvailableFilters().then(filters => { 294 281 for (const filter of filters) { 295 282 if (filter.type === "marginCollection") {
-4
src/sources/semble.ts
··· 163 163 164 164 const allSembleCards = cardsResp.data.records as CardRecord[]; 165 165 166 - // Build notes map 167 166 const notesMap = new Map<string, Array<{ uri: string; text: string }>>(); 168 167 for (const record of allSembleCards) { 169 168 if (record.value.type === "NOTE") { ··· 186 185 return true; 187 186 }); 188 187 189 - // Apply collection filter if specified 190 188 const collectionFilter = filters.find(f => f.type === "sembleCollection"); 191 189 if (collectionFilter && collectionFilter.value) { 192 190 const linksResp = await getCollectionLinks(this.client, this.repo); ··· 200 198 } 201 199 } 202 200 203 - // Create SembleItem objects 204 201 return sembleCards.map((record: CardRecord) => 205 202 new SembleItem(record, notesMap.get(record.uri) || [], plugin) 206 203 ); ··· 232 229 233 230 const chips = section.createEl("div", { cls: "atmark-filter-chips" }); 234 231 235 - // All chip 236 232 const allChip = chips.createEl("button", { 237 233 text: "All", 238 234 cls: `atmark-chip ${!activeFilters.has("sembleCollection") ? "atmark-chip-active" : ""}`,
-3
src/views/atmark.ts
··· 24 24 super(leaf); 25 25 this.plugin = plugin; 26 26 27 - // Initialize sources 28 27 if (this.plugin.client) { 29 28 const repo = this.plugin.settings.identifier; 30 29 this.sources.set("semble", { ··· 140 139 }); 141 140 } 142 141 143 - // Let the active source render its filters 144 142 const filtersContainer = container.createEl("div", { cls: "atmark-filters" }); 145 143 const sourceData = this.sources.get(this.activeSource); 146 144 if (sourceData) { ··· 173 171 cls: `atmark-badge atmark-badge-${source}`, 174 172 }); 175 173 176 - // Add edit button if item supports it 177 174 if (item.canEdit()) { 178 175 const editBtn = header.createEl("button", { 179 176 cls: "atmark-item-edit-btn",
+86 -178
styles.css
··· 628 628 flex-shrink: 0; 629 629 } 630 630 631 + 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 + 631 715 /* Semble-specific styles (for NOTE cards and attached notes) */ 632 716 .semble-card-note { 633 717 margin: 0; ··· 895 979 line-height: 1.2; 896 980 } 897 981 898 - /* 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 */ 982 + /* Semble Toolbar */ 962 983 .semble-toolbar { 963 984 display: flex; 964 985 align-items: center; ··· 1091 1112 height: 14px; 1092 1113 } 1093 1114 1094 - /* 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 */ 1115 + /* Responsive styles */ 1208 1116 @media (max-width: 600px) { 1209 1117 .atmark-view { 1210 1118 padding: 12px;