@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,390 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, use, useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../internal/iso-layout-effect.js";
|
|
5
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
6
|
+
import { createShellStore, ShellStoreContext, useShellContextStore, useShellStore } from "./store.js";
|
|
7
|
+
/**
|
|
8
|
+
* The shell: a sidebar that collapses, floats out on hover, and can be dragged
|
|
9
|
+
* wider, next to the viewport it shares the screen with.
|
|
10
|
+
*
|
|
11
|
+
* The package ships no CSS. Parts publish their state as `data-*` attributes
|
|
12
|
+
* and the sidebar's measured width as a custom property; sizing, collapsing
|
|
13
|
+
* geometry and transitions are all yours. In particular **the sidebar's width
|
|
14
|
+
* and the range it may be dragged through are CSS**:
|
|
15
|
+
*
|
|
16
|
+
* [data-shell-sidebar] {
|
|
17
|
+
* width: var(--shell-sidebar-width, 240px);
|
|
18
|
+
* min-width: 200px;
|
|
19
|
+
* max-width: 400px;
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* The drag writes that custom property, the browser clamps it against your
|
|
23
|
+
* `min-width` / `max-width`, and the sidebar reports back whatever the browser
|
|
24
|
+
* settled on. Nothing here second-guesses your stylesheet.
|
|
25
|
+
*
|
|
26
|
+
* No global key binding either. `Shell.Trigger` is the click affordance; a
|
|
27
|
+
* Cmd/Ctrl+B — or whatever your app's shortcut registry says — is four lines
|
|
28
|
+
* around `useShell((shell) => shell.toggle)`, and a package has no business
|
|
29
|
+
* claiming a window-level `keydown` for a key it cannot know is free.
|
|
30
|
+
*/
|
|
31
|
+
/** Written by the grip; read by your `width`. */
|
|
32
|
+
export const SHELL_SIDEBAR_WIDTH_VAR = "--shell-sidebar-width";
|
|
33
|
+
// Both delays are intent filters: the open delay stops a pointer merely
|
|
34
|
+
// crossing the strip from floating the panel out, and the close grace lets you
|
|
35
|
+
// clip a corner on the way from the strip to the panel without losing it.
|
|
36
|
+
const PEEK_OPEN_DELAY_MS = 200;
|
|
37
|
+
const PEEK_CLOSE_DELAY_MS = 250;
|
|
38
|
+
const selectOpen = (shell) => shell.open;
|
|
39
|
+
const selectHotspot = (shell) => shell.hotspot;
|
|
40
|
+
const selectResizing = (shell) => shell.resizing;
|
|
41
|
+
const selectWidth = (shell) => shell.width;
|
|
42
|
+
// Three primitive subscriptions rather than one selector returning an object:
|
|
43
|
+
// useSyncExternalStore compares snapshots by identity, so a selector that built
|
|
44
|
+
// a fresh object every call would never settle.
|
|
45
|
+
const usePartState = (store) => ({
|
|
46
|
+
state: useShellStore(store, selectOpen) ? "expanded" : "collapsed",
|
|
47
|
+
hotspot: useShellStore(store, selectHotspot),
|
|
48
|
+
resizing: useShellStore(store, selectResizing),
|
|
49
|
+
});
|
|
50
|
+
const ShellHotspotContext = createContext(null);
|
|
51
|
+
const useHotspotControls = (store) => {
|
|
52
|
+
const openTimer = useRef(undefined);
|
|
53
|
+
const closeTimer = useRef(undefined);
|
|
54
|
+
const controls = useMemo(() => ({
|
|
55
|
+
request: () => {
|
|
56
|
+
// Armed by the collapse that just happened under this pointer.
|
|
57
|
+
if (store.hotspotSuppressionRef.current)
|
|
58
|
+
return;
|
|
59
|
+
window.clearTimeout(closeTimer.current);
|
|
60
|
+
if (store.getSnapshot().hotspot)
|
|
61
|
+
return;
|
|
62
|
+
openTimer.current = window.setTimeout(() => {
|
|
63
|
+
// Re-checked on firing, not just on arming. A timer started while the
|
|
64
|
+
// sidebar was still expanded outlives a collapse that happens during
|
|
65
|
+
// the delay, and would then float the panel straight back out from
|
|
66
|
+
// under the pointer that just closed it.
|
|
67
|
+
if (store.hotspotSuppressionRef.current)
|
|
68
|
+
return;
|
|
69
|
+
store.getSnapshot().setHotspot(true);
|
|
70
|
+
}, PEEK_OPEN_DELAY_MS);
|
|
71
|
+
},
|
|
72
|
+
hold: () => window.clearTimeout(closeTimer.current),
|
|
73
|
+
release: () => {
|
|
74
|
+
// The pointer has moved on, so the bounce-back guard has done its job.
|
|
75
|
+
store.hotspotSuppressionRef.current = false;
|
|
76
|
+
// Darting through the strip never opens it.
|
|
77
|
+
window.clearTimeout(openTimer.current);
|
|
78
|
+
if (!store.getSnapshot().hotspot)
|
|
79
|
+
return;
|
|
80
|
+
closeTimer.current = window.setTimeout(() => store.getSnapshot().setHotspot(false), PEEK_CLOSE_DELAY_MS);
|
|
81
|
+
},
|
|
82
|
+
}), [store]);
|
|
83
|
+
// Cmd-Tab away mid-hotspot fires no pointerleave: without this the panel stays
|
|
84
|
+
// floated and a pending open timer fires into a backgrounded window.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const dismiss = () => {
|
|
87
|
+
window.clearTimeout(openTimer.current);
|
|
88
|
+
window.clearTimeout(closeTimer.current);
|
|
89
|
+
store.getSnapshot().setHotspot(false);
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener("blur", dismiss);
|
|
92
|
+
return () => {
|
|
93
|
+
window.removeEventListener("blur", dismiss);
|
|
94
|
+
window.clearTimeout(openTimer.current);
|
|
95
|
+
window.clearTimeout(closeTimer.current);
|
|
96
|
+
};
|
|
97
|
+
}, [store]);
|
|
98
|
+
return controls;
|
|
99
|
+
};
|
|
100
|
+
const useHotspotContext = () => {
|
|
101
|
+
const controls = use(ShellHotspotContext);
|
|
102
|
+
if (!controls)
|
|
103
|
+
throw new Error("Shell parts must be used within <Shell.Root>");
|
|
104
|
+
return controls;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The provider and container. Holds the store every other part reads, so
|
|
108
|
+
* `defaultOpen` and the width must arrive here rather than on a child.
|
|
109
|
+
* Renders a `<div>` element.
|
|
110
|
+
*/
|
|
111
|
+
export const ShellRoot = ({ defaultOpen, open, onOpenChange, store: storeProp, className, render, style, ...elementProps }) => {
|
|
112
|
+
const sidebarId = useId();
|
|
113
|
+
const [store] = useState(() => {
|
|
114
|
+
const created = storeProp ?? createShellStore();
|
|
115
|
+
// Before anything has subscribed, so no notify and no write-back. The
|
|
116
|
+
// controlled value wins over the default: the layout effect below would
|
|
117
|
+
// otherwise correct it only after the server had already shipped the wrong
|
|
118
|
+
// markup, which is the pre-hydration flash `defaultOpen` exists to prevent.
|
|
119
|
+
created.hydrate({ open: open ?? defaultOpen });
|
|
120
|
+
created.sidebarId = sidebarId;
|
|
121
|
+
return created;
|
|
122
|
+
});
|
|
123
|
+
// Latest-ref bridges, written during render: idempotent, and they have to be
|
|
124
|
+
// current before any event handler can read them.
|
|
125
|
+
store.controlledRef.current = open !== undefined;
|
|
126
|
+
store.onOpenChangeRef.current = onOpenChange ?? null;
|
|
127
|
+
// Before paint, so a controlled change never shows the old state first.
|
|
128
|
+
useIsomorphicLayoutEffect(() => {
|
|
129
|
+
if (open !== undefined)
|
|
130
|
+
store.commitOpen(open);
|
|
131
|
+
}, [open, store]);
|
|
132
|
+
const hotspot = useHotspotControls(store);
|
|
133
|
+
const state = usePartState(store);
|
|
134
|
+
/*
|
|
135
|
+
* The bounce-back guard is armed by every collapse, but it is only *meant* to
|
|
136
|
+
* survive one where the pointer was sitting where the strip is about to
|
|
137
|
+
* appear. Any movement outside the strip proves it wasn't, so it is cleared
|
|
138
|
+
* here — without this, collapsing by keyboard or by a trigger anywhere else
|
|
139
|
+
* arms a guard that then swallows the next genuine hover.
|
|
140
|
+
*
|
|
141
|
+
* Ref write only, no re-render, and it returns immediately when nothing is
|
|
142
|
+
* armed — which is almost always.
|
|
143
|
+
*/
|
|
144
|
+
const handlePointerMove = (event) => {
|
|
145
|
+
if (!store.hotspotSuppressionRef.current)
|
|
146
|
+
return;
|
|
147
|
+
const target = event.target;
|
|
148
|
+
if (target?.closest("[data-shell-hotspot]"))
|
|
149
|
+
return;
|
|
150
|
+
store.hotspotSuppressionRef.current = false;
|
|
151
|
+
};
|
|
152
|
+
const element = useRenderElement("div", { className, render, style }, {
|
|
153
|
+
state,
|
|
154
|
+
ref: store.rootRef,
|
|
155
|
+
props: [{ "data-shell": "", onPointerMove: handlePointerMove }, elementProps],
|
|
156
|
+
});
|
|
157
|
+
return (_jsx(ShellStoreContext, { value: store, children: _jsx(ShellHotspotContext, { value: hotspot, children: element }) }));
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* The panel, and the element whose width is measured and reported back.
|
|
161
|
+
* Carries the `id` the trigger's `aria-controls` points at.
|
|
162
|
+
* Renders a `<div>` element.
|
|
163
|
+
*/
|
|
164
|
+
export const ShellSidebar = ({ side = "left", onResize, className, render, style, ...elementProps }) => {
|
|
165
|
+
const store = useShellContextStore();
|
|
166
|
+
// Latest-ref bridge, written during render: idempotent, and current before
|
|
167
|
+
// the measurement below can report anything.
|
|
168
|
+
store.onResizeRef.current = onResize ?? null;
|
|
169
|
+
const hotspot = useHotspotContext();
|
|
170
|
+
const partState = usePartState(store);
|
|
171
|
+
// Report the width the browser settled on, so persistence and the resize
|
|
172
|
+
// handle's aria-valuenow describe what is actually on screen.
|
|
173
|
+
useIsomorphicLayoutEffect(() => {
|
|
174
|
+
const element = store.sidebarRef.current;
|
|
175
|
+
if (!element)
|
|
176
|
+
return;
|
|
177
|
+
const measure = () => {
|
|
178
|
+
const measured = element.offsetWidth;
|
|
179
|
+
// Zero means the element is not laid out — display:none, a breakpoint
|
|
180
|
+
// that hides it, a test environment without layout. That carries no
|
|
181
|
+
// information, and persisting it would throw away the width the user
|
|
182
|
+
// actually chose.
|
|
183
|
+
if (measured > 0)
|
|
184
|
+
store.getSnapshot().setWidth(measured);
|
|
185
|
+
};
|
|
186
|
+
measure();
|
|
187
|
+
if (typeof ResizeObserver === "undefined")
|
|
188
|
+
return;
|
|
189
|
+
const observer = new ResizeObserver(measure);
|
|
190
|
+
observer.observe(element);
|
|
191
|
+
return () => observer.disconnect();
|
|
192
|
+
}, [store]);
|
|
193
|
+
return useRenderElement("div", { className, render, style }, {
|
|
194
|
+
state: { ...partState, side },
|
|
195
|
+
ref: store.sidebarRef,
|
|
196
|
+
props: [
|
|
197
|
+
{
|
|
198
|
+
"data-shell-sidebar": "",
|
|
199
|
+
id: store.sidebarId,
|
|
200
|
+
onPointerEnter: hotspot.hold,
|
|
201
|
+
onPointerLeave: hotspot.release,
|
|
202
|
+
},
|
|
203
|
+
elementProps,
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* The content area beside the sidebar. Carries the same state attributes as
|
|
209
|
+
* the root, so it can react to the sidebar without a group selector.
|
|
210
|
+
* Renders a `<div>` element.
|
|
211
|
+
*/
|
|
212
|
+
export const ShellViewport = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
|
|
213
|
+
state: usePartState(useShellContextStore()),
|
|
214
|
+
props: [{ "data-shell-viewport": "" }, elementProps],
|
|
215
|
+
});
|
|
216
|
+
/**
|
|
217
|
+
* The strip along the screen edge that floats a collapsed sidebar out on
|
|
218
|
+
* hover. Give it a width and a position in CSS. Omitting it is how you opt out
|
|
219
|
+
* of hotspot entirely — there is no prop to turn it off, because not rendering it
|
|
220
|
+
* already says that.
|
|
221
|
+
*/
|
|
222
|
+
export const ShellHotspot = ({ className, render, style, ...elementProps }) => {
|
|
223
|
+
const hotspot = useHotspotContext();
|
|
224
|
+
return useRenderElement("div", { className, render, style }, {
|
|
225
|
+
state: usePartState(useShellContextStore()),
|
|
226
|
+
props: [
|
|
227
|
+
{
|
|
228
|
+
"data-shell-hotspot": "",
|
|
229
|
+
"aria-hidden": true,
|
|
230
|
+
onPointerEnter: hotspot.request,
|
|
231
|
+
onPointerLeave: hotspot.release,
|
|
232
|
+
},
|
|
233
|
+
elementProps,
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Toggles the sidebar, and pins a floated-out one open rather than closing it.
|
|
239
|
+
* Ships no copy — supply the label as children.
|
|
240
|
+
* Renders a `<button>` element.
|
|
241
|
+
*/
|
|
242
|
+
export const ShellTrigger = ({ className, render, style, ...elementProps }) => {
|
|
243
|
+
const store = useShellContextStore();
|
|
244
|
+
const state = usePartState(store);
|
|
245
|
+
const handleClick = useCallback(() => {
|
|
246
|
+
const shell = store.getSnapshot();
|
|
247
|
+
// Clicking the trigger inside a floated sidebar pins it open.
|
|
248
|
+
shell.setOpen(shell.hotspot ? true : !shell.open);
|
|
249
|
+
}, [store]);
|
|
250
|
+
return useRenderElement("button", { className, render, style }, {
|
|
251
|
+
state,
|
|
252
|
+
props: [
|
|
253
|
+
{
|
|
254
|
+
type: "button",
|
|
255
|
+
"data-shell-trigger": "",
|
|
256
|
+
"aria-expanded": state.state === "expanded",
|
|
257
|
+
"aria-controls": store.sidebarId,
|
|
258
|
+
onClick: handleClick,
|
|
259
|
+
},
|
|
260
|
+
elementProps,
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
/** The drag range, straight from the stylesheet. `max-width: none` means unbounded. */
|
|
265
|
+
const readBounds = (element) => {
|
|
266
|
+
const computed = getComputedStyle(element);
|
|
267
|
+
const min = Number.parseFloat(computed.minWidth);
|
|
268
|
+
const max = Number.parseFloat(computed.maxWidth);
|
|
269
|
+
return {
|
|
270
|
+
min: Number.isFinite(min) ? min : 0,
|
|
271
|
+
max: Number.isFinite(max) ? max : Number.POSITIVE_INFINITY,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
const clamp = (value, { min, max }) => Math.min(Math.max(value, min), max);
|
|
275
|
+
/**
|
|
276
|
+
* Publish the width on the root, not the sidebar, so anything else in the
|
|
277
|
+
* shell can size itself to match. Clamped here rather than left to the
|
|
278
|
+
* browser: `min-width` would constrain only the sidebar, and a spacer reading
|
|
279
|
+
* the raw value would then disagree with it past either end of the range.
|
|
280
|
+
*/
|
|
281
|
+
const publishWidth = (store, width, bounds) => {
|
|
282
|
+
store.rootRef.current?.style.setProperty(SHELL_SIDEBAR_WIDTH_VAR, `${clamp(width, bounds)}px`);
|
|
283
|
+
};
|
|
284
|
+
/** Dragging right widens a left sidebar and narrows a right one. */
|
|
285
|
+
const widenDirection = (sidebar) => (sidebar.dataset.side === "right" ? -1 : 1);
|
|
286
|
+
/**
|
|
287
|
+
* The drag affordance. Give it a width and a cursor in CSS; the drag range
|
|
288
|
+
* comes from the sidebar's own `min-width` and `max-width`.
|
|
289
|
+
* Renders a `<div>` element with `role="separator"`.
|
|
290
|
+
*/
|
|
291
|
+
export const ShellGrip = ({ step = 16, className, render, style, ...elementProps }) => {
|
|
292
|
+
const store = useShellContextStore();
|
|
293
|
+
const state = usePartState(store);
|
|
294
|
+
const width = useShellStore(store, selectWidth);
|
|
295
|
+
const [bounds, setBounds] = useState({ min: 0, max: Number.POSITIVE_INFINITY });
|
|
296
|
+
const drag = useRef(null);
|
|
297
|
+
// Re-read after every commit rather than on a dependency, so a stylesheet
|
|
298
|
+
// that swaps the range at a breakpoint reaches aria-valuemin/max too. Cheap:
|
|
299
|
+
// the identity guard stops it looping, and it is skipped mid-drag, where the
|
|
300
|
+
// range cannot change and the drag holds its own copy anyway.
|
|
301
|
+
useIsomorphicLayoutEffect(() => {
|
|
302
|
+
const sidebar = store.sidebarRef.current;
|
|
303
|
+
if (!sidebar || state.resizing)
|
|
304
|
+
return;
|
|
305
|
+
const next = readBounds(sidebar);
|
|
306
|
+
setBounds((current) => (current.min === next.min && current.max === next.max ? current : next));
|
|
307
|
+
});
|
|
308
|
+
const endDrag = useCallback(() => {
|
|
309
|
+
if (!drag.current)
|
|
310
|
+
return;
|
|
311
|
+
drag.current = null;
|
|
312
|
+
store.getSnapshot().setResizing(false);
|
|
313
|
+
}, [store]);
|
|
314
|
+
// A drag can end without a pointerup: cmd-tab away, or the tab going to the
|
|
315
|
+
// background. Left unhandled the flag sticks and everything downstream of
|
|
316
|
+
// `data-resizing` stays wrong until a reload.
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!state.resizing)
|
|
319
|
+
return;
|
|
320
|
+
window.addEventListener("blur", endDrag);
|
|
321
|
+
document.addEventListener("visibilitychange", endDrag);
|
|
322
|
+
return () => {
|
|
323
|
+
window.removeEventListener("blur", endDrag);
|
|
324
|
+
document.removeEventListener("visibilitychange", endDrag);
|
|
325
|
+
};
|
|
326
|
+
}, [state.resizing, endDrag]);
|
|
327
|
+
const handlePointerDown = (event) => {
|
|
328
|
+
const sidebar = store.sidebarRef.current;
|
|
329
|
+
if (!sidebar || event.button !== 0)
|
|
330
|
+
return;
|
|
331
|
+
event.preventDefault();
|
|
332
|
+
// Not every environment implements pointer capture; without it the drag
|
|
333
|
+
// still works, it just stops tracking once the pointer leaves the handle.
|
|
334
|
+
if ("setPointerCapture" in event.currentTarget) {
|
|
335
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
336
|
+
}
|
|
337
|
+
drag.current = {
|
|
338
|
+
pointerId: event.pointerId,
|
|
339
|
+
startX: event.clientX,
|
|
340
|
+
startWidth: sidebar.offsetWidth,
|
|
341
|
+
// Read once, at the start: the range cannot change mid-drag, and
|
|
342
|
+
// re-reading computed style on every pointermove would be a needless
|
|
343
|
+
// style recalculation per frame.
|
|
344
|
+
bounds: readBounds(sidebar),
|
|
345
|
+
};
|
|
346
|
+
store.getSnapshot().setResizing(true);
|
|
347
|
+
};
|
|
348
|
+
const handlePointerMove = (event) => {
|
|
349
|
+
const sidebar = store.sidebarRef.current;
|
|
350
|
+
if (!drag.current || !sidebar || event.pointerId !== drag.current.pointerId)
|
|
351
|
+
return;
|
|
352
|
+
const travelled = (event.clientX - drag.current.startX) * widenDirection(sidebar);
|
|
353
|
+
publishWidth(store, drag.current.startWidth + travelled, drag.current.bounds);
|
|
354
|
+
};
|
|
355
|
+
const handleKeyDown = (event) => {
|
|
356
|
+
const sidebar = store.sidebarRef.current;
|
|
357
|
+
if (!sidebar)
|
|
358
|
+
return;
|
|
359
|
+
const towards = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
|
|
360
|
+
if (towards === 0)
|
|
361
|
+
return;
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
const next = sidebar.offsetWidth + towards * widenDirection(sidebar) * step;
|
|
364
|
+
publishWidth(store, next, readBounds(sidebar));
|
|
365
|
+
};
|
|
366
|
+
return useRenderElement("div", { className, render, style }, {
|
|
367
|
+
state,
|
|
368
|
+
props: [
|
|
369
|
+
{
|
|
370
|
+
"data-shell-grip": "",
|
|
371
|
+
role: "separator",
|
|
372
|
+
"aria-orientation": "vertical",
|
|
373
|
+
// A focusable separator must always carry a value; omitting it until
|
|
374
|
+
// the first measurement lands would leave the widget invalid for that
|
|
375
|
+
// window. The minimum is the least misleading stand-in, and it is
|
|
376
|
+
// corrected on the next frame.
|
|
377
|
+
"aria-valuenow": width ?? bounds.min,
|
|
378
|
+
"aria-valuemin": bounds.min,
|
|
379
|
+
"aria-valuemax": Number.isFinite(bounds.max) ? bounds.max : undefined,
|
|
380
|
+
tabIndex: 0,
|
|
381
|
+
onPointerDown: handlePointerDown,
|
|
382
|
+
onPointerMove: handlePointerMove,
|
|
383
|
+
onPointerUp: endDrag,
|
|
384
|
+
onPointerCancel: endDrag,
|
|
385
|
+
onKeyDown: handleKeyDown,
|
|
386
|
+
},
|
|
387
|
+
elementProps,
|
|
388
|
+
],
|
|
389
|
+
});
|
|
390
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Shell store — all reactive shell state in one closure, so `useShell` can
|
|
4
|
+
* offer selectors and a component re-renders only for the value it reads.
|
|
5
|
+
* Not React state and not Zustand: a plain object with `subscribe` and
|
|
6
|
+
* `getSnapshot`, read through `useSyncExternalStore`.
|
|
7
|
+
*
|
|
8
|
+
* The factory takes no arguments. Everything configurable is either a
|
|
9
|
+
* `Shell.Root` prop (`defaultOpen`) seeded through `hydrate()` before the first
|
|
10
|
+
* paint, or — for anything dimensional — CSS.
|
|
11
|
+
*
|
|
12
|
+
* Nothing is persisted here. `open` goes out through `onOpenChange` and comes
|
|
13
|
+
* back as `defaultOpen`; the measured width goes out through `Shell.Sidebar`'s
|
|
14
|
+
* `onResize` and comes back as **CSS** — the custom property, which is
|
|
15
|
+
* where the width lives anyway. Where any of it is kept is the app's business,
|
|
16
|
+
* and the package needs no opinion about serialisation, keys or cookie flags.
|
|
17
|
+
*
|
|
18
|
+
* **Width is not owned here.** The sidebar's size, and the range it may be
|
|
19
|
+
* dragged through, are `width` / `min-width` / `max-width` in the consumer's
|
|
20
|
+
* stylesheet. The browser clamps; this store only records what the element
|
|
21
|
+
* actually measured, because persistence and `aria-valuenow` need a number.
|
|
22
|
+
* Measure in JS, size in CSS.
|
|
23
|
+
*
|
|
24
|
+
* Instance model: every `Shell.Root` owns a store — an explicit
|
|
25
|
+
* `Shell.createStore()` handle, or one created per mount — and provides it on
|
|
26
|
+
* `ShellStoreContext`. Inside the tree `useShell` resolves it implicitly;
|
|
27
|
+
* outside, `useShellStore(handle, selector)` takes the handle explicitly.
|
|
28
|
+
* There is deliberately **no global fallback**, so shell state is never read
|
|
29
|
+
* or driven by accident from somewhere that just happened to import this.
|
|
30
|
+
*
|
|
31
|
+
* SSR-safe by invariant: every write happens in an effect, an event handler,
|
|
32
|
+
* or `hydrate()` before anything has subscribed — so a server render only ever
|
|
33
|
+
* sees the pristine snapshot and one getter serves both sides of hydration.
|
|
34
|
+
*/
|
|
35
|
+
export type ShellState = {
|
|
36
|
+
open: boolean;
|
|
37
|
+
/** The collapsed sidebar floating over the content on hover. Never persisted. */
|
|
38
|
+
hotspot: boolean;
|
|
39
|
+
/** True for the duration of a resize drag, so parts can suppress transitions. */
|
|
40
|
+
resizing: boolean;
|
|
41
|
+
/** Measured from the sidebar element. Null until it has mounted. */
|
|
42
|
+
width: number | null;
|
|
43
|
+
setOpen: (open: boolean) => void;
|
|
44
|
+
toggle: () => void;
|
|
45
|
+
setHotspot: (hotspot: boolean) => void;
|
|
46
|
+
setResizing: (resizing: boolean) => void;
|
|
47
|
+
/** Reports a measurement. Not a request to resize — CSS decides the size. */
|
|
48
|
+
setWidth: (width: number) => void;
|
|
49
|
+
};
|
|
50
|
+
export type ShellStore = {
|
|
51
|
+
subscribe: (listener: () => void) => () => void;
|
|
52
|
+
getSnapshot: () => ShellState;
|
|
53
|
+
/**
|
|
54
|
+
* Seed state before the first render commits: no notify and no report back,
|
|
55
|
+
* because nothing has subscribed yet and re-announcing what we just restored
|
|
56
|
+
* is pointless. This is how `defaultOpen` reaches the store without a flash.
|
|
57
|
+
*/
|
|
58
|
+
hydrate: (state: {
|
|
59
|
+
open?: boolean;
|
|
60
|
+
}) => void;
|
|
61
|
+
/** Set while `open` is a controlled prop, which makes `setOpen` report-only. */
|
|
62
|
+
controlledRef: RefObject<boolean>;
|
|
63
|
+
onOpenChangeRef: RefObject<((open: boolean) => void) | null>;
|
|
64
|
+
/** Reports a settled size — never the per-frame values a drag produces. */
|
|
65
|
+
onResizeRef: RefObject<((width: number) => void) | null>;
|
|
66
|
+
/** Commits `open` past the controlled guard — how the Root syncs its prop in. */
|
|
67
|
+
commitOpen: (open: boolean) => void;
|
|
68
|
+
/**
|
|
69
|
+
* The mounted `Shell.Root`'s element. The width custom property is written
|
|
70
|
+
* here rather than on the sidebar, so siblings — an in-flow spacer holding
|
|
71
|
+
* the sidebar's place while it floats, say — can read the same value.
|
|
72
|
+
*/
|
|
73
|
+
rootRef: RefObject<HTMLElement | null>;
|
|
74
|
+
/**
|
|
75
|
+
* The mounted `Shell.Sidebar`'s element, registered by its ref. The resize
|
|
76
|
+
* handle sizes and measures it; the trigger points `aria-controls` at its id.
|
|
77
|
+
*/
|
|
78
|
+
sidebarRef: RefObject<HTMLElement | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Stable per-instance id for the sidebar, assigned by the mounting
|
|
81
|
+
* `Shell.Root` from React's `useId` (SSR-stable). The factory can't mint it,
|
|
82
|
+
* because explicit handles are created outside React.
|
|
83
|
+
*/
|
|
84
|
+
sidebarId: string;
|
|
85
|
+
/**
|
|
86
|
+
* Set while collapsing, so a panel sliding out from under a parked pointer
|
|
87
|
+
* doesn't bounce straight back as a hotspot. Cleared once the pointer leaves
|
|
88
|
+
* the edge strip. Imperative, not reactive.
|
|
89
|
+
*/
|
|
90
|
+
hotspotSuppressionRef: RefObject<boolean>;
|
|
91
|
+
};
|
|
92
|
+
export declare const createShellStore: () => ShellStore;
|
|
93
|
+
export declare const ShellStoreContext: import("react").Context<ShellStore | null>;
|
|
94
|
+
/** Internal: parts resolve the store their Shell.Root provided. */
|
|
95
|
+
export declare const useShellContextStore: () => ShellStore;
|
|
96
|
+
/**
|
|
97
|
+
* Subscribe to an explicit `Shell.createStore()` handle — the
|
|
98
|
+
* outside-the-tree twin of `useShell`, for command palettes, global shortcut
|
|
99
|
+
* handlers and anything else that isn't a descendant. Wrap it once per
|
|
100
|
+
* instance to drop the argument at call sites:
|
|
101
|
+
*
|
|
102
|
+
* const useAppShell = <T,>(selector: (shell: ShellState) => T) =>
|
|
103
|
+
* useShellStore(appShellStore, selector);
|
|
104
|
+
*/
|
|
105
|
+
export declare const useShellStore: <Selected = ShellState>(store: ShellStore, selector?: (shell: ShellState) => Selected) => Selected;
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to shell state from inside the tree. With a selector the component
|
|
108
|
+
* re-renders only when that value changes; without one it re-renders on any
|
|
109
|
+
* change. The actions are part of the snapshot and their identities never
|
|
110
|
+
* change, so selecting one never causes a render.
|
|
111
|
+
*/
|
|
112
|
+
export declare const useShell: <Selected = ShellState>(selector?: (shell: ShellState) => Selected) => Selected;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, use, useSyncExternalStore } from "react";
|
|
3
|
+
export const createShellStore = () => {
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const notify = () => {
|
|
6
|
+
for (const listener of listeners)
|
|
7
|
+
listener();
|
|
8
|
+
};
|
|
9
|
+
const controlledRef = { current: false };
|
|
10
|
+
const onOpenChangeRef = { current: null };
|
|
11
|
+
const onResizeRef = { current: null };
|
|
12
|
+
const hotspotSuppressionRef = { current: false };
|
|
13
|
+
const rootRef = { current: null };
|
|
14
|
+
const sidebarRef = { current: null };
|
|
15
|
+
let snapshot;
|
|
16
|
+
// A drag emits a measurement per frame, and reporting each one would hand the
|
|
17
|
+
// app a hundred sizes the user is still in the middle of choosing.
|
|
18
|
+
// `setResizing(false)` reports whatever it landed on.
|
|
19
|
+
const reportResize = () => {
|
|
20
|
+
if (snapshot.resizing || snapshot.width === null)
|
|
21
|
+
return;
|
|
22
|
+
onResizeRef.current?.(snapshot.width);
|
|
23
|
+
};
|
|
24
|
+
const commitOpen = (open) => {
|
|
25
|
+
if (snapshot.open === open)
|
|
26
|
+
return;
|
|
27
|
+
// Collapsing arms the bounce-back guard: a panel sliding out from under a
|
|
28
|
+
// parked pointer must not immediately float back as a hotspot.
|
|
29
|
+
if (!open)
|
|
30
|
+
hotspotSuppressionRef.current = true;
|
|
31
|
+
// Hotspot only means anything while collapsed, so opening ends it.
|
|
32
|
+
snapshot = { ...snapshot, open, hotspot: open ? false : snapshot.hotspot };
|
|
33
|
+
notify();
|
|
34
|
+
};
|
|
35
|
+
const setOpen = (open) => {
|
|
36
|
+
// Reported before the commit, and the only effect while controlled: whoever
|
|
37
|
+
// owns the prop decides, then pushes the result back through commitOpen.
|
|
38
|
+
onOpenChangeRef.current?.(open);
|
|
39
|
+
if (!controlledRef.current)
|
|
40
|
+
commitOpen(open);
|
|
41
|
+
};
|
|
42
|
+
const toggle = () => setOpen(!snapshot.open);
|
|
43
|
+
const setHotspot = (hotspot) => {
|
|
44
|
+
// There is nothing to hotspot at while the sidebar is already open.
|
|
45
|
+
const next = hotspot && !snapshot.open;
|
|
46
|
+
if (snapshot.hotspot === next)
|
|
47
|
+
return;
|
|
48
|
+
snapshot = { ...snapshot, hotspot: next };
|
|
49
|
+
notify();
|
|
50
|
+
};
|
|
51
|
+
const setWidth = (width) => {
|
|
52
|
+
if (snapshot.width === width)
|
|
53
|
+
return;
|
|
54
|
+
snapshot = { ...snapshot, width };
|
|
55
|
+
notify();
|
|
56
|
+
reportResize();
|
|
57
|
+
};
|
|
58
|
+
const setResizing = (resizing) => {
|
|
59
|
+
if (snapshot.resizing === resizing)
|
|
60
|
+
return;
|
|
61
|
+
snapshot = { ...snapshot, resizing };
|
|
62
|
+
notify();
|
|
63
|
+
if (!resizing)
|
|
64
|
+
reportResize();
|
|
65
|
+
};
|
|
66
|
+
snapshot = {
|
|
67
|
+
open: true,
|
|
68
|
+
hotspot: false,
|
|
69
|
+
resizing: false,
|
|
70
|
+
width: null,
|
|
71
|
+
setOpen,
|
|
72
|
+
toggle,
|
|
73
|
+
setHotspot,
|
|
74
|
+
setResizing,
|
|
75
|
+
setWidth,
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
subscribe: (listener) => {
|
|
79
|
+
listeners.add(listener);
|
|
80
|
+
return () => {
|
|
81
|
+
listeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
getSnapshot: () => snapshot,
|
|
85
|
+
hydrate: ({ open }) => {
|
|
86
|
+
// Width is deliberately not seedable: it is a *measurement*, and CSS has
|
|
87
|
+
// already decided the number before anything here runs.
|
|
88
|
+
snapshot = { ...snapshot, open: open ?? snapshot.open };
|
|
89
|
+
},
|
|
90
|
+
rootRef,
|
|
91
|
+
sidebarRef,
|
|
92
|
+
sidebarId: "",
|
|
93
|
+
controlledRef,
|
|
94
|
+
onOpenChangeRef,
|
|
95
|
+
onResizeRef,
|
|
96
|
+
commitOpen,
|
|
97
|
+
hotspotSuppressionRef,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Instance resolution — the nearest Shell.Root, explicitly.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
export const ShellStoreContext = createContext(null);
|
|
104
|
+
/** Internal: parts resolve the store their Shell.Root provided. */
|
|
105
|
+
export const useShellContextStore = () => {
|
|
106
|
+
const store = use(ShellStoreContext);
|
|
107
|
+
if (!store)
|
|
108
|
+
throw new Error("Shell parts must be used within <Shell.Root>");
|
|
109
|
+
return store;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Subscribe to an explicit `Shell.createStore()` handle — the
|
|
113
|
+
* outside-the-tree twin of `useShell`, for command palettes, global shortcut
|
|
114
|
+
* handlers and anything else that isn't a descendant. Wrap it once per
|
|
115
|
+
* instance to drop the argument at call sites:
|
|
116
|
+
*
|
|
117
|
+
* const useAppShell = <T,>(selector: (shell: ShellState) => T) =>
|
|
118
|
+
* useShellStore(appShellStore, selector);
|
|
119
|
+
*/
|
|
120
|
+
export const useShellStore = (store, selector) => {
|
|
121
|
+
const getValue = () => {
|
|
122
|
+
const state = store.getSnapshot();
|
|
123
|
+
// Safe: without a selector, Selected defaults to ShellState.
|
|
124
|
+
return selector ? selector(state) : state;
|
|
125
|
+
};
|
|
126
|
+
return useSyncExternalStore(store.subscribe, getValue, getValue);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Subscribe to shell state from inside the tree. With a selector the component
|
|
130
|
+
* re-renders only when that value changes; without one it re-renders on any
|
|
131
|
+
* change. The actions are part of the snapshot and their identities never
|
|
132
|
+
* change, so selecting one never causes a render.
|
|
133
|
+
*/
|
|
134
|
+
export const useShell = (selector) => useShellStore(useShellContextStore(), selector);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StepsIcon as Icon, StepsItem as Item, StepsLabel as Label, StepsPanel as Panel, StepsRoot as Root, StepsStatus as Status, StepsTrigger as Trigger, } from "./steps.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Part names for the `Steps` namespace. Deliberately carries no
|
|
2
|
+
// "use client" directive: this module and index.ts must stay server-resolvable
|
|
3
|
+
// so a React Server Component can reach Steps.Root and its siblings
|
|
4
|
+
// through them. The directive lives one level down, on steps.tsx.
|
|
5
|
+
export { StepsIcon as Icon, StepsItem as Item, StepsLabel as Label, StepsPanel as Panel, StepsRoot as Root, StepsStatus as Status, StepsTrigger as Trigger, } from "./steps.js";
|