{
  "generatedFrom": "image.contract.json",
  "component": "Image",
  "slug": "image",
  "group": "Data Display",
  "tier": "leaf-lit",
  "summary": "A framed image with rounded corners, a hover preview mask, a loading placeholder and a broken-src fallback.",
  "install": {
    "package": "@ahaslides-product/design",
    "registry": "https://npm.pkg.github.com",
    "scope": "@ahaslides-product",
    "auth": "Published to GitHub Packages — needs a GitHub token with read:packages. Configure the @ahaslides-product scope in .npmrc before installing.",
    "npmrc": "@ahaslides-product:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}   # a GitHub token with read:packages",
    "command": "npm i @ahaslides-product/design",
    "tokenLayer": "import '@ahaslides-product/design/tokens.css';",
    "import": "import '@ahaslides-product/design/aha-image';",
    "element": "aha-image",
    "registers": "aha-image"
  },
  "feeds": {
    "doc": "https://ahaslides-product.github.io/ahaslides-design/image/index.html",
    "md": "https://ahaslides-product.github.io/ahaslides-design/image/image.md",
    "agentJson": "https://ahaslides-product.github.io/ahaslides-design/image.agent.json",
    "llms": "https://ahaslides-product.github.io/ahaslides-design/image.llms.txt",
    "index": "https://ahaslides-product.github.io/ahaslides-design/llms.txt"
  },
  "recommendedSnippet": "html",
  "vibeCode": "Emit the HTML snippet by default — <aha-image> is a standard custom element that renders on open — React/Vue are thin adapters over the same element. Paste into an .html file and it renders with no build step.",
  "frameworks": {
    "react": {
      "via": "web-component",
      "ref": "aha-image"
    },
    "vue": {
      "via": "web-component",
      "ref": "aha-image"
    },
    "html": {
      "via": "web-component",
      "ref": "aha-image",
      "runnable": "paste-and-run — no build step, renders on open"
    }
  },
  "props": [
    {
      "name": "src",
      "type": "string",
      "default": "—",
      "desc": "Image source URL (empty/broken → the fallback state)"
    },
    {
      "name": "alt",
      "type": "string",
      "default": "—",
      "desc": "Alternative text for the image"
    },
    {
      "name": "width",
      "type": "number",
      "default": "—",
      "desc": "Frame width in px"
    },
    {
      "name": "height",
      "type": "number",
      "default": "—",
      "desc": "Frame height in px"
    },
    {
      "name": "preview",
      "type": "'true' | 'false'",
      "default": "true",
      "desc": "Click-to-zoom modal. `false` → a plain framed picture (no mask, no click, no dialog)"
    },
    {
      "name": "radius",
      "type": "'xs'|'sm'|'default'|'lg'|'xl'|'pill'",
      "default": "default",
      "desc": "Frame corner from the DS radius scale"
    },
    {
      "name": "placeholder",
      "type": "boolean",
      "default": "off",
      "desc": "Show a shimmer loading skeleton until the image loads"
    },
    {
      "name": "fallback",
      "type": "string",
      "default": "Image unavailable",
      "desc": "Label shown in the broken-src error state (its presence never forces the state — a broken/empty src does)"
    },
    {
      "name": "(default slot)",
      "type": "slot",
      "default": "Preview",
      "desc": "The label shown in the hover mask"
    }
  ],
  "tokens": [
    "bg-container-secondary",
    "bg-container",
    "bg-container-disabled",
    "bg-hover",
    "radius-xs",
    "radius-sm",
    "radius-default",
    "radius-lg",
    "radius-xl",
    "radius-pill",
    "text-inverse",
    "text-tertiary",
    "icon-muted",
    "icon-strong",
    "bg-overlay",
    "color-primary",
    "font-product"
  ],
  "spec": [
    {
      "label": "Frame",
      "value": "radius 8px (radius attr xs 4·sm 6·default 8·lg 12·xl 16·pill) · bg #F7F7F7 · overflow hidden · role=button, keyboard-focusable"
    },
    {
      "label": "Image",
      "value": "object-fit cover · scale 1.06 on hover, motion-slow ease-out"
    },
    {
      "label": "Mask",
      "value": "overlay rgba(26,26,46,.7) · #FFFFFF 13/SemiBold · fade motion-mid"
    },
    {
      "label": "Preview",
      "value": "click/Enter opens a modal dialog (role=dialog, aria-modal); Esc / close button / backdrop dismiss; focus moves in and is restored on close. preview=\"false\" → plain picture, no mask/click/dialog"
    },
    {
      "label": "Placeholder",
      "value": "shimmer skeleton (icon-muted on bg-secondary) shown while loading; fades on the image load, persistent node"
    },
    {
      "label": "Fallback",
      "value": "broken/empty src → system-image-square glyph + label, text-tertiary on bg-secondary, role=img"
    },
    {
      "label": "Sizing",
      "value": "width/height attributes drive the frame"
    }
  ],
  "opinion": {
    "whenToUse": [
      {
        "what": "Image",
        "when": "a framed picture that invites a click to preview (a deck cover, an upload)"
      },
      {
        "what": "Avatar",
        "when": "a small round identity image"
      },
      {
        "what": "Card",
        "when": "a picture is one part of a larger grouped surface"
      }
    ],
    "note": "Always set alt for accessibility. Keep the mask label short — 'Preview', 'View' — it is an invitation, not a caption.",
    "dsv3": "COVERED — enumerated against the DS V3 Image component-set (fileKey MwjvUjVI0HnfwD9EwPXRAy): it is width-driven with auto aspect ratio, fillable with a custom image OR a placeholder image style, and pairs with the Skeleton component for loading. That maps to antd's Image matrix — preview toggle, loading placeholder, broken-src fallback, width/height, shape/radius — all now supported. DS V3's measured cell set carries no per-state px for Image, so the styling derives from the token layer (radius scale, bg-container-secondary, bg-overlay, text-inverse, text-tertiary, icon-muted) + antd conventions. A deliberate, documented extension — not a measured match."
  },
  "surfaces": [
    "editor",
    "dashboard",
    "audience"
  ],
  "snippets": {
    "html": "<!-- Paste-and-run: save as .html and open in a browser. No build step.\n     <aha-image> is the SAME shared custom element React and Vue consume — here in its\n     native form. Theming comes only from the --aha-* tokens in tokens.css. -->\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css\">\n<script type=\"module\">\n  import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-image.js';   // registers <aha-image>\n</script>\n\n<!-- Click-to-zoom preview (default), rounded frame -->\n<aha-image width=\"240\" height=\"150\" src=\"https://picsum.photos/240/150\" alt=\"Deck cover\">Preview</aha-image>\n\n<!-- Plain framed picture — no zoom -->\n<aha-image width=\"240\" height=\"150\" preview=\"false\" radius=\"lg\" src=\"https://picsum.photos/240/150\" alt=\"Cover\"></aha-image>\n\n<!-- Loading placeholder + a broken-src fallback -->\n<aha-image width=\"240\" height=\"150\" placeholder src=\"https://picsum.photos/240/150\" alt=\"Cover\"></aha-image>\n<aha-image width=\"240\" height=\"150\" fallback=\"Couldn’t load cover\" src=\"/does-not-exist.png\" alt=\"Cover\"></aha-image>\n",
    "react": "import '@ahaslides-product/design/aha-image';   // registers <aha-image>\n\n// The custom element renders its own shadow DOM; React just passes attributes + the mask label.\n// `preview=\"false\"` → a plain framed picture; `placeholder` shows a shimmer; a broken src falls back.\nfunction Cover({ src, alt, label = 'Preview', preview = true, radius = 'default' }) {\n  return (\n    <aha-image\n      src={src} alt={alt} width={240} height={150}\n      radius={radius} placeholder\n      preview={preview ? 'true' : 'false'}\n      fallback=\"Couldn’t load image\"\n    >{label}</aha-image>\n  );\n}\n\n// usage\n<Cover src=\"https://picsum.photos/240/150\" alt=\"Deck cover\" />\n<Cover src=\"https://picsum.photos/240/150\" alt=\"Logo\" preview={false} radius=\"lg\" />\n",
    "vue": "// main.ts — register the element + mark aha-* as custom elements\nimport '@ahaslides-product/design/aha-image';\napp.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');\n\n// Component.vue\n<template>\n  <!-- Click-to-zoom preview (default) -->\n  <aha-image\n    src=\"https://picsum.photos/240/150\"\n    alt=\"Deck cover\"\n    width=\"240\"\n    height=\"150\"\n    radius=\"lg\"\n    placeholder\n    fallback=\"Couldn’t load image\"\n  >Preview</aha-image>\n\n  <!-- Plain framed picture — no zoom -->\n  <aha-image\n    src=\"https://picsum.photos/240/150\"\n    alt=\"Logo\"\n    width=\"240\"\n    height=\"150\"\n    preview=\"false\"\n  ></aha-image>\n</template>\n"
  }
}
