@keyfunc/keybricks-ds 2.3.1 → 2.4.2

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.
Files changed (83) hide show
  1. package/DEPLOY.md +11 -5
  2. package/DESIGN.md +50 -1
  3. package/INTEGRATION.md +33 -6
  4. package/_adherence.oxlintrc.json +35 -13
  5. package/assets/logos/datasource/dat.png +0 -0
  6. package/assets/logos/datasource/excel.png +0 -0
  7. package/assets/logos/datasource/ftp.png +0 -0
  8. package/assets/logos/datasource/hierarchy.png +0 -0
  9. package/assets/logos/datasource/hive.png +0 -0
  10. package/assets/logos/datasource/json.png +0 -0
  11. package/assets/logos/datasource/mariadb.png +0 -0
  12. package/assets/logos/datasource/mongodb.png +0 -0
  13. package/assets/logos/datasource/mysql.png +0 -0
  14. package/assets/logos/datasource/odbc.png +0 -0
  15. package/assets/logos/datasource/oracle.png +0 -0
  16. package/assets/logos/datasource/postgresql.png +0 -0
  17. package/assets/logos/datasource/rest-api.png +0 -0
  18. package/assets/logos/datasource/sap-alt.png +0 -0
  19. package/assets/logos/datasource/sap.png +0 -0
  20. package/assets/logos/datasource/soap.png +0 -0
  21. package/assets/logos/datasource/sql-server.png +0 -0
  22. package/components/_cards/app-chrome.card.html +33 -21
  23. package/components/_cards/context-menu.card.html +7 -2
  24. package/components/_cards/icons-0-usage.card.html +88 -0
  25. package/components/_cards/icons-4-datasource.card.html +5 -0
  26. package/components/_cards/relative-time-picker.card.html +45 -0
  27. package/components/_cards/segmented-control.card.html +9 -3
  28. package/components/_cards/smart-table.card.html +4 -1
  29. package/components/_cards/tabs.card.html +5 -2
  30. package/components/_shared/src-data.js +1 -1
  31. package/components/_shared/src-feedback.js +1 -1
  32. package/components/_shared/src-forms.js +1 -1
  33. package/components/_shared/src-navigation.js +1 -1
  34. package/components/_shared/src-tabs.js +1 -1
  35. package/components/_shared/view-source.js +12 -12
  36. package/components/_vue/index.mjs +5 -1
  37. package/components/data/SmartTable.d.ts +7 -2
  38. package/components/data/SmartTable.jsx +12 -4
  39. package/components/data/SmartTable.vue +12 -4
  40. package/components/feedback/ContextMenu.d.ts +3 -2
  41. package/components/feedback/ContextMenu.jsx +8 -4
  42. package/components/feedback/ContextMenu.vue +9 -6
  43. package/components/feedback/Menu.d.ts +1 -1
  44. package/components/feedback/feedback.prompt.md +1 -1
  45. package/components/forms/RelativeTimePicker.d.ts +39 -0
  46. package/components/forms/RelativeTimePicker.jsx +89 -0
  47. package/components/forms/RelativeTimePicker.prompt.md +72 -0
  48. package/components/forms/RelativeTimePicker.vue +81 -0
  49. package/components/forms/forms.prompt.md +3 -0
  50. package/components/forms/relative-time.d.ts +12 -0
  51. package/components/forms/relative-time.js +90 -0
  52. package/components/index.d.ts +2 -0
  53. package/components/index.js +5 -3
  54. package/components/navigation/SidePanel.d.ts +22 -0
  55. package/components/navigation/SidePanel.jsx +23 -0
  56. package/components/navigation/SidePanel.vue +21 -0
  57. package/components/navigation/Sidebar.d.ts +23 -7
  58. package/components/navigation/Sidebar.jsx +38 -37
  59. package/components/navigation/Sidebar.vue +40 -39
  60. package/components/navigation/TopBar.d.ts +28 -3
  61. package/components/navigation/TopBar.jsx +54 -7
  62. package/components/navigation/TopBar.vue +26 -8
  63. package/components/navigation/UserMenu.vue +27 -0
  64. package/components/navigation/chrome-tones.js +22 -0
  65. package/components/navigation/nav-model.js +21 -18
  66. package/components/navigation/navigation.prompt.md +2 -0
  67. package/components/tabs/CountBadge.vue +12 -0
  68. package/components/tabs/PanelTabs.vue +7 -3
  69. package/components/tabs/PillTabs.vue +7 -3
  70. package/components/tabs/SegmentedControl.d.ts +4 -2
  71. package/components/tabs/SegmentedControl.jsx +5 -3
  72. package/components/tabs/SegmentedControl.vue +4 -3
  73. package/components/tabs/Tabs.d.ts +14 -4
  74. package/components/tabs/Tabs.jsx +35 -20
  75. package/components/tabs/Tabs.vue +7 -3
  76. package/components/tabs/tabs.prompt.md +2 -2
  77. package/package.json +2 -2
  78. package/readme.md +6 -5
  79. package/readme.zh-CN.md +4 -3
  80. package/tokens/layout.css +4 -3
  81. package/tokens/tokens.d.ts +2 -0
  82. package/tokens/tokens.js +4 -1
  83. package/tokens/tokens.json +8 -1
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"feedback/Accordion.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Generic collapsible section: chevron + title/subtitle header, body slot. Controlled via `open`+`onToggle`, or uncontrolled via `defaultOpen`. */\nexport function Accordion({ title, subtitle, defaultOpen, open: openProp, onToggle, children, style }) {\n const [openState, setOpenState] = React.useState(!!defaultOpen);\n const open = openProp != null ? openProp : openState;\n const toggle = () => (onToggle ? onToggle(!open) : setOpenState(!open));\n return (\n <div style={{ borderBottom: \"1px solid var(--border-card)\", ...style }}>\n <div onClick={toggle} data-wash=\"\" style={{ display: \"flex\", alignItems: \"center\", gap: 10, padding: \"14px 8px\", margin: \"0 -4px\", borderRadius: \"var(--radius-md)\", background: \"var(--wash, transparent)\", transition: \"var(--t-hover)\", cursor: \"pointer\" }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: open ? \"rotate(90deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-right\" size={14} /></span>\n <div style={{ display: \"flex\", flexDirection: \"column\", flex: 1 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n </div>\n {open ? <div style={{ padding: \"0 4px 16px 28px\" }}>{children}</div> : null}\n </div>\n );\n}\n","feedback/Accordion.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Generic collapsible section: chevron + title/subtitle header, body slot. Controlled via `open` prop + `toggle` emit, or uncontrolled via `defaultOpen`.\nconst props = defineProps({ title: String, subtitle: String, defaultOpen: Boolean, open: { type: Boolean, default: null }, style: Object })\nconst emit = defineEmits(['toggle'])\nconst openState = ref(!!props.defaultOpen)\nconst open = computed(() => props.open != null ? props.open : openState.value)\nfunction toggle() { if (props.open != null) emit('toggle', !open.value); else openState.value = !openState.value }\n</script>\n<template>\n <div :style=\"{ borderBottom: '1px solid var(--border-card)', ...style }\">\n <div @click=\"toggle\" data-wash=\"\" style=\"display:flex;align-items:center;gap:10px;padding:14px 8px;margin:0 -4px;border-radius:var(--radius-md);background:var(--wash, transparent);transition:var(--t-hover);cursor:pointer\">\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-right\" :size=\"14\" /></span>\n <div style=\"display:flex;flex-direction:column;flex:1\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n </div>\n <div v-if=\"open\" style=\"padding:0 4px 16px 28px\"><slot /></div>\n </div>\n</template>\n","feedback/ActivityItem.jsx":"import React from \"react\";\nimport { IconTile } from \"../display/Badge.jsx\";\n\n/** Activity feed row: 32px icon tile + 14px/500 title + 12px meta + optional right text action (a text link: ink darkens on hover). */\nexport function ActivityItem({ icon = \"file-text\", title, meta, action, onAction, style }) {\n return (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"12px 16px\", ...style }}>\n <IconTile icon={icon} kind=\"row\" style={{ background: \"var(--info-bg)\", border: \"none\", color: \"var(--info-fg)\" }} />\n <div style={{ display: \"flex\", flexDirection: \"column\", flex: 1, minWidth: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {meta ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{meta}</span> : null}\n </div>\n {action ? <button type=\"button\" onClick={onAction} data-ink=\"\" style={{ border: \"none\", background: \"transparent\", padding: 0, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--ink, var(--text-secondary))\", cursor: \"pointer\", whiteSpace: \"nowrap\" }}>{action}</button> : null}\n </div>\n );\n}\n\n/** Compact dot activity row (Recent Activities card): 8px status dot + 12px text + 11px time. */\nexport function DotActivity({ tone = \"success\", text, time, style }) {\n const tones = { success: \"var(--success)\", warning: \"var(--warning)\", danger: \"var(--danger)\", info: \"var(--action-primary)\", neutral: \"var(--dot-neutral)\" };\n return (\n <div style={{ display: \"flex\", gap: 8, alignItems: \"flex-start\", ...style }}>\n <span style={{ width: 8, height: 8, borderRadius: \"var(--radius-full)\", background: tones[tone], marginTop: 5, flexShrink: 0 }} />\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-heading)\", overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\" }}>{text}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\" }}>{time}</span>\n </div>\n </div>\n );\n}\n\n/** Mono log lines (\"[14:30] Task started: Import\") — Menlo 11px on inset panel. */\nexport function LogViewer({ lines = [], height, style }) {\n return (\n <div style={{ background: \"var(--bg-inset)\", borderRadius: \"var(--radius-md)\", border: \"1px solid var(--border-subtle)\", padding: \"10px 12px\", display: \"flex\", flexDirection: \"column\", gap: 6, overflowY: \"auto\", height, boxSizing: \"border-box\", ...style }}>\n {lines.map((l, i) => <span key={i} style={{ fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: typeof l === \"object\" ? l.color || \"var(--text-body)\" : \"var(--text-body)\", whiteSpace: \"pre\" }}>{typeof l === \"object\" ? l.text : l}</span>)}\n </div>\n );\n}\n","feedback/ActivityItem.vue":"<script setup>\nimport IconTile from '../display/Badge.vue'\n// Activity feed row: 32px icon tile + 14px/500 title + 12px meta + optional right text action (emits `action`).\ndefineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, action: String, style: Object })\nconst emit = defineEmits(['action'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 16px', ...style }\">\n <IconTile :icon=\"icon\" kind=\"row\" style=\"background:var(--info-bg);border:none;color:var(--info-fg)\" />\n <div style=\"display:flex;flex-direction:column;flex:1;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"meta\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ meta }}</span>\n </div>\n <button v-if=\"action\" type=\"button\" data-ink=\"\" @click=\"emit('action')\" style=\"border:none;background:transparent;padding:0;font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--ink, var(--text-secondary));cursor:pointer;white-space:nowrap\">{{ action }}</button>\n </div>\n</template>\n","feedback/Alert.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst alertTones = {\n info: { bg: \"var(--info-bg)\", fg: \"var(--info-fg)\", icon: \"info\" },\n success: { bg: \"var(--success-bg)\", fg: \"var(--success-text)\", icon: \"circle-check\" },\n warning: { bg: \"var(--warning-bg)\", fg: \"var(--warning-text)\", icon: \"triangle-alert\" },\n danger: { bg: \"var(--danger-bg)\", fg: \"var(--danger-text)\", icon: \"triangle-alert\" },\n};\n\n/** Inline banner — tinted, no border; title and body at the base size. The icon is centred on the first text line, never on the block. */\nexport function Alert({ tone = \"info\", title, children, action, style }) {\n const t = alertTones[tone];\n return (\n <div style={{ display: \"flex\", gap: 10, alignItems: \"flex-start\", background: t.bg, borderRadius: \"var(--radius-md)\", padding: \"10px 12px\", ...style }}>\n <span style={{ color: t.fg, display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-base)\" }}><Icon name={t.icon} size={15} /></span>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2, flex: 1, minWidth: 0 }}>\n {title ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: t.fg }}>{title}</span> : null}\n {children ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-body)\" }}>{children}</span> : null}\n </div>\n {action}\n </div>\n );\n}\n\n/** Empty state — 22px muted glyph, 13px/500 title, 12px hint, optional action. */\nexport function EmptyState({ icon = \"file-text\", title, hint, action, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", gap: 6, padding: \"40px 24px\", textAlign: \"center\", ...style }}>\n <span style={{ color: \"var(--text-muted)\", marginBottom: 6 }}><Icon name={icon} size={22} strokeWidth={1.5} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {hint ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-secondary)\", maxWidth: 320 }}>{hint}</span> : null}\n {action ? <span style={{ paddingTop: 10 }}>{action}</span> : null}\n </div>\n );\n}\n\n/** Loading placeholder block. */\nexport function Skeleton({ width = \"100%\", height = 12, radius = \"var(--radius-sm)\", style }) {\n return <span style={{ display: \"block\", width, height, borderRadius: radius, background: \"var(--skeleton)\", animation: \"kb-pulse 1.4s var(--ease-in-out) infinite\", ...style }} />;\n}\n\n/** Indeterminate spinner (task-running affordance). */\nexport function Spinner({ size = 16, color = \"var(--action-primary)\", style }) {\n return (\n <span style={{ display: \"inline-block\", width: size, height: size, borderRadius: \"var(--radius-full)\", border: `2px solid var(--track)`, borderTopColor: color, animation: \"kb-spin .7s linear infinite\", ...style }} />\n );\n}\n","feedback/Alert.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst alertTones = {\n info: { bg: 'var(--info-bg)', fg: 'var(--info-fg)', icon: 'info' },\n success: { bg: 'var(--success-bg)', fg: 'var(--success-text)', icon: 'circle-check' },\n warning: { bg: 'var(--warning-bg)', fg: 'var(--warning-text)', icon: 'triangle-alert' },\n danger: { bg: 'var(--danger-bg)', fg: 'var(--danger-text)', icon: 'triangle-alert' },\n}\n// Alert — inline banner — tinted, no border, 13px title + 12px body.\nconst props = defineProps({ tone: { type: String, default: 'info' }, title: String, style: Object })\nconst t = computed(() => alertTones[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: '10px', alignItems: 'flex-start', background: t.bg, borderRadius: 'var(--radius-md)', padding: '10px 12px', ...style }\">\n <span :style=\"{ color: t.fg, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }\"><Icon :name=\"t.icon\" :size=\"15\" /></span>\n <div style=\"display:flex;flex-direction:column;gap:2px;flex:1;min-width:0\">\n <span v-if=\"title\" :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t.fg }\">{{ title }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body)\"><slot /></span>\n </div>\n <slot name=\"action\" />\n </div>\n</template>\n","feedback/Banner.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\nconst bannerTones = {\n info: { bg: \"var(--info-bg)\", fg: \"var(--info-fg)\", icon: \"info\" },\n success: { bg: \"var(--success-bg)\", fg: \"var(--success-text)\", icon: \"circle-check\" },\n warning: { bg: \"var(--warning-bg)\", fg: \"var(--warning-text)\", icon: \"triangle-alert\" },\n danger: { bg: \"var(--danger-bg)\", fg: \"var(--danger-text)\", icon: \"triangle-alert\" },\n};\n\n/** Page-level notification banner (Carbon/SLDS scoped notification): full-width tinted strip with icon, message, optional inline action link and dismiss ×. Distinct from Alert (inline, in-card) and Toast (floating). */\nexport function Banner({ tone = \"info\", title, action, onAction, dismissible, onDismiss, children, style }) {\n const t = bannerTones[tone];\n return (\n <div style={{ display: \"flex\", alignItems: \"flex-start\", gap: 10, background: t.bg, borderLeft: `3px solid ${t.fg}`, padding: \"10px 12px 10px 14px\", boxSizing: \"border-box\", ...style }}>\n <span style={{ color: t.fg, display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-base)\" }}><Icon name={t.icon} size={16} /></span>\n <span style={{ flex: 1, minWidth: 0, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-body)\" }}>\n {title && <strong style={{ fontWeight: \"var(--weight-medium)\", color: t.fg, paddingRight: 6 }}>{title}</strong>}{children}\n </span>\n {action && <button type=\"button\" onClick={onAction} data-link=\"\" style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", padding: 0, height: \"var(--leading-base)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", color: t.fg, textDecorationLine: \"underline\", textDecorationThickness: \"var(--link-thick, 1px)\", textUnderlineOffset: 2, whiteSpace: \"nowrap\", flexShrink: 0 }}>{action}</button>}\n {dismissible && <IconButton icon=\"x\" size=\"sm\" muted label=\"Dismiss\" onClick={onDismiss} style={{ margin: \"-2px 0\" }} />}\n </div>\n );\n}\n","feedback/Banner.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nconst bannerTones = {\n info: { bg: 'var(--info-bg)', fg: 'var(--info-fg)', icon: 'info' },\n success: { bg: 'var(--success-bg)', fg: 'var(--success-text)', icon: 'circle-check' },\n warning: { bg: 'var(--warning-bg)', fg: 'var(--warning-text)', icon: 'triangle-alert' },\n danger: { bg: 'var(--danger-bg)', fg: 'var(--danger-text)', icon: 'triangle-alert' },\n}\n// Page-level notification banner: tinted full-width strip, left accent bar, inline action link, dismiss ×.\nconst props = defineProps({ tone: { type: String, default: 'info' }, title: String, action: String, dismissible: Boolean, style: Object })\nconst emit = defineEmits(['action', 'dismiss'])\nconst t = computed(() => bannerTones[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'flex-start', gap: '10px', background: t.bg, borderLeft: `3px solid ${t.fg}`, padding: '10px 12px 10px 14px', boxSizing: 'border-box', ...style }\">\n <span :style=\"{ color: t.fg, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }\"><Icon :name=\"t.icon\" :size=\"16\" /></span>\n <span style=\"flex:1;min-width:0;font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body)\">\n <strong v-if=\"title\" :style=\"{ fontWeight: 'var(--weight-medium)', color: t.fg, paddingRight: '6px' }\">{{ title }}</strong><slot />\n </span>\n <button v-if=\"action\" type=\"button\" data-link=\"\" @click=\"emit('action')\" :style=\"{ border: 'none', background: 'transparent', cursor: 'pointer', padding: 0, height: 'var(--leading-base)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', color: t.fg, textDecorationLine: 'underline', textDecorationThickness: 'var(--link-thick, 1px)', textUnderlineOffset: '2px', whiteSpace: 'nowrap', flexShrink: 0 }\">{{ action }}</button>\n <IconButton v-if=\"dismissible\" icon=\"x\" size=\"sm\" muted label=\"Dismiss\" :style=\"{ margin: '-2px 0' }\" @click=\"emit('dismiss')\" />\n </div>\n</template>\n","feedback/ContextMenu.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides. */\nexport function ContextMenu({ items = [], onSelect, size, style }) {\n const [hover, setHover] = React.useState(-1);\n return (\n <div role=\"menu\" data-size={size} style={{ background: \"var(--bg-surface)\", borderRadius: \"var(--radius-lg)\", border: \"1px solid var(--border-card)\", boxShadow: \"var(--shadow-menu)\", padding: 4, minWidth: 148, display: \"inline-flex\", flexDirection: \"column\", ...style }}>\n {items.map((it, i) => it.divider ? <div key={i} style={{ height: 1, background: \"var(--border-subtle)\", margin: \"4px 0\" }} /> : (\n <button key={i} type=\"button\" onClick={() => onSelect && onSelect(it.label)}\n onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(-1)}\n role=\"menuitem\" style={{ display: \"flex\", alignItems: \"center\", gap: 10, height: \"var(--control-h)\", padding: \"0 10px\", boxSizing: \"border-box\", borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", background: hover === i ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: it.danger ? \"var(--danger)\" : \"var(--text-heading)\", textAlign: \"left\", whiteSpace: \"nowrap\" }}>\n {it.icon ? <Icon name={it.icon} size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /> : null}{it.label}\n </button>\n ))}\n </div>\n );\n}\n","feedback/ContextMenu.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.\ndefineProps({ items: { type: Array, default: () => [] }, size: String, style: Object })\nconst emit = defineEmits(['select'])\nconst hover = ref(-1)\n</script>\n<template>\n <div role=\"menu\" :data-size=\"size\" :style=\"{ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', display: 'inline-flex', flexDirection: 'column', ...style }\">\n <template v-for=\"(it, i) in items\" :key=\"i\">\n <div v-if=\"it.divider\" style=\"height:1px;background:var(--border-subtle);margin:4px 0\" />\n <button v-else type=\"button\" @click=\"emit('select', it.label)\" @mouseenter=\"hover = i\" @mouseleave=\"hover = -1\"\n role=\"menuitem\" :style=\"{ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: hover === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap' }\">\n <Icon v-if=\"it.icon\" :name=\"it.icon\" :size=\"16\" :style=\"{ width: 'var(--control-icon)', height: 'var(--control-icon)' }\" />{{ it.label }}\n </button>\n </template>\n </div>\n</template>\n","feedback/Drawer.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Right slide-out panel over a scrim: title + subtitle, scrollable body, right-aligned footer slot. inline=true renders it in-flow for embedding in cards/demos. A size region (default inherit → md: drawers are working surfaces, not dialogs); pass `size` to override. */\nexport function Drawer({ open = true, title, subtitle, width = 400, inline, height, size, footer, onClose, children, style }) {\n if (!open) return null;\n const panel = (\n <div role=\"dialog\" aria-label={title} data-size={size} style={{ width, maxWidth: \"100%\", height: inline ? (height || 420) : \"100%\", background: \"var(--bg-raised)\", borderLeft: \"1px solid var(--border-card)\", boxShadow: inline ? \"var(--shadow-menu)\" : \"-8px 0 24px rgba(0,0,0,.12)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", ...(inline ? { border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\" } : null), ...style }}>\n <div style={{ display: \"flex\", alignItems: \"flex-start\", justifyContent: \"space-between\", padding: \"16px 16px 12px\", borderBottom: \"1px solid var(--border-card)\", flexShrink: 0 }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 3 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {subtitle && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{subtitle}</span>}\n </div>\n <IconButton icon=\"x\" label=\"Close\" onClick={onClose} style={{ margin: \"calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0\" }} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 16 }}>{children}</div>\n {footer && <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"var(--control-gap)\", padding: \"12px 16px\", borderTop: \"1px solid var(--border-card)\", flexShrink: 0 }}>{footer}</div>}\n </div>\n );\n if (inline) return panel;\n return (\n <div onClick={onClose} style={{ position: \"fixed\", inset: 0, zIndex: \"var(--z-modal)\", background: \"var(--bg-scrim)\", display: \"flex\", justifyContent: \"flex-end\", animation: \"kb-fade-in var(--dur-fast) var(--ease-out)\" }}>\n <div onClick={(e) => e.stopPropagation()} style={{ height: \"100%\", animation: \"kb-slide-in var(--dur) var(--ease-out)\" }}>{panel}</div>\n </div>\n );\n}\n","feedback/Drawer.vue":"<script setup>\nimport { px } from '../px.js'\nimport IconButton from '../buttons/IconButton.vue'\n// Right slide-out panel over a scrim: title + subtitle, scrollable body, right-aligned footer slot.\n// inline=true renders it in-flow (cards, demos); otherwise it teleports to <body> over a scrim. Click the scrim or × to emit close.\n// A size region (default inherit → md: drawers are working surfaces, not dialogs); pass `size` to override.\nconst props = defineProps({ open: { type: Boolean, default: true }, title: String, subtitle: String, width: { type: [Number, String], default: 400 }, inline: Boolean, height: { type: [Number, String], default: 420 }, size: String, style: Object })\nconst emit = defineEmits(['close'])\nconst contents = { display: 'contents' }\n</script>\n<template>\n <Teleport to=\"body\" :disabled=\"inline\">\n <div v-if=\"open\" @click=\"!inline && emit('close')\"\n :style=\"inline ? contents : { position: 'fixed', inset: 0, zIndex: 'var(--z-modal)', background: 'var(--bg-scrim)', display: 'flex', justifyContent: 'flex-end', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)' }\">\n <div @click.stop :style=\"inline ? contents : { height: '100%', animation: 'kb-slide-in var(--dur) var(--ease-out)' }\">\n <div role=\"dialog\" :aria-label=\"title\" :data-size=\"size\"\n :style=\"{ width: px(width), maxWidth: '100%', height: inline ? px(height) : '100%', background: 'var(--bg-raised)', borderLeft: '1px solid var(--border-card)', boxShadow: inline ? 'var(--shadow-menu)' : '-8px 0 24px rgba(0,0,0,.12)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', ...(inline ? { border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)' } : null), ...style }\">\n <div style=\"display:flex;align-items:flex-start;justify-content:space-between;padding:16px 16px 12px;border-bottom:1px solid var(--border-card);flex-shrink:0\">\n <div style=\"display:flex;flex-direction:column;gap:3px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n <IconButton icon=\"x\" label=\"Close\" :style=\"{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }\" @click=\"emit('close')\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:16px\"><slot /></div>\n <div v-if=\"$slots.footer\" style=\"display:flex;justify-content:flex-end;gap:var(--control-gap);padding:12px 16px;border-top:1px solid var(--border-card);flex-shrink:0\"><slot name=\"footer\" /></div>\n </div>\n </div>\n </div>\n </Teleport>\n</template>\n","feedback/Menu.jsx":"import React from \"react\";\nimport { ContextMenu } from \"./ContextMenu.jsx\";\n\n/** Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click. */\nexport function Menu({ trigger, items = [], onSelect, align = \"start\", style }) {\n const [open, setOpen] = React.useState(false);\n const ref = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };\n document.addEventListener(\"mousedown\", onDoc);\n return () => document.removeEventListener(\"mousedown\", onDoc);\n }, [open]);\n return (\n <div ref={ref} style={{ position: \"relative\", display: \"inline-flex\", ...style }}>\n <span onClick={() => setOpen((o) => !o)}>{trigger}</span>\n {open ? (\n <div style={{ position: \"absolute\", top: \"calc(100% + 6px)\", [align === \"end\" ? \"right\" : \"left\"]: 0, zIndex: \"var(--z-menu)\" }}>\n <ContextMenu items={items} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />\n </div>\n ) : null}\n </div>\n );\n}\n","feedback/Menu.vue":"<script setup>\nimport { ref, onMounted, onUnmounted } from 'vue'\nimport ContextMenu from './ContextMenu.vue'\n// Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click.\nconst props = defineProps({ items: { type: Array, default: () => [] }, align: { type: String, default: 'start' }, style: Object })\nconst emit = defineEmits(['select'])\nconst open = ref(false)\nconst root = ref(null)\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\nfunction select(l) { open.value = false; emit('select', l) }\n</script>\n<template>\n <div ref=\"root\" :style=\"{ position: 'relative', display: 'inline-flex', ...style }\">\n <span @click=\"open = !open\"><slot name=\"trigger\" /></span>\n <div v-if=\"open\" :style=\"{ position: 'absolute', top: 'calc(100% + 6px)', [align === 'end' ? 'right' : 'left']: 0, zIndex: 'var(--z-menu)' }\">\n <ContextMenu :items=\"items\" @select=\"select\" />\n </div>\n </div>\n</template>\n","feedback/Modal.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Hover tooltip — 11px, inverted surface, 6px offset. Wraps its children. */\nexport function Tooltip({ label, placement = \"top\", children, style }) {\n const [show, setShow] = React.useState(false);\n const pos = {\n top: { bottom: \"100%\", left: \"50%\", transform: \"translateX(-50%)\", marginBottom: 6 },\n bottom: { top: \"100%\", left: \"50%\", transform: \"translateX(-50%)\", marginTop: 6 },\n left: { right: \"100%\", top: \"50%\", transform: \"translateY(-50%)\", marginRight: 6 },\n right: { left: \"100%\", top: \"50%\", transform: \"translateY(-50%)\", marginLeft: 6 },\n }[placement];\n return (\n <span style={{ position: \"relative\", display: \"inline-flex\", ...style }} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>\n {children}\n {show ? (\n <span style={{ position: \"absolute\", zIndex: \"var(--z-tooltip)\", whiteSpace: \"nowrap\", background: \"var(--tooltip-bg)\", color: \"var(--tooltip-fg)\", borderRadius: \"var(--radius-md)\", padding: \"5px 8px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", boxShadow: \"var(--shadow-menu)\", pointerEvents: \"none\", animation: \"kb-fade-in var(--dur-fast) var(--ease-out)\", ...pos }}>{label}</span>\n ) : null}\n </span>\n );\n}\n\n/** Centered modal dialog with scrim. width defaults to 440. */\n/** Modal dialog. A size region that defaults to lg (40px controls) whatever the page behind it uses — a dialog does not compete for space; pass `size` to override. 24px padding, footer buttons 8px apart. */\nexport function Modal({ open, title, subtitle, onClose, footer, width = 440, size = \"lg\", children, style }) {\n if (!open) return null;\n return (\n <div style={{ position: \"fixed\", inset: 0, zIndex: \"var(--z-modal)\", background: \"var(--bg-scrim)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", animation: \"kb-fade-in var(--dur-fast) var(--ease-out)\" }} onClick={onClose}>\n <div role=\"dialog\" aria-modal=\"true\" data-size={size} onClick={(e) => e.stopPropagation()}\n style={{ width, maxWidth: \"92vw\", background: \"var(--bg-raised)\", borderRadius: \"var(--radius-lg)\", border: \"1px solid var(--border-card)\", boxShadow: \"var(--shadow-modal)\", animation: \"kb-rise var(--dur) var(--ease-out)\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"flex-start\", justifyContent: \"space-between\", gap: 16, padding: \"24px 24px 0\" }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n <IconButton icon=\"x\" onClick={onClose} label=\"Close\" style={{ margin: \"calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0\" }} />\n </div>\n {children ? <div style={{ padding: \"20px 24px 0\" }}>{children}</div> : null}\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"var(--control-gap)\", padding: 24 }}>{footer}</div>\n </div>\n </div>\n );\n}\n\nconst toastTones = { success: { icon: \"circle-check\", color: \"var(--success-strong)\" }, danger: { icon: \"triangle-alert\", color: \"var(--danger)\" }, warning: { icon: \"triangle-alert\", color: \"var(--warning)\" }, info: { icon: \"info\", color: \"var(--action-primary)\" } };\n\n/** Single toast row. */\nexport function Toast({ tone = \"success\", title, meta, onClose, style }) {\n const t = toastTones[tone];\n return (\n <div style={{ display: \"flex\", alignItems: \"flex-start\", gap: 10, minWidth: 280, maxWidth: 380, background: \"var(--bg-raised)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", boxShadow: \"var(--shadow-menu)\", padding: \"12px 12px 12px 14px\", animation: \"kb-toast-in var(--dur) var(--ease-out)\", ...style }}>\n <span style={{ color: t.color, display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-base)\" }}><Icon name={t.icon} size={16} /></span>\n <div style={{ display: \"flex\", flexDirection: \"column\", flex: 1, minWidth: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {meta ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{meta}</span> : null}\n </div>\n {onClose ? <IconButton icon=\"x\" size=\"sm\" muted onClick={onClose} label=\"Dismiss\" style={{ margin: \"-2px 0\" }} /> : null}\n </div>\n );\n}\n\n/** Fixed bottom-right stack of toasts. */\nexport function ToastStack({ toasts = [], onDismiss, style }) {\n return (\n <div style={{ position: \"fixed\", right: 24, bottom: 24, zIndex: \"var(--z-toast)\", display: \"flex\", flexDirection: \"column\", gap: 8, ...style }}>\n {toasts.map((t) => <Toast key={t.id} tone={t.tone} title={t.title} meta={t.meta} onClose={() => onDismiss && onDismiss(t.id)} />)}\n </div>\n );\n}\n","feedback/Modal.vue":"<script setup>\nimport { px } from '../px.js'\nimport IconButton from '../buttons/IconButton.vue'\n// Modal dialog. A size region that defaults to lg (40px controls) whatever the page behind it uses — a dialog does not compete for space; pass `size` to override. 24px padding, footer buttons 8px apart.\ndefineProps({ open: Boolean, title: String, subtitle: String, width: { type: [Number, String], default: 440 }, size: { type: String, default: 'lg' }, style: Object })\nconst emit = defineEmits(['close'])\n</script>\n<template>\n <div v-if=\"open\" style=\"position:fixed;inset:0;z-index:var(--z-modal);background:var(--bg-scrim);display:flex;align-items:center;justify-content:center;animation:kb-fade-in var(--dur-fast) var(--ease-out)\" @click=\"emit('close')\">\n <div role=\"dialog\" aria-modal=\"true\" :data-size=\"size\" @click.stop :style=\"{ width: px(width), maxWidth: '92vw', background: 'var(--bg-raised)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-modal)', animation: 'kb-rise var(--dur) var(--ease-out)', ...style }\">\n <div style=\"display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:24px 24px 0\">\n <div style=\"display:flex;flex-direction:column;gap:2px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n <IconButton icon=\"x\" label=\"Close\" :style=\"{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }\" @click=\"emit('close')\" />\n </div>\n <div v-if=\"$slots.default\" style=\"padding:20px 24px 0\"><slot /></div>\n <div style=\"display:flex;justify-content:flex-end;gap:var(--control-gap);padding:24px\"><slot name=\"footer\" /></div>\n </div>\n </div>\n</template>\n","feedback/Stepper.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Wizard stepper (Upload & Parse → ... → Results). steps: [{label, icon}]; current index; connectors hairline #e5e5e5. */\nexport function Stepper({ steps = [], current = 0, style }) {\n return (\n <div style={{ display: \"flex\", alignItems: \"flex-start\", ...style }}>\n {steps.map((s, i) => {\n const done = i < current, is = i === current;\n return (\n <React.Fragment key={s.label}>\n {i > 0 ? <div style={{ flex: 1, height: 2, background: \"var(--track)\", marginTop: 15.25, marginLeft: 12, marginRight: 12, minWidth: 24 }} /> : null}\n <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 8, flexShrink: 0 }}>\n <span style={{ width: 32, height: 32, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: done ? \"var(--success)\" : is ? \"var(--action-primary)\" : \"var(--bg-surface)\", border: done || is ? \"none\" : \"2px solid var(--track)\", color: done || is ? \"var(--on-accent)\" : \"var(--text-muted)\" }}>\n <Icon name={done ? \"check\" : s.icon || \"file\"} size={16} />\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", fontWeight: is ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", whiteSpace: \"nowrap\" }}>{s.label}</span>\n </div>\n </React.Fragment>\n );\n })}\n </div>\n );\n}\n","feedback/Stepper.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Wizard stepper (Upload & Parse → ... → Results). steps: [{label, icon}]; current index; connectors hairline.\ndefineProps({ steps: { type: Array, default: () => [] }, current: { type: Number, default: 0 }, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'flex-start', ...style }\">\n <template v-for=\"(s, i) in steps\" :key=\"s.label\">\n <div v-if=\"i > 0\" style=\"flex:1;height:2px;background:var(--track);margin-top:15.25px;margin-left:12px;margin-right:12px;min-width:24px\" />\n <div style=\"display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0\">\n <span :style=\"{ width: '32px', height: '32px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: i < current ? 'var(--success)' : i === current ? 'var(--action-primary)' : 'var(--bg-surface)', border: (i < current || i === current) ? 'none' : '2px solid var(--track)', color: (i < current || i === current) ? 'var(--on-accent)' : 'var(--text-muted)' }\">\n <Icon :name=\"i < current ? 'check' : (s.icon || 'file')\" :size=\"16\" />\n </span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', fontWeight: i === current ? 'var(--weight-medium)' : 'var(--weight-regular)', color: i === current ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap' }\">{{ s.label }}</span>\n </div>\n </template>\n </div>\n</template>\n","feedback/Timeline.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst tlTones = { success: \"var(--success)\", danger: \"var(--danger)\", warning: \"var(--warning)\", info: \"var(--action-primary)\", neutral: \"var(--dot-neutral)\" };\n\n/** Vertical activity timeline (SLDS pattern): icon nodes on a hairline spine, 13px/500 title + 12px meta + optional body, newest first. items: [{icon,tone,title,meta,body}]. */\nexport function Timeline({ items = [], style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", ...style }}>\n {items.map((it, i) => (\n <div key={i} style={{ display: \"flex\", gap: 12 }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", flexShrink: 0 }}>\n <span style={{ width: 26, height: 26, borderRadius: \"var(--radius-full)\", background: \"var(--bg-surface)\", border: `1.5px solid ${tlTones[it.tone || \"neutral\"]}`, color: tlTones[it.tone || \"neutral\"], display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <Icon name={it.icon || \"clock\"} size={13} />\n </span>\n {i < items.length - 1 && <span style={{ width: 1.5, flex: 1, minHeight: 14, background: \"var(--border-control)\" }}></span>}\n </div>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2, paddingBottom: i < items.length - 1 ? 18 : 0, minWidth: 0, paddingTop: 3 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{it.title}</span>\n {it.meta && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\" }}>{it.meta}</span>}\n {it.body && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-body)\", paddingTop: 4 }}>{it.body}</span>}\n </div>\n </div>\n ))}\n </div>\n );\n}\n","feedback/Timeline.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nconst tlTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }\n// Vertical activity timeline: outlined icon nodes on a hairline spine. items: [{icon,tone,title,meta,body}].\ndefineProps({ items: { type: Array, default: () => [] }, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', ...style }\">\n <div v-for=\"(it, i) in items\" :key=\"i\" style=\"display:flex;gap:12px\">\n <div style=\"display:flex;flex-direction:column;align-items:center;flex-shrink:0\">\n <span :style=\"{ width: '26px', height: '26px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: `1.5px solid ${tlTones[it.tone || 'neutral']}`, color: tlTones[it.tone || 'neutral'], display: 'inline-flex', alignItems: 'center', justifyContent: 'center', boxSizing: 'border-box' }\"><Icon :name=\"it.icon || 'clock'\" :size=\"13\" /></span>\n <span v-if=\"i < items.length - 1\" style=\"width:1.5px;flex:1;min-height:14px;background:var(--border-control)\" />\n </div>\n <div :style=\"{ display: 'flex', flexDirection: 'column', gap: '2px', paddingBottom: i < items.length - 1 ? '18px' : 0, minWidth: 0, paddingTop: '3px' }\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ it.title }}</span>\n <span v-if=\"it.meta\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)\">{{ it.meta }}</span>\n <span v-if=\"it.body\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px\">{{ it.body }}</span>\n </div>\n </div>\n </div>\n</template>\n","feedback/DotActivity.vue":"<script setup>\nimport { computed } from 'vue'\nconst tonesMap = { success: 'var(--success)', warning: 'var(--warning)', danger: 'var(--danger)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }\nconst props = defineProps({ tone: { type: String, default: 'success' }, text: String, time: String, style: Object })\nconst dot = computed(() => tonesMap[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: '8px', alignItems: 'flex-start', ...style }\">\n <span :style=\"{ width: '8px', height: '8px', borderRadius: 'var(--radius-full)', background: dot, marginTop: '5px', flexShrink: 0 }\" />\n <div style=\"display:flex;flex-direction:column;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">{{ text }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)\">{{ time }}</span>\n </div>\n </div>\n</template>\n","feedback/EmptyState.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\ndefineProps({ icon: { type: String, default: 'file-text' }, title: String, hint: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '6px', padding: '40px 24px', textAlign: 'center', ...style }\">\n <span style=\"color:var(--text-muted);margin-bottom:6px\"><Icon :name=\"icon\" :size=\"22\" :stroke-width=\"1.5\" /></span>\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"hint\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-secondary);max-width:320px\">{{ hint }}</span>\n <span v-if=\"$slots.action\" style=\"padding-top:10px\"><slot name=\"action\" /></span>\n </div>\n</template>\n","feedback/LogViewer.vue":"<script setup>\ndefineProps({ lines: { type: Array, default: () => [] }, height: [Number, String], style: Object })\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-inset)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '6px', overflowY: 'auto', height, boxSizing: 'border-box', ...style }\">\n <span v-for=\"(l, i) in lines\" :key=\"i\" :style=\"{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', color: typeof l === 'object' ? (l.color || 'var(--text-body)') : 'var(--text-body)', whiteSpace: 'pre' }\">{{ typeof l === 'object' ? l.text : l }}</span>\n </div>\n</template>\n","feedback/Skeleton.vue":"<script setup>\nimport { px } from '../px.js'\ndefineProps({ width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: 12 }, radius: { type: String, default: 'var(--radius-sm)' }, style: Object })\n</script>\n<template>\n <span :style=\"{ display: 'block', width, height, borderRadius: px(radius), background: 'var(--skeleton)', animation: 'kb-pulse 1.4s var(--ease-in-out) infinite', ...style }\" />\n</template>\n","feedback/Spinner.vue":"<script setup>\nimport { px } from '../px.js'\ndefineProps({ size: { type: Number, default: 16 }, color: { type: String, default: 'var(--action-primary)' }, style: Object })\n</script>\n<template>\n <span :style=\"{ display: 'inline-block', width: px(size), height: px(size), borderRadius: 'var(--radius-full)', border: '2px solid var(--track)', borderTopColor: color, animation: 'kb-spin .7s linear infinite', ...style }\" />\n</template>\n","feedback/Toast.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nconst toastTones = { success: { icon: 'circle-check', color: 'var(--success-strong)' }, danger: { icon: 'triangle-alert', color: 'var(--danger)' }, warning: { icon: 'triangle-alert', color: 'var(--warning)' }, info: { icon: 'info', color: 'var(--action-primary)' } }\nconst props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, style: Object })\nconst emit = defineEmits(['close'])\nconst t = computed(() => toastTones[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: '280px', maxWidth: '380px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }\">\n <span :style=\"{ color: t.color, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }\"><Icon :name=\"t.icon\" :size=\"16\" /></span>\n <div style=\"display:flex;flex-direction:column;flex:1;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"meta\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ meta }}</span>\n </div>\n <IconButton v-if=\"dismissible\" icon=\"x\" size=\"sm\" muted label=\"Dismiss\" :style=\"{ margin: '-2px 0' }\" @click=\"emit('close')\" />\n </div>\n</template>\n","feedback/ToastStack.vue":"<script setup>\nimport Toast from './Toast.vue'\n// Fixed bottom-right stack of toasts. toasts: [{ id, tone, title, meta }]. Emits dismiss(id).\ndefineProps({ toasts: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['dismiss'])\n</script>\n<template>\n <div :style=\"{ position: 'fixed', right: '24px', bottom: '24px', zIndex: 'var(--z-toast)', display: 'flex', flexDirection: 'column', gap: '8px', ...style }\">\n <Toast v-for=\"t in toasts\" :key=\"t.id\" :tone=\"t.tone\" :title=\"t.title\" :meta=\"t.meta\" dismissible @close=\"emit('dismiss', t.id)\" />\n </div>\n</template>\n","feedback/Tooltip.vue":"<script setup>\nimport { ref, computed } from 'vue'\n// Hover tooltip around any inline content. placement: top | bottom | left | right. Dark pill, 11px, fades in.\nconst props = defineProps({ label: String, placement: { type: String, default: 'top' }, style: Object })\nconst show = ref(false)\nconst POS = {\n top: { bottom: '100%', left: '50%', transform: 'translateX(-50%)', marginBottom: '6px' },\n bottom: { top: '100%', left: '50%', transform: 'translateX(-50%)', marginTop: '6px' },\n left: { right: '100%', top: '50%', transform: 'translateY(-50%)', marginRight: '6px' },\n right: { left: '100%', top: '50%', transform: 'translateY(-50%)', marginLeft: '6px' }\n}\nconst pos = computed(() => POS[props.placement] || POS.top)\n</script>\n<template>\n <span :style=\"{ position: 'relative', display: 'inline-flex', ...style }\" @mouseenter=\"show = true\" @mouseleave=\"show = false\">\n <slot />\n <span v-if=\"show\" role=\"tooltip\" :style=\"{ position: 'absolute', zIndex: 'var(--z-tooltip)', whiteSpace: 'nowrap', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-md)', padding: '5px 8px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', boxShadow: 'var(--shadow-menu)', pointerEvents: 'none', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)', ...pos }\">{{ label }}</span>\n </span>\n</template>\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"feedback/Accordion.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Generic collapsible section: chevron + title/subtitle header, body slot. Controlled via `open`+`onToggle`, or uncontrolled via `defaultOpen`. */\nexport function Accordion({ title, subtitle, defaultOpen, open: openProp, onToggle, children, style }) {\n const [openState, setOpenState] = React.useState(!!defaultOpen);\n const open = openProp != null ? openProp : openState;\n const toggle = () => (onToggle ? onToggle(!open) : setOpenState(!open));\n return (\n <div style={{ borderBottom: \"1px solid var(--border-card)\", ...style }}>\n <div onClick={toggle} data-wash=\"\" style={{ display: \"flex\", alignItems: \"center\", gap: 10, padding: \"14px 8px\", margin: \"0 -4px\", borderRadius: \"var(--radius-md)\", background: \"var(--wash, transparent)\", transition: \"var(--t-hover)\", cursor: \"pointer\" }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: open ? \"rotate(90deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-right\" size={14} /></span>\n <div style={{ display: \"flex\", flexDirection: \"column\", flex: 1 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n </div>\n {open ? <div style={{ padding: \"0 4px 16px 28px\" }}>{children}</div> : null}\n </div>\n );\n}\n","feedback/Accordion.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Generic collapsible section: chevron + title/subtitle header, body slot. Controlled via `open` prop + `toggle` emit, or uncontrolled via `defaultOpen`.\nconst props = defineProps({ title: String, subtitle: String, defaultOpen: Boolean, open: { type: Boolean, default: null }, style: Object })\nconst emit = defineEmits(['toggle'])\nconst openState = ref(!!props.defaultOpen)\nconst open = computed(() => props.open != null ? props.open : openState.value)\nfunction toggle() { if (props.open != null) emit('toggle', !open.value); else openState.value = !openState.value }\n</script>\n<template>\n <div :style=\"{ borderBottom: '1px solid var(--border-card)', ...style }\">\n <div @click=\"toggle\" data-wash=\"\" style=\"display:flex;align-items:center;gap:10px;padding:14px 8px;margin:0 -4px;border-radius:var(--radius-md);background:var(--wash, transparent);transition:var(--t-hover);cursor:pointer\">\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-right\" :size=\"14\" /></span>\n <div style=\"display:flex;flex-direction:column;flex:1\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n </div>\n <div v-if=\"open\" style=\"padding:0 4px 16px 28px\"><slot /></div>\n </div>\n</template>\n","feedback/ActivityItem.jsx":"import React from \"react\";\nimport { IconTile } from \"../display/Badge.jsx\";\n\n/** Activity feed row: 32px icon tile + 14px/500 title + 12px meta + optional right text action (a text link: ink darkens on hover). */\nexport function ActivityItem({ icon = \"file-text\", title, meta, action, onAction, style }) {\n return (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"12px 16px\", ...style }}>\n <IconTile icon={icon} kind=\"row\" style={{ background: \"var(--info-bg)\", border: \"none\", color: \"var(--info-fg)\" }} />\n <div style={{ display: \"flex\", flexDirection: \"column\", flex: 1, minWidth: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {meta ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{meta}</span> : null}\n </div>\n {action ? <button type=\"button\" onClick={onAction} data-ink=\"\" style={{ border: \"none\", background: \"transparent\", padding: 0, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--ink, var(--text-secondary))\", cursor: \"pointer\", whiteSpace: \"nowrap\" }}>{action}</button> : null}\n </div>\n );\n}\n\n/** Compact dot activity row (Recent Activities card): 8px status dot + 12px text + 11px time. */\nexport function DotActivity({ tone = \"success\", text, time, style }) {\n const tones = { success: \"var(--success)\", warning: \"var(--warning)\", danger: \"var(--danger)\", info: \"var(--action-primary)\", neutral: \"var(--dot-neutral)\" };\n return (\n <div style={{ display: \"flex\", gap: 8, alignItems: \"flex-start\", ...style }}>\n <span style={{ width: 8, height: 8, borderRadius: \"var(--radius-full)\", background: tones[tone], marginTop: 5, flexShrink: 0 }} />\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-heading)\", overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\" }}>{text}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\" }}>{time}</span>\n </div>\n </div>\n );\n}\n\n/** Mono log lines (\"[14:30] Task started: Import\") — Menlo 11px on inset panel. */\nexport function LogViewer({ lines = [], height, style }) {\n return (\n <div style={{ background: \"var(--bg-inset)\", borderRadius: \"var(--radius-md)\", border: \"1px solid var(--border-subtle)\", padding: \"10px 12px\", display: \"flex\", flexDirection: \"column\", gap: 6, overflowY: \"auto\", height, boxSizing: \"border-box\", ...style }}>\n {lines.map((l, i) => <span key={i} style={{ fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: typeof l === \"object\" ? l.color || \"var(--text-body)\" : \"var(--text-body)\", whiteSpace: \"pre\" }}>{typeof l === \"object\" ? l.text : l}</span>)}\n </div>\n );\n}\n","feedback/ActivityItem.vue":"<script setup>\nimport IconTile from '../display/Badge.vue'\n// Activity feed row: 32px icon tile + 14px/500 title + 12px meta + optional right text action (emits `action`).\ndefineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, action: String, style: Object })\nconst emit = defineEmits(['action'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 16px', ...style }\">\n <IconTile :icon=\"icon\" kind=\"row\" style=\"background:var(--info-bg);border:none;color:var(--info-fg)\" />\n <div style=\"display:flex;flex-direction:column;flex:1;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"meta\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ meta }}</span>\n </div>\n <button v-if=\"action\" type=\"button\" data-ink=\"\" @click=\"emit('action')\" style=\"border:none;background:transparent;padding:0;font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--ink, var(--text-secondary));cursor:pointer;white-space:nowrap\">{{ action }}</button>\n </div>\n</template>\n","feedback/Alert.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst alertTones = {\n info: { bg: \"var(--info-bg)\", fg: \"var(--info-fg)\", icon: \"info\" },\n success: { bg: \"var(--success-bg)\", fg: \"var(--success-text)\", icon: \"circle-check\" },\n warning: { bg: \"var(--warning-bg)\", fg: \"var(--warning-text)\", icon: \"triangle-alert\" },\n danger: { bg: \"var(--danger-bg)\", fg: \"var(--danger-text)\", icon: \"triangle-alert\" },\n};\n\n/** Inline banner — tinted, no border; title and body at the base size. The icon is centred on the first text line, never on the block. */\nexport function Alert({ tone = \"info\", title, children, action, style }) {\n const t = alertTones[tone];\n return (\n <div style={{ display: \"flex\", gap: 10, alignItems: \"flex-start\", background: t.bg, borderRadius: \"var(--radius-md)\", padding: \"10px 12px\", ...style }}>\n <span style={{ color: t.fg, display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-base)\" }}><Icon name={t.icon} size={15} /></span>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2, flex: 1, minWidth: 0 }}>\n {title ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: t.fg }}>{title}</span> : null}\n {children ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-body)\" }}>{children}</span> : null}\n </div>\n {action}\n </div>\n );\n}\n\n/** Empty state — 22px muted glyph, 13px/500 title, 12px hint, optional action. */\nexport function EmptyState({ icon = \"file-text\", title, hint, action, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", gap: 6, padding: \"40px 24px\", textAlign: \"center\", ...style }}>\n <span style={{ color: \"var(--text-muted)\", marginBottom: 6 }}><Icon name={icon} size={22} strokeWidth={1.5} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {hint ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-secondary)\", maxWidth: 320 }}>{hint}</span> : null}\n {action ? <span style={{ paddingTop: 10 }}>{action}</span> : null}\n </div>\n );\n}\n\n/** Loading placeholder block. */\nexport function Skeleton({ width = \"100%\", height = 12, radius = \"var(--radius-sm)\", style }) {\n return <span style={{ display: \"block\", width, height, borderRadius: radius, background: \"var(--skeleton)\", animation: \"kb-pulse 1.4s var(--ease-in-out) infinite\", ...style }} />;\n}\n\n/** Indeterminate spinner (task-running affordance). */\nexport function Spinner({ size = 16, color = \"var(--action-primary)\", style }) {\n return (\n <span style={{ display: \"inline-block\", width: size, height: size, borderRadius: \"var(--radius-full)\", border: `2px solid var(--track)`, borderTopColor: color, animation: \"kb-spin .7s linear infinite\", ...style }} />\n );\n}\n","feedback/Alert.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst alertTones = {\n info: { bg: 'var(--info-bg)', fg: 'var(--info-fg)', icon: 'info' },\n success: { bg: 'var(--success-bg)', fg: 'var(--success-text)', icon: 'circle-check' },\n warning: { bg: 'var(--warning-bg)', fg: 'var(--warning-text)', icon: 'triangle-alert' },\n danger: { bg: 'var(--danger-bg)', fg: 'var(--danger-text)', icon: 'triangle-alert' },\n}\n// Alert — inline banner — tinted, no border, 13px title + 12px body.\nconst props = defineProps({ tone: { type: String, default: 'info' }, title: String, style: Object })\nconst t = computed(() => alertTones[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: '10px', alignItems: 'flex-start', background: t.bg, borderRadius: 'var(--radius-md)', padding: '10px 12px', ...style }\">\n <span :style=\"{ color: t.fg, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }\"><Icon :name=\"t.icon\" :size=\"15\" /></span>\n <div style=\"display:flex;flex-direction:column;gap:2px;flex:1;min-width:0\">\n <span v-if=\"title\" :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t.fg }\">{{ title }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body)\"><slot /></span>\n </div>\n <slot name=\"action\" />\n </div>\n</template>\n","feedback/Banner.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\nconst bannerTones = {\n info: { bg: \"var(--info-bg)\", fg: \"var(--info-fg)\", icon: \"info\" },\n success: { bg: \"var(--success-bg)\", fg: \"var(--success-text)\", icon: \"circle-check\" },\n warning: { bg: \"var(--warning-bg)\", fg: \"var(--warning-text)\", icon: \"triangle-alert\" },\n danger: { bg: \"var(--danger-bg)\", fg: \"var(--danger-text)\", icon: \"triangle-alert\" },\n};\n\n/** Page-level notification banner (Carbon/SLDS scoped notification): full-width tinted strip with icon, message, optional inline action link and dismiss ×. Distinct from Alert (inline, in-card) and Toast (floating). */\nexport function Banner({ tone = \"info\", title, action, onAction, dismissible, onDismiss, children, style }) {\n const t = bannerTones[tone];\n return (\n <div style={{ display: \"flex\", alignItems: \"flex-start\", gap: 10, background: t.bg, borderLeft: `3px solid ${t.fg}`, padding: \"10px 12px 10px 14px\", boxSizing: \"border-box\", ...style }}>\n <span style={{ color: t.fg, display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-base)\" }}><Icon name={t.icon} size={16} /></span>\n <span style={{ flex: 1, minWidth: 0, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-body)\" }}>\n {title && <strong style={{ fontWeight: \"var(--weight-medium)\", color: t.fg, paddingRight: 6 }}>{title}</strong>}{children}\n </span>\n {action && <button type=\"button\" onClick={onAction} data-link=\"\" style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", padding: 0, height: \"var(--leading-base)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", color: t.fg, textDecorationLine: \"underline\", textDecorationThickness: \"var(--link-thick, 1px)\", textUnderlineOffset: 2, whiteSpace: \"nowrap\", flexShrink: 0 }}>{action}</button>}\n {dismissible && <IconButton icon=\"x\" size=\"sm\" muted label=\"Dismiss\" onClick={onDismiss} style={{ margin: \"-2px 0\" }} />}\n </div>\n );\n}\n","feedback/Banner.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nconst bannerTones = {\n info: { bg: 'var(--info-bg)', fg: 'var(--info-fg)', icon: 'info' },\n success: { bg: 'var(--success-bg)', fg: 'var(--success-text)', icon: 'circle-check' },\n warning: { bg: 'var(--warning-bg)', fg: 'var(--warning-text)', icon: 'triangle-alert' },\n danger: { bg: 'var(--danger-bg)', fg: 'var(--danger-text)', icon: 'triangle-alert' },\n}\n// Page-level notification banner: tinted full-width strip, left accent bar, inline action link, dismiss ×.\nconst props = defineProps({ tone: { type: String, default: 'info' }, title: String, action: String, dismissible: Boolean, style: Object })\nconst emit = defineEmits(['action', 'dismiss'])\nconst t = computed(() => bannerTones[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'flex-start', gap: '10px', background: t.bg, borderLeft: `3px solid ${t.fg}`, padding: '10px 12px 10px 14px', boxSizing: 'border-box', ...style }\">\n <span :style=\"{ color: t.fg, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }\"><Icon :name=\"t.icon\" :size=\"16\" /></span>\n <span style=\"flex:1;min-width:0;font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body)\">\n <strong v-if=\"title\" :style=\"{ fontWeight: 'var(--weight-medium)', color: t.fg, paddingRight: '6px' }\">{{ title }}</strong><slot />\n </span>\n <button v-if=\"action\" type=\"button\" data-link=\"\" @click=\"emit('action')\" :style=\"{ border: 'none', background: 'transparent', cursor: 'pointer', padding: 0, height: 'var(--leading-base)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', color: t.fg, textDecorationLine: 'underline', textDecorationThickness: 'var(--link-thick, 1px)', textUnderlineOffset: '2px', whiteSpace: 'nowrap', flexShrink: 0 }\">{{ action }}</button>\n <IconButton v-if=\"dismissible\" icon=\"x\" size=\"sm\" muted label=\"Dismiss\" :style=\"{ margin: '-2px 0' }\" @click=\"emit('dismiss')\" />\n </div>\n</template>\n","feedback/ContextMenu.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.\n * selected: true marks the current choice of a single-choice menu (section switcher, language picker): --bg-hover fill, --weight-medium\n * --text-heading, no checkmark. A menu with any selected item spaces its rows 2px apart (like Sidebar / Tree / DocTree) so a hovered\n * fill never touches the selected one; an action menu (nothing selected) keeps its rows flush. Dividers keep 4px above and below. */\nexport function ContextMenu({ items = [], onSelect, size, style }) {\n const [hover, setHover] = React.useState(-1);\n const choice = items.some((it) => it && it.selected);\n return (\n <div role=\"menu\" data-size={size} style={{ background: \"var(--bg-surface)\", borderRadius: \"var(--radius-lg)\", border: \"1px solid var(--border-card)\", boxShadow: \"var(--shadow-menu)\", padding: 4, minWidth: 148, display: \"inline-flex\", flexDirection: \"column\", gap: choice ? 2 : 0, ...style }}>\n {items.map((it, i) => it.divider ? <div key={i} role=\"separator\" style={{ height: 1, flexShrink: 0, background: \"var(--border-subtle)\", margin: \"4px 0\" }} /> : (\n <button key={i} type=\"button\" onClick={() => onSelect && onSelect(it.label)}\n onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(-1)}\n role={choice ? \"menuitemradio\" : \"menuitem\"} aria-checked={choice ? !!it.selected : undefined} style={{ display: \"flex\", alignItems: \"center\", gap: 10, height: \"var(--control-h)\", flexShrink: 0, padding: \"0 10px\", boxSizing: \"border-box\", borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", background: it.selected || hover === i ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontWeight: it.selected ? \"var(--weight-medium)\" : \"var(--weight-regular)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: it.danger ? \"var(--danger)\" : \"var(--text-heading)\", textAlign: \"left\", whiteSpace: \"nowrap\" }}>\n {it.icon ? <Icon name={it.icon} size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /> : null}{it.label}\n </button>\n ))}\n </div>\n );\n}\n","feedback/ContextMenu.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.\n// selected: true marks the current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. A menu with any\n// selected item spaces its rows 2px apart (like Sidebar / Tree / DocTree); an action menu keeps its rows flush. Dividers keep 4px above and below.\nconst props = defineProps({ items: { type: Array, default: () => [] }, size: String, style: Object })\nconst choice = computed(() => props.items.some(it => it && it.selected))\nconst emit = defineEmits(['select'])\nconst hover = ref(-1)\n</script>\n<template>\n <div role=\"menu\" :data-size=\"size\" :style=\"{ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', display: 'inline-flex', flexDirection: 'column', gap: choice ? '2px' : 0, ...style }\">\n <template v-for=\"(it, i) in items\" :key=\"i\">\n <div v-if=\"it.divider\" role=\"separator\" :style=\"{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }\" />\n <button v-else type=\"button\" @click=\"emit('select', it.label)\" @mouseenter=\"hover = i\" @mouseleave=\"hover = -1\"\n :role=\"choice ? 'menuitemradio' : 'menuitem'\" :aria-checked=\"choice ? !!it.selected : undefined\" :style=\"{ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', flexShrink: 0, padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: it.selected || hover === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontWeight: it.selected ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap' }\">\n <Icon v-if=\"it.icon\" :name=\"it.icon\" :size=\"16\" :style=\"{ width: 'var(--control-icon)', height: 'var(--control-icon)' }\" />{{ it.label }}\n </button>\n </template>\n </div>\n</template>\n","feedback/Drawer.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Right slide-out panel over a scrim: title + subtitle, scrollable body, right-aligned footer slot. inline=true renders it in-flow for embedding in cards/demos. A size region (default inherit → md: drawers are working surfaces, not dialogs); pass `size` to override. */\nexport function Drawer({ open = true, title, subtitle, width = 400, inline, height, size, footer, onClose, children, style }) {\n if (!open) return null;\n const panel = (\n <div role=\"dialog\" aria-label={title} data-size={size} style={{ width, maxWidth: \"100%\", height: inline ? (height || 420) : \"100%\", background: \"var(--bg-raised)\", borderLeft: \"1px solid var(--border-card)\", boxShadow: inline ? \"var(--shadow-menu)\" : \"-8px 0 24px rgba(0,0,0,.12)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", ...(inline ? { border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\" } : null), ...style }}>\n <div style={{ display: \"flex\", alignItems: \"flex-start\", justifyContent: \"space-between\", padding: \"16px 16px 12px\", borderBottom: \"1px solid var(--border-card)\", flexShrink: 0 }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 3 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {subtitle && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{subtitle}</span>}\n </div>\n <IconButton icon=\"x\" label=\"Close\" onClick={onClose} style={{ margin: \"calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0\" }} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 16 }}>{children}</div>\n {footer && <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"var(--control-gap)\", padding: \"12px 16px\", borderTop: \"1px solid var(--border-card)\", flexShrink: 0 }}>{footer}</div>}\n </div>\n );\n if (inline) return panel;\n return (\n <div onClick={onClose} style={{ position: \"fixed\", inset: 0, zIndex: \"var(--z-modal)\", background: \"var(--bg-scrim)\", display: \"flex\", justifyContent: \"flex-end\", animation: \"kb-fade-in var(--dur-fast) var(--ease-out)\" }}>\n <div onClick={(e) => e.stopPropagation()} style={{ height: \"100%\", animation: \"kb-slide-in var(--dur) var(--ease-out)\" }}>{panel}</div>\n </div>\n );\n}\n","feedback/Drawer.vue":"<script setup>\nimport { px } from '../px.js'\nimport IconButton from '../buttons/IconButton.vue'\n// Right slide-out panel over a scrim: title + subtitle, scrollable body, right-aligned footer slot.\n// inline=true renders it in-flow (cards, demos); otherwise it teleports to <body> over a scrim. Click the scrim or × to emit close.\n// A size region (default inherit → md: drawers are working surfaces, not dialogs); pass `size` to override.\nconst props = defineProps({ open: { type: Boolean, default: true }, title: String, subtitle: String, width: { type: [Number, String], default: 400 }, inline: Boolean, height: { type: [Number, String], default: 420 }, size: String, style: Object })\nconst emit = defineEmits(['close'])\nconst contents = { display: 'contents' }\n</script>\n<template>\n <Teleport to=\"body\" :disabled=\"inline\">\n <div v-if=\"open\" @click=\"!inline && emit('close')\"\n :style=\"inline ? contents : { position: 'fixed', inset: 0, zIndex: 'var(--z-modal)', background: 'var(--bg-scrim)', display: 'flex', justifyContent: 'flex-end', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)' }\">\n <div @click.stop :style=\"inline ? contents : { height: '100%', animation: 'kb-slide-in var(--dur) var(--ease-out)' }\">\n <div role=\"dialog\" :aria-label=\"title\" :data-size=\"size\"\n :style=\"{ width: px(width), maxWidth: '100%', height: inline ? px(height) : '100%', background: 'var(--bg-raised)', borderLeft: '1px solid var(--border-card)', boxShadow: inline ? 'var(--shadow-menu)' : '-8px 0 24px rgba(0,0,0,.12)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', ...(inline ? { border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)' } : null), ...style }\">\n <div style=\"display:flex;align-items:flex-start;justify-content:space-between;padding:16px 16px 12px;border-bottom:1px solid var(--border-card);flex-shrink:0\">\n <div style=\"display:flex;flex-direction:column;gap:3px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n <IconButton icon=\"x\" label=\"Close\" :style=\"{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }\" @click=\"emit('close')\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:16px\"><slot /></div>\n <div v-if=\"$slots.footer\" style=\"display:flex;justify-content:flex-end;gap:var(--control-gap);padding:12px 16px;border-top:1px solid var(--border-card);flex-shrink:0\"><slot name=\"footer\" /></div>\n </div>\n </div>\n </div>\n </Teleport>\n</template>\n","feedback/Menu.jsx":"import React from \"react\";\nimport { ContextMenu } from \"./ContextMenu.jsx\";\n\n/** Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click. */\nexport function Menu({ trigger, items = [], onSelect, align = \"start\", style }) {\n const [open, setOpen] = React.useState(false);\n const ref = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };\n document.addEventListener(\"mousedown\", onDoc);\n return () => document.removeEventListener(\"mousedown\", onDoc);\n }, [open]);\n return (\n <div ref={ref} style={{ position: \"relative\", display: \"inline-flex\", ...style }}>\n <span onClick={() => setOpen((o) => !o)}>{trigger}</span>\n {open ? (\n <div style={{ position: \"absolute\", top: \"calc(100% + 6px)\", [align === \"end\" ? \"right\" : \"left\"]: 0, zIndex: \"var(--z-menu)\" }}>\n <ContextMenu items={items} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />\n </div>\n ) : null}\n </div>\n );\n}\n","feedback/Menu.vue":"<script setup>\nimport { ref, onMounted, onUnmounted } from 'vue'\nimport ContextMenu from './ContextMenu.vue'\n// Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click.\nconst props = defineProps({ items: { type: Array, default: () => [] }, align: { type: String, default: 'start' }, style: Object })\nconst emit = defineEmits(['select'])\nconst open = ref(false)\nconst root = ref(null)\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\nfunction select(l) { open.value = false; emit('select', l) }\n</script>\n<template>\n <div ref=\"root\" :style=\"{ position: 'relative', display: 'inline-flex', ...style }\">\n <span @click=\"open = !open\"><slot name=\"trigger\" /></span>\n <div v-if=\"open\" :style=\"{ position: 'absolute', top: 'calc(100% + 6px)', [align === 'end' ? 'right' : 'left']: 0, zIndex: 'var(--z-menu)' }\">\n <ContextMenu :items=\"items\" @select=\"select\" />\n </div>\n </div>\n</template>\n","feedback/Modal.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Hover tooltip — 11px, inverted surface, 6px offset. Wraps its children. */\nexport function Tooltip({ label, placement = \"top\", children, style }) {\n const [show, setShow] = React.useState(false);\n const pos = {\n top: { bottom: \"100%\", left: \"50%\", transform: \"translateX(-50%)\", marginBottom: 6 },\n bottom: { top: \"100%\", left: \"50%\", transform: \"translateX(-50%)\", marginTop: 6 },\n left: { right: \"100%\", top: \"50%\", transform: \"translateY(-50%)\", marginRight: 6 },\n right: { left: \"100%\", top: \"50%\", transform: \"translateY(-50%)\", marginLeft: 6 },\n }[placement];\n return (\n <span style={{ position: \"relative\", display: \"inline-flex\", ...style }} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>\n {children}\n {show ? (\n <span style={{ position: \"absolute\", zIndex: \"var(--z-tooltip)\", whiteSpace: \"nowrap\", background: \"var(--tooltip-bg)\", color: \"var(--tooltip-fg)\", borderRadius: \"var(--radius-md)\", padding: \"5px 8px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", boxShadow: \"var(--shadow-menu)\", pointerEvents: \"none\", animation: \"kb-fade-in var(--dur-fast) var(--ease-out)\", ...pos }}>{label}</span>\n ) : null}\n </span>\n );\n}\n\n/** Centered modal dialog with scrim. width defaults to 440. */\n/** Modal dialog. A size region that defaults to lg (40px controls) whatever the page behind it uses — a dialog does not compete for space; pass `size` to override. 24px padding, footer buttons 8px apart. */\nexport function Modal({ open, title, subtitle, onClose, footer, width = 440, size = \"lg\", children, style }) {\n if (!open) return null;\n return (\n <div style={{ position: \"fixed\", inset: 0, zIndex: \"var(--z-modal)\", background: \"var(--bg-scrim)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", animation: \"kb-fade-in var(--dur-fast) var(--ease-out)\" }} onClick={onClose}>\n <div role=\"dialog\" aria-modal=\"true\" data-size={size} onClick={(e) => e.stopPropagation()}\n style={{ width, maxWidth: \"92vw\", background: \"var(--bg-raised)\", borderRadius: \"var(--radius-lg)\", border: \"1px solid var(--border-card)\", boxShadow: \"var(--shadow-modal)\", animation: \"kb-rise var(--dur) var(--ease-out)\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"flex-start\", justifyContent: \"space-between\", gap: 16, padding: \"24px 24px 0\" }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n <IconButton icon=\"x\" onClick={onClose} label=\"Close\" style={{ margin: \"calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0\" }} />\n </div>\n {children ? <div style={{ padding: \"20px 24px 0\" }}>{children}</div> : null}\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"var(--control-gap)\", padding: 24 }}>{footer}</div>\n </div>\n </div>\n );\n}\n\nconst toastTones = { success: { icon: \"circle-check\", color: \"var(--success-strong)\" }, danger: { icon: \"triangle-alert\", color: \"var(--danger)\" }, warning: { icon: \"triangle-alert\", color: \"var(--warning)\" }, info: { icon: \"info\", color: \"var(--action-primary)\" } };\n\n/** Single toast row. */\nexport function Toast({ tone = \"success\", title, meta, onClose, style }) {\n const t = toastTones[tone];\n return (\n <div style={{ display: \"flex\", alignItems: \"flex-start\", gap: 10, minWidth: 280, maxWidth: 380, background: \"var(--bg-raised)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", boxShadow: \"var(--shadow-menu)\", padding: \"12px 12px 12px 14px\", animation: \"kb-toast-in var(--dur) var(--ease-out)\", ...style }}>\n <span style={{ color: t.color, display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-base)\" }}><Icon name={t.icon} size={16} /></span>\n <div style={{ display: \"flex\", flexDirection: \"column\", flex: 1, minWidth: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n {meta ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{meta}</span> : null}\n </div>\n {onClose ? <IconButton icon=\"x\" size=\"sm\" muted onClick={onClose} label=\"Dismiss\" style={{ margin: \"-2px 0\" }} /> : null}\n </div>\n );\n}\n\n/** Fixed bottom-right stack of toasts. */\nexport function ToastStack({ toasts = [], onDismiss, style }) {\n return (\n <div style={{ position: \"fixed\", right: 24, bottom: 24, zIndex: \"var(--z-toast)\", display: \"flex\", flexDirection: \"column\", gap: 8, ...style }}>\n {toasts.map((t) => <Toast key={t.id} tone={t.tone} title={t.title} meta={t.meta} onClose={() => onDismiss && onDismiss(t.id)} />)}\n </div>\n );\n}\n","feedback/Modal.vue":"<script setup>\nimport { px } from '../px.js'\nimport IconButton from '../buttons/IconButton.vue'\n// Modal dialog. A size region that defaults to lg (40px controls) whatever the page behind it uses — a dialog does not compete for space; pass `size` to override. 24px padding, footer buttons 8px apart.\ndefineProps({ open: Boolean, title: String, subtitle: String, width: { type: [Number, String], default: 440 }, size: { type: String, default: 'lg' }, style: Object })\nconst emit = defineEmits(['close'])\n</script>\n<template>\n <div v-if=\"open\" style=\"position:fixed;inset:0;z-index:var(--z-modal);background:var(--bg-scrim);display:flex;align-items:center;justify-content:center;animation:kb-fade-in var(--dur-fast) var(--ease-out)\" @click=\"emit('close')\">\n <div role=\"dialog\" aria-modal=\"true\" :data-size=\"size\" @click.stop :style=\"{ width: px(width), maxWidth: '92vw', background: 'var(--bg-raised)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-modal)', animation: 'kb-rise var(--dur) var(--ease-out)', ...style }\">\n <div style=\"display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:24px 24px 0\">\n <div style=\"display:flex;flex-direction:column;gap:2px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n <IconButton icon=\"x\" label=\"Close\" :style=\"{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }\" @click=\"emit('close')\" />\n </div>\n <div v-if=\"$slots.default\" style=\"padding:20px 24px 0\"><slot /></div>\n <div style=\"display:flex;justify-content:flex-end;gap:var(--control-gap);padding:24px\"><slot name=\"footer\" /></div>\n </div>\n </div>\n</template>\n","feedback/Stepper.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Wizard stepper (Upload & Parse → ... → Results). steps: [{label, icon}]; current index; connectors hairline #e5e5e5. */\nexport function Stepper({ steps = [], current = 0, style }) {\n return (\n <div style={{ display: \"flex\", alignItems: \"flex-start\", ...style }}>\n {steps.map((s, i) => {\n const done = i < current, is = i === current;\n return (\n <React.Fragment key={s.label}>\n {i > 0 ? <div style={{ flex: 1, height: 2, background: \"var(--track)\", marginTop: 15.25, marginLeft: 12, marginRight: 12, minWidth: 24 }} /> : null}\n <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 8, flexShrink: 0 }}>\n <span style={{ width: 32, height: 32, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: done ? \"var(--success)\" : is ? \"var(--action-primary)\" : \"var(--bg-surface)\", border: done || is ? \"none\" : \"2px solid var(--track)\", color: done || is ? \"var(--on-accent)\" : \"var(--text-muted)\" }}>\n <Icon name={done ? \"check\" : s.icon || \"file\"} size={16} />\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", fontWeight: is ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", whiteSpace: \"nowrap\" }}>{s.label}</span>\n </div>\n </React.Fragment>\n );\n })}\n </div>\n );\n}\n","feedback/Stepper.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Wizard stepper (Upload & Parse → ... → Results). steps: [{label, icon}]; current index; connectors hairline.\ndefineProps({ steps: { type: Array, default: () => [] }, current: { type: Number, default: 0 }, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'flex-start', ...style }\">\n <template v-for=\"(s, i) in steps\" :key=\"s.label\">\n <div v-if=\"i > 0\" style=\"flex:1;height:2px;background:var(--track);margin-top:15.25px;margin-left:12px;margin-right:12px;min-width:24px\" />\n <div style=\"display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0\">\n <span :style=\"{ width: '32px', height: '32px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: i < current ? 'var(--success)' : i === current ? 'var(--action-primary)' : 'var(--bg-surface)', border: (i < current || i === current) ? 'none' : '2px solid var(--track)', color: (i < current || i === current) ? 'var(--on-accent)' : 'var(--text-muted)' }\">\n <Icon :name=\"i < current ? 'check' : (s.icon || 'file')\" :size=\"16\" />\n </span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', fontWeight: i === current ? 'var(--weight-medium)' : 'var(--weight-regular)', color: i === current ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap' }\">{{ s.label }}</span>\n </div>\n </template>\n </div>\n</template>\n","feedback/Timeline.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst tlTones = { success: \"var(--success)\", danger: \"var(--danger)\", warning: \"var(--warning)\", info: \"var(--action-primary)\", neutral: \"var(--dot-neutral)\" };\n\n/** Vertical activity timeline (SLDS pattern): icon nodes on a hairline spine, 13px/500 title + 12px meta + optional body, newest first. items: [{icon,tone,title,meta,body}]. */\nexport function Timeline({ items = [], style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", ...style }}>\n {items.map((it, i) => (\n <div key={i} style={{ display: \"flex\", gap: 12 }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", flexShrink: 0 }}>\n <span style={{ width: 26, height: 26, borderRadius: \"var(--radius-full)\", background: \"var(--bg-surface)\", border: `1.5px solid ${tlTones[it.tone || \"neutral\"]}`, color: tlTones[it.tone || \"neutral\"], display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <Icon name={it.icon || \"clock\"} size={13} />\n </span>\n {i < items.length - 1 && <span style={{ width: 1.5, flex: 1, minHeight: 14, background: \"var(--border-control)\" }}></span>}\n </div>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 2, paddingBottom: i < items.length - 1 ? 18 : 0, minWidth: 0, paddingTop: 3 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{it.title}</span>\n {it.meta && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\" }}>{it.meta}</span>}\n {it.body && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-body)\", paddingTop: 4 }}>{it.body}</span>}\n </div>\n </div>\n ))}\n </div>\n );\n}\n","feedback/Timeline.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nconst tlTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }\n// Vertical activity timeline: outlined icon nodes on a hairline spine. items: [{icon,tone,title,meta,body}].\ndefineProps({ items: { type: Array, default: () => [] }, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', ...style }\">\n <div v-for=\"(it, i) in items\" :key=\"i\" style=\"display:flex;gap:12px\">\n <div style=\"display:flex;flex-direction:column;align-items:center;flex-shrink:0\">\n <span :style=\"{ width: '26px', height: '26px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: `1.5px solid ${tlTones[it.tone || 'neutral']}`, color: tlTones[it.tone || 'neutral'], display: 'inline-flex', alignItems: 'center', justifyContent: 'center', boxSizing: 'border-box' }\"><Icon :name=\"it.icon || 'clock'\" :size=\"13\" /></span>\n <span v-if=\"i < items.length - 1\" style=\"width:1.5px;flex:1;min-height:14px;background:var(--border-control)\" />\n </div>\n <div :style=\"{ display: 'flex', flexDirection: 'column', gap: '2px', paddingBottom: i < items.length - 1 ? '18px' : 0, minWidth: 0, paddingTop: '3px' }\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ it.title }}</span>\n <span v-if=\"it.meta\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)\">{{ it.meta }}</span>\n <span v-if=\"it.body\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px\">{{ it.body }}</span>\n </div>\n </div>\n </div>\n</template>\n","feedback/DotActivity.vue":"<script setup>\nimport { computed } from 'vue'\nconst tonesMap = { success: 'var(--success)', warning: 'var(--warning)', danger: 'var(--danger)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }\nconst props = defineProps({ tone: { type: String, default: 'success' }, text: String, time: String, style: Object })\nconst dot = computed(() => tonesMap[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: '8px', alignItems: 'flex-start', ...style }\">\n <span :style=\"{ width: '8px', height: '8px', borderRadius: 'var(--radius-full)', background: dot, marginTop: '5px', flexShrink: 0 }\" />\n <div style=\"display:flex;flex-direction:column;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">{{ text }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)\">{{ time }}</span>\n </div>\n </div>\n</template>\n","feedback/EmptyState.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\ndefineProps({ icon: { type: String, default: 'file-text' }, title: String, hint: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '6px', padding: '40px 24px', textAlign: 'center', ...style }\">\n <span style=\"color:var(--text-muted);margin-bottom:6px\"><Icon :name=\"icon\" :size=\"22\" :stroke-width=\"1.5\" /></span>\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"hint\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-secondary);max-width:320px\">{{ hint }}</span>\n <span v-if=\"$slots.action\" style=\"padding-top:10px\"><slot name=\"action\" /></span>\n </div>\n</template>\n","feedback/LogViewer.vue":"<script setup>\ndefineProps({ lines: { type: Array, default: () => [] }, height: [Number, String], style: Object })\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-inset)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '6px', overflowY: 'auto', height, boxSizing: 'border-box', ...style }\">\n <span v-for=\"(l, i) in lines\" :key=\"i\" :style=\"{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', color: typeof l === 'object' ? (l.color || 'var(--text-body)') : 'var(--text-body)', whiteSpace: 'pre' }\">{{ typeof l === 'object' ? l.text : l }}</span>\n </div>\n</template>\n","feedback/Skeleton.vue":"<script setup>\nimport { px } from '../px.js'\ndefineProps({ width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: 12 }, radius: { type: String, default: 'var(--radius-sm)' }, style: Object })\n</script>\n<template>\n <span :style=\"{ display: 'block', width, height, borderRadius: px(radius), background: 'var(--skeleton)', animation: 'kb-pulse 1.4s var(--ease-in-out) infinite', ...style }\" />\n</template>\n","feedback/Spinner.vue":"<script setup>\nimport { px } from '../px.js'\ndefineProps({ size: { type: Number, default: 16 }, color: { type: String, default: 'var(--action-primary)' }, style: Object })\n</script>\n<template>\n <span :style=\"{ display: 'inline-block', width: px(size), height: px(size), borderRadius: 'var(--radius-full)', border: '2px solid var(--track)', borderTopColor: color, animation: 'kb-spin .7s linear infinite', ...style }\" />\n</template>\n","feedback/Toast.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nconst toastTones = { success: { icon: 'circle-check', color: 'var(--success-strong)' }, danger: { icon: 'triangle-alert', color: 'var(--danger)' }, warning: { icon: 'triangle-alert', color: 'var(--warning)' }, info: { icon: 'info', color: 'var(--action-primary)' } }\nconst props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, style: Object })\nconst emit = defineEmits(['close'])\nconst t = computed(() => toastTones[props.tone])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: '280px', maxWidth: '380px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }\">\n <span :style=\"{ color: t.color, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }\"><Icon :name=\"t.icon\" :size=\"16\" /></span>\n <div style=\"display:flex;flex-direction:column;flex:1;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"meta\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)\">{{ meta }}</span>\n </div>\n <IconButton v-if=\"dismissible\" icon=\"x\" size=\"sm\" muted label=\"Dismiss\" :style=\"{ margin: '-2px 0' }\" @click=\"emit('close')\" />\n </div>\n</template>\n","feedback/ToastStack.vue":"<script setup>\nimport Toast from './Toast.vue'\n// Fixed bottom-right stack of toasts. toasts: [{ id, tone, title, meta }]. Emits dismiss(id).\ndefineProps({ toasts: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['dismiss'])\n</script>\n<template>\n <div :style=\"{ position: 'fixed', right: '24px', bottom: '24px', zIndex: 'var(--z-toast)', display: 'flex', flexDirection: 'column', gap: '8px', ...style }\">\n <Toast v-for=\"t in toasts\" :key=\"t.id\" :tone=\"t.tone\" :title=\"t.title\" :meta=\"t.meta\" dismissible @close=\"emit('dismiss', t.id)\" />\n </div>\n</template>\n","feedback/Tooltip.vue":"<script setup>\nimport { ref, computed } from 'vue'\n// Hover tooltip around any inline content. placement: top | bottom | left | right. Dark pill, 11px, fades in.\nconst props = defineProps({ label: String, placement: { type: String, default: 'top' }, style: Object })\nconst show = ref(false)\nconst POS = {\n top: { bottom: '100%', left: '50%', transform: 'translateX(-50%)', marginBottom: '6px' },\n bottom: { top: '100%', left: '50%', transform: 'translateX(-50%)', marginTop: '6px' },\n left: { right: '100%', top: '50%', transform: 'translateY(-50%)', marginRight: '6px' },\n right: { left: '100%', top: '50%', transform: 'translateY(-50%)', marginLeft: '6px' }\n}\nconst pos = computed(() => POS[props.placement] || POS.top)\n</script>\n<template>\n <span :style=\"{ position: 'relative', display: 'inline-flex', ...style }\" @mouseenter=\"show = true\" @mouseleave=\"show = false\">\n <slot />\n <span v-if=\"show\" role=\"tooltip\" :style=\"{ position: 'absolute', zIndex: 'var(--z-tooltip)', whiteSpace: 'nowrap', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-md)', padding: '5px 8px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', boxShadow: 'var(--shadow-menu)', pointerEvents: 'none', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)', ...pos }\">{{ label }}</span>\n </span>\n</template>\n"});