@instruments/materia-ui 0.4.2 → 0.4.4

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.
@@ -3,7 +3,11 @@ import type { SwatchTileSizeProp, SwatchTileVariant, TileSwatchProps } from "./s
3
3
  type SizeTier = "tiny" | "small" | "standard";
4
4
  declare const isTileSizeScale: (size: SwatchTileSizeProp) => size is SwatchTileSize;
5
5
  declare const ringShadowMap: Record<SizeTier, string>;
6
- /** Radius of the padded ring and frame ring, concentric with the border. */
6
+ /**
7
+ * Radius of the padded ring and frame ring, concentric with the border. A bare
8
+ * tile has no border, so its rings sit on its edge and take whatever corner it
9
+ * has, including one set through `className`.
10
+ */
7
11
  declare const getInnerRingRadius: ({ compact, isCard, tier, }: {
8
12
  compact: boolean;
9
13
  isCard: boolean;
@@ -68,8 +68,12 @@ const pickRadius = (map, bare, tier, isCard, compactRadius) => {
68
68
  }
69
69
  return map[tier][compactRadius ? "compact" : "normal"];
70
70
  };
71
- /** Radius of the padded ring and frame ring, concentric with the border. */
72
- const getInnerRingRadius = ({ compact, isCard, tier, }) => pickRadius(innerRingRadiusMap, "rounded-md", tier, isCard, compact);
71
+ /**
72
+ * Radius of the padded ring and frame ring, concentric with the border. A bare
73
+ * tile has no border, so its rings sit on its edge and take whatever corner it
74
+ * has, including one set through `className`.
75
+ */
76
+ const getInnerRingRadius = ({ compact, isCard, tier, }) => pickRadius(innerRingRadiusMap, "rounded-[inherit]", tier, isCard, compact);
73
77
  /** Apply the tile's boolean, size and variant defaults. */
74
78
  const withTileDefaults = (input) => ({
75
79
  compact: input.compactRadius ?? false,
@@ -111,7 +115,7 @@ const buildTileClasses = (state, { borderColor, className, }) => {
111
115
  // Round the image element itself (not just clip via `overflow-hidden`):
112
116
  // WebKit clips in-flow replaced elements inconsistently, leaving square
113
117
  // corners that poke past the radius.
114
- pickRadius(innerImgRadiusMap, "[&_img]:rounded-md", tier, isCard, compact), isCard && (dotted ? "border-2" : containerBorderMap[tier]), isCard && "shadow-xs", !isCard && "bg-muted", isCard && "bg-card", isCard && (dotted ? "border-dotted" : "border-solid"), getBorderClasses(isCard, borderColor, selected), muted && "opacity-50 hover:opacity-100", isCard && "transition-border-shadow-opacity duration-quick ease-out", disabled && "cursor-not-allowed opacity-50", size === "fluid" && "w-full", isTileSizeScale(size) && swatchTileSizeMap[size], className);
118
+ pickRadius(innerImgRadiusMap, "[&_img]:rounded-[inherit]", tier, isCard, compact), isCard && (dotted ? "border-2" : containerBorderMap[tier]), isCard && "shadow-xs", !isCard && "bg-muted", isCard && "bg-card", isCard && (dotted ? "border-dotted" : "border-solid"), getBorderClasses(isCard, borderColor, selected), muted && "opacity-50 hover:opacity-100", isCard && "transition-border-shadow-opacity duration-quick ease-out", disabled && "cursor-not-allowed opacity-50", size === "fluid" && "w-full", isTileSizeScale(size) && swatchTileSizeMap[size], className);
115
119
  };
116
120
  const buildTileStyle = (state, { borderColor, color, style, }) => {
117
121
  const { size } = state;
@@ -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] }) })] })] }));
@@ -18761,6 +18761,12 @@ export const docsManifest = {
18761
18761
  required: false,
18762
18762
  description: 'Prioritise loading images (sets loading="eager" and adds preload link)',
18763
18763
  },
18764
+ {
18765
+ name: "productBrandLines",
18766
+ type: "1 | 2 | undefined",
18767
+ required: false,
18768
+ description: "How many lines the brand may take before it clamps; see `ProductCardDisplayOptions.brandLines`.",
18769
+ },
18764
18770
  {
18765
18771
  name: "productNameLines",
18766
18772
  type: "1 | 2 | undefined",
@@ -18981,6 +18987,12 @@ export const docsManifest = {
18981
18987
  required: false,
18982
18988
  description: 'Prioritise loading images (sets loading="eager" and adds preload link)',
18983
18989
  },
18990
+ {
18991
+ name: "productBrandLines",
18992
+ type: "1 | 2 | undefined",
18993
+ required: false,
18994
+ description: "How many lines the brand may take before it clamps; see `ProductCardDisplayOptions.brandLines`.",
18995
+ },
18984
18996
  {
18985
18997
  name: "productNameLines",
18986
18998
  type: "1 | 2 | undefined",
@@ -19470,6 +19482,18 @@ export const docsManifest = {
19470
19482
  type: "CSSProperties",
19471
19483
  required: false,
19472
19484
  },
19485
+ {
19486
+ name: "showAddToCart",
19487
+ type: "boolean",
19488
+ required: false,
19489
+ description: "Draw the add-to-cart control. True by default; pass the same value as the\ncard's `showAddToCart` so a grid without a cart keeps its height when the\nproducts arrive.",
19490
+ },
19491
+ {
19492
+ name: "showSwatches",
19493
+ type: "boolean",
19494
+ required: false,
19495
+ description: "Draw the row of colourway dots. True by default; pass the card's `showSwatches`.",
19496
+ },
19473
19497
  ],
19474
19498
  },
19475
19499
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materia-ui",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Materia design system: React components, tokens and Tailwind v4 styles.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {