@keyfunc/keybricks-ds 1.0.1 → 1.0.3

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 (67) hide show
  1. package/INTEGRATION.md +42 -1
  2. package/_scripts/build-tokens.mjs +3 -3
  3. package/components/_shared/frame-theme.js +5 -0
  4. package/components/_shared/src-buttons.js +1 -1
  5. package/components/_shared/src-data.js +1 -1
  6. package/components/_shared/src-display.js +1 -1
  7. package/components/_shared/src-feedback.js +1 -1
  8. package/components/_shared/src-forms.js +1 -1
  9. package/components/_shared/src-navigation.js +1 -1
  10. package/components/_shared/src-pipeline.js +1 -1
  11. package/components/_shared/src-tabs.js +1 -1
  12. package/components/_vue/index.mjs +21 -0
  13. package/components/buttons/Button.vue +0 -20
  14. package/components/buttons/ButtonGroup.vue +16 -0
  15. package/components/data/KeyValue.vue +0 -16
  16. package/components/data/KeyValueList.vue +12 -0
  17. package/components/data/ListRow.vue +1 -1
  18. package/components/display/AreaChart.vue +14 -0
  19. package/components/display/Avatar.vue +0 -17
  20. package/components/display/AvatarGroup.vue +13 -0
  21. package/components/display/Badge.vue +0 -34
  22. package/components/display/Card.vue +0 -15
  23. package/components/display/Donut.vue +17 -0
  24. package/components/display/IconTile.vue +13 -0
  25. package/components/display/MiniBars.vue +11 -0
  26. package/components/display/ProgressBar.vue +0 -54
  27. package/components/display/StatCard.vue +11 -0
  28. package/components/display/StatusBadge.vue +11 -0
  29. package/components/feedback/ActivityItem.vue +1 -32
  30. package/components/feedback/Alert.vue +0 -36
  31. package/components/feedback/DotActivity.vue +15 -0
  32. package/components/feedback/EmptyState.vue +12 -0
  33. package/components/feedback/LogViewer.vue +8 -0
  34. package/components/feedback/Modal.vue +0 -45
  35. package/components/feedback/Skeleton.vue +6 -0
  36. package/components/feedback/Spinner.vue +6 -0
  37. package/components/feedback/Toast.vue +19 -0
  38. package/components/forms/Combobox.vue +1 -1
  39. package/components/forms/DatePicker.vue +1 -1
  40. package/components/forms/Input.jsx +2 -1
  41. package/components/forms/Input.vue +2 -24
  42. package/components/forms/Textarea.vue +20 -0
  43. package/components/forms/input-base.js +3 -0
  44. package/components/navigation/Breadcrumb.vue +0 -22
  45. package/components/navigation/PageHeading.vue +9 -0
  46. package/components/navigation/PageToolbar.vue +15 -0
  47. package/components/navigation/ThemeToggle.vue +18 -0
  48. package/components/navigation/TopBar.vue +0 -32
  49. package/components/pipeline/NodeCard.vue +2 -1
  50. package/components/pipeline/PaletteItem.vue +1 -1
  51. package/components/pipeline/PalettePanel.jsx +2 -8
  52. package/components/pipeline/PalettePanel.vue +1 -8
  53. package/components/pipeline/pipeline-catalog.js +8 -0
  54. package/components/tabs/PanelTabs.vue +10 -0
  55. package/components/tabs/PillTabs.vue +10 -0
  56. package/components/tabs/Tabs.vue +0 -28
  57. package/package.json +1 -1
  58. package/readme.md +1 -0
  59. package/tokens/alpha.css +0 -4
  60. package/tokens/charts.css +0 -6
  61. package/tokens/colors-extended.css +0 -1
  62. package/tokens/dark.css +4 -46
  63. package/tokens/interaction.css +0 -1
  64. package/tokens/theme.js +11 -0
  65. package/tokens/tokens.d.ts +1 -1
  66. package/tokens/tokens.js +1 -1
  67. package/tokens/tokens.json +2 -2
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"data/CodeBlock.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Monospace code/JSON block with a language label and copy-to-clipboard button (SQL Preview, export JSON). */\nexport function CodeBlock({ code = \"\", language, height, style }) {\n const [copied, setCopied] = React.useState(false);\n const copy = () => { try { navigator.clipboard && navigator.clipboard.writeText(code); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1400); };\n return (\n <div style={{ background: \"var(--bg-inset)\", border: \"1px solid var(--border-subtle)\", borderRadius: \"var(--radius-md)\", ...style }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", padding: \"7px 10px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\" }}>{language || \"code\"}</span>\n <button type=\"button\" onClick={copy} style={{ display: \"flex\", alignItems: \"center\", gap: 4, border: \"none\", background: \"transparent\", cursor: \"pointer\", color: copied ? \"var(--success-strong)\" : \"var(--text-secondary)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", padding: 0 }}>\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />{copied ? \"Copied\" : \"Copy\"}\n </button>\n </div>\n <pre style={{ margin: 0, padding: \"10px 12px\", overflow: \"auto\", height, fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-body)\" }}>{code}</pre>\n </div>\n );\n}\n","data/CodeBlock.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Monospace code/JSON block with a language label and copy-to-clipboard button (SQL Preview, export JSON).\nconst props = defineProps({ code: { type: String, default: '' }, language: String, height: [Number, String], style: Object })\nconst copied = ref(false)\nfunction copy() {\n try { navigator.clipboard && navigator.clipboard.writeText(props.code) } catch (e) {}\n copied.value = true; setTimeout(() => copied.value = false, 1400)\n}\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-inset)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', ...style }\">\n <div style=\"display:flex;justify-content:space-between;align-items:center;padding:7px 10px;border-bottom:1px solid var(--border-subtle)\">\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-muted)\">{{ language || 'code' }}</span>\n <button type=\"button\" @click=\"copy\" :style=\"{ display: 'flex', alignItems: 'center', gap: 4, border: 'none', background: 'transparent', cursor: 'pointer', color: copied ? 'var(--success-strong)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', padding: 0 }\">\n <Icon :name=\"copied ? 'check' : 'copy'\" :size=\"12\" />{{ copied ? 'Copied' : 'Copy' }}\n </button>\n </div>\n <pre :style=\"{ margin: 0, padding: '10px 12px', overflow: 'auto', height, fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', color: 'var(--text-body)' }\">{{ code }}</pre>\n </div>\n</template>\n","data/DataTable.jsx":"import React from \"react\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\n\n/** Keybricks data table. columns: [{key,label,width,mono,render}]. dense = 29px preview-drawer rows; default = 51.5px config rows. */\nexport function DataTable({ columns = [], rows = [], dense, selectable, selected = [], onToggle, headerBorder = true, style }) {\n const rowH = dense ? 29.5 : 51.5;\n const cellFont = (c) => c.mono\n ? { fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-heading)\" }\n : { fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-primary)\" };\n return (\n <table style={{ width: \"100%\", borderCollapse: \"collapse\", ...style }}>\n <thead>\n <tr style={{ background: \"var(--bg-inset)\", borderTop: headerBorder && !dense ? \"1px solid var(--border-control)\" : \"none\", borderBottom: headerBorder ? \"1px solid var(--border-control)\" : \"none\", height: dense ? 29 : 48.5 }}>\n {selectable ? <th style={{ width: 40, padding: \"0 0 0 16px\", textAlign: \"left\" }}><Checkbox size=\"sm\" checked={rows.length > 0 && selected.length === rows.length} onChange={(v) => onToggle && onToggle(\"all\", v)} /></th> : null}\n {columns.map((c) => (\n <th key={c.key} style={{ textAlign: c.align || \"left\", padding: \"0 12px\", width: c.width, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: dense ? \"var(--tracking-xs)\" : \".3px\", textTransform: dense ? \"none\" : \"uppercase\", color: \"var(--text-secondary)\", whiteSpace: \"nowrap\" }}>{c.label}</th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((r, ri) => (\n <tr key={ri} style={{ height: rowH, borderBottom: \"1px solid var(--border-subtle)\" }}>\n {selectable ? <td style={{ padding: \"0 0 0 16px\" }}><Checkbox size=\"sm\" checked={selected.includes(ri)} onChange={(v) => onToggle && onToggle(ri, v)} /></td> : null}\n {columns.map((c) => (\n <td key={c.key} style={{ padding: \"0 12px\", textAlign: c.align || \"left\", whiteSpace: \"nowrap\", ...cellFont(c) }}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n","data/DataTable.vue":"<script setup>\nimport { computed } from 'vue'\nimport Checkbox from '../forms/Checkbox.vue'\n// Keybricks data table. columns: [{key,label,width,mono,render}]. dense = 29px preview-drawer rows; default = 51.5px config rows.\nconst props = defineProps({ columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, dense: Boolean, selectable: Boolean, selected: { type: Array, default: () => [] }, headerBorder: { type: Boolean, default: true }, style: Object })\nconst emit = defineEmits(['toggle'])\nconst rowH = computed(() => props.dense ? 29.5 : 51.5)\nfunction cellFont(c) { return c.mono ? { fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', letterSpacing: 'var(--tracking-xs)', color: 'var(--text-heading)' } : { fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-primary)' } }\n</script>\n<template>\n <table :style=\"{ width: '100%', borderCollapse: 'collapse', ...style }\">\n <thead>\n <tr :style=\"{ background: 'var(--bg-inset)', borderTop: (headerBorder && !dense) ? '1px solid var(--border-control)' : 'none', borderBottom: headerBorder ? '1px solid var(--border-control)' : 'none', height: dense ? 29 : 48.5 }\">\n <th v-if=\"selectable\" style=\"width:40px;padding:0 0 0 16px;text-align:left\"><Checkbox size=\"sm\" :checked=\"rows.length > 0 && selected.length === rows.length\" @change=\"v => emit('toggle', 'all', v)\" /></th>\n <th v-for=\"c in columns\" :key=\"c.key\" :style=\"{ textAlign: c.align || 'left', padding: '0 12px', width: c.width, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: dense ? 'var(--tracking-xs)' : '.3px', textTransform: dense ? 'none' : 'uppercase', color: 'var(--text-secondary)', whiteSpace: 'nowrap' }\">{{ c.label }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(r, ri) in rows\" :key=\"ri\" :style=\"{ height: rowH, borderBottom: '1px solid var(--border-subtle)' }\">\n <td v-if=\"selectable\" style=\"padding:0 0 0 16px\"><Checkbox size=\"sm\" :checked=\"selected.includes(ri)\" @change=\"v => emit('toggle', ri, v)\" /></td>\n <td v-for=\"c in columns\" :key=\"c.key\" :style=\"{ padding: '0 12px', textAlign: c.align || 'left', whiteSpace: 'nowrap', ...cellFont(c) }\">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</td>\n </tr>\n </tbody>\n </table>\n</template>\n","data/DiffValue.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst toneColor = { neutral: \"var(--text-heading)\", warning: \"var(--warning-text)\", success: \"var(--success-text)\", danger: \"var(--danger-text)\" };\n\n/** Before → after value change (\"USD → EUR\"). tone colors the new value; from renders muted + struck through when both sides are given. */\nexport function DiffValue({ from, to, tone = \"warning\", mono = true, style }) {\n return (\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, fontFamily: mono ? \"var(--font-mono)\" : \"var(--font-sans)\", fontSize: \"var(--text-sm)\", ...style }}>\n {from != null ? <span style={{ color: \"var(--text-muted)\", textDecoration: \"line-through\" }}>{from}</span> : null}\n <Icon name=\"arrow-right\" size={12} style={{ color: \"var(--text-muted)\" }} />\n <span style={{ color: toneColor[tone], fontWeight: \"var(--weight-medium)\" }}>{to}</span>\n </span>\n );\n}\n","data/DiffValue.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nconst toneColor = { neutral: 'var(--text-heading)', warning: 'var(--warning-text)', success: 'var(--success-text)', danger: 'var(--danger-text)' }\n// Before → after value change (\"USD → EUR\"). tone colors the new value; from renders muted + struck through when both sides are given.\ndefineProps({ from: [String, Number], to: [String, Number], tone: { type: String, default: 'warning' }, mono: { type: Boolean, default: true }, style: Object })\n</script>\n<template>\n <span :style=\"{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 'var(--text-sm)', ...style }\">\n <span v-if=\"from != null\" style=\"color:var(--text-muted);text-decoration:line-through\">{{ from }}</span>\n <Icon name=\"arrow-right\" :size=\"12\" style=\"color:var(--text-muted)\" />\n <span :style=\"{ color: toneColor[tone], fontWeight: 'var(--weight-medium)' }\">{{ to }}</span>\n </span>\n</template>\n","data/FilterBar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Button } from \"../buttons/Button.jsx\";\n\n/** SAP Fiori-style list-report filter bar: variant title row with expand/collapse, a wrapping row of labeled filter fields (pass Field-wrapped controls as children), and Adapt Filters (n) + Go actions. */\nexport function FilterBar({ variant = \"Standard\", modified, adaptCount, defaultExpanded = true, onGo, onAdapt, children, style }) {\n const [open, setOpen] = React.useState(defaultExpanded);\n return (\n <div style={{ background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", padding: \"10px 16px 0\", boxSizing: \"border-box\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, paddingBottom: 10 }}>\n <button type=\"button\" onClick={() => setOpen(!open)} style={{ display: \"flex\", alignItems: \"center\", gap: 6, border: \"none\", background: \"transparent\", cursor: \"pointer\", padding: 0, color: \"var(--text-heading)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\" }}>\n {variant}{modified && <span style={{ color: \"var(--text-muted)\", fontWeight: \"var(--weight-regular)\" }}>*</span>}\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: open ? \"rotate(180deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-down\" size={14} /></span>\n </button>\n <span style={{ flex: 1 }}></span>\n <Button variant=\"text\" size=\"sm\" onClick={onAdapt}>Adapt Filters{adaptCount ? ` (${adaptCount})` : \"\"}</Button>\n <Button size=\"sm\" onClick={onGo}>Go</Button>\n </div>\n {open && <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: \"12px 16px\", paddingBottom: 14, borderTop: \"1px solid var(--border-subtle)\", paddingTop: 12 }}>{children}</div>}\n </div>\n );\n}\n","data/FilterBar.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Button from '../buttons/Button.vue'\n// SAP Fiori-style list-report filter bar: variant title + expand/collapse, wrapping labeled filter fields (slot), Adapt Filters (n) + Go.\nconst props = defineProps({ variant: { type: String, default: 'Standard' }, modified: Boolean, adaptCount: Number, defaultExpanded: { type: Boolean, default: true }, style: Object })\nconst emit = defineEmits(['go', 'adapt'])\nconst open = ref(props.defaultExpanded)\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-surface)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', padding: '10px 16px 0', boxSizing: 'border-box', ...style }\">\n <div style=\"display:flex;align-items:center;gap:6px;padding-bottom:10px\">\n <button type=\"button\" @click=\"open = !open\" style=\"display:flex;align-items:center;gap:6px;border:none;background:transparent;cursor:pointer;padding:0;color:var(--text-heading);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base)\">\n {{ variant }}<span v-if=\"modified\" style=\"color:var(--text-muted);font-weight:var(--weight-regular)\">*</span>\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-down\" :size=\"14\" /></span>\n </button>\n <span style=\"flex:1\" />\n <Button variant=\"text\" size=\"sm\" @click=\"emit('adapt')\">Adapt Filters{{ adaptCount ? ` (${adaptCount})` : '' }}</Button>\n <Button size=\"sm\" @click=\"emit('go')\">Go</Button>\n </div>\n <div v-if=\"open\" style=\"display:flex;flex-wrap:wrap;gap:12px 16px;padding-bottom:14px;border-top:1px solid var(--border-subtle);padding-top:12px\"><slot /></div>\n </div>\n</template>\n","data/KeyValue.jsx":"import React from \"react\";\n\n/** Label/value row (config summaries, confirmation steps). mono renders the value in Menlo. */\nexport function KeyValue({ label, value, mono, style }) {\n return (\n <div style={{ display: \"flex\", justifyContent: \"space-between\", gap: 12, padding: \"7px 0\", ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>{label}</span>\n <span style={{ fontFamily: mono ? \"var(--font-mono)\" : \"var(--font-sans)\", fontSize: mono ? \"var(--text-xs)\" : \"var(--text-sm)\", letterSpacing: mono ? \"var(--tracking-xs)\" : 0, color: \"var(--text-heading)\", textAlign: \"right\" }}>{value}</span>\n </div>\n );\n}\n\n/** Stack of KeyValue rows with hairline separators. rows: [{label,value,mono}]. */\nexport function KeyValueList({ rows = [], style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", ...style }}>\n {rows.map((r, i) => (\n <React.Fragment key={r.label}>\n <KeyValue {...r} />\n {i < rows.length - 1 ? <div style={{ height: 1, background: \"var(--border-subtle)\" }} /> : null}\n </React.Fragment>\n ))}\n </div>\n );\n}\n","data/KeyValue.vue":"<script setup>\n// Label/value row (config summaries, confirmation steps). mono renders the value in Menlo.\ndefineProps({ label: String, value: [String, Number], mono: Boolean, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', justifyContent: 'space-between', gap: 12, padding: '7px 0', ...style }\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ label }}</span>\n <span :style=\"{ fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: mono ? 'var(--text-xs)' : 'var(--text-sm)', letterSpacing: mono ? 'var(--tracking-xs)' : 0, color: 'var(--text-heading)', textAlign: 'right' }\">{{ value }}</span>\n </div>\n</template>\n\n<!-- KeyValueList — stack of KeyValue rows with hairline separators. rows: [{label,value,mono}]. -->\n<!--\n<script setup name=\"KeyValueList\">\nimport KeyValue from './KeyValue.vue'\ndefineProps({ rows: { type: Array, default: () => [] }, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', ...style }\">\n <template v-for=\"(r, i) in rows\" :key=\"r.label\">\n <KeyValue v-bind=\"r\" />\n <div v-if=\"i < rows.length - 1\" style=\"height:1px;background:var(--border-subtle)\" />\n </template>\n </div>\n</template>\n-->\n","data/ListRow.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile } from \"../display/Badge.jsx\";\n\n/** Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count. */\nexport function ListRow({ icon = \"file-text\", title, meta, trailing, expanded, onClick, children, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div style={{ borderBottom: \"1px solid var(--border-card)\", ...style }}>\n <div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"12px 16px\", cursor: \"pointer\", background: hover ? \"var(--bg-inset)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12 }}>\n <span style={{ color: \"var(--text-muted)\", transform: expanded ? \"rotate(90deg)\" : \"none\", transition: \"transform .15s ease\", display: \"inline-flex\" }}><Icon name=\"chevron-right\" size={16} /></span>\n <IconTile icon={icon} kind=\"row\" />\n <div style={{ display: \"flex\", flexDirection: \"column\" }}>\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)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>{meta}</span> : null}\n </div>\n </div>\n {trailing ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>{trailing}</span> : null}\n </div>\n {expanded && children ? <div style={{ padding: \"0 16px 12px 72px\" }}>{children}</div> : null}\n </div>\n );\n}\n","data/ListRow.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { IconTile } from '../display/Badge.vue'\n// Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count.\ndefineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div :style=\"{ borderBottom: '1px solid var(--border-card)', ...style }\">\n <div @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" :style=\"{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', cursor: 'pointer', background: hover ? 'var(--bg-inset)' : 'transparent', transition: 'var(--t-hover)' }\">\n <div style=\"display:flex;align-items:center;gap:12px\">\n <span :style=\"{ color: 'var(--text-muted)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform .15s ease', display: 'inline-flex' }\"><Icon name=\"chevron-right\" :size=\"16\" /></span>\n <IconTile :icon=\"icon\" kind=\"row\" />\n <div style=\"display:flex;flex-direction:column\">\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-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ meta }}</span>\n </div>\n </div>\n <span v-if=\"trailing\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ trailing }}</span>\n </div>\n <div v-if=\"expanded\" style=\"padding:0 16px 12px 72px\"><slot /></div>\n </div>\n</template>\n","data/Pagination.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Table pagination — 12px range label + prev/next + numbered pages. */\nexport function Pagination({ page = 1, pageCount = 1, total, pageSize = 20, onChange, style }) {\n const btn = (content, disabled, onClick, active) => (\n <button type=\"button\" disabled={disabled} onClick={onClick}\n style={{ minWidth: 26, height: 26, padding: \"0 6px\", borderRadius: \"var(--radius-sm)\", border: `1px solid ${active ? \"var(--action-primary)\" : \"var(--border-control)\"}`, background: active ? \"var(--action-primary)\" : \"var(--bg-surface)\", color: active ? \"var(--on-accent)\" : \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", cursor: disabled ? \"default\" : \"pointer\", opacity: disabled ? .45 : 1, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", transition: \"var(--t-hover)\" }}>\n {content}\n </button>\n );\n const pages = [];\n const from = Math.max(1, Math.min(page - 1, pageCount - 2));\n for (let i = from; i < from + 3 && i <= pageCount; i++) pages.push(i);\n return (\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 12, ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>\n {total != null ? `${(page - 1) * pageSize + 1}–${Math.min(page * pageSize, total)} of ${total}` : `Page ${page} of ${pageCount}`}\n </span>\n <div style={{ display: \"flex\", gap: 4 }}>\n {btn(<Icon name=\"chevron-left\" size={13} />, page <= 1, () => onChange && onChange(page - 1))}\n {pages.map((p) => <React.Fragment key={p}>{btn(p, false, () => onChange && onChange(p), p === page)}</React.Fragment>)}\n {btn(<Icon name=\"chevron-right\" size={13} />, page >= pageCount, () => onChange && onChange(page + 1))}\n </div>\n </div>\n );\n}\n","data/Pagination.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Table pagination — 12px range label + prev/next + numbered pages.\nconst props = defineProps({ page: { type: Number, default: 1 }, pageCount: { type: Number, default: 1 }, total: Number, pageSize: { type: Number, default: 20 }, style: Object })\nconst emit = defineEmits(['change'])\nconst pages = computed(() => {\n const from = Math.max(1, Math.min(props.page - 1, props.pageCount - 2)), out = []\n for (let i = from; i < from + 3 && i <= props.pageCount; i++) out.push(i)\n return out\n})\nconst label = computed(() => props.total != null ? `${(props.page - 1) * props.pageSize + 1}–${Math.min(props.page * props.pageSize, props.total)} of ${props.total}` : `Page ${props.page} of ${props.pageCount}`)\nfunction btnStyle(active, disabled) { return { minWidth: 26, height: 26, padding: '0 6px', borderRadius: 'var(--radius-sm)', border: `1px solid ${active ? 'var(--action-primary)' : 'var(--border-control)'}`, background: active ? 'var(--action-primary)' : 'var(--bg-surface)', color: active ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .45 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'var(--t-hover)' } }\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, ...style }\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ label }}</span>\n <div style=\"display:flex;gap:4px\">\n <button type=\"button\" :disabled=\"page <= 1\" @click=\"emit('change', page - 1)\" :style=\"btnStyle(false, page <= 1)\"><Icon name=\"chevron-left\" :size=\"13\" /></button>\n <button v-for=\"p in pages\" :key=\"p\" type=\"button\" @click=\"emit('change', p)\" :style=\"btnStyle(p === page, false)\">{{ p }}</button>\n <button type=\"button\" :disabled=\"page >= pageCount\" @click=\"emit('change', page + 1)\" :style=\"btnStyle(false, page >= pageCount)\"><Icon name=\"chevron-right\" :size=\"13\" /></button>\n </div>\n </div>\n</template>\n","data/SmartTable.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\nimport { Avatar } from \"../display/Avatar.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\nconst typeIcon = { text: \"type\", longText: \"align-left\", number: \"hash\", select: \"chevron-down\", multiSelect: \"tag\", date: \"calendar\", person: \"user\", checkbox: \"check-square\", url: \"link\", progress: \"bar-chart-2\" };\nconst Pill = ({ hue = \"gray\", children }) => <span style={{ display: \"inline-flex\", alignItems: \"center\", height: 20, padding: \"0 8px\", borderRadius: \"var(--radius-full)\", background: `var(--hue-${hue}-bg)`, color: `var(--hue-${hue}-fg)`, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-xs)\", lineHeight: 1, whiteSpace: \"nowrap\" }}>{children}</span>;\n\n/** Notion/Airtable-style database grid (\"smart table\"): field-type icons in headers, colored select pills, person/date/checkbox/url/progress cells, row hover + expand, inline text/number editing, + New row, add-field column, per-column footer aggregates, and a view toolbar (Hide fields · Filter · Group · Sort · search). */\nexport function SmartTable({ fields = [], rows = [], title = \"Grid view\", toolbar = true, height, rowHeight = 32, onChange, style }) {\n const [data, setData] = React.useState(rows);\n const [hover, setHover] = React.useState(-1);\n const [editing, setEditing] = React.useState(null);\n const [draft, setDraft] = React.useState(\"\");\n const setCell = (r, key, v) => { const next = data.map((row, i) => i === r ? { ...row, [key]: v } : row); setData(next); onChange && onChange(next); };\n const widths = [\"40px\", ...fields.map(f => (f.width || 140) + \"px\"), \"40px\"];\n const grid = { display: \"grid\", gridTemplateColumns: widths.join(\" \"), width: \"fit-content\", minWidth: \"100%\" };\n const cellBase = { display: \"flex\", alignItems: \"center\", gap: 5, padding: \"0 8px\", borderRight: \"1px solid var(--border-subtle)\", borderBottom: \"1px solid var(--border-subtle)\", height: rowHeight, boxSizing: \"border-box\", overflow: \"hidden\", whiteSpace: \"nowrap\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-primary)\" };\n const agg = (f) => {\n if (!f.aggregate) return \"\";\n const vals = data.map(r => r[f.key]);\n if (f.aggregate === \"count\") return \"COUNT \" + vals.filter(v => v === true || (v !== \"\" && v != null && v !== false)).length;\n const nums = vals.map(Number).filter(n => !isNaN(n));\n if (f.aggregate === \"sum\") return \"SUM \" + nums.reduce((a, b) => a + b, 0);\n if (f.aggregate === \"avg\") return \"AVG \" + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0);\n return \"\";\n };\n const renderCell = (f, row, r) => {\n const v = row[f.key];\n const isEdit = editing && editing.r === r && editing.key === f.key;\n if (isEdit) return <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { setCell(r, f.key, f.type === \"number\" && draft !== \"\" && !isNaN(Number(draft)) ? Number(draft) : draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === \"Enter\") e.target.blur(); if (e.key === \"Escape\") setEditing(null); }} style={{ position: \"absolute\", inset: 0, border: \"2px solid var(--action-primary)\", borderRadius: 2, outline: \"none\", padding: \"0 6px\", font: \"inherit\", background: \"var(--bg-surface)\", color: \"var(--text-primary)\", boxSizing: \"border-box\" }} />;\n switch (f.type) {\n case \"select\": { const hue = (f.options || {})[v] || \"gray\"; return v ? <Pill hue={hue}>{v}</Pill> : null; }\n case \"multiSelect\": return <span style={{ display: \"flex\", gap: 4, overflow: \"hidden\" }}>{(v || []).map(t => <Pill key={t} hue={(f.options || {})[t] || \"gray\"}>{t}</Pill>)}</span>;\n case \"person\": return v ? <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, minWidth: 0 }}><Avatar name={v} size={20} /><span style={{ overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{v}</span></span> : null;\n case \"checkbox\": return <Checkbox size=\"sm\" checked={!!v} onChange={(nv) => setCell(r, f.key, nv)} />;\n case \"url\": return v ? <a href={\"https://\" + v} onClick={(e) => e.preventDefault()} style={{ color: \"var(--link)\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{v}</a> : null;\n case \"progress\": return <span style={{ display: \"flex\", alignItems: \"center\", gap: 6, width: \"100%\" }}><span style={{ flex: 1, height: 4, borderRadius: \"var(--radius-full)\", background: \"var(--track)\", overflow: \"hidden\" }}><span style={{ display: \"block\", width: (v || 0) + \"%\", height: \"100%\", background: \"var(--chart-blue)\" }}></span></span><span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-secondary)\" }}>{v || 0}%</span></span>;\n case \"number\": return <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: 11.5 }}>{v}</span>;\n default: return <span style={{ overflow: \"hidden\", textOverflow: \"ellipsis\", fontWeight: f.primary ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: f.primary ? \"var(--text-heading)\" : \"var(--text-primary)\" }}>{v}</span>;\n }\n };\n const tbBtn = (icon, label) => <button key={label} type=\"button\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: 5, height: 26, padding: \"0 8px\", border: \"none\", borderRadius: \"var(--radius-sm)\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\" }} onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}><Icon name={icon} size={13} />{label}</button>;\n return (\n <div style={{ border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-lg)\", background: \"var(--bg-surface)\", overflow: \"hidden\", boxSizing: \"border-box\", ...style }}>\n {toolbar && <div style={{ display: \"flex\", alignItems: \"center\", gap: 4, padding: \"6px 10px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <button type=\"button\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, height: 26, padding: \"0 8px\", border: \"none\", borderRadius: \"var(--radius-sm)\", background: \"var(--bg-active)\", cursor: \"pointer\", color: \"var(--text-heading)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\" }}><Icon name=\"layout-grid\" size={13} />{title}<Icon name=\"chevron-down\" size={12} /></button>\n <span style={{ width: 1, height: 16, background: \"var(--border-control)\", margin: \"0 4px\" }}></span>\n {tbBtn(\"eye-off\", \"Hide fields\")}{tbBtn(\"funnel\", \"Filter\")}{tbBtn(\"layers\", \"Group\")}{tbBtn(\"arrow-down\", \"Sort\")}\n <span style={{ flex: 1 }}></span>\n <SearchInput bare placeholder=\"Search...\" style={{ width: 140, gap: 6 }} />\n </div>}\n <div style={{ overflow: \"auto\", maxHeight: height }}>\n <div style={grid}>\n <span style={{ ...cellBase, background: \"var(--bg-inset)\", justifyContent: \"center\" }}></span>\n {fields.map(f => <span key={f.key} style={{ ...cellBase, background: \"var(--bg-inset)\", color: \"var(--text-secondary)\", fontSize: \"var(--text-xs)\", fontWeight: \"var(--weight-medium)\", letterSpacing: \"var(--tracking-xs)\" }}><Icon name={typeIcon[f.type] || \"type\"} size={12} style={{ color: \"var(--text-muted)\" }} />{f.label}</span>)}\n <span style={{ ...cellBase, background: \"var(--bg-inset)\", justifyContent: \"center\" }}><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add field\" /></span>\n {data.map((row, r) => [\n <span key={\"n\" + r} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} style={{ ...cellBase, justifyContent: \"center\", color: \"var(--text-muted)\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", background: hover === r ? \"var(--bg-inset)\" : \"transparent\", cursor: \"pointer\" }}>{hover === r ? <Icon name=\"maximize-2\" size={12} style={{ color: \"var(--action-primary)\" }} /> : r + 1}</span>,\n ...fields.map(f => <span key={r + f.key} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} onDoubleClick={() => { if (f.type === \"text\" || f.type === \"number\" || f.type === \"date\" || f.type === \"url\") { setEditing({ r, key: f.key }); setDraft(String(row[f.key] == null ? \"\" : row[f.key])); } }} style={{ ...cellBase, position: \"relative\", background: hover === r ? \"var(--bg-inset)\" : \"transparent\" }}>{renderCell(f, row, r)}</span>),\n <span key={\"a\" + r} style={{ ...cellBase, background: hover === r ? \"var(--bg-inset)\" : \"transparent\" }}></span>,\n ])}\n <button type=\"button\" onClick={() => { const empty = {}; fields.forEach(f => empty[f.key] = f.type === \"multiSelect\" ? [] : f.type === \"checkbox\" ? false : \"\"); const next = [...data, empty]; setData(next); onChange && onChange(next); }} style={{ gridColumn: \"1 / -1\", display: \"flex\", alignItems: \"center\", gap: 6, height: rowHeight, padding: \"0 10px\", border: \"none\", borderBottom: \"1px solid var(--border-subtle)\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-secondary)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", textAlign: \"left\" }} onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-inset)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}><Icon name=\"plus\" size={13} />New</button>\n <span style={{ ...cellBase, borderBottom: \"none\", background: \"var(--bg-inset)\" }}></span>\n {fields.map(f => <span key={\"g\" + f.key} style={{ ...cellBase, borderBottom: \"none\", background: \"var(--bg-inset)\", justifyContent: \"flex-end\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-secondary)\" }}>{agg(f)}</span>)}\n <span style={{ ...cellBase, borderBottom: \"none\", background: \"var(--bg-inset)\" }}></span>\n </div>\n </div>\n </div>\n );\n}\n","data/SmartTable.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport Checkbox from '../forms/Checkbox.vue'\nimport Avatar from '../display/Avatar.vue'\nimport SearchInput from '../forms/SearchInput.vue'\n// Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar.\nconst typeIcon = { text: 'type', longText: 'align-left', number: 'hash', select: 'chevron-down', multiSelect: 'tag', date: 'calendar', person: 'user', checkbox: 'check-square', url: 'link', progress: 'bar-chart-2' }\nconst props = defineProps({ fields: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, title: { type: String, default: 'Grid view' }, toolbar: { type: Boolean, default: true }, height: [Number, String], rowHeight: { type: Number, default: 32 }, style: Object })\nconst emit = defineEmits(['change'])\nconst data = ref(props.rows.map(r => ({ ...r })))\nconst hover = ref(-1), editing = ref(null), draft = ref('')\nconst gridCols = computed(() => ['40px', ...props.fields.map(f => (f.width || 140) + 'px'), '40px'].join(' '))\nconst cellBase = { display: 'flex', alignItems: 'center', gap: '5px', padding: '0 8px', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', boxSizing: 'border-box', overflow: 'hidden', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-primary)' }\nfunction setCell(r, key, v) { data.value[r][key] = v; emit('change', data.value) }\nfunction commitEdit(r, f) { setCell(r, f.key, f.type === 'number' && draft.value !== '' && !isNaN(Number(draft.value)) ? Number(draft.value) : draft.value); editing.value = null }\nfunction startEdit(r, f, row) { if (['text', 'number', 'date', 'url'].includes(f.type || 'text')) { editing.value = { r, key: f.key }; draft.value = String(row[f.key] == null ? '' : row[f.key]) } }\nfunction addRow() { const empty = {}; props.fields.forEach(f => empty[f.key] = f.type === 'multiSelect' ? [] : f.type === 'checkbox' ? false : ''); data.value.push(empty); emit('change', data.value) }\nfunction agg(f) {\n if (!f.aggregate) return ''\n const vals = data.value.map(r => r[f.key])\n if (f.aggregate === 'count') return 'COUNT ' + vals.filter(v => v === true || (v !== '' && v != null && v !== false)).length\n const nums = vals.map(Number).filter(n => !isNaN(n))\n if (f.aggregate === 'sum') return 'SUM ' + nums.reduce((a, b) => a + b, 0)\n return 'AVG ' + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0)\n}\n</script>\n<template>\n <div :style=\"{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', background: 'var(--bg-surface)', overflow: 'hidden', boxSizing: 'border-box', ...style }\">\n <div v-if=\"toolbar\" style=\"display:flex;align-items:center;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border-subtle)\">\n <button type=\"button\" style=\"display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:var(--bg-active);cursor:pointer;color:var(--text-heading);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm)\"><Icon name=\"layout-grid\" :size=\"13\" />{{ title }}<Icon name=\"chevron-down\" :size=\"12\" /></button>\n <span style=\"width:1px;height:16px;background:var(--border-control);margin:0 4px\" />\n <button v-for=\"b in [['eye-off','Hide fields'],['funnel','Filter'],['layers','Group'],['arrow-down','Sort']]\" :key=\"b[1]\" type=\"button\" class=\"kb-st-btn\"><Icon :name=\"b[0]\" :size=\"13\" />{{ b[1] }}</button>\n <span style=\"flex:1\" />\n <SearchInput bare placeholder=\"Search...\" style=\"width:140px;gap:6px\" />\n </div>\n <div :style=\"{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }\">\n <div :style=\"{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content', minWidth: '100%' }\">\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)' }\" />\n <span v-for=\"f in fields\" :key=\"f.key\" :style=\"{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', color: 'var(--text-secondary)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)' }\"><Icon :name=\"typeIcon[f.type] || 'type'\" :size=\"12\" style=\"color:var(--text-muted)\" />{{ f.label }}</span>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', justifyContent: 'center' }\"><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add field\" /></span>\n <template v-for=\"(row, r) in data\" :key=\"r\">\n <span @mouseenter=\"hover = r\" @mouseleave=\"hover = -1\" :style=\"{ ...cellBase, height: rowHeight + 'px', justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', background: hover === r ? 'var(--bg-inset)' : 'transparent', cursor: 'pointer' }\">\n <Icon v-if=\"hover === r\" name=\"maximize-2\" :size=\"12\" style=\"color:var(--action-primary)\" /><template v-else>{{ r + 1 }}</template>\n </span>\n <span v-for=\"f in fields\" :key=\"r + f.key\" @mouseenter=\"hover = r\" @mouseleave=\"hover = -1\" @dblclick=\"startEdit(r, f, row)\" :style=\"{ ...cellBase, height: rowHeight + 'px', position: 'relative', background: hover === r ? 'var(--bg-inset)' : 'transparent' }\">\n <input v-if=\"editing && editing.r === r && editing.key === f.key\" autofocus :value=\"draft\" @input=\"draft = $event.target.value\" @blur=\"commitEdit(r, f)\" @keydown.enter=\"$event.target.blur()\" @keydown.esc=\"editing = null\" style=\"position:absolute;inset:0;border:2px solid var(--action-primary);border-radius:2px;outline:none;padding:0 6px;font:inherit;background:var(--bg-surface);color:var(--text-primary);box-sizing:border-box\" />\n <template v-else-if=\"f.type === 'select'\"><span v-if=\"row[f.key]\" class=\"kb-pill\" :style=\"{ background: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-fg)` }\">{{ row[f.key] }}</span></template>\n <template v-else-if=\"f.type === 'multiSelect'\"><span style=\"display:flex;gap:4px;overflow:hidden\"><span v-for=\"t in row[f.key] || []\" :key=\"t\" class=\"kb-pill\" :style=\"{ background: `var(--hue-${(f.options || {})[t] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[t] || 'gray'}-fg)` }\">{{ t }}</span></span></template>\n <template v-else-if=\"f.type === 'person'\"><span v-if=\"row[f.key]\" style=\"display:inline-flex;align-items:center;gap:6px;min-width:0\"><Avatar :name=\"row[f.key]\" :size=\"20\" /><span style=\"overflow:hidden;text-overflow:ellipsis\">{{ row[f.key] }}</span></span></template>\n <template v-else-if=\"f.type === 'checkbox'\"><Checkbox size=\"sm\" :checked=\"!!row[f.key]\" @change=\"v => setCell(r, f.key, v)\" /></template>\n <template v-else-if=\"f.type === 'url'\"><a v-if=\"row[f.key]\" href=\"#\" @click.prevent style=\"overflow:hidden;text-overflow:ellipsis\">{{ row[f.key] }}</a></template>\n <template v-else-if=\"f.type === 'progress'\"><span style=\"display:flex;align-items:center;gap:6px;width:100%\"><span style=\"flex:1;height:4px;border-radius:var(--radius-full);background:var(--track);overflow:hidden\"><span :style=\"{ display: 'block', width: (row[f.key] || 0) + '%', height: '100%', background: 'var(--chart-blue)' }\" /></span><span style=\"font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-secondary)\">{{ row[f.key] || 0 }}%</span></span></template>\n <template v-else-if=\"f.type === 'number'\"><span style=\"margin-left:auto;font-family:var(--font-mono);font-size:11.5px\">{{ row[f.key] }}</span></template>\n <template v-else><span :style=\"{ overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: f.primary ? 'var(--weight-medium)' : 'var(--weight-regular)', color: f.primary ? 'var(--text-heading)' : 'var(--text-primary)' }\">{{ row[f.key] }}</span></template>\n </span>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', background: hover === r ? 'var(--bg-inset)' : 'transparent' }\" />\n </template>\n <button type=\"button\" @click=\"addRow\" class=\"kb-st-new\" :style=\"{ gridColumn: '1 / -1', height: rowHeight + 'px' }\"><Icon name=\"plus\" :size=\"13\" />New</button>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }\" />\n <span v-for=\"f in fields\" :key=\"'g' + f.key\" :style=\"{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', color: 'var(--text-secondary)' }\">{{ agg(f) }}</span>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }\" />\n </div>\n </div>\n </div>\n</template>\n<style scoped>\n.kb-pill{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-full);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-xs);line-height:1;white-space:nowrap}\n.kb-st-btn{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:transparent;cursor:pointer;color:var(--text-body);font-family:var(--font-sans);font-size:var(--text-sm)}\n.kb-st-btn:hover{background:var(--bg-hover)}\n.kb-st-new{display:flex;align-items:center;gap:6px;padding:0 10px;border:none;border-bottom:1px solid var(--border-subtle);background:transparent;cursor:pointer;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--text-sm);text-align:left}\n.kb-st-new:hover{background:var(--bg-inset)}\n</style>\n","data/Spreadsheet.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Select } from \"../forms/Select.jsx\";\n\nexport const colName = (i) => { let s = \"\"; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26); } return s; };\nconst norm = (cell) => cell == null ? { v: \"\" } : (typeof cell === \"object\" ? { ...cell } : { v: cell });\nconst refRC = (ref) => { const m = /^([A-Z]+)(\\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 }; };\n\nfunction evalCell(cells, cell, depth = 0) {\n if (!cell || depth > 12) return \"\";\n const raw = cell.f != null ? cell.f : cell.v;\n if (typeof raw === \"string\" && raw.startsWith(\"=\")) return evalFormula(cells, raw, depth);\n return raw;\n}\nfunction numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === \"number\" ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, \"\")); return isNaN(n) ? null : n; }\nfunction evalFormula(cells, f, depth = 0) {\n try {\n let expr = f.slice(1).toUpperCase().replace(/\\s+/g, \"\");\n expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\\(([A-Z]+\\d+):([A-Z]+\\d+)\\)/g, (m, fn, a, b) => {\n const A = refRC(a), B = refRC(b), vals = [];\n for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n); }\n if (fn === \"COUNT\") return String(vals.length);\n if (!vals.length) return \"0\";\n if (fn === \"SUM\") return String(vals.reduce((x, y) => x + y, 0));\n if (fn === \"MIN\") return String(Math.min(...vals));\n if (fn === \"MAX\") return String(Math.max(...vals));\n return String(vals.reduce((x, y) => x + y, 0) / vals.length);\n });\n expr = expr.replace(/[A-Z]+\\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n); });\n if (!/^[-+*/().\\d\\seE]*$/.test(expr)) return \"#ERROR\";\n const out = Function('\"use strict\";return (' + expr + \")\")();\n return typeof out === \"number\" && isFinite(out) ? out : \"#ERROR\";\n } catch (e) { return \"#ERROR\"; }\n}\nfunction fmt(val, t, currency) {\n if (val === \"\" || val == null) return \"\";\n if (typeof val !== \"number\") return String(val);\n const r2 = Math.round(val * 100) / 100;\n if (t === \"percent\") return (Math.round(val * 1000) / 10) + \"%\";\n if (t === \"currency\") return currency + r2.toLocaleString();\n return r2.toLocaleString();\n}\n\n/** Excel-style spreadsheet: toolbar, formula bar (name box + fx), lettered/numbered frozen headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE()-style formulas, sheet tabs and a Sum/Avg/Count status bar. */\nexport function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColWidth = 92, currency = \"$\", defaultActive = \"A1\", sheets = [\"Sheet1\"], activeSheet, toolbar = true, formulaBar = true, statusBar = true, height, onChange, style }) {\n const nR = Math.max(rows || 0, data.length, 8), nC = Math.max(cols || 0, ...data.map(r => r.length), 6);\n const [cells, setCells] = React.useState(() => Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(data[r] && data[r][c]))));\n const a0 = refRC(defaultActive) || { r: 0, c: 0 };\n const [active, setActive] = React.useState(a0);\n const [anchor, setAnchor] = React.useState(a0);\n const [editing, setEditing] = React.useState(null);\n const [draft, setDraft] = React.useState(\"\");\n const [sheet, setSheet] = React.useState(activeSheet || sheets[0]);\n const cur = cells[active.r][active.c];\n const rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? \"\" : String(cell.v));\n const commit = (r, c, text) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); const cell = next[r][c]; delete cell.f; if (String(text).startsWith(\"=\")) { cell.f = text; cell.v = \"\"; } else { const n = text !== \"\" && !isNaN(Number(text)) ? Number(text) : text; cell.v = n; } setCells(next); onChange && onChange(next); };\n const lo = { r: Math.min(active.r, anchor.r), c: Math.min(active.c, anchor.c) }, hi = { r: Math.max(active.r, anchor.r), c: Math.max(active.c, anchor.c) };\n const multi = lo.r !== hi.r || lo.c !== hi.c;\n const inRange = (r, c) => r >= lo.r && r <= hi.r && c >= lo.c && c <= hi.c;\n const stats = React.useMemo(() => { if (!multi) return null; const vals = []; for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { const v = evalCell(cells, cells[r][c]); const n = typeof v === \"number\" ? v : NaN; if (!isNaN(n)) vals.push(n); } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length }; }, [cells, active, anchor]);\n const pick = (r, c, e) => { setActive({ r, c }); if (!(e && e.shiftKey)) setAnchor({ r, c }); setEditing(null); };\n const setFlag = (k) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) next[r][c][k] = !cur[k]; setCells(next); };\n const setType = (label) => { const t = { Automatic: undefined, Number: \"number\", Currency: \"currency\", Percent: \"percent\" }[label]; const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { if (t) next[r][c].t = t; else delete next[r][c].t; } setCells(next); };\n const widths = [46]; for (let c = 0; c < nC; c++) widths.push(colWidths[c] || defaultColWidth);\n const hdr = { position: \"sticky\", top: 0, zIndex: 3, background: \"var(--bg-inset)\", borderRight: \"1px solid var(--border-control)\", borderBottom: \"1px solid var(--border-control)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-secondary)\", height: 26, boxSizing: \"border-box\", userSelect: \"none\" };\n const tbBtn = (label, k, st) => <button key={label} type=\"button\" onClick={() => setFlag(k)} style={{ width: 24, height: 24, border: \"none\", borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: cur[k] ? \"var(--bg-active)\" : \"transparent\", color: \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: 12.5, ...st }}>{label}</button>;\n return (\n <div style={{ border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-lg)\", overflow: \"hidden\", background: \"var(--bg-surface)\", boxSizing: \"border-box\", ...style }}>\n {toolbar && <div style={{ height: 36, display: \"flex\", alignItems: \"center\", gap: 2, padding: \"0 8px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <IconButton icon=\"undo-2\" size=\"sm\" muted label=\"Undo\" /><IconButton icon=\"redo-2\" size=\"sm\" muted label=\"Redo\" />\n <span style={{ width: 1, height: 16, background: \"var(--border-control)\", margin: \"0 6px\" }}></span>\n {tbBtn(\"B\", \"b\", { fontWeight: 700 })}{tbBtn(\"I\", \"i\", { fontStyle: \"italic\" })}{tbBtn(\"U\", \"u\", { textDecoration: \"underline\" })}\n <span style={{ width: 1, height: 16, background: \"var(--border-control)\", margin: \"0 6px\" }}></span>\n <Select size=\"sm\" options={[\"Automatic\", \"Number\", \"Currency\", \"Percent\"]} value={{ number: \"Number\", currency: \"Currency\", percent: \"Percent\" }[cur.t] || \"Automatic\"} onChange={setType} style={{ width: 110 }} />\n </div>}\n {formulaBar && <div style={{ height: 32, display: \"flex\", alignItems: \"center\", borderBottom: \"1px solid var(--border-control)\" }}>\n <span style={{ width: 72, alignSelf: \"stretch\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", borderRight: \"1px solid var(--border-control)\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\" }}>{multi ? colName(lo.c) + (lo.r + 1) + \":\" + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1)}</span>\n <span style={{ padding: \"0 10px\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", fontStyle: \"italic\", color: \"var(--text-muted)\", borderRight: \"1px solid var(--border-subtle)\", alignSelf: \"stretch\", display: \"flex\", alignItems: \"center\" }}>fx</span>\n <input value={editing === \"fx\" ? draft : rawOf(cur)} onFocus={() => { setEditing(\"fx\"); setDraft(rawOf(cur)); }} onChange={(e) => setDraft(e.target.value)} onBlur={() => { if (editing === \"fx\") { commit(active.r, active.c, draft); setEditing(null); } }} onKeyDown={(e) => { if (e.key === \"Enter\") e.target.blur(); if (e.key === \"Escape\") { setEditing(null); e.target.blur(); } }} style={{ flex: 1, border: \"none\", outline: \"none\", background: \"transparent\", padding: \"0 10px\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-primary)\" }} />\n </div>}\n <div style={{ overflow: \"auto\", maxHeight: height }}>\n <div style={{ display: \"grid\", gridTemplateColumns: widths.map(w => w + \"px\").join(\" \"), width: \"fit-content\" }}>\n <span style={{ ...hdr, zIndex: 5, left: 0 }}><span style={{ width: 0, height: 0, borderStyle: \"solid\", borderWidth: \"0 0 10px 10px\", borderColor: \"transparent transparent var(--border-control) transparent\", alignSelf: \"flex-end\", marginLeft: \"auto\", marginRight: 3, marginBottom: 3 }}></span></span>\n {Array.from({ length: nC }, (_, c) => <span key={c} style={{ ...hdr, ...(c >= lo.c && c <= hi.c ? { background: \"var(--soft-bg)\", color: \"var(--action-primary)\" } : null) }}>{colName(c)}</span>)}\n {cells.map((row, r) => [\n <span key={\"h\" + r} style={{ ...hdr, position: \"sticky\", left: 0, top: \"auto\", zIndex: 2, height: 28, ...(r >= lo.r && r <= hi.r ? { background: \"var(--soft-bg)\", color: \"var(--action-primary)\" } : null) }}>{r + 1}</span>,\n ...row.map((cell, c) => {\n const v = evalCell(cells, cell);\n const isNum = typeof v === \"number\";\n const isActive = active.r === r && active.c === c;\n const isEdit = editing && editing !== \"fx\" && editing.r === r && editing.c === c;\n return <span key={r + \",\" + c} onMouseDown={(e) => pick(r, c, e)} onDoubleClick={() => { setEditing({ r, c }); setDraft(rawOf(cell)); }}\n style={{ position: \"relative\", height: 28, boxSizing: \"border-box\", borderRight: \"1px solid var(--border-subtle)\", borderBottom: \"1px solid var(--border-subtle)\", display: \"flex\", alignItems: \"center\", justifyContent: cell.align || (isNum ? \"flex-end\" : \"flex-start\"), padding: \"0 8px\", cursor: \"cell\", background: isActive ? \"var(--bg-surface)\" : inRange(r, c) ? \"var(--soft-bg)\" : \"var(--bg-surface)\", boxShadow: isActive ? \"inset 0 0 0 2px var(--action-primary)\" : \"none\", zIndex: isActive ? 1 : 0, fontFamily: isNum ? \"var(--font-mono)\" : \"var(--font-sans)\", fontSize: isNum ? 11.5 : \"var(--text-sm)\", color: v === \"#ERROR\" ? \"var(--danger)\" : \"var(--text-primary)\", fontWeight: cell.b ? 600 : \"var(--weight-regular)\", fontStyle: cell.i ? \"italic\" : \"normal\", textDecoration: cell.u ? \"underline\" : \"none\", whiteSpace: \"nowrap\", overflow: \"hidden\", userSelect: \"none\" }}>\n {isEdit ? <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { commit(r, c, draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === \"Enter\") e.target.blur(); if (e.key === \"Escape\") setEditing(null); }} style={{ position: \"absolute\", inset: 0, border: \"none\", outline: \"none\", padding: \"0 8px\", fontFamily: \"inherit\", fontSize: \"inherit\", background: \"var(--bg-surface)\", color: \"var(--text-primary)\" }} /> : fmt(v, cell.t, currency)}\n {isActive && !isEdit && <span style={{ position: \"absolute\", right: -3, bottom: -3, width: 6, height: 6, background: \"var(--action-primary)\", border: \"1px solid var(--bg-surface)\", cursor: \"crosshair\", zIndex: 2 }}></span>}\n </span>;\n })])}\n </div>\n </div>\n {statusBar && <div style={{ height: 32, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", borderTop: \"1px solid var(--border-control)\", background: \"var(--bg-inset)\", paddingRight: 12 }}>\n <div style={{ display: \"flex\", alignSelf: \"stretch\", alignItems: \"stretch\" }}>\n {sheets.map(s => <button key={s} type=\"button\" onClick={() => setSheet(s)} style={{ border: \"none\", borderRight: \"1px solid var(--border-subtle)\", padding: \"0 16px\", cursor: \"pointer\", background: sheet === s ? \"var(--bg-surface)\" : \"transparent\", color: sheet === s ? \"var(--text-heading)\" : \"var(--text-secondary)\", fontFamily: \"var(--font-sans)\", fontWeight: sheet === s ? \"var(--weight-medium)\" : \"var(--weight-regular)\", fontSize: \"var(--text-sm)\", boxShadow: sheet === s ? \"inset 0 2px 0 var(--action-primary)\" : \"none\" }}>{s}</button>)}\n <span style={{ display: \"flex\", alignItems: \"center\", padding: \"0 6px\" }}><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add sheet\" /></span>\n </div>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{stats ? \"Sum \" + fmt(stats.sum, cur.t, currency) + \" Avg \" + fmt(stats.avg, cur.t, currency) + \" Count \" + stats.count : \"100%\"}</span>\n </div>}\n </div>\n );\n}\n","data/Spreadsheet.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport KSelect from '../forms/Select.vue'\n// Excel-style spreadsheet: toolbar, formula bar (name box + fx), frozen lettered/numbered headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE() formulas, sheet tabs, Sum/Avg/Count status bar.\nconst props = defineProps({ data: { type: Array, default: () => [] }, rows: Number, cols: Number, colWidths: { type: Array, default: () => [] }, defaultColWidth: { type: Number, default: 92 }, currency: { type: String, default: '$' }, defaultActive: { type: String, default: 'A1' }, sheets: { type: Array, default: () => ['Sheet1'] }, toolbar: { type: Boolean, default: true }, formulaBar: { type: Boolean, default: true }, statusBar: { type: Boolean, default: true }, height: [Number, String], style: Object })\nconst emit = defineEmits(['change'])\n\nconst colName = (i) => { let s = ''; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26) } return s }\nconst refRC = (ref) => { const m = /^([A-Z]+)(\\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 } }\nconst norm = (cell) => cell == null ? { v: '' } : (typeof cell === 'object' ? { ...cell } : { v: cell })\nfunction evalCell(cells, cell, depth = 0) { if (!cell || depth > 12) return ''; const raw = cell.f != null ? cell.f : cell.v; if (typeof raw === 'string' && raw.startsWith('=')) return evalFormula(cells, raw, depth); return raw }\nfunction numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === 'number' ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, '')); return isNaN(n) ? null : n }\nfunction evalFormula(cells, f, depth = 0) {\n try {\n let expr = f.slice(1).toUpperCase().replace(/\\s+/g, '')\n expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\\(([A-Z]+\\d+):([A-Z]+\\d+)\\)/g, (m, fn, a, b) => {\n const A = refRC(a), B = refRC(b), vals = []\n for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n) }\n if (fn === 'COUNT') return String(vals.length)\n if (!vals.length) return '0'\n if (fn === 'SUM') return String(vals.reduce((x, y) => x + y, 0))\n if (fn === 'MIN') return String(Math.min(...vals))\n if (fn === 'MAX') return String(Math.max(...vals))\n return String(vals.reduce((x, y) => x + y, 0) / vals.length)\n })\n expr = expr.replace(/[A-Z]+\\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n) })\n if (!/^[-+*/().\\d\\seE]*$/.test(expr)) return '#ERROR'\n const out = Function('\"use strict\";return (' + expr + ')')()\n return typeof out === 'number' && isFinite(out) ? out : '#ERROR'\n } catch (e) { return '#ERROR' }\n}\nfunction fmt(val, t, currency) { if (val === '' || val == null) return ''; if (typeof val !== 'number') return String(val); const r2 = Math.round(val * 100) / 100; if (t === 'percent') return (Math.round(val * 1000) / 10) + '%'; if (t === 'currency') return currency + r2.toLocaleString(); return r2.toLocaleString() }\n\nconst nR = Math.max(props.rows || 0, props.data.length, 8)\nconst nC = Math.max(props.cols || 0, ...props.data.map(r => r.length), 6)\nconst cells = ref(Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(props.data[r] && props.data[r][c]))))\nconst a0 = refRC(props.defaultActive) || { r: 0, c: 0 }\nconst active = ref(a0), anchor = ref(a0), editing = ref(null), draft = ref(''), sheet = ref(props.sheets[0])\nconst cur = computed(() => cells.value[active.value.r][active.value.c])\nconst rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? '' : String(cell.v))\nconst lo = computed(() => ({ r: Math.min(active.value.r, anchor.value.r), c: Math.min(active.value.c, anchor.value.c) }))\nconst hi = computed(() => ({ r: Math.max(active.value.r, anchor.value.r), c: Math.max(active.value.c, anchor.value.c) }))\nconst multi = computed(() => lo.value.r !== hi.value.r || lo.value.c !== hi.value.c)\nconst inRange = (r, c) => r >= lo.value.r && r <= hi.value.r && c >= lo.value.c && c <= hi.value.c\nconst stats = computed(() => { if (!multi.value) return null; const vals = []; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { const v = evalCell(cells.value, cells.value[r][c]); if (typeof v === 'number') vals.push(v) } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length } })\nfunction pick(r, c, e) { active.value = { r, c }; if (!(e && e.shiftKey)) anchor.value = { r, c }; editing.value = null }\nfunction commit(r, c, text) { const cell = cells.value[r][c]; delete cell.f; if (String(text).startsWith('=')) { cell.f = text; cell.v = '' } else cell.v = text !== '' && !isNaN(Number(text)) ? Number(text) : text; emit('change', cells.value) }\nfunction setFlag(k) { const on = !cur.value[k]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) cells.value[r][c][k] = on }\nfunction setType(label) { const t = { Automatic: undefined, Number: 'number', Currency: 'currency', Percent: 'percent' }[label]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { if (t) cells.value[r][c].t = t; else delete cells.value[r][c].t } }\nconst gridCols = computed(() => ['46px', ...Array.from({ length: nC }, (_, c) => (props.colWidths[c] || props.defaultColWidth) + 'px')].join(' '))\nconst hdr = { position: 'sticky', top: 0, zIndex: 3, background: 'var(--bg-inset)', borderRight: '1px solid var(--border-control)', borderBottom: '1px solid var(--border-control)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', color: 'var(--text-secondary)', height: '26px', boxSizing: 'border-box', userSelect: 'none' }\nconst hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }\n</script>\n<template>\n <div :style=\"{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', ...style }\">\n <div v-if=\"toolbar\" style=\"height:36px;display:flex;align-items:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--border-subtle)\">\n <IconButton icon=\"undo-2\" size=\"sm\" muted label=\"Undo\" /><IconButton icon=\"redo-2\" size=\"sm\" muted label=\"Redo\" />\n <span style=\"width:1px;height:16px;background:var(--border-control);margin:0 6px\" />\n <button type=\"button\" @click=\"setFlag('b')\" :style=\"{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.b ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontWeight: 700 }\">B</button>\n <button type=\"button\" @click=\"setFlag('i')\" :style=\"{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.i ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontStyle: 'italic' }\">I</button>\n <button type=\"button\" @click=\"setFlag('u')\" :style=\"{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.u ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', textDecoration: 'underline' }\">U</button>\n <span style=\"width:1px;height:16px;background:var(--border-control);margin:0 6px\" />\n <KSelect size=\"sm\" :options=\"['Automatic','Number','Currency','Percent']\" :model-value=\"{ number: 'Number', currency: 'Currency', percent: 'Percent' }[cur.t] || 'Automatic'\" @update:modelValue=\"setType\" style=\"width:110px\" />\n </div>\n <div v-if=\"formulaBar\" style=\"height:32px;display:flex;align-items:center;border-bottom:1px solid var(--border-control)\">\n <span style=\"width:72px;align-self:stretch;display:flex;align-items:center;justify-content:center;border-right:1px solid var(--border-control);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ multi ? colName(lo.c) + (lo.r + 1) + ':' + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1) }}</span>\n <span style=\"padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);font-style:italic;color:var(--text-muted);border-right:1px solid var(--border-subtle);align-self:stretch;display:flex;align-items:center\">fx</span>\n <input :value=\"editing === 'fx' ? draft : rawOf(cur)\" @focus=\"editing = 'fx'; draft = rawOf(cur)\" @input=\"draft = $event.target.value\" @blur=\"editing === 'fx' && (commit(active.r, active.c, draft), editing = null)\" @keydown.enter=\"$event.target.blur()\" @keydown.esc=\"editing = null; $event.target.blur()\" style=\"flex:1;border:none;outline:none;background:transparent;padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary)\" />\n </div>\n <div :style=\"{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }\">\n <div :style=\"{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content' }\">\n <span :style=\"{ ...hdr, zIndex: 5, left: 0 }\" />\n <span v-for=\"c in nC\" :key=\"'c' + c\" :style=\"{ ...hdr, ...((c - 1) >= lo.c && (c - 1) <= hi.c ? hl : null) }\">{{ colName(c - 1) }}</span>\n <template v-for=\"(row, r) in cells\" :key=\"r\">\n <span :style=\"{ ...hdr, position: 'sticky', left: 0, top: 'auto', zIndex: 2, height: '28px', ...(r >= lo.r && r <= hi.r ? hl : null) }\">{{ r + 1 }}</span>\n <span v-for=\"(cell, c) in row\" :key=\"r + ',' + c\" @mousedown=\"pick(r, c, $event)\" @dblclick=\"editing = { r, c }; draft = rawOf(cell)\"\n :style=\"{ position: 'relative', height: '28px', boxSizing: 'border-box', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: cell.align || (typeof evalCell(cells, cell) === 'number' ? 'flex-end' : 'flex-start'), padding: '0 8px', cursor: 'cell', background: inRange(r, c) && !(active.r === r && active.c === c) ? 'var(--soft-bg)' : 'var(--bg-surface)', boxShadow: active.r === r && active.c === c ? 'inset 0 0 0 2px var(--action-primary)' : 'none', fontFamily: typeof evalCell(cells, cell) === 'number' ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: typeof evalCell(cells, cell) === 'number' ? '11.5px' : 'var(--text-sm)', color: evalCell(cells, cell) === '#ERROR' ? 'var(--danger)' : 'var(--text-primary)', fontWeight: cell.b ? 600 : 'var(--weight-regular)', fontStyle: cell.i ? 'italic' : 'normal', textDecoration: cell.u ? 'underline' : 'none', whiteSpace: 'nowrap', overflow: 'hidden', userSelect: 'none' }\">\n <input v-if=\"editing && editing !== 'fx' && editing.r === r && editing.c === c\" autofocus :value=\"draft\" @input=\"draft = $event.target.value\" @blur=\"commit(r, c, draft); editing = null\" @keydown.enter=\"$event.target.blur()\" @keydown.esc=\"editing = null\" style=\"position:absolute;inset:0;border:none;outline:none;padding:0 8px;font:inherit;background:var(--bg-surface);color:var(--text-primary)\" />\n <template v-else>{{ fmt(evalCell(cells, cell), cell.t, currency) }}</template>\n <span v-if=\"active.r === r && active.c === c && !editing\" style=\"position:absolute;right:-3px;bottom:-3px;width:6px;height:6px;background:var(--action-primary);border:1px solid var(--bg-surface);cursor:crosshair;z-index:2\" />\n </span>\n </template>\n </div>\n </div>\n <div v-if=\"statusBar\" style=\"height:32px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--border-control);background:var(--bg-inset);padding-right:12px\">\n <div style=\"display:flex;align-self:stretch;align-items:stretch\">\n <button v-for=\"s in sheets\" :key=\"s\" type=\"button\" @click=\"sheet = s\" :style=\"{ border: 'none', borderRight: '1px solid var(--border-subtle)', padding: '0 16px', cursor: 'pointer', background: sheet === s ? 'var(--bg-surface)' : 'transparent', color: sheet === s ? 'var(--text-heading)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontWeight: sheet === s ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--text-sm)', boxShadow: sheet === s ? 'inset 0 2px 0 var(--action-primary)' : 'none' }\">{{ s }}</button>\n <span style=\"display:flex;align-items:center;padding:0 6px\"><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add sheet\" /></span>\n </div>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary)\">{{ stats ? 'Sum ' + fmt(stats.sum, cur.t, currency) + ' Avg ' + fmt(stats.avg, cur.t, currency) + ' Count ' + stats.count : '100%' }}</span>\n </div>\n </div>\n</template>\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"data/CodeBlock.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Monospace code/JSON block with a language label and copy-to-clipboard button (SQL Preview, export JSON). */\nexport function CodeBlock({ code = \"\", language, height, style }) {\n const [copied, setCopied] = React.useState(false);\n const copy = () => { try { navigator.clipboard && navigator.clipboard.writeText(code); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1400); };\n return (\n <div style={{ background: \"var(--bg-inset)\", border: \"1px solid var(--border-subtle)\", borderRadius: \"var(--radius-md)\", ...style }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", padding: \"7px 10px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\" }}>{language || \"code\"}</span>\n <button type=\"button\" onClick={copy} style={{ display: \"flex\", alignItems: \"center\", gap: 4, border: \"none\", background: \"transparent\", cursor: \"pointer\", color: copied ? \"var(--success-strong)\" : \"var(--text-secondary)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", padding: 0 }}>\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />{copied ? \"Copied\" : \"Copy\"}\n </button>\n </div>\n <pre style={{ margin: 0, padding: \"10px 12px\", overflow: \"auto\", height, fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-body)\" }}>{code}</pre>\n </div>\n );\n}\n","data/CodeBlock.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Monospace code/JSON block with a language label and copy-to-clipboard button (SQL Preview, export JSON).\nconst props = defineProps({ code: { type: String, default: '' }, language: String, height: [Number, String], style: Object })\nconst copied = ref(false)\nfunction copy() {\n try { navigator.clipboard && navigator.clipboard.writeText(props.code) } catch (e) {}\n copied.value = true; setTimeout(() => copied.value = false, 1400)\n}\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-inset)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', ...style }\">\n <div style=\"display:flex;justify-content:space-between;align-items:center;padding:7px 10px;border-bottom:1px solid var(--border-subtle)\">\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-muted)\">{{ language || 'code' }}</span>\n <button type=\"button\" @click=\"copy\" :style=\"{ display: 'flex', alignItems: 'center', gap: 4, border: 'none', background: 'transparent', cursor: 'pointer', color: copied ? 'var(--success-strong)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', padding: 0 }\">\n <Icon :name=\"copied ? 'check' : 'copy'\" :size=\"12\" />{{ copied ? 'Copied' : 'Copy' }}\n </button>\n </div>\n <pre :style=\"{ margin: 0, padding: '10px 12px', overflow: 'auto', height, fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', color: 'var(--text-body)' }\">{{ code }}</pre>\n </div>\n</template>\n","data/DataTable.jsx":"import React from \"react\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\n\n/** Keybricks data table. columns: [{key,label,width,mono,render}]. dense = 29px preview-drawer rows; default = 51.5px config rows. */\nexport function DataTable({ columns = [], rows = [], dense, selectable, selected = [], onToggle, headerBorder = true, style }) {\n const rowH = dense ? 29.5 : 51.5;\n const cellFont = (c) => c.mono\n ? { fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-heading)\" }\n : { fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-primary)\" };\n return (\n <table style={{ width: \"100%\", borderCollapse: \"collapse\", ...style }}>\n <thead>\n <tr style={{ background: \"var(--bg-inset)\", borderTop: headerBorder && !dense ? \"1px solid var(--border-control)\" : \"none\", borderBottom: headerBorder ? \"1px solid var(--border-control)\" : \"none\", height: dense ? 29 : 48.5 }}>\n {selectable ? <th style={{ width: 40, padding: \"0 0 0 16px\", textAlign: \"left\" }}><Checkbox size=\"sm\" checked={rows.length > 0 && selected.length === rows.length} onChange={(v) => onToggle && onToggle(\"all\", v)} /></th> : null}\n {columns.map((c) => (\n <th key={c.key} style={{ textAlign: c.align || \"left\", padding: \"0 12px\", width: c.width, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: dense ? \"var(--tracking-xs)\" : \".3px\", textTransform: dense ? \"none\" : \"uppercase\", color: \"var(--text-secondary)\", whiteSpace: \"nowrap\" }}>{c.label}</th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((r, ri) => (\n <tr key={ri} style={{ height: rowH, borderBottom: \"1px solid var(--border-subtle)\" }}>\n {selectable ? <td style={{ padding: \"0 0 0 16px\" }}><Checkbox size=\"sm\" checked={selected.includes(ri)} onChange={(v) => onToggle && onToggle(ri, v)} /></td> : null}\n {columns.map((c) => (\n <td key={c.key} style={{ padding: \"0 12px\", textAlign: c.align || \"left\", whiteSpace: \"nowrap\", ...cellFont(c) }}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n","data/DataTable.vue":"<script setup>\nimport { computed } from 'vue'\nimport Checkbox from '../forms/Checkbox.vue'\n// Keybricks data table. columns: [{key,label,width,mono,render}]. dense = 29px preview-drawer rows; default = 51.5px config rows.\nconst props = defineProps({ columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, dense: Boolean, selectable: Boolean, selected: { type: Array, default: () => [] }, headerBorder: { type: Boolean, default: true }, style: Object })\nconst emit = defineEmits(['toggle'])\nconst rowH = computed(() => props.dense ? 29.5 : 51.5)\nfunction cellFont(c) { return c.mono ? { fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', letterSpacing: 'var(--tracking-xs)', color: 'var(--text-heading)' } : { fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-primary)' } }\n</script>\n<template>\n <table :style=\"{ width: '100%', borderCollapse: 'collapse', ...style }\">\n <thead>\n <tr :style=\"{ background: 'var(--bg-inset)', borderTop: (headerBorder && !dense) ? '1px solid var(--border-control)' : 'none', borderBottom: headerBorder ? '1px solid var(--border-control)' : 'none', height: dense ? 29 : 48.5 }\">\n <th v-if=\"selectable\" style=\"width:40px;padding:0 0 0 16px;text-align:left\"><Checkbox size=\"sm\" :checked=\"rows.length > 0 && selected.length === rows.length\" @change=\"v => emit('toggle', 'all', v)\" /></th>\n <th v-for=\"c in columns\" :key=\"c.key\" :style=\"{ textAlign: c.align || 'left', padding: '0 12px', width: c.width, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: dense ? 'var(--tracking-xs)' : '.3px', textTransform: dense ? 'none' : 'uppercase', color: 'var(--text-secondary)', whiteSpace: 'nowrap' }\">{{ c.label }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(r, ri) in rows\" :key=\"ri\" :style=\"{ height: rowH, borderBottom: '1px solid var(--border-subtle)' }\">\n <td v-if=\"selectable\" style=\"padding:0 0 0 16px\"><Checkbox size=\"sm\" :checked=\"selected.includes(ri)\" @change=\"v => emit('toggle', ri, v)\" /></td>\n <td v-for=\"c in columns\" :key=\"c.key\" :style=\"{ padding: '0 12px', textAlign: c.align || 'left', whiteSpace: 'nowrap', ...cellFont(c) }\">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</td>\n </tr>\n </tbody>\n </table>\n</template>\n","data/DiffValue.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst toneColor = { neutral: \"var(--text-heading)\", warning: \"var(--warning-text)\", success: \"var(--success-text)\", danger: \"var(--danger-text)\" };\n\n/** Before → after value change (\"USD → EUR\"). tone colors the new value; from renders muted + struck through when both sides are given. */\nexport function DiffValue({ from, to, tone = \"warning\", mono = true, style }) {\n return (\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, fontFamily: mono ? \"var(--font-mono)\" : \"var(--font-sans)\", fontSize: \"var(--text-sm)\", ...style }}>\n {from != null ? <span style={{ color: \"var(--text-muted)\", textDecoration: \"line-through\" }}>{from}</span> : null}\n <Icon name=\"arrow-right\" size={12} style={{ color: \"var(--text-muted)\" }} />\n <span style={{ color: toneColor[tone], fontWeight: \"var(--weight-medium)\" }}>{to}</span>\n </span>\n );\n}\n","data/DiffValue.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nconst toneColor = { neutral: 'var(--text-heading)', warning: 'var(--warning-text)', success: 'var(--success-text)', danger: 'var(--danger-text)' }\n// Before → after value change (\"USD → EUR\"). tone colors the new value; from renders muted + struck through when both sides are given.\ndefineProps({ from: [String, Number], to: [String, Number], tone: { type: String, default: 'warning' }, mono: { type: Boolean, default: true }, style: Object })\n</script>\n<template>\n <span :style=\"{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 'var(--text-sm)', ...style }\">\n <span v-if=\"from != null\" style=\"color:var(--text-muted);text-decoration:line-through\">{{ from }}</span>\n <Icon name=\"arrow-right\" :size=\"12\" style=\"color:var(--text-muted)\" />\n <span :style=\"{ color: toneColor[tone], fontWeight: 'var(--weight-medium)' }\">{{ to }}</span>\n </span>\n</template>\n","data/FilterBar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Button } from \"../buttons/Button.jsx\";\n\n/** SAP Fiori-style list-report filter bar: variant title row with expand/collapse, a wrapping row of labeled filter fields (pass Field-wrapped controls as children), and Adapt Filters (n) + Go actions. */\nexport function FilterBar({ variant = \"Standard\", modified, adaptCount, defaultExpanded = true, onGo, onAdapt, children, style }) {\n const [open, setOpen] = React.useState(defaultExpanded);\n return (\n <div style={{ background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", padding: \"10px 16px 0\", boxSizing: \"border-box\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, paddingBottom: 10 }}>\n <button type=\"button\" onClick={() => setOpen(!open)} style={{ display: \"flex\", alignItems: \"center\", gap: 6, border: \"none\", background: \"transparent\", cursor: \"pointer\", padding: 0, color: \"var(--text-heading)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\" }}>\n {variant}{modified && <span style={{ color: \"var(--text-muted)\", fontWeight: \"var(--weight-regular)\" }}>*</span>}\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: open ? \"rotate(180deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-down\" size={14} /></span>\n </button>\n <span style={{ flex: 1 }}></span>\n <Button variant=\"text\" size=\"sm\" onClick={onAdapt}>Adapt Filters{adaptCount ? ` (${adaptCount})` : \"\"}</Button>\n <Button size=\"sm\" onClick={onGo}>Go</Button>\n </div>\n {open && <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: \"12px 16px\", paddingBottom: 14, borderTop: \"1px solid var(--border-subtle)\", paddingTop: 12 }}>{children}</div>}\n </div>\n );\n}\n","data/FilterBar.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Button from '../buttons/Button.vue'\n// SAP Fiori-style list-report filter bar: variant title + expand/collapse, wrapping labeled filter fields (slot), Adapt Filters (n) + Go.\nconst props = defineProps({ variant: { type: String, default: 'Standard' }, modified: Boolean, adaptCount: Number, defaultExpanded: { type: Boolean, default: true }, style: Object })\nconst emit = defineEmits(['go', 'adapt'])\nconst open = ref(props.defaultExpanded)\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-surface)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', padding: '10px 16px 0', boxSizing: 'border-box', ...style }\">\n <div style=\"display:flex;align-items:center;gap:6px;padding-bottom:10px\">\n <button type=\"button\" @click=\"open = !open\" style=\"display:flex;align-items:center;gap:6px;border:none;background:transparent;cursor:pointer;padding:0;color:var(--text-heading);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base)\">\n {{ variant }}<span v-if=\"modified\" style=\"color:var(--text-muted);font-weight:var(--weight-regular)\">*</span>\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-down\" :size=\"14\" /></span>\n </button>\n <span style=\"flex:1\" />\n <Button variant=\"text\" size=\"sm\" @click=\"emit('adapt')\">Adapt Filters{{ adaptCount ? ` (${adaptCount})` : '' }}</Button>\n <Button size=\"sm\" @click=\"emit('go')\">Go</Button>\n </div>\n <div v-if=\"open\" style=\"display:flex;flex-wrap:wrap;gap:12px 16px;padding-bottom:14px;border-top:1px solid var(--border-subtle);padding-top:12px\"><slot /></div>\n </div>\n</template>\n","data/KeyValue.jsx":"import React from \"react\";\n\n/** Label/value row (config summaries, confirmation steps). mono renders the value in Menlo. */\nexport function KeyValue({ label, value, mono, style }) {\n return (\n <div style={{ display: \"flex\", justifyContent: \"space-between\", gap: 12, padding: \"7px 0\", ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>{label}</span>\n <span style={{ fontFamily: mono ? \"var(--font-mono)\" : \"var(--font-sans)\", fontSize: mono ? \"var(--text-xs)\" : \"var(--text-sm)\", letterSpacing: mono ? \"var(--tracking-xs)\" : 0, color: \"var(--text-heading)\", textAlign: \"right\" }}>{value}</span>\n </div>\n );\n}\n\n/** Stack of KeyValue rows with hairline separators. rows: [{label,value,mono}]. */\nexport function KeyValueList({ rows = [], style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", ...style }}>\n {rows.map((r, i) => (\n <React.Fragment key={r.label}>\n <KeyValue {...r} />\n {i < rows.length - 1 ? <div style={{ height: 1, background: \"var(--border-subtle)\" }} /> : null}\n </React.Fragment>\n ))}\n </div>\n );\n}\n","data/KeyValue.vue":"<script setup>\n// Label/value row (config summaries, confirmation steps). mono renders the value in Menlo.\ndefineProps({ label: String, value: [String, Number], mono: Boolean, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', justifyContent: 'space-between', gap: 12, padding: '7px 0', ...style }\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ label }}</span>\n <span :style=\"{ fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: mono ? 'var(--text-xs)' : 'var(--text-sm)', letterSpacing: mono ? 'var(--tracking-xs)' : 0, color: 'var(--text-heading)', textAlign: 'right' }\">{{ value }}</span>\n </div>\n</template>\n","data/ListRow.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile } from \"../display/Badge.jsx\";\n\n/** Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count. */\nexport function ListRow({ icon = \"file-text\", title, meta, trailing, expanded, onClick, children, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div style={{ borderBottom: \"1px solid var(--border-card)\", ...style }}>\n <div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"12px 16px\", cursor: \"pointer\", background: hover ? \"var(--bg-inset)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12 }}>\n <span style={{ color: \"var(--text-muted)\", transform: expanded ? \"rotate(90deg)\" : \"none\", transition: \"transform .15s ease\", display: \"inline-flex\" }}><Icon name=\"chevron-right\" size={16} /></span>\n <IconTile icon={icon} kind=\"row\" />\n <div style={{ display: \"flex\", flexDirection: \"column\" }}>\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)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>{meta}</span> : null}\n </div>\n </div>\n {trailing ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>{trailing}</span> : null}\n </div>\n {expanded && children ? <div style={{ padding: \"0 16px 12px 72px\" }}>{children}</div> : null}\n </div>\n );\n}\n","data/ListRow.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconTile from '../display/IconTile.vue'\n// Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count.\ndefineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div :style=\"{ borderBottom: '1px solid var(--border-card)', ...style }\">\n <div @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" :style=\"{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', cursor: 'pointer', background: hover ? 'var(--bg-inset)' : 'transparent', transition: 'var(--t-hover)' }\">\n <div style=\"display:flex;align-items:center;gap:12px\">\n <span :style=\"{ color: 'var(--text-muted)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform .15s ease', display: 'inline-flex' }\"><Icon name=\"chevron-right\" :size=\"16\" /></span>\n <IconTile :icon=\"icon\" kind=\"row\" />\n <div style=\"display:flex;flex-direction:column\">\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-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ meta }}</span>\n </div>\n </div>\n <span v-if=\"trailing\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ trailing }}</span>\n </div>\n <div v-if=\"expanded\" style=\"padding:0 16px 12px 72px\"><slot /></div>\n </div>\n</template>\n","data/Pagination.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Table pagination — 12px range label + prev/next + numbered pages. */\nexport function Pagination({ page = 1, pageCount = 1, total, pageSize = 20, onChange, style }) {\n const btn = (content, disabled, onClick, active) => (\n <button type=\"button\" disabled={disabled} onClick={onClick}\n style={{ minWidth: 26, height: 26, padding: \"0 6px\", borderRadius: \"var(--radius-sm)\", border: `1px solid ${active ? \"var(--action-primary)\" : \"var(--border-control)\"}`, background: active ? \"var(--action-primary)\" : \"var(--bg-surface)\", color: active ? \"var(--on-accent)\" : \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", cursor: disabled ? \"default\" : \"pointer\", opacity: disabled ? .45 : 1, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", transition: \"var(--t-hover)\" }}>\n {content}\n </button>\n );\n const pages = [];\n const from = Math.max(1, Math.min(page - 1, pageCount - 2));\n for (let i = from; i < from + 3 && i <= pageCount; i++) pages.push(i);\n return (\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 12, ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>\n {total != null ? `${(page - 1) * pageSize + 1}–${Math.min(page * pageSize, total)} of ${total}` : `Page ${page} of ${pageCount}`}\n </span>\n <div style={{ display: \"flex\", gap: 4 }}>\n {btn(<Icon name=\"chevron-left\" size={13} />, page <= 1, () => onChange && onChange(page - 1))}\n {pages.map((p) => <React.Fragment key={p}>{btn(p, false, () => onChange && onChange(p), p === page)}</React.Fragment>)}\n {btn(<Icon name=\"chevron-right\" size={13} />, page >= pageCount, () => onChange && onChange(page + 1))}\n </div>\n </div>\n );\n}\n","data/Pagination.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Table pagination — 12px range label + prev/next + numbered pages.\nconst props = defineProps({ page: { type: Number, default: 1 }, pageCount: { type: Number, default: 1 }, total: Number, pageSize: { type: Number, default: 20 }, style: Object })\nconst emit = defineEmits(['change'])\nconst pages = computed(() => {\n const from = Math.max(1, Math.min(props.page - 1, props.pageCount - 2)), out = []\n for (let i = from; i < from + 3 && i <= props.pageCount; i++) out.push(i)\n return out\n})\nconst label = computed(() => props.total != null ? `${(props.page - 1) * props.pageSize + 1}–${Math.min(props.page * props.pageSize, props.total)} of ${props.total}` : `Page ${props.page} of ${props.pageCount}`)\nfunction btnStyle(active, disabled) { return { minWidth: 26, height: 26, padding: '0 6px', borderRadius: 'var(--radius-sm)', border: `1px solid ${active ? 'var(--action-primary)' : 'var(--border-control)'}`, background: active ? 'var(--action-primary)' : 'var(--bg-surface)', color: active ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .45 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'var(--t-hover)' } }\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, ...style }\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">{{ label }}</span>\n <div style=\"display:flex;gap:4px\">\n <button type=\"button\" :disabled=\"page <= 1\" @click=\"emit('change', page - 1)\" :style=\"btnStyle(false, page <= 1)\"><Icon name=\"chevron-left\" :size=\"13\" /></button>\n <button v-for=\"p in pages\" :key=\"p\" type=\"button\" @click=\"emit('change', p)\" :style=\"btnStyle(p === page, false)\">{{ p }}</button>\n <button type=\"button\" :disabled=\"page >= pageCount\" @click=\"emit('change', page + 1)\" :style=\"btnStyle(false, page >= pageCount)\"><Icon name=\"chevron-right\" :size=\"13\" /></button>\n </div>\n </div>\n</template>\n","data/SmartTable.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\nimport { Avatar } from \"../display/Avatar.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\nconst typeIcon = { text: \"type\", longText: \"align-left\", number: \"hash\", select: \"chevron-down\", multiSelect: \"tag\", date: \"calendar\", person: \"user\", checkbox: \"check-square\", url: \"link\", progress: \"bar-chart-2\" };\nconst Pill = ({ hue = \"gray\", children }) => <span style={{ display: \"inline-flex\", alignItems: \"center\", height: 20, padding: \"0 8px\", borderRadius: \"var(--radius-full)\", background: `var(--hue-${hue}-bg)`, color: `var(--hue-${hue}-fg)`, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-xs)\", lineHeight: 1, whiteSpace: \"nowrap\" }}>{children}</span>;\n\n/** Notion/Airtable-style database grid (\"smart table\"): field-type icons in headers, colored select pills, person/date/checkbox/url/progress cells, row hover + expand, inline text/number editing, + New row, add-field column, per-column footer aggregates, and a view toolbar (Hide fields · Filter · Group · Sort · search). */\nexport function SmartTable({ fields = [], rows = [], title = \"Grid view\", toolbar = true, height, rowHeight = 32, onChange, style }) {\n const [data, setData] = React.useState(rows);\n const [hover, setHover] = React.useState(-1);\n const [editing, setEditing] = React.useState(null);\n const [draft, setDraft] = React.useState(\"\");\n const setCell = (r, key, v) => { const next = data.map((row, i) => i === r ? { ...row, [key]: v } : row); setData(next); onChange && onChange(next); };\n const widths = [\"40px\", ...fields.map(f => (f.width || 140) + \"px\"), \"40px\"];\n const grid = { display: \"grid\", gridTemplateColumns: widths.join(\" \"), width: \"fit-content\", minWidth: \"100%\" };\n const cellBase = { display: \"flex\", alignItems: \"center\", gap: 5, padding: \"0 8px\", borderRight: \"1px solid var(--border-subtle)\", borderBottom: \"1px solid var(--border-subtle)\", height: rowHeight, boxSizing: \"border-box\", overflow: \"hidden\", whiteSpace: \"nowrap\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-primary)\" };\n const agg = (f) => {\n if (!f.aggregate) return \"\";\n const vals = data.map(r => r[f.key]);\n if (f.aggregate === \"count\") return \"COUNT \" + vals.filter(v => v === true || (v !== \"\" && v != null && v !== false)).length;\n const nums = vals.map(Number).filter(n => !isNaN(n));\n if (f.aggregate === \"sum\") return \"SUM \" + nums.reduce((a, b) => a + b, 0);\n if (f.aggregate === \"avg\") return \"AVG \" + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0);\n return \"\";\n };\n const renderCell = (f, row, r) => {\n const v = row[f.key];\n const isEdit = editing && editing.r === r && editing.key === f.key;\n if (isEdit) return <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { setCell(r, f.key, f.type === \"number\" && draft !== \"\" && !isNaN(Number(draft)) ? Number(draft) : draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === \"Enter\") e.target.blur(); if (e.key === \"Escape\") setEditing(null); }} style={{ position: \"absolute\", inset: 0, border: \"2px solid var(--action-primary)\", borderRadius: 2, outline: \"none\", padding: \"0 6px\", font: \"inherit\", background: \"var(--bg-surface)\", color: \"var(--text-primary)\", boxSizing: \"border-box\" }} />;\n switch (f.type) {\n case \"select\": { const hue = (f.options || {})[v] || \"gray\"; return v ? <Pill hue={hue}>{v}</Pill> : null; }\n case \"multiSelect\": return <span style={{ display: \"flex\", gap: 4, overflow: \"hidden\" }}>{(v || []).map(t => <Pill key={t} hue={(f.options || {})[t] || \"gray\"}>{t}</Pill>)}</span>;\n case \"person\": return v ? <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, minWidth: 0 }}><Avatar name={v} size={20} /><span style={{ overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{v}</span></span> : null;\n case \"checkbox\": return <Checkbox size=\"sm\" checked={!!v} onChange={(nv) => setCell(r, f.key, nv)} />;\n case \"url\": return v ? <a href={\"https://\" + v} onClick={(e) => e.preventDefault()} style={{ color: \"var(--link)\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{v}</a> : null;\n case \"progress\": return <span style={{ display: \"flex\", alignItems: \"center\", gap: 6, width: \"100%\" }}><span style={{ flex: 1, height: 4, borderRadius: \"var(--radius-full)\", background: \"var(--track)\", overflow: \"hidden\" }}><span style={{ display: \"block\", width: (v || 0) + \"%\", height: \"100%\", background: \"var(--chart-blue)\" }}></span></span><span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-secondary)\" }}>{v || 0}%</span></span>;\n case \"number\": return <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: 11.5 }}>{v}</span>;\n default: return <span style={{ overflow: \"hidden\", textOverflow: \"ellipsis\", fontWeight: f.primary ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: f.primary ? \"var(--text-heading)\" : \"var(--text-primary)\" }}>{v}</span>;\n }\n };\n const tbBtn = (icon, label) => <button key={label} type=\"button\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: 5, height: 26, padding: \"0 8px\", border: \"none\", borderRadius: \"var(--radius-sm)\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\" }} onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}><Icon name={icon} size={13} />{label}</button>;\n return (\n <div style={{ border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-lg)\", background: \"var(--bg-surface)\", overflow: \"hidden\", boxSizing: \"border-box\", ...style }}>\n {toolbar && <div style={{ display: \"flex\", alignItems: \"center\", gap: 4, padding: \"6px 10px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <button type=\"button\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, height: 26, padding: \"0 8px\", border: \"none\", borderRadius: \"var(--radius-sm)\", background: \"var(--bg-active)\", cursor: \"pointer\", color: \"var(--text-heading)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\" }}><Icon name=\"layout-grid\" size={13} />{title}<Icon name=\"chevron-down\" size={12} /></button>\n <span style={{ width: 1, height: 16, background: \"var(--border-control)\", margin: \"0 4px\" }}></span>\n {tbBtn(\"eye-off\", \"Hide fields\")}{tbBtn(\"funnel\", \"Filter\")}{tbBtn(\"layers\", \"Group\")}{tbBtn(\"arrow-down\", \"Sort\")}\n <span style={{ flex: 1 }}></span>\n <SearchInput bare placeholder=\"Search...\" style={{ width: 140, gap: 6 }} />\n </div>}\n <div style={{ overflow: \"auto\", maxHeight: height }}>\n <div style={grid}>\n <span style={{ ...cellBase, background: \"var(--bg-inset)\", justifyContent: \"center\" }}></span>\n {fields.map(f => <span key={f.key} style={{ ...cellBase, background: \"var(--bg-inset)\", color: \"var(--text-secondary)\", fontSize: \"var(--text-xs)\", fontWeight: \"var(--weight-medium)\", letterSpacing: \"var(--tracking-xs)\" }}><Icon name={typeIcon[f.type] || \"type\"} size={12} style={{ color: \"var(--text-muted)\" }} />{f.label}</span>)}\n <span style={{ ...cellBase, background: \"var(--bg-inset)\", justifyContent: \"center\" }}><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add field\" /></span>\n {data.map((row, r) => [\n <span key={\"n\" + r} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} style={{ ...cellBase, justifyContent: \"center\", color: \"var(--text-muted)\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", background: hover === r ? \"var(--bg-inset)\" : \"transparent\", cursor: \"pointer\" }}>{hover === r ? <Icon name=\"maximize-2\" size={12} style={{ color: \"var(--action-primary)\" }} /> : r + 1}</span>,\n ...fields.map(f => <span key={r + f.key} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} onDoubleClick={() => { if (f.type === \"text\" || f.type === \"number\" || f.type === \"date\" || f.type === \"url\") { setEditing({ r, key: f.key }); setDraft(String(row[f.key] == null ? \"\" : row[f.key])); } }} style={{ ...cellBase, position: \"relative\", background: hover === r ? \"var(--bg-inset)\" : \"transparent\" }}>{renderCell(f, row, r)}</span>),\n <span key={\"a\" + r} style={{ ...cellBase, background: hover === r ? \"var(--bg-inset)\" : \"transparent\" }}></span>,\n ])}\n <button type=\"button\" onClick={() => { const empty = {}; fields.forEach(f => empty[f.key] = f.type === \"multiSelect\" ? [] : f.type === \"checkbox\" ? false : \"\"); const next = [...data, empty]; setData(next); onChange && onChange(next); }} style={{ gridColumn: \"1 / -1\", display: \"flex\", alignItems: \"center\", gap: 6, height: rowHeight, padding: \"0 10px\", border: \"none\", borderBottom: \"1px solid var(--border-subtle)\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-secondary)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", textAlign: \"left\" }} onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-inset)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}><Icon name=\"plus\" size={13} />New</button>\n <span style={{ ...cellBase, borderBottom: \"none\", background: \"var(--bg-inset)\" }}></span>\n {fields.map(f => <span key={\"g\" + f.key} style={{ ...cellBase, borderBottom: \"none\", background: \"var(--bg-inset)\", justifyContent: \"flex-end\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-secondary)\" }}>{agg(f)}</span>)}\n <span style={{ ...cellBase, borderBottom: \"none\", background: \"var(--bg-inset)\" }}></span>\n </div>\n </div>\n </div>\n );\n}\n","data/SmartTable.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport Checkbox from '../forms/Checkbox.vue'\nimport Avatar from '../display/Avatar.vue'\nimport SearchInput from '../forms/SearchInput.vue'\n// Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar.\nconst typeIcon = { text: 'type', longText: 'align-left', number: 'hash', select: 'chevron-down', multiSelect: 'tag', date: 'calendar', person: 'user', checkbox: 'check-square', url: 'link', progress: 'bar-chart-2' }\nconst props = defineProps({ fields: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, title: { type: String, default: 'Grid view' }, toolbar: { type: Boolean, default: true }, height: [Number, String], rowHeight: { type: Number, default: 32 }, style: Object })\nconst emit = defineEmits(['change'])\nconst data = ref(props.rows.map(r => ({ ...r })))\nconst hover = ref(-1), editing = ref(null), draft = ref('')\nconst gridCols = computed(() => ['40px', ...props.fields.map(f => (f.width || 140) + 'px'), '40px'].join(' '))\nconst cellBase = { display: 'flex', alignItems: 'center', gap: '5px', padding: '0 8px', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', boxSizing: 'border-box', overflow: 'hidden', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-primary)' }\nfunction setCell(r, key, v) { data.value[r][key] = v; emit('change', data.value) }\nfunction commitEdit(r, f) { setCell(r, f.key, f.type === 'number' && draft.value !== '' && !isNaN(Number(draft.value)) ? Number(draft.value) : draft.value); editing.value = null }\nfunction startEdit(r, f, row) { if (['text', 'number', 'date', 'url'].includes(f.type || 'text')) { editing.value = { r, key: f.key }; draft.value = String(row[f.key] == null ? '' : row[f.key]) } }\nfunction addRow() { const empty = {}; props.fields.forEach(f => empty[f.key] = f.type === 'multiSelect' ? [] : f.type === 'checkbox' ? false : ''); data.value.push(empty); emit('change', data.value) }\nfunction agg(f) {\n if (!f.aggregate) return ''\n const vals = data.value.map(r => r[f.key])\n if (f.aggregate === 'count') return 'COUNT ' + vals.filter(v => v === true || (v !== '' && v != null && v !== false)).length\n const nums = vals.map(Number).filter(n => !isNaN(n))\n if (f.aggregate === 'sum') return 'SUM ' + nums.reduce((a, b) => a + b, 0)\n return 'AVG ' + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0)\n}\n</script>\n<template>\n <div :style=\"{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', background: 'var(--bg-surface)', overflow: 'hidden', boxSizing: 'border-box', ...style }\">\n <div v-if=\"toolbar\" style=\"display:flex;align-items:center;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border-subtle)\">\n <button type=\"button\" style=\"display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:var(--bg-active);cursor:pointer;color:var(--text-heading);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm)\"><Icon name=\"layout-grid\" :size=\"13\" />{{ title }}<Icon name=\"chevron-down\" :size=\"12\" /></button>\n <span style=\"width:1px;height:16px;background:var(--border-control);margin:0 4px\" />\n <button v-for=\"b in [['eye-off','Hide fields'],['funnel','Filter'],['layers','Group'],['arrow-down','Sort']]\" :key=\"b[1]\" type=\"button\" class=\"kb-st-btn\"><Icon :name=\"b[0]\" :size=\"13\" />{{ b[1] }}</button>\n <span style=\"flex:1\" />\n <SearchInput bare placeholder=\"Search...\" style=\"width:140px;gap:6px\" />\n </div>\n <div :style=\"{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }\">\n <div :style=\"{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content', minWidth: '100%' }\">\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)' }\" />\n <span v-for=\"f in fields\" :key=\"f.key\" :style=\"{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', color: 'var(--text-secondary)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)' }\"><Icon :name=\"typeIcon[f.type] || 'type'\" :size=\"12\" style=\"color:var(--text-muted)\" />{{ f.label }}</span>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', justifyContent: 'center' }\"><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add field\" /></span>\n <template v-for=\"(row, r) in data\" :key=\"r\">\n <span @mouseenter=\"hover = r\" @mouseleave=\"hover = -1\" :style=\"{ ...cellBase, height: rowHeight + 'px', justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', background: hover === r ? 'var(--bg-inset)' : 'transparent', cursor: 'pointer' }\">\n <Icon v-if=\"hover === r\" name=\"maximize-2\" :size=\"12\" style=\"color:var(--action-primary)\" /><template v-else>{{ r + 1 }}</template>\n </span>\n <span v-for=\"f in fields\" :key=\"r + f.key\" @mouseenter=\"hover = r\" @mouseleave=\"hover = -1\" @dblclick=\"startEdit(r, f, row)\" :style=\"{ ...cellBase, height: rowHeight + 'px', position: 'relative', background: hover === r ? 'var(--bg-inset)' : 'transparent' }\">\n <input v-if=\"editing && editing.r === r && editing.key === f.key\" autofocus :value=\"draft\" @input=\"draft = $event.target.value\" @blur=\"commitEdit(r, f)\" @keydown.enter=\"$event.target.blur()\" @keydown.esc=\"editing = null\" style=\"position:absolute;inset:0;border:2px solid var(--action-primary);border-radius:2px;outline:none;padding:0 6px;font:inherit;background:var(--bg-surface);color:var(--text-primary);box-sizing:border-box\" />\n <template v-else-if=\"f.type === 'select'\"><span v-if=\"row[f.key]\" class=\"kb-pill\" :style=\"{ background: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-fg)` }\">{{ row[f.key] }}</span></template>\n <template v-else-if=\"f.type === 'multiSelect'\"><span style=\"display:flex;gap:4px;overflow:hidden\"><span v-for=\"t in row[f.key] || []\" :key=\"t\" class=\"kb-pill\" :style=\"{ background: `var(--hue-${(f.options || {})[t] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[t] || 'gray'}-fg)` }\">{{ t }}</span></span></template>\n <template v-else-if=\"f.type === 'person'\"><span v-if=\"row[f.key]\" style=\"display:inline-flex;align-items:center;gap:6px;min-width:0\"><Avatar :name=\"row[f.key]\" :size=\"20\" /><span style=\"overflow:hidden;text-overflow:ellipsis\">{{ row[f.key] }}</span></span></template>\n <template v-else-if=\"f.type === 'checkbox'\"><Checkbox size=\"sm\" :checked=\"!!row[f.key]\" @change=\"v => setCell(r, f.key, v)\" /></template>\n <template v-else-if=\"f.type === 'url'\"><a v-if=\"row[f.key]\" href=\"#\" @click.prevent style=\"overflow:hidden;text-overflow:ellipsis\">{{ row[f.key] }}</a></template>\n <template v-else-if=\"f.type === 'progress'\"><span style=\"display:flex;align-items:center;gap:6px;width:100%\"><span style=\"flex:1;height:4px;border-radius:var(--radius-full);background:var(--track);overflow:hidden\"><span :style=\"{ display: 'block', width: (row[f.key] || 0) + '%', height: '100%', background: 'var(--chart-blue)' }\" /></span><span style=\"font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-secondary)\">{{ row[f.key] || 0 }}%</span></span></template>\n <template v-else-if=\"f.type === 'number'\"><span style=\"margin-left:auto;font-family:var(--font-mono);font-size:11.5px\">{{ row[f.key] }}</span></template>\n <template v-else><span :style=\"{ overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: f.primary ? 'var(--weight-medium)' : 'var(--weight-regular)', color: f.primary ? 'var(--text-heading)' : 'var(--text-primary)' }\">{{ row[f.key] }}</span></template>\n </span>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', background: hover === r ? 'var(--bg-inset)' : 'transparent' }\" />\n </template>\n <button type=\"button\" @click=\"addRow\" class=\"kb-st-new\" :style=\"{ gridColumn: '1 / -1', height: rowHeight + 'px' }\"><Icon name=\"plus\" :size=\"13\" />New</button>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }\" />\n <span v-for=\"f in fields\" :key=\"'g' + f.key\" :style=\"{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', color: 'var(--text-secondary)' }\">{{ agg(f) }}</span>\n <span :style=\"{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }\" />\n </div>\n </div>\n </div>\n</template>\n<style scoped>\n.kb-pill{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-full);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-xs);line-height:1;white-space:nowrap}\n.kb-st-btn{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:transparent;cursor:pointer;color:var(--text-body);font-family:var(--font-sans);font-size:var(--text-sm)}\n.kb-st-btn:hover{background:var(--bg-hover)}\n.kb-st-new{display:flex;align-items:center;gap:6px;padding:0 10px;border:none;border-bottom:1px solid var(--border-subtle);background:transparent;cursor:pointer;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--text-sm);text-align:left}\n.kb-st-new:hover{background:var(--bg-inset)}\n</style>\n","data/Spreadsheet.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Select } from \"../forms/Select.jsx\";\n\nexport const colName = (i) => { let s = \"\"; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26); } return s; };\nconst norm = (cell) => cell == null ? { v: \"\" } : (typeof cell === \"object\" ? { ...cell } : { v: cell });\nconst refRC = (ref) => { const m = /^([A-Z]+)(\\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 }; };\n\nfunction evalCell(cells, cell, depth = 0) {\n if (!cell || depth > 12) return \"\";\n const raw = cell.f != null ? cell.f : cell.v;\n if (typeof raw === \"string\" && raw.startsWith(\"=\")) return evalFormula(cells, raw, depth);\n return raw;\n}\nfunction numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === \"number\" ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, \"\")); return isNaN(n) ? null : n; }\nfunction evalFormula(cells, f, depth = 0) {\n try {\n let expr = f.slice(1).toUpperCase().replace(/\\s+/g, \"\");\n expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\\(([A-Z]+\\d+):([A-Z]+\\d+)\\)/g, (m, fn, a, b) => {\n const A = refRC(a), B = refRC(b), vals = [];\n for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n); }\n if (fn === \"COUNT\") return String(vals.length);\n if (!vals.length) return \"0\";\n if (fn === \"SUM\") return String(vals.reduce((x, y) => x + y, 0));\n if (fn === \"MIN\") return String(Math.min(...vals));\n if (fn === \"MAX\") return String(Math.max(...vals));\n return String(vals.reduce((x, y) => x + y, 0) / vals.length);\n });\n expr = expr.replace(/[A-Z]+\\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n); });\n if (!/^[-+*/().\\d\\seE]*$/.test(expr)) return \"#ERROR\";\n const out = Function('\"use strict\";return (' + expr + \")\")();\n return typeof out === \"number\" && isFinite(out) ? out : \"#ERROR\";\n } catch (e) { return \"#ERROR\"; }\n}\nfunction fmt(val, t, currency) {\n if (val === \"\" || val == null) return \"\";\n if (typeof val !== \"number\") return String(val);\n const r2 = Math.round(val * 100) / 100;\n if (t === \"percent\") return (Math.round(val * 1000) / 10) + \"%\";\n if (t === \"currency\") return currency + r2.toLocaleString();\n return r2.toLocaleString();\n}\n\n/** Excel-style spreadsheet: toolbar, formula bar (name box + fx), lettered/numbered frozen headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE()-style formulas, sheet tabs and a Sum/Avg/Count status bar. */\nexport function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColWidth = 92, currency = \"$\", defaultActive = \"A1\", sheets = [\"Sheet1\"], activeSheet, toolbar = true, formulaBar = true, statusBar = true, height, onChange, style }) {\n const nR = Math.max(rows || 0, data.length, 8), nC = Math.max(cols || 0, ...data.map(r => r.length), 6);\n const [cells, setCells] = React.useState(() => Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(data[r] && data[r][c]))));\n const a0 = refRC(defaultActive) || { r: 0, c: 0 };\n const [active, setActive] = React.useState(a0);\n const [anchor, setAnchor] = React.useState(a0);\n const [editing, setEditing] = React.useState(null);\n const [draft, setDraft] = React.useState(\"\");\n const [sheet, setSheet] = React.useState(activeSheet || sheets[0]);\n const cur = cells[active.r][active.c];\n const rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? \"\" : String(cell.v));\n const commit = (r, c, text) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); const cell = next[r][c]; delete cell.f; if (String(text).startsWith(\"=\")) { cell.f = text; cell.v = \"\"; } else { const n = text !== \"\" && !isNaN(Number(text)) ? Number(text) : text; cell.v = n; } setCells(next); onChange && onChange(next); };\n const lo = { r: Math.min(active.r, anchor.r), c: Math.min(active.c, anchor.c) }, hi = { r: Math.max(active.r, anchor.r), c: Math.max(active.c, anchor.c) };\n const multi = lo.r !== hi.r || lo.c !== hi.c;\n const inRange = (r, c) => r >= lo.r && r <= hi.r && c >= lo.c && c <= hi.c;\n const stats = React.useMemo(() => { if (!multi) return null; const vals = []; for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { const v = evalCell(cells, cells[r][c]); const n = typeof v === \"number\" ? v : NaN; if (!isNaN(n)) vals.push(n); } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length }; }, [cells, active, anchor]);\n const pick = (r, c, e) => { setActive({ r, c }); if (!(e && e.shiftKey)) setAnchor({ r, c }); setEditing(null); };\n const setFlag = (k) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) next[r][c][k] = !cur[k]; setCells(next); };\n const setType = (label) => { const t = { Automatic: undefined, Number: \"number\", Currency: \"currency\", Percent: \"percent\" }[label]; const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { if (t) next[r][c].t = t; else delete next[r][c].t; } setCells(next); };\n const widths = [46]; for (let c = 0; c < nC; c++) widths.push(colWidths[c] || defaultColWidth);\n const hdr = { position: \"sticky\", top: 0, zIndex: 3, background: \"var(--bg-inset)\", borderRight: \"1px solid var(--border-control)\", borderBottom: \"1px solid var(--border-control)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-secondary)\", height: 26, boxSizing: \"border-box\", userSelect: \"none\" };\n const tbBtn = (label, k, st) => <button key={label} type=\"button\" onClick={() => setFlag(k)} style={{ width: 24, height: 24, border: \"none\", borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: cur[k] ? \"var(--bg-active)\" : \"transparent\", color: \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: 12.5, ...st }}>{label}</button>;\n return (\n <div style={{ border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-lg)\", overflow: \"hidden\", background: \"var(--bg-surface)\", boxSizing: \"border-box\", ...style }}>\n {toolbar && <div style={{ height: 36, display: \"flex\", alignItems: \"center\", gap: 2, padding: \"0 8px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <IconButton icon=\"undo-2\" size=\"sm\" muted label=\"Undo\" /><IconButton icon=\"redo-2\" size=\"sm\" muted label=\"Redo\" />\n <span style={{ width: 1, height: 16, background: \"var(--border-control)\", margin: \"0 6px\" }}></span>\n {tbBtn(\"B\", \"b\", { fontWeight: 700 })}{tbBtn(\"I\", \"i\", { fontStyle: \"italic\" })}{tbBtn(\"U\", \"u\", { textDecoration: \"underline\" })}\n <span style={{ width: 1, height: 16, background: \"var(--border-control)\", margin: \"0 6px\" }}></span>\n <Select size=\"sm\" options={[\"Automatic\", \"Number\", \"Currency\", \"Percent\"]} value={{ number: \"Number\", currency: \"Currency\", percent: \"Percent\" }[cur.t] || \"Automatic\"} onChange={setType} style={{ width: 110 }} />\n </div>}\n {formulaBar && <div style={{ height: 32, display: \"flex\", alignItems: \"center\", borderBottom: \"1px solid var(--border-control)\" }}>\n <span style={{ width: 72, alignSelf: \"stretch\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", borderRight: \"1px solid var(--border-control)\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\" }}>{multi ? colName(lo.c) + (lo.r + 1) + \":\" + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1)}</span>\n <span style={{ padding: \"0 10px\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", fontStyle: \"italic\", color: \"var(--text-muted)\", borderRight: \"1px solid var(--border-subtle)\", alignSelf: \"stretch\", display: \"flex\", alignItems: \"center\" }}>fx</span>\n <input value={editing === \"fx\" ? draft : rawOf(cur)} onFocus={() => { setEditing(\"fx\"); setDraft(rawOf(cur)); }} onChange={(e) => setDraft(e.target.value)} onBlur={() => { if (editing === \"fx\") { commit(active.r, active.c, draft); setEditing(null); } }} onKeyDown={(e) => { if (e.key === \"Enter\") e.target.blur(); if (e.key === \"Escape\") { setEditing(null); e.target.blur(); } }} style={{ flex: 1, border: \"none\", outline: \"none\", background: \"transparent\", padding: \"0 10px\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-primary)\" }} />\n </div>}\n <div style={{ overflow: \"auto\", maxHeight: height }}>\n <div style={{ display: \"grid\", gridTemplateColumns: widths.map(w => w + \"px\").join(\" \"), width: \"fit-content\" }}>\n <span style={{ ...hdr, zIndex: 5, left: 0 }}><span style={{ width: 0, height: 0, borderStyle: \"solid\", borderWidth: \"0 0 10px 10px\", borderColor: \"transparent transparent var(--border-control) transparent\", alignSelf: \"flex-end\", marginLeft: \"auto\", marginRight: 3, marginBottom: 3 }}></span></span>\n {Array.from({ length: nC }, (_, c) => <span key={c} style={{ ...hdr, ...(c >= lo.c && c <= hi.c ? { background: \"var(--soft-bg)\", color: \"var(--action-primary)\" } : null) }}>{colName(c)}</span>)}\n {cells.map((row, r) => [\n <span key={\"h\" + r} style={{ ...hdr, position: \"sticky\", left: 0, top: \"auto\", zIndex: 2, height: 28, ...(r >= lo.r && r <= hi.r ? { background: \"var(--soft-bg)\", color: \"var(--action-primary)\" } : null) }}>{r + 1}</span>,\n ...row.map((cell, c) => {\n const v = evalCell(cells, cell);\n const isNum = typeof v === \"number\";\n const isActive = active.r === r && active.c === c;\n const isEdit = editing && editing !== \"fx\" && editing.r === r && editing.c === c;\n return <span key={r + \",\" + c} onMouseDown={(e) => pick(r, c, e)} onDoubleClick={() => { setEditing({ r, c }); setDraft(rawOf(cell)); }}\n style={{ position: \"relative\", height: 28, boxSizing: \"border-box\", borderRight: \"1px solid var(--border-subtle)\", borderBottom: \"1px solid var(--border-subtle)\", display: \"flex\", alignItems: \"center\", justifyContent: cell.align || (isNum ? \"flex-end\" : \"flex-start\"), padding: \"0 8px\", cursor: \"cell\", background: isActive ? \"var(--bg-surface)\" : inRange(r, c) ? \"var(--soft-bg)\" : \"var(--bg-surface)\", boxShadow: isActive ? \"inset 0 0 0 2px var(--action-primary)\" : \"none\", zIndex: isActive ? 1 : 0, fontFamily: isNum ? \"var(--font-mono)\" : \"var(--font-sans)\", fontSize: isNum ? 11.5 : \"var(--text-sm)\", color: v === \"#ERROR\" ? \"var(--danger)\" : \"var(--text-primary)\", fontWeight: cell.b ? 600 : \"var(--weight-regular)\", fontStyle: cell.i ? \"italic\" : \"normal\", textDecoration: cell.u ? \"underline\" : \"none\", whiteSpace: \"nowrap\", overflow: \"hidden\", userSelect: \"none\" }}>\n {isEdit ? <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { commit(r, c, draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === \"Enter\") e.target.blur(); if (e.key === \"Escape\") setEditing(null); }} style={{ position: \"absolute\", inset: 0, border: \"none\", outline: \"none\", padding: \"0 8px\", fontFamily: \"inherit\", fontSize: \"inherit\", background: \"var(--bg-surface)\", color: \"var(--text-primary)\" }} /> : fmt(v, cell.t, currency)}\n {isActive && !isEdit && <span style={{ position: \"absolute\", right: -3, bottom: -3, width: 6, height: 6, background: \"var(--action-primary)\", border: \"1px solid var(--bg-surface)\", cursor: \"crosshair\", zIndex: 2 }}></span>}\n </span>;\n })])}\n </div>\n </div>\n {statusBar && <div style={{ height: 32, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", borderTop: \"1px solid var(--border-control)\", background: \"var(--bg-inset)\", paddingRight: 12 }}>\n <div style={{ display: \"flex\", alignSelf: \"stretch\", alignItems: \"stretch\" }}>\n {sheets.map(s => <button key={s} type=\"button\" onClick={() => setSheet(s)} style={{ border: \"none\", borderRight: \"1px solid var(--border-subtle)\", padding: \"0 16px\", cursor: \"pointer\", background: sheet === s ? \"var(--bg-surface)\" : \"transparent\", color: sheet === s ? \"var(--text-heading)\" : \"var(--text-secondary)\", fontFamily: \"var(--font-sans)\", fontWeight: sheet === s ? \"var(--weight-medium)\" : \"var(--weight-regular)\", fontSize: \"var(--text-sm)\", boxShadow: sheet === s ? \"inset 0 2px 0 var(--action-primary)\" : \"none\" }}>{s}</button>)}\n <span style={{ display: \"flex\", alignItems: \"center\", padding: \"0 6px\" }}><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add sheet\" /></span>\n </div>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{stats ? \"Sum \" + fmt(stats.sum, cur.t, currency) + \" Avg \" + fmt(stats.avg, cur.t, currency) + \" Count \" + stats.count : \"100%\"}</span>\n </div>}\n </div>\n );\n}\n","data/Spreadsheet.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport KSelect from '../forms/Select.vue'\n// Excel-style spreadsheet: toolbar, formula bar (name box + fx), frozen lettered/numbered headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE() formulas, sheet tabs, Sum/Avg/Count status bar.\nconst props = defineProps({ data: { type: Array, default: () => [] }, rows: Number, cols: Number, colWidths: { type: Array, default: () => [] }, defaultColWidth: { type: Number, default: 92 }, currency: { type: String, default: '$' }, defaultActive: { type: String, default: 'A1' }, sheets: { type: Array, default: () => ['Sheet1'] }, toolbar: { type: Boolean, default: true }, formulaBar: { type: Boolean, default: true }, statusBar: { type: Boolean, default: true }, height: [Number, String], style: Object })\nconst emit = defineEmits(['change'])\n\nconst colName = (i) => { let s = ''; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26) } return s }\nconst refRC = (ref) => { const m = /^([A-Z]+)(\\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 } }\nconst norm = (cell) => cell == null ? { v: '' } : (typeof cell === 'object' ? { ...cell } : { v: cell })\nfunction evalCell(cells, cell, depth = 0) { if (!cell || depth > 12) return ''; const raw = cell.f != null ? cell.f : cell.v; if (typeof raw === 'string' && raw.startsWith('=')) return evalFormula(cells, raw, depth); return raw }\nfunction numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === 'number' ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, '')); return isNaN(n) ? null : n }\nfunction evalFormula(cells, f, depth = 0) {\n try {\n let expr = f.slice(1).toUpperCase().replace(/\\s+/g, '')\n expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\\(([A-Z]+\\d+):([A-Z]+\\d+)\\)/g, (m, fn, a, b) => {\n const A = refRC(a), B = refRC(b), vals = []\n for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n) }\n if (fn === 'COUNT') return String(vals.length)\n if (!vals.length) return '0'\n if (fn === 'SUM') return String(vals.reduce((x, y) => x + y, 0))\n if (fn === 'MIN') return String(Math.min(...vals))\n if (fn === 'MAX') return String(Math.max(...vals))\n return String(vals.reduce((x, y) => x + y, 0) / vals.length)\n })\n expr = expr.replace(/[A-Z]+\\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n) })\n if (!/^[-+*/().\\d\\seE]*$/.test(expr)) return '#ERROR'\n const out = Function('\"use strict\";return (' + expr + ')')()\n return typeof out === 'number' && isFinite(out) ? out : '#ERROR'\n } catch (e) { return '#ERROR' }\n}\nfunction fmt(val, t, currency) { if (val === '' || val == null) return ''; if (typeof val !== 'number') return String(val); const r2 = Math.round(val * 100) / 100; if (t === 'percent') return (Math.round(val * 1000) / 10) + '%'; if (t === 'currency') return currency + r2.toLocaleString(); return r2.toLocaleString() }\n\nconst nR = Math.max(props.rows || 0, props.data.length, 8)\nconst nC = Math.max(props.cols || 0, ...props.data.map(r => r.length), 6)\nconst cells = ref(Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(props.data[r] && props.data[r][c]))))\nconst a0 = refRC(props.defaultActive) || { r: 0, c: 0 }\nconst active = ref(a0), anchor = ref(a0), editing = ref(null), draft = ref(''), sheet = ref(props.sheets[0])\nconst cur = computed(() => cells.value[active.value.r][active.value.c])\nconst rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? '' : String(cell.v))\nconst lo = computed(() => ({ r: Math.min(active.value.r, anchor.value.r), c: Math.min(active.value.c, anchor.value.c) }))\nconst hi = computed(() => ({ r: Math.max(active.value.r, anchor.value.r), c: Math.max(active.value.c, anchor.value.c) }))\nconst multi = computed(() => lo.value.r !== hi.value.r || lo.value.c !== hi.value.c)\nconst inRange = (r, c) => r >= lo.value.r && r <= hi.value.r && c >= lo.value.c && c <= hi.value.c\nconst stats = computed(() => { if (!multi.value) return null; const vals = []; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { const v = evalCell(cells.value, cells.value[r][c]); if (typeof v === 'number') vals.push(v) } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length } })\nfunction pick(r, c, e) { active.value = { r, c }; if (!(e && e.shiftKey)) anchor.value = { r, c }; editing.value = null }\nfunction commit(r, c, text) { const cell = cells.value[r][c]; delete cell.f; if (String(text).startsWith('=')) { cell.f = text; cell.v = '' } else cell.v = text !== '' && !isNaN(Number(text)) ? Number(text) : text; emit('change', cells.value) }\nfunction setFlag(k) { const on = !cur.value[k]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) cells.value[r][c][k] = on }\nfunction setType(label) { const t = { Automatic: undefined, Number: 'number', Currency: 'currency', Percent: 'percent' }[label]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { if (t) cells.value[r][c].t = t; else delete cells.value[r][c].t } }\nconst gridCols = computed(() => ['46px', ...Array.from({ length: nC }, (_, c) => (props.colWidths[c] || props.defaultColWidth) + 'px')].join(' '))\nconst hdr = { position: 'sticky', top: 0, zIndex: 3, background: 'var(--bg-inset)', borderRight: '1px solid var(--border-control)', borderBottom: '1px solid var(--border-control)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', color: 'var(--text-secondary)', height: '26px', boxSizing: 'border-box', userSelect: 'none' }\nconst hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }\n</script>\n<template>\n <div :style=\"{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', ...style }\">\n <div v-if=\"toolbar\" style=\"height:36px;display:flex;align-items:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--border-subtle)\">\n <IconButton icon=\"undo-2\" size=\"sm\" muted label=\"Undo\" /><IconButton icon=\"redo-2\" size=\"sm\" muted label=\"Redo\" />\n <span style=\"width:1px;height:16px;background:var(--border-control);margin:0 6px\" />\n <button type=\"button\" @click=\"setFlag('b')\" :style=\"{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.b ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontWeight: 700 }\">B</button>\n <button type=\"button\" @click=\"setFlag('i')\" :style=\"{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.i ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontStyle: 'italic' }\">I</button>\n <button type=\"button\" @click=\"setFlag('u')\" :style=\"{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.u ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', textDecoration: 'underline' }\">U</button>\n <span style=\"width:1px;height:16px;background:var(--border-control);margin:0 6px\" />\n <KSelect size=\"sm\" :options=\"['Automatic','Number','Currency','Percent']\" :model-value=\"{ number: 'Number', currency: 'Currency', percent: 'Percent' }[cur.t] || 'Automatic'\" @update:modelValue=\"setType\" style=\"width:110px\" />\n </div>\n <div v-if=\"formulaBar\" style=\"height:32px;display:flex;align-items:center;border-bottom:1px solid var(--border-control)\">\n <span style=\"width:72px;align-self:stretch;display:flex;align-items:center;justify-content:center;border-right:1px solid var(--border-control);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ multi ? colName(lo.c) + (lo.r + 1) + ':' + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1) }}</span>\n <span style=\"padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);font-style:italic;color:var(--text-muted);border-right:1px solid var(--border-subtle);align-self:stretch;display:flex;align-items:center\">fx</span>\n <input :value=\"editing === 'fx' ? draft : rawOf(cur)\" @focus=\"editing = 'fx'; draft = rawOf(cur)\" @input=\"draft = $event.target.value\" @blur=\"editing === 'fx' && (commit(active.r, active.c, draft), editing = null)\" @keydown.enter=\"$event.target.blur()\" @keydown.esc=\"editing = null; $event.target.blur()\" style=\"flex:1;border:none;outline:none;background:transparent;padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary)\" />\n </div>\n <div :style=\"{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }\">\n <div :style=\"{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content' }\">\n <span :style=\"{ ...hdr, zIndex: 5, left: 0 }\" />\n <span v-for=\"c in nC\" :key=\"'c' + c\" :style=\"{ ...hdr, ...((c - 1) >= lo.c && (c - 1) <= hi.c ? hl : null) }\">{{ colName(c - 1) }}</span>\n <template v-for=\"(row, r) in cells\" :key=\"r\">\n <span :style=\"{ ...hdr, position: 'sticky', left: 0, top: 'auto', zIndex: 2, height: '28px', ...(r >= lo.r && r <= hi.r ? hl : null) }\">{{ r + 1 }}</span>\n <span v-for=\"(cell, c) in row\" :key=\"r + ',' + c\" @mousedown=\"pick(r, c, $event)\" @dblclick=\"editing = { r, c }; draft = rawOf(cell)\"\n :style=\"{ position: 'relative', height: '28px', boxSizing: 'border-box', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: cell.align || (typeof evalCell(cells, cell) === 'number' ? 'flex-end' : 'flex-start'), padding: '0 8px', cursor: 'cell', background: inRange(r, c) && !(active.r === r && active.c === c) ? 'var(--soft-bg)' : 'var(--bg-surface)', boxShadow: active.r === r && active.c === c ? 'inset 0 0 0 2px var(--action-primary)' : 'none', fontFamily: typeof evalCell(cells, cell) === 'number' ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: typeof evalCell(cells, cell) === 'number' ? '11.5px' : 'var(--text-sm)', color: evalCell(cells, cell) === '#ERROR' ? 'var(--danger)' : 'var(--text-primary)', fontWeight: cell.b ? 600 : 'var(--weight-regular)', fontStyle: cell.i ? 'italic' : 'normal', textDecoration: cell.u ? 'underline' : 'none', whiteSpace: 'nowrap', overflow: 'hidden', userSelect: 'none' }\">\n <input v-if=\"editing && editing !== 'fx' && editing.r === r && editing.c === c\" autofocus :value=\"draft\" @input=\"draft = $event.target.value\" @blur=\"commit(r, c, draft); editing = null\" @keydown.enter=\"$event.target.blur()\" @keydown.esc=\"editing = null\" style=\"position:absolute;inset:0;border:none;outline:none;padding:0 8px;font:inherit;background:var(--bg-surface);color:var(--text-primary)\" />\n <template v-else>{{ fmt(evalCell(cells, cell), cell.t, currency) }}</template>\n <span v-if=\"active.r === r && active.c === c && !editing\" style=\"position:absolute;right:-3px;bottom:-3px;width:6px;height:6px;background:var(--action-primary);border:1px solid var(--bg-surface);cursor:crosshair;z-index:2\" />\n </span>\n </template>\n </div>\n </div>\n <div v-if=\"statusBar\" style=\"height:32px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--border-control);background:var(--bg-inset);padding-right:12px\">\n <div style=\"display:flex;align-self:stretch;align-items:stretch\">\n <button v-for=\"s in sheets\" :key=\"s\" type=\"button\" @click=\"sheet = s\" :style=\"{ border: 'none', borderRight: '1px solid var(--border-subtle)', padding: '0 16px', cursor: 'pointer', background: sheet === s ? 'var(--bg-surface)' : 'transparent', color: sheet === s ? 'var(--text-heading)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontWeight: sheet === s ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--text-sm)', boxShadow: sheet === s ? 'inset 0 2px 0 var(--action-primary)' : 'none' }\">{{ s }}</button>\n <span style=\"display:flex;align-items:center;padding:0 6px\"><IconButton icon=\"plus\" size=\"xs\" muted label=\"Add sheet\" /></span>\n </div>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary)\">{{ stats ? 'Sum ' + fmt(stats.sum, cur.t, currency) + ' Avg ' + fmt(stats.avg, cur.t, currency) + ' Count ' + stats.count : '100%' }}</span>\n </div>\n </div>\n</template>\n","data/KeyValueList.vue":"<script setup>\nimport KeyValue from './KeyValue.vue'\ndefineProps({ rows: { type: Array, default: () => [] }, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', ...style }\">\n <template v-for=\"(r, i) in rows\" :key=\"r.label\">\n <KeyValue v-bind=\"r\" />\n <div v-if=\"i < rows.length - 1\" style=\"height:1px;background:var(--border-subtle)\" />\n </template>\n </div>\n</template>\n"});