@shadow-js/ui 0.0.0-stage → 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { onMount, splitProps, useSignal } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
createPartContext,
|
|
4
|
+
type Direction,
|
|
5
|
+
useDirectionAround,
|
|
6
|
+
useRefSignal,
|
|
7
|
+
} from "@shadow-js/primitives";
|
|
8
|
+
import type { ComponentProps, JSX } from "@shadow-js/web";
|
|
9
|
+
import { GripVerticalIcon } from "@/components/ui/icons";
|
|
10
|
+
import { useUiStrings } from "@/lib/ui-strings";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Moves `delta` percent of the space from panel `index` to panel `index + 1` (or back, for a negative
|
|
15
|
+
* delta), keeping every panel within its bounds and the total unchanged. It is the arithmetic behind
|
|
16
|
+
* the handles, as a pure function.
|
|
17
|
+
*
|
|
18
|
+
* @param sizes The panel sizes, in percent.
|
|
19
|
+
* @param index The panel on the left (or top) of the handle.
|
|
20
|
+
* @param delta The movement in percent; positive moves the handle toward the second panel.
|
|
21
|
+
* @param min The smallest size of each panel.
|
|
22
|
+
* @param max The largest size of each panel.
|
|
23
|
+
* @returns The new sizes, as a new array.
|
|
24
|
+
* @example
|
|
25
|
+
* import { resizeSizes } from "@/components/ui/resizable";
|
|
26
|
+
*
|
|
27
|
+
* resizeSizes([50, 50], 0, 10, [0, 0], [100, 100]); // [60, 40]
|
|
28
|
+
* @since 1.0.0 * @see {@link ResizableHandle}
|
|
29
|
+
*/
|
|
30
|
+
export function resizeSizes(
|
|
31
|
+
sizes: readonly number[],
|
|
32
|
+
index: number,
|
|
33
|
+
delta: number,
|
|
34
|
+
min: readonly number[],
|
|
35
|
+
max: readonly number[],
|
|
36
|
+
): number[] {
|
|
37
|
+
const first = sizes[index];
|
|
38
|
+
const second = sizes[index + 1];
|
|
39
|
+
if (first === undefined || second === undefined) return [...sizes];
|
|
40
|
+
const firstMin = min[index] ?? 0;
|
|
41
|
+
const secondMin = min[index + 1] ?? 0;
|
|
42
|
+
const firstMax = max[index] ?? 100;
|
|
43
|
+
const secondMax = max[index + 1] ?? 100;
|
|
44
|
+
// The first panel can grow to its max or shrink to its min; the second the other way round.
|
|
45
|
+
const low = Math.max(firstMin - first, second - secondMax);
|
|
46
|
+
const high = Math.min(firstMax - first, second - secondMin);
|
|
47
|
+
const step = Math.min(high, Math.max(low, delta));
|
|
48
|
+
const next = [...sizes];
|
|
49
|
+
next[index] = first + step;
|
|
50
|
+
next[index + 1] = second - step;
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the group shares with its panels and handles. @internal */
|
|
55
|
+
interface ResizableContextValue {
|
|
56
|
+
readonly direction: () => "horizontal" | "vertical";
|
|
57
|
+
readonly dir: () => Direction;
|
|
58
|
+
readonly sizes: () => number[];
|
|
59
|
+
readonly min: () => number[];
|
|
60
|
+
readonly max: () => number[];
|
|
61
|
+
readonly claimPanel: (defaultSize: number, minSize: number, maxSize: number) => number;
|
|
62
|
+
readonly move: (index: number, delta: number) => void;
|
|
63
|
+
readonly set: (index: number, size: number) => void;
|
|
64
|
+
/** How many panels have rendered so far: a handle sits after panel `n - 1`, so its index is `n - 1`. */
|
|
65
|
+
readonly panelsClaimed: () => number;
|
|
66
|
+
readonly storageKey: () => string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reports a storage problem. The layout keeps working without storage; the warning tells the
|
|
71
|
+
* developer why the sizes are not remembered.
|
|
72
|
+
*/
|
|
73
|
+
function reportStorage(action: string, error: unknown): void {
|
|
74
|
+
console.warn(`[shadow-ui] resizable: ${action} failed; the sizes are not remembered`, error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const resizableContext = createPartContext<ResizableContextValue>("ResizablePanelGroup");
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Props of {@link ResizablePanelGroup}: the `div` attributes plus the layout options below.
|
|
81
|
+
* @since 1.0.0
|
|
82
|
+
* @see {@link ResizablePanelGroup}
|
|
83
|
+
*/
|
|
84
|
+
export interface ResizablePanelGroupProps extends ComponentProps<"div"> {
|
|
85
|
+
/**
|
|
86
|
+
* The direction of the layout: `horizontal` (panels side by side) or `vertical` (stacked).
|
|
87
|
+
* @default "horizontal"
|
|
88
|
+
*/
|
|
89
|
+
readonly direction?: "horizontal" | "vertical" | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* The shadcn name for `direction` (the v4 prop). When both are given, `direction` wins.
|
|
92
|
+
* @default "horizontal"
|
|
93
|
+
* @see {@link ResizablePanelGroupProps}
|
|
94
|
+
*/
|
|
95
|
+
readonly orientation?: "horizontal" | "vertical" | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Remembers the sizes in `localStorage`, under the key `shadow-ui:resizable:<id>`, and restores
|
|
98
|
+
* them after the group mounts, so the server markup and the first client render agree.
|
|
99
|
+
* @default none (the sizes are not remembered)
|
|
100
|
+
*/
|
|
101
|
+
readonly autoSaveId?: string | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Called with the new sizes (percent of the group) after every change the user makes, by
|
|
104
|
+
* dragging a handle or with its keys. It is not called when a remembered layout is restored.
|
|
105
|
+
* @default none
|
|
106
|
+
*/
|
|
107
|
+
readonly onLayout?: ((sizes: number[]) => void) | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* The reading direction. `"rtl"` mirrors a horizontal group, so the first panel is on the right
|
|
110
|
+
* and the handle keys swap.
|
|
111
|
+
* @default none (inherited from the nearest `DirectionProvider`, then the document, then `"ltr"`)
|
|
112
|
+
*/
|
|
113
|
+
readonly dir?: Direction | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Panels with draggable handles between them. The group keeps the sizes (percent of the group);
|
|
118
|
+
* a handle moves space from one panel to the next, and no handle takes a panel below its `minSize`
|
|
119
|
+
* or above its `maxSize`.
|
|
120
|
+
*
|
|
121
|
+
* Keyboard and ARIA: the group is a plain `div` with no role and no keys of its own. Its handles
|
|
122
|
+
* are the focusable parts; see {@link ResizableHandle}.
|
|
123
|
+
*
|
|
124
|
+
* @param props See {@link ResizablePanelGroupProps}. `class` adds to the group's classes; the rest
|
|
125
|
+
* are the `div` attributes.
|
|
126
|
+
* @returns The group `div` with its panels and handles inside.
|
|
127
|
+
* @example
|
|
128
|
+
* import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
|
129
|
+
*
|
|
130
|
+
* <ResizablePanelGroup direction="horizontal" autoSaveId="editor">
|
|
131
|
+
* <ResizablePanel defaultSize={30} minSize={15}>Files</ResizablePanel>
|
|
132
|
+
* <ResizableHandle withHandle />
|
|
133
|
+
* <ResizablePanel defaultSize={70}>Editor</ResizablePanel>
|
|
134
|
+
* </ResizablePanelGroup>
|
|
135
|
+
* @since 1.0.0
|
|
136
|
+
* @see {@link ResizablePanel}
|
|
137
|
+
*/
|
|
138
|
+
function ResizablePanelGroup(props: ResizablePanelGroupProps): JSX.Element {
|
|
139
|
+
const [local, rest] = splitProps(props, [
|
|
140
|
+
"class",
|
|
141
|
+
"direction",
|
|
142
|
+
"orientation",
|
|
143
|
+
"autoSaveId",
|
|
144
|
+
"onLayout",
|
|
145
|
+
"dir",
|
|
146
|
+
"children",
|
|
147
|
+
]);
|
|
148
|
+
const direction = (): "horizontal" | "vertical" =>
|
|
149
|
+
local.direction ?? local.orientation ?? "horizontal";
|
|
150
|
+
// A dir attribute above the group applies when there is no prop: found after mount, when the
|
|
151
|
+
// group has ancestors.
|
|
152
|
+
const [groupElement, setGroupElement] = useRefSignal<HTMLElement>();
|
|
153
|
+
let groupNode: HTMLElement | undefined;
|
|
154
|
+
const dir = useDirectionAround(() => local.dir as Direction | undefined, groupElement);
|
|
155
|
+
// The panels register their defaults while they render (plain arrays, no signal writes during a
|
|
156
|
+
// render). The sizes signal starts empty and is filled by the first change or the restore on mount.
|
|
157
|
+
const defaults: number[] = [];
|
|
158
|
+
const minimums: number[] = [];
|
|
159
|
+
const maximums: number[] = [];
|
|
160
|
+
const [sizes, setSizes] = useSignal<number[]>([]);
|
|
161
|
+
const currentSizes = (): number[] => {
|
|
162
|
+
const current = sizes();
|
|
163
|
+
return current.length > 0 ? current : [...defaults];
|
|
164
|
+
};
|
|
165
|
+
let claimed = 0;
|
|
166
|
+
const storageKey = (): string | undefined =>
|
|
167
|
+
local.autoSaveId === undefined
|
|
168
|
+
? undefined
|
|
169
|
+
: `shadow-ui:resizable:${local.autoSaveId as string}`;
|
|
170
|
+
|
|
171
|
+
const commit = (next: number[]): void => {
|
|
172
|
+
setSizes(next);
|
|
173
|
+
(local.onLayout as ((sizes: number[]) => void) | undefined)?.(next);
|
|
174
|
+
const key = storageKey();
|
|
175
|
+
if (key === undefined) return;
|
|
176
|
+
try {
|
|
177
|
+
localStorage.setItem(key, JSON.stringify(next));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Storage can be full or blocked: the layout still works, it is just not remembered.
|
|
180
|
+
reportStorage("saving the layout", error);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Restores the remembered sizes after mount, so the server and the first client render agree.
|
|
185
|
+
onMount(() => {
|
|
186
|
+
setGroupElement(groupNode);
|
|
187
|
+
const key = storageKey();
|
|
188
|
+
if (key === undefined) return;
|
|
189
|
+
try {
|
|
190
|
+
const saved: unknown = JSON.parse(localStorage.getItem(key) ?? "null");
|
|
191
|
+
if (
|
|
192
|
+
Array.isArray(saved) &&
|
|
193
|
+
saved.length === defaults.length &&
|
|
194
|
+
saved.every((value) => typeof value === "number")
|
|
195
|
+
) {
|
|
196
|
+
setSizes(saved as number[]);
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// A value that is not JSON is ignored and the defaults apply; the mismatch is reported.
|
|
200
|
+
reportStorage("reading the saved layout", error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const context: ResizableContextValue = {
|
|
205
|
+
direction,
|
|
206
|
+
dir,
|
|
207
|
+
sizes: currentSizes,
|
|
208
|
+
min: () => minimums,
|
|
209
|
+
max: () => maximums,
|
|
210
|
+
claimPanel(defaultSize, minSize, maxSize) {
|
|
211
|
+
const index = claimed;
|
|
212
|
+
claimed += 1;
|
|
213
|
+
defaults.push(defaultSize);
|
|
214
|
+
minimums.push(minSize);
|
|
215
|
+
maximums.push(maxSize);
|
|
216
|
+
return index;
|
|
217
|
+
},
|
|
218
|
+
move(index, delta) {
|
|
219
|
+
commit(resizeSizes(currentSizes(), index, delta, minimums, maximums));
|
|
220
|
+
},
|
|
221
|
+
set(index, size) {
|
|
222
|
+
const current = currentSizes();
|
|
223
|
+
const delta = size - (current[index] ?? 0);
|
|
224
|
+
commit(resizeSizes(current, index, delta, minimums, maximums));
|
|
225
|
+
},
|
|
226
|
+
panelsClaimed: () => defaults.length,
|
|
227
|
+
storageKey,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<resizableContext.Provider value={context}>
|
|
232
|
+
<div
|
|
233
|
+
ref={(node: HTMLElement) => {
|
|
234
|
+
groupNode = node;
|
|
235
|
+
}}
|
|
236
|
+
data-slot="resizable-panel-group"
|
|
237
|
+
data-panel-group-direction={direction()}
|
|
238
|
+
data-orientation={direction()}
|
|
239
|
+
dir={local.dir as Direction | undefined}
|
|
240
|
+
class={cn(
|
|
241
|
+
"flex h-full w-full",
|
|
242
|
+
direction() === "vertical" ? "flex-col" : "flex-row",
|
|
243
|
+
local.class,
|
|
244
|
+
)}
|
|
245
|
+
{...rest}
|
|
246
|
+
>
|
|
247
|
+
{local.children as JSX.Element}
|
|
248
|
+
</div>
|
|
249
|
+
</resizableContext.Provider>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Props of {@link ResizablePanel}: the `div` attributes plus the size bounds below.
|
|
255
|
+
* @since 1.0.0
|
|
256
|
+
* @see {@link ResizablePanel}
|
|
257
|
+
*/
|
|
258
|
+
export interface ResizablePanelProps extends ComponentProps<"div"> {
|
|
259
|
+
/**
|
|
260
|
+
* The starting size, in percent of the group.
|
|
261
|
+
* @default 50
|
|
262
|
+
*/
|
|
263
|
+
readonly defaultSize?: number | undefined;
|
|
264
|
+
/**
|
|
265
|
+
* The smallest size a handle can leave this panel, in percent of the group.
|
|
266
|
+
* @default 10
|
|
267
|
+
*/
|
|
268
|
+
readonly minSize?: number | undefined;
|
|
269
|
+
/**
|
|
270
|
+
* The largest size a handle can give this panel, in percent of the group.
|
|
271
|
+
* @default 100
|
|
272
|
+
*/
|
|
273
|
+
readonly maxSize?: number | undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* One panel of a {@link ResizablePanelGroup}. It takes its share of the group's space: `defaultSize`
|
|
278
|
+
* until a handle moves it, within `minSize` and `maxSize`.
|
|
279
|
+
*
|
|
280
|
+
* Keyboard and ARIA: a plain `div` with no role and no keys; its size is changed by the handles.
|
|
281
|
+
*
|
|
282
|
+
* @param props See {@link ResizablePanelProps}. `class` adds to the panel's classes; the rest are the
|
|
283
|
+
* `div` attributes.
|
|
284
|
+
* @returns The panel `div`.
|
|
285
|
+
* @throws Error when rendered outside a {@link ResizablePanelGroup}.
|
|
286
|
+
* @example
|
|
287
|
+
* import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
|
288
|
+
*
|
|
289
|
+
* <ResizablePanelGroup>
|
|
290
|
+
* <ResizablePanel defaultSize={30} minSize={15}>Files</ResizablePanel>
|
|
291
|
+
* </ResizablePanelGroup>
|
|
292
|
+
* @since 1.0.0
|
|
293
|
+
* @see {@link ResizableHandle}
|
|
294
|
+
*/
|
|
295
|
+
function ResizablePanel(props: ResizablePanelProps): JSX.Element {
|
|
296
|
+
const group = resizableContext.use("ResizablePanel");
|
|
297
|
+
const [local, rest] = splitProps(props, [
|
|
298
|
+
"class",
|
|
299
|
+
"defaultSize",
|
|
300
|
+
"minSize",
|
|
301
|
+
"maxSize",
|
|
302
|
+
"children",
|
|
303
|
+
]);
|
|
304
|
+
const index = group.claimPanel(
|
|
305
|
+
(local.defaultSize as number | undefined) ?? 50,
|
|
306
|
+
(local.minSize as number | undefined) ?? 10,
|
|
307
|
+
(local.maxSize as number | undefined) ?? 100,
|
|
308
|
+
);
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
data-slot="resizable-panel"
|
|
312
|
+
data-panel-index={index}
|
|
313
|
+
style={{ flex: `${group.sizes()[index] ?? 0} 1 0%`, minWidth: 0, minHeight: 0 }}
|
|
314
|
+
class={cn("min-h-0 min-w-0 overflow-hidden", local.class)}
|
|
315
|
+
{...rest}
|
|
316
|
+
>
|
|
317
|
+
{local.children as JSX.Element}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Props of {@link ResizableHandle}: the `div` attributes plus `withHandle`.
|
|
324
|
+
* @since 1.0.0
|
|
325
|
+
* @see {@link ResizableHandle}
|
|
326
|
+
*/
|
|
327
|
+
export interface ResizableHandleProps extends ComponentProps<"div"> {
|
|
328
|
+
/**
|
|
329
|
+
* Shows a grip in the middle of the handle, so the bar reads as draggable.
|
|
330
|
+
* @default false
|
|
331
|
+
*/
|
|
332
|
+
readonly withHandle?: boolean | undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The bar between two panels. Drag it with the pointer. Focus it and use the arrow keys to move it
|
|
337
|
+
* 5 percent a step (mirrored in right-to-left layouts), or Home and End to make the first panel as
|
|
338
|
+
* small or as large as its bounds allow.
|
|
339
|
+
*
|
|
340
|
+
* Keyboard and ARIA: a focusable `role="separator"` (`tabindex="0"`). Its `aria-orientation` is the
|
|
341
|
+
* handle's own axis (`vertical` in a horizontal group). `aria-valuenow`, `aria-valuemin` and
|
|
342
|
+
* `aria-valuemax` give the first panel's size and bounds, rounded. It is named "Resize panels" unless
|
|
343
|
+
* you pass your own `aria-label`. Left and Right move a handle in a horizontal group; Up and Down
|
|
344
|
+
* move a handle in a vertical group. Home and End set the first panel to its smallest and largest.
|
|
345
|
+
*
|
|
346
|
+
* @param props See {@link ResizableHandleProps}. `class` adds to the handle's classes; the rest are
|
|
347
|
+
* the `div` attributes.
|
|
348
|
+
* @returns The handle `div`.
|
|
349
|
+
* @throws Error when rendered outside a {@link ResizablePanelGroup}.
|
|
350
|
+
* @example
|
|
351
|
+
* import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
|
352
|
+
*
|
|
353
|
+
* <ResizablePanelGroup>
|
|
354
|
+
* <ResizablePanel>Files</ResizablePanel>
|
|
355
|
+
* <ResizableHandle withHandle />
|
|
356
|
+
* <ResizablePanel>Editor</ResizablePanel>
|
|
357
|
+
* </ResizablePanelGroup>
|
|
358
|
+
* @since 1.0.0
|
|
359
|
+
* @see {@link ResizablePanelGroup}
|
|
360
|
+
*/
|
|
361
|
+
function ResizableHandle(props: ResizableHandleProps): JSX.Element {
|
|
362
|
+
const group = resizableContext.use("ResizableHandle");
|
|
363
|
+
const t = useUiStrings();
|
|
364
|
+
const [local, rest] = splitProps(props, ["class", "withHandle"]);
|
|
365
|
+
// A handle sits right after the panel before it, so its index follows from the panels rendered.
|
|
366
|
+
const handleIndex = Math.max(0, group.panelsClaimed() - 1);
|
|
367
|
+
const step = 5;
|
|
368
|
+
const vertical = (): boolean => group.direction() === "vertical";
|
|
369
|
+
let stopDrag: (() => void) | undefined;
|
|
370
|
+
|
|
371
|
+
const startDrag = (event: PointerEvent): void => {
|
|
372
|
+
if (event.button !== 0) return;
|
|
373
|
+
stopDrag?.();
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
const axis = vertical() ? "clientY" : "clientX";
|
|
376
|
+
const start = event[axis];
|
|
377
|
+
// The group is the handle's parent: measured from the event, so no reference is needed.
|
|
378
|
+
const box = (event.currentTarget as HTMLElement | null)?.parentElement?.getBoundingClientRect();
|
|
379
|
+
const length = box === undefined ? 0 : vertical() ? box.height : box.width;
|
|
380
|
+
const mirror = !vertical() && group.dir() === "rtl" ? -1 : 1;
|
|
381
|
+
const startSize = group.sizes()[handleIndex] ?? 0;
|
|
382
|
+
// Only the pointer that pressed the handle moves it; another finger is not part of this drag.
|
|
383
|
+
const pointer = event.pointerId;
|
|
384
|
+
const move = (moveEvent: PointerEvent): void => {
|
|
385
|
+
if (moveEvent.pointerId !== pointer || length <= 0) return;
|
|
386
|
+
const percent = ((moveEvent[axis] - start) / length) * 100 * mirror;
|
|
387
|
+
group.set(handleIndex, startSize + percent);
|
|
388
|
+
};
|
|
389
|
+
const end = (finished: PointerEvent): void => {
|
|
390
|
+
if (finished.pointerId === pointer) stopDrag?.();
|
|
391
|
+
};
|
|
392
|
+
window.addEventListener("pointermove", move);
|
|
393
|
+
window.addEventListener("pointerup", end);
|
|
394
|
+
window.addEventListener("pointercancel", end);
|
|
395
|
+
stopDrag = () => {
|
|
396
|
+
window.removeEventListener("pointermove", move);
|
|
397
|
+
window.removeEventListener("pointerup", end);
|
|
398
|
+
window.removeEventListener("pointercancel", end);
|
|
399
|
+
stopDrag = undefined;
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<div
|
|
405
|
+
role="separator"
|
|
406
|
+
tabindex={0}
|
|
407
|
+
aria-orientation={vertical() ? "horizontal" : "vertical"}
|
|
408
|
+
aria-valuenow={Math.round(group.sizes()[handleIndex] ?? 0)}
|
|
409
|
+
aria-valuemin={Math.round(group.min()[handleIndex] ?? 0)}
|
|
410
|
+
aria-valuemax={Math.round(group.max()[handleIndex] ?? 100)}
|
|
411
|
+
aria-label={t("resizable.handle", "Resize panels")}
|
|
412
|
+
data-slot="resizable-handle"
|
|
413
|
+
data-orientation={vertical() ? "vertical" : "horizontal"}
|
|
414
|
+
class={cn(
|
|
415
|
+
// touch-none: a touch drag moves the handle; the browser must not pan or scroll instead.
|
|
416
|
+
"relative flex touch-none items-center justify-center bg-border outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 forced-colors:bg-[CanvasText] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
417
|
+
vertical()
|
|
418
|
+
? "h-px w-full after:absolute after:inset-x-0 after:top-1/2 after:h-1 after:-translate-y-1/2 cursor-row-resize"
|
|
419
|
+
: "h-full w-px after:absolute after:inset-y-0 after:start-1/2 after:w-1 after:-translate-x-1/2 cursor-col-resize",
|
|
420
|
+
local.class,
|
|
421
|
+
)}
|
|
422
|
+
onPointerDown={(event: PointerEvent) => startDrag(event)}
|
|
423
|
+
onKeyDown={(event: KeyboardEvent) => {
|
|
424
|
+
const forward = vertical()
|
|
425
|
+
? "ArrowDown"
|
|
426
|
+
: group.dir() === "rtl"
|
|
427
|
+
? "ArrowLeft"
|
|
428
|
+
: "ArrowRight";
|
|
429
|
+
const back = vertical() ? "ArrowUp" : group.dir() === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
430
|
+
if (event.key === forward) {
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
group.move(handleIndex, step);
|
|
433
|
+
} else if (event.key === back) {
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
group.move(handleIndex, -step);
|
|
436
|
+
} else if (event.key === "Home") {
|
|
437
|
+
event.preventDefault();
|
|
438
|
+
group.set(handleIndex, group.min()[handleIndex] ?? 0);
|
|
439
|
+
} else if (event.key === "End") {
|
|
440
|
+
event.preventDefault();
|
|
441
|
+
group.set(handleIndex, group.max()[handleIndex] ?? 100);
|
|
442
|
+
}
|
|
443
|
+
}}
|
|
444
|
+
{...rest}
|
|
445
|
+
>
|
|
446
|
+
{local.withHandle === true && (
|
|
447
|
+
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border">
|
|
448
|
+
<GripVerticalIcon class={cn("size-2.5", vertical() && "rotate-90")} />
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
ScrollArea as ScrollAreaPrimitive,
|
|
4
|
+
type ScrollAreaProps as ScrollAreaPrimitiveProps,
|
|
5
|
+
type ScrollAreaScrollbarProps as ScrollAreaScrollbarPrimitiveProps,
|
|
6
|
+
} from "@shadow-js/primitives";
|
|
7
|
+
import type { JSX } from "@shadow-js/web";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A scrollable region with thin, styled scrollbars. The scrollbars show on hover by default; set
|
|
12
|
+
* `type` to change that (`"auto"`, `"scroll"` or `"always"`). The thumb can be dragged, and a press
|
|
13
|
+
* on the track outside the thumb pages the content.
|
|
14
|
+
*
|
|
15
|
+
* Keyboard and ARIA: the viewport is the tab stop while the content overflows, and the browser's
|
|
16
|
+
* native scroll keys (arrow keys, Page Up, Page Down, Home and End) move it. The bars and thumbs
|
|
17
|
+
* take no focus and have no key handling. The component sets no role or name; the bars are
|
|
18
|
+
* `role="presentation"`. If the scrolled content needs a name, pass a `role` and an `aria-label`
|
|
19
|
+
* to the root (for example `role="region"`).
|
|
20
|
+
*
|
|
21
|
+
* shadcn/ui differences: `class` replaces `className`, and the viewport, both scrollbars, their
|
|
22
|
+
* thumbs and the corner are rendered for you, each with its `data-slot`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import { ScrollArea } from "@/components/ui/scroll-area";
|
|
27
|
+
*
|
|
28
|
+
* export function Notes() {
|
|
29
|
+
* return (
|
|
30
|
+
* <ScrollArea class="h-72 w-48 rounded-md border">
|
|
31
|
+
* <div class="p-4">Long content goes here.</div>
|
|
32
|
+
* </ScrollArea>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
* @see {@link ScrollArea}
|
|
38
|
+
*/
|
|
39
|
+
export type ScrollAreaProps = ScrollAreaPrimitiveProps;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The scroll area: a viewport that scrolls, two scrollbars and a corner, styled. Pass the content
|
|
43
|
+
* as children; the component adds the rest.
|
|
44
|
+
*
|
|
45
|
+
* @param props - The props, see {@link ScrollAreaProps}.
|
|
46
|
+
* @returns The root `div` with the viewport, the scrollbars and the corner inside.
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* import { ScrollArea } from "@/components/ui/scroll-area";
|
|
50
|
+
*
|
|
51
|
+
* export function Feed() {
|
|
52
|
+
* return (
|
|
53
|
+
* <ScrollArea type="always" class="h-40 w-64">
|
|
54
|
+
* <p>Scrollable text.</p>
|
|
55
|
+
* </ScrollArea>
|
|
56
|
+
* );
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
* @since 1.0.0
|
|
60
|
+
* @see {@link ScrollBar}
|
|
61
|
+
*/
|
|
62
|
+
function ScrollArea(props: ScrollAreaProps): JSX.Element {
|
|
63
|
+
const [local, rest] = splitProps(
|
|
64
|
+
props as ScrollAreaProps & { readonly class?: unknown; readonly children?: unknown },
|
|
65
|
+
["class", "children"],
|
|
66
|
+
);
|
|
67
|
+
return (
|
|
68
|
+
<ScrollAreaPrimitive
|
|
69
|
+
data-slot="scroll-area"
|
|
70
|
+
class={cn("relative", local.class as string | undefined)}
|
|
71
|
+
{...rest}
|
|
72
|
+
>
|
|
73
|
+
<ScrollAreaPrimitive.Viewport
|
|
74
|
+
data-slot="scroll-area-viewport"
|
|
75
|
+
class="ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 motion-reduce:transition-none"
|
|
76
|
+
>
|
|
77
|
+
{local.children as JSX.Element}
|
|
78
|
+
</ScrollAreaPrimitive.Viewport>
|
|
79
|
+
<ScrollBar orientation="vertical" />
|
|
80
|
+
<ScrollBar orientation="horizontal" />
|
|
81
|
+
<ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
|
|
82
|
+
</ScrollAreaPrimitive>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Props of {@link ScrollBar}: the primitive's scrollbar props (`orientation` and `forceMount`) and
|
|
88
|
+
* the attributes of the track `div`.
|
|
89
|
+
* @since 1.0.0
|
|
90
|
+
* @see {@link ScrollBar}
|
|
91
|
+
*/
|
|
92
|
+
export type ScrollBarProps = ScrollAreaScrollbarPrimitiveProps;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One scrollbar of a scroll area: a track along the viewport's end edge (vertical) or bottom edge
|
|
96
|
+
* (horizontal), with its thumb. {@link ScrollArea} already renders one of each, so you rarely need
|
|
97
|
+
* this component. It is exported for areas built from `@shadow-js/primitives` directly. Its
|
|
98
|
+
* `orientation` defaults to `"vertical"`.
|
|
99
|
+
*
|
|
100
|
+
* Keyboard and ARIA: the track is pointer only and has `role="presentation"`; it is hidden while the
|
|
101
|
+
* root's `type` says it should not show.
|
|
102
|
+
*
|
|
103
|
+
* @param props - The props, see {@link ScrollBarProps}.
|
|
104
|
+
* @returns The track element, with its thumb inside.
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* import { ScrollArea as ScrollAreaPrimitive } from "@shadow-js/primitives";
|
|
108
|
+
* import { ScrollBar } from "@/components/ui/scroll-area";
|
|
109
|
+
*
|
|
110
|
+
* export function WideList() {
|
|
111
|
+
* return (
|
|
112
|
+
* <ScrollAreaPrimitive class="relative h-40 w-64">
|
|
113
|
+
* <ScrollAreaPrimitive.Viewport class="size-full">
|
|
114
|
+
* <p class="w-[400px]">Wide content.</p>
|
|
115
|
+
* </ScrollAreaPrimitive.Viewport>
|
|
116
|
+
* <ScrollBar orientation="horizontal" />
|
|
117
|
+
* </ScrollAreaPrimitive>
|
|
118
|
+
* );
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
* @since 1.0.0
|
|
122
|
+
* @see {@link ScrollArea}
|
|
123
|
+
*/
|
|
124
|
+
function ScrollBar(props: ScrollBarProps): JSX.Element {
|
|
125
|
+
const [local, rest] = splitProps(
|
|
126
|
+
props as ScrollBarProps & { readonly class?: unknown; readonly orientation?: unknown },
|
|
127
|
+
["class", "orientation"],
|
|
128
|
+
);
|
|
129
|
+
return (
|
|
130
|
+
<ScrollAreaPrimitive.Scrollbar
|
|
131
|
+
data-slot="scroll-area-scrollbar"
|
|
132
|
+
orientation={(local.orientation as "vertical" | "horizontal" | undefined) ?? "vertical"}
|
|
133
|
+
class={cn(
|
|
134
|
+
"flex touch-none p-px transition-colors select-none",
|
|
135
|
+
"data-[orientation=vertical]:absolute data-[orientation=vertical]:inset-y-0 data-[orientation=vertical]:end-0 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:border-s data-[orientation=vertical]:border-s-transparent",
|
|
136
|
+
"data-[orientation=horizontal]:absolute data-[orientation=horizontal]:inset-x-0 data-[orientation=horizontal]:bottom-0 data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:border-t data-[orientation=horizontal]:border-t-transparent",
|
|
137
|
+
local.class as string | undefined,
|
|
138
|
+
)}
|
|
139
|
+
{...rest}
|
|
140
|
+
>
|
|
141
|
+
<ScrollAreaPrimitive.Thumb
|
|
142
|
+
data-slot="scroll-area-thumb"
|
|
143
|
+
class="relative flex-1 rounded-full bg-border forced-colors:bg-[CanvasText] motion-reduce:transition-none"
|
|
144
|
+
/>
|
|
145
|
+
</ScrollAreaPrimitive.Scrollbar>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { ScrollArea, ScrollBar };
|