@lyeve-labs/ui-kit 0.23.3 → 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.
- package/README.md +42 -0
- package/dist/components/AccordionItem.svelte +3 -5
- package/dist/components/AccountMenu.svelte +4 -3
- package/dist/components/Alert.svelte +2 -1
- package/dist/components/AppShell.svelte +166 -54
- package/dist/components/AppShell.svelte.d.ts +41 -6
- package/dist/components/Autocomplete.svelte +3 -2
- package/dist/components/Badge.svelte +10 -5
- package/dist/components/Banner.svelte +2 -1
- package/dist/components/Breadcrumb.svelte +7 -2
- package/dist/components/Button.svelte +15 -2
- package/dist/components/Card.svelte +1 -1
- package/dist/components/Collapsible.svelte +5 -4
- package/dist/components/CopyButton.svelte +2 -1
- package/dist/components/DatePicker.svelte +77 -62
- package/dist/components/Drawer.svelte +6 -5
- package/dist/components/Dropdown.svelte +5 -2
- package/dist/components/FileInput.svelte +1 -1
- package/dist/components/Modal.svelte +6 -3
- package/dist/components/MultiSelect.svelte +8 -6
- package/dist/components/NumberInput.svelte +2 -2
- package/dist/components/PageHeader.svelte +13 -1
- package/dist/components/PageShell.svelte +62 -7
- package/dist/components/PageShell.svelte.d.ts +27 -1
- package/dist/components/Pagination.svelte +7 -2
- package/dist/components/PasswordInput.svelte +2 -1
- package/dist/components/Progress.svelte +1 -1
- package/dist/components/SearchInput.svelte +3 -2
- package/dist/components/SectionHeading.svelte +10 -1
- package/dist/components/SegmentedControl.svelte +101 -27
- package/dist/components/SegmentedControl.svelte.d.ts +9 -0
- package/dist/components/Select.svelte +4 -3
- package/dist/components/SidebarNav.svelte +26 -17
- package/dist/components/StepIndicator.svelte +7 -9
- package/dist/components/Table.svelte +39 -29
- package/dist/components/Tabs.svelte +128 -24
- package/dist/components/Tag.svelte +2 -1
- package/dist/components/ThemeToggle.svelte +2 -1
- package/dist/components/Toaster.svelte +6 -2
- package/dist/components/Toggle.svelte +23 -5
- package/dist/components/Tooltip.svelte +10 -5
- package/dist/components/TreeView.svelte +2 -2
- package/dist/components/dialog/Dialog.svelte +15 -32
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/internal/choice.js +10 -3
- package/dist/internal/field.js +3 -3
- package/dist/internal/layout.d.ts +10 -1
- package/dist/internal/layout.js +10 -1
- package/dist/internal/panel.d.ts +13 -3
- package/dist/internal/panel.js +16 -5
- package/dist/internal/scroll-edges.d.ts +33 -0
- package/dist/internal/scroll-edges.js +33 -0
- package/dist/internal/touch.d.ts +28 -0
- package/dist/internal/touch.js +28 -0
- package/dist/motion.d.ts +63 -0
- package/dist/motion.js +165 -0
- package/dist/styles/theme.css +131 -63
- package/package.json +1 -1
- package/src/lib/styles/theme.css +131 -63
package/dist/internal/panel.js
CHANGED
|
@@ -73,7 +73,7 @@ export const PANEL_LIST = 'max-h-panel-max overflow-y-auto overscroll-contain px
|
|
|
73
73
|
* the active ring follow the row's own corners.
|
|
74
74
|
*/
|
|
75
75
|
export const PANEL_OPTION = 'flex w-full items-center gap-2.5 rounded-lg px-2 py-2 text-start text-sm ' +
|
|
76
|
-
'transition-colors
|
|
76
|
+
'transition-colors outline-none hover:bg-surface-2 ' +
|
|
77
77
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
78
78
|
/**
|
|
79
79
|
* The active descendant.
|
|
@@ -134,10 +134,19 @@ export function panelOption(state) {
|
|
|
134
134
|
}
|
|
135
135
|
/** Marks the scrolling region inside a surface, so placePanel can find it. */
|
|
136
136
|
export const PANEL_LIST_ATTR = 'data-panel-list';
|
|
137
|
+
/**
|
|
138
|
+
* The attribute's value when the region is capped by the room alone.
|
|
139
|
+
*
|
|
140
|
+
* The token cap is sized for a list of rows, and a calendar is not one: six
|
|
141
|
+
* weeks of days stand taller than the token, so under it every month would
|
|
142
|
+
* scroll by a few rows. Such a region still takes the room as its cap, so
|
|
143
|
+
* inside a short modal it scrolls rather than being cut.
|
|
144
|
+
*/
|
|
145
|
+
export const PANEL_LIST_UNCAPPED = 'uncapped';
|
|
137
146
|
/** The classes that put the surface under its anchor. */
|
|
138
|
-
export const PANEL_BELOW = 'top-full mt-1';
|
|
147
|
+
export const PANEL_BELOW = 'top-full mt-1 origin-top';
|
|
139
148
|
/** The classes that put the surface over its anchor. */
|
|
140
|
-
export const PANEL_ABOVE = 'bottom-full mb-1';
|
|
149
|
+
export const PANEL_ABOVE = 'bottom-full mb-1 origin-bottom';
|
|
141
150
|
/**
|
|
142
151
|
* Breathing room between the panel and the edge it is measured against, so a
|
|
143
152
|
* panel that fits exactly does not sit on the edge of the window or the modal.
|
|
@@ -183,7 +192,8 @@ function swap(node, remove, add) {
|
|
|
183
192
|
* under the modal's scroll edge that nobody can reach without scrolling
|
|
184
193
|
* first. Whichever side wins, the list's max-height is the smaller of the
|
|
185
194
|
* token cap and the room on that side, written as a CSS min so the token stays
|
|
186
|
-
* the cap without this code knowing its value.
|
|
195
|
+
* the cap without this code knowing its value. A region marked uncapped takes
|
|
196
|
+
* the room alone.
|
|
187
197
|
*
|
|
188
198
|
* Measured on mount, on resize and on scroll of the clipping ancestor, since
|
|
189
199
|
* each moves the anchor relative to the edge. Scroll does not bubble, so the
|
|
@@ -221,7 +231,8 @@ export function placePanel(node) {
|
|
|
221
231
|
// taken out of the room so the whole panel fits and not only its list.
|
|
222
232
|
const chrome = Math.max(0, node.offsetHeight - list.offsetHeight);
|
|
223
233
|
const room = Math.max(0, Math.round((flip ? above : below) - chrome));
|
|
224
|
-
|
|
234
|
+
const capped = list.getAttribute(PANEL_LIST_ATTR) !== PANEL_LIST_UNCAPPED;
|
|
235
|
+
list.style.maxHeight = capped ? `min(var(--spacing-panel-max), ${room}px)` : `${room}px`;
|
|
225
236
|
}
|
|
226
237
|
measure();
|
|
227
238
|
const scroller = clip ?? window;
|
|
@@ -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';
|
package/dist/motion.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/styles/theme.css
CHANGED
|
@@ -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 -
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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-
|
|
126
|
-
* nothing
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
--duration-
|
|
135
|
-
--duration-
|
|
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
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
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
|
-
@
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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:
|
|
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