@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,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
|
|
@@ -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 { jsx } from "react/jsx-runtime";
|
|
5
6
|
//#region src/skeleton/skeleton.tsx
|
|
6
7
|
/**
|
|
@@ -25,17 +26,22 @@ import { jsx } from "react/jsx-runtime";
|
|
|
25
26
|
*/
|
|
26
27
|
const skeletonVariants = cva([
|
|
27
28
|
"block bg-muted rounded-md",
|
|
28
|
-
"motion-safe:animate-pulse",
|
|
29
|
+
"motion-safe:animate-skeleton-pulse",
|
|
29
30
|
"box-border"
|
|
30
31
|
]);
|
|
31
|
-
function Skeleton({ className, ...props }) {
|
|
32
|
+
const Skeleton = forwardRef(function Skeleton({ className, index, style, ...props }, ref) {
|
|
32
33
|
return /* @__PURE__ */ jsx("div", {
|
|
34
|
+
ref,
|
|
33
35
|
...props,
|
|
36
|
+
style: index ? {
|
|
37
|
+
animationDelay: `${index * 90}ms`,
|
|
38
|
+
...style
|
|
39
|
+
} : style,
|
|
34
40
|
"aria-hidden": "true",
|
|
35
41
|
"data-slot": "skeleton",
|
|
36
42
|
className: cn(skeletonVariants(), className)
|
|
37
43
|
});
|
|
38
|
-
}
|
|
44
|
+
});
|
|
39
45
|
//#endregion
|
|
40
46
|
export { Skeleton, skeletonVariants };
|
|
41
47
|
|
|
@@ -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 type
|
|
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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SliderProps } from "react-aria-components";
|
|
2
1
|
import { ReactNode } from "react";
|
|
2
|
+
import { SliderProps } from "react-aria-components";
|
|
3
3
|
//#region src/slider/slider.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Slider, ported from the legacy shadcn/Radix component.
|
|
@@ -64,7 +64,7 @@ interface SliderProps$1 extends Omit<SliderProps<number>, "className" | "style"
|
|
|
64
64
|
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
65
65
|
className?: string;
|
|
66
66
|
}
|
|
67
|
-
declare
|
|
67
|
+
declare const Slider$1: import("react").ForwardRefExoticComponent<SliderProps$1 & import("react").RefAttributes<HTMLDivElement>>;
|
|
68
68
|
//#endregion
|
|
69
69
|
export { Slider$1 as Slider, SliderProps$1 as SliderProps };
|
|
70
70
|
//# sourceMappingURL=slider.d.ts.map
|
package/dist/slider/slider.js
CHANGED
|
@@ -1,8 +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";
|
|
5
|
+
import { forwardRef, useState } from "react";
|
|
4
6
|
import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from "react-aria-components";
|
|
5
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
8
|
//#region src/slider/slider.tsx
|
|
7
9
|
/**
|
|
8
10
|
* Slider, ported from the legacy shadcn/Radix component.
|
|
@@ -36,13 +38,18 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
36
38
|
* slider looked exactly like an enabled one.
|
|
37
39
|
*/
|
|
38
40
|
const sliderVariants = cva(["flex w-full flex-col gap-2 box-border font-sans"]);
|
|
39
|
-
const sliderTrackVariants = cva([
|
|
41
|
+
const sliderTrackVariants = cva([
|
|
42
|
+
"relative flex w-full items-center",
|
|
43
|
+
"h-6",
|
|
44
|
+
"slider-track"
|
|
45
|
+
]);
|
|
40
46
|
const sliderBarVariants = cva(["absolute h-2 w-full rounded-pill box-border", "bg-muted border-(length:--border-width) border-border"]);
|
|
41
|
-
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"]);
|
|
42
48
|
const sliderThumbVariants = cva([
|
|
43
49
|
"size-6 rounded-full box-border",
|
|
44
50
|
"bg-background border-(length:--border-width) border-border shadow-surface",
|
|
45
51
|
"cursor-grab data-[dragging]:cursor-grabbing",
|
|
52
|
+
"slider-thumb",
|
|
46
53
|
"outline-none",
|
|
47
54
|
"data-[focus-visible]:outline-solid",
|
|
48
55
|
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
@@ -50,8 +57,38 @@ const sliderThumbVariants = cva([
|
|
|
50
57
|
"data-[focus-visible]:outline-(--color-ring)",
|
|
51
58
|
"data-[disabled]:cursor-not-allowed"
|
|
52
59
|
]);
|
|
53
|
-
|
|
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
|
+
}
|
|
89
|
+
const Slider$1 = forwardRef(function Slider$2({ label, labelHidden, showValue = true, className, ...props }, ref) {
|
|
54
90
|
return /* @__PURE__ */ jsxs(Slider, {
|
|
91
|
+
ref,
|
|
55
92
|
...props,
|
|
56
93
|
"data-slot": "slider",
|
|
57
94
|
className: cn(sliderVariants(), className),
|
|
@@ -63,12 +100,16 @@ function Slider$1({ label, labelHidden, showValue = true, className, ...props })
|
|
|
63
100
|
children: label
|
|
64
101
|
}), showValue ? /* @__PURE__ */ jsx(SliderOutput, {
|
|
65
102
|
"data-slot": "slider-output",
|
|
66
|
-
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
|
+
})
|
|
67
108
|
}) : null]
|
|
68
109
|
}), /* @__PURE__ */ jsx(SliderTrack, {
|
|
69
110
|
"data-slot": "slider-track",
|
|
70
111
|
className: sliderTrackVariants(),
|
|
71
|
-
children: ({ state }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
112
|
+
children: ({ state }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
72
113
|
/* @__PURE__ */ jsx("div", {
|
|
73
114
|
"data-slot": "slider-bar",
|
|
74
115
|
className: sliderBarVariants()
|
|
@@ -76,16 +117,31 @@ function Slider$1({ label, labelHidden, showValue = true, className, ...props })
|
|
|
76
117
|
/* @__PURE__ */ jsx("div", {
|
|
77
118
|
"data-slot": "slider-fill",
|
|
78
119
|
className: sliderFillVariants(),
|
|
120
|
+
"data-dragging": state.isThumbDragging(0) || void 0,
|
|
79
121
|
style: { width: `${state.getThumbPercent(0) * 100}%` }
|
|
80
122
|
}),
|
|
81
123
|
/* @__PURE__ */ jsx(SliderThumb, {
|
|
82
124
|
"data-slot": "slider-thumb",
|
|
83
|
-
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
|
+
})
|
|
84
140
|
})
|
|
85
141
|
] })
|
|
86
142
|
})]
|
|
87
143
|
});
|
|
88
|
-
}
|
|
144
|
+
});
|
|
89
145
|
//#endregion
|
|
90
146
|
export { Slider$1 as Slider, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants };
|
|
91
147
|
|
|
@@ -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 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 function Slider({\n label,\n labelHidden,\n showValue = true,\n className,\n ...props\n}: SliderProps) {\n return (\n <AriaSlider {...props} data-slot=\"slider\" className={cn(sliderVariants(), className)}>\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,SAAgBA,SAAO,EACrB,OACA,aACA,YAAY,MACZ,WACA,GAAG,SACW;CACd,OACE,qBAACC,QAAD;EAAY,GAAI;EAAO,aAAU;EAAS,WAAW,GAAG,eAAe,GAAG,SAAS;EAAnF,UAAA,CACE,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"}
|
|
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"}
|
|
@@ -70,6 +70,6 @@ export interface SpinnerProps extends Omit<ComponentPropsWithoutRef<"span">, "ch
|
|
|
70
70
|
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
71
71
|
className?: string;
|
|
72
72
|
}
|
|
73
|
-
export declare
|
|
73
|
+
export declare const Spinner: import("react").ForwardRefExoticComponent<SpinnerProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
74
74
|
//#endregion
|
|
75
75
|
//# sourceMappingURL=spinner.d.ts.map
|
package/dist/spinner/spinner.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 { jsx } from "react/jsx-runtime";
|
|
5
6
|
//#region src/spinner/spinner.tsx
|
|
6
7
|
/**
|
|
@@ -39,8 +40,9 @@ const spinnerVariants = cva([
|
|
|
39
40
|
} },
|
|
40
41
|
defaultVariants: { size: "md" }
|
|
41
42
|
});
|
|
42
|
-
function Spinner({ className, size, label, ...props }) {
|
|
43
|
+
const Spinner = forwardRef(function Spinner({ className, size, label, ...props }, ref) {
|
|
43
44
|
return /* @__PURE__ */ jsx("span", {
|
|
45
|
+
ref,
|
|
44
46
|
...props,
|
|
45
47
|
role: "status",
|
|
46
48
|
"aria-label": label,
|
|
@@ -61,7 +63,7 @@ function Spinner({ className, size, label, ...props }) {
|
|
|
61
63
|
children: label
|
|
62
64
|
})
|
|
63
65
|
});
|
|
64
|
-
}
|
|
66
|
+
});
|
|
65
67
|
//#endregion
|
|
66
68
|
export { Spinner, spinnerVariants };
|
|
67
69
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spinner.js","names":[],"sources":["../../src/spinner/spinner.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type
|
|
1
|
+
{"version":3,"file":"spinner.js","names":[],"sources":["../../src/spinner/spinner.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, type ComponentPropsWithoutRef } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Spinner.\n *\n * There is no React Aria base: RAC ships no busy indicator, and a rotating\n * ring has no interaction model to inherit. Because it is not RAC-backed its\n * props follow ARCHITECTURE section 7 rule 1 -- VariantProps &\n * ComponentPropsWithoutRef -- rather than the Button's Omit-from-React-Aria\n * departure, which exists only to stop RAC-ignored props being advertised.\n *\n * Every string below is a STATIC literal. Tailwind scans source text, so a\n * class assembled at runtime is invisible to it and would never be emitted.\n *\n * THE LEGACY WOULD HAVE SHIPPED INVISIBLE. Its ring is\n * `border-gray-300 border-t-blue-600` and its rotation is `animate-spin`.\n * All three of those classes emit NO RULE in this repo: `--color-*: initial`\n * deleted Tailwind's default palette, and this package imports only\n * tailwindcss/utilities.css, never the default theme, so `--animate-spin` and\n * `@keyframes spin` did not exist either. A literal port would have compiled,\n * linted and type-checked into a static, colourless box.\n */\nexport const spinnerVariants = cva(\n [\n // inline-block so it sits in a line of text or inside a button without a\n // wrapper; shrink-0 so a flex parent cannot squash the circle into an oval.\n \"inline-block shrink-0\",\n // relative is load-bearing: the visually-hidden label below is positioned\n // against this box, and without a positioned ancestor it would anchor to\n // the page and drag the layout around.\n \"relative\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none,\n // and content-box would add the border on top of the size asked for --\n // turning a 16px `sm` into 20px.\n \"box-border\",\n // --radius-circle is 50%, already on the scale. The legacy's rounded-full\n // is 9999px, which is the same thing said less precisely.\n \"rounded-circle\",\n // A ring with one coloured quarter: no SVG, and the same silhouette the\n // legacy had, so migrating is a visual no-op.\n //\n // --border-width rather than border-2: this system has ONE border\n // thickness and the token exists precisely so 121 components do not each\n // pick their own. The arc reads thinner than the legacy's 2px; that is the\n // cost of the contract.\n //\n // border-border is held to the 3:1 non-text floor and primary-strong to\n // 4.5:1 on both the page and a card -- both are in the build-failing\n // contrast contract, so neither can drift into invisibility.\n //\n // FOUR LONGHANDS, NOT `border-border` PLUS `border-t-primary-strong`.\n // That shorter pairing is a shorthand followed by a longhand, and it only\n // works while our stylesheet is the last one to define `.border-border`.\n // It is not: Storybook runs its own Tailwind over the story files, emits\n // `.border-border{border-color:...}` into the SAME cascade layer after\n // ours, and the shorthand resets all four sides -- the arc silently became\n // the track colour. Caught in a real browser; `pnpm test` did not see it,\n // because the Vitest harness does not load that second stylesheet. A\n // consumer running their own Tailwind is in exactly Storybook's position,\n // so this is a real defect and not a dev-server quirk. Per-side longhands\n // cannot be clobbered by a shorthand that lands later.\n \"border-(length:--border-width)\",\n \"border-s-border border-e-border border-b-border border-t-primary-strong\",\n // motion-safe:, because motion.css's header says every component using a\n // keyframe must gate it. The trade-off is real and named: a reduced-motion\n // user gets a static ring, which is exactly why role=\"status\" and the\n // required label do the actual work of saying \"busy\" -- the animation is\n // never the only signal.\n \"motion-safe:animate-spin\",\n ],\n {\n variants: {\n size: {\n sm: \"size-(--icon-sm)\",\n md: \"size-(--icon-md)\",\n lg: \"size-(--icon-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * A busy indicator.\n *\n * Announces itself. Use it for an operation the user is waiting on; for\n * content-shaped placeholders during a page load, reach for `Skeleton`\n * instead, which is deliberately silent because thirty-three empty boxes\n * announcing themselves is noise.\n *\n * @example\n * <Spinner label=\"Loading campaigns\" />\n * <Spinner label=\"Saving\" size=\"sm\" />\n */\nexport interface SpinnerProps\n extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\" | \"role\" | \"aria-label\">,\n VariantProps<typeof spinnerVariants> {\n /**\n * What is loading. Becomes the spinner's accessible name.\n *\n * REQUIRED, so a nameless spinner is a type error rather than an omission.\n * The legacy component has no role, no name and no announced text, and it is\n * only about 1% of the spinning in that app -- there are also 63 lucide\n * `Loader2` spins and 8 hand-rolled divs, none of which announce either.\n * A required name is what makes that unrepeatable here.\n *\n * A `string` and not a `ReactNode` because it becomes an `aria-label`, which\n * is a DOM string attribute. `Progress.label` is a ReactNode only because\n * React Aria renders it; the difference is forced by the mechanism rather\n * than chosen.\n *\n * There is no `labelHidden` counterpart: not one legacy call site renders\n * text through the spinner, and the one component that shows a message\n * composes its own heading beside one. That is a layout, not a prop.\n */\n label: string;\n /**\n * Diameter, from the icon scale.\n *\n * All three steps are used by the legacy. The values come from\n * `--icon-sm/md/lg` rather than being restated, because that app already has\n * two spinners sharing this exact union and disagreeing on every pixel --\n * `h-4/6/8` against `h-8/12/16`.\n *\n * @default \"md\"\n */\n size?: VariantProps<typeof spinnerVariants>[\"size\"];\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(function Spinner(\n { className, size, label, ...props },\n ref,\n) {\n return (\n <span\n ref={ref}\n {...props}\n // Both the attribute and the text, deliberately. role=\"status\" maps to\n // ARIA `status`, whose name-from is AUTHOR ONLY -- its content does not\n // name it -- so aria-label is what turns the required prop into an\n // actual accessible name. The text is separately necessary: a live\n // region inserted empty has nothing to announce.\n role=\"status\"\n aria-label={label}\n data-slot=\"spinner\"\n className={cn(spinnerVariants({ size }), className)}\n >\n {/*\n Clipped rather than `sr-only` from a stylesheet: this package's CSS is\n scanned from source, but relying on a utility that only ever appears\n here is how a class silently fails to compile. Inline styles cannot.\n absolute + 1px + clip is the standard visually-hidden recipe, and it\n keeps the ring square -- the story asserts that.\n */}\n <span\n style={{\n position: \"absolute\",\n width: \"1px\",\n height: \"1px\",\n margin: \"-1px\",\n padding: 0,\n overflow: \"hidden\",\n clipPath: \"inset(50%)\",\n whiteSpace: \"nowrap\",\n borderWidth: 0,\n }}\n >\n {label}\n </span>\n </span>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,kBAAkB,IAC7B;CAGE;CAIA;CAIA;CAGA;CAwBA;CACA;CAMA;AACF,GACA;CACE,UAAU,EACR,MAAM;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAmDA,MAAa,UAAU,WAA0C,SAAS,QACxE,EAAE,WAAW,MAAM,OAAO,GAAG,SAC7B,KACA;CACA,OACE,oBAAC,QAAD;EACO;EACL,GAAI;EAMJ,MAAK;EACL,cAAY;EACZ,aAAU;EACV,WAAW,GAAG,gBAAgB,EAAE,KAAK,CAAC,GAAG,SAAS;EASlD,UAAA,oBAAC,QAAD;GACE,OAAO;IACL,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,UAAU;IACV,UAAU;IACV,YAAY;IACZ,aAAa;GACf;GAEC,UAAA;EACG,CAAA;CACF,CAAA;AAEV,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
|