aplance-ui 1.4.0 → 1.5.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/package.json +1 -1
- package/registry/components/primitives/icon/icon.tsx +4 -6
- package/registry/components/primitives/modal/modal.module.css +5 -0
- package/registry/components/primitives/modal/modal.tsx +32 -34
- package/registry/components/primitives/popover/popover.module.css +4 -2
- package/registry/components/primitives/popover/popover.tsx +16 -17
- package/registry/components/ui/accordion/accordion.module.css +52 -0
- package/registry/components/ui/accordion/accordion.tsx +97 -25
- package/registry/components/ui/avatar/avatar.tsx +2 -2
- package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
- package/registry/components/ui/button/button.module.css +43 -5
- package/registry/components/ui/button/button.tsx +2 -1
- package/registry/components/ui/copy-button/copy-button.module.css +2 -0
- package/registry/components/ui/data-table/data-table.module.css +68 -25
- package/registry/components/ui/data-table/data-table.tsx +1238 -1229
- package/registry/components/ui/data-table/filters.tsx +21 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
- package/registry/components/ui/data-table/types.ts +11 -1
- package/registry/components/ui/data-table/useDataTable.tsx +1 -2
- package/registry/components/ui/data-table/utils.ts +4 -3
- package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +115 -84
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +63 -11
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +51 -55
- package/registry/components/ui/input-fields/input/input.tsx +348 -348
- package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
- package/registry/components/ui/input-fields/media-input/types.ts +8 -2
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
- package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
- package/registry/components/ui/scroll-rail/scroll-rail.module.css +104 -0
- package/registry/components/ui/scroll-rail/scroll-rail.tsx +159 -0
- package/registry/components/ui/tabs/tabs.module.css +8 -12
- package/registry/components/ui/tabs/tabs.tsx +75 -44
- package/registry/components/ui/tooltip/tooltip.module.css +8 -3
- package/registry/components/ui/tooltip/tooltip.tsx +16 -3
- package/registry/lib/string.ts +3 -3
- package/registry/registry.json +33 -3
package/package.json
CHANGED
|
@@ -31,12 +31,10 @@ export const Icon: FC<IconProps> = ({
|
|
|
31
31
|
className={clsx(
|
|
32
32
|
s.wrapper,
|
|
33
33
|
s[size],
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[s.rotate_sub]: rotateSub,
|
|
39
|
-
},
|
|
34
|
+
disabled && s.disabled,
|
|
35
|
+
(!!onClick || clickable) && s.clickable,
|
|
36
|
+
rotate && s.rotate,
|
|
37
|
+
rotateSub && s.rotate_sub,
|
|
40
38
|
className,
|
|
41
39
|
)}
|
|
42
40
|
style={style}
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
opacity: 0;
|
|
22
22
|
transition: opacity var(--modal-animation-duration) var(--ease-in-out);
|
|
23
23
|
|
|
24
|
+
/* Closing, it fades out without taking clicks: the page under it answers. */
|
|
25
|
+
&[data-state='closed'] {
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
&[data-state='open'] {
|
|
25
30
|
opacity: 1;
|
|
26
31
|
}
|
|
@@ -207,23 +207,23 @@ export const Modal: FC<ModalProps> = ({
|
|
|
207
207
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
208
208
|
const modalId = useId();
|
|
209
209
|
|
|
210
|
+
// Freeze dialog content during close animation so conditional content
|
|
211
|
+
// (e.g. {editAgent && <form>}) doesn't collapse before the animation ends:
|
|
212
|
+
// open, the dialog follows its children; closed, it keeps the last ones.
|
|
213
|
+
const [openChildren, setOpenChildren] = useState(children);
|
|
214
|
+
if (isOpen && openChildren !== children) setOpenChildren(children);
|
|
215
|
+
|
|
210
216
|
// Split children: <ModalTrigger> siblings render outside the dialog so they
|
|
211
217
|
// stay visible while closed; everything else is dialog content. Order is
|
|
212
218
|
// preserved within each bucket, which is all consumers care about.
|
|
213
219
|
const nestedTriggers: ReactNode[] = [];
|
|
214
|
-
const dialogChildren: ReactNode[] = [];
|
|
215
220
|
Children.forEach(children, (child) => {
|
|
216
|
-
if (isValidElement(child) && child.type === ModalTrigger)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
if (isValidElement(child) && child.type === ModalTrigger) nestedTriggers.push(child);
|
|
222
|
+
});
|
|
223
|
+
const dialogChildren: ReactNode[] = [];
|
|
224
|
+
Children.forEach(isOpen ? children : openChildren, (child) => {
|
|
225
|
+
if (!(isValidElement(child) && child.type === ModalTrigger)) dialogChildren.push(child);
|
|
221
226
|
});
|
|
222
|
-
|
|
223
|
-
// Freeze dialog content during close animation so conditional content
|
|
224
|
-
// (e.g. {editAgent && <form>}) doesn't collapse before the animation ends.
|
|
225
|
-
const childrenSnapshot = useRef<ReactNode>(dialogChildren);
|
|
226
|
-
if (isOpen) childrenSnapshot.current = dialogChildren;
|
|
227
227
|
|
|
228
228
|
// Stash callbacks in refs so identity changes on the parent don't retrigger
|
|
229
229
|
// the open/close effect (which would leak timers and re-fire onOpen).
|
|
@@ -246,32 +246,30 @@ export const Modal: FC<ModalProps> = ({
|
|
|
246
246
|
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
|
|
247
247
|
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]);
|
|
248
248
|
|
|
249
|
-
// Open:
|
|
250
|
-
// Close:
|
|
249
|
+
// Open: the node mounts in this render → useLayoutEffect below fires → isVisible=true → open transition
|
|
250
|
+
// Close: isVisible=false in this render → close transition plays → setTimeout unmounts after duration
|
|
251
|
+
if (isOpen && !shouldRender) setShouldRender(true);
|
|
252
|
+
if (!isOpen && isVisible) setIsVisible(false);
|
|
251
253
|
useEffect(() => {
|
|
252
|
-
if (isOpen)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
onCloseRef.current?.();
|
|
259
|
-
}, animationDuration);
|
|
260
|
-
return () => clearTimeout(t);
|
|
261
|
-
}
|
|
254
|
+
if (isOpen) return undefined;
|
|
255
|
+
const t = setTimeout(() => {
|
|
256
|
+
setShouldRender(false);
|
|
257
|
+
onCloseRef.current?.();
|
|
258
|
+
}, animationDuration);
|
|
259
|
+
return () => clearTimeout(t);
|
|
262
260
|
}, [isOpen, animationDuration]);
|
|
263
261
|
|
|
264
|
-
// Fires synchronously
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
262
|
+
// Fires synchronously once the node is painted, and again when it is opened
|
|
263
|
+
// while still mounted (reopened during its close transition, which would
|
|
264
|
+
// otherwise stay closed). Force-reads offsetHeight to flush the browser's
|
|
265
|
+
// style recalc so it registers the closed CSS state before we flip to open -
|
|
266
|
+
// same trick Popover uses with isMeasured + ResizeObserver.
|
|
268
267
|
useLayoutEffect(() => {
|
|
269
|
-
if (shouldRender && mounted && contentRef.current) {
|
|
268
|
+
if (shouldRender && mounted && isOpen && contentRef.current) {
|
|
270
269
|
void contentRef.current.offsetHeight; // flush reflow
|
|
271
|
-
setIsVisible(
|
|
270
|
+
setIsVisible(true);
|
|
272
271
|
}
|
|
273
|
-
|
|
274
|
-
}, [shouldRender, mounted]); // only run when node mounts or mounted state changes
|
|
272
|
+
}, [shouldRender, mounted, isOpen]);
|
|
275
273
|
|
|
276
274
|
useEffect(() => {
|
|
277
275
|
if (isOpen) onOpenRef.current?.();
|
|
@@ -313,7 +311,7 @@ export const Modal: FC<ModalProps> = ({
|
|
|
313
311
|
|
|
314
312
|
const trap = (e: globalThis.KeyboardEvent) => {
|
|
315
313
|
if (e.key !== "Tab") return;
|
|
316
|
-
if (
|
|
314
|
+
if (!first || !last) {
|
|
317
315
|
e.preventDefault();
|
|
318
316
|
return;
|
|
319
317
|
}
|
|
@@ -388,7 +386,7 @@ export const Modal: FC<ModalProps> = ({
|
|
|
388
386
|
style={style}
|
|
389
387
|
>
|
|
390
388
|
{/* children render ONLY here - never outside the dialog */}
|
|
391
|
-
{
|
|
389
|
+
{dialogChildren}
|
|
392
390
|
</div>
|
|
393
391
|
</div>
|
|
394
392
|
) : null;
|
|
@@ -547,7 +545,7 @@ export const ModalHeader: FC<ComponentProps<"div">> = ({
|
|
|
547
545
|
{showCloseButton && (
|
|
548
546
|
<Button
|
|
549
547
|
as="button"
|
|
550
|
-
variant="
|
|
548
|
+
variant="bordered"
|
|
551
549
|
layout="box"
|
|
552
550
|
size="sm"
|
|
553
551
|
className={s.close_button}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
justify-content: center;
|
|
8
8
|
|
|
9
9
|
.trigger {
|
|
10
|
-
width: 100%;
|
|
11
|
-
|
|
10
|
+
width: 100%;
|
|
11
|
+
/* At least as wide as its content, unless the host sets the variable (a
|
|
12
|
+
form field sets 0, so a long value truncates). */
|
|
13
|
+
min-width: var(--popover-trigger-min-width, fit-content);
|
|
12
14
|
display: flex;
|
|
13
15
|
align-items: center;
|
|
14
16
|
|
|
@@ -232,11 +232,11 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
232
232
|
// in PopoverMenu, so we DON'T reset isMeasured here on close - that would lose
|
|
233
233
|
// the position cache and break re-opens that interrupt the exit animation.
|
|
234
234
|
// isMeasured is reset only when the menu actually unmounts (cleanup below).
|
|
235
|
+
// Open mounts the menu in this render; close unmounts it once the exit
|
|
236
|
+
// animation has played.
|
|
237
|
+
if (isOpen && !shouldRender) setShouldRender(true);
|
|
235
238
|
useEffect(() => {
|
|
236
|
-
if (isOpen)
|
|
237
|
-
setShouldRender(true);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
239
|
+
if (isOpen) return;
|
|
240
240
|
const timer = setTimeout(() => {
|
|
241
241
|
setShouldRender(false);
|
|
242
242
|
if (onClose) onClose();
|
|
@@ -247,21 +247,20 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
247
247
|
// Measure after menu is rendered. Cleanup resets isMeasured so the next mount
|
|
248
248
|
// re-runs the entry animation from a known starting state.
|
|
249
249
|
useIsomorphicLayoutEffect(() => {
|
|
250
|
-
if (shouldRender
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (typeof ResizeObserver === "undefined") {
|
|
254
|
-
return () => setIsMeasured(false);
|
|
255
|
-
}
|
|
250
|
+
if (!shouldRender || !menuRef.current) return;
|
|
251
|
+
calculatePosition();
|
|
256
252
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (popoverRef.current) observer.observe(popoverRef.current);
|
|
260
|
-
return () => {
|
|
261
|
-
observer.disconnect();
|
|
262
|
-
setIsMeasured(false);
|
|
263
|
-
};
|
|
253
|
+
if (typeof ResizeObserver === "undefined") {
|
|
254
|
+
return () => setIsMeasured(false);
|
|
264
255
|
}
|
|
256
|
+
|
|
257
|
+
const observer = new ResizeObserver(() => calculatePosition());
|
|
258
|
+
observer.observe(menuRef.current);
|
|
259
|
+
if (popoverRef.current) observer.observe(popoverRef.current);
|
|
260
|
+
return () => {
|
|
261
|
+
observer.disconnect();
|
|
262
|
+
setIsMeasured(false);
|
|
263
|
+
};
|
|
265
264
|
}, [shouldRender, mounted, calculatePosition]);
|
|
266
265
|
|
|
267
266
|
// Clear any pending hover-close timer when the Popover unmounts so the
|
|
@@ -110,6 +110,58 @@
|
|
|
110
110
|
padding: var(--spacing-5) var(--spacing-6);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* A heading with controls beside its button (an item's actions, a toggle
|
|
114
|
+
button): the row takes the heading's padding and its shades, and the
|
|
115
|
+
button inside it takes what is left. */
|
|
116
|
+
.header {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: var(--spacing-3);
|
|
120
|
+
width: 100%;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.header_trigger {
|
|
124
|
+
flex: 1;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
background: transparent;
|
|
128
|
+
border-radius: var(--radius-sm);
|
|
129
|
+
|
|
130
|
+
&:focus-visible {
|
|
131
|
+
box-shadow: var(--shadow-ring);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.header_bordered,
|
|
136
|
+
.header_minimal {
|
|
137
|
+
&:has(> .header_trigger:hover:not(:disabled)) {
|
|
138
|
+
background: var(--color-muted);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.header_minimal {
|
|
143
|
+
border-radius: inherit;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.actions {
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: var(--spacing-2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.toggle {
|
|
154
|
+
flex-shrink: 0;
|
|
155
|
+
|
|
156
|
+
& svg {
|
|
157
|
+
transition: transform var(--transition-base);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
&.toggle_open svg {
|
|
161
|
+
transform: rotate(var(--accordion-icon-rotation, 180deg));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
113
165
|
.title {
|
|
114
166
|
flex: 1;
|
|
115
167
|
min-width: 0;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import clsx from "clsx";
|
|
10
10
|
import { IconChevronDown } from "@tabler/icons-react";
|
|
11
11
|
import { useResizeObserver } from "../../../hooks/useResizeObserver";
|
|
12
|
+
import { Button } from "../button/button";
|
|
12
13
|
import s from "./accordion.module.css";
|
|
13
14
|
|
|
14
15
|
export type AccordionVariant = "bordered" | "minimal";
|
|
@@ -17,12 +18,23 @@ export type AccordionMode = "single" | "multiple";
|
|
|
17
18
|
|
|
18
19
|
export type AccordionSize = "sm" | "md" | "lg";
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* How an item opens. icon: the whole heading, with a chevron in it. button:
|
|
23
|
+
* the heading too, with a bordered chevron button at its end, as a table row
|
|
24
|
+
* ends in its menu button.
|
|
25
|
+
*/
|
|
26
|
+
export type AccordionToggle = "icon" | "button";
|
|
27
|
+
|
|
20
28
|
export type AccordionItem = {
|
|
21
29
|
id: string;
|
|
22
30
|
title: ReactNode;
|
|
23
31
|
content: ReactNode;
|
|
24
32
|
disabled?: boolean;
|
|
25
33
|
defaultOpen?: boolean;
|
|
34
|
+
/** Controls beside the heading, outside the button that opens it: a row's menu. */
|
|
35
|
+
actions?: ReactNode;
|
|
36
|
+
/** This item's own class, beside `itemClassName`: one marked apart from the rest. */
|
|
37
|
+
className?: string;
|
|
26
38
|
};
|
|
27
39
|
|
|
28
40
|
type AccordionBaseProps = {
|
|
@@ -35,6 +47,7 @@ type AccordionBaseProps = {
|
|
|
35
47
|
size?: AccordionSize;
|
|
36
48
|
icon?: ReactNode;
|
|
37
49
|
iconRotation?: number;
|
|
50
|
+
toggle?: AccordionToggle;
|
|
38
51
|
};
|
|
39
52
|
|
|
40
53
|
type AccordionSingleProps = {
|
|
@@ -80,6 +93,7 @@ type ItemViewProps = {
|
|
|
80
93
|
size: AccordionSize;
|
|
81
94
|
variant: AccordionVariant;
|
|
82
95
|
icon?: ReactNode;
|
|
96
|
+
toggle: AccordionToggle;
|
|
83
97
|
itemClassName?: string;
|
|
84
98
|
triggerClassName?: string;
|
|
85
99
|
contentClassName?: string;
|
|
@@ -114,6 +128,7 @@ const AccordionItemView = ({
|
|
|
114
128
|
size,
|
|
115
129
|
variant,
|
|
116
130
|
icon,
|
|
131
|
+
toggle,
|
|
117
132
|
itemClassName,
|
|
118
133
|
triggerClassName,
|
|
119
134
|
contentClassName,
|
|
@@ -138,6 +153,13 @@ const AccordionItemView = ({
|
|
|
138
153
|
|
|
139
154
|
useResizeObserver(contentRef, updateHeight, { enabled: isOpen });
|
|
140
155
|
|
|
156
|
+
const chevron = icon ?? (
|
|
157
|
+
<IconChevronDown size={size === "sm" ? 16 : size === "lg" ? 20 : 18} />
|
|
158
|
+
);
|
|
159
|
+
// With actions or a toggle button, the heading is a row: the button that
|
|
160
|
+
// opens the item, then the controls beside it, outside that button.
|
|
161
|
+
const row = toggle === "button" || item.actions !== undefined;
|
|
162
|
+
|
|
141
163
|
return (
|
|
142
164
|
<section
|
|
143
165
|
className={clsx(
|
|
@@ -146,42 +168,90 @@ const AccordionItemView = ({
|
|
|
146
168
|
isOpen && s.item_open,
|
|
147
169
|
item.disabled && s.item_disabled,
|
|
148
170
|
itemClassName,
|
|
171
|
+
item.className,
|
|
149
172
|
)}
|
|
150
173
|
data-state={isOpen ? "open" : "closed"}
|
|
151
174
|
>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
s.trigger,
|
|
161
|
-
s[`trigger_${size}`],
|
|
162
|
-
s[`trigger_${variant}`],
|
|
163
|
-
isOpen && s.trigger_open,
|
|
164
|
-
triggerClassName,
|
|
165
|
-
)}
|
|
166
|
-
>
|
|
167
|
-
<span className={clsx(s.title, s[`title_${size}`])}>{item.title}</span>
|
|
168
|
-
<span
|
|
169
|
-
className={clsx(s.icon, isOpen && s.icon_open)}
|
|
170
|
-
aria-hidden="true"
|
|
175
|
+
{row ? (
|
|
176
|
+
<div
|
|
177
|
+
className={clsx(
|
|
178
|
+
s.header,
|
|
179
|
+
s[`trigger_${size}`],
|
|
180
|
+
s[`header_${variant}`],
|
|
181
|
+
isOpen && s.trigger_open,
|
|
182
|
+
)}
|
|
171
183
|
>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
<button
|
|
185
|
+
id={triggerId}
|
|
186
|
+
type="button"
|
|
187
|
+
disabled={item.disabled}
|
|
188
|
+
aria-expanded={isOpen}
|
|
189
|
+
aria-controls={panelId}
|
|
190
|
+
onClick={onToggle}
|
|
191
|
+
className={clsx(s.trigger, s.header_trigger, triggerClassName)}
|
|
192
|
+
>
|
|
193
|
+
<span className={clsx(s.title, s[`title_${size}`])}>{item.title}</span>
|
|
194
|
+
{toggle === "icon" ? (
|
|
195
|
+
<span
|
|
196
|
+
className={clsx(s.icon, isOpen && s.icon_open)}
|
|
197
|
+
aria-hidden="true"
|
|
198
|
+
>
|
|
199
|
+
{chevron}
|
|
200
|
+
</span>
|
|
201
|
+
) : null}
|
|
202
|
+
</button>
|
|
203
|
+
{item.actions !== undefined ? (
|
|
204
|
+
<div className={s.actions}>{item.actions}</div>
|
|
205
|
+
) : null}
|
|
206
|
+
{toggle === "button" ? (
|
|
207
|
+
<Button
|
|
208
|
+
type="button"
|
|
209
|
+
variant="bordered"
|
|
210
|
+
size="sm"
|
|
211
|
+
layout="box"
|
|
212
|
+
aria-label={isOpen ? "Collapse section" : "Expand section"}
|
|
213
|
+
aria-expanded={isOpen}
|
|
214
|
+
aria-controls={panelId}
|
|
215
|
+
disabled={item.disabled}
|
|
216
|
+
onClick={onToggle}
|
|
217
|
+
className={clsx(s.toggle, isOpen && s.toggle_open)}
|
|
218
|
+
>
|
|
219
|
+
{chevron}
|
|
220
|
+
</Button>
|
|
221
|
+
) : null}
|
|
222
|
+
</div>
|
|
223
|
+
) : (
|
|
224
|
+
<button
|
|
225
|
+
id={triggerId}
|
|
226
|
+
type="button"
|
|
227
|
+
disabled={item.disabled}
|
|
228
|
+
aria-expanded={isOpen}
|
|
229
|
+
aria-controls={panelId}
|
|
230
|
+
onClick={onToggle}
|
|
231
|
+
className={clsx(
|
|
232
|
+
s.trigger,
|
|
233
|
+
s[`trigger_${size}`],
|
|
234
|
+
s[`trigger_${variant}`],
|
|
235
|
+
isOpen && s.trigger_open,
|
|
236
|
+
triggerClassName,
|
|
176
237
|
)}
|
|
177
|
-
|
|
178
|
-
|
|
238
|
+
>
|
|
239
|
+
<span className={clsx(s.title, s[`title_${size}`])}>{item.title}</span>
|
|
240
|
+
<span
|
|
241
|
+
className={clsx(s.icon, isOpen && s.icon_open)}
|
|
242
|
+
aria-hidden="true"
|
|
243
|
+
>
|
|
244
|
+
{chevron}
|
|
245
|
+
</span>
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
179
248
|
|
|
180
249
|
<div
|
|
181
250
|
id={panelId}
|
|
182
251
|
role="region"
|
|
183
252
|
aria-labelledby={triggerId}
|
|
184
253
|
aria-hidden={!isOpen}
|
|
254
|
+
inert={!isOpen}
|
|
185
255
|
className={clsx(s.panel, isOpen && s.panel_open)}
|
|
186
256
|
style={{ height: `${panelHeight}px` }}
|
|
187
257
|
>
|
|
@@ -212,6 +282,7 @@ export const Accordion = ({
|
|
|
212
282
|
size = "md",
|
|
213
283
|
icon,
|
|
214
284
|
iconRotation = 180,
|
|
285
|
+
toggle = "icon",
|
|
215
286
|
...variantProps
|
|
216
287
|
}: AccordionProps) => {
|
|
217
288
|
const uid = useId();
|
|
@@ -317,6 +388,7 @@ export const Accordion = ({
|
|
|
317
388
|
size={size}
|
|
318
389
|
variant={variant}
|
|
319
390
|
icon={icon}
|
|
391
|
+
toggle={toggle}
|
|
320
392
|
itemClassName={itemClassName}
|
|
321
393
|
triggerClassName={triggerClassName}
|
|
322
394
|
contentClassName={contentClassName}
|
|
@@ -17,10 +17,10 @@ const COLORS = [
|
|
|
17
17
|
{ bg: "oklch(0.92 0.06 85)", fg: "oklch(0.4 0.15 85)" },
|
|
18
18
|
{ bg: "oklch(0.92 0.05 25)", fg: "oklch(0.4 0.15 25)" },
|
|
19
19
|
{ bg: "oklch(0.92 0.05 330)", fg: "oklch(0.4 0.15 330)" },
|
|
20
|
-
];
|
|
20
|
+
] as const;
|
|
21
21
|
|
|
22
22
|
export function Avatar({ name, size = "md", className }: AvatarProps) {
|
|
23
|
-
const color = COLORS[hashName(name) % COLORS.length];
|
|
23
|
+
const color = COLORS[hashName(name) % COLORS.length] ?? COLORS[0];
|
|
24
24
|
const initials = getInitials(name) || "?";
|
|
25
25
|
return (
|
|
26
26
|
<div
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
--breadcrumbs-color-current: var(--color-foreground);
|
|
10
10
|
--breadcrumbs-color-separator: var(--color-muted-foreground);
|
|
11
11
|
--breadcrumbs-font-size: var(--font-size-sm);
|
|
12
|
-
--breadcrumbs-gap: var(--spacing-
|
|
13
|
-
--breadcrumbs-padding: var(--spacing-2) var(--spacing-3);
|
|
12
|
+
--breadcrumbs-gap: var(--spacing-5);
|
|
14
13
|
|
|
15
14
|
display: block;
|
|
16
15
|
width: 100%;
|
|
@@ -22,8 +21,7 @@
|
|
|
22
21
|
|
|
23
22
|
.size_sm {
|
|
24
23
|
--breadcrumbs-font-size: var(--font-size-xs);
|
|
25
|
-
--breadcrumbs-gap: var(--spacing-
|
|
26
|
-
--breadcrumbs-padding: var(--spacing-1) var(--spacing-2);
|
|
24
|
+
--breadcrumbs-gap: var(--spacing-4);
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
/* ─── List ───────────────────────────────────────────────────────────────── */
|
|
@@ -45,15 +43,18 @@
|
|
|
45
43
|
min-width: 0;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
/* ─── Crumb (link / button / span)
|
|
46
|
+
/* ─── Crumb (link / button / span) ───────────────────────────────────────────
|
|
47
|
+
A text link, not a button: no padding or hover fill, so every crumb sits on
|
|
48
|
+
the same edge as the content under it. Hover darkens the text.
|
|
49
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
49
50
|
|
|
50
51
|
.crumb {
|
|
51
52
|
display: inline-flex;
|
|
52
53
|
align-items: center;
|
|
53
54
|
gap: var(--spacing-2);
|
|
54
55
|
min-width: 0;
|
|
55
|
-
padding:
|
|
56
|
-
border-radius: var(--radius-
|
|
56
|
+
padding: 0;
|
|
57
|
+
border-radius: var(--radius-sm);
|
|
57
58
|
color: inherit;
|
|
58
59
|
background-color: transparent;
|
|
59
60
|
font: inherit;
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
white-space: nowrap;
|
|
63
64
|
cursor: default;
|
|
64
65
|
transition: var(--transition-fast);
|
|
65
|
-
transition-property:
|
|
66
|
+
transition-property: color;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
.crumb_link {
|
|
@@ -71,7 +72,6 @@
|
|
|
71
72
|
|
|
72
73
|
&:hover {
|
|
73
74
|
color: var(--breadcrumbs-color-hover);
|
|
74
|
-
background-color: var(--color-muted);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
&:focus-visible {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.base {
|
|
1
|
+
.base {
|
|
2
2
|
--button-fg: var(--color-foreground);
|
|
3
3
|
--button-placeholder: var(--button-fg);
|
|
4
4
|
--button-icon: var(--button-fg);
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
min-height: var(--spacing-11);
|
|
12
12
|
border-radius: var(--radius-md);
|
|
13
13
|
display: flex;
|
|
14
|
-
|
|
14
|
+
/* Never narrower than its label, unless the host sets the variable (a
|
|
15
|
+
full-width field trigger sets 0, so its label truncates). */
|
|
16
|
+
min-width: var(--button-min-width, fit-content);
|
|
15
17
|
align-items: center;
|
|
16
18
|
justify-content: space-between;
|
|
17
19
|
gap: var(--spacing-4);
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
outline: none;
|
|
25
27
|
border: none;
|
|
26
28
|
|
|
27
|
-
line-height: 1.125;
|
|
29
|
+
line-height: 1.125;
|
|
28
30
|
|
|
29
31
|
svg {
|
|
30
32
|
cursor: pointer;
|
|
@@ -36,12 +38,16 @@
|
|
|
36
38
|
font-size: var(--font-size-xs);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
/* An icon button: as wide as it is high, its own size and never its
|
|
42
|
+
content's (the base keeps a button at least as wide as its content), so
|
|
43
|
+
a round one stays round; the icon sits in its middle. */
|
|
44
|
+
&.box {
|
|
40
45
|
width: var(--spacing-11);
|
|
46
|
+
min-width: 0;
|
|
41
47
|
min-height: unset;
|
|
42
48
|
justify-content: center;
|
|
43
49
|
height: var(--spacing-11);
|
|
44
|
-
padding:
|
|
50
|
+
padding: 0;
|
|
45
51
|
|
|
46
52
|
&.sm {
|
|
47
53
|
width: var(--spacing-10);
|
|
@@ -183,6 +189,38 @@
|
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
/* Same pattern - danger is the soft red a Badge's error tone uses, for an
|
|
193
|
+
action that cannot be taken back: deleting, purging, removing. It reads in
|
|
194
|
+
both themes from the error tokens alone. */
|
|
195
|
+
:is(.danger, .tone_danger) {
|
|
196
|
+
--button-fg: var(--color-error-foreground);
|
|
197
|
+
--button-placeholder: var(--color-error-foreground);
|
|
198
|
+
--button-icon: var(--color-error-foreground);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.danger {
|
|
202
|
+
background-color: var(--color-error-subtle);
|
|
203
|
+
border: 1px solid
|
|
204
|
+
color-mix(in oklch, var(--color-error-foreground) 24%, var(--color-border));
|
|
205
|
+
color: var(--button-fg);
|
|
206
|
+
box-shadow: var(--shadow-sm);
|
|
207
|
+
|
|
208
|
+
svg {
|
|
209
|
+
color: var(--button-icon);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
&:hover:not(.disabled),
|
|
213
|
+
&:active:not(.disabled),
|
|
214
|
+
&.input_focus:focus,
|
|
215
|
+
&.active {
|
|
216
|
+
background-color: color-mix(
|
|
217
|
+
in oklch,
|
|
218
|
+
var(--color-error-foreground) 14%,
|
|
219
|
+
var(--color-error-subtle)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
186
224
|
/* Same pattern - ghost is transparent background; tone alias lets composing
|
|
187
225
|
components inherit these vars without applying the full ghost layout rules. */
|
|
188
226
|
:is(.ghost, .tone_ghost) {
|
|
@@ -12,7 +12,8 @@ export type ButtonVariant =
|
|
|
12
12
|
| "secondary"
|
|
13
13
|
| "bordered"
|
|
14
14
|
| "border_b"
|
|
15
|
-
| "ghost"
|
|
15
|
+
| "ghost"
|
|
16
|
+
| "danger";
|
|
16
17
|
export type ButtonSize = "default" | "sm";
|
|
17
18
|
export type ButtonLayout = "default" | "box";
|
|
18
19
|
export type AsProp = "button" | "div" | "span" | "link";
|