@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.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/dev-route.d.ts +29 -0
- package/dist/dev-route.js +15 -0
- package/dist/error-boundary.d.ts +29 -0
- package/dist/error-boundary.js +30 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/l10n/repo-ui/en.json +10 -0
- package/dist/page-tree.d.ts +25 -0
- package/dist/page-tree.js +45 -0
- package/dist/slider/index.d.ts +8 -0
- package/dist/slider/index.js +7 -0
- package/dist/slider/scrubber-state.d.ts +118 -0
- package/dist/slider/scrubber-state.js +179 -0
- package/dist/slider/scrubber.d.ts +77 -0
- package/dist/slider/scrubber.js +664 -0
- package/dist/slider/slider-parts.d.ts +16 -0
- package/dist/slider/slider-parts.js +11 -0
- package/dist/slider/slider-state.d.ts +75 -0
- package/dist/slider/slider-state.js +183 -0
- package/dist/slider/slider.d.ts +100 -0
- package/dist/slider/slider.js +966 -0
- package/dist/slider/slider.messages.d.json.ts +8 -0
- package/dist/slider/slider.messages.json +10 -0
- package/dist/slider/timeline-scrubber-state.d.ts +13 -0
- package/dist/slider/timeline-scrubber-state.js +21 -0
- package/dist/slider/timeline-scrubber.d.ts +26 -0
- package/dist/slider/timeline-scrubber.js +142 -0
- package/dist/sorted.d.ts +16 -0
- package/dist/sorted.js +40 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.js +55 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.js +42 -0
- package/dist/testkit/index.d.ts +12 -0
- package/dist/testkit/index.js +42 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.js +3 -0
- package/dist/theme/theme-context.d.ts +11 -0
- package/dist/theme/theme-context.js +68 -0
- package/dist/theme/theme.d.ts +28 -0
- package/dist/theme/theme.js +37 -0
- package/dist/theme/themed-component.d.ts +8 -0
- package/dist/theme/themed-component.js +10 -0
- package/dist/timeline.d.ts +34 -0
- package/dist/timeline.js +47 -0
- package/dist/tree/index.d.ts +2 -0
- package/dist/tree/index.js +1 -0
- package/dist/tree/tree.d.ts +27 -0
- package/dist/tree/tree.js +299 -0
- package/dist/tree/use-tree.d.ts +38 -0
- package/dist/tree/use-tree.js +155 -0
- package/dist/use-copy-feedback.d.ts +15 -0
- package/dist/use-copy-feedback.js +72 -0
- package/dist/use-frame.d.ts +3 -0
- package/dist/use-frame.js +37 -0
- package/dist/use-hover-focus.d.ts +18 -0
- package/dist/use-hover-focus.js +87 -0
- package/dist/use-media-query.d.ts +5 -0
- package/dist/use-media-query.js +42 -0
- package/dist/use-pausable-hover.d.ts +27 -0
- package/dist/use-pausable-hover.js +100 -0
- package/dist/use-pulse-style.d.ts +3 -0
- package/dist/use-pulse-style.js +19 -0
- package/l10n.json +10 -0
- 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
|
-
#
|
|
1
|
+
# `@astralarium/repo-ui`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/dist/index.d.ts
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.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 {};
|