AT protocol bookmarking platforms in obsidian

better types

+661 -598
+44 -4
src/components/cardDetailModal.ts
··· 1 - import { Modal, Notice } from "obsidian"; 1 + import { Modal, Notice, setIcon } from "obsidian"; 2 2 import type ATmarkPlugin from "../main"; 3 3 import { createNoteCard, deleteRecord } from "../lib"; 4 4 import type { ATmarkItem } from "../sources/types"; ··· 32 32 // Render item detail content 33 33 this.item.renderDetail(contentEl); 34 34 35 + // Render notes with delete buttons (semble-specific) 36 + if (this.item.canAddNotes() && "getAttachedNotes" in this.item) { 37 + this.renderNotesSection(contentEl); 38 + } 39 + 35 40 // Add note form (only for items that support it) 36 41 if (this.item.canAddNotes()) { 37 42 this.renderAddNoteForm(contentEl); ··· 45 50 }); 46 51 } 47 52 53 + private renderNotesSection(contentEl: HTMLElement) { 54 + // Type guard to check if item has getAttachedNotes method 55 + interface ItemWithNotes { 56 + getAttachedNotes(): Array<{ uri: string; text: string }>; 57 + } 58 + 59 + const hasNotes = (item: ATmarkItem): item is ATmarkItem & ItemWithNotes => { 60 + return "getAttachedNotes" in item && typeof (item as ItemWithNotes).getAttachedNotes === "function"; 61 + }; 62 + 63 + if (!hasNotes(this.item)) return; 64 + 65 + const notes = this.item.getAttachedNotes(); 66 + if (notes.length === 0) return; 67 + 68 + const notesSection = contentEl.createEl("div", { cls: "semble-detail-notes-section" }); 69 + notesSection.createEl("h3", { text: "Notes", cls: "atmark-detail-section-title" }); 70 + 71 + for (const note of notes) { 72 + const noteEl = notesSection.createEl("div", { cls: "semble-detail-note" }); 73 + 74 + const noteContent = noteEl.createEl("div", { cls: "semble-detail-note-content" }); 75 + const noteIcon = noteContent.createEl("span", { cls: "semble-detail-note-icon" }); 76 + setIcon(noteIcon, "message-square"); 77 + noteContent.createEl("p", { text: note.text, cls: "semble-detail-note-text" }); 78 + 79 + // Delete button 80 + const deleteBtn = noteEl.createEl("button", { cls: "semble-note-delete-btn" }); 81 + setIcon(deleteBtn, "trash-2"); 82 + deleteBtn.addEventListener("click", () => { 83 + void this.handleDeleteNote(note.uri); 84 + }); 85 + } 86 + } 87 + 48 88 private renderAddNoteForm(contentEl: HTMLElement) { 49 89 const formSection = contentEl.createEl("div", { cls: "semble-detail-add-note" }); 50 - formSection.createEl("h3", { text: "Add a note", cls: "semble-detail-section-title" }); 90 + formSection.createEl("h3", { text: "Add a note", cls: "atmark-detail-section-title" }); 51 91 52 92 const form = formSection.createEl("div", { cls: "semble-add-note-form" }); 53 93 54 94 this.noteInput = form.createEl("textarea", { 55 - cls: "semble-textarea semble-note-input", 95 + cls: "atmark-textarea semble-note-input", 56 96 attr: { placeholder: "Write a note about this item..." }, 57 97 }); 58 98 59 - const addBtn = form.createEl("button", { text: "Add note", cls: "semble-btn semble-btn-primary" }); 99 + const addBtn = form.createEl("button", { text: "Add note", cls: "atmark-btn atmark-btn-primary" }); 60 100 addBtn.addEventListener("click", () => { void this.handleAddNote(); }); 61 101 } 62 102
+7 -7
src/components/createTagModal.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 tag" }); 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 29 // Tag value field 30 - const tagGroup = form.createEl("div", { cls: "semble-form-group" }); 30 + const tagGroup = form.createEl("div", { cls: "atmark-form-group" }); 31 31 tagGroup.createEl("label", { text: "Tag", attr: { for: "tag-value" } }); 32 32 const tagInput = tagGroup.createEl("input", { 33 33 type: "text", 34 - cls: "semble-input", 34 + cls: "atmark-input", 35 35 attr: { id: "tag-value", placeholder: "Tag name", required: "true" }, 36 36 }); 37 37 38 38 // Action buttons 39 - const actions = form.createEl("div", { cls: "semble-modal-actions" }); 39 + const actions = form.createEl("div", { cls: "atmark-modal-actions" }); 40 40 41 41 const cancelBtn = actions.createEl("button", { 42 42 text: "Cancel", 43 - cls: "semble-btn semble-btn-secondary", 43 + cls: "atmark-btn atmark-btn-secondary", 44 44 type: "button", 45 45 }); 46 46 cancelBtn.addEventListener("click", () => this.close()); 47 47 48 48 const createBtn = actions.createEl("button", { 49 49 text: "Create", 50 - cls: "semble-btn semble-btn-primary", 50 + cls: "atmark-btn atmark-btn-primary", 51 51 type: "submit", 52 52 }); 53 53
+29 -25
src/components/editBookmarkModal.ts
··· 1 1 import { Modal, Notice } from "obsidian"; 2 + import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 + import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 2 4 import type ATmarkPlugin from "../main"; 3 5 import { putRecord, deleteRecord } from "../lib"; 6 + 7 + type BookmarkRecord = Record & { value: Bookmark }; 4 8 5 9 export class EditBookmarkModal extends Modal { 6 10 plugin: ATmarkPlugin; 7 - record: any; 11 + record: BookmarkRecord; 8 12 onSuccess?: () => void; 9 13 tagInputs: HTMLInputElement[] = []; 10 14 11 - constructor(plugin: ATmarkPlugin, record: any, onSuccess?: () => void) { 15 + constructor(plugin: ATmarkPlugin, record: BookmarkRecord, onSuccess?: () => void) { 12 16 super(plugin.app); 13 17 this.plugin = plugin; 14 18 this.record = record; ··· 18 22 onOpen() { 19 23 const { contentEl } = this; 20 24 contentEl.empty(); 21 - contentEl.addClass("semble-collection-modal"); 25 + contentEl.addClass("atmark-modal"); 22 26 23 27 contentEl.createEl("h2", { text: "Edit bookmark tags" }); 24 28 ··· 29 33 30 34 const existingTags = this.record.value.tags || []; 31 35 32 - const form = contentEl.createEl("div", { cls: "semble-form" }); 36 + const form = contentEl.createEl("div", { cls: "atmark-form" }); 33 37 34 38 // Tags section 35 - const tagsGroup = form.createEl("div", { cls: "semble-form-group" }); 39 + const tagsGroup = form.createEl("div", { cls: "atmark-form-group" }); 36 40 tagsGroup.createEl("label", { text: "Tags" }); 37 41 38 - const tagsContainer = tagsGroup.createEl("div", { cls: "semble-tags-container" }); 42 + const tagsContainer = tagsGroup.createEl("div", { cls: "atmark-tags-container" }); 39 43 40 44 // Render existing tags 41 45 for (const tag of existingTags) { ··· 47 51 48 52 // Add tag button 49 53 const addTagBtn = tagsGroup.createEl("button", { 50 - text: "+ Add tag", 51 - cls: "semble-btn semble-btn-secondary" 54 + text: "Add tag", 55 + cls: "atmark-btn atmark-btn-secondary" 52 56 }); 53 57 addTagBtn.addEventListener("click", (e) => { 54 58 e.preventDefault(); ··· 56 60 }); 57 61 58 62 // Action buttons 59 - const actions = contentEl.createEl("div", { cls: "semble-modal-actions" }); 63 + const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 60 64 61 65 const deleteBtn = actions.createEl("button", { 62 66 text: "Delete", 63 - cls: "semble-btn semble-btn-danger" 67 + cls: "atmark-btn atmark-btn-danger" 64 68 }); 65 69 deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 66 70 67 - actions.createEl("div", { cls: "semble-spacer" }); 71 + actions.createEl("div", { cls: "atmark-spacer" }); 68 72 69 73 const cancelBtn = actions.createEl("button", { 70 74 text: "Cancel", 71 - cls: "semble-btn semble-btn-secondary" 75 + cls: "atmark-btn atmark-btn-secondary" 72 76 }); 73 77 cancelBtn.addEventListener("click", () => { this.close(); }); 74 78 75 79 const saveBtn = actions.createEl("button", { 76 80 text: "Save", 77 - cls: "semble-btn semble-btn-primary" 81 + cls: "atmark-btn atmark-btn-primary" 78 82 }); 79 83 saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 80 84 } 81 85 82 86 private addTagInput(container: HTMLElement, value: string) { 83 - const tagRow = container.createEl("div", { cls: "semble-tag-row" }); 87 + const tagRow = container.createEl("div", { cls: "atmark-tag-row" }); 84 88 85 89 const input = tagRow.createEl("input", { 86 90 type: "text", 87 - cls: "semble-input", 91 + cls: "atmark-input", 88 92 value, 89 93 attr: { placeholder: "Enter tag..." } 90 94 }); ··· 92 96 93 97 const removeBtn = tagRow.createEl("button", { 94 98 text: "×", 95 - cls: "semble-btn semble-btn-secondary semble-tag-remove-btn" 99 + cls: "atmark-btn atmark-btn-secondary atmark-tag-remove-btn" 96 100 }); 97 101 removeBtn.addEventListener("click", (e) => { 98 102 e.preventDefault(); ··· 104 108 private confirmDelete(contentEl: HTMLElement) { 105 109 contentEl.empty(); 106 110 contentEl.createEl("h2", { text: "Delete bookmark" }); 107 - contentEl.createEl("p", { text: "Delete this bookmark?", cls: "semble-warning-text" }); 111 + contentEl.createEl("p", { text: "Delete this bookmark?", cls: "atmark-warning-text" }); 108 112 109 - const actions = contentEl.createEl("div", { cls: "semble-modal-actions" }); 113 + const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 110 114 111 115 const cancelBtn = actions.createEl("button", { 112 116 text: "Cancel", 113 - cls: "semble-btn semble-btn-secondary" 117 + cls: "atmark-btn atmark-btn-secondary" 114 118 }); 115 119 cancelBtn.addEventListener("click", () => { 116 120 void this.onOpen(); ··· 118 122 119 123 const confirmBtn = actions.createEl("button", { 120 124 text: "Delete", 121 - cls: "semble-btn semble-btn-danger" 125 + cls: "atmark-btn atmark-btn-danger" 122 126 }); 123 127 confirmBtn.addEventListener("click", () => { void this.deleteBookmark(); }); 124 128 } ··· 134 138 const rkey = this.record.uri.split("/").pop(); 135 139 if (!rkey) { 136 140 contentEl.empty(); 137 - contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "semble-error" }); 141 + contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmark-error" }); 138 142 return; 139 143 } 140 144 ··· 151 155 } catch (err) { 152 156 contentEl.empty(); 153 157 const message = err instanceof Error ? err.message : String(err); 154 - contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "semble-error" }); 158 + contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmark-error" }); 155 159 } 156 160 } 157 161 ··· 173 177 const rkey = this.record.uri.split("/").pop(); 174 178 if (!rkey) { 175 179 contentEl.empty(); 176 - contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "semble-error" }); 180 + contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmark-error" }); 177 181 return; 178 182 } 179 183 180 184 // Update the record with new tags 181 - const updatedRecord = { 185 + const updatedRecord: Bookmark = { 182 186 ...this.record.value, 183 187 tags, 184 188 }; ··· 197 201 } catch (err) { 198 202 contentEl.empty(); 199 203 const message = err instanceof Error ? err.message : String(err); 200 - contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "semble-error" }); 204 + contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmark-error" }); 201 205 } 202 206 } 203 207
+19
src/lexicons/types/community/lexicon/bookmarks/bookmark.ts
··· 1 + /** 2 + * community.lexicon.bookmarks.bookmark 3 + */ 4 + 5 + export interface Main { 6 + $type?: "community.lexicon.bookmarks.bookmark"; 7 + subject: string; 8 + title?: string; 9 + description?: string; 10 + tags?: string[]; 11 + enriched?: { 12 + title?: string; 13 + description?: string; 14 + image?: string; 15 + thumb?: string; 16 + siteName?: string; 17 + }; 18 + createdAt: string; 19 + }
+2 -2
src/lib/atproto.ts
··· 21 21 }); 22 22 } 23 23 24 - export async function putRecord(client: Client, repo: string, collection: string, rkey: string, record: any) { 24 + export async function putRecord<T = unknown>(client: Client, repo: string, collection: string, rkey: string, record: T) { 25 25 return await client.post("com.atproto.repo.putRecord", { 26 26 input: { 27 27 repo: repo as ActorIdentifier, 28 28 collection: collection as Nsid, 29 29 rkey, 30 - record, 30 + record: record as unknown as { [key: string]: unknown }, 31 31 }, 32 32 }); 33 33 }
+3 -2
src/lib/cosmik.ts
··· 38 38 }); 39 39 } 40 40 41 - export async function createSembleNote(client: Client, repo: string, text: string, originalCard?: { uri: string; cid: string }) { 41 + export async function createSembleNote(client: Client, repo: string, text: string, parentCard?: { uri: string; cid: string }) { 42 42 return await client.post("com.atproto.repo.createRecord", { 43 43 input: { 44 44 repo: repo as ActorIdentifier, ··· 50 50 $type: "network.cosmik.card#noteContent", 51 51 text, 52 52 }, 53 - originalCard: originalCard ? { uri: originalCard.uri, cid: originalCard.cid } : undefined, 53 + // Only set parentCard as per Semble documentation 54 + parentCard: parentCard ? { uri: parentCard.uri, cid: parentCard.cid } : undefined, 54 55 createdAt: new Date().toISOString(), 55 56 }, 56 57 },
+2 -2
src/main.ts
··· 20 20 }); 21 21 22 22 this.addCommand({ 23 - id: "view-atmark", 24 - name: "View ATmark", 23 + id: "view", 24 + name: "Open view", 25 25 callback: () => { void this.activateView(VIEW_TYPE_ATMARK); }, 26 26 }); 27 27
+37 -33
src/sources/bookmark.ts
··· 1 1 import type { Client } from "@atcute/client"; 2 + import type { Record } from "@atcute/atproto/types/repo/listRecords"; 2 3 import { setIcon } from "obsidian"; 3 4 import type ATmarkPlugin from "../main"; 4 5 import { getBookmarks } from "../lib"; 5 6 import type { ATmarkItem, DataSource, SourceFilter } from "./types"; 7 + import { EditBookmarkModal } from "../components/editBookmarkModal"; 8 + import { CreateTagModal } from "../components/createTagModal"; 9 + import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 10 + 11 + type BookmarkRecord = Record & { value: Bookmark }; 6 12 7 13 class BookmarkItem implements ATmarkItem { 8 - private record: any; 14 + private record: BookmarkRecord; 9 15 private plugin: ATmarkPlugin; 10 16 11 - constructor(record: any, plugin: ATmarkPlugin) { 17 + constructor(record: BookmarkRecord, plugin: ATmarkPlugin) { 12 18 this.record = record; 13 19 this.plugin = plugin; 14 20 } ··· 38 44 } 39 45 40 46 openEditModal(onSuccess?: () => void): void { 41 - const { EditBookmarkModal } = require("../components/editBookmarkModal"); 42 47 new EditBookmarkModal(this.plugin, this.record, onSuccess).open(); 43 48 } 44 49 45 50 render(container: HTMLElement): void { 46 - const el = container.createEl("div", { cls: "semble-card-content" }); 51 + const el = container.createEl("div", { cls: "atmark-item-content" }); 47 52 const bookmark = this.record.value; 48 53 const enriched = bookmark.enriched; 49 54 50 55 // Display tags 51 56 if (bookmark.tags && bookmark.tags.length > 0) { 52 - const tagsContainer = el.createEl("div", { cls: "semble-card-tags" }); 57 + const tagsContainer = el.createEl("div", { cls: "atmark-item-tags" }); 53 58 for (const tag of bookmark.tags) { 54 - tagsContainer.createEl("span", { text: tag, cls: "semble-tag" }); 59 + tagsContainer.createEl("span", { text: tag, cls: "atmark-tag" }); 55 60 } 56 61 } 57 62 58 63 const title = enriched?.title || bookmark.title; 59 64 if (title) { 60 - el.createEl("div", { text: title, cls: "semble-card-title" }); 65 + el.createEl("div", { text: title, cls: "atmark-item-title" }); 61 66 } 62 67 63 68 const imageUrl = enriched?.image || enriched?.thumb; 64 69 if (imageUrl) { 65 - const img = el.createEl("img", { cls: "semble-card-image" }); 70 + const img = el.createEl("img", { cls: "atmark-item-image" }); 66 71 img.src = imageUrl; 67 72 img.alt = title || "Image"; 68 73 } ··· 72 77 const desc = description.length > 200 73 78 ? description.slice(0, 200) + "…" 74 79 : description; 75 - el.createEl("p", { text: desc, cls: "semble-card-desc" }); 80 + el.createEl("p", { text: desc, cls: "atmark-item-desc" }); 76 81 } 77 82 78 83 if (enriched?.siteName) { 79 - el.createEl("span", { text: enriched.siteName, cls: "semble-card-site" }); 84 + el.createEl("span", { text: enriched.siteName, cls: "atmark-item-site" }); 80 85 } 81 86 82 87 const link = el.createEl("a", { 83 88 text: bookmark.subject, 84 89 href: bookmark.subject, 85 - cls: "semble-card-url", 90 + cls: "atmark-item-url", 86 91 }); 87 92 link.setAttr("target", "_blank"); 88 93 } 89 94 90 95 renderDetail(container: HTMLElement): void { 91 - const body = container.createEl("div", { cls: "semble-detail-body" }); 96 + const body = container.createEl("div", { cls: "atmark-detail-body" }); 92 97 const bookmark = this.record.value; 93 98 const enriched = bookmark.enriched; 94 99 95 100 const title = enriched?.title || bookmark.title; 96 101 if (title) { 97 - body.createEl("h2", { text: title, cls: "semble-detail-title" }); 102 + body.createEl("h2", { text: title, cls: "atmark-detail-title" }); 98 103 } 99 104 100 105 const imageUrl = enriched?.image || enriched?.thumb; 101 106 if (imageUrl) { 102 - const img = body.createEl("img", { cls: "semble-detail-image" }); 107 + const img = body.createEl("img", { cls: "atmark-detail-image" }); 103 108 img.src = imageUrl; 104 109 img.alt = title || "Image"; 105 110 } 106 111 107 112 const description = enriched?.description || bookmark.description; 108 113 if (description) { 109 - body.createEl("p", { text: description, cls: "semble-detail-description" }); 114 + body.createEl("p", { text: description, cls: "atmark-detail-description" }); 110 115 } 111 116 112 117 if (enriched?.siteName) { 113 - const metaGrid = body.createEl("div", { cls: "semble-detail-meta" }); 114 - const item = metaGrid.createEl("div", { cls: "semble-detail-meta-item" }); 115 - item.createEl("span", { text: "Site", cls: "semble-detail-meta-label" }); 116 - item.createEl("span", { text: enriched.siteName, cls: "semble-detail-meta-value" }); 118 + const metaGrid = body.createEl("div", { cls: "atmark-detail-meta" }); 119 + const item = metaGrid.createEl("div", { cls: "atmark-detail-meta-item" }); 120 + item.createEl("span", { text: "Site", cls: "atmark-detail-meta-label" }); 121 + item.createEl("span", { text: enriched.siteName, cls: "atmark-detail-meta-value" }); 117 122 } 118 123 119 - const linkWrapper = body.createEl("div", { cls: "semble-detail-link-wrapper" }); 124 + const linkWrapper = body.createEl("div", { cls: "atmark-detail-link-wrapper" }); 120 125 const link = linkWrapper.createEl("a", { 121 126 text: bookmark.subject, 122 127 href: bookmark.subject, 123 - cls: "semble-detail-link", 128 + cls: "atmark-detail-link", 124 129 }); 125 130 link.setAttr("target", "_blank"); 126 131 127 132 // Tags section 128 133 if (bookmark.tags && bookmark.tags.length > 0) { 129 - const tagsSection = container.createEl("div", { cls: "semble-detail-tags-section" }); 130 - tagsSection.createEl("h3", { text: "Tags", cls: "semble-detail-section-title" }); 131 - const tagsContainer = tagsSection.createEl("div", { cls: "semble-card-tags" }); 134 + const tagsSection = container.createEl("div", { cls: "atmark-item-tags-section" }); 135 + tagsSection.createEl("h3", { text: "Tags", cls: "atmark-detail-section-title" }); 136 + const tagsContainer = tagsSection.createEl("div", { cls: "atmark-item-tags" }); 132 137 for (const tag of bookmark.tags) { 133 - tagsContainer.createEl("span", { text: tag, cls: "semble-tag" }); 138 + tagsContainer.createEl("span", { text: tag, cls: "atmark-tag" }); 134 139 } 135 140 } 136 141 } ··· 158 163 const bookmarksResp = await getBookmarks(this.client, this.repo); 159 164 if (!bookmarksResp.ok) return []; 160 165 161 - let bookmarks = bookmarksResp.data.records; 166 + let bookmarks = bookmarksResp.data.records as BookmarkRecord[]; 162 167 163 168 // Apply tag filter if specified 164 169 const tagFilter = filters.find(f => f.type === "bookmarkTag"); 165 170 if (tagFilter && tagFilter.value) { 166 - bookmarks = bookmarks.filter((record: any) => 171 + bookmarks = bookmarks.filter((record: BookmarkRecord) => 167 172 record.value.tags?.includes(tagFilter.value) 168 173 ); 169 174 } 170 175 171 - return bookmarks.map((record: any) => new BookmarkItem(record, plugin)); 176 + return bookmarks.map((record: BookmarkRecord) => new BookmarkItem(record, plugin)); 172 177 } 173 178 174 179 async getAvailableFilters(): Promise<SourceFilter[]> { ··· 177 182 178 183 // Extract unique tags 179 184 const tagSet = new Set<string>(); 180 - const records = bookmarksResp.data.records as any[]; 185 + const records = bookmarksResp.data.records as BookmarkRecord[]; 181 186 for (const record of records) { 182 - if (record.value?.tags) { 187 + if (record.value.tags) { 183 188 for (const tag of record.value.tags) { 184 189 tagSet.add(tag); 185 190 } ··· 193 198 })); 194 199 } 195 200 196 - renderFilterUI(container: HTMLElement, activeFilters: Map<string, any>, onChange: () => void, plugin: ATmarkPlugin): void { 201 + renderFilterUI(container: HTMLElement, activeFilters: Map<string, SourceFilter>, onChange: () => void, plugin: ATmarkPlugin): void { 197 202 const section = container.createEl("div", { cls: "atmark-filter-section" }); 198 203 199 204 const titleRow = section.createEl("div", { cls: "atmark-filter-title-row" }); ··· 202 207 const createBtn = titleRow.createEl("button", { cls: "atmark-filter-create-btn" }); 203 208 setIcon(createBtn, "plus"); 204 209 createBtn.addEventListener("click", () => { 205 - const { CreateTagModal } = require("../components/createTagModal"); 206 210 new CreateTagModal(plugin, onChange).open(); 207 211 }); 208 212 ··· 222 226 void this.getAvailableFilters().then(tags => { 223 227 for (const tag of tags) { 224 228 const chip = chips.createEl("button", { 225 - text: (tag as any).label, 229 + text: tag.label, 226 230 cls: `atmark-chip ${activeFilters.get("bookmarkTag")?.value === tag.value ? "atmark-chip-active" : ""}`, 227 231 }); 228 232 chip.addEventListener("click", () => {
+46 -54
src/sources/semble.ts
··· 1 1 import type { Client } from "@atcute/client"; 2 + import type { Record } from "@atcute/atproto/types/repo/listRecords"; 2 3 import { setIcon } from "obsidian"; 3 4 import type ATmarkPlugin from "../main"; 4 5 import { getCards, getCollections, getCollectionLinks } from "../lib"; 5 - import type { NoteContent, UrlContent } from "../lexicons/types/network/cosmik/card"; 6 + import type { Main as Card, NoteContent, UrlContent } from "../lexicons/types/network/cosmik/card"; 7 + import type { Main as Collection } from "../lexicons/types/network/cosmik/collection"; 8 + import type { Main as CollectionLink } from "../lexicons/types/network/cosmik/collectionLink"; 6 9 import type { ATmarkItem, DataSource, SourceFilter } from "./types"; 10 + import { EditCardModal } from "../components/editCardModal"; 11 + import { CreateCollectionModal } from "../components/createCollectionModal"; 12 + 13 + type CardRecord = Record & { value: Card }; 14 + type CollectionRecord = Record & { value: Collection }; 15 + type CollectionLinkRecord = Record & { value: CollectionLink }; 7 16 8 17 class SembleItem implements ATmarkItem { 9 - private record: any; 18 + private record: CardRecord; 10 19 private attachedNotes: Array<{ uri: string; text: string }>; 11 20 private plugin: ATmarkPlugin; 12 21 13 - constructor(record: any, attachedNotes: Array<{ uri: string; text: string }>, plugin: ATmarkPlugin) { 22 + constructor(record: CardRecord, attachedNotes: Array<{ uri: string; text: string }>, plugin: ATmarkPlugin) { 14 23 this.record = record; 15 24 this.attachedNotes = attachedNotes; 16 25 this.plugin = plugin; ··· 25 34 } 26 35 27 36 getCreatedAt(): string { 28 - return this.record.value.createdAt; 37 + return this.record.value.createdAt || new Date().toISOString(); 29 38 } 30 39 31 40 getSource(): "semble" { ··· 41 50 } 42 51 43 52 openEditModal(onSuccess?: () => void): void { 44 - const { EditCardModal } = require("../components/editCardModal"); 45 53 new EditCardModal(this.plugin, this.record.uri, this.record.cid, onSuccess).open(); 46 54 } 47 55 48 56 render(container: HTMLElement): void { 49 - const el = container.createEl("div", { cls: "semble-card-content" }); 57 + const el = container.createEl("div", { cls: "atmark-item-content" }); 50 58 51 - // Display attached notes 59 + // Display attached notes (semble-specific) 52 60 if (this.attachedNotes.length > 0) { 53 61 for (const note of this.attachedNotes) { 54 62 el.createEl("p", { text: note.text, cls: "semble-card-note" }); ··· 65 73 const meta = content.metadata; 66 74 67 75 if (meta?.title) { 68 - el.createEl("div", { text: meta.title, cls: "semble-card-title" }); 76 + el.createEl("div", { text: meta.title, cls: "atmark-item-title" }); 69 77 } 70 78 71 79 if (meta?.imageUrl) { 72 - const img = el.createEl("img", { cls: "semble-card-image" }); 80 + const img = el.createEl("img", { cls: "atmark-item-image" }); 73 81 img.src = meta.imageUrl; 74 82 img.alt = meta.title || "Image"; 75 83 } ··· 78 86 const desc = meta.description.length > 200 79 87 ? meta.description.slice(0, 200) + "…" 80 88 : meta.description; 81 - el.createEl("p", { text: desc, cls: "semble-card-desc" }); 89 + el.createEl("p", { text: desc, cls: "atmark-item-desc" }); 82 90 } 83 91 84 92 if (meta?.siteName) { 85 - el.createEl("span", { text: meta.siteName, cls: "semble-card-site" }); 93 + el.createEl("span", { text: meta.siteName, cls: "atmark-item-site" }); 86 94 } 87 95 88 96 const link = el.createEl("a", { 89 97 text: content.url, 90 98 href: content.url, 91 - cls: "semble-card-url", 99 + cls: "atmark-item-url", 92 100 }); 93 101 link.setAttr("target", "_blank"); 94 102 } 95 103 } 96 104 97 105 renderDetail(container: HTMLElement): void { 98 - const body = container.createEl("div", { cls: "semble-detail-body" }); 106 + const body = container.createEl("div", { cls: "atmark-detail-body" }); 99 107 const card = this.record.value; 100 108 101 109 if (card.type === "NOTE") { ··· 106 114 const meta = content.metadata; 107 115 108 116 if (meta?.title) { 109 - body.createEl("h2", { text: meta.title, cls: "semble-detail-title" }); 117 + body.createEl("h2", { text: meta.title, cls: "atmark-detail-title" }); 110 118 } 111 119 112 120 if (meta?.imageUrl) { 113 - const img = body.createEl("img", { cls: "semble-detail-image" }); 121 + const img = body.createEl("img", { cls: "atmark-detail-image" }); 114 122 img.src = meta.imageUrl; 115 123 img.alt = meta.title || "Image"; 116 124 } 117 125 118 126 if (meta?.description) { 119 - body.createEl("p", { text: meta.description, cls: "semble-detail-description" }); 127 + body.createEl("p", { text: meta.description, cls: "atmark-detail-description" }); 120 128 } 121 129 122 130 if (meta?.siteName) { 123 - const metaGrid = body.createEl("div", { cls: "semble-detail-meta" }); 124 - const item = metaGrid.createEl("div", { cls: "semble-detail-meta-item" }); 125 - item.createEl("span", { text: "Site", cls: "semble-detail-meta-label" }); 126 - item.createEl("span", { text: meta.siteName, cls: "semble-detail-meta-value" }); 131 + const metaGrid = body.createEl("div", { cls: "atmark-detail-meta" }); 132 + const item = metaGrid.createEl("div", { cls: "atmark-detail-meta-item" }); 133 + item.createEl("span", { text: "Site", cls: "atmark-detail-meta-label" }); 134 + item.createEl("span", { text: meta.siteName, cls: "atmark-detail-meta-value" }); 127 135 } 128 136 129 - const linkWrapper = body.createEl("div", { cls: "semble-detail-link-wrapper" }); 137 + const linkWrapper = body.createEl("div", { cls: "atmark-detail-link-wrapper" }); 130 138 const link = linkWrapper.createEl("a", { 131 139 text: content.url, 132 140 href: content.url, 133 - cls: "semble-detail-link", 141 + cls: "atmark-detail-link", 134 142 }); 135 143 link.setAttr("target", "_blank"); 136 144 } 137 145 138 - // Attached notes section 139 - if (this.attachedNotes.length > 0) { 140 - const notesSection = container.createEl("div", { cls: "semble-detail-notes-section" }); 141 - notesSection.createEl("h3", { text: "Notes", cls: "semble-detail-section-title" }); 142 - 143 - for (const note of this.attachedNotes) { 144 - const noteEl = notesSection.createEl("div", { cls: "semble-detail-note" }); 145 - 146 - const noteContent = noteEl.createEl("div", { cls: "semble-detail-note-content" }); 147 - const noteIcon = noteContent.createEl("span", { cls: "semble-detail-note-icon" }); 148 - setIcon(noteIcon, "message-square"); 149 - noteContent.createEl("p", { text: note.text, cls: "semble-detail-note-text" }); 150 - 151 - // Note: delete functionality would need to be handled by the modal 152 - } 153 - } 154 146 } 155 147 156 148 getAttachedNotes() { ··· 176 168 const cardsResp = await getCards(this.client, this.repo); 177 169 if (!cardsResp.ok) return []; 178 170 179 - const allSembleCards = cardsResp.data.records; 171 + const allSembleCards = cardsResp.data.records as CardRecord[]; 180 172 181 173 // Build notes map 182 174 const notesMap = new Map<string, Array<{ uri: string; text: string }>>(); 183 - for (const record of allSembleCards as any[]) { 175 + for (const record of allSembleCards) { 184 176 if (record.value.type === "NOTE") { 185 - const parentUri = record.value.originalCard?.uri || record.value.parentCard?.uri; 177 + const parentUri = record.value.parentCard?.uri; 186 178 if (parentUri) { 187 179 const noteContent = record.value.content as NoteContent; 188 180 const existing = notesMap.get(parentUri) || []; ··· 193 185 } 194 186 195 187 // Filter out NOTE cards that are attached to other cards 196 - let sembleCards = allSembleCards.filter((record: any) => { 188 + let sembleCards = allSembleCards.filter((record: CardRecord) => { 197 189 if (record.value.type === "NOTE") { 198 - const hasParent = record.value.originalCard?.uri || record.value.parentCard?.uri; 190 + const hasParent = record.value.parentCard?.uri; 199 191 return !hasParent; 200 192 } 201 193 return true; ··· 206 198 if (collectionFilter && collectionFilter.value) { 207 199 const linksResp = await getCollectionLinks(this.client, this.repo); 208 200 if (linksResp.ok) { 209 - const links = linksResp.data.records.filter((link: any) => 201 + const links = linksResp.data.records as CollectionLinkRecord[]; 202 + const filteredLinks = links.filter((link: CollectionLinkRecord) => 210 203 link.value.collection.uri === collectionFilter.value 211 204 ); 212 - const cardUris = new Set(links.map((link: any) => link.value.card.uri)); 213 - sembleCards = sembleCards.filter((card: any) => cardUris.has(card.uri)); 205 + const cardUris = new Set(filteredLinks.map((link: CollectionLinkRecord) => link.value.card.uri)); 206 + sembleCards = sembleCards.filter((card: CardRecord) => cardUris.has(card.uri)); 214 207 } 215 208 } 216 209 217 210 // Create SembleItem objects 218 - return sembleCards.map((record: any) => 211 + return sembleCards.map((record: CardRecord) => 219 212 new SembleItem(record, notesMap.get(record.uri) || [], plugin) 220 213 ); 221 214 } ··· 224 217 const collectionsResp = await getCollections(this.client, this.repo); 225 218 if (!collectionsResp.ok) return []; 226 219 227 - const collections = collectionsResp.data.records; 228 - return collections.map((c: any) => ({ 220 + const collections = collectionsResp.data.records as CollectionRecord[]; 221 + return collections.map((c: CollectionRecord) => ({ 229 222 type: "sembleCollection", 230 223 value: c.uri, 231 224 label: c.value.name, 232 225 })); 233 226 } 234 227 235 - renderFilterUI(container: HTMLElement, activeFilters: Map<string, any>, onChange: () => void, plugin: ATmarkPlugin): void { 228 + renderFilterUI(container: HTMLElement, activeFilters: Map<string, SourceFilter>, onChange: () => void, plugin: ATmarkPlugin): void { 236 229 const section = container.createEl("div", { cls: "atmark-filter-section" }); 237 230 238 231 const titleRow = section.createEl("div", { cls: "atmark-filter-title-row" }); 239 - titleRow.createEl("h3", { text: "Semble Collections", cls: "atmark-filter-title" }); 232 + titleRow.createEl("h3", { text: "Semble collections", cls: "atmark-filter-title" }); 240 233 241 234 const createBtn = titleRow.createEl("button", { cls: "atmark-filter-create-btn" }); 242 235 setIcon(createBtn, "plus"); 243 236 createBtn.addEventListener("click", () => { 244 - const { CreateCollectionModal } = require("../components/createCollectionModal"); 245 237 new CreateCollectionModal(plugin, onChange).open(); 246 238 }); 247 239 ··· 262 254 void this.getAvailableFilters().then(collections => { 263 255 for (const collection of collections) { 264 256 const chip = chips.createEl("button", { 265 - text: (collection as any).label, 266 - cls: `atmark-chip ${activeFilters.get("sembleCollection") === collection.value ? "atmark-chip-active" : ""}`, 257 + text: collection.label, 258 + cls: `atmark-chip ${activeFilters.get("sembleCollection")?.value === collection.value ? "atmark-chip-active" : ""}`, 267 259 }); 268 260 chip.addEventListener("click", () => { 269 261 activeFilters.set("sembleCollection", collection);
+3 -2
src/sources/types.ts
··· 14 14 15 15 export interface SourceFilter { 16 16 type: string; 17 - value: any; 17 + value: string; 18 + label?: string; 18 19 } 19 20 20 21 export interface DataSource { 21 22 readonly name: "semble" | "bookmark"; 22 23 fetchItems(filters: SourceFilter[], plugin: ATmarkPlugin): Promise<ATmarkItem[]>; 23 24 getAvailableFilters(): Promise<SourceFilter[]>; 24 - renderFilterUI(container: HTMLElement, activeFilters: Map<string, any>, onChange: () => void, plugin: ATmarkPlugin): void; 25 + renderFilterUI(container: HTMLElement, activeFilters: Map<string, SourceFilter>, onChange: () => void, plugin: ATmarkPlugin): void; 25 26 }
+5
src/views/atmark.ts
··· 1 + /* eslint-disable @typescript-eslint/no-explicit-any */ 2 + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 + /* eslint-disable @typescript-eslint/no-unsafe-return */ 4 + /* eslint-disable @typescript-eslint/no-unsafe-call */ 1 5 import { ItemView, WorkspaceLeaf, setIcon } from "obsidian"; 2 6 import type ATmarkPlugin from "../main"; 3 7 import { renderProfileIcon } from "../components/profileIcon"; ··· 38 42 } 39 43 40 44 getDisplayText() { 45 + // eslint-disable-next-line obsidianmd/ui/sentence-case 41 46 return "ATmark"; 42 47 } 43 48
+464 -467
styles.css
··· 260 260 color: var(--text-error); 261 261 } 262 262 263 - /* Legacy Semble classes for backwards compatibility */ 264 - .semble-card-grid { 265 - display: grid; 266 - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 267 - gap: 16px; 268 - padding: 8px 0; 269 - } 270 263 271 - .semble-card { 272 - background: var(--background-secondary); 273 - border: 1px solid var(--background-modifier-border); 274 - border-radius: var(--radius-m); 275 - padding: 16px; 264 + /* Item Content (shared between sources) */ 265 + .atmark-item-content { 276 266 display: flex; 277 267 flex-direction: column; 278 268 gap: 8px; 279 - transition: box-shadow 0.15s ease, border-color 0.15s ease; 280 - cursor: pointer; 281 269 } 282 270 283 - .semble-card:hover { 284 - box-shadow: var(--shadow-s); 285 - border-color: var(--background-modifier-border-hover); 271 + .atmark-item-title { 272 + font-weight: var(--font-semibold); 273 + font-size: 1.1em; 274 + color: var(--text-normal); 286 275 } 287 276 288 - .semble-card-header { 289 - display: flex; 290 - justify-content: space-between; 291 - align-items: flex-start; 292 - gap: 8px; 277 + .atmark-item-image { 278 + width: 100%; 279 + max-height: 120px; 280 + object-fit: cover; 281 + border-radius: var(--radius-s); 282 + margin: 4px 0; 293 283 } 294 284 295 - .semble-card-title { 296 - font-weight: var(--font-semibold); 297 - font-size: 1.1em; 298 - color: var(--text-normal); 285 + .atmark-item-desc { 286 + color: var(--text-muted); 287 + font-size: var(--font-small); 288 + margin: 0; 289 + flex-grow: 1; 299 290 } 300 291 301 - .semble-badge { 292 + .atmark-item-site { 302 293 font-size: var(--font-smallest); 303 - padding: 2px 8px; 304 - border-radius: var(--radius-s); 305 - text-transform: uppercase; 306 - font-weight: var(--font-medium); 307 - flex-shrink: 0; 294 + color: var(--text-faint); 308 295 } 309 296 310 - .semble-badge-open { 311 - background: var(--color-green); 312 - color: var(--text-on-accent); 297 + .atmark-item-url { 298 + font-size: var(--font-small); 299 + color: var(--text-accent); 300 + text-decoration: none; 301 + word-break: break-all; 313 302 } 314 303 315 - .semble-badge-closed { 316 - background: var(--color-orange); 317 - color: var(--text-on-accent); 304 + .atmark-item-url:hover { 305 + text-decoration: underline; 318 306 } 319 307 320 - /* Access type icons */ 321 - .semble-access-icon { 308 + .atmark-item-tags { 322 309 display: flex; 323 - align-items: center; 324 - justify-content: center; 325 - flex-shrink: 0; 310 + flex-wrap: wrap; 311 + gap: 6px; 312 + margin-bottom: 8px; 326 313 } 327 314 328 - .semble-access-icon svg { 329 - width: 12px; 330 - height: 12px; 315 + .atmark-tag { 316 + font-size: var(--font-smallest); 317 + padding: 2px 8px; 318 + border-radius: var(--radius-s); 319 + background: var(--background-modifier-border); 320 + color: var(--text-muted); 321 + border: 1px solid var(--background-modifier-border-hover); 331 322 } 332 323 333 - .semble-access-open { 334 - color: var(--color-green); 324 + .atmark-item-tags-section { 325 + margin-top: 20px; 326 + padding-top: 20px; 327 + border-top: 1px solid var(--background-modifier-border); 335 328 } 336 329 337 - .semble-access-closed { 338 - color: var(--color-orange); 330 + /* Detail Modal (shared between sources) */ 331 + .atmark-detail-body { 332 + display: flex; 333 + flex-direction: column; 334 + gap: 16px; 335 + } 336 + 337 + .atmark-detail-title { 338 + margin: 0; 339 + font-size: var(--h2-size); 340 + font-weight: var(--font-semibold); 341 + color: var(--text-normal); 342 + line-height: 1.3; 343 + } 344 + 345 + .atmark-detail-image { 346 + max-width: 100%; 347 + max-height: 200px; 348 + object-fit: contain; 349 + border-radius: var(--radius-m); 339 350 } 340 351 341 - .semble-card-desc { 342 - color: var(--text-muted); 343 - font-size: var(--font-small); 352 + .atmark-detail-description { 344 353 margin: 0; 345 - flex-grow: 1; 354 + color: var(--text-normal); 355 + line-height: var(--line-height-normal); 356 + } 357 + 358 + .atmark-detail-meta { 359 + display: grid; 360 + grid-template-columns: repeat(2, 1fr); 361 + gap: 12px; 362 + padding: 16px; 363 + background: var(--background-secondary); 364 + border-radius: var(--radius-m); 346 365 } 347 366 348 - .semble-card-footer { 367 + .atmark-detail-meta-item { 349 368 display: flex; 350 - justify-content: space-between; 369 + flex-direction: column; 370 + gap: 2px; 371 + } 372 + 373 + .atmark-detail-meta-label { 351 374 font-size: var(--font-smallest); 352 375 color: var(--text-faint); 353 - margin-top: auto; 376 + text-transform: uppercase; 377 + letter-spacing: 0.5px; 378 + } 379 + 380 + .atmark-detail-meta-value { 381 + font-size: var(--font-small); 382 + color: var(--text-normal); 383 + } 384 + 385 + .atmark-detail-link-wrapper { 354 386 padding-top: 8px; 355 - border-top: 1px solid var(--background-modifier-border); 356 387 } 357 388 358 - .semble-error { 359 - color: var(--text-error); 389 + .atmark-detail-link { 390 + font-size: var(--font-small); 391 + color: var(--text-accent); 392 + text-decoration: none; 393 + word-break: break-all; 360 394 } 361 395 362 - /* Card type badges */ 363 - .semble-badge-note { 364 - background: var(--color-blue); 365 - color: var(--text-on-accent); 396 + .atmark-detail-link:hover { 397 + text-decoration: underline; 366 398 } 367 399 368 - .semble-badge-url { 369 - background: var(--color-purple); 370 - color: var(--text-on-accent); 400 + .atmark-detail-section-title { 401 + margin: 0 0 12px 0; 402 + font-size: var(--font-small); 403 + font-weight: var(--font-semibold); 404 + color: var(--text-muted); 405 + text-transform: uppercase; 406 + letter-spacing: 0.5px; 371 407 } 372 408 373 - .semble-badge-bookmark { 374 - background: var(--color-cyan); 375 - color: var(--text-on-accent); 409 + /* Modals and Forms (shared) */ 410 + .atmark-modal { 411 + padding: 16px; 376 412 } 377 413 378 - .semble-badge-source { 379 - font-size: var(--font-smallest); 380 - opacity: 0.8; 414 + .atmark-modal h2 { 415 + margin: 0 0 16px 0; 416 + font-size: var(--h2-size); 417 + font-weight: var(--font-semibold); 418 + color: var(--text-normal); 381 419 } 382 420 383 - .semble-badge-semble { 384 - background: var(--color-green); 385 - color: var(--text-on-accent); 421 + .atmark-form { 422 + display: flex; 423 + flex-direction: column; 424 + gap: 16px; 386 425 } 387 426 388 - /* Tags */ 389 - .semble-card-tags { 427 + .atmark-form-group { 390 428 display: flex; 391 - flex-wrap: wrap; 429 + flex-direction: column; 392 430 gap: 6px; 393 - margin-bottom: 8px; 394 431 } 395 432 396 - .semble-tag { 397 - font-size: var(--font-smallest); 398 - padding: 2px 8px; 433 + .atmark-form-group label { 434 + font-size: var(--font-small); 435 + font-weight: var(--font-medium); 436 + color: var(--text-normal); 437 + } 438 + 439 + .atmark-input, 440 + .atmark-textarea { 441 + padding: 8px 12px; 442 + background: var(--background-primary); 443 + border: 1px solid var(--background-modifier-border); 399 444 border-radius: var(--radius-s); 400 - background: var(--background-modifier-border); 401 - color: var(--text-muted); 402 - border: 1px solid var(--background-modifier-border-hover); 445 + color: var(--text-normal); 446 + font-size: var(--font-ui-medium); 447 + font-family: inherit; 448 + transition: border-color 0.15s ease; 403 449 } 404 450 405 - .semble-detail-tags-section { 406 - margin-top: 20px; 407 - padding-top: 20px; 408 - border-top: 1px solid var(--background-modifier-border); 451 + .atmark-input:focus, 452 + .atmark-textarea:focus { 453 + outline: none; 454 + border-color: var(--interactive-accent); 455 + box-shadow: 0 0 0 2px var(--background-modifier-border-focus); 409 456 } 410 457 411 - /* Page header */ 412 - .semble-page-header { 413 - margin-bottom: 16px; 414 - padding-bottom: 16px; 415 - border-bottom: 1px solid var(--background-modifier-border); 458 + .atmark-input::placeholder, 459 + .atmark-textarea::placeholder { 460 + color: var(--text-faint); 416 461 } 417 462 418 - .semble-nav-row { 463 + .atmark-textarea { 464 + resize: vertical; 465 + min-height: 60px; 466 + } 467 + 468 + .atmark-modal-actions { 419 469 display: flex; 420 470 align-items: center; 421 - gap: 12px; 422 - margin-bottom: 8px; 471 + gap: 8px; 472 + padding-top: 16px; 473 + border-top: 1px solid var(--background-modifier-border); 423 474 } 424 475 425 - .semble-brand { 476 + .atmark-spacer { 477 + flex: 1; 478 + } 479 + 480 + .atmark-btn { 481 + padding: 8px 16px; 482 + border-radius: var(--radius-s); 426 483 font-size: var(--font-small); 427 - font-weight: var(--font-semibold); 428 - color: var(--text-accent); 429 - text-transform: uppercase; 430 - letter-spacing: 0.5px; 484 + font-weight: var(--font-medium); 485 + cursor: pointer; 486 + transition: all 0.15s ease; 487 + } 488 + 489 + .atmark-btn:disabled { 490 + opacity: 0.5; 491 + cursor: not-allowed; 431 492 } 432 493 433 - .semble-page-title { 434 - margin: 0; 435 - font-size: var(--h1-size); 436 - font-weight: var(--font-bold); 494 + .atmark-btn-secondary { 495 + background: var(--background-secondary); 496 + border: 1px solid var(--background-modifier-border); 437 497 color: var(--text-normal); 438 498 } 439 499 440 - .semble-card-text { 441 - margin: 0; 442 - white-space: pre-wrap; 443 - line-height: var(--line-height-normal); 444 - color: var(--text-normal); 500 + .atmark-btn-secondary:hover:not(:disabled) { 501 + background: var(--background-modifier-hover); 502 + } 503 + 504 + .atmark-btn-primary { 505 + background: var(--interactive-accent); 506 + border: 1px solid var(--interactive-accent); 507 + color: var(--text-on-accent); 508 + } 509 + 510 + .atmark-btn-primary:hover:not(:disabled) { 511 + background: var(--interactive-accent-hover); 512 + } 513 + 514 + .atmark-btn-danger { 515 + background: color-mix(in srgb, var(--color-red) 15%, transparent); 516 + border: none; 517 + color: var(--color-red); 518 + } 519 + 520 + .atmark-btn-danger:hover:not(:disabled) { 521 + background: color-mix(in srgb, var(--color-red) 25%, transparent); 522 + } 523 + 524 + .atmark-warning-text { 525 + color: var(--text-muted); 526 + margin-bottom: 16px; 527 + } 528 + 529 + .atmark-tags-container { 530 + display: flex; 531 + flex-direction: column; 532 + gap: 8px; 533 + margin-bottom: 8px; 445 534 } 446 535 536 + .atmark-tag-row { 537 + display: flex; 538 + align-items: center; 539 + gap: 8px; 540 + } 541 + 542 + .atmark-tag-row .atmark-input { 543 + flex: 1; 544 + } 545 + 546 + .atmark-tag-remove-btn { 547 + width: 32px; 548 + height: 32px; 549 + padding: 0; 550 + font-size: 20px; 551 + line-height: 1; 552 + flex-shrink: 0; 553 + } 554 + 555 + /* Semble-specific styles (for NOTE cards and attached notes) */ 447 556 .semble-card-note { 448 557 margin: 0; 449 558 padding: 8px 12px; ··· 457 566 line-height: var(--line-height-normal); 458 567 } 459 568 460 - .semble-card-url { 461 - font-size: var(--font-small); 462 - color: var(--text-accent); 463 - text-decoration: none; 464 - word-break: break-all; 569 + .semble-card-text { 570 + margin: 0; 571 + white-space: pre-wrap; 572 + line-height: var(--line-height-normal); 573 + color: var(--text-normal); 465 574 } 466 575 467 - .semble-card-url:hover { 468 - text-decoration: underline; 576 + .semble-detail-text { 577 + margin: 0; 578 + white-space: pre-wrap; 579 + line-height: var(--line-height-normal); 580 + color: var(--text-normal); 581 + font-size: 1.1em; 469 582 } 470 583 471 - .semble-card-site { 472 - font-size: var(--font-smallest); 473 - color: var(--text-faint); 584 + .semble-detail-notes-section { 585 + margin-top: 20px; 586 + padding-top: 20px; 587 + border-top: 1px solid var(--background-modifier-border); 474 588 } 475 589 476 - .semble-card-image { 477 - width: 100%; 478 - max-height: 120px; 479 - object-fit: cover; 590 + .semble-detail-note { 591 + display: flex; 592 + align-items: flex-start; 593 + justify-content: space-between; 594 + gap: 12px; 595 + padding: 12px 16px; 596 + background: var(--background-secondary); 597 + border-left: 3px solid var(--color-blue); 480 598 border-radius: var(--radius-s); 481 - margin: 4px 0; 599 + margin-bottom: 8px; 482 600 } 483 601 484 - /* Back button */ 485 - .semble-back-btn { 602 + .semble-detail-note-content { 486 603 display: flex; 487 - align-items: center; 488 - justify-content: center; 489 - width: 32px; 490 - height: 32px; 491 - padding: 0; 492 - background: transparent; 493 - border: 1px solid var(--background-modifier-border); 494 - border-radius: var(--radius-s); 495 - cursor: pointer; 496 - color: var(--text-muted); 604 + gap: 12px; 605 + flex: 1; 606 + min-width: 0; 607 + } 608 + 609 + .semble-detail-note-icon { 610 + flex-shrink: 0; 611 + color: var(--color-blue); 612 + } 613 + 614 + .semble-detail-note-icon svg { 615 + width: 16px; 616 + height: 16px; 497 617 } 498 618 499 - .semble-back-btn:hover { 500 - background: var(--background-modifier-hover); 619 + .semble-detail-note-text { 620 + margin: 0; 501 621 color: var(--text-normal); 622 + line-height: var(--line-height-normal); 623 + white-space: pre-wrap; 502 624 } 503 625 504 - /* Source filter toggles */ 505 - .semble-source-filters { 506 - display: flex; 507 - align-items: center; 626 + /* Legacy Semble classes (backwards compatibility - will be removed in future) */ 627 + .semble-card-grid { 628 + display: grid; 629 + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 508 630 gap: 16px; 509 - padding: 12px 0; 510 - margin-bottom: 12px; 631 + padding: 8px 0; 511 632 } 512 633 513 - .semble-source-filter-label { 634 + .semble-card { 635 + background: var(--background-secondary); 636 + border: 1px solid var(--background-modifier-border); 637 + border-radius: var(--radius-m); 638 + padding: 16px; 514 639 display: flex; 515 - align-items: center; 640 + flex-direction: column; 516 641 gap: 8px; 642 + transition: box-shadow 0.15s ease, border-color 0.15s ease; 517 643 cursor: pointer; 518 - user-select: none; 519 644 } 520 645 521 - .semble-source-filter-checkbox { 522 - width: 16px; 523 - height: 16px; 524 - cursor: pointer; 525 - accent-color: var(--interactive-accent); 646 + .semble-card:hover { 647 + box-shadow: var(--shadow-s); 648 + border-color: var(--background-modifier-border-hover); 526 649 } 527 650 528 - .semble-source-filter-text { 529 - font-size: var(--font-small); 651 + .semble-card-header { 652 + display: flex; 653 + justify-content: space-between; 654 + align-items: flex-start; 655 + gap: 8px; 656 + } 657 + 658 + .semble-badge { 659 + font-size: var(--font-smallest); 660 + padding: 2px 8px; 661 + border-radius: var(--radius-s); 662 + text-transform: uppercase; 530 663 font-weight: var(--font-medium); 531 - color: var(--text-normal); 664 + flex-shrink: 0; 665 + } 666 + 667 + .semble-badge-open { 668 + background: var(--color-green); 669 + color: var(--text-on-accent); 670 + } 671 + 672 + .semble-badge-closed { 673 + background: var(--color-orange); 674 + color: var(--text-on-accent); 675 + } 676 + 677 + /* Card type badges */ 678 + .semble-badge-note { 679 + background: var(--color-blue); 680 + color: var(--text-on-accent); 681 + } 682 + 683 + .semble-badge-url { 684 + background: var(--color-purple); 685 + color: var(--text-on-accent); 686 + } 687 + 688 + .semble-badge-source { 689 + font-size: var(--font-smallest); 690 + opacity: 0.8; 532 691 } 533 692 534 - /* Filter chips */ 535 - .semble-filter-chips { 536 - display: flex; 537 - flex-wrap: wrap; 538 - gap: 8px; 693 + .semble-badge-semble { 694 + background: var(--color-green); 695 + color: var(--text-on-accent); 696 + } 697 + 698 + /* Semble-specific page components */ 699 + .semble-page-header { 539 700 margin-bottom: 16px; 701 + padding-bottom: 16px; 702 + border-top: 1px solid var(--background-modifier-border); 703 + } 704 + 705 + .semble-nav-row { 706 + display: flex; 707 + align-items: center; 708 + gap: 12px; 709 + margin-bottom: 8px; 540 710 } 541 711 542 - .semble-chip { 543 - padding: 6px 14px; 544 - border-radius: var(--radius-full); 545 - border: 1px solid var(--background-modifier-border); 546 - background: var(--background-secondary); 547 - color: var(--text-muted); 712 + .semble-brand { 548 713 font-size: var(--font-small); 549 - cursor: pointer; 550 - transition: all 0.15s ease; 714 + font-weight: var(--font-semibold); 715 + color: var(--text-accent); 716 + text-transform: uppercase; 717 + letter-spacing: 0.5px; 551 718 } 552 719 553 - .semble-chip:hover { 554 - background: var(--background-modifier-hover); 720 + .semble-page-title { 721 + margin: 0; 722 + font-size: var(--h1-size); 723 + font-weight: var(--font-bold); 555 724 color: var(--text-normal); 556 725 } 557 726 558 - .semble-chip-active { 559 - background: var(--interactive-accent); 560 - color: var(--text-on-accent); 561 - border-color: var(--interactive-accent); 727 + .semble-back-btn { 728 + display: flex; 729 + align-items: center; 730 + justify-content: center; 731 + width: 32px; 732 + height: 32px; 733 + padding: 0; 734 + background: transparent; 735 + border: 1px solid var(--background-modifier-border); 736 + border-radius: var(--radius-s); 737 + cursor: pointer; 738 + color: var(--text-muted); 562 739 } 563 740 564 - .semble-chip-active:hover { 565 - background: var(--interactive-accent-hover); 741 + .semble-back-btn:hover { 742 + background: var(--background-modifier-hover); 743 + color: var(--text-normal); 566 744 } 567 745 568 - /* Profile Icon */ 746 + /* Semble-specific Profile Icon */ 569 747 .semble-profile-icon { 570 748 display: flex; 571 749 align-items: center; ··· 638 816 line-height: 1.2; 639 817 } 640 818 641 - /* Card Menu Button */ 642 - .semble-card-menu-btn { 643 - display: flex; 644 - align-items: center; 645 - justify-content: center; 646 - width: 24px; 647 - height: 24px; 648 - padding: 0; 649 - margin-left: auto; 650 - background: transparent; 651 - border: none; 652 - border-radius: var(--radius-s); 653 - cursor: pointer; 654 - color: var(--text-faint); 655 - opacity: 0.6; 656 - transition: all 0.15s ease; 657 - } 658 - 659 - .semble-card:hover .semble-card-menu-btn { 660 - opacity: 1; 661 - } 662 - 663 - .semble-card-menu-btn:hover { 664 - background: var(--background-modifier-hover); 665 - color: var(--text-normal); 666 - opacity: 1; 667 - } 668 - 669 - .semble-card-menu-btn svg { 670 - width: 14px; 671 - height: 14px; 672 - } 673 - 674 - /* Collection Modal */ 819 + /* Semble-specific Collection UI */ 675 820 .semble-collection-modal { 676 821 padding: 16px; 677 822 } ··· 734 879 color: var(--text-muted); 735 880 } 736 881 737 - /* Modal Actions */ 738 - .semble-modal-actions { 739 - display: flex; 740 - align-items: center; 741 - gap: 8px; 742 - padding-top: 16px; 743 - border-top: 1px solid var(--background-modifier-border); 744 - } 745 - 746 - .semble-spacer { 747 - flex: 1; 748 - } 749 - 750 - .semble-btn { 751 - padding: 8px 16px; 752 - border-radius: var(--radius-s); 753 - font-size: var(--font-small); 754 - font-weight: var(--font-medium); 755 - cursor: pointer; 756 - transition: all 0.15s ease; 757 - } 758 - 759 - .semble-btn:disabled { 760 - opacity: 0.5; 761 - cursor: not-allowed; 762 - } 763 - 764 - .semble-btn-secondary { 765 - background: var(--background-secondary); 766 - border: 1px solid var(--background-modifier-border); 767 - color: var(--text-normal); 768 - } 769 - 770 - .semble-btn-secondary:hover:not(:disabled) { 771 - background: var(--background-modifier-hover); 772 - } 773 - 774 - .semble-btn-primary { 775 - background: var(--interactive-accent); 776 - border: 1px solid var(--interactive-accent); 777 - color: var(--text-on-accent); 778 - } 779 - 780 - .semble-btn-primary:hover:not(:disabled) { 781 - background: var(--interactive-accent-hover); 782 - } 783 - 784 - .semble-btn-danger { 785 - background: color-mix(in srgb, var(--color-red) 15%, transparent); 786 - border: none; 787 - color: var(--color-red); 788 - } 789 - 790 - .semble-btn-danger:hover:not(:disabled) { 791 - background: color-mix(in srgb, var(--color-red) 25%, transparent); 792 - } 793 - 794 - /* Warning text */ 795 - .semble-warning-text { 796 - color: var(--text-muted); 797 - margin-bottom: 16px; 798 - } 799 - 800 - /* Toolbar */ 882 + /* Semble-specific Toolbar */ 801 883 .semble-toolbar { 802 884 display: flex; 803 885 align-items: center; ··· 854 936 height: 14px; 855 937 } 856 938 857 - /* Form Elements */ 858 - .semble-form { 859 - display: flex; 860 - flex-direction: column; 861 - gap: 16px; 862 - } 863 - 864 - .semble-form-group { 865 - display: flex; 866 - flex-direction: column; 867 - gap: 6px; 868 - } 869 - 870 - .semble-form-group label { 871 - font-size: var(--font-small); 872 - font-weight: var(--font-medium); 873 - color: var(--text-normal); 874 - } 875 - 876 - .semble-input, 877 - .semble-textarea { 878 - padding: 8px 12px; 879 - background: var(--background-primary); 880 - border: 1px solid var(--background-modifier-border); 881 - border-radius: var(--radius-s); 882 - color: var(--text-normal); 883 - font-size: var(--font-ui-medium); 884 - font-family: inherit; 885 - transition: border-color 0.15s ease; 886 - } 887 - 888 - .semble-input:focus, 889 - .semble-textarea:focus { 890 - outline: none; 891 - border-color: var(--interactive-accent); 892 - box-shadow: 0 0 0 2px var(--background-modifier-border-focus); 893 - } 894 - 895 - .semble-input::placeholder, 896 - .semble-textarea::placeholder { 897 - color: var(--text-faint); 898 - } 899 - 900 - .semble-textarea { 901 - resize: vertical; 902 - min-height: 60px; 903 - } 904 - 905 - /* Card Detail Modal */ 939 + /* Semble-specific Card Detail Modal */ 906 940 .semble-detail-modal { 907 941 padding: 20px; 908 942 max-width: 600px; ··· 912 946 margin-bottom: 16px; 913 947 } 914 948 915 - .semble-detail-body { 916 - display: flex; 917 - flex-direction: column; 918 - gap: 16px; 949 + .semble-detail-footer { 950 + margin-top: 20px; 951 + padding-top: 16px; 952 + border-top: 1px solid var(--background-modifier-border); 919 953 } 920 954 921 - .semble-detail-title { 922 - margin: 0; 923 - font-size: var(--h2-size); 924 - font-weight: var(--font-semibold); 925 - color: var(--text-normal); 926 - line-height: 1.3; 927 - } 928 - 929 - .semble-detail-image { 930 - max-width: 100%; 931 - max-height: 200px; 932 - object-fit: contain; 933 - border-radius: var(--radius-m); 934 - } 935 - 936 - .semble-detail-description { 937 - margin: 0; 938 - color: var(--text-normal); 939 - line-height: var(--line-height-normal); 940 - } 941 - 942 - .semble-detail-text { 943 - margin: 0; 944 - white-space: pre-wrap; 945 - line-height: var(--line-height-normal); 946 - color: var(--text-normal); 947 - font-size: 1.1em; 948 - } 949 - 950 - .semble-detail-meta { 951 - display: grid; 952 - grid-template-columns: repeat(2, 1fr); 953 - gap: 12px; 954 - padding: 16px; 955 - background: var(--background-secondary); 956 - border-radius: var(--radius-m); 957 - } 958 - 959 - .semble-detail-meta-item { 960 - display: flex; 961 - flex-direction: column; 962 - gap: 2px; 963 - } 964 - 965 - .semble-detail-meta-label { 966 - font-size: var(--font-smallest); 955 + .semble-detail-date { 956 + font-size: var(--font-small); 967 957 color: var(--text-faint); 968 - text-transform: uppercase; 969 - letter-spacing: 0.5px; 970 - } 971 - 972 - .semble-detail-meta-value { 973 - font-size: var(--font-small); 974 - color: var(--text-normal); 975 - } 976 - 977 - .semble-detail-link-wrapper { 978 - padding-top: 8px; 979 - } 980 - 981 - .semble-detail-link { 982 - font-size: var(--font-small); 983 - color: var(--text-accent); 984 - text-decoration: none; 985 - word-break: break-all; 986 - } 987 - 988 - .semble-detail-link:hover { 989 - text-decoration: underline; 990 - } 991 - 992 - .semble-detail-notes-section { 993 - margin-top: 20px; 994 - padding-top: 20px; 995 - border-top: 1px solid var(--background-modifier-border); 996 958 } 997 959 998 960 .semble-detail-section-title { ··· 1004 966 letter-spacing: 0.5px; 1005 967 } 1006 968 1007 - .semble-detail-note { 1008 - display: flex; 1009 - align-items: flex-start; 1010 - justify-content: space-between; 1011 - gap: 12px; 1012 - padding: 12px 16px; 1013 - background: var(--background-secondary); 1014 - border-left: 3px solid var(--color-blue); 1015 - border-radius: var(--radius-s); 1016 - margin-bottom: 8px; 969 + /* Semble-specific Add Note Form */ 970 + .semble-detail-add-note { 971 + margin-top: 20px; 972 + padding-top: 20px; 973 + border-top: 1px solid var(--background-modifier-border); 1017 974 } 1018 975 1019 - .semble-detail-note-content { 976 + .semble-add-note-form { 1020 977 display: flex; 978 + flex-direction: column; 1021 979 gap: 12px; 1022 - flex: 1; 1023 - min-width: 0; 1024 980 } 1025 981 1026 - .semble-detail-note-icon { 1027 - flex-shrink: 0; 1028 - color: var(--color-blue); 1029 - } 1030 - 1031 - .semble-detail-note-icon svg { 1032 - width: 16px; 1033 - height: 16px; 1034 - } 1035 - 1036 - .semble-detail-note-text { 1037 - margin: 0; 1038 - color: var(--text-normal); 1039 - line-height: var(--line-height-normal); 1040 - white-space: pre-wrap; 982 + .semble-note-input { 983 + min-height: 80px; 984 + resize: vertical; 1041 985 } 1042 986 1043 987 .semble-note-delete-btn { ··· 1068 1012 height: 14px; 1069 1013 } 1070 1014 1071 - .semble-detail-footer { 1072 - margin-top: 20px; 1015 + /* Semble-specific legacy classes that need to be migrated to atmark-* */ 1016 + .semble-modal-actions { 1017 + display: flex; 1018 + align-items: center; 1019 + gap: 8px; 1073 1020 padding-top: 16px; 1074 1021 border-top: 1px solid var(--background-modifier-border); 1075 1022 } 1076 1023 1077 - .semble-detail-date { 1024 + .semble-spacer { 1025 + flex: 1; 1026 + } 1027 + 1028 + .semble-btn { 1029 + padding: 8px 16px; 1030 + border-radius: var(--radius-s); 1078 1031 font-size: var(--font-small); 1079 - color: var(--text-faint); 1032 + font-weight: var(--font-medium); 1033 + cursor: pointer; 1034 + transition: all 0.15s ease; 1080 1035 } 1081 1036 1082 - /* Add Note Form */ 1083 - .semble-detail-add-note { 1084 - margin-top: 20px; 1085 - padding-top: 20px; 1086 - border-top: 1px solid var(--background-modifier-border); 1037 + .semble-btn:disabled { 1038 + opacity: 0.5; 1039 + cursor: not-allowed; 1087 1040 } 1088 1041 1089 - .semble-add-note-form { 1090 - display: flex; 1091 - flex-direction: column; 1092 - gap: 12px; 1042 + .semble-btn-secondary { 1043 + background: var(--background-secondary); 1044 + border: 1px solid var(--background-modifier-border); 1045 + color: var(--text-normal); 1046 + } 1047 + 1048 + .semble-btn-secondary:hover:not(:disabled) { 1049 + background: var(--background-modifier-hover); 1050 + } 1051 + 1052 + .semble-btn-primary { 1053 + background: var(--interactive-accent); 1054 + border: 1px solid var(--interactive-accent); 1055 + color: var(--text-on-accent); 1056 + } 1057 + 1058 + .semble-btn-primary:hover:not(:disabled) { 1059 + background: var(--interactive-accent-hover); 1093 1060 } 1094 1061 1095 - .semble-note-input { 1096 - min-height: 80px; 1097 - resize: vertical; 1062 + .semble-btn-danger { 1063 + background: color-mix(in srgb, var(--color-red) 15%, transparent); 1064 + border: none; 1065 + color: var(--color-red); 1098 1066 } 1099 1067 1100 - .semble-add-note-form .semble-btn { 1101 - align-self: flex-end; 1068 + .semble-btn-danger:hover:not(:disabled) { 1069 + background: color-mix(in srgb, var(--color-red) 25%, transparent); 1102 1070 } 1103 1071 1104 - /* Tag editing */ 1105 - .semble-tags-container { 1072 + .semble-warning-text { 1073 + color: var(--text-muted); 1074 + margin-bottom: 16px; 1075 + } 1076 + 1077 + .semble-form { 1106 1078 display: flex; 1107 1079 flex-direction: column; 1108 - gap: 8px; 1109 - margin-bottom: 8px; 1080 + gap: 16px; 1110 1081 } 1111 1082 1112 - .semble-tag-row { 1083 + .semble-form-group { 1113 1084 display: flex; 1114 - align-items: center; 1115 - gap: 8px; 1085 + flex-direction: column; 1086 + gap: 6px; 1116 1087 } 1117 1088 1118 - .semble-tag-row .semble-input { 1119 - flex: 1; 1089 + .semble-form-group label { 1090 + font-size: var(--font-small); 1091 + font-weight: var(--font-medium); 1092 + color: var(--text-normal); 1120 1093 } 1121 1094 1122 - .semble-tag-remove-btn { 1123 - width: 32px; 1124 - height: 32px; 1125 - padding: 0; 1126 - font-size: 20px; 1127 - line-height: 1; 1128 - flex-shrink: 0; 1095 + .semble-input, 1096 + .semble-textarea { 1097 + padding: 8px 12px; 1098 + background: var(--background-primary); 1099 + border: 1px solid var(--background-modifier-border); 1100 + border-radius: var(--radius-s); 1101 + color: var(--text-normal); 1102 + font-size: var(--font-ui-medium); 1103 + font-family: inherit; 1104 + transition: border-color 0.15s ease; 1105 + } 1106 + 1107 + .semble-input:focus, 1108 + .semble-textarea:focus { 1109 + outline: none; 1110 + border-color: var(--interactive-accent); 1111 + box-shadow: 0 0 0 2px var(--background-modifier-border-focus); 1112 + } 1113 + 1114 + .semble-input::placeholder, 1115 + .semble-textarea::placeholder { 1116 + color: var(--text-faint); 1117 + } 1118 + 1119 + .semble-textarea { 1120 + resize: vertical; 1121 + min-height: 60px; 1122 + } 1123 + 1124 + .semble-error { 1125 + color: var(--text-error); 1129 1126 }