@nucel/ui 0.2.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +8 -32
  2. package/src/lib/components/BottomSheet.svelte +96 -0
  3. package/src/lib/components/Breadcrumbs.svelte +57 -0
  4. package/src/lib/components/Checkbox.svelte +64 -0
  5. package/src/lib/components/CodeBlock.svelte +264 -0
  6. package/src/lib/components/CodeEditor.svelte +175 -0
  7. package/src/lib/components/ColorInput.svelte +41 -0
  8. package/src/lib/components/ColorInput.test.ts +126 -0
  9. package/src/lib/components/Combobox.svelte +103 -0
  10. package/src/lib/components/CommandPalette.svelte +135 -0
  11. package/src/lib/components/CopyButton.svelte +95 -0
  12. package/src/lib/components/CopyButton.test.ts +213 -0
  13. package/src/lib/components/DataTable.svelte +202 -0
  14. package/src/lib/components/DateRangePicker.svelte +185 -0
  15. package/src/lib/components/DiffEditor.svelte +174 -0
  16. package/src/lib/components/Drawer.svelte +69 -0
  17. package/src/lib/components/Fab.svelte +59 -0
  18. package/src/lib/components/Form.svelte +38 -0
  19. package/src/lib/components/FormField.svelte +51 -0
  20. package/src/lib/components/IconButton.svelte +86 -0
  21. package/src/lib/components/IconButton.test.ts +139 -0
  22. package/src/lib/components/InlineCode.svelte +28 -0
  23. package/src/lib/components/Pagination.svelte +65 -0
  24. package/src/lib/components/Radio.svelte +60 -0
  25. package/src/lib/components/RadioGroup.svelte +26 -0
  26. package/src/lib/components/SearchInput.svelte +77 -0
  27. package/src/lib/components/Skeleton.svelte +76 -0
  28. package/src/lib/components/StatCard.svelte +97 -0
  29. package/src/lib/components/ThemeProvider.svelte +157 -0
  30. package/src/lib/components/ThemeToggle.svelte +68 -0
  31. package/src/lib/components/ThreeWayMerge.svelte +185 -0
  32. package/src/lib/components/ui/MarkdownRenderer.svelte +126 -8
  33. package/src/lib/components/ui/Sparkline.svelte +1 -1
  34. package/src/lib/components/ui/StatusBadge.svelte +6 -3
  35. package/src/lib/components/ui/StatusDot.svelte +3 -3
  36. package/src/lib/index.ts +120 -45
  37. package/src/lib/utils/detectLanguage.ts +187 -0
  38. package/src/lib/utils/monaco-workers.d.ts +32 -0
  39. package/src/lib/utils/monacoLoader.ts +167 -0
  40. package/src/lib/utils/shikiHighlighter.ts +78 -0
  41. package/src/styles.css +100 -32
  42. package/src/lib/components/ui/Alert.svelte +0 -47
  43. package/src/lib/components/ui/Alert.test.ts +0 -206
  44. package/src/lib/components/ui/AppCard.svelte +0 -76
  45. package/src/lib/components/ui/AppShell.svelte +0 -14
  46. package/src/lib/components/ui/AppSidebar.svelte +0 -45
  47. package/src/lib/components/ui/BranchPill.svelte +0 -19
  48. package/src/lib/components/ui/BranchPill.test.ts +0 -121
  49. package/src/lib/components/ui/CommentPill.svelte +0 -12
  50. package/src/lib/components/ui/CostDisplay.svelte +0 -26
  51. package/src/lib/components/ui/CostDisplay.test.ts +0 -1115
  52. package/src/lib/components/ui/FormField.svelte +0 -34
  53. package/src/lib/components/ui/FormField.test.ts +0 -41
  54. package/src/lib/components/ui/ListCard.svelte +0 -9
  55. package/src/lib/components/ui/NavItem.svelte +0 -42
  56. package/src/lib/components/ui/NavSection.svelte +0 -17
  57. package/src/lib/components/ui/PageHeader.svelte +0 -25
  58. package/src/lib/components/ui/PageHeader.test.ts +0 -72
  59. package/src/lib/components/ui/PermissionChips.svelte +0 -49
  60. package/src/lib/components/ui/ProgressRing.test.ts +0 -239
  61. package/src/lib/components/ui/Section.svelte +0 -21
  62. package/src/lib/components/ui/Section.test.ts +0 -44
  63. package/src/lib/components/ui/SectionTitle.svelte +0 -16
  64. package/src/lib/components/ui/StatCard.svelte +0 -19
  65. package/src/lib/components/ui/StatusBadge.test.ts +0 -150
  66. package/src/lib/components/ui/StatusPill.svelte +0 -54
  67. package/src/lib/components/ui/StatusPill.test.ts +0 -125
  68. package/src/lib/components/ui/editor/RichEditor.svelte +0 -580
  69. package/src/lib/components/ui/editor/mention-suggestion.ts +0 -144
  70. package/src/lib/components/ui/table/Table.svelte +0 -12
  71. package/src/lib/components/ui/table/Table.test.ts +0 -317
  72. package/src/lib/components/ui/table/TableBody.svelte +0 -10
  73. package/src/lib/components/ui/table/TableCaption.svelte +0 -10
  74. package/src/lib/components/ui/table/TableCell.svelte +0 -10
  75. package/src/lib/components/ui/table/TableHead.svelte +0 -10
  76. package/src/lib/components/ui/table/TableHeader.svelte +0 -10
  77. package/src/lib/components/ui/table/TableRow.svelte +0 -10
  78. package/src/lib/components/ui/table/index.ts +0 -7
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Map a file path / extension to a Shiki bundled language id.
3
+ *
4
+ * Returns `'plaintext'` for anything unknown — never throws, never returns
5
+ * undefined. Safe to feed straight into `<CodeBlock language={...}>`.
6
+ */
7
+
8
+ const EXT_TO_LANG: Record<string, string> = {
9
+ // JS / TS family
10
+ js: 'javascript',
11
+ mjs: 'javascript',
12
+ cjs: 'javascript',
13
+ jsx: 'jsx',
14
+ ts: 'typescript',
15
+ mts: 'typescript',
16
+ cts: 'typescript',
17
+ tsx: 'tsx',
18
+
19
+ // Web / markup
20
+ html: 'html',
21
+ htm: 'html',
22
+ svelte: 'svelte',
23
+ vue: 'vue',
24
+ astro: 'astro',
25
+ xml: 'xml',
26
+ svg: 'xml',
27
+
28
+ // Styles
29
+ css: 'css',
30
+ scss: 'scss',
31
+ sass: 'sass',
32
+ less: 'less',
33
+ styl: 'stylus',
34
+
35
+ // Data / config
36
+ json: 'json',
37
+ jsonc: 'jsonc',
38
+ json5: 'json5',
39
+ yaml: 'yaml',
40
+ yml: 'yaml',
41
+ toml: 'toml',
42
+ ini: 'ini',
43
+ env: 'shellscript',
44
+
45
+ // Markdown / docs
46
+ md: 'markdown',
47
+ mdx: 'mdx',
48
+ markdown: 'markdown',
49
+ rst: 'plaintext',
50
+ tex: 'latex',
51
+
52
+ // Systems languages
53
+ rs: 'rust',
54
+ go: 'go',
55
+ c: 'c',
56
+ h: 'c',
57
+ cpp: 'cpp',
58
+ cxx: 'cpp',
59
+ cc: 'cpp',
60
+ hpp: 'cpp',
61
+ cs: 'csharp',
62
+ java: 'java',
63
+ kt: 'kotlin',
64
+ kts: 'kotlin',
65
+ swift: 'swift',
66
+ scala: 'scala',
67
+ zig: 'zig',
68
+ d: 'd',
69
+ nim: 'nim',
70
+ v: 'v',
71
+
72
+ // Scripting
73
+ py: 'python',
74
+ pyw: 'python',
75
+ rb: 'ruby',
76
+ php: 'php',
77
+ pl: 'perl',
78
+ pm: 'perl',
79
+ lua: 'lua',
80
+ r: 'r',
81
+ jl: 'julia',
82
+ groovy: 'groovy',
83
+ clj: 'clojure',
84
+ cljs: 'clojure',
85
+ cljc: 'clojure',
86
+ ex: 'elixir',
87
+ exs: 'elixir',
88
+ erl: 'erlang',
89
+ hrl: 'erlang',
90
+ hs: 'haskell',
91
+ ml: 'ocaml',
92
+ mli: 'ocaml',
93
+ fs: 'fsharp',
94
+ fsx: 'fsharp',
95
+
96
+ // Shell / ops
97
+ sh: 'shellscript',
98
+ bash: 'shellscript',
99
+ zsh: 'shellscript',
100
+ fish: 'fish',
101
+ ps1: 'powershell',
102
+ bat: 'batch',
103
+ cmd: 'batch',
104
+ nu: 'nushell',
105
+ dockerfile: 'dockerfile',
106
+ dockerignore: 'plaintext',
107
+ gitignore: 'plaintext',
108
+ gitattributes: 'plaintext',
109
+ makefile: 'makefile',
110
+ mk: 'makefile',
111
+
112
+ // Queries / DB
113
+ sql: 'sql',
114
+ graphql: 'graphql',
115
+ gql: 'graphql',
116
+ prisma: 'prisma',
117
+
118
+ // IaC
119
+ tf: 'terraform',
120
+ tfvars: 'terraform',
121
+ hcl: 'hcl',
122
+ nix: 'nix',
123
+
124
+ // Diff / patch / proto
125
+ diff: 'diff',
126
+ patch: 'diff',
127
+ proto: 'proto',
128
+ };
129
+
130
+ const FILENAME_TO_LANG: Record<string, string> = {
131
+ dockerfile: 'dockerfile',
132
+ 'docker-compose.yml': 'yaml',
133
+ 'docker-compose.yaml': 'yaml',
134
+ makefile: 'makefile',
135
+ gnumakefile: 'makefile',
136
+ cmakelists: 'cmake',
137
+ 'cmakelists.txt': 'cmake',
138
+ rakefile: 'ruby',
139
+ gemfile: 'ruby',
140
+ 'gemfile.lock': 'plaintext',
141
+ procfile: 'plaintext',
142
+ jenkinsfile: 'groovy',
143
+ 'cargo.toml': 'toml',
144
+ 'cargo.lock': 'toml',
145
+ 'package.json': 'json',
146
+ 'tsconfig.json': 'jsonc',
147
+ '.gitignore': 'plaintext',
148
+ '.dockerignore': 'plaintext',
149
+ '.env': 'shellscript',
150
+ '.env.local': 'shellscript',
151
+ '.editorconfig': 'ini',
152
+ };
153
+
154
+ /**
155
+ * Detect Shiki language id from a file path.
156
+ * Falls back to `'plaintext'` for unknown extensions.
157
+ *
158
+ * @example
159
+ * detectLanguageFromPath('src/main.rs') // 'rust'
160
+ * detectLanguageFromPath('Dockerfile') // 'dockerfile'
161
+ * detectLanguageFromPath('a/b/.eslintrc.json') // 'json'
162
+ * detectLanguageFromPath('LICENSE') // 'plaintext'
163
+ */
164
+ export function detectLanguageFromPath(path: string): string {
165
+ if (!path) return 'plaintext';
166
+
167
+ const segments = path.split('/');
168
+ const file = (segments[segments.length - 1] ?? '').toLowerCase();
169
+
170
+ // Exact filename match (Dockerfile, Makefile, Cargo.toml, ...)
171
+ if (FILENAME_TO_LANG[file]) return FILENAME_TO_LANG[file];
172
+
173
+ // Extension lookup — handle `.eslintrc.json` style (last `.<ext>` wins)
174
+ const dot = file.lastIndexOf('.');
175
+ if (dot > -1 && dot < file.length - 1) {
176
+ const ext = file.slice(dot + 1);
177
+ if (EXT_TO_LANG[ext]) return EXT_TO_LANG[ext];
178
+ }
179
+
180
+ // Bare dotfiles like `.bashrc`, `.zshrc`
181
+ if (file.startsWith('.')) {
182
+ const stem = file.slice(1);
183
+ if (stem.endsWith('rc') || stem.endsWith('profile')) return 'shellscript';
184
+ }
185
+
186
+ return 'plaintext';
187
+ }
@@ -0,0 +1,32 @@
1
+ // Type declarations for Monaco worker imports.
2
+ //
3
+ // These are Vite-specific `?worker` query imports that resolve to a
4
+ // constructor for a Web Worker. TypeScript otherwise can't find them
5
+ // because they don't physically exist as ESM modules on disk — Vite
6
+ // rewrites them at build time. The same pattern is used for Vite's
7
+ // `vite/client` types but we only need the Monaco ones here.
8
+
9
+ declare module 'monaco-editor/esm/vs/editor/editor.worker?worker' {
10
+ const WorkerCtor: { new (): Worker };
11
+ export default WorkerCtor;
12
+ }
13
+
14
+ declare module 'monaco-editor/esm/vs/language/json/json.worker?worker' {
15
+ const WorkerCtor: { new (): Worker };
16
+ export default WorkerCtor;
17
+ }
18
+
19
+ declare module 'monaco-editor/esm/vs/language/css/css.worker?worker' {
20
+ const WorkerCtor: { new (): Worker };
21
+ export default WorkerCtor;
22
+ }
23
+
24
+ declare module 'monaco-editor/esm/vs/language/html/html.worker?worker' {
25
+ const WorkerCtor: { new (): Worker };
26
+ export default WorkerCtor;
27
+ }
28
+
29
+ declare module 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' {
30
+ const WorkerCtor: { new (): Worker };
31
+ export default WorkerCtor;
32
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Monaco Editor lazy loader.
3
+ *
4
+ * Why a dedicated loader:
5
+ * - `monaco-editor` is ~3-5MB un-gzipped. Importing it eagerly would bloat
6
+ * the initial bundle of every consumer of `@nucel/ui`. We always load it
7
+ * lazily via dynamic `import()`.
8
+ * - Monaco's web workers must be told where to load their JS from via the
9
+ * global `MonacoEnvironment.getWorker` hook. We register this once per
10
+ * page, on the first call.
11
+ * - The whole thing is browser-only (no `window` → bail out) so it stays
12
+ * SSR-safe.
13
+ *
14
+ * Worker bundling: each worker is imported with Vite's `?worker` query.
15
+ * Vite (and Storybook with the Vite builder) emits a separate chunk for
16
+ * each one, so they're code-split and downloaded on demand.
17
+ *
18
+ * Consumers should NOT import `monaco-editor` directly — always go through
19
+ * `loadMonaco()`.
20
+ */
21
+
22
+ import type * as MonacoNs from 'monaco-editor';
23
+
24
+ let monacoPromise: Promise<typeof MonacoNs> | null = null;
25
+ let themesRegistered = false;
26
+
27
+ declare global {
28
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
29
+ interface Window {
30
+ MonacoEnvironment?: {
31
+ getWorker?: (workerId: string, label: string) => Worker | Promise<Worker>;
32
+ getWorkerUrl?: (workerId: string, label: string) => string;
33
+ };
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Lazily load Monaco. Safe to call from any environment; resolves to `null`
39
+ * during SSR. Subsequent calls return the same in-flight promise.
40
+ */
41
+ export function loadMonaco(): Promise<typeof MonacoNs> | null {
42
+ if (typeof window === 'undefined') return null;
43
+ if (monacoPromise) return monacoPromise;
44
+
45
+ monacoPromise = (async () => {
46
+ // Register the worker factory before any editor is created.
47
+ // Each worker is imported as a Vite `?worker` for proper code-splitting.
48
+ // `getWorker` may return a promise, which Monaco awaits internally.
49
+ if (!window.MonacoEnvironment) {
50
+ window.MonacoEnvironment = {
51
+ getWorker: async (_workerId: string, label: string): Promise<Worker> => {
52
+ // Dynamic `?worker` imports return `{ default: WorkerCtor }`.
53
+ // Vite emits a separate chunk per worker.
54
+ switch (label) {
55
+ case 'json': {
56
+ const { default: WorkerCtor } = await import(
57
+ 'monaco-editor/esm/vs/language/json/json.worker?worker'
58
+ );
59
+ return new WorkerCtor();
60
+ }
61
+ case 'css':
62
+ case 'scss':
63
+ case 'less': {
64
+ const { default: WorkerCtor } = await import(
65
+ 'monaco-editor/esm/vs/language/css/css.worker?worker'
66
+ );
67
+ return new WorkerCtor();
68
+ }
69
+ case 'html':
70
+ case 'handlebars':
71
+ case 'razor': {
72
+ const { default: WorkerCtor } = await import(
73
+ 'monaco-editor/esm/vs/language/html/html.worker?worker'
74
+ );
75
+ return new WorkerCtor();
76
+ }
77
+ case 'typescript':
78
+ case 'javascript': {
79
+ const { default: WorkerCtor } = await import(
80
+ 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
81
+ );
82
+ return new WorkerCtor();
83
+ }
84
+ default: {
85
+ const { default: WorkerCtor } = await import(
86
+ 'monaco-editor/esm/vs/editor/editor.worker?worker'
87
+ );
88
+ return new WorkerCtor();
89
+ }
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ const monaco = await import('monaco-editor');
96
+ registerNucelThemes(monaco);
97
+ return monaco;
98
+ })();
99
+
100
+ return monacoPromise;
101
+ }
102
+
103
+ /**
104
+ * Register two custom Monaco themes loosely matched to the nucel design
105
+ * tokens. We pin bg/fg/border/cursor/selection to nucel values and let
106
+ * Monaco's built-in `vs` / `vs-dark` cover the rest of the token palette.
107
+ *
108
+ * Monaco wants hex colors (#rrggbb / #rrggbbaa) which means we can't pipe
109
+ * our oklch tokens through directly. The values below are hand-tuned to
110
+ * match the surface/foreground colors used elsewhere in @nucel/ui.
111
+ */
112
+ function registerNucelThemes(monaco: typeof MonacoNs): void {
113
+ if (themesRegistered) return;
114
+ themesRegistered = true;
115
+
116
+ monaco.editor.defineTheme('nucel-light', {
117
+ base: 'vs',
118
+ inherit: true,
119
+ rules: [],
120
+ colors: {
121
+ 'editor.background': '#ffffff',
122
+ 'editor.foreground': '#0f172a',
123
+ 'editorLineNumber.foreground': '#94a3b8',
124
+ 'editorLineNumber.activeForeground': '#0f172a',
125
+ 'editor.selectionBackground': '#dbeafe',
126
+ 'editor.inactiveSelectionBackground': '#e2e8f0',
127
+ 'editorCursor.foreground': '#0f172a',
128
+ 'editor.lineHighlightBackground': '#f8fafc',
129
+ 'editorGutter.background': '#ffffff',
130
+ 'editorWidget.background': '#ffffff',
131
+ 'editorWidget.border': '#e2e8f0',
132
+ 'diffEditor.insertedTextBackground': '#bbf7d055',
133
+ 'diffEditor.removedTextBackground': '#fecaca55',
134
+ },
135
+ });
136
+
137
+ monaco.editor.defineTheme('nucel-dark', {
138
+ base: 'vs-dark',
139
+ inherit: true,
140
+ rules: [],
141
+ colors: {
142
+ 'editor.background': '#0a0a0c',
143
+ 'editor.foreground': '#f1f5f9',
144
+ 'editorLineNumber.foreground': '#475569',
145
+ 'editorLineNumber.activeForeground': '#f1f5f9',
146
+ 'editor.selectionBackground': '#1e3a8a55',
147
+ 'editor.inactiveSelectionBackground': '#1e293b',
148
+ 'editorCursor.foreground': '#f1f5f9',
149
+ 'editor.lineHighlightBackground': '#13131a',
150
+ 'editorGutter.background': '#0a0a0c',
151
+ 'editorWidget.background': '#13131a',
152
+ 'editorWidget.border': '#1e293b',
153
+ 'diffEditor.insertedTextBackground': '#16653433',
154
+ 'diffEditor.removedTextBackground': '#7f1d1d33',
155
+ },
156
+ });
157
+ }
158
+
159
+ /** Resolve a "light" | "dark" | "auto" mode to a registered theme name. */
160
+ export function resolveMonacoTheme(mode: 'light' | 'dark' | 'auto'): string {
161
+ if (typeof window === 'undefined') return 'nucel-light';
162
+ if (mode === 'auto') {
163
+ const isDark = document.documentElement.classList.contains('dark');
164
+ return isDark ? 'nucel-dark' : 'nucel-light';
165
+ }
166
+ return mode === 'dark' ? 'nucel-dark' : 'nucel-light';
167
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Lazy, cached Shiki highlighter shared across all CodeBlock instances.
3
+ *
4
+ * Strategy:
5
+ * - One singleton `HighlighterCore` created via `createHighlighter` from
6
+ * `shiki/bundle/web` (web bundle — no fs / oniguruma node dependency).
7
+ * - Themes (`github-light`, `github-dark`) and `plaintext` are loaded
8
+ * eagerly on first `getHighlighter()` call.
9
+ * - Additional languages are loaded on-demand via `loadLanguage(...)`.
10
+ * - SSR-safe: callers must guard with `typeof window !== 'undefined'` or
11
+ * invoke from `onMount()`. The Shiki web bundle pulls in an Oniguruma
12
+ * WASM payload that is not appropriate for SSR.
13
+ */
14
+
15
+ import type { Highlighter, BundledLanguage, BundledTheme } from 'shiki/bundle/web';
16
+
17
+ export const SHIKI_LIGHT_THEME: BundledTheme = 'github-light';
18
+ export const SHIKI_DARK_THEME: BundledTheme = 'github-dark';
19
+
20
+ export const SHIKI_DEFAULT_LANGS: BundledLanguage[] = ['plaintext' as BundledLanguage];
21
+
22
+ let highlighterPromise: Promise<Highlighter> | null = null;
23
+ const loadedLangs = new Set<string>(['plaintext']);
24
+ const inFlightLangs = new Map<string, Promise<void>>();
25
+
26
+ /**
27
+ * Get (or create) the singleton highlighter.
28
+ * Idempotent — subsequent calls return the cached promise.
29
+ */
30
+ export function getHighlighter(): Promise<Highlighter> {
31
+ if (!highlighterPromise) {
32
+ highlighterPromise = import('shiki/bundle/web').then(({ createHighlighter }) =>
33
+ createHighlighter({
34
+ themes: [SHIKI_LIGHT_THEME, SHIKI_DARK_THEME],
35
+ langs: SHIKI_DEFAULT_LANGS,
36
+ }),
37
+ );
38
+ }
39
+ return highlighterPromise;
40
+ }
41
+
42
+ /**
43
+ * Lazily register a language with the shared highlighter.
44
+ * Idempotent and safe to call concurrently from many CodeBlock instances.
45
+ */
46
+ export async function loadLanguage(lang: string): Promise<void> {
47
+ if (!lang || lang === 'plaintext' || loadedLangs.has(lang)) return;
48
+
49
+ const inFlight = inFlightLangs.get(lang);
50
+ if (inFlight) return inFlight;
51
+
52
+ const task = (async () => {
53
+ const highlighter = await getHighlighter();
54
+ try {
55
+ await highlighter.loadLanguage(lang as BundledLanguage);
56
+ loadedLangs.add(lang);
57
+ } catch (err) {
58
+ // Unknown language — fall back silently to plaintext at render time.
59
+ // (Don't throw — failed highlighting should never break the consumer page.)
60
+ console.warn(`[@nucel/ui CodeBlock] Unknown language "${lang}":`, err);
61
+ } finally {
62
+ inFlightLangs.delete(lang);
63
+ }
64
+ })();
65
+
66
+ inFlightLangs.set(lang, task);
67
+ return task;
68
+ }
69
+
70
+ /**
71
+ * Returns the Shiki language to use, falling back to `plaintext` if the
72
+ * requested language hasn't been loaded yet (or failed to load).
73
+ */
74
+ export function resolveLang(lang: string): string {
75
+ if (!lang) return 'plaintext';
76
+ if (loadedLangs.has(lang)) return lang;
77
+ return 'plaintext';
78
+ }
package/src/styles.css CHANGED
@@ -7,25 +7,61 @@
7
7
  inherits: false;
8
8
  }
9
9
 
10
+ /* -------------------------------------------------------------------------- */
11
+ /* Semantic color tokens (v0.6.0) */
12
+ /* */
13
+ /* Light theme is the default on :root. The .dark class on <html> (or any */
14
+ /* ancestor) switches to dark values via <ThemeProvider>. */
15
+ /* */
16
+ /* Naming convention */
17
+ /* ---------------- */
18
+ /* - "Raw" tokens live on :root / .dark with unprefixed names (--bg, --fg, */
19
+ /* --success, ...). These hold the actual color values. */
20
+ /* - Tailwind utility tokens are exposed via `@theme inline` further down */
21
+ /* by mapping --color-* names to the raw tokens. Tailwind 4 then */
22
+ /* generates utilities such as `bg-bg-elevated`, `text-fg-muted`, */
23
+ /* `border-success`, etc. */
24
+ /* - Legacy shadcn tokens (--background, --foreground, --primary, ...) are */
25
+ /* kept and aliased to the new raw tokens where they overlap, so existing */
26
+ /* components ("bg-background", "text-foreground", ...) keep working. */
27
+ /* -------------------------------------------------------------------------- */
28
+
10
29
  :root {
11
30
  --radius: 0.5rem;
12
- --background: oklch(1 0 0);
13
- --foreground: oklch(0.141 0.005 285.823);
14
- --card: oklch(1 0 0);
15
- --card-foreground: oklch(0.141 0.005 285.823);
16
- --popover: oklch(1 0 0);
17
- --popover-foreground: oklch(0.141 0.005 285.823);
18
- --primary: oklch(0.21 0.006 285.885);
19
- --primary-foreground: oklch(0.985 0 0);
31
+
32
+ /* Semantic surface + foreground (raw) */
33
+ --bg: oklch(1 0 0);
34
+ --bg-elevated: oklch(0.985 0 0);
35
+ --fg: oklch(0.141 0.005 285.823);
36
+ --fg-muted: oklch(0.552 0.016 285.938);
37
+
38
+ /* Semantic intent (raw) */
39
+ --primary-raw: oklch(0.21 0.006 285.885);
40
+ --primary-foreground-raw: oklch(0.985 0 0);
41
+ --success: oklch(0.62 0.17 145);
42
+ --success-foreground: oklch(0.99 0 0);
43
+ --warning: oklch(0.78 0.16 80);
44
+ --warning-foreground: oklch(0.18 0.02 80);
45
+ --danger: oklch(0.577 0.245 27.325);
46
+ --danger-foreground: oklch(0.985 0 0);
47
+
48
+ /* Legacy shadcn tokens — aliased to raw tokens where applicable. */
49
+ --background: var(--bg);
50
+ --foreground: var(--fg);
51
+ --card: var(--bg);
52
+ --card-foreground: var(--fg);
53
+ --popover: var(--bg);
54
+ --popover-foreground: var(--fg);
55
+ --primary: var(--primary-raw);
56
+ --primary-foreground: var(--primary-foreground-raw);
20
57
  --secondary: oklch(0.967 0.001 286.375);
21
58
  --secondary-foreground: oklch(0.21 0.006 285.885);
22
59
  --muted: oklch(0.967 0.001 286.375);
23
- --muted-foreground: oklch(0.552 0.016 285.938);
60
+ --muted-foreground: var(--fg-muted);
24
61
  --accent: oklch(0.967 0.001 286.375);
25
62
  --accent-foreground: oklch(0.21 0.006 285.885);
26
- --destructive: oklch(0.577 0.245 27.325);
27
- --success: oklch(0.527 0.154 150.069);
28
- --warning: oklch(0.769 0.188 70.08);
63
+ --destructive: var(--danger);
64
+ --destructive-foreground: var(--danger-foreground);
29
65
  --border: oklch(0.92 0.004 286.32);
30
66
  --input: oklch(0.92 0.004 286.32);
31
67
  --ring: oklch(0.705 0.015 286.067);
@@ -34,10 +70,10 @@
34
70
  --chart-3: oklch(0.398 0.07 227.392);
35
71
  --chart-4: oklch(0.828 0.189 84.429);
36
72
  --chart-5: oklch(0.769 0.188 70.08);
37
- --sidebar: oklch(0.985 0 0);
38
- --sidebar-foreground: oklch(0.141 0.005 285.823);
39
- --sidebar-primary: oklch(0.21 0.006 285.885);
40
- --sidebar-primary-foreground: oklch(0.985 0 0);
73
+ --sidebar: var(--bg-elevated);
74
+ --sidebar-foreground: var(--fg);
75
+ --sidebar-primary: var(--primary-raw);
76
+ --sidebar-primary-foreground: var(--primary-foreground-raw);
41
77
  --sidebar-accent: oklch(0.967 0.001 286.375);
42
78
  --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
43
79
  --sidebar-border: oklch(0.92 0.004 286.32);
@@ -45,23 +81,39 @@
45
81
  }
46
82
 
47
83
  .dark {
48
- --background: oklch(0.07 0.004 285.823);
49
- --foreground: oklch(0.97 0 0);
50
- --card: oklch(0.13 0.006 285.885);
51
- --card-foreground: oklch(0.97 0 0);
52
- --popover: oklch(0.13 0.006 285.885);
53
- --popover-foreground: oklch(0.97 0 0);
54
- --primary: oklch(0.92 0.004 286.32);
55
- --primary-foreground: oklch(0.13 0.006 285.885);
84
+ /* Semantic surface + foreground (raw) */
85
+ --bg: oklch(0.07 0.004 285.823);
86
+ --bg-elevated: oklch(0.13 0.006 285.885);
87
+ --fg: oklch(0.97 0 0);
88
+ --fg-muted: oklch(0.62 0.015 286.067);
89
+
90
+ /* Semantic intent (raw) */
91
+ --primary-raw: oklch(0.92 0.004 286.32);
92
+ --primary-foreground-raw: oklch(0.13 0.006 285.885);
93
+ --success: oklch(0.72 0.17 152);
94
+ --success-foreground: oklch(0.1 0.02 152);
95
+ --warning: oklch(0.82 0.17 85);
96
+ --warning-foreground: oklch(0.13 0.02 85);
97
+ --danger: oklch(0.704 0.191 22.216);
98
+ --danger-foreground: oklch(0.99 0 0);
99
+
100
+ /* Legacy shadcn tokens — dark-mode overrides where they diverge from raw. */
101
+ --background: var(--bg);
102
+ --foreground: var(--fg);
103
+ --card: var(--bg-elevated);
104
+ --card-foreground: var(--fg);
105
+ --popover: var(--bg-elevated);
106
+ --popover-foreground: var(--fg);
107
+ --primary: var(--primary-raw);
108
+ --primary-foreground: var(--primary-foreground-raw);
56
109
  --secondary: oklch(0.19 0.006 286.033);
57
110
  --secondary-foreground: oklch(0.97 0 0);
58
111
  --muted: oklch(0.19 0.006 286.033);
59
- --muted-foreground: oklch(0.62 0.015 286.067);
112
+ --muted-foreground: var(--fg-muted);
60
113
  --accent: oklch(0.22 0.006 286.033);
61
114
  --accent-foreground: oklch(0.97 0 0);
62
- --destructive: oklch(0.704 0.191 22.216);
63
- --success: oklch(0.627 0.168 150.069);
64
- --warning: oklch(0.828 0.189 84.429);
115
+ --destructive: var(--danger);
116
+ --destructive-foreground: var(--danger-foreground);
65
117
  --border: oklch(1 0 0 / 8%);
66
118
  --input: oklch(1 0 0 / 12%);
67
119
  --ring: oklch(0.552 0.016 285.938);
@@ -70,8 +122,8 @@
70
122
  --chart-3: oklch(0.769 0.188 70.08);
71
123
  --chart-4: oklch(0.627 0.265 303.9);
72
124
  --chart-5: oklch(0.645 0.246 16.439);
73
- --sidebar: oklch(0.13 0.006 285.885);
74
- --sidebar-foreground: oklch(0.97 0 0);
125
+ --sidebar: var(--bg-elevated);
126
+ --sidebar-foreground: var(--fg);
75
127
  --sidebar-primary: oklch(0.488 0.243 264.376);
76
128
  --sidebar-primary-foreground: oklch(0.97 0 0);
77
129
  --sidebar-accent: oklch(0.22 0.006 286.033);
@@ -86,6 +138,22 @@
86
138
  --radius-lg: var(--radius);
87
139
  --radius-xl: calc(var(--radius) + 6px);
88
140
  --radius-2xl: calc(var(--radius) + 12px);
141
+
142
+ /* New semantic surface + foreground utilities */
143
+ --color-bg: var(--bg);
144
+ --color-bg-elevated: var(--bg-elevated);
145
+ --color-fg: var(--fg);
146
+ --color-fg-muted: var(--fg-muted);
147
+
148
+ /* Intent utilities: bg-success / text-warning / border-danger / etc. */
149
+ --color-success: var(--success);
150
+ --color-success-foreground: var(--success-foreground);
151
+ --color-warning: var(--warning);
152
+ --color-warning-foreground: var(--warning-foreground);
153
+ --color-danger: var(--danger);
154
+ --color-danger-foreground: var(--danger-foreground);
155
+
156
+ /* Legacy shadcn utilities (kept for backwards-compat). */
89
157
  --color-background: var(--background);
90
158
  --color-foreground: var(--foreground);
91
159
  --color-card: var(--card);
@@ -101,8 +169,7 @@
101
169
  --color-accent: var(--accent);
102
170
  --color-accent-foreground: var(--accent-foreground);
103
171
  --color-destructive: var(--destructive);
104
- --color-success: var(--success);
105
- --color-warning: var(--warning);
172
+ --color-destructive-foreground: var(--destructive-foreground);
106
173
  --color-border: var(--border);
107
174
  --color-input: var(--input);
108
175
  --color-ring: var(--ring);
@@ -119,6 +186,7 @@
119
186
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
120
187
  --color-sidebar-border: var(--sidebar-border);
121
188
  --color-sidebar-ring: var(--sidebar-ring);
189
+
122
190
  --font-sans: 'Inter', sans-serif;
123
191
  --font-mono: 'JetBrains Mono', monospace;
124
192
  }