@instruments/materia-ui 0.1.0 → 0.2.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/agent-docs.md +4 -3
- package/dist/components/button/button-variants.js +53 -8
- package/dist/components/button-group/button-group-root.js +8 -6
- package/dist/components/calendar/calendar-root.js +39 -13
- package/dist/components/chart/chart-tooltip.js +7 -5
- package/dist/components/empty/empty-media.js +5 -5
- package/dist/components/input-group/input-group-textarea.d.ts +3 -2
- package/dist/components/input-group/input-group-textarea.js +8 -4
- package/dist/components/pill/pill-variants.js +21 -10
- package/dist/components/progress/progress-root.d.ts +7 -5
- package/dist/components/progress/progress-root.js +26 -5
- package/dist/components/radio-group/radio-group-root.d.ts +7 -3
- package/dist/components/radio-group/radio-group-root.js +14 -4
- package/dist/components/textarea/textarea-root.d.ts +1 -1
- package/dist/components/textarea/textarea-root.js +6 -9
- package/dist/components/textarea/use-textarea-rows.d.ts +22 -0
- package/dist/components/textarea/use-textarea-rows.js +43 -0
- package/dist/compositions/data-grid-pagination/data-grid-pagination-root.d.ts +8 -6
- package/dist/compositions/data-grid-pagination/data-grid-pagination-root.js +11 -20
- package/dist/compositions/product-card/product-card-color-count.js +26 -26
- package/dist/compositions/product-card/product-card-compact-arm.js +2 -2
- package/dist/compositions/product-card/product-card-content-types.d.ts +5 -0
- package/dist/compositions/product-card/product-card-full-arm.js +2 -2
- package/dist/compositions/product-card/product-card-provider-types.d.ts +10 -0
- package/dist/compositions/product-card/product-card-provider.js +1 -1
- package/dist/compositions/product-card/product-card-swatch-selector.js +4 -3
- package/dist/data/docs-manifest.js +24 -24
- package/dist/styles/index.css +17 -6
- package/lint/tokens.mjs +1 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { cn } from "@instruments/materia-ui/utils/cn";
|
|
|
6
6
|
import { hasErrorInput } from "../../tokens/has-error-input.js";
|
|
7
7
|
import { INPUT_SHELL } from "../../tokens/input-shell.js";
|
|
8
8
|
import { useFieldContext } from "../field/field-context.js";
|
|
9
|
+
import { useTextareaRows } from "./use-textarea-rows.js";
|
|
9
10
|
/**
|
|
10
11
|
* Renders a textarea element for multi-line text input (Base UI
|
|
11
12
|
* `Field.Control` rendering a `<textarea>`). Inside a Field it is the field's
|
|
@@ -26,21 +27,17 @@ import { useFieldContext } from "../field/field-context.js";
|
|
|
26
27
|
* ```
|
|
27
28
|
*
|
|
28
29
|
* @bestPractice Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.
|
|
29
|
-
* @bestPractice It auto-grows with content (`field-sizing-content`); set `rows` for
|
|
30
|
+
* @bestPractice It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.
|
|
30
31
|
* @bestPractice 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.
|
|
31
32
|
* @category forms
|
|
32
33
|
*/
|
|
33
34
|
const Textarea = ({ className, testId, hasError, id, name, disabled, rows, style, ...props }) => {
|
|
34
35
|
const field = useFieldContext();
|
|
35
36
|
const resolvedError = hasError ?? field?.invalid ?? false;
|
|
36
|
-
|
|
37
|
-
// `rows` sets the starting height instead: that many lines plus the py-2
|
|
38
|
-
// padding and the 1px border, before the box grows with its content.
|
|
39
|
-
const sizedStyle = rows === undefined
|
|
40
|
-
? style
|
|
41
|
-
: { minHeight: `calc(${rows}lh + 1rem + 2px)`, ...style };
|
|
37
|
+
const { className: sizingClassName, ref: sizingRef, style: sizingStyle, } = useTextareaRows(rows);
|
|
42
38
|
// The control owns id/name/disabled so Field can label and validate it;
|
|
43
|
-
// textarea-only props ride on the rendered element.
|
|
44
|
-
|
|
39
|
+
// textarea-only props ride on the rendered element. The sizing ref sits on
|
|
40
|
+
// the control so a caller's own `ref` on the textarea still lands.
|
|
41
|
+
return (_jsx(FieldPrimitive.Control, { ref: sizingRef, render: _jsx("textarea", { className: cn(focusInput(), `flex w-full ${INPUT_SHELL} placeholder:text-muted-foreground transition-color-shadow text-paragraph md:text-paragraph-sm px-3 py-2 shadow-xs disabled:cursor-not-allowed disabled:opacity-50`, sizingClassName, hasErrorInput, className), "data-component": "textarea", "data-slot": "textarea", rows: rows, style: { ...sizingStyle, ...style }, ...(hasError === true ? { "aria-invalid": true } : {}), ...(resolvedError ? { "data-error": true } : {}), ...(testId === undefined ? {} : { "data-testid": testId }), ...props }), ...(disabled === undefined ? {} : { disabled }), ...(id === undefined ? {} : { id }), ...(name === undefined ? {} : { name }) }));
|
|
45
42
|
};
|
|
46
43
|
export { Textarea };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CSSProperties, RefCallback } from "react";
|
|
2
|
+
interface TextareaRowsSizing {
|
|
3
|
+
/** Sizing classes: `field-sizing-content`, plus `min-h-16` when there are no `rows`. */
|
|
4
|
+
className: string;
|
|
5
|
+
/** Goes on the textarea, to read back its padding and border once mounted. */
|
|
6
|
+
ref: RefCallback<HTMLTextAreaElement>;
|
|
7
|
+
/** The `rows`-high minimum, once the edges are known. */
|
|
8
|
+
style: CSSProperties | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Lets a textarea grow with its text (`field-sizing: content`) without
|
|
12
|
+
* losing `rows`, which content sizing ignores in every browser.
|
|
13
|
+
*
|
|
14
|
+
* Until it mounts, a textarea with `rows` is left to the browser, which
|
|
15
|
+
* sizes it from `rows` with whatever padding and border it has. Once
|
|
16
|
+
* mounted, those edges are read back from its computed style, the minimum
|
|
17
|
+
* height becomes `rows` lines plus them, and content sizing takes over to
|
|
18
|
+
* grow it beyond that. The `lh` unit keeps the minimum in step when the line
|
|
19
|
+
* height changes at a breakpoint.
|
|
20
|
+
*/
|
|
21
|
+
export declare const useTextareaRows: (rows: number | undefined) => TextareaRowsSizing;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Lets a textarea grow with its text (`field-sizing: content`) without
|
|
5
|
+
* losing `rows`, which content sizing ignores in every browser.
|
|
6
|
+
*
|
|
7
|
+
* Until it mounts, a textarea with `rows` is left to the browser, which
|
|
8
|
+
* sizes it from `rows` with whatever padding and border it has. Once
|
|
9
|
+
* mounted, those edges are read back from its computed style, the minimum
|
|
10
|
+
* height becomes `rows` lines plus them, and content sizing takes over to
|
|
11
|
+
* grow it beyond that. The `lh` unit keeps the minimum in step when the line
|
|
12
|
+
* height changes at a breakpoint.
|
|
13
|
+
*/
|
|
14
|
+
export const useTextareaRows = (rows) => {
|
|
15
|
+
const [edges, setEdges] = useState();
|
|
16
|
+
const ref = useCallback((element) => {
|
|
17
|
+
if (element === null || rows === undefined) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const style = getComputedStyle(element);
|
|
21
|
+
setEdges([
|
|
22
|
+
style.paddingTop,
|
|
23
|
+
style.paddingBottom,
|
|
24
|
+
style.borderTopWidth,
|
|
25
|
+
style.borderBottomWidth,
|
|
26
|
+
].join(" + "));
|
|
27
|
+
}, [rows]);
|
|
28
|
+
if (rows === undefined) {
|
|
29
|
+
return {
|
|
30
|
+
className: "field-sizing-content min-h-16",
|
|
31
|
+
ref,
|
|
32
|
+
style: undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (edges === undefined) {
|
|
36
|
+
return { className: "", ref, style: undefined };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
className: "field-sizing-content",
|
|
40
|
+
ref,
|
|
41
|
+
style: { minHeight: `calc(${rows}lh + ${edges})` },
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
//
|
|
56
|
-
const
|
|
57
|
-
const
|
|
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
|
|
19
|
-
*
|
|
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
|
|
23
|
+
* Circles a card under 240px wide can show beside the overflow label.
|
|
23
24
|
*
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
const mediumOverflow = inlineOverflow + (
|
|
52
|
-
// WHICH
|
|
53
|
-
// SELECTED colourway
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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 ??
|
|
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
|
-
//
|
|
14
|
-
|
|
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",
|
|
@@ -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
|
|
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
|
|
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
|
{
|
|
@@ -12413,6 +12421,7 @@ export const docsManifest = {
|
|
|
12413
12421
|
"page-header",
|
|
12414
12422
|
"pixel-loader",
|
|
12415
12423
|
"product-card",
|
|
12424
|
+
"radio-group",
|
|
12416
12425
|
"section-header",
|
|
12417
12426
|
"site-footer",
|
|
12418
12427
|
"slider",
|
|
@@ -12447,11 +12456,11 @@ export const docsManifest = {
|
|
|
12447
12456
|
],
|
|
12448
12457
|
bestPractices: [
|
|
12449
12458
|
"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
|
|
12459
|
+
"It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.",
|
|
12451
12460
|
"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
12461
|
],
|
|
12453
12462
|
uses: ["field"],
|
|
12454
|
-
usedBy: [],
|
|
12463
|
+
usedBy: ["input-group"],
|
|
12455
12464
|
},
|
|
12456
12465
|
{
|
|
12457
12466
|
slug: "thinking-loader",
|
|
@@ -14506,13 +14515,15 @@ export const docsManifest = {
|
|
|
14506
14515
|
name: "more",
|
|
14507
14516
|
type: "boolean",
|
|
14508
14517
|
required: false,
|
|
14518
|
+
defaultValue: "true",
|
|
14519
|
+
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
14520
|
},
|
|
14510
14521
|
{
|
|
14511
14522
|
name: "moreLimit",
|
|
14512
14523
|
type: "number",
|
|
14513
14524
|
required: false,
|
|
14514
14525
|
defaultValue: "5",
|
|
14515
|
-
description: "Number of page buttons shown before collapsing to an ellipsis.",
|
|
14526
|
+
description: "Number of page buttons shown before collapsing to an ellipsis. Needs\n`more`.",
|
|
14516
14527
|
},
|
|
14517
14528
|
{
|
|
14518
14529
|
name: "nextPageLabel",
|
|
@@ -14542,23 +14553,6 @@ export const docsManifest = {
|
|
|
14542
14553
|
defaultValue: "[5, 10, 25, 50, 100]",
|
|
14543
14554
|
description: "Page-size options offered by the rows-per-page select.",
|
|
14544
14555
|
},
|
|
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
14556
|
{
|
|
14563
14557
|
name: "sizesSkeleton",
|
|
14564
14558
|
type: "ReactNode",
|
|
@@ -18517,6 +18511,8 @@ export const docsManifest = {
|
|
|
18517
18511
|
name: "maxSwatches",
|
|
18518
18512
|
type: "number | undefined",
|
|
18519
18513
|
required: false,
|
|
18514
|
+
defaultValue: "5",
|
|
18515
|
+
description: "How many colourway swatches the row shows before the rest go behind `+N`.\nCards under 240px wide show at most four.",
|
|
18520
18516
|
},
|
|
18521
18517
|
{
|
|
18522
18518
|
name: "menuOpen",
|
|
@@ -18643,6 +18639,8 @@ export const docsManifest = {
|
|
|
18643
18639
|
name: "showSwatches",
|
|
18644
18640
|
type: "boolean | undefined",
|
|
18645
18641
|
required: false,
|
|
18642
|
+
defaultValue: "true",
|
|
18643
|
+
description: "Show the row of colourway swatches. Single-colour products and `single`\nmode never show it.",
|
|
18646
18644
|
},
|
|
18647
18645
|
{
|
|
18648
18646
|
name: "similarityScore",
|
|
@@ -19119,6 +19117,8 @@ export const docsManifest = {
|
|
|
19119
19117
|
name: "maxSwatches",
|
|
19120
19118
|
type: "number | undefined",
|
|
19121
19119
|
required: false,
|
|
19120
|
+
defaultValue: "5",
|
|
19121
|
+
description: "How many colourway swatches the row shows before the rest go behind `+N`.\nCards under 240px wide show at most four.",
|
|
19122
19122
|
},
|
|
19123
19123
|
{
|
|
19124
19124
|
name: "mode",
|
package/dist/styles/index.css
CHANGED
|
@@ -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
|
-
|
|
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: #
|
|
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: #
|
|
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-
|
|
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
|
-
|
|
606
|
-
|
|
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. */
|
package/lint/tokens.mjs
CHANGED