@keyfunc/keybricks-ds 2.4.6 → 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.
- package/DEPLOY.md +2 -2
- package/DESIGN.md +4 -4
- package/INTEGRATION.md +24 -2
- package/_adherence.oxlintrc.json +4 -4
- package/assets/icons/icon-index.js +1 -1
- package/assets/icons/icon-index.json +1 -1
- package/components/_cards/segmented-control.card.html +1 -1
- package/components/_cards/table.card.html +139 -6
- package/components/_cards/toggles.card.html +27 -27
- package/components/_shared/src-data.js +1 -1
- package/components/_shared/src-display.js +1 -1
- package/components/_shared/src-forms.js +1 -1
- package/components/_shared/src-icons.js +1 -1
- package/components/_shared/src-navigation.js +1 -1
- package/components/_shared/src-tabs.js +1 -1
- package/components/data/SmartTable.d.ts +9 -1
- package/components/data/SmartTable.jsx +24 -4
- package/components/data/SmartTable.vue +18 -5
- package/components/data/Spreadsheet.d.ts +5 -1
- package/components/data/Spreadsheet.jsx +23 -3
- package/components/data/Spreadsheet.vue +18 -5
- package/components/data/Table.d.ts +25 -4
- package/components/data/Table.jsx +74 -20
- package/components/data/Table.vue +56 -14
- package/components/display/Tag.d.ts +1 -1
- package/components/display/Tag.jsx +1 -1
- package/components/display/Tag.vue +1 -1
- package/components/forms/Switch.d.ts +2 -2
- package/components/forms/Switch.jsx +10 -8
- package/components/forms/Switch.vue +5 -4
- package/components/forms/forms.prompt.md +1 -1
- package/components/icons/Icon.jsx +3 -2
- package/components/icons/icon-paths.js +3 -2
- package/components/navigation/Breadcrumb.jsx +9 -7
- package/components/navigation/Breadcrumb.vue +16 -14
- package/components/tabs/CountBadge.vue +1 -1
- package/components/tabs/Tabs.d.ts +1 -1
- package/components/tabs/Tabs.jsx +1 -1
- package/package.json +1 -1
- 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
|
|
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-
|
|
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) => (
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
|
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
|
|
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) => (
|
|
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
|
|
104
|
-
const codeChip = { fontFamily: 'var(--font-mono)', fontSize: 'var(--text-code)', lineHeight: 'var(--leading-code)', letterSpacing: 0, background: 'var(--bg-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
37
|
-
|
|
38
|
-
const
|
|
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
|
|
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:
|
|
28
|
-
|
|
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
|
|
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="
|
|
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"/>',
|
|
@@ -109,7 +109,7 @@ export const ICON_PATHS = {
|
|
|
109
109
|
admin: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
|
110
110
|
analytics: '<rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/><path d="M6 7h4"/><path d="M6 10h4"/><path d="M6 13h2"/><path d="m13 13 2-3 2 1 2-4"/>',
|
|
111
111
|
automation: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><polygon points="10.5 9 15 12 10.5 15 10.5 9"/>',
|
|
112
|
-
"data_integration": '<
|
|
112
|
+
"data_integration": '<ellipse cx="17" cy="7" rx="5" ry="2"/><path d="M12 7v10a5 2 0 0 0 10 0V7"/><path d="M12 12a5 2 0 0 0 10 0"/><circle cx="3" cy="5" r="2"/><circle cx="3" cy="12" r="2"/><circle cx="3" cy="19" r="2"/><path d="M5 5h1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5"/><path d="M5 12h7"/>',
|
|
113
113
|
"reconciliation-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"/>',
|
|
114
114
|
"reconciliation-balance": '<path d="M13.744 17.736a6 6 0 1 1-7.48-7.48"/><path d="M15 6h1v4"/><path d="m6.134 14.768.866-.5 2 3.464"/><circle cx="16" cy="8" r="6"/>',
|
|
115
115
|
scenario: '<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/>',
|
|
@@ -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";
|