@inshorts/glint-ui 0.5.4 → 0.6.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/README.md +3 -1
- package/dist/alert.cjs +96 -0
- package/dist/alert.cjs.map +1 -0
- package/dist/alert.d.cts +29 -0
- package/dist/alert.d.ts +29 -0
- package/dist/alert.js +10 -0
- package/dist/alert.js.map +1 -0
- package/dist/autocomplete.cjs +578 -0
- package/dist/autocomplete.cjs.map +1 -0
- package/dist/autocomplete.d.cts +56 -0
- package/dist/autocomplete.d.ts +56 -0
- package/dist/autocomplete.js +11 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/badge.cjs +89 -0
- package/dist/badge.cjs.map +1 -0
- package/dist/badge.d.cts +25 -0
- package/dist/badge.d.ts +25 -0
- package/dist/badge.js +10 -0
- package/dist/badge.js.map +1 -0
- package/dist/breadcrumb.cjs +88 -0
- package/dist/breadcrumb.cjs.map +1 -0
- package/dist/breadcrumb.d.cts +14 -0
- package/dist/breadcrumb.d.ts +14 -0
- package/dist/breadcrumb.js +8 -0
- package/dist/breadcrumb.js.map +1 -0
- package/dist/button.cjs +143 -0
- package/dist/button.cjs.map +1 -0
- package/dist/button.d.cts +34 -0
- package/dist/button.d.ts +34 -0
- package/dist/button.js +9 -0
- package/dist/button.js.map +1 -0
- package/dist/card.cjs +111 -0
- package/dist/card.cjs.map +1 -0
- package/dist/card.d.cts +15 -0
- package/dist/card.d.ts +15 -0
- package/dist/card.js +18 -0
- package/dist/card.js.map +1 -0
- package/dist/checkbox.cjs +66 -0
- package/dist/checkbox.cjs.map +1 -0
- package/{src/checkbox.tsx → dist/checkbox.d.cts} +5 -22
- package/dist/checkbox.d.ts +25 -0
- package/dist/checkbox.js +10 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-43ZUFY63.js +85 -0
- package/dist/chunk-43ZUFY63.js.map +1 -0
- package/dist/chunk-67ACNS42.js +29 -0
- package/dist/chunk-67ACNS42.js.map +1 -0
- package/dist/chunk-6URKHMCF.js +76 -0
- package/dist/chunk-6URKHMCF.js.map +1 -0
- package/dist/chunk-ASU6CUTZ.js +85 -0
- package/dist/chunk-ASU6CUTZ.js.map +1 -0
- package/dist/chunk-EWSBFU54.js +53 -0
- package/dist/chunk-EWSBFU54.js.map +1 -0
- package/dist/chunk-G4ZQUSAJ.js +38 -0
- package/dist/chunk-G4ZQUSAJ.js.map +1 -0
- package/dist/chunk-GMCUXEFI.js +67 -0
- package/dist/chunk-GMCUXEFI.js.map +1 -0
- package/dist/chunk-HBBOR43A.js +85 -0
- package/dist/chunk-HBBOR43A.js.map +1 -0
- package/dist/chunk-HCA5ODCD.js +11 -0
- package/dist/chunk-HCA5ODCD.js.map +1 -0
- package/dist/chunk-HYRJYUPI.js +26 -0
- package/dist/chunk-HYRJYUPI.js.map +1 -0
- package/dist/chunk-IRCTGJQB.js +26 -0
- package/dist/chunk-IRCTGJQB.js.map +1 -0
- package/dist/chunk-JDE5XYPT.js +30 -0
- package/dist/chunk-JDE5XYPT.js.map +1 -0
- package/dist/chunk-JSJEEKR3.js +59 -0
- package/dist/chunk-JSJEEKR3.js.map +1 -0
- package/dist/chunk-KR3BCFP7.js +85 -0
- package/dist/chunk-KR3BCFP7.js.map +1 -0
- package/dist/chunk-MQT5RAQV.js +202 -0
- package/dist/chunk-MQT5RAQV.js.map +1 -0
- package/dist/chunk-MUPW2TFJ.js +102 -0
- package/dist/chunk-MUPW2TFJ.js.map +1 -0
- package/dist/chunk-PM2VE7DJ.js +100 -0
- package/dist/chunk-PM2VE7DJ.js.map +1 -0
- package/dist/chunk-PPYR3O3L.js +44 -0
- package/dist/chunk-PPYR3O3L.js.map +1 -0
- package/dist/chunk-PUTC52SI.js +98 -0
- package/dist/chunk-PUTC52SI.js.map +1 -0
- package/dist/chunk-PVK4WQJZ.js +513 -0
- package/dist/chunk-PVK4WQJZ.js.map +1 -0
- package/dist/chunk-PWAKTEXF.js +18 -0
- package/dist/chunk-PWAKTEXF.js.map +1 -0
- package/dist/chunk-QTFWORA7.js +26 -0
- package/dist/chunk-QTFWORA7.js.map +1 -0
- package/dist/chunk-RUPL2H6V.js +49 -0
- package/dist/chunk-RUPL2H6V.js.map +1 -0
- package/dist/chunk-S7PVZAPL.js +28 -0
- package/dist/chunk-S7PVZAPL.js.map +1 -0
- package/dist/chunk-SQDV2AKJ.js +49 -0
- package/dist/chunk-SQDV2AKJ.js.map +1 -0
- package/dist/chunk-T2KXVH3O.js +297 -0
- package/dist/chunk-T2KXVH3O.js.map +1 -0
- package/dist/chunk-T4WCCHJQ.js +29 -0
- package/dist/chunk-T4WCCHJQ.js.map +1 -0
- package/dist/chunk-UC3ZIQPE.js +93 -0
- package/dist/chunk-UC3ZIQPE.js.map +1 -0
- package/dist/chunk-UE7JY7VR.js +28 -0
- package/dist/chunk-UE7JY7VR.js.map +1 -0
- package/dist/chunk-WVNRFO5F.js +70 -0
- package/dist/chunk-WVNRFO5F.js.map +1 -0
- package/dist/chunk-Y5WCT6XN.js +93 -0
- package/dist/chunk-Y5WCT6XN.js.map +1 -0
- package/dist/chunk-YDICVETX.js +151 -0
- package/dist/chunk-YDICVETX.js.map +1 -0
- package/dist/chunk-YSU4VWRD.js +90 -0
- package/dist/chunk-YSU4VWRD.js.map +1 -0
- package/dist/chunk-ZFSQVMLJ.js +126 -0
- package/dist/chunk-ZFSQVMLJ.js.map +1 -0
- package/dist/chunk-ZHATD73Y.js +46 -0
- package/dist/chunk-ZHATD73Y.js.map +1 -0
- package/dist/chunk-ZLAA4DGG.js +111 -0
- package/dist/chunk-ZLAA4DGG.js.map +1 -0
- package/dist/chunk-ZP252OJZ.js +56 -0
- package/dist/chunk-ZP252OJZ.js.map +1 -0
- package/dist/clipboard.cjs +125 -0
- package/dist/clipboard.cjs.map +1 -0
- package/dist/clipboard.d.cts +52 -0
- package/dist/clipboard.d.ts +52 -0
- package/dist/clipboard.js +23 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/collapsible.cjs +94 -0
- package/dist/collapsible.cjs.map +1 -0
- package/dist/collapsible.d.cts +21 -0
- package/dist/collapsible.d.ts +21 -0
- package/dist/collapsible.js +9 -0
- package/dist/collapsible.js.map +1 -0
- package/dist/confirm-dialog.cjs +369 -0
- package/dist/confirm-dialog.cjs.map +1 -0
- package/dist/confirm-dialog.d.cts +16 -0
- package/dist/confirm-dialog.d.ts +16 -0
- package/dist/confirm-dialog.js +13 -0
- package/dist/confirm-dialog.js.map +1 -0
- package/dist/copy-button.cjs +102 -0
- package/dist/copy-button.cjs.map +1 -0
- package/dist/copy-button.d.cts +18 -0
- package/dist/copy-button.d.ts +18 -0
- package/dist/copy-button.js +10 -0
- package/dist/copy-button.js.map +1 -0
- package/dist/data-table.cjs +140 -0
- package/dist/data-table.cjs.map +1 -0
- package/dist/data-table.d.cts +81 -0
- package/dist/data-table.d.ts +81 -0
- package/dist/data-table.js +8 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-range-picker/dateRange.cjs +138 -0
- package/dist/date-range-picker/dateRange.cjs.map +1 -0
- package/dist/date-range-picker/dateRange.d.cts +57 -0
- package/dist/date-range-picker/dateRange.d.ts +57 -0
- package/dist/date-range-picker/dateRange.js +31 -0
- package/dist/date-range-picker/dateRange.js.map +1 -0
- package/dist/date-range-picker/index.cjs +375 -0
- package/dist/date-range-picker/index.cjs.map +1 -0
- package/dist/date-range-picker/index.d.cts +18 -0
- package/dist/date-range-picker/index.d.ts +18 -0
- package/dist/date-range-picker/index.js +10 -0
- package/dist/date-range-picker/index.js.map +1 -0
- package/dist/dialogLayer.cjs +121 -0
- package/dist/dialogLayer.cjs.map +1 -0
- package/dist/dialogLayer.d.cts +11 -0
- package/dist/dialogLayer.d.ts +11 -0
- package/dist/dialogLayer.js +8 -0
- package/dist/dialogLayer.js.map +1 -0
- package/dist/feature-access-gate.cjs +160 -0
- package/dist/feature-access-gate.cjs.map +1 -0
- package/dist/feature-access-gate.d.cts +14 -0
- package/dist/feature-access-gate.d.ts +14 -0
- package/dist/feature-access-gate.js +11 -0
- package/dist/feature-access-gate.js.map +1 -0
- package/dist/field.cjs +58 -0
- package/dist/field.cjs.map +1 -0
- package/dist/field.d.cts +23 -0
- package/dist/field.d.ts +23 -0
- package/dist/field.js +8 -0
- package/dist/field.js.map +1 -0
- package/dist/image-uploader.cjs +293 -0
- package/dist/image-uploader.cjs.map +1 -0
- package/dist/image-uploader.d.cts +34 -0
- package/dist/image-uploader.d.ts +34 -0
- package/dist/image-uploader.js +12 -0
- package/dist/image-uploader.js.map +1 -0
- package/dist/index.cjs +3012 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +232 -0
- package/dist/index.js.map +1 -0
- package/dist/input.cjs +58 -0
- package/dist/input.cjs.map +1 -0
- package/dist/input.d.cts +7 -0
- package/dist/input.d.ts +7 -0
- package/dist/input.js +10 -0
- package/dist/input.js.map +1 -0
- package/dist/label.cjs +67 -0
- package/dist/label.cjs.map +1 -0
- package/dist/label.d.cts +7 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.js +9 -0
- package/dist/label.js.map +1 -0
- package/dist/modal.cjs +212 -0
- package/dist/modal.cjs.map +1 -0
- package/dist/modal.d.cts +39 -0
- package/dist/modal.d.ts +39 -0
- package/dist/modal.js +12 -0
- package/dist/modal.js.map +1 -0
- package/dist/page-header.cjs +78 -0
- package/dist/page-header.cjs.map +1 -0
- package/dist/page-header.d.cts +35 -0
- package/dist/page-header.d.ts +35 -0
- package/dist/page-header.js +8 -0
- package/dist/page-header.js.map +1 -0
- package/dist/pagination.cjs +237 -0
- package/dist/pagination.cjs.map +1 -0
- package/dist/pagination.d.cts +15 -0
- package/dist/pagination.d.ts +15 -0
- package/dist/pagination.js +11 -0
- package/dist/pagination.js.map +1 -0
- package/dist/panel-brand-mark.cjs +59 -0
- package/dist/panel-brand-mark.cjs.map +1 -0
- package/dist/panel-brand-mark.d.cts +13 -0
- package/dist/panel-brand-mark.d.ts +13 -0
- package/dist/panel-brand-mark.js +8 -0
- package/dist/panel-brand-mark.js.map +1 -0
- package/dist/panel-sign-in.cjs +256 -0
- package/dist/panel-sign-in.cjs.map +1 -0
- package/dist/panel-sign-in.d.cts +31 -0
- package/dist/panel-sign-in.d.ts +31 -0
- package/dist/panel-sign-in.js +14 -0
- package/dist/panel-sign-in.js.map +1 -0
- package/dist/segmented-control.cjs +126 -0
- package/dist/segmented-control.cjs.map +1 -0
- package/dist/segmented-control.d.cts +60 -0
- package/dist/segmented-control.d.ts +60 -0
- package/dist/segmented-control.js +9 -0
- package/dist/segmented-control.js.map +1 -0
- package/dist/select.cjs +648 -0
- package/dist/select.cjs.map +1 -0
- package/dist/select.d.cts +30 -0
- package/dist/select.d.ts +30 -0
- package/dist/select.js +12 -0
- package/dist/select.js.map +1 -0
- package/dist/separator.cjs +70 -0
- package/dist/separator.cjs.map +1 -0
- package/dist/separator.d.cts +13 -0
- package/dist/separator.d.ts +13 -0
- package/dist/separator.js +9 -0
- package/dist/separator.js.map +1 -0
- package/dist/side-drawer.cjs +321 -0
- package/dist/side-drawer.cjs.map +1 -0
- package/dist/side-drawer.d.cts +21 -0
- package/dist/side-drawer.d.ts +21 -0
- package/dist/side-drawer.js +13 -0
- package/dist/side-drawer.js.map +1 -0
- package/dist/spinner.cjs +57 -0
- package/dist/spinner.cjs.map +1 -0
- package/{src/spinner.tsx → dist/spinner.d.cts} +11 -22
- package/dist/spinner.d.ts +27 -0
- package/dist/spinner.js +8 -0
- package/dist/spinner.js.map +1 -0
- package/dist/spreadsheet-grid.cjs +238 -0
- package/dist/spreadsheet-grid.cjs.map +1 -0
- package/dist/spreadsheet-grid.d.cts +48 -0
- package/dist/spreadsheet-grid.d.ts +48 -0
- package/dist/spreadsheet-grid.js +8 -0
- package/dist/spreadsheet-grid.js.map +1 -0
- package/dist/state-panel.cjs +86 -0
- package/dist/state-panel.cjs.map +1 -0
- package/dist/state-panel.d.cts +23 -0
- package/dist/state-panel.d.ts +23 -0
- package/dist/state-panel.js +9 -0
- package/dist/state-panel.js.map +1 -0
- package/dist/switch.cjs +87 -0
- package/dist/switch.cjs.map +1 -0
- package/dist/switch.d.cts +13 -0
- package/dist/switch.d.ts +13 -0
- package/dist/switch.js +9 -0
- package/dist/switch.js.map +1 -0
- package/dist/table.cjs +139 -0
- package/dist/table.cjs.map +1 -0
- package/dist/table.d.cts +16 -0
- package/dist/table.d.ts +16 -0
- package/dist/table.js +22 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.cjs +168 -0
- package/dist/tabs.cjs.map +1 -0
- package/dist/tabs.d.cts +59 -0
- package/dist/tabs.d.ts +59 -0
- package/dist/tabs.js +11 -0
- package/dist/tabs.js.map +1 -0
- package/dist/textarea.cjs +66 -0
- package/dist/textarea.cjs.map +1 -0
- package/dist/textarea.d.cts +18 -0
- package/dist/textarea.d.ts +18 -0
- package/dist/textarea.js +10 -0
- package/dist/textarea.js.map +1 -0
- package/dist/utils.cjs +35 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.cts +5 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/dist/utils.js.map +1 -0
- package/package.json +390 -13
- package/src/alert.tsx +0 -74
- package/src/autocomplete.tsx +0 -666
- package/src/badge.tsx +0 -60
- package/src/breadcrumb.tsx +0 -71
- package/src/button.tsx +0 -113
- package/src/card.tsx +0 -79
- package/src/clipboard.ts +0 -144
- package/src/collapsible.tsx +0 -63
- package/src/confirm-dialog.tsx +0 -84
- package/src/copy-button.tsx +0 -58
- package/src/data-table.tsx +0 -190
- package/src/date-range-picker/dateRange.ts +0 -142
- package/src/date-range-picker/index.tsx +0 -366
- package/src/dialogLayer.ts +0 -142
- package/src/feature-access-gate.tsx +0 -38
- package/src/field.tsx +0 -56
- package/src/image-uploader.tsx +0 -181
- package/src/index.ts +0 -37
- package/src/input.tsx +0 -16
- package/src/label.tsx +0 -23
- package/src/modal.tsx +0 -117
- package/src/page-header.tsx +0 -75
- package/src/pagination.tsx +0 -116
- package/src/panel-brand-mark.tsx +0 -32
- package/src/panel-sign-in.tsx +0 -134
- package/src/segmented-control.tsx +0 -155
- package/src/select.tsx +0 -105
- package/src/separator.tsx +0 -32
- package/src/side-drawer.tsx +0 -87
- package/src/spreadsheet-grid.tsx +0 -264
- package/src/state-panel.tsx +0 -46
- package/src/switch.tsx +0 -47
- package/src/table.tsx +0 -110
- package/src/tabs.tsx +0 -201
- package/src/textarea.tsx +0 -32
- package/src/utils.ts +0 -6
package/src/dialogLayer.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The keyboard and focus behaviour every dialog needs, in one place.
|
|
7
|
-
*
|
|
8
|
-
* `Modal` and `SideDrawer` each had a copy of the scroll lock and an Escape
|
|
9
|
-
* handler, and neither did anything about focus: opening a dialog left focus
|
|
10
|
-
* on the button behind it, Tab walked the page underneath — through the
|
|
11
|
-
* full-screen backdrop button, among other things — and closing left focus
|
|
12
|
-
* wherever it had wandered. That is the whole keyboard story for thirty
|
|
13
|
-
* dialogs, so it is fixed once here rather than thirty times.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Open dialogs, innermost last.
|
|
18
|
-
*
|
|
19
|
-
* Nesting is real here — "Create audience group" opens on top of "Send
|
|
20
|
-
* notification" — and without an ordering both would answer the same Escape
|
|
21
|
-
* and fight over the same Tab. Only the top layer acts.
|
|
22
|
-
*/
|
|
23
|
-
const layers: symbol[] = [];
|
|
24
|
-
|
|
25
|
-
const FOCUSABLE = [
|
|
26
|
-
"a[href]",
|
|
27
|
-
"button:not([disabled])",
|
|
28
|
-
"input:not([disabled])",
|
|
29
|
-
"select:not([disabled])",
|
|
30
|
-
"textarea:not([disabled])",
|
|
31
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
32
|
-
].join(",");
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Deliberately not `offsetParent` or `getClientRects`: the first is null for
|
|
36
|
-
* anything `position: fixed`, which a dialog panel usually is, and the second
|
|
37
|
-
* is empty for *everything* under a test renderer that does no layout — which
|
|
38
|
-
* would collapse the trap to "no focusable elements" in every test.
|
|
39
|
-
*/
|
|
40
|
-
function isVisible(element: HTMLElement): boolean {
|
|
41
|
-
if (element.closest("[hidden]")) return false;
|
|
42
|
-
if (element.getAttribute("aria-hidden") === "true") return false;
|
|
43
|
-
return typeof element.checkVisibility === "function"
|
|
44
|
-
? element.checkVisibility()
|
|
45
|
-
: true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function focusableWithin(container: HTMLElement): HTMLElement[] {
|
|
49
|
-
return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
|
|
50
|
-
isVisible,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function useDialogLayer({
|
|
55
|
-
active,
|
|
56
|
-
containerRef,
|
|
57
|
-
onEscape,
|
|
58
|
-
}: {
|
|
59
|
-
active: boolean;
|
|
60
|
-
/** The panel. Focus is moved into it and kept inside it. */
|
|
61
|
-
containerRef: React.RefObject<HTMLElement | null>;
|
|
62
|
-
/** Called on Escape, only while this is the top layer. Omit to ignore it. */
|
|
63
|
-
onEscape?: () => void;
|
|
64
|
-
}): void {
|
|
65
|
-
// Read through a ref so a caller passing a fresh closure every render does
|
|
66
|
-
// not tear the listener down and rebuild it on every keystroke.
|
|
67
|
-
const escapeRef = React.useRef(onEscape);
|
|
68
|
-
React.useEffect(() => {
|
|
69
|
-
escapeRef.current = onEscape;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
React.useEffect(() => {
|
|
73
|
-
if (!active) return;
|
|
74
|
-
|
|
75
|
-
const layer = Symbol("dialog-layer");
|
|
76
|
-
layers.push(layer);
|
|
77
|
-
const isTopLayer = () => layers[layers.length - 1] === layer;
|
|
78
|
-
|
|
79
|
-
const previouslyFocused =
|
|
80
|
-
document.activeElement instanceof HTMLElement
|
|
81
|
-
? document.activeElement
|
|
82
|
-
: null;
|
|
83
|
-
|
|
84
|
-
const container = containerRef.current;
|
|
85
|
-
// Something inside may have claimed focus already — an `autoFocus` input —
|
|
86
|
-
// and that choice is better than any this could make.
|
|
87
|
-
if (container && !container.contains(document.activeElement)) {
|
|
88
|
-
const [first] = focusableWithin(container);
|
|
89
|
-
// The panel itself, not its first control: the first control is a Delete
|
|
90
|
-
// button often enough that focusing it would put a destructive action
|
|
91
|
-
// one Enter away from an accidental keypress.
|
|
92
|
-
(container.hasAttribute("tabindex") ? container : first)?.focus();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
96
|
-
if (!isTopLayer()) return;
|
|
97
|
-
|
|
98
|
-
if (event.key === "Escape") {
|
|
99
|
-
escapeRef.current?.();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (event.key !== "Tab") return;
|
|
104
|
-
const panel = containerRef.current;
|
|
105
|
-
if (!panel) return;
|
|
106
|
-
|
|
107
|
-
const items = focusableWithin(panel);
|
|
108
|
-
if (items.length === 0) {
|
|
109
|
-
event.preventDefault();
|
|
110
|
-
panel.focus();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const first = items[0]!;
|
|
115
|
-
const last = items[items.length - 1]!;
|
|
116
|
-
const active = document.activeElement;
|
|
117
|
-
const outside = !panel.contains(active);
|
|
118
|
-
|
|
119
|
-
if (event.shiftKey && (active === first || outside)) {
|
|
120
|
-
event.preventDefault();
|
|
121
|
-
last.focus();
|
|
122
|
-
} else if (!event.shiftKey && (active === last || outside)) {
|
|
123
|
-
event.preventDefault();
|
|
124
|
-
first.focus();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
129
|
-
const previousOverflow = document.body.style.overflow;
|
|
130
|
-
document.body.style.overflow = "hidden";
|
|
131
|
-
|
|
132
|
-
return () => {
|
|
133
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
134
|
-
document.body.style.overflow = previousOverflow;
|
|
135
|
-
const index = layers.indexOf(layer);
|
|
136
|
-
if (index !== -1) layers.splice(index, 1);
|
|
137
|
-
// Back where it came from, so closing a dialog opened by keyboard does
|
|
138
|
-
// not drop the caret at the top of the document.
|
|
139
|
-
if (previouslyFocused?.isConnected) previouslyFocused.focus();
|
|
140
|
-
};
|
|
141
|
-
}, [active, containerRef]);
|
|
142
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { Alert } from "./alert";
|
|
4
|
-
import { StatePanel } from "./state-panel";
|
|
5
|
-
|
|
6
|
-
type FeatureAccessGateProps = {
|
|
7
|
-
isLoading?: boolean;
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
deniedMessage: React.ReactNode;
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Shared loading / denied shell for feature Managers (showcase, headers, …).
|
|
15
|
-
*/
|
|
16
|
-
export function FeatureAccessGate({
|
|
17
|
-
isLoading,
|
|
18
|
-
allowed,
|
|
19
|
-
deniedMessage,
|
|
20
|
-
children,
|
|
21
|
-
}: FeatureAccessGateProps) {
|
|
22
|
-
if (isLoading) {
|
|
23
|
-
return (
|
|
24
|
-
<StatePanel loading className="p-6">
|
|
25
|
-
Checking permissions…
|
|
26
|
-
</StatePanel>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!allowed) {
|
|
31
|
-
// `Alert`, not a hand-rolled banner: this is the denial surface for
|
|
32
|
-
// seventeen panels, and it was the one copy the Alert migration missed —
|
|
33
|
-
// so being refused a panel announced nothing to a screen reader.
|
|
34
|
-
return <Alert>{deniedMessage}</Alert>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return <>{children}</>;
|
|
38
|
-
}
|
package/src/field.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "./utils";
|
|
4
|
-
|
|
5
|
-
interface FieldProps {
|
|
6
|
-
label: React.ReactNode;
|
|
7
|
-
required?: boolean;
|
|
8
|
-
/**
|
|
9
|
-
* Associates the label with a control via `htmlFor` / `id`. Prefer this for
|
|
10
|
-
* native inputs rendered as siblings (not wrapped). For composite controls
|
|
11
|
-
* (Autocomplete), omit and use `as="label"` wrapping or an `aria-label`.
|
|
12
|
-
*/
|
|
13
|
-
htmlFor?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Render as a `<label>` (wraps a native control so clicking the text focuses
|
|
16
|
-
* it) or a `<div>` (for composite controls like Autocomplete that manage
|
|
17
|
-
* their own focus / render in a portal). Defaults to `div`.
|
|
18
|
-
*/
|
|
19
|
-
as?: "label" | "div";
|
|
20
|
-
className?: string;
|
|
21
|
-
children: React.ReactNode;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function Field({
|
|
25
|
-
label,
|
|
26
|
-
required,
|
|
27
|
-
htmlFor,
|
|
28
|
-
as = "div",
|
|
29
|
-
className,
|
|
30
|
-
children,
|
|
31
|
-
}: FieldProps) {
|
|
32
|
-
const Wrapper = as;
|
|
33
|
-
const labelNode = (
|
|
34
|
-
<>
|
|
35
|
-
{label}
|
|
36
|
-
{required ? (
|
|
37
|
-
<span className="text-destructive ml-0.5" aria-hidden="true">
|
|
38
|
-
*
|
|
39
|
-
</span>
|
|
40
|
-
) : null}
|
|
41
|
-
</>
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<Wrapper className={cn("space-y-1 text-sm", className)}>
|
|
46
|
-
{htmlFor && as === "div" ? (
|
|
47
|
-
<label htmlFor={htmlFor} className="text-muted-foreground">
|
|
48
|
-
{labelNode}
|
|
49
|
-
</label>
|
|
50
|
-
) : (
|
|
51
|
-
<span className="text-muted-foreground">{labelNode}</span>
|
|
52
|
-
)}
|
|
53
|
-
{children}
|
|
54
|
-
</Wrapper>
|
|
55
|
-
);
|
|
56
|
-
}
|
package/src/image-uploader.tsx
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { ImagePlus, Loader2, X } from "lucide-react";
|
|
4
|
-
import * as React from "react";
|
|
5
|
-
|
|
6
|
-
import { Button } from "./button";
|
|
7
|
-
import { Input } from "./input";
|
|
8
|
-
import { cn } from "./utils";
|
|
9
|
-
|
|
10
|
-
export type ImageUploaderProps = {
|
|
11
|
-
value?: string;
|
|
12
|
-
onChange: (url: string) => void;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
label?: string;
|
|
15
|
-
className?: string;
|
|
16
|
-
/** Allow pasting a remote URL in addition to file upload. Default true. */
|
|
17
|
-
allowUrlPaste?: boolean;
|
|
18
|
-
accept?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Upload a picked file and return its hosted URL.
|
|
21
|
-
* Injected by the panel — Upload stays enabled either way; without a
|
|
22
|
-
* handler, picking a file surfaces a configuration error.
|
|
23
|
-
*/
|
|
24
|
-
uploadFile?: (file: File) => Promise<string>;
|
|
25
|
-
/**
|
|
26
|
-
* Optional transform for pasted values (e.g. host a relative path).
|
|
27
|
-
* When omitted, any pasted non-empty string is applied as-is. Absolute
|
|
28
|
-
* `http(s)` URLs never call this.
|
|
29
|
-
*/
|
|
30
|
-
uploadFromUrl?: (imageUrl: string) => Promise<string>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Reusable image uploader.
|
|
35
|
-
*
|
|
36
|
-
* Upload and URL paste are both enabled by default. Hosting is injected:
|
|
37
|
-
* each panel passes `uploadFile` / `uploadFromUrl` so the kit stays free of
|
|
38
|
-
* panel-specific API routes.
|
|
39
|
-
*/
|
|
40
|
-
export function ImageUploader({
|
|
41
|
-
value = "",
|
|
42
|
-
onChange,
|
|
43
|
-
disabled,
|
|
44
|
-
label,
|
|
45
|
-
className,
|
|
46
|
-
allowUrlPaste = true,
|
|
47
|
-
accept = "image/*,.gif",
|
|
48
|
-
uploadFile,
|
|
49
|
-
uploadFromUrl,
|
|
50
|
-
}: ImageUploaderProps) {
|
|
51
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
52
|
-
const [uploading, setUploading] = React.useState(false);
|
|
53
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
54
|
-
const busy = Boolean(disabled || uploading);
|
|
55
|
-
|
|
56
|
-
const handleFile = async (file: File) => {
|
|
57
|
-
if (!uploadFile) {
|
|
58
|
-
setError("File upload is not configured for this panel.");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
setUploading(true);
|
|
62
|
-
setError(null);
|
|
63
|
-
try {
|
|
64
|
-
onChange(await uploadFile(file));
|
|
65
|
-
} catch (err) {
|
|
66
|
-
setError(err instanceof Error ? err.message : "Upload failed");
|
|
67
|
-
} finally {
|
|
68
|
-
setUploading(false);
|
|
69
|
-
if (inputRef.current) inputRef.current.value = "";
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const handleUrl = async (imageUrl: string) => {
|
|
74
|
-
const trimmed = imageUrl.trim();
|
|
75
|
-
if (!trimmed) {
|
|
76
|
-
onChange("");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (trimmed === value) return;
|
|
80
|
-
|
|
81
|
-
// Absolute URLs — and any paste when no transform is configured — apply
|
|
82
|
-
// as-is. Paste is never gated behind a disabled input.
|
|
83
|
-
if (!uploadFromUrl || /^https?:\/\//i.test(trimmed)) {
|
|
84
|
-
setError(null);
|
|
85
|
-
onChange(trimmed);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
setUploading(true);
|
|
90
|
-
setError(null);
|
|
91
|
-
try {
|
|
92
|
-
onChange(await uploadFromUrl(trimmed));
|
|
93
|
-
} catch (err) {
|
|
94
|
-
setError(err instanceof Error ? err.message : "Upload failed");
|
|
95
|
-
} finally {
|
|
96
|
-
setUploading(false);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div className={cn("space-y-2", className)}>
|
|
102
|
-
{label ? (
|
|
103
|
-
<div className="text-muted-foreground text-xs font-medium">{label}</div>
|
|
104
|
-
) : null}
|
|
105
|
-
|
|
106
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
107
|
-
<input
|
|
108
|
-
ref={inputRef}
|
|
109
|
-
type="file"
|
|
110
|
-
accept={accept}
|
|
111
|
-
className="hidden"
|
|
112
|
-
disabled={busy}
|
|
113
|
-
onChange={(e) => {
|
|
114
|
-
const file = e.target.files?.[0];
|
|
115
|
-
if (file) void handleFile(file);
|
|
116
|
-
}}
|
|
117
|
-
/>
|
|
118
|
-
<Button
|
|
119
|
-
type="button"
|
|
120
|
-
variant="outline"
|
|
121
|
-
size="sm"
|
|
122
|
-
disabled={busy}
|
|
123
|
-
onClick={() => inputRef.current?.click()}
|
|
124
|
-
>
|
|
125
|
-
{uploading ? (
|
|
126
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
127
|
-
) : (
|
|
128
|
-
<ImagePlus className="h-4 w-4" />
|
|
129
|
-
)}
|
|
130
|
-
{uploading ? "Uploading…" : "Upload"}
|
|
131
|
-
</Button>
|
|
132
|
-
{value ? (
|
|
133
|
-
<Button
|
|
134
|
-
type="button"
|
|
135
|
-
variant="ghost"
|
|
136
|
-
size="sm"
|
|
137
|
-
disabled={busy}
|
|
138
|
-
onClick={() => onChange("")}
|
|
139
|
-
aria-label="Clear image"
|
|
140
|
-
>
|
|
141
|
-
<X className="h-4 w-4" />
|
|
142
|
-
Clear
|
|
143
|
-
</Button>
|
|
144
|
-
) : null}
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
{allowUrlPaste ? (
|
|
148
|
-
<Input
|
|
149
|
-
key={value || "empty"}
|
|
150
|
-
defaultValue={value}
|
|
151
|
-
disabled={busy}
|
|
152
|
-
placeholder="Or paste image URL…"
|
|
153
|
-
onBlur={(e) => {
|
|
154
|
-
void handleUrl(e.target.value);
|
|
155
|
-
}}
|
|
156
|
-
onKeyDown={(e) => {
|
|
157
|
-
if (e.key === "Enter") {
|
|
158
|
-
e.preventDefault();
|
|
159
|
-
void handleUrl((e.target as HTMLInputElement).value);
|
|
160
|
-
}
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
) : null}
|
|
164
|
-
|
|
165
|
-
{value ? (
|
|
166
|
-
// eslint-disable-next-line @next/next/no-img-element -- CDN URLs vary
|
|
167
|
-
<img
|
|
168
|
-
src={value}
|
|
169
|
-
alt=""
|
|
170
|
-
className="border-border max-h-24 rounded border bg-white object-contain p-1"
|
|
171
|
-
/>
|
|
172
|
-
) : null}
|
|
173
|
-
|
|
174
|
-
{error ? (
|
|
175
|
-
<p className="text-destructive text-xs" role="alert">
|
|
176
|
-
{error}
|
|
177
|
-
</p>
|
|
178
|
-
) : null}
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export * from "./alert";
|
|
2
|
-
export * from "./autocomplete";
|
|
3
|
-
export * from "./badge";
|
|
4
|
-
export * from "./breadcrumb";
|
|
5
|
-
export * from "./button";
|
|
6
|
-
export * from "./card";
|
|
7
|
-
export * from "./checkbox";
|
|
8
|
-
export * from "./clipboard";
|
|
9
|
-
export * from "./collapsible";
|
|
10
|
-
export * from "./confirm-dialog";
|
|
11
|
-
export * from "./copy-button";
|
|
12
|
-
export * from "./data-table";
|
|
13
|
-
export * from "./date-range-picker";
|
|
14
|
-
export * from "./date-range-picker/dateRange";
|
|
15
|
-
export * from "./dialogLayer";
|
|
16
|
-
export * from "./feature-access-gate";
|
|
17
|
-
export * from "./field";
|
|
18
|
-
export * from "./image-uploader";
|
|
19
|
-
export * from "./input";
|
|
20
|
-
export * from "./label";
|
|
21
|
-
export * from "./modal";
|
|
22
|
-
export * from "./page-header";
|
|
23
|
-
export * from "./panel-brand-mark";
|
|
24
|
-
export * from "./panel-sign-in";
|
|
25
|
-
export * from "./pagination";
|
|
26
|
-
export * from "./segmented-control";
|
|
27
|
-
export * from "./select";
|
|
28
|
-
export * from "./separator";
|
|
29
|
-
export * from "./side-drawer";
|
|
30
|
-
export * from "./spinner";
|
|
31
|
-
export * from "./spreadsheet-grid";
|
|
32
|
-
export * from "./state-panel";
|
|
33
|
-
export * from "./switch";
|
|
34
|
-
export * from "./table";
|
|
35
|
-
export * from "./tabs";
|
|
36
|
-
export * from "./textarea";
|
|
37
|
-
export * from "./utils";
|
package/src/input.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "./utils";
|
|
4
|
-
|
|
5
|
-
export const inputClass =
|
|
6
|
-
"h-9 w-full rounded-md border border-border bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
|
|
7
|
-
|
|
8
|
-
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
9
|
-
|
|
10
|
-
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
11
|
-
({ className, ...props }, ref) => (
|
|
12
|
-
<input ref={ref} className={cn(inputClass, className)} {...props} />
|
|
13
|
-
),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
Input.displayName = "Input";
|
package/src/label.tsx
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
|
|
5
|
-
import { cn } from "./utils";
|
|
6
|
-
|
|
7
|
-
export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
|
|
8
|
-
|
|
9
|
-
/** Form label — pair with a native control via `htmlFor`. */
|
|
10
|
-
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
11
|
-
({ className, ...props }, ref) => (
|
|
12
|
-
<label
|
|
13
|
-
ref={ref}
|
|
14
|
-
className={cn(
|
|
15
|
-
"text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
16
|
-
className,
|
|
17
|
-
)}
|
|
18
|
-
{...props}
|
|
19
|
-
/>
|
|
20
|
-
),
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
Label.displayName = "Label";
|
package/src/modal.tsx
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { createPortal } from "react-dom";
|
|
5
|
-
|
|
6
|
-
import { useDialogLayer } from "./dialogLayer";
|
|
7
|
-
import { cn } from "./utils";
|
|
8
|
-
|
|
9
|
-
const emptySubscribe = () => () => {};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Returns false during SSR and the first client render, then true once mounted.
|
|
13
|
-
* `useSyncExternalStore` is the React-recommended way to read client-only state
|
|
14
|
-
* without a setState-in-effect (which triggers cascading renders).
|
|
15
|
-
*/
|
|
16
|
-
export function useHasMounted(): boolean {
|
|
17
|
-
return React.useSyncExternalStore(
|
|
18
|
-
emptySubscribe,
|
|
19
|
-
() => true,
|
|
20
|
-
() => false,
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ModalProps {
|
|
25
|
-
isOpen: boolean;
|
|
26
|
-
/** id of the element labelling the dialog; wired to aria-labelledby. */
|
|
27
|
-
labelledBy: string;
|
|
28
|
-
/**
|
|
29
|
-
* Optional id(s) of elements describing the dialog (APG alertdialog body).
|
|
30
|
-
* Space-separated when multiple.
|
|
31
|
-
*/
|
|
32
|
-
describedBy?: string;
|
|
33
|
-
/** Accessible label for the backdrop dismiss button. */
|
|
34
|
-
closeLabel: string;
|
|
35
|
-
onClose: () => void;
|
|
36
|
-
children: React.ReactNode;
|
|
37
|
-
/**
|
|
38
|
-
* When false, Escape and backdrop clicks do not close the modal (e.g. while a
|
|
39
|
-
* save is in flight). Defaults to true.
|
|
40
|
-
*/
|
|
41
|
-
dismissible?: boolean;
|
|
42
|
-
/** Use "alertdialog" for destructive confirmations. Defaults to "dialog". */
|
|
43
|
-
role?: "dialog" | "alertdialog";
|
|
44
|
-
/** Tailwind max-width class for the panel. Defaults to "max-w-md". */
|
|
45
|
-
panelClassName?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible
|
|
50
|
-
* overlay with a centered panel. Callers provide the panel body (heading, form,
|
|
51
|
-
* footer). Consolidates the boilerplate previously copied across every dialog.
|
|
52
|
-
*/
|
|
53
|
-
export function Modal({
|
|
54
|
-
isOpen,
|
|
55
|
-
labelledBy,
|
|
56
|
-
describedBy,
|
|
57
|
-
closeLabel,
|
|
58
|
-
onClose,
|
|
59
|
-
children,
|
|
60
|
-
dismissible = true,
|
|
61
|
-
role = "dialog",
|
|
62
|
-
panelClassName,
|
|
63
|
-
}: ModalProps) {
|
|
64
|
-
const mounted = useHasMounted();
|
|
65
|
-
const panelRef = React.useRef<HTMLDivElement>(null);
|
|
66
|
-
|
|
67
|
-
useDialogLayer({
|
|
68
|
-
// Gated on `mounted` too: before that the panel is not rendered, so there
|
|
69
|
-
// would be nothing to move focus into.
|
|
70
|
-
active: isOpen && mounted,
|
|
71
|
-
containerRef: panelRef,
|
|
72
|
-
onEscape: dismissible ? onClose : undefined,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
if (!mounted || !isOpen) return null;
|
|
76
|
-
|
|
77
|
-
return createPortal(
|
|
78
|
-
/*
|
|
79
|
-
`overflow-y-auto` on the overlay, `my-auto` on the panel — not
|
|
80
|
-
`items-center`. A flex item centred with `align-items` overflows equally
|
|
81
|
-
in both directions once it is taller than its container, and the half
|
|
82
|
-
above the top edge cannot be scrolled to: with the page behind it locked,
|
|
83
|
-
an over-tall dialog was simply unreachable at both ends. Auto margins
|
|
84
|
-
centre a short panel the same way and collapse to zero on a tall one,
|
|
85
|
-
which scrolls.
|
|
86
|
-
*/
|
|
87
|
-
<div className="fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4">
|
|
88
|
-
{/* `fixed`, not `absolute`: inside a scrolling overlay an absolute
|
|
89
|
-
backdrop scrolls away with the content and leaves the rest of a tall
|
|
90
|
-
dialog sitting on an undimmed page. */}
|
|
91
|
-
<button
|
|
92
|
-
type="button"
|
|
93
|
-
aria-label={closeLabel}
|
|
94
|
-
tabIndex={-1}
|
|
95
|
-
className="fixed inset-0 bg-slate-950/50"
|
|
96
|
-
onClick={() => {
|
|
97
|
-
if (dismissible) onClose();
|
|
98
|
-
}}
|
|
99
|
-
/>
|
|
100
|
-
<div
|
|
101
|
-
ref={panelRef}
|
|
102
|
-
role={role}
|
|
103
|
-
aria-modal="true"
|
|
104
|
-
aria-labelledby={labelledBy}
|
|
105
|
-
aria-describedby={describedBy}
|
|
106
|
-
tabIndex={-1}
|
|
107
|
-
className={cn(
|
|
108
|
-
"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none",
|
|
109
|
-
panelClassName ?? "max-w-md p-6",
|
|
110
|
-
)}
|
|
111
|
-
>
|
|
112
|
-
{children}
|
|
113
|
-
</div>
|
|
114
|
-
</div>,
|
|
115
|
-
document.body,
|
|
116
|
-
);
|
|
117
|
-
}
|
package/src/page-header.tsx
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "./utils";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The title block every panel opens with.
|
|
7
|
-
*
|
|
8
|
-
* Twenty Managers wrote the same `<h1>` and subtitle by hand, differing only in
|
|
9
|
-
* whether the row said `gap-3` or `gap-x-4 gap-y-2` and whether the inner block
|
|
10
|
-
* carried `min-w-0` — differences nobody chose, which is what made them worth
|
|
11
|
-
* removing.
|
|
12
|
-
*
|
|
13
|
-
* `description` takes a node rather than a string because several panels put a
|
|
14
|
-
* `<code>` in theirs, naming the Firebase field they write. That is the useful
|
|
15
|
-
* kind of subtitle and it should not have to opt out of the shared component to
|
|
16
|
-
* stay.
|
|
17
|
-
*
|
|
18
|
-
* `min-w-0` is not optional here. Without it a long title refuses to shrink and
|
|
19
|
-
* pushes the actions off the row instead of wrapping — which is the bug the
|
|
20
|
-
* three panels that set it had already hit, and the seven that did not had not
|
|
21
|
-
* yet.
|
|
22
|
-
*/
|
|
23
|
-
export interface PageHeaderProps {
|
|
24
|
-
title: React.ReactNode;
|
|
25
|
-
description?: React.ReactNode;
|
|
26
|
-
/**
|
|
27
|
-
* Sits above the title: a breadcrumb, or the back link the filter editors
|
|
28
|
-
* use. Four panels had one, each spacing it by hand.
|
|
29
|
-
*/
|
|
30
|
-
eyebrow?: React.ReactNode;
|
|
31
|
-
/** Buttons belonging to the page, laid out at the end of the title row. */
|
|
32
|
-
actions?: React.ReactNode;
|
|
33
|
-
className?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function PageHeader({
|
|
37
|
-
title,
|
|
38
|
-
description,
|
|
39
|
-
eyebrow,
|
|
40
|
-
actions,
|
|
41
|
-
className,
|
|
42
|
-
}: PageHeaderProps) {
|
|
43
|
-
const heading = (
|
|
44
|
-
<div className="min-w-0">
|
|
45
|
-
{eyebrow}
|
|
46
|
-
<h1
|
|
47
|
-
className={cn(
|
|
48
|
-
"text-2xl font-semibold tracking-tight",
|
|
49
|
-
eyebrow != null && "mt-1",
|
|
50
|
-
)}
|
|
51
|
-
>
|
|
52
|
-
{title}
|
|
53
|
-
</h1>
|
|
54
|
-
{description != null ? (
|
|
55
|
-
<p className="text-muted-foreground mt-1 text-sm">{description}</p>
|
|
56
|
-
) : null}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
if (!actions) {
|
|
61
|
-
return <div className={className}>{heading}</div>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div
|
|
66
|
-
className={cn(
|
|
67
|
-
"flex flex-wrap items-center justify-between gap-3",
|
|
68
|
-
className,
|
|
69
|
-
)}
|
|
70
|
-
>
|
|
71
|
-
{heading}
|
|
72
|
-
{actions}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|