@instruments/materia-ui 0.4.1 → 0.4.3

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 (118) hide show
  1. package/README.md +2 -4
  2. package/agent-docs.md +9 -9
  3. package/dist/components/accordion/accordion-root.d.ts +1 -0
  4. package/dist/components/accordion/accordion-root.js +1 -0
  5. package/dist/components/alert/alert-root.d.ts +1 -0
  6. package/dist/components/alert/alert-root.js +1 -0
  7. package/dist/components/alert-dialog/alert-dialog-root.d.ts +1 -0
  8. package/dist/components/alert-dialog/alert-dialog-root.js +1 -0
  9. package/dist/components/avatar/avatar-root.d.ts +1 -0
  10. package/dist/components/avatar/avatar-root.js +1 -0
  11. package/dist/components/badge/badge-root.d.ts +1 -0
  12. package/dist/components/badge/badge-root.js +1 -0
  13. package/dist/components/calendar/calendar-root.js +1 -0
  14. package/dist/components/carousel/carousel-root.d.ts +1 -0
  15. package/dist/components/carousel/carousel-root.js +1 -0
  16. package/dist/components/chart/chart-root.d.ts +1 -0
  17. package/dist/components/chart/chart-root.js +1 -0
  18. package/dist/components/collapsible/collapsible-root.d.ts +1 -0
  19. package/dist/components/collapsible/collapsible-root.js +1 -0
  20. package/dist/components/combobox/combobox-root.d.ts +1 -0
  21. package/dist/components/combobox/combobox-root.js +1 -0
  22. package/dist/components/command/command-root.d.ts +1 -0
  23. package/dist/components/command/command-root.js +1 -0
  24. package/dist/components/dialog/dialog-content.d.ts +2 -0
  25. package/dist/components/dialog/dialog-content.js +3 -2
  26. package/dist/components/dialog/dialog-footer.d.ts +2 -0
  27. package/dist/components/dialog/dialog-footer.js +8 -8
  28. package/dist/components/dialog/dialog-root.d.ts +1 -0
  29. package/dist/components/dialog/dialog-root.js +1 -0
  30. package/dist/components/dialog/dialog-title.d.ts +2 -0
  31. package/dist/components/dialog/dialog-title.js +3 -1
  32. package/dist/components/drawer/drawer-root.d.ts +1 -0
  33. package/dist/components/drawer/drawer-root.js +1 -0
  34. package/dist/components/heading/heading-root.d.ts +1 -0
  35. package/dist/components/heading/heading-root.js +1 -0
  36. package/dist/components/input/input-root.d.ts +1 -0
  37. package/dist/components/input/input-root.js +1 -0
  38. package/dist/components/kbd/kbd-root.d.ts +1 -0
  39. package/dist/components/kbd/kbd-root.js +1 -0
  40. package/dist/components/logo/logo-mark.d.ts +1 -1
  41. package/dist/components/logo/logo-mark.js +1 -1
  42. package/dist/components/logotype/logotype-paths.d.ts +1 -1
  43. package/dist/components/logotype/logotype-paths.js +1 -1
  44. package/dist/components/month-year-picker/month-year-picker-root.d.ts +1 -0
  45. package/dist/components/month-year-picker/month-year-picker-root.js +1 -0
  46. package/dist/components/native-select/native-select-root.d.ts +1 -0
  47. package/dist/components/native-select/native-select-root.js +1 -0
  48. package/dist/components/pill/pill-root.d.ts +1 -0
  49. package/dist/components/pill/pill-root.js +1 -0
  50. package/dist/components/popover/popover-root.d.ts +1 -0
  51. package/dist/components/popover/popover-root.js +1 -0
  52. package/dist/components/quantity-control/quantity-control-root.d.ts +1 -0
  53. package/dist/components/quantity-control/quantity-control-root.js +1 -0
  54. package/dist/components/select/select-root.d.ts +1 -0
  55. package/dist/components/select/select-root.js +1 -0
  56. package/dist/components/separator/separator-root.d.ts +1 -0
  57. package/dist/components/separator/separator-root.js +1 -0
  58. package/dist/components/sheet/sheet-root.d.ts +1 -0
  59. package/dist/components/sheet/sheet-root.js +1 -0
  60. package/dist/components/spinner/spinner-root.d.ts +1 -0
  61. package/dist/components/spinner/spinner-root.js +1 -0
  62. package/dist/components/stepper/stepper-root.d.ts +1 -0
  63. package/dist/components/stepper/stepper-root.js +1 -0
  64. package/dist/components/success-tick-ripple/success-tick-ripple-root.d.ts +1 -0
  65. package/dist/components/success-tick-ripple/success-tick-ripple-root.js +1 -0
  66. package/dist/components/switch/switch-root.d.ts +1 -0
  67. package/dist/components/switch/switch-root.js +1 -0
  68. package/dist/components/tag/tag-root.d.ts +1 -0
  69. package/dist/components/tag/tag-root.js +1 -0
  70. package/dist/components/textarea/textarea-root.d.ts +1 -0
  71. package/dist/components/textarea/textarea-root.js +1 -0
  72. package/dist/components/time-slot-picker/time-slot-picker-root.d.ts +1 -0
  73. package/dist/components/time-slot-picker/time-slot-picker-root.js +1 -0
  74. package/dist/components/toast/toast-root.d.ts +1 -0
  75. package/dist/components/toast/toast-root.js +1 -0
  76. package/dist/components/tooltip/tooltip-root.d.ts +1 -0
  77. package/dist/components/tooltip/tooltip-root.js +1 -0
  78. package/dist/compositions/empty-state/empty-state-root.d.ts +1 -0
  79. package/dist/compositions/empty-state/empty-state-root.js +1 -0
  80. package/dist/compositions/file-upload/file-upload-dropzone.d.ts +1 -0
  81. package/dist/compositions/file-upload/file-upload-dropzone.js +1 -0
  82. package/dist/compositions/product-card/product-card-caption.js +4 -2
  83. package/dist/compositions/product-card/product-card-content-types.d.ts +8 -0
  84. package/dist/compositions/product-card/product-card-meta.js +2 -2
  85. package/dist/compositions/product-card/product-card-provider-types.d.ts +3 -0
  86. package/dist/compositions/product-card/product-card-provider.js +4 -1
  87. package/dist/compositions/product-card/product-card-root.d.ts +1 -1
  88. package/dist/compositions/product-card/product-card-root.js +2 -1
  89. package/dist/compositions/product-card/product-card-skeleton.d.ts +14 -5
  90. package/dist/compositions/product-card/product-card-skeleton.js +6 -5
  91. package/dist/data/decision-trees.d.ts +1 -1
  92. package/dist/data/decision-trees.js +1 -1
  93. package/dist/data/docs-manifest.d.ts +2 -0
  94. package/dist/data/docs-manifest.js +257 -67
  95. package/dist/styles/index.css +0 -1
  96. package/dist/utils/cn-theme.js +1 -1
  97. package/lint/README.md +6 -2
  98. package/lint/doctrine-rules.mjs +4 -4
  99. package/lint/oxlint/index.d.mts +13 -5
  100. package/lint/oxlint/index.mjs +26 -0
  101. package/lint/oxlint/no-naked-element.mjs +123 -65
  102. package/lint/oxlint/no-render-layout-as.mjs +61 -56
  103. package/lint/scan-classes.d.mts +58 -5
  104. package/lint/scan-classes.mjs +274 -38
  105. package/lint/tokens.d.mts +16 -8
  106. package/lint/tokens.mjs +13 -3
  107. package/lint/type-case.d.mts +16 -8
  108. package/lint/type-case.mjs +17 -0
  109. package/package.json +27 -14
  110. package/lint/oxlint/fixtures/allowed.tsx +0 -12
  111. package/lint/oxlint/fixtures/fixture.config.mjs +0 -29
  112. package/lint/oxlint/fixtures/invalid.tsx +0 -24
  113. package/lint/oxlint/fixtures/layout-only.tsx +0 -26
  114. package/lint/oxlint/fixtures/render-as-invalid.tsx +0 -11
  115. package/lint/oxlint/fixtures/render-as-valid.tsx +0 -14
  116. package/lint/oxlint/fixtures/valid.tsx +0 -18
  117. package/lint/oxlint/no-naked-element.test.ts +0 -155
  118. package/lint/oxlint/no-render-layout-as.test.ts +0 -81
@@ -24,6 +24,7 @@ import { OVERLAY_SCRIM_DARK, POPOVER_CONTENT_ANIMATION, POPOVER_SURFACE, } from
24
24
  * @bestPractice Use `Popover` for an anchored non-blocking panel dismissed on outside-click / Esc; a page-blocking modal is a `Dialog`, a menu of actions is a `DropdownMenu`.
25
25
  * @bestPractice It's non-modal by default (`modal={false}`) so the page stays interactive - pass `modal` only when you must trap focus and block the background.
26
26
  * @bestPractice `PopoverContent` portals to the body; anchor to a non-trigger element with its `anchor` prop.
27
+ * @keywords popup, flyout, popout
27
28
  * @category overlays
28
29
  */
29
30
  const Popover = ({ ...props }) => (_jsx(PopoverPrimitive.Root, { "data-component": "popover", "data-slot": "popover", ...props }));
@@ -44,6 +44,7 @@ interface QuantityControlProps {
44
44
  * @bestPractice Pass `onRemove` when hitting the minimum should delete the item - the decrement button then swaps to a trash icon; without it, decrement just stops at `min`.
45
45
  * @bestPractice It's controlled - pass the current `value` and update it in `onIncrement`/`onDecrement`, which fire once per step for clicks, arrow keys and each repeat of a held button. Typing a number is not supported.
46
46
  * @bestPractice Tab focuses the quantity itself (the stepper buttons are skipped, as in any number field), announced by its `label`. In a list of lines, pass a `label` naming the item so each field is told apart.
47
+ * @keywords counter, number input, increment, spinbutton
47
48
  * @category actions
48
49
  */
49
50
  export declare const QuantityControl: ({ value, label, onIncrement, onDecrement, onRemove, min, max, disabled, radius, testId, }: QuantityControlProps) => React.JSX.Element;
@@ -47,6 +47,7 @@ const stepperProps = {
47
47
  * @bestPractice Pass `onRemove` when hitting the minimum should delete the item - the decrement button then swaps to a trash icon; without it, decrement just stops at `min`.
48
48
  * @bestPractice It's controlled - pass the current `value` and update it in `onIncrement`/`onDecrement`, which fire once per step for clicks, arrow keys and each repeat of a held button. Typing a number is not supported.
49
49
  * @bestPractice Tab focuses the quantity itself (the stepper buttons are skipped, as in any number field), announced by its `label`. In a list of lines, pass a `label` naming the item so each field is told apart.
50
+ * @keywords counter, number input, increment, spinbutton
50
51
  * @category actions
51
52
  */
52
53
  export const QuantityControl = ({ value, label = "Quantity", onIncrement, onDecrement, onRemove, min = 1, max = 99, disabled = false, radius = "rounded", testId, }) => {
@@ -9,6 +9,7 @@ import type { WithTestId } from "../../utils/with-test-id.js";
9
9
  * @bestPractice Use `Select` to pick one option from a short closed list; when users need type-to-filter, use `Combobox` instead.
10
10
  * @bestPractice Drive it with `value` + `onValueChange` (controlled) or `defaultValue` (uncontrolled), and give it an accessible name via the trigger (`Field`/`Label`).
11
11
  * @bestPractice Pass `items` (a value-to-label map or `{ value, label }[]`) so the trigger shows the selected label even before the popup has mounted.
12
+ * @keywords dropdown, picker
12
13
  * @category forms
13
14
  */
14
15
  declare const Select: <Value, Multiple extends boolean | undefined = false>({ testId, ...props }: WithTestId<SelectPrimitive.Root.Props<Value, Multiple>>) => import("react").JSX.Element;
@@ -10,6 +10,7 @@ import { Select as SelectPrimitive } from "@base-ui/react/select";
10
10
  * @bestPractice Use `Select` to pick one option from a short closed list; when users need type-to-filter, use `Combobox` instead.
11
11
  * @bestPractice Drive it with `value` + `onValueChange` (controlled) or `defaultValue` (uncontrolled), and give it an accessible name via the trigger (`Field`/`Label`).
12
12
  * @bestPractice Pass `items` (a value-to-label map or `{ value, label }[]`) so the trigger shows the selected label even before the popup has mounted.
13
+ * @keywords dropdown, picker
13
14
  * @category forms
14
15
  */
15
16
  const Select = ({ testId, ...props }) => (_jsx(SelectPrimitive.Root, { "data-component": "select", "data-slot": "select", "data-testid": testId, ...props }));
@@ -24,6 +24,7 @@ interface SeparatorProps extends SeparatorPrimitive.Props {
24
24
  *
25
25
  * @bestPractice It is announced as a separator by default - pass `aria-hidden` when the rule is purely decorative.
26
26
  * @bestPractice For an inline rule set `orientation="vertical"` and give it an explicit height (e.g. `className="h-6"`) - a vertical separator has no intrinsic length.
27
+ * @keywords divider, rule, hr
27
28
  * @category layout
28
29
  */
29
30
  declare const Separator: ({ className, orientation, variant, ...props }: SeparatorProps) => import("react").JSX.Element;
@@ -22,6 +22,7 @@ const VARIANT_CLASSES = {
22
22
  *
23
23
  * @bestPractice It is announced as a separator by default - pass `aria-hidden` when the rule is purely decorative.
24
24
  * @bestPractice For an inline rule set `orientation="vertical"` and give it an explicit height (e.g. `className="h-6"`) - a vertical separator has no intrinsic length.
25
+ * @keywords divider, rule, hr
25
26
  * @category layout
26
27
  */
27
28
  const Separator = ({ className, orientation = "horizontal", variant = "default", ...props }) => (_jsx(SeparatorPrimitive, { className: cn("shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", VARIANT_CLASSES[variant], className), "data-component": "separator", "data-slot": "separator", orientation: orientation, ...props }));
@@ -5,6 +5,7 @@ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
5
5
  *
6
6
  * @bestPractice Use `Sheet` for a simple edge panel holding a focused form / settings; a drag-to-dismiss bottom sheet is a `Drawer`, and nested panels are a `Sidepanel`.
7
7
  * @bestPractice Set the entry edge via `SheetContent` `side`; always render a `SheetTitle` (wrap in `sr-only` if hidden) so the panel is announced.
8
+ * @keywords drawer, side sheet, slide-over
8
9
  * @category overlays
9
10
  */
10
11
  export declare const Sheet: (props: DialogPrimitive.Root.Props<unknown>) => import("react").ReactElement;
@@ -6,6 +6,7 @@ import { makeOverlayRoot } from "../overlay-primitives.js";
6
6
  *
7
7
  * @bestPractice Use `Sheet` for a simple edge panel holding a focused form / settings; a drag-to-dismiss bottom sheet is a `Drawer`, and nested panels are a `Sidepanel`.
8
8
  * @bestPractice Set the entry edge via `SheetContent` `side`; always render a `SheetTitle` (wrap in `sr-only` if hidden) so the panel is announced.
9
+ * @keywords drawer, side sheet, slide-over
9
10
  * @category overlays
10
11
  */
11
12
  export const Sheet = makeOverlayRoot(DialogPrimitive.Root, {
@@ -20,6 +20,7 @@ export type SpinnerProps = LucideProps & {
20
20
  *
21
21
  * @bestPractice Use `Spinner` for small inline / action loading (inside a `Button`, next to text); a blocking full region is `LoadingState`, a layout placeholder is `Skeleton`.
22
22
  * @bestPractice A bare `Spinner` carries no accessible name - wrap it in a labelled live region (e.g. `LoadingState`) when it's the sole loading signal.
23
+ * @keywords loader, loading, activity indicator
23
24
  * @category feedback
24
25
  */
25
26
  export declare const Spinner: ({ size, className, ...props }: SpinnerProps) => import("react").JSX.Element;
@@ -41,6 +41,7 @@ const SPINNER_STROKE = {
41
41
  *
42
42
  * @bestPractice Use `Spinner` for small inline / action loading (inside a `Button`, next to text); a blocking full region is `LoadingState`, a layout placeholder is `Skeleton`.
43
43
  * @bestPractice A bare `Spinner` carries no accessible name - wrap it in a labelled live region (e.g. `LoadingState`) when it's the sole loading signal.
44
+ * @keywords loader, loading, activity indicator
44
45
  * @category feedback
45
46
  */
46
47
  export const Spinner = ({ size = "base", className, ...props }) => {
@@ -26,6 +26,7 @@ export type StepperProps = {
26
26
  *
27
27
  * @bestPractice Give each step a `StepperItem` child in order - progress is derived from child index, so conditionally rendering steps shifts the fill.
28
28
  * @bestPractice Control completion via `value`/`onValueChange` (or uncontrolled `defaultValue`); the fill reaches only the last *completed* step, not the current one.
29
+ * @keywords wizard, steps, multi-step
29
30
  * @category navigation
30
31
  */
31
32
  export declare const Stepper: ({ value, defaultValue, onValueChange, size, className, children, ...rest }: StepperProps) => React.JSX.Element;
@@ -13,6 +13,7 @@ import { DEFAULT_STEPPER_SIZE, STEPPER_TRACK_SIZE_CLASSES, stepperRootVariants,
13
13
  *
14
14
  * @bestPractice Give each step a `StepperItem` child in order - progress is derived from child index, so conditionally rendering steps shifts the fill.
15
15
  * @bestPractice Control completion via `value`/`onValueChange` (or uncontrolled `defaultValue`); the fill reaches only the last *completed* step, not the current one.
16
+ * @keywords wizard, steps, multi-step
16
17
  * @category navigation
17
18
  */
18
19
  export const Stepper = ({ value, defaultValue = 0, onValueChange, size = "base", className, children, ...rest }) => {
@@ -30,6 +30,7 @@ export interface SuccessTickRippleProps {
30
30
  *
31
31
  * @bestPractice Wrap `SuccessTickRipple` in a `position: relative; overflow: hidden` parent - the ripple field is absolutely positioned and bleeds past an unbounded container.
32
32
  * @bestPractice It's decorative (`aria-hidden`) unless you pass `aria-label`, which exposes it as an `img`; animations are CSS-only and already collapse under `prefers-reduced-motion`.
33
+ * @keywords celebrate, success, confirmation
33
34
  * @category feedback
34
35
  */
35
36
  export declare const SuccessTickRipple: ({ size, className, "aria-label": ariaLabel, }: SuccessTickRippleProps) => import("react").JSX.Element;
@@ -23,6 +23,7 @@ const CHECK_PATH = "M5 13.5 9 17.5 19 6.5";
23
23
  *
24
24
  * @bestPractice Wrap `SuccessTickRipple` in a `position: relative; overflow: hidden` parent - the ripple field is absolutely positioned and bleeds past an unbounded container.
25
25
  * @bestPractice It's decorative (`aria-hidden`) unless you pass `aria-label`, which exposes it as an `img`; animations are CSS-only and already collapse under `prefers-reduced-motion`.
26
+ * @keywords celebrate, success, confirmation
26
27
  * @category feedback
27
28
  */
28
29
  export const SuccessTickRipple = ({ size = 64, className, "aria-label": ariaLabel, }) => {
@@ -42,6 +42,7 @@ type SwitchProps = Omit<React.ComponentProps<typeof SwitchPrimitive.Root>, "size
42
42
  * @bestPractice Use `Switch` for an instantly-applied on/off setting; use `Checkbox` for opt-ins confirmed by a submit, and `RadioGroup` for mutually-exclusive choices.
43
43
  * @bestPractice Controlled use needs `checked` + `onCheckedChange`; with only `checked` the switch won't move - use `defaultChecked` for uncontrolled.
44
44
  * @bestPractice `Switch` has no built-in label - associate a `Label` via `htmlFor`/`id` so the toggle has an accessible name.
45
+ * @keywords toggle, on off
45
46
  * @category forms
46
47
  */
47
48
  declare const Switch: ({ className, size, ...props }: SwitchProps) => React.JSX.Element;
@@ -57,6 +57,7 @@ export const SWITCH_THUMB_CLASSES = {
57
57
  * @bestPractice Use `Switch` for an instantly-applied on/off setting; use `Checkbox` for opt-ins confirmed by a submit, and `RadioGroup` for mutually-exclusive choices.
58
58
  * @bestPractice Controlled use needs `checked` + `onCheckedChange`; with only `checked` the switch won't move - use `defaultChecked` for uncontrolled.
59
59
  * @bestPractice `Switch` has no built-in label - associate a `Label` via `htmlFor`/`id` so the toggle has an accessible name.
60
+ * @keywords toggle, on off
60
61
  * @category forms
61
62
  */
62
63
  const Switch = ({ className, size, ...props }) => (_jsx(SwitchPrimitive.Root, { nativeButton: true, render: (buttonProps) => _jsx("button", { type: "button", ...buttonProps }), className: cn("peer data-checked:bg-primary data-unchecked:bg-muted dark:data-checked:bg-muted-foreground dark:data-unchecked:bg-secondary inline-flex shrink-0 items-center rounded-full border-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50 data-checked:shadow-xs", getResponsiveSizeClasses(size, SWITCH_TRACK_CLASSES, DEFAULT_SWITCH_SIZE), focusInput(), hasErrorInput,
@@ -53,6 +53,7 @@ export interface TagProps extends Omit<React.HTMLAttributes<HTMLElement>, "child
53
53
  * @bestPractice With both actions, HTML attributes (including the accessible name and selected state) apply to the selection button; `className` and `style` style the outer chip.
54
54
  * @bestPractice Set at most one leading visual - `loading` > `imageUrl` > `dot` > `icon` (in that precedence); pass a Lucide component to `icon` (not `<Icon/>`), and `imageUrl` renders a circular `<img alt="">`, so keep it decorative.
55
55
  * @bestPractice When `onDismiss` is set, Backspace/Delete also removes the tag while it's focused - don't bind your own conflicting key handler.
56
+ * @keywords chip, token
56
57
  * @category data
57
58
  */
58
59
  export declare const Tag: ({ className, style, tone, size, emphasis, asInput, radius, disabled, children, icon, dot, imageUrl, loading, onClick, onDismiss, dismissIcon, hideDismiss, ...props }: TagProps) => React.JSX.Element;
@@ -161,6 +161,7 @@ const getTagAppearanceProps = ({ emphasis, asInput, disabled, radius, }) => {
161
161
  * @bestPractice With both actions, HTML attributes (including the accessible name and selected state) apply to the selection button; `className` and `style` style the outer chip.
162
162
  * @bestPractice Set at most one leading visual - `loading` > `imageUrl` > `dot` > `icon` (in that precedence); pass a Lucide component to `icon` (not `<Icon/>`), and `imageUrl` renders a circular `<img alt="">`, so keep it decorative.
163
163
  * @bestPractice When `onDismiss` is set, Backspace/Delete also removes the tag while it's focused - don't bind your own conflicting key handler.
164
+ * @keywords chip, token
164
165
  * @category data
165
166
  */
166
167
  export const Tag = ({ className, style, tone, size = "base", emphasis, asInput, radius, disabled, children, icon, dot, imageUrl, loading = false, onClick, onDismiss, dismissIcon, hideDismiss = false, ...props }) => {
@@ -26,6 +26,7 @@ interface TextareaProps extends WithTestId<React.ComponentProps<"textarea">> {
26
26
  * @bestPractice Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.
27
27
  * @bestPractice It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.
28
28
  * @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.
29
+ * @keywords multiline, text area
29
30
  * @category forms
30
31
  */
31
32
  declare const Textarea: ({ className, testId, hasError, id, name, disabled, rows, style, ...props }: TextareaProps) => React.JSX.Element;
@@ -29,6 +29,7 @@ import { useTextareaRows } from "./use-textarea-rows.js";
29
29
  * @bestPractice Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.
30
30
  * @bestPractice It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.
31
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.
32
+ * @keywords multiline, text area
32
33
  * @category forms
33
34
  */
34
35
  const Textarea = ({ className, testId, hasError, id, name, disabled, rows, style, ...props }) => {
@@ -34,6 +34,7 @@ export interface TimeSlotPickerProps extends Omit<React.ComponentProps<"div">, "
34
34
  * @bestPractice Pass slot values already formatted for display, or give each slot a `label` - the picker does no time parsing or 12/24-hour formatting.
35
35
  * @bestPractice Name the group with `aria-label` or `aria-labelledby`; screen readers announce it before the first slot.
36
36
  * @bestPractice Use `TimeSlotPicker` for a fixed set of offered times; for any time the user types, use `Input type="time"`.
37
+ * @keywords date, appointment, booking, schedule
37
38
  * @category forms
38
39
  */
39
40
  export declare const TimeSlotPicker: ({ className, defaultValue, disabled, name, onValueChange, slots, value, ...props }: TimeSlotPickerProps) => React.JSX.Element;
@@ -19,6 +19,7 @@ focusRing(), "inline-flex h-9 cursor-pointer items-center justify-center px-4 se
19
19
  * @bestPractice Pass slot values already formatted for display, or give each slot a `label` - the picker does no time parsing or 12/24-hour formatting.
20
20
  * @bestPractice Name the group with `aria-label` or `aria-labelledby`; screen readers announce it before the first slot.
21
21
  * @bestPractice Use `TimeSlotPicker` for a fixed set of offered times; for any time the user types, use `Input type="time"`.
22
+ * @keywords date, appointment, booking, schedule
22
23
  * @category forms
23
24
  */
24
25
  export const TimeSlotPicker = ({ className, defaultValue, disabled, name, onValueChange, slots, value, ...props }) => {
@@ -34,6 +34,7 @@ export interface ToasterProps {
34
34
  * @bestPractice Use toasts for transient, auto-dismissing notifications; a persistent inline message is an `Alert`.
35
35
  * @bestPractice Mount `<Toaster />` exactly once near the app root, then fire toasts with `toast()` from `@instruments/materia-ui/components/toast` - don't render a `Toaster` per view.
36
36
  * @bestPractice Press F6 to move focus into the toasts; hovering or focusing the stack pauses dismissal and fans it open.
37
+ * @keywords notification, snackbar, flash message
37
38
  * @category feedback
38
39
  */
39
40
  export declare const Toaster: ({ position, duration, limit, className, }: ToasterProps) => React.JSX.Element;
@@ -101,6 +101,7 @@ const ToastList = ({ edge }) => {
101
101
  * @bestPractice Use toasts for transient, auto-dismissing notifications; a persistent inline message is an `Alert`.
102
102
  * @bestPractice Mount `<Toaster />` exactly once near the app root, then fire toasts with `toast()` from `@instruments/materia-ui/components/toast` - don't render a `Toaster` per view.
103
103
  * @bestPractice Press F6 to move focus into the toasts; hovering or focusing the stack pauses dismissal and fans it open.
104
+ * @keywords notification, snackbar, flash message
104
105
  * @category feedback
105
106
  */
106
107
  export const Toaster = ({ position = "bottom-center", duration = 4000, limit = 3, className, }) => {
@@ -28,6 +28,7 @@ declare const TooltipProvider: ({ delay, ...props }: TooltipPrimitive.Provider.P
28
28
  * @bestPractice Keep `TooltipContent` a short, non-interactive label - no buttons or links inside; for rich or actionable hover content use `HoverCard`.
29
29
  * @bestPractice The trigger must be focusable (a button / link, or `render` onto one) or keyboard and AT users never see the tip.
30
30
  * @bestPractice Share one `TooltipProvider` to tune `delay` / skip-delay for a group; `Tooltip` wraps its own provider, so you only need it for grouping.
31
+ * @keywords hint, title
31
32
  * @category overlays
32
33
  */
33
34
  declare const Tooltip: <Payload>({ delay, ...props }: TooltipPrimitive.Root.Props<Payload> & {
@@ -32,6 +32,7 @@ const TooltipProvider = ({ delay = 0, ...props }) => (_jsx(TooltipPrimitive.Prov
32
32
  * @bestPractice Keep `TooltipContent` a short, non-interactive label - no buttons or links inside; for rich or actionable hover content use `HoverCard`.
33
33
  * @bestPractice The trigger must be focusable (a button / link, or `render` onto one) or keyboard and AT users never see the tip.
34
34
  * @bestPractice Share one `TooltipProvider` to tune `delay` / skip-delay for a group; `Tooltip` wraps its own provider, so you only need it for grouping.
35
+ * @keywords hint, title
35
36
  * @category overlays
36
37
  */
37
38
  const Tooltip = ({ delay, ...props }) => (_jsx(TooltipProvider, { ...(delay === undefined ? {} : { delay }), children: _jsx(TooltipPrimitive.Root, { "data-component": "tooltip", "data-slot": "tooltip", ...props }) }));
@@ -41,6 +41,7 @@ export interface EmptyStateProps extends Omit<EmptyProps, "children" | "title">
41
41
  *
42
42
  * @bestPractice Use `EmptyState` for "no data / nothing here"; a failure the user can retry is an `ErrorState` (which also composes `Empty`), so don't hand-roll a retry empty.
43
43
  * @bestPractice Set `level` to keep the document outline gapless where you place it - a full-page empty near an `h2` wants `level="3"`, but a nested one may need `"4"`.
44
+ * @keywords no results, zero state, blank slate
44
45
  * @category states
45
46
  */
46
47
  export declare const EmptyState: ({ actions, description, icon, iconPlain, iconTone, level, media, title, ...emptyProps }: EmptyStateProps) => React.JSX.Element;
@@ -23,6 +23,7 @@ import { EmptyTitle } from "../../components/empty/empty-title.js";
23
23
  *
24
24
  * @bestPractice Use `EmptyState` for "no data / nothing here"; a failure the user can retry is an `ErrorState` (which also composes `Empty`), so don't hand-roll a retry empty.
25
25
  * @bestPractice Set `level` to keep the document outline gapless where you place it - a full-page empty near an `h2` wants `level="3"`, but a nested one may need `"4"`.
26
+ * @keywords no results, zero state, blank slate
26
27
  * @category states
27
28
  */
28
29
  export const EmptyState = ({ actions, description, icon, iconPlain, iconTone, level = "3", media, title, ...emptyProps }) => {
@@ -46,6 +46,7 @@ interface FileUploadDropzoneProps {
46
46
  * @bestPractice `FileUploadDropzone` is presentational - wire it to the `useFileUpload` hook for the actual file input, drag state, and file list; it stores nothing itself.
47
47
  * @bestPractice Forward the hook's `handleDragEnter/Leave/Over/Drop` and drag state into `dragging` so the drag styling matches the real drop-target state.
48
48
  * @bestPractice Keep `title`/`description` (accepted types, size limit) accurate - they are the zone's only affordance copy; override the whole body via `children` when needed.
49
+ * @keywords dropzone, attachment, file input
49
50
  * @category inputs
50
51
  */
51
52
  export declare const FileUploadDropzone: ({ ref, dragging, disabled, icon, title, description, onDragEnter, onDragLeave, onDragOver, onDrop, onClick, className, children, }: FileUploadDropzoneProps) => import("react").JSX.Element;
@@ -28,6 +28,7 @@ import { Text } from "../../components/text.js";
28
28
  * @bestPractice `FileUploadDropzone` is presentational - wire it to the `useFileUpload` hook for the actual file input, drag state, and file list; it stores nothing itself.
29
29
  * @bestPractice Forward the hook's `handleDragEnter/Leave/Over/Drop` and drag state into `dragging` so the drag styling matches the real drop-target state.
30
30
  * @bestPractice Keep `title`/`description` (accepted types, size limit) accurate - they are the zone's only affordance copy; override the whole body via `children` when needed.
31
+ * @keywords dropzone, attachment, file input
31
32
  * @category inputs
32
33
  */
33
34
  export const FileUploadDropzone = ({ ref, dragging = false, disabled = false, icon, title = "Drop files here or click to upload", description = "PNG, JPG, GIF up to 10MB", onDragEnter, onDragLeave, onDragOver, onDrop, onClick, className, children, }) => {
@@ -5,7 +5,7 @@ import { cn } from "@instruments/materia-ui/utils/cn";
5
5
  import { createElement, useCallback } from "react";
6
6
  import { useProductCard } from "./product-card-context.js";
7
7
  export const ProductCardCaption = ({ scale = "default", }) => {
8
- const { availability, currentVariant, linkComponent, linkPrefetch, onCardClick, product, productNameLines, productUrl, showProductBrand, showProductName, } = useProductCard();
8
+ const { availability, currentVariant, linkComponent, linkPrefetch, onCardClick, product, productBrandLines, productNameLines, productUrl, showProductBrand, showProductName, } = useProductCard();
9
9
  const handleLinkClick = useCallback((event) => {
10
10
  if (onCardClick) {
11
11
  event.preventDefault();
@@ -33,7 +33,9 @@ export const ProductCardCaption = ({ scale = "default", }) => {
33
33
  prefetch: isExternalUrl ? undefined : linkPrefetch,
34
34
  };
35
35
  return (_jsxs(_Fragment, { children: [showProductBrand && hasBrandName
36
- ? createElement(LinkComp, linkProps, _jsx(Text, { className: cn("line-clamp-1 truncate", brandSize), weight: "semibold", children: brandName }))
36
+ ? createElement(LinkComp, linkProps, _jsx(Text, { className: cn(productBrandLines === 2
37
+ ? "line-clamp-2 text-pretty"
38
+ : "line-clamp-1 truncate", brandSize), weight: "semibold", children: brandName }))
37
39
  : null, showProductName
38
40
  ? createElement(LinkComp, linkProps, _jsx(Text, { className: cn("text-foreground",
39
41
  // `truncate` and `line-clamp-2` cannot both apply: truncate
@@ -16,6 +16,14 @@ interface ProductCardProcessingProps {
16
16
  interface ProductCardDisplayOptions {
17
17
  addToCart?: boolean | undefined;
18
18
  brand?: boolean | undefined;
19
+ /**
20
+ * How many lines the brand may occupy before it clamps. One by default.
21
+ * Raise it to two where brands run long - *Scuffmaster Wolf-Gordon* clamps
22
+ * to *Scuffmaster Wolf-...* on one line of a 208px card - and the brand is
23
+ * how people tell the cards apart. Like `nameLines`, the clamp is a maximum,
24
+ * not a reserved height.
25
+ */
26
+ brandLines?: 1 | 2 | undefined;
19
27
  name?: boolean | undefined;
20
28
  /**
21
29
  * How many lines the product name may occupy before it clamps. One by
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Box } from "@instruments/materia-ui/components/box";
4
+ import { MarqueeText } from "@instruments/materia-ui/components/marquee-text";
4
5
  import { HStack } from "@instruments/materia-ui/components/stack";
5
- import { Text } from "@instruments/materia-ui/components/text";
6
6
  import { ProductCardCategory } from "./product-card-category.js";
7
7
  import { useProductCard } from "./product-card-context.js";
8
8
  import { ProductCardCost, hasProductCardPrice } from "./product-card-cost.js";
@@ -22,5 +22,5 @@ export const ProductCardMeta = () => {
22
22
  return null;
23
23
  }
24
24
  const showCostSeparator = showCategory || showProductType;
25
- return (_jsxs(HStack, { className: "text-muted-foreground text-caption w-full items-center", gap: "none", children: [showCategory ? _jsx(ProductCardCategory, {}) : null, showProductType ? (_jsx(Text, { size: "caption", variant: "muted", render: _jsx("span", { className: "min-w-0 truncate" }), children: product.productType })) : null, showCost ? (_jsxs(_Fragment, { children: [_jsx(Dot, { show: showCostSeparator }), _jsx(Box, { as: "span", className: "shrink-0", children: _jsx(ProductCardCost, {}) })] })) : null] }));
25
+ return (_jsxs(HStack, { className: "text-muted-foreground text-caption w-full items-center", gap: "none", children: [showCategory ? _jsx(ProductCardCategory, {}) : null, showProductType ? (_jsx(MarqueeText, { children: product.productType ?? "" })) : null, showCost ? (_jsxs(_Fragment, { children: [_jsx(Dot, { show: showCostSeparator }), _jsx(Box, { as: "span", className: "shrink-0", children: _jsx(ProductCardCost, {}) })] })) : null] }));
26
26
  };
@@ -72,6 +72,7 @@ interface ProductCardContextValue {
72
72
  showingInstallShot: boolean;
73
73
  toggleInstallShot: () => void;
74
74
  showAddToCart: boolean;
75
+ productBrandLines: 1 | 2;
75
76
  productNameLines: 1 | 2;
76
77
  showProductBrand: boolean;
77
78
  showProductName: boolean;
@@ -254,6 +255,8 @@ type ProductCardProps = ComponentProps<"div"> & Pick<ProductCardProviderProps, "
254
255
  overrideImageUrl?: string | undefined;
255
256
  /** Prioritise loading images (sets loading="eager" and adds preload link) */
256
257
  priority?: boolean | undefined;
258
+ /** How many lines the brand may take before it clamps; see `ProductCardDisplayOptions.brandLines`. */
259
+ productBrandLines?: 1 | 2 | undefined;
257
260
  productNameLines?: 1 | 2 | undefined;
258
261
  showAddToCart?: boolean | undefined;
259
262
  showProductBrand?: boolean | undefined;
@@ -62,6 +62,7 @@ const useVariantSelection = ({ mode, onVariantChange, product, variantId, }) =>
62
62
  * renderer exists: no renderAddToCart slot means no cart UI at all.
63
63
  */
64
64
  const resolveDisplay = (display, hasRenderAddToCart) => ({
65
+ productBrandLines: display?.brandLines ?? 1,
65
66
  productNameLines: display?.nameLines ?? 1,
66
67
  showAddToCart: (display?.addToCart ?? true) && hasRenderAddToCart,
67
68
  showProductBrand: display?.brand ?? true,
@@ -94,12 +95,13 @@ const useResolvedCard = ({ availability: availabilityProp, display, mode, onVari
94
95
  const availability = availabilityProp ??
95
96
  (currentVariant.available === false ? "out-of-stock" : "available");
96
97
  const productUrl = buildProductUrl(currentVariant);
97
- const { productNameLines, showAddToCart, showProductBrand, showProductName, showSwatches, } = resolveDisplay(display, renderAddToCart !== undefined);
98
+ const { productBrandLines, productNameLines, showAddToCart, showProductBrand, showProductName, showSwatches, } = resolveDisplay(display, renderAddToCart !== undefined);
98
99
  return useMemo(() => ({
99
100
  availability,
100
101
  currentVariant,
101
102
  hasInstallShot,
102
103
  onVariantChange: handleVariantChange,
104
+ productBrandLines,
103
105
  productNameLines,
104
106
  productUrl,
105
107
  selectedVariantId,
@@ -114,6 +116,7 @@ const useResolvedCard = ({ availability: availabilityProp, display, mode, onVari
114
116
  currentVariant,
115
117
  handleVariantChange,
116
118
  hasInstallShot,
119
+ productBrandLines,
117
120
  productNameLines,
118
121
  productUrl,
119
122
  selectedVariantId,
@@ -1,5 +1,5 @@
1
1
  import type { ProductCardProps } from "./product-card-types.js";
2
- declare const ProductCardBase: ({ className, product, variantId, mode, width, active, dragging, availability, labels, showActivePointer, similarityScore, overrideImageUrl, priority, showSwatches, showAddToCart, productNameLines, showProductBrand, showProductName, maxSwatches, menuOpen, onAdd, addGlyph, onFindSimilar, price, processing, onCardClick, onVariantChange, linkAs, productUrl, linkPrefetch, renderTools, renderImage, renderAddToCart, dragAdapter, action, primaryAction, statusBadge, onQuickView, onAddToCartSuccess, ...props }: ProductCardProps & {
2
+ declare const ProductCardBase: ({ className, product, variantId, mode, width, active, dragging, availability, labels, showActivePointer, similarityScore, overrideImageUrl, priority, showSwatches, showAddToCart, productBrandLines, productNameLines, showProductBrand, showProductName, maxSwatches, menuOpen, onAdd, addGlyph, onFindSimilar, price, processing, onCardClick, onVariantChange, linkAs, productUrl, linkPrefetch, renderTools, renderImage, renderAddToCart, dragAdapter, action, primaryAction, statusBadge, onQuickView, onAddToCartSuccess, ...props }: ProductCardProps & {
3
3
  onAddToCartSuccess?: (() => void) | undefined;
4
4
  }) => import("react").JSX.Element;
5
5
  /**
@@ -3,9 +3,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { memo } from "react";
4
4
  import { ProductCardContent } from "./product-card-content.js";
5
5
  import { ProductCardProvider } from "./product-card-provider.js";
6
- const ProductCardBase = ({ className, product, variantId, mode = "group", width, active = false, dragging, availability, labels, showActivePointer, similarityScore, overrideImageUrl, priority, showSwatches, showAddToCart, productNameLines, showProductBrand, showProductName, maxSwatches, menuOpen, onAdd, addGlyph, onFindSimilar, price, processing, onCardClick, onVariantChange, linkAs, productUrl, linkPrefetch, renderTools, renderImage, renderAddToCart, dragAdapter, action, primaryAction, statusBadge, onQuickView, onAddToCartSuccess, ...props }) => (_jsx(ProductCardProvider, { action: action, display: {
6
+ const ProductCardBase = ({ className, product, variantId, mode = "group", width, active = false, dragging, availability, labels, showActivePointer, similarityScore, overrideImageUrl, priority, showSwatches, showAddToCart, productBrandLines, productNameLines, showProductBrand, showProductName, maxSwatches, menuOpen, onAdd, addGlyph, onFindSimilar, price, processing, onCardClick, onVariantChange, linkAs, productUrl, linkPrefetch, renderTools, renderImage, renderAddToCart, dragAdapter, action, primaryAction, statusBadge, onQuickView, onAddToCartSuccess, ...props }) => (_jsx(ProductCardProvider, { action: action, display: {
7
7
  addToCart: showAddToCart,
8
8
  brand: showProductBrand,
9
+ brandLines: productBrandLines,
9
10
  name: showProductName,
10
11
  nameLines: productNameLines,
11
12
  swatches: showSwatches,
@@ -2,6 +2,14 @@ import type { CSSProperties } from "react";
2
2
  interface ProductCardSkeletonProps {
3
3
  className?: string;
4
4
  style?: CSSProperties;
5
+ /**
6
+ * Draw the add-to-cart control. True by default; pass the same value as the
7
+ * card's `showAddToCart` so a grid without a cart keeps its height when the
8
+ * products arrive.
9
+ */
10
+ showAddToCart?: boolean;
11
+ /** Draw the row of colourway dots. True by default; pass the card's `showSwatches`. */
12
+ showSwatches?: boolean;
5
13
  }
6
14
  /**
7
15
  * Loading placeholder for `ProductCard`, at whatever size it is given.
@@ -19,10 +27,11 @@ interface ProductCardSkeletonProps {
19
27
  * frame's bottom above it), the same padding, gaps and radius steps. Copying
20
28
  * the classes keeps it in step as the card changes.
21
29
  *
22
- * It draws the control unconditionally. A card whose product has no capability
23
- * renders none, so the placeholder overshoots that case - the reverse error, a
24
- * placeholder shorter than every card that does have one, misjudged the common
25
- * case instead.
30
+ * It draws the control and the swatch row by default. A card whose product has
31
+ * no capability renders no control, so the placeholder overshoots that case -
32
+ * the reverse error, a placeholder shorter than every card that does have one,
33
+ * misjudged the common case instead. A grid that turns either off on every
34
+ * card passes the same flags here.
26
35
  */
27
- export declare const ProductCardSkeleton: ({ className, style, }: ProductCardSkeletonProps) => import("react").JSX.Element;
36
+ export declare const ProductCardSkeleton: ({ className, showAddToCart, showSwatches, style, }: ProductCardSkeletonProps) => import("react").JSX.Element;
28
37
  export {};
@@ -23,9 +23,10 @@ const SwatchRow = () => (_jsx(HStack, { className: "min-h-5 gap-2.5", children:
23
23
  * frame's bottom above it), the same padding, gaps and radius steps. Copying
24
24
  * the classes keeps it in step as the card changes.
25
25
  *
26
- * It draws the control unconditionally. A card whose product has no capability
27
- * renders none, so the placeholder overshoots that case - the reverse error, a
28
- * placeholder shorter than every card that does have one, misjudged the common
29
- * case instead.
26
+ * It draws the control and the swatch row by default. A card whose product has
27
+ * no capability renders no control, so the placeholder overshoots that case -
28
+ * the reverse error, a placeholder shorter than every card that does have one,
29
+ * misjudged the common case instead. A grid that turns either off on every
30
+ * card passes the same flags here.
30
31
  */
31
- export const ProductCardSkeleton = ({ className, style, }) => (_jsxs(Box, { className: cn("@container/product-card w-full", className), style: style, children: [_jsxs(VStack, { className: "flex @[180px]/product-card:hidden", gap: "none", children: [_jsx(Skeleton, { className: "aspect-square w-full rounded-3xl" }), _jsxs(VStack, { className: "gap-2.5 px-1.5 py-2", gap: "none", children: [_jsxs(VStack, { className: "gap-1 px-0.5", gap: "none", children: [_jsxs(VStack, { className: "gap-0.5", gap: "none", children: [_jsx(Skeleton, { className: "h-5 w-3/5" }), _jsx(Skeleton, { className: "h-4 w-4/5" })] }), _jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(SwatchRow, {})] }), _jsx(Skeleton, { className: "h-9 w-full rounded-full" })] })] }), _jsxs(VStack, { className: "bg-card hidden overflow-hidden rounded-3xl shadow-xs @[180px]/product-card:flex @[240px]/product-card:rounded-4xl", gap: "none", children: [_jsx(Box, { className: "w-full px-4 pt-4", children: _jsx(Skeleton, { className: "aspect-square w-full rounded-2xl", variant: "light" }) }), _jsx(VStack, { className: "pt-2 pb-4", gap: "none", children: _jsxs(VStack, { className: "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(VStack, { gap: "none", children: [_jsx(Skeleton, { className: "h-5.5 w-3/5 rounded", variant: "light" }), _jsx(Skeleton, { className: "h-5 w-4/5 rounded", variant: "light" })] }), _jsx(Skeleton, { className: "h-4 w-2/3 rounded", variant: "light" }), _jsx(Box, { className: "pt-1", children: _jsx(SwatchRow, {}) })] }), _jsx(HStack, { children: _jsx(Skeleton, { className: "h-9 w-full rounded-full @[240px]/product-card:w-32", variant: "light" }) })] }) })] })] }));
32
+ export const ProductCardSkeleton = ({ className, showAddToCart = true, showSwatches = true, style, }) => (_jsxs(Box, { className: cn("@container/product-card w-full", className), style: style, children: [_jsxs(VStack, { className: "flex @[180px]/product-card:hidden", gap: "none", children: [_jsx(Skeleton, { className: "aspect-square w-full rounded-3xl" }), _jsxs(VStack, { className: "gap-2.5 px-1.5 py-2", gap: "none", children: [_jsxs(VStack, { className: "gap-1 px-0.5", gap: "none", children: [_jsxs(VStack, { className: "gap-0.5", gap: "none", children: [_jsx(Skeleton, { className: "h-5 w-3/5" }), _jsx(Skeleton, { className: "h-4 w-4/5" })] }), _jsx(Skeleton, { className: "h-4 w-2/3" }), showSwatches ? _jsx(SwatchRow, {}) : null] }), showAddToCart ? (_jsx(Skeleton, { className: "h-9 w-full rounded-full" })) : null] })] }), _jsxs(VStack, { className: "bg-card hidden overflow-hidden rounded-3xl shadow-xs @[180px]/product-card:flex @[240px]/product-card:rounded-4xl", gap: "none", children: [_jsx(Box, { className: "w-full px-4 pt-4", children: _jsx(Skeleton, { className: "aspect-square w-full rounded-2xl", variant: "light" }) }), _jsx(VStack, { className: "pt-2 pb-4", gap: "none", children: _jsxs(VStack, { className: "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(VStack, { gap: "none", children: [_jsx(Skeleton, { className: "h-5.5 w-3/5 rounded", variant: "light" }), _jsx(Skeleton, { className: "h-5 w-4/5 rounded", variant: "light" })] }), _jsx(Skeleton, { className: "h-4 w-2/3 rounded", variant: "light" }), showSwatches ? (_jsx(Box, { className: "pt-1", children: _jsx(SwatchRow, {}) })) : null] }), showAddToCart ? (_jsx(HStack, { children: _jsx(Skeleton, { className: "h-9 w-full rounded-full @[240px]/product-card:w-32", variant: "light" }) })) : null] }) })] })] }));
@@ -10,7 +10,7 @@
10
10
  * unit's page (`/components/<slug>` or `/compositions/<slug>`).
11
11
  *
12
12
  * Every leaf slug is validated against the generated docs manifest at
13
- * generation time (`scripts/generate-docs-manifest.mjs`), so a component rename
13
+ * generation time (`scripts/generate-docs-manifest.ts`), so a component rename
14
14
  * fails the manifest freshness test until the tree is updated - the drift gate. Family `id`s
15
15
  * are validated against `COMPONENT_CATEGORIES`; a family renders on the shelf of
16
16
  * its `id`, and several families may share a shelf (three feedback families
@@ -10,7 +10,7 @@
10
10
  * unit's page (`/components/<slug>` or `/compositions/<slug>`).
11
11
  *
12
12
  * Every leaf slug is validated against the generated docs manifest at
13
- * generation time (`scripts/generate-docs-manifest.mjs`), so a component rename
13
+ * generation time (`scripts/generate-docs-manifest.ts`), so a component rename
14
14
  * fails the manifest freshness test until the tree is updated - the drift gate. Family `id`s
15
15
  * are validated against `COMPONENT_CATEGORIES`; a family renders on the shelf of
16
16
  * its `id`, and several families may share a shelf (three feedback families
@@ -26,6 +26,8 @@ export interface DocsComponentEntry {
26
26
  description: string;
27
27
  exports: DocsExportEntry[];
28
28
  bestPractices: string[];
29
+ /** Words an agent might search for that the title and description don't use, from `@keywords`. */
30
+ keywords: string[];
29
31
  /** Same-layer component/composition slugs this unit's implementation imports. */
30
32
  uses: string[];
31
33
  /** Same-layer component/composition slugs that import this unit. */