UI Package (@lapis-notes/ui)
The UI package is the shared design-system layer. It wraps headless component libraries (Bits UI, paneforge, svelte-sonner) in Tailwind-styled Svelte components that the workspace shell and API-level components share.
Source: packages/ui/src/lib/
Design Principles
- Presentation only — no app state, workspace behavior, plugin lifecycle, storage, or metadata.
- Thin wrappers over headless primitives with consistent Tailwind styling.
- Variant-based APIs using
tailwind-variantsandclass-variance-authority. - Composable: most complex components are exported as namespaced sub-components (e.g.,
Dialog.Root,Dialog.Content). - Plugin-specific chrome, selector namespaces, and stylesheet ownership stay in the owning plugin package. Shared UI components should remain generic building blocks instead of accumulating plugin-specific override rules.
- Theme-first styling: shared components consume
theme.csssemantic variables and prefer hairline borders, 4-8px radii, visible neutral hover states through--background-modifier-hover, selected neutral states through--background-modifier-active-hover, and minimal shadows so workspace and plugin chrome inherit the same Obsidian-aligned surface system.
Theme Tokens
theme.css is the canonical shared palette and Tailwind token bridge entrypoint. Internally it imports grouped partials from src/lib/styles/theme/ for the light palette, dark overrides, semantic aliases, shared foundations, document/content roles, shared surfaces, and Tailwind bridge tokens. Dark mode is the primary target and follows the attached Obsidian default ladder: #1e1e1e background, #242424 card/alternate surface, #262626 secondary/sidebar surfaces, #363636 hairline/sidebar borders, #3f3f3f strong/input borders and sidebar hover surfaces, #dadada text, and #b3b3b3 muted text. Accent roles resolve through --color-accent*; components use semantic --interactive-accent, --text-accent, and --text-on-accent instead of raw --accent-interactive.
Light mode remains supported as a restrained structural mirror. Existing font imports and the mono font stack remain unchanged.
The shared theme continues to expose shadcn/Tailwind variables (--background, --card, --primary, --border, etc.), Obsidian-compatible aliases (--background-primary, --background-modifier-border, --text-normal, --text-muted, --interactive-accent, --text-on-accent), and compatibility helpers (--color-base-*, --color-accent-*, --mono-rgb-*, theme-level selection and shadow tokens, the blur/raised surface chain, and form-field hover helpers) so workspace chrome, first-party plugins, and community-style plugin CSS inherit a single token source.
Theme ownership also includes class-family compatibility: the shared token layer must resolve correctly for both dark/light and theme-dark/theme-light classes when the runtime applies them on either html or body.
The cross-package Style System page is the durable contract for effective tokens, shape language, plugin inheritance, and update rules.
Utilities
cn(...inputs)— Class merging viaclsx+tailwind-merge.fuzzySearch(items, query, { keys })— Shared Fuse.js-backed fuzzy ranking helper for batch filtering whenCommand.RootusesshouldFilter={false}.fuzzyMatchScore(value, query, keywords?)/createFuzzyMatchScore()— Shared per-item Fuse scorer for menus, tags, metadata autocomplete, and other ranked filter lists.commandFuzzyFilter/createCommandFuzzyFilter()— Aliases of the per-item scorer for bits-uiCommand.Rootfilterprops.WithoutChild<T>,WithoutChildren<T>,WithoutChildrenOrChild<T>— Type utilities for removing slot props.WithElementRef<T, U>— Adds bindablerefproperty.
Hooks
IsMobile
IsMobile extends MediaQuery. Constructor takes breakpoint (default 768px). Provides reactive mobile detection.
Component Catalog
Layout Components
| Component | Backing Library | Sub-components | Key Props |
|---|---|---|---|
| Accordion | bits-ui | Root, Item, Trigger, Content | value, onValueChange |
| Collapsible | bits-ui | Root, Trigger, Content | open (bindable) |
| Resizable | paneforge | PaneGroup, Pane, Handle | direction; Handle keeps a constant hit area and thickens its visible separator line from 2px to 4px in var(--interactive-accent) on hover/focus-visible |
| Sidebar | custom | 25+ sub-components | open, side, variant, collapsible; Rail uses the same hover/focus accent-line treatment |
| Sheet | bits-ui Dialog | Root, Trigger, Overlay, Content, Header, Title, Footer, Close | side (“top”/“bottom”/“left”/“right”) |
| Card | custom | Root, Header, Title, Description, Content, Footer, Action | — |
| Item | custom | Root, Content, Title, Description, Actions | variant (default/outline/muted), size (default/sm) |
Form Controls
| Component | Backing Library | Sub-components | Key Props |
|---|---|---|---|
| Button | custom | Root only | variant (default/destructive/outline/secondary/ghost/link), size (default/sm/xs/lg/icon); neutral variants hover through --background-modifier-hover |
| Input | custom | Root only | ref, value (bindable), type |
| Textarea | custom | Root only | ref, value (bindable), field-sizing support |
| Checkbox | bits-ui | Root only | checked (bindable), indeterminate (bindable) |
| Switch | bits-ui | Root only | checked (bindable) |
| Slider | bits-ui | Root only | value (bindable array), orientation; thumb labels render as tooltip-like callouts with bottom arrows on hover/focus |
| Select | bits-ui | Root, Trigger, Content, Item, Group, GroupHeading, Label, Separator, ScrollUp, ScrollDown | size on Trigger |
| Toggle | bits-ui | Root only | pressed (bindable), variant, size; hover uses --background-modifier-hover, pressed uses --background-modifier-active-hover |
| Toggle Group | bits-ui | Root, Item | — |
| Label | bits-ui | Root only | Standard form label |
| Search | custom | Root only | ref, value (bindable), integrated clear button |
| Color Picker | custom | Root only | — |
Overlay Components
| Component | Backing Library | Sub-components | Notes |
|---|---|---|---|
| Dialog | bits-ui | Root, Trigger, Portal, Overlay, Content, Header, Title, Description, Footer, Close | showCloseButton prop on Content |
| Dropdown Menu | bits-ui | 15 sub-components including Sub, CheckboxItem, RadioItem, RadioGroup | — |
| Context Menu | bits-ui | 15 sub-components (mirrors Dropdown Menu) | — |
| Popover | bits-ui | Root, Trigger, Content, Close | — |
| Hover Card | bits-ui LinkPreview | Root, Trigger, Content | — |
| Tooltip | bits-ui | Root, Provider, Trigger, Content, Portal | — |
| Modal | custom + Dialog | open (bindable), title, content | Supports DocumentFragment content |
| Date Time Picker Dialog | custom + Dialog/Input | Root only | Modal date/time entry with apply, cancel, and clear actions |
| Command | bits-ui | Root, Dialog, Input, List, Empty, Group, Item, Separator, Shortcut, LinkItem + Loading, GroupHeading | Command palette building blocks |
| Autocomplete | custom | Root, List, Item, Empty, Footer | Presentational list surfaces (data-slot) aligned with Command/Popover; CodeMirror completion DOM uses @lapis-notes/ui/codemirror-autocomplete.css |
| CodeMirror lint CSS | workspace-owned | — | Lint range, marker, panel, custom tooltip shell, and inline-problem chrome live in the workspace app stylesheet while consuming UI theme tokens |
Trigger-backed overlay wrappers now carry a shared popup-document fallback for their portal target. When callers do not pass explicit portalProps, the shared Dropdown Menu, Context Menu, Hover Card, Tooltip, Popover, and Select wrappers portal into the active trigger element’s ownerDocument.body rather than the ambient root document. That keeps popup-hosted overlays attached to the window that raised them without requiring every popup-aware call site to wire its own portal target.
Data Display
| Component | Notes |
|---|---|
| Table | Root, Header, Body, Footer, Row, Cell, Head, Caption |
| Table DnD | Shared row/column reorder primitives for plugin table previews (@lapis-notes/ui/table-dnd, @lapis-notes/ui/table-dnd/sensors, @lapis-notes/ui/table-dnd/utils): tableReorderSensors, TableDragGrip, dropIndicatorClasses, parseTableDragData, resolveTableDragTargetIndex |
| Data Table | TanStack Table integration via createSvelteTable(options). Helper: FlexRender, renderComponent, renderSnippet, mergeObjects |
| Badge | variant (default/secondary/destructive/outline), renders as <a> or <span> |
| Tag | Shared Obsidian-style tag/chip renderer exported from @lapis-notes/ui/tag; used by Markdown previews, Bases, and editable metadata property chips |
| Progress | value, max (default 100), animated CSS transform |
| Skeleton | Pulsing placeholder animation |
| Separator | Simple visual divider |
Navigation
| Component | Notes |
|---|---|
| Breadcrumb | Root, List, Item, Link, Page, Separator, Ellipsis |
| Scroll Area | bits-ui. orientation (“vertical”/“horizontal”/“both”), customizable scrollbar classes |
Feedback
| Component | Notes |
|---|---|
| Sonner (Toaster) | Wraps svelte-sonner. Auto-syncs theme with mode-watcher. Custom CSS variables for theming. |
| Alert | Root, Title, Description. variant (“default”/“destructive”) |
Validation And Exports
The package builds with svelte-package, copies theme.css, styles.css, and codemirror-autocomplete.css into dist, and exposes component subpaths through package exports. Consumers import components directly or through the package index.
During local validation, the package’s Svelte/TypeScript config maps @lapis-notes/ui self-imports and component subpaths back to src/lib so svelte-check runs in a clean checkout without relying on generated dist/ declarations.
Sidebar System
The Sidebar is the most complex component group, designed for the workspace shell’s dock panels:
State Management
SidebarStateclass —open,openMobile,state(“expanded”/“collapsed”),isMobile.useSidebar()— Context hook for accessing sidebar state.toggle(),setOpen(),setOpenMobile(),handleShortcutKeydown().- Keyboard shortcut:
Cmd+B/Ctrl+B.
Constants
SIDEBAR_WIDTH="16rem",SIDEBAR_WIDTH_MOBILE="18rem",SIDEBAR_WIDTH_ICON="3rem".SIDEBAR_COOKIE_NAME="sidebar:state",SIDEBAR_COOKIE_MAX_AGE= 7 days.
Components
- Provider — Sets up state, CSS variables for widths.
- Root — Conditionally renders as Sheet on mobile.
- Content, Header, Footer, Inset — Layout sections.
- Menu, MenuItem, MenuButton — Menu items with variants (
size,variant), tooltip support, icon collapse. - MenuSub, MenuSubButton, MenuSubItem — Nested menus.
- MenuAction, MenuBadge, MenuSkeleton — Auxiliary menu elements.
- Group, GroupLabel, GroupContent, GroupAction — Section grouping.
- Input, Rail, Trigger, Separator — Utility components.
Railshares the same hover/focus-visible accent-line treatment asResizable.Handle: the visible separator line stays centered, grows from2pxto4px, and switches tovar(--interactive-accent)without changing the drag hit target.- NestedProvider — For nested sidebar contexts (e.g., right sidebar inside left sidebar).
Dependencies
- bits-ui — Headless Svelte components.
- paneforge — Resizable pane library.
- svelte-sonner — Toast notifications.
- @tanstack/table-core — Data table engine.
- mode-watcher — Dark/light theme detection.
- tailwind-variants / class-variance-authority — Variant styling.
- lucide-svelte — Icons (SearchIcon, X, ChevronDown, Check, Minus used internally).
- @dnd-kit/dom, @dnd-kit/svelte — Table row/column reorder sensors and grip helpers (
table-dnd); presentation-only, no vault or document model logic.