@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
@@ -8,9 +8,9 @@ import { Icon } from "../icons/Icon.jsx";
8
8
  first column's text equals the edge padding (16px; 12px in sm) whatever the table width or column widths (a separate column took a share
9
9
  of any spare width). */
10
10
  const CHECK_PAD = "calc(var(--edge-pad) * 2 + var(--check-size))";
11
- /* Config-code chip (`code: true` columns): Menlo at --text-code on the inset fill — the one place the code font appears in a table.
11
+ /* Config-code chip (`code: true` columns): Menlo at --text-code on the hover fill with a control border (stays visible on a hovered row) — the one place the code font appears in a table.
12
12
  Only text and numbers get the chip; an element in the cell (a Skeleton while loading, a link) renders as is. */
13
- const codeChip = { fontFamily: "var(--font-mono)", fontSize: "var(--text-code)", lineHeight: "var(--leading-code)", letterSpacing: 0, background: "var(--bg-inset)", border: "1px solid var(--border-subtle)", borderRadius: 4, padding: "1px 6px", color: "var(--text-primary)", whiteSpace: "nowrap" };
13
+ const codeChip = { fontFamily: "var(--font-mono)", fontSize: "var(--text-code)", lineHeight: "var(--leading-code)", letterSpacing: 0, background: "var(--bg-hover)", border: "1px solid var(--border-control)", borderRadius: 4, padding: "1px 6px", color: "var(--text-primary)", whiteSpace: "nowrap" };
14
14
  const checkSlot = { position: "absolute", left: "var(--edge-pad)", top: 0, bottom: 0, display: "flex", alignItems: "center" };
15
15
 
16
16
  /* Two-line title cell (`twoLine` column): icon tile · title over description · trailing ⋯. Every measure is a --title-cell-* tier token:
@@ -31,7 +31,11 @@ const SORT_BTN = 32;
31
31
  const isEmpty = (v) => v == null || v === "" || v === "–";
32
32
  /* Every word the table draws itself. Pass `labels` to translate; missing keys fall back to these. The sort tooltip names the NEXT step:
33
33
  unsorted → sortAsc, ascending → sortDesc, descending → sortClear. */
34
- const DEFAULT_LABELS = { sortAsc: "Sort ascending", sortDesc: "Sort descending", sortClear: "Clear sorting", more: "More actions", expand: "Expand", collapse: "Collapse" };
34
+ const DEFAULT_LABELS = { sortAsc: "Sort ascending", sortDesc: "Sort descending", sortClear: "Clear sorting", more: "More actions", expand: "Expand", collapse: "Collapse", pinned: "Selected", others: "Others", unavailable: "Unavailable", clear: "Clear" };
35
+ /* pinSelected="labels": a 28px group row above each group, on the header tint, 12px semibold --text-secondary. The header rule, the group rows' rules and
36
+ the rule closing the pinned group turn --border-control (#e5e5e5) — --border-subtle is the header tint itself, so it would not show. */
37
+ const GROUP_CELL = { height: 28, padding: "0 var(--edge-pad)", background: "var(--bg-table-header)", borderBottom: "1px solid var(--border-control)", fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", fontWeight: "var(--weight-semibold)", color: "var(--text-secondary)", whiteSpace: "nowrap" };
38
+ const CLEAR_BTN = { marginLeft: "auto", border: "none", background: "none", padding: 0, cursor: "pointer", fontFamily: "inherit", fontSize: "inherit", lineHeight: "inherit", fontWeight: "var(--weight-regular)", color: "var(--link)" };
35
39
  const sortLabel = (L, sorted) => sorted === "asc" ? L.sortDesc : sorted === "desc" ? L.sortClear : L.sortAsc;
36
40
  /* Tree (`tree` on the table, `children` on a row): the twoLine column indents 24px per level and reserves a 20px arrow slot + 4px,
37
41
  so a child's arrow sits exactly under its parent's tile. */
@@ -39,7 +43,7 @@ const TREE_INDENT = 24, TREE_TOGGLE = 20;
39
43
  /* Builds the visible rows. index = depth-first position over EVERY row (expanded or not) — for a flat table that is the position in
40
44
  `rows`. It is assigned before sorting, so selection, onRowMore and onRowOpen indices never move when the user sorts or expands.
41
45
  Sorting reorders siblings only; children stay under their parent. */
42
- function buildView(rows, { tree, rowKey, expanded, col, dir, manualSort }) {
46
+ function buildView(rows, { tree, rowKey, expanded, col, dir, manualSort, pin, unavail }) {
43
47
  const idx = new Map(); let n = 0;
44
48
  const number = (list) => list.forEach((r) => { idx.set(r, n++); if (tree && Array.isArray(r.children)) number(r.children); });
45
49
  number(rows);
@@ -48,8 +52,16 @@ function buildView(rows, { tree, rowKey, expanded, col, dir, manualSort }) {
48
52
  if (ea || eb) return ea && eb ? idx.get(a) - idx.get(b) : ea ? 1 : -1; // empty values last in both directions
49
53
  return (col.compare ? col.compare(va, vb, a, b) : compareValues(va, vb)) * dir || idx.get(a) - idx.get(b);
50
54
  } : null;
55
+ /* pinSelected: checked rows move to the top of their sibling list (sorted among themselves), the rest follow in their own order.
56
+ unavail (pinSelected="labels" + isRowDisabled): locked unchecked rows go last — Selected, Others, Unavailable, each sorted on its own. */
57
+ const order = (list) => {
58
+ const l = cmp ? list.slice().sort(cmp) : list;
59
+ if (!pin) return l;
60
+ const rest = l.filter((r) => !pin.has(idx.get(r)));
61
+ return [...l.filter((r) => pin.has(idx.get(r))), ...(unavail ? [...rest.filter((r) => !unavail.has(idx.get(r))), ...rest.filter((r) => unavail.has(idx.get(r)))] : rest)];
62
+ };
51
63
  const out = [];
52
- const walk = (list, level) => (cmp ? list.slice().sort(cmp) : list).forEach((r) => {
64
+ const walk = (list, level) => order(list).forEach((r) => {
53
65
  const ri = idx.get(r), key = r[rowKey] != null ? r[rowKey] : String(ri);
54
66
  const kids = !!tree && ((Array.isArray(r.children) && r.children.length > 0) || !!r.hasChildren);
55
67
  const open = kids && expanded.has(key);
@@ -57,7 +69,7 @@ function buildView(rows, { tree, rowKey, expanded, col, dir, manualSort }) {
57
69
  if (open && Array.isArray(r.children)) walk(r.children, level + 1);
58
70
  });
59
71
  walk(rows, 0);
60
- return { list: out, total: n };
72
+ return { list: out, total: n, flat: [...idx.keys()] };
61
73
  }
62
74
  const nextSort = (cur, key) => !cur || cur.key !== key ? { key, direction: "asc" } : cur.direction === "asc" ? { key, direction: "desc" } : null;
63
75
  const num = (v) => typeof v === "number" ? v : typeof v === "string" && /^\s*-?[\d,]+(\.\d+)?\s*$/.test(v) ? parseFloat(v.replace(/,/g, "")) : NaN;
@@ -123,10 +135,19 @@ function TitleCell({ c, item, onRowMore, onRowOpen, labels, tree, onToggleRow })
123
135
  * Every rule in the table is one weight (--border-subtle); the header is set apart by its --bg-table-header tint, never by a heavier line.
124
136
  * bordered — the table draws its own rounded card frame (border-card, radius-lg) and drops the edge rules that would double it.
125
137
  * framed — the table sits flush inside a container that already has a border (Card pad={false}); drops only the header's top rule.
138
+ * pinSelected — with selectable: checked rows move to the top (of their level in a tree), in the current sort order; unchecking returns a row to its place.
139
+ * Pinned rows sit on --bg-inset; while any are pinned, the header rule and the rule closing the group are --border-control. true | "divider": just that. "labels": a 28px group row above each group
140
+ * ("Selected · n" with Clear → onToggle("all", false), "Others · n") on the header tint. labels.pinned / others / clear translate them.
141
+ * With isRowDisabled, locked unchecked rows form a third group after Others — "Unavailable · n" (labels.unavailable). Group order is fixed under sorting
142
+ * (Selected · Others · Unavailable; the sort applies inside each); a row changes group as its lock changes. Empty groups show no group row.
143
+ * isRowDisabled(row, index) — with selectable: the app's lock for a row, asked on every render. A locked row's checkbox is disabled (50%, not-allowed, no hover, no onToggle)
144
+ * and the row takes no hover wash; its cells are left to the app. The lock never changes selected — a locked, checked row stays checked.
145
+ * Select-all checks every unlocked row, Clear unchecks them; locked rows keep their state — onToggle("all", v, next) passes the result.
146
+ * With pinSelected, a locked unchecked row stays with the others. disabledReason(row, index) → the checkbox tooltip.
126
147
  * resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
127
- * Widths live in local state unless you pass columnWidths/onColumnResize to control them. Min width = column.minWidth ?? 48.
148
+ * Widths live in local state unless you pass columnWidths (then update it from onColumnResize). onColumnResize(key, px) fires on every drag either way. Min width = column.minWidth ?? 48.
128
149
  */
129
- export function Table({ columns = [], rows = [], size, dense, selectable, selected = [], onToggle, headerBorder = true, bordered, framed, resizable, columnWidths, onColumnResize, onRowMore, onRowOpen, sort, defaultSort = null, onSortChange, manualSort, tree, rowKey = "id", expandedKeys, defaultExpandedKeys, onExpandChange, labels, empty, style }) {
150
+ export function Table({ columns = [], rows = [], size, dense, selectable, selected = [], onToggle, pinSelected, isRowDisabled, disabledReason, headerBorder = true, bordered, framed, resizable, columnWidths, onColumnResize, onRowMore, onRowOpen, sort, defaultSort = null, onSortChange, manualSort, tree, rowKey = "id", expandedKeys, defaultExpandedKeys, onExpandChange, labels, empty, style }) {
130
151
  const L = { ...DEFAULT_LABELS, ...labels };
131
152
  const [localExp, setLocalExp] = React.useState(defaultExpandedKeys || []);
132
153
  const expKeys = expandedKeys !== undefined ? expandedKeys : localExp;
@@ -134,10 +155,37 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
134
155
  const sortState = sort !== undefined ? sort : localSort;
135
156
  const [hotTh, setHotTh] = React.useState(null); // header under the pointer or holding focus
136
157
  const cycleSort = (key) => { const n = nextSort(sortState, key); if (sort === undefined) setLocalSort(n); if (onSortChange) onSortChange(n); };
137
- const { list: view, total } = React.useMemo(() => buildView(rows, {
138
- tree, rowKey, expanded: new Set(expKeys), manualSort,
158
+ /* Locked unchecked rows, asked of isRowDisabled on every render (never cached) — only "labels" groups them; the key re-runs the view when the set changes. */
159
+ const allRows = React.useMemo(() => { const a = []; const f = (l) => l.forEach((r) => { a.push(r); if (tree && Array.isArray(r.children)) f(r.children); }); f(rows); return a; }, [rows, tree]);
160
+ const unavail = selectable && pinSelected === "labels" && isRowDisabled ? new Set(allRows.map((r, i) => (!selected.includes(i) && isRowDisabled(r, i) ? i : -1)).filter((i) => i >= 0)) : null;
161
+ const unKey = unavail ? [...unavail].join(",") : "";
162
+ const { list: view, total, flat } = React.useMemo(() => buildView(rows, {
163
+ tree, rowKey, expanded: new Set(expKeys), manualSort, pin: selectable && pinSelected ? new Set(selected) : null, unavail: unavail && unavail.size ? unavail : null,
139
164
  col: sortState && columns.find((x) => x.key === sortState.key), dir: sortState && sortState.direction === "desc" ? -1 : 1,
140
- }), [rows, columns, sortState, manualSort, tree, rowKey, expKeys]);
165
+ }), [rows, columns, sortState, manualSort, tree, rowKey, expKeys, selectable, pinSelected, selected, unKey]);
166
+ /* pinSelected: true | "divider" → pinned rows on --bg-inset + a 1px --border-control rule under the group; "labels" → the same fill + a group row above each group.
167
+ While rows are pinned the header rule is --border-control too (--border-subtle is the header tint itself, so it would not show). */
168
+ const pinInfo = (() => {
169
+ if (!selectable || !pinSelected) return null;
170
+ const sel = new Set(selected), top = view.filter((x) => x.level === 0 && sel.has(x.ri)).length;
171
+ const style = pinSelected === "labels" ? "labels" : "divider", un = unavail ? unavail.size : 0;
172
+ if (style === "divider") return { style, sel, top, strong: top > 0, n: selected.length, others: total - selected.length, rest: top ? view.findIndex((x) => x.level === 0 && !sel.has(x.ri)) : -1, unStart: -1, un: 0 };
173
+ /* labels: up to three groups — Selected · n, Others · n, Unavailable · n. A group row shows only when its group has top-level rows; the bands show while any group does. */
174
+ const unStart = un ? view.findIndex((x) => x.level === 0 && unavail.has(x.ri)) : -1, grouped = top > 0 || unStart >= 0;
175
+ const rest = grouped ? view.findIndex((x) => x.level === 0 && !sel.has(x.ri) && !(unavail && unavail.has(x.ri))) : -1;
176
+ return { style, sel, top, grouped, strong: grouped, n: selected.length, others: total - selected.length - un, rest, unStart, un };
177
+ })();
178
+ /* isRowDisabled(row, index) — the app's lock, asked again on every render (it may depend on selected). It blocks checking only: a locked row keeps
179
+ whatever selected says. Select-all counts and changes only unlocked rows; onToggle("all", v, next) hands the app the resulting selection. */
180
+ const isDis = (r, ri) => !!(selectable && isRowDisabled && isRowDisabled(r, ri));
181
+ const selSet = new Set(selected);
182
+ const enabledIdx = selectable ? flat.map((r, i) => (isDis(r, i) ? -1 : i)).filter((i) => i >= 0) : [];
183
+ const allOn = enabledIdx.length > 0 && enabledIdx.every((i) => selSet.has(i));
184
+ const toggleAll = (v) => { if (!onToggle) return; const next = v ? [...new Set([...selected, ...enabledIdx])] : selected.filter((i) => flat[i] && isDis(flat[i], i)); onToggle("all", v, next); };
185
+ const groupRow = (text, clear) => <tr><td colSpan={columns.length || 1} style={GROUP_CELL}><span style={{ display: "flex", alignItems: "center", gap: 8 }}>{text}{clear ? <button type="button" data-link="" onClick={() => toggleAll(false)} style={CLEAR_BTN}>{L.clear}</button> : null}</span></td></tr>;
186
+ const groupAt = (pos) => !pinInfo || pinInfo.style !== "labels" || !pinInfo.grouped ? null : pos === 0 && pinInfo.top ? groupRow(`${L.pinned} · ${pinInfo.n}`, true) : pos === pinInfo.rest ? groupRow(`${L.others} · ${pinInfo.others}`) : pos === pinInfo.unStart ? groupRow(`${L.unavailable} · ${pinInfo.un}`) : null;
187
+ /* The row above a group row (or closing the pinned band) takes --border-control: --border-subtle is the header tint the group row sits on. */
188
+ const ruleBelow = (pos) => !!pinInfo && ((pinInfo.rest > 0 && pos === pinInfo.rest - 1) || (pinInfo.unStart > 0 && pos === pinInfo.unStart - 1));
141
189
  const toggleRow = (item) => {
142
190
  const next = item.open ? expKeys.filter((k) => k !== item.key) : [...expKeys, item.key];
143
191
  if (expandedKeys === undefined) setLocalExp(next);
@@ -151,17 +199,23 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
151
199
  const [hot, setHot] = React.useState(null); // key whose handle is hovered
152
200
  const thRefs = React.useRef({});
153
201
  const widths = columnWidths || localW;
154
- const widthOf = (c) => widths[c.key] ?? c.width;
202
+ /* Resizable: the last column (no divider) takes no width and absorbs the slack. Otherwise, when every column has a width, a fixed-layout table
203
+ scales them all to fill, and the dragged column would not follow the cursor. */
204
+ const widthOf = (c) => (resizable && c === columns[columns.length - 1] ? undefined : widths[c.key] ?? c.width);
155
205
 
156
206
  const startDrag = (e, c) => {
157
207
  e.preventDefault();
158
208
  const th = thRefs.current[c.key];
159
- const startX = e.clientX, startW = th ? th.getBoundingClientRect().width : (parseFloat(widthOf(c)) || 120);
209
+ /* A column width is the content box (the cell padding sits outside it), so start from the rendered width minus the padding — else the column jumps by its padding on the first move. */
210
+ const cs = th && getComputedStyle(th);
211
+ const startX = e.clientX, startW = th ? th.getBoundingClientRect().width - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) : (parseFloat(widthOf(c)) || 120);
160
212
  const min = c.minWidth ?? 48;
161
213
  setDrag(c.key);
162
214
  const move = (ev) => {
163
215
  const w = Math.max(min, Math.round(startW + ev.clientX - startX));
164
- if (onColumnResize) onColumnResize(c.key, w); else setLocalW((s) => ({ ...s, [c.key]: w }));
216
+ /* Uncontrolled unless columnWidths is passed; onColumnResize always hears the new width (listening alone must not freeze the column). */
217
+ if (!columnWidths) setLocalW((s) => ({ ...s, [c.key]: w }));
218
+ if (onColumnResize) onColumnResize(c.key, w);
165
219
  };
166
220
  const up = () => { setDrag(null); window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); document.body.style.cursor = ""; document.body.style.userSelect = ""; };
167
221
  document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none";
@@ -178,7 +232,7 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
178
232
  const table = (
179
233
  <table data-size={tier} style={{ width: "100%", borderCollapse: "collapse", tableLayout: resizable ? "fixed" : "auto", ...(bordered ? null : style) }}>
180
234
  <thead>
181
- <tr style={{ background: "var(--bg-table-header)", borderTop: headerBorder && !flushTop ? "1px solid var(--border-subtle)" : "none", borderBottom: headerBorder ? "1px solid var(--border-subtle)" : "none", height: "var(--bar-h)" }}>
235
+ <tr style={{ background: "var(--bg-table-header)", borderTop: headerBorder && !flushTop ? "1px solid var(--border-subtle)" : "none", borderBottom: headerBorder ? (pinInfo && pinInfo.strong ? "1px solid var(--border-control)" : "1px solid var(--border-subtle)") : "none", height: "var(--bar-h)" }}>
182
236
  {columns.map((c, i) => {
183
237
  const last = i === columns.length - 1;
184
238
  const active = drag === c.key, lit = active || hot === c.key;
@@ -188,7 +242,7 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
188
242
  <th key={c.key} ref={(el) => { thRefs.current[c.key] = el; }} aria-sort={c.sortable ? (sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none") : undefined}
189
243
  onMouseEnter={c.sortable ? () => setHotTh(c.key) : undefined} onMouseLeave={c.sortable ? () => setHotTh(null) : undefined} onFocus={c.sortable ? () => setHotTh(c.key) : undefined} onBlur={c.sortable ? () => setHotTh(null) : undefined}
190
244
  style={{ position: "relative", textAlign: c.align || "left", ...cellPad(i), width: widthOf(c), fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: sorted ? "var(--text-heading)" : "var(--text-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
191
- {selectable && i === 0 ? <span style={checkSlot}><Checkbox checked={total > 0 && selected.length === total} indeterminate={selected.length > 0 && selected.length < total} onChange={(v) => onToggle && onToggle("all", v)} /></span> : null}
245
+ {selectable && i === 0 ? <span style={checkSlot}><Checkbox checked={allOn} indeterminate={!allOn && selected.length > 0} disabled={total > 0 && !enabledIdx.length} onChange={toggleAll} /></span> : null}
192
246
  {c.sortable ? (
193
247
  <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
194
248
  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0, ...(c.align === "right" ? { marginLeft: "auto" } : c.align === "center" ? { margin: "0 auto" } : null), ...(typeof widthOf(c) === "number" ? { maxWidth: widthOf(c) - SORT_BTN } : null) }}>{c.label}</span>
@@ -207,12 +261,12 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
207
261
  </tr>
208
262
  </thead>
209
263
  <tbody>
210
- {view.map((item, pos) => { const { r, ri } = item; return (
211
- <tr key={item.key} data-row="" aria-level={tree ? item.level + 1 : undefined} aria-expanded={item.kids ? item.open : undefined} style={{ height: "var(--bar-h)", background: "var(--row-wash, transparent)", transition: "var(--t-hover)", borderBottom: bordered && pos === view.length - 1 ? "none" : "1px solid var(--border-subtle)" }}>
264
+ {view.map((item, pos) => { const { r, ri } = item; const dis = isDis(r, ri); return (<React.Fragment key={item.key}>{groupAt(pos)}
265
+ <tr data-row="" aria-level={tree ? item.level + 1 : undefined} aria-expanded={item.kids ? item.open : undefined} style={{ ...(dis ? { "--row-wash": "initial" } : null), height: "var(--bar-h)", background: pinInfo && pinInfo.sel.has(ri) ? "var(--row-wash, var(--bg-inset))" : "var(--row-wash, transparent)", transition: "var(--t-hover)", borderBottom: ruleBelow(pos) ? "1px solid var(--border-control)" : bordered && pos === view.length - 1 ? "none" : "1px solid var(--border-subtle)" }}>
212
266
  {columns.map((c, i) => (
213
- <td 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) }}>{selectable && i === 0 ? <span style={checkSlot}><Checkbox checked={selected.includes(ri)} onChange={(v) => onToggle && onToggle(ri, v)} /></span> : null}{c.twoLine ? <TitleCell c={c} item={item} onRowMore={onRowMore} onRowOpen={onRowOpen} labels={L} tree={tree && c === columns.find((x) => x.twoLine)} onToggleRow={toggleRow} /> : (c.code && (typeof r[c.key] === "string" || typeof r[c.key] === "number") && r[c.key] !== "") ? <code style={codeChip}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</code> : c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>
267
+ <td 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) }}>{selectable && i === 0 ? <span style={checkSlot} title={dis && disabledReason ? disabledReason(r, ri) : undefined}><Checkbox checked={selected.includes(ri)} disabled={dis} onChange={(v) => onToggle && onToggle(ri, v)} /></span> : null}{c.twoLine ? <TitleCell c={c} item={item} onRowMore={onRowMore} onRowOpen={onRowOpen} labels={L} tree={tree && c === columns.find((x) => x.twoLine)} onToggleRow={toggleRow} /> : (c.code && (typeof r[c.key] === "string" || typeof r[c.key] === "number") && r[c.key] !== "") ? <code style={codeChip}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</code> : c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>
214
268
  ))}
215
- </tr>
269
+ </tr></React.Fragment>
216
270
  ); })}
217
271
  {rows.length === 0 && empty ? <tr><td colSpan={colCount} style={{ padding: 0 }}>{empty}</td></tr> : null}
218
272
  </tbody>
@@ -22,12 +22,19 @@ import Icon from '../icons/Icon.vue'
22
22
  // bordered — the table draws its own rounded card frame (border-card, radius-lg) and drops the edge rules that would double it.
23
23
  // framed — the table sits flush inside a container that already has a border (Card :pad="false"); drops only the header's top rule.
24
24
  // resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
25
- // Widths live in local state unless you pass columnWidths + listen to @column-resize(key, px). Min width = column.minWidth ?? 48.
25
+ // Widths live in local state unless you pass columnWidths (then update it from @column-resize). @column-resize(key, px) fires on every drag either way. Min width = column.minWidth ?? 48.
26
+ // pinSelected — with selectable: checked rows move to the top (of their level in a tree), in the current sort order; unchecking returns a row to its place.
27
+ // Pinned rows sit on --bg-inset; while any are pinned, the header rule and the rule closing the group are --border-control. true | 'divider': just that. 'labels': a 28px group row above each group
28
+ // ('Selected · n' with Clear → @toggle('all', false), 'Others · n') on the header tint. labels.pinned / others / clear translate them.
29
+ // With isRowDisabled, locked unchecked rows form a third group after Others — 'Unavailable · n' (labels.unavailable). Order is fixed under
30
+ // sorting (Selected · Others · Unavailable; the sort applies inside each); a row changes group as its lock changes; empty groups show no group row.
31
+ // isRowDisabled(row, index) — with selectable: the app's lock for a row, called on every render. Locked: checkbox disabled (50%, not-allowed, no hover, no toggle), no row hover wash,
32
+ // cells left to the app. Never changes selected. Select-all / Clear skip locked rows — @toggle('all', v, next) passes the result. disabledReason(row, index) → checkbox tooltip.
26
33
  // #empty slot renders inside the table when rows is empty.
27
- const props = defineProps({ columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, size: String, dense: Boolean, selectable: Boolean, selected: { type: Array, default: () => [] }, headerBorder: { type: Boolean, default: true }, bordered: Boolean, framed: Boolean, resizable: Boolean, columnWidths: Object, sort: { type: Object, default: undefined }, defaultSort: { type: Object, default: null }, manualSort: Boolean, tree: Boolean, rowKey: { type: String, default: 'id' }, expandedKeys: { type: Array, default: undefined }, defaultExpandedKeys: { type: Array, default: () => [] }, labels: Object, style: Object })
34
+ const props = defineProps({ columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, size: String, dense: Boolean, selectable: Boolean, pinSelected: [Boolean, String], isRowDisabled: Function, disabledReason: Function, selected: { type: Array, default: () => [] }, headerBorder: { type: Boolean, default: true }, bordered: Boolean, framed: Boolean, resizable: Boolean, columnWidths: Object, sort: { type: Object, default: undefined }, defaultSort: { type: Object, default: null }, manualSort: Boolean, tree: Boolean, rowKey: { type: String, default: 'id' }, expandedKeys: { type: Array, default: undefined }, defaultExpandedKeys: { type: Array, default: () => [] }, labels: Object, style: Object })
28
35
  const emit = defineEmits(['toggle', 'columnResize', 'rowMore', 'rowOpen', 'sortChange', 'update:sort', 'expandChange', 'update:expandedKeys'])
29
36
  // Every word the table draws itself; `labels` translates them. The sort tooltip names the NEXT step: unsorted → sortAsc, ascending → sortDesc, descending → sortClear.
30
- const DEFAULT_LABELS = { sortAsc: 'Sort ascending', sortDesc: 'Sort descending', sortClear: 'Clear sorting', more: 'More actions', expand: 'Expand', collapse: 'Collapse' }
37
+ const DEFAULT_LABELS = { sortAsc: 'Sort ascending', sortDesc: 'Sort descending', sortClear: 'Clear sorting', more: 'More actions', expand: 'Expand', collapse: 'Collapse', pinned: 'Selected', others: 'Others', unavailable: 'Unavailable', clear: 'Clear' }
31
38
  const L = computed(() => ({ ...DEFAULT_LABELS, ...(props.labels || {}) }))
32
39
  const sortLabel = (sorted) => sorted === 'asc' ? L.value.sortDesc : sorted === 'desc' ? L.value.sortClear : L.value.sortAsc
33
40
  // the tile and title open the row only when a @row-open listener is attached
@@ -68,8 +75,15 @@ const built = computed(() => {
68
75
  const s = sortState.value, col = s && props.columns.find(x => x.key === s.key), dir = s && s.direction === 'desc' ? -1 : 1
69
76
  const isEmpty = (x) => x == null || x === '' || x === '–'
70
77
  const cmp = col && !props.manualSort ? (a, b) => { const va = a[col.key], vb = b[col.key], ea = isEmpty(va), eb = isEmpty(vb); if (ea || eb) return ea && eb ? idx.get(a) - idx.get(b) : ea ? 1 : -1; return (col.compare ? col.compare(va, vb, a, b) : compareValues(va, vb)) * dir || idx.get(a) - idx.get(b) } : null
78
+ // pinSelected: checked rows move to the top of their sibling list (sorted among themselves), the rest follow in their own order
79
+ const pin = props.selectable && props.pinSelected ? new Set(props.selected) : null
80
+ // pinSelected='labels' + isRowDisabled: locked unchecked rows go last — Selected, Others, Unavailable, each sorted on its own. The predicate runs inside
81
+ // this computed, so Vue re-runs it whenever a prop or any reactive state the predicate reads changes.
82
+ const un = pin && props.pinSelected === 'labels' && props.isRowDisabled ? new Set([...idx.keys()].map((r, i) => !pin.has(i) && props.isRowDisabled(r, i) ? i : -1).filter(i => i >= 0)) : null
83
+ const unavail = un && un.size ? un : null
84
+ const order = (list) => { const l = cmp ? list.slice().sort(cmp) : list; if (!pin) return l; const rest = l.filter(r => !pin.has(idx.get(r))); return [...l.filter(r => pin.has(idx.get(r))), ...(unavail ? [...rest.filter(r => !unavail.has(idx.get(r))), ...rest.filter(r => unavail.has(idx.get(r)))] : rest)] }
71
85
  const exp = new Set(expKeys.value), out = []
72
- const walk = (list, level) => (cmp ? list.slice().sort(cmp) : list).forEach(r => {
86
+ const walk = (list, level) => order(list).forEach(r => {
73
87
  const ri = idx.get(r), key = r[props.rowKey] != null ? r[props.rowKey] : String(ri)
74
88
  const kids = props.tree && ((Array.isArray(r.children) && r.children.length > 0) || !!r.hasChildren)
75
89
  const open = kids && exp.has(key)
@@ -77,10 +91,30 @@ const built = computed(() => {
77
91
  if (open && Array.isArray(r.children)) walk(r.children, level + 1)
78
92
  })
79
93
  walk(props.rows, 0)
80
- return { list: out, total: n }
94
+ return { list: out, total: n, flat: [...idx.keys()], unavail }
81
95
  })
82
96
  const view = computed(() => built.value.list)
83
97
  const total = computed(() => built.value.total)
98
+ // isRowDisabled(row, index) — the app's lock, called on every render (it may depend on selected), never cached. It blocks checking only: a locked row keeps
99
+ // whatever selected says. Select-all counts and changes only unlocked rows; @toggle('all', v, next) hands the app the resulting selection.
100
+ const isDis = (r, ri) => !!(props.selectable && props.isRowDisabled && props.isRowDisabled(r, ri))
101
+ const enabledIdx = () => props.selectable ? built.value.flat.map((r, i) => isDis(r, i) ? -1 : i).filter(i => i >= 0) : []
102
+ function head() { const en = enabledIdx(), sel = new Set(props.selected), all = en.length > 0 && en.every(i => sel.has(i)); return { all, ind: !all && props.selected.length > 0, none: total.value > 0 && !en.length } }
103
+ function toggleAll(v) { const flat = built.value.flat; const next = v ? [...new Set([...props.selected, ...enabledIdx()])] : props.selected.filter(i => flat[i] && isDis(flat[i], i)); emit('toggle', 'all', v, next) }
104
+ // pinSelected: true | 'divider' → pinned rows on --bg-inset + a 1px --border-control rule under the group; 'labels' → the same fill + a group row above each group.
105
+ // While rows are pinned the header rule is --border-control too (--border-subtle is the header tint itself, so it would not show).
106
+ const pinInfo = computed(() => {
107
+ if (!props.selectable || !props.pinSelected) return null
108
+ const sel = new Set(props.selected), v = view.value, top = v.filter(x => x.level === 0 && sel.has(x.ri)).length
109
+ const style = props.pinSelected === 'labels' ? 'labels' : 'divider', ua = built.value.unavail, un = ua ? ua.size : 0
110
+ if (style === 'divider') return { style, sel, top, strong: top > 0, n: props.selected.length, others: total.value - props.selected.length, rest: top ? v.findIndex(x => x.level === 0 && !sel.has(x.ri)) : -1, unStart: -1, un: 0 }
111
+ // labels: up to three groups — Selected · n, Others · n, Unavailable · n; a group row shows only when its group has top-level rows
112
+ const unStart = un ? v.findIndex(x => x.level === 0 && ua.has(x.ri)) : -1, grouped = top > 0 || unStart >= 0
113
+ const rest = grouped ? v.findIndex(x => x.level === 0 && !sel.has(x.ri) && !(ua && ua.has(x.ri))) : -1
114
+ return { style, sel, top, grouped, strong: grouped, n: props.selected.length, others: total.value - props.selected.length - un, rest, unStart, un }
115
+ })
116
+ const GROUP_CELL = { height: '28px', padding: '0 var(--edge-pad)', background: 'var(--bg-table-header)', borderBottom: '1px solid var(--border-control)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-secondary)', whiteSpace: 'nowrap' }
117
+ const CLEAR_BTN = { marginLeft: 'auto', border: 'none', background: 'none', padding: 0, cursor: 'pointer', fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit', fontWeight: 'var(--weight-regular)', color: 'var(--link)' }
84
118
  const sortedDir = (c) => sortState.value && sortState.value.key === c.key ? sortState.value.direction : null
85
119
  // Two-line title cell — every measure is a --title-cell-* tier token (md: 32px tile r8 · 12px to text · 14/12px · 2px gap · 10px padding → 58px).
86
120
  const T_TILE = { width: 'var(--title-cell-tile)', height: 'var(--title-cell-tile)', borderRadius: 'var(--title-cell-tile-r)' }
@@ -100,20 +134,23 @@ const flushTop = computed(() => props.bordered || props.framed)
100
134
  const colCount = computed(() => props.columns.length || 1)
101
135
  // Selection checkbox sits in the first column's leading padding (no column of its own): the gap to the text equals the edge padding.
102
136
  const CHECK_PAD = 'calc(var(--edge-pad) * 2 + var(--check-size))'
103
- // Config-code chip (`code: true` columns): Menlo at --text-code on the inset fill.
104
- const codeChip = { fontFamily: 'var(--font-mono)', fontSize: 'var(--text-code)', lineHeight: 'var(--leading-code)', letterSpacing: 0, background: 'var(--bg-inset)', border: '1px solid var(--border-subtle)', borderRadius: '4px', padding: '1px 6px', color: 'var(--text-primary)', whiteSpace: 'nowrap' }
137
+ // Config-code chip (`code: true` columns): Menlo at --text-code on the hover fill with a control border (stays visible on a hovered row).
138
+ const codeChip = { fontFamily: 'var(--font-mono)', fontSize: 'var(--text-code)', lineHeight: 'var(--leading-code)', letterSpacing: 0, background: 'var(--bg-hover)', border: '1px solid var(--border-control)', borderRadius: '4px', padding: '1px 6px', color: 'var(--text-primary)', whiteSpace: 'nowrap' }
105
139
  const checkSlot = { position: 'absolute', left: 'var(--edge-pad)', top: 0, bottom: 0, display: 'flex', alignItems: 'center' }
106
140
  const frame = computed(() => props.bordered ? { background: 'var(--bg-surface)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', ...props.style } : { display: 'contents' })
107
141
  const localW = reactive({})
108
142
  const drag = ref(null), hot = ref(null)
109
143
  const thEls = {}
110
- const widthOf = (c) => px((props.columnWidths || localW)[c.key] ?? c.width)
144
+ // Resizable: the last column (no divider) takes no width and absorbs the slack — otherwise a fixed-layout table scales every width to fill and the dragged column drifts from the cursor.
145
+ const widthOf = (c) => props.resizable && c === props.columns[props.columns.length - 1] ? undefined : px((props.columnWidths || localW)[c.key] ?? c.width)
111
146
  function startDrag(e, c) {
112
147
  e.preventDefault()
113
148
  const th = thEls[c.key]
114
- const startX = e.clientX, startW = th ? th.getBoundingClientRect().width : (parseFloat(widthOf(c)) || 120), min = c.minWidth ?? 48
149
+ // A column width is the content box (cell padding sits outside it): start from the rendered width minus the padding, or the column jumps on the first move.
150
+ const cs = th && getComputedStyle(th)
151
+ const startX = e.clientX, startW = th ? th.getBoundingClientRect().width - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) : (parseFloat(widthOf(c)) || 120), min = c.minWidth ?? 48
115
152
  drag.value = c.key
116
- const move = (ev) => { const w = Math.max(min, Math.round(startW + ev.clientX - startX)); if (props.columnWidths) emit('columnResize', c.key, w); else localW[c.key] = w }
153
+ const move = (ev) => { const w = Math.max(min, Math.round(startW + ev.clientX - startX)); if (!props.columnWidths) localW[c.key] = w; emit('columnResize', c.key, w) }
117
154
  const up = () => { drag.value = null; window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); document.body.style.cursor = ''; document.body.style.userSelect = '' }
118
155
  document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'
119
156
  window.addEventListener('mousemove', move); window.addEventListener('mouseup', up)
@@ -126,11 +163,11 @@ function cellPad(i) { return { ...(i === 0 && props.selectable ? { position: 're
126
163
  <div :style="frame" :data-size="bordered ? tier : undefined">
127
164
  <table :data-size="tier" :style="{ width: '100%', borderCollapse: 'collapse', tableLayout: resizable ? 'fixed' : 'auto', ...(bordered ? null : style) }">
128
165
  <thead>
129
- <tr :style="{ background: 'var(--bg-table-header)', borderTop: (headerBorder && !flushTop) ? '1px solid var(--border-subtle)' : 'none', borderBottom: headerBorder ? '1px solid var(--border-subtle)' : 'none', height: 'var(--bar-h)' }">
166
+ <tr :style="{ background: 'var(--bg-table-header)', borderTop: (headerBorder && !flushTop) ? '1px solid var(--border-subtle)' : 'none', borderBottom: headerBorder ? (pinInfo && pinInfo.strong ? '1px solid var(--border-control)' : '1px solid var(--border-subtle)') : 'none', height: 'var(--bar-h)' }">
130
167
  <th v-for="(c, i) in columns" :key="c.key" :ref="el => { thEls[c.key] = el }" :aria-sort="c.sortable ? (sortedDir(c) === 'asc' ? 'ascending' : sortedDir(c) === 'desc' ? 'descending' : 'none') : undefined"
131
168
  @mouseenter="c.sortable && (hotTh = c.key)" @mouseleave="c.sortable && (hotTh = null)" @focusin="c.sortable && (hotTh = c.key)" @focusout="c.sortable && (hotTh = null)"
132
169
  :style="{ position: 'relative', textAlign: c.align || 'left', ...cellPad(i), width: widthOf(c), fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-semibold)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: sortedDir(c) ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }">
133
- <span v-if="selectable && i === 0" :style="checkSlot"><Checkbox :checked="total > 0 && selected.length === total" :indeterminate="selected.length > 0 && selected.length < total" @change="v => emit('toggle', 'all', v)" /></span>
170
+ <span v-if="selectable && i === 0" :style="checkSlot"><Checkbox v-for="h in [head()]" :key="0" :checked="h.all" :indeterminate="h.ind" :disabled="h.none" @change="toggleAll" /></span>
134
171
  <span v-if="c.sortable" :style="{ display: 'flex', alignItems: 'center', gap: '4px' }"><span :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0, ...(c.align === 'right' ? { marginLeft: 'auto' } : c.align === 'center' ? { margin: '0 auto' } : null), ...(typeof ((columnWidths || localW)[c.key] ?? c.width) === 'number' ? { maxWidth: (((columnWidths || localW)[c.key] ?? c.width) - 32) + 'px' } : null) }">{{ c.label }}</span><IconButton :icon="sortedDir(c) === 'desc' ? 'arrow-down' : 'arrow-up'" size="sm" muted :label="sortLabel(sortedDir(c))" :title="sortLabel(sortedDir(c))" :style="{ opacity: (sortedDir(c) || hotTh === c.key) ? 1 : 0, ...(c.align === 'right' ? null : { marginLeft: 'auto' }) }" @click="cycleSort(c.key)" /></span>
135
172
  <template v-else>{{ c.label }}</template>
136
173
  <span v-if="resizable && i < columns.length - 1" aria-hidden="true" @mousedown="e => startDrag(e, c)" @mouseenter="hot = c.key" @mouseleave="hot = null"
@@ -141,11 +178,16 @@ function cellPad(i) { return { ...(i === 0 && props.selectable ? { position: 're
141
178
  </tr>
142
179
  </thead>
143
180
  <tbody>
144
- <tr v-for="(item, pos) in view" :key="item.key" data-row="" :aria-level="tree ? item.level + 1 : undefined" :aria-expanded="item.kids ? item.open : undefined" :style="{ height: 'var(--bar-h)', background: 'var(--row-wash, transparent)', transition: 'var(--t-hover)', borderBottom: bordered && pos === view.length - 1 ? 'none' : '1px solid var(--border-subtle)' }">
181
+ <template v-for="(item, pos) in view" :key="item.key">
182
+ <tr v-if="pinInfo && pinInfo.style === 'labels' && pinInfo.grouped && pinInfo.top && pos === 0"><td :colspan="columns.length || 1" :style="GROUP_CELL"><span style="display:flex;align-items:center;gap:8px">{{ L.pinned }} · {{ pinInfo.n }}<button type="button" data-link="" @click="toggleAll(false)" :style="CLEAR_BTN">{{ L.clear }}</button></span></td></tr>
183
+ <tr v-else-if="pinInfo && pinInfo.style === 'labels' && pinInfo.grouped && pos === pinInfo.rest"><td :colspan="columns.length || 1" :style="GROUP_CELL">{{ L.others }} · {{ pinInfo.others }}</td></tr>
184
+ <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
+ <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)' }">
145
186
  <template v-for="{ r, ri } in [item]" :key="'r'">
146
- <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"><Checkbox :checked="selected.includes(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>
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>
147
188
  </template>
148
189
  </tr>
190
+ </template>
149
191
  <tr v-if="rows.length === 0 && $slots.empty"><td :colspan="colCount" style="padding:0"><slot name="empty" /></td></tr>
150
192
  </tbody>
151
193
  </table>
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- /** Small removable filter tag (full radius; sm 20 · md 24 · lg 28 tall, 12px text). Addition — distinct from Badge/ActionChip. */
2
+ /** Small removable filter tag (full radius; sm 20 · md and lg 24 tall, 12px text). Addition — distinct from Badge/ActionChip. */
3
3
  export interface TagProps {
4
4
  children?: React.ReactNode;
5
5
  /** Size tier. Omit to inherit the enclosing region's tier (the default). */
@@ -3,7 +3,7 @@ import { Icon } from "../icons/Icon.jsx";
3
3
 
4
4
  const tones = { neutral: { bg: "var(--bg-hover)", fg: "var(--text-body)" }, info: { bg: "var(--info-bg)", fg: "var(--info-fg)" } };
5
5
 
6
- /** Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). Height follows the region's tier (sm 20 · md 24 · lg 28, --tag-h); text stays 12px. */
6
+ /** Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). Height follows the region's tier (sm 20 · md and lg 24, --tag-h); text stays 12px. */
7
7
  export function Tag({ children, onRemove, tone = "neutral", size, style }) {
8
8
  const t = tones[tone] || tones.neutral;
9
9
  return (
@@ -2,7 +2,7 @@
2
2
  import { computed } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  const tones = { neutral: { bg: 'var(--bg-hover)', fg: 'var(--text-body)' }, info: { bg: 'var(--info-bg)', fg: 'var(--info-fg)' } }
5
- // Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). Height follows the region's tier (sm 20 · md 24 · lg 28, --tag-h); text stays 12px.
5
+ // Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). Height follows the region's tier (sm 20 · md and lg 24, --tag-h); text stays 12px.
6
6
  const props = defineProps({ tone: { type: String, default: 'neutral' }, removable: Boolean, size: String, style: Object })
7
7
  defineEmits(['remove'])
8
8
  const t = computed(() => tones[props.tone] || tones.neutral)
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  /**
3
- * Switch — pill track at the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms ease-out slide. Controlled.
3
+ * Switch — pill track at the region's tier (sm 28×16 · md and lg 32×18), knob 4px smaller, 160ms ease-out slide. Controlled.
4
4
  * Variants: knob glyph (icon or one character), text inside the pill, captions beside it,
5
5
  * a semantic red→green tone, and an async gate that shows a spinner in the knob.
6
6
  */
@@ -8,7 +8,7 @@ export interface SwitchProps {
8
8
  checked?: boolean;
9
9
  onChange?(next: boolean): void;
10
10
  disabled?: boolean;
11
- /** Size tier: sm 28×16 · md 36×20 · lg 44×24. Omit to inherit the enclosing region's tier (the default); lg is roomy enough for a knob glyph */
11
+ /** Size tier: sm 28×16 · md and lg 32×18. Omit to inherit the enclosing region's tier (the default); lg keeps the md track — a switch never grows past 32×18 */
12
12
  size?: "sm" | "md" | "lg";
13
13
  /** primary = action blue on / border-strong off · semantic = success on / danger off */
14
14
  tone?: "primary" | "semantic";
@@ -2,12 +2,12 @@ import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
4
  /**
5
- * Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms ease-out slide.
5
+ * Switch — pill track sized by the region's tier (sm 28×16 · md and lg 32×18), knob 4px smaller, 160ms ease-out slide.
6
6
  * Track, knob, glyph and label all read the tier tokens (tokens/layout.css), so a Switch in a form row matches its neighbours.
7
7
  *
8
8
  * checked / onChange(next) controlled boolean
9
9
  * disabled 50% opacity, no pointer
10
- * size "sm" | "md" | "lg" — omit to inherit the region's tier; lg is roomy enough for knobIcon / knobText
10
+ * size "sm" | "md" | "lg" — omit to inherit the region's tier; lg keeps the md track — a switch never grows past 32×18
11
11
  * tone "primary" (default, action blue) | "semantic" (red off → green on)
12
12
  * onColor / offColor explicit track colors; override tone
13
13
  * knobIcon / knobText glyph inside the knob: string | { on, off }. knobText is ONE letter or ONE CJK character.
@@ -33,9 +33,11 @@ export function Switch({ checked = false, onChange, disabled, size, tone = "prim
33
33
  const loading = loadingProp || busy;
34
34
  if (inlinePrompt) { insideOn = insideOn ?? inlinePrompt.on; insideOff = insideOff ?? inlinePrompt.off; }
35
35
  const hasInside = insideOn != null || insideOff != null;
36
- // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character
37
- const insideChars = hasInside ? Math.max(String(insideOn ?? "").length, String(insideOff ?? "").length) : 0;
38
- const w = hasInside ? `max(${W}, calc(${H} * ${(1 + insideChars * .34).toFixed(2)} + 6px))` : W;
36
+ // inside text needs a wider track so the knob never covers the word: knob slot (track height) + text + 8px.
37
+ // Text is TEXT px; a CJK character is ~1em wide, a Latin one ~.68em.
38
+ const insideStr = [insideOn, insideOff].map((s) => String(s ?? ""));
39
+ const insideEm = hasInside ? Math.max(...insideStr.map((s) => s.length * (/[\u3000-\u9fff\uac00-\ud7af]/.test(s) ? 1 : .68))) : 0;
40
+ const w = hasInside ? `max(${W}, calc(${H} + ${TEXT} * ${insideEm.toFixed(2)} + 8px))` : W;
39
41
 
40
42
  const onTrack = onColor || (tone === "semantic" ? "var(--success)" : "var(--action-primary)");
41
43
  const offTrack = offColor || (tone === "semantic" ? "var(--danger)" : "var(--border-strong)");
@@ -78,11 +80,11 @@ export function Switch({ checked = false, onChange, disabled, size, tone = "prim
78
80
  </span>;
79
81
 
80
82
  const capBase = { fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", fontFamily: "var(--font-sans)", cursor: inert ? "default" : "pointer", userSelect: "none" };
81
- if (single) return <span data-size={size} data-sw="" style={{ display: "inline-flex", alignItems: "center", gap: "var(--control-gap)", opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ ...capBase, fontWeight: "var(--weight-regular)", color: "var(--text-body)" }}>{label}</span></span>;
82
- if (labelOn == null && labelOff == null) return <span data-size={size} style={{ display: "inline-flex", alignItems: "center", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;
83
+ if (single) return <span data-size={size} data-sw="" style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", gap: "var(--control-gap)", opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ ...capBase, fontWeight: "var(--weight-regular)", color: "var(--text-body)" }}>{label}</span></span>;
84
+ if (labelOn == null && labelOff == null) return <span data-size={size} style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;
83
85
 
84
86
  const cap = (text, active) => text == null ? null : <span style={{ ...capBase, fontWeight: active ? "var(--weight-medium)" : "var(--weight-regular)", color: active ? "var(--text-heading)" : "var(--text-muted)", transition: "color 160ms ease-out" }} onClick={flip}>{text}</span>;
85
- return <span data-size={size} data-sw="" style={{ display: "inline-flex", alignItems: "center", gap: "var(--control-gap)", opacity: disabled ? .5 : 1, ...style }}>
87
+ return <span data-size={size} data-sw="" style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", gap: "var(--control-gap)", opacity: disabled ? .5 : 1, ...style }}>
86
88
  {cap(labelOff, !checked)}{pill}{cap(labelOn, checked)}
87
89
  </span>;
88
90
  }
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { ref, computed, onBeforeUnmount } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
- // Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms slide. v-model:checked.
4
+ // Switch — pill track sized by the region's tier (sm 28×16 · md and lg 32×18), knob 4px smaller, 160ms slide. v-model:checked.
5
5
  // Geometry is CSS (tokens/layout.css --switch-w/-h), so it follows the tier without JavaScript; `size` stamps data-size to override.
6
6
  // tone "primary" | "semantic" (red off → green on); onColor/offColor override.
7
7
  // knobIcon / knobText: string | { on, off } — knobText is ONE letter or ONE CJK char.
@@ -24,8 +24,9 @@ const inert = computed(() => props.disabled || isLoading.value)
24
24
  const inOn = computed(() => props.insideOn ?? (props.inlinePrompt ? props.inlinePrompt.on : undefined))
25
25
  const inOff = computed(() => props.insideOff ?? (props.inlinePrompt ? props.inlinePrompt.off : undefined))
26
26
  const hasInside = computed(() => inOn.value != null || inOff.value != null)
27
- // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character
28
- const width = computed(() => { if (!hasInside.value) return W; const n = Math.max((inOn.value || '').length, (inOff.value || '').length); return `max(${W}, calc(${H} * ${(1 + n * .34).toFixed(2)} + 6px))` })
27
+ // inside text needs a wider track so the knob never covers the word: knob slot (track height) + text + 8px.
28
+ // Text is TEXT px; a CJK character is ~1em wide, a Latin one ~.68em.
29
+ const width = computed(() => { if (!hasInside.value) return W; const em = Math.max(...[inOn.value, inOff.value].map((s) => { s = String(s ?? ''); return s.length * (/[\u3000-\u9fff\uac00-\ud7af]/.test(s) ? 1 : .68) })); return `max(${W}, calc(${H} + ${TEXT} * ${em.toFixed(2)} + 8px))` })
29
30
  const track = computed(() => props.checked ? (props.onColor || (props.tone === 'semantic' ? 'var(--success)' : 'var(--action-primary)')) : (props.offColor || (props.tone === 'semantic' ? 'var(--danger)' : 'var(--border-strong)')))
30
31
  const pick = (v) => v && typeof v === 'object' ? (props.checked ? v.on : v.off) : v
31
32
  const glyphIcon = computed(() => pick(props.knobIcon))
@@ -47,7 +48,7 @@ const insideStyle = (side, on) => ({ position: 'absolute', top: 0, bottom: 0, [s
47
48
  const cap = (active) => ({ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: active ? 'var(--weight-medium)' : 'var(--weight-regular)', color: active ? 'var(--text-heading)' : 'var(--text-muted)', transition: 'color 160ms ease-out', cursor: inert.value ? 'default' : 'pointer', userSelect: 'none' })
48
49
  </script>
49
50
  <template>
50
- <span :data-size="size" data-sw="" :style="{ display: 'inline-flex', alignItems: 'center', gap: 'var(--control-gap)', opacity: disabled ? .5 : 1, ...style }">
51
+ <span :data-size="size" data-sw="" :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', gap: 'var(--control-gap)', opacity: disabled ? .5 : 1, ...style }">
51
52
  <span v-if="labelOff != null" :style="cap(!checked)" @click="flip">{{ labelOff }}</span>
52
53
  <span role="switch" :aria-checked="checked" :aria-busy="isLoading || undefined" :aria-disabled="disabled || undefined" :tabindex="inert ? -1 : 0" @click="flip" @keydown.space.prevent="flip" @keydown.enter.prevent="flip"
53
54
  :style="{ position: 'relative', display: 'inline-block', width: width, height: H, borderRadius: 'var(--radius-full)', '--sw-track': track, background: 'var(--sw-bg, var(--sw-track))', transition: 'background 160ms ease-out', cursor: inert ? (isLoading ? 'progress' : 'not-allowed') : 'pointer', flexShrink: 0, verticalAlign: 'middle', outline: 'none' }">
@@ -15,7 +15,7 @@ Form controls, split by component (each with its own specimen card covering defa
15
15
  Everything is 12px text in 36px controls; readOnly inputs go #fafafa; disabled = 50% opacity; error = red border + red helper text via `Field`'s `error` prop.
16
16
 
17
17
 
18
- `Switch` / `Radio` / `RadioGroup` (additions, not in the source file). Switch: the track follows the size tier (sm 28×16 · md 36×20 · lg 44×24), blue-on / border-strong-off; a knob glyph needs the lg tier (`size="lg"` only when the switch stands alone) — `knobIcon` (string or `{on,off}`) or `knobText` (ONE letter / ONE CJK char); `insideOn`/`insideOff` put 1–3 characters inside the pill; `labelOn`/`labelOff` put captions on both sides with the active one in text-heading; `tone="semantic"` = danger off → success on, only for states with consequence (a live integration, a kill switch); `beforeChange` async-gates the flip with a spinner in the knob, `loading` does the same under your control.
18
+ `Switch` / `Radio` / `RadioGroup` (additions, not in the source file). Switch: the track follows the size tier (sm 28×16 · md and lg 32×18 — two sizes), blue-on / border-strong-off; a knob glyph needs md or lg (the sm knob is too small) — `knobIcon` (string or `{on,off}`) or `knobText` (ONE letter / ONE CJK char); `insideOn`/`insideOff` put 1–3 characters inside the pill; `labelOn`/`labelOff` put captions on both sides with the active one in text-heading; `tone="semantic"` = danger off → success on, only for states with consequence (a live integration, a kill switch); `beforeChange` async-gates the flip with a spinner in the knob, `loading` does the same under your control.
19
19
 
20
20
  ```jsx
21
21
  <Switch label="Debug Mode" checked={on} onChange={setOn}/>
@@ -97,7 +97,7 @@ export const ICON_PATHS = {
97
97
  "dimension-custom_default": '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
98
98
  "dimension-entity": '<path d="M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18"/><path d="M14 10h6a2 2 0 0 1 2 2v10"/><path d="M1 22h22"/><path d="M6 6h.01"/><path d="M10 6h.01"/><path d="M6 10h.01"/><path d="M10 10h.01"/><path d="M6 14h.01"/><path d="M10 14h.01"/><path d="M18 14h.01"/><path d="M18 18h.01"/><path d="M6 22v-3h4v3"/>',
99
99
  "dimension-group": '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
100
- "dimension-intercompany": '<path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>',
100
+ "dimension-intercompany": '<path d="M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18"/><path d="M1 22h22"/><path d="M6 6h.01"/><path d="M10 6h.01"/><path d="M6 10h.01"/><path d="M6 14h.01"/><path d="M10 10h.01"/><path d="M10 14h.01"/><path d="M6 22v-3h4v3"/><path d="M14 15h6a2 2 0 0 1 2 2v5"/><path d="M18 18.5h.01"/><path d="M16 4h6"/><path d="m20 2 2 2-2 2"/><path d="M22 9h-6"/><path d="m18 7-2 2 2 2"/>',
101
101
  "dimension-subtable": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 9v12"/><path d="M15 9v12"/>',
102
102
  "dimension-time": '<path d="M16 14v2.2l1.6 1"/><path d="M16 2v4"/><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M3 10h5"/><path d="M8 2v4"/><circle cx="16" cy="16" r="6"/>',
103
103
  "dimension-version": '<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
@@ -166,6 +166,7 @@ export const ICON_PATHS = {
166
166
  "task-etl": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><ellipse cx="12" cy="10.5" rx="3.5" ry="1.5"/><path d="M8.5 10.5v6c0 .83 1.57 1.5 3.5 1.5s3.5-.67 3.5-1.5v-6"/><path d="M8.5 13.5c0 .83 1.57 1.5 3.5 1.5s3.5-.67 3.5-1.5"/>',
167
167
  "task-shared": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><circle cx="15.3" cy="6.8" r=".75"/><circle cx="9.7" cy="10" r=".75"/><circle cx="15.3" cy="13.2" r=".75"/><path d="m11.35 9.06 2.3-1.32"/><path d="m11.35 10.94 2.3 1.32"/></g>',
168
168
  "task-intercompany": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><path d="M8.5 14V6.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V14"/><path d="M13.5 9.5H16a1 1 0 0 1 1 1V14"/><path d="M7.5 14h10"/><path d="M11 8.5h.01"/><path d="M11 11h.01"/></g>',
169
+ ownership: '<path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="m2 16 6 6"/><circle cx="9" cy="5" r="3"/><path d="M12 5h9"/><path d="M18 5v2.5"/><path d="M21 5v2"/>',
169
170
  };
170
171
 
171
172
  import { FEATHER_PATHS } from "./feather-paths.js";
@@ -92,7 +92,7 @@ export const VUE_ICON_PATHS = {
92
92
  "dimension-custom_default": '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
93
93
  "dimension-entity": '<path d="M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18"/><path d="M14 10h6a2 2 0 0 1 2 2v10"/><path d="M1 22h22"/><path d="M6 6h.01"/><path d="M10 6h.01"/><path d="M6 10h.01"/><path d="M10 10h.01"/><path d="M6 14h.01"/><path d="M10 14h.01"/><path d="M18 14h.01"/><path d="M18 18h.01"/><path d="M6 22v-3h4v3"/>',
94
94
  "dimension-group": '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
95
- "dimension-intercompany": '<path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>',
95
+ "dimension-intercompany": '<path d="M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18"/><path d="M1 22h22"/><path d="M6 6h.01"/><path d="M10 6h.01"/><path d="M6 10h.01"/><path d="M6 14h.01"/><path d="M10 10h.01"/><path d="M10 14h.01"/><path d="M6 22v-3h4v3"/><path d="M14 15h6a2 2 0 0 1 2 2v5"/><path d="M18 18.5h.01"/><path d="M16 4h6"/><path d="m20 2 2 2-2 2"/><path d="M22 9h-6"/><path d="m18 7-2 2 2 2"/>',
96
96
  "dimension-subtable": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 9v12"/><path d="M15 9v12"/>',
97
97
  "dimension-time": '<path d="M16 14v2.2l1.6 1"/><path d="M16 2v4"/><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M3 10h5"/><path d="M8 2v4"/><circle cx="16" cy="16" r="6"/>',
98
98
  "dimension-version": '<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
@@ -161,4 +161,5 @@ export const VUE_ICON_PATHS = {
161
161
  "task-etl": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><ellipse cx="12" cy="10.5" rx="3.5" ry="1.5"/><path d="M8.5 10.5v6c0 .83 1.57 1.5 3.5 1.5s3.5-.67 3.5-1.5v-6"/><path d="M8.5 13.5c0 .83 1.57 1.5 3.5 1.5s3.5-.67 3.5-1.5"/>',
162
162
  "task-shared": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><circle cx="15.3" cy="6.8" r=".75"/><circle cx="9.7" cy="10" r=".75"/><circle cx="15.3" cy="13.2" r=".75"/><path d="m11.35 9.06 2.3-1.32"/><path d="m11.35 10.94 2.3 1.32"/></g>',
163
163
  "task-intercompany": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><path d="M8.5 14V6.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V14"/><path d="M13.5 9.5H16a1 1 0 0 1 1 1V14"/><path d="M7.5 14h10"/><path d="M11 8.5h.01"/><path d="M11 11h.01"/></g>',
164
+ ownership: '<path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="m2 16 6 6"/><circle cx="9" cy="5" r="3"/><path d="M12 5h9"/><path d="M18 5v2.5"/><path d="M21 5v2"/>',
164
165
  };