@shadow-js/ui 1.0.1 → 1.0.3
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 +26 -0
- package/README.md +3 -3
- package/dist/cli.mjs +453 -262
- package/dist/dev/theme/theme-provider.js +1 -1
- package/dist/dev/theme/theme-provider.js.map +1 -1
- package/dist/prod/theme/theme-provider.js +1 -1
- package/dist/prod/theme/theme-provider.js.map +1 -1
- package/package.json +11 -11
- package/registry/carousel/carousel.tsx +79 -15
- package/registry/chart/chart.tsx +5 -1
- package/registry/input-group/input-group.tsx +2 -1
- package/registry/resizable/resizable.tsx +175 -34
- package/registry.json +7 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onMount, splitProps, useSignal } from "@shadow-js/core";
|
|
1
|
+
import { onCleanup, onMount, splitProps, useSignal } from "@shadow-js/core";
|
|
2
2
|
import {
|
|
3
3
|
createPartContext,
|
|
4
4
|
type Direction,
|
|
@@ -51,6 +51,18 @@ export function resizeSizes(
|
|
|
51
51
|
return next;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** One panel as the group tracks it. @internal */
|
|
55
|
+
interface PanelEntry {
|
|
56
|
+
readonly defaultSize: number;
|
|
57
|
+
readonly minSize: number;
|
|
58
|
+
readonly maxSize: number;
|
|
59
|
+
/** Set once a handle, a restored layout or a change of panels gave the panel a size; until then `defaultSize` applies. */
|
|
60
|
+
size: number | undefined;
|
|
61
|
+
element: HTMLElement | undefined;
|
|
62
|
+
/** Registered after the group had mounted: it still has to find its place among the panels that were there. */
|
|
63
|
+
late: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
/** What the group shares with its panels and handles. @internal */
|
|
55
67
|
interface ResizableContextValue {
|
|
56
68
|
readonly direction: () => "horizontal" | "vertical";
|
|
@@ -58,7 +70,18 @@ interface ResizableContextValue {
|
|
|
58
70
|
readonly sizes: () => number[];
|
|
59
71
|
readonly min: () => number[];
|
|
60
72
|
readonly max: () => number[];
|
|
61
|
-
readonly claimPanel: (defaultSize: number, minSize: number, maxSize: number) =>
|
|
73
|
+
readonly claimPanel: (defaultSize: number, minSize: number, maxSize: number) => PanelEntry;
|
|
74
|
+
/** The place of a panel among the panels now, in document order. Reactive. */
|
|
75
|
+
readonly indexOf: (panel: PanelEntry) => number;
|
|
76
|
+
/** Called when a panel mounted: a panel that arrived late is put in its place and given its share. */
|
|
77
|
+
readonly placePanel: (panel: PanelEntry) => void;
|
|
78
|
+
/** Called when a panel unmounted: its share goes back to the others. */
|
|
79
|
+
readonly releasePanel: (panel: PanelEntry) => void;
|
|
80
|
+
/**
|
|
81
|
+
* The panel before a handle, as an index. Before the handle is in the document that is the number of panels rendered so far
|
|
82
|
+
* minus one (`fallback`); after, it is read from the document, so a panel that came or went moves the handle with it. Reactive.
|
|
83
|
+
*/
|
|
84
|
+
readonly handleIndex: (handle: HTMLElement | undefined, fallback: number) => number;
|
|
62
85
|
readonly move: (index: number, delta: number) => void;
|
|
63
86
|
readonly set: (index: number, size: number) => void;
|
|
64
87
|
/** How many panels have rendered so far: a handle sits after panel `n - 1`, so its index is `n - 1`. */
|
|
@@ -66,6 +89,43 @@ interface ResizableContextValue {
|
|
|
66
89
|
readonly storageKey: () => string | undefined;
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
const PRECEDING = 2;
|
|
93
|
+
const FOLLOWING = 4;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Brings sizes to what the group can show: scaled so they sum to 100, then each held within its panel's bounds with the
|
|
97
|
+
* difference spread over the panels that still have room. Best effort when the bounds cannot all be met (their minimums
|
|
98
|
+
* already add up to more than 100).
|
|
99
|
+
*/
|
|
100
|
+
function fitSizes(
|
|
101
|
+
sizes: readonly number[],
|
|
102
|
+
min: readonly number[],
|
|
103
|
+
max: readonly number[],
|
|
104
|
+
): number[] {
|
|
105
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
106
|
+
if (!(total > 0)) return [...sizes];
|
|
107
|
+
const out = sizes.map((size) => (size * 100) / total);
|
|
108
|
+
const clampAll = (): void => {
|
|
109
|
+
for (let at = 0; at < out.length; at += 1) {
|
|
110
|
+
out[at] = Math.min(max[at] ?? 100, Math.max(min[at] ?? 0, out[at] ?? 0));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
for (let pass = 0; pass <= out.length; pass += 1) {
|
|
114
|
+
clampAll();
|
|
115
|
+
const gap = 100 - out.reduce((sum, size) => sum + size, 0);
|
|
116
|
+
if (Math.abs(gap) < 1e-9) break;
|
|
117
|
+
const roomy: number[] = [];
|
|
118
|
+
for (let at = 0; at < out.length; at += 1) {
|
|
119
|
+
const size = out[at] ?? 0;
|
|
120
|
+
if (gap > 0 ? size < (max[at] ?? 100) : size > (min[at] ?? 0)) roomy.push(at);
|
|
121
|
+
}
|
|
122
|
+
if (roomy.length === 0) break;
|
|
123
|
+
for (const at of roomy) out[at] = (out[at] ?? 0) + gap / roomy.length;
|
|
124
|
+
}
|
|
125
|
+
clampAll();
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
69
129
|
/**
|
|
70
130
|
* Reports a storage problem. The layout keeps working without storage; the warning tells the
|
|
71
131
|
* developer why the sizes are not remembered.
|
|
@@ -152,24 +212,48 @@ function ResizablePanelGroup(props: ResizablePanelGroupProps): JSX.Element {
|
|
|
152
212
|
const [groupElement, setGroupElement] = useRefSignal<HTMLElement>();
|
|
153
213
|
let groupNode: HTMLElement | undefined;
|
|
154
214
|
const dir = useDirectionAround(() => local.dir as Direction | undefined, groupElement);
|
|
155
|
-
// The panels register
|
|
156
|
-
//
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
215
|
+
// The panels register while they render (plain array, no signal writes during a render), in document order. A panel that
|
|
216
|
+
// arrives or leaves later changes the array and bumps `version`, which everything that reads the sizes or places follows.
|
|
217
|
+
const panels: PanelEntry[] = [];
|
|
218
|
+
const [version, setVersion] = useSignal(0);
|
|
219
|
+
const touch = (): void => {
|
|
220
|
+
setVersion((count) => count + 1);
|
|
221
|
+
};
|
|
222
|
+
let mounted = false;
|
|
223
|
+
let disposed = false;
|
|
224
|
+
onCleanup(() => {
|
|
225
|
+
disposed = true;
|
|
226
|
+
});
|
|
161
227
|
const currentSizes = (): number[] => {
|
|
162
|
-
|
|
163
|
-
return
|
|
228
|
+
version();
|
|
229
|
+
return panels.map((panel) => panel.size ?? panel.defaultSize);
|
|
230
|
+
};
|
|
231
|
+
const minimums = (): number[] => {
|
|
232
|
+
version();
|
|
233
|
+
return panels.map((panel) => panel.minSize);
|
|
234
|
+
};
|
|
235
|
+
const maximums = (): number[] => {
|
|
236
|
+
version();
|
|
237
|
+
return panels.map((panel) => panel.maxSize);
|
|
238
|
+
};
|
|
239
|
+
const apply = (sizes: readonly number[]): void => {
|
|
240
|
+
panels.forEach((panel, at) => {
|
|
241
|
+
panel.size = sizes[at];
|
|
242
|
+
});
|
|
243
|
+
touch();
|
|
244
|
+
};
|
|
245
|
+
/** The sizes the panels have now, fitted to 100 and to their bounds: what a change of the panels leaves behind. */
|
|
246
|
+
const refit = (): void => {
|
|
247
|
+
if (panels.length === 0) return;
|
|
248
|
+
apply(fitSizes(currentSizes(), minimums(), maximums()));
|
|
164
249
|
};
|
|
165
|
-
let claimed = 0;
|
|
166
250
|
const storageKey = (): string | undefined =>
|
|
167
251
|
local.autoSaveId === undefined
|
|
168
252
|
? undefined
|
|
169
253
|
: `shadow-ui:resizable:${local.autoSaveId as string}`;
|
|
170
254
|
|
|
171
255
|
const commit = (next: number[]): void => {
|
|
172
|
-
|
|
256
|
+
apply(next);
|
|
173
257
|
(local.onLayout as ((sizes: number[]) => void) | undefined)?.(next);
|
|
174
258
|
const key = storageKey();
|
|
175
259
|
if (key === undefined) return;
|
|
@@ -183,6 +267,7 @@ function ResizablePanelGroup(props: ResizablePanelGroupProps): JSX.Element {
|
|
|
183
267
|
|
|
184
268
|
// Restores the remembered sizes after mount, so the server and the first client render agree.
|
|
185
269
|
onMount(() => {
|
|
270
|
+
mounted = true;
|
|
186
271
|
setGroupElement(groupNode);
|
|
187
272
|
const key = storageKey();
|
|
188
273
|
if (key === undefined) return;
|
|
@@ -190,10 +275,11 @@ function ResizablePanelGroup(props: ResizablePanelGroupProps): JSX.Element {
|
|
|
190
275
|
const saved: unknown = JSON.parse(localStorage.getItem(key) ?? "null");
|
|
191
276
|
if (
|
|
192
277
|
Array.isArray(saved) &&
|
|
193
|
-
saved.length ===
|
|
194
|
-
saved.every((value) => typeof value === "number")
|
|
278
|
+
saved.length === panels.length &&
|
|
279
|
+
saved.every((value) => typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
195
280
|
) {
|
|
196
|
-
|
|
281
|
+
// Whatever is stored (edited by hand, saved by an older layout) is made to fit the bounds and the total.
|
|
282
|
+
apply(fitSizes(saved as number[], minimums(), maximums()));
|
|
197
283
|
}
|
|
198
284
|
} catch (error) {
|
|
199
285
|
// A value that is not JSON is ignored and the defaults apply; the mismatch is reported.
|
|
@@ -205,25 +291,65 @@ function ResizablePanelGroup(props: ResizablePanelGroupProps): JSX.Element {
|
|
|
205
291
|
direction,
|
|
206
292
|
dir,
|
|
207
293
|
sizes: currentSizes,
|
|
208
|
-
min:
|
|
209
|
-
max:
|
|
294
|
+
min: minimums,
|
|
295
|
+
max: maximums,
|
|
210
296
|
claimPanel(defaultSize, minSize, maxSize) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
297
|
+
const panel: PanelEntry = {
|
|
298
|
+
defaultSize,
|
|
299
|
+
minSize,
|
|
300
|
+
maxSize,
|
|
301
|
+
size: undefined,
|
|
302
|
+
element: undefined,
|
|
303
|
+
late: mounted,
|
|
304
|
+
};
|
|
305
|
+
panels.push(panel);
|
|
306
|
+
return panel;
|
|
307
|
+
},
|
|
308
|
+
indexOf(panel) {
|
|
309
|
+
version();
|
|
310
|
+
return panels.indexOf(panel);
|
|
311
|
+
},
|
|
312
|
+
placePanel(panel) {
|
|
313
|
+
if (!panel.late || disposed) return;
|
|
314
|
+
panel.late = false;
|
|
315
|
+
// It was appended when it rendered; the document says where it really is.
|
|
316
|
+
panels.sort((first, second) => {
|
|
317
|
+
if (first.element === undefined || second.element === undefined) return 0;
|
|
318
|
+
return (first.element.compareDocumentPosition(second.element) & FOLLOWING) !== 0 ? -1 : 1;
|
|
319
|
+
});
|
|
320
|
+
refit();
|
|
321
|
+
},
|
|
322
|
+
releasePanel(panel) {
|
|
323
|
+
const at = panels.indexOf(panel);
|
|
324
|
+
if (at === -1) return;
|
|
325
|
+
panels.splice(at, 1);
|
|
326
|
+
if (disposed) return;
|
|
327
|
+
if (panels.length === 0) touch();
|
|
328
|
+
else refit();
|
|
329
|
+
},
|
|
330
|
+
handleIndex(handle, fallback) {
|
|
331
|
+
version();
|
|
332
|
+
if (handle === undefined || !mounted) return fallback;
|
|
333
|
+
let before = 0;
|
|
334
|
+
for (const panel of panels) {
|
|
335
|
+
if (
|
|
336
|
+
panel.element !== undefined &&
|
|
337
|
+
(handle.compareDocumentPosition(panel.element) & PRECEDING) !== 0
|
|
338
|
+
) {
|
|
339
|
+
before += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return Math.max(0, before - 1);
|
|
217
343
|
},
|
|
218
344
|
move(index, delta) {
|
|
219
|
-
commit(resizeSizes(currentSizes(), index, delta, minimums, maximums));
|
|
345
|
+
commit(resizeSizes(currentSizes(), index, delta, minimums(), maximums()));
|
|
220
346
|
},
|
|
221
347
|
set(index, size) {
|
|
222
348
|
const current = currentSizes();
|
|
223
349
|
const delta = size - (current[index] ?? 0);
|
|
224
|
-
commit(resizeSizes(current, index, delta, minimums, maximums));
|
|
350
|
+
commit(resizeSizes(current, index, delta, minimums(), maximums()));
|
|
225
351
|
},
|
|
226
|
-
panelsClaimed: () =>
|
|
352
|
+
panelsClaimed: () => panels.length,
|
|
227
353
|
storageKey,
|
|
228
354
|
};
|
|
229
355
|
|
|
@@ -301,16 +427,22 @@ function ResizablePanel(props: ResizablePanelProps): JSX.Element {
|
|
|
301
427
|
"maxSize",
|
|
302
428
|
"children",
|
|
303
429
|
]);
|
|
304
|
-
const
|
|
430
|
+
const panel = group.claimPanel(
|
|
305
431
|
(local.defaultSize as number | undefined) ?? 50,
|
|
306
432
|
(local.minSize as number | undefined) ?? 10,
|
|
307
433
|
(local.maxSize as number | undefined) ?? 100,
|
|
308
434
|
);
|
|
435
|
+
const index = (): number => group.indexOf(panel);
|
|
436
|
+
onMount(() => group.placePanel(panel));
|
|
437
|
+
onCleanup(() => group.releasePanel(panel));
|
|
309
438
|
return (
|
|
310
439
|
<div
|
|
440
|
+
ref={(node: HTMLElement) => {
|
|
441
|
+
panel.element = node;
|
|
442
|
+
}}
|
|
311
443
|
data-slot="resizable-panel"
|
|
312
|
-
data-panel-index={index}
|
|
313
|
-
style={{ flex: `${group.sizes()[index] ?? 0} 1 0%`, minWidth: 0, minHeight: 0 }}
|
|
444
|
+
data-panel-index={index()}
|
|
445
|
+
style={{ flex: `${group.sizes()[index()] ?? 0} 1 0%`, minWidth: 0, minHeight: 0 }}
|
|
314
446
|
class={cn("min-h-0 min-w-0 overflow-hidden", local.class)}
|
|
315
447
|
{...rest}
|
|
316
448
|
>
|
|
@@ -362,11 +494,15 @@ function ResizableHandle(props: ResizableHandleProps): JSX.Element {
|
|
|
362
494
|
const group = resizableContext.use("ResizableHandle");
|
|
363
495
|
const t = useUiStrings();
|
|
364
496
|
const [local, rest] = splitProps(props, ["class", "withHandle"]);
|
|
365
|
-
// A handle sits right after the panel before it
|
|
366
|
-
const
|
|
497
|
+
// A handle sits right after the panel before it: from the panels rendered so far, then from the document once it is there.
|
|
498
|
+
const fallbackIndex = Math.max(0, group.panelsClaimed() - 1);
|
|
499
|
+
let handleNode: HTMLElement | undefined;
|
|
500
|
+
const handleIndexNow = (): number => group.handleIndex(handleNode, fallbackIndex);
|
|
367
501
|
const step = 5;
|
|
368
502
|
const vertical = (): boolean => group.direction() === "vertical";
|
|
369
503
|
let stopDrag: (() => void) | undefined;
|
|
504
|
+
// A handle that unmounts mid-drag (a panel closes) must not keep resizing from window listeners.
|
|
505
|
+
onCleanup(() => stopDrag?.());
|
|
370
506
|
|
|
371
507
|
const startDrag = (event: PointerEvent): void => {
|
|
372
508
|
if (event.button !== 0) return;
|
|
@@ -378,6 +514,7 @@ function ResizableHandle(props: ResizableHandleProps): JSX.Element {
|
|
|
378
514
|
const box = (event.currentTarget as HTMLElement | null)?.parentElement?.getBoundingClientRect();
|
|
379
515
|
const length = box === undefined ? 0 : vertical() ? box.height : box.width;
|
|
380
516
|
const mirror = !vertical() && group.dir() === "rtl" ? -1 : 1;
|
|
517
|
+
const handleIndex = handleIndexNow();
|
|
381
518
|
const startSize = group.sizes()[handleIndex] ?? 0;
|
|
382
519
|
// Only the pointer that pressed the handle moves it; another finger is not part of this drag.
|
|
383
520
|
const pointer = event.pointerId;
|
|
@@ -402,12 +539,15 @@ function ResizableHandle(props: ResizableHandleProps): JSX.Element {
|
|
|
402
539
|
|
|
403
540
|
return (
|
|
404
541
|
<div
|
|
542
|
+
ref={(node: HTMLElement) => {
|
|
543
|
+
handleNode = node;
|
|
544
|
+
}}
|
|
405
545
|
role="separator"
|
|
406
546
|
tabindex={0}
|
|
407
547
|
aria-orientation={vertical() ? "horizontal" : "vertical"}
|
|
408
|
-
aria-valuenow={Math.round(group.sizes()[
|
|
409
|
-
aria-valuemin={Math.round(group.min()[
|
|
410
|
-
aria-valuemax={Math.round(group.max()[
|
|
548
|
+
aria-valuenow={Math.round(group.sizes()[handleIndexNow()] ?? 0)}
|
|
549
|
+
aria-valuemin={Math.round(group.min()[handleIndexNow()] ?? 0)}
|
|
550
|
+
aria-valuemax={Math.round(group.max()[handleIndexNow()] ?? 100)}
|
|
411
551
|
aria-label={t("resizable.handle", "Resize panels")}
|
|
412
552
|
data-slot="resizable-handle"
|
|
413
553
|
data-orientation={vertical() ? "vertical" : "horizontal"}
|
|
@@ -421,6 +561,7 @@ function ResizableHandle(props: ResizableHandleProps): JSX.Element {
|
|
|
421
561
|
)}
|
|
422
562
|
onPointerDown={(event: PointerEvent) => startDrag(event)}
|
|
423
563
|
onKeyDown={(event: KeyboardEvent) => {
|
|
564
|
+
const handleIndex = handleIndexNow();
|
|
424
565
|
const forward = vertical()
|
|
425
566
|
? "ArrowDown"
|
|
426
567
|
: group.dir() === "rtl"
|
package/registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadow-js/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"frameworkDependencies": [
|
|
5
5
|
"@shadow-js/core",
|
|
6
6
|
"@shadow-js/primitives",
|
|
@@ -587,7 +587,9 @@
|
|
|
587
587
|
],
|
|
588
588
|
"primitives": [
|
|
589
589
|
"Direction",
|
|
590
|
+
"callHandler",
|
|
590
591
|
"createPartContext",
|
|
592
|
+
"mergeElementProps",
|
|
591
593
|
"useDirectionAround",
|
|
592
594
|
"useRefSignal"
|
|
593
595
|
],
|
|
@@ -1061,7 +1063,7 @@
|
|
|
1061
1063
|
"utils"
|
|
1062
1064
|
],
|
|
1063
1065
|
"dependencies": {
|
|
1064
|
-
"@shadow-js/table": "1.0.
|
|
1066
|
+
"@shadow-js/table": "1.0.3"
|
|
1065
1067
|
},
|
|
1066
1068
|
"exports": [
|
|
1067
1069
|
"DataTable",
|
|
@@ -1663,7 +1665,9 @@
|
|
|
1663
1665
|
"InputGroupText",
|
|
1664
1666
|
"InputGroupTextarea"
|
|
1665
1667
|
],
|
|
1666
|
-
"primitives": [
|
|
1668
|
+
"primitives": [
|
|
1669
|
+
"callHandler"
|
|
1670
|
+
],
|
|
1667
1671
|
"tailwind": {
|
|
1668
1672
|
"tokens": [
|
|
1669
1673
|
"accent",
|