@dorsk/tsumikit 0.1.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/autoresize.d.ts +11 -0
  4. package/dist/autoresize.js +24 -0
  5. package/dist/components/atoms/Badge.svelte +72 -0
  6. package/dist/components/atoms/Badge.svelte.d.ts +12 -0
  7. package/dist/components/atoms/Button.svelte +156 -0
  8. package/dist/components/atoms/Button.svelte.d.ts +13 -0
  9. package/dist/components/atoms/Card.svelte +46 -0
  10. package/dist/components/atoms/Card.svelte.d.ts +11 -0
  11. package/dist/components/atoms/Checkbox.svelte +99 -0
  12. package/dist/components/atoms/Checkbox.svelte.d.ts +10 -0
  13. package/dist/components/atoms/Chip.svelte +53 -0
  14. package/dist/components/atoms/Chip.svelte.d.ts +11 -0
  15. package/dist/components/atoms/Heading.svelte +66 -0
  16. package/dist/components/atoms/Heading.svelte.d.ts +13 -0
  17. package/dist/components/atoms/Icon.svelte +151 -0
  18. package/dist/components/atoms/Icon.svelte.d.ts +18 -0
  19. package/dist/components/atoms/Input.svelte +42 -0
  20. package/dist/components/atoms/Input.svelte.d.ts +10 -0
  21. package/dist/components/atoms/Link.svelte +31 -0
  22. package/dist/components/atoms/Link.svelte.d.ts +10 -0
  23. package/dist/components/atoms/Progress.svelte +59 -0
  24. package/dist/components/atoms/Progress.svelte.d.ts +9 -0
  25. package/dist/components/atoms/Select.svelte +95 -0
  26. package/dist/components/atoms/Select.svelte.d.ts +11 -0
  27. package/dist/components/atoms/Slider.svelte +136 -0
  28. package/dist/components/atoms/Slider.svelte.d.ts +14 -0
  29. package/dist/components/atoms/Switch.svelte +64 -0
  30. package/dist/components/atoms/Switch.svelte.d.ts +8 -0
  31. package/dist/components/atoms/Text.svelte +127 -0
  32. package/dist/components/atoms/Text.svelte.d.ts +16 -0
  33. package/dist/components/atoms/Textarea.svelte +62 -0
  34. package/dist/components/atoms/Textarea.svelte.d.ts +11 -0
  35. package/dist/components/layouts/AppShell.svelte +304 -0
  36. package/dist/components/layouts/AppShell.svelte.d.ts +21 -0
  37. package/dist/components/layouts/AutoGrid.svelte +36 -0
  38. package/dist/components/layouts/AutoGrid.svelte.d.ts +12 -0
  39. package/dist/components/layouts/Cluster.svelte +45 -0
  40. package/dist/components/layouts/Cluster.svelte.d.ts +14 -0
  41. package/dist/components/layouts/Container.svelte +40 -0
  42. package/dist/components/layouts/Container.svelte.d.ts +13 -0
  43. package/dist/components/layouts/NavItem.svelte +95 -0
  44. package/dist/components/layouts/NavItem.svelte.d.ts +14 -0
  45. package/dist/components/layouts/Stack.svelte +44 -0
  46. package/dist/components/layouts/Stack.svelte.d.ts +13 -0
  47. package/dist/components/molecules/Accordion.svelte +94 -0
  48. package/dist/components/molecules/Accordion.svelte.d.ts +16 -0
  49. package/dist/components/molecules/CodeBlock.svelte +119 -0
  50. package/dist/components/molecules/CodeBlock.svelte.d.ts +17 -0
  51. package/dist/components/molecules/CopyButton.svelte +80 -0
  52. package/dist/components/molecules/CopyButton.svelte.d.ts +13 -0
  53. package/dist/components/molecules/Dropzone.svelte +140 -0
  54. package/dist/components/molecules/Dropzone.svelte.d.ts +13 -0
  55. package/dist/components/molecules/Field.svelte +57 -0
  56. package/dist/components/molecules/Field.svelte.d.ts +12 -0
  57. package/dist/components/molecules/FileButton.svelte +68 -0
  58. package/dist/components/molecules/FileButton.svelte.d.ts +14 -0
  59. package/dist/components/molecules/FontScalePicker.svelte +21 -0
  60. package/dist/components/molecules/FontScalePicker.svelte.d.ts +6 -0
  61. package/dist/components/molecules/IconButton.svelte +36 -0
  62. package/dist/components/molecules/IconButton.svelte.d.ts +13 -0
  63. package/dist/components/molecules/Menu.svelte +120 -0
  64. package/dist/components/molecules/Menu.svelte.d.ts +17 -0
  65. package/dist/components/molecules/Modal.svelte +263 -0
  66. package/dist/components/molecules/Modal.svelte.d.ts +13 -0
  67. package/dist/components/molecules/OptionButton.svelte +76 -0
  68. package/dist/components/molecules/OptionButton.svelte.d.ts +10 -0
  69. package/dist/components/molecules/Popover.svelte +125 -0
  70. package/dist/components/molecules/Popover.svelte.d.ts +18 -0
  71. package/dist/components/molecules/RadioGroup.svelte +110 -0
  72. package/dist/components/molecules/RadioGroup.svelte.d.ts +16 -0
  73. package/dist/components/molecules/SelectButton.svelte +52 -0
  74. package/dist/components/molecules/SelectButton.svelte.d.ts +15 -0
  75. package/dist/components/molecules/Tabs.svelte +119 -0
  76. package/dist/components/molecules/Tabs.svelte.d.ts +15 -0
  77. package/dist/components/molecules/ThemePicker.svelte +22 -0
  78. package/dist/components/molecules/ThemePicker.svelte.d.ts +6 -0
  79. package/dist/components/molecules/Toaster.svelte +73 -0
  80. package/dist/components/molecules/Toaster.svelte.d.ts +18 -0
  81. package/dist/components/molecules/Toggle.svelte +68 -0
  82. package/dist/components/molecules/Toggle.svelte.d.ts +11 -0
  83. package/dist/components/molecules/Tooltip.svelte +106 -0
  84. package/dist/components/molecules/Tooltip.svelte.d.ts +10 -0
  85. package/dist/components/organisms/DataTable.svelte +145 -0
  86. package/dist/components/organisms/DataTable.svelte.d.ts +43 -0
  87. package/dist/env.d.ts +1 -0
  88. package/dist/env.js +4 -0
  89. package/dist/index.d.ts +46 -0
  90. package/dist/index.js +56 -0
  91. package/dist/stores/fontscale.svelte.d.ts +15 -0
  92. package/dist/stores/fontscale.svelte.js +49 -0
  93. package/dist/stores/theme.svelte.d.ts +96 -0
  94. package/dist/stores/theme.svelte.js +71 -0
  95. package/dist/stores/toast.svelte.d.ts +19 -0
  96. package/dist/stores/toast.svelte.js +26 -0
  97. package/dist/styles/app.css +522 -0
  98. package/dist/styles/variables.css +651 -0
  99. package/package.json +71 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dorsk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @dorsk/tsumikit
2
+
3
+ A minimal, **dependency-free** UI kit for Svelte 5 + pure CSS. Token-driven
4
+ atoms and molecules with theming, font-scaling, a color-blind-safe theme and
5
+ mobile/a11y baked in. No CDNs, no runtime UI dependencies — just Svelte 5 as a
6
+ peer.
7
+
8
+ ## Design
9
+
10
+ Each layer reaches only one layer down:
11
+
12
+ ```
13
+ variables.css → every color/space/radius/font/size is a var(--…). No
14
+ component hard-codes a hex or a pixel.
15
+
16
+ atoms → the only place raw <button>/<input>/<select>/<h*>/text lives.
17
+
18
+ molecules → compose or specialize atoms (never reimplement a primitive).
19
+
20
+ your app → assemble molecules + layout.
21
+ ```
22
+
23
+ **Rules:** primitives live in atoms only · specialize, don't reimplement · no
24
+ hard-coded values · override by specificity, never by forking · props (incl.
25
+ `aria-*`) spread down onto the underlying element.
26
+
27
+ **Live demo:** https://dorskfr.github.io/tsumikit
28
+
29
+ ## Develop
30
+
31
+ ```bash
32
+ npm install
33
+ npm run dev # showcase at the printed URL (+ /shell for the AppShell demo)
34
+ npm run lint # biome (ts/js)
35
+ npm run check # svelte-check
36
+ npm run build # prerendered static site in /build
37
+ npm run package # build the publishable library into /dist (+ publint)
38
+ ```
39
+
40
+ Git hooks (Biome on commit, `svelte-check` on push) are installed by lefthook
41
+ via the `prepare` script. The library is published to npm from a release tag
42
+ via GitHub Actions using npm **trusted publishing** (OIDC — no token secret).
43
+
44
+ ## Use in your own project
45
+
46
+ ```ts
47
+ import '@dorsk/tsumikit/styles/app.css'; // once, at the app root
48
+ import { Button, Field, Input, Modal, ThemePicker } from '@dorsk/tsumikit';
49
+ ```
50
+
51
+ ```svelte
52
+ <Field label="Name" for="name">
53
+ <Input id="name" bind:value={name} />
54
+ </Field>
55
+ <Button variant="primary" onclick={save}>Save</Button>
56
+ ```
57
+
58
+ ## Theming
59
+
60
+ - 17 themes ship (dark, light, sepia, **colorblind** — Okabe-Ito —, plus mocha,
61
+ dracula, nord, tokyonight, gruvbox, solarized, rosepine, onedark, everforest,
62
+ monokai, amoled, highcontrast).
63
+ - A new theme = one entry in `THEMES` (`stores/theme.svelte.ts`) + one
64
+ `[data-theme="id"]` block in `variables.css`. Nothing else changes.
65
+ - `<ThemePicker />` and `<FontScalePicker />` wire the stores to the UI. Theme
66
+ is persisted to `localStorage` and applied with no flash (head snippet in
67
+ `app.html`) and updates the mobile `<meta name="theme-color">`.
68
+
69
+ ## Components
70
+
71
+ **Atoms:** Text, Heading, Button, Input, Textarea, Select, Switch, Checkbox,
72
+ Slider, Progress, Card, Badge, Chip, Link, Icon (open registry — pass a
73
+ `children` snippet for any custom SVG).
74
+
75
+ **Molecules:** Field, IconButton, SelectButton, Toggle, OptionButton, Modal,
76
+ Popover, Menu, Tabs, RadioGroup, Tooltip, Accordion, CopyButton, FileButton,
77
+ Dropzone, CodeBlock, Toaster, ThemePicker, FontScalePicker.
78
+
79
+ **Organisms:** DataTable (generic `<T>`, typed columns + cell snippets).
80
+
81
+ **Layouts:** AppShell (responsive header/sidebar/main/footer — persistent
82
+ sidebar on desktop, overlay drawer on mobile, optionally resizable), NavItem
83
+ (collapses to an icon rail when the sidebar is narrow), Container, Stack
84
+ (vertical), Cluster (wrapping row), AutoGrid (intrinsically responsive columns —
85
+ no media/container query needed).
86
+
87
+ ## Container queries
88
+
89
+ AppShell's `main` and `sidebar` are query containers (`container-name: main` /
90
+ `sidebar`), so components respond to **their box's** width, not the viewport.
91
+ Use the `.cq-*` utilities (`.cq-hide`, `.cq-stack`, `.cq-truncate`,
92
+ `.cq-hide-xs`) on children of any `.cq` container — e.g. wrap a button's label in
93
+ `.cq-hide` and it becomes icon-only when its column is tight. `NavItem` uses the
94
+ `sidebar` container to drop labels below ~8rem; `AppShell resizableSidebar` lets
95
+ you drag the sidebar down to that icon rail (width persisted).
96
+
97
+ **Stores:** `theme`, `toasts`, `fontScale` (opt-in). **Actions:** `autoresize`.
98
+
99
+ ## Sizing & zoom
100
+
101
+ The kit is **`rem`-based and never resets the root font-size**, so the user's
102
+ browser/OS font-size preference and browser zoom scale everything
103
+ proportionally with no code. That's the recommended path for magnification.
104
+
105
+ `fontScale` / `<FontScalePicker>` is an **opt-in** extra (drives `--fs-scale`,
106
+ text tokens only) for reading-dense apps that want larger body text while
107
+ keeping chrome compact. It isn't wired into AppShell or any default.
108
+
109
+ ## Built on the platform
110
+
111
+ Interactive components lean on modern web features rather than reimplementing
112
+ them in JS — less code, better a11y, fewer edge cases:
113
+
114
+ - **`<dialog>`** for Modal — top-layer rendering, real focus trap, inert
115
+ background, focus restore and `::backdrop`, all from the browser.
116
+ - **Popover API** (`popover` / `popovertarget`) for Popover & Menu — top layer
117
+ (no z-index races), light-dismiss and Escape handled natively; we add only
118
+ smart placement (anchors to the trigger, flips into the viewport).
119
+ - **Native form controls** under Checkbox / RadioGroup / Select / Switch — real
120
+ keyboard, form participation and a11y; only the visuals are tokenised.
121
+ - **`color-scheme`** per theme so native widgets/scrollbars match; **`@media
122
+ (forced-colors)`** (Windows High Contrast) and **`prefers-contrast`** support;
123
+ **`prefers-reduced-motion`** disables animation globally.
124
+ - **Intrinsic responsive layout**: `.auto-grid` (auto-fit + `minmax`) and `.cq`
125
+ (container queries) adapt to available space, not just viewport breakpoints.
126
+
127
+ ## Syntax highlighting (CodeBlock)
128
+
129
+ The kit ships **no** highlighter — that keeps it zero-dep and avoids shipping a
130
+ big grammar bundle to every consumer. `CodeBlock` renders the chrome (language
131
+ label, copy, line numbers, wrap, scroll) and takes code three ways: plain
132
+ `code`, a `highlight={(code, lang) => htmlString}` callback, or pre-rendered
133
+ `html`. Pick a highlighter per app:
134
+
135
+ - **highlight.js** — class-based output (`hljs-*`). Recommended for this kit:
136
+ those classes are already mapped to the `--syn-*` theme tokens (in `app.css`),
137
+ so highlighted code re-themes with every theme for free. Import only the
138
+ languages you need to keep it lean.
139
+ - **Prism** — also class-based (`token.*`), mapped the same way. Similar fit.
140
+ - **Shiki** — VS Code-grade accuracy, but it emits *inline colors* from a fixed
141
+ theme, so it won't follow your themes out of the box. Use its
142
+ `css-variables` theme and map `--shiki-*` to your `--syn-*` tokens if you want
143
+ it themed. Best when you want exact editor fidelity over theme-following.
144
+
145
+ ```svelte
146
+ <script>
147
+ import hljs from 'highlight.js/lib/core';
148
+ import ts from 'highlight.js/lib/languages/typescript';
149
+ hljs.registerLanguage('typescript', ts);
150
+ const hl = (code, lang) => hljs.highlight(code, { language: lang ?? 'typescript' }).value;
151
+ </script>
152
+ <CodeBlock {code} lang="typescript" highlight={hl} showLineNumbers />
153
+ ```
154
+
155
+ ## Accessibility baseline
156
+
157
+ - Every interactive atom spreads `...rest`, so `id`, `aria-*`, `title`,
158
+ `disabled` and native events pass through.
159
+ - Visible `:focus-visible` rings; ARIA patterns implemented for switch, menu
160
+ (`role=menu` + roving focus), tabs (`tablist` + arrow keys), radiogroup,
161
+ dialog; polite live region for toasts; `.sr-only`.
162
+ - Mobile-first: one `min-width: 640px` breakpoint, bottom-sheet→centered-modal,
163
+ safe-area insets, 16px-min inputs (no iOS zoom).
164
+ - A verified color-blind-safe theme (Okabe-Ito); meaning never relies on hue
165
+ alone (icons + text accompany every status color).
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Svelte action: grow a <textarea> with its content up to a max height, then
3
+ * scroll. Pass the bound value so it re-measures on programmatic changes
4
+ * (drafts loading, clearing, etc.).
5
+ *
6
+ * Usage: <textarea use:autoresize={value} ...></textarea>
7
+ */
8
+ export declare function autoresize(node: HTMLTextAreaElement, _value?: string): {
9
+ update(): void;
10
+ destroy(): void;
11
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Svelte action: grow a <textarea> with its content up to a max height, then
3
+ * scroll. Pass the bound value so it re-measures on programmatic changes
4
+ * (drafts loading, clearing, etc.).
5
+ *
6
+ * Usage: <textarea use:autoresize={value} ...></textarea>
7
+ */
8
+ export function autoresize(node, _value) {
9
+ const resize = () => {
10
+ node.style.height = 'auto';
11
+ node.style.height = `${node.scrollHeight}px`;
12
+ };
13
+ resize();
14
+ node.addEventListener('input', resize);
15
+ return {
16
+ update() {
17
+ // re-measure when the bound value changes from outside (e.g. cleared)
18
+ resize();
19
+ },
20
+ destroy() {
21
+ node.removeEventListener('input', resize);
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ // Base badge/pill primitive. Owns the shape + tone palette from theme tokens.
3
+ // Polymorphic via `as` so it can be a static <span> or an interactive
4
+ // <button> (e.g. SubagentBadge). Specialized badges compose this and add only
5
+ // their specifics (per-machine hue, toggle state).
6
+ import type { Snippet } from 'svelte';
7
+
8
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
9
+
10
+ let {
11
+ tone = 'neutral',
12
+ as = 'span',
13
+ class: klass = '',
14
+ children,
15
+ ...rest
16
+ }: {
17
+ tone?: Tone;
18
+ as?: 'span' | 'button';
19
+ class?: string;
20
+ children?: Snippet;
21
+ [key: string]: unknown;
22
+ } = $props();
23
+ </script>
24
+
25
+ <svelte:element
26
+ this={as}
27
+ class="badge {klass}"
28
+ class:badge-ok={tone === 'ok'}
29
+ class:badge-warn={tone === 'warn'}
30
+ class:badge-danger={tone === 'danger'}
31
+ class:badge-info={tone === 'info'}
32
+ {...rest}
33
+ >
34
+ {@render children?.()}
35
+ </svelte:element>
36
+
37
+ <style>
38
+ .badge {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ gap: var(--sp-1);
42
+ padding: 0.15rem var(--sp-2);
43
+ border-radius: var(--r-pill);
44
+ font-size: var(--fs-xs);
45
+ font-weight: var(--fw-medium);
46
+ line-height: 1.4;
47
+ background: var(--bg-elevated-2);
48
+ color: var(--text-muted);
49
+ border: 1px solid var(--border);
50
+ white-space: nowrap;
51
+ }
52
+ .badge-ok {
53
+ color: var(--ok);
54
+ border-color: color-mix(in srgb, var(--ok) 40%, transparent);
55
+ background: color-mix(in srgb, var(--ok) 12%, transparent);
56
+ }
57
+ .badge-warn {
58
+ color: var(--warn);
59
+ border-color: color-mix(in srgb, var(--warn) 40%, transparent);
60
+ background: color-mix(in srgb, var(--warn) 12%, transparent);
61
+ }
62
+ .badge-danger {
63
+ color: var(--danger);
64
+ border-color: color-mix(in srgb, var(--danger) 40%, transparent);
65
+ background: color-mix(in srgb, var(--danger) 12%, transparent);
66
+ }
67
+ .badge-info {
68
+ color: var(--info);
69
+ border-color: color-mix(in srgb, var(--info) 40%, transparent);
70
+ background: color-mix(in srgb, var(--info) 12%, transparent);
71
+ }
72
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
3
+ type $$ComponentProps = {
4
+ tone?: Tone;
5
+ as?: 'span' | 'button';
6
+ class?: string;
7
+ children?: Snippet;
8
+ [key: string]: unknown;
9
+ };
10
+ declare const Badge: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type Badge = ReturnType<typeof Badge>;
12
+ export default Badge;
@@ -0,0 +1,156 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+
5
+ type ButtonProps = HTMLButtonAttributes & {
6
+ variant?: 'default' | 'primary' | 'ghost' | 'danger';
7
+ size?: 'sm' | 'md';
8
+ control?: boolean;
9
+ block?: boolean;
10
+ class?: string;
11
+ children?: Snippet;
12
+ };
13
+
14
+ let {
15
+ variant = 'default',
16
+ size = 'md',
17
+ control = false,
18
+ block = false,
19
+ type = 'button',
20
+ disabled = false,
21
+ title,
22
+ onclick,
23
+ class: klass = '',
24
+ children,
25
+ ...rest
26
+ }: ButtonProps = $props();
27
+ </script>
28
+
29
+ <button
30
+ {...rest}
31
+ {type}
32
+ {disabled}
33
+ {title}
34
+ class="btn {klass}"
35
+ class:btn-primary={variant === 'primary'}
36
+ class:btn-ghost={variant === 'ghost'}
37
+ class:btn-danger={variant === 'danger'}
38
+ class:btn-sm={size === 'sm'}
39
+ class:btn-control={control}
40
+ class:btn-block={block}
41
+ onclick={onclick}
42
+ >
43
+ {@render children?.()}
44
+ </button>
45
+
46
+ <style>
47
+ /* The canonical control: owns its variants/sizes from theme tokens. Svelte 5
48
+ scopes via :where() (zero added specificity), so these match the same way
49
+ the global .btn rules did — feature-component overrides (e.g. .dhead
50
+ .tapbtn) keep winning by specificity. Icons size themselves (Icon.svelte),
51
+ so no svg sizing rule lives here. */
52
+ .btn {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: var(--sp-2);
57
+ padding: var(--sp-2) var(--sp-4);
58
+ min-height: 2.5rem;
59
+ border: 1px solid var(--border-strong);
60
+ border-radius: var(--r-md);
61
+ background: var(--surface);
62
+ color: var(--text);
63
+ font-weight: var(--fw-medium);
64
+ font-size: var(--fs-sm);
65
+ line-height: 1;
66
+ transition:
67
+ background 0.12s var(--ease),
68
+ border-color 0.12s var(--ease),
69
+ opacity 0.12s var(--ease);
70
+ user-select: none;
71
+ white-space: nowrap;
72
+ }
73
+ .btn:hover:not(:disabled) {
74
+ border-color: var(--accent);
75
+ }
76
+ .btn:disabled {
77
+ opacity: 0.45;
78
+ cursor: not-allowed;
79
+ }
80
+ .btn-primary {
81
+ background: var(--accent);
82
+ border-color: var(--accent);
83
+ color: var(--text-on-accent);
84
+ font-weight: var(--fw-semibold);
85
+ }
86
+ .btn-primary:hover:not(:disabled) {
87
+ filter: brightness(1.08);
88
+ }
89
+ .btn-danger {
90
+ color: var(--danger);
91
+ border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
92
+ }
93
+ .btn-danger:hover:not(:disabled) {
94
+ background: color-mix(in srgb, var(--danger) 14%, transparent);
95
+ border-color: var(--danger);
96
+ }
97
+ .btn-ghost {
98
+ background: transparent;
99
+ border-color: transparent;
100
+ }
101
+ .btn-ghost:hover:not(:disabled) {
102
+ background: var(--bg-elevated-2);
103
+ border-color: transparent;
104
+ }
105
+ .btn-sm {
106
+ min-height: 2rem;
107
+ padding: var(--sp-1) var(--sp-3);
108
+ font-size: var(--fs-xs);
109
+ }
110
+ .btn-block {
111
+ width: 100%;
112
+ }
113
+ /* Uniform-height control (CCT-250 item 1): icon buttons, inputs and action
114
+ buttons that share a toolbar/composer row line up exactly. */
115
+ .btn-control {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: var(--sp-2);
120
+ height: var(--control-height);
121
+ min-height: var(--control-height);
122
+ padding: 0 var(--sp-3);
123
+ border: 1px solid var(--border-strong);
124
+ border-radius: var(--r-md);
125
+ background: var(--surface);
126
+ color: var(--text);
127
+ font-weight: var(--fw-medium);
128
+ font-size: var(--fs-sm);
129
+ line-height: 1;
130
+ white-space: nowrap;
131
+ transition:
132
+ background 0.12s var(--ease),
133
+ border-color 0.12s var(--ease),
134
+ opacity 0.12s var(--ease);
135
+ user-select: none;
136
+ }
137
+ .btn-control:hover:not(:disabled) {
138
+ border-color: var(--accent);
139
+ }
140
+ .btn-control:disabled {
141
+ opacity: 0.45;
142
+ cursor: not-allowed;
143
+ }
144
+ /* .btn-control follows .btn-primary in source order and would otherwise paint
145
+ the primary action with the neutral --surface; restore the accent fill when
146
+ both are present (CCT-345). */
147
+ .btn-control.btn-primary {
148
+ background: var(--accent);
149
+ border-color: var(--accent);
150
+ color: var(--text-on-accent);
151
+ }
152
+ .btn-control.btn-primary:hover:not(:disabled) {
153
+ border-color: var(--accent);
154
+ filter: brightness(1.08);
155
+ }
156
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ type ButtonProps = HTMLButtonAttributes & {
4
+ variant?: 'default' | 'primary' | 'ghost' | 'danger';
5
+ size?: 'sm' | 'md';
6
+ control?: boolean;
7
+ block?: boolean;
8
+ class?: string;
9
+ children?: Snippet;
10
+ };
11
+ declare const Button: import("svelte").Component<ButtonProps, {}, "">;
12
+ type Button = ReturnType<typeof Button>;
13
+ export default Button;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ // Elevated surface primitive — the canonical card/panel container. Owns its
3
+ // background/border/radius/padding from theme tokens. `tap` adds the
4
+ // interactive hover/active affordance for tappable list items (e.g. session
5
+ // rows); `as` lets it be a button/anchor when the whole surface is clickable.
6
+ import type { Snippet } from 'svelte';
7
+
8
+ let {
9
+ tap = false,
10
+ as = 'div',
11
+ class: klass = '',
12
+ children,
13
+ ...rest
14
+ }: {
15
+ tap?: boolean;
16
+ as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
17
+ class?: string;
18
+ children?: Snippet;
19
+ [key: string]: unknown;
20
+ } = $props();
21
+ </script>
22
+
23
+ <svelte:element this={as} class="card {klass}" class:card-tap={tap} {...rest}>
24
+ {@render children?.()}
25
+ </svelte:element>
26
+
27
+ <style>
28
+ .card {
29
+ background: var(--bg-elevated);
30
+ border: 1px solid var(--border);
31
+ border-radius: var(--r-lg);
32
+ padding: var(--sp-4);
33
+ }
34
+ .card-tap {
35
+ cursor: pointer;
36
+ transition:
37
+ border-color 0.12s var(--ease),
38
+ background 0.12s var(--ease);
39
+ }
40
+ .card-tap:active {
41
+ background: var(--bg-elevated-2);
42
+ }
43
+ .card-tap:hover {
44
+ border-color: var(--border-strong);
45
+ }
46
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ tap?: boolean;
4
+ as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
5
+ class?: string;
6
+ children?: Snippet;
7
+ [key: string]: unknown;
8
+ };
9
+ declare const Card: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Card = ReturnType<typeof Card>;
11
+ export default Card;
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ // Checkbox primitive. A real <input type="checkbox"> (keeps native keyboard,
3
+ // form participation and a11y) with a token-styled box and an associated
4
+ // label. Supports `bind:checked`, `indeterminate`, and passes through every
5
+ // native attribute via `...rest`.
6
+ import type { HTMLInputAttributes } from 'svelte/elements';
7
+
8
+ let {
9
+ checked = $bindable(false),
10
+ indeterminate = false,
11
+ label,
12
+ class: klass = '',
13
+ el = $bindable(null),
14
+ ...rest
15
+ }: HTMLInputAttributes & {
16
+ checked?: boolean;
17
+ indeterminate?: boolean;
18
+ label: string;
19
+ el?: HTMLInputElement | null;
20
+ } = $props();
21
+
22
+ // `indeterminate` is a property, not an attribute — sync it imperatively.
23
+ $effect(() => {
24
+ if (el) el.indeterminate = indeterminate;
25
+ });
26
+ </script>
27
+
28
+ <label class="checkbox {klass}">
29
+ <input bind:this={el} type="checkbox" bind:checked {...rest} />
30
+ <span class="box" aria-hidden="true"></span>
31
+ <span class="label-text">{label}</span>
32
+ </label>
33
+
34
+ <style>
35
+ .checkbox {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ gap: var(--sp-2);
39
+ cursor: pointer;
40
+ font-size: var(--fs-sm);
41
+ color: var(--text);
42
+ }
43
+ /* Visually hide the native control but keep it in the a11y tree + hit area. */
44
+ input {
45
+ position: absolute;
46
+ width: 1px;
47
+ height: 1px;
48
+ opacity: 0;
49
+ margin: 0;
50
+ }
51
+ .box {
52
+ position: relative;
53
+ flex: none;
54
+ width: 1.15rem;
55
+ height: 1.15rem;
56
+ border: 1px solid var(--border-strong);
57
+ border-radius: var(--r-sm);
58
+ background: var(--bg);
59
+ transition:
60
+ background 0.12s var(--ease),
61
+ border-color 0.12s var(--ease);
62
+ }
63
+ /* check mark */
64
+ .box::after {
65
+ content: '';
66
+ position: absolute;
67
+ inset: 0;
68
+ background: var(--text-on-accent);
69
+ clip-path: polygon(41% 67%, 79% 26%, 87% 35%, 41% 84%, 15% 56%, 24% 47%);
70
+ opacity: 0;
71
+ transition: opacity 0.1s var(--ease);
72
+ }
73
+ input:checked + .box {
74
+ background: var(--accent);
75
+ border-color: var(--accent);
76
+ }
77
+ input:checked + .box::after {
78
+ opacity: 1;
79
+ }
80
+ /* indeterminate dash */
81
+ input:indeterminate + .box {
82
+ background: var(--accent);
83
+ border-color: var(--accent);
84
+ }
85
+ input:indeterminate + .box::after {
86
+ opacity: 1;
87
+ clip-path: polygon(22% 42%, 78% 42%, 78% 58%, 22% 58%);
88
+ }
89
+ input:focus-visible + .box {
90
+ outline: 2px solid var(--accent);
91
+ outline-offset: 2px;
92
+ }
93
+ input:disabled ~ * {
94
+ opacity: 0.45;
95
+ }
96
+ .checkbox:has(input:disabled) {
97
+ cursor: not-allowed;
98
+ }
99
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ type $$ComponentProps = HTMLInputAttributes & {
3
+ checked?: boolean;
4
+ indeterminate?: boolean;
5
+ label: string;
6
+ el?: HTMLInputElement | null;
7
+ };
8
+ declare const Checkbox: import("svelte").Component<$$ComponentProps, {}, "checked" | "el">;
9
+ type Checkbox = ReturnType<typeof Checkbox>;
10
+ export default Checkbox;