@keyfunc/keybricks-ds 2.4.13 → 2.5.1

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 (145) hide show
  1. package/DEPLOY.md +4 -4
  2. package/INTEGRATION.md +28 -6
  3. package/_adherence.oxlintrc.json +10 -4
  4. package/_ds_bundle.js +20559 -0
  5. package/_src/icon-index.js +1 -0
  6. package/_src/unicons-index.js +1 -0
  7. package/_src/usage-code.js +737 -0
  8. package/changelog.js +17 -0
  9. package/components/_cards/accordion.card.html +1 -1
  10. package/components/_cards/action-chip.card.html +1 -1
  11. package/components/_cards/activity.card.html +1 -1
  12. package/components/_cards/alert.card.html +1 -1
  13. package/components/_cards/app-chrome.card.html +1 -1
  14. package/components/_cards/banner.card.html +1 -1
  15. package/components/_cards/breadcrumb.card.html +1 -1
  16. package/components/_cards/button-1-base.card.html +1 -1
  17. package/components/_cards/button-2-states.card.html +1 -1
  18. package/components/_cards/button-3-sizes-icons.card.html +1 -1
  19. package/components/_cards/button-4-group.card.html +1 -1
  20. package/components/_cards/cards-indicators.card.html +1 -1
  21. package/components/_cards/charts-1-sparkline.card.html +1 -1
  22. package/components/_cards/charts-10-gauge.card.html +1 -1
  23. package/components/_cards/charts-11-funnel.card.html +1 -1
  24. package/components/_cards/charts-12-treemap.card.html +1 -1
  25. package/components/_cards/charts-14-radar.card.html +1 -1
  26. package/components/_cards/charts-15-distribution.card.html +1 -1
  27. package/components/_cards/charts-16-pareto.card.html +1 -1
  28. package/components/_cards/charts-17-candlestick.card.html +1 -1
  29. package/components/_cards/charts-18-sankey.card.html +1 -1
  30. package/components/_cards/charts-19-chord.card.html +1 -1
  31. package/components/_cards/charts-2-line.card.html +1 -1
  32. package/components/_cards/charts-20-sunburst.card.html +1 -1
  33. package/components/_cards/charts-21-worldmap.card.html +1 -1
  34. package/components/_cards/charts-22-gantt.card.html +1 -1
  35. package/components/_cards/charts-23-bar3d.card.html +1 -1
  36. package/components/_cards/charts-24-pie3d.card.html +1 -1
  37. package/components/_cards/charts-25-surface3d.card.html +1 -1
  38. package/components/_cards/charts-26-scatter3d.card.html +1 -1
  39. package/components/_cards/charts-27-ribbon3d.card.html +1 -1
  40. package/components/_cards/charts-3-bar.card.html +1 -1
  41. package/components/_cards/charts-4-barlist.card.html +1 -1
  42. package/components/_cards/charts-5-donut.card.html +1 -1
  43. package/components/_cards/charts-6-scatter.card.html +1 -1
  44. package/components/_cards/charts-7-heatmap.card.html +1 -1
  45. package/components/_cards/charts-8-waterfall.card.html +1 -1
  46. package/components/_cards/charts-9-bullet.card.html +1 -1
  47. package/components/_cards/checkbox.card.html +1 -1
  48. package/components/_cards/codeblock.card.html +1 -1
  49. package/components/_cards/combobox.card.html +1 -1
  50. package/components/_cards/context-menu.card.html +1 -1
  51. package/components/_cards/date-picker.card.html +1 -1
  52. package/components/_cards/diffvalue.card.html +1 -1
  53. package/components/_cards/divider.card.html +1 -1
  54. package/components/_cards/drawer.card.html +1 -1
  55. package/components/_cards/dropdown-menu.card.html +1 -1
  56. package/components/_cards/dropzone.card.html +1 -1
  57. package/components/_cards/empty-state.card.html +1 -1
  58. package/components/_cards/icon-button.card.html +1 -1
  59. package/components/_cards/input.card.html +1 -1
  60. package/components/_cards/kbd.card.html +1 -1
  61. package/components/_cards/keyvalue.card.html +1 -1
  62. package/components/_cards/listrow.card.html +1 -1
  63. package/components/_cards/loading.card.html +1 -1
  64. package/components/_cards/log-viewer.card.html +1 -1
  65. package/components/_cards/multi-select.card.html +1 -1
  66. package/components/_cards/number-stepper.card.html +1 -1
  67. package/components/_cards/pagination.card.html +1 -1
  68. package/components/_cards/people.card.html +1 -1
  69. package/components/_cards/pipeline-node.card.html +1 -1
  70. package/components/_cards/pipeline-palette.card.html +1 -1
  71. package/components/_cards/radio.card.html +1 -1
  72. package/components/_cards/relative-time-picker.card.html +1 -1
  73. package/components/_cards/search.card.html +1 -1
  74. package/components/_cards/segmented-control.card.html +1 -1
  75. package/components/_cards/select.card.html +1 -1
  76. package/components/_cards/smart-table.card.html +1 -1
  77. package/components/_cards/spreadsheet.card.html +1 -1
  78. package/components/_cards/stepper.card.html +1 -1
  79. package/components/_cards/table.card.html +7 -2
  80. package/components/_cards/tabs.card.html +1 -1
  81. package/components/_cards/tag.card.html +1 -1
  82. package/components/_cards/textarea.card.html +1 -1
  83. package/components/_cards/timeline.card.html +1 -1
  84. package/components/_cards/toast.card.html +1 -1
  85. package/components/_cards/toggles.card.html +1 -1
  86. package/components/_cards/tooltip.card.html +1 -1
  87. package/components/_cards/tree.card.html +1 -1
  88. package/components/_shared/view-source.js +15 -185
  89. package/components/charts/BarList.jsx +1 -1
  90. package/components/charts/BarList.vue +8 -3
  91. package/components/charts/Ribbon3D.d.ts +2 -0
  92. package/components/charts/Scatter3D.d.ts +4 -0
  93. package/components/charts/SunburstChart.jsx +6 -1
  94. package/components/charts/SunburstChart.vue +7 -1
  95. package/components/charts/Surface3D.d.ts +2 -0
  96. package/components/charts/WorldMap.jsx +6 -1
  97. package/components/charts/WorldMap.vue +7 -1
  98. package/components/data/ListRow.d.ts +6 -3
  99. package/components/data/ListRow.jsx +21 -13
  100. package/components/data/ListRow.vue +25 -15
  101. package/components/data/Table.jsx +1 -1
  102. package/components/data/Table.vue +6 -2
  103. package/components/display/ProgressBar.jsx +1 -1
  104. package/components/display/ProgressBar.vue +1 -1
  105. package/components/feedback/Accordion.jsx +1 -1
  106. package/components/feedback/Accordion.vue +1 -1
  107. package/components/feedback/ContextMenu.jsx +3 -3
  108. package/components/feedback/ContextMenu.vue +4 -4
  109. package/components/feedback/Drawer.jsx +3 -3
  110. package/components/feedback/Drawer.vue +3 -3
  111. package/components/feedback/Timeline.vue +4 -1
  112. package/components/forms/Checkbox.d.ts +2 -0
  113. package/components/forms/Checkbox.jsx +9 -4
  114. package/components/forms/Checkbox.vue +6 -2
  115. package/components/forms/DatePicker.jsx +3 -1
  116. package/components/forms/DatePicker.vue +2 -1
  117. package/components/forms/Dropzone.jsx +1 -1
  118. package/components/forms/Dropzone.vue +1 -1
  119. package/components/forms/MultiSelect.jsx +5 -3
  120. package/components/forms/MultiSelect.vue +4 -3
  121. package/components/forms/Slider.jsx +1 -1
  122. package/components/forms/Slider.vue +1 -1
  123. package/components/forms/Switch.jsx +4 -4
  124. package/components/forms/Switch.vue +4 -4
  125. package/components/navigation/Sidebar.jsx +2 -2
  126. package/components/navigation/Sidebar.vue +2 -2
  127. package/components/navigation/TopBar.jsx +1 -1
  128. package/components/navigation/TopBar.vue +1 -1
  129. package/components/navigation/Tree.jsx +2 -2
  130. package/components/navigation/Tree.vue +14 -5
  131. package/components/pipeline/DataPreview.jsx +1 -1
  132. package/components/pipeline/DataPreview.vue +1 -1
  133. package/components/pipeline/NodeCard.jsx +1 -1
  134. package/components/pipeline/NodeCard.vue +1 -1
  135. package/components/pipeline/PaletteItem.vue +1 -1
  136. package/components/pipeline/PalettePanel.jsx +3 -3
  137. package/components/pipeline/PalettePanel.vue +2 -2
  138. package/components/tabs/SegmentedControl.jsx +2 -2
  139. package/components/tabs/SegmentedControl.vue +2 -2
  140. package/index.html +6 -56
  141. package/package.json +4 -2
  142. package/tokens/dark.css +1 -1
  143. package/tokens/interaction.css +4 -0
  144. package/tokens/layout.css +3 -0
  145. package/tokens/typography.css +2 -1
@@ -2,24 +2,32 @@ import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
  import { IconTile } from "../display/Badge.jsx";
4
4
 
5
- /** Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count. */
5
+ const T_TITLE = { fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
6
+ const T_META = { fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
7
+ const T_TRAIL = { fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", fontVariantNumeric: "tabular-nums", color: "var(--text-secondary)", whiteSpace: "nowrap", flexShrink: 0 };
8
+
9
+ /** Expandable workflow list row: chevron + 32px hairline icon tile + title / meta + right count.
10
+ * The chevron shows when the row has content to expand (children); the row is a button only when it has onClick —
11
+ * focusable, Enter / Space, aria-expanded, hover wash from tokens/hover.css [data-row]. A row with neither is static.
12
+ * Rows without a chevron keep its 16px slot, so tiles line up in a mixed list. Expanded content starts under the title. */
6
13
  export function ListRow({ icon = "file-text", title, meta, trailing, expanded, onClick, children, style }) {
7
- const [hover, setHover] = React.useState(false);
14
+ const hasContent = children != null && children !== false;
15
+ const clickable = typeof onClick === "function";
16
+ const onKey = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(e); } };
8
17
  return (
9
18
  <div style={{ borderBottom: "1px solid var(--border-card)", ...style }}>
10
- <div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
11
- style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", cursor: "pointer", background: hover ? "var(--bg-inset)" : "transparent", transition: "var(--t-hover)" }}>
12
- <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
13
- <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>
14
- <IconTile icon={icon} kind="row" />
15
- <div style={{ display: "flex", flexDirection: "column" }}>
16
- <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>
17
- {meta ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-secondary)" }}>{meta}</span> : null}
18
- </div>
19
+ <div data-row={clickable ? "" : undefined} role={clickable ? "button" : undefined} tabIndex={clickable ? 0 : undefined}
20
+ aria-expanded={clickable && hasContent ? !!expanded : undefined} onClick={clickable ? onClick : undefined} onKeyDown={clickable ? onKey : undefined}
21
+ style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px var(--edge-pad)", cursor: clickable ? "pointer" : "default", background: "var(--row-wash, transparent)", outlineOffset: -2, transition: "var(--t-hover)" }}>
22
+ <span aria-hidden="true" style={{ width: 16, flexShrink: 0, display: "inline-flex", color: "var(--text-muted)", transform: expanded ? "rotate(90deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}>{hasContent ? <Icon name="chevron-right" size={16} /> : null}</span>
23
+ <IconTile icon={icon} kind="row" />
24
+ <div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
25
+ <span style={T_TITLE}>{title}</span>
26
+ {meta ? <span style={T_META}>{meta}</span> : null}
19
27
  </div>
20
- {trailing ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", fontVariantNumeric: "tabular-nums", color: "var(--text-secondary)" }}>{trailing}</span> : null}
28
+ {trailing != null && trailing !== "" ? <span style={T_TRAIL}>{trailing}</span> : null}
21
29
  </div>
22
- {expanded && children ? <div style={{ padding: "0 16px 12px 72px" }}>{children}</div> : null}
30
+ {expanded && hasContent ? <div style={{ padding: "0 var(--edge-pad) 12px calc(var(--edge-pad) + 72px)" }}>{children}</div> : null}
23
31
  </div>
24
32
  );
25
33
  }
@@ -1,25 +1,35 @@
1
1
  <script setup>
2
- import { ref } from 'vue'
2
+ import { computed, getCurrentInstance, useSlots } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  import IconTile from '../display/IconTile.vue'
5
- // Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count.
6
- defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })
7
- defineEmits(['click'])
8
- const hover = ref(false)
5
+ // Expandable workflow list row: chevron + 32px hairline icon tile + title / meta + right count.
6
+ // The chevron shows when the row has content to expand (default slot); the row is a button only when it has a @click
7
+ // listener — focusable, Enter / Space, aria-expanded, hover wash from tokens/hover.css [data-row]. A row with neither is static.
8
+ // Rows without a chevron keep its 16px slot, so tiles line up in a mixed list. Expanded content starts under the title.
9
+ const props = defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })
10
+ const emit = defineEmits(['click'])
11
+ const slots = useSlots()
12
+ const inst = getCurrentInstance()
13
+ const hasContent = computed(() => !!slots.default)
14
+ const clickable = computed(() => !!(inst && inst.vnode.props && inst.vnode.props.onClick))
15
+ function onKey(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); emit('click', e) } }
16
+ const T_TITLE = { fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
17
+ const T_META = { fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
18
+ const T_TRAIL = { fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', fontVariantNumeric: 'tabular-nums', color: 'var(--text-secondary)', whiteSpace: 'nowrap', flexShrink: 0 }
9
19
  </script>
10
20
  <template>
11
21
  <div :style="{ borderBottom: '1px solid var(--border-card)', ...style }">
12
- <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)' }">
13
- <div style="display:flex;align-items:center;gap:12px">
14
- <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>
15
- <IconTile :icon="icon" kind="row" />
16
- <div style="display:flex;flex-direction:column">
17
- <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>
18
- <span v-if="meta" style="font-family:var(--font-sans);font-weight:var(--weight-regular);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)">{{ meta }}</span>
19
- </div>
22
+ <div :data-row="clickable ? '' : undefined" :role="clickable ? 'button' : undefined" :tabindex="clickable ? 0 : undefined"
23
+ :aria-expanded="clickable && hasContent ? !!expanded : undefined" @click="clickable && emit('click', $event)" @keydown="clickable && onKey($event)"
24
+ :style="{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px var(--edge-pad)', cursor: clickable ? 'pointer' : 'default', background: 'var(--row-wash, transparent)', outlineOffset: '-2px', transition: 'var(--t-hover)' }">
25
+ <span aria-hidden="true" :style="{ width: '16px', flexShrink: 0, display: 'inline-flex', color: 'var(--text-muted)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon v-if="hasContent" name="chevron-right" :size="16" /></span>
26
+ <IconTile :icon="icon" kind="row" />
27
+ <div style="display:flex;flex-direction:column;min-width:0;flex:1">
28
+ <span :style="T_TITLE">{{ title }}</span>
29
+ <span v-if="meta" :style="T_META">{{ meta }}</span>
20
30
  </div>
21
- <span v-if="trailing" 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);font-variant-numeric:tabular-nums;color:var(--text-secondary)">{{ trailing }}</span>
31
+ <span v-if="trailing != null && trailing !== ''" :style="T_TRAIL">{{ trailing }}</span>
22
32
  </div>
23
- <div v-if="expanded" style="padding:0 16px 12px 72px"><slot /></div>
33
+ <div v-if="expanded && hasContent" style="padding:0 var(--edge-pad) 12px calc(var(--edge-pad) + 72px)"><slot /></div>
24
34
  </div>
25
35
  </template>
@@ -89,7 +89,7 @@ function TreeToggle({ open, label, onClick }) {
89
89
  return (
90
90
  <button type="button" aria-expanded={open} aria-label={label} title={label} onClick={(e) => { e.stopPropagation(); onClick(); }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
91
91
  style={{ width: TREE_TOGGLE, height: TREE_TOGGLE, padding: 0, border: "none", borderRadius: "var(--radius-sm)", display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer", background: hover ? "var(--overlay-hover)" : "transparent", color: hover ? "var(--text-body)" : "var(--text-muted)", transition: "var(--t-hover)" }}>
92
- <Icon name="chevron-right" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", transform: open ? "rotate(90deg)" : "none", transition: "transform .15s ease" }} />
92
+ <Icon name="chevron-right" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", transform: open ? "rotate(90deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }} />
93
93
  </button>
94
94
  );
95
95
  }
@@ -5,7 +5,11 @@ import Checkbox from '../forms/Checkbox.vue'
5
5
  import IconTile from '../display/IconTile.vue'
6
6
  import IconButton from '../buttons/IconButton.vue'
7
7
  import Icon from '../icons/Icon.vue'
8
- // Keybricks table. columns: [{key,label,width,minWidth,mono,code,twoLine,align,render}].
8
+ // A cell: a #cell-<key> slot ({ value, row, index }) when the page gives one; else column.render's result — text or a VNode from h() — or the raw value.
9
+ const Cell = (p) => p.v
10
+ Cell.props = ['v']
11
+ // Keybricks table. columns: [{key,label,width,minWidth,mono,code,twoLine,align,render}]. Custom cells: <template #cell-status="{ value, row, index }">…</template>,
12
+ // or render(value, row, index) returning text or a VNode (h()).
9
13
  // twoLine — the name column of a document list: icon tile, title over a description, optional mark ("favorite" = filled star), trailing ⋯
10
14
  // (@row-more(row, index, event)); @row-open(row, index, event) makes the tile and the title open the row (hand cursor, Enter / Space on the title).
11
15
  // tree — rows nest through `children` (or `hasChildren` for rows you load on demand); the twoLine column indents 24px per level with an
@@ -184,7 +188,7 @@ function cellPad(i) { return { ...(i === 0 && props.selectable ? { position: 're
184
188
  <tr v-else-if="pinInfo && pinInfo.style === 'labels' && pinInfo.grouped && pos === pinInfo.unStart"><td :colspan="columns.length || 1" :style="GROUP_CELL">{{ L.unavailable }} · {{ pinInfo.un }}</td></tr>
185
189
  <tr data-row="" :aria-level="tree ? item.level + 1 : undefined" :aria-expanded="item.kids ? item.open : undefined" :style="{ ...(isDis(item.r, item.ri) ? { '--row-wash': 'initial' } : null), height: 'var(--bar-h)', background: pinInfo && pinInfo.sel.has(item.ri) ? 'var(--row-wash, var(--bg-inset))' : 'var(--row-wash, transparent)', transition: 'var(--t-hover)', borderBottom: pinInfo && ((pinInfo.rest > 0 && pos === pinInfo.rest - 1) || (pinInfo.unStart > 0 && pos === pinInfo.unStart - 1)) ? '1px solid var(--border-control)' : bordered && pos === view.length - 1 ? 'none' : '1px solid var(--border-subtle)' }">
186
190
  <template v-for="{ r, ri } in [item]" :key="'r'">
187
- <td v-for="(c, i) in columns" :key="c.key" :style="{ ...cellPad(i), ...(c.twoLine ? { paddingTop: 'var(--title-cell-py)', paddingBottom: 'var(--title-cell-py)', maxWidth: 0 } : null), textAlign: c.align || 'left', whiteSpace: 'nowrap', ...(resizable ? { overflow: 'hidden', textOverflow: 'ellipsis' } : null), ...cellFont(c) }"><span v-if="selectable && i === 0" :style="checkSlot" :title="isDis(r, ri) && disabledReason ? disabledReason(r, ri) : undefined"><Checkbox :checked="selected.includes(ri)" :disabled="isDis(r, ri)" @change="v => emit('toggle', ri, v)" /></span><div v-if="c.twoLine" :style="{ display: 'flex', alignItems: 'center', minWidth: px(c.minWidth ?? 180), ...(treeCol === c ? { paddingLeft: (item.level * TREE_INDENT) + 'px' } : null) }"><span v-if="treeCol === c" :style="{ width: TREE_TOGGLE + 'px', flexShrink: 0, marginRight: (TREE_INDENT - TREE_TOGGLE) + 'px', display: 'inline-flex' }"><button v-if="item.kids" type="button" :aria-expanded="item.open" :aria-label="item.open ? L.collapse : L.expand" :title="item.open ? L.collapse : L.expand" @click.stop="toggleRow(item)" @mouseenter="hotToggle = item.key" @mouseleave="hotToggle = null" :style="{ width: TREE_TOGGLE + 'px', height: TREE_TOGGLE + 'px', padding: 0, border: 'none', borderRadius: 'var(--radius-sm)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: hotToggle === item.key ? 'var(--overlay-hover)' : 'transparent', color: hotToggle === item.key ? 'var(--text-body)' : 'var(--text-muted)', transition: 'var(--t-hover)' }"><Icon name="chevron-right" :size="16" :style="{ width: 'var(--control-icon)', height: 'var(--control-icon)', transform: item.open ? 'rotate(90deg)' : 'none', transition: 'transform .15s ease' }" /></button></span><div style="display:flex;align-items:center;gap:var(--title-cell-tile-gap);flex:1;min-width:0"><template v-for="t in [tl(c, r, ri)]" :key="0"><span v-if="t.icon" :style="{ display: 'inline-flex', flexShrink: 0, cursor: canOpen ? 'pointer' : undefined }" @click="e => canOpen && openRow(r, ri, e)"><IconTile :icon="t.icon" :hue="t.hue || 'neutral'" kind="row" :style="T_TILE" :icon-style="T_ICON" /></span><div style="display:flex;flex-direction:column;gap:var(--title-cell-gap);flex:1;min-width:0"><div style="display:flex;align-items:center;gap:4px;min-width:0"><span v-if="canOpen" role="button" tabindex="0" :style="{ ...T_TITLE, cursor: 'pointer' }" @click="e => openRow(r, ri, e)" @keydown="e => openKey(r, ri, e)">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</span><span v-else :style="T_TITLE">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</span><svg v-if="t.mark === 'favorite'" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round" role="img" aria-label="Favorite" style="display:block;flex-shrink:0;color:var(--warning)"><path :d="STAR_FILLED" /></svg><span v-else-if="t.mark" role="img" :aria-label="t.mark.label" :style="{ display: 'inline-flex', flexShrink: 0, color: t.mark.color || 'var(--text-muted)' }"><Icon :name="t.mark.icon" :size="14" /></span></div><span v-if="t.desc" :style="T_DESC">{{ t.desc }}</span></div><IconButton v-if="t.more" icon="ellipsis" size="sm" muted :label="L.more" :title="L.more" @click="e => more(r, ri, e)" /></template></div></div><code v-else-if="c.code && (typeof r[c.key] === 'string' || typeof r[c.key] === 'number') && r[c.key] !== ''" :style="codeChip">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</code><template v-else>{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</template></td>
191
+ <td v-for="(c, i) in columns" :key="c.key" :style="{ ...cellPad(i), ...(c.twoLine ? { paddingTop: 'var(--title-cell-py)', paddingBottom: 'var(--title-cell-py)', maxWidth: 0 } : null), textAlign: c.align || 'left', whiteSpace: 'nowrap', ...(resizable ? { overflow: 'hidden', textOverflow: 'ellipsis' } : null), ...cellFont(c) }"><span v-if="selectable && i === 0" :style="checkSlot" :title="isDis(r, ri) && disabledReason ? disabledReason(r, ri) : undefined"><Checkbox :checked="selected.includes(ri)" :disabled="isDis(r, ri)" @change="v => emit('toggle', ri, v)" /></span><div v-if="c.twoLine" :style="{ display: 'flex', alignItems: 'center', minWidth: px(c.minWidth ?? 180), ...(treeCol === c ? { paddingLeft: (item.level * TREE_INDENT) + 'px' } : null) }"><span v-if="treeCol === c" :style="{ width: TREE_TOGGLE + 'px', flexShrink: 0, marginRight: (TREE_INDENT - TREE_TOGGLE) + 'px', display: 'inline-flex' }"><button v-if="item.kids" type="button" :aria-expanded="item.open" :aria-label="item.open ? L.collapse : L.expand" :title="item.open ? L.collapse : L.expand" @click.stop="toggleRow(item)" @mouseenter="hotToggle = item.key" @mouseleave="hotToggle = null" :style="{ width: TREE_TOGGLE + 'px', height: TREE_TOGGLE + 'px', padding: 0, border: 'none', borderRadius: 'var(--radius-sm)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: hotToggle === item.key ? 'var(--overlay-hover)' : 'transparent', color: hotToggle === item.key ? 'var(--text-body)' : 'var(--text-muted)', transition: 'var(--t-hover)' }"><Icon name="chevron-right" :size="16" :style="{ width: 'var(--control-icon)', height: 'var(--control-icon)', transform: item.open ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }" /></button></span><div style="display:flex;align-items:center;gap:var(--title-cell-tile-gap);flex:1;min-width:0"><template v-for="t in [tl(c, r, ri)]" :key="0"><span v-if="t.icon" :style="{ display: 'inline-flex', flexShrink: 0, cursor: canOpen ? 'pointer' : undefined }" @click="e => canOpen && openRow(r, ri, e)"><IconTile :icon="t.icon" :hue="t.hue || 'neutral'" kind="row" :style="T_TILE" :icon-style="T_ICON" /></span><div style="display:flex;flex-direction:column;gap:var(--title-cell-gap);flex:1;min-width:0"><div style="display:flex;align-items:center;gap:4px;min-width:0"><span v-if="canOpen" role="button" tabindex="0" :style="{ ...T_TITLE, cursor: 'pointer' }" @click="e => openRow(r, ri, e)" @keydown="e => openKey(r, ri, e)"><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></span><span v-else :style="T_TITLE"><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></span><svg v-if="t.mark === 'favorite'" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round" role="img" aria-label="Favorite" style="display:block;flex-shrink:0;color:var(--warning)"><path :d="STAR_FILLED" /></svg><span v-else-if="t.mark" role="img" :aria-label="t.mark.label" :style="{ display: 'inline-flex', flexShrink: 0, color: t.mark.color || 'var(--text-muted)' }"><Icon :name="t.mark.icon" :size="14" /></span></div><span v-if="t.desc" :style="T_DESC">{{ t.desc }}</span></div><IconButton v-if="t.more" icon="ellipsis" size="sm" muted :label="L.more" :title="L.more" @click="e => more(r, ri, e)" /></template></div></div><code v-else-if="c.code && (typeof r[c.key] === 'string' || typeof r[c.key] === 'number') && r[c.key] !== ''" :style="codeChip"><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></code><template v-else><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></template></td>
188
192
  </template>
189
193
  </tr>
190
194
  </template>
@@ -11,7 +11,7 @@ export function ProgressBar({ label, value = 50, color = "var(--chart-blue)", sh
11
11
  </div>
12
12
  ) : null}
13
13
  <div style={{ height: 6, borderRadius: "var(--radius-full)", background: "var(--track)", overflow: "hidden" }}>
14
- <div style={{ width: `${value}%`, height: "100%", borderRadius: "var(--radius-full)", background: color, transition: "width .3s ease" }} />
14
+ <div style={{ width: `${value}%`, height: "100%", borderRadius: "var(--radius-full)", background: color, transition: "width var(--dur-slow) var(--ease-out)" }} />
15
15
  </div>
16
16
  </div>
17
17
  );
@@ -9,7 +9,7 @@ defineProps({ label: String, value: { type: Number, default: 50 }, color: { type
9
9
  <span v-if="showValue" style="color:var(--text-heading)">{{ value }}%</span>
10
10
  </div>
11
11
  <div style="height:6px;border-radius:var(--radius-full);background:var(--track);overflow:hidden">
12
- <div :style="{ width: value + '%', height: '100%', borderRadius: 'var(--radius-full)', background: color, transition: 'width .3s ease' }" />
12
+ <div :style="{ width: value + '%', height: '100%', borderRadius: 'var(--radius-full)', background: color, transition: 'width var(--dur-slow) var(--ease-out)' }" />
13
13
  </div>
14
14
  </div>
15
15
  </template>
@@ -8,7 +8,7 @@ export function Accordion({ title, subtitle, defaultOpen, open: openProp, onTogg
8
8
  const toggle = () => (onToggle ? onToggle(!open) : setOpenState(!open));
9
9
  return (
10
10
  <div style={{ borderBottom: "1px solid var(--border-card)", ...style }}>
11
- <div onClick={toggle} data-wash="" style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 8px", margin: "0 -4px", borderRadius: "var(--radius-md)", background: "var(--wash, transparent)", transition: "var(--t-hover)", cursor: "pointer" }}>
11
+ <div role="button" tabIndex={0} aria-expanded={!!open} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } }} onClick={toggle} data-wash="" style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 8px", margin: "0 -4px", borderRadius: "var(--radius-md)", background: "var(--wash, transparent)", transition: "var(--t-hover)", cursor: "pointer" }}>
12
12
  <span style={{ color: "var(--text-muted)", display: "inline-flex", transform: open ? "rotate(90deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}><Icon name="chevron-right" size={14} /></span>
13
13
  <div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
14
14
  <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-heading)" }}>{title}</span>
@@ -10,7 +10,7 @@ function toggle() { if (props.open != null) emit('toggle', !open.value); else op
10
10
  </script>
11
11
  <template>
12
12
  <div :style="{ borderBottom: '1px solid var(--border-card)', ...style }">
13
- <div @click="toggle" data-wash="" style="display:flex;align-items:center;gap:10px;padding:14px 8px;margin:0 -4px;border-radius:var(--radius-md);background:var(--wash, transparent);transition:var(--t-hover);cursor:pointer">
13
+ <div role="button" tabindex="0" :aria-expanded="!!open" @keydown.enter.prevent="toggle" @keydown.space.prevent="toggle" @click="toggle" data-wash="" style="display:flex;align-items:center;gap:10px;padding:14px 8px;margin:0 -4px;border-radius:var(--radius-md);background:var(--wash, transparent);transition:var(--t-hover);cursor:pointer">
14
14
  <span :style="{ color: 'var(--text-muted)', display: 'inline-flex', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon name="chevron-right" :size="14" /></span>
15
15
  <div style="display:flex;flex-direction:column;flex:1">
16
16
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
@@ -30,13 +30,13 @@ export function ContextMenu({ items = [], onSelect, size, descriptionSide = "rig
30
30
  };
31
31
  const onBlur = (e) => { setHover(-1); if (hasHint && !(wrap.current && wrap.current.contains(e.relatedTarget))) setHint(-1); };
32
32
  const menu = (
33
- <div role="menu" data-size={hasHint ? undefined : size} style={{ background: "var(--bg-surface)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-menu)", padding: 4, minWidth: 148, display: "inline-flex", flexDirection: "column", gap: choice ? 2 : 0, ...style }}>
33
+ <div role="menu" data-size={hasHint ? undefined : size} style={{ background: "var(--bg-surface)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-menu)", padding: 4, minWidth: 148, maxWidth: 320, display: "inline-flex", flexDirection: "column", gap: choice ? 2 : 0, ...style }}>
34
34
  {items.map((it, i) => it.divider ? <div key={i} role="separator" style={{ height: 1, flexShrink: 0, background: "var(--border-subtle)", margin: "4px 0" }} /> : (
35
35
  <button key={i} type="button" onClick={() => onSelect && onSelect(it.label)}
36
36
  onMouseEnter={() => show(i)} onMouseLeave={() => setHover(-1)} onFocus={() => show(i)} onBlur={onBlur}
37
37
  aria-describedby={hint === i ? hintId : undefined}
38
- role={choice ? "menuitemradio" : "menuitem"} aria-checked={choice ? !!it.selected : undefined} style={{ display: "flex", alignItems: "center", gap: 10, height: "var(--control-h)", flexShrink: 0, padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", background: it.selected || hover === i || hint === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontWeight: it.selected ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: it.danger ? "var(--danger)" : "var(--text-heading)", textAlign: "left", whiteSpace: "nowrap" }}>
39
- {it.icon ? <Icon name={it.icon} size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) }} /> : null}{it.label}
38
+ role={choice ? "menuitemradio" : "menuitem"} aria-checked={choice ? !!it.selected : undefined} style={{ display: "flex", alignItems: "center", gap: 10, height: "var(--control-h)", flexShrink: 0, padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", background: it.selected || hover === i || hint === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontWeight: it.selected ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: it.danger ? "var(--danger)" : "var(--text-heading)", textAlign: "left", whiteSpace: "nowrap", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }} title={it.label}>
39
+ {it.icon ? <Icon name={it.icon} size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) }} /> : null}<span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{it.label}</span>
40
40
  </button>
41
41
  ))}
42
42
  </div>
@@ -26,8 +26,8 @@ function show(i) {
26
26
  }
27
27
  function onBlur(e) { hover.value = -1; if (hasHint.value && !(wrap.value && wrap.value.contains(e.relatedTarget))) hint.value = -1 }
28
28
  const cur = computed(() => props.items[hint.value])
29
- const menuStyle = computed(() => ({ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', display: 'inline-flex', flexDirection: 'column', gap: choice.value ? '2px' : 0, ...props.style }))
30
- const rowStyle = (it, i) => ({ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', flexShrink: 0, padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: it.selected || hover.value === i || hint.value === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontWeight: it.selected ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap' })
29
+ const menuStyle = computed(() => ({ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', maxWidth: '320px', display: 'inline-flex', flexDirection: 'column', gap: choice.value ? '2px' : 0, ...props.style }))
30
+ const rowStyle = (it, i) => ({ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', flexShrink: 0, padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: it.selected || hover.value === i || hint.value === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontWeight: it.selected ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' })
31
31
  const iconStyle = (it) => ({ width: 'var(--control-icon)', height: 'var(--control-icon)', flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) })
32
32
  const hintStyle = computed(() => ({ position: 'absolute', top: 0, [side.value === 'left' ? 'right' : 'left']: `calc(100% + ${HINT_GAP}px)`, width: HINT_W + 'px', boxSizing: 'border-box', padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: '6px', background: 'var(--bg-surface)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', fontFamily: 'var(--font-sans)', animation: 'kb-fade-in var(--dur) var(--ease-out)' }))
33
33
  </script>
@@ -38,7 +38,7 @@ const hintStyle = computed(() => ({ position: 'absolute', top: 0, [side.value ==
38
38
  <div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
39
39
  <button v-else type="button" @click="emit('select', it.label)" @mouseenter="show(i)" @mouseleave="hover = -1" @focus="show(i)" @blur="onBlur"
40
40
  :aria-describedby="hint === i ? hintId : undefined" :role="choice ? 'menuitemradio' : 'menuitem'" :aria-checked="choice ? !!it.selected : undefined" :style="rowStyle(it, i)">
41
- <Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)" />{{ it.label }}
41
+ <Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)" /><span style="min-width:0;overflow:hidden;text-overflow:ellipsis">{{ it.label }}</span>
42
42
  </button>
43
43
  </template>
44
44
  </div>
@@ -55,7 +55,7 @@ const hintStyle = computed(() => ({ position: 'absolute', top: 0, [side.value ==
55
55
  <div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
56
56
  <button v-else type="button" @click="emit('select', it.label)" @mouseenter="hover = i" @mouseleave="hover = -1" @focus="hover = i" @blur="hover = -1"
57
57
  :role="choice ? 'menuitemradio' : 'menuitem'" :aria-checked="choice ? !!it.selected : undefined" :style="rowStyle(it, i)">
58
- <Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)" />{{ it.label }}
58
+ <Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)" /><span style="min-width:0;overflow:hidden;text-overflow:ellipsis">{{ it.label }}</span>
59
59
  </button>
60
60
  </template>
61
61
  </div>
@@ -5,10 +5,10 @@ import { IconButton } from "../buttons/IconButton.jsx";
5
5
  export function Drawer({ open = true, title, subtitle, width = 400, inline, height, size, footer, onClose, children, style }) {
6
6
  if (!open) return null;
7
7
  const panel = (
8
- <div role="dialog" aria-label={title} data-size={size} style={{ width, maxWidth: "100%", height: inline ? (height || 420) : "100%", background: "var(--bg-raised)", borderLeft: "1px solid var(--border-card)", boxShadow: inline ? "var(--shadow-menu)" : "-8px 0 24px rgba(0,0,0,.12)", display: "flex", flexDirection: "column", boxSizing: "border-box", ...(inline ? { border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)" } : null), ...style }}>
8
+ <div role="dialog" aria-label={title} data-size={size} style={{ width, maxWidth: "100%", height: inline ? (height || 420) : "100%", background: "var(--bg-raised)", borderLeft: "1px solid var(--border-card)", boxShadow: inline ? "var(--shadow-menu)" : "var(--shadow-drawer)", display: "flex", flexDirection: "column", boxSizing: "border-box", ...(inline ? { border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)" } : null), ...style }}>
9
9
  <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", padding: "16px 16px 12px", borderBottom: "1px solid var(--border-card)", flexShrink: 0 }}>
10
- <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
11
- <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--text-md)", lineHeight: "var(--leading-md)", letterSpacing: "var(--tracking-md)", color: "var(--text-heading)" }}>{title}</span>
10
+ <div style={{ display: "flex", flexDirection: "column", gap: 3, minWidth: 0 }}>
11
+ <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--text-md)", lineHeight: "var(--leading-md)", letterSpacing: "var(--tracking-md)", color: "var(--text-heading)", minWidth: 0, overflowWrap: "anywhere" }}>{title}</span>
12
12
  {subtitle && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-secondary)" }}>{subtitle}</span>}
13
13
  </div>
14
14
  <IconButton icon="x" label="Close" onClick={onClose} style={{ margin: "calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0" }} />
@@ -14,10 +14,10 @@ const contents = { display: 'contents' }
14
14
  :style="inline ? contents : { position: 'fixed', inset: 0, zIndex: 'var(--z-modal)', background: 'var(--bg-scrim)', display: 'flex', justifyContent: 'flex-end', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)' }">
15
15
  <div @click.stop :style="inline ? contents : { height: '100%', animation: 'kb-slide-in var(--dur) var(--ease-out)' }">
16
16
  <div role="dialog" :aria-label="title" :data-size="size"
17
- :style="{ width: px(width), maxWidth: '100%', height: inline ? px(height) : '100%', background: 'var(--bg-raised)', borderLeft: '1px solid var(--border-card)', boxShadow: inline ? 'var(--shadow-menu)' : '-8px 0 24px rgba(0,0,0,.12)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', ...(inline ? { border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)' } : null), ...style }">
17
+ :style="{ width: px(width), maxWidth: '100%', height: inline ? px(height) : '100%', background: 'var(--bg-raised)', borderLeft: '1px solid var(--border-card)', boxShadow: inline ? 'var(--shadow-menu)' : 'var(--shadow-drawer)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', ...(inline ? { border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)' } : null), ...style }">
18
18
  <div style="display:flex;align-items:flex-start;justify-content:space-between;padding:16px 16px 12px;border-bottom:1px solid var(--border-card);flex-shrink:0">
19
- <div style="display:flex;flex-direction:column;gap:3px">
20
- <span style="font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)">{{ title }}</span>
19
+ <div style="display:flex;flex-direction:column;gap:3px;min-width:0">
20
+ <span style="font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading);min-width:0;overflow-wrap:anywhere">{{ title }}</span>
21
21
  <span v-if="subtitle" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)">{{ subtitle }}</span>
22
22
  </div>
23
23
  <IconButton icon="x" label="Close" :style="{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }" @click="emit('close')" />
@@ -3,6 +3,9 @@ import Icon from '../icons/Icon.vue'
3
3
  const tlTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }
4
4
  // Vertical activity timeline: outlined icon nodes on a hairline spine. items: [{icon,tone,title,meta,body}].
5
5
  defineProps({ items: { type: Array, default: () => [] }, style: Object })
6
+ // body may be text or a VNode (h(...)); a #body="{ item, index }" slot overrides it.
7
+ const Cell = (p) => p.v
8
+ Cell.props = ['v']
6
9
  </script>
7
10
  <template>
8
11
  <div :style="{ display: 'flex', flexDirection: 'column', ...style }">
@@ -14,7 +17,7 @@ defineProps({ items: { type: Array, default: () => [] }, style: Object })
14
17
  <div :style="{ display: 'flex', flexDirection: 'column', gap: '2px', paddingBottom: i < items.length - 1 ? '18px' : 0, minWidth: 0, paddingTop: '3px' }">
15
18
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ it.title }}</span>
16
19
  <span v-if="it.meta" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)">{{ it.meta }}</span>
17
- <span v-if="it.body" style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px">{{ it.body }}</span>
20
+ <span v-if="it.body" style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px"><slot name="body" :item="it" :index="i"><Cell :v="it.body" /></slot></span>
18
21
  </div>
19
22
  </div>
20
23
  </div>
@@ -9,6 +9,8 @@ export interface CheckboxProps {
9
9
  /** Size tier. Omit to inherit the enclosing region's tier (the default). */
10
10
  size?: "sm" | "md" | "lg";
11
11
  disabled?: boolean;
12
+ /** Tab order. Default 0; pass -1 inside a control that owns focus (Tree row, MultiSelect option). */
13
+ tabIndex?: number;
12
14
  style?: React.CSSProperties;
13
15
  }
14
16
  export declare function Checkbox(props: CheckboxProps): JSX.Element;
@@ -1,13 +1,18 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- /** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text. */
5
- export function Checkbox({ checked, indeterminate, onChange, label, size, disabled, style }) {
4
+ /** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.
5
+ * Keyboard: the label is role="checkbox" (aria-checked true / false / "mixed"), a Tab stop, Space toggles. Inside a control that owns focus
6
+ * (a Tree row, a MultiSelect option) pass tabIndex={-1} so the row stays the single stop. */
7
+ export function Checkbox({ checked, indeterminate, onChange, label, size, disabled, tabIndex, style }) {
8
+ const flip = () => { if (!disabled && onChange) onChange(!checked); };
6
9
  const box = "var(--check-size)", glyph = { width: "calc(var(--check-size) - 4px)", height: "calc(var(--check-size) - 4px)" };
7
10
  const filled = checked || indeterminate;
8
11
  return (
9
- <label data-size={size} data-check={disabled ? undefined : ""} style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--control-leading)", gap: "var(--control-gap)", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, ...style }}
10
- onClick={() => !disabled && onChange && onChange(!checked)}>
12
+ <label data-size={size} data-check={disabled ? undefined : ""} role="checkbox" aria-checked={indeterminate && !checked ? "mixed" : !!checked} aria-disabled={disabled || undefined}
13
+ tabIndex={disabled ? -1 : tabIndex ?? 0} onKeyDown={(e) => { if (e.key === " ") { e.preventDefault(); e.stopPropagation(); flip(); } }}
14
+ style={{ outlineOffset: 2, display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--control-leading)", gap: "var(--control-gap)", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, ...style }}
15
+ onClick={flip}>
11
16
  <span style={{ width: box, height: box, borderRadius: 4, boxSizing: "border-box", border: filled ? "none" : "1.5px solid var(--check-border, var(--checkbox-border))", background: filled ? "var(--action-primary)" : "var(--bg-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--on-accent)", flexShrink: 0, transition: "var(--t-hover)" }}>
12
17
  {checked ? <Icon name="check" size={12} strokeWidth={3} style={glyph} /> : indeterminate ? <Icon name="minus" size={12} strokeWidth={3} style={glyph} /> : null}
13
18
  </span>
@@ -2,13 +2,17 @@
2
2
  import { computed } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  // Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.
5
- const props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: String, disabled: Boolean, style: Object })
5
+ // Keyboard: the label is role="checkbox" (aria-checked true / false / "mixed"), a Tab stop, Space toggles. Inside a control that owns focus
6
+ // (a Tree row, a MultiSelect option) pass :tabindex="-1" so the row stays the single stop.
7
+ const props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: String, disabled: Boolean, tabindex: { type: [Number, String], default: 0 }, style: Object })
6
8
  const emit = defineEmits(['change'])
7
9
  const box = 'var(--check-size)', glyph = { width: 'calc(var(--check-size) - 4px)', height: 'calc(var(--check-size) - 4px)' }
8
10
  const filled = computed(() => props.checked || props.indeterminate)
11
+ function flip() { if (!props.disabled) emit('change', !props.checked) }
12
+ function onKey(e) { if (e.key === ' ') { e.preventDefault(); e.stopPropagation(); flip() } }
9
13
  </script>
10
14
  <template>
11
- <label :data-size="size" :data-check="disabled ? undefined : ''" :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="!disabled && emit('change', !checked)">
15
+ <label :data-size="size" :data-check="disabled ? undefined : ''" role="checkbox" :aria-checked="indeterminate && !checked ? 'mixed' : !!checked" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : tabindex" @keydown="onKey" :style="{ outlineOffset: '2px', display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="flip">
12
16
  <span :style="{ width: box, height: box, borderRadius: '4px', boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid var(--check-border, var(--checkbox-border))', background: filled ? 'var(--action-primary)' : 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, transition: 'var(--t-hover)' }">
13
17
  <Icon v-if="checked" name="check" :size="12" :stroke-width="3" :style="glyph" />
14
18
  <Icon v-else-if="indeterminate" name="minus" :size="12" :stroke-width="3" :style="glyph" />
@@ -26,7 +26,9 @@ export function DatePicker({ value = "", placeholder = "Select date", disabled,
26
26
  const monthTitle = fmt(locale, { year: "numeric", month: "long" }).format(new Date(view.y, view.m, 1));
27
27
  return (
28
28
  <div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
29
- <div data-field={disabled ? undefined : ""} onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: "flex", alignItems: "center", justifyContent: "space-between", cursor: disabled ? "not-allowed" : "pointer", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--field-border, var(--border-control))", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, color: label ? "var(--text-primary)" : "var(--text-muted)" }}>
29
+ <div data-field={disabled ? undefined : ""} role="combobox" aria-haspopup="dialog" aria-expanded={open} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
30
+ onKeyDown={(e) => { if (disabled) return; if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault(); setOpen(true); } else if (e.key === "Escape") setOpen(false); }}
31
+ onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: "flex", alignItems: "center", justifyContent: "space-between", cursor: disabled ? "not-allowed" : "pointer", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--field-border, var(--border-control))", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, color: label ? "var(--text-primary)" : "var(--text-muted)" }}>
30
32
  {label || placeholder}<span style={{ color: "var(--text-muted)", display: "inline-flex" }}><Icon name="calendar" size={16} style={tierIcon} /></span>
31
33
  </div>
32
34
  {open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, zIndex: "var(--z-menu)", width: 252, background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-menu)", padding: 12, boxSizing: "border-box" }}>
@@ -26,10 +26,11 @@ function pick(i) { emit('update:modelValue', dayIso(i)); open.value = false }
26
26
  function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
27
27
  onMounted(() => document.addEventListener('mousedown', onDoc))
28
28
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
29
+ function onFieldKey(e) { if (props.disabled) return; if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); open.value = true } else if (e.key === 'Escape') open.value = false }
29
30
  </script>
30
31
  <template>
31
32
  <div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
32
- <div :data-field="disabled ? undefined : ''" @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
33
+ <div :data-field="disabled ? undefined : ''" role="combobox" aria-haspopup="dialog" :aria-expanded="open" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0" @keydown="onFieldKey" @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
33
34
  {{ label || placeholder }}<span style="color:var(--text-muted);display:inline-flex"><Icon name="calendar" :size="16" :style="tierIcon" /></span>
34
35
  </div>
35
36
  <div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-menu);width:252px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);padding:12px;box-sizing:border-box">
@@ -22,7 +22,7 @@ export function Dropzone({ title = "Click to upload or drag and drop", hint = "J
22
22
  const tone = disabled ? "default" : error ? "error" : dragActive ? "drag" : hover ? "hover" : "default";
23
23
  const stop = (e) => { e.preventDefault(); e.stopPropagation(); };
24
24
  return (
25
- <div onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
25
+ <div role="button" tabIndex={disabled ? -1 : 0} aria-disabled={disabled || undefined} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (!disabled && onClick) onClick(e); } }} onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
26
26
  onDragEnter={disabled ? undefined : (e) => { stop(e); setDragging(true); }}
27
27
  onDragOver={disabled ? undefined : stop}
28
28
  onDragLeave={disabled ? undefined : (e) => { stop(e); setDragging(false); }}
@@ -17,7 +17,7 @@ function onDrop(e) { stop(e); dragging.value = false; emit('dropFiles', e.dataTr
17
17
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)">{{ file }}</span>
18
18
  <button type="button" data-link="" @click="$emit('remove')" style="display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-base);padding:0;text-decoration-line:var(--link-line, none);text-underline-offset:2px"><Icon name="trash-2" :size="13" />Remove</button>
19
19
  </div>
20
- <div v-else @click="!disabled && $emit('click')" @mouseenter="hover = true" @mouseleave="hover = false"
20
+ <div v-else role="button" :tabindex="disabled ? -1 : 0" :aria-disabled="disabled || undefined" @keydown.enter.prevent="!disabled && $emit('click')" @keydown.space.prevent="!disabled && $emit('click')" @click="!disabled && $emit('click')" @mouseenter="hover = true" @mouseleave="hover = false"
21
21
  @dragenter="!disabled && ((e) => { stop(e); dragging = true })($event)" @dragover="!disabled && stop($event)"
22
22
  @dragleave="!disabled && ((e) => { stop(e); dragging = false })($event)" @drop="!disabled && onDrop($event)"
23
23
  :style="{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }">
@@ -14,7 +14,9 @@ export function MultiSelect({ options = [], value = [], placeholder = "Select...
14
14
  const shown = value.slice(0, maxTags), extra = value.length - shown.length;
15
15
  return (
16
16
  <div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
17
- <div data-field={disabled ? undefined : ""} onClick={() => !disabled && setOpen(!open)} style={{ minHeight: "var(--control-h)", borderRadius: "var(--radius-md)", border: `1px solid ${open ? "var(--focus-border)" : "var(--field-border, var(--border-control))"}`, boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", background: "var(--bg-surface)", padding: "calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)", boxSizing: "border-box", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, position: "relative" }}>
17
+ <div data-field={disabled ? undefined : ""} role="combobox" aria-haspopup="listbox" aria-expanded={open} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
18
+ onKeyDown={(e) => { if (disabled) return; if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault(); setOpen(true); } else if (e.key === "Escape") setOpen(false); }}
19
+ onClick={() => !disabled && setOpen(!open)} style={{ minHeight: "var(--control-h)", borderRadius: "var(--radius-md)", border: `1px solid ${open ? "var(--focus-border)" : "var(--field-border, var(--border-control))"}`, boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", background: "var(--bg-surface)", padding: "calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)", boxSizing: "border-box", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, position: "relative" }}>
18
20
  {value.length === 0 && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-muted)", paddingLeft: 4 }}>{placeholder}</span>}
19
21
  {shown.map(v => <Tag key={v} removable onRemove={() => toggle(v)}>{v}</Tag>)}
20
22
  {extra > 0 && <Tag>+{extra}</Tag>}
@@ -24,9 +26,9 @@ export function MultiSelect({ options = [], value = [], placeholder = "Select...
24
26
  </span>
25
27
  </div>
26
28
  {open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, right: 0, zIndex: "var(--z-menu)", background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", boxShadow: "var(--shadow-menu)", padding: 4, maxHeight: 208, overflowY: "auto" }}>
27
- {options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: "flex", alignItems: "center", gap: 8, height: "var(--control-h)", padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
29
+ {options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(o); } else if (e.key === "Escape") setOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, height: "var(--control-h)", padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
28
30
  onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
29
- <Checkbox checked={value.includes(o)} onChange={() => toggle(o)} />
31
+ <Checkbox checked={value.includes(o)} tabIndex={-1} />
30
32
  <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)" }}>{o}</span>
31
33
  </div>)}
32
34
  </div>}
@@ -14,10 +14,11 @@ const extra = computed(() => props.modelValue.length - shown.value.length)
14
14
  function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
15
15
  onMounted(() => document.addEventListener('mousedown', onDoc))
16
16
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
17
+ function onFieldKey(e) { if (props.disabled) return; if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); open.value = true } else if (e.key === 'Escape') open.value = false }
17
18
  </script>
18
19
  <template>
19
20
  <div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
20
- <div :data-field="disabled ? undefined : ''" @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
21
+ <div :data-field="disabled ? undefined : ''" role="combobox" aria-haspopup="listbox" :aria-expanded="open" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0" @keydown="onFieldKey" @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
21
22
  <span v-if="modelValue.length === 0" style="font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);padding-left:4px">{{ placeholder }}</span>
22
23
  <Tag v-for="v in shown" :key="v" removable @remove="toggle(v)">{{ v }}</Tag>
23
24
  <Tag v-if="extra > 0">+{{ extra }}</Tag>
@@ -27,8 +28,8 @@ onUnmounted(() => document.removeEventListener('mousedown', onDoc))
27
28
  </span>
28
29
  </div>
29
30
  <div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto">
30
- <div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer">
31
- <Checkbox :checked="modelValue.includes(o)" @change="toggle(o)" />
31
+ <div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" @keydown.enter.prevent="toggle(o)" @keydown.space.prevent="toggle(o)" @keydown.esc="open = false" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer">
32
+ <Checkbox :checked="modelValue.includes(o)" :tabindex="-1" />
32
33
  <span style="font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)">{{ o }}</span>
33
34
  </div>
34
35
  </div>
@@ -12,7 +12,7 @@ export function Slider({ value = 50, min = 0, max = 100, step = 1, labels, showV
12
12
  <div ref={track} data-thumb={disabled ? undefined : ""} onMouseDown={disabled ? undefined : (e) => { setDrag(true); set(e.clientX); }} style={{ position: "relative", height: 20, display: "flex", alignItems: "center", cursor: disabled ? "not-allowed" : "pointer" }}>
13
13
  <div style={{ position: "absolute", left: 0, right: 0, height: 6, borderRadius: "var(--radius-full)", background: "var(--track)" }}></div>
14
14
  <div style={{ position: "absolute", left: 0, width: pct + "%", height: 6, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }}></div>
15
- <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", width: 16, height: 16, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", border: "2px solid var(--action-primary)", boxSizing: "border-box", boxShadow: drag ? "0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)" : "var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))", transition: "box-shadow var(--dur-fast) var(--ease-out)" }}></span>
15
+ <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", width: 16, height: 16, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", border: "2px solid var(--action-primary)", boxSizing: "border-box", boxShadow: drag ? "0 0 0 4px var(--focus-ring), var(--shadow-thumb)" : "var(--thumb-ring, var(--shadow-thumb))", transition: "box-shadow var(--dur-fast) var(--ease-out)" }}></span>
16
16
  {(drag || showValue) && <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", bottom: 22, background: "var(--tooltip-bg)", color: "var(--tooltip-fg)", borderRadius: "var(--radius-sm)", padding: "2px 6px", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", whiteSpace: "nowrap", pointerEvents: "none" }}>{value}{unit}</span>}
17
17
  </div>
18
18
  {labels && <div style={{ display: "flex", justifyContent: "space-between", paddingTop: 4, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-muted)" }}><span>{min}{unit}</span><span>{max}{unit}</span></div>}
@@ -15,7 +15,7 @@ onUnmounted(up)
15
15
  <div ref="track" :data-thumb="disabled ? undefined : ''" @mousedown="!disabled && (drag = true, set($event.clientX))" :style="{ position: 'relative', height: '20px', display: 'flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }">
16
16
  <div style="position:absolute;left:0;right:0;height:6px;border-radius:var(--radius-full);background:var(--track)" />
17
17
  <div :style="{ position: 'absolute', left: 0, width: pct + '%', height: '6px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }" />
18
- <span :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)' : 'var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))', transition: 'box-shadow var(--dur-fast) var(--ease-out)' }" />
18
+ <span :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring), var(--shadow-thumb)' : 'var(--thumb-ring, var(--shadow-thumb))', transition: 'box-shadow var(--dur-fast) var(--ease-out)' }" />
19
19
  <span v-if="drag || showValue" :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', bottom: '22px', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-sm)', padding: '2px 6px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', whiteSpace: 'nowrap', pointerEvents: 'none' }">{{ modelValue }}{{ unit }}</span>
20
20
  </div>
21
21
  <div v-if="labels" style="display:flex;justify-content:space-between;padding-top:4px;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-muted)"><span>{{ min }}{{ unit }}</span><span>{{ max }}{{ unit }}</span></div>