@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
package/dist/tabs/tabs.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { mergeRefs } from "../lib/merge-refs.js";
|
|
3
4
|
import { cva } from "class-variance-authority";
|
|
4
|
-
import { forwardRef } from "react";
|
|
5
|
+
import { forwardRef, useEffect, useMemo, useState } from "react";
|
|
5
6
|
import { Tab, TabList, TabPanel, Tabs } from "react-aria-components";
|
|
6
7
|
import { jsx } from "react/jsx-runtime";
|
|
7
8
|
//#region src/tabs/tabs.tsx
|
|
@@ -37,13 +38,140 @@ import { jsx } from "react/jsx-runtime";
|
|
|
37
38
|
* Aria's `shouldForceMount` on TabPanel restores the old behaviour where
|
|
38
39
|
* a call site needs it.
|
|
39
40
|
*/
|
|
40
|
-
const tabsVariants = cva(["flex flex-col gap-2 box-border font-sans"]);
|
|
41
|
+
const tabsVariants = cva(["flex flex-col gap-2 box-border font-sans", "min-w-0 max-w-full"]);
|
|
41
42
|
const tabListVariants = cva([
|
|
42
43
|
"inline-flex w-fit items-center justify-center gap-1",
|
|
43
44
|
"rounded-lg p-1",
|
|
44
45
|
"bg-muted text-muted-foreground",
|
|
45
|
-
"
|
|
46
|
+
"tab-highlight",
|
|
47
|
+
"box-border",
|
|
48
|
+
"max-w-full",
|
|
49
|
+
"[--fade-tabs:--spacing(6)] [--fade-tabs-from:calc(100%-var(--fade-tabs))]",
|
|
50
|
+
"data-[overflowing]:overflow-x-auto data-[overflowing]:overscroll-x-contain",
|
|
51
|
+
"data-[overflowing]:justify-start",
|
|
52
|
+
"ltr:data-[fade-start]:mask-l-from-(--fade-tabs-from) rtl:data-[fade-start]:mask-r-from-(--fade-tabs-from)",
|
|
53
|
+
"ltr:data-[fade-end]:mask-r-from-(--fade-tabs-from) rtl:data-[fade-end]:mask-l-from-(--fade-tabs-from)"
|
|
46
54
|
]);
|
|
55
|
+
/**
|
|
56
|
+
* Sets data-overflowing, data-fade-start and data-fade-end on a tab row
|
|
57
|
+
* while its tabs are wider than its box, and on the first measurement that
|
|
58
|
+
* finds it too wide, scrolls the selected tab into view INSIDE the row
|
|
59
|
+
* (never the page, so a row below the fold does not drag the page to it).
|
|
60
|
+
*
|
|
61
|
+
* The attributes are written straight onto the element, so a scroll or a
|
|
62
|
+
* resize re-renders nothing. Arrow keys need no help: React Aria keeps the
|
|
63
|
+
* focused tab in view in the row that scrolls.
|
|
64
|
+
*/
|
|
65
|
+
const FADE_PX = 24;
|
|
66
|
+
function useScrollWhenTooWide() {
|
|
67
|
+
const [row, setRow] = useState(null);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!row || typeof ResizeObserver === "undefined") return;
|
|
70
|
+
let revealed = false;
|
|
71
|
+
const measure = () => {
|
|
72
|
+
const overflowing = row.scrollWidth > row.clientWidth + 1;
|
|
73
|
+
const scrolled = Math.abs(row.scrollLeft);
|
|
74
|
+
row.toggleAttribute("data-overflowing", overflowing);
|
|
75
|
+
row.toggleAttribute("data-fade-start", overflowing && scrolled > 1);
|
|
76
|
+
row.toggleAttribute("data-fade-end", overflowing && scrolled + row.clientWidth < row.scrollWidth - 1);
|
|
77
|
+
if (!overflowing || revealed) return;
|
|
78
|
+
revealed = true;
|
|
79
|
+
const selected = row.querySelector("[data-selected]");
|
|
80
|
+
if (!selected) return;
|
|
81
|
+
const fade = FADE_PX;
|
|
82
|
+
const box = row.getBoundingClientRect();
|
|
83
|
+
const tab = selected.getBoundingClientRect();
|
|
84
|
+
if (tab.left < box.left + fade) row.scrollLeft -= box.left + fade - tab.left;
|
|
85
|
+
else if (tab.right > box.right - fade) row.scrollLeft += tab.right - (box.right - fade);
|
|
86
|
+
measure();
|
|
87
|
+
};
|
|
88
|
+
const resized = new ResizeObserver(measure);
|
|
89
|
+
const watch = () => {
|
|
90
|
+
resized.disconnect();
|
|
91
|
+
resized.observe(row);
|
|
92
|
+
for (const tab of Array.from(row.children)) resized.observe(tab);
|
|
93
|
+
};
|
|
94
|
+
const added = new MutationObserver(() => {
|
|
95
|
+
watch();
|
|
96
|
+
measure();
|
|
97
|
+
});
|
|
98
|
+
watch();
|
|
99
|
+
added.observe(row, { childList: true });
|
|
100
|
+
row.addEventListener("scroll", measure, { passive: true });
|
|
101
|
+
measure();
|
|
102
|
+
return () => {
|
|
103
|
+
resized.disconnect();
|
|
104
|
+
added.disconnect();
|
|
105
|
+
row.removeEventListener("scroll", measure);
|
|
106
|
+
};
|
|
107
|
+
}, [row]);
|
|
108
|
+
return setRow;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Places the selection highlight (see tab-highlight in styles.css) on the selected
|
|
112
|
+
* tab and keeps it there as tabs resize, and tells the tabs root which way the
|
|
113
|
+
* selection moved (`--tab-dir`) so the next panel arrives from that side. All of it
|
|
114
|
+
* is written straight onto the elements, so no render happens. The first placement
|
|
115
|
+
* has no transition; `data-hl-ready` turns it on two frames later.
|
|
116
|
+
*/
|
|
117
|
+
function useTabHighlight() {
|
|
118
|
+
const [row, setRow] = useState(null);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!row || typeof ResizeObserver === "undefined") return;
|
|
121
|
+
let previous = null;
|
|
122
|
+
let frames = [0, 0];
|
|
123
|
+
const place = () => {
|
|
124
|
+
const selected = row.querySelector("[role=\"tab\"][data-selected]");
|
|
125
|
+
if (!selected) {
|
|
126
|
+
row.removeAttribute("data-hl");
|
|
127
|
+
previous = null;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
row.style.setProperty("--hl-x", `${selected.offsetLeft}px`);
|
|
131
|
+
row.style.setProperty("--hl-y", `${selected.offsetTop}px`);
|
|
132
|
+
row.style.setProperty("--hl-w", `${selected.offsetWidth}px`);
|
|
133
|
+
row.style.setProperty("--hl-h", `${selected.offsetHeight}px`);
|
|
134
|
+
row.setAttribute("data-hl", "");
|
|
135
|
+
if (previous && previous !== selected && previous.isConnected) {
|
|
136
|
+
const all = Array.from(row.querySelectorAll("[role=\"tab\"]"));
|
|
137
|
+
const forward = all.indexOf(selected) > all.indexOf(previous);
|
|
138
|
+
const flip = getComputedStyle(row).direction === "rtl";
|
|
139
|
+
const root = row.closest("[data-slot=\"tabs\"]");
|
|
140
|
+
root?.style.setProperty("--tab-dir", String(forward !== flip ? 1 : -1));
|
|
141
|
+
root?.setAttribute("data-tab-changed", "");
|
|
142
|
+
}
|
|
143
|
+
previous = selected;
|
|
144
|
+
};
|
|
145
|
+
const resized = new ResizeObserver(place);
|
|
146
|
+
const watch = () => {
|
|
147
|
+
resized.disconnect();
|
|
148
|
+
resized.observe(row);
|
|
149
|
+
for (const tab of Array.from(row.children)) resized.observe(tab);
|
|
150
|
+
};
|
|
151
|
+
const changed = new MutationObserver((records) => {
|
|
152
|
+
if (records.some((record) => record.type === "childList")) watch();
|
|
153
|
+
place();
|
|
154
|
+
});
|
|
155
|
+
watch();
|
|
156
|
+
changed.observe(row, {
|
|
157
|
+
attributes: true,
|
|
158
|
+
attributeFilter: ["data-selected"],
|
|
159
|
+
childList: true,
|
|
160
|
+
subtree: true
|
|
161
|
+
});
|
|
162
|
+
place();
|
|
163
|
+
frames[0] = requestAnimationFrame(() => {
|
|
164
|
+
frames[1] = requestAnimationFrame(() => row.setAttribute("data-hl-ready", ""));
|
|
165
|
+
});
|
|
166
|
+
return () => {
|
|
167
|
+
resized.disconnect();
|
|
168
|
+
changed.disconnect();
|
|
169
|
+
frames.forEach(cancelAnimationFrame);
|
|
170
|
+
frames = [0, 0];
|
|
171
|
+
};
|
|
172
|
+
}, [row]);
|
|
173
|
+
return setRow;
|
|
174
|
+
}
|
|
47
175
|
const tabVariants = cva([
|
|
48
176
|
"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap",
|
|
49
177
|
"rounded-md px-2 py-1",
|
|
@@ -51,8 +179,8 @@ const tabVariants = cva([
|
|
|
51
179
|
"cursor-pointer select-none box-border",
|
|
52
180
|
"border-(length:--border-width) border-transparent",
|
|
53
181
|
"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard",
|
|
54
|
-
"data-[selected]:bg-background data-[selected]:border-border",
|
|
55
|
-
"data-[selected]:shadow-surface",
|
|
182
|
+
"not-in-data-[hl]:data-[selected]:bg-background not-in-data-[hl]:data-[selected]:border-border",
|
|
183
|
+
"not-in-data-[hl]:data-[selected]:shadow-surface",
|
|
56
184
|
"outline-none",
|
|
57
185
|
"data-[focus-visible]:outline-solid",
|
|
58
186
|
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
@@ -64,6 +192,7 @@ const tabVariants = cva([
|
|
|
64
192
|
]);
|
|
65
193
|
const tabPanelVariants = cva([
|
|
66
194
|
"flex-1 box-border font-sans text-foreground",
|
|
195
|
+
"tab-panel-motion",
|
|
67
196
|
"outline-none",
|
|
68
197
|
"data-[focus-visible]:outline-solid",
|
|
69
198
|
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
@@ -79,8 +208,15 @@ const Tabs$1 = forwardRef(function Tabs$2({ className, ...props }, ref) {
|
|
|
79
208
|
});
|
|
80
209
|
});
|
|
81
210
|
const TabList$1 = forwardRef(function TabList$2({ className, ...props }, ref) {
|
|
82
|
-
|
|
211
|
+
const watch = useScrollWhenTooWide();
|
|
212
|
+
const glide = useTabHighlight();
|
|
213
|
+
const merged = useMemo(() => mergeRefs(ref, watch, glide), [
|
|
83
214
|
ref,
|
|
215
|
+
watch,
|
|
216
|
+
glide
|
|
217
|
+
]);
|
|
218
|
+
return /* @__PURE__ */ jsx(TabList, {
|
|
219
|
+
ref: merged,
|
|
84
220
|
...props,
|
|
85
221
|
"data-slot": "tab-list",
|
|
86
222
|
className: cn(tabListVariants(), className)
|
package/dist/tabs/tabs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.js","names":["Tabs","AriaTabs","TabList","AriaTabList","Tab","AriaTab","TabPanel","AriaTabPanel"],"sources":["../../src/tabs/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport {\n Tab as AriaTab,\n TabList as AriaTabList,\n TabPanel as AriaTabPanel,\n Tabs as AriaTabs,\n type TabListProps as AriaTabListProps,\n type TabPanelProps as AriaTabPanelProps,\n type TabProps as AriaTabProps,\n type TabsProps as AriaTabsProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Tabs, ported from the legacy shadcn/Radix component.\n *\n * The largest legacy surface in this migration: 18 files, `TabsTrigger`\n * 51, `TabsContent` 31, `Tabs` 17, `TabsList` 17. Props: `value` 88,\n * `className` 76, `defaultValue` 12, `onValueChange` 10, `disabled` 6.\n *\n * The rename is mechanical but it touches every one of those:\n * `value` -> `selectedKey`, `defaultValue` -> `defaultSelectedKey`,\n * `onValueChange` -> `onSelectionChange`, and the `value` on each trigger\n * and panel becomes `id`. React Aria pairs a Tab to its TabPanel by that\n * id, exactly as Radix paired them by value.\n *\n * THE SELECTED TAB GAINS A BORDER, and that is a contrast fix rather than\n * a decoration. The legacy marks the selected tab with `bg-background` on\n * a `bg-muted` list -- 1.13:1, which is not a boundary anybody can see --\n * and leans on `shadow-sm` to carry the rest. Under WCAG 1.4.11 the thing\n * telling you which tab is active has to clear 3:1. `border-border` on\n * `muted` is 4.65:1 and is already in the contrast contract.\n *\n * Two bracket values are gone because check:hardcoding rejects them:\n * `p-[3px]` on the list and `h-[calc(100%-1px)]` on the trigger. The list\n * is `p-1` and the trigger sizes to the list rather than to a value one\n * pixel short of it.\n *\n * ONE BEHAVIOUR DIFFERS AND IS WORTH KNOWING. Radix keeps every mounted\n * TabsContent in the DOM and hides the inactive ones; React Aria renders\n * ONLY the selected panel. A panel that holds uncommitted form state, a\n * running video or a scroll position will lose it on tab change. React\n * Aria's `shouldForceMount` on TabPanel restores the old behaviour where\n * a call site needs it.\n */\nexport const tabsVariants = cva([\"flex flex-col gap-2 box-border font-sans\"]);\n\nexport const tabListVariants = cva([\n \"inline-flex w-fit items-center justify-center gap-1\",\n // p-1, not the legacy p-[3px]: a bracket value check:hardcoding rejects.\n \"rounded-lg p-1\",\n \"bg-muted text-muted-foreground\",\n \"box-border\",\n]);\n\nexport const tabVariants = cva([\n \"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap\",\n \"rounded-md px-2 py-1\",\n \"text-sm leading-(--leading-normal) font-medium text-foreground\",\n \"cursor-pointer select-none box-border\",\n // A transparent border at rest so selecting one does not move the text.\n \"border-(length:--border-width) border-transparent\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n\n // The selected tab. bg-background on a muted list is 1.13:1 -- invisible\n // as a boundary -- so the border is what satisfies WCAG 1.4.11 at\n // 4.65:1. The fill and the shadow are the polish on top of it.\n \"data-[selected]:bg-background data-[selected]:border-border\",\n \"data-[selected]:shadow-surface\",\n\n // outline-solid is load-bearing: outline-none sets --tw-outline-style to\n // none and the width utility resolves its style from that same variable,\n // so a focus rule can apply a width and a colour and paint nothing.\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\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const tabPanelVariants = cva([\n \"flex-1 box-border font-sans text-foreground\",\n // React Aria gives the panel tabindex=0 when it holds no focusable\n // child, so it can take a ring it does not want at rest.\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]);\n\n/**\n * A set of panels, one visible at a time.\n *\n * @example\n * <Tabs defaultSelectedKey=\"overview\">\n * <TabList aria-label=\"Sections\">\n * <Tab id=\"overview\">Overview</Tab>\n * <Tab id=\"activity\">Activity</Tab>\n * </TabList>\n * <TabPanel id=\"overview\">...</TabPanel>\n * <TabPanel id=\"activity\">...</TabPanel>\n * </Tabs>\n */\nexport interface TabsProps extends Omit<AriaTabsProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Tabs = forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabs ref={ref} {...props} data-slot=\"tabs\" className={cn(tabsVariants(), className)} />\n );\n});\n\n/**\n * The row of tabs.\n *\n * `aria-label` is worth passing: a tablist is a landmark-ish grouping and\n * a page with two of them is ambiguous without one.\n */\nexport interface TabListProps extends Omit<AriaTabListProps<object>, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabList = forwardRef<HTMLDivElement, TabListProps>(function TabList(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabList\n ref={ref}\n {...props}\n data-slot=\"tab-list\"\n className={cn(tabListVariants(), className)}\n />\n );\n});\n\n/** One tab. Its `id` pairs it with the panel of the same `id`. */\nexport interface TabProps extends Omit<AriaTabProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTab ref={ref} {...props} data-slot=\"tab\" className={cn(tabVariants(), className)} />\n );\n});\n\n/** One panel. Its `id` pairs it with the tab of the same `id`. */\nexport interface TabPanelProps extends Omit<AriaTabPanelProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(function TabPanel(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabPanel\n ref={ref}\n {...props}\n data-slot=\"tab-panel\"\n className={cn(tabPanelVariants(), className)}\n />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,eAAe,IAAI,CAAC,0CAA0C,CAAC;AAE5E,MAAa,kBAAkB,IAAI;CACjC;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,cAAc,IAAI;CAC7B;CACA;CACA;CACA;CAEA;CACA;CAKA;CACA;CAKA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,mBAAmB,IAAI;CAClC;CAGA;CACA;CACA;CACA;CACA;AACF,CAAC;AAoBD,MAAaA,SAAO,WAAsC,SAASA,OACjE,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,MAAD;EAAe;EAAK,GAAI;EAAO,aAAU;EAAO,WAAW,GAAG,aAAa,GAAG,SAAS;CAAI,CAAA;AAE/F,CAAC;AAYD,MAAaC,YAAU,WAAyC,SAASA,UACvE,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,SAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,gBAAgB,GAAG,SAAS;CAC3C,CAAA;AAEL,CAAC;AAOD,MAAaC,QAAM,WAAqC,SAASA,MAC/D,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,KAAD;EAAc;EAAK,GAAI;EAAO,aAAU;EAAM,WAAW,GAAG,YAAY,GAAG,SAAS;CAAI,CAAA;AAE5F,CAAC;AAOD,MAAaC,aAAW,WAA0C,SAASA,WACzE,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,UAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;CAC5C,CAAA;AAEL,CAAC"}
|
|
1
|
+
{"version":3,"file":"tabs.js","names":["Tabs","AriaTabs","TabList","AriaTabList","Tab","AriaTab","TabPanel","AriaTabPanel"],"sources":["../../src/tabs/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useEffect, useMemo, useState } from \"react\";\nimport {\n Tab as AriaTab,\n TabList as AriaTabList,\n TabPanel as AriaTabPanel,\n Tabs as AriaTabs,\n type TabListProps as AriaTabListProps,\n type TabPanelProps as AriaTabPanelProps,\n type TabProps as AriaTabProps,\n type TabsProps as AriaTabsProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\n\n/**\n * Tabs, ported from the legacy shadcn/Radix component.\n *\n * The largest legacy surface in this migration: 18 files, `TabsTrigger`\n * 51, `TabsContent` 31, `Tabs` 17, `TabsList` 17. Props: `value` 88,\n * `className` 76, `defaultValue` 12, `onValueChange` 10, `disabled` 6.\n *\n * The rename is mechanical but it touches every one of those:\n * `value` -> `selectedKey`, `defaultValue` -> `defaultSelectedKey`,\n * `onValueChange` -> `onSelectionChange`, and the `value` on each trigger\n * and panel becomes `id`. React Aria pairs a Tab to its TabPanel by that\n * id, exactly as Radix paired them by value.\n *\n * THE SELECTED TAB GAINS A BORDER, and that is a contrast fix rather than\n * a decoration. The legacy marks the selected tab with `bg-background` on\n * a `bg-muted` list -- 1.13:1, which is not a boundary anybody can see --\n * and leans on `shadow-sm` to carry the rest. Under WCAG 1.4.11 the thing\n * telling you which tab is active has to clear 3:1. `border-border` on\n * `muted` is 4.65:1 and is already in the contrast contract.\n *\n * Two bracket values are gone because check:hardcoding rejects them:\n * `p-[3px]` on the list and `h-[calc(100%-1px)]` on the trigger. The list\n * is `p-1` and the trigger sizes to the list rather than to a value one\n * pixel short of it.\n *\n * ONE BEHAVIOUR DIFFERS AND IS WORTH KNOWING. Radix keeps every mounted\n * TabsContent in the DOM and hides the inactive ones; React Aria renders\n * ONLY the selected panel. A panel that holds uncommitted form state, a\n * running video or a scroll position will lose it on tab change. React\n * Aria's `shouldForceMount` on TabPanel restores the old behaviour where\n * a call site needs it.\n */\nexport const tabsVariants = cva([\n \"flex flex-col gap-2 box-border font-sans\",\n // May be narrower than its tabs: in a flex row (a toolbar beside a search\n // field) it shrinks to the space it has instead of pushing the page wider.\n \"min-w-0 max-w-full\",\n]);\n\nexport const tabListVariants = cva([\n \"inline-flex w-fit items-center justify-center gap-1\",\n // p-1, not the legacy p-[3px]: a bracket value check:hardcoding rejects.\n \"rounded-lg p-1\",\n \"bg-muted text-muted-foreground\",\n // The selection highlight that glides between tabs (styles.css).\n \"tab-highlight\",\n \"box-border\",\n\n // A ROW WIDER THAN ITS BOX SCROLLS INSIDE IT. Capped at the parent's\n // width; while its tabs are wider than that, TabList sets\n // data-overflowing and the row scrolls sideways on its own. A row that\n // fits never carries the attribute, so nothing about it changes.\n \"max-w-full\",\n // How wide the fade is, and where it starts measured from the far edge.\n \"[--fade-tabs:--spacing(6)] [--fade-tabs-from:calc(100%-var(--fade-tabs))]\",\n \"data-[overflowing]:overflow-x-auto data-[overflowing]:overscroll-x-contain\",\n // justify-center spills centred overflow out of BOTH sides, and a scroll\n // box cannot reach the spill on its start side: the first tab would be\n // cut off for good. Scrolling rows start at the start edge.\n \"data-[overflowing]:justify-start\",\n // The cue that there is more: the side with tabs out of view fades out.\n // Masks are physical, so the start and end swap sides in RTL.\n \"ltr:data-[fade-start]:mask-l-from-(--fade-tabs-from) rtl:data-[fade-start]:mask-r-from-(--fade-tabs-from)\",\n \"ltr:data-[fade-end]:mask-r-from-(--fade-tabs-from) rtl:data-[fade-end]:mask-l-from-(--fade-tabs-from)\",\n]);\n\n/**\n * Sets data-overflowing, data-fade-start and data-fade-end on a tab row\n * while its tabs are wider than its box, and on the first measurement that\n * finds it too wide, scrolls the selected tab into view INSIDE the row\n * (never the page, so a row below the fold does not drag the page to it).\n *\n * The attributes are written straight onto the element, so a scroll or a\n * resize re-renders nothing. Arrow keys need no help: React Aria keeps the\n * focused tab in view in the row that scrolls.\n */\nconst FADE_PX = 24;\n\nfunction useScrollWhenTooWide() {\n const [row, setRow] = useState<HTMLDivElement | null>(null);\n useEffect(() => {\n if (!row || typeof ResizeObserver === \"undefined\") return;\n let revealed = false;\n const measure = () => {\n const overflowing = row.scrollWidth > row.clientWidth + 1;\n // Distance from the start edge; scrollLeft runs negative in RTL.\n const scrolled = Math.abs(row.scrollLeft);\n row.toggleAttribute(\"data-overflowing\", overflowing);\n row.toggleAttribute(\"data-fade-start\", overflowing && scrolled > 1);\n row.toggleAttribute(\n \"data-fade-end\",\n overflowing && scrolled + row.clientWidth < row.scrollWidth - 1,\n );\n if (!overflowing || revealed) return;\n revealed = true;\n const selected = row.querySelector<HTMLElement>(\"[data-selected]\");\n if (!selected) return;\n // Clear of the fade: --spacing(6), the width the mask draws.\n const fade = FADE_PX;\n const box = row.getBoundingClientRect();\n const tab = selected.getBoundingClientRect();\n if (tab.left < box.left + fade) row.scrollLeft -= box.left + fade - tab.left;\n else if (tab.right > box.right - fade) row.scrollLeft += tab.right - (box.right - fade);\n // The scroll event lands a frame later; the fades are right on first paint.\n measure();\n };\n // The row's box changes with its parent; a tab's width changes when its\n // label does (a count that loads).\n const resized = new ResizeObserver(measure);\n const watch = () => {\n resized.disconnect();\n resized.observe(row);\n for (const tab of Array.from(row.children)) resized.observe(tab);\n };\n const added = new MutationObserver(() => {\n watch();\n measure();\n });\n watch();\n added.observe(row, { childList: true });\n row.addEventListener(\"scroll\", measure, { passive: true });\n measure();\n return () => {\n resized.disconnect();\n added.disconnect();\n row.removeEventListener(\"scroll\", measure);\n };\n }, [row]);\n return setRow;\n}\n\n/**\n * Places the selection highlight (see tab-highlight in styles.css) on the selected\n * tab and keeps it there as tabs resize, and tells the tabs root which way the\n * selection moved (`--tab-dir`) so the next panel arrives from that side. All of it\n * is written straight onto the elements, so no render happens. The first placement\n * has no transition; `data-hl-ready` turns it on two frames later.\n */\nfunction useTabHighlight() {\n const [row, setRow] = useState<HTMLDivElement | null>(null);\n useEffect(() => {\n if (!row || typeof ResizeObserver === \"undefined\") return;\n let previous: HTMLElement | null = null;\n let frames = [0, 0];\n const place = () => {\n const selected = row.querySelector<HTMLElement>('[role=\"tab\"][data-selected]');\n if (!selected) {\n row.removeAttribute(\"data-hl\");\n previous = null;\n return;\n }\n row.style.setProperty(\"--hl-x\", `${selected.offsetLeft}px`);\n row.style.setProperty(\"--hl-y\", `${selected.offsetTop}px`);\n row.style.setProperty(\"--hl-w\", `${selected.offsetWidth}px`);\n row.style.setProperty(\"--hl-h\", `${selected.offsetHeight}px`);\n row.setAttribute(\"data-hl\", \"\");\n if (previous && previous !== selected && previous.isConnected) {\n const all = Array.from(row.querySelectorAll('[role=\"tab\"]'));\n const forward = all.indexOf(selected) > all.indexOf(previous);\n // The row reads right to left in RTL, so the same order change is the other way.\n const flip = getComputedStyle(row).direction === \"rtl\";\n const root = row.closest<HTMLElement>('[data-slot=\"tabs\"]');\n root?.style.setProperty(\"--tab-dir\", String(forward !== flip ? 1 : -1));\n root?.setAttribute(\"data-tab-changed\", \"\");\n }\n previous = selected;\n };\n const resized = new ResizeObserver(place);\n const watch = () => {\n resized.disconnect();\n resized.observe(row);\n for (const tab of Array.from(row.children)) resized.observe(tab);\n };\n const changed = new MutationObserver((records) => {\n if (records.some((record) => record.type === \"childList\")) watch();\n place();\n });\n watch();\n changed.observe(row, { attributes: true, attributeFilter: [\"data-selected\"], childList: true, subtree: true });\n place();\n frames[0] = requestAnimationFrame(() => {\n frames[1] = requestAnimationFrame(() => row.setAttribute(\"data-hl-ready\", \"\"));\n });\n return () => {\n resized.disconnect();\n changed.disconnect();\n frames.forEach(cancelAnimationFrame);\n frames = [0, 0];\n };\n }, [row]);\n return setRow;\n}\n\nexport const tabVariants = cva([\n \"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap\",\n \"rounded-md px-2 py-1\",\n \"text-sm leading-(--leading-normal) font-medium text-foreground\",\n \"cursor-pointer select-none box-border\",\n // A transparent border at rest so selecting one does not move the text.\n \"border-(length:--border-width) border-transparent\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n\n // The selected tab. bg-background on a muted list is 1.13:1 -- invisible\n // as a boundary -- so the border is what satisfies WCAG 1.4.11 at\n // 4.65:1. The fill and the shadow are the polish on top of it.\n // Until the row has placed its gliding highlight (data-hl), the tab paints\n // its own fill so there is never a frame without a boundary.\n \"not-in-data-[hl]:data-[selected]:bg-background not-in-data-[hl]:data-[selected]:border-border\",\n \"not-in-data-[hl]:data-[selected]:shadow-surface\",\n\n // outline-solid is load-bearing: outline-none sets --tw-outline-style to\n // none and the width utility resolves its style from that same variable,\n // so a focus rule can apply a width and a colour and paint nothing.\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\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const tabPanelVariants = cva([\n \"flex-1 box-border font-sans text-foreground\",\n // Arrives from the side the new tab sits on, once the selection has changed.\n \"tab-panel-motion\",\n // React Aria gives the panel tabindex=0 when it holds no focusable\n // child, so it can take a ring it does not want at rest.\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]);\n\n/**\n * A set of panels, one visible at a time.\n *\n * @example\n * <Tabs defaultSelectedKey=\"overview\">\n * <TabList aria-label=\"Sections\">\n * <Tab id=\"overview\">Overview</Tab>\n * <Tab id=\"activity\">Activity</Tab>\n * </TabList>\n * <TabPanel id=\"overview\">...</TabPanel>\n * <TabPanel id=\"activity\">...</TabPanel>\n * </Tabs>\n */\nexport interface TabsProps extends Omit<AriaTabsProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Tabs = forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabs ref={ref} {...props} data-slot=\"tabs\" className={cn(tabsVariants(), className)} />\n );\n});\n\n/**\n * The row of tabs.\n *\n * `aria-label` is worth passing: a tablist is a landmark-ish grouping and\n * a page with two of them is ambiguous without one.\n */\nexport interface TabListProps extends Omit<AriaTabListProps<object>, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabList = forwardRef<HTMLDivElement, TabListProps>(function TabList(\n { className, ...props },\n ref,\n) {\n const watch = useScrollWhenTooWide();\n const glide = useTabHighlight();\n // Memoised: a new callback every render would detach and reattach the row.\n const merged = useMemo(() => mergeRefs(ref, watch, glide), [ref, watch, glide]);\n return (\n <AriaTabList\n ref={merged}\n {...props}\n data-slot=\"tab-list\"\n className={cn(tabListVariants(), className)}\n />\n );\n});\n\n/** One tab. Its `id` pairs it with the panel of the same `id`. */\nexport interface TabProps extends Omit<AriaTabProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab({ className, ...props }, ref) {\n return <AriaTab ref={ref} {...props} data-slot=\"tab\" className={cn(tabVariants(), className)} />;\n});\n\n/** One panel. Its `id` pairs it with the tab of the same `id`. */\nexport interface TabPanelProps extends Omit<AriaTabPanelProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(function TabPanel(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabPanel\n ref={ref}\n {...props}\n data-slot=\"tab-panel\"\n className={cn(tabPanelVariants(), className)}\n />\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
1
2
|
import { TextAreaProps } from "react-aria-components";
|
|
2
3
|
//#region src/textarea/textarea.d.ts
|
|
3
4
|
/**
|
|
@@ -21,6 +22,28 @@ export declare const textareaVariants: (props?: import("class-variance-authority
|
|
|
21
22
|
export interface TextareaProps extends Omit<TextAreaProps, "className" | "style"> {
|
|
22
23
|
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
23
24
|
className?: string;
|
|
25
|
+
/**
|
|
26
|
+
* A visible label above the field, tied to it with `htmlFor`. Passing any of
|
|
27
|
+
* `label`, `description` or `error` (even as `undefined`) wraps the field;
|
|
28
|
+
* with none of them the DOM is exactly the bare textarea. Do not also pass
|
|
29
|
+
* `aria-label`: it wins over the label (WCAG 2.5.3). Ignored inside a React
|
|
30
|
+
* Aria `TextField`, which supplies its own label and messages.
|
|
31
|
+
*/
|
|
32
|
+
label?: ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* Helper text under the field, named by `aria-describedby`. A count in it,
|
|
35
|
+
* such as "12/200 characters", rolls digit by digit as it changes. Opens on a
|
|
36
|
+
* spring when it appears after mount; nothing animates on first render, and
|
|
37
|
+
* reduced motion swaps it instantly. A string, because the copy is animated
|
|
38
|
+
* word by word.
|
|
39
|
+
*/
|
|
40
|
+
description?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Error text under the field. Sets `aria-invalid`, so the field takes its
|
|
43
|
+
* invalid border, and is announced as an alert when it appears. An empty or
|
|
44
|
+
* `undefined` value closes the row.
|
|
45
|
+
*/
|
|
46
|
+
error?: string;
|
|
24
47
|
}
|
|
25
48
|
export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
26
49
|
//#endregion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { fieldSurfaceVariants } from "../lib/field-surface.js";
|
|
4
|
+
import { FieldShell, hasFieldProps, useFieldWiring } from "../lib/field-shell.js";
|
|
4
5
|
import { cva } from "class-variance-authority";
|
|
5
6
|
import { forwardRef } from "react";
|
|
6
7
|
import { TextArea } from "react-aria-components";
|
|
@@ -25,13 +26,34 @@ const textareaVariants = cva([
|
|
|
25
26
|
"resize-y",
|
|
26
27
|
"leading-(--leading-normal)"
|
|
27
28
|
]);
|
|
28
|
-
const Textarea = forwardRef(function Textarea(
|
|
29
|
-
|
|
29
|
+
const Textarea = forwardRef(function Textarea(allProps, ref) {
|
|
30
|
+
const { className, label, description, error, ...props } = allProps;
|
|
31
|
+
const field = useFieldWiring({
|
|
32
|
+
enabled: hasFieldProps(allProps),
|
|
33
|
+
id: props.id,
|
|
34
|
+
describedBy: props["aria-describedby"],
|
|
35
|
+
invalid: props["aria-invalid"],
|
|
36
|
+
description,
|
|
37
|
+
error
|
|
38
|
+
});
|
|
39
|
+
const textarea = /* @__PURE__ */ jsx(TextArea, {
|
|
30
40
|
ref,
|
|
31
41
|
...props,
|
|
42
|
+
...field.controlProps,
|
|
32
43
|
"data-slot": "textarea",
|
|
33
44
|
className: cn(textareaVariants(), className)
|
|
34
45
|
});
|
|
46
|
+
if (!field.active) return textarea;
|
|
47
|
+
return /* @__PURE__ */ jsx(FieldShell, {
|
|
48
|
+
label,
|
|
49
|
+
controlId: field.controlId,
|
|
50
|
+
description,
|
|
51
|
+
hintId: field.hintId,
|
|
52
|
+
error,
|
|
53
|
+
errorId: field.errorId,
|
|
54
|
+
wordExit: "quick",
|
|
55
|
+
children: textarea
|
|
56
|
+
});
|
|
35
57
|
});
|
|
36
58
|
//#endregion
|
|
37
59
|
export { Textarea, textareaVariants };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.js","names":["AriaTextArea"],"sources":["../../src/textarea/textarea.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport {\n TextArea as AriaTextArea,\n type TextAreaProps as AriaTextAreaProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Textarea, ported from the legacy component onto React Aria's TextArea.\n *\n * Census: 3 files, 3 uses -- `value`, `onChange`, `placeholder`, `id`,\n * `ref`. All native props, all still native here.\n *\n * The surface comes from `fieldSurfaceVariants`, shared with Input. The two\n * legacy files styled it separately and had already drifted -- different\n * background, different focus ring, invalid styling on one and not the\n * other -- and none of those differences was a decision anyone made.\n */\nexport const textareaVariants = cva([\n fieldSurfaceVariants(),\n\n // min-h-20 is 80px, the legacy min-h-[80px] -- but as a spacing-scale step\n // rather than a bracket value, which check:hardcoding rejects.\n \"min-h-20\",\n \"px-(--density-control-pad-md) py-(--density-control-pad-sm)\",\n \"text-base\",\n\n // A textarea is resizable by the user agent, and horizontal resize breaks\n // any layout it sits in. Vertical only.\n \"resize-y\",\n // Height is content, not a control slot, so the line-height has to be set\n // explicitly -- unset it computes to `normal`, which varies with the\n // font's own metrics.\n \"leading-(--leading-normal)\",\n]);\n\n/**\n * A multi-line text field.\n *\n * @example\n * <Textarea placeholder=\"Tell us more\" value={v} onChange={e => set(e.target.value)} />\n */\nexport interface TextareaProps extends Omit<AriaTextAreaProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n
|
|
1
|
+
{"version":3,"file":"textarea.js","names":["AriaTextArea"],"sources":["../../src/textarea/textarea.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n TextArea as AriaTextArea,\n type TextAreaProps as AriaTextAreaProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { FieldShell, hasFieldProps, useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Textarea, ported from the legacy component onto React Aria's TextArea.\n *\n * Census: 3 files, 3 uses -- `value`, `onChange`, `placeholder`, `id`,\n * `ref`. All native props, all still native here.\n *\n * The surface comes from `fieldSurfaceVariants`, shared with Input. The two\n * legacy files styled it separately and had already drifted -- different\n * background, different focus ring, invalid styling on one and not the\n * other -- and none of those differences was a decision anyone made.\n */\nexport const textareaVariants = cva([\n fieldSurfaceVariants(),\n\n // min-h-20 is 80px, the legacy min-h-[80px] -- but as a spacing-scale step\n // rather than a bracket value, which check:hardcoding rejects.\n \"min-h-20\",\n \"px-(--density-control-pad-md) py-(--density-control-pad-sm)\",\n \"text-base\",\n\n // A textarea is resizable by the user agent, and horizontal resize breaks\n // any layout it sits in. Vertical only.\n \"resize-y\",\n // Height is content, not a control slot, so the line-height has to be set\n // explicitly -- unset it computes to `normal`, which varies with the\n // font's own metrics.\n \"leading-(--leading-normal)\",\n]);\n\n/**\n * A multi-line text field.\n *\n * @example\n * <Textarea placeholder=\"Tell us more\" value={v} onChange={e => set(e.target.value)} />\n */\nexport interface TextareaProps extends Omit<AriaTextAreaProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * A visible label above the field, tied to it with `htmlFor`. Passing any of\n * `label`, `description` or `error` (even as `undefined`) wraps the field;\n * with none of them the DOM is exactly the bare textarea. Do not also pass\n * `aria-label`: it wins over the label (WCAG 2.5.3). Ignored inside a React\n * Aria `TextField`, which supplies its own label and messages.\n */\n label?: ReactNode;\n /**\n * Helper text under the field, named by `aria-describedby`. A count in it,\n * such as \"12/200 characters\", rolls digit by digit as it changes. Opens on a\n * spring when it appears after mount; nothing animates on first render, and\n * reduced motion swaps it instantly. A string, because the copy is animated\n * word by word.\n */\n description?: string;\n /**\n * Error text under the field. Sets `aria-invalid`, so the field takes its\n * invalid border, and is announced as an alert when it appears. An empty or\n * `undefined` value closes the row.\n */\n error?: string;\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(\n function Textarea(allProps, ref) {\n const { className, label, description, error, ...props } = allProps;\n const field = useFieldWiring({\n enabled: hasFieldProps(allProps),\n id: props.id,\n describedBy: props[\"aria-describedby\"],\n invalid: props[\"aria-invalid\"],\n description,\n error,\n });\n const textarea = (\n <AriaTextArea\n ref={ref}\n {...props}\n {...field.controlProps}\n data-slot=\"textarea\"\n className={cn(textareaVariants(), className)}\n />\n );\n if (!field.active) return textarea;\n return (\n <FieldShell\n label={label}\n controlId={field.controlId}\n description={description}\n hintId={field.hintId}\n error={error}\n errorId={field.errorId}\n wordExit=\"quick\"\n >\n {textarea}\n </FieldShell>\n );\n },\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,mBAAmB,IAAI;CAClC,qBAAqB;CAIrB;CACA;CACA;CAIA;CAIA;AACF,CAAC;AAmCD,MAAa,WAAW,WACtB,SAAS,SAAS,UAAU,KAAK;CAC/B,MAAM,EAAE,WAAW,OAAO,aAAa,OAAO,GAAG,UAAU;CAC3D,MAAM,QAAQ,eAAe;EAC3B,SAAS,cAAc,QAAQ;EAC/B,IAAI,MAAM;EACV,aAAa,MAAM;EACnB,SAAS,MAAM;EACf;EACA;CACF,CAAC;CACD,MAAM,WACJ,oBAACA,UAAD;EACO;EACL,GAAI;EACJ,GAAI,MAAM;EACV,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;CAC5C,CAAA;CAEH,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,OACE,oBAAC,YAAD;EACS;EACP,WAAW,MAAM;EACJ;EACb,QAAQ,MAAM;EACP;EACP,SAAS,MAAM;EACf,UAAS;EAER,UAAA;CACS,CAAA;AAEhB,CACF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/theme-switch/theme-switch.d.ts
|
|
2
|
+
export type Theme = "light" | "dark";
|
|
3
|
+
/** Which way the page transition travels. The switch nudges its icon the same way; the transition itself is the caller's. */
|
|
4
|
+
export type ThemeSwitchVariant = "reveal" | "eclipse" | "split" | "rise";
|
|
5
|
+
export interface ThemeSwitchProps {
|
|
6
|
+
/** The current theme. The switch shows the icon for it and offers the other. */
|
|
7
|
+
theme: Theme;
|
|
8
|
+
/** @default "reveal" */
|
|
9
|
+
variant?: ThemeSwitchVariant;
|
|
10
|
+
/**
|
|
11
|
+
* Called when the switch is pressed, with the theme to move to, the variant,
|
|
12
|
+
* and the button (the origin a page transition can grow from). The component
|
|
13
|
+
* does not change the theme itself: it is controlled.
|
|
14
|
+
*/
|
|
15
|
+
onThemeChange: (next: Theme, variant: ThemeSwitchVariant, trigger: HTMLElement) => void;
|
|
16
|
+
/** The accessible name. @default "Switch to light mode" / "Switch to dark mode" */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Only the icon, a square button. @default false */
|
|
19
|
+
iconOnly?: boolean;
|
|
20
|
+
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A light/dark switch. The sun and the moon turn the same way, clockwise into
|
|
25
|
+
* dark and back out of it, so the swap reads as one rotation; the icon is
|
|
26
|
+
* nudged the way the page transition travels (`variant`) and eases back when
|
|
27
|
+
* the theme returns.
|
|
28
|
+
*
|
|
29
|
+
* Controlled: it shows `theme` and calls `onThemeChange`; it never changes
|
|
30
|
+
* the theme itself, so the page transition can run in the caller.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* <ThemeSwitch theme={theme} onThemeChange={(next) => setTheme(next)} />
|
|
34
|
+
*/
|
|
35
|
+
export declare const ThemeSwitch: import("react").ForwardRefExoticComponent<ThemeSwitchProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
36
|
+
//#endregion
|
|
37
|
+
//# sourceMappingURL=theme-switch.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { pressScaleFor, useExitBackstop, useLeavingItems } from "../lib/motion.js";
|
|
4
|
+
import { buttonVariants } from "../button/button.js";
|
|
5
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { Button } from "react-aria-components";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/theme-switch/theme-switch.tsx
|
|
9
|
+
/**
|
|
10
|
+
* A stored preference usually reaches the theme prop just after hydration. That
|
|
11
|
+
* correction swaps the icon in place; only changes after the first painted
|
|
12
|
+
* frames animate, so a page that loads in dark mode never spins its switches.
|
|
13
|
+
*/
|
|
14
|
+
function useSettled() {
|
|
15
|
+
const [settled, setSettled] = useState(false);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let second = 0;
|
|
18
|
+
const first = requestAnimationFrame(() => {
|
|
19
|
+
second = requestAnimationFrame(() => setSettled(true));
|
|
20
|
+
});
|
|
21
|
+
return () => {
|
|
22
|
+
cancelAnimationFrame(first);
|
|
23
|
+
cancelAnimationFrame(second);
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
return settled;
|
|
27
|
+
}
|
|
28
|
+
const ANGLE = {
|
|
29
|
+
light: "30deg",
|
|
30
|
+
dark: "-30deg"
|
|
31
|
+
};
|
|
32
|
+
function Icon({ theme }) {
|
|
33
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
34
|
+
viewBox: "0 0 24 24",
|
|
35
|
+
fill: "none",
|
|
36
|
+
stroke: "currentColor",
|
|
37
|
+
strokeWidth: 1.75,
|
|
38
|
+
strokeLinecap: "round",
|
|
39
|
+
strokeLinejoin: "round",
|
|
40
|
+
"aria-hidden": "true",
|
|
41
|
+
className: "size-4",
|
|
42
|
+
children: theme === "light" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("circle", {
|
|
43
|
+
cx: "12",
|
|
44
|
+
cy: "12",
|
|
45
|
+
r: "4"
|
|
46
|
+
}), /* @__PURE__ */ jsx("path", { d: "M12 2v2M12 20v2m-7.07-17.07 1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" })] }) : /* @__PURE__ */ jsx("path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" })
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function LeavingIcon({ id, theme, settled, onDone }) {
|
|
50
|
+
const remove = useCallback(() => onDone(id), [onDone, id]);
|
|
51
|
+
useExitBackstop(true, remove);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!settled) remove();
|
|
54
|
+
}, [settled, remove]);
|
|
55
|
+
if (!settled) return null;
|
|
56
|
+
return /* @__PURE__ */ jsx("span", {
|
|
57
|
+
"aria-hidden": "true",
|
|
58
|
+
onAnimationEnd: (event) => {
|
|
59
|
+
if (event.target === event.currentTarget) remove();
|
|
60
|
+
},
|
|
61
|
+
style: { "--theme-angle": ANGLE[theme] },
|
|
62
|
+
className: "absolute grid size-4 place-items-center motion-safe:animate-theme-icon-out motion-reduce:animate-fade-out",
|
|
63
|
+
children: /* @__PURE__ */ jsx(Icon, { theme })
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A light/dark switch. The sun and the moon turn the same way, clockwise into
|
|
68
|
+
* dark and back out of it, so the swap reads as one rotation; the icon is
|
|
69
|
+
* nudged the way the page transition travels (`variant`) and eases back when
|
|
70
|
+
* the theme returns.
|
|
71
|
+
*
|
|
72
|
+
* Controlled: it shows `theme` and calls `onThemeChange`; it never changes
|
|
73
|
+
* the theme itself, so the page transition can run in the caller.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* <ThemeSwitch theme={theme} onThemeChange={(next) => setTheme(next)} />
|
|
77
|
+
*/
|
|
78
|
+
const ThemeSwitch = forwardRef(function ThemeSwitch({ theme, variant = "reveal", onThemeChange, label, iconOnly = false, className }, ref) {
|
|
79
|
+
const settled = useSettled();
|
|
80
|
+
const next = theme === "light" ? "dark" : "light";
|
|
81
|
+
const phaseRef = useRef(null);
|
|
82
|
+
const { leaving, changed, remove } = useLeavingItems(theme, theme, () => phaseRef.current);
|
|
83
|
+
const pressStart = (event) => {
|
|
84
|
+
const target = event.target;
|
|
85
|
+
target.style.setProperty("--press-scale", pressScaleFor(target.offsetWidth));
|
|
86
|
+
};
|
|
87
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
88
|
+
ref,
|
|
89
|
+
"data-slot": "button",
|
|
90
|
+
"data-mode": theme,
|
|
91
|
+
"data-variant": variant,
|
|
92
|
+
"aria-label": label ?? "Switch to " + next + " mode",
|
|
93
|
+
"aria-pressed": theme === "dark",
|
|
94
|
+
onPressStart: pressStart,
|
|
95
|
+
onPress: (event) => onThemeChange(next, variant, event.target),
|
|
96
|
+
className: cn(buttonVariants({
|
|
97
|
+
variant: "outlineNeutral",
|
|
98
|
+
size: "sm"
|
|
99
|
+
}) + (iconOnly ? " w-(--density-control-h-sm) px-0" : ""), className),
|
|
100
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
101
|
+
"data-settled": settled || void 0,
|
|
102
|
+
className: "theme-nudge relative inline-grid size-5 shrink-0 place-items-center",
|
|
103
|
+
children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingIcon, {
|
|
104
|
+
id: item.id,
|
|
105
|
+
theme: item.value,
|
|
106
|
+
settled,
|
|
107
|
+
onDone: remove
|
|
108
|
+
}, item.id)), /* @__PURE__ */ jsx("span", {
|
|
109
|
+
ref: phaseRef,
|
|
110
|
+
style: { "--theme-angle": ANGLE[theme] },
|
|
111
|
+
className: cn("absolute grid size-4 place-items-center", changed && settled && "motion-safe:animate-theme-icon-in motion-reduce:animate-fade-in"),
|
|
112
|
+
children: /* @__PURE__ */ jsx(Icon, { theme })
|
|
113
|
+
}, theme)]
|
|
114
|
+
}), iconOnly ? null : /* @__PURE__ */ jsx("span", {
|
|
115
|
+
className: "whitespace-nowrap",
|
|
116
|
+
children: "Switch theme"
|
|
117
|
+
})]
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
//#endregion
|
|
121
|
+
export { ThemeSwitch };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=theme-switch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-switch.js","names":["AriaButton"],"sources":["../../src/theme-switch/theme-switch.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useRef, useState } from \"react\";\nimport { Button as AriaButton, type PressEvent } from \"react-aria-components\";\n\nimport { buttonVariants } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { pressScaleFor, useExitBackstop, useLeavingItems } from \"../lib/motion.js\";\n\nexport type Theme = \"light\" | \"dark\";\n/** Which way the page transition travels. The switch nudges its icon the same way; the transition itself is the caller's. */\nexport type ThemeSwitchVariant = \"reveal\" | \"eclipse\" | \"split\" | \"rise\";\n\nexport interface ThemeSwitchProps {\n /** The current theme. The switch shows the icon for it and offers the other. */\n theme: Theme;\n /** @default \"reveal\" */\n variant?: ThemeSwitchVariant;\n /**\n * Called when the switch is pressed, with the theme to move to, the variant,\n * and the button (the origin a page transition can grow from). The component\n * does not change the theme itself: it is controlled.\n */\n onThemeChange: (next: Theme, variant: ThemeSwitchVariant, trigger: HTMLElement) => void;\n /** The accessible name. @default \"Switch to light mode\" / \"Switch to dark mode\" */\n label?: string;\n /** Only the icon, a square button. @default false */\n iconOnly?: boolean;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/**\n * A stored preference usually reaches the theme prop just after hydration. That\n * correction swaps the icon in place; only changes after the first painted\n * frames animate, so a page that loads in dark mode never spins its switches.\n */\nfunction useSettled() {\n const [settled, setSettled] = useState(false);\n useEffect(() => {\n let second = 0;\n const first = requestAnimationFrame(() => {\n second = requestAnimationFrame(() => setSettled(true));\n });\n return () => {\n cancelAnimationFrame(first);\n cancelAnimationFrame(second);\n };\n }, []);\n return settled;\n}\n\nconst ANGLE = { light: \"30deg\", dark: \"-30deg\" } as const;\n\nfunction Icon({ theme }: { theme: Theme }) {\n return (\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\"\n >\n {theme === \"light\" ? (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"4\" />\n <path d=\"M12 2v2M12 20v2m-7.07-17.07 1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\" />\n </>\n ) : (\n <path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\" />\n )}\n </svg>\n );\n}\n\nfunction LeavingIcon({\n id,\n theme,\n settled,\n onDone,\n}: {\n id: number;\n theme: Theme;\n settled: boolean;\n onDone: (id: number) => void;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n // Before the switch has settled there is nothing to animate: the old icon is simply gone.\n useEffect(() => {\n if (!settled) remove();\n }, [settled, remove]);\n if (!settled) return null;\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={{ \"--theme-angle\": ANGLE[theme] } as React.CSSProperties}\n className=\"absolute grid size-4 place-items-center motion-safe:animate-theme-icon-out motion-reduce:animate-fade-out\"\n >\n <Icon theme={theme} />\n </span>\n );\n}\n\n/**\n * A light/dark switch. The sun and the moon turn the same way, clockwise into\n * dark and back out of it, so the swap reads as one rotation; the icon is\n * nudged the way the page transition travels (`variant`) and eases back when\n * the theme returns.\n *\n * Controlled: it shows `theme` and calls `onThemeChange`; it never changes\n * the theme itself, so the page transition can run in the caller.\n *\n * @example\n * <ThemeSwitch theme={theme} onThemeChange={(next) => setTheme(next)} />\n */\nexport const ThemeSwitch = forwardRef<HTMLButtonElement, ThemeSwitchProps>(function ThemeSwitch(\n { theme, variant = \"reveal\", onThemeChange, label, iconOnly = false, className },\n ref,\n) {\n const settled = useSettled();\n const next: Theme = theme === \"light\" ? \"dark\" : \"light\";\n const phaseRef = useRef<HTMLSpanElement>(null);\n const { leaving, changed, remove } = useLeavingItems<Theme>(theme, theme, () => phaseRef.current);\n\n const pressStart = (event: PressEvent) => {\n const target = event.target as HTMLElement;\n target.style.setProperty(\"--press-scale\", pressScaleFor(target.offsetWidth));\n };\n\n return (\n <AriaButton\n ref={ref}\n data-slot=\"button\"\n data-mode={theme}\n data-variant={variant}\n aria-label={label ?? \"Switch to \" + next + \" mode\"}\n aria-pressed={theme === \"dark\"}\n onPressStart={pressStart}\n onPress={(event) => onThemeChange(next, variant, event.target as HTMLElement)}\n className={cn(\n buttonVariants({ variant: \"outlineNeutral\", size: \"sm\" }) +\n (iconOnly ? \" w-(--density-control-h-sm) px-0\" : \"\"),\n className,\n )}\n >\n <span\n data-settled={settled || undefined}\n className=\"theme-nudge relative inline-grid size-5 shrink-0 place-items-center\"\n >\n {leaving.map((item) => (\n <LeavingIcon\n key={item.id}\n id={item.id}\n theme={item.value}\n settled={settled}\n onDone={remove}\n />\n ))}\n <span\n key={theme}\n ref={phaseRef}\n style={{ \"--theme-angle\": ANGLE[theme] } as React.CSSProperties}\n className={cn(\n \"absolute grid size-4 place-items-center\",\n changed && settled && \"motion-safe:animate-theme-icon-in motion-reduce:animate-fade-in\",\n )}\n >\n <Icon theme={theme} />\n </span>\n </span>\n {iconOnly ? null : <span className=\"whitespace-nowrap\">Switch theme</span>}\n </AriaButton>\n );\n});\n"],"mappings":";;;;;;;;;;;;;AAqCA,SAAS,aAAa;CACpB,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,gBAAgB;EACd,IAAI,SAAS;EACb,MAAM,QAAQ,4BAA4B;GACxC,SAAS,4BAA4B,WAAW,IAAI,CAAC;EACvD,CAAC;EACD,aAAa;GACX,qBAAqB,KAAK;GAC1B,qBAAqB,MAAM;EAC7B;CACF,GAAG,CAAC,CAAC;CACL,OAAO;AACT;AAEA,MAAM,QAAQ;CAAE,OAAO;CAAS,MAAM;AAAS;AAE/C,SAAS,KAAK,EAAE,SAA2B;CACzC,OACE,oBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAa;EACb,eAAc;EACd,gBAAe;EACf,eAAY;EACZ,WAAU;EAET,UAAA,UAAU,UACT,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,UAAD;GAAQ,IAAG;GAAK,IAAG;GAAK,GAAE;EAAK,CAAA,GAC/B,oBAAC,QAAD,EAAM,GAAE,wHAAyH,CAAA,CACjI,EAAA,CAAA,IAEF,oBAAC,QAAD,EAAM,GAAE,qCAAsC,CAAA;CAE7C,CAAA;AAET;AAEA,SAAS,YAAY,EACnB,IACA,OACA,SACA,UAMC;CACD,MAAM,SAAS,kBAAkB,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;CACzD,gBAAgB,MAAM,MAAM;CAE5B,gBAAgB;EACd,IAAI,CAAC,SAAS,OAAO;CACvB,GAAG,CAAC,SAAS,MAAM,CAAC;CACpB,IAAI,CAAC,SAAS,OAAO;CACrB,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,iBAAiB,UAAU;GACzB,IAAI,MAAM,WAAW,MAAM,eAAe,OAAO;EACnD;EACA,OAAO,EAAE,iBAAiB,MAAM,OAAO;EACvC,WAAU;EAEV,UAAA,oBAAC,MAAD,EAAa,MAAQ,CAAA;CACjB,CAAA;AAEV;;;;;;;;;;;;;AAcA,MAAa,cAAc,WAAgD,SAAS,YAClF,EAAE,OAAO,UAAU,UAAU,eAAe,OAAO,WAAW,OAAO,aACrE,KACA;CACA,MAAM,UAAU,WAAW;CAC3B,MAAM,OAAc,UAAU,UAAU,SAAS;CACjD,MAAM,WAAW,OAAwB,IAAI;CAC7C,MAAM,EAAE,SAAS,SAAS,WAAW,gBAAuB,OAAO,aAAa,SAAS,OAAO;CAEhG,MAAM,cAAc,UAAsB;EACxC,MAAM,SAAS,MAAM;EACrB,OAAO,MAAM,YAAY,iBAAiB,cAAc,OAAO,WAAW,CAAC;CAC7E;CAEA,OACE,qBAACA,QAAD;EACO;EACL,aAAU;EACV,aAAW;EACX,gBAAc;EACd,cAAY,SAAS,eAAe,OAAO;EAC3C,gBAAc,UAAU;EACxB,cAAc;EACd,UAAU,UAAU,cAAc,MAAM,SAAS,MAAM,MAAqB;EAC5E,WAAW,GACT,eAAe;GAAE,SAAS;GAAkB,MAAM;EAAK,CAAC,KACrD,WAAW,qCAAqC,KACnD,SACF;EAbF,UAAA,CAeE,qBAAC,QAAD;GACE,gBAAc,WAAW,KAAA;GACzB,WAAU;GAFZ,UAAA,CAIG,QAAQ,KAAK,SACZ,oBAAC,aAAD;IAEE,IAAI,KAAK;IACT,OAAO,KAAK;IACH;IACT,QAAQ;GACT,GALM,KAAK,EAKX,CACF,GACD,oBAAC,QAAD;IAEE,KAAK;IACL,OAAO,EAAE,iBAAiB,MAAM,OAAO;IACvC,WAAW,GACT,2CACA,WAAW,WAAW,iEACxB;IAEA,UAAA,oBAAC,MAAD,EAAa,MAAQ,CAAA;GACjB,GATC,KASD,CACF;EACL,CAAA,GAAA,WAAW,OAAO,oBAAC,QAAD;GAAM,WAAU;GAAoB,UAAA;EAAkB,CAAA,CAC/D;;AAEhB,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TimeValue } from "react-aria-components";
|
|
2
|
+
//#region src/time-picker/time-picker.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* TimePicker, a field that shows a time and opens a list of times to pick from.
|
|
5
|
+
*
|
|
6
|
+
* Values are `TimeValue` objects from `@internationalized/date`, as Calendar and
|
|
7
|
+
* DatePicker use `DateValue` (ADR 0006), not "HH:MM" strings: a time of day has
|
|
8
|
+
* no time zone or format of its own. Unlike those two, this one builds a `Time`
|
|
9
|
+
* at runtime to hand back, so the package is a real (peer) dependency here and
|
|
10
|
+
* not only a type.
|
|
11
|
+
*
|
|
12
|
+
* The control is a React Aria `Select`, so the list is a listbox with typeahead,
|
|
13
|
+
* arrow keys and a roving highlight; the motion (the value rolling like a clock
|
|
14
|
+
* face, the chevron, the list growing from the field) sits on top of it.
|
|
15
|
+
*/
|
|
16
|
+
/** The list: it grows from the field, and the highlight follows the pointer and the arrows. */
|
|
17
|
+
export declare const timePickerListVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
18
|
+
/** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */
|
|
19
|
+
export declare const timePickerItemVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
20
|
+
export interface TimePickerProps {
|
|
21
|
+
/** What is being chosen, and the control's accessible name. Required, as on Select. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Render the label for screen readers only. */
|
|
24
|
+
labelHidden?: boolean;
|
|
25
|
+
/** Help text under the control, named in `aria-describedby`. */
|
|
26
|
+
description?: string;
|
|
27
|
+
/** Shown while no time is chosen. @default "Select a time" */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** The chosen time. Controlled: pass `null` for none. */
|
|
30
|
+
value?: TimeValue | null;
|
|
31
|
+
/** The starting time when uncontrolled. @default null */
|
|
32
|
+
defaultValue?: TimeValue | null;
|
|
33
|
+
/** Called with the chosen time, as a `Time`. */
|
|
34
|
+
onChange?: (value: TimeValue) => void;
|
|
35
|
+
/** Minutes between the times in the list. @default 15 */
|
|
36
|
+
minuteStep?: 1 | 5 | 10 | 15 | 30;
|
|
37
|
+
/** 12-hour (9:00 AM) or 24-hour (09:00) times. @default 12 */
|
|
38
|
+
hourCycle?: 12 | 24;
|
|
39
|
+
/** Control scale of the trigger. @default "md" */
|
|
40
|
+
size?: "sm" | "md" | "lg";
|
|
41
|
+
/** Disable the control. */
|
|
42
|
+
isDisabled?: boolean;
|
|
43
|
+
/** Extra classes for the wrapper. Merged last. */
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A time picker.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* <TimePicker label="Start time" value={time} onChange={setTime} minuteStep={30} />
|
|
51
|
+
*/
|
|
52
|
+
export declare const TimePicker: import("react").ForwardRefExoticComponent<TimePickerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//# sourceMappingURL=time-picker.d.ts.map
|