@seekalfred/primitives 0.5.0 → 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 +664 -9
- package/dist/accordion/accordion.d.ts +5 -5
- package/dist/accordion/accordion.js +22 -11
- package/dist/accordion/accordion.js.map +1 -1
- package/dist/action-button/action-button.d.ts +50 -0
- package/dist/action-button/action-button.js +159 -0
- package/dist/action-button/action-button.js.map +1 -0
- package/dist/action-button/index.d.ts +2 -0
- package/dist/action-button/index.js +2 -0
- package/dist/alert/alert.d.ts +16 -2
- package/dist/alert/alert.js +118 -20
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +1 -1
- package/dist/alert-dialog/alert-dialog.js +4 -2
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -1
- package/dist/avatar/avatar.js +51 -18
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +10 -3
- package/dist/badge/badge.js +37 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/breadcrumb/breadcrumb.d.ts +3 -30
- package/dist/breadcrumb/breadcrumb.js +25 -8
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/button/button.d.ts +36 -23
- package/dist/button/button.js +178 -33
- package/dist/button/button.js.map +1 -1
- package/dist/button-group/button-group.d.ts +76 -0
- package/dist/button-group/button-group.js +403 -0
- package/dist/button-group/button-group.js.map +1 -0
- package/dist/button-group/index.d.ts +2 -0
- package/dist/button-group/index.js +2 -0
- package/dist/calendar/calendar.d.ts +3 -3
- package/dist/calendar/calendar.js +263 -34
- package/dist/calendar/calendar.js.map +1 -1
- package/dist/card/card.d.ts +14 -7
- package/dist/card/card.js +32 -14
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +2 -9
- package/dist/checkbox/checkbox.js +28 -29
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/confirm-morph/confirm-morph.d.ts +85 -0
- package/dist/confirm-morph/confirm-morph.js +652 -0
- package/dist/confirm-morph/confirm-morph.js.map +1 -0
- package/dist/confirm-morph/index.d.ts +2 -0
- package/dist/confirm-morph/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +32 -0
- package/dist/copy-button/copy-button.js +134 -0
- package/dist/copy-button/copy-button.js.map +1 -0
- package/dist/copy-button/index.d.ts +2 -0
- package/dist/copy-button/index.js +2 -0
- package/dist/date-picker/date-picker.d.ts +51 -0
- package/dist/date-picker/date-picker.js +241 -0
- package/dist/date-picker/date-picker.js.map +1 -0
- package/dist/date-picker/date-range-picker.d.ts +78 -0
- package/dist/date-picker/date-range-picker.js +1015 -0
- package/dist/date-picker/date-range-picker.js.map +1 -0
- package/dist/date-picker/index.d.ts +4 -0
- package/dist/date-picker/index.js +4 -0
- package/dist/date-picker/picker-parts.d.ts +46 -0
- package/dist/date-picker/picker-parts.js +135 -0
- package/dist/date-picker/picker-parts.js.map +1 -0
- package/dist/dialog/dialog.d.ts +12 -16
- package/dist/dialog/dialog.js +45 -14
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +44 -0
- package/dist/empty-state/empty-state.js +83 -0
- package/dist/empty-state/empty-state.js.map +1 -0
- package/dist/empty-state/index.d.ts +2 -0
- package/dist/empty-state/index.js +2 -0
- package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
- package/dist/expanding-button-group/expanding-button-group.js +350 -0
- package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
- package/dist/expanding-button-group/index.d.ts +2 -0
- package/dist/expanding-button-group/index.js +2 -0
- package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
- package/dist/file-drop-zone/file-drop-zone.js +116 -0
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
- package/dist/file-drop-zone/index.d.ts +2 -0
- package/dist/file-drop-zone/index.js +2 -0
- package/dist/filter-bar/filter-bar.d.ts +121 -0
- package/dist/filter-bar/filter-bar.js +300 -0
- package/dist/filter-bar/filter-bar.js.map +1 -0
- package/dist/filter-bar/index.d.ts +2 -0
- package/dist/filter-bar/index.js +2 -0
- package/dist/floating-button-group/floating-button-group.d.ts +61 -0
- package/dist/floating-button-group/floating-button-group.js +340 -0
- package/dist/floating-button-group/floating-button-group.js.map +1 -0
- package/dist/floating-button-group/index.d.ts +2 -0
- package/dist/floating-button-group/index.js +2 -0
- package/dist/icon-morph/icon-morph.d.ts +55 -0
- package/dist/icon-morph/icon-morph.js +331 -0
- package/dist/icon-morph/icon-morph.js.map +1 -0
- package/dist/icon-morph/index.d.ts +2 -0
- package/dist/icon-morph/index.js +2 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +29 -7
- package/dist/input/input.d.ts +23 -1
- package/dist/input/input.js +37 -13
- package/dist/input/input.js.map +1 -1
- package/dist/label/label.d.ts +1 -1
- package/dist/label/label.js +4 -2
- package/dist/label/label.js.map +1 -1
- package/dist/lib/count-to.js +113 -0
- package/dist/lib/count-to.js.map +1 -0
- package/dist/lib/eye-morph.js +62 -0
- package/dist/lib/eye-morph.js.map +1 -0
- package/dist/lib/field-message.js +336 -0
- package/dist/lib/field-message.js.map +1 -0
- package/dist/lib/field-shell.js +81 -0
- package/dist/lib/field-shell.js.map +1 -0
- package/dist/lib/field-surface.js +4 -2
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/flip.js +80 -0
- package/dist/lib/flip.js.map +1 -0
- package/dist/lib/height-follow.js +59 -0
- package/dist/lib/height-follow.js.map +1 -0
- package/dist/lib/icon-swap.js +47 -0
- package/dist/lib/icon-swap.js.map +1 -0
- package/dist/lib/leaving-list.js +57 -0
- package/dist/lib/leaving-list.js.map +1 -0
- package/dist/lib/merge-refs.js +22 -0
- package/dist/lib/merge-refs.js.map +1 -0
- package/dist/lib/morph-text.js +128 -0
- package/dist/lib/morph-text.js.map +1 -0
- package/dist/lib/motion.js +162 -0
- package/dist/lib/motion.js.map +1 -0
- package/dist/lib/pausable-timeout.js +67 -0
- package/dist/lib/pausable-timeout.js.map +1 -0
- package/dist/lib/rolling-value.js +31 -0
- package/dist/lib/rolling-value.js.map +1 -0
- package/dist/lib/spring-progress.js +114 -0
- package/dist/lib/spring-progress.js.map +1 -0
- package/dist/lib/svg-id.js +10 -0
- package/dist/lib/svg-id.js.map +1 -0
- package/dist/lib/swap-text.js +77 -0
- package/dist/lib/swap-text.js.map +1 -0
- package/dist/lib/use-copy-feedback.js +36 -0
- package/dist/lib/use-copy-feedback.js.map +1 -0
- package/dist/menu/menu.d.ts +5 -5
- package/dist/menu/menu.js +10 -6
- package/dist/menu/menu.js.map +1 -1
- package/dist/page-header/index.d.ts +2 -0
- package/dist/page-header/index.js +2 -0
- package/dist/page-header/page-header.d.ts +75 -0
- package/dist/page-header/page-header.js +83 -0
- package/dist/page-header/page-header.js.map +1 -0
- package/dist/password-field/index.d.ts +2 -0
- package/dist/password-field/index.js +2 -0
- package/dist/password-field/password-field.d.ts +37 -0
- package/dist/password-field/password-field.js +101 -0
- package/dist/password-field/password-field.js.map +1 -0
- package/dist/password-strength/index.d.ts +2 -0
- package/dist/password-strength/index.js +2 -0
- package/dist/password-strength/password-strength.d.ts +127 -0
- package/dist/password-strength/password-strength.js +455 -0
- package/dist/password-strength/password-strength.js.map +1 -0
- package/dist/popover/popover.d.ts +10 -2
- package/dist/popover/popover.js +6 -4
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.d.ts +2 -2
- package/dist/progress/progress.js +50 -7
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +3 -3
- package/dist/radio-group/radio-group.js +14 -10
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.d.ts +17 -6
- package/dist/scroll-area/scroll-area.js +30 -6
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/search-field/index.d.ts +2 -0
- package/dist/search-field/index.js +2 -0
- package/dist/search-field/search-field.d.ts +26 -0
- package/dist/search-field/search-field.js +155 -0
- package/dist/search-field/search-field.js.map +1 -0
- package/dist/select/select.d.ts +3 -3
- package/dist/select/select.js +85 -24
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/separator/separator.js +4 -2
- package/dist/separator/separator.js.map +1 -1
- package/dist/sheet/sheet.d.ts +1 -1
- package/dist/sheet/sheet.js +6 -3
- package/dist/sheet/sheet.js.map +1 -1
- package/dist/side-nav/index.d.ts +2 -0
- package/dist/side-nav/index.js +2 -0
- package/dist/side-nav/side-nav.d.ts +72 -0
- package/dist/side-nav/side-nav.js +142 -0
- package/dist/side-nav/side-nav.js.map +1 -0
- package/dist/skeleton/skeleton.d.ts +8 -2
- package/dist/skeleton/skeleton.js +9 -3
- package/dist/skeleton/skeleton.js.map +1 -1
- package/dist/slider/slider.d.ts +2 -2
- package/dist/slider/slider.js +64 -8
- package/dist/slider/slider.js.map +1 -1
- package/dist/spinner/spinner.d.ts +1 -1
- package/dist/spinner/spinner.js +4 -2
- package/dist/spinner/spinner.js.map +1 -1
- package/dist/split-button/index.d.ts +2 -0
- package/dist/split-button/index.js +2 -0
- package/dist/split-button/split-button.d.ts +46 -0
- package/dist/split-button/split-button.js +122 -0
- package/dist/split-button/split-button.js.map +1 -0
- package/dist/stat-card/index.d.ts +2 -0
- package/dist/stat-card/index.js +2 -0
- package/dist/stat-card/stat-card.d.ts +81 -0
- package/dist/stat-card/stat-card.js +152 -0
- package/dist/stat-card/stat-card.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/switch/switch.d.ts +2 -2
- package/dist/switch/switch.js +11 -8
- package/dist/switch/switch.js.map +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +38 -7
- package/dist/table/table.js +114 -12
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -4
- package/dist/tabs/tabs.js +153 -12
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/textarea/textarea.d.ts +24 -1
- package/dist/textarea/textarea.js +27 -3
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/theme-switch/index.d.ts +2 -0
- package/dist/theme-switch/index.js +2 -0
- package/dist/theme-switch/theme-switch.d.ts +37 -0
- package/dist/theme-switch/theme-switch.js +123 -0
- package/dist/theme-switch/theme-switch.js.map +1 -0
- package/dist/time-picker/index.d.ts +2 -0
- package/dist/time-picker/index.js +2 -0
- package/dist/time-picker/time-picker.d.ts +54 -0
- package/dist/time-picker/time-picker.js +201 -0
- package/dist/time-picker/time-picker.js.map +1 -0
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toast/toast.js +61 -39
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +3 -3
- package/dist/tooltip/tooltip.js +66 -15
- package/dist/tooltip/tooltip.js.map +1 -1
- package/package.json +83 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SwapText } from "./swap-text.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/lib/rolling-value.tsx
|
|
4
|
+
/**
|
|
5
|
+
* The value shown in a picker's trigger. The caller's own text stays, visually
|
|
6
|
+
* hidden, as what a screen reader reads (`srValue`); the copy the eye sees rolls
|
|
7
|
+
* when it changes: a later value rises in from below, an earlier one drops in from
|
|
8
|
+
* above, and the old one lifts away. Nothing animates on first mount. Long text is
|
|
9
|
+
* cut with an ellipsis. `instanceKey` remounts the swap, which is how a caller whose
|
|
10
|
+
* first value arrives a render late makes that arrival the first mount.
|
|
11
|
+
*/
|
|
12
|
+
function RollingValue({ srValue, swapKey, dir, instanceKey, children }) {
|
|
13
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
14
|
+
className: "grid min-w-0 flex-1",
|
|
15
|
+
children: [srValue, /* @__PURE__ */ jsx("span", {
|
|
16
|
+
"aria-hidden": "true",
|
|
17
|
+
className: "min-w-0 overflow-hidden whitespace-nowrap",
|
|
18
|
+
children: /* @__PURE__ */ jsx(SwapText, {
|
|
19
|
+
swapKey,
|
|
20
|
+
dir,
|
|
21
|
+
motion: "roll",
|
|
22
|
+
className: "block [&>span:not([aria-hidden])]:max-w-full [&>span:not([aria-hidden])]:truncate",
|
|
23
|
+
children
|
|
24
|
+
}, instanceKey)
|
|
25
|
+
})]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { RollingValue };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=rolling-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rolling-value.js","names":[],"sources":["../../src/lib/rolling-value.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { SwapText } from \"./swap-text.js\";\n\n/**\n * The value shown in a picker's trigger. The caller's own text stays, visually\n * hidden, as what a screen reader reads (`srValue`); the copy the eye sees rolls\n * when it changes: a later value rises in from below, an earlier one drops in from\n * above, and the old one lifts away. Nothing animates on first mount. Long text is\n * cut with an ellipsis. `instanceKey` remounts the swap, which is how a caller whose\n * first value arrives a render late makes that arrival the first mount.\n */\nexport function RollingValue({\n srValue,\n swapKey,\n dir,\n instanceKey,\n children,\n}: {\n srValue: ReactNode;\n swapKey: string;\n dir: number;\n instanceKey?: string;\n children: ReactNode;\n}) {\n return (\n <span className=\"grid min-w-0 flex-1\">\n {srValue}\n <span aria-hidden=\"true\" className=\"min-w-0 overflow-hidden whitespace-nowrap\">\n <SwapText\n key={instanceKey}\n swapKey={swapKey}\n dir={dir}\n motion=\"roll\"\n className=\"block [&>span:not([aria-hidden])]:max-w-full [&>span:not([aria-hidden])]:truncate\"\n >\n {children}\n </SwapText>\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,aAAa,EAC3B,SACA,SACA,KACA,aACA,YAOC;CACD,OACE,qBAAC,QAAD;EAAM,WAAU;EAAhB,UAAA,CACG,SACD,oBAAC,QAAD;GAAM,eAAY;GAAO,WAAU;GACjC,UAAA,oBAAC,UAAD;IAEW;IACJ;IACL,QAAO;IACP,WAAU;IAET;GACO,GAPH,WAOG;EACN,CAAA,CACF;;AAEV"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { prefersReducedMotion } from "./motion.js";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
//#region src/lib/spring-progress.ts
|
|
4
|
+
/**
|
|
5
|
+
* The morph spring (--ease-morph, --duration-morph: visualDuration 0.42s, bounce
|
|
6
|
+
* 0.16), sampled every 16ms from Motion's solver. It is here as numbers because a
|
|
7
|
+
* script has to evaluate it: CSS cannot interpolate the `d` of a path in every
|
|
8
|
+
* browser, so an icon that morphs its outline drives the path from this.
|
|
9
|
+
*/
|
|
10
|
+
const MORPH_CURVE = [
|
|
11
|
+
0,
|
|
12
|
+
.0178,
|
|
13
|
+
.0636,
|
|
14
|
+
.1279,
|
|
15
|
+
.2033,
|
|
16
|
+
.2841,
|
|
17
|
+
.366,
|
|
18
|
+
.4459,
|
|
19
|
+
.5218,
|
|
20
|
+
.5921,
|
|
21
|
+
.6562,
|
|
22
|
+
.7136,
|
|
23
|
+
.7643,
|
|
24
|
+
.8086,
|
|
25
|
+
.8466,
|
|
26
|
+
.879,
|
|
27
|
+
.9063,
|
|
28
|
+
.9289,
|
|
29
|
+
.9475,
|
|
30
|
+
.9626,
|
|
31
|
+
.9746,
|
|
32
|
+
.9841,
|
|
33
|
+
.9914,
|
|
34
|
+
.9969,
|
|
35
|
+
1.001,
|
|
36
|
+
1.0038,
|
|
37
|
+
1.0058,
|
|
38
|
+
1.0069,
|
|
39
|
+
1.0075,
|
|
40
|
+
1.0077,
|
|
41
|
+
1.0076,
|
|
42
|
+
1.0072,
|
|
43
|
+
1.0067,
|
|
44
|
+
1.0062,
|
|
45
|
+
1.0055,
|
|
46
|
+
1.0049,
|
|
47
|
+
1.0043,
|
|
48
|
+
1.0037,
|
|
49
|
+
1.0032,
|
|
50
|
+
1.0027,
|
|
51
|
+
1.0022,
|
|
52
|
+
1.0018,
|
|
53
|
+
1.0015,
|
|
54
|
+
1.0012,
|
|
55
|
+
1.0009,
|
|
56
|
+
1.0007,
|
|
57
|
+
1
|
|
58
|
+
];
|
|
59
|
+
const MORPH_MS = 736;
|
|
60
|
+
const ease = (t) => {
|
|
61
|
+
const at = t * MORPH_MS / 16;
|
|
62
|
+
const i = Math.min(Math.floor(at), MORPH_CURVE.length - 2);
|
|
63
|
+
const a = MORPH_CURVE[i];
|
|
64
|
+
return a + (MORPH_CURVE[i + 1] - a) * (at - i);
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Follows `target` (0 or 1) on the morph spring and reports progress to
|
|
68
|
+
* `apply` every frame, mutating the DOM directly rather than re-rendering.
|
|
69
|
+
* It is interruptible: a new target mid-flight starts from where the value is.
|
|
70
|
+
* The first render, and reduced motion, jump straight to the target.
|
|
71
|
+
*/
|
|
72
|
+
function useSpringProgress(target, apply) {
|
|
73
|
+
const current = useRef(target);
|
|
74
|
+
const frame = useRef(0);
|
|
75
|
+
const applyRef = useRef(apply);
|
|
76
|
+
const first = useRef(true);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
applyRef.current = apply;
|
|
79
|
+
});
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
cancelAnimationFrame(frame.current);
|
|
82
|
+
if (first.current || prefersReducedMotion()) {
|
|
83
|
+
first.current = false;
|
|
84
|
+
current.current = target;
|
|
85
|
+
applyRef.current(target);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const from = current.current;
|
|
89
|
+
const start = performance.now();
|
|
90
|
+
const tick = (now) => {
|
|
91
|
+
const t = Math.min(Math.max((now - start) / MORPH_MS, 0), 1);
|
|
92
|
+
current.current = from + (target - from) * ease(t);
|
|
93
|
+
applyRef.current(current.current);
|
|
94
|
+
if (t < 1) frame.current = requestAnimationFrame(tick);
|
|
95
|
+
};
|
|
96
|
+
frame.current = requestAnimationFrame(tick);
|
|
97
|
+
return () => cancelAnimationFrame(frame.current);
|
|
98
|
+
}, [target]);
|
|
99
|
+
}
|
|
100
|
+
/** Interpolates the numbers of a path whose two states have the same commands. */
|
|
101
|
+
function lerpPath(from, to, progress) {
|
|
102
|
+
const a = from.match(/-?\d*\.?\d+/g).map(Number);
|
|
103
|
+
const b = to.match(/-?\d*\.?\d+/g).map(Number);
|
|
104
|
+
let i = 0;
|
|
105
|
+
return from.replace(/-?\d*\.?\d+/g, () => {
|
|
106
|
+
const n = a[i] + (b[i] - a[i]) * progress;
|
|
107
|
+
i += 1;
|
|
108
|
+
return String(Math.round(n * 1e3) / 1e3);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { lerpPath, useSpringProgress };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=spring-progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spring-progress.js","names":[],"sources":["../../src/lib/spring-progress.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\nimport { prefersReducedMotion } from \"./motion.js\";\n\n/**\n * The morph spring (--ease-morph, --duration-morph: visualDuration 0.42s, bounce\n * 0.16), sampled every 16ms from Motion's solver. It is here as numbers because a\n * script has to evaluate it: CSS cannot interpolate the `d` of a path in every\n * browser, so an icon that morphs its outline drives the path from this.\n */\nconst MORPH_CURVE: readonly number[] = [\n 0, 0.0178, 0.0636, 0.1279, 0.2033, 0.2841, 0.366, 0.4459, 0.5218, 0.5921, 0.6562, 0.7136, 0.7643,\n 0.8086, 0.8466, 0.879, 0.9063, 0.9289, 0.9475, 0.9626, 0.9746, 0.9841, 0.9914, 0.9969, 1.001,\n 1.0038, 1.0058, 1.0069, 1.0075, 1.0077, 1.0076, 1.0072, 1.0067, 1.0062, 1.0055, 1.0049, 1.0043,\n 1.0037, 1.0032, 1.0027, 1.0022, 1.0018, 1.0015, 1.0012, 1.0009, 1.0007, 1,\n];\nconst MORPH_MS = 736;\n\nconst ease = (t: number) => {\n const at = (t * MORPH_MS) / 16;\n const i = Math.min(Math.floor(at), MORPH_CURVE.length - 2);\n const a = MORPH_CURVE[i]!;\n const b = MORPH_CURVE[i + 1]!;\n return a + (b - a) * (at - i);\n};\n\n/**\n * Follows `target` (0 or 1) on the morph spring and reports progress to\n * `apply` every frame, mutating the DOM directly rather than re-rendering.\n * It is interruptible: a new target mid-flight starts from where the value is.\n * The first render, and reduced motion, jump straight to the target.\n */\nexport function useSpringProgress(target: number, apply: (progress: number) => void) {\n const current = useRef(target);\n const frame = useRef(0);\n const applyRef = useRef(apply);\n const first = useRef(true);\n\n useEffect(() => {\n applyRef.current = apply;\n });\n\n useEffect(() => {\n cancelAnimationFrame(frame.current);\n if (first.current || prefersReducedMotion()) {\n first.current = false;\n current.current = target;\n applyRef.current(target);\n return;\n }\n const from = current.current;\n const start = performance.now();\n const tick = (now: number) => {\n // A frame's timestamp can precede `start`; a negative t would index the curve at -1.\n const t = Math.min(Math.max((now - start) / MORPH_MS, 0), 1);\n current.current = from + (target - from) * ease(t);\n applyRef.current(current.current);\n if (t < 1) frame.current = requestAnimationFrame(tick);\n };\n frame.current = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(frame.current);\n }, [target]);\n}\n\n/** Interpolates the numbers of a path whose two states have the same commands. */\nexport function lerpPath(from: string, to: string, progress: number) {\n const a = from.match(/-?\\d*\\.?\\d+/g)!.map(Number);\n const b = to.match(/-?\\d*\\.?\\d+/g)!.map(Number);\n let i = 0;\n return from.replace(/-?\\d*\\.?\\d+/g, () => {\n const n = a[i]! + (b[i]! - a[i]!) * progress;\n i += 1;\n return String(Math.round(n * 1000) / 1000);\n });\n}\n"],"mappings":";;;;;;;;;AAUA,MAAM,cAAiC;CACrC;CAAG;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAC1F;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CACvF;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CACxF;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;AAC1E;AACA,MAAM,WAAW;AAEjB,MAAM,QAAQ,MAAc;CAC1B,MAAM,KAAM,IAAI,WAAY;CAC5B,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,EAAE,GAAG,YAAY,SAAS,CAAC;CACzD,MAAM,IAAI,YAAY;CAEtB,OAAO,KADG,YAAY,IAAI,KACV,MAAM,KAAK;AAC7B;;;;;;;AAQA,SAAgB,kBAAkB,QAAgB,OAAmC;CACnF,MAAM,UAAU,OAAO,MAAM;CAC7B,MAAM,QAAQ,OAAO,CAAC;CACtB,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,QAAQ,OAAO,IAAI;CAEzB,gBAAgB;EACd,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,qBAAqB,MAAM,OAAO;EAClC,IAAI,MAAM,WAAW,qBAAqB,GAAG;GAC3C,MAAM,UAAU;GAChB,QAAQ,UAAU;GAClB,SAAS,QAAQ,MAAM;GACvB;EACF;EACA,MAAM,OAAO,QAAQ;EACrB,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,QAAQ,QAAgB;GAE5B,MAAM,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,SAAS,UAAU,CAAC,GAAG,CAAC;GAC3D,QAAQ,UAAU,QAAQ,SAAS,QAAQ,KAAK,CAAC;GACjD,SAAS,QAAQ,QAAQ,OAAO;GAChC,IAAI,IAAI,GAAG,MAAM,UAAU,sBAAsB,IAAI;EACvD;EACA,MAAM,UAAU,sBAAsB,IAAI;EAC1C,aAAa,qBAAqB,MAAM,OAAO;CACjD,GAAG,CAAC,MAAM,CAAC;AACb;;AAGA,SAAgB,SAAS,MAAc,IAAY,UAAkB;CACnE,MAAM,IAAI,KAAK,MAAM,cAAc,CAAC,CAAE,IAAI,MAAM;CAChD,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,CAAE,IAAI,MAAM;CAC9C,IAAI,IAAI;CACR,OAAO,KAAK,QAAQ,sBAAsB;EACxC,MAAM,IAAI,EAAE,MAAO,EAAE,KAAM,EAAE,MAAO;EACpC,KAAK;EACL,OAAO,OAAO,KAAK,MAAM,IAAI,GAAI,IAAI,GAAI;CAC3C,CAAC;AACH"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
//#region src/lib/svg-id.ts
|
|
3
|
+
/** An id safe inside url(#...), for masks and clip paths: React's own ids hold characters (colons, underscores) that break the reference. */
|
|
4
|
+
function useSvgId() {
|
|
5
|
+
return useId().replace(/[^a-zA-Z0-9-]/g, "");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { useSvgId };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=svg-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg-id.js","names":[],"sources":["../../src/lib/svg-id.ts"],"sourcesContent":["import { useId } from \"react\";\n\n/** An id safe inside url(#...), for masks and clip paths: React's own ids hold characters (colons, underscores) that break the reference. */\nexport function useSvgId() {\n return useId().replace(/[^a-zA-Z0-9-]/g, \"\");\n}\n"],"mappings":";;;AAGA,SAAgB,WAAW;CACzB,OAAO,MAAM,CAAC,CAAC,QAAQ,kBAAkB,EAAE;AAC7C"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn } from "./cn.js";
|
|
2
|
+
import { useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems } from "./motion.js";
|
|
3
|
+
import { useCallback, useRef } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/lib/swap-text.tsx
|
|
6
|
+
const LEAVING = {
|
|
7
|
+
rise: "absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out",
|
|
8
|
+
roll: "absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out",
|
|
9
|
+
slide: "absolute motion-safe:animate-calendar-title-out motion-reduce:animate-fade-out",
|
|
10
|
+
word: "absolute motion-safe:animate-range-word-out motion-reduce:animate-range-word-fade-out",
|
|
11
|
+
glyph: "absolute motion-safe:animate-glyph-swap-out motion-reduce:animate-fade-out",
|
|
12
|
+
dot: "absolute motion-safe:animate-avatar-dot-out motion-reduce:animate-fade-out"
|
|
13
|
+
};
|
|
14
|
+
const ARRIVING = {
|
|
15
|
+
rise: "inline-block motion-safe:animate-field-swap-in motion-reduce:animate-field-swap-fade-in",
|
|
16
|
+
roll: "inline-block motion-safe:animate-select-value-in motion-reduce:animate-fade-in",
|
|
17
|
+
slide: "inline-block motion-safe:animate-calendar-title-in motion-reduce:animate-fade-in",
|
|
18
|
+
word: "inline-block motion-safe:animate-range-word-in motion-reduce:animate-fade-in",
|
|
19
|
+
glyph: "inline-block motion-safe:animate-glyph-swap-in motion-reduce:animate-fade-in",
|
|
20
|
+
dot: "inline-block motion-safe:animate-avatar-dot-in motion-reduce:animate-fade-in"
|
|
21
|
+
};
|
|
22
|
+
/** A replaced piece of content on its way out, popped out of flow where it stood and lifting away. */
|
|
23
|
+
function Leaving({ id, empty, rect, motion, onDone, children }) {
|
|
24
|
+
const remove = useCallback(() => onDone(id), [onDone, id]);
|
|
25
|
+
useExitBackstop(true, remove);
|
|
26
|
+
useIsomorphicLayoutEffect(() => {
|
|
27
|
+
if (empty) remove();
|
|
28
|
+
}, [empty, remove]);
|
|
29
|
+
if (empty) return null;
|
|
30
|
+
return /* @__PURE__ */ jsx("span", {
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
"data-leaving": "",
|
|
33
|
+
onAnimationEnd: (event) => {
|
|
34
|
+
if (event.target === event.currentTarget) remove();
|
|
35
|
+
},
|
|
36
|
+
style: rect,
|
|
37
|
+
className: LEAVING[motion],
|
|
38
|
+
children
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Content that is replaced as a whole when `swapKey` changes: the new content
|
|
43
|
+
* rises in from `dir` (1 from below, -1 from above; one variable shared with the
|
|
44
|
+
* leaving content so both follow the newest direction) while the old content,
|
|
45
|
+
* kept where it stood, lifts away the other way. An empty `swapKey` shows nothing
|
|
46
|
+
* (and the content before it leaves). Nothing animates on first mount, and
|
|
47
|
+
* reduced motion fades instead of moving. This is for the eye only: a screen
|
|
48
|
+
* reader reads the caller's own copy. The wrapper is `position: relative`, which
|
|
49
|
+
* the popped-out content is placed against.
|
|
50
|
+
*/
|
|
51
|
+
function SwapText({ swapKey, dir = 1, motion = "rise", className, children }) {
|
|
52
|
+
const ref = useRef(null);
|
|
53
|
+
const { leaving, changed, remove } = useLeavingItems(swapKey, {
|
|
54
|
+
key: swapKey,
|
|
55
|
+
node: children
|
|
56
|
+
}, () => ref.current);
|
|
57
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
58
|
+
style: { "--dir": dir },
|
|
59
|
+
className: cn("relative", className),
|
|
60
|
+
children: [leaving.map((item) => /* @__PURE__ */ jsx(Leaving, {
|
|
61
|
+
id: item.id,
|
|
62
|
+
empty: item.value.key === "",
|
|
63
|
+
rect: item.rect,
|
|
64
|
+
motion,
|
|
65
|
+
onDone: remove,
|
|
66
|
+
children: item.value.node
|
|
67
|
+
}, item.id)), swapKey ? /* @__PURE__ */ jsx("span", {
|
|
68
|
+
ref,
|
|
69
|
+
className: changed ? ARRIVING[motion] : "inline-block",
|
|
70
|
+
children
|
|
71
|
+
}, swapKey) : null]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { SwapText };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=swap-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swap-text.js","names":[],"sources":["../../src/lib/swap-text.tsx"],"sourcesContent":["import { useCallback, useRef, type CSSProperties, type ReactNode } from \"react\";\n\nimport { cn } from \"./cn.js\";\nimport { useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems } from \"./motion.js\";\nimport type { Rect } from \"./motion.js\";\n\n/**\n * How replaced content moves. `rise`: up or down a little (a field message). `roll`:\n * a little further, and a 120ms fade under reduced motion (the value a picker shows).\n * `slide`: sideways (a calendar's month title). `word`: a word of a rolling label.\n * `glyph`: an icon, which grows in from small and blurred. `dot`: a presence dot.\n */\nexport type SwapMotion = \"rise\" | \"roll\" | \"slide\" | \"word\" | \"glyph\" | \"dot\";\n\n// Static strings: Tailwind scans source text, so a class built at runtime would never be emitted.\nconst LEAVING: Record<SwapMotion, string> = {\n rise: \"absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out\",\n roll: \"absolute motion-safe:animate-field-swap-out motion-reduce:animate-fade-out\",\n slide: \"absolute motion-safe:animate-calendar-title-out motion-reduce:animate-fade-out\",\n word: \"absolute motion-safe:animate-range-word-out motion-reduce:animate-range-word-fade-out\",\n glyph: \"absolute motion-safe:animate-glyph-swap-out motion-reduce:animate-fade-out\",\n dot: \"absolute motion-safe:animate-avatar-dot-out motion-reduce:animate-fade-out\",\n};\n\nconst ARRIVING: Record<SwapMotion, string> = {\n rise: \"inline-block motion-safe:animate-field-swap-in motion-reduce:animate-field-swap-fade-in\",\n roll: \"inline-block motion-safe:animate-select-value-in motion-reduce:animate-fade-in\",\n slide: \"inline-block motion-safe:animate-calendar-title-in motion-reduce:animate-fade-in\",\n word: \"inline-block motion-safe:animate-range-word-in motion-reduce:animate-fade-in\",\n glyph: \"inline-block motion-safe:animate-glyph-swap-in motion-reduce:animate-fade-in\",\n dot: \"inline-block motion-safe:animate-avatar-dot-in motion-reduce:animate-fade-in\",\n};\n\n/** A replaced piece of content on its way out, popped out of flow where it stood and lifting away. */\nfunction Leaving({\n id,\n empty,\n rect,\n motion,\n onDone,\n children,\n}: {\n id: number;\n empty: boolean;\n rect: Rect;\n motion: SwapMotion;\n onDone: (id: number) => void;\n children: ReactNode;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n // The empty value of \"nothing shown\" has nothing to lift away.\n useIsomorphicLayoutEffect(() => {\n if (empty) remove();\n }, [empty, remove]);\n if (empty) return null;\n return (\n <span\n aria-hidden=\"true\"\n data-leaving=\"\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={rect}\n className={LEAVING[motion]}\n >\n {children}\n </span>\n );\n}\n\n/**\n * Content that is replaced as a whole when `swapKey` changes: the new content\n * rises in from `dir` (1 from below, -1 from above; one variable shared with the\n * leaving content so both follow the newest direction) while the old content,\n * kept where it stood, lifts away the other way. An empty `swapKey` shows nothing\n * (and the content before it leaves). Nothing animates on first mount, and\n * reduced motion fades instead of moving. This is for the eye only: a screen\n * reader reads the caller's own copy. The wrapper is `position: relative`, which\n * the popped-out content is placed against.\n */\nexport function SwapText({\n swapKey,\n dir = 1,\n motion = \"rise\",\n className,\n children,\n}: {\n swapKey: string;\n dir?: number;\n /** How it moves. `dir` is 1 from below or forward, -1 from above or back. @default \"rise\" */\n motion?: SwapMotion;\n className?: string;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLSpanElement>(null);\n const { leaving, changed, remove } = useLeavingItems(\n swapKey,\n { key: swapKey, node: children },\n () => ref.current,\n );\n return (\n <span style={{ \"--dir\": dir } as CSSProperties} className={cn(\"relative\", className)}>\n {leaving.map((item) => (\n <Leaving\n key={item.id}\n id={item.id}\n empty={item.value.key === \"\"}\n rect={item.rect}\n motion={motion}\n onDone={remove}\n >\n {item.value.node}\n </Leaving>\n ))}\n {swapKey ? (\n <span\n key={swapKey}\n ref={ref}\n className={changed ? ARRIVING[motion] : \"inline-block\"}\n >\n {children}\n </span>\n ) : null}\n </span>\n );\n}\n"],"mappings":";;;;;AAeA,MAAM,UAAsC;CAC1C,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,KAAK;AACP;AAEA,MAAM,WAAuC;CAC3C,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,KAAK;AACP;;AAGA,SAAS,QAAQ,EACf,IACA,OACA,MACA,QACA,QACA,YAQC;CACD,MAAM,SAAS,kBAAkB,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;CACzD,gBAAgB,MAAM,MAAM;CAE5B,gCAAgC;EAC9B,IAAI,OAAO,OAAO;CACpB,GAAG,CAAC,OAAO,MAAM,CAAC;CAClB,IAAI,OAAO,OAAO;CAClB,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,gBAAa;EACb,iBAAiB,UAAU;GACzB,IAAI,MAAM,WAAW,MAAM,eAAe,OAAO;EACnD;EACA,OAAO;EACP,WAAW,QAAQ;EAElB;CACG,CAAA;AAEV;;;;;;;;;;;AAYA,SAAgB,SAAS,EACvB,SACA,MAAM,GACN,SAAS,QACT,WACA,YAQC;CACD,MAAM,MAAM,OAAwB,IAAI;CACxC,MAAM,EAAE,SAAS,SAAS,WAAW,gBACnC,SACA;EAAE,KAAK;EAAS,MAAM;CAAS,SACzB,IAAI,OACZ;CACA,OACE,qBAAC,QAAD;EAAM,OAAO,EAAE,SAAS,IAAI;EAAoB,WAAW,GAAG,YAAY,SAAS;EAAnF,UAAA,CACG,QAAQ,KAAK,SACZ,oBAAC,SAAD;GAEE,IAAI,KAAK;GACT,OAAO,KAAK,MAAM,QAAQ;GAC1B,MAAM,KAAK;GACH;GACR,QAAQ;GAEP,UAAA,KAAK,MAAM;EACL,GARF,KAAK,EAQH,CACV,GACA,UACC,oBAAC,QAAD;GAEO;GACL,WAAW,UAAU,SAAS,UAAU;GAEvC;EACG,GALC,OAKD,IACJ,IACA;;AAEV"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
//#region src/lib/use-copy-feedback.ts
|
|
3
|
+
/** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */
|
|
4
|
+
function useCopyFeedback(duration = 1900) {
|
|
5
|
+
const [state, setState] = useState("idle");
|
|
6
|
+
const timeout = useRef(null);
|
|
7
|
+
const reset = useCallback(() => {
|
|
8
|
+
if (timeout.current) clearTimeout(timeout.current);
|
|
9
|
+
timeout.current = null;
|
|
10
|
+
setState("idle");
|
|
11
|
+
}, []);
|
|
12
|
+
useEffect(() => () => {
|
|
13
|
+
if (timeout.current) clearTimeout(timeout.current);
|
|
14
|
+
}, []);
|
|
15
|
+
return {
|
|
16
|
+
state,
|
|
17
|
+
copy: useCallback(async (value) => {
|
|
18
|
+
if (timeout.current) clearTimeout(timeout.current);
|
|
19
|
+
try {
|
|
20
|
+
await navigator.clipboard.writeText(value);
|
|
21
|
+
setState("copied");
|
|
22
|
+
timeout.current = setTimeout(reset, duration);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
setState("error");
|
|
26
|
+
timeout.current = setTimeout(reset, duration);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}, [duration, reset]),
|
|
30
|
+
reset
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { useCopyFeedback };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=use-copy-feedback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-copy-feedback.js","names":[],"sources":["../../src/lib/use-copy-feedback.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type CopyFeedbackState = \"idle\" | \"copied\" | \"error\";\n\n/** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */\nexport function useCopyFeedback(duration = 1900) {\n const [state, setState] = useState<CopyFeedbackState>(\"idle\");\n const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const reset = useCallback(() => {\n if (timeout.current) clearTimeout(timeout.current);\n timeout.current = null;\n setState(\"idle\");\n }, []);\n\n useEffect(\n () => () => {\n if (timeout.current) clearTimeout(timeout.current);\n },\n [],\n );\n\n /** Resolves true if the text reached the clipboard. */\n const copy = useCallback(\n async (value: string) => {\n if (timeout.current) clearTimeout(timeout.current);\n try {\n await navigator.clipboard.writeText(value);\n setState(\"copied\");\n timeout.current = setTimeout(reset, duration);\n return true;\n } catch {\n setState(\"error\");\n timeout.current = setTimeout(reset, duration);\n return false;\n }\n },\n [duration, reset],\n );\n\n return { state, copy, reset };\n}\n"],"mappings":";;;AAKA,SAAgB,gBAAgB,WAAW,MAAM;CAC/C,MAAM,CAAC,OAAO,YAAY,SAA4B,MAAM;CAC5D,MAAM,UAAU,OAA6C,IAAI;CAEjE,MAAM,QAAQ,kBAAkB;EAC9B,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;EACjD,QAAQ,UAAU;EAClB,SAAS,MAAM;CACjB,GAAG,CAAC,CAAC;CAEL,sBACc;EACV,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;CACnD,GACA,CAAC,CACH;CAoBA,OAAO;EAAE;EAAO,MAjBH,YACX,OAAO,UAAkB;GACvB,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;GACjD,IAAI;IACF,MAAM,UAAU,UAAU,UAAU,KAAK;IACzC,SAAS,QAAQ;IACjB,QAAQ,UAAU,WAAW,OAAO,QAAQ;IAC5C,OAAO;GACT,QAAQ;IACN,SAAS,OAAO;IAChB,QAAQ,UAAU,WAAW,OAAO,QAAQ;IAC5C,OAAO;GACT;EACF,GACA,CAAC,UAAU,KAAK,CAGC;EAAG;CAAM;AAC9B"}
|
package/dist/menu/menu.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MenuItemProps, MenuProps, MenuTrigger, PopoverProps } from "react-aria-components";
|
|
2
1
|
import { ReactNode } from "react";
|
|
2
|
+
import { MenuItemProps, MenuProps, MenuTrigger, PopoverProps } from "react-aria-components";
|
|
3
3
|
//#region src/menu/menu.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Menu, ported from the legacy shadcn/Radix `dropdown-menu` onto React
|
|
@@ -68,7 +68,7 @@ interface MenuProps$1 extends Omit<MenuProps<object>, "className" | "style" | "c
|
|
|
68
68
|
/** Gap between the trigger and the menu. @default 4 */
|
|
69
69
|
offset?: number;
|
|
70
70
|
}
|
|
71
|
-
declare
|
|
71
|
+
declare const Menu$1: import("react").ForwardRefExoticComponent<MenuProps$1 & import("react").RefAttributes<HTMLDivElement>>;
|
|
72
72
|
/**
|
|
73
73
|
* One menu row.
|
|
74
74
|
*
|
|
@@ -80,7 +80,7 @@ interface MenuItemProps$1 extends Omit<MenuItemProps, "className" | "style"> {
|
|
|
80
80
|
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
81
81
|
className?: string;
|
|
82
82
|
}
|
|
83
|
-
declare
|
|
83
|
+
declare const MenuItem$1: import("react").ForwardRefExoticComponent<MenuItemProps$1 & import("react").RefAttributes<HTMLDivElement>>;
|
|
84
84
|
/**
|
|
85
85
|
* A divider between groups of items.
|
|
86
86
|
*
|
|
@@ -90,9 +90,9 @@ declare function MenuItem$1({ className, ...props }: MenuItemProps$1): import("r
|
|
|
90
90
|
* standalone, because `MenuInner` supplies `elementType: 'div'` -- an
|
|
91
91
|
* `<hr>` is not valid inside a `role="menu"` container.
|
|
92
92
|
*/
|
|
93
|
-
export declare
|
|
93
|
+
export declare const MenuSeparator: import("react").ForwardRefExoticComponent<{
|
|
94
94
|
className?: string;
|
|
95
|
-
}
|
|
95
|
+
} & import("react").RefAttributes<HTMLElement>>;
|
|
96
96
|
//#endregion
|
|
97
97
|
export { Menu$1 as Menu, MenuItem$1 as MenuItem, MenuItemProps$1 as MenuItemProps, MenuProps$1 as MenuProps, MenuTrigger };
|
|
98
98
|
//# sourceMappingURL=menu.d.ts.map
|
package/dist/menu/menu.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
+
import { forwardRef } from "react";
|
|
4
5
|
import { Menu, MenuItem, MenuTrigger, Popover, Separator } from "react-aria-components";
|
|
5
6
|
import { jsx } from "react/jsx-runtime";
|
|
6
7
|
//#region src/menu/menu.tsx
|
|
@@ -57,27 +58,29 @@ const menuItemVariants = cva([
|
|
|
57
58
|
"[&_svg:not([class*='size-'])]:size-(--icon-sm)"
|
|
58
59
|
]);
|
|
59
60
|
const menuSeparatorVariants = cva(["-mx-1 my-1 h-px bg-border border-none"]);
|
|
60
|
-
|
|
61
|
+
const Menu$1 = forwardRef(function Menu$2({ children, className, placement = "bottom start", offset = 4, ...props }, ref) {
|
|
61
62
|
return /* @__PURE__ */ jsx(Popover, {
|
|
62
63
|
placement,
|
|
63
64
|
offset,
|
|
64
65
|
"data-slot": "menu-popover",
|
|
65
66
|
className: cn(menuPopoverVariants(), className),
|
|
66
67
|
children: /* @__PURE__ */ jsx(Menu, {
|
|
68
|
+
ref,
|
|
67
69
|
...props,
|
|
68
70
|
"data-slot": "menu",
|
|
69
71
|
className: menuVariants(),
|
|
70
72
|
children
|
|
71
73
|
})
|
|
72
74
|
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
+
});
|
|
76
|
+
const MenuItem$1 = forwardRef(function MenuItem$2({ className, ...props }, ref) {
|
|
75
77
|
return /* @__PURE__ */ jsx(MenuItem, {
|
|
78
|
+
ref,
|
|
76
79
|
...props,
|
|
77
80
|
"data-slot": "menu-item",
|
|
78
81
|
className: cn(menuItemVariants(), className)
|
|
79
82
|
});
|
|
80
|
-
}
|
|
83
|
+
});
|
|
81
84
|
/**
|
|
82
85
|
* A divider between groups of items.
|
|
83
86
|
*
|
|
@@ -87,12 +90,13 @@ function MenuItem$1({ className, ...props }) {
|
|
|
87
90
|
* standalone, because `MenuInner` supplies `elementType: 'div'` -- an
|
|
88
91
|
* `<hr>` is not valid inside a `role="menu"` container.
|
|
89
92
|
*/
|
|
90
|
-
function MenuSeparator({ className }) {
|
|
93
|
+
const MenuSeparator = forwardRef(function MenuSeparator({ className }, ref) {
|
|
91
94
|
return /* @__PURE__ */ jsx(Separator, {
|
|
95
|
+
ref,
|
|
92
96
|
"data-slot": "menu-separator",
|
|
93
97
|
className: cn(menuSeparatorVariants(), className)
|
|
94
98
|
});
|
|
95
|
-
}
|
|
99
|
+
});
|
|
96
100
|
//#endregion
|
|
97
101
|
export { Menu$1 as Menu, MenuItem$1 as MenuItem, MenuSeparator, MenuTrigger, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants };
|
|
98
102
|
|
package/dist/menu/menu.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.js","names":["Menu","AriaPopover","AriaMenu","MenuItem","AriaMenuItem","AriaSeparator"],"sources":["../../src/menu/menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type
|
|
1
|
+
{"version":3,"file":"menu.js","names":["Menu","AriaPopover","AriaMenu","MenuItem","AriaMenuItem","AriaSeparator"],"sources":["../../src/menu/menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Menu as AriaMenu,\n MenuItem as AriaMenuItem,\n Popover as AriaPopover,\n Separator as AriaSeparator,\n type MenuItemProps as AriaMenuItemProps,\n type MenuProps as AriaMenuProps,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Menu, ported from the legacy shadcn/Radix `dropdown-menu` onto React\n * Aria's Menu collection.\n *\n * FIFTEEN EXPORTS BECOME FOUR, and the census is unambiguous about it. Ten\n * files import the legacy component but only EIGHT render anything --\n * `header.tsx` and `PollQuizCreator.tsx` import parts and use none. Across\n * those eight: `DropdownMenuItem` 42, `DropdownMenu` 13,\n * `DropdownMenuTrigger` 13, `DropdownMenuContent` 13,\n * `DropdownMenuSeparator` 2.\n *\n * Everything else is ZERO: `Group`, `CheckboxItem`, `RadioGroup`,\n * `RadioItem`, `Label`, `Shortcut`, `Sub`, `SubTrigger`, `SubContent`,\n * `Portal`. Nine exports, no call sites. React Aria has `MenuSection`,\n * `Header` and selection modes ready if any of them turns out to be\n * wanted -- they are one component each, added the day something needs one.\n *\n * `variant=\"destructive\"` is also never passed, which resolves a real\n * problem rather than dodging one: `destructive-strong` on a `muted` hover\n * fill measures 4.29:1 against a 4.5 floor. The legacy has the same defect\n * unmeasured and worse (its `destructive/10` composite gives 4.01:1). With\n * no call site there is nothing to ship and nothing to weaken the contract\n * for.\n *\n * The surface is `card`, not a `popover` role: in the legacy theme\n * `--popover` is byte-identical to `--card` in both themes. Item hover is\n * `muted`, not `accent`: `--accent` is byte-identical to `--muted`. Two\n * more of the migration plan's proposed roles that already exist.\n *\n * Every string in the cva blocks below is a STATIC literal.\n */\nexport const menuPopoverVariants = cva([\n // min-w-32 is 8rem, the legacy min-w-[8rem] -- but as a spacing-scale\n // step rather than a bracket value that check:hardcoding rejects.\n \"min-w-32 overflow-y-auto\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\nexport const menuVariants = cva([\n \"flex flex-col outline-none\",\n]);\n\nexport const menuItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n \"rounded-2xs px-2 py-1.5\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n \"outline-none\",\n\n // data-focused, NOT :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the arrow\n // keys reached are the same row -- the legacy used `focus:` for the\n // keyboard and nothing for hover, so a mouse user saw no highlight at all\n // until the item took focus.\n \"data-[focused]:bg-muted\",\n\n // The focus ring goes on the ITEM, and it is an outline rather than a\n // background alone: a highlight made only of colour disappears under\n // forced-colors, and axe has no rule that would notice.\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const menuSeparatorVariants = cva([\"-mx-1 my-1 h-px bg-border border-none\"]);\n\n/**\n * A dropdown menu.\n *\n * @example\n * <MenuTrigger>\n * <Button>Actions</Button>\n * <Menu onAction={(key) => run(key)}>\n * <MenuItem id=\"edit\">Edit</MenuItem>\n * <MenuSeparator />\n * <MenuItem id=\"delete\">Delete</MenuItem>\n * </Menu>\n * </MenuTrigger>\n */\nexport interface MenuProps\n extends Omit<AriaMenuProps<object>, \"className\" | \"style\" | \"children\" | \"items\"> {\n /** `MenuItem` and `MenuSeparator` children. */\n children?: ReactNode;\n /** Extra classes for the popover. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Where the menu opens, as React Aria's single `placement` string.\n *\n * The legacy split this across `side`, `align`, `sideOffset` and\n * `alignOffset`. Its census reads `align=\"end\"` 10 and `align=\"center\"`\n * 2, always with the default bottom side -- so `\"bottom end\"` covers ten\n * of the twelve.\n *\n * @default \"bottom start\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n /** Gap between the trigger and the menu. @default 4 */\n offset?: number;\n}\n\nexport const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(\n { children, className, placement = \"bottom start\", offset = 4, ...props },\n ref,\n) {\n return (\n <AriaPopover\n placement={placement}\n offset={offset}\n data-slot=\"menu-popover\"\n className={cn(menuPopoverVariants(), className)}\n >\n <AriaMenu\n // The ref goes on the MENU, not the POPOVER above. The popover here\n // is purely a positioner -- same role as an overlay wrapper -- and\n // the props spread and children already land on AriaMenu.\n ref={ref}\n {...props}\n data-slot=\"menu\"\n className={menuVariants()}\n >\n {children}\n </AriaMenu>\n </AriaPopover>\n );\n});\n\n/**\n * One menu row.\n *\n * @example\n * <MenuItem id=\"edit\">Edit</MenuItem>\n * <MenuItem href=\"/settings\">Settings</MenuItem>\n */\nexport interface MenuItemProps extends Omit<AriaMenuItemProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem(\n { className, ...props },\n ref,\n) {\n return (\n <AriaMenuItem\n ref={ref}\n {...props}\n data-slot=\"menu-item\"\n className={cn(menuItemVariants(), className)}\n />\n );\n});\n\n/**\n * A divider between groups of items.\n *\n * A real collection child: React Aria's `Separator` is a `SeparatorNode`,\n * so unlike an arbitrary element it is legal inside a Menu. Inside one it\n * renders `<div role=\"separator\">` rather than the `<hr>` it uses\n * standalone, because `MenuInner` supplies `elementType: 'div'` -- an\n * `<hr>` is not valid inside a `role=\"menu\"` container.\n */\nexport const MenuSeparator = forwardRef<HTMLElement, { className?: string }>(\n function MenuSeparator({ className }, ref) {\n return (\n <AriaSeparator\n ref={ref}\n data-slot=\"menu-separator\"\n className={cn(menuSeparatorVariants(), className)}\n />\n );\n },\n);\n\n/**\n * Pairs a trigger with a menu and owns the open state.\n *\n * Re-exported from React Aria unchanged. It takes the trigger element\n * directly as its first child, which is what `asChild` was doing on all 13\n * legacy call sites.\n */\nexport { MenuTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,sBAAsB,IAAI;CAGrC;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,eAAe,IAAI,CAC9B,4BACF,CAAC;AAED,MAAa,mBAAmB,IAAI;CAClC;CACA;CACA;CACA;CAOA;CAKA;CACA;CACA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,wBAAwB,IAAI,CAAC,uCAAuC,CAAC;AAoClF,MAAaA,SAAO,WAAsC,SAASA,OACjE,EAAE,UAAU,WAAW,YAAY,gBAAgB,SAAS,GAAG,GAAG,SAClE,KACA;CACA,OACE,oBAACC,SAAD;EACa;EACH;EACR,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;EAE9C,UAAA,oBAACC,MAAD;GAIO;GACL,GAAI;GACJ,aAAU;GACV,WAAW,aAAa;GAEvB;EACO,CAAA;CACC,CAAA;AAEjB,CAAC;AAcD,MAAaC,aAAW,WAA0C,SAASA,WACzE,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,UAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;CAC5C,CAAA;AAEL,CAAC;;;;;;;;;;AAWD,MAAa,gBAAgB,WAC3B,SAAS,cAAc,EAAE,aAAa,KAAK;CACzC,OACE,oBAACC,WAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,SAAS;CACjD,CAAA;AAEL,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { PageHeader, PageHeaderProps, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants } from "./page-header.js";
|
|
2
|
+
export { PageHeader, type PageHeaderProps, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { PageHeader, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants } from "./page-header.js";
|
|
2
|
+
export { PageHeader, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
//#region src/page-header/page-header.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* PageHeader: the top of a page. One title, an optional line saying what the
|
|
5
|
+
* page is for, the page's own actions, a short piece of metadata and an
|
|
6
|
+
* optional way back.
|
|
7
|
+
*
|
|
8
|
+
* Presentational, so there is no React Aria base. It exists because a page
|
|
9
|
+
* title is the one element every screen has, and without a shared component
|
|
10
|
+
* each screen picks its own size, weight, spacing and action placement, so the
|
|
11
|
+
* title moves as a person moves between pages.
|
|
12
|
+
*
|
|
13
|
+
* The title renders the page's single `<h1>`. A page has one, and putting it
|
|
14
|
+
* here means a consumer cannot render two by accident.
|
|
15
|
+
*
|
|
16
|
+
* Every string in the cva blocks below is a STATIC literal. Tailwind scans
|
|
17
|
+
* source text, so a class assembled at runtime is invisible to it and would
|
|
18
|
+
* simply never be emitted.
|
|
19
|
+
*/
|
|
20
|
+
export declare const pageHeaderVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Title block and actions share one row. `items-start` keeps the actions level
|
|
23
|
+
* with the title when a description wraps under it; `flex-wrap` moves the
|
|
24
|
+
* actions below the title on a narrow screen instead of squeezing it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const pageHeaderRowVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
27
|
+
export declare const pageHeaderTitleVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
28
|
+
export declare const pageHeaderMetaVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Capped at the md breakpoint (48rem), a readable measure for a sentence or
|
|
31
|
+
* two. The scale defines no container size between `max` and nothing, and a
|
|
32
|
+
* bracket length is rejected by check:hardcoding, so the token is referenced.
|
|
33
|
+
*/
|
|
34
|
+
export declare const pageHeaderDescriptionVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
35
|
+
export declare const pageHeaderActionsVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
36
|
+
export declare const pageHeaderBackVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
37
|
+
/**
|
|
38
|
+
* The header at the top of a page.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <PageHeader
|
|
42
|
+
* title="Projects"
|
|
43
|
+
* description="Everything your team is working on."
|
|
44
|
+
* meta="updated 14 Oct 2026"
|
|
45
|
+
* actions={<Button>New project</Button>}
|
|
46
|
+
* />
|
|
47
|
+
*/
|
|
48
|
+
export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"header">, "title"> {
|
|
49
|
+
/** The page's title. Rendered as the page's single `<h1>`. */
|
|
50
|
+
title: ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* One or two sentences under the title saying what the page is for.
|
|
53
|
+
* Muted, and capped at a readable measure.
|
|
54
|
+
*/
|
|
55
|
+
description?: ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* The page's primary actions, right-aligned on the title row. They wrap
|
|
58
|
+
* below the title on a narrow screen. Pass buttons or links; the header
|
|
59
|
+
* spaces them and does not style them.
|
|
60
|
+
*/
|
|
61
|
+
actions?: ReactNode;
|
|
62
|
+
/**
|
|
63
|
+
* A short piece of context inline after the title, on the same baseline:
|
|
64
|
+
* a date the figures are as of, a status chip, a count.
|
|
65
|
+
*/
|
|
66
|
+
meta?: ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* A slot above the title for a way back, on a detail page. Supply the link
|
|
69
|
+
* yourself so it uses your router; the header sizes and colours the slot.
|
|
70
|
+
*/
|
|
71
|
+
back?: ReactNode;
|
|
72
|
+
}
|
|
73
|
+
export declare const PageHeader: import("react").ForwardRefExoticComponent<PageHeaderProps & import("react").RefAttributes<HTMLElement>>;
|
|
74
|
+
//#endregion
|
|
75
|
+
//# sourceMappingURL=page-header.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/page-header/page-header.tsx
|
|
7
|
+
/**
|
|
8
|
+
* PageHeader: the top of a page. One title, an optional line saying what the
|
|
9
|
+
* page is for, the page's own actions, a short piece of metadata and an
|
|
10
|
+
* optional way back.
|
|
11
|
+
*
|
|
12
|
+
* Presentational, so there is no React Aria base. It exists because a page
|
|
13
|
+
* title is the one element every screen has, and without a shared component
|
|
14
|
+
* each screen picks its own size, weight, spacing and action placement, so the
|
|
15
|
+
* title moves as a person moves between pages.
|
|
16
|
+
*
|
|
17
|
+
* The title renders the page's single `<h1>`. A page has one, and putting it
|
|
18
|
+
* here means a consumer cannot render two by accident.
|
|
19
|
+
*
|
|
20
|
+
* Every string in the cva blocks below is a STATIC literal. Tailwind scans
|
|
21
|
+
* source text, so a class assembled at runtime is invisible to it and would
|
|
22
|
+
* simply never be emitted.
|
|
23
|
+
*/
|
|
24
|
+
const pageHeaderVariants = cva(["mb-6 block", "box-border font-sans text-foreground"]);
|
|
25
|
+
/**
|
|
26
|
+
* Title block and actions share one row. `items-start` keeps the actions level
|
|
27
|
+
* with the title when a description wraps under it; `flex-wrap` moves the
|
|
28
|
+
* actions below the title on a narrow screen instead of squeezing it.
|
|
29
|
+
*/
|
|
30
|
+
const pageHeaderRowVariants = cva("flex flex-wrap items-start justify-between gap-3");
|
|
31
|
+
const pageHeaderTitleVariants = cva(["font-display text-h3 leading-(--leading-snug) font-semibold", "m-0"]);
|
|
32
|
+
const pageHeaderMetaVariants = cva("text-xs leading-(--leading-normal) text-muted-foreground");
|
|
33
|
+
/**
|
|
34
|
+
* Capped at the md breakpoint (48rem), a readable measure for a sentence or
|
|
35
|
+
* two. The scale defines no container size between `max` and nothing, and a
|
|
36
|
+
* bracket length is rejected by check:hardcoding, so the token is referenced.
|
|
37
|
+
*/
|
|
38
|
+
const pageHeaderDescriptionVariants = cva(["mt-1 mb-0 max-w-(--breakpoint-md)", "text-sm leading-(--leading-normal) text-muted-foreground"]);
|
|
39
|
+
const pageHeaderActionsVariants = cva("flex shrink-0 flex-wrap items-center gap-2");
|
|
40
|
+
const pageHeaderBackVariants = cva("mb-2 text-xs leading-(--leading-normal) text-muted-foreground");
|
|
41
|
+
const PageHeader = forwardRef(function PageHeader({ className, title, description, actions, meta, back, ...props }, ref) {
|
|
42
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
43
|
+
ref,
|
|
44
|
+
...props,
|
|
45
|
+
"data-slot": "page-header",
|
|
46
|
+
className: cn(pageHeaderVariants(), className),
|
|
47
|
+
children: [back === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
48
|
+
"data-slot": "page-header-back",
|
|
49
|
+
className: pageHeaderBackVariants(),
|
|
50
|
+
children: back
|
|
51
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
52
|
+
"data-slot": "page-header-row",
|
|
53
|
+
className: pageHeaderRowVariants(),
|
|
54
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
55
|
+
className: "min-w-0 grow basis-64",
|
|
56
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
57
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1",
|
|
58
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
59
|
+
"data-slot": "page-header-title",
|
|
60
|
+
className: pageHeaderTitleVariants(),
|
|
61
|
+
children: title
|
|
62
|
+
}), meta === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
63
|
+
"data-slot": "page-header-meta",
|
|
64
|
+
className: pageHeaderMetaVariants(),
|
|
65
|
+
children: meta
|
|
66
|
+
})]
|
|
67
|
+
}), description === void 0 ? null : /* @__PURE__ */ jsx("p", {
|
|
68
|
+
"data-slot": "page-header-description",
|
|
69
|
+
className: pageHeaderDescriptionVariants(),
|
|
70
|
+
children: description
|
|
71
|
+
})]
|
|
72
|
+
}), actions === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
73
|
+
"data-slot": "page-header-actions",
|
|
74
|
+
className: pageHeaderActionsVariants(),
|
|
75
|
+
children: actions
|
|
76
|
+
})]
|
|
77
|
+
})]
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//#endregion
|
|
81
|
+
export { PageHeader, pageHeaderActionsVariants, pageHeaderBackVariants, pageHeaderDescriptionVariants, pageHeaderMetaVariants, pageHeaderRowVariants, pageHeaderTitleVariants, pageHeaderVariants };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=page-header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-header.js","names":[],"sources":["../../src/page-header/page-header.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * PageHeader: the top of a page. One title, an optional line saying what the\n * page is for, the page's own actions, a short piece of metadata and an\n * optional way back.\n *\n * Presentational, so there is no React Aria base. It exists because a page\n * title is the one element every screen has, and without a shared component\n * each screen picks its own size, weight, spacing and action placement, so the\n * title moves as a person moves between pages.\n *\n * The title renders the page's single `<h1>`. A page has one, and putting it\n * here means a consumer cannot render two by accident.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const pageHeaderVariants = cva([\n \"mb-6 block\",\n // box-border and font-sans are not boilerplate: the reset in\n // @seekalfred/styles is opt-in, so a consumer may load none, and an\n // unstyled document renders text in a serif.\n \"box-border font-sans text-foreground\",\n]);\n\n/**\n * Title block and actions share one row. `items-start` keeps the actions level\n * with the title when a description wraps under it; `flex-wrap` moves the\n * actions below the title on a narrow screen instead of squeezing it.\n */\nexport const pageHeaderRowVariants = cva(\"flex flex-wrap items-start justify-between gap-3\");\n\nexport const pageHeaderTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-display text-h3 leading-(--leading-snug) font-semibold\",\n // The user-agent <h1> ships a 0.67em block margin and font-size 2em.\n \"m-0\",\n]);\n\nexport const pageHeaderMetaVariants = cva(\n \"text-xs leading-(--leading-normal) text-muted-foreground\",\n);\n\n/**\n * Capped at the md breakpoint (48rem), a readable measure for a sentence or\n * two. The scale defines no container size between `max` and nothing, and a\n * bracket length is rejected by check:hardcoding, so the token is referenced.\n */\nexport const pageHeaderDescriptionVariants = cva([\n \"mt-1 mb-0 max-w-(--breakpoint-md)\",\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n]);\n\nexport const pageHeaderActionsVariants = cva(\"flex shrink-0 flex-wrap items-center gap-2\");\n\nexport const pageHeaderBackVariants = cva(\n \"mb-2 text-xs leading-(--leading-normal) text-muted-foreground\",\n);\n\n/**\n * The header at the top of a page.\n *\n * @example\n * <PageHeader\n * title=\"Projects\"\n * description=\"Everything your team is working on.\"\n * meta=\"updated 14 Oct 2026\"\n * actions={<Button>New project</Button>}\n * />\n */\nexport interface PageHeaderProps\n // The native `title` attribute is omitted so this component can use the\n // name for its heading. A tooltip on a page header is not keyboard\n // reachable, and silently shadowing it with a ReactNode would be a type\n // error waiting for the first consumer who tried both.\n extends Omit<ComponentPropsWithoutRef<\"header\">, \"title\"> {\n /** The page's title. Rendered as the page's single `<h1>`. */\n title: ReactNode;\n /**\n * One or two sentences under the title saying what the page is for.\n * Muted, and capped at a readable measure.\n */\n description?: ReactNode;\n /**\n * The page's primary actions, right-aligned on the title row. They wrap\n * below the title on a narrow screen. Pass buttons or links; the header\n * spaces them and does not style them.\n */\n actions?: ReactNode;\n /**\n * A short piece of context inline after the title, on the same baseline:\n * a date the figures are as of, a status chip, a count.\n */\n meta?: ReactNode;\n /**\n * A slot above the title for a way back, on a detail page. Supply the link\n * yourself so it uses your router; the header sizes and colours the slot.\n */\n back?: ReactNode;\n}\n\nexport const PageHeader = forwardRef<HTMLElement, PageHeaderProps>(function PageHeader(\n { className, title, description, actions, meta, back, ...props },\n ref,\n) {\n return (\n <header\n ref={ref}\n {...props}\n data-slot=\"page-header\"\n className={cn(pageHeaderVariants(), className)}\n >\n {back === undefined ? null : (\n <div data-slot=\"page-header-back\" className={pageHeaderBackVariants()}>\n {back}\n </div>\n )}\n <div data-slot=\"page-header-row\" className={pageHeaderRowVariants()}>\n {/*\n * grow from a 16rem basis, not flex-1. flex-1 is a 0% basis, so the\n * row never wraps: the actions keep their width and the title is\n * crushed into a one-word column beside them on a phone. A real basis\n * is the width the title needs before the actions move below it.\n */}\n <div className=\"min-w-0 grow basis-64\">\n <div className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1\">\n <h1 data-slot=\"page-header-title\" className={pageHeaderTitleVariants()}>\n {title}\n </h1>\n {meta === undefined ? null : (\n <div data-slot=\"page-header-meta\" className={pageHeaderMetaVariants()}>\n {meta}\n </div>\n )}\n </div>\n {description === undefined ? null : (\n <p data-slot=\"page-header-description\" className={pageHeaderDescriptionVariants()}>\n {description}\n </p>\n )}\n </div>\n {actions === undefined ? null : (\n <div data-slot=\"page-header-actions\" className={pageHeaderActionsVariants()}>\n {actions}\n </div>\n )}\n </div>\n </header>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,qBAAqB,IAAI,CACpC,cAIA,sCACF,CAAC;;;;;;AAOD,MAAa,wBAAwB,IAAI,kDAAkD;AAE3F,MAAa,0BAA0B,IAAI,CAGzC,+DAEA,KACF,CAAC;AAED,MAAa,yBAAyB,IACpC,0DACF;;;;;;AAOA,MAAa,gCAAgC,IAAI,CAC/C,qCACA,0DACF,CAAC;AAED,MAAa,4BAA4B,IAAI,4CAA4C;AAEzF,MAAa,yBAAyB,IACpC,+DACF;AA4CA,MAAa,aAAa,WAAyC,SAAS,WAC1E,EAAE,WAAW,OAAO,aAAa,SAAS,MAAM,MAAM,GAAG,SACzD,KACA;CACA,OACE,qBAAC,UAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAJ/C,UAAA,CAMG,SAAS,KAAA,IAAY,OACpB,oBAAC,OAAD;GAAK,aAAU;GAAmB,WAAW,uBAAuB;GACjE,UAAA;EACE,CAAA,GAEP,qBAAC,OAAD;GAAK,aAAU;GAAkB,WAAW,sBAAsB;GAAlE,UAAA,CAOE,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,MAAD;MAAI,aAAU;MAAoB,WAAW,wBAAwB;MAClE,UAAA;KACC,CAAA,GACH,SAAS,KAAA,IAAY,OACpB,oBAAC,OAAD;MAAK,aAAU;MAAmB,WAAW,uBAAuB;MACjE,UAAA;KACE,CAAA,CAEJ;IACJ,CAAA,GAAA,gBAAgB,KAAA,IAAY,OAC3B,oBAAC,KAAD;KAAG,aAAU;KAA0B,WAAW,8BAA8B;KAC7E,UAAA;IACA,CAAA,CAEF;GACJ,CAAA,GAAA,YAAY,KAAA,IAAY,OACvB,oBAAC,OAAD;IAAK,aAAU;IAAsB,WAAW,0BAA0B;IACvE,UAAA;GACE,CAAA,CAEJ;EACC,CAAA,CAAA;;AAEZ,CAAC"}
|