@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.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/autoresize.d.ts +11 -0
- package/dist/autoresize.js +24 -0
- package/dist/components/atoms/Badge.svelte +72 -0
- package/dist/components/atoms/Badge.svelte.d.ts +12 -0
- package/dist/components/atoms/Button.svelte +156 -0
- package/dist/components/atoms/Button.svelte.d.ts +13 -0
- package/dist/components/atoms/Card.svelte +46 -0
- package/dist/components/atoms/Card.svelte.d.ts +11 -0
- package/dist/components/atoms/Checkbox.svelte +99 -0
- package/dist/components/atoms/Checkbox.svelte.d.ts +10 -0
- package/dist/components/atoms/Chip.svelte +53 -0
- package/dist/components/atoms/Chip.svelte.d.ts +11 -0
- package/dist/components/atoms/Heading.svelte +66 -0
- package/dist/components/atoms/Heading.svelte.d.ts +13 -0
- package/dist/components/atoms/Icon.svelte +151 -0
- package/dist/components/atoms/Icon.svelte.d.ts +18 -0
- package/dist/components/atoms/Input.svelte +42 -0
- package/dist/components/atoms/Input.svelte.d.ts +10 -0
- package/dist/components/atoms/Link.svelte +31 -0
- package/dist/components/atoms/Link.svelte.d.ts +10 -0
- package/dist/components/atoms/Progress.svelte +59 -0
- package/dist/components/atoms/Progress.svelte.d.ts +9 -0
- package/dist/components/atoms/Select.svelte +95 -0
- package/dist/components/atoms/Select.svelte.d.ts +11 -0
- package/dist/components/atoms/Slider.svelte +136 -0
- package/dist/components/atoms/Slider.svelte.d.ts +14 -0
- package/dist/components/atoms/Switch.svelte +64 -0
- package/dist/components/atoms/Switch.svelte.d.ts +8 -0
- package/dist/components/atoms/Text.svelte +127 -0
- package/dist/components/atoms/Text.svelte.d.ts +16 -0
- package/dist/components/atoms/Textarea.svelte +62 -0
- package/dist/components/atoms/Textarea.svelte.d.ts +11 -0
- package/dist/components/layouts/AppShell.svelte +304 -0
- package/dist/components/layouts/AppShell.svelte.d.ts +21 -0
- package/dist/components/layouts/AutoGrid.svelte +36 -0
- package/dist/components/layouts/AutoGrid.svelte.d.ts +12 -0
- package/dist/components/layouts/Cluster.svelte +45 -0
- package/dist/components/layouts/Cluster.svelte.d.ts +14 -0
- package/dist/components/layouts/Container.svelte +40 -0
- package/dist/components/layouts/Container.svelte.d.ts +13 -0
- package/dist/components/layouts/NavItem.svelte +95 -0
- package/dist/components/layouts/NavItem.svelte.d.ts +14 -0
- package/dist/components/layouts/Stack.svelte +44 -0
- package/dist/components/layouts/Stack.svelte.d.ts +13 -0
- package/dist/components/molecules/Accordion.svelte +94 -0
- package/dist/components/molecules/Accordion.svelte.d.ts +16 -0
- package/dist/components/molecules/CodeBlock.svelte +119 -0
- package/dist/components/molecules/CodeBlock.svelte.d.ts +17 -0
- package/dist/components/molecules/CopyButton.svelte +80 -0
- package/dist/components/molecules/CopyButton.svelte.d.ts +13 -0
- package/dist/components/molecules/Dropzone.svelte +140 -0
- package/dist/components/molecules/Dropzone.svelte.d.ts +13 -0
- package/dist/components/molecules/Field.svelte +57 -0
- package/dist/components/molecules/Field.svelte.d.ts +12 -0
- package/dist/components/molecules/FileButton.svelte +68 -0
- package/dist/components/molecules/FileButton.svelte.d.ts +14 -0
- package/dist/components/molecules/FontScalePicker.svelte +21 -0
- package/dist/components/molecules/FontScalePicker.svelte.d.ts +6 -0
- package/dist/components/molecules/IconButton.svelte +36 -0
- package/dist/components/molecules/IconButton.svelte.d.ts +13 -0
- package/dist/components/molecules/Menu.svelte +120 -0
- package/dist/components/molecules/Menu.svelte.d.ts +17 -0
- package/dist/components/molecules/Modal.svelte +263 -0
- package/dist/components/molecules/Modal.svelte.d.ts +13 -0
- package/dist/components/molecules/OptionButton.svelte +76 -0
- package/dist/components/molecules/OptionButton.svelte.d.ts +10 -0
- package/dist/components/molecules/Popover.svelte +125 -0
- package/dist/components/molecules/Popover.svelte.d.ts +18 -0
- package/dist/components/molecules/RadioGroup.svelte +110 -0
- package/dist/components/molecules/RadioGroup.svelte.d.ts +16 -0
- package/dist/components/molecules/SelectButton.svelte +52 -0
- package/dist/components/molecules/SelectButton.svelte.d.ts +15 -0
- package/dist/components/molecules/Tabs.svelte +119 -0
- package/dist/components/molecules/Tabs.svelte.d.ts +15 -0
- package/dist/components/molecules/ThemePicker.svelte +22 -0
- package/dist/components/molecules/ThemePicker.svelte.d.ts +6 -0
- package/dist/components/molecules/Toaster.svelte +73 -0
- package/dist/components/molecules/Toaster.svelte.d.ts +18 -0
- package/dist/components/molecules/Toggle.svelte +68 -0
- package/dist/components/molecules/Toggle.svelte.d.ts +11 -0
- package/dist/components/molecules/Tooltip.svelte +106 -0
- package/dist/components/molecules/Tooltip.svelte.d.ts +10 -0
- package/dist/components/organisms/DataTable.svelte +145 -0
- package/dist/components/organisms/DataTable.svelte.d.ts +43 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +4 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +56 -0
- package/dist/stores/fontscale.svelte.d.ts +15 -0
- package/dist/stores/fontscale.svelte.js +49 -0
- package/dist/stores/theme.svelte.d.ts +96 -0
- package/dist/stores/theme.svelte.js +71 -0
- package/dist/stores/toast.svelte.d.ts +19 -0
- package/dist/stores/toast.svelte.js +26 -0
- package/dist/styles/app.css +522 -0
- package/dist/styles/variables.css +651 -0
- 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;
|