Style System
The application style system is a shared Obsidian-aligned theme layered over shadcn-svelte components and first-party plugin CSS. The goal is a note-app interface that feels close to Obsidian’s restrained dark product chrome while preserving the existing Svelte component structure, light-mode support, and plugin ownership boundaries.
Ownership
@lapis-notes/ui/theme.css is the canonical palette and Tailwind token bridge entrypoint. It imports grouped partials under src/lib/styles/theme/ for the light palette, dark overrides, semantic aliases, shared foundations, document/content roles, shared surfaces, and Tailwind bridge tokens. Together they define the shared shadcn/Tailwind variables (--background, --card, --popover, --primary, --border, --ring, etc.), the Obsidian-compatible aliases that runtime and plugin code consume (--background-primary, --background-primary-alt, --background-secondary, --background-modifier-border, --text-normal, --text-muted, --text-faint, --interactive-accent, --text-on-accent, etc.), and the shared compatibility helpers that still bridge older Obsidian-style CSS (--color-base-*, --color-accent-*, --mono-rgb-*, shared selection and shadow helpers, the blur/raised surface chain, and related form-field hover helpers).
The workspace workspace-shell.css file owns app-specific Obsidian shell variables and selectors for tabs, stacked tabs, ribbons, status bar, and layout behavior. It should consume shared theme tokens instead of redefining the global palette.
CodeMirror 6 autocomplete tooltip surfaces (.cm-tooltip-autocomplete, completion info) are owned by @lapis-notes/ui/codemirror-autocomplete.css (exported beside theme.css). CodeMirror lint chrome is workspace-owned in packages/workspace/src/lib/styles/codemirror-lint.css because the workspace app is the runtime that loads editor shell CSS; it covers lint ranges, gutter markers, the Lapis-owned hover tooltip shell, the lint panel, and the inline View Problem widget while consuming shared UI theme tokens.
First-party plugins own plugin-prefixed selectors and variables in their package app.css files. Plugin variables should default to shared core variables and only introduce plugin-specific roles for real semantic states such as task status, due dates, trace status, graph categories, warnings, errors, or success states.
Effective Tokens
Dark mode is the primary target:
| Role | Token examples | Effective value |
|---|---|---|
| Canvas | --background, --background-primary | #1e1e1e |
| Surface | --card, --background-primary-alt | #242424 |
| Raised/secondary surface | --secondary, --muted, --sidebar, --background-secondary | #262626 |
| Hairline | --border, --background-modifier-border, --sidebar-border | #363636 |
| Strong/input border | --input, --sidebar-accent, --background-modifier-border-hover | #3f3f3f |
| Neutral hover | --background-modifier-hover, sidebar rows use --sidebar-accent | translucent |
| Neutral selected | --background-modifier-active-hover | accent tint |
| Primary text | --foreground, --text-normal | #dadada |
| Muted text | --muted-foreground, --text-muted | #b3b3b3 |
| Accent text/focus | --text-accent, --ring | #a78bfa |
| Filled interactive | --interactive-accent, --primary bridge | configured accent |
| Pressed interactive | --interactive-accent-pressed | darker accent |
Light mode remains supported as a restrained structural mirror. Dark mode should be treated as the visual source of truth when choosing new token mappings.
Theme compatibility must continue to work for both class families, dark/light and theme-dark/theme-light, whether they are applied on html or body. Shared theme partials own that selector compatibility; plugin CSS should not work around it with duplicate palette blocks.
Existing font imports remain in place. The current implementation does not adopt a no-web-font rule.
Component Language
Shared UI components should use semantic Tailwind tokens and compact Obsidian-like chrome:
- Prefer hairline borders and surface changes over shadows.
- Neutral controls use
--background-modifier-hoverfor hover state and--background-modifier-active-hoverfor selected or toggled state. Sidebar row-style controls use--sidebar-accentfor hover and active row surfaces. - Prefer 4-8px radii for cards, inputs, menus, dialogs, table containers, and repeated rows.
- Reserve pill geometry for controls that are naturally pill-shaped, such as switches, chips, badges, and compact status markers.
- Use violet sparingly for links, focus, selected state, and filled primary controls.
- Runtime components and plugin CSS should consume semantic roles such as
--interactive-accent,--interactive-accent-hover,--text-accent, and--text-on-accent; raw--accent-interactivetokens are theme implementation details except where the Tailwind bridge exposes utility aliases. - Use semantic state colors only for status meaning: destructive/error, warning/due, success/completed, graph/category, and telemetry trace state.
Workspace chrome follows the same language: tabs, sidebars, status bar, command palette, settings, startup panels, editor tooltips, and toasts should inherit shared variables and avoid introducing standalone palettes.
Sidebar chrome is placement-aware. A leaf or tab group moved into a left or right sidedock should inherit sidebar surface and boundary roles from workspace-shell.css even if the mounted view uses the main --background-primary canvas internally. Dock outer borders, internal pane separators, tab/header borders, grouped-panel borders, and collapsed-sidebar rails should remain visible against both sidebar and editor backgrounds.
Accent Usage
Obsidian treats the accent color as an attention cue for interactive elements, not as a general decoration color. In this codebase:
- Use
--interactive-accentfor filled primary actions, selected checkboxes or switches, active resize handles, and other controls whose active state needs a strong user-action signal. - Pair any filled
--interactive-accentbackground with--text-on-accent. Do not assume--primary-foregroundor white remains readable after user theme changes. - Use
--interactive-accent-hoverand--interactive-accent-pressedonly for hover/pressed states of controls that already use--interactive-accent. - Use
--text-accentfor inline links, linked file names, focused status affordances, and small text/icon cues that should read as navigable or actionable without becoming filled controls. - Use
--ringor--background-modifier-border-focusfor focus outlines. Do not replace keyboard focus indicators with a background-only accent. - Use
--background-modifier-hoverfor neutral hover rows, toolbar buttons, menus, and list items. Hover alone is not enough reason to use violet. - Use
--background-modifier-active-hoverfor selected or toggled neutral controls when a filled accent would overstate the state. - Use
--sidebar-accentfor sidebar row hover/active surfaces unless the row is a primary action or selected control that needs the stronger accent treatment. - Use low-opacity accent mixes, such as
color-mix()or/10utility forms, only for drop targets, search/tag highlights, table selection guides, and other temporary interaction hints. - Avoid raw
--accent-interactivein component and plugin CSS. It is a theme implementation token; use the semantic aliases above.
Plugin Inheritance
Plugins should not depend on workspace-global selectors for plugin chrome. They should expose plugin-prefixed selectors and variables while inheriting core theme roles:
- Surface roles default to
--background-primary,--background-primary-alt,--card, or sidebar tokens for sidebar-hosted views. - Border roles default to
--background-modifier-border,--border, or--input. - Text roles default to
--text-normal,--text-muted, and--text-faint. - Accent roles default to
--interactive-accent,--text-accent, and--text-on-accent. - Error, warning, and success roles default to
--text-error,--text-warning, and--text-successwhen present.
Standalone plugin builds continue to include @lapis-notes/ui/theme.css through the plugin CSS contract so plugin-local testing and native-style installs see the same effective token system.
Plugins should consume shared compatibility tokens for common Obsidian-derived roles such as callout palette values, table selection, raised surfaces, and form-field surfaces. When a plugin needs its own semantic colors, it should derive local aliases from the shared state tokens instead of re-declaring theme-light and theme-dark palette ramps.
Change Rules
Style changes that alter the global palette, semantic variable meanings, component shape language, plugin inheritance rules, or dark/light behavior must update this page. Package-local styling behavior should also update the owning package page or package-local spec.md when it changes durable behavior.