@hilum/ui 3.8.6 → 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.
Files changed (90) hide show
  1. package/README.md +32 -1
  2. package/dist/{chunk-RI6QJR2L.js → chunk-R576DV2A.js} +120 -14
  3. package/dist/chunk-R576DV2A.js.map +1 -0
  4. package/dist/create-theme.js +2 -1
  5. package/dist/create-theme.js.map +1 -1
  6. package/dist/index.d.ts +733 -68
  7. package/dist/index.js +3097 -1619
  8. package/dist/index.js.map +1 -1
  9. package/dist/tokens.css +163 -19
  10. package/dist/tokens.d.ts +69 -5
  11. package/dist/tokens.js +1 -1
  12. package/llms.txt +11 -4
  13. package/package.json +11 -10
  14. package/src/components/accordion.tsx +2 -2
  15. package/src/components/account-menu.tsx +1 -1
  16. package/src/components/alert-dialog.tsx +107 -0
  17. package/src/components/ask-user-questions.tsx +24 -18
  18. package/src/components/badge.tsx +36 -3
  19. package/src/components/breadcrumb.tsx +4 -2
  20. package/src/components/button-group.tsx +1 -1
  21. package/src/components/button.tsx +31 -23
  22. package/src/components/calendar.tsx +1 -1
  23. package/src/components/card.tsx +4 -2
  24. package/src/components/checkbox-card.tsx +1 -1
  25. package/src/components/checkbox-group.tsx +2 -1
  26. package/src/components/code-block.tsx +99 -0
  27. package/src/components/color-input.tsx +9 -6
  28. package/src/components/color-picker.tsx +38 -26
  29. package/src/components/combobox.tsx +59 -12
  30. package/src/components/command-palette.tsx +150 -42
  31. package/src/components/command.tsx +5 -3
  32. package/src/components/context-menu.tsx +18 -15
  33. package/src/components/contextual-save-bar.tsx +156 -0
  34. package/src/components/data-table.tsx +34 -5
  35. package/src/components/date-picker.tsx +275 -28
  36. package/src/components/date-text.tsx +90 -0
  37. package/src/components/dropdown-menu.tsx +11 -8
  38. package/src/components/dropdown.tsx +6 -3
  39. package/src/components/empty-state.tsx +85 -20
  40. package/src/components/file-dropzone.tsx +2 -1
  41. package/src/components/file-thumbnail.tsx +84 -20
  42. package/src/components/filter-bar.tsx +91 -0
  43. package/src/components/grid-list.tsx +2 -2
  44. package/src/components/help-tooltip.tsx +1 -1
  45. package/src/components/input-copy.tsx +1 -1
  46. package/src/components/input-group.tsx +1 -1
  47. package/src/components/input-message.tsx +5 -4
  48. package/src/components/input-number.tsx +103 -11
  49. package/src/components/input.tsx +5 -3
  50. package/src/components/label.tsx +1 -0
  51. package/src/components/media-asset-card.tsx +1 -1
  52. package/src/components/menu-item.tsx +37 -3
  53. package/src/components/menubar.tsx +21 -18
  54. package/src/components/native-select.tsx +1 -1
  55. package/src/components/navigation-menu.tsx +2 -2
  56. package/src/components/notification.tsx +1 -1
  57. package/src/components/pagination.tsx +97 -3
  58. package/src/components/popover.tsx +2 -0
  59. package/src/components/property-row.tsx +7 -4
  60. package/src/components/radio-card.tsx +1 -1
  61. package/src/components/radio-group.tsx +2 -1
  62. package/src/components/rating.tsx +136 -0
  63. package/src/components/resizable.tsx +1 -1
  64. package/src/components/resource-item.tsx +133 -0
  65. package/src/components/rich-text-editor.tsx +1 -0
  66. package/src/components/search-input.tsx +98 -0
  67. package/src/components/searchable-table.tsx +10 -11
  68. package/src/components/select.tsx +28 -9
  69. package/src/components/sidebar.tsx +4 -4
  70. package/src/components/slider.tsx +78 -36
  71. package/src/components/sonner.tsx +18 -4
  72. package/src/components/stat-card.tsx +121 -37
  73. package/src/components/status-badge.tsx +142 -47
  74. package/src/components/status-tile.tsx +9 -0
  75. package/src/components/switch.tsx +6 -5
  76. package/src/components/tabs-subtle.tsx +10 -2
  77. package/src/components/tabs.tsx +23 -3
  78. package/src/components/textarea.tsx +2 -3
  79. package/src/components/thinking-steps.tsx +3 -3
  80. package/src/components/toggle.tsx +14 -13
  81. package/src/components/tooltip.tsx +11 -8
  82. package/src/components/url-redirect-prompt.tsx +0 -1
  83. package/src/index.ts +15 -0
  84. package/src/lib/density-context.tsx +65 -0
  85. package/src/lib/format.tsx +301 -0
  86. package/src/lib/interaction.ts +12 -4
  87. package/src/lib/scroll-fade.tsx +39 -0
  88. package/src/tokens/tokens.test.ts +77 -0
  89. package/src/tokens/tokens.ts +114 -5
  90. 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-[#6B97FF] z-20",
779
+ "absolute pointer-events-none border-2 border-ring z-20",
774
780
  shape.focusRing,
775
781
  )}
776
782
  initial={{
@@ -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
- ({ className, variant = "solid", size = "md", color, children, style, ...props }, ref) => {
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/30",
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
- "focus-visible:ring-1 focus-visible:ring-[#6B97FF]",
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-background",
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-brand group-hover:bg-brand/90 group-active:bg-brand/80",
83
- secondary: "bg-accent group-hover:bg-accent/80 group-active:bg-accent",
84
- outline: "bg-transparent group-hover:bg-hover group-active:bg-active",
85
- tertiary: "bg-transparent group-hover:bg-hover group-active:bg-active",
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-hover group-active:bg-active",
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-brand/80",
95
- secondary: "bg-accent",
96
- outline: "bg-active",
97
- tertiary: "bg-active",
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-active",
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-brand-primary/30",
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",
@@ -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-1 has-[:focus-visible]:ring-[#6B97FF]",
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-[#6B97FF]`}
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
- "focus-within:border-brand-primary focus-within:ring-2 focus-within:ring-brand-primary/20",
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
- className="w-[5.5rem] caption text-foreground px-2 bg-transparent border-l border-border focus:outline-none uppercase"
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
- className="w-12 caption text-foreground px-2 bg-transparent focus:outline-none text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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
  %