@keyfunc/keybricks-ds 2.4.7 → 2.4.8

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 (40) hide show
  1. package/DEPLOY.md +2 -2
  2. package/DESIGN.md +4 -4
  3. package/INTEGRATION.md +17 -2
  4. package/_adherence.oxlintrc.json +4 -4
  5. package/assets/icons/icon-index.js +1 -1
  6. package/assets/icons/icon-index.json +1 -1
  7. package/components/_cards/segmented-control.card.html +1 -1
  8. package/components/_cards/table.card.html +139 -6
  9. package/components/_cards/toggles.card.html +27 -27
  10. package/components/_shared/src-data.js +1 -1
  11. package/components/_shared/src-display.js +1 -1
  12. package/components/_shared/src-forms.js +1 -1
  13. package/components/_shared/src-icons.js +1 -1
  14. package/components/_shared/src-navigation.js +1 -1
  15. package/components/_shared/src-tabs.js +1 -1
  16. package/components/data/SmartTable.d.ts +9 -1
  17. package/components/data/SmartTable.jsx +24 -4
  18. package/components/data/SmartTable.vue +18 -5
  19. package/components/data/Spreadsheet.d.ts +5 -1
  20. package/components/data/Spreadsheet.jsx +23 -3
  21. package/components/data/Spreadsheet.vue +18 -5
  22. package/components/data/Table.d.ts +25 -4
  23. package/components/data/Table.jsx +74 -20
  24. package/components/data/Table.vue +56 -14
  25. package/components/display/Tag.d.ts +1 -1
  26. package/components/display/Tag.jsx +1 -1
  27. package/components/display/Tag.vue +1 -1
  28. package/components/forms/Switch.d.ts +2 -2
  29. package/components/forms/Switch.jsx +10 -8
  30. package/components/forms/Switch.vue +5 -4
  31. package/components/forms/forms.prompt.md +1 -1
  32. package/components/icons/Icon.jsx +2 -1
  33. package/components/icons/icon-paths.js +2 -1
  34. package/components/navigation/Breadcrumb.jsx +9 -7
  35. package/components/navigation/Breadcrumb.vue +16 -14
  36. package/components/tabs/CountBadge.vue +1 -1
  37. package/components/tabs/Tabs.d.ts +1 -1
  38. package/components/tabs/Tabs.jsx +1 -1
  39. package/package.json +1 -1
  40. package/tokens/layout.css +5 -5
@@ -40,7 +40,7 @@ function Overflow({ items, onSelect }) {
40
40
  </span>
41
41
  {open ? (
42
42
  <span style={{ position: "absolute", top: "calc(100% + 5px)", left: 0, zIndex: "var(--z-menu)", minWidth: 168, maxWidth: 260, background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", boxShadow: "var(--shadow-menu)", padding: 4, display: "flex", flexDirection: "column" }}>
43
- {items.map((it, i) => <OverflowItem key={it} label={it} depth={i} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />)}
43
+ {items.map((it, i) => <OverflowItem key={i} label={it} depth={i} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />)}
44
44
  </span>
45
45
  ) : null}
46
46
  </span>
@@ -67,23 +67,25 @@ export function Breadcrumb({ items = [], onSelect, maxItems = 4, hoveredIndex, s
67
67
  const head = collapse ? items.slice(0, 1) : items;
68
68
  const hidden = collapse ? items.slice(1, items.length - tailCount) : [];
69
69
  const tail = collapse ? items.slice(items.length - tailCount) : [];
70
- const lastLabel = items[items.length - 1];
70
+ /* Crumbs are identified by position, never by label: a trail may repeat a name ("Workflow › Workflow", two "2026" folders). Only the last index is the current page. */
71
+ const lastIndex = items.length - 1;
72
+ const tailStart = items.length - tailCount;
71
73
  return (
72
74
  <nav style={{ display: "flex", alignItems: "center", gap: 4, ...style }}>
73
75
  {head.map((it, i) => (
74
- <React.Fragment key={it}>
76
+ <React.Fragment key={i}>
75
77
  {i > 0 ? <Sep /> : null}
76
- <Crumb label={it} last={it === lastLabel} hovered={hoveredIndex === i} onSelect={onSelect} />
78
+ <Crumb label={it} last={i === lastIndex} hovered={hoveredIndex === i} onSelect={onSelect} />
77
79
  </React.Fragment>
78
80
  ))}
79
81
  {collapse ? (
80
82
  <React.Fragment>
81
83
  <Sep />
82
84
  <Overflow items={hidden} onSelect={onSelect} />
83
- {tail.map((it) => (
84
- <React.Fragment key={it}>
85
+ {tail.map((it, k) => (
86
+ <React.Fragment key={tailStart + k}>
85
87
  <Sep />
86
- <Crumb label={it} last={it === lastLabel} onSelect={onSelect} />
88
+ <Crumb label={it} last={tailStart + k === lastIndex} onSelect={onSelect} />
87
89
  </React.Fragment>
88
90
  ))}
89
91
  </React.Fragment>
@@ -16,23 +16,25 @@ const tailCount = computed(() => props.maxItems - 2)
16
16
  const head = computed(() => collapse.value ? props.items.slice(0, 1) : props.items)
17
17
  const hidden = computed(() => collapse.value ? props.items.slice(1, props.items.length - tailCount.value) : [])
18
18
  const tail = computed(() => collapse.value ? props.items.slice(props.items.length - tailCount.value) : [])
19
- const lastLabel = computed(() => props.items[props.items.length - 1])
20
- const isLast = (it) => it === lastLabel.value
21
- const on = (i, it) => (hover.value === i || props.hoveredIndex === i) && !isLast(it)
22
- function pick(it) { if (!isLast(it)) emit('select', it) }
19
+ // Crumbs are identified by position, never by label: a trail may repeat a name ("Workflow › Workflow", two "2026" folders). Only the last index is the current page.
20
+ const tailStart = computed(() => props.items.length - tailCount.value)
21
+ const isLast = (i) => i === props.items.length - 1
22
+ // i = the crumb's index in items; hoveredIndex reaches the leading crumbs only (as in React)
23
+ const on = (i, lead) => (hover.value === i || (lead && props.hoveredIndex === i)) && !isLast(i)
24
+ function pick(it, i) { if (!isLast(i)) emit('select', it) }
23
25
  function pickHidden(it) { open.value = false; emit('select', it) }
24
26
  function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
25
27
  watch(open, (v) => { if (v) document.addEventListener('mousedown', onDoc); else document.removeEventListener('mousedown', onDoc) })
26
28
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
27
- const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: isLast(it) || on(i, it) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(it) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(it) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })
29
+ const crumbStyle = (i, lead) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: isLast(i) || on(i, lead) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(i) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(i) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })
28
30
  </script>
29
31
  <template>
30
32
  <nav :style="{ display: 'flex', alignItems: 'center', gap: '4px', ...style }">
31
- <template v-for="(it, i) in head" :key="it">
33
+ <template v-for="(it, i) in head" :key="i">
32
34
  <span v-if="i > 0" style="color:var(--text-muted);display:inline-flex"><Icon name="chevron-right" :size="13" /></span>
33
- <span :role="isLast(it) ? undefined : 'button'" :tabindex="isLast(it) ? undefined : 0"
34
- @click="pick(it)" @keydown.enter.prevent="pick(it)" @keydown.space.prevent="pick(it)"
35
- @mouseenter="hover = i" @mouseleave="hover = -1" :style="crumbStyle(i, it)">{{ it }}</span>
35
+ <span :role="isLast(i) ? undefined : 'button'" :tabindex="isLast(i) ? undefined : 0"
36
+ @click="pick(it, i)" @keydown.enter.prevent="pick(it, i)" @keydown.space.prevent="pick(it, i)"
37
+ @mouseenter="hover = i" @mouseleave="hover = -1" :style="crumbStyle(i, true)">{{ it }}</span>
36
38
  </template>
37
39
  <template v-if="collapse">
38
40
  <span style="color:var(--text-muted);display:inline-flex"><Icon name="chevron-right" :size="13" /></span>
@@ -44,7 +46,7 @@ const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(
44
46
  <Icon name="ellipsis" :size="14" />
45
47
  </span>
46
48
  <span v-if="open" style="position:absolute;top:calc(100% + 5px);left:0;z-index:var(--z-menu);min-width:168px;max-width:260px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;display:flex;flex-direction:column">
47
- <span v-for="(h, hi) in hidden" :key="h" role="menuitem" tabindex="0"
49
+ <span v-for="(h, hi) in hidden" :key="hi" role="menuitem" tabindex="0"
48
50
  @click="pickHidden(h)" @keydown.enter.prevent="pickHidden(h)" @keydown.space.prevent="pickHidden(h)"
49
51
  @mouseenter="menuHover = hi" @mouseleave="menuHover = -1"
50
52
  :style="{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }">
@@ -52,11 +54,11 @@ const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(
52
54
  </span>
53
55
  </span>
54
56
  </span>
55
- <template v-for="it in tail" :key="it">
57
+ <template v-for="(it, k) in tail" :key="tailStart + k">
56
58
  <span style="color:var(--text-muted);display:inline-flex"><Icon name="chevron-right" :size="13" /></span>
57
- <span :role="isLast(it) ? undefined : 'button'" :tabindex="isLast(it) ? undefined : 0"
58
- @click="pick(it)" @keydown.enter.prevent="pick(it)" @keydown.space.prevent="pick(it)"
59
- :style="crumbStyle(-1, it)">{{ it }}</span>
59
+ <span :role="isLast(tailStart + k) ? undefined : 'button'" :tabindex="isLast(tailStart + k) ? undefined : 0"
60
+ @click="pick(it, tailStart + k)" @keydown.enter.prevent="pick(it, tailStart + k)" @keydown.space.prevent="pick(it, tailStart + k)"
61
+ @mouseenter="hover = tailStart + k" @mouseleave="hover = -1" :style="crumbStyle(tailStart + k, false)">{{ it }}</span>
60
62
  </template>
61
63
  </template>
62
64
  </nav>
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed } from 'vue'
3
- // Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures
3
+ // Count badge beside a segment / pill label: --count-h tall (sm 16 · md and lg 18), 6px sides, full radius, 12px tabular figures
4
4
  // (digits are Latin, so not bumped in CJK). Fill: the alpha wash --neutral-alpha-8, so it darkens any surface by the same step (white tab,
5
5
  // #f5f5f5 active pill, dark theme); text tells selected from unselected: --text-heading vs --text-body (a lighter grey fails 4.5:1 at 12px).
6
6
  // Hidden for 0 or a missing count unless showZero, which draws a muted 0.
@@ -20,6 +20,6 @@ export type PillTab = TabItem;
20
20
  export type PillTabsProps = TabsProps;
21
21
  /** Text pill filter tabs at control height; active gets a tinted pill. A tab's count renders as a CountBadge 6px right of the label. */
22
22
  export declare function PillTabs(props: PillTabsProps): JSX.Element;
23
- /** Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures. Fill --neutral-alpha-8 on any surface; text --text-body, --text-heading when selected. Renders nothing for 0 or undefined unless showZero (muted 0). */
23
+ /** Count badge beside a segment / pill label: --count-h tall (sm 16 · md and lg 18), 6px sides, full radius, 12px tabular figures. Fill --neutral-alpha-8 on any surface; text --text-body, --text-heading when selected. Renders nothing for 0 or undefined unless showZero (muted 0). */
24
24
  export interface CountBadgeProps { count?: number; selected?: boolean; showZero?: boolean; style?: React.CSSProperties; }
25
25
  export declare function CountBadge(props: CountBadgeProps): JSX.Element | null;
@@ -39,7 +39,7 @@ export function PanelTabs({ tabs = [], active, onChange, size, showZero, style }
39
39
  }
40
40
 
41
41
 
42
- /* Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures
42
+ /* Count badge beside a segment / pill label: --count-h tall (sm 16 · md and lg 18), 6px sides, full radius, 12px tabular figures
43
43
  (digits are Latin, so the size is not bumped in CJK). The fill is the alpha wash --neutral-alpha-8, so it darkens whatever it sits on by the
44
44
  same step (white tab, #f5f5f5 active pill, dark theme); the text tells selected from unselected: --text-heading vs --text-body
45
45
  (a lighter grey fails 4.5:1 on a grey fill at 12px). Hidden for 0 or a missing count unless showZero, which draws a muted 0. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyfunc/keybricks-ds",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
4
4
  "description": "Keybricks design system — design tokens, three size tiers and 121 UI components in React and Vue 3, source-only.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/tokens/layout.css CHANGED
@@ -32,9 +32,9 @@
32
32
  --edge-pad:16px; /* card edge → first control or cell */
33
33
  --cell-px:12px; /* horizontal padding of a table cell */
34
34
  --check-size:16px; /* checkbox and radio box */
35
- --tag-h:24px; /* Tag */
36
- --count-h:18px; /* count badge beside a segment / pill label (12px tabular figures, 6px sides, full radius) */
37
- --switch-w:36px;--switch-h:20px; /* Switch track */
35
+ --tag-h:24px; /* Tag — two sizes only: lg keeps the md height */
36
+ --count-h:18px; /* count badge beside a segment / pill label (12px tabular figures, 6px sides, full radius) — lg keeps the md height */
37
+ --switch-w:32px;--switch-h:18px; /* Switch track — two sizes only: lg keeps the md track */
38
38
  /* Two-line title cell (Table column `twoLine`): row padding, title → description gap, icon tile, tile → text gap. md row = 10 + 20 + 2 + 16 + 10 = 58px */
39
39
  --title-cell-py:10px;--title-cell-gap:2px;--title-cell-tile:32px;--title-cell-icon:16px;--title-cell-tile-r:8px;--title-cell-tile-gap:12px;
40
40
  /* Compatibility aliases — superseded by the tier tokens above */
@@ -51,5 +51,5 @@
51
51
  }
52
52
  /* The three tiers. Declared after :root so a tier set on <html> itself also wins. */
53
53
  [data-size="sm"]{--control-h:28px;--control-text:var(--text-xs);--control-leading:var(--leading-xs);--control-tracking:var(--tracking-xs);--control-icon:14px;--control-px:8px;--control-gap:8px;--group-gap:12px;--bar-h:36px;--bar-card-h:44px;--edge-pad:12px;--cell-px:8px;--check-size:14px;--tag-h:20px;--count-h:16px;--switch-w:28px;--switch-h:16px;--text-code:11px;--leading-code:16px;--title-cell-py:8px;--title-cell-gap:2px;--title-cell-tile:28px;--title-cell-icon:14px;--title-cell-tile-r:8px;--title-cell-tile-gap:8px}
54
- [data-size="md"]{--control-h:32px;--control-text:var(--text-base);--control-leading:var(--leading-base);--control-tracking:var(--tracking-base);--control-icon:16px;--control-px:12px;--control-gap:8px;--group-gap:16px;--bar-h:40px;--bar-card-h:48px;--edge-pad:16px;--cell-px:12px;--check-size:16px;--tag-h:24px;--count-h:18px;--switch-w:36px;--switch-h:20px;--text-code:12px;--leading-code:18px;--title-cell-py:10px;--title-cell-gap:2px;--title-cell-tile:32px;--title-cell-icon:16px;--title-cell-tile-r:8px;--title-cell-tile-gap:12px}
55
- [data-size="lg"]{--control-h:40px;--control-text:var(--text-base);--control-leading:var(--leading-base);--control-tracking:var(--tracking-base);--control-icon:16px;--control-px:16px;--control-gap:8px;--group-gap:24px;--bar-h:48px;--bar-card-h:56px;--edge-pad:16px;--cell-px:16px;--check-size:16px;--tag-h:28px;--count-h:20px;--switch-w:44px;--switch-h:24px;--text-code:12px;--leading-code:18px;--title-cell-py:14px;--title-cell-gap:4px;--title-cell-tile:40px;--title-cell-icon:20px;--title-cell-tile-r:10px;--title-cell-tile-gap:12px}
54
+ [data-size="md"]{--control-h:32px;--control-text:var(--text-base);--control-leading:var(--leading-base);--control-tracking:var(--tracking-base);--control-icon:16px;--control-px:12px;--control-gap:8px;--group-gap:16px;--bar-h:40px;--bar-card-h:48px;--edge-pad:16px;--cell-px:12px;--check-size:16px;--tag-h:24px;--count-h:18px;--switch-w:32px;--switch-h:18px;--text-code:12px;--leading-code:18px;--title-cell-py:10px;--title-cell-gap:2px;--title-cell-tile:32px;--title-cell-icon:16px;--title-cell-tile-r:8px;--title-cell-tile-gap:12px}
55
+ [data-size="lg"]{--control-h:40px;--control-text:var(--text-base);--control-leading:var(--leading-base);--control-tracking:var(--tracking-base);--control-icon:16px;--control-px:16px;--control-gap:8px;--group-gap:24px;--bar-h:48px;--bar-card-h:56px;--edge-pad:16px;--cell-px:16px;--check-size:16px;--tag-h:24px;--count-h:18px;--switch-w:32px;--switch-h:18px;--text-code:12px;--leading-code:18px;--title-cell-py:14px;--title-cell-gap:4px;--title-cell-tile:40px;--title-cell-icon:20px;--title-cell-tile-r:10px;--title-cell-tile-gap:12px}