{"version":3,"file":"icon.cjs","names":[],"sources":["../src/content/icon/icon.ts"],"sourcesContent":["import { bind, define, html, prop, ref, watchEffect, when } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport * as lucideModule from 'lucide';\n\nimport { warn } from '../../_dev';\nimport styles from './icon.css?inline';\n\nexport type IconNode = Array<[string, Record<string, string | number | undefined>]>;\n\nconst DEFAULT_SIZE = 16;\nconst DEFAULT_STROKE_WIDTH = 2;\nconst LUCIDE_VIEWBOX_SIZE = 24;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Allowlisted SVG shape/structure element names. Only these tags are permitted\n * when building the SVG DOM from an IconNode, preventing arbitrary element\n * injection (e.g. <script>, event-handler-bearing elements).\n */\nconst ALLOWED_SVG_TAGS = new Set([\n  'circle',\n  'clipPath',\n  'defs',\n  'ellipse',\n  'g',\n  'line',\n  'linearGradient',\n  'mask',\n  'path',\n  'polygon',\n  'polyline',\n  'radialGradient',\n  'rect',\n  'stop',\n  'symbol',\n  'use',\n]);\n\n/**\n * Allowlisted SVG presentation / structural attribute names.\n * Event handler attributes (on*) and \"xlink:href\" are excluded.\n */\nconst ATTR_KEY_RE = /^[a-zA-Z][a-zA-Z0-9:_-]*$/;\nconst BLOCKED_ATTR_RE = /^(on|xlink:|xml:|href$)/i;\n\n// Sync registry seeded from lucide at module load; extend at any time via registerIcons()\nconst registry = new Map<string, IconNode>(\n  Object.entries((lucideModule as unknown as { icons: Record<string, IconNode> }).icons),\n);\n\n/**\n * Register additional icons (or override existing ones) by name.\n * Keys may be kebab-case or PascalCase — both are accepted by `ore-icon`.\n */\nexport function registerIcons(icons: Record<string, IconNode>): void {\n  for (const [name, node] of Object.entries(icons)) {\n    registry.set(name, node);\n  }\n}\n\nconst toPascalCase = (value: string): string =>\n  value\n    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n    .split(/[^a-zA-Z0-9]+/)\n    .filter(Boolean)\n    .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())\n    .join('');\n\nconst parseSize = (value: string | null): number | string => {\n  if (value == null || value === '') return DEFAULT_SIZE;\n\n  const n = Number(value);\n\n  return Number.isFinite(n) && n > 0 && /^\\d+(\\.\\d+)?$/.test(value.trim()) ? n : value;\n};\n\nconst resolveIcon = (name: string): IconNode | undefined => registry.get(name) ?? registry.get(toPascalCase(name));\n\n/**\n * Builds an allowlisted SVG child element via `createElementNS` — real DOM construction, not\n * HTML-string parsing, so this never touches Ore's `unsafeHtml()` directive (and its \"unsanitized\n * HTML\" dev warning) at all. `iconNode` entries come from the bundled Lucide icon set or\n * `registerIcons()` (a developer/build-time API, never runtime user input), but the tag/attr\n * allowlist stays as defense in depth regardless of where the data originated.\n */\nconst createSvgChild = (tag: string, attrs: Record<string, string | number | undefined>): SVGElement | null => {\n  if (!ALLOWED_SVG_TAGS.has(tag)) return null;\n\n  const el = document.createElementNS(SVG_NS, tag);\n\n  for (const [key, value] of Object.entries(attrs)) {\n    if (value === undefined) continue;\n\n    if (!ATTR_KEY_RE.test(key) || BLOCKED_ATTR_RE.test(key)) continue;\n\n    el.setAttribute(key, String(value));\n  }\n\n  return el;\n};\n\n/** Icon component properties */\nexport type OreIconProps = {\n  /** Keep stroke width visually consistent when icon size changes */\n  absoluteStrokeWidth?: boolean;\n  /** Accessible text label. Decorative icons should omit this. */\n  label?: string;\n  /** Lucide icon name, e.g. `search` or `chevron-right` */\n  name?: string;\n  /** Icon width/height in px by default. Accepts CSS lengths. */\n  size?: number | string;\n  /** Render as a filled/solid shape instead of a stroked outline */\n  solid?: boolean;\n  /** SVG stroke width */\n  strokeWidth?: number;\n};\n\n/**\n * Icon wrapper for consistent Lucide rendering across Block.\n *\n * @element ore-icon\n *\n * @attr {string} name - Lucide icon name, e.g. `search` or `chevron-right`\n * @attr {number|string} size - Width/height (default: 16)\n * @attr {number} stroke-width - SVG stroke width (default: 2)\n * @attr {boolean} absolute-stroke-width - Keep stroke width visually stable across icon sizes\n * @attr {boolean} solid - Render as filled shape instead of stroked outline\n * @attr {string} label - Accessible label; when omitted the icon is decorative\n *\n * @csspart svg - Internal SVG element\n *\n * @example\n * ```html\n * <ore-icon name=\"search\"></ore-icon>\n * <ore-icon name=\"chevron-right\" size=\"18\"></ore-icon>\n * <ore-icon name=\"trash-2\" label=\"Delete\"></ore-icon>\n * <ore-icon name=\"star\" solid></ore-icon>\n * ```\n */\nexport const ICON_TAG = 'ore-icon' as const;\ndefine<OreIconProps>(ICON_TAG, {\n  props: {\n    absoluteStrokeWidth: prop.bool(),\n    label: prop.string(),\n    name: prop.string(),\n    size: { default: DEFAULT_SIZE as number | string, parse: parseSize, reflect: false },\n    solid: prop.bool(),\n    strokeWidth: prop.number(DEFAULT_STROKE_WIDTH),\n  },\n  setup(props) {\n    bind({\n      attr: {\n        'aria-hidden': () => ((props.label.value ?? '').trim() ? null : 'true'),\n        'aria-label': () => (props.label.value ?? '').trim() || null,\n        role: () => ((props.label.value ?? '').trim() ? 'img' : null),\n      },\n      style: {\n        '--_size-px': () => {\n          const s = props.size.value;\n\n          return typeof s === 'number' ? `${s}px` : String(s);\n        },\n      },\n    });\n\n    const svgRef = ref<SVGSVGElement>();\n\n    // Memoized so an unknown icon name only warns once per change, not once per reactive read.\n    const iconNode = computed(() => {\n      const name = (props.name.value ?? '').trim();\n\n      if (!name) return undefined;\n\n      const node = resolveIcon(name);\n\n      if (!node) warn(`ore-icon: icon not found: \"${name.slice(0, 64)}\"`);\n\n      return node;\n    });\n\n    // Real DOM construction (createElementNS), not the unsafeHtml() directive — see createSvgChild's\n    // doc comment. Rebuilds the <svg>'s attributes and children whenever the icon name, size,\n    // or presentation props change, or when `svgRef` first attaches (a fresh element mounts\n    // every time `when()`'s branch below flips truthy).\n    watchEffect(() => {\n      const svg = svgRef.value;\n      const node = iconNode.value;\n\n      if (!svg || !node) return;\n\n      const size = props.size.value;\n      const numericSize = typeof size === 'number' ? size : LUCIDE_VIEWBOX_SIZE;\n      const strokeWidth =\n        props.absoluteStrokeWidth.value && !props.solid.value\n          ? props.strokeWidth.value! * (LUCIDE_VIEWBOX_SIZE / numericSize)\n          : props.strokeWidth.value!;\n\n      svg.setAttribute('fill', props.solid.value ? 'currentColor' : 'none');\n      svg.setAttribute('stroke', props.solid.value ? 'none' : 'currentColor');\n      svg.setAttribute('stroke-linecap', 'round');\n      svg.setAttribute('stroke-linejoin', 'round');\n      svg.setAttribute('stroke-width', String(props.solid.value ? 0 : strokeWidth));\n      svg.setAttribute('viewBox', '0 0 24 24');\n\n      const children = node.map(([tag, attrs]) => createSvgChild(tag, attrs)).filter((el): el is SVGElement => !!el);\n\n      svg.replaceChildren(...children);\n    });\n\n    return html`\n      ${when(\n        () => !!iconNode.value,\n        () => html`\n          <svg ref=\"${svgRef}\" part=\"svg\" style=\"width:100%;height:100%\"></svg>\n        `,\n      )}\n    `;\n  },\n  styles: [styles],\n});\n"],"mappings":"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"}