@instruments/materia-ui 0.4.4 → 0.4.6
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 -0
- package/dist/components/aspect-ratio/aspect-ratio-root.d.ts +7 -3
- package/dist/components/aspect-ratio/aspect-ratio-root.js +5 -3
- package/dist/components/media-frame/media-frame-root.d.ts +2 -0
- package/dist/components/media-frame/media-frame-root.js +2 -0
- package/dist/components/swatch/swatch-root.d.ts +2 -0
- package/dist/components/swatch/swatch-root.js +2 -0
- package/dist/compositions/product-card/product-card-image.js +5 -4
- package/dist/compositions/product-card/product-card-radius.d.ts +9 -0
- package/dist/compositions/product-card/product-card-radius.js +9 -0
- package/dist/compositions/product-card/product-card-root.d.ts +2 -0
- package/dist/compositions/product-card/product-card-root.js +2 -0
- package/dist/compositions/product-card/product-card-skeleton.js +2 -1
- package/dist/compositions/product-card/product-card-swatch.js +2 -1
- package/dist/data/docs-manifest.js +29 -6
- package/package.json +2 -1
package/agent-docs.md
CHANGED
|
@@ -80,6 +80,7 @@ Constrains its content to a fixed width-to-height ratio, sizing itself to the av
|
|
|
80
80
|
|
|
81
81
|
- Give it exactly one child and let it fill - set the media to `size-full object-cover`; `AspectRatio` sizes to the available width and derives height from `ratio`.
|
|
82
82
|
- Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.
|
|
83
|
+
- Show a material, swatch or sample at the default 1:1 - never landscape. A sample is cut square, and a wide crop reads as room photography; set a wide `ratio` only for a scene, an interior or video.
|
|
83
84
|
|
|
84
85
|
## AttributeCard
|
|
85
86
|
|
|
@@ -1033,6 +1034,7 @@ Extracted from the product-card and editorial-card media frames; available to an
|
|
|
1033
1034
|
- Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.
|
|
1034
1035
|
- Set `edge` to frame photography with an inset hairline that stays above the media; do not fake it with a border or an inset shadow on the frame, which the media hides.
|
|
1035
1036
|
- Reach for `raised` only when the frame sits on the same fill as its surface; in a dense row the edge already separates it.
|
|
1037
|
+
- Frame a material, swatch or sample square: give the frame `aspect-square` (or wrap it in `AspectRatio`, which defaults to 1:1). Never crop a material landscape.
|
|
1036
1038
|
|
|
1037
1039
|
## MediaFrameEmbed
|
|
1038
1040
|
|
|
@@ -1244,6 +1246,7 @@ This is a presentational component that accepts handlers via props for app-speci
|
|
|
1244
1246
|
|
|
1245
1247
|
- In Next.js apps pass `renderImage` wrapping `next/image` and `linkAs={Link}` - the defaults are a plain `<img>` and `<a>`.
|
|
1246
1248
|
- Cart UI renders only when a `renderAddToCart` slot is provided; keep `dragAdapter` referentially stable (module-level constant).
|
|
1249
|
+
- Use ProductCard for a material in a grid or list; its image is square, which is how a material is always shown. Don't rebuild the card with a landscape image.
|
|
1247
1250
|
|
|
1248
1251
|
## ProductCardProvider
|
|
1249
1252
|
|
|
@@ -1833,6 +1836,7 @@ Chips paint the darkening overlay on every fill and draw the double-ring selecti
|
|
|
1833
1836
|
- Feed fills through `color`/`background`/`colors`, never a hex in `className`/`style` - the props drive the contrast-aware text colour, selection ring, and darkening overlay.
|
|
1834
1837
|
- Reach for `Swatch` for a selectable colour/gradient/material chip or tile; a tiny non-interactive status marker is `Dot`, and a display-only status/category label is `Badge`.
|
|
1835
1838
|
- For a gradient or image chip fill pass `light` - auto contrast detection (`isLightColor`) only reads solid colours, so overlaid icons/labels lose legibility without it.
|
|
1839
|
+
- A material swatch is always square, as Swatch draws it; never stretch a tile into a landscape crop, and in a grid let the column set the size.
|
|
1836
1840
|
|
|
1837
1841
|
## SwatchFan
|
|
1838
1842
|
|
|
@@ -14,7 +14,9 @@ interface AspectRatioProps extends useRender.ComponentProps<"div"> {
|
|
|
14
14
|
as?: LayoutElement;
|
|
15
15
|
/**
|
|
16
16
|
* Desired width-to-height ratio, expressed as a single number (width / height).
|
|
17
|
-
*
|
|
17
|
+
* Square unless asked: a material is shown at 1:1, and only scenes and
|
|
18
|
+
* video take a wide ratio.
|
|
19
|
+
* @default 1
|
|
18
20
|
*/
|
|
19
21
|
ratio?: number;
|
|
20
22
|
}
|
|
@@ -25,13 +27,15 @@ interface AspectRatioProps extends useRender.ComponentProps<"div"> {
|
|
|
25
27
|
*
|
|
26
28
|
* @example
|
|
27
29
|
* ```tsx
|
|
28
|
-
* <AspectRatio
|
|
29
|
-
* <img alt="" className="size-full object-cover" src="/
|
|
30
|
+
* <AspectRatio>
|
|
31
|
+
* <img alt="" className="size-full object-cover" src="/oak-plank.jpg" />
|
|
30
32
|
* </AspectRatio>
|
|
31
33
|
* ```
|
|
32
34
|
*
|
|
33
35
|
* @bestPractice Give it exactly one child and let it fill - set the media to `size-full object-cover`; `AspectRatio` sizes to the available width and derives height from `ratio`.
|
|
34
36
|
* @bestPractice Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.
|
|
37
|
+
* @bestPractice Show a material, swatch or sample at the default 1:1 - never landscape. A sample is cut square, and a wide crop reads as room photography; set a wide `ratio` only for a scene, an interior or video.
|
|
38
|
+
* @keywords material image, swatch image, sample image, thumbnail, square, image ratio
|
|
35
39
|
* @category layout
|
|
36
40
|
*/
|
|
37
41
|
declare const AspectRatio: ({ as, className, ratio, ref, render, style, ...props }: AspectRatioProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useRender } from "@base-ui/react/use-render";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
4
|
import { reportDevIssue } from "../../utils/dev-issue.js";
|
|
5
|
-
const DEFAULT_RATIO =
|
|
5
|
+
const DEFAULT_RATIO = 1;
|
|
6
6
|
/**
|
|
7
7
|
* Constrains its content to a fixed width-to-height ratio, sizing itself to the
|
|
8
8
|
* available width with CSS `aspect-ratio`; drop a single child (image, video,
|
|
@@ -10,13 +10,15 @@ const DEFAULT_RATIO = 16 / 9;
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```tsx
|
|
13
|
-
* <AspectRatio
|
|
14
|
-
* <img alt="" className="size-full object-cover" src="/
|
|
13
|
+
* <AspectRatio>
|
|
14
|
+
* <img alt="" className="size-full object-cover" src="/oak-plank.jpg" />
|
|
15
15
|
* </AspectRatio>
|
|
16
16
|
* ```
|
|
17
17
|
*
|
|
18
18
|
* @bestPractice Give it exactly one child and let it fill - set the media to `size-full object-cover`; `AspectRatio` sizes to the available width and derives height from `ratio`.
|
|
19
19
|
* @bestPractice Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.
|
|
20
|
+
* @bestPractice Show a material, swatch or sample at the default 1:1 - never landscape. A sample is cut square, and a wide crop reads as room photography; set a wide `ratio` only for a scene, an interior or video.
|
|
21
|
+
* @keywords material image, swatch image, sample image, thumbnail, square, image ratio
|
|
20
22
|
* @category layout
|
|
21
23
|
*/
|
|
22
24
|
const AspectRatio = ({ as, className, ratio = DEFAULT_RATIO, ref, render, style, ...props }) => {
|
|
@@ -51,6 +51,8 @@ export interface MediaFrameProps extends useRender.ComponentProps<"div"> {
|
|
|
51
51
|
* @bestPractice Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.
|
|
52
52
|
* @bestPractice Set `edge` to frame photography with an inset hairline that stays above the media; do not fake it with a border or an inset shadow on the frame, which the media hides.
|
|
53
53
|
* @bestPractice Reach for `raised` only when the frame sits on the same fill as its surface; in a dense row the edge already separates it.
|
|
54
|
+
* @bestPractice Frame a material, swatch or sample square: give the frame `aspect-square` (or wrap it in `AspectRatio`, which defaults to 1:1). Never crop a material landscape.
|
|
55
|
+
* @keywords material image, product image, photo frame, thumbnail
|
|
54
56
|
* @category layout
|
|
55
57
|
*/
|
|
56
58
|
export declare const MediaFrame: ({ as, className, children, edge, raised, ref, render, ...props }: MediaFrameProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -25,6 +25,8 @@ import { reportDevIssue } from "../../utils/dev-issue.js";
|
|
|
25
25
|
* @bestPractice Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.
|
|
26
26
|
* @bestPractice Set `edge` to frame photography with an inset hairline that stays above the media; do not fake it with a border or an inset shadow on the frame, which the media hides.
|
|
27
27
|
* @bestPractice Reach for `raised` only when the frame sits on the same fill as its surface; in a dense row the edge already separates it.
|
|
28
|
+
* @bestPractice Frame a material, swatch or sample square: give the frame `aspect-square` (or wrap it in `AspectRatio`, which defaults to 1:1). Never crop a material landscape.
|
|
29
|
+
* @keywords material image, product image, photo frame, thumbnail
|
|
28
30
|
* @category layout
|
|
29
31
|
*/
|
|
30
32
|
export const MediaFrame = ({ as, className, children, edge = false, raised = false, ref, render, ...props }) => {
|
|
@@ -38,6 +38,8 @@ type SwatchProps = ChipSwatchProps | TileSwatchProps;
|
|
|
38
38
|
* @bestPractice Feed fills through `color`/`background`/`colors`, never a hex in `className`/`style` - the props drive the contrast-aware text colour, selection ring, and darkening overlay.
|
|
39
39
|
* @bestPractice Reach for `Swatch` for a selectable colour/gradient/material chip or tile; a tiny non-interactive status marker is `Dot`, and a display-only status/category label is `Badge`.
|
|
40
40
|
* @bestPractice For a gradient or image chip fill pass `light` - auto contrast detection (`isLightColor`) only reads solid colours, so overlaid icons/labels lose legibility without it.
|
|
41
|
+
* @bestPractice A material swatch is always square, as Swatch draws it; never stretch a tile into a landscape crop, and in a grid let the column set the size.
|
|
42
|
+
* @keywords material swatch, material sample, finish, colourway, colour chip
|
|
41
43
|
* @category color
|
|
42
44
|
*/
|
|
43
45
|
declare const Swatch: (props: SwatchProps) => import("react").JSX.Element;
|
|
@@ -29,6 +29,8 @@ import { SwatchTile } from "./swatch-tile.js";
|
|
|
29
29
|
* @bestPractice Feed fills through `color`/`background`/`colors`, never a hex in `className`/`style` - the props drive the contrast-aware text colour, selection ring, and darkening overlay.
|
|
30
30
|
* @bestPractice Reach for `Swatch` for a selectable colour/gradient/material chip or tile; a tiny non-interactive status marker is `Dot`, and a display-only status/category label is `Badge`.
|
|
31
31
|
* @bestPractice For a gradient or image chip fill pass `light` - auto contrast detection (`isLightColor`) only reads solid colours, so overlaid icons/labels lose legibility without it.
|
|
32
|
+
* @bestPractice A material swatch is always square, as Swatch draws it; never stretch a tile into a landscape crop, and in a grid let the column set the size.
|
|
33
|
+
* @keywords material swatch, material sample, finish, colourway, colour chip
|
|
32
34
|
* @category color
|
|
33
35
|
*/
|
|
34
36
|
const Swatch = (props) => {
|
|
@@ -6,6 +6,7 @@ import { cn } from "@instruments/materia-ui/utils/cn";
|
|
|
6
6
|
import { memo, useMemo } from "react";
|
|
7
7
|
import { ProductCardAvailabilityStatus } from "./product-card-availability.js";
|
|
8
8
|
import { useProductCard } from "./product-card-context.js";
|
|
9
|
+
import { PRODUCT_CARD_IMAGE_RADIUS, PRODUCT_CARD_SWATCH_IMAGE_RADIUS, } from "./product-card-radius.js";
|
|
9
10
|
/**
|
|
10
11
|
* Handle used when no `dragAdapter` is provided: a static, non-draggable
|
|
11
12
|
* image. Module-level so its identity is stable across renders.
|
|
@@ -24,7 +25,7 @@ const NoPhotographyState = ({ className, inspiration, productName, }) => {
|
|
|
24
25
|
if (inspiration) {
|
|
25
26
|
return (_jsx("output", { "aria-label": `Photography unavailable for ${productName}`, className: cn("bg-muted/45 absolute inset-0 overflow-hidden rounded-3xl", className), "data-component": "product-card-image-unavailable" }));
|
|
26
27
|
}
|
|
27
|
-
return (_jsx("output", { "aria-label": `Photography unavailable for ${productName}`, className: cn("block w-full @[180px]/product-card:px-4 @[180px]/product-card:pt-4", className), "data-component": "product-card-image-unavailable", children: _jsx("span", { className: "bg-muted/45 block aspect-square w-full overflow-hidden
|
|
28
|
+
return (_jsx("output", { "aria-label": `Photography unavailable for ${productName}`, className: cn("block w-full @[180px]/product-card:px-4 @[180px]/product-card:pt-4", className), "data-component": "product-card-image-unavailable", children: _jsx("span", { className: cn("bg-muted/45 block aspect-square w-full overflow-hidden", PRODUCT_CARD_IMAGE_RADIUS) }) }));
|
|
28
29
|
};
|
|
29
30
|
/**
|
|
30
31
|
* The photograph itself, filling the frame `DraggableImage` insets - the
|
|
@@ -46,7 +47,7 @@ const ImageContent = ({ imageSrc, hoverImageSrc, showingInstallShot, productName
|
|
|
46
47
|
productImageFade = showingInstallShot ? "opacity-0" : "";
|
|
47
48
|
installImageFade = showingInstallShot ? "opacity-100" : "opacity-0";
|
|
48
49
|
}
|
|
49
|
-
return (_jsx(Box, { className: "absolute inset-0 overflow-hidden", children: _jsxs(MediaFrame, { className: "size-full
|
|
50
|
+
return (_jsx(Box, { className: "absolute inset-0 overflow-hidden", children: _jsxs(MediaFrame, { className: cn("size-full", PRODUCT_CARD_IMAGE_RADIUS), children: [renderImage({
|
|
50
51
|
alt: `${productName} - Product image`,
|
|
51
52
|
className: cn("object-cover", hasHoverImage && "duration-normal transition-opacity ease-out", hasHoverImage && productImageFade),
|
|
52
53
|
draggable: false,
|
|
@@ -89,11 +90,11 @@ const DraggableImage = ({ className, imageSrc, hoverImageSrc, variantName, showS
|
|
|
89
90
|
scale: dragging ? 0.9 : 1,
|
|
90
91
|
touchAction: "none",
|
|
91
92
|
transition: "opacity 300ms ease-out, scale 300ms ease-out",
|
|
92
|
-
}, ...dragListeners, ...props, children: _jsxs(MediaFrame, { className: "@[180px]/product-card:bg-card relative aspect-square w-full
|
|
93
|
+
}, ...dragListeners, ...props, children: _jsxs(MediaFrame, { className: cn("@[180px]/product-card:bg-card relative aspect-square w-full bg-gray-100", PRODUCT_CARD_IMAGE_RADIUS), children: [_jsx(ImageContent, { hoverImageSrc: hoverImageSrc, imageSrc: imageSrc, showingInstallShot: showingInstallShot, priority: priority, productName: variantName, sizes: sizes }), shouldShowSimilarityScore ? (_jsx(Box, { className: "absolute right-2 bottom-2 z-10", children: _jsxs(Box, { as: "span", className: "glass-media text-micro rounded px-1.5 py-0.5 text-white/70", children: [(similarityScore * 100).toFixed(0), "%"] }) })) : null, _jsx(ProductCardAvailabilityStatus, {})] }) }));
|
|
93
94
|
};
|
|
94
95
|
const SwatchImage = ({ className, dragListeners, imageSizes, isDragging, mainImage, priority, ref, ...props }) => {
|
|
95
96
|
const { currentVariant, renderImage } = useProductCard();
|
|
96
|
-
return (_jsxs(Box, { className: cn("relative size-full overflow-hidden
|
|
97
|
+
return (_jsxs(Box, { className: cn("relative size-full overflow-hidden bg-transparent", PRODUCT_CARD_SWATCH_IMAGE_RADIUS, className), "data-component": "product-card-image", onClickCapture: (e) => {
|
|
97
98
|
if (isDragging) {
|
|
98
99
|
e.preventDefault();
|
|
99
100
|
e.stopPropagation();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The photograph's corner, concentric with the card's. Below 180px the
|
|
3
|
+
* photograph is the card and takes its corner; from 180px it sits 16px inside
|
|
4
|
+
* the card, so its corner is the card's less 16px (8px, then 20px once the card
|
|
5
|
+
* takes the 4xl corner at 240px).
|
|
6
|
+
*/
|
|
7
|
+
export declare const PRODUCT_CARD_IMAGE_RADIUS = "rounded-3xl @[180px]/product-card:rounded-[calc(var(--radius-3xl)_-_1rem)] @[240px]/product-card:rounded-[calc(var(--radius-4xl)_-_1rem)]";
|
|
8
|
+
/** The swatch tile's photograph, 4px inside the tile's 3xl corner. */
|
|
9
|
+
export declare const PRODUCT_CARD_SWATCH_IMAGE_RADIUS = "rounded-[calc(var(--radius-3xl)_-_0.25rem)]";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The photograph's corner, concentric with the card's. Below 180px the
|
|
3
|
+
* photograph is the card and takes its corner; from 180px it sits 16px inside
|
|
4
|
+
* the card, so its corner is the card's less 16px (8px, then 20px once the card
|
|
5
|
+
* takes the 4xl corner at 240px).
|
|
6
|
+
*/
|
|
7
|
+
export const PRODUCT_CARD_IMAGE_RADIUS = "rounded-3xl @[180px]/product-card:rounded-[calc(var(--radius-3xl)_-_1rem)] @[240px]/product-card:rounded-[calc(var(--radius-4xl)_-_1rem)]";
|
|
8
|
+
/** The swatch tile's photograph, 4px inside the tile's 3xl corner. */
|
|
9
|
+
export const PRODUCT_CARD_SWATCH_IMAGE_RADIUS = "rounded-[calc(var(--radius-3xl)_-_0.25rem)]";
|
|
@@ -33,6 +33,8 @@ declare const ProductCardBase: ({ className, product, variantId, mode, width, ac
|
|
|
33
33
|
* `linkAs={Link}` - the defaults are a plain `<img>` and `<a>`.
|
|
34
34
|
* @bestPractice Cart UI renders only when a `renderAddToCart` slot is
|
|
35
35
|
* provided; keep `dragAdapter` referentially stable (module-level constant).
|
|
36
|
+
* @bestPractice Use ProductCard for a material in a grid or list; its image is square, which is how a material is always shown. Don't rebuild the card with a landscape image.
|
|
37
|
+
* @keywords material card, material tile, sample card, catalogue card, product tile
|
|
36
38
|
* @category catalogue
|
|
37
39
|
*/
|
|
38
40
|
export declare const ProductCard: import("react").MemoExoticComponent<typeof ProductCardBase>;
|
|
@@ -42,6 +42,8 @@ const ProductCardBase = ({ className, product, variantId, mode = "group", width,
|
|
|
42
42
|
* `linkAs={Link}` - the defaults are a plain `<img>` and `<a>`.
|
|
43
43
|
* @bestPractice Cart UI renders only when a `renderAddToCart` slot is
|
|
44
44
|
* provided; keep `dragAdapter` referentially stable (module-level constant).
|
|
45
|
+
* @bestPractice Use ProductCard for a material in a grid or list; its image is square, which is how a material is always shown. Don't rebuild the card with a landscape image.
|
|
46
|
+
* @keywords material card, material tile, sample card, catalogue card, product tile
|
|
45
47
|
* @category catalogue
|
|
46
48
|
*/
|
|
47
49
|
export const ProductCard = memo(ProductCardBase);
|
|
@@ -3,6 +3,7 @@ import { Box } from "@instruments/materia-ui/components/box";
|
|
|
3
3
|
import { Skeleton } from "@instruments/materia-ui/components/skeleton";
|
|
4
4
|
import { HStack, VStack } from "@instruments/materia-ui/components/stack";
|
|
5
5
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
6
|
+
import { PRODUCT_CARD_IMAGE_RADIUS } from "./product-card-radius.js";
|
|
6
7
|
/** The fifth dot, which the card itself only shows from 240px up. */
|
|
7
8
|
const SWATCH_PLACEHOLDERS = [0, 1, 2, 3, 4];
|
|
8
9
|
const SwatchRow = () => (_jsx(HStack, { className: "min-h-5 gap-2.5", children: SWATCH_PLACEHOLDERS.map((index) => (_jsx(Skeleton, { className: cn("size-5 rounded-full", index === SWATCH_PLACEHOLDERS.length - 1 &&
|
|
@@ -29,4 +30,4 @@ const SwatchRow = () => (_jsx(HStack, { className: "min-h-5 gap-2.5", children:
|
|
|
29
30
|
* misjudged the common case instead. A grid that turns either off on every
|
|
30
31
|
* card passes the same flags here.
|
|
31
32
|
*/
|
|
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
|
|
33
|
+
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: cn("aspect-square w-full", PRODUCT_CARD_IMAGE_RADIUS), 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] }) })] })] }));
|
|
@@ -9,6 +9,7 @@ import { ProductCardImage } from "./product-card-image.js";
|
|
|
9
9
|
import { ProductCardLink } from "./product-card-link.js";
|
|
10
10
|
import { ProductCardProcessing } from "./product-card-processing.js";
|
|
11
11
|
import { ProductCardQuickView } from "./product-card-quick-view.js";
|
|
12
|
+
import { PRODUCT_CARD_SWATCH_IMAGE_RADIUS } from "./product-card-radius.js";
|
|
12
13
|
import { ProductCardSwatchAdd } from "./product-card-swatch-add.js";
|
|
13
14
|
const SwatchShell = ({ active, children, dragging, processing, }) => (_jsx(Box, { className: cn(
|
|
14
15
|
// Bare at rest, so the tile reads as its image alone; the card surface
|
|
@@ -32,7 +33,7 @@ const SwatchCaption = () => {
|
|
|
32
33
|
/** The square photograph with the add control and Quick view over it. */
|
|
33
34
|
const SwatchMedia = ({ overrideImageUrl, priority, }) => {
|
|
34
35
|
const { active, addGlyph, availability, currentVariant, dragging, labels, linkComponent, linkPrefetch, menuOpen, onAdd, onCardClick, onQuickView, processing, productUrl, } = useProductCard();
|
|
35
|
-
return (_jsxs(Box, { className: "relative aspect-square w-full overflow-hidden
|
|
36
|
+
return (_jsxs(Box, { className: cn("relative aspect-square w-full overflow-hidden", PRODUCT_CARD_SWATCH_IMAGE_RADIUS), children: [_jsx(ProductCardLink, { blocked: availability === "blocked", className: "block size-full", href: productUrl, label: `View ${currentVariant.name}`, linkComponent: linkComponent, onActivate: onCardClick, prefetch: linkPrefetch, children: _jsx(ProductCardImage, { overrideImageUrl: overrideImageUrl, priority: priority }) }), dragging ? null : (_jsx(ProductCardSwatchAdd, { active: active, glyph: addGlyph, labels: labels, menuOpen: menuOpen, onAdd: onAdd })), dragging || processing !== undefined ? null : (_jsx(ProductCardQuickView, { align: "start", onQuickView: onQuickView }))] }));
|
|
36
37
|
};
|
|
37
38
|
/**
|
|
38
39
|
* ProductCardSwatch - the image-first tile for boards and canvases.
|
|
@@ -458,8 +458,8 @@ export const docsManifest = {
|
|
|
458
458
|
name: "ratio",
|
|
459
459
|
type: "number",
|
|
460
460
|
required: false,
|
|
461
|
-
defaultValue: "
|
|
462
|
-
description: "Desired width-to-height ratio, expressed as a single number (width / height).",
|
|
461
|
+
defaultValue: "1",
|
|
462
|
+
description: "Desired width-to-height ratio, expressed as a single number (width / height).\nSquare unless asked: a material is shown at 1:1, and only scenes and\nvideo take a wide ratio.",
|
|
463
463
|
},
|
|
464
464
|
],
|
|
465
465
|
},
|
|
@@ -472,8 +472,16 @@ export const docsManifest = {
|
|
|
472
472
|
bestPractices: [
|
|
473
473
|
"Give it exactly one child and let it fill - set the media to `size-full object-cover`; `AspectRatio` sizes to the available width and derives height from `ratio`.",
|
|
474
474
|
"Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.",
|
|
475
|
+
"Show a material, swatch or sample at the default 1:1 - never landscape. A sample is cut square, and a wide crop reads as room photography; set a wide `ratio` only for a scene, an interior or video.",
|
|
476
|
+
],
|
|
477
|
+
keywords: [
|
|
478
|
+
"material image",
|
|
479
|
+
"swatch image",
|
|
480
|
+
"sample image",
|
|
481
|
+
"thumbnail",
|
|
482
|
+
"square",
|
|
483
|
+
"image ratio",
|
|
475
484
|
],
|
|
476
|
-
keywords: [],
|
|
477
485
|
uses: [],
|
|
478
486
|
usedBy: [],
|
|
479
487
|
anatomy: {
|
|
@@ -6957,8 +6965,9 @@ export const docsManifest = {
|
|
|
6957
6965
|
"Use `as` to change the element (a tag); use `render` only to swap in a component (e.g. a router Link). Never `render={<Box as=…/>}` - pass `as` directly.",
|
|
6958
6966
|
"Set `edge` to frame photography with an inset hairline that stays above the media; do not fake it with a border or an inset shadow on the frame, which the media hides.",
|
|
6959
6967
|
"Reach for `raised` only when the frame sits on the same fill as its surface; in a dense row the edge already separates it.",
|
|
6968
|
+
"Frame a material, swatch or sample square: give the frame `aspect-square` (or wrap it in `AspectRatio`, which defaults to 1:1). Never crop a material landscape.",
|
|
6960
6969
|
],
|
|
6961
|
-
keywords: [],
|
|
6970
|
+
keywords: ["material image", "product image", "photo frame", "thumbnail"],
|
|
6962
6971
|
uses: [],
|
|
6963
6972
|
usedBy: ["editorial-card", "product-card"],
|
|
6964
6973
|
},
|
|
@@ -11704,8 +11713,15 @@ export const docsManifest = {
|
|
|
11704
11713
|
"Feed fills through `color`/`background`/`colors`, never a hex in `className`/`style` - the props drive the contrast-aware text colour, selection ring, and darkening overlay.",
|
|
11705
11714
|
"Reach for `Swatch` for a selectable colour/gradient/material chip or tile; a tiny non-interactive status marker is `Dot`, and a display-only status/category label is `Badge`.",
|
|
11706
11715
|
"For a gradient or image chip fill pass `light` - auto contrast detection (`isLightColor`) only reads solid colours, so overlaid icons/labels lose legibility without it.",
|
|
11716
|
+
"A material swatch is always square, as Swatch draws it; never stretch a tile into a landscape crop, and in a grid let the column set the size.",
|
|
11717
|
+
],
|
|
11718
|
+
keywords: [
|
|
11719
|
+
"material swatch",
|
|
11720
|
+
"material sample",
|
|
11721
|
+
"finish",
|
|
11722
|
+
"colourway",
|
|
11723
|
+
"colour chip",
|
|
11707
11724
|
],
|
|
11708
|
-
keywords: [],
|
|
11709
11725
|
uses: ["center", "flex", "icon"],
|
|
11710
11726
|
usedBy: [
|
|
11711
11727
|
"swatch-fan",
|
|
@@ -19657,10 +19673,17 @@ export const docsManifest = {
|
|
|
19657
19673
|
bestPractices: [
|
|
19658
19674
|
"In Next.js apps pass `renderImage` wrapping `next/image` and\n`linkAs={Link}` - the defaults are a plain `<img>` and `<a>`.",
|
|
19659
19675
|
"Cart UI renders only when a `renderAddToCart` slot is\nprovided; keep `dragAdapter` referentially stable (module-level constant).",
|
|
19676
|
+
"Use ProductCard for a material in a grid or list; its image is square, which is how a material is always shown. Don't rebuild the card with a landscape image.",
|
|
19660
19677
|
"Pass `renderImage` wrapping `next/image` in Next.js apps -\nthe default renderer is a plain `<img>` and skips Next's optimisation.",
|
|
19661
19678
|
"Keep `dragAdapter` referentially stable (a module-level\nconstant) - an inline factory remounts the image subtree and drops the\ndrag ref mid-drag.",
|
|
19662
19679
|
],
|
|
19663
|
-
keywords: [
|
|
19680
|
+
keywords: [
|
|
19681
|
+
"material card",
|
|
19682
|
+
"material tile",
|
|
19683
|
+
"sample card",
|
|
19684
|
+
"catalogue card",
|
|
19685
|
+
"product tile",
|
|
19686
|
+
],
|
|
19664
19687
|
uses: [
|
|
19665
19688
|
"badge",
|
|
19666
19689
|
"box",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instruments/materia-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Materia design system: React components, tokens and Tailwind v4 styles.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -898,6 +898,7 @@
|
|
|
898
898
|
"scripts": {
|
|
899
899
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/finish-dist.ts",
|
|
900
900
|
"deploy:figma-connect": "tsx figma/connect-templates.ts --publish",
|
|
901
|
+
"deploy:figma-descriptions": "tsx figma/descriptions.ts push",
|
|
901
902
|
"deploy:figma-storybook-links": "tsx figma/storybook-links.ts --publish",
|
|
902
903
|
"deploy:figma-tokens": "tsx figma/deploy-tokens.ts",
|
|
903
904
|
"generate:agent-docs": "node scripts/generate-agent-docs.ts",
|