@primitiv-ui/core 0.1.59 → 0.1.61

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.
@@ -0,0 +1,13 @@
1
+ /** A checkbox's state: checked, unchecked, or `"indeterminate"` (mixed). */
2
+ export type CheckboxCardCheckedState = boolean | "indeterminate";
3
+ /** Everything a checkbox card's toggle depends on, as plain data. */
4
+ export type CheckboxCardState = {
5
+ checked: CheckboxCardCheckedState;
6
+ disabled: boolean;
7
+ };
8
+ /**
9
+ * The state a press on a checkbox card asks for: the opposite of checked, with
10
+ * a mixed card becoming checked — a press always resolves the mix. `null` while
11
+ * the card is disabled, so no change is reported.
12
+ */
13
+ export declare function checkboxCardToggle(state: CheckboxCardState): boolean | null;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The state a press on a checkbox card asks for: the opposite of checked, with
3
+ * a mixed card becoming checked — a press always resolves the mix. `null` while
4
+ * the card is disabled, so no change is reported.
5
+ */
6
+ export function checkboxCardToggle(state) {
7
+ if (state.disabled)
8
+ return null;
9
+ return state.checked === "indeterminate" ? true : !state.checked;
10
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,10 @@ export { resolveRovingTarget, type ResolveRovingTargetOptions, } from "./keyboar
4
4
  export { tabsClick, tabsKeydown, tabsTabStop, type TabsActivationMode, type TabsKeyOutcome, type TabsOrientation, type TabsReadingDirection, type TabsState, } from "./tabs/policy.js";
5
5
  export { accordionKeydown, accordionToggle, type AccordionKeyOutcome, type AccordionOrientation, type AccordionReadingDirection, type AccordionState, } from "./accordion/policy.js";
6
6
  export { toggleGroupKeydown, toggleGroupTabStop, toggleGroupToggle, type ToggleGroupKeyOutcome, type ToggleGroupOrientation, type ToggleGroupReadingDirection, type ToggleGroupState, } from "./toggle-group/policy.js";
7
+ export { radioGroupKeydown, radioGroupSelect, radioGroupTabStop, type RadioGroupKeyOutcome, type RadioGroupOrientation, type RadioGroupReadingDirection, type RadioGroupState, } from "./radio-group/policy.js";
8
+ export { segmentedControlKeydown, segmentedControlSelect, segmentedControlTabStop, type SegmentedControlKeyOutcome, type SegmentedControlOrientation, type SegmentedControlReadingDirection, type SegmentedControlState, } from "./segmented-control/policy.js";
9
+ export { checkboxCardToggle, type CheckboxCardCheckedState, type CheckboxCardState, } from "./checkbox-card/policy.js";
10
+ export { sliderClosestThumb, sliderEdge, sliderKeyValue, sliderPercent, sliderPointerValue, sliderRangeInsets, sliderSetThumb, sliderSnap, sliderThumbInset, type SliderAxis, type SliderEdge, type SliderInset, type SliderOrientation, type SliderReadingDirection, type SliderScale, } from "./slider/policy.js";
7
11
  export { collapsibleKeydown, collapsibleToggle, type CollapsibleKeyOutcome, type CollapsibleState, } from "./collapsible/policy.js";
8
12
  export { FOCUSABLE_SELECTOR } from "./focus/focusable.js";
9
13
  export { MENU_ITEM_SELECTOR, MENU_TYPEAHEAD_RESET_MS, menuKeydown, menuTypeahead, type MenuKeyOutcome, type MenuKeyState, type MenuTypeaheadOutcome, type MenuTypeaheadState, } from "./menu/policy.js";
package/dist/index.js CHANGED
@@ -4,6 +4,10 @@ export { resolveRovingTarget, } from "./keyboard/resolveRovingTarget.js";
4
4
  export { tabsClick, tabsKeydown, tabsTabStop, } from "./tabs/policy.js";
5
5
  export { accordionKeydown, accordionToggle, } from "./accordion/policy.js";
6
6
  export { toggleGroupKeydown, toggleGroupTabStop, toggleGroupToggle, } from "./toggle-group/policy.js";
7
+ export { radioGroupKeydown, radioGroupSelect, radioGroupTabStop, } from "./radio-group/policy.js";
8
+ export { segmentedControlKeydown, segmentedControlSelect, segmentedControlTabStop, } from "./segmented-control/policy.js";
9
+ export { checkboxCardToggle, } from "./checkbox-card/policy.js";
10
+ export { sliderClosestThumb, sliderEdge, sliderKeyValue, sliderPercent, sliderPointerValue, sliderRangeInsets, sliderSetThumb, sliderSnap, sliderThumbInset, } from "./slider/policy.js";
7
11
  export { collapsibleKeydown, collapsibleToggle, } from "./collapsible/policy.js";
8
12
  export { FOCUSABLE_SELECTOR } from "./focus/focusable.js";
9
13
  export { MENU_ITEM_SELECTOR, MENU_TYPEAHEAD_RESET_MS, menuKeydown, menuTypeahead, } from "./menu/policy.js";
@@ -0,0 +1,52 @@
1
+ /** Which arrow pairs move between items: one axis, or `"both"` for all four
2
+ * arrows (a grid of cards that wraps). */
3
+ export type RadioGroupOrientation = "horizontal" | "vertical" | "both";
4
+ /** Reading direction. Under `"rtl"` the horizontal arrows are mirrored. */
5
+ export type RadioGroupReadingDirection = "ltr" | "rtl";
6
+ /**
7
+ * Everything a radio group's selection and keyboard rules depend on, as plain
8
+ * data — shared by every single-select radio-group component (SegmentedControl,
9
+ * RadioCard). Each surface builds it from wherever it keeps state and applies
10
+ * the outcome back the same way. A whole-group disable is every value in
11
+ * `disabled`.
12
+ */
13
+ export type RadioGroupState = {
14
+ /** Every item's value, in DOM order, disabled ones included. */
15
+ values: readonly string[];
16
+ /** The values of the disabled items. */
17
+ disabled: ReadonlySet<string>;
18
+ /** The selected item's value, or `undefined` while none is selected. */
19
+ selected: string | undefined;
20
+ orientation: RadioGroupOrientation;
21
+ dir: RadioGroupReadingDirection;
22
+ };
23
+ /** What a key press on an item asks for: an item to focus, and the item to
24
+ * select — `null` when it is already selected, so no change is reported. */
25
+ export type RadioGroupKeyOutcome = {
26
+ focus: string;
27
+ select: string | null;
28
+ };
29
+ /**
30
+ * The item a click on `value` selects: `null` when it is disabled or already
31
+ * selected. A selected item never lets go, so the selection cannot be cleared
32
+ * to nothing.
33
+ */
34
+ export declare function radioGroupSelect(state: Pick<RadioGroupState, "disabled" | "selected">, value: string): string | null;
35
+ /**
36
+ * The WAI-ARIA radio group's keyboard contract for a key press on the item
37
+ * `focused`: the arrows follow `orientation` (all four under `"both"`), the
38
+ * horizontal pair mirrored under RTL, wrap at the ends and skip disabled items,
39
+ * and each one moves focus **and** selection. While focus sits on a disabled
40
+ * item they do nothing. There is no Home / End, and Enter / Space are the
41
+ * native button's click.
42
+ *
43
+ * Returns `null` for a key the group does not handle, which tells the caller to
44
+ * leave the event alone (no `preventDefault`).
45
+ */
46
+ export declare function radioGroupKeydown(state: RadioGroupState, focused: string, key: string): RadioGroupKeyOutcome | null;
47
+ /**
48
+ * The item that takes part in the page's Tab order (the roving tab stop): the
49
+ * selected item, else the first enabled one so the group is still reachable by
50
+ * keyboard — or none, while nothing is selected and every item is disabled.
51
+ */
52
+ export declare function radioGroupTabStop(state: Pick<RadioGroupState, "values" | "disabled" | "selected">): string | undefined;
@@ -0,0 +1,35 @@
1
+ import { getKeyToActionMap } from "../keyboard/getKeyToActionMap.js";
2
+ import { resolveRovingTarget } from "../keyboard/resolveRovingTarget.js";
3
+ /**
4
+ * The item a click on `value` selects: `null` when it is disabled or already
5
+ * selected. A selected item never lets go, so the selection cannot be cleared
6
+ * to nothing.
7
+ */
8
+ export function radioGroupSelect(state, value) {
9
+ return state.disabled.has(value) || state.selected === value ? null : value;
10
+ }
11
+ /**
12
+ * The WAI-ARIA radio group's keyboard contract for a key press on the item
13
+ * `focused`: the arrows follow `orientation` (all four under `"both"`), the
14
+ * horizontal pair mirrored under RTL, wrap at the ends and skip disabled items,
15
+ * and each one moves focus **and** selection. While focus sits on a disabled
16
+ * item they do nothing. There is no Home / End, and Enter / Space are the
17
+ * native button's click.
18
+ *
19
+ * Returns `null` for a key the group does not handle, which tells the caller to
20
+ * leave the event alone (no `preventDefault`).
21
+ */
22
+ export function radioGroupKeydown(state, focused, key) {
23
+ const action = getKeyToActionMap({ orientation: state.orientation, dir: state.dir })[key];
24
+ const navigable = state.values.filter((value) => !state.disabled.has(value));
25
+ const result = resolveRovingTarget({ action, navigable, currentKey: focused });
26
+ return result && { focus: result.target, select: radioGroupSelect(state, result.target) };
27
+ }
28
+ /**
29
+ * The item that takes part in the page's Tab order (the roving tab stop): the
30
+ * selected item, else the first enabled one so the group is still reachable by
31
+ * keyboard — or none, while nothing is selected and every item is disabled.
32
+ */
33
+ export function radioGroupTabStop(state) {
34
+ return state.selected ?? state.values.find((value) => !state.disabled.has(value));
35
+ }
@@ -0,0 +1,51 @@
1
+ /** Layout axis of the control: which arrow pair moves between segments. */
2
+ export type SegmentedControlOrientation = "horizontal" | "vertical";
3
+ /** Reading direction. Under `"rtl"` the horizontal arrows are mirrored. */
4
+ export type SegmentedControlReadingDirection = "ltr" | "rtl";
5
+ /**
6
+ * Everything SegmentedControl's selection and keyboard rules depend on, as
7
+ * plain data. Each surface builds it from wherever it keeps state — React
8
+ * state, or a custom element's attributes — and applies the outcome back the
9
+ * same way. A whole-control disable is every value in `disabled`.
10
+ */
11
+ export type SegmentedControlState = {
12
+ /** Every segment's value, in DOM order, disabled ones included. */
13
+ values: readonly string[];
14
+ /** The values of the disabled segments. */
15
+ disabled: ReadonlySet<string>;
16
+ /** The selected segment's value, or `undefined` while none is selected. */
17
+ selected: string | undefined;
18
+ orientation: SegmentedControlOrientation;
19
+ dir: SegmentedControlReadingDirection;
20
+ };
21
+ /** What a key press on a segment asks for: a segment to focus, and the segment
22
+ * to select — `null` when it is already selected, so no change is reported. */
23
+ export type SegmentedControlKeyOutcome = {
24
+ focus: string;
25
+ select: string | null;
26
+ };
27
+ /**
28
+ * The segment a click on `value` selects: `null` when it is disabled or
29
+ * already selected. A selected segment never lets go, so — unlike a toggle
30
+ * group — the selection cannot be cleared to nothing. The radio group's rule.
31
+ */
32
+ export declare function segmentedControlSelect(state: Pick<SegmentedControlState, "disabled" | "selected">, value: string): string | null;
33
+ /**
34
+ * SegmentedControl's keyboard contract — the WAI-ARIA radio group's — for a key
35
+ * press on the segment `focused`: the arrows follow `orientation`, mirrored
36
+ * under RTL, wrap at the ends and skip disabled segments, and each one moves
37
+ * focus **and** selection. While focus sits on a disabled segment they do
38
+ * nothing. There is no Home / End, and Enter / Space are the native button's
39
+ * click.
40
+ *
41
+ * Returns `null` for a key the control does not handle, which tells the caller
42
+ * to leave the event alone (no `preventDefault`).
43
+ */
44
+ export declare function segmentedControlKeydown(state: SegmentedControlState, focused: string, key: string): SegmentedControlKeyOutcome | null;
45
+ /**
46
+ * The segment that takes part in the page's Tab order (the roving tab stop):
47
+ * the selected segment, else the first enabled one so the control is still
48
+ * reachable by keyboard — or none, while nothing is selected and every segment
49
+ * is disabled.
50
+ */
51
+ export declare function segmentedControlTabStop(state: Pick<SegmentedControlState, "values" | "disabled" | "selected">): string | undefined;
@@ -0,0 +1,32 @@
1
+ import { radioGroupKeydown, radioGroupSelect, radioGroupTabStop } from "../radio-group/policy.js";
2
+ /**
3
+ * The segment a click on `value` selects: `null` when it is disabled or
4
+ * already selected. A selected segment never lets go, so — unlike a toggle
5
+ * group — the selection cannot be cleared to nothing. The radio group's rule.
6
+ */
7
+ export function segmentedControlSelect(state, value) {
8
+ return radioGroupSelect(state, value);
9
+ }
10
+ /**
11
+ * SegmentedControl's keyboard contract — the WAI-ARIA radio group's — for a key
12
+ * press on the segment `focused`: the arrows follow `orientation`, mirrored
13
+ * under RTL, wrap at the ends and skip disabled segments, and each one moves
14
+ * focus **and** selection. While focus sits on a disabled segment they do
15
+ * nothing. There is no Home / End, and Enter / Space are the native button's
16
+ * click.
17
+ *
18
+ * Returns `null` for a key the control does not handle, which tells the caller
19
+ * to leave the event alone (no `preventDefault`).
20
+ */
21
+ export function segmentedControlKeydown(state, focused, key) {
22
+ return radioGroupKeydown(state, focused, key);
23
+ }
24
+ /**
25
+ * The segment that takes part in the page's Tab order (the roving tab stop):
26
+ * the selected segment, else the first enabled one so the control is still
27
+ * reachable by keyboard — or none, while nothing is selected and every segment
28
+ * is disabled.
29
+ */
30
+ export function segmentedControlTabStop(state) {
31
+ return radioGroupTabStop(state);
32
+ }
@@ -0,0 +1,77 @@
1
+ /** Layout axis of the slider. */
2
+ export type SliderOrientation = "horizontal" | "vertical";
3
+ /** Reading direction. Under `"rtl"` a horizontal slider runs right to left. */
4
+ export type SliderReadingDirection = "ltr" | "rtl";
5
+ /** Which way a slider runs: its axis, reading direction, and whether it is inverted. */
6
+ export type SliderAxis = {
7
+ orientation: SliderOrientation;
8
+ dir: SliderReadingDirection;
9
+ /** Run from the opposite end: max at the start edge. */
10
+ inverted: boolean;
11
+ };
12
+ /** The values a slider takes: `min` to `max` in `step`s. */
13
+ export type SliderScale = {
14
+ min: number;
15
+ max: number;
16
+ step: number;
17
+ };
18
+ /** A physical edge of the track. */
19
+ export type SliderEdge = "left" | "right" | "top" | "bottom";
20
+ /** A distance from one edge of the track, as a percentage of its length. */
21
+ export type SliderInset = {
22
+ edge: SliderEdge;
23
+ percent: number;
24
+ };
25
+ /** `value` rounded to the nearest step counted from `min`, at the step's own precision. */
26
+ export declare function sliderSnap(value: number, min: number, step: number): number;
27
+ /** Where `value` sits between `min` and `max`, as a percentage clamped to 0–100. */
28
+ export declare function sliderPercent(value: number, min: number, max: number): number;
29
+ /**
30
+ * The edge a slider measures from — where its `min` sits: the left (right under
31
+ * RTL) of a horizontal track, the bottom of a vertical one, and the opposite
32
+ * edge when inverted.
33
+ */
34
+ export declare function sliderEdge({ orientation, dir, inverted }: SliderAxis): SliderEdge;
35
+ /** Where a thumb at `value` sits: its distance from the start edge. */
36
+ export declare function sliderThumbInset(value: number, { min, max }: Pick<SliderScale, "min" | "max">, axis: SliderAxis): SliderInset;
37
+ /**
38
+ * Where the filled range sits: from the start edge to a single thumb, or between
39
+ * the outermost of several — as its two insets, from the start edge and from the
40
+ * opposite one.
41
+ */
42
+ export declare function sliderRangeInsets(values: readonly number[], { min, max }: Pick<SliderScale, "min" | "max">, axis: SliderAxis): {
43
+ start: SliderInset;
44
+ end: SliderInset;
45
+ };
46
+ /**
47
+ * The value under a pointer at `point` on a track whose box is `rect`: read
48
+ * along the axis from the start edge, clamped to the track, snapped to the step
49
+ * and kept within the scale.
50
+ */
51
+ export declare function sliderPointerValue(point: {
52
+ x: number;
53
+ y: number;
54
+ }, rect: {
55
+ left: number;
56
+ width: number;
57
+ bottom: number;
58
+ height: number;
59
+ }, { min, max, step, orientation, dir, inverted }: SliderScale & SliderAxis): number;
60
+ /**
61
+ * The WAI-ARIA slider keys for a thumb at `value`: the arrows step one `step`
62
+ * (up and toward the end increase, mirrored under RTL and when inverted),
63
+ * Page Up / Page Down ten, Home / End jump to `min` / `max`. The result is
64
+ * snapped to the step but not clamped — the thumb's own bounds do that. `null`
65
+ * for any other key.
66
+ */
67
+ export declare function sliderKeyValue(key: string, value: number, { min, max, step, orientation, dir, inverted }: SliderScale & SliderAxis): number | null;
68
+ /**
69
+ * Every thumb's value after moving thumb `index` to `value`: kept within the
70
+ * scale and between its neighbours, `minStepsBetweenThumbs` steps apart. `null`
71
+ * when the thumb would not move, so no change is reported.
72
+ */
73
+ export declare function sliderSetThumb(values: readonly number[], index: number, value: number, { min, max, step, minStepsBetweenThumbs }: SliderScale & {
74
+ minStepsBetweenThumbs: number;
75
+ }): number[] | null;
76
+ /** The index of the thumb nearest `value`, the first on a tie. */
77
+ export declare function sliderClosestThumb(value: number, values: readonly number[]): number;
@@ -0,0 +1,102 @@
1
+ const OPPOSITE = { left: "right", right: "left", top: "bottom", bottom: "top" };
2
+ function clamp(value, min, max) {
3
+ return Math.min(Math.max(value, min), max);
4
+ }
5
+ /** `value` rounded to the nearest step counted from `min`, at the step's own precision. */
6
+ export function sliderSnap(value, min, step) {
7
+ const snapped = min + Math.round((value - min) / step) * step;
8
+ const decimals = String(step).split(".")[1]?.length ?? 0;
9
+ return Number(snapped.toFixed(decimals));
10
+ }
11
+ /** Where `value` sits between `min` and `max`, as a percentage clamped to 0–100. */
12
+ export function sliderPercent(value, min, max) {
13
+ return clamp(((value - min) / (max - min)) * 100, 0, 100);
14
+ }
15
+ /**
16
+ * The edge a slider measures from — where its `min` sits: the left (right under
17
+ * RTL) of a horizontal track, the bottom of a vertical one, and the opposite
18
+ * edge when inverted.
19
+ */
20
+ export function sliderEdge({ orientation, dir, inverted }) {
21
+ const start = orientation === "vertical" ? "bottom" : dir === "rtl" ? "right" : "left";
22
+ return inverted ? OPPOSITE[start] : start;
23
+ }
24
+ /** Where a thumb at `value` sits: its distance from the start edge. */
25
+ export function sliderThumbInset(value, { min, max }, axis) {
26
+ return { edge: sliderEdge(axis), percent: sliderPercent(value, min, max) };
27
+ }
28
+ /**
29
+ * Where the filled range sits: from the start edge to a single thumb, or between
30
+ * the outermost of several — as its two insets, from the start edge and from the
31
+ * opposite one.
32
+ */
33
+ export function sliderRangeInsets(values, { min, max }, axis) {
34
+ const edge = sliderEdge(axis);
35
+ const percents = values.map((value) => sliderPercent(value, min, max));
36
+ const start = values.length > 1 ? Math.min(...percents) : 0;
37
+ return { start: { edge, percent: start }, end: { edge: OPPOSITE[edge], percent: 100 - Math.max(...percents) } };
38
+ }
39
+ /**
40
+ * The value under a pointer at `point` on a track whose box is `rect`: read
41
+ * along the axis from the start edge, clamped to the track, snapped to the step
42
+ * and kept within the scale.
43
+ */
44
+ export function sliderPointerValue(point, rect, { min, max, step, orientation, dir, inverted }) {
45
+ const vertical = orientation === "vertical";
46
+ const fromStart = vertical ? (rect.bottom - point.y) / rect.height : (point.x - rect.left) / rect.width;
47
+ const reversed = vertical ? inverted : (dir === "rtl") !== inverted;
48
+ const percent = clamp(reversed ? 1 - fromStart : fromStart, 0, 1);
49
+ return clamp(sliderSnap(min + percent * (max - min), min, step), min, max);
50
+ }
51
+ /**
52
+ * The WAI-ARIA slider keys for a thumb at `value`: the arrows step one `step`
53
+ * (up and toward the end increase, mirrored under RTL and when inverted),
54
+ * Page Up / Page Down ten, Home / End jump to `min` / `max`. The result is
55
+ * snapped to the step but not clamped — the thumb's own bounds do that. `null`
56
+ * for any other key.
57
+ */
58
+ export function sliderKeyValue(key, value, { min, max, step, orientation, dir, inverted }) {
59
+ if (key === "Home")
60
+ return min;
61
+ if (key === "End")
62
+ return max;
63
+ let increases;
64
+ if (key === "PageUp" || key === "PageDown") {
65
+ increases = key === "PageUp";
66
+ }
67
+ else if (key === "ArrowUp" || key === "ArrowDown") {
68
+ increases = (key === "ArrowUp") !== (orientation === "vertical" && inverted);
69
+ }
70
+ else if (key === "ArrowRight" || key === "ArrowLeft") {
71
+ const rightIncreases = orientation === "vertical" || (dir === "ltr") !== inverted;
72
+ increases = (key === "ArrowRight") === rightIncreases;
73
+ }
74
+ else {
75
+ return null;
76
+ }
77
+ const magnitude = key.startsWith("Page") ? step * 10 : step;
78
+ return sliderSnap(value + (increases ? magnitude : -magnitude), min, step);
79
+ }
80
+ /**
81
+ * Every thumb's value after moving thumb `index` to `value`: kept within the
82
+ * scale and between its neighbours, `minStepsBetweenThumbs` steps apart. `null`
83
+ * when the thumb would not move, so no change is reported.
84
+ */
85
+ export function sliderSetThumb(values, index, value, { min, max, step, minStepsBetweenThumbs }) {
86
+ const gap = minStepsBetweenThumbs * step;
87
+ const lower = index > 0 ? values[index - 1] + gap : min;
88
+ const upper = index < values.length - 1 ? values[index + 1] - gap : max;
89
+ const next = clamp(value, lower, upper);
90
+ if (values[index] === next)
91
+ return null;
92
+ return values.map((entry, i) => (i === index ? next : entry));
93
+ }
94
+ /** The index of the thumb nearest `value`, the first on a tie. */
95
+ export function sliderClosestThumb(value, values) {
96
+ let closest = 0;
97
+ values.forEach((thumb, index) => {
98
+ if (Math.abs(thumb - value) < Math.abs(values[closest] - value))
99
+ closest = index;
100
+ });
101
+ return closest;
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/core",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "The framework-free behaviour core shared by every Primitiv surface — keyboard maps, roving-focus targets, id derivation and per-component policy, as pure functions with no DOM and no dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",