@astralarium/repo-ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/dev-route.d.ts +29 -0
  4. package/dist/dev-route.js +15 -0
  5. package/dist/error-boundary.d.ts +29 -0
  6. package/dist/error-boundary.js +30 -0
  7. package/dist/index.d.ts +16 -0
  8. package/dist/index.js +16 -0
  9. package/dist/l10n/repo-ui/en.json +10 -0
  10. package/dist/page-tree.d.ts +25 -0
  11. package/dist/page-tree.js +45 -0
  12. package/dist/slider/index.d.ts +8 -0
  13. package/dist/slider/index.js +7 -0
  14. package/dist/slider/scrubber-state.d.ts +118 -0
  15. package/dist/slider/scrubber-state.js +179 -0
  16. package/dist/slider/scrubber.d.ts +77 -0
  17. package/dist/slider/scrubber.js +664 -0
  18. package/dist/slider/slider-parts.d.ts +16 -0
  19. package/dist/slider/slider-parts.js +11 -0
  20. package/dist/slider/slider-state.d.ts +75 -0
  21. package/dist/slider/slider-state.js +183 -0
  22. package/dist/slider/slider.d.ts +100 -0
  23. package/dist/slider/slider.js +966 -0
  24. package/dist/slider/slider.messages.d.json.ts +8 -0
  25. package/dist/slider/slider.messages.json +10 -0
  26. package/dist/slider/timeline-scrubber-state.d.ts +13 -0
  27. package/dist/slider/timeline-scrubber-state.js +21 -0
  28. package/dist/slider/timeline-scrubber.d.ts +26 -0
  29. package/dist/slider/timeline-scrubber.js +142 -0
  30. package/dist/sorted.d.ts +16 -0
  31. package/dist/sorted.js +40 -0
  32. package/dist/status.d.ts +9 -0
  33. package/dist/status.js +55 -0
  34. package/dist/storage.d.ts +21 -0
  35. package/dist/storage.js +42 -0
  36. package/dist/testkit/index.d.ts +12 -0
  37. package/dist/testkit/index.js +42 -0
  38. package/dist/theme/index.d.ts +3 -0
  39. package/dist/theme/index.js +3 -0
  40. package/dist/theme/theme-context.d.ts +11 -0
  41. package/dist/theme/theme-context.js +68 -0
  42. package/dist/theme/theme.d.ts +28 -0
  43. package/dist/theme/theme.js +37 -0
  44. package/dist/theme/themed-component.d.ts +8 -0
  45. package/dist/theme/themed-component.js +10 -0
  46. package/dist/timeline.d.ts +34 -0
  47. package/dist/timeline.js +47 -0
  48. package/dist/tree/index.d.ts +2 -0
  49. package/dist/tree/index.js +1 -0
  50. package/dist/tree/tree.d.ts +27 -0
  51. package/dist/tree/tree.js +299 -0
  52. package/dist/tree/use-tree.d.ts +38 -0
  53. package/dist/tree/use-tree.js +155 -0
  54. package/dist/use-copy-feedback.d.ts +15 -0
  55. package/dist/use-copy-feedback.js +72 -0
  56. package/dist/use-frame.d.ts +3 -0
  57. package/dist/use-frame.js +37 -0
  58. package/dist/use-hover-focus.d.ts +18 -0
  59. package/dist/use-hover-focus.js +87 -0
  60. package/dist/use-media-query.d.ts +5 -0
  61. package/dist/use-media-query.js +42 -0
  62. package/dist/use-pausable-hover.d.ts +27 -0
  63. package/dist/use-pausable-hover.js +100 -0
  64. package/dist/use-pulse-style.d.ts +3 -0
  65. package/dist/use-pulse-style.js +19 -0
  66. package/l10n.json +10 -0
  67. package/package.json +84 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Astralarium
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # `@astralarium/repo-ui`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Headless React UI following Base UI patterns.
4
+
5
+ App setup:
6
+
7
+ - `themeScript(cookie)` in `<head>` to apply theme pre-paint.
@@ -0,0 +1,29 @@
1
+ import { type RouteComponent } from "@tanstack/react-router";
2
+ import type { ReactNode } from "react";
3
+ /** A dev page module: its default export renders the page. */
4
+ export interface DevPageModule {
5
+ default: () => ReactNode;
6
+ }
7
+ /**
8
+ * The page module's loader, or `false` once the build compiled it out.
9
+ * Call sites write `import.meta.env.DEV && (() => import("~dev/…"))`: the
10
+ * literal guard satisfies `dev-only/imports`, and `vite build` folds it
11
+ * to `false`.
12
+ */
13
+ export type DevPageLoad = false | (() => Promise<DevPageModule>);
14
+ /** How a dev page renders. */
15
+ export interface DevRouteOptions {
16
+ /** Render in the browser only (`ssr: false`): canvas scenes. */
17
+ clientOnly?: boolean;
18
+ }
19
+ /** TanStack route options; spread beside `validateSearch` and the like. */
20
+ export interface DevRoute {
21
+ /** Throws `notFound()` where the page was compiled out. */
22
+ beforeLoad: () => void;
23
+ /** The lazily loaded page; renders nothing where compiled out. */
24
+ component: RouteComponent;
25
+ /** `false` with `clientOnly`; absent otherwise. */
26
+ ssr?: false;
27
+ }
28
+ /** Route options for the dev page `load` loads; a 404 where compiled out. */
29
+ export declare function devRoute(load: DevPageLoad, { clientOnly }?: DevRouteOptions): DevRoute;
@@ -0,0 +1,15 @@
1
+ import { lazyRouteComponent, notFound } from "@tanstack/react-router";
2
+ /** Route options for the dev page `load` loads; a 404 where compiled out. */
3
+ export function devRoute(load, {
4
+ clientOnly = false
5
+ } = {}) {
6
+ return {
7
+ beforeLoad: () => {
8
+ if (!load) throw notFound();
9
+ },
10
+ component: load ? lazyRouteComponent(load) : () => null,
11
+ ...(clientOnly ? {
12
+ ssr: false
13
+ } : {})
14
+ };
15
+ }
@@ -0,0 +1,29 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
+ /** `ErrorBoundary` props. */
3
+ export interface ErrorBoundaryProps {
4
+ /** A change clears a caught fault and renders `children` afresh. */
5
+ resetKey: string;
6
+ /** Shown in place of `children` after a fault. */
7
+ fallback: ReactNode;
8
+ /** Reports each caught fault. */
9
+ onCatch?: (error: unknown, info: ErrorInfo) => void;
10
+ children: ReactNode;
11
+ }
12
+ interface ErrorBoundaryState {
13
+ /** The caught fault, boxed: a thrown value may be falsy. */
14
+ caught: {
15
+ error: unknown;
16
+ } | null;
17
+ /** The `resetKey` the fault belongs to. */
18
+ resetKey: string;
19
+ }
20
+ /** Catches render faults below, showing `fallback` until `resetKey`
21
+ * changes. */
22
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
23
+ state: ErrorBoundaryState;
24
+ static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState>;
25
+ static getDerivedStateFromProps(props: ErrorBoundaryProps, state: ErrorBoundaryState): Partial<ErrorBoundaryState> | null;
26
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
27
+ render(): ReactNode;
28
+ }
29
+ export {};
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { Component } from "react";
4
+ /** Catches render faults below, showing `fallback` until `resetKey`
5
+ * changes. */
6
+ export class ErrorBoundary extends Component {
7
+ state = {
8
+ caught: null,
9
+ resetKey: this.props.resetKey
10
+ };
11
+ static getDerivedStateFromError(error) {
12
+ return {
13
+ caught: {
14
+ error
15
+ }
16
+ };
17
+ }
18
+ static getDerivedStateFromProps(props, state) {
19
+ return props.resetKey === state.resetKey ? null : {
20
+ caught: null,
21
+ resetKey: props.resetKey
22
+ };
23
+ }
24
+ componentDidCatch(error, info) {
25
+ this.props.onCatch?.(error, info);
26
+ }
27
+ render() {
28
+ return this.state.caught ? this.props.fallback : this.props.children;
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ /** Headless React building blocks: theme, tree, page tree, timeline, sorted
2
+ * lookups, status, error boundary, storage and small hooks. */
3
+ export * from "./error-boundary.tsx";
4
+ export * from "./page-tree.ts";
5
+ export * from "./sorted.ts";
6
+ export * from "./status.tsx";
7
+ export * from "./storage.ts";
8
+ export * from "./theme/index.ts";
9
+ export * from "./timeline.ts";
10
+ export * from "./tree/index.ts";
11
+ export * from "./use-copy-feedback.ts";
12
+ export * from "./use-frame.ts";
13
+ export * from "./use-hover-focus.ts";
14
+ export * from "./use-media-query.ts";
15
+ export * from "./use-pausable-hover.ts";
16
+ export * from "./use-pulse-style.ts";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /** Headless React building blocks: theme, tree, page tree, timeline, sorted
2
+ * lookups, status, error boundary, storage and small hooks. */
3
+ export * from "./error-boundary.js";
4
+ export * from "./page-tree.js";
5
+ export * from "./sorted.js";
6
+ export * from "./status.js";
7
+ export * from "./storage.js";
8
+ export * from "./theme/index.js";
9
+ export * from "./timeline.js";
10
+ export * from "./tree/index.js";
11
+ export * from "./use-copy-feedback.js";
12
+ export * from "./use-frame.js";
13
+ export * from "./use-hover-focus.js";
14
+ export * from "./use-media-query.js";
15
+ export * from "./use-pausable-hover.js";
16
+ export * from "./use-pulse-style.js";
@@ -0,0 +1,10 @@
1
+ {
2
+ "@astralarium/repo-ui/slider/slider.messages#rangeEnd": {
3
+ "defaultMessage": "{value}, range end",
4
+ "description": "Spoken value of a range slider's last thumb. Shows: screen readers, sliders with several thumbs. Arguments: value (string) the thumb's formatted value."
5
+ },
6
+ "@astralarium/repo-ui/slider/slider.messages#rangeStart": {
7
+ "defaultMessage": "{value}, range start",
8
+ "description": "Spoken value of a range slider's first thumb. Shows: screen readers, sliders with several thumbs. Arguments: value (string) the thumb's formatted value."
9
+ }
10
+ }
@@ -0,0 +1,25 @@
1
+ /** The router's static paths as a tree, and a breadcrumb trail through it. */
2
+ /** A static path: a page, an ancestor of pages, or both. */
3
+ export interface PageNode {
4
+ /** The path's last segment; empty at the root. */
5
+ segment: string;
6
+ /** The path from the root. */
7
+ path: string;
8
+ /** A page serves `path`. */
9
+ page: boolean;
10
+ /** Static paths one segment deeper, in first-given order. */
11
+ children: PageNode[];
12
+ }
13
+ /** One path segment of the trail. */
14
+ export interface Crumb {
15
+ /** The path through this segment. */
16
+ path: string;
17
+ /** The static paths beside `path`, itself included; only itself for
18
+ * the trail's first crumb; empty off the tree. */
19
+ siblings: PageNode[];
20
+ }
21
+ /** `paths` as a tree rooted at `/`. Paths with a param or splat segment
22
+ * (`$`) are left out. */
23
+ export declare function pageTree(paths: Iterable<string>): PageNode;
24
+ /** `pathname`'s segments, located in `tree`. */
25
+ export declare function crumbsOf(pathname: string, tree: PageNode): Crumb[];
@@ -0,0 +1,45 @@
1
+ /** The router's static paths as a tree, and a breadcrumb trail through it. */
2
+ /** `paths` as a tree rooted at `/`. Paths with a param or splat segment
3
+ * (`$`) are left out. */
4
+ export function pageTree(paths) {
5
+ const root = {
6
+ segment: "",
7
+ path: "/",
8
+ page: false,
9
+ children: []
10
+ };
11
+ for (const path of paths) {
12
+ const segments = path.split("/").filter(Boolean);
13
+ if (segments.some(segment => segment.includes("$"))) continue;
14
+ let node = root;
15
+ for (const segment of segments) {
16
+ let child = node.children.find(c => c.segment === segment);
17
+ if (!child) {
18
+ child = {
19
+ segment,
20
+ path: `${node === root ? "" : node.path}/${segment}`,
21
+ page: false,
22
+ children: []
23
+ };
24
+ node.children.push(child);
25
+ }
26
+ node = child;
27
+ }
28
+ node.page = true;
29
+ }
30
+ return root;
31
+ }
32
+ /** `pathname`'s segments, located in `tree`. */
33
+ export function crumbsOf(pathname, tree) {
34
+ const segments = pathname.split("/").filter(Boolean);
35
+ let parent;
36
+ let node = tree;
37
+ return segments.map((segment, i) => {
38
+ parent = node;
39
+ node = parent?.children.find(c => c.segment === segment);
40
+ return {
41
+ path: `/${segments.slice(0, i + 1).join("/")}`,
42
+ siblings: !node ? [] : i > 0 && parent ? parent.children : [node]
43
+ };
44
+ });
45
+ }
@@ -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 {};