@keyfunc/keybricks-ds 2.1.1 → 2.4.0
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 +20 -5
- package/DESIGN.md +148 -20
- package/INTEGRATION.md +97 -24
- package/SKILL.md +11 -11
- package/_adherence.oxlintrc.json +56 -24
- package/_scripts/build-index.mjs +15 -4
- package/assets/icons/icon-index.js +1 -1
- package/assets/icons/icon-index.json +1 -1
- package/assets/logos/datasource/dat.png +0 -0
- package/assets/logos/datasource/excel.png +0 -0
- package/assets/logos/datasource/ftp.png +0 -0
- package/assets/logos/datasource/hierarchy.png +0 -0
- package/assets/logos/datasource/hive.png +0 -0
- package/assets/logos/datasource/json.png +0 -0
- package/assets/logos/datasource/mariadb.png +0 -0
- package/assets/logos/datasource/mongodb.png +0 -0
- package/assets/logos/datasource/mysql.png +0 -0
- package/assets/logos/datasource/odbc.png +0 -0
- package/assets/logos/datasource/oracle.png +0 -0
- package/assets/logos/datasource/postgresql.png +0 -0
- package/assets/logos/datasource/rest-api.png +0 -0
- package/assets/logos/datasource/sap-alt.png +0 -0
- package/assets/logos/datasource/sap.png +0 -0
- package/assets/logos/datasource/soap.png +0 -0
- package/assets/logos/datasource/sql-server.png +0 -0
- package/components/_cards/accordion.card.html +1 -1
- package/components/_cards/action-chip.card.html +11 -14
- package/components/_cards/activity.card.html +2 -2
- package/components/_cards/alert.card.html +2 -2
- package/components/_cards/app-chrome.card.html +34 -22
- package/components/_cards/banner.card.html +1 -1
- package/components/_cards/breadcrumb.card.html +4 -4
- package/components/_cards/button-1-base.card.html +23 -26
- package/components/_cards/button-2-states.card.html +14 -12
- package/components/_cards/button-3-sizes-icons.card.html +20 -19
- package/components/_cards/button-4-group.card.html +14 -31
- package/components/_cards/canvas-zoom-controls.card.html +1 -1
- package/components/_cards/cards-indicators.card.html +1 -1
- package/components/_cards/charts-1-sparkline.card.html +4 -4
- package/components/_cards/charts-10-gauge.card.html +2 -2
- package/components/_cards/charts-11-funnel.card.html +2 -2
- package/components/_cards/charts-12-treemap.card.html +2 -2
- package/components/_cards/charts-14-radar.card.html +3 -3
- package/components/_cards/charts-15-distribution.card.html +2 -2
- package/components/_cards/charts-16-pareto.card.html +2 -2
- package/components/_cards/charts-17-candlestick.card.html +2 -2
- package/components/_cards/charts-18-sankey.card.html +2 -2
- package/components/_cards/charts-19-chord.card.html +2 -2
- package/components/_cards/charts-2-line.card.html +3 -3
- package/components/_cards/charts-20-sunburst.card.html +2 -2
- package/components/_cards/charts-21-worldmap.card.html +3 -3
- package/components/_cards/charts-22-gantt.card.html +2 -2
- package/components/_cards/charts-23-bar3d.card.html +2 -2
- package/components/_cards/charts-24-pie3d.card.html +2 -2
- package/components/_cards/charts-25-surface3d.card.html +2 -2
- package/components/_cards/charts-26-scatter3d.card.html +2 -2
- package/components/_cards/charts-27-ribbon3d.card.html +2 -2
- package/components/_cards/charts-3-bar.card.html +6 -6
- package/components/_cards/charts-4-barlist.card.html +2 -2
- package/components/_cards/charts-5-donut.card.html +2 -2
- package/components/_cards/charts-6-scatter.card.html +2 -2
- package/components/_cards/charts-7-heatmap.card.html +2 -2
- package/components/_cards/charts-8-waterfall.card.html +2 -2
- package/components/_cards/charts-9-bullet.card.html +3 -3
- package/components/_cards/charts-99-fullpage.card.html +1 -1
- package/components/_cards/checkbox.card.html +6 -3
- package/components/_cards/codeblock.card.html +2 -2
- package/components/_cards/combobox.card.html +6 -4
- package/components/_cards/context-menu.card.html +12 -5
- package/components/_cards/date-picker.card.html +8 -5
- package/components/_cards/diffvalue.card.html +2 -2
- package/components/_cards/divider.card.html +2 -2
- package/components/_cards/drawer.card.html +3 -3
- package/components/_cards/dropdown-menu.card.html +2 -2
- package/components/_cards/dropzone.card.html +5 -3
- package/components/_cards/empty-state.card.html +2 -2
- package/components/_cards/filter-bar.card.html +1 -1
- package/components/_cards/icon-button.card.html +14 -14
- package/components/_cards/icons-0-usage.card.html +88 -0
- package/components/_cards/icons-1-core.card.html +4 -4
- package/components/_cards/icons-2-feather.card.html +4 -4
- package/components/_cards/icons-3-unicons.card.html +3 -3
- package/components/_cards/icons-4-datasource.card.html +5 -0
- package/components/_cards/input.card.html +5 -5
- package/components/_cards/kbd.card.html +1 -1
- package/components/_cards/keyvalue.card.html +1 -1
- package/components/_cards/listrow.card.html +1 -1
- package/components/_cards/loading.card.html +2 -2
- package/components/_cards/log-viewer.card.html +2 -2
- package/components/_cards/modal.card.html +1 -1
- package/components/_cards/multi-select.card.html +6 -4
- package/components/_cards/number-stepper.card.html +9 -6
- package/components/_cards/pagination.card.html +6 -4
- package/components/_cards/people.card.html +2 -2
- package/components/_cards/pipeline-node.card.html +8 -8
- package/components/_cards/pipeline-palette.card.html +2 -2
- package/components/_cards/radio.card.html +12 -7
- package/components/_cards/search.card.html +6 -6
- package/components/_cards/segmented-control.card.html +19 -12
- package/components/_cards/select.card.html +7 -3
- package/components/_cards/slider.card.html +2 -2
- package/components/_cards/smart-table.card.html +3 -3
- package/components/_cards/spreadsheet.card.html +2 -2
- package/components/_cards/stepper.card.html +2 -2
- package/components/_cards/table.card.html +79 -17
- package/components/_cards/tabs.card.html +10 -5
- package/components/_cards/tag.card.html +6 -6
- package/components/_cards/textarea.card.html +1 -1
- package/components/_cards/timeline.card.html +3 -3
- package/components/_cards/toast.card.html +7 -5
- package/components/_cards/toggles.card.html +14 -9
- package/components/_cards/tooltip.card.html +3 -3
- package/components/_cards/tree.card.html +2 -2
- package/components/_shared/card-kit.css +25 -0
- package/components/_shared/card-kit.js +50 -0
- package/components/_shared/src-buttons.js +1 -1
- package/components/_shared/src-charts.js +1 -1
- package/components/_shared/src-data.js +1 -1
- package/components/_shared/src-display.js +1 -1
- package/components/_shared/src-feedback.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/_shared/view-source.js +12 -12
- package/components/_vue/index.mjs +10 -0
- package/components/buttons/ActionChip.d.ts +1 -1
- package/components/buttons/Button.d.ts +1 -1
- package/components/buttons/Button.jsx +1 -1
- package/components/buttons/Button.vue +1 -1
- package/components/buttons/IconButton.d.ts +1 -1
- package/components/buttons/IconButton.jsx +5 -3
- package/components/buttons/IconButton.vue +3 -2
- package/components/charts/Bar3D.jsx +1 -1
- package/components/charts/Bar3D.vue +2 -2
- package/components/charts/BarChart.jsx +3 -3
- package/components/charts/BarChart.vue +6 -6
- package/components/charts/BarList.jsx +3 -3
- package/components/charts/BarList.vue +3 -3
- package/components/charts/BoxPlot.vue +2 -2
- package/components/charts/BulletChart.jsx +5 -5
- package/components/charts/BulletChart.vue +5 -5
- package/components/charts/CandlestickChart.vue +3 -3
- package/components/charts/ChartKit.jsx +4 -3
- package/components/charts/ChartTooltip.vue +1 -1
- package/components/charts/ChordChart.jsx +1 -1
- package/components/charts/ChordChart.vue +2 -2
- package/components/charts/DonutChart.jsx +1 -1
- package/components/charts/DonutChart.vue +2 -2
- package/components/charts/FunnelChart.jsx +3 -3
- package/components/charts/FunnelChart.vue +4 -4
- package/components/charts/GanttChart.vue +1 -1
- package/components/charts/GaugeChart.jsx +2 -2
- package/components/charts/GaugeChart.vue +2 -2
- package/components/charts/HeatmapChart.jsx +1 -1
- package/components/charts/HeatmapChart.vue +1 -1
- package/components/charts/Histogram.jsx +2 -2
- package/components/charts/Histogram.vue +4 -4
- package/components/charts/LineChart.vue +2 -2
- package/components/charts/ParetoChart.jsx +1 -1
- package/components/charts/ParetoChart.vue +3 -3
- package/components/charts/Pie3D.jsx +1 -1
- package/components/charts/Pie3D.vue +2 -2
- package/components/charts/PlotGrid.vue +1 -1
- package/components/charts/RadarChart.vue +1 -1
- package/components/charts/Ribbon3D.jsx +1 -1
- package/components/charts/Ribbon3D.vue +2 -2
- package/components/charts/SankeyChart.jsx +1 -1
- package/components/charts/SankeyChart.vue +2 -2
- package/components/charts/Scatter3D.vue +1 -1
- package/components/charts/ScatterChart.jsx +1 -1
- package/components/charts/ScatterChart.vue +3 -3
- package/components/charts/Sparkline.jsx +1 -1
- package/components/charts/Sparkline.vue +1 -1
- package/components/charts/SunburstChart.vue +1 -1
- package/components/charts/Surface3D.jsx +1 -1
- package/components/charts/Surface3D.vue +2 -2
- package/components/charts/TreemapChart.jsx +1 -1
- package/components/charts/TreemapChart.vue +2 -2
- package/components/charts/WaterfallChart.jsx +1 -1
- package/components/charts/WaterfallChart.vue +5 -4
- package/components/charts/WorldMap.jsx +4 -4
- package/components/charts/WorldMap.vue +5 -5
- package/components/data/DiffValue.d.ts +1 -0
- package/components/data/DiffValue.jsx +1 -1
- package/components/data/DiffValue.vue +1 -1
- package/components/data/KeyValue.d.ts +1 -1
- package/components/data/KeyValue.jsx +2 -2
- package/components/data/KeyValue.vue +2 -2
- package/components/data/Pagination.jsx +2 -2
- package/components/data/Pagination.vue +1 -1
- package/components/data/SmartTable.jsx +5 -5
- package/components/data/SmartTable.vue +6 -6
- package/components/data/Spreadsheet.jsx +5 -5
- package/components/data/Spreadsheet.vue +6 -6
- package/components/data/Table.d.ts +58 -4
- package/components/data/Table.jsx +153 -15
- package/components/data/Table.vue +105 -15
- package/components/data/data.prompt.md +1 -1
- package/components/display/Avatar.jsx +2 -2
- package/components/display/Avatar.vue +1 -1
- package/components/display/AvatarGroup.vue +1 -1
- package/components/display/Badge.d.ts +4 -1
- package/components/display/Badge.jsx +10 -5
- package/components/display/IconTile.vue +9 -4
- package/components/display/Kbd.d.ts +1 -1
- package/components/display/Kbd.jsx +1 -1
- package/components/display/Kbd.vue +1 -1
- package/components/feedback/ActivityItem.jsx +1 -1
- package/components/feedback/ContextMenu.d.ts +3 -2
- package/components/feedback/ContextMenu.jsx +8 -4
- package/components/feedback/ContextMenu.vue +9 -6
- package/components/feedback/Menu.d.ts +1 -1
- package/components/feedback/Modal.jsx +1 -1
- package/components/feedback/Modal.vue +1 -1
- package/components/feedback/Timeline.vue +2 -2
- package/components/feedback/feedback.prompt.md +1 -1
- package/components/forms/Combobox.d.ts +1 -1
- package/components/forms/DatePicker.d.ts +1 -1
- package/components/forms/Input.d.ts +1 -1
- package/components/forms/Input.jsx +2 -2
- package/components/forms/Input.vue +1 -1
- package/components/forms/MultiSelect.d.ts +1 -1
- package/components/forms/NumberStepper.d.ts +1 -1
- package/components/forms/SearchInput.d.ts +1 -1
- package/components/forms/Select.d.ts +1 -1
- package/components/forms/Select.jsx +1 -1
- package/components/forms/Select.vue +1 -1
- package/components/forms/Slider.jsx +1 -1
- package/components/forms/Slider.vue +1 -1
- package/components/forms/Switch.jsx +3 -3
- package/components/forms/Switch.vue +11 -8
- package/components/forms/Textarea.vue +1 -1
- package/components/forms/input-base.js +1 -1
- package/components/icons/Icon.jsx +17 -1
- package/components/icons/Icon.prompt.md +2 -0
- package/components/icons/Icon.vue +1 -1
- package/components/icons/icon-paths.js +15 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +5 -4
- package/components/navigation/Breadcrumb.jsx +3 -7
- package/components/navigation/Breadcrumb.vue +2 -8
- package/components/navigation/SidePanel.d.ts +22 -0
- package/components/navigation/SidePanel.jsx +23 -0
- package/components/navigation/SidePanel.vue +21 -0
- package/components/navigation/Sidebar.d.ts +23 -7
- package/components/navigation/Sidebar.jsx +38 -56
- package/components/navigation/Sidebar.vue +40 -44
- package/components/navigation/TopBar.d.ts +28 -3
- package/components/navigation/TopBar.jsx +54 -7
- package/components/navigation/TopBar.vue +26 -8
- package/components/navigation/Tree.jsx +1 -1
- package/components/navigation/Tree.vue +9 -4
- package/components/navigation/UserMenu.vue +27 -0
- package/components/navigation/chrome-tones.js +22 -0
- package/components/navigation/nav-model.js +26 -0
- package/components/navigation/navigation.prompt.md +2 -0
- package/components/navigation/theme-store.js +9 -0
- package/components/tabs/CountBadge.vue +12 -0
- package/components/tabs/PanelTabs.vue +7 -3
- package/components/tabs/PillTabs.vue +8 -4
- package/components/tabs/SegmentedControl.d.ts +5 -3
- package/components/tabs/SegmentedControl.jsx +6 -4
- package/components/tabs/SegmentedControl.vue +4 -3
- package/components/tabs/Tabs.d.ts +14 -4
- package/components/tabs/Tabs.jsx +35 -20
- package/components/tabs/Tabs.vue +7 -3
- package/components/tabs/ViewToggle.d.ts +1 -1
- package/components/tabs/tabs.prompt.md +2 -2
- package/package.json +2 -2
- package/readme.md +10 -9
- package/readme.zh-CN.md +8 -7
- package/tokens/alpha.css +3 -1
- package/tokens/colors.css +1 -1
- package/tokens/dark.css +1 -1
- package/tokens/layout.css +8 -5
- package/tokens/tokens.d.ts +24 -0
- package/tokens/tokens.js +42 -6
- package/tokens/tokens.json +97 -11
- package/tokens/typography.css +26 -9
|
@@ -1,10 +1,123 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Checkbox } from "../forms/Checkbox.jsx";
|
|
3
|
+
import { IconTile } from "../display/Badge.jsx";
|
|
4
|
+
import { IconButton } from "../buttons/IconButton.jsx";
|
|
5
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
6
|
+
|
|
7
|
+
/* Selection checkbox: sits in the first column's leading padding, not in a column of its own, so the gap to the
|
|
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
|
+
of any spare width). */
|
|
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.
|
|
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" };
|
|
14
|
+
const checkSlot = { position: "absolute", left: "var(--edge-pad)", top: 0, bottom: 0, display: "flex", alignItems: "center" };
|
|
15
|
+
|
|
16
|
+
/* Two-line title cell (`twoLine` column): icon tile · title over description · trailing ⋯. Every measure is a --title-cell-* tier token:
|
|
17
|
+
md 32px tile (r8) · 12px to the text · 14px title / 12px description · 2px between · 10px row padding → 58px row. */
|
|
18
|
+
const T_TILE = { width: "var(--title-cell-tile)", height: "var(--title-cell-tile)", borderRadius: "var(--title-cell-tile-r)" };
|
|
19
|
+
const T_ICON = { width: "var(--title-cell-icon)", height: "var(--title-cell-icon)" };
|
|
20
|
+
const T_ONE = { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 };
|
|
21
|
+
const T_TITLE = { ...T_ONE, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)" };
|
|
22
|
+
const T_DESC = { ...T_ONE, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)" };
|
|
23
|
+
const STAR_FILLED = "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z";
|
|
24
|
+
// field name (string) or accessor (row, index) => value
|
|
25
|
+
const pick = (a, r, ri) => typeof a === "function" ? a(r, ri) : a != null ? r[a] : undefined;
|
|
26
|
+
/* Sorting: one column at a time. Click cycles ascending → descending → none; another column starts at ascending.
|
|
27
|
+
The tooltip names the next step. Rows are reordered here unless manualSort (server-side: rows arrive ordered). */
|
|
28
|
+
// the label is capped at the declared width minus the button (28) and its gap (4), so a long label truncates and the cell never
|
|
29
|
+
// shrinks below label + button — the button is never pushed out
|
|
30
|
+
const SORT_BTN = 32;
|
|
31
|
+
const isEmpty = (v) => v == null || v === "" || v === "–";
|
|
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
|
+
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" };
|
|
35
|
+
const sortLabel = (L, sorted) => sorted === "asc" ? L.sortDesc : sorted === "desc" ? L.sortClear : L.sortAsc;
|
|
36
|
+
/* Tree (`tree` on the table, `children` on a row): the twoLine column indents 24px per level and reserves a 20px arrow slot + 4px,
|
|
37
|
+
so a child's arrow sits exactly under its parent's tile. */
|
|
38
|
+
const TREE_INDENT = 24, TREE_TOGGLE = 20;
|
|
39
|
+
/* Builds the visible rows. index = depth-first position over EVERY row (expanded or not) — for a flat table that is the position in
|
|
40
|
+
`rows`. It is assigned before sorting, so selection, onRowMore and onRowOpen indices never move when the user sorts or expands.
|
|
41
|
+
Sorting reorders siblings only; children stay under their parent. */
|
|
42
|
+
function buildView(rows, { tree, rowKey, expanded, col, dir, manualSort }) {
|
|
43
|
+
const idx = new Map(); let n = 0;
|
|
44
|
+
const number = (list) => list.forEach((r) => { idx.set(r, n++); if (tree && Array.isArray(r.children)) number(r.children); });
|
|
45
|
+
number(rows);
|
|
46
|
+
const cmp = col && !manualSort ? (a, b) => {
|
|
47
|
+
const va = a[col.key], vb = b[col.key], ea = isEmpty(va), eb = isEmpty(vb);
|
|
48
|
+
if (ea || eb) return ea && eb ? idx.get(a) - idx.get(b) : ea ? 1 : -1; // empty values last in both directions
|
|
49
|
+
return (col.compare ? col.compare(va, vb, a, b) : compareValues(va, vb)) * dir || idx.get(a) - idx.get(b);
|
|
50
|
+
} : null;
|
|
51
|
+
const out = [];
|
|
52
|
+
const walk = (list, level) => (cmp ? list.slice().sort(cmp) : list).forEach((r) => {
|
|
53
|
+
const ri = idx.get(r), key = r[rowKey] != null ? r[rowKey] : String(ri);
|
|
54
|
+
const kids = !!tree && ((Array.isArray(r.children) && r.children.length > 0) || !!r.hasChildren);
|
|
55
|
+
const open = kids && expanded.has(key);
|
|
56
|
+
out.push({ r, ri, key, level, kids, open });
|
|
57
|
+
if (open && Array.isArray(r.children)) walk(r.children, level + 1);
|
|
58
|
+
});
|
|
59
|
+
walk(rows, 0);
|
|
60
|
+
return { list: out, total: n };
|
|
61
|
+
}
|
|
62
|
+
const nextSort = (cur, key) => !cur || cur.key !== key ? { key, direction: "asc" } : cur.direction === "asc" ? { key, direction: "desc" } : null;
|
|
63
|
+
const num = (v) => typeof v === "number" ? v : typeof v === "string" && /^\s*-?[\d,]+(\.\d+)?\s*$/.test(v) ? parseFloat(v.replace(/,/g, "")) : NaN;
|
|
64
|
+
function compareValues(a, b) {
|
|
65
|
+
const ea = a == null || a === "" || a === "–", eb = b == null || b === "" || b === "–";
|
|
66
|
+
if (ea || eb) return ea && eb ? 0 : ea ? 1 : -1;
|
|
67
|
+
const na = num(a), nb = num(b);
|
|
68
|
+
if (!isNaN(na) && !isNaN(nb)) return na - nb;
|
|
69
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
70
|
+
}
|
|
71
|
+
function TitleMark({ m }) {
|
|
72
|
+
if (m === "favorite") return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" role="img" aria-label="Favorite" style={{ display: "block", flexShrink: 0, color: "var(--warning)" }}><path d={STAR_FILLED} /></svg>;
|
|
73
|
+
return <span role="img" aria-label={m.label} style={{ display: "inline-flex", flexShrink: 0, color: m.color || "var(--text-muted)" }}><Icon name={m.icon} size={14} /></span>;
|
|
74
|
+
}
|
|
75
|
+
function TreeToggle({ open, label, onClick }) {
|
|
76
|
+
const [hover, setHover] = React.useState(false);
|
|
77
|
+
return (
|
|
78
|
+
<button type="button" aria-expanded={open} aria-label={label} title={label} onClick={(e) => { e.stopPropagation(); onClick(); }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
79
|
+
style={{ width: TREE_TOGGLE, height: TREE_TOGGLE, padding: 0, border: "none", borderRadius: "var(--radius-sm)", display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer", background: hover ? "var(--overlay-hover)" : "transparent", color: hover ? "var(--text-body)" : "var(--text-muted)", transition: "var(--t-hover)" }}>
|
|
80
|
+
<Icon name="chevron-right" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", transform: open ? "rotate(90deg)" : "none", transition: "transform .15s ease" }} />
|
|
81
|
+
</button>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
function TitleCell({ c, item, onRowMore, onRowOpen, labels, tree, onToggleRow }) {
|
|
85
|
+
const { r, ri } = item;
|
|
86
|
+
const o = c.twoLine === true ? {} : c.twoLine;
|
|
87
|
+
const icon = pick(o.icon ?? "icon", r, ri), hue = pick(o.hue ?? "hue", r, ri), desc = pick(o.description ?? "description", r, ri), mark = pick(o.mark ?? "mark", r, ri);
|
|
88
|
+
// onRowOpen: the tile and the title open the row (hand cursor); the title is also reachable by keyboard (Enter / Space)
|
|
89
|
+
const open = onRowOpen ? (e) => onRowOpen(r, ri, e) : null;
|
|
90
|
+
return (
|
|
91
|
+
<div style={{ display: "flex", alignItems: "center", minWidth: c.minWidth ?? 180, ...(tree ? { paddingLeft: item.level * TREE_INDENT } : null) }}>
|
|
92
|
+
{tree ? <span style={{ width: TREE_TOGGLE, flexShrink: 0, marginRight: TREE_INDENT - TREE_TOGGLE, display: "inline-flex" }}>{item.kids ? <TreeToggle open={item.open} label={item.open ? labels.collapse : labels.expand} onClick={() => onToggleRow(item)} /> : null}</span> : null}
|
|
93
|
+
<div style={{ display: "flex", alignItems: "center", gap: "var(--title-cell-tile-gap)", flex: 1, minWidth: 0 }}>
|
|
94
|
+
{icon ? <span onClick={open || undefined} style={{ display: "inline-flex", flexShrink: 0, cursor: open ? "pointer" : undefined }}><IconTile icon={icon} hue={hue || "neutral"} kind="row" style={T_TILE} iconStyle={T_ICON} /></span> : null}
|
|
95
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "var(--title-cell-gap)", flex: 1, minWidth: 0 }}>
|
|
96
|
+
<div style={{ display: "flex", alignItems: "center", gap: 4, minWidth: 0 }}>
|
|
97
|
+
<span style={{ ...T_TITLE, ...(open ? { cursor: "pointer" } : null) }} {...(open ? { role: "button", tabIndex: 0, onClick: open, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(e); } } } : null)}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</span>
|
|
98
|
+
{mark ? <TitleMark m={mark} /> : null}
|
|
99
|
+
</div>
|
|
100
|
+
{desc ? <span style={T_DESC}>{desc}</span> : null}
|
|
101
|
+
</div>
|
|
102
|
+
{o.more !== false ? <IconButton icon="ellipsis" size="sm" muted label={labels.more} title={labels.more} onClick={(e) => { if (e && e.stopPropagation) e.stopPropagation(); if (onRowMore) onRowMore(r, ri, e); }} /> : null}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
3
107
|
|
|
4
108
|
/**
|
|
5
|
-
* Keybricks table. columns: [{key,label,width,minWidth,mono,align,render}].
|
|
109
|
+
* Keybricks table. columns: [{key,label,width,minWidth,mono,code,twoLine,sortable,compare,align,render}].
|
|
110
|
+
* sortable — hover the header for a muted sm sort button at the cell's right end (in line with a two-line row's ⋯; arrow-up); click: ascending → descending → none.
|
|
111
|
+
* While sorted the button stays and the label turns --text-heading. One column at a time. sort / onSortChange control it
|
|
112
|
+
* (defaultSort to seed it uncontrolled); compare(a, b, rowA, rowB) overrides the default comparison; manualSort keeps rows as given.
|
|
113
|
+
* twoLine — the name column of a document list: icon tile, title over a description, optional mark ("favorite" = filled star), trailing ⋯
|
|
114
|
+
* (onRowMore(row, index, event)); onRowOpen(row, index, event) makes the tile and the title open the row (hand cursor, Enter / Space on the title).
|
|
115
|
+
* tree — rows nest through `children` (or `hasChildren` for rows you load on demand); the twoLine column indents 24px per level with an
|
|
116
|
+
* expand arrow; expandedKeys / defaultExpandedKeys / onExpandChange(row, expanded, keys); rowKey names the key field (default "id").
|
|
117
|
+
* labels — { sortAsc, sortDesc, sortClear, more, expand, collapse } for every word the table draws (English by default).
|
|
118
|
+
* Give it a width; it shrinks to minWidth (default 180) before the other columns give way, and title and description truncate to whatever it gets. Row grows to 58px in md (50 sm · 68 lg).
|
|
6
119
|
* The table is a size region: `size` ("sm" | "md" | "lg", default inherit → md) sets its rows, header, cell padding and every
|
|
7
|
-
* control inside the cells from the tier tokens — rows are the tier's bar height (sm
|
|
120
|
+
* control inside the cells from the tier tokens — rows are the tier's bar height (sm 36 · md 40 · lg 48), cells read the tier's
|
|
8
121
|
* control text, header cells are the same size in weight 600 / text-secondary, sentence case, no letter-spacing.
|
|
9
122
|
* `dense` is kept as an alias of size="sm" (the data-preview drawer).
|
|
10
123
|
* 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.
|
|
@@ -13,10 +126,26 @@ import { Checkbox } from "../forms/Checkbox.jsx";
|
|
|
13
126
|
* resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
|
|
14
127
|
* Widths live in local state unless you pass columnWidths/onColumnResize to control them. Min width = column.minWidth ?? 48.
|
|
15
128
|
*/
|
|
16
|
-
export function Table({ columns = [], rows = [], size, dense, selectable, selected = [], onToggle, headerBorder = true, bordered, framed, resizable, columnWidths, onColumnResize, empty, style }) {
|
|
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 }) {
|
|
130
|
+
const L = { ...DEFAULT_LABELS, ...labels };
|
|
131
|
+
const [localExp, setLocalExp] = React.useState(defaultExpandedKeys || []);
|
|
132
|
+
const expKeys = expandedKeys !== undefined ? expandedKeys : localExp;
|
|
133
|
+
const [localSort, setLocalSort] = React.useState(defaultSort);
|
|
134
|
+
const sortState = sort !== undefined ? sort : localSort;
|
|
135
|
+
const [hotTh, setHotTh] = React.useState(null); // header under the pointer or holding focus
|
|
136
|
+
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,
|
|
139
|
+
col: sortState && columns.find((x) => x.key === sortState.key), dir: sortState && sortState.direction === "desc" ? -1 : 1,
|
|
140
|
+
}), [rows, columns, sortState, manualSort, tree, rowKey, expKeys]);
|
|
141
|
+
const toggleRow = (item) => {
|
|
142
|
+
const next = item.open ? expKeys.filter((k) => k !== item.key) : [...expKeys, item.key];
|
|
143
|
+
if (expandedKeys === undefined) setLocalExp(next);
|
|
144
|
+
if (onExpandChange) onExpandChange(item.r, !item.open, next);
|
|
145
|
+
};
|
|
17
146
|
const tier = size || (dense ? "sm" : undefined);
|
|
18
147
|
const flushTop = bordered || framed;
|
|
19
|
-
const colCount = columns.length
|
|
148
|
+
const colCount = columns.length || 1;
|
|
20
149
|
const [localW, setLocalW] = React.useState({});
|
|
21
150
|
const [drag, setDrag] = React.useState(null); // key being dragged
|
|
22
151
|
const [hot, setHot] = React.useState(null); // key whose handle is hovered
|
|
@@ -39,27 +168,37 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
|
|
|
39
168
|
window.addEventListener("mousemove", move); window.addEventListener("mouseup", up);
|
|
40
169
|
};
|
|
41
170
|
|
|
171
|
+
// `mono` columns (codes, IDs, amounts) use the UI font with tabular figures; the monospace font is for code only
|
|
42
172
|
const cellFont = (c) => c.mono
|
|
43
|
-
? { fontFamily: "var(--font-
|
|
173
|
+
? { fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", fontVariantNumeric: "tabular-nums", color: "var(--text-primary)" }
|
|
44
174
|
: { fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-primary)" };
|
|
45
175
|
// first and last cells keep the card's edge padding; every other cell the tier's cell padding
|
|
46
|
-
const cellPad = (i) => ({ paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0
|
|
176
|
+
const cellPad = (i) => ({ ...(i === 0 && selectable ? { position: "relative" } : null), paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0 ? (selectable ? CHECK_PAD : "var(--edge-pad)") : "var(--cell-px)", paddingRight: i === columns.length - 1 ? "var(--edge-pad)" : "var(--cell-px)" });
|
|
47
177
|
|
|
48
178
|
const table = (
|
|
49
179
|
<table data-size={tier} style={{ width: "100%", borderCollapse: "collapse", tableLayout: resizable ? "fixed" : "auto", ...(bordered ? null : style) }}>
|
|
50
180
|
<thead>
|
|
51
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)" }}>
|
|
52
|
-
{selectable ? <th style={{ width: "calc(var(--edge-pad) + var(--check-size) + var(--cell-px))", padding: "0 0 0 var(--edge-pad)", textAlign: "left" }}><Checkbox checked={rows.length > 0 && selected.length === rows.length} indeterminate={selected.length > 0 && selected.length < rows.length} onChange={(v) => onToggle && onToggle("all", v)} /></th> : null}
|
|
53
182
|
{columns.map((c, i) => {
|
|
54
183
|
const last = i === columns.length - 1;
|
|
55
184
|
const active = drag === c.key, lit = active || hot === c.key;
|
|
185
|
+
const sorted = sortState && sortState.key === c.key ? sortState.direction : null;
|
|
186
|
+
const showSort = c.sortable && (sorted || hotTh === c.key);
|
|
56
187
|
return (
|
|
57
|
-
<th key={c.key} ref={(el) => { thRefs.current[c.key] = el; }}
|
|
58
|
-
{c.
|
|
188
|
+
<th key={c.key} ref={(el) => { thRefs.current[c.key] = el; }} aria-sort={c.sortable ? (sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none") : undefined}
|
|
189
|
+
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
|
+
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}
|
|
192
|
+
{c.sortable ? (
|
|
193
|
+
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
|
194
|
+
<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>
|
|
195
|
+
<IconButton icon={sorted === "desc" ? "arrow-down" : "arrow-up"} size="sm" muted label={sortLabel(L, sorted)} title={sortLabel(L, sorted)} onClick={() => cycleSort(c.key)} style={{ opacity: showSort ? 1 : 0, ...(c.align === "right" ? null : { marginLeft: "auto" }) }} />
|
|
196
|
+
</span>
|
|
197
|
+
) : c.label}
|
|
59
198
|
{resizable && !last ? (
|
|
60
199
|
<span aria-hidden="true" onMouseDown={(e) => startDrag(e, c)} onMouseEnter={() => setHot(c.key)} onMouseLeave={() => setHot(null)}
|
|
61
200
|
style={{ position: "absolute", top: 0, right: -5, width: 11, height: "100%", cursor: "col-resize", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1 }}>
|
|
62
|
-
<span style={{ width: active ? 2 : 1, height: "50%", background: lit ? "var(--action-primary)" : "var(--border-
|
|
201
|
+
<span style={{ width: active ? 2 : 1, height: "50%", background: lit ? "var(--action-primary)" : "var(--border-split)", borderRadius: 1, transition: "var(--t-hover)" }}></span>
|
|
63
202
|
</span>
|
|
64
203
|
) : null}
|
|
65
204
|
</th>
|
|
@@ -68,14 +207,13 @@ export function Table({ columns = [], rows = [], size, dense, selectable, select
|
|
|
68
207
|
</tr>
|
|
69
208
|
</thead>
|
|
70
209
|
<tbody>
|
|
71
|
-
{
|
|
72
|
-
<tr key={
|
|
73
|
-
{selectable ? <td style={{ padding: "0 0 0 var(--edge-pad)" }}><Checkbox checked={selected.includes(ri)} onChange={(v) => onToggle && onToggle(ri, v)} /></td> : null}
|
|
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)" }}>
|
|
74
212
|
{columns.map((c, i) => (
|
|
75
|
-
<td key={c.key} style={{ ...cellPad(i), textAlign: c.align || "left", whiteSpace: "nowrap", ...(resizable ? { overflow: "hidden", textOverflow: "ellipsis" } : null), ...cellFont(c) }}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>
|
|
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>
|
|
76
214
|
))}
|
|
77
215
|
</tr>
|
|
78
|
-
))}
|
|
216
|
+
); })}
|
|
79
217
|
{rows.length === 0 && empty ? <tr><td colSpan={colCount} style={{ padding: 0 }}>{empty}</td></tr> : null}
|
|
80
218
|
</tbody>
|
|
81
219
|
</table>
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { px } from '../px.js'
|
|
3
|
-
import { ref, computed, reactive } from 'vue'
|
|
3
|
+
import { ref, computed, reactive, getCurrentInstance } from 'vue'
|
|
4
4
|
import Checkbox from '../forms/Checkbox.vue'
|
|
5
|
-
|
|
5
|
+
import IconTile from '../display/IconTile.vue'
|
|
6
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
7
|
+
import Icon from '../icons/Icon.vue'
|
|
8
|
+
// Keybricks table. columns: [{key,label,width,minWidth,mono,code,twoLine,align,render}].
|
|
9
|
+
// twoLine — the name column of a document list: icon tile, title over a description, optional mark ("favorite" = filled star), trailing ⋯
|
|
10
|
+
// (@row-more(row, index, event)); @row-open(row, index, event) makes the tile and the title open the row (hand cursor, Enter / Space on the title).
|
|
11
|
+
// tree — rows nest through `children` (or `hasChildren` for rows you load on demand); the twoLine column indents 24px per level with an
|
|
12
|
+
// expand arrow; v-model:expandedKeys / defaultExpandedKeys / @expand-change(row, expanded, keys); rowKey names the key field (default "id").
|
|
13
|
+
// labels — { sortAsc, sortDesc, sortClear, more, expand, collapse } for every word the table draws (English by default).
|
|
14
|
+
// Give it a width; it shrinks to minWidth (default 180) before the other columns give way, and title and description truncate to whatever it gets. Row grows to 58px in md (50 sm · 68 lg).
|
|
15
|
+
// sortable — hover the header for a muted sm sort button at the cell's right end (in line with a two-line row's ⋯; arrow-up); click: ascending → descending → none. While sorted the
|
|
16
|
+
// button stays and the label turns --text-heading. One column at a time. v-model:sort / @sort-change (defaultSort seeds it
|
|
17
|
+
// uncontrolled); compare(a, b, rowA, rowB) overrides the default comparison; manualSort keeps rows as given (server-side).
|
|
6
18
|
// The table is a size region: `size` ("sm" | "md" | "lg", default inherit → md) sets its rows, header, cell padding and every control
|
|
7
|
-
// inside the cells from the tier tokens — rows are the tier's bar height (sm
|
|
19
|
+
// inside the cells from the tier tokens — rows are the tier's bar height (sm 36 · md 40 · lg 48), cells read the tier's control text,
|
|
8
20
|
// header cells are the same size in weight 600 / text-secondary, sentence case, no letter-spacing. `dense` is an alias of size="sm".
|
|
9
21
|
// 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.
|
|
10
22
|
// bordered — the table draws its own rounded card frame (border-card, radius-lg) and drops the edge rules that would double it.
|
|
@@ -12,11 +24,85 @@ import Checkbox from '../forms/Checkbox.vue'
|
|
|
12
24
|
// resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
|
|
13
25
|
// Widths live in local state unless you pass columnWidths + listen to @column-resize(key, px). Min width = column.minWidth ?? 48.
|
|
14
26
|
// #empty slot renders inside the table when rows is empty.
|
|
15
|
-
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, style: Object })
|
|
16
|
-
const emit = defineEmits(['toggle', 'columnResize'])
|
|
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 })
|
|
28
|
+
const emit = defineEmits(['toggle', 'columnResize', 'rowMore', 'rowOpen', 'sortChange', 'update:sort', 'expandChange', 'update:expandedKeys'])
|
|
29
|
+
// 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' }
|
|
31
|
+
const L = computed(() => ({ ...DEFAULT_LABELS, ...(props.labels || {}) }))
|
|
32
|
+
const sortLabel = (sorted) => sorted === 'asc' ? L.value.sortDesc : sorted === 'desc' ? L.value.sortClear : L.value.sortAsc
|
|
33
|
+
// the tile and title open the row only when a @row-open listener is attached
|
|
34
|
+
const inst = getCurrentInstance()
|
|
35
|
+
const canOpen = computed(() => !!(inst && inst.vnode.props && (inst.vnode.props.onRowOpen || inst.vnode.props['onRow-open'])))
|
|
36
|
+
function openRow(r, ri, e) { emit('rowOpen', r, ri, e) }
|
|
37
|
+
function openKey(r, ri, e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openRow(r, ri, e) } }
|
|
38
|
+
// Tree: 24px per level; a 20px arrow slot + 4px, so a child's arrow sits under its parent's tile.
|
|
39
|
+
const TREE_INDENT = 24, TREE_TOGGLE = 20
|
|
40
|
+
const localExp = ref([...props.defaultExpandedKeys])
|
|
41
|
+
const expKeys = computed(() => props.expandedKeys !== undefined ? props.expandedKeys : localExp.value)
|
|
42
|
+
const hotToggle = ref(null)
|
|
43
|
+
function toggleRow(item) {
|
|
44
|
+
const next = item.open ? expKeys.value.filter(k => k !== item.key) : [...expKeys.value, item.key]
|
|
45
|
+
if (props.expandedKeys === undefined) localExp.value = next
|
|
46
|
+
emit('update:expandedKeys', next); emit('expandChange', item.r, !item.open, next)
|
|
47
|
+
}
|
|
48
|
+
const treeCol = computed(() => props.tree ? props.columns.find(x => x.twoLine) : null)
|
|
49
|
+
const localSort = ref(props.defaultSort)
|
|
50
|
+
const sortState = computed(() => props.sort !== undefined ? props.sort : localSort.value)
|
|
51
|
+
const hotTh = ref(null)
|
|
52
|
+
const nextSort = (cur, key) => !cur || cur.key !== key ? { key, direction: 'asc' } : cur.direction === 'asc' ? { key, direction: 'desc' } : null
|
|
53
|
+
function cycleSort(key) { const n = nextSort(sortState.value, key); if (props.sort === undefined) localSort.value = n; emit('update:sort', n); emit('sortChange', n) }
|
|
54
|
+
const num = (v) => typeof v === 'number' ? v : typeof v === 'string' && /^\s*-?[\d,]+(\.\d+)?\s*$/.test(v) ? parseFloat(v.replace(/,/g, '')) : NaN
|
|
55
|
+
function compareValues(a, b) {
|
|
56
|
+
const ea = a == null || a === '' || a === '–', eb = b == null || b === '' || b === '–'
|
|
57
|
+
if (ea || eb) return ea && eb ? 0 : ea ? 1 : -1
|
|
58
|
+
const na = num(a), nb = num(b)
|
|
59
|
+
if (!isNaN(na) && !isNaN(nb)) return na - nb
|
|
60
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' })
|
|
61
|
+
}
|
|
62
|
+
// Visible rows. index = depth-first position over EVERY row (expanded or not) — for a flat table the position in `rows` — assigned
|
|
63
|
+
// before sorting, so selection and event indices never move. Sorting reorders siblings only; children stay under their parent.
|
|
64
|
+
const built = computed(() => {
|
|
65
|
+
const idx = new Map(); let n = 0
|
|
66
|
+
const number = (list) => list.forEach(r => { idx.set(r, n++); if (props.tree && Array.isArray(r.children)) number(r.children) })
|
|
67
|
+
number(props.rows)
|
|
68
|
+
const s = sortState.value, col = s && props.columns.find(x => x.key === s.key), dir = s && s.direction === 'desc' ? -1 : 1
|
|
69
|
+
const isEmpty = (x) => x == null || x === '' || x === '–'
|
|
70
|
+
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
|
|
71
|
+
const exp = new Set(expKeys.value), out = []
|
|
72
|
+
const walk = (list, level) => (cmp ? list.slice().sort(cmp) : list).forEach(r => {
|
|
73
|
+
const ri = idx.get(r), key = r[props.rowKey] != null ? r[props.rowKey] : String(ri)
|
|
74
|
+
const kids = props.tree && ((Array.isArray(r.children) && r.children.length > 0) || !!r.hasChildren)
|
|
75
|
+
const open = kids && exp.has(key)
|
|
76
|
+
out.push({ r, ri, key, level, kids, open })
|
|
77
|
+
if (open && Array.isArray(r.children)) walk(r.children, level + 1)
|
|
78
|
+
})
|
|
79
|
+
walk(props.rows, 0)
|
|
80
|
+
return { list: out, total: n }
|
|
81
|
+
})
|
|
82
|
+
const view = computed(() => built.value.list)
|
|
83
|
+
const total = computed(() => built.value.total)
|
|
84
|
+
const sortedDir = (c) => sortState.value && sortState.value.key === c.key ? sortState.value.direction : null
|
|
85
|
+
// 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
|
+
const T_TILE = { width: 'var(--title-cell-tile)', height: 'var(--title-cell-tile)', borderRadius: 'var(--title-cell-tile-r)' }
|
|
87
|
+
const T_ICON = { width: 'var(--title-cell-icon)', height: 'var(--title-cell-icon)' }
|
|
88
|
+
const T_ONE = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }
|
|
89
|
+
const T_TITLE = { ...T_ONE, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-heading)' }
|
|
90
|
+
const T_DESC = { ...T_ONE, fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', color: 'var(--text-secondary)' }
|
|
91
|
+
const STAR_FILLED = 'M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z'
|
|
92
|
+
const pick = (a, r, ri) => typeof a === 'function' ? a(r, ri) : a != null ? r[a] : undefined
|
|
93
|
+
function tl(c, r, ri) {
|
|
94
|
+
const o = c.twoLine === true ? {} : c.twoLine
|
|
95
|
+
return { icon: pick(o.icon ?? 'icon', r, ri), hue: pick(o.hue ?? 'hue', r, ri), desc: pick(o.description ?? 'description', r, ri), mark: pick(o.mark ?? 'mark', r, ri), more: o.more !== false }
|
|
96
|
+
}
|
|
97
|
+
function more(r, ri, e) { if (e && e.stopPropagation) e.stopPropagation(); emit('rowMore', r, ri, e) }
|
|
17
98
|
const tier = computed(() => props.size || (props.dense ? 'sm' : undefined))
|
|
18
99
|
const flushTop = computed(() => props.bordered || props.framed)
|
|
19
|
-
const colCount = computed(() => props.columns.length
|
|
100
|
+
const colCount = computed(() => props.columns.length || 1)
|
|
101
|
+
// 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
|
+
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' }
|
|
105
|
+
const checkSlot = { position: 'absolute', left: 'var(--edge-pad)', top: 0, bottom: 0, display: 'flex', alignItems: 'center' }
|
|
20
106
|
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' })
|
|
21
107
|
const localW = reactive({})
|
|
22
108
|
const drag = ref(null), hot = ref(null)
|
|
@@ -32,29 +118,33 @@ function startDrag(e, c) {
|
|
|
32
118
|
document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'
|
|
33
119
|
window.addEventListener('mousemove', move); window.addEventListener('mouseup', up)
|
|
34
120
|
}
|
|
35
|
-
function cellFont(c) { return c.mono ? { fontFamily: 'var(--font-
|
|
121
|
+
function cellFont(c) { return c.mono ? { fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontVariantNumeric: 'tabular-nums', color: 'var(--text-primary)' } : { fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)' } }
|
|
36
122
|
// first and last cells keep the card's edge padding; every other cell the tier's cell padding
|
|
37
|
-
function cellPad(i) { return { paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0
|
|
123
|
+
function cellPad(i) { return { ...(i === 0 && props.selectable ? { position: 'relative' } : null), paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0 ? (props.selectable ? CHECK_PAD : 'var(--edge-pad)') : 'var(--cell-px)', paddingRight: i === props.columns.length - 1 ? 'var(--edge-pad)' : 'var(--cell-px)' } }
|
|
38
124
|
</script>
|
|
39
125
|
<template>
|
|
40
126
|
<div :style="frame" :data-size="bordered ? tier : undefined">
|
|
41
127
|
<table :data-size="tier" :style="{ width: '100%', borderCollapse: 'collapse', tableLayout: resizable ? 'fixed' : 'auto', ...(bordered ? null : style) }">
|
|
42
128
|
<thead>
|
|
43
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)' }">
|
|
44
|
-
<th v-
|
|
45
|
-
|
|
46
|
-
{
|
|
130
|
+
<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
|
+
@mouseenter="c.sortable && (hotTh = c.key)" @mouseleave="c.sortable && (hotTh = null)" @focusin="c.sortable && (hotTh = c.key)" @focusout="c.sortable && (hotTh = null)"
|
|
132
|
+
: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>
|
|
134
|
+
<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
|
+
<template v-else>{{ c.label }}</template>
|
|
47
136
|
<span v-if="resizable && i < columns.length - 1" aria-hidden="true" @mousedown="e => startDrag(e, c)" @mouseenter="hot = c.key" @mouseleave="hot = null"
|
|
48
137
|
style="position:absolute;top:0;right:-5px;width:11px;height:100%;cursor:col-resize;display:flex;align-items:center;justify-content:center;z-index:1">
|
|
49
|
-
<span :style="{ width: drag === c.key ? '2px' : '1px', height: '50%', background: (drag === c.key || hot === c.key) ? 'var(--action-primary)' : 'var(--border-
|
|
138
|
+
<span :style="{ width: drag === c.key ? '2px' : '1px', height: '50%', background: (drag === c.key || hot === c.key) ? 'var(--action-primary)' : 'var(--border-split)', borderRadius: '1px', transition: 'var(--t-hover)' }"></span>
|
|
50
139
|
</span>
|
|
51
140
|
</th>
|
|
52
141
|
</tr>
|
|
53
142
|
</thead>
|
|
54
143
|
<tbody>
|
|
55
|
-
<tr v-for="(
|
|
56
|
-
<
|
|
57
|
-
<td v-for="(c, i) in columns" :key="c.key" :style="{ ...cellPad(i), textAlign: c.align || 'left', whiteSpace: 'nowrap', ...(resizable ? { overflow: 'hidden', textOverflow: 'ellipsis' } : null), ...cellFont(c) }">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</td>
|
|
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)' }">
|
|
145
|
+
<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>
|
|
147
|
+
</template>
|
|
58
148
|
</tr>
|
|
59
149
|
<tr v-if="rows.length === 0 && $slots.empty"><td :colspan="colCount" style="padding:0"><slot name="empty" /></td></tr>
|
|
60
150
|
</tbody>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Data lists: `Table` (hairline table, mono cells, selectable; a size region — `size="sm"` for preview grids) and `ListRow` (expandable workflow rows in a `Card pad={false}`).
|
|
1
|
+
Data lists: `Table` (hairline table, mono cells, selectable; a size region — `size="sm"` for preview grids; `twoLine` name column with `onRowOpen`, `sortable` headers, `tree` rows with expand arrows, `labels` to translate its tooltips) and `ListRow` (expandable workflow rows in a `Card pad={false}`).
|
|
2
2
|
|
|
3
3
|
```jsx
|
|
4
4
|
<Table selectable columns={[
|
|
@@ -13,7 +13,7 @@ export function Avatar({ name = "", src, size = 28, style }) {
|
|
|
13
13
|
const hue = palette[[...name].reduce((a, c) => a + c.charCodeAt(0), 0) % palette.length];
|
|
14
14
|
if (src) return <img src={src} alt={name} style={{ width: size, height: size, borderRadius: "var(--radius-full)", objectFit: "cover", flexShrink: 0, ...style }} />;
|
|
15
15
|
return (
|
|
16
|
-
<span title={name} style={{ width: size, height: size, borderRadius: "var(--radius-full)", background: "var(--bg-hover)", color: hue, display: "inline-flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize:
|
|
16
|
+
<span title={name} style={{ width: size, height: size, borderRadius: "var(--radius-full)", background: "var(--bg-hover)", color: hue, display: "inline-flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: `max(${Math.round(size * .4)}px, var(--text-glyph-min))`, letterSpacing: 0, flexShrink: 0, boxSizing: "border-box", border: "1px solid var(--border-card)", ...style }}>
|
|
17
17
|
{initials(name)}
|
|
18
18
|
</span>
|
|
19
19
|
);
|
|
@@ -25,7 +25,7 @@ export function AvatarGroup({ names = [], size = 24, max = 4, style }) {
|
|
|
25
25
|
return (
|
|
26
26
|
<div style={{ display: "flex", alignItems: "center", ...style }}>
|
|
27
27
|
{shown.map((n, i) => <Avatar key={n + i} name={n} size={size} style={{ marginLeft: i ? -8 : 0, boxShadow: "var(--ring-surface)" }} />)}
|
|
28
|
-
{rest > 0 ? <span style={{ marginLeft: -8, width: size, height: size, borderRadius: "var(--radius-full)", background: "var(--bg-active)", color: "var(--text-secondary)", boxShadow: "var(--ring-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontSize:
|
|
28
|
+
{rest > 0 ? <span style={{ marginLeft: -8, width: size, height: size, borderRadius: "var(--radius-full)", background: "var(--bg-active)", color: "var(--text-secondary)", boxShadow: "var(--ring-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontSize: `max(${Math.round(size * .38)}px, var(--text-glyph-min))` }}>+{rest}</span> : null}
|
|
29
29
|
</div>
|
|
30
30
|
);
|
|
31
31
|
}
|
|
@@ -14,5 +14,5 @@ const hue = computed(() => palette[[...props.name].reduce((a, c) => a + c.charCo
|
|
|
14
14
|
</script>
|
|
15
15
|
<template>
|
|
16
16
|
<img v-if="src" :src="src" :alt="name" :style="{ width: px(size), height: px(size), borderRadius: 'var(--radius-full)', objectFit: 'cover', flexShrink: 0, ...style }" />
|
|
17
|
-
<span v-else :title="name" :style="{ width: px(size), height: px(size), borderRadius: 'var(--radius-full)', background: 'var(--bg-hover)', color: hue, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize:
|
|
17
|
+
<span v-else :title="name" :style="{ width: px(size), height: px(size), borderRadius: 'var(--radius-full)', background: 'var(--bg-hover)', color: hue, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: `max(${Math.round(size * .4)}px, var(--text-glyph-min))`, letterSpacing: 0, flexShrink: 0, boxSizing: 'border-box', border: '1px solid var(--border-card)', ...style }">{{ initials(name) }}</span>
|
|
18
18
|
</template>
|
|
@@ -9,6 +9,6 @@ const rest = computed(() => props.names.length - shown.value.length)
|
|
|
9
9
|
<template>
|
|
10
10
|
<div :style="{ display: 'flex', alignItems: 'center', ...style }">
|
|
11
11
|
<Avatar v-for="(n, i) in shown" :key="n + i" :name="n" :size="size" :style="{ marginLeft: i ? '-8px' : 0, boxShadow: 'var(--ring-surface)' }" />
|
|
12
|
-
<span v-if="rest > 0" :style="{ marginLeft: '-8px', width: px(size), height: px(size), borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize:
|
|
12
|
+
<span v-if="rest > 0" :style="{ marginLeft: '-8px', width: px(size), height: px(size), borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize: `max(${Math.round(size * .38)}px, var(--text-glyph-min))` }">+{{ rest }}</span>
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
@@ -20,8 +20,11 @@ export declare function Badge(props: BadgeProps): JSX.Element;
|
|
|
20
20
|
/** Tinted category icon tile. hue: source|join|transform|merge|output; kind: palette 40/r14 · node 48/r16 · row 32/r8. */
|
|
21
21
|
export interface IconTileProps {
|
|
22
22
|
icon: string;
|
|
23
|
-
hue
|
|
23
|
+
/** Category hue. On kind="row" the tile is a hairline square unless hue is set. "neutral" = grey tile (folders, containers). */
|
|
24
|
+
hue?: "source" | "join" | "transform" | "merge" | "output" | "neutral";
|
|
24
25
|
kind?: "palette" | "node" | "row";
|
|
25
26
|
style?: React.CSSProperties;
|
|
27
|
+
/** Forwarded to the inner Icon (e.g. width/height from a token). */
|
|
28
|
+
iconStyle?: React.CSSProperties;
|
|
26
29
|
}
|
|
27
30
|
export declare function IconTile(props: IconTileProps): JSX.Element;
|
|
@@ -27,14 +27,19 @@ export function Badge({ tone = "neutral", children, style }) {
|
|
|
27
27
|
return <span style={{ display: "inline-flex", alignItems: "center", height: 20, padding: "0 8px", borderRadius: "var(--radius-full)", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-xs)", lineHeight: 1, letterSpacing: "var(--tracking-xs)", ...tones[tone], ...style }}>{children}</span>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** Category icon tile — the signature tinted square. kind: palette 40/r14 · node 48/r16 · row 32/r8
|
|
31
|
-
|
|
30
|
+
/** Category icon tile — the signature tinted square. kind: palette 40/r14 · node 48/r16 · row 32/r8.
|
|
31
|
+
* A row tile is a hairline square unless you pass `hue`, then it is tinted like the others. hue "neutral" is the grey tile (folders, containers).
|
|
32
|
+
* iconStyle is forwarded to the Icon (Table's two-line cell sizes it from the tier tokens). */
|
|
33
|
+
export function IconTile({ icon, hue, kind = "palette", style, iconStyle }) {
|
|
32
34
|
const dims = { palette: { box: 40, r: "var(--radius-tile)", icon: 18 }, node: { box: 48, r: "var(--radius-node)", icon: 22 }, row: { box: 32, r: "var(--radius-md)", icon: 16 } };
|
|
33
35
|
const t = dims[kind];
|
|
34
|
-
const
|
|
36
|
+
const h = hue || "source";
|
|
37
|
+
const tinted = kind !== "row" || !!hue;
|
|
38
|
+
const bg = !tinted ? "var(--bg-surface)" : h === "neutral" ? "var(--bg-hover)" : `var(--cat-${h}-bg)`;
|
|
39
|
+
const fg = !tinted ? "var(--text-body)" : h === "neutral" ? "var(--text-secondary)" : `var(--cat-${h})`;
|
|
35
40
|
return (
|
|
36
|
-
<span style={{ width: t.box, height: t.box, borderRadius: t.r, background:
|
|
37
|
-
<Icon name={icon} size={t.icon} strokeWidth={2} />
|
|
41
|
+
<span style={{ width: t.box, height: t.box, borderRadius: t.r, background: bg, border: tinted ? "none" : "1px solid var(--border-control)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: fg, flexShrink: 0, boxSizing: "border-box", ...style }}>
|
|
42
|
+
<Icon name={icon} size={t.icon} strokeWidth={2} style={iconStyle} />
|
|
38
43
|
</span>
|
|
39
44
|
);
|
|
40
45
|
}
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
import { px } from '../px.js'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import Icon from '../icons/Icon.vue'
|
|
5
|
+
// Category icon tile. kind: palette 40/r14 · node 48/r16 · row 32/r8. A row tile is a hairline square unless `hue` is set, then it is tinted.
|
|
6
|
+
// hue "neutral" is the grey tile (folders, containers). iconStyle is forwarded to the Icon (Table's two-line cell sizes it from the tier tokens).
|
|
5
7
|
const dims = { palette: { box: 40, r: 'var(--radius-tile)', icon: 18 }, node: { box: 48, r: 'var(--radius-node)', icon: 22 }, row: { box: 32, r: 'var(--radius-md)', icon: 16 } }
|
|
6
|
-
const props = defineProps({ icon: String, hue:
|
|
8
|
+
const props = defineProps({ icon: String, hue: String, kind: { type: String, default: 'palette' }, style: Object, iconStyle: Object })
|
|
7
9
|
const t = computed(() => dims[props.kind])
|
|
8
|
-
const
|
|
10
|
+
const h = computed(() => props.hue || 'source')
|
|
11
|
+
const tinted = computed(() => props.kind !== 'row' || !!props.hue)
|
|
12
|
+
const bg = computed(() => !tinted.value ? 'var(--bg-surface)' : h.value === 'neutral' ? 'var(--bg-hover)' : `var(--cat-${h.value}-bg)`)
|
|
13
|
+
const fg = computed(() => !tinted.value ? 'var(--text-body)' : h.value === 'neutral' ? 'var(--text-secondary)' : `var(--cat-${h.value})`)
|
|
9
14
|
</script>
|
|
10
15
|
<template>
|
|
11
|
-
<span :style="{ width: px(t.box), height: px(t.box), borderRadius: px(t.r), background:
|
|
12
|
-
<Icon :name="icon" :size="t.icon" :stroke-width="2" />
|
|
16
|
+
<span :style="{ width: px(t.box), height: px(t.box), borderRadius: px(t.r), background: bg, border: tinted ? 'none' : '1px solid var(--border-control)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: fg, flexShrink: 0, boxSizing: 'border-box', ...style }">
|
|
17
|
+
<Icon :name="icon" :size="t.icon" :stroke-width="2" :style="iconStyle" />
|
|
13
18
|
</span>
|
|
14
19
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** Keyboard shortcut keycap row (20px
|
|
2
|
+
/** Keyboard shortcut keycap row (20px). Addition — for a power-user data tool's shortcut hints. */
|
|
3
3
|
export interface KbdProps {
|
|
4
4
|
keys?: string[];
|
|
5
5
|
style?: React.CSSProperties;
|
|
@@ -5,7 +5,7 @@ export function Kbd({ keys = [], style }) {
|
|
|
5
5
|
return (
|
|
6
6
|
<span style={{ display: "inline-flex", gap: 3, ...style }}>
|
|
7
7
|
{keys.map((k, i) => (
|
|
8
|
-
<span key={i} style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 20, height: 20, padding: "0 5px", borderRadius: "var(--radius-sm)", background: "var(--bg-inset)", border: "1px solid var(--border-control)", boxSizing: "border-box", fontFamily: "var(--font-
|
|
8
|
+
<span key={i} style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 20, height: 20, padding: "0 5px", borderRadius: "var(--radius-sm)", background: "var(--bg-inset)", border: "1px solid var(--border-control)", boxSizing: "border-box", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{k}</span>
|
|
9
9
|
))}
|
|
10
10
|
</span>
|
|
11
11
|
);
|
|
@@ -4,6 +4,6 @@ defineProps({ keys: { type: Array, default: () => [] }, style: Object })
|
|
|
4
4
|
</script>
|
|
5
5
|
<template>
|
|
6
6
|
<span :style="{ display: 'inline-flex', gap: '3px', ...style }">
|
|
7
|
-
<span v-for="(k, i) in keys" :key="i" style="display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border-radius:var(--radius-sm);background:var(--bg-inset);border:1px solid var(--border-control);box-sizing:border-box;font-family:var(--font-
|
|
7
|
+
<span v-for="(k, i) in keys" :key="i" style="display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border-radius:var(--radius-sm);background:var(--bg-inset);border:1px solid var(--border-control);box-sizing:border-box;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-secondary)">{{ k }}</span>
|
|
8
8
|
</span>
|
|
9
9
|
</template>
|
|
@@ -33,7 +33,7 @@ export function DotActivity({ tone = "success", text, time, style }) {
|
|
|
33
33
|
export function LogViewer({ lines = [], height, style }) {
|
|
34
34
|
return (
|
|
35
35
|
<div style={{ background: "var(--bg-inset)", borderRadius: "var(--radius-md)", border: "1px solid var(--border-subtle)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 6, overflowY: "auto", height, boxSizing: "border-box", ...style }}>
|
|
36
|
-
{lines.map((l, i) => <span key={i} style={{ fontFamily: "var(--font-
|
|
36
|
+
{lines.map((l, i) => <span key={i} style={{ fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: typeof l === "object" ? l.color || "var(--text-body)" : "var(--text-body)", whiteSpace: "pre" }}>{typeof l === "object" ? l.text : l}</span>)}
|
|
37
37
|
</div>
|
|
38
38
|
);
|
|
39
39
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** Floating context menu — r10, soft shadow, control-height rows at the region's tier (32px / 14px at md), red danger row.
|
|
2
|
+
/** Floating context menu — r10, soft shadow, control-height rows at the region's tier (32px / 14px at md), red danger row.
|
|
3
|
+
* Action menus keep rows flush; a single-choice menu (any item `selected`) spaces rows 2px apart. Dividers keep 4px above and below. */
|
|
3
4
|
export interface ContextMenuProps {
|
|
4
|
-
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean }[];
|
|
5
|
+
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
5
6
|
onSelect?: (label: string) => void;
|
|
6
7
|
/** Size tier of the rows. Omit to inherit the tier of the control that opened the menu (the default). */
|
|
7
8
|
size?: "sm" | "md" | "lg";
|