@hilum/ui 3.8.5 → 3.10.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 +32 -1
- package/dist/{chunk-RI6QJR2L.js → chunk-R576DV2A.js} +120 -14
- package/dist/chunk-R576DV2A.js.map +1 -0
- package/dist/create-theme.js +2 -1
- package/dist/create-theme.js.map +1 -1
- package/dist/index.d.ts +733 -68
- package/dist/index.js +3097 -1619
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +163 -19
- package/dist/tokens.d.ts +69 -5
- package/dist/tokens.js +1 -1
- package/llms.txt +11 -4
- package/package.json +11 -10
- package/src/components/accordion.tsx +2 -2
- package/src/components/account-menu.tsx +1 -1
- package/src/components/alert-dialog.tsx +107 -0
- package/src/components/ask-user-questions.tsx +24 -18
- package/src/components/badge.tsx +36 -3
- package/src/components/breadcrumb.tsx +4 -2
- package/src/components/button-group.tsx +1 -1
- package/src/components/button.tsx +31 -23
- package/src/components/calendar.tsx +1 -1
- package/src/components/card.tsx +4 -2
- package/src/components/checkbox-card.tsx +1 -1
- package/src/components/checkbox-group.tsx +2 -1
- package/src/components/code-block.tsx +99 -0
- package/src/components/color-input.tsx +9 -6
- package/src/components/color-picker.tsx +38 -26
- package/src/components/combobox.tsx +59 -12
- package/src/components/command-palette.tsx +150 -42
- package/src/components/command.tsx +5 -3
- package/src/components/context-menu.tsx +18 -15
- package/src/components/contextual-save-bar.tsx +156 -0
- package/src/components/data-table.tsx +34 -5
- package/src/components/date-picker.tsx +275 -28
- package/src/components/date-text.tsx +90 -0
- package/src/components/dropdown-menu.tsx +11 -8
- package/src/components/dropdown.tsx +6 -3
- package/src/components/empty-state.tsx +85 -20
- package/src/components/file-dropzone.tsx +2 -1
- package/src/components/file-thumbnail.tsx +84 -20
- package/src/components/filter-bar.tsx +91 -0
- package/src/components/grid-list.tsx +2 -2
- package/src/components/help-tooltip.tsx +1 -1
- package/src/components/input-copy.tsx +1 -1
- package/src/components/input-group.tsx +1 -1
- package/src/components/input-message.tsx +5 -4
- package/src/components/input-number.tsx +103 -11
- package/src/components/input.tsx +5 -3
- package/src/components/label.tsx +1 -0
- package/src/components/media-asset-card.tsx +1 -1
- package/src/components/menu-item.tsx +37 -3
- package/src/components/menubar.tsx +21 -18
- package/src/components/native-select.tsx +1 -1
- package/src/components/navigation-menu.tsx +2 -2
- package/src/components/notification.tsx +1 -1
- package/src/components/pagination.tsx +97 -3
- package/src/components/popover.tsx +2 -0
- package/src/components/property-row.tsx +7 -4
- package/src/components/radio-card.tsx +1 -1
- package/src/components/radio-group.tsx +2 -1
- package/src/components/rating.tsx +136 -0
- package/src/components/resizable.tsx +1 -1
- package/src/components/resource-item.tsx +133 -0
- package/src/components/rich-text-editor.tsx +1 -0
- package/src/components/search-input.tsx +98 -0
- package/src/components/searchable-table.tsx +10 -11
- package/src/components/select.tsx +28 -9
- package/src/components/sidebar.tsx +4 -4
- package/src/components/slider.tsx +78 -36
- package/src/components/sonner.tsx +18 -4
- package/src/components/stat-card.tsx +121 -37
- package/src/components/status-badge.tsx +142 -47
- package/src/components/status-tile.tsx +9 -0
- package/src/components/switch.tsx +6 -5
- package/src/components/tabs-subtle.tsx +10 -2
- package/src/components/tabs.tsx +23 -3
- package/src/components/textarea.tsx +2 -3
- package/src/components/thinking-steps.tsx +3 -3
- package/src/components/toggle.tsx +14 -13
- package/src/components/tooltip.tsx +11 -8
- package/src/components/url-redirect-prompt.tsx +0 -1
- package/src/index.ts +15 -0
- package/src/lib/density-context.tsx +65 -0
- package/src/lib/format.tsx +301 -0
- package/src/lib/interaction.ts +12 -4
- package/src/lib/scroll-fade.tsx +39 -0
- package/src/tokens/tokens.test.ts +77 -0
- package/src/tokens/tokens.ts +114 -5
- package/dist/chunk-RI6QJR2L.js.map +0 -1
|
@@ -254,6 +254,7 @@ const AskUserQuestions = forwardRef<HTMLDivElement, AskUserQuestionsProps>(
|
|
|
254
254
|
// height morph and the footer move together. A ResizeObserver keeps the
|
|
255
255
|
// target in sync across question swaps, shape changes, and text wrapping.
|
|
256
256
|
const contentMeasureRef = useRef<HTMLDivElement>(null);
|
|
257
|
+
const hasQuestion = question != null;
|
|
257
258
|
const [contentHeight, setContentHeight] = useState<number | "auto">("auto");
|
|
258
259
|
useEffect(() => {
|
|
259
260
|
const el = contentMeasureRef.current;
|
|
@@ -263,7 +264,9 @@ const AskUserQuestions = forwardRef<HTMLDivElement, AskUserQuestionsProps>(
|
|
|
263
264
|
const ro = new ResizeObserver(update);
|
|
264
265
|
ro.observe(el);
|
|
265
266
|
return () => ro.disconnect();
|
|
266
|
-
|
|
267
|
+
// hasQuestion: the measured node only mounts once there is a question
|
|
268
|
+
// to render, so re-attach when it appears (e.g. questions load async).
|
|
269
|
+
}, [hasQuestion]);
|
|
267
270
|
|
|
268
271
|
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
269
272
|
|
|
@@ -527,22 +530,6 @@ const AskUserQuestions = forwardRef<HTMLDivElement, AskUserQuestionsProps>(
|
|
|
527
530
|
if (hasAnswer) handleMultiNext();
|
|
528
531
|
};
|
|
529
532
|
|
|
530
|
-
if (!question) {
|
|
531
|
-
return (
|
|
532
|
-
<div
|
|
533
|
-
ref={ref}
|
|
534
|
-
className={cn(
|
|
535
|
-
"w-full max-w-[520px] p-5 bg-card border border-border",
|
|
536
|
-
shape.container,
|
|
537
|
-
className,
|
|
538
|
-
)}
|
|
539
|
-
{...rest}
|
|
540
|
-
>
|
|
541
|
-
<p className="text-[13px] text-muted-foreground">No questions.</p>
|
|
542
|
-
</div>
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
533
|
// ── Layout calculations for hover/focus indicators ───────────
|
|
547
534
|
const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;
|
|
548
535
|
// The blue morphing focus ring is intentionally suppressed for the Other
|
|
@@ -616,11 +603,30 @@ const AskUserQuestions = forwardRef<HTMLDivElement, AskUserQuestionsProps>(
|
|
|
616
603
|
// use shape.bg, so corners animate around its radius.
|
|
617
604
|
const blocks = useMergeSplitBlocks(selectedGroups, itemRects, shape.bgRadius);
|
|
618
605
|
|
|
606
|
+
// Empty state. Kept below every hook call so the hook order stays stable
|
|
607
|
+
// when `questions` goes from [] to populated (e.g. loaded async).
|
|
608
|
+
if (!question) {
|
|
609
|
+
return (
|
|
610
|
+
<div
|
|
611
|
+
ref={ref}
|
|
612
|
+
className={cn(
|
|
613
|
+
"w-full max-w-[520px] p-5 bg-card border border-border",
|
|
614
|
+
shape.container,
|
|
615
|
+
className,
|
|
616
|
+
)}
|
|
617
|
+
{...rest}
|
|
618
|
+
>
|
|
619
|
+
<p className="text-[13px] text-muted-foreground">No questions.</p>
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
619
624
|
const showBack = total > 1 && safeIndex > 0;
|
|
620
625
|
const showSkip = total > 1 && isSkippable;
|
|
621
626
|
const showFooter = showBack || showSkip || isMulti;
|
|
622
627
|
|
|
623
628
|
return (
|
|
629
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated Cmd/Ctrl+Enter handler for keys bubbling from the focusable rows/buttons inside; the card itself isn't a control
|
|
624
630
|
<div
|
|
625
631
|
ref={ref}
|
|
626
632
|
className={cn(
|
|
@@ -770,7 +776,7 @@ const AskUserQuestions = forwardRef<HTMLDivElement, AskUserQuestionsProps>(
|
|
|
770
776
|
<motion.div
|
|
771
777
|
aria-hidden
|
|
772
778
|
className={cn(
|
|
773
|
-
"absolute pointer-events-none border border-
|
|
779
|
+
"absolute pointer-events-none border-2 border-ring z-20",
|
|
774
780
|
shape.focusRing,
|
|
775
781
|
)}
|
|
776
782
|
initial={{
|
package/src/components/badge.tsx
CHANGED
|
@@ -27,6 +27,30 @@ const badgeColors = {
|
|
|
27
27
|
|
|
28
28
|
type BadgeColor = keyof typeof badgeColors;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Semantic tones — the convention every product surface should use instead
|
|
32
|
+
* of picking badge colours by hand:
|
|
33
|
+
*
|
|
34
|
+
* - `success` — finished and healthy: paid, fulfilled, active, delivered.
|
|
35
|
+
* - `info` — in motion, nothing to do yet: processing, shipped, scheduled.
|
|
36
|
+
* - `attention` — needs a merchant action soon: unfulfilled, partially paid, on hold.
|
|
37
|
+
* - `warning` — at risk / waiting on someone else: pending, past due, expiring.
|
|
38
|
+
* - `critical` — failed or blocked: failed, cancelled, suspended, declined.
|
|
39
|
+
* - `neutral` — inactive or terminal-but-fine: draft, archived, refunded, closed.
|
|
40
|
+
*/
|
|
41
|
+
type StatusTone = "success" | "info" | "attention" | "warning" | "critical" | "neutral";
|
|
42
|
+
|
|
43
|
+
type ToneBadgeVariant = "success" | "brand" | "warning" | "destructive" | "secondary";
|
|
44
|
+
|
|
45
|
+
const STATUS_TONE_BADGE: Record<StatusTone, { variant: ToneBadgeVariant; color?: BadgeColor }> = {
|
|
46
|
+
success: { variant: "success" },
|
|
47
|
+
info: { variant: "brand", color: "blue" },
|
|
48
|
+
attention: { variant: "warning", color: "orange" },
|
|
49
|
+
warning: { variant: "warning" },
|
|
50
|
+
critical: { variant: "destructive" },
|
|
51
|
+
neutral: { variant: "secondary" },
|
|
52
|
+
};
|
|
53
|
+
|
|
30
54
|
const badgeVariants = cva("inline-flex items-center font-medium whitespace-nowrap", {
|
|
31
55
|
variants: {
|
|
32
56
|
variant: {
|
|
@@ -55,11 +79,19 @@ const badgeVariants = cva("inline-flex items-center font-medium whitespace-nowra
|
|
|
55
79
|
interface BadgeProps
|
|
56
80
|
extends Omit<HTMLAttributes<HTMLSpanElement>, "color">, VariantProps<typeof badgeVariants> {
|
|
57
81
|
color?: BadgeColor;
|
|
82
|
+
/** Semantic tone — wins over `variant`/`color`. See `StatusTone`. */
|
|
83
|
+
tone?: StatusTone;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|
61
|
-
(
|
|
87
|
+
(
|
|
88
|
+
{ className, variant: variantProp = "solid", size = "md", color: colorProp, tone, children, style, ...props },
|
|
89
|
+
ref,
|
|
90
|
+
) => {
|
|
62
91
|
const shape = useShape();
|
|
92
|
+
const toneBadge = tone ? STATUS_TONE_BADGE[tone] : undefined;
|
|
93
|
+
const variant = toneBadge?.variant ?? variantProp;
|
|
94
|
+
const color = toneBadge?.color ?? colorProp;
|
|
63
95
|
const resolvedColor =
|
|
64
96
|
color ??
|
|
65
97
|
(variant === "brand"
|
|
@@ -90,6 +122,7 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|
|
90
122
|
<span
|
|
91
123
|
ref={ref}
|
|
92
124
|
className={cn(badgeVariants({ variant, size }), shape.item, className)}
|
|
125
|
+
{...(tone ? { "data-tone": tone } : {})}
|
|
93
126
|
style={{ ...colorStyle, ...style }}
|
|
94
127
|
{...props}
|
|
95
128
|
>
|
|
@@ -111,5 +144,5 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|
|
111
144
|
|
|
112
145
|
Badge.displayName = "Badge";
|
|
113
146
|
|
|
114
|
-
export { Badge, badgeVariants, badgeColors };
|
|
115
|
-
export type { BadgeProps, BadgeColor };
|
|
147
|
+
export { Badge, badgeVariants, badgeColors, STATUS_TONE_BADGE };
|
|
148
|
+
export type { BadgeProps, BadgeColor, StatusTone };
|
|
@@ -27,8 +27,10 @@ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li"
|
|
|
27
27
|
BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
28
28
|
|
|
29
29
|
const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, React.ComponentProps<"a">>(
|
|
30
|
-
({ className, ...props }, ref) => (
|
|
31
|
-
<a ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props}
|
|
30
|
+
({ className, children, ...props }, ref) => (
|
|
31
|
+
<a ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props}>
|
|
32
|
+
{children}
|
|
33
|
+
</a>
|
|
32
34
|
),
|
|
33
35
|
);
|
|
34
36
|
BreadcrumbLink.displayName = "BreadcrumbLink";
|
|
@@ -30,7 +30,7 @@ function ButtonGroupItem({ active, className, children, ...props }: ButtonGroupI
|
|
|
30
30
|
className={cn(
|
|
31
31
|
"relative inline-flex min-h-8 items-center justify-center gap-1 rounded-[10px] px-3 py-1 body-sm font-medium",
|
|
32
32
|
"transition-[background-color,box-shadow,color,opacity,scale] active:scale-[0.96]",
|
|
33
|
-
"focus:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
|
33
|
+
"focus:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
34
34
|
active
|
|
35
35
|
? "bg-card text-foreground shadow-natural"
|
|
36
36
|
: "text-muted-foreground hover:text-foreground",
|
|
@@ -20,31 +20,34 @@ const buttonVariants = cva(
|
|
|
20
20
|
"text-box-trim-both text-box-edge-cap-alphabetic",
|
|
21
21
|
"transition-colors duration-80",
|
|
22
22
|
"disabled:opacity-50 disabled:pointer-events-none",
|
|
23
|
-
"
|
|
23
|
+
"compact:rounded-[5px]",
|
|
24
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
24
25
|
],
|
|
25
26
|
{
|
|
26
27
|
variants: {
|
|
27
28
|
variant: {
|
|
28
29
|
default: "text-background",
|
|
29
30
|
primary: "text-background",
|
|
30
|
-
brand: "text-
|
|
31
|
+
brand: "text-primary-foreground",
|
|
31
32
|
secondary: "text-foreground",
|
|
32
|
-
outline: "border border-border text-foreground",
|
|
33
|
-
tertiary: "border border-border text-foreground",
|
|
33
|
+
outline: "border border-border text-foreground hover:border-border-strong",
|
|
34
|
+
tertiary: "border border-border text-foreground hover:border-border-strong",
|
|
34
35
|
destructive: "text-destructive border border-destructive/30",
|
|
35
36
|
ghost: "text-muted-foreground hover:text-foreground",
|
|
36
37
|
link: "text-foreground underline-offset-4 hover:underline",
|
|
37
38
|
},
|
|
39
|
+
// `compact:` classes apply under data-density="compact" (editor chrome):
|
|
40
|
+
// 24px controls, 12px text, 8–10px padding, 5px radius.
|
|
38
41
|
size: {
|
|
39
|
-
xs: "h-6 px-2.5 text-[11px] gap-1",
|
|
40
|
-
sm: "h-7 px-3 text-[12px] gap-1",
|
|
41
|
-
default: "h-8 px-4 text-[13px] gap-1.5",
|
|
42
|
-
md: "h-8 px-4 text-[13px] gap-1.5",
|
|
43
|
-
lg: "h-9 px-5 text-[14px] gap-1.5",
|
|
44
|
-
"icon-xs": "h-7 w-7 p-0 [&_svg]:h-3 [&_svg]:w-3",
|
|
45
|
-
"icon-sm": "h-8 w-8 p-0 [&_svg]:h-3.5 [&_svg]:w-3.5",
|
|
46
|
-
icon: "h-9 w-9 p-0 [&_svg]:h-4 [&_svg]:w-4",
|
|
47
|
-
"icon-lg": "h-10 w-10 p-0 [&_svg]:h-5 [&_svg]:w-5",
|
|
42
|
+
xs: "h-6 px-2.5 text-[11px] gap-1 compact:px-2",
|
|
43
|
+
sm: "h-7 px-3 text-[12px] gap-1 compact:h-6 compact:px-2",
|
|
44
|
+
default: "h-8 px-4 text-[13px] gap-1.5 compact:h-6 compact:px-2.5 compact:text-[12px]",
|
|
45
|
+
md: "h-8 px-4 text-[13px] gap-1.5 compact:h-6 compact:px-2.5 compact:text-[12px]",
|
|
46
|
+
lg: "h-9 px-5 text-[14px] gap-1.5 compact:h-7 compact:px-3 compact:text-[12px]",
|
|
47
|
+
"icon-xs": "h-7 w-7 p-0 [&_svg]:h-3 [&_svg]:w-3 compact:h-6 compact:w-6",
|
|
48
|
+
"icon-sm": "h-8 w-8 p-0 [&_svg]:h-3.5 [&_svg]:w-3.5 compact:h-6 compact:w-6",
|
|
49
|
+
icon: "h-9 w-9 p-0 [&_svg]:h-4 [&_svg]:w-4 compact:h-7 compact:w-7",
|
|
50
|
+
"icon-lg": "h-10 w-10 p-0 [&_svg]:h-5 [&_svg]:w-5 compact:h-8 compact:w-8",
|
|
48
51
|
},
|
|
49
52
|
iconLeft: { true: "" },
|
|
50
53
|
iconRight: { true: "" },
|
|
@@ -76,27 +79,32 @@ interface ButtonProps
|
|
|
76
79
|
active?: boolean;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// Variant fills. Neutral variants tint with `foreground` alpha rather than a
|
|
83
|
+
// fixed palette colour so they mean the same thing in light, mid and dark:
|
|
84
|
+
// primary = inverted foreground, secondary = soft neutral fill, outline /
|
|
85
|
+
// tertiary / ghost = transparent with a neutral hover wash.
|
|
79
86
|
const bgVariants: Record<string, string> = {
|
|
80
87
|
default: "bg-foreground group-hover:bg-foreground/90 group-active:bg-foreground/80",
|
|
81
88
|
primary: "bg-foreground group-hover:bg-foreground/90 group-active:bg-foreground/80",
|
|
82
|
-
brand: "bg-
|
|
83
|
-
secondary:
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
brand: "bg-primary group-hover:bg-primary/90 group-active:bg-primary/80",
|
|
90
|
+
secondary:
|
|
91
|
+
"bg-foreground/[0.07] group-hover:bg-foreground/[0.11] group-active:bg-foreground/[0.15]",
|
|
92
|
+
outline: "bg-transparent group-hover:bg-foreground/[0.05] group-active:bg-foreground/[0.09]",
|
|
93
|
+
tertiary: "bg-transparent group-hover:bg-foreground/[0.05] group-active:bg-foreground/[0.09]",
|
|
86
94
|
destructive: "bg-destructive/10 group-hover:bg-destructive/15 group-active:bg-destructive/20",
|
|
87
|
-
ghost: "bg-transparent group-hover:bg-
|
|
95
|
+
ghost: "bg-transparent group-hover:bg-foreground/[0.06] group-active:bg-foreground/[0.1]",
|
|
88
96
|
link: "bg-transparent",
|
|
89
97
|
};
|
|
90
98
|
|
|
91
99
|
const activeBgVariants: Record<string, string> = {
|
|
92
100
|
default: "bg-foreground/80",
|
|
93
101
|
primary: "bg-foreground/80",
|
|
94
|
-
brand: "bg-
|
|
95
|
-
secondary: "bg-
|
|
96
|
-
outline: "bg-
|
|
97
|
-
tertiary: "bg-
|
|
102
|
+
brand: "bg-primary/80",
|
|
103
|
+
secondary: "bg-foreground/[0.15]",
|
|
104
|
+
outline: "bg-foreground/[0.09]",
|
|
105
|
+
tertiary: "bg-foreground/[0.09]",
|
|
98
106
|
destructive: "bg-destructive/20",
|
|
99
|
-
ghost: "bg-
|
|
107
|
+
ghost: "bg-foreground/[0.1]",
|
|
100
108
|
link: "bg-transparent",
|
|
101
109
|
};
|
|
102
110
|
|
|
@@ -34,7 +34,7 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
|
|
|
34
34
|
day_button: cn(
|
|
35
35
|
"flex h-9 w-9 items-center justify-center rounded-full body font-normal",
|
|
36
36
|
"text-foreground hover:bg-muted transition-colors",
|
|
37
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-
|
|
37
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
38
38
|
),
|
|
39
39
|
selected:
|
|
40
40
|
"[&>button]:bg-brand-primary [&>button]:text-background [&>button]:hover:bg-brand-primary/90",
|
package/src/components/card.tsx
CHANGED
|
@@ -65,13 +65,15 @@ const CardHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>
|
|
|
65
65
|
CardHeader.displayName = "CardHeader";
|
|
66
66
|
|
|
67
67
|
const CardTitle = React.forwardRef<HTMLHeadingElement, React.ComponentProps<"h3">>(
|
|
68
|
-
({ className, ...props }, ref) => (
|
|
68
|
+
({ className, children, ...props }, ref) => (
|
|
69
69
|
<h3
|
|
70
70
|
ref={ref}
|
|
71
71
|
data-slot="card-title"
|
|
72
72
|
className={cn("subheading text-balance text-foreground", className)}
|
|
73
73
|
{...props}
|
|
74
|
-
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</h3>
|
|
75
77
|
),
|
|
76
78
|
);
|
|
77
79
|
CardTitle.displayName = "CardTitle";
|
|
@@ -28,7 +28,7 @@ const CheckboxCard = React.forwardRef<HTMLLabelElement, CheckboxCardProps>(
|
|
|
28
28
|
"group flex min-w-0 items-start gap-3 rounded-lg border border-border bg-card p-3",
|
|
29
29
|
"text-sm text-foreground shadow-natural transition-[background-color,border-color,box-shadow,opacity,scale]",
|
|
30
30
|
"hover:bg-muted/40 active:scale-[0.98]",
|
|
31
|
-
"has-[:focus-visible]:ring-
|
|
31
|
+
"has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring",
|
|
32
32
|
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50",
|
|
33
33
|
className,
|
|
34
34
|
)}
|
|
@@ -138,6 +138,7 @@ const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
|
|
138
138
|
|
|
139
139
|
return (
|
|
140
140
|
<CheckboxGroupContext.Provider value={{ registerItem, activeIndex }}>
|
|
141
|
+
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- roving-focus group: focus/key events bubble up from the focusable checkbox rows */}
|
|
141
142
|
<div
|
|
142
143
|
ref={(node) => {
|
|
143
144
|
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
@@ -230,7 +231,7 @@ const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
|
|
230
231
|
<AnimatePresence>
|
|
231
232
|
{focusRect && (
|
|
232
233
|
<motion.div
|
|
233
|
-
className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-
|
|
234
|
+
className={`absolute ${shape.focusRing} pointer-events-none z-20 border-2 border-ring`}
|
|
234
235
|
initial={false}
|
|
235
236
|
animate={{
|
|
236
237
|
left: focusRect.left - 2,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, Copy } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
interface CodeBlockProps {
|
|
8
|
+
/** Code as a string (copied verbatim). */
|
|
9
|
+
children: string;
|
|
10
|
+
/** Optional language / filename label in the header. */
|
|
11
|
+
language?: string;
|
|
12
|
+
/** Max height before the block scrolls, e.g. 320 or "20rem". */
|
|
13
|
+
maxHeight?: number | string;
|
|
14
|
+
/** Show a copy button. Default: true. */
|
|
15
|
+
copy?: boolean;
|
|
16
|
+
/** Soft-wrap long lines instead of scrolling horizontally. Default: false. */
|
|
17
|
+
wrap?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Styled, scrollable `<pre>` with copy-to-clipboard — for API keys, webhook
|
|
23
|
+
* payloads, snippets and logs. No syntax highlighting (bring your own).
|
|
24
|
+
*/
|
|
25
|
+
function CodeBlock({
|
|
26
|
+
children,
|
|
27
|
+
language,
|
|
28
|
+
maxHeight,
|
|
29
|
+
copy = true,
|
|
30
|
+
wrap = false,
|
|
31
|
+
className,
|
|
32
|
+
}: CodeBlockProps) {
|
|
33
|
+
const [copied, setCopied] = React.useState(false);
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!copied) return;
|
|
36
|
+
const id = window.setTimeout(() => setCopied(false), 1500);
|
|
37
|
+
return () => window.clearTimeout(id);
|
|
38
|
+
}, [copied]);
|
|
39
|
+
|
|
40
|
+
const onCopy = async () => {
|
|
41
|
+
try {
|
|
42
|
+
await navigator.clipboard.writeText(children);
|
|
43
|
+
setCopied(true);
|
|
44
|
+
} catch {
|
|
45
|
+
setCopied(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="code-block"
|
|
52
|
+
className={cn(
|
|
53
|
+
"relative min-w-0 overflow-hidden rounded-lg border border-border bg-muted text-foreground",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{(language || copy) && (
|
|
58
|
+
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-1.5">
|
|
59
|
+
<span className="caption-sm font-mono text-muted-foreground">{language}</span>
|
|
60
|
+
{copy && (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={onCopy}
|
|
64
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
65
|
+
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
66
|
+
>
|
|
67
|
+
{copied ? (
|
|
68
|
+
<Check size={14} aria-hidden="true" />
|
|
69
|
+
) : (
|
|
70
|
+
<Copy size={14} aria-hidden="true" />
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
<pre
|
|
77
|
+
// Scrollable regions must be keyboard-focusable (axe scrollable-region-focusable).
|
|
78
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
79
|
+
tabIndex={0}
|
|
80
|
+
className={cn(
|
|
81
|
+
"overflow-auto p-3 font-mono text-xs leading-relaxed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
82
|
+
wrap ? "whitespace-pre-wrap break-words" : "whitespace-pre",
|
|
83
|
+
)}
|
|
84
|
+
style={
|
|
85
|
+
maxHeight !== undefined
|
|
86
|
+
? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight }
|
|
87
|
+
: undefined
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
<code>{children}</code>
|
|
91
|
+
</pre>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
CodeBlock.displayName = "CodeBlock";
|
|
97
|
+
|
|
98
|
+
export { CodeBlock };
|
|
99
|
+
export type { CodeBlockProps };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
|
-
import { motionClasses } from "../lib/interaction";
|
|
5
|
+
import { inputFocusWithinClasses, motionClasses } from "../lib/interaction";
|
|
6
6
|
import { useShape } from "../lib/shape-context";
|
|
7
7
|
import { ColorPickerPopover } from "./color-picker";
|
|
8
8
|
import type { ControlMobileSurface } from "./input";
|
|
@@ -53,9 +53,10 @@ function ColorInput({
|
|
|
53
53
|
return (
|
|
54
54
|
<div
|
|
55
55
|
className={cn(
|
|
56
|
-
"inline-flex h-8 items-stretch gap-0 overflow-hidden border border-border bg-background",
|
|
56
|
+
"inline-flex h-8 items-stretch gap-0 overflow-hidden border border-border bg-background hover:border-border-strong",
|
|
57
57
|
shape.input,
|
|
58
|
-
"
|
|
58
|
+
"compact:h-6 compact:rounded-[5px]",
|
|
59
|
+
inputFocusWithinClasses,
|
|
59
60
|
motionClasses,
|
|
60
61
|
mobileSurfaceClass,
|
|
61
62
|
disabled && "opacity-50 pointer-events-none",
|
|
@@ -69,7 +70,7 @@ function ColorInput({
|
|
|
69
70
|
{...(disabled !== undefined && { disabled })}
|
|
70
71
|
triggerShowValue={false}
|
|
71
72
|
hideEyedropper
|
|
72
|
-
triggerClassName="h-full w-8 border-0 rounded-none bg-transparent px-1 hover:bg-hover focus-visible:ring-0"
|
|
73
|
+
triggerClassName="h-full w-8 border-0 rounded-none bg-transparent px-1 hover:bg-hover focus-visible:ring-0 compact:h-full compact:w-6 compact:px-0 compact:justify-center compact:rounded-none compact:[&>span]:!size-4"
|
|
73
74
|
/>
|
|
74
75
|
<input
|
|
75
76
|
type="text"
|
|
@@ -81,7 +82,8 @@ function ColorInput({
|
|
|
81
82
|
commitHex("#" + (e.target as HTMLInputElement).value.replace(/^#/, ""));
|
|
82
83
|
}}
|
|
83
84
|
spellCheck={false}
|
|
84
|
-
|
|
85
|
+
aria-label="Hex colour"
|
|
86
|
+
className="w-[5.5rem] caption tabular-nums text-foreground px-2 bg-transparent border-l border-border focus:outline-none uppercase compact:w-[4.5rem] compact:px-1.5 compact:text-[12px]"
|
|
85
87
|
/>
|
|
86
88
|
{typeof opacity === "number" && onOpacityChange && (
|
|
87
89
|
<div className="relative flex items-center border-l border-border">
|
|
@@ -91,7 +93,8 @@ function ColorInput({
|
|
|
91
93
|
max={100}
|
|
92
94
|
value={Math.round(opacity)}
|
|
93
95
|
onChange={(e) => onOpacityChange(Number(e.target.value))}
|
|
94
|
-
|
|
96
|
+
aria-label="Opacity"
|
|
97
|
+
className="w-12 caption tabular-nums text-foreground px-2 bg-transparent focus:outline-none text-right compact:w-10 compact:px-1.5 compact:text-[12px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
95
98
|
/>
|
|
96
99
|
<span aria-hidden className="caption-xs text-muted-foreground pr-2">
|
|
97
100
|
%
|