AT protocol bookmarking platforms in obsidian

show and add card notes

+578 -23
+2
eslint.config.mts
··· 30 30 "version-bump.mjs", 31 31 "versions.json", 32 32 "main.js", 33 + "lex.config.js", 34 + "src/lexicons", 33 35 ]), 34 36 );
+228
src/components/cardDetailModal.ts
··· 1 + import { Modal, Notice, setIcon } from "obsidian"; 2 + import type ATmarkPlugin from "../main"; 3 + import type { Main as Card, NoteContent, UrlContent } from "../lexicons/types/network/cosmik/card"; 4 + import { createNoteCard, deleteRecord } from "../lib"; 5 + 6 + interface AttachedNote { 7 + uri: string; 8 + text: string; 9 + } 10 + 11 + interface CardRecord { 12 + uri: string; 13 + cid: string; 14 + value: Card; 15 + } 16 + 17 + interface CardWithNotes extends CardRecord { 18 + attachedNotes: AttachedNote[]; 19 + } 20 + 21 + export class CardDetailModal extends Modal { 22 + plugin: ATmarkPlugin; 23 + card: CardWithNotes; 24 + onSuccess?: () => void; 25 + noteInput: HTMLTextAreaElement | null = null; 26 + 27 + constructor(plugin: ATmarkPlugin, card: CardWithNotes, onSuccess?: () => void) { 28 + super(plugin.app); 29 + this.plugin = plugin; 30 + this.card = card; 31 + this.onSuccess = onSuccess; 32 + } 33 + 34 + onOpen() { 35 + const { contentEl } = this; 36 + contentEl.empty(); 37 + contentEl.addClass("semble-detail-modal"); 38 + 39 + const card = this.card.value; 40 + 41 + // Header with type badge 42 + const header = contentEl.createEl("div", { cls: "semble-detail-header" }); 43 + header.createEl("span", { 44 + text: card.type, 45 + cls: `semble-badge semble-badge-${card.type?.toLowerCase() || "unknown"}`, 46 + }); 47 + 48 + if (card.type === "NOTE") { 49 + this.renderNoteCard(contentEl, card); 50 + } else if (card.type === "URL") { 51 + this.renderUrlCard(contentEl, card); 52 + } 53 + 54 + // Attached notes section 55 + if (this.card.attachedNotes.length > 0) { 56 + const notesSection = contentEl.createEl("div", { cls: "semble-detail-notes-section" }); 57 + notesSection.createEl("h3", { text: "Notes", cls: "semble-detail-section-title" }); 58 + 59 + for (const note of this.card.attachedNotes) { 60 + const noteEl = notesSection.createEl("div", { cls: "semble-detail-note" }); 61 + 62 + const noteContent = noteEl.createEl("div", { cls: "semble-detail-note-content" }); 63 + const noteIcon = noteContent.createEl("span", { cls: "semble-detail-note-icon" }); 64 + setIcon(noteIcon, "message-square"); 65 + noteContent.createEl("p", { text: note.text, cls: "semble-detail-note-text" }); 66 + 67 + const deleteBtn = noteEl.createEl("button", { cls: "semble-note-delete-btn" }); 68 + setIcon(deleteBtn, "trash-2"); 69 + deleteBtn.setAttribute("aria-label", "Delete note"); 70 + deleteBtn.addEventListener("click", () => { void this.handleDeleteNote(note.uri); }); 71 + } 72 + } 73 + 74 + // Add note form 75 + this.renderAddNoteForm(contentEl); 76 + 77 + // Footer with date 78 + if (card.createdAt) { 79 + const footer = contentEl.createEl("div", { cls: "semble-detail-footer" }); 80 + footer.createEl("span", { 81 + text: `Created ${new Date(card.createdAt).toLocaleDateString()}`, 82 + cls: "semble-detail-date", 83 + }); 84 + } 85 + } 86 + 87 + private renderNoteCard(contentEl: HTMLElement, card: Card) { 88 + const content = card.content as NoteContent; 89 + const body = contentEl.createEl("div", { cls: "semble-detail-body" }); 90 + body.createEl("p", { text: content.text, cls: "semble-detail-text" }); 91 + } 92 + 93 + private renderUrlCard(contentEl: HTMLElement, card: Card) { 94 + const content = card.content as UrlContent; 95 + const meta = content.metadata; 96 + const body = contentEl.createEl("div", { cls: "semble-detail-body" }); 97 + 98 + // Title 99 + if (meta?.title) { 100 + body.createEl("h2", { text: meta.title, cls: "semble-detail-title" }); 101 + } 102 + 103 + // Image 104 + if (meta?.imageUrl) { 105 + const img = body.createEl("img", { cls: "semble-detail-image" }); 106 + img.src = meta.imageUrl; 107 + img.alt = meta.title || "Image"; 108 + } 109 + 110 + // Full description 111 + if (meta?.description) { 112 + body.createEl("p", { text: meta.description, cls: "semble-detail-description" }); 113 + } 114 + 115 + // Metadata grid 116 + const metaGrid = body.createEl("div", { cls: "semble-detail-meta" }); 117 + 118 + if (meta?.siteName) { 119 + this.addMetaItem(metaGrid, "Site", meta.siteName); 120 + } 121 + 122 + if (meta?.author) { 123 + this.addMetaItem(metaGrid, "Author", meta.author); 124 + } 125 + 126 + if (meta?.publishedDate) { 127 + this.addMetaItem(metaGrid, "Published", new Date(meta.publishedDate).toLocaleDateString()); 128 + } 129 + 130 + if (meta?.type) { 131 + this.addMetaItem(metaGrid, "Type", meta.type); 132 + } 133 + 134 + if (meta?.doi) { 135 + this.addMetaItem(metaGrid, "DOI", meta.doi); 136 + } 137 + 138 + if (meta?.isbn) { 139 + this.addMetaItem(metaGrid, "ISBN", meta.isbn); 140 + } 141 + 142 + // URL link 143 + const linkWrapper = body.createEl("div", { cls: "semble-detail-link-wrapper" }); 144 + const link = linkWrapper.createEl("a", { 145 + text: content.url, 146 + href: content.url, 147 + cls: "semble-detail-link", 148 + }); 149 + link.setAttr("target", "_blank"); 150 + } 151 + 152 + private renderAddNoteForm(contentEl: HTMLElement) { 153 + const formSection = contentEl.createEl("div", { cls: "semble-detail-add-note" }); 154 + formSection.createEl("h3", { text: "Add a note", cls: "semble-detail-section-title" }); 155 + 156 + const form = formSection.createEl("div", { cls: "semble-add-note-form" }); 157 + 158 + this.noteInput = form.createEl("textarea", { 159 + cls: "semble-textarea semble-note-input", 160 + attr: { placeholder: "Write a note about this card..." }, 161 + }); 162 + 163 + const addBtn = form.createEl("button", { text: "Add note", cls: "semble-btn semble-btn-primary" }); 164 + addBtn.addEventListener("click", () => { void this.handleAddNote(); }); 165 + } 166 + 167 + private async handleAddNote() { 168 + if (!this.plugin.client || !this.noteInput) return; 169 + 170 + const text = this.noteInput.value.trim(); 171 + if (!text) { 172 + new Notice("Please enter a note"); 173 + return; 174 + } 175 + 176 + try { 177 + await createNoteCard( 178 + this.plugin.client, 179 + this.plugin.settings.identifier, 180 + text, 181 + { uri: this.card.uri, cid: this.card.cid } 182 + ); 183 + 184 + new Notice("Note added"); 185 + this.close(); 186 + this.onSuccess?.(); 187 + } catch (err) { 188 + const message = err instanceof Error ? err.message : String(err); 189 + new Notice(`Failed to add note: ${message}`); 190 + } 191 + } 192 + 193 + private async handleDeleteNote(noteUri: string) { 194 + if (!this.plugin.client) return; 195 + 196 + const rkey = noteUri.split("/").pop(); 197 + if (!rkey) { 198 + new Notice("Invalid note uri"); 199 + return; 200 + } 201 + 202 + try { 203 + await deleteRecord( 204 + this.plugin.client, 205 + this.plugin.settings.identifier, 206 + "network.cosmik.card", 207 + rkey 208 + ); 209 + 210 + new Notice("Note deleted"); 211 + this.close(); 212 + this.onSuccess?.(); 213 + } catch (err) { 214 + const message = err instanceof Error ? err.message : String(err); 215 + new Notice(`Failed to delete note: ${message}`); 216 + } 217 + } 218 + 219 + private addMetaItem(container: HTMLElement, label: string, value: string) { 220 + const item = container.createEl("div", { cls: "semble-detail-meta-item" }); 221 + item.createEl("span", { text: label, cls: "semble-detail-meta-label" }); 222 + item.createEl("span", { text: value, cls: "semble-detail-meta-value" }); 223 + } 224 + 225 + onClose() { 226 + this.contentEl.empty(); 227 + } 228 + }
+45
src/lib.ts
··· 35 35 }); 36 36 } 37 37 38 + 39 + export async function createNoteCard(client: Client, repo: string, text: string, originalCard?: { uri: string; cid: string }) { 40 + return await client.post("com.atproto.repo.createRecord", { 41 + input: { 42 + repo: repo as ActorIdentifier, 43 + collection: "network.cosmik.card" as Nsid, 44 + record: { 45 + $type: "network.cosmik.card", 46 + type: "NOTE", 47 + content: { 48 + $type: "network.cosmik.card#noteContent", 49 + text, 50 + }, 51 + originalCard: originalCard ? { uri: originalCard.uri, cid: originalCard.cid } : undefined, 52 + createdAt: new Date().toISOString(), 53 + }, 54 + }, 55 + }); 56 + } 57 + 58 + export async function createUrlCard(client: Client, repo: string, url: string, metadata?: { 59 + title?: string; 60 + description?: string; 61 + imageUrl?: string; 62 + siteName?: string; 63 + }) { 64 + return await client.post("com.atproto.repo.createRecord", { 65 + input: { 66 + repo: repo as ActorIdentifier, 67 + collection: "network.cosmik.card" as Nsid, 68 + record: { 69 + $type: "network.cosmik.card", 70 + type: "URL", 71 + url, 72 + content: { 73 + $type: "network.cosmik.card#urlContent", 74 + url, 75 + metadata: metadata ? { $type: "network.cosmik.card#urlMetadata", ...metadata } : undefined, 76 + }, 77 + createdAt: new Date().toISOString(), 78 + }, 79 + }, 80 + }); 81 + } 82 + 38 83 export async function getCards(client: Client, repo: string) { 39 84 return await client.get("com.atproto.repo.listRecords", { 40 85 params: {
+15 -14
src/main.ts
··· 2 2 import type { Client } from "@atcute/client"; 3 3 import { DEFAULT_SETTINGS, AtProtoSettings, SettingTab } from "./settings"; 4 4 import { createAuthenticatedClient, createPublicClient } from "./auth"; 5 - import { getCollections, getProfile } from "./lib"; 5 + import { getProfile } from "./lib"; 6 6 import { SembleCollectionsView, VIEW_TYPE_SEMBLE_COLLECTIONS } from "views/collections"; 7 7 import { SembleCardsView, VIEW_TYPE_SEMBLE_CARDS } from "views/cards"; 8 8 import { CreateCardModal } from "components/cardForm"; ··· 26 26 }); 27 27 this.addCommand({ 28 28 id: 'semble-add-card', 29 - name: 'Create Semble Card', 29 + name: 'Create semble card', 30 30 editorCheckCallback: (checking: boolean, editor: Editor, _view: MarkdownView) => { 31 31 const sel = editor.getSelection() 32 32 33 33 if (!this.settings.identifier || !this.settings.appPassword) { 34 - new Notice("Please set your Bluesky credentials in the plugin settings to create new records."); 34 + new Notice("Please set your credentials in the plugin settings to create new records."); 35 35 return false; 36 36 } 37 37 if (!checking) { ··· 45 45 46 46 this.addCommand({ 47 47 id: "view-semble-collections", 48 - name: "View Semble Collections", 49 - callback: () => this.activateView(VIEW_TYPE_SEMBLE_COLLECTIONS), 48 + name: "View semble collections", 49 + callback: () => { void this.activateView(VIEW_TYPE_SEMBLE_COLLECTIONS); }, 50 50 }); 51 51 52 52 this.addCommand({ 53 53 id: "view-semble-cards", 54 - name: "View Semble Cards", 55 - callback: () => this.activateView(VIEW_TYPE_SEMBLE_CARDS), 54 + name: "View semble cards", 55 + callback: () => { void this.activateView(VIEW_TYPE_SEMBLE_CARDS); }, 56 56 }); 57 57 58 58 this.addSettingTab(new SettingTab(this.app, this)); ··· 65 65 try { 66 66 this.client = await createAuthenticatedClient({ identifier, password: appPassword }); 67 67 await this.fetchProfile(); 68 - new Notice("Connected to Bluesky"); 69 - } catch (e) { 70 - new Notice(`Auth failed: ${e}`); 68 + new Notice("Connected"); 69 + } catch (err) { 70 + const message = err instanceof Error ? err.message : String(err); 71 + new Notice(`Auth failed: ${message}`); 71 72 this.client = createPublicClient(); 72 73 this.profile = null; 73 74 } ··· 114 115 if (leaves.length > 0) { 115 116 // A leaf with our view already exists, use that 116 117 leaf = leaves[0] as WorkspaceLeaf; 117 - workspace.revealLeaf(leaf); 118 + void workspace.revealLeaf(leaf); 118 119 return; 119 120 } 120 121 ··· 126 127 127 128 // "Reveal" the leaf in case it is in a collapsed sidebar 128 129 if (leaf) { 129 - workspace.revealLeaf(leaf); 130 + void workspace.revealLeaf(leaf); 130 131 } 131 132 } 132 133 ··· 138 139 const view = leaf.view as SembleCardsView; 139 140 view.setCollection(uri, name); 140 141 141 - workspace.revealLeaf(leaf); 142 + void workspace.revealLeaf(leaf); 142 143 } 143 144 144 145 async loadSettings() { 145 - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 146 + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<AtProtoSettings>); 146 147 } 147 148 148 149 async saveSettings() {
+7 -3
src/settings.ts
··· 24 24 containerEl.empty(); 25 25 26 26 new Setting(containerEl) 27 + // eslint-disable-next-line obsidianmd/ui/sentence-case 27 28 .setName("Handle or DID") 28 - .setDesc("Your Bluesky handle (e.g., user.bsky.social) or DID") 29 + .setDesc("Your handle or did (e.g., user.bsky.social)") 29 30 .addText((text) => 30 31 text 32 + // eslint-disable-next-line obsidianmd/ui/sentence-case 31 33 .setPlaceholder("user.bsky.social") 32 34 .setValue(this.plugin.settings.identifier) 33 35 .onChange(async (value) => { ··· 37 39 ); 38 40 39 41 new Setting(containerEl) 40 - .setName("App Password") 41 - .setDesc("Create one at Settings → Privacy and Security → App Passwords") 42 + .setName("App password") 43 + // eslint-disable-next-line obsidianmd/ui/sentence-case 44 + .setDesc("Create one at Settings → Privacy and security → App passwords") 42 45 .addText((text) => { 43 46 text.inputEl.type = "password"; 44 47 text 48 + // eslint-disable-next-line obsidianmd/ui/sentence-case 45 49 .setPlaceholder("xxxx-xxxx-xxxx-xxxx") 46 50 .setValue(this.plugin.settings.appPassword) 47 51 .onChange(async (value) => {
+69 -6
src/views/cards.ts
··· 7 7 import { VIEW_TYPE_SEMBLE_COLLECTIONS } from "./collections"; 8 8 import { renderProfileIcon } from "../components/profileIcon"; 9 9 import { EditCardModal } from "../components/editCardModal"; 10 + import { CardDetailModal } from "../components/cardDetailModal"; 10 11 11 12 export const VIEW_TYPE_SEMBLE_CARDS = "semble-cards-view"; 12 13 ··· 14 15 uri: string; 15 16 cid: string; 16 17 value: Card; 18 + } 19 + 20 + export interface AttachedNote { 21 + uri: string; 22 + text: string; 23 + } 24 + 25 + export interface CardWithNotes extends CardRecord { 26 + attachedNotes: AttachedNote[]; 17 27 } 18 28 19 29 interface CollectionLinkRecord { ··· 58 68 await this.render(); 59 69 } 60 70 61 - async getAllCards() { 71 + /** 72 + * Process cards to attach notes to their parent cards and filter out attached notes. 73 + * Notes with originalCard or parentCard references are attached to those cards 74 + * instead of being shown as separate cards. 75 + */ 76 + private processCardsWithNotes(cards: CardRecord[]): CardWithNotes[] { 77 + // Build a map of card URI -> attached notes with their URIs 78 + const notesMap = new Map<string, AttachedNote[]>(); 79 + 80 + // Find all NOTE cards that reference other cards 81 + for (const record of cards) { 82 + if (record.value.type === "NOTE") { 83 + const parentUri = record.value.originalCard?.uri || record.value.parentCard?.uri; 84 + if (parentUri) { 85 + const noteContent = record.value.content as NoteContent; 86 + const existing = notesMap.get(parentUri) || []; 87 + existing.push({ uri: record.uri, text: noteContent.text }); 88 + notesMap.set(parentUri, existing); 89 + } 90 + } 91 + } 92 + 93 + // Filter out NOTE cards that are attached to other cards 94 + const filteredCards = cards.filter((record) => { 95 + if (record.value.type === "NOTE") { 96 + const hasParent = record.value.originalCard?.uri || record.value.parentCard?.uri; 97 + return !hasParent; // Only keep standalone notes 98 + } 99 + return true; 100 + }); 101 + 102 + // Add attached notes to each card 103 + return filteredCards.map((record) => ({ 104 + ...record, 105 + attachedNotes: notesMap.get(record.uri) || [], 106 + })); 107 + } 108 + 109 + async getAllCards(): Promise<CardWithNotes[]> { 62 110 if (!this.plugin.client) return []; 63 111 64 112 const repo = this.plugin.settings.identifier; 65 113 const cardsResp = await getCards(this.plugin.client, repo); 66 114 if (!cardsResp.ok) return []; 67 - return cardsResp.data.records as unknown as CardRecord[]; 115 + const cards = cardsResp.data.records as unknown as CardRecord[]; 116 + return this.processCardsWithNotes(cards); 68 117 } 69 118 70 - async getCardsInCollection(collectionUri: string) { 119 + async getCardsInCollection(collectionUri: string): Promise<CardWithNotes[]> { 71 120 if (!this.plugin.client) return []; 72 121 73 122 const repo = this.plugin.settings.identifier; ··· 87 136 const cardUris = new Set(links.map((link) => String(link.value.card.uri))); 88 137 const cards = allCards.filter((card) => cardUris.has(String(card.uri))); 89 138 90 - return cards; 139 + return this.processCardsWithNotes(cards); 91 140 } 92 141 93 142 async render() { ··· 104 153 105 154 try { 106 155 107 - let cards: CardRecord[] = []; 156 + let cards: CardWithNotes[] = []; 108 157 try { 109 158 if (this.collectionUri) { 110 159 cards = await this.getCardsInCollection(this.collectionUri); ··· 195 244 } 196 245 } 197 246 198 - private renderCard(container: HTMLElement, record: CardRecord) { 247 + private renderCard(container: HTMLElement, record: CardWithNotes) { 199 248 const card = record.value; 200 249 const el = container.createEl("div", { cls: "semble-card" }); 201 250 251 + // Open detail modal on click 252 + el.addEventListener("click", () => { 253 + new CardDetailModal(this.plugin, record, () => { 254 + void this.render(); 255 + }).open(); 256 + }); 257 + 202 258 const header = el.createEl("div", { cls: "semble-card-header" }); 203 259 header.createEl("span", { 204 260 text: card.type, ··· 214 270 void this.render(); 215 271 }).open(); 216 272 }); 273 + 274 + // Display attached notes at the top of the card 275 + if (record.attachedNotes.length > 0) { 276 + for (const note of record.attachedNotes) { 277 + el.createEl("p", { text: note.text, cls: "semble-card-note" }); 278 + } 279 + } 217 280 218 281 if (card.type === "NOTE") { 219 282 const content = card.content as NoteContent;
+212
styles.css
··· 143 143 color: var(--text-normal); 144 144 } 145 145 146 + .semble-card-note { 147 + margin: 0; 148 + padding: 8px 12px; 149 + background: var(--background-primary); 150 + border-left: 3px solid var(--color-blue); 151 + border-radius: var(--radius-s); 152 + font-size: var(--font-small); 153 + font-style: italic; 154 + color: var(--text-muted); 155 + white-space: pre-wrap; 156 + line-height: var(--line-height-normal); 157 + } 158 + 146 159 .semble-card-url { 147 160 font-size: var(--font-small); 148 161 color: var(--text-accent); ··· 557 570 resize: vertical; 558 571 min-height: 60px; 559 572 } 573 + 574 + /* Card Detail Modal */ 575 + .semble-detail-modal { 576 + padding: 20px; 577 + max-width: 600px; 578 + } 579 + 580 + .semble-detail-header { 581 + margin-bottom: 16px; 582 + } 583 + 584 + .semble-detail-body { 585 + display: flex; 586 + flex-direction: column; 587 + gap: 16px; 588 + } 589 + 590 + .semble-detail-title { 591 + margin: 0; 592 + font-size: var(--h2-size); 593 + font-weight: var(--font-semibold); 594 + color: var(--text-normal); 595 + line-height: 1.3; 596 + } 597 + 598 + .semble-detail-image { 599 + max-width: 100%; 600 + max-height: 200px; 601 + object-fit: contain; 602 + border-radius: var(--radius-m); 603 + } 604 + 605 + .semble-detail-description { 606 + margin: 0; 607 + color: var(--text-normal); 608 + line-height: var(--line-height-normal); 609 + } 610 + 611 + .semble-detail-text { 612 + margin: 0; 613 + white-space: pre-wrap; 614 + line-height: var(--line-height-normal); 615 + color: var(--text-normal); 616 + font-size: 1.1em; 617 + } 618 + 619 + .semble-detail-meta { 620 + display: grid; 621 + grid-template-columns: repeat(2, 1fr); 622 + gap: 12px; 623 + padding: 16px; 624 + background: var(--background-secondary); 625 + border-radius: var(--radius-m); 626 + } 627 + 628 + .semble-detail-meta-item { 629 + display: flex; 630 + flex-direction: column; 631 + gap: 2px; 632 + } 633 + 634 + .semble-detail-meta-label { 635 + font-size: var(--font-smallest); 636 + color: var(--text-faint); 637 + text-transform: uppercase; 638 + letter-spacing: 0.5px; 639 + } 640 + 641 + .semble-detail-meta-value { 642 + font-size: var(--font-small); 643 + color: var(--text-normal); 644 + } 645 + 646 + .semble-detail-link-wrapper { 647 + padding-top: 8px; 648 + } 649 + 650 + .semble-detail-link { 651 + font-size: var(--font-small); 652 + color: var(--text-accent); 653 + text-decoration: none; 654 + word-break: break-all; 655 + } 656 + 657 + .semble-detail-link:hover { 658 + text-decoration: underline; 659 + } 660 + 661 + .semble-detail-notes-section { 662 + margin-top: 20px; 663 + padding-top: 20px; 664 + border-top: 1px solid var(--background-modifier-border); 665 + } 666 + 667 + .semble-detail-section-title { 668 + margin: 0 0 12px 0; 669 + font-size: var(--font-small); 670 + font-weight: var(--font-semibold); 671 + color: var(--text-muted); 672 + text-transform: uppercase; 673 + letter-spacing: 0.5px; 674 + } 675 + 676 + .semble-detail-note { 677 + display: flex; 678 + align-items: flex-start; 679 + justify-content: space-between; 680 + gap: 12px; 681 + padding: 12px 16px; 682 + background: var(--background-secondary); 683 + border-left: 3px solid var(--color-blue); 684 + border-radius: var(--radius-s); 685 + margin-bottom: 8px; 686 + } 687 + 688 + .semble-detail-note-content { 689 + display: flex; 690 + gap: 12px; 691 + flex: 1; 692 + min-width: 0; 693 + } 694 + 695 + .semble-detail-note-icon { 696 + flex-shrink: 0; 697 + color: var(--color-blue); 698 + } 699 + 700 + .semble-detail-note-icon svg { 701 + width: 16px; 702 + height: 16px; 703 + } 704 + 705 + .semble-detail-note-text { 706 + margin: 0; 707 + color: var(--text-normal); 708 + line-height: var(--line-height-normal); 709 + white-space: pre-wrap; 710 + } 711 + 712 + .semble-note-delete-btn { 713 + display: flex; 714 + align-items: center; 715 + justify-content: center; 716 + width: 28px; 717 + height: 28px; 718 + padding: 0; 719 + flex-shrink: 0; 720 + background: transparent; 721 + border: none; 722 + border-radius: var(--radius-s); 723 + cursor: pointer; 724 + color: var(--text-faint); 725 + opacity: 0.6; 726 + transition: all 0.15s ease; 727 + } 728 + 729 + .semble-note-delete-btn:hover { 730 + background: color-mix(in srgb, var(--color-red) 15%, transparent); 731 + color: var(--color-red); 732 + opacity: 1; 733 + } 734 + 735 + .semble-note-delete-btn svg { 736 + width: 14px; 737 + height: 14px; 738 + } 739 + 740 + .semble-detail-footer { 741 + margin-top: 20px; 742 + padding-top: 16px; 743 + border-top: 1px solid var(--background-modifier-border); 744 + } 745 + 746 + .semble-detail-date { 747 + font-size: var(--font-small); 748 + color: var(--text-faint); 749 + } 750 + 751 + /* Add Note Form */ 752 + .semble-detail-add-note { 753 + margin-top: 20px; 754 + padding-top: 20px; 755 + border-top: 1px solid var(--background-modifier-border); 756 + } 757 + 758 + .semble-add-note-form { 759 + display: flex; 760 + flex-direction: column; 761 + gap: 12px; 762 + } 763 + 764 + .semble-note-input { 765 + min-height: 80px; 766 + resize: vertical; 767 + } 768 + 769 + .semble-add-note-form .semble-btn { 770 + align-self: flex-end; 771 + }