@seekalfred/primitives 0.5.1 → 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.js +10 -4
- 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 +15 -1
- package/dist/alert/alert.js +116 -20
- package/dist/alert/alert.js.map +1 -1
- package/dist/avatar/avatar.d.ts +6 -0
- package/dist/avatar/avatar.js +47 -16
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +11 -4
- package/dist/badge/badge.js +35 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/breadcrumb/breadcrumb.d.ts +0 -27
- package/dist/breadcrumb/breadcrumb.js +20 -6
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/button/button.d.ts +35 -22
- package/dist/button/button.js +177 -34
- 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 +1 -1
- package/dist/calendar/calendar.js +257 -30
- package/dist/calendar/calendar.js.map +1 -1
- package/dist/card/card.d.ts +9 -2
- package/dist/card/card.js +15 -4
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +0 -7
- package/dist/checkbox/checkbox.js +24 -27
- 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 +10 -14
- package/dist/dialog/dialog.js +39 -11
- 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 +22 -0
- package/dist/input/input.js +36 -12
- package/dist/input/input.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 +3 -3
- package/dist/lib/merge-refs.js.map +1 -1
- 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/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 +8 -0
- package/dist/popover/popover.js +3 -3
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.js +46 -5
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.js +7 -6
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.d.ts +10 -0
- package/dist/scroll-area/scroll-area.js +25 -3
- 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.js +79 -21
- package/dist/select/select.js.map +1 -1
- package/dist/sheet/sheet.js +2 -1
- 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 +6 -2
- package/dist/skeleton/skeleton.js.map +1 -1
- package/dist/slider/slider.js +61 -7
- package/dist/slider/slider.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.js +9 -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 +36 -5
- package/dist/table/table.js +112 -11
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.js +142 -6
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/textarea/textarea.d.ts +23 -0
- package/dist/textarea/textarea.js +24 -2
- 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.js +58 -38
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +1 -1
- package/dist/tooltip/tooltip.js +63 -14
- package/dist/tooltip/tooltip.js.map +1 -1
- package/package.json +83 -3
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { Select as Select$1, SelectItem } from "../select/select.js";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { forwardRef, useId } from "react";
|
|
6
|
+
import { Header, ListBox, ListBoxItem, ListBoxSection } from "react-aria-components";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/side-nav/side-nav.tsx
|
|
9
|
+
/**
|
|
10
|
+
* SideNav: the sections of one screen, listed down its side.
|
|
11
|
+
*
|
|
12
|
+
* A row of tabs holds a handful of choices. Past about eight it no longer
|
|
13
|
+
* fits a laptop, hides its ends off the edge and stops being a switch: it is
|
|
14
|
+
* a list of sections. SideNav lists them vertically, grouped under plain
|
|
15
|
+
* headings, and shows the chosen one beside the list.
|
|
16
|
+
*
|
|
17
|
+
* WIDE (its own box at least 48rem: a container query, so it follows the
|
|
18
|
+
* space the page gives it, not the window): the grouped list on the inline
|
|
19
|
+
* start, sticky while the section scrolls. It is React Aria's ListBox with one
|
|
20
|
+
* section per group, so each group is announced by its heading; the up and
|
|
21
|
+
* down arrows move through every section ACROSS the groups and choose as
|
|
22
|
+
* they go (the same automatic activation as Tabs); Home and End jump to the
|
|
23
|
+
* ends; typing a section's first letters jumps to it.
|
|
24
|
+
*
|
|
25
|
+
* NARROW (under 48rem): a column of sections does not fit beside anything on
|
|
26
|
+
* a phone, so the list gives way to one Select above the body.
|
|
27
|
+
*
|
|
28
|
+
* The chosen section's title is the body's `<h2>`, one size for every
|
|
29
|
+
* section, with the screen's description of that section under it -- so no
|
|
30
|
+
* section draws its own title at its own size.
|
|
31
|
+
*
|
|
32
|
+
* WHY NOT `Tabs orientation="vertical"`: React Aria's TabList is a flat
|
|
33
|
+
* collection with no sections, so it cannot carry group headings, and a
|
|
34
|
+
* heading placed among the tabs would be announced as part of a tab's name.
|
|
35
|
+
* A group with no sections is not drawn.
|
|
36
|
+
*/
|
|
37
|
+
const sideNavVariants = cva(["@container/side-nav box-border font-sans text-foreground"]);
|
|
38
|
+
const sideNavMenuVariants = cva(["hidden w-56 shrink-0 box-border", "side-nav-wide:sticky side-nav-wide:top-4 side-nav-wide:block side-nav-wide:max-h-screen side-nav-wide:overflow-y-auto"]);
|
|
39
|
+
const sideNavHeaderVariants = cva(["mb-1 px-2 text-2xs leading-(--leading-normal) font-semibold uppercase tracking-wider text-muted-foreground"]);
|
|
40
|
+
const sideNavItemVariants = cva([
|
|
41
|
+
"flex cursor-pointer items-center rounded-md px-2 py-1 box-border",
|
|
42
|
+
"text-sm leading-(--leading-normal) text-foreground",
|
|
43
|
+
"border-s-2 border-transparent",
|
|
44
|
+
"data-[hovered]:bg-muted",
|
|
45
|
+
"data-[selected]:bg-muted data-[selected]:font-medium data-[selected]:border-foreground",
|
|
46
|
+
"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard",
|
|
47
|
+
"outline-none",
|
|
48
|
+
"data-[focus-visible]:outline-solid",
|
|
49
|
+
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
50
|
+
"data-[focus-visible]:-outline-offset-(--focus-ring-width)",
|
|
51
|
+
"data-[focus-visible]:outline-(--color-ring)"
|
|
52
|
+
]);
|
|
53
|
+
const SideNav = forwardRef(function SideNav({ "aria-label": label, groups, selectedKey, onSelectionChange, selectLabel = "Section", description, children, className }, ref) {
|
|
54
|
+
const shown = groups.filter((g) => g.sections.length > 0);
|
|
55
|
+
const all = shown.flatMap((g) => g.sections);
|
|
56
|
+
const current = all.find((s) => s.id === selectedKey);
|
|
57
|
+
const headingId = useId();
|
|
58
|
+
const choose = (key) => {
|
|
59
|
+
if (key != null && String(key) !== selectedKey) onSelectionChange(String(key));
|
|
60
|
+
};
|
|
61
|
+
const chooseFromList = (keys) => {
|
|
62
|
+
if (keys === "all") return;
|
|
63
|
+
choose(Array.from(keys)[0] ?? null);
|
|
64
|
+
};
|
|
65
|
+
return /* @__PURE__ */ jsx("div", {
|
|
66
|
+
ref,
|
|
67
|
+
"data-slot": "side-nav",
|
|
68
|
+
className: cn(sideNavVariants(), className),
|
|
69
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
70
|
+
"data-slot": "side-nav-layout",
|
|
71
|
+
className: "side-nav-wide:flex side-nav-wide:items-start side-nav-wide:gap-8",
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ jsx("nav", {
|
|
74
|
+
"aria-label": label,
|
|
75
|
+
"data-slot": "side-nav-menu",
|
|
76
|
+
className: sideNavMenuVariants(),
|
|
77
|
+
children: /* @__PURE__ */ jsx(ListBox, {
|
|
78
|
+
"aria-label": label,
|
|
79
|
+
selectionMode: "single",
|
|
80
|
+
selectionBehavior: "replace",
|
|
81
|
+
disallowEmptySelection: true,
|
|
82
|
+
selectedKeys: current ? [current.id] : [],
|
|
83
|
+
onSelectionChange: chooseFromList,
|
|
84
|
+
className: "flex flex-col gap-3 outline-none",
|
|
85
|
+
children: shown.map((group) => /* @__PURE__ */ jsxs(ListBoxSection, {
|
|
86
|
+
id: group.label,
|
|
87
|
+
className: "flex flex-col gap-0.5",
|
|
88
|
+
children: [/* @__PURE__ */ jsx(Header, {
|
|
89
|
+
"data-slot": "side-nav-header",
|
|
90
|
+
className: sideNavHeaderVariants(),
|
|
91
|
+
children: group.label
|
|
92
|
+
}), group.sections.map((section) => /* @__PURE__ */ jsx(ListBoxItem, {
|
|
93
|
+
id: section.id,
|
|
94
|
+
textValue: section.label,
|
|
95
|
+
"data-slot": "side-nav-item",
|
|
96
|
+
className: sideNavItemVariants(),
|
|
97
|
+
children: section.label
|
|
98
|
+
}, section.id))]
|
|
99
|
+
}, group.label))
|
|
100
|
+
})
|
|
101
|
+
}),
|
|
102
|
+
/* @__PURE__ */ jsx("div", {
|
|
103
|
+
"data-slot": "side-nav-select",
|
|
104
|
+
className: "mb-4 side-nav-wide:hidden",
|
|
105
|
+
children: /* @__PURE__ */ jsx(Select$1, {
|
|
106
|
+
label: selectLabel,
|
|
107
|
+
selectedKey: current?.id ?? null,
|
|
108
|
+
onSelectionChange: choose,
|
|
109
|
+
children: all.map((section) => /* @__PURE__ */ jsx(SelectItem, {
|
|
110
|
+
id: section.id,
|
|
111
|
+
children: section.label
|
|
112
|
+
}, section.id))
|
|
113
|
+
})
|
|
114
|
+
}),
|
|
115
|
+
/* @__PURE__ */ jsxs("section", {
|
|
116
|
+
"aria-labelledby": headingId,
|
|
117
|
+
"data-slot": "side-nav-body",
|
|
118
|
+
className: "min-w-0 flex-1",
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx("h2", {
|
|
121
|
+
id: headingId,
|
|
122
|
+
className: "font-display text-h4 leading-(--leading-snug) font-semibold text-foreground",
|
|
123
|
+
children: current?.label
|
|
124
|
+
}),
|
|
125
|
+
description == null ? null : /* @__PURE__ */ jsx("p", {
|
|
126
|
+
className: "mt-1 max-w-3xl text-sm leading-(--leading-normal) text-muted-foreground",
|
|
127
|
+
children: description
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx("div", {
|
|
130
|
+
className: "mt-4",
|
|
131
|
+
children
|
|
132
|
+
})
|
|
133
|
+
]
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
//#endregion
|
|
140
|
+
export { SideNav, sideNavHeaderVariants, sideNavItemVariants, sideNavMenuVariants, sideNavVariants };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=side-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"side-nav.js","names":["AriaListBox","AriaListBoxSection","AriaHeader","AriaListBoxItem","Select"],"sources":["../../src/side-nav/side-nav.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useId, type ReactNode } from \"react\";\nimport {\n Header as AriaHeader,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n ListBoxSection as AriaListBoxSection,\n type Key,\n type Selection,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { Select, SelectItem } from \"../select/select.js\";\n\n/**\n * SideNav: the sections of one screen, listed down its side.\n *\n * A row of tabs holds a handful of choices. Past about eight it no longer\n * fits a laptop, hides its ends off the edge and stops being a switch: it is\n * a list of sections. SideNav lists them vertically, grouped under plain\n * headings, and shows the chosen one beside the list.\n *\n * WIDE (its own box at least 48rem: a container query, so it follows the\n * space the page gives it, not the window): the grouped list on the inline\n * start, sticky while the section scrolls. It is React Aria's ListBox with one\n * section per group, so each group is announced by its heading; the up and\n * down arrows move through every section ACROSS the groups and choose as\n * they go (the same automatic activation as Tabs); Home and End jump to the\n * ends; typing a section's first letters jumps to it.\n *\n * NARROW (under 48rem): a column of sections does not fit beside anything on\n * a phone, so the list gives way to one Select above the body.\n *\n * The chosen section's title is the body's `<h2>`, one size for every\n * section, with the screen's description of that section under it -- so no\n * section draws its own title at its own size.\n *\n * WHY NOT `Tabs orientation=\"vertical\"`: React Aria's TabList is a flat\n * collection with no sections, so it cannot carry group headings, and a\n * heading placed among the tabs would be announced as part of a tab's name.\n * A group with no sections is not drawn.\n */\n\nexport const sideNavVariants = cva([\"@container/side-nav box-border font-sans text-foreground\"]);\n\nexport const sideNavMenuVariants = cva([\n \"hidden w-56 shrink-0 box-border\",\n \"side-nav-wide:sticky side-nav-wide:top-4 side-nav-wide:block side-nav-wide:max-h-screen side-nav-wide:overflow-y-auto\",\n]);\n\nexport const sideNavHeaderVariants = cva([\n \"mb-1 px-2 text-2xs leading-(--leading-normal) font-semibold uppercase tracking-wider text-muted-foreground\",\n]);\n\nexport const sideNavItemVariants = cva([\n \"flex cursor-pointer items-center rounded-md px-2 py-1 box-border\",\n \"text-sm leading-(--leading-normal) text-foreground\",\n // A bar on the inline start marks the chosen section. The fill alone\n // (muted on background) is 1.13:1, no boundary anyone can see; the bar\n // in the foreground colour is what clears WCAG 1.4.11. Transparent at\n // rest so choosing a section does not move its text.\n \"border-s-2 border-transparent\",\n \"data-[hovered]:bg-muted\",\n \"data-[selected]:bg-muted data-[selected]:font-medium data-[selected]:border-foreground\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n // Inset: the menu scrolls on a short window, which would clip a ring\n // drawn outside the item.\n \"data-[focus-visible]:-outline-offset-(--focus-ring-width)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n]);\n\n/** One section of the screen. */\nexport interface SideNavSection {\n /** The section's key, reported by `onSelectionChange`. */\n id: string;\n /** Its name in the menu, and the body's heading while it is chosen. */\n label: string;\n}\n\n/** A heading and the sections under it. */\nexport interface SideNavGroup {\n /** The plain heading the sections sit under. */\n label: string;\n /** The group's sections, in order. A group with none is not drawn. */\n sections: readonly SideNavSection[];\n}\n\nexport interface SideNavProps {\n /** What the menu is for. Names the navigation landmark and the list. */\n \"aria-label\": string;\n /** The sections, grouped. */\n groups: readonly SideNavGroup[];\n /** The chosen section's `id`. Controlled: the screen owns it (often in the URL). */\n selectedKey: string;\n /** Called with a section's `id` when a different one is chosen. */\n onSelectionChange: (key: string) => void;\n /**\n * The visible label of the Select that replaces the menu under 48rem wide.\n * @default \"Section\"\n */\n selectLabel?: string;\n /** One or two sentences under the chosen section's title. */\n description?: ReactNode;\n /** The chosen section's body. */\n children?: ReactNode;\n /** Extra classes for the root. Merged last. */\n className?: string;\n}\n\nexport const SideNav = forwardRef<HTMLDivElement, SideNavProps>(function SideNav(\n {\n \"aria-label\": label,\n groups,\n selectedKey,\n onSelectionChange,\n selectLabel = \"Section\",\n description,\n children,\n className,\n },\n ref,\n) {\n const shown = groups.filter((g) => g.sections.length > 0);\n const all = shown.flatMap((g) => g.sections);\n const current = all.find((s) => s.id === selectedKey);\n const headingId = useId();\n\n const choose = (key: Key | null) => {\n if (key != null && String(key) !== selectedKey) onSelectionChange(String(key));\n };\n const chooseFromList = (keys: Selection) => {\n if (keys === \"all\") return;\n choose(Array.from(keys)[0] ?? null);\n };\n\n return (\n <div ref={ref} data-slot=\"side-nav\" className={cn(sideNavVariants(), className)}>\n <div\n data-slot=\"side-nav-layout\"\n className=\"side-nav-wide:flex side-nav-wide:items-start side-nav-wide:gap-8\"\n >\n <nav aria-label={label} data-slot=\"side-nav-menu\" className={sideNavMenuVariants()}>\n <AriaListBox\n aria-label={label}\n selectionMode=\"single\"\n selectionBehavior=\"replace\"\n disallowEmptySelection\n selectedKeys={current ? [current.id] : []}\n onSelectionChange={chooseFromList}\n className=\"flex flex-col gap-3 outline-none\"\n >\n {shown.map((group) => (\n <AriaListBoxSection\n key={group.label}\n id={group.label}\n className=\"flex flex-col gap-0.5\"\n >\n <AriaHeader data-slot=\"side-nav-header\" className={sideNavHeaderVariants()}>\n {group.label}\n </AriaHeader>\n {group.sections.map((section) => (\n <AriaListBoxItem\n key={section.id}\n id={section.id}\n textValue={section.label}\n data-slot=\"side-nav-item\"\n className={sideNavItemVariants()}\n >\n {section.label}\n </AriaListBoxItem>\n ))}\n </AriaListBoxSection>\n ))}\n </AriaListBox>\n </nav>\n\n <div data-slot=\"side-nav-select\" className=\"mb-4 side-nav-wide:hidden\">\n <Select label={selectLabel} selectedKey={current?.id ?? null} onSelectionChange={choose}>\n {all.map((section) => (\n <SelectItem key={section.id} id={section.id}>\n {section.label}\n </SelectItem>\n ))}\n </Select>\n </div>\n\n <section aria-labelledby={headingId} data-slot=\"side-nav-body\" className=\"min-w-0 flex-1\">\n <h2\n id={headingId}\n className=\"font-display text-h4 leading-(--leading-snug) font-semibold text-foreground\"\n >\n {current?.label}\n </h2>\n {description == null ? null : (\n <p className=\"mt-1 max-w-3xl text-sm leading-(--leading-normal) text-muted-foreground\">\n {description}\n </p>\n )}\n <div className=\"mt-4\">{children}</div>\n </section>\n </div>\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,MAAa,kBAAkB,IAAI,CAAC,0DAA0D,CAAC;AAE/F,MAAa,sBAAsB,IAAI,CACrC,mCACA,uHACF,CAAC;AAED,MAAa,wBAAwB,IAAI,CACvC,4GACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CACrC;CACA;CAKA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;AACF,CAAC;AAwCD,MAAa,UAAU,WAAyC,SAAS,QACvE,EACE,cAAc,OACd,QACA,aACA,mBACA,cAAc,WACd,aACA,UACA,aAEF,KACA;CACA,MAAM,QAAQ,OAAO,QAAQ,MAAM,EAAE,SAAS,SAAS,CAAC;CACxD,MAAM,MAAM,MAAM,SAAS,MAAM,EAAE,QAAQ;CAC3C,MAAM,UAAU,IAAI,MAAM,MAAM,EAAE,OAAO,WAAW;CACpD,MAAM,YAAY,MAAM;CAExB,MAAM,UAAU,QAAoB;EAClC,IAAI,OAAO,QAAQ,OAAO,GAAG,MAAM,aAAa,kBAAkB,OAAO,GAAG,CAAC;CAC/E;CACA,MAAM,kBAAkB,SAAoB;EAC1C,IAAI,SAAS,OAAO;EACpB,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,MAAM,IAAI;CACpC;CAEA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAW,WAAW,GAAG,gBAAgB,GAAG,SAAS;EAC5E,UAAA,qBAAC,OAAD;GACE,aAAU;GACV,WAAU;GAFZ,UAAA;IAIE,oBAAC,OAAD;KAAK,cAAY;KAAO,aAAU;KAAgB,WAAW,oBAAoB;KAC/E,UAAA,oBAACA,SAAD;MACE,cAAY;MACZ,eAAc;MACd,mBAAkB;MAClB,wBAAA;MACA,cAAc,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC;MACxC,mBAAmB;MACnB,WAAU;MAET,UAAA,MAAM,KAAK,UACV,qBAACC,gBAAD;OAEE,IAAI,MAAM;OACV,WAAU;OAHZ,UAAA,CAKE,oBAACC,QAAD;QAAY,aAAU;QAAkB,WAAW,sBAAsB;QACtE,UAAA,MAAM;OACG,CAAA,GACX,MAAM,SAAS,KAAK,YACnB,oBAACC,aAAD;QAEE,IAAI,QAAQ;QACZ,WAAW,QAAQ;QACnB,aAAU;QACV,WAAW,oBAAoB;QAE9B,UAAA,QAAQ;OACM,GAPV,QAAQ,EAOE,CAClB,CACiB;MAlBb,GAAA,MAAM,KAkBO,CACrB;KACU,CAAA;IACV,CAAA;IAEL,oBAAC,OAAD;KAAK,aAAU;KAAkB,WAAU;KACzC,UAAA,oBAACC,UAAD;MAAQ,OAAO;MAAa,aAAa,SAAS,MAAM;MAAM,mBAAmB;MAC9E,UAAA,IAAI,KAAK,YACR,oBAAC,YAAD;OAA6B,IAAI,QAAQ;OACtC,UAAA,QAAQ;MACC,GAFK,QAAQ,EAEb,CACb;KACK,CAAA;IACL,CAAA;IAEL,qBAAC,WAAD;KAAS,mBAAiB;KAAW,aAAU;KAAgB,WAAU;KAAzE,UAAA;MACE,oBAAC,MAAD;OACE,IAAI;OACJ,WAAU;OAET,UAAA,SAAS;MACR,CAAA;MACH,eAAe,OAAO,OACrB,oBAAC,KAAD;OAAG,WAAU;OACV,UAAA;MACA,CAAA;MAEL,oBAAC,OAAD;OAAK,WAAU;OAAQ;MAAc,CAAA;KAC9B;;GACN;;CACF,CAAA;AAET,CAAC"}
|
|
@@ -27,7 +27,13 @@ export declare const skeletonVariants: (props?: import("class-variance-authority
|
|
|
27
27
|
* @example
|
|
28
28
|
* <Skeleton className="h-4 w-32" />
|
|
29
29
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
30
|
+
export interface SkeletonProps extends ComponentPropsWithoutRef<"div"> {
|
|
31
|
+
/**
|
|
32
|
+
* Position among stacked skeletons. Each block starts its pulse 90ms after
|
|
33
|
+
* the one above it, so a list dims in a wave instead of all at once.
|
|
34
|
+
*/
|
|
35
|
+
index?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare const Skeleton: import("react").ForwardRefExoticComponent<SkeletonProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
32
38
|
//#endregion
|
|
33
39
|
//# sourceMappingURL=skeleton.d.ts.map
|
|
@@ -26,13 +26,17 @@ import { jsx } from "react/jsx-runtime";
|
|
|
26
26
|
*/
|
|
27
27
|
const skeletonVariants = cva([
|
|
28
28
|
"block bg-muted rounded-md",
|
|
29
|
-
"motion-safe:animate-pulse",
|
|
29
|
+
"motion-safe:animate-skeleton-pulse",
|
|
30
30
|
"box-border"
|
|
31
31
|
]);
|
|
32
|
-
const Skeleton = forwardRef(function Skeleton({ className, ...props }, ref) {
|
|
32
|
+
const Skeleton = forwardRef(function Skeleton({ className, index, style, ...props }, ref) {
|
|
33
33
|
return /* @__PURE__ */ jsx("div", {
|
|
34
34
|
ref,
|
|
35
35
|
...props,
|
|
36
|
+
style: index ? {
|
|
37
|
+
animationDelay: `${index * 90}ms`,
|
|
38
|
+
...style
|
|
39
|
+
} : style,
|
|
36
40
|
"aria-hidden": "true",
|
|
37
41
|
"data-slot": "skeleton",
|
|
38
42
|
className: cn(skeletonVariants(), className)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skeleton.js","names":[],"sources":["../../src/skeleton/skeleton.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ComponentPropsWithoutRef } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Skeleton, ported from the legacy shadcn component.\n *\n * Census: 2 files, 33 uses, and every single one passes `className` and\n * nothing else -- the size always comes from the call site. So there is no\n * variant axis to invent here; the component is a surface and a pulse.\n *\n * THE PULSE DID NOT EXIST. `animate-pulse` names `--animate-pulse`, which\n * this repo does not define: it imports only `tailwindcss/utilities.css`,\n * never Tailwind's default theme, so both the custom property and the\n * `@keyframes pulse` are absent and the utility emits NO RULE AT ALL. The\n * class was present, tsc and ESLint were happy, and nothing moved. Same\n * failure mode as the Dialog's `max-w-lg`. This port adds the token\n * (`scale.json` -> `--animate-pulse`) and the keyframes\n * (`@seekalfred/styles/motion.css`).\n *\n * `bg-accent` becomes `bg-muted`: in the legacy theme `--accent` is\n * character-for-character `--muted` in both themes, so this is a rename to\n * the role Alfred actually has, not a colour change.\n */\nexport const skeletonVariants = cva([\n \"block bg-muted rounded-md\",\n // motion-safe: a full-screen page of these pulsing is exactly what
|
|
1
|
+
{"version":3,"file":"skeleton.js","names":[],"sources":["../../src/skeleton/skeleton.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ComponentPropsWithoutRef } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Skeleton, ported from the legacy shadcn component.\n *\n * Census: 2 files, 33 uses, and every single one passes `className` and\n * nothing else -- the size always comes from the call site. So there is no\n * variant axis to invent here; the component is a surface and a pulse.\n *\n * THE PULSE DID NOT EXIST. `animate-pulse` names `--animate-pulse`, which\n * this repo does not define: it imports only `tailwindcss/utilities.css`,\n * never Tailwind's default theme, so both the custom property and the\n * `@keyframes pulse` are absent and the utility emits NO RULE AT ALL. The\n * class was present, tsc and ESLint were happy, and nothing moved. Same\n * failure mode as the Dialog's `max-w-lg`. This port adds the token\n * (`scale.json` -> `--animate-pulse`) and the keyframes\n * (`@seekalfred/styles/motion.css`).\n *\n * `bg-accent` becomes `bg-muted`: in the legacy theme `--accent` is\n * character-for-character `--muted` in both themes, so this is a rename to\n * the role Alfred actually has, not a colour change.\n */\nexport const skeletonVariants = cva([\n \"block bg-muted rounded-md\",\n // A finite pulse (11 beats, then rest). motion-safe: a full-screen page of\n // these pulsing is exactly what prefers-reduced-motion exists for. Without the animation it is still a\n // visible placeholder, which is the whole job.\n \"motion-safe:animate-skeleton-pulse\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none,\n // and every call site sizes this through className -- content-box would\n // add any padding they set on top of the height they asked for.\n \"box-border\",\n]);\n\n/**\n * A loading placeholder.\n *\n * @example\n * <Skeleton className=\"h-4 w-32\" />\n */\nexport interface SkeletonProps extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * Position among stacked skeletons. Each block starts its pulse 90ms after\n * the one above it, so a list dims in a wave instead of all at once.\n */\n index?: number;\n}\n\nexport const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(function Skeleton(\n { className, index, style, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n style={index ? { animationDelay: `${index * 90}ms`, ...style } : style}\n // aria-hidden: a placeholder is not content. Thirty-three empty boxes\n // announced while a page loads is noise, and the loading STATE belongs\n // on the region being replaced (aria-busy), which only the consumer\n // can place.\n aria-hidden=\"true\"\n data-slot=\"skeleton\"\n className={cn(skeletonVariants(), className)}\n />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,mBAAmB,IAAI;CAClC;CAIA;CAIA;AACF,CAAC;AAgBD,MAAa,WAAW,WAA0C,SAAS,SACzE,EAAE,WAAW,OAAO,OAAO,GAAG,SAC9B,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,OAAO,QAAQ;GAAE,gBAAgB,GAAG,QAAQ,GAAG;GAAK,GAAG;EAAM,IAAI;EAKjE,eAAY;EACZ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;CAC5C,CAAA;AAEL,CAAC"}
|
package/dist/slider/slider.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn, cnAll } from "../lib/cn.js";
|
|
3
|
+
import { SwapText } from "../lib/swap-text.js";
|
|
3
4
|
import { cva } from "class-variance-authority";
|
|
4
|
-
import { forwardRef } from "react";
|
|
5
|
+
import { forwardRef, useState } from "react";
|
|
5
6
|
import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from "react-aria-components";
|
|
6
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/slider/slider.tsx
|
|
8
9
|
/**
|
|
9
10
|
* Slider, ported from the legacy shadcn/Radix component.
|
|
@@ -37,13 +38,18 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
37
38
|
* slider looked exactly like an enabled one.
|
|
38
39
|
*/
|
|
39
40
|
const sliderVariants = cva(["flex w-full flex-col gap-2 box-border font-sans"]);
|
|
40
|
-
const sliderTrackVariants = cva([
|
|
41
|
+
const sliderTrackVariants = cva([
|
|
42
|
+
"relative flex w-full items-center",
|
|
43
|
+
"h-6",
|
|
44
|
+
"slider-track"
|
|
45
|
+
]);
|
|
41
46
|
const sliderBarVariants = cva(["absolute h-2 w-full rounded-pill box-border", "bg-muted border-(length:--border-width) border-border"]);
|
|
42
|
-
const sliderFillVariants = cva(["absolute h-2 rounded-pill bg-primary-strong"]);
|
|
47
|
+
const sliderFillVariants = cva(["absolute h-2 rounded-pill bg-primary-strong", "slider-fill"]);
|
|
43
48
|
const sliderThumbVariants = cva([
|
|
44
49
|
"size-6 rounded-full box-border",
|
|
45
50
|
"bg-background border-(length:--border-width) border-border shadow-surface",
|
|
46
51
|
"cursor-grab data-[dragging]:cursor-grabbing",
|
|
52
|
+
"slider-thumb",
|
|
47
53
|
"outline-none",
|
|
48
54
|
"data-[focus-visible]:outline-solid",
|
|
49
55
|
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
@@ -51,6 +57,35 @@ const sliderThumbVariants = cva([
|
|
|
51
57
|
"data-[focus-visible]:outline-(--color-ring)",
|
|
52
58
|
"data-[disabled]:cursor-not-allowed"
|
|
53
59
|
]);
|
|
60
|
+
/**
|
|
61
|
+
* Text that rolls when its value changes: a larger value rises in from below, a
|
|
62
|
+
* smaller one drops in from above. The caller's own text stays for a screen
|
|
63
|
+
* reader; the copy the eye sees is aria-hidden.
|
|
64
|
+
*/
|
|
65
|
+
function RollingText({ value, text }) {
|
|
66
|
+
const [trail, setTrail] = useState({
|
|
67
|
+
value,
|
|
68
|
+
dir: 1
|
|
69
|
+
});
|
|
70
|
+
if (trail.value !== value) setTrail({
|
|
71
|
+
value,
|
|
72
|
+
dir: value > trail.value ? 1 : -1
|
|
73
|
+
});
|
|
74
|
+
const dir = trail.value === value ? trail.dir : value > trail.value ? 1 : -1;
|
|
75
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
76
|
+
className: "sr-only",
|
|
77
|
+
children: text
|
|
78
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
79
|
+
"aria-hidden": "true",
|
|
80
|
+
children: /* @__PURE__ */ jsx(SwapText, {
|
|
81
|
+
swapKey: text,
|
|
82
|
+
dir,
|
|
83
|
+
motion: "roll",
|
|
84
|
+
className: "inline-block",
|
|
85
|
+
children: text
|
|
86
|
+
})
|
|
87
|
+
})] });
|
|
88
|
+
}
|
|
54
89
|
const Slider$1 = forwardRef(function Slider$2({ label, labelHidden, showValue = true, className, ...props }, ref) {
|
|
55
90
|
return /* @__PURE__ */ jsxs(Slider, {
|
|
56
91
|
ref,
|
|
@@ -65,12 +100,16 @@ const Slider$1 = forwardRef(function Slider$2({ label, labelHidden, showValue =
|
|
|
65
100
|
children: label
|
|
66
101
|
}), showValue ? /* @__PURE__ */ jsx(SliderOutput, {
|
|
67
102
|
"data-slot": "slider-output",
|
|
68
|
-
className: "text-sm leading-(--leading-normal) text-muted-foreground"
|
|
103
|
+
className: "text-sm leading-(--leading-normal) text-muted-foreground",
|
|
104
|
+
children: ({ state }) => /* @__PURE__ */ jsx(RollingText, {
|
|
105
|
+
value: state.getThumbValue(0),
|
|
106
|
+
text: state.getThumbValueLabel(0)
|
|
107
|
+
})
|
|
69
108
|
}) : null]
|
|
70
109
|
}), /* @__PURE__ */ jsx(SliderTrack, {
|
|
71
110
|
"data-slot": "slider-track",
|
|
72
111
|
className: sliderTrackVariants(),
|
|
73
|
-
children: ({ state }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
112
|
+
children: ({ state }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
74
113
|
/* @__PURE__ */ jsx("div", {
|
|
75
114
|
"data-slot": "slider-bar",
|
|
76
115
|
className: sliderBarVariants()
|
|
@@ -78,11 +117,26 @@ const Slider$1 = forwardRef(function Slider$2({ label, labelHidden, showValue =
|
|
|
78
117
|
/* @__PURE__ */ jsx("div", {
|
|
79
118
|
"data-slot": "slider-fill",
|
|
80
119
|
className: sliderFillVariants(),
|
|
120
|
+
"data-dragging": state.isThumbDragging(0) || void 0,
|
|
81
121
|
style: { width: `${state.getThumbPercent(0) * 100}%` }
|
|
82
122
|
}),
|
|
83
123
|
/* @__PURE__ */ jsx(SliderThumb, {
|
|
84
124
|
"data-slot": "slider-thumb",
|
|
85
|
-
className: sliderThumbVariants()
|
|
125
|
+
className: sliderThumbVariants(),
|
|
126
|
+
children: ({ isDragging, isFocusVisible, state: thumbState }) => /* @__PURE__ */ jsx("span", {
|
|
127
|
+
"aria-hidden": "true",
|
|
128
|
+
className: "slider-bubble-anchor pointer-events-none absolute bottom-full left-1/2 mb-2",
|
|
129
|
+
style: { "--at": thumbState.getThumbPercent(0) * 100 },
|
|
130
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
131
|
+
"data-slot": "slider-bubble",
|
|
132
|
+
"data-shown": isDragging || isFocusVisible || void 0,
|
|
133
|
+
className: "slider-bubble block whitespace-nowrap rounded-pill bg-foreground px-3 py-1 text-sm leading-none font-medium text-background shadow-surface",
|
|
134
|
+
children: /* @__PURE__ */ jsx(RollingText, {
|
|
135
|
+
value: thumbState.getThumbValue(0),
|
|
136
|
+
text: thumbState.getThumbValueLabel(0)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
86
140
|
})
|
|
87
141
|
] })
|
|
88
142
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slider.js","names":["Slider","AriaSlider","AriaLabel","AriaSliderOutput","AriaSliderTrack","AriaSliderThumb"],"sources":["../../src/slider/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n Slider as AriaSlider,\n SliderOutput as AriaSliderOutput,\n SliderThumb as AriaSliderThumb,\n SliderTrack as AriaSliderTrack,\n type SliderProps as AriaSliderProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Slider, ported from the legacy shadcn/Radix component.\n *\n * Census: 4 call sites, all single-thumb, none of them named. Radix only\n * generates thumb labels at two or more thumbs, so all four are announced\n * as an unnamed slider -- `AudioSettings.tsx` even renders a\n * `<Label>Input Volume</Label>` directly above one, with no `htmlFor` and\n * no `id`, so it is associated with nothing at all.\n *\n * `label` IS REQUIRED here, the same answer Progress and Dialog give.\n *\n * THE PALETTE DELIBERATELY DOES NOT MATCH SWITCH, and the reason is worth\n * stating because the two look like the same problem. A Switch has no\n * fill: its track is one colour and the thumb tells you the state, so\n * `border` unchecked and `primary-strong` checked works. A Slider has a\n * filled portion whose LENGTH is the value -- and `primary-strong` on a\n * `border` track is 1.01:1. The two halves would be the same grey and the\n * value would be invisible to anyone not reading the thumb position.\n *\n * So the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`: fill against track 4.61:1, track against page 5.25:1,\n * thumb against fill 5.20:1, thumb edge against track 4.65:1. All four\n * pairs are in the build-failing contrast contract.\n *\n * Three legacy defects go with it: the focus ring was `ring-1`, a 1px\n * box-shadow that forced-colors discards entirely; the thumb border was\n * `border-primary/50`, an alpha edge with no measurable contrast; and the\n * disabled styling used `disabled:`, a pseudo-class that only applies to\n * form elements -- Radix renders a `<span role=\"slider\">`, so a disabled\n * slider looked exactly like an enabled one.\n */\nexport const sliderVariants = cva([\"flex w-full flex-col gap-2 box-border font-sans\"]);\n\nexport const sliderTrackVariants = cva([\n \"relative flex w-full items-center\",\n // h-6 is the TRACK ROW, not the visible bar -- it gives the 24px\n // pointer target WCAG 2.5.8 wants. The bar itself is drawn inside.\n \"h-6\",\n]);\n\nexport const sliderBarVariants = cva([\n \"absolute h-2 w-full rounded-pill box-border\",\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const sliderFillVariants = cva([\"absolute h-2 rounded-pill bg-primary-strong\"]);\n\nexport const sliderThumbVariants = cva([\n // size-6 is 24px: WCAG 2.5.8 target size. The legacy thumb was 16px at\n // all four call sites, and one of them crams the whole control into 80px.\n \"size-6 rounded-full box-border\",\n \"bg-background border-(length:--border-width) border-border shadow-surface\",\n \"cursor-grab data-[dragging]:cursor-grabbing\",\n // A real outline, not the legacy ring-1 box-shadow.\n \"outline-none\",\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 // data-[disabled], NOT disabled:. The legacy used the CSS :disabled\n // pseudo-class, which applies only to form elements -- React Aria and\n // Radix both render a span, so that styling never fired.\n \"data-[disabled]:cursor-not-allowed\",\n]);\n\n/**\n * A value along a range.\n *\n * @example\n * <Slider label=\"Input volume\" defaultValue={40} />\n * <Slider label=\"Playback position\" value={t} onChange={seek} formatOptions={{ style: \"percent\" }} />\n */\nexport interface SliderProps\n extends Omit<AriaSliderProps<number>, \"className\" | \"style\" | \"children\"> {\n /**\n * What the slider controls, and its accessible name.\n *\n * **Required.** All four legacy call sites are unnamed sliders.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /**\n * Show the current value as text beside the label.\n *\n * On by default. It is the text equivalent of the fill length, which\n * matters because in greyscale the filled and unfilled halves are\n * distinguishable only by the thumb.\n */\n showValue?: boolean;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Slider = forwardRef<HTMLDivElement, SliderProps>(function Slider(\n { label, labelHidden, showValue = true, className, ...props },\n ref,\n) {\n return (\n <AriaSlider\n ref={ref}\n {...props}\n data-slot=\"slider\"\n className={cn(sliderVariants(), className)}\n >\n <div className={cnAll(\"flex items-center justify-between gap-2\", labelHidden && \"sr-only\")}>\n <AriaLabel\n data-slot=\"slider-label\"\n className=\"text-sm leading-(--leading-normal) text-foreground\"\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <AriaSliderOutput\n data-slot=\"slider-output\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground\"\n />\n ) : null}\n </div>\n\n <AriaSliderTrack data-slot=\"slider-track\" className={sliderTrackVariants()}>\n {({ state }) => (\n <>\n <div data-slot=\"slider-bar\" className={sliderBarVariants()} />\n <div\n data-slot=\"slider-fill\"\n className={sliderFillVariants()}\n style={{ width: `${state.getThumbPercent(0) * 100}%` }}\n />\n <AriaSliderThumb data-slot=\"slider-thumb\" className={sliderThumbVariants()} />\n </>\n )}\n </AriaSliderTrack>\n </AriaSlider>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,iBAAiB,IAAI,CAAC,iDAAiD,CAAC;AAErF,MAAa,sBAAsB,IAAI,CACrC,qCAGA,KACF,CAAC;AAED,MAAa,oBAAoB,IAAI,CACnC,+CACA,uDACF,CAAC;AAED,MAAa,qBAAqB,IAAI,CAAC,6CAA6C,CAAC;AAErF,MAAa,sBAAsB,IAAI;CAGrC;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAIA;AACF,CAAC;AA+BD,MAAaA,WAAS,WAAwC,SAASA,SACrE,EAAE,OAAO,aAAa,YAAY,MAAM,WAAW,GAAG,SACtD,KACA;CACA,OACE,qBAACC,QAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,eAAe,GAAG,SAAS;EAJ3C,UAAA,CAME,qBAAC,OAAD;GAAK,WAAW,MAAM,2CAA2C,eAAe,SAAS;GAAzF,UAAA,CACE,oBAACC,OAAD;IACE,aAAU;IACV,WAAU;IAET,UAAA;GACQ,CAAA,GACV,YACC,oBAACC,cAAD;IACE,aAAU;IACV,WAAU;GACX,CAAA,IACC,IACD;EAEL,CAAA,GAAA,oBAACC,aAAD;GAAiB,aAAU;GAAe,WAAW,oBAAoB;GACrE,WAAA,EAAE,YACF,qBAAA,UAAA,EAAA,UAAA;IACE,oBAAC,OAAD;KAAK,aAAU;KAAa,WAAW,kBAAkB;IAAI,CAAA;IAC7D,oBAAC,OAAD;KACE,aAAU;KACV,WAAW,mBAAmB;KAC9B,OAAO,EAAE,OAAO,GAAG,MAAM,gBAAgB,CAAC,IAAI,IAAI,GAAG;IACtD,CAAA;IACD,oBAACC,aAAD;KAAiB,aAAU;KAAe,WAAW,oBAAoB;IAAI,CAAA;GAC7E,EAAA,CAAA;EAEW,CAAA,CACP;;AAEhB,CAAC"}
|
|
1
|
+
{"version":3,"file":"slider.js","names":["Slider","AriaSlider","AriaLabel","AriaSliderOutput","AriaSliderTrack","AriaSliderThumb"],"sources":["../../src/slider/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n Slider as AriaSlider,\n SliderOutput as AriaSliderOutput,\n SliderThumb as AriaSliderThumb,\n SliderTrack as AriaSliderTrack,\n type SliderProps as AriaSliderProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Slider, ported from the legacy shadcn/Radix component.\n *\n * Census: 4 call sites, all single-thumb, none of them named. Radix only\n * generates thumb labels at two or more thumbs, so all four are announced\n * as an unnamed slider -- `AudioSettings.tsx` even renders a\n * `<Label>Input Volume</Label>` directly above one, with no `htmlFor` and\n * no `id`, so it is associated with nothing at all.\n *\n * `label` IS REQUIRED here, the same answer Progress and Dialog give.\n *\n * THE PALETTE DELIBERATELY DOES NOT MATCH SWITCH, and the reason is worth\n * stating because the two look like the same problem. A Switch has no\n * fill: its track is one colour and the thumb tells you the state, so\n * `border` unchecked and `primary-strong` checked works. A Slider has a\n * filled portion whose LENGTH is the value -- and `primary-strong` on a\n * `border` track is 1.01:1. The two halves would be the same grey and the\n * value would be invisible to anyone not reading the thumb position.\n *\n * So the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`: fill against track 4.61:1, track against page 5.25:1,\n * thumb against fill 5.20:1, thumb edge against track 4.65:1. All four\n * pairs are in the build-failing contrast contract.\n *\n * Three legacy defects go with it: the focus ring was `ring-1`, a 1px\n * box-shadow that forced-colors discards entirely; the thumb border was\n * `border-primary/50`, an alpha edge with no measurable contrast; and the\n * disabled styling used `disabled:`, a pseudo-class that only applies to\n * form elements -- Radix renders a `<span role=\"slider\">`, so a disabled\n * slider looked exactly like an enabled one.\n */\nexport const sliderVariants = cva([\"flex w-full flex-col gap-2 box-border font-sans\"]);\n\nexport const sliderTrackVariants = cva([\n \"relative flex w-full items-center\",\n // h-6 is the TRACK ROW, not the visible bar -- it gives the 24px\n // pointer target WCAG 2.5.8 wants. The bar itself is drawn inside.\n \"h-6\",\n // A size container, so the value bubble can hold inside it (styles.css).\n \"slider-track\",\n]);\n\nexport const sliderBarVariants = cva([\n \"absolute h-2 w-full rounded-pill box-border\",\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const sliderFillVariants = cva([\"absolute h-2 rounded-pill bg-primary-strong\", \"slider-fill\"]);\n\nexport const sliderThumbVariants = cva([\n // size-6 is 24px: WCAG 2.5.8 target size. The legacy thumb was 16px at\n // all four call sites, and one of them crams the whole control into 80px.\n \"size-6 rounded-full box-border\",\n \"bg-background border-(length:--border-width) border-border shadow-surface\",\n \"cursor-grab data-[dragging]:cursor-grabbing\",\n // Follows an outside value on a spring, swells while held (styles.css).\n \"slider-thumb\",\n // A real outline, not the legacy ring-1 box-shadow.\n \"outline-none\",\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 // data-[disabled], NOT disabled:. The legacy used the CSS :disabled\n // pseudo-class, which applies only to form elements -- React Aria and\n // Radix both render a span, so that styling never fired.\n \"data-[disabled]:cursor-not-allowed\",\n]);\n\n/**\n * Text that rolls when its value changes: a larger value rises in from below, a\n * smaller one drops in from above. The caller's own text stays for a screen\n * reader; the copy the eye sees is aria-hidden.\n */\nfunction RollingText({ value, text }: { value: number; text: string }) {\n const [trail, setTrail] = useState({ value, dir: 1 });\n if (trail.value !== value) setTrail({ value, dir: value > trail.value ? 1 : -1 });\n const dir = trail.value === value ? trail.dir : value > trail.value ? 1 : -1;\n return (\n <>\n <span className=\"sr-only\">{text}</span>\n <span aria-hidden=\"true\">\n <SwapText swapKey={text} dir={dir} motion=\"roll\" className=\"inline-block\">\n {text}\n </SwapText>\n </span>\n </>\n );\n}\n\n/**\n * A value along a range.\n *\n * @example\n * <Slider label=\"Input volume\" defaultValue={40} />\n * <Slider label=\"Playback position\" value={t} onChange={seek} formatOptions={{ style: \"percent\" }} />\n */\nexport interface SliderProps\n extends Omit<AriaSliderProps<number>, \"className\" | \"style\" | \"children\"> {\n /**\n * What the slider controls, and its accessible name.\n *\n * **Required.** All four legacy call sites are unnamed sliders.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /**\n * Show the current value as text beside the label.\n *\n * On by default. It is the text equivalent of the fill length, which\n * matters because in greyscale the filled and unfilled halves are\n * distinguishable only by the thumb.\n */\n showValue?: boolean;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Slider = forwardRef<HTMLDivElement, SliderProps>(function Slider(\n { label, labelHidden, showValue = true, className, ...props },\n ref,\n) {\n return (\n <AriaSlider\n ref={ref}\n {...props}\n data-slot=\"slider\"\n className={cn(sliderVariants(), className)}\n >\n <div className={cnAll(\"flex items-center justify-between gap-2\", labelHidden && \"sr-only\")}>\n <AriaLabel\n data-slot=\"slider-label\"\n className=\"text-sm leading-(--leading-normal) text-foreground\"\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <AriaSliderOutput\n data-slot=\"slider-output\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground\"\n >\n {({ state }) => (\n <RollingText value={state.getThumbValue(0)} text={state.getThumbValueLabel(0)} />\n )}\n </AriaSliderOutput>\n ) : null}\n </div>\n\n <AriaSliderTrack data-slot=\"slider-track\" className={sliderTrackVariants()}>\n {({ state }) => (\n <>\n <div data-slot=\"slider-bar\" className={sliderBarVariants()} />\n <div\n data-slot=\"slider-fill\"\n className={sliderFillVariants()}\n data-dragging={state.isThumbDragging(0) || undefined}\n style={{ width: `${state.getThumbPercent(0) * 100}%` }}\n />\n <AriaSliderThumb data-slot=\"slider-thumb\" className={sliderThumbVariants()}>\n {({ isDragging, isFocusVisible, state: thumbState }) => (\n // Decorative: the output beside the label already carries the value.\n <span\n aria-hidden=\"true\"\n className=\"slider-bubble-anchor pointer-events-none absolute bottom-full left-1/2 mb-2\"\n style={{ \"--at\": thumbState.getThumbPercent(0) * 100 } as React.CSSProperties}\n >\n <span\n data-slot=\"slider-bubble\"\n data-shown={isDragging || isFocusVisible || undefined}\n className=\"slider-bubble block whitespace-nowrap rounded-pill bg-foreground px-3 py-1 text-sm leading-none font-medium text-background shadow-surface\"\n >\n <RollingText\n value={thumbState.getThumbValue(0)}\n text={thumbState.getThumbValueLabel(0)}\n />\n </span>\n </span>\n )}\n </AriaSliderThumb>\n </>\n )}\n </AriaSliderTrack>\n </AriaSlider>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,iBAAiB,IAAI,CAAC,iDAAiD,CAAC;AAErF,MAAa,sBAAsB,IAAI;CACrC;CAGA;CAEA;AACF,CAAC;AAED,MAAa,oBAAoB,IAAI,CACnC,+CACA,uDACF,CAAC;AAED,MAAa,qBAAqB,IAAI,CAAC,+CAA+C,aAAa,CAAC;AAEpG,MAAa,sBAAsB,IAAI;CAGrC;CACA;CACA;CAEA;CAEA;CACA;CACA;CACA;CACA;CAIA;AACF,CAAC;;;;;;AAOD,SAAS,YAAY,EAAE,OAAO,QAAyC;CACrE,MAAM,CAAC,OAAO,YAAY,SAAS;EAAE;EAAO,KAAK;CAAE,CAAC;CACpD,IAAI,MAAM,UAAU,OAAO,SAAS;EAAE;EAAO,KAAK,QAAQ,MAAM,QAAQ,IAAI;CAAG,CAAC;CAChF,MAAM,MAAM,MAAM,UAAU,QAAQ,MAAM,MAAM,QAAQ,MAAM,QAAQ,IAAI;CAC1E,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EAAM,WAAU;EAAW,UAAA;CAAW,CAAA,GACtC,oBAAC,QAAD;EAAM,eAAY;EAChB,UAAA,oBAAC,UAAD;GAAU,SAAS;GAAW;GAAK,QAAO;GAAO,WAAU;GACxD,UAAA;EACO,CAAA;CACN,CAAA,CACN,EAAA,CAAA;AAEN;AA+BA,MAAaA,WAAS,WAAwC,SAASA,SACrE,EAAE,OAAO,aAAa,YAAY,MAAM,WAAW,GAAG,SACtD,KACA;CACA,OACE,qBAACC,QAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,eAAe,GAAG,SAAS;EAJ3C,UAAA,CAME,qBAAC,OAAD;GAAK,WAAW,MAAM,2CAA2C,eAAe,SAAS;GAAzF,UAAA,CACE,oBAACC,OAAD;IACE,aAAU;IACV,WAAU;IAET,UAAA;GACQ,CAAA,GACV,YACC,oBAACC,cAAD;IACE,aAAU;IACV,WAAU;IAER,WAAA,EAAE,YACF,oBAAC,aAAD;KAAa,OAAO,MAAM,cAAc,CAAC;KAAG,MAAM,MAAM,mBAAmB,CAAC;IAAI,CAAA;GAElE,CAAA,IAChB,IACD;EAEL,CAAA,GAAA,oBAACC,aAAD;GAAiB,aAAU;GAAe,WAAW,oBAAoB;GACrE,WAAA,EAAE,YACF,qBAAA,YAAA,EAAA,UAAA;IACE,oBAAC,OAAD;KAAK,aAAU;KAAa,WAAW,kBAAkB;IAAI,CAAA;IAC7D,oBAAC,OAAD;KACE,aAAU;KACV,WAAW,mBAAmB;KAC9B,iBAAe,MAAM,gBAAgB,CAAC,KAAK,KAAA;KAC3C,OAAO,EAAE,OAAO,GAAG,MAAM,gBAAgB,CAAC,IAAI,IAAI,GAAG;IACtD,CAAA;IACD,oBAACC,aAAD;KAAiB,aAAU;KAAe,WAAW,oBAAoB;KACrE,WAAA,EAAE,YAAY,gBAAgB,OAAO,iBAErC,oBAAC,QAAD;MACE,eAAY;MACZ,WAAU;MACV,OAAO,EAAE,QAAQ,WAAW,gBAAgB,CAAC,IAAI,IAAI;MAErD,UAAA,oBAAC,QAAD;OACE,aAAU;OACV,cAAY,cAAc,kBAAkB,KAAA;OAC5C,WAAU;OAEV,UAAA,oBAAC,aAAD;QACE,OAAO,WAAW,cAAc,CAAC;QACjC,MAAM,WAAW,mBAAmB,CAAC;OACtC,CAAA;MACG,CAAA;KACF,CAAA;IAEO,CAAA;GACjB,EAAA,CAAA;EAEW,CAAA,CACP;;AAEhB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/split-button/split-button.d.ts
|
|
3
|
+
/** One entry in the menu half. */
|
|
4
|
+
export interface SplitButtonAction {
|
|
5
|
+
/** The item's text, and its identity. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Runs when the item is chosen. */
|
|
8
|
+
onSelect?: () => void;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
/** Tints the item as destructive. */
|
|
11
|
+
destructive?: boolean;
|
|
12
|
+
/** An icon before the label. Mark it `aria-hidden`. */
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export interface SplitButtonProps {
|
|
16
|
+
/** The main action's label. A plain string: it is split into letters to morph when it changes. */
|
|
17
|
+
children: string;
|
|
18
|
+
/** The menu half's items: the nearby alternatives to the main action. */
|
|
19
|
+
actions: SplitButtonAction[];
|
|
20
|
+
/** Runs when the main half is pressed. */
|
|
21
|
+
onPress?: () => void;
|
|
22
|
+
isDisabled?: boolean;
|
|
23
|
+
/** An icon before the label. Swapping it for another icon morphs; re-rendering the same icon does not. Mark it `aria-hidden`. */
|
|
24
|
+
icon?: ReactNode;
|
|
25
|
+
/** `secondary` is the quiet, outlined pill. @default "primary" */
|
|
26
|
+
variant?: "primary" | "secondary";
|
|
27
|
+
/** Extra classes for the pill. Merged last, so they win conflicts with ours. */
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One pill, two actions: a main action and a menu of nearby alternatives.
|
|
32
|
+
*
|
|
33
|
+
* The main half morphs in place: change `children` (or `icon`) and the letters
|
|
34
|
+
* that differ rise in while the old ones lift away, the icon swaps by scaling,
|
|
35
|
+
* and the width springs to fit. The whole pill presses in for the main action;
|
|
36
|
+
* the menu half answers in colour only, so the menu opens from a still anchor,
|
|
37
|
+
* growing from its trigger, its items arriving one after another.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* <SplitButton onPress={save} actions={[{ label: "Save as draft" }, { label: "Delete", destructive: true }]}>
|
|
41
|
+
* Save
|
|
42
|
+
* </SplitButton>
|
|
43
|
+
*/
|
|
44
|
+
export declare function SplitButton({ children: label, actions, onPress, isDisabled, icon, variant, className }: SplitButtonProps): import("react").JSX.Element;
|
|
45
|
+
//#endregion
|
|
46
|
+
//# sourceMappingURL=split-button.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { useMorphWidth } from "../lib/motion.js";
|
|
4
|
+
import { MorphText } from "../lib/morph-text.js";
|
|
5
|
+
import { Menu as Menu$1, MenuItem as MenuItem$1, MenuTrigger } from "../menu/menu.js";
|
|
6
|
+
import { IconSwap } from "../lib/icon-swap.js";
|
|
7
|
+
import { isValidElement, useRef } from "react";
|
|
8
|
+
import { Button } from "react-aria-components";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/split-button/split-button.tsx
|
|
11
|
+
/** Names the icon element, so swapping Copy for Check morphs while a re-render of the same icon stays still. */
|
|
12
|
+
function iconKey(node) {
|
|
13
|
+
if (!isValidElement(node)) return typeof node === "string" || typeof node === "number" ? String(node) : "";
|
|
14
|
+
const type = node.type;
|
|
15
|
+
return typeof type === "string" ? type : type?.displayName ?? type?.name ?? "icon";
|
|
16
|
+
}
|
|
17
|
+
const half = [
|
|
18
|
+
"relative inline-flex items-center justify-center cursor-pointer select-none font-sans font-medium",
|
|
19
|
+
"[-webkit-tap-highlight-color:transparent]",
|
|
20
|
+
"outline-none",
|
|
21
|
+
"data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
22
|
+
"data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)",
|
|
23
|
+
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-button-disabled)",
|
|
24
|
+
"motion-safe:transition-[background-color,color,opacity] motion-safe:duration-(--duration-quick) motion-safe:ease-settle"
|
|
25
|
+
].join(" ");
|
|
26
|
+
const looks = {
|
|
27
|
+
primary: {
|
|
28
|
+
pill: "bg-primary text-primary-foreground shadow-surface rounded-md",
|
|
29
|
+
main: "h-(--density-control-h-md) px-(--density-control-pad-md) text-base rounded-s-md data-[hovered]:opacity-91 data-[pressed]:opacity-84",
|
|
30
|
+
trigger: "h-(--density-control-h-md) w-9 rounded-e-md border-s-(length:--border-width) border-primary-foreground/20 data-[hovered]:opacity-91 data-[pressed]:opacity-84"
|
|
31
|
+
},
|
|
32
|
+
secondary: {
|
|
33
|
+
pill: "bg-background text-foreground border-(length:--border-width) border-border shadow-surface rounded-md",
|
|
34
|
+
main: "h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm rounded-s-md data-[hovered]:bg-muted data-[pressed]:bg-muted",
|
|
35
|
+
trigger: "h-(--density-control-h-sm) w-8 rounded-e-md border-s-(length:--border-width) border-border data-[hovered]:bg-muted data-[pressed]:bg-muted"
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* One pill, two actions: a main action and a menu of nearby alternatives.
|
|
40
|
+
*
|
|
41
|
+
* The main half morphs in place: change `children` (or `icon`) and the letters
|
|
42
|
+
* that differ rise in while the old ones lift away, the icon swaps by scaling,
|
|
43
|
+
* and the width springs to fit. The whole pill presses in for the main action;
|
|
44
|
+
* the menu half answers in colour only, so the menu opens from a still anchor,
|
|
45
|
+
* growing from its trigger, its items arriving one after another.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* <SplitButton onPress={save} actions={[{ label: "Save as draft" }, { label: "Delete", destructive: true }]}>
|
|
49
|
+
* Save
|
|
50
|
+
* </SplitButton>
|
|
51
|
+
*/
|
|
52
|
+
function SplitButton({ children: label, actions, onPress, isDisabled, icon, variant = "primary", className }) {
|
|
53
|
+
const contentRef = useRef(null);
|
|
54
|
+
const glyph = iconKey(icon);
|
|
55
|
+
const look = looks[variant];
|
|
56
|
+
useMorphWidth(contentRef, glyph + "|" + label);
|
|
57
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
58
|
+
role: "group",
|
|
59
|
+
className: cn("split-pill inline-flex items-stretch " + look.pill, className),
|
|
60
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
61
|
+
"data-slot": "split-main",
|
|
62
|
+
className: cn(half + " " + look.main),
|
|
63
|
+
isDisabled,
|
|
64
|
+
onPress,
|
|
65
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
66
|
+
className: "button-slot [--morph-clip:inset(-50%_calc(var(--spacing)*-2))]",
|
|
67
|
+
"aria-hidden": "true",
|
|
68
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
69
|
+
ref: contentRef,
|
|
70
|
+
className: "inline-flex flex-none items-center gap-2 whitespace-nowrap",
|
|
71
|
+
children: [icon ? /* @__PURE__ */ jsx(IconSwap, {
|
|
72
|
+
iconKey: glyph,
|
|
73
|
+
children: icon
|
|
74
|
+
}) : null, /* @__PURE__ */ jsx(MorphText, {
|
|
75
|
+
text: label,
|
|
76
|
+
morphWidth: false
|
|
77
|
+
})]
|
|
78
|
+
})
|
|
79
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
80
|
+
className: "sr-only",
|
|
81
|
+
"aria-live": "polite",
|
|
82
|
+
children: label
|
|
83
|
+
})]
|
|
84
|
+
}), /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(Button, {
|
|
85
|
+
"data-slot": "split-trigger",
|
|
86
|
+
"aria-label": label + " more actions",
|
|
87
|
+
isDisabled,
|
|
88
|
+
className: cn("group/trigger " + half + " " + look.trigger),
|
|
89
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
90
|
+
viewBox: "0 0 24 24",
|
|
91
|
+
fill: "none",
|
|
92
|
+
stroke: "currentColor",
|
|
93
|
+
strokeWidth: 1.75,
|
|
94
|
+
strokeLinecap: "round",
|
|
95
|
+
strokeLinejoin: "round",
|
|
96
|
+
"aria-hidden": "true",
|
|
97
|
+
className: "size-4 motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180",
|
|
98
|
+
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
|
|
99
|
+
})
|
|
100
|
+
}), /* @__PURE__ */ jsx(Menu$1, {
|
|
101
|
+
placement: "bottom end",
|
|
102
|
+
shouldFocusWrap: true,
|
|
103
|
+
className: "menu-motion [--menu-ox:right]",
|
|
104
|
+
children: actions.map((action, index) => /* @__PURE__ */ jsxs(MenuItem$1, {
|
|
105
|
+
id: action.label,
|
|
106
|
+
textValue: action.label,
|
|
107
|
+
isDisabled: action.isDisabled,
|
|
108
|
+
onAction: action.onSelect,
|
|
109
|
+
"data-i": Math.min(index, 4),
|
|
110
|
+
className: cn("menu-item-motion gap-2.5" + (action.destructive ? " text-destructive-strong data-[focused]:bg-[color-mix(in_oklab,var(--color-destructive-strong)_8%,var(--color-card))]" : "")),
|
|
111
|
+
children: [action.icon ? /* @__PURE__ */ jsx("span", {
|
|
112
|
+
"aria-hidden": "true",
|
|
113
|
+
children: action.icon
|
|
114
|
+
}) : null, action.label]
|
|
115
|
+
}, action.label))
|
|
116
|
+
})] })]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { SplitButton };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=split-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split-button.js","names":["AriaButton","Menu","MenuItem"],"sources":["../../src/split-button/split-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { isValidElement, useRef, type ReactNode } from \"react\";\nimport { Button as AriaButton } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { IconSwap } from \"../lib/icon-swap.js\";\nimport { MorphText } from \"../lib/morph-text.js\";\nimport { useMorphWidth } from \"../lib/motion.js\";\nimport { Menu, MenuItem, MenuTrigger } from \"../menu/menu.js\";\n\n/** One entry in the menu half. */\nexport interface SplitButtonAction {\n /** The item's text, and its identity. */\n label: string;\n /** Runs when the item is chosen. */\n onSelect?: () => void;\n isDisabled?: boolean;\n /** Tints the item as destructive. */\n destructive?: boolean;\n /** An icon before the label. Mark it `aria-hidden`. */\n icon?: ReactNode;\n}\n\nexport interface SplitButtonProps {\n /** The main action's label. A plain string: it is split into letters to morph when it changes. */\n children: string;\n /** The menu half's items: the nearby alternatives to the main action. */\n actions: SplitButtonAction[];\n /** Runs when the main half is pressed. */\n onPress?: () => void;\n isDisabled?: boolean;\n /** An icon before the label. Swapping it for another icon morphs; re-rendering the same icon does not. Mark it `aria-hidden`. */\n icon?: ReactNode;\n /** `secondary` is the quiet, outlined pill. @default \"primary\" */\n variant?: \"primary\" | \"secondary\";\n /** Extra classes for the pill. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/** Names the icon element, so swapping Copy for Check morphs while a re-render of the same icon stays still. */\nfunction iconKey(node: ReactNode): string {\n if (!isValidElement(node))\n return typeof node === \"string\" || typeof node === \"number\" ? String(node) : \"\";\n const type = node.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type?.displayName ?? type?.name ?? \"icon\");\n}\n\n/*\n * Class strings are static literals: Tailwind scans source text.\n *\n * Both halves share the focus outline, the tap-highlight reset and the\n * disabled look. The pill, not either half, scales on press (split-pill), and\n * the menu half never scales, so the menu opens from a still anchor.\n */\nconst half = [\n \"relative inline-flex items-center justify-center cursor-pointer select-none font-sans font-medium\",\n \"[-webkit-tap-highlight-color:transparent]\",\n \"outline-none\",\n \"data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-button-disabled)\",\n \"motion-safe:transition-[background-color,color,opacity] motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n].join(\" \");\n\nconst looks = {\n primary: {\n pill: \"bg-primary text-primary-foreground shadow-surface rounded-md\",\n main: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base rounded-s-md data-[hovered]:opacity-91 data-[pressed]:opacity-84\",\n trigger:\n \"h-(--density-control-h-md) w-9 rounded-e-md border-s-(length:--border-width) border-primary-foreground/20 data-[hovered]:opacity-91 data-[pressed]:opacity-84\",\n },\n secondary: {\n pill: \"bg-background text-foreground border-(length:--border-width) border-border shadow-surface rounded-md\",\n main: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm rounded-s-md data-[hovered]:bg-muted data-[pressed]:bg-muted\",\n trigger:\n \"h-(--density-control-h-sm) w-8 rounded-e-md border-s-(length:--border-width) border-border data-[hovered]:bg-muted data-[pressed]:bg-muted\",\n },\n} as const;\n\nconst itemBase = \"gap-2.5\";\nconst itemDestructive =\n \"text-destructive-strong data-[focused]:bg-[color-mix(in_oklab,var(--color-destructive-strong)_8%,var(--color-card))]\";\n\n/**\n * One pill, two actions: a main action and a menu of nearby alternatives.\n *\n * The main half morphs in place: change `children` (or `icon`) and the letters\n * that differ rise in while the old ones lift away, the icon swaps by scaling,\n * and the width springs to fit. The whole pill presses in for the main action;\n * the menu half answers in colour only, so the menu opens from a still anchor,\n * growing from its trigger, its items arriving one after another.\n *\n * @example\n * <SplitButton onPress={save} actions={[{ label: \"Save as draft\" }, { label: \"Delete\", destructive: true }]}>\n * Save\n * </SplitButton>\n */\nexport function SplitButton({\n children: label,\n actions,\n onPress,\n isDisabled,\n icon,\n variant = \"primary\",\n className,\n}: SplitButtonProps) {\n const contentRef = useRef<HTMLSpanElement>(null);\n const glyph = iconKey(icon);\n const look = looks[variant];\n useMorphWidth(contentRef, glyph + \"|\" + label);\n\n return (\n <div\n role=\"group\"\n className={cn(\"split-pill inline-flex items-stretch \" + look.pill, className)}\n >\n <AriaButton\n data-slot=\"split-main\"\n className={cn(half + \" \" + look.main)}\n isDisabled={isDisabled}\n onPress={onPress}\n >\n <span\n className=\"button-slot [--morph-clip:inset(-50%_calc(var(--spacing)*-2))]\"\n aria-hidden=\"true\"\n >\n <span\n ref={contentRef}\n className=\"inline-flex flex-none items-center gap-2 whitespace-nowrap\"\n >\n {icon ? <IconSwap iconKey={glyph}>{icon}</IconSwap> : null}\n <MorphText text={label} morphWidth={false} />\n </span>\n </span>\n <span className=\"sr-only\" aria-live=\"polite\">\n {label}\n </span>\n </AriaButton>\n <MenuTrigger>\n <AriaButton\n data-slot=\"split-trigger\"\n aria-label={label + \" more actions\"}\n isDisabled={isDisabled}\n className={cn(\"group/trigger \" + half + \" \" + look.trigger)}\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-4 motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </AriaButton>\n <Menu placement=\"bottom end\" shouldFocusWrap className=\"menu-motion [--menu-ox:right]\">\n {actions.map((action, index) => (\n <MenuItem\n key={action.label}\n id={action.label}\n textValue={action.label}\n isDisabled={action.isDisabled}\n onAction={action.onSelect}\n data-i={Math.min(index, 4)}\n className={cn(\n \"menu-item-motion \" + itemBase + (action.destructive ? \" \" + itemDestructive : \"\"),\n )}\n >\n {action.icon ? <span aria-hidden=\"true\">{action.icon}</span> : null}\n {action.label}\n </MenuItem>\n ))}\n </Menu>\n </MenuTrigger>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAyCA,SAAS,QAAQ,MAAyB;CACxC,IAAI,CAAC,eAAe,IAAI,GACtB,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,WAAW,OAAO,IAAI,IAAI;CAC/E,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,WAAW,OAAQ,MAAM,eAAe,MAAM,QAAQ;AAC/E;AASA,MAAM,OAAO;CACX;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;AAEV,MAAM,QAAQ;CACZ,SAAS;EACP,MAAM;EACN,MAAM;EACN,SACE;CACJ;CACA,WAAW;EACT,MAAM;EACN,MAAM;EACN,SACE;CACJ;AACF;;;;;;;;;;;;;;;AAoBA,SAAgB,YAAY,EAC1B,UAAU,OACV,SACA,SACA,YACA,MACA,UAAU,WACV,aACmB;CACnB,MAAM,aAAa,OAAwB,IAAI;CAC/C,MAAM,QAAQ,QAAQ,IAAI;CAC1B,MAAM,OAAO,MAAM;CACnB,cAAc,YAAY,QAAQ,MAAM,KAAK;CAE7C,OACE,qBAAC,OAAD;EACE,MAAK;EACL,WAAW,GAAG,0CAA0C,KAAK,MAAM,SAAS;EAF9E,UAAA,CAIE,qBAACA,QAAD;GACE,aAAU;GACV,WAAW,GAAG,OAAO,MAAM,KAAK,IAAI;GACxB;GACH;GAJX,UAAA,CAME,oBAAC,QAAD;IACE,WAAU;IACV,eAAY;IAEZ,UAAA,qBAAC,QAAD;KACE,KAAK;KACL,WAAU;KAFZ,UAAA,CAIG,OAAO,oBAAC,UAAD;MAAU,SAAS;MAAQ,UAAA;KAAe,CAAA,IAAI,MACtD,oBAAC,WAAD;MAAW,MAAM;MAAO,YAAY;KAAQ,CAAA,CACxC;;GACF,CAAA,GACN,oBAAC,QAAD;IAAM,WAAU;IAAU,aAAU;IACjC,UAAA;GACG,CAAA,CACI;EACZ,CAAA,GAAA,qBAAC,aAAD,EAAA,UAAA,CACE,oBAACA,QAAD;GACE,aAAU;GACV,cAAY,QAAQ;GACR;GACZ,WAAW,GAAG,mBAAmB,OAAO,MAAM,KAAK,OAAO;GAE1D,UAAA,oBAAC,OAAD;IACE,SAAQ;IACR,MAAK;IACL,QAAO;IACP,aAAa;IACb,eAAc;IACd,gBAAe;IACf,eAAY;IACZ,WAAU;IAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA;GACrB,CAAA;EACK,CAAA,GACZ,oBAACC,QAAD;GAAM,WAAU;GAAa,iBAAA;GAAgB,WAAU;GACpD,UAAA,QAAQ,KAAK,QAAQ,UACpB,qBAACC,YAAD;IAEE,IAAI,OAAO;IACX,WAAW,OAAO;IAClB,YAAY,OAAO;IACnB,UAAU,OAAO;IACjB,UAAQ,KAAK,IAAI,OAAO,CAAC;IACzB,WAAW,GACT,8BAAkC,OAAO,cAAc,0HAAwB,GACjF;IATF,UAAA,CAWG,OAAO,OAAO,oBAAC,QAAD;KAAM,eAAY;KAAQ,UAAA,OAAO;IAAW,CAAA,IAAI,MAC9D,OAAO,KACA;GAZH,GAAA,OAAO,KAYJ,CACX;EACG,CAAA,CACK,EAAA,CAAA,CACV;;AAET"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { StatCard, StatCardProps, StatCardTone, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
|
|
2
|
+
export { StatCard, type StatCardProps, type StatCardTone, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|