{
  "generatedFrom": "image-action-button.contract.json",
  "component": "Image action button",
  "slug": "image-action-button",
  "group": "Settings",
  "tier": "leaf-lit",
  "summary": "The compact per-option image control — an image icon button with empty / loading / thumbnail states and a Change/Edit/Delete menu; it emits intents, the host owns the modals.",
  "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-action-button';",
    "element": "aha-image-action-button",
    "registers": "aha-image-action-button"
  },
  "feeds": {
    "doc": "https://ahaslides-product.github.io/ahaslides-design/image-action-button/index.html",
    "md": "https://ahaslides-product.github.io/ahaslides-design/image-action-button/image-action-button.md",
    "agentJson": "https://ahaslides-product.github.io/ahaslides-design/image-action-button.agent.json",
    "llms": "https://ahaslides-product.github.io/ahaslides-design/image-action-button.llms.txt",
    "index": "https://ahaslides-product.github.io/ahaslides-design/llms.txt"
  },
  "recommendedSnippet": "html",
  "vibeCode": "Emit the HTML snippet by default — <aha-image-action-button> 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-action-button"
    },
    "vue": {
      "via": "web-component",
      "ref": "aha-image-action-button"
    },
    "html": {
      "via": "web-component",
      "ref": "aha-image-action-button",
      "runnable": "paste-and-run — no build step, renders on open"
    }
  },
  "props": [
    {
      "name": "state",
      "type": "empty | loading | filled",
      "default": "empty",
      "desc": "The control's state — drives which affordance shows"
    },
    {
      "name": "src",
      "type": "string",
      "default": "—",
      "desc": "The thumbnail image URL when filled"
    },
    {
      "name": "label",
      "type": "string",
      "default": "Add image",
      "desc": "The empty-state tooltip / aria-label"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "desc": "Non-interactive; grey fill"
    },
    {
      "name": "add / change / edit / delete",
      "type": "CustomEvent",
      "default": "—",
      "desc": "Composed intents — the host runs the corresponding modal/flow"
    }
  ],
  "tokens": [
    "border",
    "border-hover",
    "border-disabled",
    "color-primary",
    "color-error",
    "bg-container",
    "bg-container-disabled",
    "bg-elevated",
    "bg-hover",
    "bg-overlay",
    "text-default",
    "text-inverse",
    "text-disabled",
    "icon-muted",
    "radius-default",
    "radius-sm",
    "radius-pill",
    "ink-a10"
  ],
  "spec": [
    {
      "label": "Trigger",
      "value": "36×36 icon button, radius 8, 1px #E3E3E3 border"
    },
    {
      "label": "Empty",
      "value": "system-image-square glyph + \"Add image\" tooltip; click emits `add`"
    },
    {
      "label": "Loading",
      "value": "spinner at the same 36×36 size — no layout jump; role=status, aria-label \"Loading\", aria-busy"
    },
    {
      "label": "Filled",
      "value": "thumbnail + hover pencil overlay; click opens Change/Edit/Delete"
    },
    {
      "label": "Menu",
      "value": "Change / Edit / Delete (Delete in #F5222D); each emits an intent"
    },
    {
      "label": "Intents",
      "value": "add · change · edit · delete — host owns the modals, this owns none"
    },
    {
      "label": "Disabled",
      "value": "#F1F1F1 fill, #EBEBEB border, not-allowed"
    }
  ],
  "opinion": {
    "whenToUse": [
      {
        "what": "Image action button",
        "when": "a per-OPTION image cell in an option row"
      },
      {
        "what": "Image dropzone",
        "when": "a standalone MAIN image field where the image IS the field (SETTINGS-44)"
      }
    ],
    "note": "Per-option image support is all-or-nothing: present with its FULL state set, or the control is omitted entirely — never a disabled/greyed stub when unsupported (SETTINGS-22). It is a compact icon button, not a text \"Image\" button (SETTINGS-32). It emits intents; the host owns the upload/crop modals — never a self-built uploader.",
    "dsv3": "COVERED — the settings-lab ImageActionButton control mapped into the DS. The icon button, overlay and menu reuse DS V3 tokens; the empty/loading/filled state machine + intent events are the settings-specific shape (SETTINGS-22/32)."
  },
  "surfaces": [
    "editor",
    "settings"
  ],
  "snippets": {
    "html": "<!-- Paste-and-run: save as .html and open in a browser. No build step.\n     <aha-image-action-button> 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. It EMITS intents\n     (add/change/edit/delete) — the host runs the upload/crop modals; this control owns none. -->\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-action-button.js';   // registers <aha-image-action-button>\n</script>\n\n<div style=\"display:flex; gap:12px; align-items:center\">\n  <!-- empty: click emits `add` -->\n  <aha-image-action-button id=\"img\" state=\"empty\"></aha-image-action-button>\n  <!-- loading: spinner at the same size, no layout jump -->\n  <aha-image-action-button state=\"loading\"></aha-image-action-button>\n  <!-- filled: hover for the pencil overlay, click for Change/Edit/Delete -->\n  <aha-image-action-button state=\"filled\" src=\"https://picsum.photos/80\"></aha-image-action-button>\n</div>\n\n<script type=\"module\">\n  const el = document.getElementById('img');\n  // The host owns the flow: on `add`, open your uploader, flip to loading, then filled + src.\n  el.addEventListener('add', () => {\n    el.state = 'loading';\n    setTimeout(() => { el.src = 'https://picsum.photos/80'; el.state = 'filled'; }, 800);\n  });\n  el.addEventListener('delete', () => { el.removeAttribute('src'); el.state = 'empty'; });\n</script>\n",
    "react": "import '@ahaslides-product/design/aha-image-action-button'; // registers <aha-image-action-button>\nimport { useRef, useEffect } from 'react';\n\n// React 18 needs a thin wrapper; React 19 can use <aha-image-action-button> directly.\n// It emits intents; the host owns the modals — drive state/src from your upload flow.\nfunction AhaImageActionButton({ state, src, label, disabled, onAdd, onChange, onEdit, onDelete }) {\n  const ref = useRef();\n  useEffect(() => {\n    const el = ref.current;\n    const map = { add: onAdd, change: onChange, edit: onEdit, delete: onDelete };\n    const on = Object.entries(map).map(([k, fn]) => { const h = () => fn?.(); el.addEventListener(k, h); return [k, h]; });\n    return () => on.forEach(([k, h]) => el.removeEventListener(k, h));\n  }, [onAdd, onChange, onEdit, onDelete]);\n  return (\n    <aha-image-action-button\n      ref={ref}\n      state={state || 'empty'}\n      {...(src ? { src } : {})}\n      {...(label ? { label } : {})}\n      {...(disabled ? { disabled: '' } : {})}\n    />\n  );\n}\n\n// usage — the host runs the uploader and flips state\n<AhaImageActionButton\n  state={img ? 'filled' : 'empty'}\n  src={img}\n  onAdd={openUploader}\n  onEdit={openCropper}\n  onDelete={() => setImg(null)}\n/>\n",
    "vue": "// main.ts — register the element + mark aha-* as custom elements\nimport '@ahaslides-product/design/aha-image-action-button';   // registers <aha-image-action-button>\napp.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');\n\n// Component.vue\n<script setup>\nimport { ref } from 'vue';\nconst img = ref(null);       // the option's image URL\nconst state = ref('empty');  // empty | loading | filled\nfunction onAdd() { state.value = 'loading'; upload().then((url) => { img.value = url; state.value = 'filled'; }); }\nfunction onDelete() { img.value = null; state.value = 'empty'; }\n</script>\n\n<template>\n  <!-- emits intents; the host owns the upload/crop modals -->\n  <aha-image-action-button\n    :state=\"state\"\n    :src=\"img || undefined\"\n    @add=\"onAdd\"\n    @edit=\"openCropper\"\n    @delete=\"onDelete\"\n  />\n</template>\n"
  }
}
