@dorsk/tsumikit 0.78.0 → 0.80.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 (90) hide show
  1. package/dist/components/atoms/Artwork.svelte +5 -3
  2. package/dist/components/atoms/Badge.svelte +2 -5
  3. package/dist/components/atoms/Button.svelte +327 -17
  4. package/dist/components/atoms/Button.svelte.d.ts +4 -1
  5. package/dist/components/atoms/Card.svelte +160 -12
  6. package/dist/components/atoms/Checkbox.svelte +181 -3
  7. package/dist/components/atoms/Gauge.svelte +2 -2
  8. package/dist/components/atoms/Input.svelte +41 -3
  9. package/dist/components/atoms/Kbd.svelte +1 -1
  10. package/dist/components/atoms/Link.svelte +61 -0
  11. package/dist/components/atoms/Progress.svelte +75 -2
  12. package/dist/components/atoms/Progress.svelte.d.ts +1 -0
  13. package/dist/components/atoms/SegmentedProgress.svelte +2 -2
  14. package/dist/components/atoms/Select.svelte +39 -3
  15. package/dist/components/atoms/Skeleton.svelte +54 -3
  16. package/dist/components/atoms/Skeleton.svelte.d.ts +1 -0
  17. package/dist/components/atoms/Slider.svelte +8 -6
  18. package/dist/components/atoms/Spinner.svelte +152 -3
  19. package/dist/components/atoms/Spinner.svelte.d.ts +1 -1
  20. package/dist/components/atoms/Swatch.svelte +1 -1
  21. package/dist/components/atoms/Switch.svelte +138 -2
  22. package/dist/components/atoms/Textarea.svelte +42 -3
  23. package/dist/components/atoms/Thinking.svelte +130 -0
  24. package/dist/components/atoms/Thinking.svelte.d.ts +15 -0
  25. package/dist/components/layouts/AppShell.svelte +6 -6
  26. package/dist/components/layouts/MasterDetail.svelte +3 -3
  27. package/dist/components/layouts/NavBar.svelte +4 -4
  28. package/dist/components/layouts/NavSection.svelte +1 -1
  29. package/dist/components/layouts/ResizablePanel.svelte +7 -7
  30. package/dist/components/layouts/TabBar.svelte +2 -2
  31. package/dist/components/molecules/Accordion.svelte +2 -2
  32. package/dist/components/molecules/AttachmentList.svelte +2 -2
  33. package/dist/components/molecules/CapBar.svelte +2 -2
  34. package/dist/components/molecules/CodeBlock.svelte +5 -5
  35. package/dist/components/molecules/Combobox.svelte +3 -3
  36. package/dist/components/molecules/CopyButton.svelte +146 -5
  37. package/dist/components/molecules/Drawer.svelte +8 -8
  38. package/dist/components/molecules/EmojiPicker.svelte +1 -1
  39. package/dist/components/molecules/Field.svelte +55 -1
  40. package/dist/components/molecules/FileButton.svelte +20 -6
  41. package/dist/components/molecules/FilterInput.svelte +4 -4
  42. package/dist/components/molecules/GitRef.svelte +2 -2
  43. package/dist/components/molecules/GlyphStack.svelte +21 -3
  44. package/dist/components/molecules/InputGroup.svelte +3 -3
  45. package/dist/components/molecules/LookPicker.svelte +87 -0
  46. package/dist/components/molecules/LookPicker.svelte.d.ts +17 -0
  47. package/dist/components/molecules/Modal.svelte +5 -5
  48. package/dist/components/molecules/MotionSettings.svelte +166 -0
  49. package/dist/components/molecules/MotionSettings.svelte.d.ts +18 -0
  50. package/dist/components/molecules/OptionButton.svelte +8 -3
  51. package/dist/components/molecules/Pagination.svelte +3 -1
  52. package/dist/components/molecules/Popover.svelte +18 -11
  53. package/dist/components/molecules/Prose.svelte +6 -6
  54. package/dist/components/molecules/RadialMenu.svelte +3 -3
  55. package/dist/components/molecules/RadioGroup.svelte +67 -4
  56. package/dist/components/molecules/RichSelect.svelte +6 -4
  57. package/dist/components/molecules/SectionHeader.svelte +1 -1
  58. package/dist/components/molecules/SeekSlider.svelte +6 -6
  59. package/dist/components/molecules/SegmentedControl.svelte +7 -7
  60. package/dist/components/molecules/SortableList.svelte +2 -2
  61. package/dist/components/molecules/SplitButton.svelte +1 -1
  62. package/dist/components/molecules/Tabs.svelte +160 -7
  63. package/dist/components/molecules/ThemePicker.svelte +1 -1
  64. package/dist/components/molecules/Timestamp.svelte +1 -1
  65. package/dist/components/molecules/Toggle.svelte +5 -3
  66. package/dist/components/molecules/Tooltip.svelte +2 -2
  67. package/dist/components/molecules/VolumeSlider.svelte +3 -3
  68. package/dist/components/organisms/ConversationFeed.svelte +1 -1
  69. package/dist/components/organisms/ConversationPanel.svelte +3 -3
  70. package/dist/components/organisms/DataTable.svelte +18 -16
  71. package/dist/fx.d.ts +149 -0
  72. package/dist/fx.js +111 -0
  73. package/dist/fx.md +58 -0
  74. package/dist/index.d.ts +8 -2
  75. package/dist/index.js +8 -2
  76. package/dist/looks/README.md +101 -0
  77. package/dist/looks/depth.css +939 -0
  78. package/dist/looks/editorial.css +545 -0
  79. package/dist/looks/tactile.css +871 -0
  80. package/dist/pointer.d.ts +3 -0
  81. package/dist/pointer.js +40 -0
  82. package/dist/stores/fx.svelte.d.ts +17 -0
  83. package/dist/stores/fx.svelte.js +64 -0
  84. package/dist/stores/theme.svelte.d.ts +8 -2
  85. package/dist/stores/theme.svelte.js +33 -7
  86. package/dist/styles/themes.css +249 -249
  87. package/dist/styles/tokens.css +37 -2
  88. package/dist/theme-mode.d.ts +31 -0
  89. package/dist/theme-mode.js +19 -0
  90. package/package.json +3 -2
package/dist/fx.js ADDED
@@ -0,0 +1,111 @@
1
+ // `data-fx-<family>="<variant>"` on <html> or any ancestor picks a family's
2
+ // effect; without the attribute the first variant applies.
3
+ export const FX = {
4
+ hover: {
5
+ label: 'Button hover',
6
+ variants: [
7
+ { id: 'smoke', label: 'Smoke' },
8
+ { id: 'ember', label: 'Ember' },
9
+ { id: 'aurora', label: 'Aurora' },
10
+ { id: 'none', label: 'Off' },
11
+ ],
12
+ },
13
+ check: {
14
+ label: 'Checkbox',
15
+ variants: [
16
+ { id: 'draw', label: 'Draw' },
17
+ { id: 'pop', label: 'Pop' },
18
+ { id: 'confetti', label: 'Confetti' },
19
+ { id: 'morph', label: 'Morph' },
20
+ { id: 'none', label: 'Off' },
21
+ ],
22
+ },
23
+ radio: {
24
+ label: 'Radio',
25
+ variants: [
26
+ { id: 'spring', label: 'Spring' },
27
+ { id: 'liquid', label: 'Liquid' },
28
+ { id: 'none', label: 'Off' },
29
+ ],
30
+ },
31
+ switch: {
32
+ label: 'Switch',
33
+ variants: [
34
+ { id: 'squash', label: 'Squash' },
35
+ { id: 'glow', label: 'Glow' },
36
+ { id: 'calm', label: 'Calm' },
37
+ { id: 'none', label: 'Off' },
38
+ ],
39
+ },
40
+ focus: {
41
+ label: 'Input focus',
42
+ variants: [
43
+ { id: 'bloom', label: 'Bloom' },
44
+ { id: 'label', label: 'Spring label' },
45
+ { id: 'none', label: 'Off' },
46
+ ],
47
+ },
48
+ tabs: {
49
+ label: 'Tabs',
50
+ variants: [
51
+ { id: 'pill', label: 'Pill' },
52
+ { id: 'stretch', label: 'Stretch' },
53
+ { id: 'ink', label: 'Ink' },
54
+ { id: 'none', label: 'Off' },
55
+ ],
56
+ },
57
+ copy: {
58
+ label: 'Copy',
59
+ variants: [
60
+ { id: 'burst', label: 'Burst' },
61
+ { id: 'tick', label: 'Tick' },
62
+ { id: 'none', label: 'Off' },
63
+ ],
64
+ },
65
+ link: {
66
+ label: 'Link hover',
67
+ variants: [
68
+ { id: 'sweep', label: 'Sweep' },
69
+ { id: 'highlight', label: 'Highlight' },
70
+ { id: 'none', label: 'Off' },
71
+ ],
72
+ },
73
+ card: {
74
+ label: 'Card hover',
75
+ variants: [
76
+ { id: 'spotlight', label: 'Spotlight' },
77
+ { id: 'orbit', label: 'Orbit' },
78
+ { id: 'none', label: 'Off' },
79
+ ],
80
+ },
81
+ };
82
+ export const FX_FAMILIES = Object.keys(FX);
83
+ export const DEFAULT_FX = Object.fromEntries(FX_FAMILIES.map((f) => [f, FX[f].variants[0].id]));
84
+ export function fxAttr(family) {
85
+ return `data-fx-${family}`;
86
+ }
87
+ export function isFxVariant(family, v) {
88
+ return FX[family].variants.some((x) => x.id === v);
89
+ }
90
+ /** Settings from an untrusted blob: unknown families are dropped, unknown
91
+ * variants fall back to `defaults`. */
92
+ export function parseFx(raw, defaults = DEFAULT_FX) {
93
+ const blob = raw && typeof raw === 'object' ? raw : {};
94
+ const out = { ...defaults };
95
+ for (const f of FX_FAMILIES)
96
+ if (isFxVariant(f, blob[f]))
97
+ out[f] = blob[f];
98
+ return out;
99
+ }
100
+ export function fxDiff(settings) {
101
+ const out = {};
102
+ for (const f of FX_FAMILIES)
103
+ if (settings[f] !== DEFAULT_FX[f])
104
+ out[f] = settings[f];
105
+ return out;
106
+ }
107
+ /** Inline <head> script painting the persisted `data-fx-*` before first paint. */
108
+ export function fxPrepaint(key) {
109
+ const valid = Object.fromEntries(FX_FAMILIES.map((f) => [f, FX[f].variants.slice(1).map((v) => v.id)]));
110
+ return `(function(){try{var s=localStorage.getItem(${JSON.stringify(key)});if(!s)return;var b=JSON.parse(s),v=${JSON.stringify(valid)},d=document.documentElement;if(!b||typeof b!=='object')return;for(var f in v)if(v[f].indexOf(b[f])>=0)d.setAttribute('data-fx-'+f,b[f])}catch(e){}})();`;
111
+ }
package/dist/fx.md ADDED
@@ -0,0 +1,58 @@
1
+ # Micro-interactions
2
+
3
+ Each component family has a set of effects. One attribute on `<html>`, or on any ancestor, picks the
4
+ effect for that family:
5
+
6
+ ```html
7
+ <html data-fx-hover="ember" data-fx-check="pop"></html>
8
+ ```
9
+
10
+ With no attribute the first variant applies. `none` turns the family's effect off.
11
+
12
+ | Family | Attribute | Variants (first = default) |
13
+ | -------- | ---------------- | --------------------------------------- |
14
+ | `hover` | `data-fx-hover` | `smoke` `ember` `aurora` `none` |
15
+ | `check` | `data-fx-check` | `draw` `pop` `confetti` `morph` `none` |
16
+ | `radio` | `data-fx-radio` | `spring` `liquid` `none` |
17
+ | `switch` | `data-fx-switch` | `squash` `glow` `calm` `none` |
18
+ | `focus` | `data-fx-focus` | `bloom` `label` `none` |
19
+ | `tabs` | `data-fx-tabs` | `pill` `stretch` `ink` `none` |
20
+ | `copy` | `data-fx-copy` | `burst` `tick` `none` |
21
+ | `link` | `data-fx-link` | `sweep` `highlight` `none` |
22
+ | `card` | `data-fx-card` | `spotlight` `orbit` `none` |
23
+
24
+ Loaders aren't a page setting. They're component props: Spinner, Progress, Skeleton and Button
25
+ `loadingStyle`.
26
+
27
+ All effects take their colours from the theme, fall back to a static state change under
28
+ `prefers-reduced-motion: reduce`, only animate hover on fine pointers, and never shift layout.
29
+
30
+ ## Persisted settings
31
+
32
+ ```svelte
33
+ <script>
34
+ import { FX_PREPAINT, MotionSettings, fx } from '@dorsk/tsumikit';
35
+ </script>
36
+
37
+ <svelte:head>{@html `<script>${FX_PREPAINT}</` + `script>`}</svelte:head>
38
+
39
+ <MotionSettings size="sm" />
40
+ ```
41
+
42
+ - `fx` holds the settings (`fx.settings`, `fx.get(family)`, `fx.set(family, variant)`, `fx.reset()`), stores
43
+ the non-default families in localStorage under `tsumikit-fx` and writes `data-fx-*` on `<html>`. Defaults
44
+ leave the attribute absent.
45
+ - `FX_PREPAINT` paints the saved attributes before first paint. Inline it in `<head>` next to
46
+ `THEME_PREPAINT`.
47
+ - `fx.onchange` and `fx.hydrate(settings)` let an app keep the settings in its own (server-side) store.
48
+ - `FX`, `FX_FAMILIES`, `DEFAULT_FX` and `parseFx` expose the registry for custom controls.
49
+
50
+ ## Writing an effect
51
+
52
+ Scope every variant with `:where()` so the selector keeps zero specificity. Give the first variant the
53
+ `:not([…])` default:
54
+
55
+ ```css
56
+ :where(:root:not([data-fx-hover]), [data-fx-hover='smoke']) .btn-primary::before { … }
57
+ :where([data-fx-hover='ember']) .btn-primary::before { … }
58
+ ```
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export { default as Swatch } from './components/atoms/Swatch.svelte';
31
31
  export { default as Switch } from './components/atoms/Switch.svelte';
32
32
  export { default as Text } from './components/atoms/Text.svelte';
33
33
  export { default as Textarea } from './components/atoms/Textarea.svelte';
34
+ export { default as Thinking } from './components/atoms/Thinking.svelte';
34
35
  export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
35
36
  export { default as AppShell } from './components/layouts/AppShell.svelte';
36
37
  export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
@@ -74,10 +75,12 @@ export { default as IconButton } from './components/molecules/IconButton.svelte'
74
75
  export { default as InputGroup } from './components/molecules/InputGroup.svelte';
75
76
  export { default as KeyValue, type KeyValueRow, type KeyValueTone, } from './components/molecules/KeyValue.svelte';
76
77
  export { default as LoadMore } from './components/molecules/LoadMore.svelte';
78
+ export { default as LookPicker } from './components/molecules/LookPicker.svelte';
77
79
  export { default as Menu, type MenuItem } from './components/molecules/Menu.svelte';
78
80
  export { default as Meter } from './components/molecules/Meter.svelte';
79
81
  export { default as Metric, default as StatTile, type MetricSegment, } from './components/molecules/Metric.svelte';
80
82
  export { default as Modal } from './components/molecules/Modal.svelte';
83
+ export { default as MotionSettings } from './components/molecules/MotionSettings.svelte';
81
84
  export { default as OptionButton } from './components/molecules/OptionButton.svelte';
82
85
  export { default as Pagination } from './components/molecules/Pagination.svelte';
83
86
  export { default as Popover } from './components/molecules/Popover.svelte';
@@ -111,18 +114,21 @@ export { default as FilterSearchBar } from './components/organisms/FilterSearchB
111
114
  export { formatCount, hasCount } from './count';
112
115
  export { EMOJI_GROUPS, type EmojiEntry, type EmojiGroup, searchEmoji } from './emoji';
113
116
  export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
117
+ export { DEFAULT_FX, FX, FX_FAMILIES, type FxFamily, type FxSettings, type FxVariant, fxAttr, fxDiff, fxPrepaint, isFxVariant, parseFx, } from './fx';
114
118
  export { escapeHtml, type Highlight, type RenderMarkdownOptions, renderMarkdown, stripAnsi, } from './markdown';
115
119
  export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
120
+ export { trackPointer } from './pointer';
116
121
  export * as filterQuery from './query';
117
122
  export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, singleQuery, suggest, suggestSingle, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
118
123
  export { type OptionSection, sectionOptions } from './select-options';
119
124
  export type { ControlSize } from './size';
120
125
  export { playSound, type SoundMuted, type SoundName } from './sound';
121
126
  export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
127
+ export { FX_PREPAINT, fx } from './stores/fx.svelte';
122
128
  export { sound } from './stores/sound.svelte';
123
- export { type Mode, THEMES, theme } from './stores/theme.svelte';
129
+ export { type Mode, THEME_PREPAINT, THEMES, theme } from './stores/theme.svelte';
124
130
  export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type ToastToneInput, toasts, } from './stores/toast.svelte';
125
- export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, } from './theme-mode';
131
+ export { AUTO_THEME, chooseTheme, DEFAULT_LOOK, DEFAULT_THEME_PREFERENCE, LOOKS, type LookId, type LookStatus, parseLook, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, themePrepaint, } from './theme-mode';
126
132
  export { formatTimestamp, localTimeZone, relativeTime, type TimeInput, type TimestampMode, } from './timestamp';
127
133
  export { canonicalTone, type Tone } from './tone';
128
134
  export { pathCandidates, type TruncateMode, type TruncateOptions, truncate } from './truncate';
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ export { default as Switch } from './components/atoms/Switch.svelte';
34
34
  // ---- atoms ----
35
35
  export { default as Text } from './components/atoms/Text.svelte';
36
36
  export { default as Textarea } from './components/atoms/Textarea.svelte';
37
+ export { default as Thinking } from './components/atoms/Thinking.svelte';
37
38
  export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
38
39
  // ---- layouts ----
39
40
  export { default as AppShell } from './components/layouts/AppShell.svelte';
@@ -79,12 +80,14 @@ export { default as IconButton } from './components/molecules/IconButton.svelte'
79
80
  export { default as InputGroup } from './components/molecules/InputGroup.svelte';
80
81
  export { default as KeyValue, } from './components/molecules/KeyValue.svelte';
81
82
  export { default as LoadMore } from './components/molecules/LoadMore.svelte';
83
+ export { default as LookPicker } from './components/molecules/LookPicker.svelte';
82
84
  export { default as Menu } from './components/molecules/Menu.svelte';
83
85
  export { default as Meter } from './components/molecules/Meter.svelte';
84
86
  export { default as Metric,
85
87
  // StatTile is an alias for Metric — same component, dashboard-friendly name.
86
88
  default as StatTile, } from './components/molecules/Metric.svelte';
87
89
  export { default as Modal } from './components/molecules/Modal.svelte';
90
+ export { default as MotionSettings } from './components/molecules/MotionSettings.svelte';
88
91
  export { default as OptionButton } from './components/molecules/OptionButton.svelte';
89
92
  export { default as Pagination } from './components/molecules/Pagination.svelte';
90
93
  export { default as Popover } from './components/molecules/Popover.svelte';
@@ -119,19 +122,22 @@ export { default as FilterSearchBar } from './components/organisms/FilterSearchB
119
122
  export { formatCount, hasCount } from './count';
120
123
  export { EMOJI_GROUPS, searchEmoji } from './emoji';
121
124
  export { FIELD_KEY, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
125
+ export { DEFAULT_FX, FX, FX_FAMILIES, fxAttr, fxDiff, fxPrepaint, isFxVariant, parseFx, } from './fx';
122
126
  export { escapeHtml, renderMarkdown, stripAnsi, } from './markdown';
123
127
  export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
128
+ export { trackPointer } from './pointer';
124
129
  // ---- query core (headless: schema / parser / AST / suggest / compilers) ----
125
130
  export * as filterQuery from './query';
126
131
  export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
127
132
  export { sectionOptions } from './select-options';
128
133
  export { playSound } from './sound';
129
134
  export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
135
+ export { FX_PREPAINT, fx } from './stores/fx.svelte';
130
136
  // ---- stores / actions ----
131
137
  export { sound } from './stores/sound.svelte';
132
- export { THEMES, theme } from './stores/theme.svelte';
138
+ export { THEME_PREPAINT, THEMES, theme } from './stores/theme.svelte';
133
139
  export { toasts, } from './stores/toast.svelte';
134
- export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, } from './theme-mode';
140
+ export { AUTO_THEME, chooseTheme, DEFAULT_LOOK, DEFAULT_THEME_PREFERENCE, LOOKS, parseLook, pickerValue, preferenceFrom, resolveTheme, themePrepaint, } from './theme-mode';
135
141
  export { formatTimestamp, localTimeZone, relativeTime, } from './timestamp';
136
142
  export { canonicalTone } from './tone';
137
143
  export { pathCandidates, truncate } from './truncate';
@@ -0,0 +1,101 @@
1
+ # Looks
2
+
3
+ A look is the kit's structural style: surfaces, edges, elevation, control fills and
4
+ interactions. It is its own axis, independent of the theme (the colors) and of the
5
+ light/dark mode. Any look works with any of the 30 built-in themes and with your own.
6
+
7
+ | id | status | character |
8
+ | ----------- | ------------ | ------------------------------------------------------ |
9
+ | `default` | stable | polished: the kit as shipped, no stylesheet needed |
10
+ | `depth` | stable | layered elevation, inner highlights, accent glow |
11
+ | `tactile` | stable | soft raised and pressed surfaces |
12
+ | `editorial` | experimental | bold type scale, rules, solid tones; may change |
13
+
14
+ ## Use
15
+
16
+ Import the looks you offer, next to the kit's stylesheet. A look costs nothing until it is
17
+ active: every rule is scoped to `:root[data-look="<id>"]`.
18
+
19
+ ```ts
20
+ import '@dorsk/tsumikit/styles/app.css';
21
+ import '@dorsk/tsumikit/looks/depth.css';
22
+ import '@dorsk/tsumikit/looks/tactile.css';
23
+ ```
24
+
25
+ The theme store owns the look and persists it with the theme preference
26
+ (`localStorage['tsumikit-theme']`, a `look` key that is absent for the default look):
27
+
28
+ ```svelte
29
+ <script lang="ts">
30
+ import { LookPicker, ThemePicker, theme } from '@dorsk/tsumikit';
31
+ </script>
32
+
33
+ <LookPicker />
34
+ <ThemePicker auto />
35
+ <p>{theme.look}</p>
36
+ <button onclick={() => theme.setLook('depth')}>Depth</button>
37
+ ```
38
+
39
+ `LOOKS` lists `{ id, label, status }`. `<LookPicker experimental={false} />` hides
40
+ experimental looks; `looks={['default', 'depth']}` restricts and orders them.
41
+
42
+ The store paints `data-theme`, `data-mode="light|dark"` and `data-look` (absent for the
43
+ default look) on `<html>`. Inline `THEME_PREPAINT` in `<head>` to paint all three before
44
+ first paint:
45
+
46
+ ```svelte
47
+ <script lang="ts">
48
+ import { THEME_PREPAINT } from '@dorsk/tsumikit';
49
+ </script>
50
+
51
+ <svelte:head>
52
+ {@html `<script>${THEME_PREPAINT}</` + `script>`}
53
+ </svelte:head>
54
+ ```
55
+
56
+ If you register your own themes, build the script with
57
+ `themePrepaint('tsumikit-theme', [...THEMES, ...yours])` so legacy single-id values resolve.
58
+
59
+ Apps that persist settings server-side get `look` inside the preference passed to
60
+ `theme.onchange`, and can replay it with `theme.hydrate(pref)`. A preference without
61
+ `look` keeps the current look.
62
+
63
+ ### Pointer effects
64
+
65
+ Some looks light the surface under the mouse. Call `trackPointer()` once, client side; it
66
+ sets `--pointer-x` / `--pointer-y` on the hovered `[data-tsu]` element and its `[data-tsu]`
67
+ ancestors and returns a disposer. Looks degrade gracefully without it.
68
+
69
+ ```ts
70
+ $effect(() => trackPointer());
71
+ ```
72
+
73
+ ## Writing a look
74
+
75
+ `src/lib/looks/<id>.css`, plus an entry in `LOOKS` (`src/lib/theme-mode.ts`).
76
+ `tests/looks-lint.test.js` enforces the first three rules.
77
+
78
+ 1. **Scope everything** under `:root[data-look="<id>"]`. Light-only tweaks go under
79
+ `:root[data-look="<id>"][data-mode="light"]`; prefer recipes that work in both modes.
80
+ `@keyframes` are global: namespace their names.
81
+ 2. **No literal colors.** No hex, no `rgb()`/`hsl()`/`oklch()`/`lab()` with channel
82
+ numbers, no named colors (`black` and `white` included) except `transparent` and
83
+ `currentColor`. Derive every color from the active theme:
84
+ `var(--accent)`, `color-mix(in oklab, var(--accent) 30%, transparent)`,
85
+ `oklch(from var(--accent) l c h / 0.4)`; shadows from
86
+ `oklch(from var(--bg) calc(l * 0.4) c h / 0.6)` or
87
+ `color-mix(in oklab, var(--text) 18%, transparent)`.
88
+ 3. **No `transition: all`.** Durations come from `--dur-*`.
89
+ 4. **Tokens first.** Override the structural tokens in `styles/tokens.css` ("Structural
90
+ look": surface, raised, overlay, control, track, thumb and button fills, edges and
91
+ shadows, `--edge-width`, `--hover-lift`, `--focus-glow`). Add per-component rules
92
+ (`:root[data-look="<id>"] [data-tsu="Card"]`) only where a token cannot express it.
93
+ 5. **Motion:** hover-only effects inside `@media (hover: hover) and (pointer: fine)`;
94
+ respect `prefers-reduced-motion`.
95
+ 6. **Contrast**, for every theme in both modes: text ≥ 4.5:1, UI boundaries and focus
96
+ rings ≥ 3:1. A look must not lower text contrast; it should rarely touch text colors.
97
+
98
+ Palette tokens available to a look: `--bg`, `--bg-elevated`, `--bg-elevated-2`,
99
+ `--surface`, `--border`, `--border-strong`, `--text`, `--text-muted`, `--text-faint`,
100
+ `--accent`, `--text-on-accent`, `--ok`, `--warn`, `--danger`, `--info` and the `--c-*`
101
+ palette.