@astralarium/repo-ui 0.0.0-stage → 0.2.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/dist/error-boundary.d.ts +29 -0
  4. package/dist/error-boundary.js +30 -0
  5. package/dist/index.d.ts +23 -0
  6. package/dist/index.js +23 -0
  7. package/dist/l10n/repo-ui/en.json +14 -0
  8. package/dist/lazy-google-analytics.d.ts +4 -0
  9. package/dist/lazy-google-analytics.js +34 -0
  10. package/dist/page-tree.d.ts +25 -0
  11. package/dist/page-tree.js +45 -0
  12. package/dist/responsive-table.d.ts +54 -0
  13. package/dist/responsive-table.js +180 -0
  14. package/dist/router/dev-route.d.ts +29 -0
  15. package/dist/router/dev-route.js +15 -0
  16. package/dist/router/index.d.ts +5 -0
  17. package/dist/router/index.js +5 -0
  18. package/dist/router/tab-bar.d.ts +12 -0
  19. package/dist/router/tab-bar.js +98 -0
  20. package/dist/router/tab-bar.messages.d.json.ts +7 -0
  21. package/dist/router/tab-bar.messages.json +6 -0
  22. package/dist/router/tabs.d.ts +21 -0
  23. package/dist/router/tabs.js +37 -0
  24. package/dist/router/use-search-param.d.ts +5 -0
  25. package/dist/router/use-search-param.js +46 -0
  26. package/dist/slider/index.d.ts +8 -0
  27. package/dist/slider/index.js +7 -0
  28. package/dist/slider/scrubber-state.d.ts +118 -0
  29. package/dist/slider/scrubber-state.js +179 -0
  30. package/dist/slider/scrubber.d.ts +77 -0
  31. package/dist/slider/scrubber.js +664 -0
  32. package/dist/slider/slider-parts.d.ts +16 -0
  33. package/dist/slider/slider-parts.js +11 -0
  34. package/dist/slider/slider-state.d.ts +75 -0
  35. package/dist/slider/slider-state.js +183 -0
  36. package/dist/slider/slider.d.ts +100 -0
  37. package/dist/slider/slider.js +966 -0
  38. package/dist/slider/slider.messages.d.json.ts +8 -0
  39. package/dist/slider/slider.messages.json +10 -0
  40. package/dist/slider/timeline-scrubber-state.d.ts +13 -0
  41. package/dist/slider/timeline-scrubber-state.js +21 -0
  42. package/dist/slider/timeline-scrubber.d.ts +26 -0
  43. package/dist/slider/timeline-scrubber.js +142 -0
  44. package/dist/sorted.d.ts +16 -0
  45. package/dist/sorted.js +40 -0
  46. package/dist/status.d.ts +9 -0
  47. package/dist/status.js +55 -0
  48. package/dist/sticky-height.d.ts +10 -0
  49. package/dist/sticky-height.js +133 -0
  50. package/dist/storage.d.ts +21 -0
  51. package/dist/storage.js +42 -0
  52. package/dist/testkit/index.d.ts +12 -0
  53. package/dist/testkit/index.js +42 -0
  54. package/dist/theme/index.d.ts +3 -0
  55. package/dist/theme/index.js +3 -0
  56. package/dist/theme/theme-context.d.ts +11 -0
  57. package/dist/theme/theme-context.js +68 -0
  58. package/dist/theme/theme.d.ts +28 -0
  59. package/dist/theme/theme.js +37 -0
  60. package/dist/theme/themed-component.d.ts +8 -0
  61. package/dist/theme/themed-component.js +10 -0
  62. package/dist/timeline.d.ts +34 -0
  63. package/dist/timeline.js +47 -0
  64. package/dist/tree/index.d.ts +2 -0
  65. package/dist/tree/index.js +1 -0
  66. package/dist/tree/tree.d.ts +27 -0
  67. package/dist/tree/tree.js +299 -0
  68. package/dist/tree/use-tree.d.ts +38 -0
  69. package/dist/tree/use-tree.js +155 -0
  70. package/dist/use-copy-feedback.d.ts +15 -0
  71. package/dist/use-copy-feedback.js +72 -0
  72. package/dist/use-debounce.d.ts +3 -0
  73. package/dist/use-debounce.js +66 -0
  74. package/dist/use-external-script.d.ts +10 -0
  75. package/dist/use-external-script.js +73 -0
  76. package/dist/use-frame.d.ts +3 -0
  77. package/dist/use-frame.js +37 -0
  78. package/dist/use-hover-focus.d.ts +18 -0
  79. package/dist/use-hover-focus.js +87 -0
  80. package/dist/use-media-query.d.ts +5 -0
  81. package/dist/use-media-query.js +42 -0
  82. package/dist/use-pausable-hover.d.ts +27 -0
  83. package/dist/use-pausable-hover.js +100 -0
  84. package/dist/use-pulse-style.d.ts +3 -0
  85. package/dist/use-pulse-style.js +19 -0
  86. package/dist/use-sticky-height.d.ts +10 -0
  87. package/dist/use-sticky-height.js +37 -0
  88. package/l10n.json +12 -0
  89. package/package.json +84 -3
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ /**
4
+ * A `<nav>` of tabs. Each tab carries `data-slot="tab"`; the current tab
5
+ * carries `data-status="active"` and `aria-current`: `"page"` on a link,
6
+ * `"true"` on a button. Everything visual comes from the app.
7
+ */
8
+ import { c as _c } from "react/compiler-runtime";
9
+ import { useFormat } from "@astralarium/localizable";
10
+ import { Link } from "@tanstack/react-router";
11
+ import messages from "./tab-bar.messages.json" with { type: "json" };
12
+ /** A link tab's default `activeOptions`: current on its exact path,
13
+ * search aside. */
14
+ import { jsx as _jsx } from "react/jsx-runtime";
15
+ const TAB_ACTIVE_OPTIONS = {
16
+ exact: true,
17
+ includeSearch: false
18
+ };
19
+ /** A tab bar: router `Link`s current on their exact path, search aside,
20
+ * unless the link sets `activeOptions`; buttons current while `active`.
21
+ * Named "Tabs" unless `aria-label` says otherwise. */
22
+ export function TabBar(t0) {
23
+ const $ = _c(15);
24
+ let props;
25
+ let tabClassName;
26
+ let tabs;
27
+ if ($[0] !== t0) {
28
+ ({
29
+ tabs,
30
+ tabClassName,
31
+ ...props
32
+ } = t0);
33
+ $[0] = t0;
34
+ $[1] = props;
35
+ $[2] = tabClassName;
36
+ $[3] = tabs;
37
+ } else {
38
+ props = $[1];
39
+ tabClassName = $[2];
40
+ tabs = $[3];
41
+ }
42
+ const format = useFormat();
43
+ let t1;
44
+ if ($[4] !== format) {
45
+ t1 = format(messages.barName);
46
+ $[4] = format;
47
+ $[5] = t1;
48
+ } else {
49
+ t1 = $[5];
50
+ }
51
+ let t2;
52
+ if ($[6] !== tabClassName || $[7] !== tabs) {
53
+ let t3;
54
+ if ($[9] !== tabClassName) {
55
+ t3 = tab => tab.link ? /*#__PURE__*/_jsx(Link, {
56
+ ...tab.link,
57
+ activeOptions: tab.link.activeOptions ?? TAB_ACTIVE_OPTIONS,
58
+ className: tabClassName,
59
+ "data-slot": "tab",
60
+ children: tab.label
61
+ }, tab.name) : /*#__PURE__*/_jsx("button", {
62
+ type: "button",
63
+ onClick: () => tab.onClick?.(tab),
64
+ className: tabClassName,
65
+ "aria-current": tab.active ? "true" : undefined,
66
+ "data-slot": "tab",
67
+ "data-status": tab.active ? "active" : undefined,
68
+ children: tab.label
69
+ }, tab.name);
70
+ $[9] = tabClassName;
71
+ $[10] = t3;
72
+ } else {
73
+ t3 = $[10];
74
+ }
75
+ t2 = tabs.map(t3);
76
+ $[6] = tabClassName;
77
+ $[7] = tabs;
78
+ $[8] = t2;
79
+ } else {
80
+ t2 = $[8];
81
+ }
82
+ let t3;
83
+ if ($[11] !== props || $[12] !== t1 || $[13] !== t2) {
84
+ t3 = /*#__PURE__*/_jsx("nav", {
85
+ "aria-label": t1,
86
+ "data-slot": "tab-bar",
87
+ ...props,
88
+ children: t2
89
+ });
90
+ $[11] = props;
91
+ $[12] = t1;
92
+ $[13] = t2;
93
+ $[14] = t3;
94
+ } else {
95
+ t3 = $[14];
96
+ }
97
+ return t3;
98
+ }
@@ -0,0 +1,7 @@
1
+ // Generated by l10n-types.
2
+ import type { Message } from "@astralarium/localizable";
3
+
4
+ declare const messages: {
5
+ barName: Message<Record<never, never>>;
6
+ };
7
+ export = messages;
@@ -0,0 +1,6 @@
1
+ {
2
+ "barName": {
3
+ "id": "@astralarium/repo-ui/router/tab-bar.messages#barName",
4
+ "defaultMessage": "Tabs"
5
+ }
6
+ }
@@ -0,0 +1,21 @@
1
+ import { type LinkOptions } from "@tanstack/react-router";
2
+ import type { ReactNode } from "react";
3
+ /** A tab: a router link, or a button. */
4
+ export interface Tab {
5
+ /** Identifies the tab; `buildTabs` selects by it. */
6
+ name: string;
7
+ label: ReactNode;
8
+ /** Destination of a link tab; current on its exact path, search
9
+ * aside, unless `activeOptions` says otherwise. */
10
+ link?: LinkOptions;
11
+ /** Press of a button tab. */
12
+ onClick?: (tab: Tab) => unknown;
13
+ /** The selected tab: current among button tabs. */
14
+ active?: boolean;
15
+ }
16
+ /** Binds fixed tabs to a selection: `tabsOf(tab, setTab, onChange)` marks
17
+ * tab `tab` active and selects a pressed tab. */
18
+ export declare function buildTabs(tabs: readonly Omit<Tab, "onClick" | "active">[]): (tab: string, setTab: (name: string) => unknown, onChange?: () => unknown) => Tab[];
19
+ /** Selects a tab outside a `TabBar`, e.g. from a dropdown: navigates to a
20
+ * link tab, presses a button tab. */
21
+ export declare function useSelectTab(): (tab: Tab) => void;
@@ -0,0 +1,37 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ /** Binds fixed tabs to a selection: `tabsOf(tab, setTab, onChange)` marks
4
+ * tab `tab` active and selects a pressed tab. */
5
+ export function buildTabs(tabs) {
6
+ return function tabsOf(tab, setTab, onChange) {
7
+ return tabs.map(x => ({
8
+ ...x,
9
+ active: x.name === tab,
10
+ onClick: () => {
11
+ setTab(x.name);
12
+ onChange?.();
13
+ }
14
+ }));
15
+ };
16
+ }
17
+ /** Selects a tab outside a `TabBar`, e.g. from a dropdown: navigates to a
18
+ * link tab, presses a button tab. */
19
+ export function useSelectTab() {
20
+ const $ = _c(2);
21
+ const navigate = useNavigate();
22
+ let t0;
23
+ if ($[0] !== navigate) {
24
+ t0 = tab => {
25
+ if (tab.link) {
26
+ navigate(tab.link);
27
+ } else {
28
+ tab.onClick?.(tab);
29
+ }
30
+ };
31
+ $[0] = navigate;
32
+ $[1] = t0;
33
+ } else {
34
+ t0 = $[1];
35
+ }
36
+ return t0;
37
+ }
@@ -0,0 +1,5 @@
1
+ /** String-valued search param `name`, written back after `ms` idle
2
+ * (`useDebounce`), replacing the history entry and keeping the hash;
3
+ * empty text removes it. Other values read as JSON and write back as
4
+ * strings. */
5
+ export declare function useSearchParam(name: string, ms?: number): [string, (text: string) => void];
@@ -0,0 +1,46 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useNavigate, useSearch } from "@tanstack/react-router";
3
+ import { useDebounce } from "../use-debounce.js";
4
+ /** A search param's value as text; empty when absent. */
5
+ function searchText(value) {
6
+ if (value === undefined || value === null) return "";
7
+ return typeof value === "string" ? value : JSON.stringify(value);
8
+ }
9
+ /** String-valued search param `name`, written back after `ms` idle
10
+ * (`useDebounce`), replacing the history entry and keeping the hash;
11
+ * empty text removes it. Other values read as JSON and write back as
12
+ * strings. */
13
+ export function useSearchParam(name, ms) {
14
+ const $ = _c(5);
15
+ let t0;
16
+ if ($[0] !== name) {
17
+ t0 = {
18
+ strict: false,
19
+ select: search => searchText(search[name])
20
+ };
21
+ $[0] = name;
22
+ $[1] = t0;
23
+ } else {
24
+ t0 = $[1];
25
+ }
26
+ const value = useSearch(t0);
27
+ const navigate = useNavigate();
28
+ let t1;
29
+ if ($[2] !== name || $[3] !== navigate) {
30
+ t1 = text => navigate({
31
+ to: ".",
32
+ search: prev => ({
33
+ ...prev,
34
+ [name]: text || undefined
35
+ }),
36
+ hash: true,
37
+ replace: true
38
+ });
39
+ $[2] = name;
40
+ $[3] = navigate;
41
+ $[4] = t1;
42
+ } else {
43
+ t1 = $[4];
44
+ }
45
+ return useDebounce(value, t1, ms);
46
+ }
@@ -0,0 +1,8 @@
1
+ /** Sliders and scrubbers over Base UI: parts, state and timeline scrubbing. */
2
+ export * from "./scrubber.tsx";
3
+ export type { ScrubberStep, ScrubberTick } from "./scrubber-state.ts";
4
+ export * from "./slider.tsx";
5
+ export * from "./slider-parts.ts";
6
+ export { type SliderContextValue, type SliderFill, type SliderSpan, type SliderTickLight, useSlider, useSliderTick, } from "./slider-state.ts";
7
+ export * from "./timeline-scrubber.tsx";
8
+ export { type TimelineMark, timelineNames } from "./timeline-scrubber-state.ts";
@@ -0,0 +1,7 @@
1
+ /** Sliders and scrubbers over Base UI: parts, state and timeline scrubbing. */
2
+ export * from "./scrubber.js";
3
+ export * from "./slider.js";
4
+ export * from "./slider-parts.js";
5
+ export { useSlider, useSliderTick } from "./slider-state.js";
6
+ export * from "./timeline-scrubber.js";
7
+ export { timelineNames } from "./timeline-scrubber-state.js";
@@ -0,0 +1,118 @@
1
+ /** Scrubber rules: tick groups, landing, stepping, lighting, reach,
2
+ * and the split fill. */
3
+ import type { Slider as SliderPrimitive } from "@base-ui/react/slider";
4
+ import type { ReactNode } from "react";
5
+ import type { SliderSpan } from "./slider-state.ts";
6
+ /** A position along a scrubber's track: a snap and step target. Named
7
+ * or glyphed, a mark, always drawn; bare, a stop, drawn faintly near
8
+ * the pointer. */
9
+ export interface ScrubberTick {
10
+ value: number;
11
+ /** Tooltip and spoken text; names sharing a value list together. */
12
+ name?: string;
13
+ /** Drawn at the tick; the lowest-ranked glyph at a value draws. Its
14
+ * mark carries the thumb's reach along the fill as `data-reach`:
15
+ * `ahead` short of the mark, `held` on it or within the hold window
16
+ * past it, `past` beyond. */
17
+ glyph?: ReactNode;
18
+ /** Lower draws over higher at a shared value; ties keep order. */
19
+ rank?: number;
20
+ }
21
+ /** The ticks at one value. */
22
+ export interface ScrubberTickGroup {
23
+ value: number;
24
+ /** The named or glyphed ticks, most prominent first; none for a
25
+ * stop. */
26
+ marks: readonly ScrubberTick[];
27
+ /** The marks' names, most prominent first. */
28
+ names: readonly string[];
29
+ }
30
+ /** `ticks` grouped by value, ascending. */
31
+ export declare function groupTicks(ticks: readonly ScrubberTick[]): ScrubberTickGroup[];
32
+ /** The names of the `groups` at `value`, most prominent first. */
33
+ export declare function tickNames(groups: readonly ScrubberTickGroup[], value: number): readonly string[];
34
+ /** The names of the latest of `groups` at or before `value`, most
35
+ * prominent first. */
36
+ export declare function latestNames(groups: readonly ScrubberTickGroup[], value: number): readonly string[];
37
+ /** A key step: `by` toward `max` (1) or `min` (-1), resolved for
38
+ * orientation and direction; `large` with Shift or PageUp/PageDown. */
39
+ export interface ScrubberStep {
40
+ by: -1 | 1;
41
+ large: boolean;
42
+ }
43
+ /** Where a step from `from` lands: the adjacent tick; a large step's
44
+ * the adjacent mark, or tick where none is a mark. Undefined past the
45
+ * outermost. */
46
+ export declare function stepTick(groups: readonly ScrubberTickGroup[], from: number, { by, large }: ScrubberStep): number | undefined;
47
+ /** The ticks and range landings snap within. */
48
+ export interface ScrubberGrid {
49
+ groups: readonly ScrubberTickGroup[];
50
+ min: number;
51
+ max: number;
52
+ /** How near a tick a landing snaps to it. */
53
+ snapWindow: number;
54
+ }
55
+ /** Where `at` lands, within the range: snapping, the tick nearest it
56
+ * within the snap window. */
57
+ export declare function landAt({ groups, min, max, snapWindow }: ScrubberGrid, at: number, snaps: boolean): number;
58
+ /** A value change, as the scrubber reads it. */
59
+ export interface ScrubberChange {
60
+ /** Base UI's value; for a thumb key while playing, the end it heads
61
+ * for. */
62
+ next: number;
63
+ reason: SliderPrimitive.Root.ChangeEventReason;
64
+ /** A step's way: `next`'s side of Base UI's value, or a thumb key's
65
+ * (`keyWay`). */
66
+ by: -1 | 1;
67
+ /** The key pressed, on a keyboard change. */
68
+ key?: string;
69
+ /** Shift held, where the event carries modifiers. */
70
+ shiftKey?: boolean;
71
+ }
72
+ /** What a change does: lands at `land`, or steps by `step`, landing at
73
+ * `land` unless a step handler takes the step. */
74
+ export type ScrubberMove = {
75
+ land: number;
76
+ } | {
77
+ step: ScrubberStep;
78
+ land: number | undefined;
79
+ };
80
+ /** Which way a thumb key heads, resolved for orientation and direction:
81
+ * Home and End toward their ends; null for other keys. */
82
+ export declare function keyWay(key: string, rtl: boolean): -1 | 1 | null;
83
+ /** What `change` does to the scrubber at `current`: presses and drags
84
+ * land snapped (`snapping`, unless the event says whether Shift is
85
+ * held), arrows, Page keys, and input changes step their way, Home and
86
+ * End land on the ends. */
87
+ export declare function resolveChange(grid: ScrubberGrid, { next, reason, by, key, shiftKey }: ScrubberChange, { current, snapping }: {
88
+ current: number;
89
+ snapping: boolean;
90
+ }): ScrubberMove;
91
+ /** The tick an off-tick `target` shows: the latest at or before it;
92
+ * null on a tick, before the first, or with no target. */
93
+ export declare function hintedTick(groups: readonly ScrubberTickGroup[], target: number | null): number | null;
94
+ /** A stop drawn near the pointer. */
95
+ export interface NearStop {
96
+ value: number;
97
+ /** 1 for the nearest, falling with each stop farther. */
98
+ fade: number;
99
+ }
100
+ /** The `count` of `stops` nearest `pointer`, fading with distance. */
101
+ export declare function nearStops(stops: readonly number[], pointer: number, count: number): NearStop[];
102
+ /** How far `at` has come along `marks`: one per mark it stands on, two
103
+ * per mark behind it. Changes exactly as `at` reaches or leaves a
104
+ * mark. */
105
+ export declare function crossings(marks: readonly number[], at: number): number;
106
+ /** Where the thumb stands by a mark (`data-reach`). */
107
+ export type ScrubberReach = "ahead" | "held" | "past";
108
+ /** The thumb at `value` by the mark at `at`, along the way `fill`
109
+ * grows: `held` on it or up to `hold` past it. */
110
+ export declare function tickReach(value: number, at: number, hold: number, fill: "start" | "end"): ScrubberReach;
111
+ /** A range segment: a run of the fill and how a press changes it. */
112
+ export interface RangeSegment {
113
+ change: "kept" | "shrink" | "grow";
114
+ span: SliderSpan;
115
+ }
116
+ /** The fill over `shown`, split by what a press leaving `predicted`
117
+ * keeps, shrinks, and grows. */
118
+ export declare function rangeSegments(shown: SliderSpan, predicted: SliderSpan | null): RangeSegment[];
@@ -0,0 +1,179 @@
1
+ import { adjacent, nearest } from "../sorted.js";
2
+ /** Whether `tick` draws as a mark. */
3
+ function isMark(tick) {
4
+ return tick.name !== undefined || tick.glyph !== undefined;
5
+ }
6
+ /** `ticks` grouped by value, ascending. */
7
+ export function groupTicks(ticks) {
8
+ const at = new Map();
9
+ for (const tick of ticks) {
10
+ at.set(tick.value, [...(at.get(tick.value) ?? []), tick]);
11
+ }
12
+ return [...at].sort(([a], [b]) => a - b).map(([value, here]) => {
13
+ const marks = here.filter(isMark).sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0));
14
+ return {
15
+ value,
16
+ marks,
17
+ names: marks.flatMap(mark => mark.name === undefined ? [] : [mark.name])
18
+ };
19
+ });
20
+ }
21
+ /** The names of the `groups` at `value`, most prominent first. */
22
+ export function tickNames(groups, value) {
23
+ return groups.find(group => group.value === value)?.names ?? [];
24
+ }
25
+ /** The latest of `groups` at or before `value`; undefined before the
26
+ * first. */
27
+ function latestTick(groups, value) {
28
+ return groups.findLast(group => group.value <= value);
29
+ }
30
+ /** The names of the latest of `groups` at or before `value`, most
31
+ * prominent first. */
32
+ export function latestNames(groups, value) {
33
+ return latestTick(groups, value)?.names ?? [];
34
+ }
35
+ /** Where a step from `from` lands: the adjacent tick; a large step's
36
+ * the adjacent mark, or tick where none is a mark. Undefined past the
37
+ * outermost. */
38
+ export function stepTick(groups, from, {
39
+ by,
40
+ large
41
+ }) {
42
+ const marks = groups.filter(group => group.marks.length > 0);
43
+ const over = (large && marks.length > 0 ? marks : groups).map(group => group.value);
44
+ return adjacent(over, from, by);
45
+ }
46
+ /** Where `at` lands, within the range: snapping, the tick nearest it
47
+ * within the snap window. */
48
+ export function landAt({
49
+ groups,
50
+ min,
51
+ max,
52
+ snapWindow
53
+ }, at, snaps) {
54
+ const ticks = groups.map(group => group.value);
55
+ return Math.min(max, Math.max(min, snaps ? nearest(ticks, at, snapWindow) : at));
56
+ }
57
+ /** A step from `current` by `step`: to the adjacent tick; without
58
+ * ticks, to `next`. */
59
+ function stepMove({
60
+ groups
61
+ }, current, next, step) {
62
+ return {
63
+ step,
64
+ land: groups.length === 0 ? next : stepTick(groups, current, step)
65
+ };
66
+ }
67
+ /** Which way a thumb key heads, resolved for orientation and direction:
68
+ * Home and End toward their ends; null for other keys. */
69
+ export function keyWay(key, rtl) {
70
+ switch (key) {
71
+ case "ArrowUp":
72
+ case "PageUp":
73
+ case "End":
74
+ return 1;
75
+ case "ArrowDown":
76
+ case "PageDown":
77
+ case "Home":
78
+ return -1;
79
+ case "ArrowRight":
80
+ return rtl ? -1 : 1;
81
+ case "ArrowLeft":
82
+ return rtl ? 1 : -1;
83
+ default:
84
+ return null;
85
+ }
86
+ }
87
+ /** What `change` does to the scrubber at `current`: presses and drags
88
+ * land snapped (`snapping`, unless the event says whether Shift is
89
+ * held), arrows, Page keys, and input changes step their way, Home and
90
+ * End land on the ends. */
91
+ export function resolveChange(grid, {
92
+ next,
93
+ reason,
94
+ by,
95
+ key,
96
+ shiftKey
97
+ }, {
98
+ current,
99
+ snapping
100
+ }) {
101
+ switch (reason) {
102
+ case "track-press":
103
+ case "drag":
104
+ return {
105
+ land: landAt(grid, next, shiftKey === undefined ? snapping : !shiftKey)
106
+ };
107
+ // Assistive tech adjusts the thumb a step at a time.
108
+ case "input-change":
109
+ return stepMove(grid, current, next, {
110
+ by,
111
+ large: false
112
+ });
113
+ case "keyboard":
114
+ if (key === "Home" || key === "End") return {
115
+ land: next
116
+ };
117
+ return stepMove(grid, current, next, {
118
+ by,
119
+ large: shiftKey === true || key === "PageUp" || key === "PageDown"
120
+ });
121
+ case "none":
122
+ return {
123
+ land: next
124
+ };
125
+ default:
126
+ return reason;
127
+ }
128
+ }
129
+ /** The tick an off-tick `target` shows: the latest at or before it;
130
+ * null on a tick, before the first, or with no target. */
131
+ export function hintedTick(groups, target) {
132
+ if (target === null) return null;
133
+ const tick = latestTick(groups, target);
134
+ return tick === undefined || tick.value === target ? null : tick.value;
135
+ }
136
+ /** The `count` of `stops` nearest `pointer`, fading with distance. */
137
+ export function nearStops(stops, pointer, count) {
138
+ return [...stops].sort((a, b) => Math.abs(a - pointer) - Math.abs(b - pointer)).slice(0, count).map((value, rank) => ({
139
+ value,
140
+ fade: 1 - rank / count
141
+ }));
142
+ }
143
+ /** How far `at` has come along `marks`: one per mark it stands on, two
144
+ * per mark behind it. Changes exactly as `at` reaches or leaves a
145
+ * mark. */
146
+ export function crossings(marks, at) {
147
+ let count = 0;
148
+ for (const mark of marks) count += mark < at ? 2 : mark === at ? 1 : 0;
149
+ return count;
150
+ }
151
+ /** The thumb at `value` by the mark at `at`, along the way `fill`
152
+ * grows: `held` on it or up to `hold` past it. */
153
+ export function tickReach(value, at, hold, fill) {
154
+ const past = fill === "start" ? value - at : at - value;
155
+ return past < 0 ? "ahead" : past <= hold ? "held" : "past";
156
+ }
157
+ /** `a`'s runs outside `b`. */
158
+ function spanMinus([a0, a1], [b0, b1]) {
159
+ return [[a0, Math.min(a1, b0)], [Math.max(a0, b1), a1]].filter(([lo, hi]) => hi > lo);
160
+ }
161
+ /** The fill over `shown`, split by what a press leaving `predicted`
162
+ * keeps, shrinks, and grows. */
163
+ export function rangeSegments(shown, predicted) {
164
+ if (predicted === null) return [{
165
+ change: "kept",
166
+ span: shown
167
+ }];
168
+ const kept = [Math.max(shown[0], predicted[0]), Math.min(shown[1], predicted[1])];
169
+ return [...(kept[1] > kept[0] ? [{
170
+ change: "kept",
171
+ span: kept
172
+ }] : []), ...spanMinus(shown, predicted).map(span => ({
173
+ change: "shrink",
174
+ span
175
+ })), ...spanMinus(predicted, shown).map(span => ({
176
+ change: "grow",
177
+ span
178
+ }))];
179
+ }
@@ -0,0 +1,77 @@
1
+ import type { Slider as SliderPrimitive } from "@base-ui/react/slider";
2
+ import { type ScrubberStep, type ScrubberTick } from "./scrubber-state.ts";
3
+ import type { SliderRootProps, SliderThumbProps } from "./slider.tsx";
4
+ import { type SliderParts } from "./slider-parts.ts";
5
+ /** What moved the value: Base UI's reasons, `track-press` as the
6
+ * scrubber lands a press, `keyboard` as it lands a thumb key while
7
+ * playing, or `modifier` as Shift toggles snapping mid-drag. */
8
+ export type ScrubberChangeDetails = Exclude<SliderPrimitive.Root.ChangeEventDetails, {
9
+ reason: "track-press";
10
+ }> | {
11
+ reason: "track-press";
12
+ event: PointerEvent;
13
+ } | {
14
+ reason: "keyboard";
15
+ event: KeyboardEvent;
16
+ } | {
17
+ reason: "modifier";
18
+ event: KeyboardEvent;
19
+ };
20
+ /** Where a thumb key steps from the playhead: between the ticks, or
21
+ * through the step handler. */
22
+ type ScrubberStepping = {
23
+ playhead?: null;
24
+ /** Landings snap to these; arrows step between them. */
25
+ ticks?: readonly ScrubberTick[];
26
+ /** Arrows, Page keys, and input changes (assistive tech adjusting
27
+ * the thumb) step through this in place of the ticks: it lands
28
+ * the step itself. Home and End land on the ends regardless. */
29
+ onStep?: (step: ScrubberStep) => void;
30
+ } | {
31
+ /** The shown value, read every frame while set (playback): the
32
+ * thumb and fill glide with it (`SliderRoot` `glide`); `value`
33
+ * stays the spoken one. Thumb keys, input changes, and presses
34
+ * move from it. */
35
+ playhead: (() => number) | null;
36
+ ticks: readonly ScrubberTick[];
37
+ onStep?: (step: ScrubberStep) => void;
38
+ } | {
39
+ playhead: (() => number) | null;
40
+ ticks?: readonly ScrubberTick[];
41
+ onStep: (step: ScrubberStep) => void;
42
+ };
43
+ /** `Scrubber` props: a single-thumb slider's, over ticks. */
44
+ export type ScrubberProps = Omit<SliderRootProps<number>, "value" | "defaultValue" | "onValueChange" | "onValueCommitted" | "children" | "fill" | "minStepsBetweenValues" | "thumbCollisionBehavior"> & {
45
+ value: number;
46
+ /** Lands at `value`: presses, drags, input changes, and steps. */
47
+ onValueChange: (value: number, details: ScrubberChangeDetails) => void;
48
+ /** How near a tick a landing snaps to it, as a share of the range; 0
49
+ * lands on ticks alone. A mark holds the thumb as far past it
50
+ * (`data-reach`); playing, the thumb passes marks. */
51
+ snapShare?: number;
52
+ /** A value shown ahead of a pick elsewhere: the thumb there,
53
+ * tentative (`data-tentative`), its tick lit. */
54
+ tentative?: number | null;
55
+ /** The pointer previews its press: the fill it keeps, shrinks, and
56
+ * grows, and the tick it lands on. */
57
+ pointerPreview?: boolean;
58
+ /** The run the range fills. */
59
+ fill?: "start" | "end";
60
+ /** The spoken value at `value`, given the names of the latest tick at
61
+ * or before it; by default the names of the ticks at `value` as a
62
+ * conjunction in the messages locale, else the formatted value. */
63
+ valueText?: (value: number, names: readonly string[]) => string;
64
+ /** The thumb's props (accessible name); its ref, style, and focus
65
+ * handlers merge with the scrubber's own. */
66
+ thumb?: Omit<SliderThumbProps, "index" | "getAriaValueText">;
67
+ /** The parts drawn; the unstyled ones by default. */
68
+ parts?: SliderParts;
69
+ } & ScrubberStepping;
70
+ /** A single-thumb slider over ticks: landings snap to them, arrows
71
+ * step between them, marks draw always, stops near the pointer. The
72
+ * pointer previews its press; Shift lands at the pointer. State shows
73
+ * as `data-tentative` on the root, `data-change` on the fill's
74
+ * segments (`kept`, `shrink`, `grow`), `data-light` and `data-reach`
75
+ * on ticks, and `data-hover` on the thumb. */
76
+ export declare function Scrubber({ value, onValueChange, ticks, snapShare, tentative, playhead, pointerPreview, fill, onStep, valueText, thumb, parts, onDraggingChange, min, max, disabled, onKeyDownCapture, ...props }: ScrubberProps): import("react/jsx-runtime").JSX.Element;
77
+ export {};