Skip to contentVibraUI
Data display

Product art

A product's picture without a photograph: line art of the thing itself on a tile tinted from its name, for every product in the sample store.

Every product in db.products has a drawing that depicts it, found by a keyword in its own name — only the rain jacket and the denim jacket share one, because they are one silhouette — and each department carries a few staples besides (a phone, a laptop, a hoodie, a plant). The keyword that ends latest in the name wins, and of two that end on the same word the longer: a coffee table is a table, "Laptop Stand" and "TV Stand" are two different things, and a sleeping bag is not a tote bag. A name that names nothing falls back by its seed among its category's glyphs, so it still draws the same thing every time. The drawings live one department to a file in product-art/glyphs/, each glyph with the keywords that name it; add a product line by adding a glyph there. The tint is one of the four chart hues or the first chart grey, a tenth into the card plane (a sixth in the dark), so a preset that re-points the chart palette re-tints every tile; the ink leans a fifth towards the same hue. The name seeds the tint; tint paints one of the five instead, so a gallery that turns one product through its views gives each view a ground of its own without copying the map. The drawing is 72% of the tile's shorter side, centred, whatever the tile's shape: a 4:3 card, a 16:10 banner or a tile that fills a tall sheet (h-full) needs no sizing of its own — the drawing's box is 72% of the tile each way and the square drawing fits inside it. The stroke is set in screen pixels and steps up with the tile — 1.5 on a thumbnail, 1.75 from 10rem, 2 from 20rem — so a drawing never thickens into an icon or thins into a hairline as it scales. Decoration by default (aria-hidden), because the product's name beside it says what it is; pass label where the picture stands alone.

Install

npx shadcn@latest add @vibra/product-art

Needs the @vibra registry in your components.json — set it up once.

Examples

Every glyph

The whole set, department by department, each on the tint its name seeds.

Props

PropTypeDefaultDescription
namestring—The product's name: picks the glyph and seeds the tint.
categorystring—The product's department — "Apparel", "Home", "Kitchen"… — read only for a name that names no glyph.
glyphProductGlyph—Draws this glyph whatever the name reads as.
tint"blue" | "teal" | "amber" | "violet" | "stone"the one the name seedsPaints this tint instead of the name's own — one of PRODUCT_ART_TINTS.
labelstring—Makes the tile a role="img" with this name. Without it the tile is aria-hidden.
productArtMatch(name: string) => { glyph: ProductGlyph; keyword: string } | undefined—The glyph a name names, and the keyword in the name that named it; undefined when none does.
productArtFor(name: string, category?: string) => ProductGlyph—The glyph a name draws: the one it names, else one picked by its seed within its department.
PRODUCT_GLYPH_DEPARTMENTSreadonly { name: string; glyphs: readonly ProductGlyph[] }[]—Electronics, Office, Furniture, Apparel, Home and Outdoor, each with the glyphs filed under it.

Dependencies

Registry

Source

components/ui/product-art/index.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

import { APPAREL } from "./glyphs/apparel"
import { ELECTRONICS } from "./glyphs/electronics"
import { FURNITURE } from "./glyphs/furniture"
import { HOME } from "./glyphs/home"
import { OFFICE } from "./glyphs/office"
import { OUTDOOR } from "./glyphs/outdoor"
import type { GlyphSpec } from "./glyphs/shared"

/**
 * A store's products, drawn rather than photographed: a product card needs a
 * picture, and a photograph of a real product is somebody's product. Line art
 * in the ink on a tinted tile — the same register as the Lorelei faces people
 * wear — so a grid of products sits inside an achromatic page without
 * shouting. The set is the sample store's own: every product in
 * `db.products` has a drawing that depicts it, filed under its department,
 * with a few staples of each department besides.
 */
const DEPARTMENTS = [
  { name: "Electronics", words: ["electronics", "tech", "audio", "gadgets", "computing", "computers"], glyphs: ELECTRONICS },
  { name: "Office", words: ["office", "stationery", "workspace"], glyphs: OFFICE },
  { name: "Furniture", words: ["furniture"], glyphs: FURNITURE },
  { name: "Apparel", words: ["apparel", "clothing", "fashion", "footwear", "accessories"], glyphs: APPAREL },
  { name: "Home", words: ["home", "kitchen", "living", "decor", "dining", "bath"], glyphs: HOME },
  { name: "Outdoor", words: ["outdoor", "outdoors", "camping", "hiking", "sports", "travel"], glyphs: OUTDOOR },
] as const

export type ProductGlyph =
  | keyof typeof ELECTRONICS
  | keyof typeof OFFICE
  | keyof typeof FURNITURE
  | keyof typeof APPAREL
  | keyof typeof HOME
  | keyof typeof OUTDOOR

const GLYPHS: Record<ProductGlyph, GlyphSpec> = { ...ELECTRONICS, ...OFFICE, ...FURNITURE, ...APPAREL, ...HOME, ...OUTDOOR }

/** Every glyph, department by department. */
export const PRODUCT_GLYPHS = Object.keys(GLYPHS) as ProductGlyph[]

/** The departments and the glyphs filed under each, in order: what a catalogue of the set shows. */
export const PRODUCT_GLYPH_DEPARTMENTS: readonly { name: string; glyphs: readonly ProductGlyph[] }[] = DEPARTMENTS.map(
  (department) => ({ name: department.name, glyphs: Object.keys(department.glyphs) as ProductGlyph[] })
)

/**
 * The tile's tints, each a chart hue — or the first chart grey — mixed a
 * tenth into the card plane, so a preset that re-points the chart palette
 * re-tints every tile with it. Non-semantic hues only: a tile is never green
 * for "in stock" or red for "sale".
 */
const TINTS = {
  blue: "var(--chart-2)",
  teal: "var(--chart-3)",
  amber: "var(--chart-4)",
  violet: "var(--chart-5)",
  stone: "var(--chart-1)",
} as const

export type ProductArtTint = keyof typeof TINTS

export const PRODUCT_ART_TINTS = Object.keys(TINTS) as ProductArtTint[]

/** `"Cotton T-Shirt Pack"` → `["cotton", "t", "shirt", "pack"]`. */
function words(value: string): string[] {
  return value.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
}

/** A word of a name reads as a keyword's word in the singular or with a plural ending. */
function reads(word: string, keyword: string): boolean {
  return word === keyword || word === `${keyword}s` || word === `${keyword}es`
}

const KEYWORDS = PRODUCT_GLYPHS.flatMap((glyph) =>
  GLYPHS[glyph].keywords.map((keyword) => ({ glyph, keyword, parts: words(keyword) }))
)

/** Which glyph a name names, and the keyword it was named by. */
export type ProductArtMatch = { glyph: ProductGlyph; keyword: string }

/**
 * The glyph a product's name names, or undefined when no keyword is in it.
 * English puts the thing last — a coffee table is a table, a laptop stand is
 * a stand — so the keyword that ends latest in the name wins, and of two that
 * end on the same word, the longer: "Sleeping Bag" is a sleeping bag before
 * it is a bag, and "TV Stand" and "Laptop Stand" are two different things.
 */
export function productArtMatch(name: string): ProductArtMatch | undefined {
  const named = words(name)
  let best: (ProductArtMatch & { end: number; length: number }) | undefined
  for (const { glyph, keyword, parts } of KEYWORDS) {
    for (let start = 0; start + parts.length <= named.length; start++) {
      if (!parts.every((part, index) => reads(named[start + index], part))) continue
      const end = start + parts.length - 1
      if (!best || end > best.end || (end === best.end && parts.length > best.length)) {
        best = { glyph, keyword, end, length: parts.length }
      }
    }
  }
  return best ? { glyph: best.glyph, keyword: best.keyword } : undefined
}

/** FNV-1a: a string to a 32-bit number, so a name alone fixes a pick. */
function hash(value: string): number {
  let h = 2166136261
  for (let i = 0; i < value.length; i++) {
    h ^= value.charCodeAt(i)
    h = Math.imul(h, 16777619)
  }
  return h >>> 0
}

const seedOf = (name: string) => words(name).join(" ")

/**
 * The glyph a product is drawn as: the one its name names. A name that names
 * none — never one of the sample store's, which the set is drawn to cover —
 * falls back by its seed among its department's glyphs when `category` names
 * one ("Apparel", "Kitchen"), else among them all, so the same name draws the
 * same thing every time.
 */
export function productArtFor(name: string, category?: string): ProductGlyph {
  const named = productArtMatch(name)
  if (named) return named.glyph
  const said = category ? words(category) : []
  const department = DEPARTMENTS.find((entry) => entry.words.some((word) => said.includes(word)))
  const pool = department ? (Object.keys(department.glyphs) as ProductGlyph[]) : PRODUCT_GLYPHS
  return pool[hash(seedOf(name)) % pool.length]
}

/** The tint a name's tile takes: seeded apart from its glyph, so two mugs need not match. */
export function productArtTint(name: string): ProductArtTint {
  return PRODUCT_ART_TINTS[hash(`${seedOf(name)}#tile`) % PRODUCT_ART_TINTS.length]
}

export type ProductArtProps = Omit<React.ComponentProps<"div">, "children"> & {
  /** The product's name: it picks the glyph and seeds the tint. */
  name: string
  /** The product's department, read only for a name that names no glyph: "Apparel" keeps a scarf among the clothes. */
  category?: string
  /** Draws this glyph whatever the name reads as. */
  glyph?: ProductGlyph
  /** Paints this tint whatever the name seeds: a gallery that turns one product through its views gives each view a ground of its own. */
  tint?: ProductArtTint
  /** Names the picture. Without one it is decoration, and the product's name beside it carries the meaning. */
  label?: string
}

/**
 * A product's picture: its glyph on a square tile tinted from its name. Size
 * it with the tile — `className="w-40"`, or let a grid cell set the width —
 * and the drawing scales with it: 72% of the tile's shorter side, centred,
 * so a 4:3 or a tall tile holds its drawing as a square one does. The stroke
 * does not scale with it: it is set in screen pixels and steps up only as
 * the tile widens, so a 40px thumbnail reads as a crisp icon and a 320px card
 * as a fine drawing, never a thick one.
 */
function ProductArt({ className, style, name, category, glyph, tint, label, ...props }: ProductArtProps) {
  const drawn = glyph ?? productArtFor(name, category)
  // Guarded rather than trusted: `tint` is typed to the five, but an unchecked
  // string from JavaScript would paint no ground at all.
  const painted = tint !== undefined && PRODUCT_ART_TINTS.includes(tint) ? tint : productArtTint(name)

  return (
    <div
      data-slot="product-art"
      data-glyph={drawn}
      data-tint={painted}
      role={label ? "img" : undefined}
      aria-label={label}
      aria-hidden={label ? undefined : true}
      className={cn(
        // One row and one column, each the tile's own size: the drawing's
        // percentages resolve against the tile, never against the drawing.
        "@container/product-art relative grid aspect-square w-full grid-cols-1 grid-rows-1 place-items-center overflow-hidden rounded-lg",
        // The tint, a tenth into the card plane — a little more in the dark,
        // where the same share of a hue reads fainter against a dark card.
        "[--product-art-mix:10%] dark:[--product-art-mix:16%] bg-[color-mix(in_oklab,var(--product-art-tint)_var(--product-art-mix),var(--card))]",
        // The ink leans a fifth towards the tile's hue, so the drawing and its
        // ground read as one object rather than a black icon on a colour.
        "text-[color-mix(in_oklab,var(--foreground)_80%,var(--product-art-tint))]",
        className
      )}
      style={{ "--product-art-tint": TINTS[painted], ...style } as React.CSSProperties}
      {...props}
    >
      <svg
        viewBox="0 0 64 64"
        fill="none"
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        aria-hidden="true"
        focusable="false"
        className={cn(
          // A box 72% of the tile each way; the square drawing fits inside it
          // (the viewBox's own xMidYMid meet), so it is 72% of the shorter side.
          "size-[72%] overflow-visible [&_*]:[stroke-width:var(--product-art-stroke)] [&_*]:[vector-effect:non-scaling-stroke]",
          // Screen-pixel strokes, stepped by the tile's width — a container
          // query, so it is the tile that decides and not the viewport: 1.5 on
          // a thumbnail, 1.75 on a card, 2 on a hero.
          "[--product-art-stroke:1.5px] @min-[10rem]/product-art:[--product-art-stroke:1.75px] @min-[20rem]/product-art:[--product-art-stroke:2px]"
        )}
      >
        {GLYPHS[drawn].drawing}
      </svg>
    </div>
  )
}

export { ProductArt }
components/ui/product-art/glyphs/shared.tsx
import * as React from "react"

/**
 * The drawing kit every department file draws with, on one 64-unit grid.
 *
 * Every object stands on the same floor — its base near y 53, a soft shadow
 * at y 55 — and a tall one and a wide one fill about the same area, so a row
 * of mixed products reads as one shelf. Outlines are the ink; seams, grain,
 * keys and slots are the same ink at half strength, the two-weight line that
 * keeps a drawing from reading as an icon. Strokes are sized by the tile
 * (`--product-art-stroke`, in screen pixels), never in a drawing.
 */

/** One glyph: the words that name it, and the drawing. */
export type GlyphSpec = {
  /**
   * The words a product name uses for this thing, written as a name writes
   * them — "office chair", "t-shirt", "sticky note". Singular: a plural
   * ending (-s, -es) is read off the name before it is matched.
   */
  keywords: readonly string[]
  drawing: React.ReactNode
}

/** The ink at half strength: the inner lines of a drawing. */
export function Detail({ children }: { children: React.ReactNode }) {
  return <g opacity={0.45}>{children}</g>
}

/** The shadow an object casts on the floor it stands on. */
export function Floor({ width, y = 55, cx = 32 }: { width: number; y?: number; cx?: number }) {
  return <ellipse cx={cx} cy={y} rx={width / 2} ry="1.5" fill="currentColor" stroke="none" opacity={0.08} />
}

/** A small solid point: a button, a rivet, a lens's centre, a status light. */
export function Dot({ cx, cy, r = 1 }: { cx: number; cy: number; r?: number }) {
  return <circle cx={cx} cy={cy} r={r} fill="currentColor" stroke="none" />
}
components/ui/product-art/glyphs/electronics.tsx
import * as React from "react"

import { Detail, Dot, Floor, type GlyphSpec } from "./shared"

/** Electronics: the desk's peripherals, and the phone, laptop, watch and camera a storefront leads with. */
export const ELECTRONICS = {
  mouse: {
    keywords: ["mouse", "mice"],
    drawing: (
      <>
        <Floor width={24} />
        <path d="M32 12c-6.1 0-11 4.9-11 11v17c0 7.2 4.9 13 11 13s11-5.8 11-13V23c0-6.1-4.9-11-11-11Z" />
        <rect x="30.5" y="17" width="3" height="7" rx="1.5" />
        <Detail>
          <path d="M32 12v5M32 24v6.8M21.3 29c3.1 1.2 6.7 1.8 10.7 1.8s7.6-.6 10.7-1.8" />
        </Detail>
      </>
    ),
  },
  keyboard: {
    keywords: ["keyboard", "keypad"],
    drawing: (
      <g transform="translate(0 5)">
        <Floor width={50} y={50} />
        <rect x="6" y="26" width="52" height="21" rx="4" />
        <path d="M32 26v-5c0-2.8 2.2-5 5-5h3c2.8 0 5-2.2 5-5" />
        <Detail>
          <path d="M11 31.5h2M16.5 31.5h2M22 31.5h2M27.5 31.5h2M33 31.5h2M38.5 31.5h2M44 31.5h2M49.5 31.5h3.5M11 36.5h3.5M18 36.5h2M23.5 36.5h2M29 36.5h2M34.5 36.5h2M40 36.5h2M45.5 36.5h2M51 36.5h2M11 41.5h4M20 41.5h24M48 41.5h5" />
        </Detail>
      </g>
    ),
  },
  hub: {
    keywords: ["hub", "dock", "docking station"],
    drawing: (
      <>
        <Floor width={38} cx={37} />
        <rect x="20" y="36" width="36" height="17" rx="4" />
        <rect x="25" y="42" width="8" height="5" rx="1" />
        <rect x="36" y="42" width="8" height="5" rx="1" />
        <rect x="47" y="43" width="5" height="3" rx="1.5" />
        <path d="M20 44.5h-3.5a4.5 4.5 0 0 1-4.5-4.5v-9" />
        <rect x="8.5" y="21" width="7" height="10" rx="2" />
        <path d="M10 21v-3.5c0-.8.7-1.5 1.5-1.5h1c.8 0 1.5.7 1.5 1.5V21" />
        <Detail>
          <path d="M27 44.5h4M38 44.5h4M24 49.5h28" />
        </Detail>
      </>
    ),
  },
  monitor: {
    keywords: ["monitor", "computer monitor"],
    drawing: (
      <>
        <Floor width={26} />
        <rect x="7" y="9" width="50" height="31" rx="2.5" />
        <path d="M29 40l-1 8M35 40l1 8M21 53c0-2.8 4.9-5 11-5s11 2.2 11 5Z" />
        <Detail>
          <rect x="10" y="12" width="44" height="25" rx="1" />
        </Detail>
      </>
    ),
  },
  headphones: {
    keywords: ["headphones", "headphone", "headset", "earbuds", "earphones"],
    drawing: (
      <g transform="translate(0 2)">
        <Floor width={44} y={53} />
        <path d="M14.5 38v-5c0-9.7 7.8-17.5 17.5-17.5S49.5 23.3 49.5 33v5" />
        <rect x="10" y="35" width="10" height="16" rx="4.5" />
        <rect x="44" y="35" width="10" height="16" rx="4.5" />
        <Detail>
          <path d="M18.5 34.5V33c0-7.5 6-13.5 13.5-13.5S45.5 25.5 45.5 33v1.5" />
          <rect x="20" y="37.5" width="3" height="11" rx="1.5" />
          <rect x="41" y="37.5" width="3" height="11" rx="1.5" />
        </Detail>
      </g>
    ),
  },
  webcam: {
    keywords: ["webcam", "web camera"],
    drawing: (
      <>
        <Floor width={26} />
        <rect x="13" y="15" width="38" height="18" rx="9" />
        <circle cx="32" cy="24" r="6" />
        <path d="M30.5 33v6.5M33.5 33v6.5M24 53l4-9h8l4 9Z" />
        <circle cx="32" cy="41.5" r="2.5" />
        <Dot cx={44} cy={24} r={1.1} />
        <Detail>
          <circle cx="32" cy="24" r="2.6" />
          <path d="M18.5 22v4M21 22v4" />
        </Detail>
      </>
    ),
  },
  ssd: {
    keywords: ["ssd", "hard drive", "external drive", "portable drive"],
    drawing: (
      <>
        <Floor width={46} cx={33} />
        <path d="M11 38 16.5 26h27l5.5 12" />
        <rect x="11" y="38" width="38" height="9" rx="2.5" />
        <rect x="38.5" y="41" width="6" height="3" rx="1.5" />
        <path d="M41.5 44v5.5c0 1.9 1.6 3.5 3.5 3.5h4" />
        <rect x="49" y="50" width="7" height="6" rx="1.5" />
        <Dot cx={16} cy={42.5} r={1} />
        <Detail>
          <path d="M20 29h20l3 6H17Z" />
        </Detail>
      </>
    ),
  },
  speaker: {
    keywords: ["speaker", "soundbar", "subwoofer"],
    drawing: (
      <>
        <Floor width={26} />
        <rect x="18.5" y="10" width="27" height="43" rx="7" />
        <circle cx="32" cy="37" r="8.5" />
        <circle cx="32" cy="20.5" r="4" />
        <Dot cx={32} cy={20.5} r={1} />
        <Detail>
          <circle cx="32" cy="37" r="3.5" />
          <path d="M30.5 49.5h3" />
        </Detail>
      </>
    ),
  },
  "power-strip": {
    keywords: ["power strip", "surge protector", "extension lead", "power bar"],
    drawing: (
      <>
        <Floor width={46} />
        <rect x="10" y="20" width="46" height="15" rx="4" />
        <rect x="14" y="23.5" width="8" height="8" rx="2" />
        <rect x="25" y="23.5" width="8" height="8" rx="2" />
        <rect x="36" y="23.5" width="8" height="8" rx="2" />
        <rect x="47.5" y="24" width="4.5" height="7" rx="1.5" />
        <path d="M10 27.5H8.5A3.5 3.5 0 0 0 5 31v10.5A3.5 3.5 0 0 0 8.5 45H15M23 46.5h4M23 50h4" />
        <rect x="15" y="43" width="8" height="10" rx="2" />
        <Detail>
          <path d="M17 26v3M19 26v3M28 26v3M30 26v3M39 26v3M41 26v3" />
        </Detail>
      </>
    ),
  },
  "laptop-stand": {
    keywords: ["laptop stand", "laptop riser", "notebook stand"],
    drawing: (
      <>
        <Floor width={42} />
        <rect x="16" y="7" width="32" height="22" rx="2" />
        <path d="M11 29h42v1.5c0 1.4-1.1 2.5-2.5 2.5h-37c-1.4 0-2.5-1.1-2.5-2.5Z" />
        <path d="M12.5 35.5h39M17.5 35.5l6 15.5M46.5 35.5l-6 15.5M19 53h26" />
        <Detail>
          <rect x="19" y="10" width="26" height="16" rx="0.8" />
        </Detail>
      </>
    ),
  },
  phone: {
    keywords: ["phone", "smartphone", "tablet", "handset"],
    drawing: (
      <>
        <Floor width={24} />
        <rect x="19.5" y="7.5" width="25" height="46" rx="5.5" />
        <path d="M29.5 13.5h5" />
        <Detail>
          <rect x="22.5" y="11" width="19" height="39" rx="3" />
          <path d="M29 47h6" />
        </Detail>
      </>
    ),
  },
  laptop: {
    keywords: ["laptop", "computer", "chromebook"],
    drawing: (
      <g transform="translate(0 6)">
        <Floor width={48} y={49} />
        <rect x="13.5" y="11.5" width="37" height="28" rx="2.5" />
        <path d="M7 39.5h50v2c0 2.2-1.8 4-4 4H11c-2.2 0-4-1.8-4-4Z" />
        <Detail>
          <rect x="17" y="15" width="30" height="21" rx="1" />
          <path d="M27.5 39.5v.7c0 .8.7 1.5 1.5 1.5h6c.8 0 1.5-.7 1.5-1.5v-.7" />
        </Detail>
      </g>
    ),
  },
  watch: {
    keywords: ["watch", "smartwatch"],
    drawing: (
      <g transform="translate(0 -1.5)">
        <Floor width={14} y={56.5} />
        <path d="m26.5 22 1.3-11.4c.1-.9.8-1.6 1.7-1.6h5c.9 0 1.6.7 1.7 1.6L37.5 22M26.5 42l1.3 11.4c.1.9.8 1.6 1.7 1.6h5c.9 0 1.6-.7 1.7-1.6L37.5 42" />
        <circle cx="32" cy="32" r="11.5" />
        <path d="M43.4 29.8h1.8c.6 0 1 .4 1 1v2.4c0 .6-.4 1-1 1h-1.8M32 32v-5.5M32 32l3.5 2" />
        <Dot cx={32} cy={32} r={1} />
        <Detail>
          <circle cx="32" cy="32" r="8.8" />
          <path d="M32 47.6v.8M32 50.6v.8" />
        </Detail>
      </g>
    ),
  },
  camera: {
    keywords: ["camera", "camcorder"],
    drawing: (
      <g transform="translate(0 3)">
        <Floor width={44} y={52} />
        <rect x="9" y="21" width="46" height="29" rx="5" />
        <path d="m22.5 21 2.5-5c.4-.8 1.2-1.3 2-1.3h10c.8 0 1.6.5 2 1.3l2.5 5M45 21v-2.5c0-.6.4-1 1-1h4c.6 0 1 .4 1 1V21" />
        <circle cx="32" cy="35.5" r="10" />
        <Detail>
          <circle cx="32" cy="35.5" r="6" />
          <path d="M28.5 32.5c.8-.9 1.8-1.5 3-1.7" />
          <rect x="13" y="25" width="6" height="3.5" rx="1" />
        </Detail>
      </g>
    ),
  },
} as const satisfies Record<string, GlyphSpec>
components/ui/product-art/glyphs/office.tsx
import * as React from "react"

import { Detail, Dot, Floor, type GlyphSpec } from "./shared"

/** One ballpoint, drawn upright; the pen set leans two of them together. */
function Ballpoint() {
  return (
    <>
      <rect x="30.5" y="6" width="3" height="4" rx="1" />
      <rect x="29" y="10" width="6" height="29" rx="2" />
      <path d="M29.5 39h5l-.6 6.5h-3.8ZM30.3 45.5 32 50.5l1.7-5M35 12.5v12.5c0 .9-.6 1.6-1.4 1.8" />
      <Detail>
        <path d="M30 41.5h4M30 43.5h4" />
      </Detail>
    </>
  )
}

/** Office: the desk, what sits on it, and what keeps it in order. */
export const OFFICE = {
  "office-chair": {
    keywords: ["office chair", "desk chair", "task chair", "gaming chair"],
    drawing: (
      <>
        <Floor width={36} />
        <rect x="21" y="8" width="22" height="21" rx="7" />
        <rect x="16" y="31.5" width="32" height="6" rx="3" />
        <path d="M32 29v2.5M13 26.5h6.5M44.5 26.5H51M16.5 26.5v5M47.5 26.5v5M32 45.5 17 49.5M32 45.5l15 4M32 45.5v4" />
        <rect x="30.5" y="37.5" width="3" height="8" rx="1" />
        <circle cx="16" cy="51.3" r="1.8" />
        <circle cx="48" cy="51.3" r="1.8" />
        <circle cx="32" cy="51.3" r="1.8" />
        <Detail>
          <rect x="24.5" y="11.5" width="15" height="14" rx="4.5" />
        </Detail>
      </>
    ),
  },
  "standing-desk": {
    keywords: ["standing desk", "sit-stand desk", "desk"],
    drawing: (
      <>
        <Floor width={48} />
        <rect x="21" y="7" width="22" height="14" rx="1.5" />
        <path d="M32 21v3.5M27.5 24.5h9" />
        <rect x="8" y="24.5" width="48" height="4" rx="1.5" />
        <rect x="38" y="28.5" width="6" height="2.5" rx="1" />
        <rect x="14" y="28.5" width="3" height="11" />
        <rect x="47" y="28.5" width="3" height="11" />
        <rect x="13" y="39.5" width="5" height="12" rx="1" />
        <rect x="46" y="39.5" width="5" height="12" rx="1" />
        <rect x="9" y="51.5" width="13" height="2.5" rx="1.25" />
        <rect x="42" y="51.5" width="13" height="2.5" rx="1.25" />
        <Detail>
          <rect x="23.5" y="9.5" width="17" height="9" rx="0.5" />
        </Detail>
      </>
    ),
  },
  organizer: {
    keywords: ["desk organizer", "organizer", "organiser", "desk tidy", "caddy"],
    drawing: (
      <>
        <Floor width={36} />
        <rect x="14" y="25" width="16" height="28" rx="1.5" />
        <rect x="30" y="35" width="20" height="18" rx="1.5" />
        <path d="M18.5 25V13.5l1.5-3.5 1.5 3.5V25M24 25V15.5l1.5-3.5 1.5 3.5V25" />
        <rect x="33" y="17" width="5.5" height="18" rx="0.8" />
        <rect x="41" y="27.5" width="6.5" height="7.5" rx="0.8" />
        <Detail>
          <path d="M33 20.5h2M33 24h2.5M33 27.5h2M33 31h2.5M18.5 13.5h3M24 15.5h3M43 30.5h2.5M34 44h12" />
        </Detail>
      </>
    ),
  },
  whiteboard: {
    keywords: ["whiteboard", "dry-erase board", "marker board"],
    drawing: (
      <>
        <Floor width={44} />
        <rect x="13" y="9" width="38" height="27" rx="1.5" />
        <rect x="15" y="36" width="34" height="2.5" rx="1" />
        <path d="M11 11v40M53 11v40M5.5 51h11M47.5 51h11M11 22h2M51 22h2" />
        <rect x="38" y="33.5" width="7" height="2.5" rx="1" />
        <Detail>
          <path d="M17 30.5l6-6 5 3 7-8 5 4M17 14.5h11M17 18h7" />
        </Detail>
      </>
    ),
  },
  "sticky-notes": {
    keywords: ["sticky note", "memo pad"],
    drawing: (
      <>
        <Floor width={32} />
        <path d="M18 17h28v18L36 45H18ZM46 35h-7.5c-1.4 0-2.5 1.1-2.5 2.5V45M18 45v5c0 .8.7 1.5 1.5 1.5h25c.8 0 1.5-.7 1.5-1.5V39" />
        <Detail>
          <path d="M23 24h14M23 29h10M23 34h8M18 48.5h28" />
        </Detail>
      </>
    ),
  },
  pen: {
    keywords: ["pen", "ballpoint", "rollerball"],
    drawing: (
      <>
        <Floor width={34} />
        <g transform="rotate(24 25 30) translate(-7 0)">
          <Ballpoint />
        </g>
        <g transform="rotate(24 39 30) translate(7 0)">
          <Ballpoint />
        </g>
      </>
    ),
  },
  shredder: {
    keywords: ["shredder"],
    drawing: (
      <>
        <Floor width={32} />
        <path d="M24 23.5V11h16v12.5" />
        <rect x="14" y="20" width="36" height="9" rx="2.5" />
        <path d="M20 24.5h24M16 29h32l-2 22c-.1 1.1-1 2-2.1 2H20.1c-1.1 0-2-.9-2.1-2Z" />
        <Dot cx={46} cy={24.5} r={1.2} />
        <Detail>
          <path d="M27.5 15h9M27.5 18.5h6" />
          <rect x="22" y="34" width="20" height="12" rx="2" />
          <path d="M26 36.5v7M29.5 36.5v7M33 36.5v7M36.5 36.5v7" />
        </Detail>
      </>
    ),
  },
  "filing-cabinet": {
    keywords: ["filing cabinet", "file cabinet"],
    drawing: (
      <>
        <Floor width={28} />
        <rect x="19" y="8" width="26" height="45" rx="2" />
        <path d="M19 19.3h26M19 30.5h26M19 41.8h26M29 15.8h6M29 27h6M29 38.3h6M29 49.5h6" />
        <Detail>
          <path d="M28.5 10.5h7v2.5h-7ZM28.5 21.8h7v2.5h-7ZM28.5 33h7v2.5h-7ZM28.5 44.3h7v2.5h-7Z" />
        </Detail>
      </>
    ),
  },
  "desk-lamp": {
    keywords: ["desk lamp", "task lamp", "reading lamp"],
    drawing: (
      <>
        <Floor width={22} cx={26} />
        <path d="M16 53c0-2.5 3.6-4.5 8-4.5s8 2 8 4.5ZM24 48.5l11.5-19M35.2 25.7 22.5 16.3" />
        <circle cx="36.8" cy="27.3" r="2.2" />
        <path d="M12.3 18.5 24.2 12c.9-.5 2-.2 2.5.7l.8 1.5c.5.9.2 2-.7 2.5L14.9 23.2c-.9.5-2 .2-2.5-.7l-.8-1.5c-.5-.9-.2-2 .7-2.5Z" />
        <Detail>
          <path d="M26.2 48 37.3 29.6M33.8 24.3 23.9 17M13.5 25.5l-2 3M17.5 24l-1 3.5M21 22l-.2 3.5" />
        </Detail>
      </>
    ),
  },
  "cable-tray": {
    keywords: ["cable management", "cable tray", "cable organizer", "cable organiser"],
    drawing: (
      <>
        <Floor width={40} />
        <rect x="6" y="12" width="52" height="4.5" rx="1.5" />
        <path d="M17 16.5v7M47 16.5v7M13 23.5h38V35c0 2.2-1.8 4-4 4H17c-2.2 0-4-1.8-4-4ZM20 39c0 8.5 10 8.5 10 0M38 39c0 4.5 4 6 4 9.5M42.5 52.5v2M45 52.5v2" />
        <rect x="39.5" y="47.5" width="7.5" height="5" rx="1.2" />
        <Detail>
          <path d="M19 23.5V39M25 23.5V39M31 23.5V39M37 23.5V39M43 23.5V39M13 31h38" />
        </Detail>
      </>
    ),
  },
  notebook: {
    keywords: ["notebook", "journal", "planner", "sketchbook", "diary", "notepad"],
    drawing: (
      <>
        <Floor width={32} />
        <rect x="17" y="10" width="30" height="43" rx="3" />
        <path d="M40.5 10v43" />
        <Detail>
          <path d="M22.5 10v43M47 13.5h1.5c.8 0 1.5.7 1.5 1.5v34c0 .8-.7 1.5-1.5 1.5H47" />
          <rect x="27" y="18" width="9" height="5" rx="1" />
        </Detail>
      </>
    ),
  },
} as const satisfies Record<string, GlyphSpec>
components/ui/product-art/glyphs/furniture.tsx
import * as React from "react"

import { Detail, Dot, Floor, type GlyphSpec } from "./shared"

/** One bar stool, centred on x 22; the set stands two of them side by side. */
function BarStool() {
  return (
    <>
      <rect x="14" y="16" width="16" height="4.5" rx="2.25" />
      <path d="M16.5 20.5 13.5 53M27.5 20.5l3 32.5M15 40.5h14" />
      <Detail>
        <path d="M20 20.5 19 53M24 20.5l1 32.5" />
      </Detail>
    </>
  )
}

/** Furniture: the pieces a room is furnished with, from the shelf to the screen. */
export const FURNITURE = {
  bookshelf: {
    keywords: ["bookshelf", "bookshelves", "bookcase", "book shelf", "shelving unit"],
    drawing: (
      <>
        <Floor width={32} />
        <rect x="17" y="6" width="30" height="47" rx="1.5" />
        <path d="M17 15.4h30M17 24.8h30M17 34.2h30M17 43.6h30M20.5 15.4V9h3v6.4M23.5 15.4V10h3v5.4M27.5 15.4l2.2-5.6 2.8 1.1-1.8 4.5M20.5 24.8v-6.5h2.6v6.5M23.1 24.8v-5.5h2.6v5.5M38 24.8v-4h6v4M34 34.2v-6.4h2.8v6.4M36.8 34.2v-5.6h2.8v5.6M39.6 34.2v-6.4h2.8v6.4M20.5 43.6v-2.2h9v2.2M21.5 41.4v-2h7v2M31 53v-7h3v7M34 53v-6h3v6" />
      </>
    ),
  },
  armchair: {
    keywords: ["armchair", "accent chair", "lounge chair", "wingback", "recliner"],
    drawing: (
      <>
        <Floor width={40} />
        <path d="M19.5 33V16.5c0-4.1 3.4-7.5 7.5-7.5h10c4.1 0 7.5 3.4 7.5 7.5V33" />
        <rect x="11" y="26" width="9" height="18" rx="4.5" />
        <rect x="44" y="26" width="9" height="18" rx="4.5" />
        <rect x="20" y="33" width="24" height="6" rx="2" />
        <path d="M20 44h24M15 44l-1.5 9M49 44l1.5 9M20 39v5M44 39v5" />
        <Detail>
          <path d="M23 30.5V18.5c0-2.8 2.2-5 5-5h8c2.8 0 5 2.2 5 5v12" />
        </Detail>
      </>
    ),
  },
  "coffee-table": {
    keywords: ["coffee table", "side table", "end table"],
    drawing: (
      <>
        <Floor width={52} />
        <rect x="7" y="33" width="50" height="4" rx="1.5" />
        <path d="M11 37v16M53 37v16M11 45h42M17 33v-4c0-.6.4-1 1-1h4c.6 0 1 .4 1 1v4M23 29.5h1a1.5 1.5 0 0 1 0 3h-1" />
        <rect x="35" y="29.5" width="13" height="3.5" rx="0.6" />
        <rect x="36.5" y="26.5" width="9" height="3" rx="0.6" />
        <Detail>
          <path d="M17 45v-3.5h11V45M19 41.5v-2h7v2" />
        </Detail>
      </>
    ),
  },
  ottoman: {
    keywords: ["ottoman", "pouf", "footstool"],
    drawing: (
      <>
        <Floor width={40} />
        <rect x="13" y="22" width="38" height="27" rx="4" />
        <path d="M13 30h38M17.5 49v4M46.5 49v4" />
        <Dot cx={22} cy={39} r={0.9} />
        <Dot cx={32} cy={39} r={0.9} />
        <Dot cx={42} cy={39} r={0.9} />
        <Detail>
          <path d="M17 34.5l5 4.5 5-4.5 5 4.5 5-4.5 5 4.5 5-4.5M17 43.5l5-4.5 5 4.5 5-4.5 5 4.5 5-4.5 5 4.5M13 26h38" />
        </Detail>
      </>
    ),
  },
  "wall-shelf": {
    keywords: ["wall shelf", "floating shelf", "shelf", "shelves"],
    drawing: (
      <>
        <Floor width={46} y={43} />
        <rect x="8" y="35" width="48" height="4.5" rx="1.5" />
        <path d="M13 35V22.5h4.2V35M17.2 35V25h4.2v10M21.6 35 26 24.4l3.6 1.5-4 9.1M37 35l-1.2-6.5h8.4L43 35ZM40 28.5c-.8-3.4-3.3-5.6-6.7-5.8.4 3.3 3 5.6 6.7 5.8ZM40 28.5c1-3.8 3.7-5.9 7.2-5.8-.7 3.4-3.4 5.6-7.2 5.8ZM51 35c-2 0-3-1.5-3-3.8 0-2.6 1.6-3.6 1.6-6.2h2.8c0 2.6 1.6 3.6 1.6 6.2 0 2.3-1 3.8-3 3.8Z" />
        <Detail>
          <path d="M13 26h4.2M17.2 28h4.2" />
        </Detail>
      </>
    ),
  },
  "tv-stand": {
    keywords: ["tv stand", "tv unit", "media console", "entertainment center"],
    drawing: (
      <>
        <Floor width={52} />
        <rect x="13" y="8" width="38" height="23" rx="1.5" />
        <path d="M19 31l-2 4M45 31l2 4M23.5 35v14M40.5 35v14M11 49v4M53 49v4" />
        <rect x="7" y="35" width="50" height="14" rx="1.5" />
        <Detail>
          <rect x="15.5" y="10.5" width="33" height="18" rx="0.6" />
          <path d="M20.5 41v2.5M43.5 41v2.5M27 44h10" />
        </Detail>
      </>
    ),
  },
  "bar-stool": {
    keywords: ["bar stool", "counter stool", "stool"],
    drawing: (
      <>
        <Floor width={48} />
        <g transform="translate(-2 0)">
          <BarStool />
        </g>
        <g transform="translate(18 0)">
          <BarStool />
        </g>
      </>
    ),
  },
  "console-table": {
    keywords: ["console table", "hall table", "table"],
    drawing: (
      <>
        <Floor width={42} />
        <rect x="10" y="25" width="44" height="3.5" rx="1.2" />
        <rect x="13" y="28.5" width="38" height="6" />
        <path d="M14 34.5l-1 18.5M50 34.5l1 18.5M13.5 46h37M19.5 25c-1.7 0-2.8-1.3-2.8-3.3 0-2.4 1.6-3.3 1.6-5.7h2.4c0 2.4 1.6 3.3 1.6 5.7 0 2-1.1 3.3-2.8 3.3ZM19.5 16c-.4-3 1-5.5 3.5-7" />
        <rect x="38" y="21" width="11" height="4" rx="0.6" />
        <rect x="39.5" y="18" width="8" height="3" rx="0.6" />
        <Detail>
          <path d="M32 28.5v6M24 31.5h2M38 31.5h2M20.2 12.2c1.4-.3 2.4.3 3 1.4M18 46v-3.5h10V46" />
        </Detail>
      </>
    ),
  },
  "room-divider": {
    keywords: ["room divider", "divider", "folding screen", "privacy screen"],
    drawing: (
      <>
        <Floor width={52} />
        <path d="M8 10.5 22 13v37.5L8 53ZM22 13l20-2.5V53l-20-2.5M42 10.5 56 13v37.5L42 53" />
        <Detail>
          <path d="M8 20.5l14 1.7 20-2 14 1.8M8 43.5l14-1.6 20 2.1 14-1.6M15 12v39.5M32 11.8v39.8M49 11.8v39.5" />
        </Detail>
      </>
    ),
  },
  "corner-desk": {
    keywords: ["corner desk", "l-shaped desk"],
    drawing: (
      <>
        <Floor width={50} />
        <path d="M7 35h36l12-12h-9l-7.5 7.5H11.5ZM7 35v2.5h36l12-12V23M7 37.5V53l4.5-4.5V33M43 37.5V53M46 25.5V41h9V25.5M25 32.5V29" />
        <rect x="17" y="18" width="16" height="11" rx="1" />
        <Detail>
          <rect x="19" y="20" width="12" height="7" rx="0.5" />
          <path d="M14 33.5h8" />
        </Detail>
      </>
    ),
  },
  chair: {
    keywords: ["chair", "dining chair"],
    drawing: (
      <>
        <Floor width={34} />
        <path d="M19.5 13c5-2.2 20-2.2 25 0v4.5c-5-2.2-20-2.2-25 0ZM20.5 17.2v16.3M43.5 17.2v16.3" />
        <rect x="15" y="33.5" width="34" height="6" rx="3" />
        <path d="M18.5 39.5 17 53M45.5 39.5 47 53" />
        <Detail>
          <path d="M26 16.1v17.4M32 15.8v17.7M38 16.1v17.4M24 39.5V49M40 39.5V49" />
        </Detail>
      </>
    ),
  },
  sofa: {
    keywords: ["sofa", "couch", "loveseat", "settee", "sectional"],
    drawing: (
      <g transform="translate(0 2)">
        <Floor width={46} y={53} />
        <path d="M15.5 34V24c0-2.8 2.2-5 5-5h23c2.8 0 5 2.2 5 5v10" />
        <rect x="8.5" y="28" width="9" height="18" rx="4.5" />
        <rect x="46.5" y="28" width="9" height="18" rx="4.5" />
        <path d="M17.5 35h29M17.5 46h29M12.5 46l-.7 4.5M51.5 46l.7 4.5" />
        <Detail>
          <path d="M32 21v12.5M32 35.5v10M17.5 40h29" />
        </Detail>
      </g>
    ),
  },
} as const satisfies Record<string, GlyphSpec>
components/ui/product-art/glyphs/apparel.tsx
import * as React from "react"

import { Detail, Dot, Floor, type GlyphSpec } from "./shared"

/** One ankle sock, heel left and toe right; the pack stands two of them side by side. */
function Sock() {
  return (
    <>
      <path d="M8 22h11v11c0 1.5.8 2.8 2.1 3.4l6.4 3c2.3 1.1 3.6 3.6 3.1 6.1-.5 2.8-3.2 4.7-6 4.1L13.5 47C10.3 46.3 8 43.5 8 40.2Z" />
      <Detail>
        <path d="M8 26.5h11M8.4 43.5c1.2 1.6 2.9 2.6 5 3M26.5 40.5c-1.2 2.2-1.3 4.9-.2 7.6" />
      </Detail>
    </>
  )
}

/** Apparel: what a wardrobe is built from, one silhouette per kind. */
export const APPAREL = {
  sweater: {
    keywords: ["sweater", "jumper", "cardigan", "pullover"],
    drawing: (
      <>
        <Floor width={34} />
        <path d="M25.5 13.5 15.2 17.5c-2.5 1-4.2 3.3-4.5 6l-2.1 19c-.1.9.6 1.7 1.5 1.7h3.8c.8 0 1.5-.6 1.6-1.4l2-13.6V51c0 1.1.9 2 2 2h25c1.1 0 2-.9 2-2V29.2l2 13.6c.1.8.8 1.4 1.6 1.4h3.8c.9 0 1.6-.8 1.5-1.7l-2.1-19c-.3-2.7-2-5-4.5-6L38.5 13.5c-1.3 2.6-3.7 4.2-6.5 4.2s-5.2-1.6-6.5-4.2Z" />
        <Detail>
          <path d="M27.3 14.2c1.1 1.9 2.8 3 4.7 3s3.6-1.1 4.7-3M9 40.8h6.6M48.4 40.8H55M17.5 48.5h29M29.5 21c-1.3 1.7 1.3 3.5 0 5.2s1.3 3.5 0 5.2 1.3 3.5 0 5.2M34.5 21c1.3 1.7-1.3 3.5 0 5.2s-1.3 3.5 0 5.2-1.3 3.5 0 5.2" />
        </Detail>
      </>
    ),
  },
  jacket: {
    keywords: ["jacket", "coat", "parka", "raincoat", "windbreaker", "anorak"],
    drawing: (
      <>
        <Floor width={34} />
        <path d="M25 13.5 15.2 17.5c-2.5 1-4.2 3.3-4.5 6l-2.1 19c-.1.9.6 1.7 1.5 1.7h3.8c.8 0 1.5-.6 1.6-1.4l2-13.6V51c0 1.1.9 2 2 2h25c1.1 0 2-.9 2-2V29.2l2 13.6c.1.8.8 1.4 1.6 1.4h3.8c.9 0 1.6-.8 1.5-1.7l-2.1-19c-.3-2.7-2-5-4.5-6L39 13.5M25 13.5l-1 6.5 5.5 2.5 2.5-4 2.5 4 5.5-2.5-1-6.5M25 13.5c2.3-1.3 4.6-2 7-2s4.7.7 7 2M32 18.5V53" />
        <Detail>
          <path d="M20.5 29h7v5h-7ZM36.5 29h7v5h-7ZM9 40.8h6.6M48.4 40.8H55M17.5 49h13M33.5 49h13" />
        </Detail>
      </>
    ),
  },
  tee: {
    keywords: ["t-shirt", "tshirt", "tee", "shirt", "tank top", "polo"],
    drawing: (
      <>
        <Floor width={30} />
        <path d="M23.5 12.5 13.3 16.6c-1 .4-1.8 1.2-2.2 2.2L7 28.4l7.8 3.4 2.8-4.5V51c0 1.1.9 2 2 2h24.8c1.1 0 2-.9 2-2V27.3l2.8 4.5 7.8-3.4-4.1-9.6c-.4-1-1.2-1.8-2.2-2.2l-10.2-4.1c-1.3 3.6-4.6 6-8.5 6s-7.2-2.4-8.5-6Z" />
        <Detail>
          <path d="M24.9 13.2C26.5 17.4 29 20 32 20s5.5-2.6 7.1-6.8M8.4 25.6l7.5 3.3M55.6 25.6l-7.5 3.3M17.6 48.5h28.8" />
        </Detail>
      </>
    ),
  },
  sneaker: {
    keywords: ["sneaker", "running shoe", "shoe", "trainer"],
    drawing: (
      <>
        <Floor width={48} />
        <path d="M8 47.5h46.6c2.3 0 3.9 1.7 3.5 3.8l-.1.3c-.3 1.5-1.6 2.4-3.1 2.4H10.5C9.1 54 8 52.9 8 51.5ZM10.5 47.5c-.2-5.5.3-12.4 1.7-16.2.4-1.1 1.6-1.7 2.8-1.4 2.8.7 5.4 1.9 7.6 3.3.7-1.8 1.9-3.4 3.4-4.1 1.1-.5 2.3-.2 3 .7 2 2.4 4.8 4.8 8.4 5.8 4.6 1.2 9.6 1.7 13.8 4 2.8 1.5 5.4 4.4 6.2 7.9" />
        <Detail>
          <path d="M8.4 50.6H58M11.4 37.6c3.9.3 7 3.3 7.7 9.9M30.8 31.6l-1.6 2.3M32.9 33.4l-1.6 2.3M35.2 34.9l-1.6 2.3M47.2 37.5c1.9 2.6 2.6 5.8 2.3 10" />
        </Detail>
      </>
    ),
  },
  beanie: {
    keywords: ["beanie", "knit hat", "bobble hat", "toque"],
    drawing: (
      <>
        <Floor width={38} />
        <path d="M15.5 43c0-10.2 7.4-18 16.5-18s16.5 7.8 16.5 18" />
        <rect x="13" y="42" width="38" height="11" rx="3" />
        <circle cx="32" cy="20.5" r="4.5" />
        <Detail>
          <path d="M19 44.5v6M24 44.5v6M29 44.5v6M35 44.5v6M40 44.5v6M45 44.5v6M24.5 27.5c-2.4 4.5-3.5 9.4-3.5 15M39.5 27.5c2.4 4.5 3.5 9.4 3.5 15M32 25v17" />
        </Detail>
      </>
    ),
  },
  tote: {
    keywords: ["tote bag", "tote", "shopper bag"],
    drawing: (
      <>
        <Floor width={34} />
        <path d="M15 27h34l-2 24.2c-.1 1-.9 1.8-2 1.8H19c-1 0-1.9-.8-2-1.8ZM22 27c0-8.8 4.5-15 10-15s10 6.2 10 15" />
        <Detail>
          <path d="M25.5 27c0-6.2 2.9-10.5 6.5-10.5s6.5 4.3 6.5 10.5M15.3 31h33.4" />
          <circle cx="32" cy="41.5" r="4.5" />
        </Detail>
      </>
    ),
  },
  belt: {
    keywords: ["belt"],
    drawing: (
      <>
        <Floor width={50} y={50} />
        <path d="M5 26q27 16 54 0v10q-27 16-54 0ZM59 26c1.8 1.4 2.8 3.2 2.8 5s-1 3.6-2.8 5" />
        <rect x="25" y="30" width="14" height="15" rx="3" />
        <path d="M32 33.5v8" />
        <Dot cx={43.5} cy={37.6} r={0.9} />
        <Dot cx={48.5} cy={36.1} r={0.9} />
        <Dot cx={53.5} cy={34.1} r={0.9} />
        <Detail>
          <path d="M28 33.5h8v8h-8Z" />
        </Detail>
      </>
    ),
  },
  vest: {
    keywords: ["vest", "gilet", "bodywarmer"],
    drawing: (
      <>
        <Floor width={30} />
        <path d="M26.5 12 20 14.7c-1.9.8-3.3 2.4-3.9 4.3l-.3 1.2c2.4 2.3 3.7 5.3 3.7 8.5V51c0 1.1.9 2 2 2h21c1.1 0 2-.9 2-2V28.7c0-3.2 1.3-6.2 3.7-8.5l-.3-1.2c-.6-1.9-2-3.5-3.9-4.3L37.5 12M26.5 12c.3-1.9 2.6-3.5 5.5-3.5s5.2 1.6 5.5 3.5M26.5 12c.7 2.6 2.9 4.5 5.5 4.5s4.8-1.9 5.5-4.5M32 16.5V53" />
        <Detail>
          <path d="M18.3 21.6c1.7 1.9 2.7 4.3 2.7 6.9M45.7 21.6c-1.7 1.9-2.7 4.3-2.7 6.9M22 39.5l3.5 4.5M42 39.5l-3.5 4.5M20 49h24" />
        </Detail>
      </>
    ),
  },
  socks: {
    keywords: ["sock"],
    drawing: (
      <>
        <Floor width={48} />
        <g transform="translate(1 3)">
          <Sock />
        </g>
        <g transform="translate(24 -1)">
          <Sock />
        </g>
      </>
    ),
  },
  hoodie: {
    keywords: ["hoodie", "sweatshirt"],
    drawing: (
      <>
        <Floor width={30} />
        <path d="m24.6 17-9.4 3.7c-2.5 1-4.2 3.3-4.5 6l-2.1 19c-.1.9.6 1.7 1.5 1.7h3.8c.8 0 1.5-.6 1.6-1.4l2-13.6V51c0 1.1.9 2 2 2h25c1.1 0 2-.9 2-2V32.4l2 13.6c.1.8.8 1.4 1.6 1.4h3.8c.9 0 1.6-.8 1.5-1.7l-2.1-19c-.3-2.7-2-5-4.5-6L39.4 17M24.6 17c0-4.8 3.3-8.4 7.4-8.4s7.4 3.6 7.4 8.4M24.6 17c.4 5.1 3.5 8.4 7.4 8.4s7-3.3 7.4-8.4" />
        <Detail>
          <path d="M27.6 17.2c.2-3.5 2-5.7 4.4-5.7s4.2 2.2 4.4 5.7M29.6 25v6.5M34.4 25v6.5M22.6 49v-5.4l3-6.1h12.8l3 6.1V49M9.3 43.2h6M48.7 43.2h6M17.5 49.4h29" />
        </Detail>
      </>
    ),
  },
  cap: {
    keywords: ["cap", "baseball cap", "hat"],
    drawing: (
      <g transform="translate(0 5)">
        <Floor width={40} y={50} />
        <path d="M12 42c-.6-10.6 7.4-19.5 18-20 9.8-.5 17.6 6.4 19 16.3l.6 4.3M12 42c12 2.8 25 3 37.6.6M47.5 41.2c4.5-.2 9 .8 11.2 2.7.9.8.5 2.2-.7 2.4-5 .9-10.7.7-15.2-.7" />
        <Dot cx={30} cy={22} r={1.3} />
        <Detail>
          <path d="M30 22.6c-4.4 4.6-6.6 11.9-6.4 21.3M30 22.6c4.4 5.2 6.4 12.4 6.2 21" />
        </Detail>
      </g>
    ),
  },
  sunglasses: {
    keywords: ["sunglasses", "eyewear"],
    drawing: (
      <>
        <Floor width={40} y={48} />
        <path d="M8.5 26.5h20v5c0 5.3-3.5 9-8.5 9h-2.5c-5 0-9-4-9-9ZM35.5 26.5h20v5c0 5-4 9-9 9H44c-5 0-8.5-3.7-8.5-9ZM28.5 28.5c1.8-1.7 5.2-1.7 7 0M8.5 27.5 5 25.5M55.5 27.5l3.5-2" />
        <Detail>
          <path d="m12.5 31 3.5 3.5M39.5 31l3.5 3.5" />
        </Detail>
      </>
    ),
  },
} as const satisfies Record<string, GlyphSpec>
components/ui/product-art/glyphs/home.tsx
import * as React from "react"

import { Detail, Dot, Floor, type GlyphSpec } from "./shared"

/** One knife in the block: the riveted handle and its steel bolster, leaning `angle` degrees from its slot. */
function Handle({ x, top, angle }: { x: number; top: number; angle: number }) {
  return (
    <g transform={`rotate(${angle} ${x + 2.75} 30)`}>
      <rect x={x} y={top} width="5.5" height={26.5 - top} rx="2.5" />
      <rect x={x + 0.75} y="26.5" width="4" height="3.5" rx="0.5" />
      <Dot cx={x + 2.75} cy={top + 4.5} r={0.8} />
      <Dot cx={x + 2.75} cy={top + 10} r={0.8} />
    </g>
  )
}

/** Home: the kitchen, the bath and the living room's soft things. */
export const HOME = {
  mug: {
    keywords: ["mug", "cup", "teacup"],
    drawing: (
      <>
        <Floor width={34} />
        <path d="M15.5 22.5h28V43c0 5.5-4.5 10-10 10h-8c-5.5 0-10-4.5-10-10ZM43.5 27.5h3c4.1 0 7.5 3.4 7.5 7.5s-3.4 7.5-7.5 7.5h-3" />
        <Detail>
          <path d="M43.5 31.5h3c1.9 0 3.5 1.6 3.5 3.5s-1.6 3.5-3.5 3.5h-3M15.5 26.5h28M25.5 17.5c-1.4-1.7 1.4-3.3 0-5M32.5 17.5c-1.4-1.7 1.4-3.3 0-5" />
        </Detail>
      </>
    ),
  },
  skillet: {
    keywords: ["skillet", "frying pan", "pan", "wok"],
    drawing: (
      <>
        <Floor width={42} cx={29} />
        <ellipse cx="26" cy="39" rx="18" ry="5.5" />
        <path d="M8 39v4.5c0 4 8 7.5 18 7.5s18-3.5 18-7.5V39M43.2 37.2l13.6-4.5c1.3-.4 2.7.3 3.1 1.6s-.3 2.7-1.6 3.1L44 41.8M8.2 37.4c-1.9-.5-3.6.4-3.9 2-.3 1.5.8 2.8 2.5 3l1.2.1" />
        <Detail>
          <ellipse cx="26" cy="39" rx="14.5" ry="3.8" />
          <circle cx="56.8" cy="35" r="0.9" />
        </Detail>
      </>
    ),
  },
  "cutting-board": {
    keywords: ["cutting board", "chopping board", "serving board"],
    drawing: (
      <>
        <Floor width={32} />
        <rect x="16" y="22" width="32" height="31" rx="4" />
        <path d="M27 22v-6.5c0-1.9 1.6-3.5 3.5-3.5h3c1.9 0 3.5 1.6 3.5 3.5V22" />
        <circle cx="32" cy="16.5" r="1.8" />
        <Detail>
          <path d="M21 30c4-1.5 7 1.5 11 0s7-1.5 11 0M21 38c4-1.5 7 1.5 11 0s7-1.5 11 0M21 46c4-1.5 7 1.5 11 0s7-1.5 11 0" />
        </Detail>
      </>
    ),
  },
  diffuser: {
    keywords: ["diffuser", "humidifier"],
    drawing: (
      <>
        <Floor width={32} />
        <path d="M19.5 42c-.5-8.8 3-16.3 8.8-19h7.4c5.8 2.7 9.3 10.2 8.8 19M17.5 42h29v8c0 1.7-1.3 3-3 3h-23c-1.7 0-3-1.3-3-3ZM29.5 19.5c-1.6-1.8.6-3.6-1-5.6M34.5 19c1.6-1.8-.6-3.8 1-5.8" />
        <Dot cx={32} cy={47.5} r={1.1} />
        <Detail>
          <path d="M21 46c3-1 6 1 9 0M35 49.5c3-1 5 .8 7 0M32 16.5c-.9-1.2.4-2.4-.4-3.8M23 36c.6-4.2 2.2-7.8 5-10.5" />
        </Detail>
      </>
    ),
  },
  towels: {
    keywords: ["towel", "bath towel", "washcloth"],
    drawing: (
      <>
        <Floor width={42} />
        <rect x="12" y="42" width="40" height="11" rx="5" />
        <rect x="14" y="31" width="36" height="11" rx="5" />
        <rect x="16" y="20" width="32" height="11" rx="5" />
        <Detail>
          <path d="M17 49.5h30M19 38.5h26M21 27.5h22" />
        </Detail>
      </>
    ),
  },
  "baking-sheet": {
    keywords: ["baking sheet", "sheet pan", "baking tray", "baking pan", "cookie sheet"],
    drawing: (
      <>
        <Floor width={52} />
        <path d="M13 30h38l6 15H7ZM7 45v2.5c0 .8.7 1.5 1.5 1.5h47c.8 0 1.5-.7 1.5-1.5V45" />
        <ellipse cx="20.5" cy="34.3" rx="3.6" ry="1.5" />
        <ellipse cx="32" cy="34.3" rx="3.6" ry="1.5" />
        <ellipse cx="43.5" cy="34.3" rx="3.6" ry="1.5" />
        <ellipse cx="18.5" cy="40.3" rx="4" ry="1.7" />
        <ellipse cx="32" cy="40.3" rx="4" ry="1.7" />
        <ellipse cx="45.5" cy="40.3" rx="4" ry="1.7" />
        <Detail>
          <path d="M15 32h34l4.4 10.5H10.6Z" />
        </Detail>
      </>
    ),
  },
  containers: {
    keywords: ["food container", "storage container", "container", "lunch box"],
    drawing: (
      <>
        <Floor width={42} />
        <path d="M13 38h38l-1.5 13.5c-.1 1-1 1.5-2 1.5H16.5c-1 0-1.9-.5-2-1.5ZM17 23.5h30l-1.2 9c-.1.9-.9 1.5-1.8 1.5H20c-.9 0-1.7-.6-1.8-1.5Z" />
        <rect x="11" y="34" width="42" height="4" rx="1.5" />
        <rect x="15" y="19.5" width="34" height="4" rx="1.5" />
        <rect x="20" y="36.5" width="5" height="4" rx="1" />
        <rect x="39" y="36.5" width="5" height="4" rx="1" />
        <rect x="22" y="22" width="4.5" height="3.5" rx="1" />
        <rect x="37.5" y="22" width="4.5" height="3.5" rx="1" />
        <Detail>
          <path d="M16.5 46.5h31M20.5 30h23M17.5 42.5l1.5 3.5M21 26.5l1 2.5" />
        </Detail>
      </>
    ),
  },
  blanket: {
    keywords: ["blanket", "quilt"],
    drawing: (
      <>
        <Floor width={44} />
        <rect x="11" y="23" width="42" height="11" rx="4" />
        <path d="M11 38c0-2.2 1.8-4 4-4h34c2.2 0 4 1.8 4 4v7H11ZM14 45v5M18.5 45v5M23 45v5M27.5 45v5M32 45v5M36.5 45v5M41 45v5M45.5 45v5M50 45v5" />
        <Detail>
          <path d="M21 23v22M32 23v22M43 23v22M11 28.5h42M11 40h42" />
        </Detail>
      </>
    ),
  },
  kettle: {
    keywords: ["kettle", "teapot"],
    drawing: (
      <>
        <Floor width={36} />
        <path d="M18.2 51c-2-1.2-3.2-3.2-3.2-5.5C15 34 22.6 25 32 25s17 9 17 20.5c0 2.3-1.2 4.3-3.2 5.5ZM15.4 40 9 31.5c-.5-.6-.3-1.5.4-1.8l1.8-.8c.6-.3 1.3-.1 1.7.4l3.8 4.6M29.5 25v-2.5c0-1.4 1.1-2.5 2.5-2.5s2.5 1.1 2.5 2.5V25M22 28c0-8 4.5-14 10-14s10 6 10 14" />
        <Detail>
          <path d="M16 44h32M24.5 34c1.3-2.3 3.1-4 5.5-5" />
        </Detail>
      </>
    ),
  },
  "knife-block": {
    keywords: ["knife block", "knife", "knives"],
    drawing: (
      <>
        <Floor width={34} />
        <path d="M16.5 53l4-23h23l4 23Z" />
        <Handle x={19.5} top={11} angle={-16} />
        <Handle x={29.25} top={8} angle={0} />
        <Handle x={39} top={12} angle={16} />
        <Detail>
          <path d="M21.5 38.5h21M20.5 45.5h23" />
        </Detail>
      </>
    ),
  },
  lamp: {
    keywords: ["lamp", "table lamp", "floor lamp", "bedside lamp"],
    drawing: (
      <>
        <Floor width={22} />
        <path d="M22.5 11.5h19c.9 0 1.7.6 1.9 1.5l4.2 15.5c.2.8-.4 1.5-1.2 1.5H17.6c-.8 0-1.4-.7-1.2-1.5L20.6 13c.2-.9 1-1.5 1.9-1.5ZM32 30v18M23 53c0-2.8 4-5 9-5s9 2.2 9 5Z" />
        <Detail>
          <path d="M18.4 25.5h27.2M39 30v6" />
          <circle cx="39" cy="37.3" r="1.2" />
        </Detail>
      </>
    ),
  },
  plant: {
    keywords: ["plant", "succulent", "cactus", "planter", "bonsai", "fern", "monstera"],
    drawing: (
      <>
        <Floor width={20} />
        <path d="M21.5 39.5h21L40 51.2c-.2 1-1.1 1.8-2.2 1.8H26.2c-1.1 0-2-.8-2.2-1.8Z" />
        <rect x="19.5" y="35" width="25" height="4.5" rx="1.5" />
        <path d="M32 35c-4.5-6-4.8-15.5 0-23.5 4.8 8 4.5 17.5 0 23.5ZM30.6 35c-7.2-.4-13-5.2-14.2-12.6 7.2-.2 12.6 5 14.2 12.6ZM33.4 35c7.2-.4 13-5.2 14.2-12.6-7.2-.2-12.6 5-14.2 12.6Z" />
        <Detail>
          <path d="M32 33V17M29.8 33.8c-4-2.2-7.8-5.4-10.4-9.2M34.2 33.8c4-2.2 7.8-5.4 10.4-9.2" />
        </Detail>
      </>
    ),
  },
} as const satisfies Record<string, GlyphSpec>
components/ui/product-art/glyphs/outdoor.tsx
import * as React from "react"

import { Detail, Floor, type GlyphSpec } from "./shared"

/** One trekking pole, drawn upright from its grip at `x`, 10; the pair leans two of them together. */
function Pole({ x, lean }: { x: number; lean: number }) {
  return (
    <g transform={`rotate(${lean} ${x} 10)`}>
      <rect x={x - 2.5} y="8" width="5" height="13" rx="2.5" />
      <path d={`M${x} 21v29.5M${x} 50.5v3`} />
      <ellipse cx={x} cy="46" rx="4" ry="1.3" />
      <Detail>
        <path d={`M${x - 2} 9.5c-3 .5-4.5 3.5-4 7 .3 2 1.6 3.3 3.1 3.6M${x - 1.5} 34.5h3`} />
      </Detail>
    </g>
  )
}

/** Outdoor: what goes in the car for a weekend away. */
export const OUTDOOR = {
  tent: {
    keywords: ["tent"],
    drawing: (
      <>
        <Floor width={50} />
        <path d="M8 53c1.5-14.5 10.5-29.5 24-34 13.5 4.5 22.5 19.5 24 34ZM22.5 53c.9-8.6 4.4-15.4 9.5-18.5 5.1 3.1 8.6 9.9 9.5 18.5M32 19v-3.5" />
        <Detail>
          <path d="M32 19v15.5M32 34.5V53M11.5 41 5 53M52.5 41 59 53" />
        </Detail>
      </>
    ),
  },
  bottle: {
    keywords: ["water bottle", "bottle", "flask", "thermos", "canteen"],
    drawing: (
      <>
        <Floor width={24} />
        <rect x="26" y="8" width="12" height="7" rx="2" />
        <path d="M27.5 15v2.3c-4.3 1.2-7 4.6-7 8.7v24c0 1.7 1.3 3 3 3h17c1.7 0 3-1.3 3-3V26c0-4.1-2.7-7.5-7-8.7V15" />
        <Detail>
          <path d="M20.5 31h23M20.5 46h23M29.5 10.5v2M32 10.5v2M34.5 10.5v2M25 35v7" />
        </Detail>
      </>
    ),
  },
  "trekking-poles": {
    keywords: ["trekking pole", "hiking pole", "walking pole", "pole"],
    drawing: (
      <>
        <Floor width={36} />
        <Pole x={28} lean={13} />
        <Pole x={36} lean={-13} />
      </>
    ),
  },
  grill: {
    keywords: ["grill", "bbq", "barbecue", "smoker"],
    drawing: (
      <>
        <Floor width={40} />
        <path d="M13 31c0-9.4 8.5-17 19-17s19 7.6 19 17ZM27 14v-3h10v3M13 32.5c0 7.7 8.5 13 19 13s19-5.3 19-13ZM22 43.5l-4 9.5M42 43.5l4 9.5M32 45.5V53M13 31h-3M51 31h3" />
        <Detail>
          <path d="M23 21.5h18M20.5 26h23" />
        </Detail>
      </>
    ),
  },
  "sleeping-bag": {
    keywords: ["sleeping bag", "bedroll"],
    drawing: (
      <>
        <Floor width={46} />
        <path d="M18 26h26c5.5 0 10 5.8 10 13s-4.5 13-10 13H18M29 26v26M32.5 26v26M42 26v26M45.5 26v26" />
        <ellipse cx="18" cy="39" rx="8" ry="13" />
        <path d="M18.6 39.6c.3-1.3 1.5-2 2.6-1.5 1.5.7 1.7 2.9.5 4.3-1.5 1.8-4.2 1.7-5.7-.1-2-2.2-1.6-5.7.8-7.6 3-2.4 7.4-1.5 9.2 1.8" />
        <Detail>
          <path d="M37.2 26.5c-.9 4.5-.9 20.5 0 25" />
        </Detail>
      </>
    ),
  },
  backpack: {
    keywords: ["backpack", "rucksack", "daypack"],
    drawing: (
      <>
        <Floor width={30} />
        <rect x="16.5" y="17" width="31" height="36" rx="9" />
        <path d="M27 17v-2.5c0-1.9 1.6-3.5 3.5-3.5h3c1.9 0 3.5 1.6 3.5 3.5V17" />
        <rect x="21" y="35" width="22" height="13.5" rx="4" />
        <Detail>
          <path d="M16.5 27c6 3.4 25 3.4 31 0M21 40h22M37.5 40v3.5M47.5 22c3 2 4 8 3 14" />
        </Detail>
      </>
    ),
  },
  "camp-chair": {
    keywords: ["camp chair", "camping chair", "folding chair", "beach chair"],
    drawing: (
      <>
        <Floor width={40} />
        <path d="M20.5 12c4 1.2 19 1.2 23 0v17c-4 1.2-19 1.2-23 0ZM19 10v22M45 10v22M17 31c5 2.5 25 2.5 30 0v3c-5 2.5-25 2.5-30 0ZM18.5 35.5 45.5 53M45.5 35.5 18.5 53M13 27h9M42 27h9M15 27v5M49 27v5" />
        <rect x="49.5" y="27" width="5" height="5.5" rx="1" />
        <Detail>
          <path d="M24 17.5h16M24 23h16" />
        </Detail>
      </>
    ),
  },
  lantern: {
    keywords: ["lantern", "camping light"],
    drawing: (
      <>
        <Floor width={26} />
        <path d="M25 12c0-3.9 3.1-7 7-7s7 3.1 7 7M22 17h20l-2.5-5h-15ZM22.5 21c-1.3 4-2 8-2 12s.7 8 2 12M41.5 21c1.3 4 2 8 2 12s-.7 8-2 12" />
        <rect x="20.5" y="17" width="23" height="4" rx="1" />
        <rect x="20" y="45" width="24" height="8" rx="2" />
        <Detail>
          <path d="M27.5 13.5h9M26 15.5h12M27 21.5v23M37 21.5v23" />
          <rect x="30" y="27" width="4" height="12" rx="2" />
        </Detail>
      </>
    ),
  },
  cooler: {
    keywords: ["cooler", "cool box", "ice chest"],
    drawing: (
      <>
        <Floor width={46} />
        <rect x="10" y="20" width="44" height="8" rx="3" />
        <path d="M12 28h40v21c0 2.2-1.8 4-4 4H16c-2.2 0-4-1.8-4-4ZM12 33H9.5c-.8 0-1.5.7-1.5 1.5v3c0 .8.7 1.5 1.5 1.5H12M52 33h2.5c.8 0 1.5.7 1.5 1.5v3c0 .8-.7 1.5-1.5 1.5H52" />
        <rect x="29" y="25.5" width="6" height="6" rx="1" />
        <Detail>
          <path d="M12 42h40" />
          <circle cx="18" cy="48" r="1.2" />
        </Detail>
      </>
    ),
  },
  hammock: {
    keywords: ["hammock"],
    drawing: (
      <>
        <Floor width={54} />
        <path d="M10 51h44M12 51 6.5 22M52 51l5.5-29M6.5 22l7 8.5M57.5 22l-7 8.5M13.5 30.5c6.5 9.5 30.5 9.5 37 0M13.5 30.5c6 13.5 31 13.5 37 0" />
        <Detail>
          <path d="M22 37v3.5M32 38.5V42M42 37v3.5" />
        </Detail>
      </>
    ),
  },
} as const satisfies Record<string, GlyphSpec>