@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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// App chrome tones — framework-neutral, shared by Sidebar and SidePanel in React and Vue.
|
|
2
|
+
// recessed (default): grey rail, the active item is a white tile with a hairline ring.
|
|
3
|
+
// flush: white rail, neutral selected fill. contrast: the rail is scoped to the dark theme.
|
|
4
|
+
// custom: dark scope on a solid colour (SIDEBAR_COLORS or any hex); washes are white at 10 / 16 %.
|
|
5
|
+
// chromeTone() returns the data-theme for the rail and the CSS custom properties it sets on itself:
|
|
6
|
+
// --sb-* drive the rail's own surfaces; --bg-hover / --bg-active / … are remapped so components inside
|
|
7
|
+
// (SearchInput, SegmentedControl, Tree) sit correctly on the rail colour.
|
|
8
|
+
export const SIDEBAR_COLORS = { slate: '#1e293b', navy: '#14305c', indigo: '#312e81', violet: '#4c1d95', plum: '#701a75', wine: '#7f1d1d', forest: '#14532d', teal: '#134e4a', umber: '#78350f', charcoal: '#262626' }
|
|
9
|
+
const wash = (p) => 'color-mix(in srgb, var(--on-accent) ' + p + '%, transparent)'
|
|
10
|
+
const BASE = { '--sb-border': 'var(--border-card)', '--sb-text': 'var(--text-body)', '--sb-brand': 'var(--text-primary)', '--sb-active-text': 'var(--text-heading)', '--sb-muted': 'var(--text-muted)' }
|
|
11
|
+
const GREY = { ...BASE, '--sb-bg': 'var(--bg-table-header)', '--sb-active': 'var(--bg-surface)', '--sb-hover': 'var(--bg-press)', '--sb-shadow': '0 0 0 1px var(--border-card)', '--bg-hover': 'var(--bg-press)', '--bg-active': 'var(--bg-surface)', '--bg-inset': 'var(--overlay-hover)' }
|
|
12
|
+
const WHITE = { ...BASE, '--sb-bg': 'var(--bg-surface)', '--sb-active': 'var(--bg-active)', '--sb-hover': 'var(--bg-hover)', '--sb-shadow': 'none' }
|
|
13
|
+
export function chromeTone(tone, color) {
|
|
14
|
+
const t = tone || 'recessed'
|
|
15
|
+
if (t === 'flush') return { theme: undefined, vars: WHITE }
|
|
16
|
+
if (t === 'contrast') return { theme: 'dark', vars: GREY }
|
|
17
|
+
if (t === 'custom') {
|
|
18
|
+
const bg = SIDEBAR_COLORS[color] || color || SIDEBAR_COLORS.slate
|
|
19
|
+
return { theme: 'dark', vars: { ...GREY, '--sb-bg': bg, '--sb-border': wash(12), '--sb-active': wash(16), '--sb-hover': wash(10), '--sb-shadow': 'none', '--bg-hover': wash(10), '--bg-press': wash(16), '--bg-active': wash(16), '--bg-inset': wash(10), '--border-control': wash(16) } }
|
|
20
|
+
}
|
|
21
|
+
return { theme: undefined, vars: GREY }
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The product's nav model. Framework-neutral: re-exported by the React entry and exported from the Vue entry.
|
|
2
|
+
// KEYBRICKS_NAV — the main rail; trailing: true marks an item that opens a side panel (chevron on the right).
|
|
3
|
+
// KEYBRICKS_NAV_FOOTER — pinned at the bottom of the rail. KEYBRICKS_MGMT — the Management side panel's groups.
|
|
4
|
+
export const KEYBRICKS_NAV = [
|
|
5
|
+
{ icon: 'house', label: 'Home' },
|
|
6
|
+
{ icon: 'file-text', label: 'Documents', trailing: true },
|
|
7
|
+
{ icon: 'journal', label: 'Voucher' },
|
|
8
|
+
{ icon: 'credit-card', label: 'Fund Payment Plan' },
|
|
9
|
+
{ icon: 'database', label: 'Data Integration' },
|
|
10
|
+
{ icon: 'git-branch', label: 'Data Flow' },
|
|
11
|
+
{ icon: 'reconciliation', label: 'Reconciliation' },
|
|
12
|
+
{ icon: 'consolidation', label: 'Consolidation' },
|
|
13
|
+
{ icon: 'task-execution', label: 'Task Execution' },
|
|
14
|
+
]
|
|
15
|
+
export const KEYBRICKS_NAV_FOOTER = [
|
|
16
|
+
{ icon: 'settings', label: 'Management', trailing: true },
|
|
17
|
+
{ icon: 'book-open', label: 'Knowledge Base' },
|
|
18
|
+
]
|
|
19
|
+
export const KEYBRICKS_MGMT = [
|
|
20
|
+
{ icon: 'box', label: 'Modeling', children: ['Dimension', 'Model', 'Environment', 'Journal Template', 'Detail Template', 'Relationship'] },
|
|
21
|
+
{ icon: 'repeat', label: 'Workflow', children: ['Workflow', 'Requests', 'Task Tracker'] },
|
|
22
|
+
{ icon: 'shield', label: 'Security Management', children: ['Users', 'Roles', 'Permissions'] },
|
|
23
|
+
{ icon: 'refresh-cw', label: 'System Migration' },
|
|
24
|
+
{ icon: 'circle-check', label: 'Validation Rules' },
|
|
25
|
+
{ icon: 'settings-2', label: 'Control Center' },
|
|
26
|
+
]
|
|
@@ -20,3 +20,5 @@ Set `logo` to the correct relative path to `assets/keybricks-logo.png`.
|
|
|
20
20
|
<ThemeToggle dark={dark} onToggle={setDark}/> // controlled
|
|
21
21
|
applyStoredTheme(); // call once at app start
|
|
22
22
|
```
|
|
23
|
+
|
|
24
|
+
App chrome: `<Sidebar tone="recessed" version="Version 2.4.0" onSelect={…}/>` (items default to `KEYBRICKS_NAV`, footer to `KEYBRICKS_NAV_FOOTER`; `trailing: true` items open a `<SidePanel title="Documents" onClose={…}>` docked beside the rail — same `tone`) and `<TopBar user={{ name, email }} userMenu={<UserMenu … theme={t} onThemeChange={setT}/>}/>`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Theme persistence shared by ThemeToggle (React and Vue). Framework-neutral.
|
|
2
|
+
// applyStoredTheme(): reads localStorage("kb-theme") and sets <html data-theme> before first paint —
|
|
3
|
+
// call once at app start (main.jsx / main.js).
|
|
4
|
+
export function applyStoredTheme() {
|
|
5
|
+
if (typeof document === 'undefined') return;
|
|
6
|
+
let t = 'light';
|
|
7
|
+
try { t = localStorage.getItem('kb-theme') || 'light'; } catch (e) {}
|
|
8
|
+
document.documentElement.dataset.theme = t;
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
// Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures
|
|
4
|
+
// (digits are Latin, so not bumped in CJK). Fill: the alpha wash --neutral-alpha-8, so it darkens any surface by the same step (white tab,
|
|
5
|
+
// #f5f5f5 active pill, dark theme); text tells selected from unselected: --text-heading vs --text-body (a lighter grey fails 4.5:1 at 12px).
|
|
6
|
+
// Hidden for 0 or a missing count unless showZero, which draws a muted 0.
|
|
7
|
+
const props = defineProps({ count: Number, selected: Boolean, showZero: Boolean, style: Object })
|
|
8
|
+
const zero = computed(() => props.count === 0 || props.count == null)
|
|
9
|
+
</script>
|
|
10
|
+
<template>
|
|
11
|
+
<span v-if="!zero || showZero" aria-hidden="true" :style="{ display: 'inline-flex', alignItems: 'center', height: 'var(--count-h)', padding: '0 6px', borderRadius: 'var(--radius-full)', background: 'var(--neutral-alpha-8)', color: zero ? 'var(--text-muted)' : selected ? 'var(--text-heading)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: '12px', lineHeight: 'var(--count-h)', fontWeight: 'var(--weight-medium)', letterSpacing: 0, fontVariantNumeric: 'tabular-nums', flexShrink: 0, ...style }">{{ zero ? 0 : count }}</span>
|
|
12
|
+
</template>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import CountBadge from './CountBadge.vue'
|
|
2
4
|
// Panel tabs (Node Info · Calc Rules · …): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look.
|
|
3
|
-
|
|
5
|
+
// tabs: string[] | { label, count?, showZero? }[] — a count renders as a CountBadge 6px right of the label; active is matched by label.
|
|
6
|
+
const props = defineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, showZero: Boolean, style: Object })
|
|
4
7
|
const emit = defineEmits(['change'])
|
|
8
|
+
const items = computed(() => props.tabs.map(t => typeof t === 'string' ? { label: t } : t))
|
|
5
9
|
</script>
|
|
6
10
|
<template>
|
|
7
11
|
<div role="tablist" :data-size="size" :style="{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }">
|
|
8
|
-
<button v-for="t in
|
|
9
|
-
:style="{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--action-primary)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }">{{ t }}
|
|
12
|
+
<button v-for="t in items" :key="t.label" type="button" role="tab" :aria-selected="t.label === active" :data-ink="t.label === active ? undefined : ''" @click="emit('change', t.label)"
|
|
13
|
+
:style="{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t.label === active ? '2px solid var(--action-primary)' : '2px solid transparent', display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t.label === active ? 'var(--action-primary)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }">{{ t.label }}<CountBadge :count="t.count" :selected="t.label === active" :show-zero="t.showZero ?? showZero" /></button>
|
|
10
14
|
</div>
|
|
11
15
|
</template>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import CountBadge from './CountBadge.vue'
|
|
4
|
+
// Text pill filter tabs. Active = tinted pill. Pills are control-height (sm 28 · md 32 · lg 40); omit `size` to inherit.
|
|
5
|
+
// tabs: string[] | { label, count?, showZero? }[] — a count draws a CountBadge 6px right of the label; active is matched by label.
|
|
6
|
+
const props = defineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, showZero: Boolean, style: Object })
|
|
4
7
|
const emit = defineEmits(['change'])
|
|
8
|
+
const items = computed(() => props.tabs.map(t => typeof t === 'string' ? { label: t } : t))
|
|
5
9
|
</script>
|
|
6
10
|
<template>
|
|
7
11
|
<div role="tablist" :data-size="size" :style="{ display: 'flex', gap: 'var(--control-gap)', ...style }">
|
|
8
|
-
<button v-for="t in
|
|
9
|
-
:style="{ background: t === active ? 'var(--bg-active)' : 'var(--wash, transparent)', border: 'none', cursor: 'pointer', height: 'var(--control-h)', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }">{{ t }}
|
|
12
|
+
<button v-for="t in items" :key="t.label" type="button" role="tab" :aria-selected="t.label === active" :data-ink="t.label === active ? undefined : ''" :data-wash="t.label === active ? undefined : ''" @click="emit('change', t.label)"
|
|
13
|
+
:style="{ background: t.label === active ? 'var(--bg-active)' : 'var(--wash, transparent)', border: 'none', cursor: 'pointer', height: 'var(--control-h)', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)', display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t.label === active ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }">{{ t.label }}<CountBadge :count="t.count" :selected="t.label === active" :show-zero="t.showZero ?? showZero" /></button>
|
|
10
14
|
</div>
|
|
11
15
|
</template>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
/** Inset segmented control with a raised active segment — switches views of the same data, applies instantly. ← → move between enabled segments. */
|
|
3
3
|
export interface SegmentedControlProps {
|
|
4
|
-
/** Plain labels, or { label, icon?, disabled? } entries. Value is matched by label. */
|
|
5
|
-
options?: (string | { label: string; icon?: string; disabled?: boolean })[];
|
|
4
|
+
/** Plain labels, or { label, icon?, disabled?, count?, showZero? } entries. Value is matched by label; count renders as a CountBadge 6px right of the label (hidden in iconOnly). */
|
|
5
|
+
options?: (string | { label: string; icon?: string; disabled?: boolean; count?: number; showZero?: boolean })[];
|
|
6
6
|
value?: string;
|
|
7
7
|
onChange?: (value: string) => void;
|
|
8
|
-
/** Size tier: sm
|
|
8
|
+
/** Size tier: sm 28 · md 32 · lg 40. Omit to inherit the enclosing region's tier (the default). */
|
|
9
9
|
size?: "sm" | "md" | "lg";
|
|
10
10
|
/** Hide labels; each label becomes the segment's aria-label and tooltip */
|
|
11
11
|
iconOnly?: boolean;
|
|
@@ -13,6 +13,8 @@ export interface SegmentedControlProps {
|
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
/** Stretch to the container, segments share the width evenly */
|
|
15
15
|
fullWidth?: boolean;
|
|
16
|
+
/** Show a muted 0 badge for zero counts on every option (an option's own showZero wins). */
|
|
17
|
+
showZero?: boolean;
|
|
16
18
|
style?: React.CSSProperties;
|
|
17
19
|
}
|
|
18
20
|
export declare function SegmentedControl(props: SegmentedControlProps): JSX.Element;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { CountBadge } from "./Tabs.jsx";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.
|
|
6
|
-
* options: string[] | { label, icon?, disabled? }[] value/onChange by label
|
|
7
|
-
*
|
|
7
|
+
* options: string[] | { label, icon?, disabled?, count?, showZero? }[] value/onChange by label
|
|
8
|
+
* count draws a CountBadge 6px right of the label (hidden for 0 unless showZero — per option or on the control); the match stays by label.
|
|
9
|
+
* size sm 28 | md 32 | lg 40 — omit it to inherit the region's tier · iconOnly hides labels (label becomes aria-label / title)
|
|
8
10
|
* disabled greys the whole control · fullWidth stretches segments evenly
|
|
9
11
|
* Keyboard: ← → move between enabled segments.
|
|
10
12
|
*/
|
|
11
13
|
const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
|
|
12
|
-
export function SegmentedControl({ options = [], value, onChange, size, iconOnly, disabled, fullWidth, style }) {
|
|
14
|
+
export function SegmentedControl({ options = [], value, onChange, size, iconOnly, disabled, fullWidth, showZero, style }) {
|
|
13
15
|
const opts = options.map((o) => (typeof o === "string" ? { label: o } : o));
|
|
14
16
|
const enabled = opts.filter((o) => !o.disabled);
|
|
15
17
|
const onKey = (e) => {
|
|
@@ -27,7 +29,7 @@ export function SegmentedControl({ options = [], value, onChange, size, iconOnly
|
|
|
27
29
|
<button key={o.label} type="button" role="tab" aria-selected={is} aria-label={iconOnly ? o.label : undefined} title={iconOnly ? o.label : undefined} disabled={off} tabIndex={is ? 0 : -1} data-ink={is || off ? undefined : ""}
|
|
28
30
|
onClick={() => !off && onChange && onChange(o.label)}
|
|
29
31
|
style={{ height: "calc(var(--control-h) - 6px)", padding: iconOnly ? "0 calc(var(--control-px) - 4px)" : "0 calc(var(--control-px) - 2px)", flex: fullWidth ? 1 : "none", justifyContent: "center", display: "inline-flex", alignItems: "center", gap: 6, borderRadius: "var(--radius-sm)", border: "none", cursor: off ? "not-allowed" : "pointer", background: is ? "var(--bg-surface)" : "transparent", boxShadow: is ? "0 1px 2px rgba(0,0,0,.08)" : "none", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--text-heading)" : "var(--ink, var(--text-secondary))", opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: "nowrap", transition: "var(--t-hover)" }}>
|
|
30
|
-
{o.icon ? <Icon name={o.icon} size={16} style={tierIcon} /> : null}{iconOnly ? null : o.label}
|
|
32
|
+
{o.icon ? <Icon name={o.icon} size={16} style={tierIcon} /> : null}{iconOnly ? null : o.label}{iconOnly ? null : <CountBadge count={o.count} selected={is} showZero={o.showZero ?? showZero} />}
|
|
31
33
|
</button>
|
|
32
34
|
);
|
|
33
35
|
})}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
|
+
import CountBadge from './CountBadge.vue'
|
|
4
5
|
// Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.
|
|
5
|
-
// options: string[] | { label, icon?, disabled? }[] · v-model by label · size sm
|
|
6
|
+
// options: string[] | { label, icon?, disabled?, count?, showZero? }[] · v-model by label · count draws a CountBadge 6px right of the label (hidden for 0 unless showZero) · size sm 28 | md 32 | lg 40 — omit it to inherit the region's tier
|
|
6
7
|
// iconOnly hides labels (label → aria-label/title) · disabled greys the control · fullWidth stretches segments · ← → keyboard.
|
|
7
8
|
const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
|
|
8
|
-
const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: String, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, style: Object })
|
|
9
|
+
const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: String, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, showZero: Boolean, style: Object })
|
|
9
10
|
const emit = defineEmits(['update:modelValue'])
|
|
10
11
|
const opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o } : o))
|
|
11
12
|
const enabled = computed(() => opts.value.filter(o => !o.disabled))
|
|
@@ -21,7 +22,7 @@ function onKey(e) {
|
|
|
21
22
|
<div role="tablist" :data-size="size" :aria-disabled="disabled || undefined" @keydown="onKey" :style="{ display: fullWidth ? 'flex' : 'inline-flex', height: 'var(--control-h)', padding: '2px', gap: '2px', background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }">
|
|
22
23
|
<button v-for="o in opts" :key="o.label" type="button" role="tab" :aria-selected="modelValue === o.label" :aria-label="iconOnly ? o.label : undefined" :title="iconOnly ? o.label : undefined" :disabled="disabled || o.disabled" :tabindex="modelValue === o.label ? 0 : -1" :data-ink="modelValue === o.label || disabled || o.disabled ? undefined : ''" @click="pick(o)"
|
|
23
24
|
:style="{ height: 'calc(var(--control-h) - 6px)', padding: iconOnly ? '0 calc(var(--control-px) - 4px)' : '0 calc(var(--control-px) - 2px)', flex: fullWidth ? 1 : 'none', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: (disabled || o.disabled) ? 'not-allowed' : 'pointer', background: modelValue === o.label ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === o.label ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: modelValue === o.label ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: 'nowrap', transition: 'var(--t-hover)' }">
|
|
24
|
-
<Icon v-if="o.icon" :name="o.icon" :size="16" :style="tierIcon" /><template v-if="!iconOnly">{{ o.label }}
|
|
25
|
+
<Icon v-if="o.icon" :name="o.icon" :size="16" :style="tierIcon" /><template v-if="!iconOnly">{{ o.label }}<CountBadge :count="o.count" :selected="modelValue === o.label" :show-zero="o.showZero ?? showZero" /></template>
|
|
25
26
|
</button>
|
|
26
27
|
</div>
|
|
27
28
|
</template>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** Underline tabs (Import/Export). Active: blue text + 2px blue underline on hairline baseline. The bar is the region's bar height (sm 32 · md 40 · lg 44). */
|
|
2
|
+
/** Underline tabs (Import/Export). Active: blue text + 2px blue underline on hairline baseline. The bar is the region's bar height (sm 32 · md 40 · lg 44). A tab's count renders as a neutral CountBadge right of the label. */
|
|
3
|
+
export interface TabItem { label: string; /** Count badge 6px right of the label; hidden when 0 or missing unless showZero. */ count?: number; showZero?: boolean; }
|
|
3
4
|
export interface TabsProps {
|
|
4
|
-
|
|
5
|
+
/** Plain labels or { label, count?, showZero? }. Active is matched by label. */
|
|
6
|
+
tabs?: (string | TabItem)[];
|
|
7
|
+
/** Show a muted 0 badge for zero counts on every tab (a tab's own showZero wins). */
|
|
8
|
+
showZero?: boolean;
|
|
5
9
|
active?: string;
|
|
6
10
|
onChange?: (tab: string) => void;
|
|
7
11
|
/** Size tier. Omit to inherit the enclosing region's tier (the default). */
|
|
@@ -11,5 +15,11 @@ export interface TabsProps {
|
|
|
11
15
|
export declare function Tabs(props: TabsProps): JSX.Element;
|
|
12
16
|
/** Lighter underline tabs for panels (Node Info / Calc Rules / ...), regular weight; an sm region gives the compact 32px/12px look. */
|
|
13
17
|
export declare function PanelTabs(props: TabsProps): JSX.Element;
|
|
14
|
-
/**
|
|
15
|
-
export
|
|
18
|
+
/** @deprecated use TabItem */
|
|
19
|
+
export type PillTab = TabItem;
|
|
20
|
+
export type PillTabsProps = TabsProps;
|
|
21
|
+
/** Text pill filter tabs at control height; active gets a tinted pill. A tab's count renders as a CountBadge 6px right of the label. */
|
|
22
|
+
export declare function PillTabs(props: PillTabsProps): JSX.Element;
|
|
23
|
+
/** Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures. Fill --neutral-alpha-8 on any surface; text --text-body, --text-heading when selected. Renders nothing for 0 or undefined unless showZero (muted 0). */
|
|
24
|
+
export interface CountBadgeProps { count?: number; selected?: boolean; showZero?: boolean; style?: React.CSSProperties; }
|
|
25
|
+
export declare function CountBadge(props: CountBadgeProps): JSX.Element | null;
|
package/components/tabs/Tabs.jsx
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/* tabs: string[] | { label, count?, showZero? }[] — shared by Tabs, PanelTabs and PillTabs; active is matched by label. */
|
|
4
|
+
const norm = (tabs) => tabs.map((t) => (typeof t === "string" ? { label: t } : t));
|
|
5
|
+
|
|
6
|
+
/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit.
|
|
7
|
+
* A tab's count renders as a CountBadge 6px right of the label (the badge stays neutral under the blue active tab). */
|
|
8
|
+
export function Tabs({ tabs = [], active, onChange, size, showZero, style }) {
|
|
5
9
|
return (
|
|
6
10
|
<div role="tablist" data-size={size} style={{ display: "flex", gap: 4, borderBottom: "1px solid var(--border-control)", ...style }}>
|
|
7
|
-
{tabs.map((t) => {
|
|
8
|
-
const is = t === active;
|
|
11
|
+
{norm(tabs).map((t) => {
|
|
12
|
+
const is = t.label === active;
|
|
9
13
|
return (
|
|
10
|
-
<button key={t} type="button" role="tab" aria-selected={is} data-ink={is ? undefined : ""} onClick={() => onChange && onChange(t)}
|
|
11
|
-
style={{ background: "transparent", border: "none", cursor: "pointer", height: "var(--bar-h)", boxSizing: "border-box", padding: "0 var(--control-px)", marginBottom: -1, borderBottom: is ? "2px solid var(--action-primary)" : "2px solid transparent", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--action-primary)" : "var(--ink, var(--text-body))", whiteSpace: "nowrap" }}>
|
|
12
|
-
{t}
|
|
14
|
+
<button key={t.label} type="button" role="tab" aria-selected={is} data-ink={is ? undefined : ""} onClick={() => onChange && onChange(t.label)}
|
|
15
|
+
style={{ background: "transparent", border: "none", cursor: "pointer", height: "var(--bar-h)", boxSizing: "border-box", padding: "0 var(--control-px)", marginBottom: -1, borderBottom: is ? "2px solid var(--action-primary)" : "2px solid transparent", display: "inline-flex", alignItems: "center", gap: 6, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--action-primary)" : "var(--ink, var(--text-body))", whiteSpace: "nowrap" }}>
|
|
16
|
+
{t.label}<CountBadge count={t.count} selected={is} showZero={t.showZero ?? showZero} />
|
|
13
17
|
</button>
|
|
14
18
|
);
|
|
15
19
|
})}
|
|
@@ -18,15 +22,15 @@ export function Tabs({ tabs = [], active, onChange, size, style }) {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** Panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look. */
|
|
21
|
-
export function PanelTabs({ tabs = [], active, onChange, size, style }) {
|
|
25
|
+
export function PanelTabs({ tabs = [], active, onChange, size, showZero, style }) {
|
|
22
26
|
return (
|
|
23
27
|
<div role="tablist" data-size={size} style={{ display: "flex", borderBottom: "1px solid var(--border-subtle)", ...style }}>
|
|
24
|
-
{tabs.map((t) => {
|
|
25
|
-
const is = t === active;
|
|
28
|
+
{norm(tabs).map((t) => {
|
|
29
|
+
const is = t.label === active;
|
|
26
30
|
return (
|
|
27
|
-
<button key={t} type="button" role="tab" aria-selected={is} data-ink={is ? undefined : ""} onClick={() => onChange && onChange(t)}
|
|
28
|
-
style={{ background: "transparent", border: "none", cursor: "pointer", height: "var(--bar-h)", boxSizing: "border-box", padding: "0 var(--control-px)", marginBottom: -1, borderBottom: is ? "2px solid var(--action-primary)" : "2px solid transparent", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--action-primary)" : "var(--ink, var(--text-secondary))", whiteSpace: "nowrap" }}>
|
|
29
|
-
{t}
|
|
31
|
+
<button key={t.label} type="button" role="tab" aria-selected={is} data-ink={is ? undefined : ""} onClick={() => onChange && onChange(t.label)}
|
|
32
|
+
style={{ background: "transparent", border: "none", cursor: "pointer", height: "var(--bar-h)", boxSizing: "border-box", padding: "0 var(--control-px)", marginBottom: -1, borderBottom: is ? "2px solid var(--action-primary)" : "2px solid transparent", display: "inline-flex", alignItems: "center", gap: 6, fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--action-primary)" : "var(--ink, var(--text-secondary))", whiteSpace: "nowrap" }}>
|
|
33
|
+
{t.label}<CountBadge count={t.count} selected={is} showZero={t.showZero ?? showZero} />
|
|
30
34
|
</button>
|
|
31
35
|
);
|
|
32
36
|
})}
|
|
@@ -34,16 +38,27 @@ export function PanelTabs({ tabs = [], active, onChange, size, style }) {
|
|
|
34
38
|
);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
|
|
42
|
+
/* Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures
|
|
43
|
+
(digits are Latin, so the size is not bumped in CJK). The fill is the alpha wash --neutral-alpha-8, so it darkens whatever it sits on by the
|
|
44
|
+
same step (white tab, #f5f5f5 active pill, dark theme); the text tells selected from unselected: --text-heading vs --text-body
|
|
45
|
+
(a lighter grey fails 4.5:1 on a grey fill at 12px). Hidden for 0 or a missing count unless showZero, which draws a muted 0. */
|
|
46
|
+
export function CountBadge({ count, selected, showZero, style }) {
|
|
47
|
+
const zero = count === 0 || count == null;
|
|
48
|
+
if (zero && !showZero) return null;
|
|
49
|
+
return <span aria-hidden="true" style={{ display: "inline-flex", alignItems: "center", height: "var(--count-h)", padding: "0 6px", borderRadius: "var(--radius-full)", background: "var(--neutral-alpha-8)", color: zero ? "var(--text-muted)" : selected ? "var(--text-heading)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: 12, lineHeight: "var(--count-h)", fontWeight: "var(--weight-medium)", letterSpacing: 0, fontVariantNumeric: "tabular-nums", flexShrink: 0, ...style }}>{zero ? 0 : count}</span>;
|
|
50
|
+
}
|
|
51
|
+
/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = tinted pill. Pills are control-height (sm 28 · md 32 · lg 40); omit `size` to inherit.
|
|
52
|
+
* tabs: string[] | { label, count?, showZero? }[] — a count draws a CountBadge 6px right of the label; active is matched by label. */
|
|
53
|
+
export function PillTabs({ tabs = [], active, onChange, size, showZero, style }) {
|
|
39
54
|
return (
|
|
40
55
|
<div role="tablist" data-size={size} style={{ display: "flex", gap: "var(--control-gap)", ...style }}>
|
|
41
|
-
{tabs.map((t) => {
|
|
42
|
-
const is = t === active;
|
|
56
|
+
{norm(tabs).map((t) => {
|
|
57
|
+
const is = t.label === active;
|
|
43
58
|
return (
|
|
44
|
-
<button key={t} type="button" role="tab" aria-selected={is} data-ink={is ? undefined : ""} data-wash={is ? undefined : ""} onClick={() => onChange && onChange(t)}
|
|
45
|
-
style={{ background: is ? "var(--bg-active)" : "var(--wash, transparent)", border: "none", cursor: "pointer", height: "var(--control-h)", padding: "0 var(--control-px)", borderRadius: "var(--radius-md)", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--text-heading)" : "var(--ink, var(--text-secondary))", whiteSpace: "nowrap" }}>
|
|
46
|
-
{t}
|
|
59
|
+
<button key={t.label} type="button" role="tab" aria-selected={is} data-ink={is ? undefined : ""} data-wash={is ? undefined : ""} onClick={() => onChange && onChange(t.label)}
|
|
60
|
+
style={{ background: is ? "var(--bg-active)" : "var(--wash, transparent)", border: "none", cursor: "pointer", height: "var(--control-h)", padding: "0 var(--control-px)", borderRadius: "var(--radius-md)", display: "inline-flex", alignItems: "center", gap: 6, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: is ? "var(--text-heading)" : "var(--ink, var(--text-secondary))", whiteSpace: "nowrap" }}>
|
|
61
|
+
{t.label}<CountBadge count={t.count} selected={is} showZero={t.showZero ?? showZero} />
|
|
47
62
|
</button>
|
|
48
63
|
);
|
|
49
64
|
})}
|
package/components/tabs/Tabs.vue
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import CountBadge from './CountBadge.vue'
|
|
2
4
|
// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit.
|
|
3
|
-
|
|
5
|
+
// tabs: string[] | { label, count?, showZero? }[] — a count renders as a CountBadge 6px right of the label (neutral under the blue active tab); active is matched by label.
|
|
6
|
+
const props = defineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, showZero: Boolean, style: Object })
|
|
4
7
|
const emit = defineEmits(['change'])
|
|
8
|
+
const items = computed(() => props.tabs.map(t => typeof t === 'string' ? { label: t } : t))
|
|
5
9
|
</script>
|
|
6
10
|
<template>
|
|
7
11
|
<div role="tablist" :data-size="size" :style="{ display: 'flex', gap: '4px', borderBottom: '1px solid var(--border-control)', ...style }">
|
|
8
|
-
<button v-for="t in
|
|
9
|
-
:style="{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--action-primary)' : 'var(--ink, var(--text-body))', whiteSpace: 'nowrap' }">{{ t }}
|
|
12
|
+
<button v-for="t in items" :key="t.label" type="button" role="tab" :aria-selected="t.label === active" :data-ink="t.label === active ? undefined : ''" @click="emit('change', t.label)"
|
|
13
|
+
:style="{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t.label === active ? '2px solid var(--action-primary)' : '2px solid transparent', display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t.label === active ? 'var(--action-primary)' : 'var(--ink, var(--text-body))', whiteSpace: 'nowrap' }">{{ t.label }}<CountBadge :count="t.count" :selected="t.label === active" :show-zero="t.showZero ?? showZero" /></button>
|
|
10
14
|
</div>
|
|
11
15
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** list/grid icon toggle — two IconButtons at the region's tier (sm
|
|
2
|
+
/** list/grid icon toggle — two IconButtons at the region's tier (sm 28 · md 32 · lg 40), active gets the tinted wash. */
|
|
3
3
|
export interface ViewToggleProps {
|
|
4
4
|
value?: "list" | "grid";
|
|
5
5
|
/** Size tier. Omit to inherit the enclosing region's tier (the default). */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Tab systems: `Tabs` (underline — page level), `PanelTabs` (lighter underline — config panel, put the panel in an sm region), `PillTabs` (pill filters), `ViewToggle` (list/grid icons). All read the region's size tier: bars are 32 / 40 / 44px, pills 24 / 32 / 36px.
|
|
2
2
|
|
|
3
3
|
```jsx
|
|
4
|
-
<Tabs tabs={["Import","Export"]} active={tab} onChange={setTab}/>
|
|
4
|
+
<Tabs tabs={[{label:"Import",count:3},"Export"]} active={tab} onChange={setTab}/> // any tab kind takes { label, count }
|
|
5
5
|
<PanelTabs tabs={["Node Info","Calc Rules","Output Fields","SQL Preview"]} active={t} onChange={setT}/>
|
|
6
|
-
<PillTabs tabs={["For Review","To be Submitted","For Approval"]} active={p} onChange={setP}/>
|
|
6
|
+
<PillTabs tabs={[{label:"For Review",count:8},{label:"To be Submitted",count:3},"For Approval"]} active={p} onChange={setP}/> // count → badge right of the label; 0 hidden unless showZero
|
|
7
7
|
<ViewToggle value={view} onChange={setView}/>
|
|
8
8
|
```
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyfunc/keybricks-ds",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Keybricks design system —
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Keybricks design system — design tokens, three size tiers and 120 UI components in React and Vue 3, source-only.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/readme.md
CHANGED
|
@@ -11,7 +11,7 @@ One product: the Keybricks web app (desktop, 1512×~860 design size). Key surfac
|
|
|
11
11
|
- **Voice**: terse, technical, verb-first. Buttons are imperative: "Run Test", "Submit", "Save Settings", "Continue to Confirmation", "Mark as New", "Skip Selected".
|
|
12
12
|
- **Casing**: Title Case for nav, headings, buttons, table headers ("Import History", "Custom Parameters"); sentence case for helper copy ("Import and manage configuration migrations across systems", "Click to upload or drag and drop", "JSON files only").
|
|
13
13
|
- **Person**: no "I/we/you" — labels are objective and system-centric. Personal touch is limited to the dashboard greeting "Good evening, Felix" + full date "Monday, April 21, 2026".
|
|
14
|
-
- **Terminology**: domain-precise EPM/ETL vocabulary — Detail Sheet, Dimension, Model, Association Match, Multi-Condition Judge, Group Calculation, Concat Merge, Reconciliation, Consolidation. Config codes
|
|
14
|
+
- **Terminology**: domain-precise EPM/ETL vocabulary — Detail Sheet, Dimension, Model, Association Match, Multi-Condition Judge, Group Calculation, Concat Merge, Reconciliation, Consolidation. Config codes are ALL_CAPS_SNAKE ("FIN_CURRENCY_DEFAULT") and render as code chips — Menlo 12px on a light inset fill.
|
|
15
15
|
- **Numbers & meta**: counts as "1 document", "2 items", relative time "2 hours ago", log lines "[14:30] Task started: Import".
|
|
16
16
|
- **Status language**: "• Editing" dot-prefixed state chip; steps named as nouns ("Upload & Parse", "Comparison", "Confirmation", "Execution", "Results").
|
|
17
17
|
- **Emoji**: never. Icons carry all decoration.
|
|
@@ -25,21 +25,23 @@ One product: the Keybricks web app (desktop, 1512×~860 design size). Key surfac
|
|
|
25
25
|
3. **Component** — `--cat-*`, `--tooltip-bg`, `--dropzone-border-hover`: one part of one component, added only when no tier-2 role fits.
|
|
26
26
|
|
|
27
27
|
Rules that don't bend: step 400 and lighter never carry body text on white (they are surfaces, borders, disabled glyphs); `--on-accent` (#FFFFFF) is the only foreground allowed on a saturated fill, in **both** themes; label hues are mechanical (light = 100-step bg + 700-step text, dark = 950-step bg + 300-step text) so a new category needs no judgement; hue means *category*, the four feedback roles mean *severity*. Contrast targets: 4.5:1 body text, 3:1 for 18px+/bold and for UI boundaries. Three Colors cards carry the values: Palette (every ramp), Schemes (sets that work together), Transparency (the alpha scale).
|
|
28
|
-
- **Type**: Inter only (300 stat numerals, 400 body, 500 UI-medium, 600 titles and table headers) on the 14px scale — 12 captions · 14 body and controls · 16 section titles · 20 page titles; 24px only for the greeting + stat numbers (weight 300); 11px for chart
|
|
28
|
+
- **Type**: Inter only (300 stat numerals, 400 body, 500 UI-medium, 600 titles and table headers) on the 14px scale — 12 captions · 14 body and controls · 16 section titles · 20 page titles; 24px only for the greeting + stat numbers (weight 300); 11px for chart text and nothing else. Negative tracking from 14px up (-.15 to -.54px), 0 at 12px. Menlo for data/code/logs at the row's own size with tabular figures. Chinese (Simplified and Traditional), Japanese and Korean (`lang="zh|ja|ko"`) follow one rule: below 14px they are one step larger than Latin (12 → 13, 11 → 12, so no CJK text is under 12px), weights 500 and 600 both → 500 (CJK glyphs read heavier at the same number; medium 400 in sm regions, where bold strokes merge), tracking 0, never caps.
|
|
29
29
|
- **Spacing**: 4-px family in practice (2/4/6/8/10/12/16/24/32); page padding 24×32; card padding 21 (dense 17); section gap 24. Inside a bar the gaps come from the size tier: 8 inside a group, 12/16/24 between groups, 12/16 from a card edge to the first control.
|
|
30
|
-
- **Size tiers**: one tier per region — `data-size="sm" | "md" | "lg"` gives
|
|
30
|
+
- **Size tiers**: one tier per region — `data-size="sm" | "md" | "lg"` gives 28 / 32 / 40px controls, 12 / 14 / 14px control text (13 / 14 / 14 in CJK) and 36 / 40 / 48px rows; every control inside inherits, nothing interactive is below 24px, and heights or text sizes are never mixed in one row. md is the app default; dialogs are lg; dense grids are sm.
|
|
31
31
|
- **Radii**: 4 small controls · 8 buttons/inputs/nav/chips · 10 cards/search/dropzone · 14 palette tiles · 16 canvas nodes · full circles for ports/steps/pills.
|
|
32
32
|
- **Borders**: hairline everywhere — cards rgba(229,229,229,.6); controls #e5e5e5; row separators #f5f5f5; checkbox #d1d5db 1.5px; ports #d4d4d4 2px; dropzone dashed #d4d4d4 2px.
|
|
33
33
|
- **Shadows**: nearly flat. Canvas nodes only: `0 1px 3px rgba(0,0,0,.1), 0 1px 2px -1px rgba(0,0,0,.1)`; selection = blue-400 border + 2px blue-100 ring; badges get a 2px white ring. Everything else relies on borders.
|
|
34
34
|
- **Backgrounds & imagery**: no photography, no illustration. The pipeline canvas is #fafafa with a subtle dot/curve grid; chart areas use soft blue gradient fills. Only bitmap: the cube logo.
|
|
35
35
|
- **States**: every interactive element carries five states beyond rest. **Hover** moves the fill one step (`--bg-hover` wash on ghost items, `--action-primary-hover` on solid buttons, `--soft-hover` on tinted ones); controls whose colours are inline get it from the hover hooks in `tokens/hover.css`. **Press** moves it a second step: Button and IconButton use explicit tokens (`--action-primary-active`, `--action-dark-active`, `--bg-press`, `--soft-active`, `--danger-active`, `--danger-soft-active`) and opt out of the global layer with `data-press="own"`; every other control — nav rows, tree items, menu items, chips, tabs, list rows, option rows, palette tiles — gets press from the one global rule in `tokens/interaction.css` (`filter: var(--press-filter)` on `:active`, which multiplies whatever wash hover is already showing, and brightens rather than darkens in dark mode). No scale transforms, no shadow shifts; text-only buttons drop to 70% opacity instead. **Keyboard focus** is also global: a 2px `--focus-border` outline at 2px offset on `:focus-visible` only, so a mouse click never draws it. **Selected/active** is persistent, not transient: active nav = `--bg-active` pill + heading-weight text; selected node = blue border + ring. **Disabled** is 55% opacity (50% on icon buttons) with the default cursor.
|
|
36
36
|
- **Motion**: kept minimal by design (see Motion below) — 120–160ms ease-out on hover/selection, nothing bouncy or far-traveling.
|
|
37
|
-
- **Layout chrome**: left sidebar 240px (collapsible to a 64px icon rail), 56px top bar (search left
|
|
37
|
+
- **Layout chrome**: left sidebar 240px (collapsible to a 64px icon rail), 56px top bar (search left; black "AI Assistant" pill, bell, help and the user menu right), sidebar tones recessed (grey, default) · flush · contrast · custom, optional 240px secondary panel (Documents tree, pipeline palette), 400px right config panel, 250px bottom data-preview drawer.
|
|
38
38
|
- **Cards**: white, radius 10, hairline border, 21px padding, 14px/500 title — never elevated.
|
|
39
|
-
- **Density**: high; table rows 32 (sm) · 40 (md) · 44 (lg); header cells the same size as body cells in weight 600, sentence case;
|
|
39
|
+
- **Density**: high; table rows 32 (sm) · 40 (md) · 44 (lg); header cells the same size as body cells in weight 600, sentence case; amount cells at the row's size with tabular figures; config codes as 12px code chips.
|
|
40
40
|
|
|
41
41
|
## Iconography
|
|
42
42
|
- The product's icons are the **Lucide** stroke set (16/14px at 1.33px effective stroke, `currentColor`). We ship `components/icons/Icon.jsx`, embedding the exact Lucide path data for every glyph the product uses. Usage: `<Icon name="search" size={16}/>`.
|
|
43
|
+
- **Keybricks glyphs**: thirteen icons drawn for Keybricks itself sit in the core set beside the Lucide glyphs — AI (`ai`, `ai-sparkles`), modules (`reconciliation`, `consolidation`, `journal`, `event`, `view-tree`) and actions (`drilldown`, `add-below`, `copy-data`, `task-execution`, `time-future`, `time-restatement`). Search the Icon Gallery before drawing a new one: *hierarchy* turned out to be `layers`, and *change* is `repeat`.
|
|
44
|
+
- **Drawing rules** (every core glyph, and any glyph added later): 24×24 grid; paths stay inside 2–22, which leaves 1px of padding once the stroke is drawn; stroke 2, round caps and joins, no fill, `currentColor`; integer coordinates; at least 2px of clear space between separate elements; rect corners rx 2, or rx 1 at 8px and smaller. Check a new glyph at 14 and 16px before adding it.
|
|
43
45
|
- Icon color follows text color (#525252 default chrome, #a1a1a1 muted, category hue inside tinted tiles).
|
|
44
46
|
- Tinted icon tiles are a signature motif: 40×40 r14 (palette), 48×48 r16 (nodes), 32×32 r8 hairline-bordered (list rows) — 100-tint bg + 600-hue glyph.
|
|
45
47
|
- Emoji or unicode-as-icon: never.
|
|
@@ -67,7 +69,7 @@ One product: the Keybricks web app (desktop, 1512×~860 design size). Key surfac
|
|
|
67
69
|
|
|
68
70
|
**For developers integrating this system**
|
|
69
71
|
- `DESIGN.md` — the rules file for AI coding agents: absolute token values, interaction states, an explicit "do not" list, and voice. Point Claude Code / Cursor / Copilot at this before they generate UI.
|
|
70
|
-
- `INTEGRATION.md` — the integration guide: three steps to first paint, the full
|
|
72
|
+
- `INTEGRATION.md` — the integration guide: three steps to first paint, the full 120-component index with import paths, token reference, dark mode, interaction states, versioning.
|
|
71
73
|
- `DEPLOY.md` — running the system on GitLab: what ships in the package and what stays on Pages, the entry points, the release flow, and the bundler config each consumer needs.
|
|
72
74
|
- `index.html` · `gallery.html` — the documentation site: the browsable system and the filterable thumbnail grid, both deployed to GitLab Pages by CI.
|
|
73
75
|
- `package.json` · `.gitlab-ci.yml` · `_scripts/` — makes the system an installable package: a version tag publishes to the GitLab Package Registry, the default branch redeploys the docs site, and `npm run check` fails the pipeline if the generated tokens or barrels drift from source.
|
|
@@ -76,18 +78,17 @@ One product: the Keybricks web app (desktop, 1512×~860 design size). Key surfac
|
|
|
76
78
|
- `styles.css` — global CSS entry (imports all token files); `keybricks.css` — the package's stylesheet entry (tokens + hover hooks).
|
|
77
79
|
- `assets/` — keybricks-logo.png and the icon keyword indexes.
|
|
78
80
|
- `guidelines/` — foundation showcase cards (Design System tab).
|
|
79
|
-
- `ui_kits/keybricks-app/` — interactive product recreation: Home, Documents, Pipeline editor, System Migration.
|
|
80
81
|
- `SKILL.md` — agent skill entry file.
|
|
81
82
|
|
|
82
83
|
## Components
|
|
83
|
-
- `components/icons/` — Icon (+ ICON_PATHS glyph data + feather-paths.js, the full Feather fallback), Unicon (Unicons CDN webfont).
|
|
84
|
+
- `components/icons/` — Icon (+ ICON_PATHS glyph data + feather-paths.js, the full Feather fallback), DataSourceLogo (+ DATA_SOURCE_LOGOS), Unicon (Unicons CDN webfont).
|
|
84
85
|
- `components/buttons/` — Button, IconButton, ActionChip.
|
|
85
86
|
- `components/forms/` — Field, Input, Textarea, Select, SearchInput, Checkbox, NumberStepper, Dropzone, Switch, Radio, RadioGroup, Combobox, MultiSelect, DatePicker, Slider.
|
|
86
87
|
- `components/tabs/` — Tabs, PanelTabs, PillTabs, ViewToggle, SegmentedControl.
|
|
87
88
|
- `components/display/` — Card, StatCard, Badge, StatusBadge, IconTile, ProgressBar, Donut, AreaChart, MiniBars, Avatar, AvatarGroup, Tag, Divider, Kbd.
|
|
88
89
|
- `components/feedback/` — Stepper, ContextMenu, ActivityItem, DotActivity, LogViewer, Tooltip, Modal, Toast, ToastStack, Alert, EmptyState, Skeleton, Spinner, Menu, Accordion, Banner, Drawer, Timeline.
|
|
89
90
|
- `components/data/` — Table, ListRow, Pagination, KeyValue, KeyValueList, DiffValue, CodeBlock, Spreadsheet, SmartTable, FilterBar.
|
|
90
|
-
- `components/navigation/` — Sidebar (+ KEYBRICKS_NAV), TopBar, PageToolbar, PageHeading, DocTree, Breadcrumb, Tree, ThemeToggle (+ applyStoredTheme).
|
|
91
|
+
- `components/navigation/` — Sidebar (+ KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT, SIDEBAR_COLORS), SidePanel, TopBar, UserMenu, PageToolbar, PageHeading, DocTree, Breadcrumb, Tree, ThemeToggle (+ applyStoredTheme).
|
|
91
92
|
- `components/pipeline/` — NodeCard, Port, PortLabel, PaletteItem, PaletteListItem, PalettePanel (+ PIPELINE_CATALOG), PipelineCanvas, DataPreview, ConfigPanel, ZoomControls.
|
|
92
93
|
- `components/charts/` — 27 chart types: Sparkline, LineChart, BarChart, BarList, DonutChart, ScatterChart, HeatmapChart, WaterfallChart, BulletChart, GaugeChart, FunnelChart, TreemapChart, RadarChart, Histogram, BoxPlot, ParetoChart, CandlestickChart, SankeyChart, ChordChart, SunburstChart, WorldMap, GanttChart, and the 3D set Surface3D, Scatter3D, Bar3D, Pie3D, Ribbon3D (SVG painter's-algorithm rendering, drag to rotate; + ChartKit shared legend/tooltip, `tokens/charts.css` series colors and plot furniture). Three scales: micro inside stat tiles, interactive mid-size inside cards, full-page analysis; every chart keeps its hover read-out at every scale. WorldMap uses real Natural Earth geography and needs the page to load pinned d3 and topojson.
|
|
93
94
|
|
package/readme.zh-CN.md
CHANGED
|
@@ -13,7 +13,7 @@ Keybricks 是面向财务团队的企业绩效管理(EPM)平台:文档与
|
|
|
13
13
|
- **语气**:简洁、技术化、动词开头。按钮用祈使句:"Run Test"、"Submit"、"Save Settings"、"Continue to Confirmation"、"Mark as New"、"Skip Selected"。
|
|
14
14
|
- **大小写**:导航、标题、按钮、表头用词首大写(Title Case),如 "Import History"、"Custom Parameters";辅助说明文字用句首大写(sentence case),如 "Import and manage configuration migrations across systems"、"Click to upload or drag and drop"、"JSON files only"。
|
|
15
15
|
- **人称**:不使用"我 / 我们 / 你"——文案客观、以系统为中心。唯一的人性化表达是仪表盘问候语 "Good evening, Felix" 加完整日期 "Monday, April 21, 2026"。
|
|
16
|
-
- **术语**:精确的 EPM/ETL 领域词汇——Detail Sheet、Dimension、Model、Association Match、Multi-Condition Judge、Group Calculation、Concat Merge、Reconciliation、Consolidation
|
|
16
|
+
- **术语**:精确的 EPM/ETL 领域词汇——Detail Sheet、Dimension、Model、Association Match、Multi-Condition Judge、Group Calculation、Concat Merge、Reconciliation、Consolidation。配置代码写作全大写下划线("FIN_CURRENCY_DEFAULT"),显示为代码标签:Menlo 12px、浅灰底。配置代码用等宽字体、全大写下划线格式("FIN_CURRENCY_DEFAULT")。
|
|
17
17
|
- **数字与元信息**:计数写作 "1 document"、"2 items";相对时间写作 "2 hours ago";日志行写作 "[14:30] Task started: Import"。
|
|
18
18
|
- **状态文案**:状态标签带圆点前缀,如 "• Editing";步骤用名词命名("Upload & Parse"、"Comparison"、"Confirmation"、"Execution"、"Results")。
|
|
19
19
|
- **Emoji**:从不使用。所有装饰性表达交给图标。
|
|
@@ -27,9 +27,9 @@ Keybricks 是面向财务团队的企业绩效管理(EPM)平台:文档与
|
|
|
27
27
|
3. **组件层** —— `--cat-*`、`--tooltip-bg`、`--dropzone-border-hover`:只属于某个组件的某个部位,仅在第二层确实没有合适角色时才新增。
|
|
28
28
|
|
|
29
29
|
不可打破的规则:400 及更浅的档位不得在白底上承载正文(它们是表面色、边框色、禁用态图标色);饱和填充之上的前景色只能用 `--on-accent`(#FFFFFF),**两套主题都是**;标签色相按机械规则取值(浅色 = 100 档底 + 700 档字,深色 = 950 档底 + 300 档字),新增分类无需主观判断;色相表达*分类*,四组反馈色表达*严重程度*。对比度目标:正文 4.5:1,18px 以上或加粗文字及界面边界 3:1。Colors 分组下三张卡片给出全部色值:Palette(完整色阶)、Schemes(可搭配使用的配色组合)、Transparency(透明度阶)。
|
|
30
|
-
- **字体**:只用 Inter(300 用于统计数字,400 正文,500 界面中等字重,600 标题与表头),采用 14px 字号阶梯——12 说明文字 · 14 正文与控件 · 16 区块标题 · 20 页面标题;24px 只用于问候语和统计数字(字重 300);11px
|
|
30
|
+
- **字体**:只用 Inter(300 用于统计数字,400 正文,500 界面中等字重,600 标题与表头),采用 14px 字号阶梯——12 说明文字 · 14 正文与控件 · 16 区块标题 · 20 页面标题;24px 只用于问候语和统计数字(字重 300);11px 只用于图表文字。14px 起使用负字距(-.15 到 -.54px),12px 字距为 0。数据 / 代码 / 日志用 Menlo,与所在行同字号,使用等宽数字。中文(简体与繁体)、日文、韩文(`lang="zh|ja|ko"`)统一一条规则:14px 以下比西文大一号(12 → 13,11 → 12,任何 CJK 文字都不小于 12px);500 和 600 字重都改为 500(同样的数字,中日韩文字看起来比字母粗;sm 区域内中等字重改为 400,因为小字号加粗会让笔画粘连);字距为 0,不做大写转换。
|
|
31
31
|
- **间距**:实际采用 4px 体系(2/4/6/8/10/12/16/24/32);页面内边距 24×32;卡片内边距 21(紧凑型 17);区块间距 24。工具栏内的间距由尺寸层级决定:组内 8,组间 12/16/24,卡片边缘到第一个控件 12/16。
|
|
32
|
-
- **尺寸层级**:每个区域只用一档——`data-size="sm" | "md" | "lg"` 对应
|
|
32
|
+
- **尺寸层级**:每个区域只用一档——`data-size="sm" | "md" | "lg"` 对应 28 / 32 / 40px 控件、12 / 14 / 14px 控件文字(CJK 为 13 / 14 / 14)、36 / 40 / 48px 行高;区域内所有控件自动继承,可交互元素不小于 24px,同一行内绝不混用高度或字号。md 是应用默认,对话框为 lg,密集表格为 sm。
|
|
33
33
|
- **圆角**:4 小型控件 · 8 按钮 / 输入框 / 导航 / 标签 · 10 卡片 / 搜索框 / 上传区 · 14 组件面板图标块 · 16 画布节点 · 端口 / 步骤圆点 / 胶囊用全圆角。
|
|
34
34
|
- **边框**:全局使用发丝级细边框——卡片 rgba(229,229,229,.6);控件 #e5e5e5;行分隔线 #f5f5f5;复选框 #d1d5db 1.5px;端口 #d4d4d4 2px;上传区虚线 #d4d4d4 2px。
|
|
35
35
|
- **阴影**:几乎全平面。只有画布节点有阴影:`0 1px 3px rgba(0,0,0,.1), 0 1px 2px -1px rgba(0,0,0,.1)`;选中态 = blue-400 边框 + 2px blue-100 光环;徽标带 2px 白色描边。其余一律靠边框区分层次。
|
|
@@ -38,10 +38,12 @@ Keybricks 是面向财务团队的企业绩效管理(EPM)平台:文档与
|
|
|
38
38
|
- **动效**:刻意克制(见下方"动效"一节)——悬停 / 选中 120–160ms 缓出,不弹跳,不大幅位移。
|
|
39
39
|
- **布局框架**:左侧边栏 240px(可收起为 64px 图标栏);顶栏 56px(左侧搜索,右侧铃铛 + 黑色 "AI Assistant" 胶囊);可选的 240px 二级面板(文档树、管道组件面板);右侧配置面板 400px;底部数据预览抽屉 250px。
|
|
40
40
|
- **卡片**:白底、10px 圆角、细边框、21px 内边距、14px / 500 字重标题——从不使用投影抬高。
|
|
41
|
-
- **密度**:高密度;表格行高 32(sm)· 40(md)· 44(lg);表头与单元格同字号、字重 600
|
|
41
|
+
- **密度**:高密度;表格行高 32(sm)· 40(md)· 44(lg);表头与单元格同字号、字重 600、句首大写;金额单元格与所在行同字号、同字体,只加等宽数字(tabular figures);配置代码用 12px 代码标签。等宽字体只用于代码。
|
|
42
42
|
|
|
43
43
|
## 图标系统
|
|
44
44
|
- 产品图标是 **Lucide** 线性图标集(16/14px,实际描边约 1.33px,颜色为 `currentColor`)。我们提供 `components/icons/Icon.jsx`,内嵌产品用到的每个图形的 Lucide 原始路径数据。用法:`<Icon name="search" size={16}/>`。
|
|
45
|
+
- **Keybricks 自绘图标**:为 Keybricks 自行绘制的 13 个图标与 Lucide 图标一起放在核心集中——AI 类(`ai`、`ai-sparkles`)、模块类(`reconciliation`、`consolidation`、`journal`、`event`、`view-tree`)和操作类(`drilldown`、`add-below`、`copy-data`、`task-execution`、`time-future`、`time-restatement`)。新画图标之前,先在图标库里搜索:*hierarchy* 其实就是 `layers`,*change* 就是 `repeat`。
|
|
46
|
+
- **绘制规范**(所有核心图标,以及今后新增的图标):24×24 网格;路径限制在 2–22 范围内,描边后四周留 1px 边距;描边 2,圆头圆角,无填充,`currentColor`;坐标取整数;相互独立的元素之间至少留 2px 空隙;矩形圆角 rx 2,8px 及以下的小矩形用 rx 1。新增图标前,先在 14px 和 16px 下检查。
|
|
45
47
|
- 图标颜色跟随文字颜色(默认框架色 #525252,弱化态 #a1a1a1,彩色图标块内为对应分类色)。
|
|
46
48
|
- 彩色图标块是标志性元素:40×40 圆角 14(组件面板)、48×48 圆角 16(节点)、32×32 圆角 8 带细边框(列表行)——100 档浅色底 + 600 档分类色图标。
|
|
47
49
|
- 从不用 emoji 或 unicode 字符充当图标。
|
|
@@ -69,7 +71,7 @@ Keybricks 是面向财务团队的企业绩效管理(EPM)平台:文档与
|
|
|
69
71
|
|
|
70
72
|
**开发团队接入入口**
|
|
71
73
|
- `DESIGN.md` —— 给 AI 编程代理读的规则文件:绝对数值、交互状态、明确的"禁止"清单、文案语气。让 Claude Code / Cursor / Copilot 在生成界面前先读它。
|
|
72
|
-
- `INTEGRATION.md` —— 接入指南:三步跑通首屏、全部
|
|
74
|
+
- `INTEGRATION.md` —— 接入指南:三步跑通首屏、全部 120 个组件的清单与引入路径、token 速查、深色模式、交互状态、版本策略。
|
|
73
75
|
- `DEPLOY.md` —— 在 GitLab 上运行本系统:包里发布什么、什么留在 Pages 上、入口点、发布流程、各消费方需要的构建配置。
|
|
74
76
|
- `index.html` · `gallery.html` —— 文档站:可浏览的系统主页与可筛选的缩略图网格,均由 CI 部署到 GitLab Pages。
|
|
75
77
|
- `package.json` · `.gitlab-ci.yml` · `_scripts/` —— 让本系统成为可安装的包:打版本 tag 即发布到 GitLab Package Registry,默认分支更新即重新部署文档站,生成的 token 或入口文件与源码不一致时 `npm run check` 会让流水线失败。
|
|
@@ -78,7 +80,6 @@ Keybricks 是面向财务团队的企业绩效管理(EPM)平台:文档与
|
|
|
78
80
|
- `styles.css` —— 全局 CSS 入口(引入所有 token 文件);`keybricks.css` —— 包的样式入口(token + 悬停钩子)。
|
|
79
81
|
- `assets/` —— keybricks-logo.png 与图标关键词索引。
|
|
80
82
|
- `guidelines/` —— 基础规范展示卡片(Design System 标签页)。
|
|
81
|
-
- `ui_kits/keybricks-app/` —— 可交互的产品复刻:首页、文档、管道编辑器、系统迁移。
|
|
82
83
|
- `SKILL.md` —— Agent 技能入口文件。
|
|
83
84
|
|
|
84
85
|
## 组件清单
|
|
@@ -89,7 +90,7 @@ Keybricks 是面向财务团队的企业绩效管理(EPM)平台:文档与
|
|
|
89
90
|
- `components/display/` —— Card、StatCard、Badge、StatusBadge、IconTile、ProgressBar、Donut、AreaChart、MiniBars、Avatar、AvatarGroup、Tag、Divider、Kbd。
|
|
90
91
|
- `components/feedback/` —— Stepper、ContextMenu、ActivityItem、DotActivity、LogViewer、Tooltip、Modal、Toast、ToastStack、Alert、EmptyState、Skeleton、Spinner、Menu、Accordion、Banner、Drawer、Timeline。
|
|
91
92
|
- `components/data/` —— Table、ListRow、Pagination、KeyValue、KeyValueList、DiffValue、CodeBlock、Spreadsheet、SmartTable、FilterBar。
|
|
92
|
-
- `components/navigation/` —— Sidebar(+ KEYBRICKS_NAV)、TopBar、PageToolbar、PageHeading、DocTree、Breadcrumb、Tree、ThemeToggle(+ applyStoredTheme)。
|
|
93
|
+
- `components/navigation/` —— Sidebar(+ KEYBRICKS_NAV)、SidePanel、TopBar、UserMenu、PageToolbar、PageHeading、DocTree、Breadcrumb、Tree、ThemeToggle(+ applyStoredTheme)。
|
|
93
94
|
- `components/pipeline/` —— NodeCard、Port、PortLabel、PaletteItem、PaletteListItem、PalettePanel(+ PIPELINE_CATALOG)、PipelineCanvas、DataPreview、ConfigPanel、ZoomControls。
|
|
94
95
|
- `components/charts/` —— 27 种图表:Sparkline、LineChart、BarChart、BarList、DonutChart、ScatterChart、HeatmapChart、WaterfallChart、BulletChart、GaugeChart、FunnelChart、TreemapChart、RadarChart、Histogram、BoxPlot、ParetoChart、CandlestickChart、SankeyChart、ChordChart、SunburstChart、WorldMap、GanttChart,以及 3D 系列 Surface3D、Scatter3D、Bar3D、Pie3D、Ribbon3D(SVG 画家算法渲染,拖拽旋转;+ ChartKit 共享图例 / 提示框,`tokens/charts.css` 提供序列色与绘图配件)。三种尺寸:统计卡内的微型、卡片内可交互的中型、整页分析的大型;每种图表在任何尺寸都保留悬停读数。WorldMap 使用真实的 Natural Earth 地理数据,需要页面加载固定版本的 d3 与 topojson。
|
|
95
96
|
|
package/tokens/alpha.css
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
--neutral-alpha-2:rgba(10,10,10,.02);--neutral-alpha-4:rgba(10,10,10,.04);--neutral-alpha-8:rgba(10,10,10,.08);--neutral-alpha-12:rgba(10,10,10,.12);--neutral-alpha-16:rgba(10,10,10,.16);--neutral-alpha-24:rgba(10,10,10,.24);--neutral-alpha-32:rgba(10,10,10,.32);--neutral-alpha-48:rgba(10,10,10,.48);--neutral-alpha-64:rgba(10,10,10,.64);--neutral-alpha-80:rgba(10,10,10,.8);
|
|
16
16
|
--brand-alpha-8:rgba(21,93,252,.08);--brand-alpha-12:rgba(21,93,252,.12);--brand-alpha-16:rgba(21,93,252,.16);--brand-alpha-24:rgba(21,93,252,.24);--brand-alpha-32:rgba(21,93,252,.32);
|
|
17
17
|
/* Semantic overlays built from the ramps above */
|
|
18
|
-
--overlay-hover:var(--neutral-alpha-4);--overlay-press:var(--neutral-alpha-
|
|
18
|
+
--overlay-hover:var(--neutral-alpha-4);--overlay-press:var(--neutral-alpha-12); /* on white these equal --bg-hover / --bg-press; on any other surface they darken it by the same step */
|
|
19
|
+
--overlay-selected:var(--brand-alpha-8);--overlay-drag:var(--brand-alpha-12);
|
|
19
20
|
}
|
|
20
21
|
[data-theme="dark"]{
|
|
21
22
|
--neutral-alpha-2:rgba(255,255,255,.02);--neutral-alpha-4:rgba(255,255,255,.04);--neutral-alpha-8:rgba(255,255,255,.08);--neutral-alpha-12:rgba(255,255,255,.12);--neutral-alpha-16:rgba(255,255,255,.16);--neutral-alpha-24:rgba(255,255,255,.24);--neutral-alpha-32:rgba(255,255,255,.32);--neutral-alpha-48:rgba(255,255,255,.48);--neutral-alpha-64:rgba(255,255,255,.64);--neutral-alpha-80:rgba(255,255,255,.8);
|
|
22
23
|
--brand-alpha-8:rgba(43,127,255,.1);--brand-alpha-12:rgba(43,127,255,.14);--brand-alpha-16:rgba(43,127,255,.2);--brand-alpha-24:rgba(43,127,255,.28);--brand-alpha-32:rgba(43,127,255,.36);
|
|
24
|
+
--overlay-hover:var(--neutral-alpha-8);--overlay-press:var(--neutral-alpha-16); /* dark: matches the --bg-hover / --bg-press steps on --bg-surface */
|
|
23
25
|
}
|
package/tokens/colors.css
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
--on-accent:#ffffff;
|
|
26
26
|
/* Foreground on the neutral "dark pill" (AI Assistant), which inverts in dark */
|
|
27
27
|
--action-dark-fg:#ffffff;
|
|
28
|
-
--border-card:rgba(229,229,229,.6);--border-control:var(--neutral-200);--border-subtle:var(--neutral-100);--border-strong:var(--neutral-300);
|
|
28
|
+
--border-card:rgba(229,229,229,.6);--border-control:var(--neutral-200);--border-split:var(--neutral-300);--border-subtle:var(--neutral-100);--border-strong:var(--neutral-300);
|
|
29
29
|
--action-primary:var(--blue-600);--action-primary-hover:var(--blue-700);--action-primary-active:#1039c4;--action-dark:var(--neutral-900);--action-dark-hover:var(--neutral-800);--action-dark-active:var(--neutral-950);
|
|
30
30
|
/* Press (active) fills — one step past the hover value in each family */
|
|
31
31
|
--bg-press:var(--neutral-200);--soft-active:var(--blue-200);--danger-active:var(--red-800);--danger-soft-hover:var(--red-100);--danger-soft-active:var(--red-200);
|