Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384import { createElement, Folder, Calendar, Search, FilePlus, Settings, Puzzle, Type, Shapes, PanelLeft, PanelBottom, Link, Link2, ChevronRight, ChevronDown, ChevronLeft, ChevronUp, Plus, Trash2, X, Check, AlertTriangle, Info, Eye, EyeOff, Copy, ExternalLink, RotateCcw, MapPin, Map, CircleDot, SquareTerminal, Clock, List, Tag, FileText, Hand, IndentIncrease, IndentDecrease, Table, ArrowUpFromLine, ArrowDownFromLine, ArrowLeftFromLine, ArrowRightFromLine, AlignCenter, Rows3, Columns3, Cloud, Pencil, Keyboard, ArrowDownAZ, ArrowUpAZ, ArrowDown01, ArrowUp01, History, AtSign, User, Highlighter, Bold, Italic, Strikethrough, Code, Heading, ListOrdered, CheckSquare, Quote, Brackets, Asterisk, Camera } from 'lucide'import { TableProperties, MessageSquare, HardDrive, Bug } from 'lucide'import type { IconNode } from 'lucide'import type { IconName } from '../obsidian-stubs'
/** * Registry mapping lowercase icon names to Lucide icon nodes. * * Add icons here as plugins require them. This enables tree-shaking * so only used icons are bundled. * * Icon names should match what plugins pass to addRibbonIcon(). * * Currently used icons: * - folder: FileTreePlugin * - calendar: DailyNotesPlugin * - search: QuickSearchPlugin * - file-plus: CreateFilePlugin, file tree header dropdown * - settings: Ribbon gear icon for settings modal * - puzzle: Settings modal nav (Plugins section) * - pencil: Settings modal nav (Editor section) * - keyboard: Settings modal nav (Hotkeys section) * - type: Settings modal nav (Formatting Toolbar section) * - panel-left: Settings modal nav (Ribbon section) * - panel-bottom: Settings modal nav (Status Bar section) * - circle-dot: Generic icon for status bar items in settings panel * - square-terminal: Command palette ribbon icon (core system) * - obsidian: Settings modal nav (Obsidian Plugins section) - custom SVG * - links-coming-in: BacklinksView (tab icon in side panel) * - links-going-out: OutgoingLinksView (right panel) * - list: TocView (right panel) * - tag: TagsView (right panel) * - file-text: FrontmatterView (right panel) */const ICON_REGISTRY: Record<string, IconNode> = { folder: Folder, calendar: Calendar, search: Search, 'file-plus': FilePlus, settings: Settings, puzzle: Puzzle, cloud: Cloud, type: Type, shapes: Shapes, // Settings modal nav: Editor section ("pencil") and Hotkeys section ("keyboard"). pencil: Pencil, keyboard: Keyboard, user: User, 'hard-drive': HardDrive, 'message-square': MessageSquare, bug: Bug, // Settings modal nav: Atmosphere Account section ("at-sign"). 'at-sign': AtSign, 'panel-left': PanelLeft, 'panel-bottom': PanelBottom, 'circle-dot': CircleDot, 'square-terminal': SquareTerminal, link: Link, 'link-2': Link2, // Common icons used by Obsidian plugins (settings UI, menus, etc.) 'chevron-right': ChevronRight, 'chevron-down': ChevronDown, 'chevron-left': ChevronLeft, 'chevron-up': ChevronUp, plus: Plus, trash: Trash2, 'trash-2': Trash2, cross: X, x: X, 'lucide-x': X, checkmark: Check, check: Check, 'alert-triangle': AlertTriangle, info: Info, eye: Eye, 'eye-off': EyeOff, copy: Copy, 'external-link': ExternalLink, 'reset': RotateCcw, // Map icons (used by Map Viewer plugin) 'map-pin': MapPin, 'map': Map, // Recent Files plugin (sidebar view icon). // Also used by SortMenuButton for the "Modified (recent)" sort mode menu item. clock: Clock, // Sort menu icons consumed by SortMenuButton (sidebar sort dropdown). // The lucide-react SortMenuButton component imports these by name for // the chrome-row button glyph; setIcon() in the menu items uses the // string ids below for the dropdown rows. Both paths must resolve or // the menu item appears as a label with no glyph (the bug found in // dogfooding right after the sortable-sidebars merge). 'arrow-down-a-z': ArrowDownAZ, 'arrow-up-a-z': ArrowUpAZ, 'arrow-down-0-1': ArrowDown01, 'arrow-up-0-1': ArrowUp01, history: History, // Document info panel icons (right panel views) list: List, tag: Tag, 'file-text': FileText, // Gesture settings nav icon hand: Hand, // Indent/outdent (formatting toolbar) bold: Bold, italic: Italic, strikethrough: Strikethrough, code: Code, heading: Heading, 'list-ordered': ListOrdered, 'check-square': CheckSquare, quote: Quote, 'square-brackets': Brackets, asterisk: Asterisk, camera: Camera, 'indent-increase': IndentIncrease, 'indent-decrease': IndentDecrease, // Table formatting actions (formatting toolbar). Used by the 8 table actions // wired up in builtin-formatting-actions.ts. The icon names are referenced // by ID strings; if any of these were missing, getIcon() would return null // and the toolbar would render an empty <button> with no glyph (silent UX // bug — caught only by visual inspection in the dev-server checkpoint). table: Table, 'table-properties': TableProperties, 'arrow-up-from-line': ArrowUpFromLine, 'arrow-down-from-line': ArrowDownFromLine, 'arrow-left-from-line': ArrowLeftFromLine, 'arrow-right-from-line': ArrowRightFromLine, 'align-center': AlignCenter, // Row/column orientation icons. No longer used by Delete row/column // (those switched to composite `trash-row` / `trash-column` glyphs in // CUSTOM_ICONS to convey both action and target). Kept registered as // generic primitives that future actions may reference. 'rows-3': Rows3, 'columns-3': Columns3, highlighter: Highlighter,}
/** * Custom SVG icons not available in Lucide. * These are stored as raw SVG strings and parsed on demand. * * Composite icons follow the lucide convention: 24×24 viewBox, no fill * on strokes, currentColor stroke, stroke-width: 2, round line caps * and joins. Ride alongside lucide icons in toolbars at the same size. */const CUSTOM_ICONS: Record<string, string> = { // Paired sidebar glyphs: the same chain link with a directional arrow below it. 'links-coming-in': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide"><g transform="translate(3 0) scale(0.75)"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></g><path d="M14 19H2m3-3-3 3 3 3"/></svg>`, 'links-going-out': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide"><g transform="translate(3 0) scale(0.75)"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></g><path d="M10 19h12m-3-3 3 3-3 3"/></svg>`, // Obsidian brand mark (Simple Icons), inheriting the surrounding UI color. obsidian: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="none" class="lucide"><path d="M19.355 18.538a68.967 68.959 0 0 0 1.858-2.954.81.81 0 0 0-.062-.9c-.516-.685-1.504-2.075-2.042-3.362-.553-1.321-.636-3.375-.64-4.377a1.707 1.707 0 0 0-.358-1.05l-3.198-4.064a3.744 3.744 0 0 1-.076.543c-.106.503-.307 1.004-.536 1.5-.134.29-.29.6-.446.914l-.31.626c-.516 1.068-.997 2.227-1.132 3.59-.124 1.26.046 2.73.815 4.481.128.011.257.025.386.044a6.363 6.363 0 0 1 3.326 1.505c.916.79 1.744 1.922 2.415 3.5zM8.199 22.569c.073.012.146.02.22.02.78.024 2.095.092 3.16.29.87.16 2.593.64 4.01 1.055 1.083.316 2.198-.548 2.355-1.664.114-.814.33-1.735.725-2.58l-.01.005c-.67-1.87-1.522-3.078-2.416-3.849a5.295 5.295 0 0 0-2.778-1.257c-1.54-.216-2.952.19-3.84.45.532 2.218.368 4.829-1.425 7.531zM5.533 9.938c-.023.1-.056.197-.098.29L2.82 16.059a1.602 1.602 0 0 0 .313 1.772l4.116 4.24c2.103-3.101 1.796-6.02.836-8.3-.728-1.73-1.832-3.081-2.55-3.831zM9.32 14.01c.615-.183 1.606-.465 2.745-.534-.683-1.725-.848-3.233-.716-4.577.154-1.552.7-2.847 1.235-3.95.113-.235.223-.454.328-.664.149-.297.288-.577.419-.86.217-.47.379-.885.46-1.27.08-.38.08-.72-.014-1.043-.095-.325-.297-.675-.68-1.06a1.6 1.6 0 0 0-1.475.36l-4.95 4.452a1.602 1.602 0 0 0-.513.952l-.427 2.83c.672.59 2.328 2.316 3.335 4.711.09.21.175.43.253.653z"/></svg>`,
// Delete-row composite: Trash2 in the upper-left, "rows badge" in // the lower-right. The trash icon is scaled DOWN slightly (from full // 24×24 to ~16×16 anchored top-left) so the larger badge has room // to sit prominently without overlapping the trash silhouette. The // badge is filled with `--bg-secondary` (the toolbar background) // so it reads as a distinct glyph overlaid on the trash if there // is any incidental overlap. // // SVG layout (24x24 viewBox): // Trash2: scaled to ~67% (transform="scale(0.67)") so the // full-size paths now occupy x≈0-16, y≈0-16. // Badge group: translate(11 11), 13×13 area = corner [11..24]. // The badge fills ~54% of the icon width — large enough that // the row/column lines inside read clearly at the toolbar's // 16px display size. The slight scale-down of the trash icon // prevents the badge from visually competing with the trash // handle/lid. // - rect: filled background masks any underlying trash strokes // - 3 short horizontal lines: row glyph 'trash-row': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide"><g transform="scale(0.67)" stroke-width="3"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 1 1 1 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></g><g stroke-width="1.5" transform="translate(11 11)"><rect x="0" y="0" width="13" height="13" rx="1.5" fill="var(--bg-secondary, #2d2d2d)"/><line x1="2.5" y1="3.5" x2="10.5" y2="3.5"/><line x1="2.5" y1="6.5" x2="10.5" y2="6.5"/><line x1="2.5" y1="9.5" x2="10.5" y2="9.5"/></g></svg>`,
// Delete-column composite: Trash2 + badge with three VERTICAL // lines (rotation of the row badge). Same construction, sizing, // and masking rationale as `trash-row`. 'trash-column': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide"><g transform="scale(0.67)" stroke-width="3"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 1 1 1 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></g><g stroke-width="1.5" transform="translate(11 11)"><rect x="0" y="0" width="13" height="13" rx="1.5" fill="var(--bg-secondary, #2d2d2d)"/><line x1="3.5" y1="2.5" x2="3.5" y2="10.5"/><line x1="6.5" y1="2.5" x2="6.5" y2="10.5"/><line x1="9.5" y1="2.5" x2="9.5" y2="10.5"/></g></svg>`,}
/** * Plugin-registered icons via addIcon(). * Plugins call addIcon(iconId, svgContent) to register custom SVG icons * that can then be used with setIcon(), getIcon(), addRibbonIcon(), etc. * Stored as raw SVG strings, parsed on demand (same as CUSTOM_ICONS). */const PLUGIN_ICONS: Record<string, string> = {}
/** * Parse an SVG string and return the root SVGElement. * DOMParser's 'image/svg+xml' mode returns an SVGSVGElement as documentElement, * but TypeScript types it as Element. This helper provides the correct cast. * Returns null if parsing produced a <parsererror>. */function parseSvgString(svgString: string): SVGElement | null { const parser = new DOMParser() const doc = parser.parseFromString(svgString, 'image/svg+xml') if (doc.querySelector('parsererror')) return null return doc.documentElement as unknown as SVGElement}
/** * Default attributes for all icons. * * Note: We add 'lucide' class for consistency with lucide conventions * and to enable CSS targeting of icons. * * We don't set width/height here - lucide's viewBox is 24x24, and setting * different dimensions causes scaling/blurriness. Instead, CSS controls * the display size via .lucide selector in Ribbon.css. */const DEFAULT_ATTRS = { class: 'lucide',}
/** * Check whether an icon name is registered (Lucide or custom). * Pure lookup — does not create an SVG element. * * @param iconName - Icon name (lowercase, e.g., 'folder') * @returns true if the icon is registered */export function hasIcon(iconName: string): boolean { return iconName in ICON_REGISTRY || iconName in CUSTOM_ICONS || iconName in PLUGIN_ICONS}
/** * Internal icon creation with extended options. * Used by setIconExtended() and other internal APIs that need custom attributes. * * @param iconName - Icon name (lowercase, e.g., 'folder') * @param attrs - Optional custom attributes (e.g., { 'stroke-width': 1.5 }) * @returns SVG element or null if icon not found */function getIconInternal( iconName: string, attrs: Record<string, string | number> = {}): SVGSVGElement | null { // Check Lucide registry first const iconNode = ICON_REGISTRY[iconName] if (iconNode) { return createElement(iconNode, { ...DEFAULT_ATTRS, ...attrs }) as SVGSVGElement }
// Check custom icons (built-in CUSTOM_ICONS are full <svg> strings) const customSvg = CUSTOM_ICONS[iconName] if (customSvg) { const svg = parseSvgString(customSvg) if (!svg) return null for (const [key, value] of Object.entries({ ...DEFAULT_ATTRS, ...attrs })) { svg.setAttribute(key, String(value)) } return svg as SVGSVGElement }
// Check plugin-registered icons (via addIcon()). // Obsidian convention: addIcon() receives SVG *inner content* (paths, groups, etc.) // NOT a complete <svg> element. We wrap it in an SVG with viewBox="0 0 100 100" // which matches Obsidian's expected coordinate system for custom icons. const pluginSvg = PLUGIN_ICONS[iconName] if (pluginSvg) { const trimmed = pluginSvg.trim() const svgString = trimmed.toLowerCase().startsWith('<svg') ? trimmed : `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="currentColor" class="lucide">${trimmed}</svg>` const svg = parseSvgString(svgString) if (!svg) { console.warn(`getIcon: failed to parse plugin SVG for '${iconName}'`) return null } for (const [key, value] of Object.entries({ ...DEFAULT_ATTRS, ...attrs })) { svg.setAttribute(key, String(value)) } return svg as SVGSVGElement }
return null}
/** * Create an SVG from an iconId. Returns null if no icon associated with the iconId. * Obsidian-compatible API. * * @param iconId - the icon ID * @returns SVG element or null if icon not found */export function getIcon(iconId: string): SVGSVGElement | null { return getIconInternal(iconId)}
/** * Create an SVG from an iconId with custom attributes. * This is a Reams-internal API extension that supports attributes. * For Obsidian-compatible code, use getIcon() instead. * * @param iconName - Icon name (lowercase, e.g., 'folder') * @param attrs - Custom attributes (e.g., { width: 20, height: 20 }) * @returns SVG element or null if icon not found */export function getIconWithAttrs( iconName: string, attrs: Record<string, string | number>): SVGSVGElement | null { return getIconInternal(iconName, attrs)}
/** * Register a custom icon by name. * Obsidian-compatible API: plugins call addIcon(iconId, svgContent) to register * custom SVG icons that can then be used with setIcon(), getIcon(), etc. * * @param iconId - Unique icon identifier (e.g., 'calendar-day') * @param svgContent - Raw SVG markup string */export function addIcon(iconId: string, svgContent: string): void { PLUGIN_ICONS[iconId] = svgContent}
/** * Remove a previously registered custom icon. * * @param iconId - Icon identifier to remove */export function removeIcon(iconId: string): void { delete PLUGIN_ICONS[iconId]}
/** * Get the list of registered icon IDs. Obsidian-compatible API. */export function getIconIds(): IconName[] { // Deduplicate in case plugin icons shadow built-ins return [...new Set([ ...Object.keys(ICON_REGISTRY), ...Object.keys(CUSTOM_ICONS), ...Object.keys(PLUGIN_ICONS), ])]}
/** * Render an icon into an HTML element with extended options. * * This is Reams' internal extended version that supports: * - Optional custom attributes or a numeric size * - Returns boolean indicating whether the icon was found * * For Obsidian-compatible code, use setIcon() which matches Obsidian's signature. * * @param el - Target element to render icon into * @param iconName - Icon name (lowercase, e.g., 'folder') * @param attrsOrSize - Optional custom attributes or numeric size * @returns true if icon was found and rendered, false otherwise */export function setIconExtended( el: HTMLElement, iconName: string, attrsOrSize?: Record<string, string | number> | number): boolean { const attrs: Record<string, string | number> = typeof attrsOrSize === 'number' ? { width: attrsOrSize, height: attrsOrSize } : (attrsOrSize ?? {}) const svg = getIconInternal(iconName, attrs) if (!svg) { return false }
el.innerHTML = '' el.appendChild(svg) return true}
/** * Render an icon into an HTML element. * * If the icon is found, clears existing content and appends the icon SVG. * Does nothing if no icon is associated with the iconId. * This matches Obsidian's setIcon() API exactly. * * @param parent - The HTML element to insert the icon * @param iconId - The icon ID */export function setIcon(parent: HTMLElement, iconId: string): void { setIconExtended(parent, iconId)}