@lyeve-labs/ui-kit 0.24.0 → 0.25.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 (60) hide show
  1. package/README.md +42 -0
  2. package/dist/components/AccordionItem.svelte +3 -5
  3. package/dist/components/AccountMenu.svelte +4 -3
  4. package/dist/components/Alert.svelte +2 -1
  5. package/dist/components/AppShell.svelte +166 -54
  6. package/dist/components/AppShell.svelte.d.ts +41 -6
  7. package/dist/components/Autocomplete.svelte +3 -2
  8. package/dist/components/Badge.svelte +10 -5
  9. package/dist/components/Banner.svelte +2 -1
  10. package/dist/components/Breadcrumb.svelte +7 -2
  11. package/dist/components/Button.svelte +15 -2
  12. package/dist/components/Card.svelte +1 -1
  13. package/dist/components/Collapsible.svelte +5 -4
  14. package/dist/components/CopyButton.svelte +2 -1
  15. package/dist/components/DatePicker.svelte +77 -62
  16. package/dist/components/Drawer.svelte +6 -5
  17. package/dist/components/Dropdown.svelte +5 -2
  18. package/dist/components/FileInput.svelte +1 -1
  19. package/dist/components/Modal.svelte +6 -3
  20. package/dist/components/MultiSelect.svelte +8 -6
  21. package/dist/components/NumberInput.svelte +2 -2
  22. package/dist/components/PageHeader.svelte +13 -1
  23. package/dist/components/PageShell.svelte +37 -19
  24. package/dist/components/PageShell.svelte.d.ts +12 -0
  25. package/dist/components/Pagination.svelte +7 -2
  26. package/dist/components/PasswordInput.svelte +2 -1
  27. package/dist/components/Progress.svelte +1 -1
  28. package/dist/components/SearchInput.svelte +3 -2
  29. package/dist/components/SectionHeading.svelte +10 -1
  30. package/dist/components/SegmentedControl.svelte +101 -27
  31. package/dist/components/SegmentedControl.svelte.d.ts +9 -0
  32. package/dist/components/Select.svelte +4 -3
  33. package/dist/components/SidebarNav.svelte +26 -17
  34. package/dist/components/StepIndicator.svelte +7 -9
  35. package/dist/components/Table.svelte +39 -29
  36. package/dist/components/Tabs.svelte +128 -24
  37. package/dist/components/Tag.svelte +2 -1
  38. package/dist/components/ThemeToggle.svelte +2 -1
  39. package/dist/components/Toaster.svelte +6 -2
  40. package/dist/components/Toggle.svelte +3 -2
  41. package/dist/components/Tooltip.svelte +10 -5
  42. package/dist/components/TreeView.svelte +2 -2
  43. package/dist/components/dialog/Dialog.svelte +15 -32
  44. package/dist/index.d.ts +4 -1
  45. package/dist/index.js +7 -1
  46. package/dist/internal/choice.js +10 -3
  47. package/dist/internal/field.js +3 -3
  48. package/dist/internal/layout.d.ts +10 -1
  49. package/dist/internal/layout.js +10 -1
  50. package/dist/internal/panel.d.ts +13 -3
  51. package/dist/internal/panel.js +16 -5
  52. package/dist/internal/scroll-edges.d.ts +33 -0
  53. package/dist/internal/scroll-edges.js +33 -0
  54. package/dist/internal/touch.d.ts +28 -0
  55. package/dist/internal/touch.js +28 -0
  56. package/dist/motion.d.ts +63 -0
  57. package/dist/motion.js +165 -0
  58. package/dist/styles/theme.css +131 -63
  59. package/package.json +1 -1
  60. package/src/lib/styles/theme.css +131 -63
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Which edges of a horizontal scroll box have content behind them.
3
+ *
4
+ * Table and Tabs both fade the edge the content continues past, because a
5
+ * phone paints its scrollbar only while a finger is moving and the column or
6
+ * the tab past the edge is otherwise unannounced. The arithmetic is the same
7
+ * in both and lives here so a third scroller cannot get it subtly different.
8
+ *
9
+ * Not exported from the package entry point - this is an implementation detail.
10
+ */
11
+ export interface ScrollEdges {
12
+ before: boolean;
13
+ after: boolean;
14
+ }
15
+ /** The three metrics the answer is read from; an element has all of them. */
16
+ export interface ScrollMetrics {
17
+ scrollWidth: number;
18
+ clientWidth: number;
19
+ scrollLeft: number;
20
+ }
21
+ export declare function scrollEdges(el: ScrollMetrics): ScrollEdges;
22
+ /**
23
+ * The fade itself: a dark gradient on the edge, outside the scroll box so it
24
+ * does not scroll away with the content it describes. Dark rather than a
25
+ * palette colour, because the box sits on the page in one place and inside
26
+ * a card in another and no surface token is the colour to fade to; the
27
+ * scrims use black at an alpha for the same reason and it reads in both
28
+ * themes. The direction is logical, so a right-to-left page fades the other
29
+ * way.
30
+ */
31
+ export declare const SCROLL_EDGE = "pointer-events-none absolute inset-y-px w-6";
32
+ export declare const SCROLL_EDGE_START = "start-0 bg-linear-to-r rtl:bg-linear-to-l from-black/25 to-transparent";
33
+ export declare const SCROLL_EDGE_END = "end-0 bg-linear-to-l rtl:bg-linear-to-r from-black/25 to-transparent";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Which edges of a horizontal scroll box have content behind them.
3
+ *
4
+ * Table and Tabs both fade the edge the content continues past, because a
5
+ * phone paints its scrollbar only while a finger is moving and the column or
6
+ * the tab past the edge is otherwise unannounced. The arithmetic is the same
7
+ * in both and lives here so a third scroller cannot get it subtly different.
8
+ *
9
+ * Not exported from the package entry point - this is an implementation detail.
10
+ */
11
+ export function scrollEdges(el) {
12
+ const slack = el.scrollWidth - el.clientWidth;
13
+ // A sub-pixel layout leaves scrollLeft a fraction short of its own maximum
14
+ // at the far end, so an exact comparison never reports the end as reached.
15
+ if (slack <= 1)
16
+ return { before: false, after: false };
17
+ // A right-to-left box reports the offset as a negative number, and the two
18
+ // edges are the same two edges either way round.
19
+ const travelled = Math.abs(el.scrollLeft);
20
+ return { before: travelled > 1, after: travelled < slack - 1 };
21
+ }
22
+ /**
23
+ * The fade itself: a dark gradient on the edge, outside the scroll box so it
24
+ * does not scroll away with the content it describes. Dark rather than a
25
+ * palette colour, because the box sits on the page in one place and inside
26
+ * a card in another and no surface token is the colour to fade to; the
27
+ * scrims use black at an alpha for the same reason and it reads in both
28
+ * themes. The direction is logical, so a right-to-left page fades the other
29
+ * way.
30
+ */
31
+ export const SCROLL_EDGE = 'pointer-events-none absolute inset-y-px w-6';
32
+ export const SCROLL_EDGE_START = 'start-0 bg-linear-to-r rtl:bg-linear-to-l from-black/25 to-transparent';
33
+ export const SCROLL_EDGE_END = 'end-0 bg-linear-to-l rtl:bg-linear-to-r from-black/25 to-transparent';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The two ways a control reaches 44px under a finger, named once so a
3
+ * component picks one by name rather than respelling the classes.
4
+ *
5
+ * Both compile to nothing under a mouse. `pointer: coarse` is the query, and
6
+ * the theme file is where it is declared: the `coarse:` variant, the 44px
7
+ * value of `--spacing-control` and the `hit-area` utility all live beside the
8
+ * palette so a consumer's build emits them.
9
+ *
10
+ * Not exported from the package entry point - this is an implementation detail.
11
+ */
12
+ /**
13
+ * The control grows. For a control whose visual may take the finger's size:
14
+ * a button, a tab, a sidebar row, a menu item. It reads the control token, so
15
+ * the button and the input beside it grow together and stay level.
16
+ */
17
+ export declare const TOUCH_GROW = "coarse:min-h-control coarse:min-w-control";
18
+ /**
19
+ * The control stays as drawn and its hit box grows. For a control that is
20
+ * small on purpose: a switch, a checkbox, a breadcrumb, a pager step, a close
21
+ * cross, an icon inside an input. The element must be positioned, so
22
+ * `relative` comes with it. An element that is `absolute` already takes
23
+ * HIT_AREA_POSITIONED instead: two position utilities on one element resolve
24
+ * by stylesheet order, not by the order they are written.
25
+ */
26
+ export declare const HIT_AREA = "relative hit-area";
27
+ /** The same hit box on an element that positions itself already. */
28
+ export declare const HIT_AREA_POSITIONED = "hit-area";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The two ways a control reaches 44px under a finger, named once so a
3
+ * component picks one by name rather than respelling the classes.
4
+ *
5
+ * Both compile to nothing under a mouse. `pointer: coarse` is the query, and
6
+ * the theme file is where it is declared: the `coarse:` variant, the 44px
7
+ * value of `--spacing-control` and the `hit-area` utility all live beside the
8
+ * palette so a consumer's build emits them.
9
+ *
10
+ * Not exported from the package entry point - this is an implementation detail.
11
+ */
12
+ /**
13
+ * The control grows. For a control whose visual may take the finger's size:
14
+ * a button, a tab, a sidebar row, a menu item. It reads the control token, so
15
+ * the button and the input beside it grow together and stay level.
16
+ */
17
+ export const TOUCH_GROW = 'coarse:min-h-control coarse:min-w-control';
18
+ /**
19
+ * The control stays as drawn and its hit box grows. For a control that is
20
+ * small on purpose: a switch, a checkbox, a breadcrumb, a pager step, a close
21
+ * cross, an icon inside an input. The element must be positioned, so
22
+ * `relative` comes with it. An element that is `absolute` already takes
23
+ * HIT_AREA_POSITIONED instead: two position utilities on one element resolve
24
+ * by stylesheet order, not by the order they are written.
25
+ */
26
+ export const HIT_AREA = 'relative hit-area';
27
+ /** The same hit box on an element that positions itself already. */
28
+ export const HIT_AREA_POSITIONED = 'hit-area';
@@ -0,0 +1,63 @@
1
+ import type { AnimationConfig } from 'svelte/animate';
2
+ import type { TransitionConfig } from 'svelte/transition';
3
+ export type Rung = 'fast' | 'base' | 'slow' | 'progress';
4
+ export type Curve = 'enter' | 'exit' | 'move';
5
+ /** What `theme.css` declares, for a document that has not loaded it. */
6
+ export declare const FALLBACK: {
7
+ readonly duration: {
8
+ fast: number;
9
+ base: number;
10
+ slow: number;
11
+ progress: number;
12
+ };
13
+ readonly ease: {
14
+ enter: number[];
15
+ exit: number[];
16
+ move: number[];
17
+ };
18
+ };
19
+ /**
20
+ * What a preset hands Svelte. The function form is resolved once the element
21
+ * is in the DOM, with the direction the animation is really playing in; under
22
+ * `transition:` the directive itself only ever says `both`, so this is the
23
+ * one way a single directive can tune its exit apart from its entrance.
24
+ */
25
+ export type Motion = TransitionConfig | ((opts?: {
26
+ direction: 'in' | 'out';
27
+ }) => TransitionConfig);
28
+ /** A `--duration-*` token in milliseconds, or its fallback. */
29
+ export declare function duration(rung: Rung): number;
30
+ /**
31
+ * A cubic bezier as the function Svelte samples, from its four control
32
+ * points. Solves x for t by Newton's method with a bisection fallback, the
33
+ * same shape WebKit's UnitBezier takes, so the curve a transition follows is
34
+ * the curve the stylesheet names.
35
+ */
36
+ export declare function bezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number;
37
+ /** An `--ease-*` token as a function, or its fallback. */
38
+ export declare function easing(curve: Curve): (x: number) => number;
39
+ /** True when the document asked for no motion, or cannot play any. */
40
+ export declare function still(node: Element): boolean;
41
+ /** A centred dialog: rises a little and settles to full size. */
42
+ export declare const dialog: (node: Element) => Motion;
43
+ /** The scrim behind a dialog or drawer, on the same clock as its surface. */
44
+ export declare const scrim: (node: Element) => Motion;
45
+ /**
46
+ * A panel docked to a viewport edge: the whole panel slides in from its own
47
+ * edge. No fade, since a surface arriving from off screen is never half there.
48
+ */
49
+ export declare function drawer(node: Element, params?: {
50
+ side?: 'left' | 'right';
51
+ }): Motion;
52
+ /** A menu, listbox, tooltip or date grid anchored to its trigger. */
53
+ export declare const popover: (node: Element) => Motion;
54
+ /** One toast arriving in the stack, and leaving it. */
55
+ export declare const toast: (node: Element) => Motion;
56
+ /**
57
+ * Siblings closing the gap a removed item leaves, for `animate:`. Svelte's
58
+ * flip measures the move; the kit supplies the clock.
59
+ */
60
+ export declare function reorder(node: Element, positions: {
61
+ from: DOMRect;
62
+ to: DOMRect;
63
+ }, _params?: unknown): AnimationConfig;
package/dist/motion.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The entrances and exits, as Svelte transitions that read the motion tokens.
3
+ *
4
+ * <div in:motion.dialog out:motion.dialog>
5
+ * <div transition:motion.popover>
6
+ * <li animate:motion.reorder>
7
+ *
8
+ * A CSS animation plays an entrance and nothing else: the element it ran on
9
+ * is gone the moment `{#if}` turns false, so a dialog that eased open snapped
10
+ * shut. A Svelte transition keeps the element until the exit has played, and
11
+ * the same function serves both directions, so an entrance and its exit are
12
+ * written once and cannot drift apart.
13
+ *
14
+ * Every preset takes its duration and curve from `theme.css` at the moment it
15
+ * runs, so a consumer that retunes a token retunes these too. The numbers
16
+ * below are the fallback for a document that has not loaded the stylesheet,
17
+ * and a test holds them equal to the tokens.
18
+ *
19
+ * Two cases return no duration at all. A reader who asked for reduced motion
20
+ * gets the state change and none of the travel; the reduced-motion block in
21
+ * theme.css cannot reach these, because Svelte drives them through the Web
22
+ * Animations API and not through a stylesheet. And a document with no
23
+ * `Element.animate`, which is every jsdom test, mounts and unmounts at once,
24
+ * exactly as it did before the exits existed.
25
+ *
26
+ * This is the one module that may import `svelte/animate`, `svelte/easing`
27
+ * or `svelte/transition`. A component that reaches for them directly has
28
+ * chosen its own numbers, which is the thing the tokens exist to end.
29
+ */
30
+ import { flip } from 'svelte/animate';
31
+ /** What `theme.css` declares, for a document that has not loaded it. */
32
+ export const FALLBACK = {
33
+ duration: { fast: 120, base: 200, slow: 320, progress: 500 },
34
+ ease: {
35
+ enter: [0.22, 1, 0.36, 1],
36
+ exit: [0.32, 0, 0.67, 0],
37
+ move: [0.65, 0, 0.35, 1],
38
+ },
39
+ };
40
+ /** How the two directions of one surface relate: one rung faster on the way out. */
41
+ const EXIT_RUNG = {
42
+ fast: 'fast',
43
+ base: 'fast',
44
+ slow: 'base',
45
+ progress: 'base',
46
+ };
47
+ function token(name) {
48
+ if (typeof document === 'undefined')
49
+ return '';
50
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
51
+ }
52
+ /** A `--duration-*` token in milliseconds, or its fallback. */
53
+ export function duration(rung) {
54
+ const m = /^(\d*\.?\d+)(ms|s)$/.exec(token(`--duration-${rung}`));
55
+ if (!m)
56
+ return FALLBACK.duration[rung];
57
+ return m[2] === 's' ? Number(m[1]) * 1000 : Number(m[1]);
58
+ }
59
+ /**
60
+ * A cubic bezier as the function Svelte samples, from its four control
61
+ * points. Solves x for t by Newton's method with a bisection fallback, the
62
+ * same shape WebKit's UnitBezier takes, so the curve a transition follows is
63
+ * the curve the stylesheet names.
64
+ */
65
+ export function bezier(x1, y1, x2, y2) {
66
+ const cx = 3 * x1;
67
+ const bx = 3 * (x2 - x1) - cx;
68
+ const ax = 1 - cx - bx;
69
+ const cy = 3 * y1;
70
+ const by = 3 * (y2 - y1) - cy;
71
+ const ay = 1 - cy - by;
72
+ const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
73
+ const sampleY = (t) => ((ay * t + by) * t + cy) * t;
74
+ const slopeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
75
+ const solve = (x) => {
76
+ let t = x;
77
+ for (let i = 0; i < 8; i += 1) {
78
+ const dx = sampleX(t) - x;
79
+ if (Math.abs(dx) < 1e-6)
80
+ return t;
81
+ const slope = slopeX(t);
82
+ if (Math.abs(slope) < 1e-6)
83
+ break;
84
+ t -= dx / slope;
85
+ }
86
+ let lo = 0;
87
+ let hi = 1;
88
+ t = x;
89
+ while (lo < hi) {
90
+ const s = sampleX(t);
91
+ if (Math.abs(s - x) < 1e-6)
92
+ return t;
93
+ if (x > s)
94
+ lo = t;
95
+ else
96
+ hi = t;
97
+ t = (lo + hi) / 2;
98
+ }
99
+ return t;
100
+ };
101
+ return (x) => (x <= 0 ? 0 : x >= 1 ? 1 : sampleY(solve(x)));
102
+ }
103
+ const curves = new Map();
104
+ /** An `--ease-*` token as a function, or its fallback. */
105
+ export function easing(curve) {
106
+ const raw = token(`--ease-${curve}`);
107
+ const m = /^cubic-bezier\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)\)$/.exec(raw);
108
+ const points = m ? m.slice(1, 5).map(Number) : FALLBACK.ease[curve];
109
+ const key = points.join(',');
110
+ let fn = curves.get(key);
111
+ if (!fn) {
112
+ fn = bezier(points[0], points[1], points[2], points[3]);
113
+ curves.set(key, fn);
114
+ }
115
+ return fn;
116
+ }
117
+ /** True when the document asked for no motion, or cannot play any. */
118
+ export function still(node) {
119
+ if (typeof node.animate !== 'function')
120
+ return true;
121
+ return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
122
+ }
123
+ /**
124
+ * One transition from the rung its entrance runs at and the frame it draws.
125
+ * The exit is derived, never chosen: one rung faster, on the exit curve.
126
+ */
127
+ function surface(rung, css) {
128
+ return (node) => {
129
+ if (still(node))
130
+ return { duration: 0 };
131
+ return ({ direction } = { direction: 'in' }) => {
132
+ const leaving = direction === 'out';
133
+ return {
134
+ duration: duration(leaving ? EXIT_RUNG[rung] : rung),
135
+ easing: easing(leaving ? 'exit' : 'enter'),
136
+ css,
137
+ };
138
+ };
139
+ };
140
+ }
141
+ /** A centred dialog: rises a little and settles to full size. */
142
+ export const dialog = surface('slow', (t, u) => `opacity: ${t}; transform: translateY(${8 * u}px) scale(${0.96 + 0.04 * t})`);
143
+ /** The scrim behind a dialog or drawer, on the same clock as its surface. */
144
+ export const scrim = surface('slow', (t) => `opacity: ${t}`);
145
+ /**
146
+ * A panel docked to a viewport edge: the whole panel slides in from its own
147
+ * edge. No fade, since a surface arriving from off screen is never half there.
148
+ */
149
+ export function drawer(node, params = {}) {
150
+ const sign = params.side === 'left' ? -1 : 1;
151
+ return surface('slow', (_t, u) => `transform: translateX(${sign * 100 * u}%)`)(node);
152
+ }
153
+ /** A menu, listbox, tooltip or date grid anchored to its trigger. */
154
+ export const popover = surface('base', (t, u) => `opacity: ${t}; transform: translateY(${-4 * u}px) scale(${0.96 + 0.04 * t})`);
155
+ /** One toast arriving in the stack, and leaving it. */
156
+ export const toast = surface('base', (t, u) => `opacity: ${t}; transform: translateY(${8 * u}px) scale(${0.96 + 0.04 * t})`);
157
+ /**
158
+ * Siblings closing the gap a removed item leaves, for `animate:`. Svelte's
159
+ * flip measures the move; the kit supplies the clock.
160
+ */
161
+ export function reorder(node, positions, _params) {
162
+ if (still(node))
163
+ return { duration: 0 };
164
+ return flip(node, positions, { duration: duration('base'), easing: easing('move') });
165
+ }
@@ -115,24 +115,59 @@
115
115
  --z-index-toast: 600; /* transient status */
116
116
  --z-index-skip-link: 700; /* the first tab stop in the document */
117
117
 
118
- /* -- Motion - how long a transition or an entrance runs -------- *
119
- * Four durations shipped as literals inside the animate-[...]
120
- * utility of the component that used them, so the only way to
121
- * read the kit's motion was to grep four components. They are
122
- * stated here at the values they already ran at.
118
+ /* -- Motion - four durations, three curves, one rule for exits -- *
119
+ * Named for what they are used for, so the choice is made by
120
+ * reading the name:
121
+ *
122
+ * fast a state change in place: colour, opacity, border,
123
+ * shadow, focus
124
+ * base movement in place (a knob, a chevron, a collapse, a
125
+ * tab indicator) and a small surface arriving (menu,
126
+ * popover, tooltip, toast)
127
+ * slow a large surface arriving (dialog, drawer)
128
+ * progress a value moving (a progress bar's width)
129
+ *
130
+ * enter arriving: starts fast and settles
131
+ * exit leaving: starts slow and accelerates away
132
+ * move changing in place
133
+ *
134
+ * An exit runs one rung faster than its entrance and on exit:
135
+ * slow in, base out; base in, fast out. Entrances and exits move
136
+ * only opacity and transform.
123
137
  *
124
138
  * Tailwind builds no utility from a --duration-* token: there is
125
- * no such theme namespace, and `duration-collapse` compiles to
126
- * nothing at all. Each one is read through var() from an
127
- * arbitrary utility instead, which is why they are listed in
128
- * VAR_ONLY in theme-tokens.test.ts.
139
+ * no such theme namespace, and `duration-fast` would compile to
140
+ * nothing. The @utility rules after this block declare the four
141
+ * by hand, which is why the tokens are listed in VAR_ONLY in
142
+ * theme-tokens.test.ts. --ease-* is a real namespace and the
143
+ * three curves become ease-enter, ease-exit and ease-move on
144
+ * their own.
145
+ *
146
+ * Every transition-* utility reads the two --default-* values, so
147
+ * `transition-colors` alone is complete. Before this a component
148
+ * wrote `duration-150` fifty times to say the same thing.
129
149
  *
130
150
  * The reduced-motion block at the foot of this file overrides
131
- * every one of them and stays the authority on whether they run. */
132
- --duration-modal-in: 120ms; /* Modal, the centred entrance */
133
- --duration-toast-in: 140ms; /* Toaster, one toast arriving */
134
- --duration-drawer-in: 150ms; /* Drawer, either edge */
135
- --duration-collapse: 200ms; /* Accordion and Collapsible */
151
+ * every one of them and stays the authority on whether they run;
152
+ * the Svelte presets in motion.ts honour the same preference for
153
+ * the entrances and exits that run outside CSS. */
154
+ --duration-fast: 120ms;
155
+ --duration-base: 200ms;
156
+ --duration-slow: 320ms;
157
+ --duration-progress: 500ms;
158
+
159
+ --ease-enter: cubic-bezier(0.22, 1, 0.36, 1);
160
+ --ease-exit: cubic-bezier(0.32, 0, 0.67, 0);
161
+ --ease-move: cubic-bezier(0.65, 0, 0.35, 1);
162
+
163
+ --default-transition-duration: var(--duration-fast);
164
+ --default-transition-timing-function: var(--ease-move);
165
+
166
+ /* The one entrance that is a CSS animation. A panel inside <details> is
167
+ * shown and removed by the browser, so a Svelte transition never sees it
168
+ * leave; it plays this on the way in and nothing on the way out. Every
169
+ * other surface enters and leaves through the presets in motion.ts. */
170
+ --animate-popover-in: popover-in var(--duration-base) var(--ease-enter);
136
171
 
137
172
  /* ── Typography · families ───────────────────────────────────── */
138
173
  --font-sans:
@@ -242,7 +277,8 @@
242
277
  --spacing-input-y: 0.5rem; /* 8px - input vertical padding */
243
278
  /* 38px - the height of every single-line control. Stated once so an Input,
244
279
  * a Select and a NumberInput sitting in the same row line up. Derived from
245
- * the 13px/1.25 control text plus input-y padding and a 1px border. */
280
+ * the 13px/1.25 control text plus input-y padding and a 1px border. Under a
281
+ * coarse pointer it is 44px; see the touch-target block below the palette. */
246
282
  --spacing-control: 2.375rem;
247
283
  --spacing-panel-max: 15rem; /* 240px - the scroll cap on a floating option list */
248
284
  /* 56px - the app header bar, and the sidebar's brand row above the nav.
@@ -257,6 +293,58 @@
257
293
  --spacing-inline: 0.5rem; /* 8px - default inline gap */
258
294
  }
259
295
 
296
+ /*
297
+ * Touch targets.
298
+ *
299
+ * Measured over 48 admin routes at 400px: 2,007 of 2,097 visible controls
300
+ * were under the 44px SC 2.5.5 asks of a finger, and the kit owned 96% of
301
+ * them, because the control height above is 38px and it is right at 38px on
302
+ * a desktop. The size a pointer needs is a property of the pointer, not of
303
+ * the screen width, so the fix keys on `pointer: coarse` and a mouse never
304
+ * sees it: the desktop visual is unchanged to the pixel.
305
+ *
306
+ * Two mechanisms, chosen per control by whether the visual may grow.
307
+ *
308
+ * The token moves. Under a coarse pointer `--spacing-control` is 44px, so
309
+ * every single-line field, and every control that states `min-h-control`
310
+ * under the `coarse:` variant (Button, Tabs, a sidebar row, a menu item),
311
+ * grows to the finger's size and the row they share still lines up.
312
+ *
313
+ * `hit-area` does not move the visual. A switch, a checkbox, a breadcrumb, a
314
+ * pager button, a close cross and an icon inside an input are drawn small on
315
+ * purpose and grown to 44px would push their neighbours around; each one
316
+ * draws an invisible box centred on itself that is never smaller than the
317
+ * control size, and a tap on the box lands on the control. The element must
318
+ * be positioned (`relative`, or `absolute` where it already is) for the box
319
+ * to centre on it; the utility does not set position itself, because a
320
+ * position it set would fight the `absolute` an input's clear button relies
321
+ * on. Two neighbours' boxes may overlap; the later one in document order
322
+ * wins the overlap, which is the spacing exception SC 2.5.8 allows for.
323
+ */
324
+ @custom-variant coarse (@media (pointer: coarse));
325
+
326
+ @media (pointer: coarse) {
327
+ :root {
328
+ --spacing-control: 2.75rem; /* 44px */
329
+ }
330
+ }
331
+
332
+ @utility hit-area {
333
+ @media (pointer: coarse) {
334
+ &::before {
335
+ content: '';
336
+ position: absolute;
337
+ left: 50%;
338
+ top: 50%;
339
+ width: 100%;
340
+ height: 100%;
341
+ min-width: var(--spacing-control);
342
+ min-height: var(--spacing-control);
343
+ translate: -50% -50%;
344
+ }
345
+ }
346
+ }
347
+
260
348
  /* ── Soft Light · activated by html[data-theme="light"] ──────────────────── */
261
349
  html[data-theme='light'] {
262
350
  --color-ink: #f6f7f9;
@@ -290,61 +378,41 @@ html[data-theme='light'] {
290
378
  }
291
379
 
292
380
  /*
293
- * The entrance keyframes, declared here and not in the components that play
294
- * them.
295
- *
296
- * Modal, Drawer and Toaster each held their own `@keyframes` in a scoped
297
- * `<style>` block and started it from a Tailwind `animate-[...]` class. Svelte
298
- * renames a scoped keyframe to `svelte-<hash>-<name>` and rewrites only the
299
- * references inside that same style block, so the class asked the browser for
300
- * `modal-in` while the only rule declared was `svelte-ta60gp-modal-in`. An
301
- * animation-name that resolves to nothing is not an error: the element simply
302
- * appears, and all four entrances had been dead in every consumer.
303
- *
304
- * A keyframe named from a class has to be global, and the file every consumer
305
- * already imports is the one place it can be global from.
381
+ * The four durations as utilities. A --duration-* token generates nothing
382
+ * (see the motion section of the @theme block), so each is declared here the
383
+ * way Tailwind's own duration-* is: the custom property it sets is what the
384
+ * transition-* utilities read, and the longhand covers an element that
385
+ * writes `transition` by hand.
306
386
  */
307
- @keyframes modal-in {
308
- from {
309
- opacity: 0;
310
- transform: translateY(6px) scale(0.98);
311
- }
312
- to {
313
- opacity: 1;
314
- transform: translateY(0) scale(1);
315
- }
387
+ @utility duration-fast {
388
+ --tw-duration: var(--duration-fast);
389
+ transition-duration: var(--duration-fast);
316
390
  }
317
-
318
- @keyframes drawer-in-right {
319
- from {
320
- opacity: 0;
321
- transform: translateX(16px);
322
- }
323
- to {
324
- opacity: 1;
325
- transform: translateX(0);
326
- }
391
+ @utility duration-base {
392
+ --tw-duration: var(--duration-base);
393
+ transition-duration: var(--duration-base);
327
394
  }
328
-
329
- @keyframes drawer-in-left {
330
- from {
331
- opacity: 0;
332
- transform: translateX(-16px);
333
- }
334
- to {
335
- opacity: 1;
336
- transform: translateX(0);
337
- }
395
+ @utility duration-slow {
396
+ --tw-duration: var(--duration-slow);
397
+ transition-duration: var(--duration-slow);
398
+ }
399
+ @utility duration-progress {
400
+ --tw-duration: var(--duration-progress);
401
+ transition-duration: var(--duration-progress);
338
402
  }
339
403
 
340
- @keyframes toast-in {
404
+ /*
405
+ * Declared here and not in the component that plays it. Svelte renames a
406
+ * scoped keyframe to `svelte-<hash>-<name>` and rewrites only the references
407
+ * inside that same style block, so a class asking for `popover-in` would find
408
+ * no rule and the element would simply appear. A keyframe named from a class
409
+ * has to be global, and the file every consumer imports is the one place it
410
+ * can be global from. It draws the same frame as the popover preset.
411
+ */
412
+ @keyframes popover-in {
341
413
  from {
342
414
  opacity: 0;
343
- transform: translateX(12px);
344
- }
345
- to {
346
- opacity: 1;
347
- transform: translateX(0);
415
+ transform: translateY(-4px) scale(0.96);
348
416
  }
349
417
  }
350
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",