@instruments/materia-ui 0.1.0 → 0.3.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 (55) hide show
  1. package/agent-docs.md +4 -3
  2. package/dist/components/animated-number/number-digit.d.ts +5 -3
  3. package/dist/components/animated-number/number-digit.js +39 -16
  4. package/dist/components/animated-number/number-section.d.ts +7 -5
  5. package/dist/components/animated-number/number-section.js +81 -65
  6. package/dist/components/animated-number/number-symbol.js +2 -0
  7. package/dist/components/animated-number/target-widths.d.ts +7 -0
  8. package/dist/components/animated-number/target-widths.js +13 -1
  9. package/dist/components/button/button-variants.js +53 -8
  10. package/dist/components/button-group/button-group-root.js +8 -6
  11. package/dist/components/calendar/calendar-root.js +39 -13
  12. package/dist/components/chart/chart-tooltip.js +7 -5
  13. package/dist/components/empty/empty-media.js +5 -5
  14. package/dist/components/input-group/input-group-textarea.d.ts +3 -2
  15. package/dist/components/input-group/input-group-textarea.js +8 -4
  16. package/dist/components/pill/pill-variants.js +21 -10
  17. package/dist/components/progress/progress-root.d.ts +7 -5
  18. package/dist/components/progress/progress-root.js +26 -5
  19. package/dist/components/radio-group/radio-group-root.d.ts +7 -3
  20. package/dist/components/radio-group/radio-group-root.js +14 -4
  21. package/dist/components/segmented-control/segmented-control-indicator.js +1 -1
  22. package/dist/components/segmented-control/segmented-control-item.js +1 -1
  23. package/dist/components/segmented-control/segmented-control-root.js +1 -1
  24. package/dist/components/segmented-control/segmented-control-variants.js +1 -1
  25. package/dist/components/stepper/stepper-item.js +6 -2
  26. package/dist/components/stepper/stepper-variants.d.ts +1 -0
  27. package/dist/components/stepper/stepper-variants.js +11 -1
  28. package/dist/components/tab-navigation/tab-navigation-link.js +1 -1
  29. package/dist/components/tabs/tabs-indicator.js +1 -1
  30. package/dist/components/tabs/tabs-trigger.js +1 -1
  31. package/dist/components/tabs/tabs-variants.js +1 -1
  32. package/dist/components/textarea/textarea-root.d.ts +1 -1
  33. package/dist/components/textarea/textarea-root.js +6 -9
  34. package/dist/components/textarea/use-textarea-rows.d.ts +22 -0
  35. package/dist/components/textarea/use-textarea-rows.js +43 -0
  36. package/dist/compositions/data-grid-pagination/data-grid-pagination-root.d.ts +8 -6
  37. package/dist/compositions/data-grid-pagination/data-grid-pagination-root.js +11 -20
  38. package/dist/compositions/product-card/product-card-color-count.js +26 -26
  39. package/dist/compositions/product-card/product-card-compact-arm.js +2 -2
  40. package/dist/compositions/product-card/product-card-content-types.d.ts +5 -0
  41. package/dist/compositions/product-card/product-card-full-arm.js +2 -2
  42. package/dist/compositions/product-card/product-card-provider-types.d.ts +10 -0
  43. package/dist/compositions/product-card/product-card-provider.js +1 -1
  44. package/dist/compositions/product-card/product-card-swatch-selector.js +4 -3
  45. package/dist/data/docs-manifest.d.ts +1 -1
  46. package/dist/data/docs-manifest.js +29 -74
  47. package/dist/styles/index.css +17 -6
  48. package/dist/{components/segmented/segmented-variants.d.ts → tokens/segmented.d.ts} +12 -1
  49. package/dist/{components/segmented/segmented-variants.js → tokens/segmented.js} +11 -0
  50. package/lint/tokens.mjs +1 -0
  51. package/package.json +5 -5
  52. package/dist/components/segmented/segmented-root.d.ts +0 -12
  53. package/dist/components/segmented/segmented-root.js +0 -12
  54. package/dist/components/segmented.d.ts +0 -1
  55. package/dist/components/segmented.js +0 -1
@@ -13,9 +13,16 @@ interface DataGridPaginationProps {
13
13
  info?: string;
14
14
  /** Placeholder rendered in place of the range summary while loading. */
15
15
  infoSkeleton?: ReactNode;
16
+ /**
17
+ * Collapse the page buttons into groups of `moreLimit`, with ellipsis
18
+ * buttons that jump to the next or previous group. Off shows a button for
19
+ * every page.
20
+ * @default true
21
+ */
16
22
  more?: boolean;
17
23
  /**
18
- * Number of page buttons shown before collapsing to an ellipsis.
24
+ * Number of page buttons shown before collapsing to an ellipsis. Needs
25
+ * `more`.
19
26
  * @default 5
20
27
  */
21
28
  moreLimit?: number;
@@ -39,11 +46,6 @@ interface DataGridPaginationProps {
39
46
  * @default [5, 10, 25, 50, 100]
40
47
  */
41
48
  sizes?: number[];
42
- /** @default "per page" */
43
- sizesDescription?: string;
44
- sizesInfo?: string;
45
- /** @default "Show" */
46
- sizesLabel?: string;
47
49
  /** Placeholder rendered in place of the rows-per-page select while loading. */
48
50
  sizesSkeleton?: ReactNode;
49
51
  }
@@ -6,28 +6,17 @@ import { Skeleton } from "@instruments/materia-ui/components/skeleton";
6
6
  import { useDataGrid } from "@instruments/materia-ui/compositions/data-grid";
7
7
  import { cn } from "@instruments/materia-ui/utils/cn";
8
8
  import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
9
+ import { useId } from "react";
9
10
  const defaultProps = {
10
- /** Ellipsis text */
11
11
  ellipsisText: "...",
12
12
  info: "{from} - {to} of {count}",
13
- /** Info skeleton */
14
13
  infoSkeleton: _jsx(Skeleton, { className: "h-8 w-60" }),
15
- more: false,
16
- /** More limit */
14
+ more: true,
17
15
  moreLimit: 5,
18
- /** Next page label */
19
16
  nextPageLabel: "Go to next page",
20
- /** Previous page label */
21
17
  previousPageLabel: "Go to previous page",
22
- /** Rows per page label */
23
18
  rowsPerPageLabel: "Rows per page",
24
- /** Component size */
25
19
  sizes: [5, 10, 25, 50, 100],
26
- /** Component size */
27
- sizesDescription: "per page",
28
- /** Component size */
29
- sizesLabel: "Show",
30
- /** Component size */
31
20
  sizesSkeleton: _jsx(Skeleton, { className: "h-8 w-44" }),
32
21
  };
33
22
  const BTN_BASE_CLASSES = "size-7 p-0 text-paragraph-sm";
@@ -41,20 +30,22 @@ const formatPaginationInfo = (template, from, to, count) => template !== undefin
41
30
  : `${from} - ${to} of ${count}`;
42
31
  const RowsPerPageSelect = ({ rowsPerPageLabel, sizes, table, }) => {
43
32
  const { pageSize } = table.state.pagination;
44
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground text-paragraph-sm", children: rowsPerPageLabel }), _jsxs(Select, { onValueChange: (value) => {
33
+ const labelId = useId();
34
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground text-paragraph-sm", id: labelId, children: rowsPerPageLabel }), _jsxs(Select, { onValueChange: (value) => {
45
35
  const newPageSize = Number(value);
46
36
  table.setPageSize(newPageSize);
47
- }, value: `${pageSize}`, children: [_jsx(SelectTrigger, { className: "w-fit", size: "sm", children: _jsx(SelectValue, { placeholder: `${pageSize}` }) }), _jsx(SelectContent, { className: "min-w-12.5", side: "top", children: sizes?.map((size) => (_jsx(SelectItem, { value: `${size}`, children: size }, size))) })] })] }));
37
+ }, value: `${pageSize}`, children: [_jsx(SelectTrigger, { "aria-labelledby": labelId, className: "w-fit", size: "sm", children: _jsx(SelectValue, { placeholder: `${pageSize}` }) }), _jsx(SelectContent, { className: "min-w-12.5", side: "top", children: sizes?.map((size) => (_jsx(SelectItem, { value: `${size}`, children: size }, size))) })] })] }));
48
38
  };
49
- const PaginationNav = ({ ellipsisText, moreLimit, nextPageLabel, previousPageLabel, table, }) => {
39
+ const PaginationNav = ({ ellipsisText, more, moreLimit, nextPageLabel, previousPageLabel, table, }) => {
50
40
  const { pageIndex } = table.state.pagination;
51
41
  const pageCount = table.getPageCount();
52
42
  if (pageCount <= 1) {
53
43
  return null;
54
44
  }
55
- // Determine the start and end of the pagination group
56
- const currentGroupStart = Math.floor(pageIndex / moreLimit) * moreLimit;
57
- const currentGroupEnd = Math.min(currentGroupStart + moreLimit, pageCount);
45
+ // Without `more`, one group holds every page and no ellipsis renders.
46
+ const groupSize = more ? moreLimit : pageCount;
47
+ const currentGroupStart = Math.floor(pageIndex / groupSize) * groupSize;
48
+ const currentGroupEnd = Math.min(currentGroupStart + groupSize, pageCount);
58
49
  // Page indices for the current group
59
50
  const pageIndices = Array.from({ length: Math.max(0, currentGroupEnd - currentGroupStart) }, (_, idx) => currentGroupStart + idx);
60
51
  return (_jsxs(Flex, { align: "center", className: "order-1 space-x-1 sm:order-2", children: [_jsx(Button, { "aria-label": previousPageLabel, emphasis: "ghost", className: BTN_ARROW_CLASSES, disabled: !table.getCanPreviousPage(), icon: ChevronLeftIcon, onClick: () => {
@@ -91,6 +82,6 @@ const DataGridPagination = (props) => {
91
82
  const from = pageIndex * pageSize + 1;
92
83
  const to = Math.min((pageIndex + 1) * pageSize, recordCount);
93
84
  const paginationInfo = formatPaginationInfo(mergedProps.info, from, to, recordCount);
94
- return (_jsxs(Flex, { align: "center", className: cn("grow gap-2.5 py-2.5 sm:flex-row sm:py-0", mergedProps?.className), "data-component": "data-grid-pagination", "data-slot": "data-grid-pagination", direction: "column", justify: "space-between", wrap: "wrap", children: [_jsx(Flex, { align: "center", className: "order-2 space-x-2.5 pb-2.5 sm:order-1 sm:pb-0", wrap: "wrap", children: loading === true ? (mergedProps?.sizesSkeleton) : (_jsx(RowsPerPageSelect, { rowsPerPageLabel: mergedProps.rowsPerPageLabel, sizes: mergedProps.sizes, table: table })) }), _jsx(Flex, { align: "center", className: "order-1 gap-2.5 pt-2.5 sm:order-2 sm:flex-row sm:justify-end sm:pt-0", direction: "column", justify: "center", children: loading === true ? (mergedProps?.infoSkeleton) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground text-paragraph-sm order-2 text-nowrap sm:order-1", children: paginationInfo }), _jsx(PaginationNav, { ellipsisText: mergedProps.ellipsisText, moreLimit: mergedProps?.moreLimit ?? 5, nextPageLabel: mergedProps.nextPageLabel, previousPageLabel: mergedProps.previousPageLabel, table: table })] })) })] }));
85
+ return (_jsxs(Flex, { align: "center", className: cn("grow gap-2.5 py-2.5 sm:flex-row sm:py-0", mergedProps?.className), "data-component": "data-grid-pagination", "data-slot": "data-grid-pagination", direction: "column", justify: "space-between", wrap: "wrap", children: [_jsx(Flex, { align: "center", className: "order-2 space-x-2.5 pb-2.5 sm:order-1 sm:pb-0", wrap: "wrap", children: loading === true ? (mergedProps?.sizesSkeleton) : (_jsx(RowsPerPageSelect, { rowsPerPageLabel: mergedProps.rowsPerPageLabel, sizes: mergedProps.sizes, table: table })) }), _jsx(Flex, { align: "center", className: "order-1 gap-2.5 pt-2.5 sm:order-2 sm:flex-row sm:justify-end sm:pt-0", direction: "column", justify: "center", children: loading === true ? (mergedProps?.infoSkeleton) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground text-paragraph-sm order-2 text-nowrap sm:order-1", children: paginationInfo }), _jsx(PaginationNav, { ellipsisText: mergedProps.ellipsisText, more: mergedProps.more ?? true, moreLimit: mergedProps?.moreLimit ?? 5, nextPageLabel: mergedProps.nextPageLabel, previousPageLabel: mergedProps.previousPageLabel, table: table })] })) })] }));
95
86
  };
96
87
  export { DataGridPagination };
@@ -15,25 +15,24 @@ import { useProductCard } from "./product-card-context.js";
15
15
  * instantly where a count cannot.
16
16
  *
17
17
  * The design draws 16px dots on a 26px pitch, the selected one wearing a ring,
18
- * then `+N` for the remainder; what renders is the nearest token step - see
19
- * {@link INLINE_SWATCHES}. Clicking `+N` opens the full set.
18
+ * then `+N` for the remainder; what renders is the nearest token step. The
19
+ * card's `maxSwatches` caps the row (five by default) and clicking `+N` opens
20
+ * the full set.
20
21
  */
21
22
  /**
22
- * Circles shown inline before the rest go behind `+N`.
23
+ * Circles a card under 240px wide can show beside the overflow label.
23
24
  *
24
- * The design draws five dots on a 26px pitch. the kit's smallest chip step is
25
- * `xs` (20px), which on the row's 10px gap gives a 30px pitch - five dots plus
26
- * the overflow label come to ~170px, which fits the full card's 204px content
27
- * column.
28
- *
29
- * Below 240px one dot is dropped and counted into `+N` instead: a 180px card
30
- * offers 144px and the label would clip off the right edge.
25
+ * The kit's smallest chip step is `xs` (20px), which on the row's 10px gap
26
+ * gives a 30px pitch. Five dots plus the label come to ~170px, which fits the
27
+ * full card's 204px content column, but a 180px card offers 144px and the label
28
+ * would clip off the right edge. So below 240px the row stops at four and
29
+ * counts the rest into `+N`.
31
30
  */
32
- const INLINE_SWATCHES = 5;
31
+ const NARROW_SWATCHES = 4;
33
32
  /** The popover still caps its grid, so a 200-colourway family stays a panel. */
34
33
  const MAX_POPOVER_SWATCHES = 25;
35
34
  export const ProductCardColorCount = () => {
36
- const { product, selectedVariantId, onVariantChange, renderImage } = useProductCard();
35
+ const { maxSwatches, onVariantChange, product, renderImage, selectedVariantId, } = useProductCard();
37
36
  const variantCount = product.variantCount ?? 1;
38
37
  const variants = product.variants ?? [product];
39
38
  // One colourway is not a choice, so it is not a control. Same rule the primary
@@ -41,22 +40,23 @@ export const ProductCardColorCount = () => {
41
40
  if (variantCount <= 1) {
42
41
  return null;
43
42
  }
44
- const inlineVariants = variants.slice(0, INLINE_SWATCHES);
43
+ const inlineVariants = variants.slice(0, Math.max(0, maxSwatches));
45
44
  // The overflow counts against the GRAPH's total, not the array we happen to
46
45
  // hold - a page of variants is not the family.
47
46
  const inlineOverflow = variantCount - inlineVariants.length;
48
- // The fifth dot is hidden below 240px, so the label there must count it. It
49
- // only exists to be hidden when the row actually reached five.
50
- const hasBreakpointOnlySwatch = inlineVariants.length === INLINE_SWATCHES;
51
- const mediumOverflow = inlineOverflow + (hasBreakpointOnlySwatch ? 1 : 0);
52
- // WHICH dot drops below 240px. The last one, unless the last one is the
53
- // SELECTED colourway - hiding that leaves a narrow card showing four dots
54
- // with no ring on any of them, so the card reads as if nothing is chosen
55
- // while its photograph shows a colourway the row does not contain.
56
- const lastIndex = INLINE_SWATCHES - 1;
57
- const breakpointOnlyIndex = inlineVariants[lastIndex]?.id === selectedVariantId
58
- ? lastIndex - 1
59
- : lastIndex;
47
+ // Dots past NARROW_SWATCHES are hidden below 240px, so the label there must
48
+ // count them too.
49
+ const narrowCount = Math.min(inlineVariants.length, NARROW_SWATCHES);
50
+ const mediumOverflow = inlineOverflow + (inlineVariants.length - narrowCount);
51
+ // WHICH dots drop below 240px: the ones past the cut, unless one of them is
52
+ // the SELECTED colourway. Hiding that leaves a narrow card with no ring on
53
+ // any dot, so the card reads as if nothing is chosen while its photograph
54
+ // shows a colourway the row does not contain. The selected dot keeps its
55
+ // place and the last dot before the cut drops instead.
56
+ const selectedIndex = inlineVariants.findIndex((variant) => variant.id === selectedVariantId);
57
+ const isNarrowHidden = (index) => index < narrowCount
58
+ ? selectedIndex >= narrowCount && index === narrowCount - 1
59
+ : index !== selectedIndex;
60
60
  const hasPopoverOverflow = variantCount > MAX_POPOVER_SWATCHES;
61
61
  const popoverVisibleCount = hasPopoverOverflow
62
62
  ? MAX_POPOVER_SWATCHES - 1
@@ -71,7 +71,7 @@ export const ProductCardColorCount = () => {
71
71
  sizes: size,
72
72
  src: variant.images[0] ?? "",
73
73
  })] }));
74
- return (_jsxs(Box, { className: "flex items-center gap-2.5", children: [_jsx(SwatchSelector, { className: "flex items-center gap-2.5", onValueChange: onVariantChange, size: "xs", value: selectedVariantId, children: inlineVariants.map((variant, index) => (_jsx(Swatch, { className: hasBreakpointOnlySwatch && index === breakpointOnlyIndex
74
+ return (_jsxs(Box, { className: "flex items-center gap-2.5", children: [_jsx(SwatchSelector, { className: "flex items-center gap-2.5", onValueChange: onVariantChange, size: "xs", value: selectedVariantId, children: inlineVariants.map((variant, index) => (_jsx(Swatch, { className: isNarrowHidden(index)
75
75
  ? "hidden @[240px]/product-card:inline-flex"
76
76
  : undefined, unavailable: variant.available === false, value: variant.id, children: swatchImage(variant, "20px") }, variant.id))) }), mediumOverflow > 0 ? (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { render: _jsx("button", { "aria-label": `Show all ${variantCount} ${product.variantLabel ?? "colours"}`, className: cn("text-muted-foreground hover:text-foreground text-caption cursor-pointer tabular-nums",
77
77
  // Exactly five colourways leaves nothing behind once the fifth
@@ -14,8 +14,8 @@ import { ProductCardStatusBadge } from "./product-card-status-badge.js";
14
14
  import { ProductCardTools } from "./product-card-tools.js";
15
15
  /** Compact variant: below 180px, or always in compact mode. */
16
16
  export const ProductCardCompactArm = ({ compactFooterControl, overrideImageUrl, priority, similarityScore, toolsActive, }) => {
17
- const { action, mode, onQuickView, statusBadge } = useProductCard();
17
+ const { action, mode, onQuickView, showSwatches, statusBadge } = useProductCard();
18
18
  const isCompactMode = mode === "compact";
19
19
  const hasCompactFooterControl = compactFooterControl !== null && compactFooterControl !== undefined;
20
- return (_jsxs(Box, { className: cn("flex-col", isCompactMode ? "flex" : "flex h-full @[180px]/product-card:hidden"), children: [_jsxs(Box, { className: "relative w-full overflow-hidden rounded-3xl shadow-sm", children: [_jsx(ProductCardImageLink, { imageClassName: isCompactMode ? "!p-0" : undefined, overrideImageUrl: overrideImageUrl, priority: priority, similarityScore: similarityScore }), _jsx(ProductCardTools, { active: toolsActive }), _jsx(ProductCardQuickView, { forceVisible: true, onQuickView: onQuickView })] }), _jsxs(VStack, { className: "min-h-0 flex-1 gap-2.5 px-1.5 py-2", children: [_jsxs(VStack, { className: "gap-1 px-0.5", children: [_jsxs(Box, { className: "flex items-start justify-between gap-2", children: [_jsx(VStack, { className: "min-w-0 flex-1 gap-0.5", children: _jsx(ProductCardCaption, { scale: "compact" }) }), statusBadge?.icon ? (_jsx(ProductCardStatusBadge, { ...statusBadge })) : null] }), _jsx(ProductCardMeta, {}), action ? _jsx(ProductCardAction, { ...action }) : null, mode !== "single" && (_jsx(Box, { className: "min-h-5 pt-2", children: _jsx(ProductCardColorCount, {}) }))] }), hasCompactFooterControl ? (_jsx(Box, { className: "mt-auto flex justify-end", children: compactFooterControl })) : null] })] }));
20
+ return (_jsxs(Box, { className: cn("flex-col", isCompactMode ? "flex" : "flex h-full @[180px]/product-card:hidden"), children: [_jsxs(Box, { className: "relative w-full overflow-hidden rounded-3xl shadow-sm", children: [_jsx(ProductCardImageLink, { imageClassName: isCompactMode ? "!p-0" : undefined, overrideImageUrl: overrideImageUrl, priority: priority, similarityScore: similarityScore }), _jsx(ProductCardTools, { active: toolsActive }), _jsx(ProductCardQuickView, { forceVisible: true, onQuickView: onQuickView })] }), _jsxs(VStack, { className: "min-h-0 flex-1 gap-2.5 px-1.5 py-2", children: [_jsxs(VStack, { className: "gap-1 px-0.5", children: [_jsxs(Box, { className: "flex items-start justify-between gap-2", children: [_jsx(VStack, { className: "min-w-0 flex-1 gap-0.5", children: _jsx(ProductCardCaption, { scale: "compact" }) }), statusBadge?.icon ? (_jsx(ProductCardStatusBadge, { ...statusBadge })) : null] }), _jsx(ProductCardMeta, {}), action ? _jsx(ProductCardAction, { ...action }) : null, showSwatches && mode !== "single" && (_jsx(Box, { className: "min-h-5 pt-2", children: _jsx(ProductCardColorCount, {}) }))] }), hasCompactFooterControl ? (_jsx(Box, { className: "mt-auto flex justify-end", children: compactFooterControl })) : null] })] }));
21
21
  };
@@ -28,6 +28,11 @@ interface ProductCardDisplayOptions {
28
28
  * reserved height.
29
29
  */
30
30
  nameLines?: 1 | 2 | undefined;
31
+ /**
32
+ * Show the row of colourway swatches. Single-colour products and `single`
33
+ * mode never show it.
34
+ * @default true
35
+ */
31
36
  swatches?: boolean | undefined;
32
37
  }
33
38
  /**
@@ -31,6 +31,6 @@ import { ProductCardTools } from "./product-card-tools.js";
31
31
  * fetched - and the popover mounts nothing until it opens.
32
32
  */
33
33
  export const ProductCardFullArm = ({ footerControl, overrideImageUrl, priority, similarityScore, toolsActive, }) => {
34
- const { action, active, mode, onQuickView, statusBadge } = useProductCard();
35
- return (_jsxs(VStack, { className: cn("bg-card duration-normal h-full overflow-hidden rounded-3xl transition-shadow ease-in-out @[240px]/product-card:rounded-4xl", mode === "compact" ? "hidden" : "hidden @[180px]/product-card:flex", active ? "shadow-lg" : "shadow-sm hover:shadow-md"), gap: "none", children: [_jsxs(Box, { className: "relative w-full", children: [_jsx(ProductCardImageLink, { overrideImageUrl: overrideImageUrl, priority: priority, similarityScore: similarityScore }), statusBadge && !statusBadge.icon ? (_jsx(ProductCardStatusBadge, { ...statusBadge })) : null, _jsx(ProductCardTools, { active: toolsActive }), _jsx(ProductCardQuickView, { onQuickView: onQuickView })] }), _jsx(VStack, { className: "flex-1 pt-2 pb-4", gap: "none", children: _jsxs(VStack, { className: "min-h-0 flex-1 gap-2.5 px-4 @[240px]/product-card:gap-4", gap: "none", children: [_jsxs(VStack, { className: "gap-1 px-0.5", gap: "none", children: [_jsxs(Box, { className: "flex items-start justify-between gap-2", children: [_jsx(VStack, { className: "min-w-0 flex-1", gap: "none", children: _jsx(ProductCardCaption, {}) }), statusBadge?.icon ? (_jsx(ProductCardStatusBadge, { ...statusBadge })) : null] }), _jsx(ProductCardMeta, {}), action ? _jsx(ProductCardAction, { ...action }) : null, mode !== "single" && (_jsx(Box, { className: "pt-2", children: _jsx(Box, { className: "min-h-5", children: _jsx(ProductCardColorCount, {}) }) }))] }), footerControl !== null && footerControl !== undefined ? (_jsx(HStack, { className: "mt-auto", children: footerControl })) : null] }) })] }));
34
+ const { action, active, mode, onQuickView, showSwatches, statusBadge } = useProductCard();
35
+ return (_jsxs(VStack, { className: cn("bg-card duration-normal h-full overflow-hidden rounded-3xl transition-shadow ease-in-out @[240px]/product-card:rounded-4xl", mode === "compact" ? "hidden" : "hidden @[180px]/product-card:flex", active ? "shadow-lg" : "shadow-sm hover:shadow-md"), gap: "none", children: [_jsxs(Box, { className: "relative w-full", children: [_jsx(ProductCardImageLink, { overrideImageUrl: overrideImageUrl, priority: priority, similarityScore: similarityScore }), statusBadge && !statusBadge.icon ? (_jsx(ProductCardStatusBadge, { ...statusBadge })) : null, _jsx(ProductCardTools, { active: toolsActive }), _jsx(ProductCardQuickView, { onQuickView: onQuickView })] }), _jsx(VStack, { className: "flex-1 pt-2 pb-4", gap: "none", children: _jsxs(VStack, { className: "min-h-0 flex-1 gap-2.5 px-4 @[240px]/product-card:gap-4", gap: "none", children: [_jsxs(VStack, { className: "gap-1 px-0.5", gap: "none", children: [_jsxs(Box, { className: "flex items-start justify-between gap-2", children: [_jsx(VStack, { className: "min-w-0 flex-1", gap: "none", children: _jsx(ProductCardCaption, {}) }), statusBadge?.icon ? (_jsx(ProductCardStatusBadge, { ...statusBadge })) : null] }), _jsx(ProductCardMeta, {}), action ? _jsx(ProductCardAction, { ...action }) : null, showSwatches && mode !== "single" && (_jsx(Box, { className: "pt-2", children: _jsx(Box, { className: "min-h-5", children: _jsx(ProductCardColorCount, {}) }) }))] }), footerControl !== null && footerControl !== undefined ? (_jsx(HStack, { className: "mt-auto", children: footerControl })) : null] }) })] }));
36
36
  };
@@ -134,6 +134,11 @@ interface ProductCardProviderProps {
134
134
  * dynamic routes).
135
135
  */
136
136
  linkPrefetch?: boolean | undefined;
137
+ /**
138
+ * How many colourway swatches the row shows before the rest go behind `+N`.
139
+ * Cards under 240px wide show at most four.
140
+ * @default 5
141
+ */
137
142
  maxSwatches?: number | undefined;
138
143
  /**
139
144
  * Mode determines the card behavior:
@@ -253,6 +258,11 @@ type ProductCardProps = ComponentProps<"div"> & Pick<ProductCardProviderProps, "
253
258
  showAddToCart?: boolean | undefined;
254
259
  showProductBrand?: boolean | undefined;
255
260
  showProductName?: boolean | undefined;
261
+ /**
262
+ * Show the row of colourway swatches. Single-colour products and `single`
263
+ * mode never show it.
264
+ * @default true
265
+ */
256
266
  showSwatches?: boolean | undefined;
257
267
  similarityScore?: number | undefined;
258
268
  };
@@ -66,7 +66,7 @@ const resolveDisplay = (display, hasRenderAddToCart) => ({
66
66
  showAddToCart: (display?.addToCart ?? true) && hasRenderAddToCart,
67
67
  showProductBrand: display?.brand ?? true,
68
68
  showProductName: display?.name ?? true,
69
- showSwatches: display?.swatches ?? false,
69
+ showSwatches: display?.swatches ?? true,
70
70
  });
71
71
  /**
72
72
  * Which image the card is showing. Card-local because it is a way of LOOKING
@@ -9,9 +9,10 @@ export const ProductCardSwatchSelector = ({ className, ...props }) => {
9
9
  return null;
10
10
  }
11
11
  const variants = product.variants ?? [product];
12
- const visibleVariants = variants.slice(0, maxSwatches);
13
- // Use variantCount (total in DB) for overflow, not just loaded variants
14
- const overflowCount = (product.variantCount ?? 1) - maxSwatches;
12
+ const visibleVariants = variants.slice(0, Math.max(0, maxSwatches));
13
+ // Count the overflow against the family's total (variantCount), not the
14
+ // loaded variants, and against the dots actually shown, not the cap.
15
+ const overflowCount = (product.variantCount ?? variants.length) - visibleVariants.length;
15
16
  return (_jsxs(SwatchSelector, { className: cn("px-6", className), onValueChange: onVariantChange, value: selectedVariantId, ...props, children: [visibleVariants.map((variant) => (_jsx(Swatch, { unavailable: variant.available === false, value: variant.id, children: renderImage({
16
17
  alt: `${variant.name} variant`,
17
18
  className: "object-cover",
@@ -72,5 +72,5 @@ export interface DocsManifest {
72
72
  storybook: DocsStorybookInventory;
73
73
  }
74
74
  /** The slug of every documented component and composition. */
75
- export type DocsUnitSlug = "accordion" | "alert" | "alert-dialog" | "anchor" | "animated-number" | "aspect-ratio" | "attribute-card" | "avatar" | "badge" | "border-beam" | "box" | "breadcrumb" | "button" | "button-group" | "calendar" | "card" | "card-grid" | "carousel" | "center" | "chart" | "checkbox" | "checkbox-card" | "checkbox-field" | "code" | "collapsible" | "color-palette" | "color-wheel" | "combobox" | "command" | "container" | "context-menu" | "cookie-banner" | "country-combobox" | "country-select" | "data-grid" | "data-grid-column-filter" | "data-grid-column-header" | "data-grid-column-visibility" | "data-grid-pagination" | "data-grid-table" | "delivery-banner" | "description-list" | "dialog" | "document-list" | "dot" | "dot-matrix-loader" | "drawer" | "dropdown-menu" | "easing-curve" | "editorial-card" | "empty" | "empty-state" | "error-boundary" | "error-state" | "expand-button" | "expandable-text" | "fade-overlay" | "field" | "file-upload" | "flex" | "folder-tile" | "gallery" | "grid" | "group" | "heading" | "hotspot-indicator" | "hover-card" | "icon" | "icon-box" | "icon-card" | "icon-morph" | "identified-surface" | "input" | "input-card" | "input-group" | "input-otp" | "item" | "kbd" | "label" | "loading-state" | "location-input" | "logo" | "marquee-text" | "media-frame" | "mega-menu" | "menu-item" | "menubar" | "mobile-navigation" | "month-year-picker" | "morph-dialog" | "motion-config" | "native-select" | "navigation-menu" | "page-header" | "page-toolbar" | "pagination" | "phone-input" | "pill" | "pixel-loader" | "popover" | "product-card" | "product-specifications-selector" | "progress" | "progress-dots" | "prompt-panel" | "quantity-control" | "quick-view" | "radio" | "radio-card" | "radio-group" | "requirement-item" | "resizable" | "scrim" | "scroll-area" | "scroll-fade" | "scroll-frame" | "search-input" | "section" | "section-header" | "segmented" | "segmented-control" | "select" | "separator" | "settings-card" | "sheet" | "shimmer" | "shine" | "sidebar" | "sidepanel" | "simple-grid" | "site-footer" | "skeleton" | "slider" | "social-icon" | "space" | "spinner" | "stack" | "stagger" | "stepper" | "streaming-text" | "sub-navigation" | "success-tick-ripple" | "swatch" | "swatch-fan" | "swatch-group" | "swatch-selector" | "switch" | "tab-navigation" | "table" | "tabs" | "tag" | "tag-selector" | "text" | "textarea" | "thinking-loader" | "thinking-orb" | "thinking-trace" | "time-slot-picker" | "toast" | "toggle" | "toggle-group" | "tooltip" | "variant-selector" | "verge" | "visually-hidden" | "voice";
75
+ export type DocsUnitSlug = "accordion" | "alert" | "alert-dialog" | "anchor" | "animated-number" | "aspect-ratio" | "attribute-card" | "avatar" | "badge" | "border-beam" | "box" | "breadcrumb" | "button" | "button-group" | "calendar" | "card" | "card-grid" | "carousel" | "center" | "chart" | "checkbox" | "checkbox-card" | "checkbox-field" | "code" | "collapsible" | "color-palette" | "color-wheel" | "combobox" | "command" | "container" | "context-menu" | "cookie-banner" | "country-combobox" | "country-select" | "data-grid" | "data-grid-column-filter" | "data-grid-column-header" | "data-grid-column-visibility" | "data-grid-pagination" | "data-grid-table" | "delivery-banner" | "description-list" | "dialog" | "document-list" | "dot" | "dot-matrix-loader" | "drawer" | "dropdown-menu" | "easing-curve" | "editorial-card" | "empty" | "empty-state" | "error-boundary" | "error-state" | "expand-button" | "expandable-text" | "fade-overlay" | "field" | "file-upload" | "flex" | "folder-tile" | "gallery" | "grid" | "group" | "heading" | "hotspot-indicator" | "hover-card" | "icon" | "icon-box" | "icon-card" | "icon-morph" | "identified-surface" | "input" | "input-card" | "input-group" | "input-otp" | "item" | "kbd" | "label" | "loading-state" | "location-input" | "logo" | "marquee-text" | "media-frame" | "mega-menu" | "menu-item" | "menubar" | "mobile-navigation" | "month-year-picker" | "morph-dialog" | "motion-config" | "native-select" | "navigation-menu" | "page-header" | "page-toolbar" | "pagination" | "phone-input" | "pill" | "pixel-loader" | "popover" | "product-card" | "product-specifications-selector" | "progress" | "progress-dots" | "prompt-panel" | "quantity-control" | "quick-view" | "radio" | "radio-card" | "radio-group" | "requirement-item" | "resizable" | "scrim" | "scroll-area" | "scroll-fade" | "scroll-frame" | "search-input" | "section" | "section-header" | "segmented-control" | "select" | "separator" | "settings-card" | "sheet" | "shimmer" | "shine" | "sidebar" | "sidepanel" | "simple-grid" | "site-footer" | "skeleton" | "slider" | "social-icon" | "space" | "spinner" | "stack" | "stagger" | "stepper" | "streaming-text" | "sub-navigation" | "success-tick-ripple" | "swatch" | "swatch-fan" | "swatch-group" | "swatch-selector" | "switch" | "tab-navigation" | "table" | "tabs" | "tag" | "tag-selector" | "text" | "textarea" | "thinking-loader" | "thinking-orb" | "thinking-trace" | "time-slot-picker" | "toast" | "toggle" | "toggle-group" | "tooltip" | "variant-selector" | "verge" | "visually-hidden" | "voice";
76
76
  export declare const docsManifest: DocsManifest;
@@ -5118,6 +5118,7 @@ export const docsManifest = {
5118
5118
  "pixel-loader",
5119
5119
  "prompt-panel",
5120
5120
  "quick-view",
5121
+ "radio-group",
5121
5122
  "search-input",
5122
5123
  "site-footer",
5123
5124
  "streaming-text",
@@ -6197,7 +6198,7 @@ export const docsManifest = {
6197
6198
  "Set `size`/`radius`/`disabled` once on `InputGroup`; children inherit them via context, so don't re-set them per child.",
6198
6199
  "Inside a `Field` the group inherits invalid state and its `InputGroupInput`/`InputGroupTextarea` becomes the field's control (labelled, described, `aria-invalid`) - pass `hasError` only to force error styling and `aria-invalid` independently.",
6199
6200
  ],
6200
- uses: ["button", "field", "flex", "icon"],
6201
+ uses: ["button", "field", "flex", "icon", "textarea"],
6201
6202
  usedBy: ["country-select", "search-input"],
6202
6203
  anatomy: {
6203
6204
  children: [
@@ -8361,7 +8362,7 @@ export const docsManifest = {
8361
8362
  module: "components/progress",
8362
8363
  title: "Progress",
8363
8364
  category: "feedback",
8364
- description: "Linear progress bar showing completion from 0 to 100; pass `value={null}`\nfor an indeterminate (empty) track. Built on Base UI Progress for accessible\nsemantics.",
8365
+ description: "Linear progress bar showing completion from 0 to 100. Pass `value={null}`\nwhen the amount of work is unknown: a short bar slides across the track on a\nloop, and under reduced motion it holds still in the middle. Built on Base\nUI Progress for accessible semantics.",
8365
8366
  exports: [
8366
8367
  {
8367
8368
  name: "Progress",
@@ -8437,8 +8438,9 @@ export const docsManifest = {
8437
8438
  },
8438
8439
  ],
8439
8440
  bestPractices: [
8440
- "Use `Progress` for determinate 0–100 completion (`value={null}` for an indeterminate track); position in a stepped flow is `ProgressDots`.",
8441
+ "Use `Progress` for determinate 0–100 completion and `value={null}` when the total is unknown; position in a stepped flow is `ProgressDots`.",
8441
8442
  "For a ring instead of a bar use `ProgressCircle` - it needs an explicit `label` for its native `<progress>` accessible name.",
8443
+ "Mount `ReducedMotionProvider` at the app root - the indeterminate bar slides via `m.*` (LazyMotion).",
8442
8444
  ],
8443
8445
  uses: ["center"],
8444
8446
  usedBy: ["file-upload"],
@@ -8787,6 +8789,12 @@ export const docsManifest = {
8787
8789
  defaultValue: '"sm"',
8788
8790
  description: 'Radio size - a single token or a per-breakpoint object like\n`{ base: "sm", md: "lg" }`.',
8789
8791
  },
8792
+ {
8793
+ name: "description",
8794
+ type: "React.ReactNode",
8795
+ required: false,
8796
+ description: "Optional helper text below the label; wired to the radio via `aria-describedby`.",
8797
+ },
8790
8798
  ],
8791
8799
  },
8792
8800
  {
@@ -8800,7 +8808,7 @@ export const docsManifest = {
8800
8808
  "Drive selection with `value` + `onValueChange` (controlled) or `defaultValue` (uncontrolled) - don't set both on the same group.",
8801
8809
  "Prefer `RadioGroupItem` (radio + wired `Label`) over hand-pairing `Radio` and `Label`; it auto-generates the `id`/`htmlFor` link.",
8802
8810
  ],
8803
- uses: ["flex", "label", "radio"],
8811
+ uses: ["flex", "grid", "label", "radio", "text"],
8804
8812
  usedBy: [],
8805
8813
  },
8806
8814
  {
@@ -9385,51 +9393,6 @@ export const docsManifest = {
9385
9393
  uses: [],
9386
9394
  usedBy: [],
9387
9395
  },
9388
- {
9389
- slug: "segmented",
9390
- module: "components/segmented",
9391
- title: "Segmented",
9392
- category: "forms",
9393
- description: "Shared class-name token primitives for the segmented-control visual language.\n\nConsumed by SegmentedControl, Tabs, and TabNavigation so they render an\nidentical shell, padding, gap, and active indicator at any size.\nNot components - these are Tailwind class strings and size→class maps.",
9394
- exports: [
9395
- {
9396
- name: "SEGMENTED_GAP",
9397
- kind: "const",
9398
- props: [],
9399
- },
9400
- {
9401
- name: "SEGMENTED_INDICATOR_ACTIVE_CLASS",
9402
- kind: "const",
9403
- props: [],
9404
- },
9405
- {
9406
- name: "SEGMENTED_INDICATOR_CLASS",
9407
- kind: "const",
9408
- props: [],
9409
- },
9410
- {
9411
- name: "SEGMENTED_SHELL_BASE",
9412
- kind: "const",
9413
- props: [],
9414
- },
9415
- {
9416
- name: "SEGMENTED_SHELL_PADDING",
9417
- kind: "const",
9418
- props: [],
9419
- },
9420
- {
9421
- name: "SEGMENTED_TRIGGER_PADDING",
9422
- kind: "const",
9423
- props: [],
9424
- },
9425
- ],
9426
- bestPractices: [
9427
- "These are class-name strings and size→class maps, not components - apply them via `className`, never render them as JSX.",
9428
- 'Reuse these tokens to give a custom control the segmented "pill" shell; for actual segmented UIs prefer `SegmentedControl`; `Tabs` and `TabNavigation` consume the same tokens for panel and navigation switching.',
9429
- ],
9430
- uses: [],
9431
- usedBy: ["segmented-control", "tab-navigation", "tabs"],
9432
- },
9433
9396
  {
9434
9397
  slug: "segmented-control",
9435
9398
  module: "components/segmented-control",
@@ -9534,7 +9497,7 @@ export const docsManifest = {
9534
9497
  "Switching visible content panels is Tabs; navigating URLs is TabNavigation - SegmentedControl is an input that selects a value or mode.",
9535
9498
  "Icon-only items must carry an `aria-label`.",
9536
9499
  ],
9537
- uses: ["dot", "flex", "icon", "scroll-area", "scroll-fade", "segmented"],
9500
+ uses: ["dot", "flex", "icon", "scroll-area", "scroll-fade"],
9538
9501
  usedBy: ["color-palette"],
9539
9502
  anatomy: {
9540
9503
  children: [
@@ -11933,7 +11896,7 @@ export const docsManifest = {
11933
11896
  "Use `TabNavigation` when each tab is a route/URL (it renders a `<nav>` of links); for in-page panels that swap without navigating, use `Tabs`.",
11934
11897
  'It shares the Tabs look via context but defaults to `variant="line"`; mark the active link yourself (e.g. router `aria-current`) - there\'s no Base UI value tracking here.',
11935
11898
  ],
11936
- uses: ["flex", "icon", "segmented", "tabs"],
11899
+ uses: ["flex", "icon", "tabs"],
11937
11900
  usedBy: [],
11938
11901
  anatomy: {
11939
11902
  children: [
@@ -12175,7 +12138,7 @@ export const docsManifest = {
12175
12138
  "Reach for `Tabs` to swap in-page panels via `TabsContent`; when each tab is a route, use `TabNavigation` instead (renders links, no panels).",
12176
12139
  "Compose `TabsList`/`TabsTrigger`/`TabsContent` - shared `variant`/`size`/`fullWidth` flow through context, so set them on `Tabs`, not each trigger.",
12177
12140
  ],
12178
- uses: ["dot", "flex", "icon", "scroll-area", "segmented"],
12141
+ uses: ["dot", "flex", "icon", "scroll-area"],
12179
12142
  usedBy: ["tab-navigation"],
12180
12143
  anatomy: {
12181
12144
  children: [
@@ -12413,6 +12376,7 @@ export const docsManifest = {
12413
12376
  "page-header",
12414
12377
  "pixel-loader",
12415
12378
  "product-card",
12379
+ "radio-group",
12416
12380
  "section-header",
12417
12381
  "site-footer",
12418
12382
  "slider",
@@ -12447,11 +12411,11 @@ export const docsManifest = {
12447
12411
  ],
12448
12412
  bestPractices: [
12449
12413
  "Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.",
12450
- "It auto-grows with content (`field-sizing-content`); set `rows` for an initial height rather than pinning height via CSS.",
12414
+ "It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.",
12451
12415
  "Give it a label - inside a `Field` a `FieldLabel` targets it with no ids to wire; standalone, point a `Label`'s `htmlFor` at its `id`. A placeholder is not an accessible name.",
12452
12416
  ],
12453
12417
  uses: ["field"],
12454
- usedBy: [],
12418
+ usedBy: ["input-group"],
12455
12419
  },
12456
12420
  {
12457
12421
  slug: "thinking-loader",
@@ -14506,13 +14470,15 @@ export const docsManifest = {
14506
14470
  name: "more",
14507
14471
  type: "boolean",
14508
14472
  required: false,
14473
+ defaultValue: "true",
14474
+ description: "Collapse the page buttons into groups of `moreLimit`, with ellipsis\nbuttons that jump to the next or previous group. Off shows a button for\nevery page.",
14509
14475
  },
14510
14476
  {
14511
14477
  name: "moreLimit",
14512
14478
  type: "number",
14513
14479
  required: false,
14514
14480
  defaultValue: "5",
14515
- description: "Number of page buttons shown before collapsing to an ellipsis.",
14481
+ description: "Number of page buttons shown before collapsing to an ellipsis. Needs\n`more`.",
14516
14482
  },
14517
14483
  {
14518
14484
  name: "nextPageLabel",
@@ -14542,23 +14508,6 @@ export const docsManifest = {
14542
14508
  defaultValue: "[5, 10, 25, 50, 100]",
14543
14509
  description: "Page-size options offered by the rows-per-page select.",
14544
14510
  },
14545
- {
14546
- name: "sizesDescription",
14547
- type: "string",
14548
- required: false,
14549
- defaultValue: '"per page"',
14550
- },
14551
- {
14552
- name: "sizesInfo",
14553
- type: "string",
14554
- required: false,
14555
- },
14556
- {
14557
- name: "sizesLabel",
14558
- type: "string",
14559
- required: false,
14560
- defaultValue: '"Show"',
14561
- },
14562
14511
  {
14563
14512
  name: "sizesSkeleton",
14564
14513
  type: "ReactNode",
@@ -18517,6 +18466,8 @@ export const docsManifest = {
18517
18466
  name: "maxSwatches",
18518
18467
  type: "number | undefined",
18519
18468
  required: false,
18469
+ defaultValue: "5",
18470
+ description: "How many colourway swatches the row shows before the rest go behind `+N`.\nCards under 240px wide show at most four.",
18520
18471
  },
18521
18472
  {
18522
18473
  name: "menuOpen",
@@ -18643,6 +18594,8 @@ export const docsManifest = {
18643
18594
  name: "showSwatches",
18644
18595
  type: "boolean | undefined",
18645
18596
  required: false,
18597
+ defaultValue: "true",
18598
+ description: "Show the row of colourway swatches. Single-colour products and `single`\nmode never show it.",
18646
18599
  },
18647
18600
  {
18648
18601
  name: "similarityScore",
@@ -19119,6 +19072,8 @@ export const docsManifest = {
19119
19072
  name: "maxSwatches",
19120
19073
  type: "number | undefined",
19121
19074
  required: false,
19075
+ defaultValue: "5",
19076
+ description: "How many colourway swatches the row shows before the rest go behind `+N`.\nCards under 240px wide show at most four.",
19122
19077
  },
19123
19078
  {
19124
19079
  name: "mode",
@@ -23055,7 +23010,7 @@ export const docsManifest = {
23055
23010
  storybook: {
23056
23011
  addons: ["@storybook/addon-docs", "@storybook/react-vite"],
23057
23012
  anatomyFiles: 31,
23058
- storyFiles: 172,
23059
- storiesWithPlay: 64,
23013
+ storyFiles: 171,
23014
+ storiesWithPlay: 63,
23060
23015
  },
23061
23016
  };
@@ -164,7 +164,9 @@
164
164
  --color-muted-foreground: var(--color-gray-600);
165
165
  --color-accent: var(--color-gray-200);
166
166
  --color-accent-foreground: var(--color-gray-900);
167
- --color-destructive: var(--color-red-600);
167
+ /* Red-700, not 600: red-50 on red-600 is 4.4:1 and drops under the button
168
+ sheen and hover; red-700 holds 4.5:1 as a fill and as text on the page. */
169
+ --color-destructive: var(--color-red-700);
168
170
  --color-destructive-foreground: var(--color-red-50);
169
171
  --color-favorite: var(--color-red-500);
170
172
  --color-border: var(--color-gray-200);
@@ -213,8 +215,9 @@
213
215
  --color-text-muted: var(--color-gray-600);
214
216
 
215
217
  /* Calm semantic colours - Next.js inspired */
218
+ /* Accents double as text on the page, so they hold 4.5:1 on white. */
216
219
  --color-affirmative-soft: #f0fdf4; /* green-50 */
217
- --color-affirmative-accent: #16a34a; /* green-600 */
220
+ --color-affirmative-accent: #15803d; /* green-700 */
218
221
  --color-affirmative-foreground: #14532d; /* green-900 */
219
222
 
220
223
  --color-warning-soft: #fffbeb; /* very soft amber */
@@ -226,13 +229,17 @@
226
229
  --color-error-foreground: #991b1b; /* red-800 */
227
230
 
228
231
  --color-info-soft: #eff6ff; /* blue-50 */
229
- --color-info-accent: #3b82f6; /* blue-500 */
232
+ --color-info-accent: #2563eb; /* blue-600 */
230
233
  --color-info-foreground: #1e3a5f; /* blue-900 */
231
234
 
232
235
  --color-brand: #ffe600; /* Brand yellow, per the Figma base/brand token */
233
236
  --color-brand-soft: #fffef5; /* very soft brand yellow (code-only) */
234
237
  --color-brand-accent: #dda600; /* per Figma base/brand accent - consumed at /40 for borders/accents */
235
238
  --color-brand-foreground: var(--color-gray-800); /* #1F2937 - per Figma */
239
+ /* Brand-hued text on the page background, for brand buttons without a fill.
240
+ 6.1:1 on white, so it still passes under a faded hover; override it
241
+ alongside --color-brand. */
242
+ --color-brand-strong: #776000;
236
243
 
237
244
  /* Status border tokens (light mode base values - desaturated blends) */
238
245
  --color-affirmative-border: #dcfce7; /* green-100 - muted */
@@ -547,7 +554,7 @@
547
554
  --color-muted-foreground: var(--color-gray-600);
548
555
  --color-accent: var(--color-gray-200);
549
556
  --color-accent-foreground: var(--color-gray-900);
550
- --color-destructive: var(--color-red-600);
557
+ --color-destructive: var(--color-red-700);
551
558
  --color-destructive-foreground: var(--color-red-50);
552
559
  --color-favorite: var(--color-red-500);
553
560
  --color-border: var(--color-gray-200);
@@ -602,8 +609,10 @@
602
609
  --color-muted-foreground: var(--color-gray-400);
603
610
  --color-accent: var(--color-surface-2);
604
611
  --color-accent-foreground: var(--color-foreground);
605
- --color-destructive: var(--color-red-500);
606
- --color-destructive-foreground: var(--color-red-50);
612
+ /* Like the other dark solids, danger is a light fill with dark text: no
613
+ red reads both as text on graphite and under light text. */
614
+ --color-destructive: var(--color-red-400);
615
+ --color-destructive-foreground: var(--color-surface-sunk);
607
616
  --color-favorite: var(--color-red-400);
608
617
  --color-border: oklch(0.82 0.007 250 / 0.1);
609
618
  --color-input: var(--color-surface-1);
@@ -629,6 +638,8 @@
629
638
  --color-info-accent: oklch(0.72 0.1 245);
630
639
  --color-info-foreground: oklch(0.74 0.08 245);
631
640
  --color-brand-soft: var(--color-surface-1);
641
+ /* The brand itself reads on graphite (14.6:1 on the background). */
642
+ --color-brand-strong: var(--color-brand);
632
643
 
633
644
  /* Dark surfaces are separated by the role ladder and hairline, never
634
645
  elevation. This also neutralises custom shadow utilities package-wide. */