@intentface/ui 0.0.0-stage → 0.7.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 +147 -2
- package/dist/ask-user/ask-user.d.ts +104 -0
- package/dist/ask-user/ask-user.js +245 -0
- package/dist/ask-user/index.d.ts +3 -0
- package/dist/ask-user/index.js +3 -0
- package/dist/ask-user/index.parts.d.ts +1 -0
- package/dist/ask-user/index.parts.js +5 -0
- package/dist/attachments/attachments.d.ts +73 -0
- package/dist/attachments/attachments.js +121 -0
- package/dist/attachments/index.d.ts +3 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.parts.d.ts +1 -0
- package/dist/attachments/index.parts.js +5 -0
- package/dist/chip/chip.d.ts +45 -0
- package/dist/chip/chip.js +43 -0
- package/dist/chip/index.d.ts +2 -0
- package/dist/chip/index.js +2 -0
- package/dist/chip/index.parts.d.ts +1 -0
- package/dist/chip/index.parts.js +5 -0
- package/dist/chip-markdown.d.ts +25 -0
- package/dist/chip-markdown.js +54 -0
- package/dist/composer/actions.d.ts +35 -0
- package/dist/composer/actions.js +76 -0
- package/dist/composer/ask-user-machine.d.ts +52 -0
- package/dist/composer/ask-user-machine.js +164 -0
- package/dist/composer/attachments-machine.d.ts +34 -0
- package/dist/composer/attachments-machine.js +91 -0
- package/dist/composer/attachments.d.ts +16 -0
- package/dist/composer/attachments.js +32 -0
- package/dist/composer/command-list.d.ts +49 -0
- package/dist/composer/command-list.js +360 -0
- package/dist/composer/commands.d.ts +28 -0
- package/dist/composer/commands.js +56 -0
- package/dist/composer/container.d.ts +3 -0
- package/dist/composer/container.js +32 -0
- package/dist/composer/controller.d.ts +3 -0
- package/dist/composer/controller.js +6 -0
- package/dist/composer/editor-dom.d.ts +57 -0
- package/dist/composer/editor-dom.js +517 -0
- package/dist/composer/fuzzy.d.ts +4 -0
- package/dist/composer/fuzzy.js +49 -0
- package/dist/composer/index.d.ts +25 -0
- package/dist/composer/index.js +13 -0
- package/dist/composer/index.parts.d.ts +11 -0
- package/dist/composer/index.parts.js +15 -0
- package/dist/composer/internals.d.ts +30 -0
- package/dist/composer/internals.js +102 -0
- package/dist/composer/keyboard.d.ts +61 -0
- package/dist/composer/keyboard.js +75 -0
- package/dist/composer/panel.d.ts +28 -0
- package/dist/composer/panel.js +76 -0
- package/dist/composer/placeholder.d.ts +10 -0
- package/dist/composer/placeholder.js +8 -0
- package/dist/composer/popover.d.ts +12 -0
- package/dist/composer/popover.js +62 -0
- package/dist/composer/prefix-detection.d.ts +23 -0
- package/dist/composer/prefix-detection.js +67 -0
- package/dist/composer/root.d.ts +21 -0
- package/dist/composer/root.js +124 -0
- package/dist/composer/segments.d.ts +108 -0
- package/dist/composer/segments.js +366 -0
- package/dist/composer/store.d.ts +98 -0
- package/dist/composer/store.js +457 -0
- package/dist/composer/textarea.d.ts +39 -0
- package/dist/composer/textarea.js +64 -0
- package/dist/composer/trigger-tracker.d.ts +12 -0
- package/dist/composer/trigger-tracker.js +90 -0
- package/dist/composer/types.d.ts +127 -0
- package/dist/composer/types.js +4 -0
- package/dist/composer/use-composer-editor.d.ts +43 -0
- package/dist/composer/use-composer-editor.js +653 -0
- package/dist/generative/catalog.d.ts +177 -0
- package/dist/generative/catalog.js +20 -0
- package/dist/generative/index.d.ts +26 -0
- package/dist/generative/index.js +20 -0
- package/dist/generative/json-schema.d.ts +45 -0
- package/dist/generative/json-schema.js +216 -0
- package/dist/generative/markdown.d.ts +21 -0
- package/dist/generative/markdown.js +76 -0
- package/dist/generative/tree.d.ts +56 -0
- package/dist/generative/tree.js +365 -0
- package/dist/generative/use-tree.d.ts +12 -0
- package/dist/generative/use-tree.js +64 -0
- package/dist/internal/change-event-details.d.ts +12 -0
- package/dist/internal/change-event-details.js +16 -0
- package/dist/internal/collapsible.d.ts +22 -0
- package/dist/internal/collapsible.js +94 -0
- package/dist/internal/collection.d.ts +32 -0
- package/dist/internal/collection.js +72 -0
- package/dist/internal/iso-layout-effect.d.ts +2 -0
- package/dist/internal/iso-layout-effect.js +6 -0
- package/dist/internal/portal.d.ts +6 -0
- package/dist/internal/portal.js +18 -0
- package/dist/internal/primitive-props.d.ts +4 -0
- package/dist/internal/primitive-props.js +3 -0
- package/dist/internal/render/baseTypes.d.ts +18 -0
- package/dist/internal/render/baseTypes.js +4 -0
- package/dist/internal/render/empty.d.ts +3 -0
- package/dist/internal/render/empty.js +5 -0
- package/dist/internal/render/getReactElementRef.d.ts +5 -0
- package/dist/internal/render/getReactElementRef.js +15 -0
- package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
- package/dist/internal/render/getStateAttributesProps.js +23 -0
- package/dist/internal/render/mergeObjects.d.ts +1 -0
- package/dist/internal/render/mergeObjects.js +14 -0
- package/dist/internal/render/mergeProps.d.ts +55 -0
- package/dist/internal/render/mergeProps.js +183 -0
- package/dist/internal/render/positioning.d.ts +33 -0
- package/dist/internal/render/positioning.js +112 -0
- package/dist/internal/render/reactVersion.d.ts +3 -0
- package/dist/internal/render/reactVersion.js +7 -0
- package/dist/internal/render/resolveClassName.d.ts +8 -0
- package/dist/internal/render/resolveClassName.js +12 -0
- package/dist/internal/render/resolveStyle.d.ts +8 -0
- package/dist/internal/render/resolveStyle.js +12 -0
- package/dist/internal/render/transition.d.ts +47 -0
- package/dist/internal/render/transition.js +259 -0
- package/dist/internal/render/types.d.ts +74 -0
- package/dist/internal/render/types.js +3 -0
- package/dist/internal/render/useMergedRefs.d.ts +21 -0
- package/dist/internal/render/useMergedRefs.js +102 -0
- package/dist/internal/render/useRefWithInit.d.ts +10 -0
- package/dist/internal/render/useRefWithInit.js +12 -0
- package/dist/internal/render/useRender.d.ts +71 -0
- package/dist/internal/render/useRender.js +12 -0
- package/dist/internal/render/useRenderElement.d.ts +62 -0
- package/dist/internal/render/useRenderElement.js +157 -0
- package/dist/internal/render/warn.d.ts +1 -0
- package/dist/internal/render/warn.js +15 -0
- package/dist/internal/safe-polygon.d.ts +21 -0
- package/dist/internal/safe-polygon.js +179 -0
- package/dist/internal/scroll-into-view-within.d.ts +1 -0
- package/dist/internal/scroll-into-view-within.js +26 -0
- package/dist/internal/standard-schema.d.ts +38 -0
- package/dist/internal/standard-schema.js +4 -0
- package/dist/internal/state-mappings.d.ts +8 -0
- package/dist/internal/state-mappings.js +17 -0
- package/dist/internal/use-controlled.d.ts +19 -0
- package/dist/internal/use-controlled.js +33 -0
- package/dist/message/index.d.ts +3 -0
- package/dist/message/index.js +3 -0
- package/dist/message/index.parts.d.ts +1 -0
- package/dist/message/index.parts.js +5 -0
- package/dist/message/message.d.ts +58 -0
- package/dist/message/message.js +93 -0
- package/dist/message-utils.d.ts +55 -0
- package/dist/message-utils.js +79 -0
- package/dist/nav/index.d.ts +4 -0
- package/dist/nav/index.js +5 -0
- package/dist/nav/index.parts.d.ts +3 -0
- package/dist/nav/index.parts.js +7 -0
- package/dist/nav/nav.d.ts +152 -0
- package/dist/nav/nav.js +660 -0
- package/dist/nav/store.d.ts +50 -0
- package/dist/nav/store.js +95 -0
- package/dist/reasoning/index.d.ts +3 -0
- package/dist/reasoning/index.js +3 -0
- package/dist/reasoning/index.parts.d.ts +1 -0
- package/dist/reasoning/index.parts.js +5 -0
- package/dist/reasoning/reasoning.d.ts +38 -0
- package/dist/reasoning/reasoning.js +67 -0
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/index.parts.d.ts +3 -0
- package/dist/shell/index.parts.js +7 -0
- package/dist/shell/shell.d.ts +108 -0
- package/dist/shell/shell.js +390 -0
- package/dist/shell/store.d.ts +112 -0
- package/dist/shell/store.js +134 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +2 -0
- package/dist/steps/index.parts.d.ts +1 -0
- package/dist/steps/index.parts.js +5 -0
- package/dist/steps/steps.d.ts +90 -0
- package/dist/steps/steps.js +116 -0
- package/dist/tabs/anchored.d.ts +59 -0
- package/dist/tabs/anchored.js +109 -0
- package/dist/tabs/hover.d.ts +33 -0
- package/dist/tabs/hover.js +169 -0
- package/dist/tabs/index.d.ts +5 -0
- package/dist/tabs/index.js +4 -0
- package/dist/tabs/index.parts.d.ts +4 -0
- package/dist/tabs/index.parts.js +8 -0
- package/dist/tabs/store.d.ts +131 -0
- package/dist/tabs/store.js +248 -0
- package/dist/tabs/tabs.d.ts +217 -0
- package/dist/tabs/tabs.js +458 -0
- package/dist/thread/geometry.d.ts +23 -0
- package/dist/thread/geometry.js +48 -0
- package/dist/thread/index.d.ts +4 -0
- package/dist/thread/index.js +3 -0
- package/dist/thread/index.parts.d.ts +1 -0
- package/dist/thread/index.parts.js +5 -0
- package/dist/thread/stores.d.ts +21 -0
- package/dist/thread/stores.js +63 -0
- package/dist/thread/thread.d.ts +57 -0
- package/dist/thread/thread.js +663 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +17 -0
- package/package.json +125 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import { type ChangeEventDetails } from "../internal/change-event-details.js";
|
|
3
|
+
import { type TabsHover } from "./hover.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tabs store — an open-ended, closable collection with at most one tab open.
|
|
6
|
+
*
|
|
7
|
+
* Two things follow from that sentence and shape everything else:
|
|
8
|
+
*
|
|
9
|
+
* **Open-ness is the selection.** There is no separate `open` flag; `value` is
|
|
10
|
+
* either a tab id or `null`, and `null` means nothing is showing. A strip of
|
|
11
|
+
* page tabs simply never reaches `null`; a dock of chat tabs does, every time
|
|
12
|
+
* you close the last one.
|
|
13
|
+
*
|
|
14
|
+
* **Order is data.** `items` is an ordered array on the store, not something
|
|
15
|
+
* derived from the DOM. That is why reordering needs no support here beyond
|
|
16
|
+
* `move` — drag it with whatever library you like through the `render` prop,
|
|
17
|
+
* and the result is a state change like any other.
|
|
18
|
+
*
|
|
19
|
+
* The instance model matches the shell's: `useTabs` resolves the nearest
|
|
20
|
+
* `Tabs.Root` through context, `useTabsStore(handle, selector)` takes an
|
|
21
|
+
* explicit handle for anywhere else, and there is no global fallback.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Where the selection lands when the open tab is closed.
|
|
25
|
+
*
|
|
26
|
+
* Unset means nowhere: closing what was open shows nothing. Handing over is a
|
|
27
|
+
* behaviour, and a primitive should not invent one you did not ask for — you
|
|
28
|
+
* would have no reason to suspect it was configurable.
|
|
29
|
+
*/
|
|
30
|
+
export type TabsSelectOnClose =
|
|
31
|
+
/** Whatever slides into the vacated slot — an editor's behaviour. */
|
|
32
|
+
"adjacent"
|
|
33
|
+
/** The tab you were in before this one, falling back to adjacent. */
|
|
34
|
+
| "recent";
|
|
35
|
+
/** Why the selection or the collection changed. */
|
|
36
|
+
export type TabsRootChangeEventReason =
|
|
37
|
+
/** A click, Enter or Space on a tab. */
|
|
38
|
+
"trigger-press"
|
|
39
|
+
/** The pointer resting on, or leaving, an `openOnHover` tab. */
|
|
40
|
+
| "trigger-hover"
|
|
41
|
+
/** Arrowing onto a tab with `activateOnFocus`. */
|
|
42
|
+
| "list-navigation"
|
|
43
|
+
/** A press on `Tabs.Close`. */
|
|
44
|
+
| "close-press"
|
|
45
|
+
/** Delete or Backspace on a focused tab. */
|
|
46
|
+
| "keyboard" | "escape-key"
|
|
47
|
+
/** A store action called from your own code. */
|
|
48
|
+
| "imperative-action";
|
|
49
|
+
export type TabsRootChangeEventDetails = ChangeEventDetails<TabsRootChangeEventReason>;
|
|
50
|
+
/** Which way the selection just moved, for panels that slide rather than fade. */
|
|
51
|
+
export type TabsDirection = "left" | "right" | "up" | "down" | "none";
|
|
52
|
+
export type TabsState = {
|
|
53
|
+
value: string | null;
|
|
54
|
+
/** Set on every selection change and left alone until the next one. */
|
|
55
|
+
direction: TabsDirection;
|
|
56
|
+
items: string[];
|
|
57
|
+
/** Most recently opened first, and what the `recent` close policy reads. */
|
|
58
|
+
recent: string[];
|
|
59
|
+
/** Whether a viewport is rendered, so a trigger only claims one that exists. */
|
|
60
|
+
hasViewport: boolean;
|
|
61
|
+
/** Ids that cannot be selected. They stay in the arrow-key ring regardless. */
|
|
62
|
+
disabled: ReadonlySet<string>;
|
|
63
|
+
/** Add a tab (or move it, if already open) and select it. */
|
|
64
|
+
open: (value: string, options?: {
|
|
65
|
+
at?: number;
|
|
66
|
+
}) => void;
|
|
67
|
+
close: (value: string) => void;
|
|
68
|
+
select: (value: string | null) => void;
|
|
69
|
+
/** Step the selection — what the arrow keys drive. */
|
|
70
|
+
selectRelative: (direction: 1 | -1, options?: {
|
|
71
|
+
loop?: boolean;
|
|
72
|
+
}) => void;
|
|
73
|
+
move: (value: string, toIndex: number) => void;
|
|
74
|
+
setItems: (items: string[]) => void;
|
|
75
|
+
};
|
|
76
|
+
export type TabsStore = {
|
|
77
|
+
subscribe: (listener: () => void) => () => void;
|
|
78
|
+
getSnapshot: () => TabsState;
|
|
79
|
+
/** Seed state before the first render commits: no notify, no write-back. */
|
|
80
|
+
hydrate: (state: {
|
|
81
|
+
items?: string[];
|
|
82
|
+
value?: string | null;
|
|
83
|
+
}) => void;
|
|
84
|
+
selectOnCloseRef: RefObject<TabsSelectOnClose | undefined>;
|
|
85
|
+
orientationRef: RefObject<"horizontal" | "vertical">;
|
|
86
|
+
valueControlledRef: RefObject<boolean>;
|
|
87
|
+
itemsControlledRef: RefObject<boolean>;
|
|
88
|
+
onValueChangeRef: RefObject<((value: string | null, eventDetails: TabsRootChangeEventDetails) => void) | null>;
|
|
89
|
+
onItemsChangeRef: RefObject<((items: string[], eventDetails: TabsRootChangeEventDetails) => void) | null>;
|
|
90
|
+
/** Commit past the controlled guard — how the owner pushes its decision in. */
|
|
91
|
+
commitValue: (value: string | null) => void;
|
|
92
|
+
commitItems: (items: string[]) => void;
|
|
93
|
+
/** `select` and `close` as the parts call them, with the reason behind the change. */
|
|
94
|
+
selectWithDetails: (value: string | null, eventDetails: TabsRootChangeEventDetails) => void;
|
|
95
|
+
closeWithDetails: (value: string, eventDetails: TabsRootChangeEventDetails) => void;
|
|
96
|
+
/** Hover for every `openOnHover` tab and the popup, decided in one place. */
|
|
97
|
+
hover: TabsHover;
|
|
98
|
+
registerViewport: (present: boolean) => void;
|
|
99
|
+
registerDisabled: (value: string, disabled: boolean) => void;
|
|
100
|
+
/**
|
|
101
|
+
* Tab elements by id. Imperative, not reactive: it exists so focus can be
|
|
102
|
+
* moved somewhere sensible *before* a closing tab unmounts, which is the one
|
|
103
|
+
* moment a reactive lookup would already be too late.
|
|
104
|
+
*/
|
|
105
|
+
elements: Map<string, HTMLElement>;
|
|
106
|
+
registerElement: (value: string, element: HTMLElement | null) => void;
|
|
107
|
+
/**
|
|
108
|
+
* What a floating surface anchors to, which is not the same element focus
|
|
109
|
+
* goes to. Focus belongs on the tab's button; the popup should line up with
|
|
110
|
+
* the whole visual item, close button included — otherwise it sits short by
|
|
111
|
+
* exactly the width of the ×.
|
|
112
|
+
*/
|
|
113
|
+
anchors: Map<string, HTMLElement>;
|
|
114
|
+
registerAnchor: (value: string, element: HTMLElement | null) => void;
|
|
115
|
+
/** The floating surface while it is mounted: where the hover cone aims, and what Escape returns focus from. */
|
|
116
|
+
popupRef: RefObject<HTMLElement | null>;
|
|
117
|
+
/** Stable per-instance id, assigned by the mounting Tabs.Root from useId. */
|
|
118
|
+
baseId: string;
|
|
119
|
+
};
|
|
120
|
+
export declare const createTabsStore: () => TabsStore;
|
|
121
|
+
export declare const TabsStoreContext: import("react").Context<TabsStore | null>;
|
|
122
|
+
/** Internal: parts resolve the store their Tabs.Root provided. */
|
|
123
|
+
export declare const useTabsContextStore: () => TabsStore;
|
|
124
|
+
/** Subscribe to an explicit `Tabs.createStore()` handle, from outside the tree. */
|
|
125
|
+
export declare const useTabsStore: <Selected = TabsState>(store: TabsStore, selector?: (tabs: TabsState) => Selected) => Selected;
|
|
126
|
+
/**
|
|
127
|
+
* Subscribe from inside the tree. Select a primitive — a boolean, an id — so
|
|
128
|
+
* the subscription can settle; a selector building a fresh array or object on
|
|
129
|
+
* every call never will.
|
|
130
|
+
*/
|
|
131
|
+
export declare const useTabs: <Selected = TabsState>(selector?: (tabs: TabsState) => Selected) => Selected;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, use, useSyncExternalStore } from "react";
|
|
3
|
+
import { createChangeEventDetails, } from "../internal/change-event-details.js";
|
|
4
|
+
import { adjacentTo, insertAt, moveTo, step } from "../internal/collection.js";
|
|
5
|
+
import { createTabsHover } from "./hover.js";
|
|
6
|
+
/** Enough history to be useful, bounded so a long session cannot grow it forever. */
|
|
7
|
+
const RECENT_LIMIT = 50;
|
|
8
|
+
const sameOrder = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
|
|
9
|
+
export const createTabsStore = () => {
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
const notify = () => {
|
|
12
|
+
for (const listener of listeners)
|
|
13
|
+
listener();
|
|
14
|
+
};
|
|
15
|
+
const selectOnCloseRef = { current: undefined };
|
|
16
|
+
const orientationRef = { current: "horizontal" };
|
|
17
|
+
const valueControlledRef = { current: false };
|
|
18
|
+
const itemsControlledRef = { current: false };
|
|
19
|
+
const onValueChangeRef = { current: null };
|
|
20
|
+
const onItemsChangeRef = { current: null };
|
|
21
|
+
const popupRef = { current: null };
|
|
22
|
+
const elements = new Map();
|
|
23
|
+
const anchors = new Map();
|
|
24
|
+
let snapshot;
|
|
25
|
+
const anchorFor = (value) => anchors.get(value) ?? elements.get(value) ?? null;
|
|
26
|
+
/**
|
|
27
|
+
* Which way the selection moved, measured off the two tabs' own rectangles
|
|
28
|
+
* so a reordered strip still reports the truth.
|
|
29
|
+
*
|
|
30
|
+
* When either element is missing — a tab opened and selected in the same
|
|
31
|
+
* commit has not rendered yet — fall back to list order. Comparing the
|
|
32
|
+
* *values* would only work when they happen to sort meaningfully; we own the
|
|
33
|
+
* ordered array, so we can just look.
|
|
34
|
+
*/
|
|
35
|
+
const directionBetween = (from, to) => {
|
|
36
|
+
if (from === null || to === null || from === to)
|
|
37
|
+
return "none";
|
|
38
|
+
const horizontal = orientationRef.current === "horizontal";
|
|
39
|
+
const fromElement = anchorFor(from);
|
|
40
|
+
const toElement = anchorFor(to);
|
|
41
|
+
if (!fromElement || !toElement) {
|
|
42
|
+
const fromIndex = snapshot.items.indexOf(from);
|
|
43
|
+
const toIndex = snapshot.items.indexOf(to);
|
|
44
|
+
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex)
|
|
45
|
+
return "none";
|
|
46
|
+
const forward = toIndex > fromIndex;
|
|
47
|
+
return horizontal ? (forward ? "right" : "left") : forward ? "down" : "up";
|
|
48
|
+
}
|
|
49
|
+
const fromRect = fromElement.getBoundingClientRect();
|
|
50
|
+
const toRect = toElement.getBoundingClientRect();
|
|
51
|
+
if (horizontal) {
|
|
52
|
+
if (toRect.left === fromRect.left)
|
|
53
|
+
return "none";
|
|
54
|
+
return toRect.left > fromRect.left ? "right" : "left";
|
|
55
|
+
}
|
|
56
|
+
if (toRect.top === fromRect.top)
|
|
57
|
+
return "none";
|
|
58
|
+
return toRect.top > fromRect.top ? "down" : "up";
|
|
59
|
+
};
|
|
60
|
+
const commitValue = (value) => {
|
|
61
|
+
if (snapshot.value === value)
|
|
62
|
+
return;
|
|
63
|
+
const direction = directionBetween(snapshot.value, value);
|
|
64
|
+
const recent = value === null
|
|
65
|
+
? snapshot.recent
|
|
66
|
+
: [value, ...snapshot.recent.filter((id) => id !== value)].slice(0, RECENT_LIMIT);
|
|
67
|
+
snapshot = { ...snapshot, value, direction, recent };
|
|
68
|
+
notify();
|
|
69
|
+
};
|
|
70
|
+
const commitItems = (items) => {
|
|
71
|
+
if (sameOrder(snapshot.items, items))
|
|
72
|
+
return;
|
|
73
|
+
snapshot = { ...snapshot, items };
|
|
74
|
+
notify();
|
|
75
|
+
};
|
|
76
|
+
const imperative = () => createChangeEventDetails("imperative-action");
|
|
77
|
+
/** The selection after it has been asked about: hover hears of it, then it commits. */
|
|
78
|
+
const landValue = (value, eventDetails) => {
|
|
79
|
+
hover.changed(value, eventDetails.reason);
|
|
80
|
+
if (!valueControlledRef.current)
|
|
81
|
+
commitValue(value);
|
|
82
|
+
};
|
|
83
|
+
const select = (value, eventDetails) => {
|
|
84
|
+
// A disabled tab is still reachable by keyboard — that is the point — but
|
|
85
|
+
// arrowing onto one must not open it.
|
|
86
|
+
if (value !== null && snapshot.disabled.has(value))
|
|
87
|
+
return;
|
|
88
|
+
onValueChangeRef.current?.(value, eventDetails);
|
|
89
|
+
if (eventDetails.isCanceled)
|
|
90
|
+
return;
|
|
91
|
+
landValue(value, eventDetails);
|
|
92
|
+
};
|
|
93
|
+
const setItems = (items, eventDetails) => {
|
|
94
|
+
onItemsChangeRef.current?.(items, eventDetails);
|
|
95
|
+
if (eventDetails.isCanceled)
|
|
96
|
+
return;
|
|
97
|
+
if (!itemsControlledRef.current)
|
|
98
|
+
commitItems(items);
|
|
99
|
+
};
|
|
100
|
+
/** Read against the collection as it stands *before* the close lands. */
|
|
101
|
+
const successorTo = (closed) => {
|
|
102
|
+
const policy = selectOnCloseRef.current;
|
|
103
|
+
if (!policy)
|
|
104
|
+
return null;
|
|
105
|
+
// Handing over to a tab that cannot be opened would leave nothing showing.
|
|
106
|
+
const eligible = (id) => !snapshot.disabled.has(id);
|
|
107
|
+
if (policy === "recent") {
|
|
108
|
+
const candidate = snapshot.recent.find((id) => id !== closed && snapshot.items.includes(id) && eligible(id));
|
|
109
|
+
// No history yet — a fresh session, or everything since has been closed.
|
|
110
|
+
if (candidate)
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
return adjacentTo(snapshot.items, closed, eligible);
|
|
114
|
+
};
|
|
115
|
+
const open = (value, options) => {
|
|
116
|
+
const eventDetails = imperative();
|
|
117
|
+
setItems(insertAt(snapshot.items, value, options?.at), eventDetails);
|
|
118
|
+
if (eventDetails.isCanceled)
|
|
119
|
+
return;
|
|
120
|
+
select(value, imperative());
|
|
121
|
+
};
|
|
122
|
+
const close = (value, eventDetails) => {
|
|
123
|
+
const wasOpen = snapshot.value === value;
|
|
124
|
+
// Computed first: once the item is gone there is no neighbour to find.
|
|
125
|
+
const successor = wasOpen ? successorTo(value) : snapshot.value;
|
|
126
|
+
// Cancelling the items change keeps the tab, and the selection with it.
|
|
127
|
+
setItems(snapshot.items.filter((item) => item !== value), eventDetails);
|
|
128
|
+
if (eventDetails.isCanceled || !wasOpen)
|
|
129
|
+
return;
|
|
130
|
+
// The tab is gone, so the selection must follow it: reported, but not cancellable.
|
|
131
|
+
const followUp = createChangeEventDetails(eventDetails.reason, eventDetails.event, eventDetails.trigger);
|
|
132
|
+
onValueChangeRef.current?.(successor, followUp);
|
|
133
|
+
landValue(successor, followUp);
|
|
134
|
+
};
|
|
135
|
+
const selectRelative = (direction, options) => select(step(snapshot.items, snapshot.value, direction, options), imperative());
|
|
136
|
+
const move = (value, toIndex) => setItems(moveTo(snapshot.items, value, toIndex), imperative());
|
|
137
|
+
const registerDisabled = (value, disabled) => {
|
|
138
|
+
if (snapshot.disabled.has(value) === disabled)
|
|
139
|
+
return;
|
|
140
|
+
const next = new Set(snapshot.disabled);
|
|
141
|
+
if (disabled)
|
|
142
|
+
next.add(value);
|
|
143
|
+
else
|
|
144
|
+
next.delete(value);
|
|
145
|
+
snapshot = { ...snapshot, disabled: next };
|
|
146
|
+
notify();
|
|
147
|
+
};
|
|
148
|
+
const registerViewport = (present) => {
|
|
149
|
+
if (snapshot.hasViewport === present)
|
|
150
|
+
return;
|
|
151
|
+
snapshot = { ...snapshot, hasViewport: present };
|
|
152
|
+
notify();
|
|
153
|
+
};
|
|
154
|
+
const hover = createTabsHover({
|
|
155
|
+
getSnapshot: () => snapshot,
|
|
156
|
+
selectWithDetails: select,
|
|
157
|
+
popupRef,
|
|
158
|
+
});
|
|
159
|
+
snapshot = {
|
|
160
|
+
value: null,
|
|
161
|
+
direction: "none",
|
|
162
|
+
items: [],
|
|
163
|
+
recent: [],
|
|
164
|
+
hasViewport: false,
|
|
165
|
+
disabled: new Set(),
|
|
166
|
+
open,
|
|
167
|
+
close: (value) => close(value, imperative()),
|
|
168
|
+
select: (value) => select(value, imperative()),
|
|
169
|
+
selectRelative,
|
|
170
|
+
move,
|
|
171
|
+
setItems: (items) => setItems(items, imperative()),
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
subscribe: (listener) => {
|
|
175
|
+
listeners.add(listener);
|
|
176
|
+
return () => {
|
|
177
|
+
listeners.delete(listener);
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
getSnapshot: () => snapshot,
|
|
181
|
+
hydrate: ({ items, value }) => {
|
|
182
|
+
const nextItems = items ?? snapshot.items;
|
|
183
|
+
const nextValue = value === undefined ? snapshot.value : value;
|
|
184
|
+
snapshot = {
|
|
185
|
+
...snapshot,
|
|
186
|
+
items: nextItems,
|
|
187
|
+
// A restored selection whose tab is gone is not a selection.
|
|
188
|
+
value: nextValue !== null && nextItems.includes(nextValue) ? nextValue : null,
|
|
189
|
+
recent: nextValue !== null && nextItems.includes(nextValue) ? [nextValue] : snapshot.recent,
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
selectOnCloseRef,
|
|
193
|
+
orientationRef,
|
|
194
|
+
valueControlledRef,
|
|
195
|
+
itemsControlledRef,
|
|
196
|
+
onValueChangeRef,
|
|
197
|
+
onItemsChangeRef,
|
|
198
|
+
commitValue,
|
|
199
|
+
commitItems,
|
|
200
|
+
selectWithDetails: select,
|
|
201
|
+
closeWithDetails: close,
|
|
202
|
+
hover,
|
|
203
|
+
registerViewport,
|
|
204
|
+
registerDisabled,
|
|
205
|
+
elements,
|
|
206
|
+
registerElement: (value, element) => {
|
|
207
|
+
if (element)
|
|
208
|
+
elements.set(value, element);
|
|
209
|
+
else
|
|
210
|
+
elements.delete(value);
|
|
211
|
+
},
|
|
212
|
+
anchors,
|
|
213
|
+
registerAnchor: (value, element) => {
|
|
214
|
+
if (element)
|
|
215
|
+
anchors.set(value, element);
|
|
216
|
+
else
|
|
217
|
+
anchors.delete(value);
|
|
218
|
+
},
|
|
219
|
+
popupRef,
|
|
220
|
+
baseId: "",
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Instance resolution — the nearest Tabs.Root, explicitly.
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
export const TabsStoreContext = createContext(null);
|
|
227
|
+
/** Internal: parts resolve the store their Tabs.Root provided. */
|
|
228
|
+
export const useTabsContextStore = () => {
|
|
229
|
+
const store = use(TabsStoreContext);
|
|
230
|
+
if (!store)
|
|
231
|
+
throw new Error("Tabs parts must be used within <Tabs.Root>");
|
|
232
|
+
return store;
|
|
233
|
+
};
|
|
234
|
+
/** Subscribe to an explicit `Tabs.createStore()` handle, from outside the tree. */
|
|
235
|
+
export const useTabsStore = (store, selector) => {
|
|
236
|
+
const getValue = () => {
|
|
237
|
+
const state = store.getSnapshot();
|
|
238
|
+
// Safe: without a selector, Selected defaults to TabsState.
|
|
239
|
+
return selector ? selector(state) : state;
|
|
240
|
+
};
|
|
241
|
+
return useSyncExternalStore(store.subscribe, getValue, getValue);
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Subscribe from inside the tree. Select a primitive — a boolean, an id — so
|
|
245
|
+
* the subscription can settle; a selector building a fresh array or object on
|
|
246
|
+
* every call never will.
|
|
247
|
+
*/
|
|
248
|
+
export const useTabs = (selector) => useTabsStore(useTabsContextStore(), selector);
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { PrimitiveProps } from "../internal/primitive-props.js";
|
|
3
|
+
import type { TabsDirection, TabsRootChangeEventDetails, TabsSelectOnClose, TabsStore } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* An open-ended, closable collection of tabs with at most one open.
|
|
6
|
+
*
|
|
7
|
+
* The parts are the same whether the content sits in the layout or floats
|
|
8
|
+
* above it — that difference is a matter of what you wrap the viewport in, not
|
|
9
|
+
* a prop:
|
|
10
|
+
*
|
|
11
|
+
* List + Viewport in the layout
|
|
12
|
+
* List + Portal > Positioner > Popup > Viewport anchored to the open tab
|
|
13
|
+
*
|
|
14
|
+
* The strip and the content both take a function, so neither needs a loop, a
|
|
15
|
+
* key, or a subscription of its own:
|
|
16
|
+
*
|
|
17
|
+
* <Tabs.Root defaultItems={["a", "b"]} defaultValue="a">
|
|
18
|
+
* <Tabs.List>
|
|
19
|
+
* {(id) => (
|
|
20
|
+
* <Tabs.Trigger value={id}>
|
|
21
|
+
* <Tabs.Icon />
|
|
22
|
+
* {id}
|
|
23
|
+
* <Tabs.Action>
|
|
24
|
+
* <Tabs.Close />
|
|
25
|
+
* </Tabs.Action>
|
|
26
|
+
* </Tabs.Trigger>
|
|
27
|
+
* )}
|
|
28
|
+
* </Tabs.List>
|
|
29
|
+
* <Tabs.Viewport>{(id) => <Document id={id} />}</Tabs.Viewport>
|
|
30
|
+
* </Tabs.Root>
|
|
31
|
+
*
|
|
32
|
+
* The viewport is one box rather than a panel per tab, and it can sit anywhere
|
|
33
|
+
* in the tree — inside the popup for a floating surface, or in a card three
|
|
34
|
+
* components away while the strip stays in the window chrome.
|
|
35
|
+
*
|
|
36
|
+
* ## Why this is a toolbar and not a tablist
|
|
37
|
+
*
|
|
38
|
+
* ARIA's `tablist` cannot describe this widget, for two independent reasons.
|
|
39
|
+
* A tablist must own only tabs, so there is nowhere to put a close button; and
|
|
40
|
+
* it requires exactly one selected tab, so `value: null` — a dock with
|
|
41
|
+
* everything closed — is not a state it can express. Both are real, and axe
|
|
42
|
+
* fails the first one outright.
|
|
43
|
+
*
|
|
44
|
+
* So the strip is a `toolbar` of buttons, each a disclosure for its panel:
|
|
45
|
+
* `aria-expanded` says whether its panel is showing and `aria-controls` names
|
|
46
|
+
* it. Close buttons are real buttons rather than pointer-only affordances,
|
|
47
|
+
* arrow-key roving focus is exactly what a toolbar is expected to do, and
|
|
48
|
+
* nothing open is an ordinary state.
|
|
49
|
+
*
|
|
50
|
+
* Static, always-one-selected tabs are a different widget, and a `tablist` is
|
|
51
|
+
* the right role for those.
|
|
52
|
+
*/
|
|
53
|
+
export type TabsOrientation = "horizontal" | "vertical";
|
|
54
|
+
export type TabsPartState = {
|
|
55
|
+
orientation: TabsOrientation;
|
|
56
|
+
disabled: boolean;
|
|
57
|
+
/** Nothing is open. A page-tab strip never sees this; a dock does. */
|
|
58
|
+
empty: boolean;
|
|
59
|
+
/** Which way the selection last moved, so panels can slide the right way. */
|
|
60
|
+
direction: TabsDirection;
|
|
61
|
+
};
|
|
62
|
+
export type TabsTriggerState = TabsPartState & {
|
|
63
|
+
selected: boolean;
|
|
64
|
+
};
|
|
65
|
+
export type TabsRootProps<Value extends string = string> = Omit<PrimitiveProps<"div", TabsPartState>, "defaultValue"> & {
|
|
66
|
+
defaultValue?: Value | null;
|
|
67
|
+
value?: Value | null;
|
|
68
|
+
/** Called before the selection changes. `eventDetails.cancel()` keeps it, except after a close. */
|
|
69
|
+
onValueChange?: (value: Value | null, eventDetails: TabsRootChangeEventDetails) => void;
|
|
70
|
+
defaultItems?: Value[];
|
|
71
|
+
items?: Value[];
|
|
72
|
+
/** Called before a tab is added, removed or moved. Cancelling a close keeps the tab. */
|
|
73
|
+
onItemsChange?: (items: Value[], eventDetails: TabsRootChangeEventDetails) => void;
|
|
74
|
+
/** Where the selection lands when a tab is closed. Unset means nowhere. */
|
|
75
|
+
selectOnClose?: TabsSelectOnClose;
|
|
76
|
+
/** An explicit `Tabs.createStore()` handle. Must be stable for the Root's life. */
|
|
77
|
+
store?: TabsStore;
|
|
78
|
+
orientation?: TabsOrientation;
|
|
79
|
+
/** Wrap at the ends of the collection when arrowing. */
|
|
80
|
+
loop?: boolean;
|
|
81
|
+
/** Select on arrow rather than only on Enter/Space. */
|
|
82
|
+
activateOnFocus?: boolean;
|
|
83
|
+
/** Disables every tab at once — a read-only view of the collection. */
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Escape closes whatever is open. The only dismissal the primitive takes on:
|
|
87
|
+
* a non-modal surface is one you work behind, so an outside press belongs to
|
|
88
|
+
* the page, not to us.
|
|
89
|
+
*/
|
|
90
|
+
dismissOnEscape?: boolean;
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The provider and container. Owns the item order, the selection and the
|
|
95
|
+
* roving focus, so every other part reads rather than holds state.
|
|
96
|
+
* Renders a `<div>` element.
|
|
97
|
+
*/
|
|
98
|
+
export declare const TabsRoot: <Value extends string = string>({ defaultValue, value, onValueChange, defaultItems, items, onItemsChange, selectOnClose, store: storeProp, orientation, loop, activateOnFocus, disabled, dismissOnEscape, className, render, style, ...elementProps }: TabsRootProps<Value>) => import("react").JSX.Element;
|
|
99
|
+
export type TabsListProps = Omit<PrimitiveProps<"div", TabsPartState>, "children"> & {
|
|
100
|
+
/**
|
|
101
|
+
* Plain children when you are laying the strip out yourself. A function
|
|
102
|
+
* renders one call per tab, in order, and saves you the map, the keys and a
|
|
103
|
+
* subscription of your own.
|
|
104
|
+
*/
|
|
105
|
+
children?: ReactNode | ((value: string, index: number) => ReactNode);
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The strip. A `toolbar` of disclosures rather than a `tablist`, because a
|
|
109
|
+
* tablist may hold only tabs and must always have one selected.
|
|
110
|
+
* Renders a `<div>` element.
|
|
111
|
+
*/
|
|
112
|
+
export declare const TabsList: ({ children, className, render, style, ...elementProps }: TabsListProps) => import("react").JSX.Element;
|
|
113
|
+
export type TabsTriggerProps = PrimitiveProps<"div", TabsTriggerState> & {
|
|
114
|
+
/** Required outside a `Tabs.List`; inside one it comes from the collection. */
|
|
115
|
+
value?: string;
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
/** Open the tab when the mouse rests on it. Touch has no hover, so a tap still presses. */
|
|
118
|
+
openOnHover?: boolean;
|
|
119
|
+
/** How long the mouse rests on the tab before it opens, in ms. Needs `openOnHover`. */
|
|
120
|
+
openDelay?: number;
|
|
121
|
+
/** How long a hover-opened tab stays open after the mouse leaves, in ms. Needs `openOnHover`. */
|
|
122
|
+
closeDelay?: number;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* A tab: the whole pill, and the thing you press.
|
|
126
|
+
*
|
|
127
|
+
* It is a `div` with `role="button"` rather than a `<button>`, so that a close
|
|
128
|
+
* button can live inside it. That nesting is what Linear does and it is what
|
|
129
|
+
* makes the entire pill a click target, but it is not free: `role="button"`
|
|
130
|
+
* has children-presentational in ARIA, so `axe` reports `nested-interactive`
|
|
131
|
+
* and the suite suppresses that one rule with a note. A real `<button>` would
|
|
132
|
+
* be worse — the HTML parser closes an outer button when it meets an inner
|
|
133
|
+
* one, so server-rendered markup would come back reshaped and hydration would
|
|
134
|
+
* mismatch.
|
|
135
|
+
*
|
|
136
|
+
* The price of not being a native button is that activation is ours to
|
|
137
|
+
* implement: Enter and Space, by hand.
|
|
138
|
+
*
|
|
139
|
+
* One part, two situations, decided by where it sits rather than by a prop:
|
|
140
|
+
*
|
|
141
|
+
* - **Inside a `Tabs.List`** it is one of the collection, joins the roving
|
|
142
|
+
* focus, and selects. `Delete` closes it.
|
|
143
|
+
* - **Outside one** it takes an explicit value, keeps its own tab stop, and
|
|
144
|
+
* toggles — a panel with no tab behind it, opened anchored to the button
|
|
145
|
+
* that owns it and promoted to a real tab only when you call `open()`.
|
|
146
|
+
*
|
|
147
|
+
* With `openOnHover`, leaving closes the tab again unless the mouse heads into
|
|
148
|
+
* the popup. A press on the tab, or a press or focus inside the popup, keeps it.
|
|
149
|
+
*/
|
|
150
|
+
export declare const TabsTrigger: ({ value: explicitValue, disabled: ownDisabled, openOnHover, openDelay, closeDelay, className, render, style, ...elementProps }: TabsTriggerProps) => import("react").JSX.Element;
|
|
151
|
+
export type TabsIconProps = PrimitiveProps<"span", TabsTriggerState> & {
|
|
152
|
+
value?: string;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* A decorative mark inside a trigger.
|
|
156
|
+
*
|
|
157
|
+
* `aria-hidden`, deliberately: the trigger already has an accessible name, and
|
|
158
|
+
* an icon that repeats it only makes the announcement longer. It carries the
|
|
159
|
+
* part state too, so the mark can respond to its tab being open without the
|
|
160
|
+
* consumer reaching for a group selector.
|
|
161
|
+
*/
|
|
162
|
+
export declare const TabsIcon: ({ value: explicitValue, className, render, style, ...elementProps }: TabsIconProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
163
|
+
export type TabsActionProps = PrimitiveProps<"div", TabsTriggerState> & {
|
|
164
|
+
/** Only outside a tab, where there is none to inherit from. */
|
|
165
|
+
value?: string;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* The trailing slot inside a tab — where the close button lives.
|
|
169
|
+
*
|
|
170
|
+
* Its own part rather than a div of yours for one reason: a tab narrow enough
|
|
171
|
+
* to truncate has nowhere to put a control. Give this `position: absolute` and
|
|
172
|
+
* a background that fades to the tab's own colour, and the label runs the full
|
|
173
|
+
* width and disappears under it instead of colliding with it. A flex box with
|
|
174
|
+
* padding then centres whatever is inside.
|
|
175
|
+
*
|
|
176
|
+
* All of that is your CSS. What the part supplies is the element and the
|
|
177
|
+
* trigger's state on it — `data-selected` above all, since the fade has to end
|
|
178
|
+
* in whatever colour the tab currently is.
|
|
179
|
+
*/
|
|
180
|
+
export declare const TabsAction: ({ value: explicitValue, className, render, style, ...elementProps }: TabsActionProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
181
|
+
export type TabsCloseProps = PrimitiveProps<"div", TabsTriggerState> & {
|
|
182
|
+
value?: string;
|
|
183
|
+
disabled?: boolean;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* The × inside a tab.
|
|
187
|
+
*
|
|
188
|
+
* Also a `div` with `role="button"`, for the same reason its parent is. It
|
|
189
|
+
* keeps out of the roving order — arrowing along a strip should walk tabs, not
|
|
190
|
+
* alternate between each tab and its close button — so the keyboard route to
|
|
191
|
+
* closing is `Delete` on the tab itself. Every event it handles stops there:
|
|
192
|
+
* nested inside the trigger, anything that escaped would open the tab on its
|
|
193
|
+
* way out of closing it.
|
|
194
|
+
*/
|
|
195
|
+
export declare const TabsClose: ({ value: explicitValue, disabled: ownDisabled, className, render, style, ...elementProps }: TabsCloseProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
196
|
+
export type TabsViewportState = TabsPartState & {
|
|
197
|
+
/** The open tab, or null when nothing is. */
|
|
198
|
+
value: string | null;
|
|
199
|
+
};
|
|
200
|
+
export type TabsViewportProps = Omit<PrimitiveProps<"div", TabsViewportState>, "children"> & {
|
|
201
|
+
/** Renders the content for whatever is open. Not called when nothing is. */
|
|
202
|
+
children?: (value: string) => ReactNode;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The one box that shows a tab's content.
|
|
206
|
+
*
|
|
207
|
+
* There is no per-tab element and nothing mounts or unmounts as you switch —
|
|
208
|
+
* the same box stays put and re-renders its children for whichever value is
|
|
209
|
+
* open. That is what makes a floating surface simply move rather than tear
|
|
210
|
+
* itself down, and it is why nothing here needs overlaying or a mount flag.
|
|
211
|
+
*
|
|
212
|
+
* The corollary, and it is deliberate: nothing off-screen exists. A tab you
|
|
213
|
+
* are not looking at has no component, so anything that must keep running
|
|
214
|
+
* while you are elsewhere — a reply still streaming — belongs in a store
|
|
215
|
+
* rather than in the panel's own state.
|
|
216
|
+
*/
|
|
217
|
+
export declare const TabsViewport: ({ children, className, render, style, ...elementProps }: TabsViewportProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|