@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,21 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
4
|
+
import { chromeTone } from './chrome-tones.js'
|
|
5
|
+
// Side panel docked beside the Sidebar (Documents, Management). 240px, same tone as the rail; opens by growing from 0 while
|
|
6
|
+
// its content fades in from 12px left. 56px header: title + #actions slot + close (@close). Default slot is the scrolling body.
|
|
7
|
+
const props = defineProps({ open: { type: Boolean, default: true }, title: String, closable: { type: Boolean, default: true }, closeLabel: { type: String, default: 'Close' }, closeIcon: { type: String, default: 'x' }, tone: { type: String, default: 'recessed' }, color: String, width: { type: Number, default: 240 }, size: String, style: Object })
|
|
8
|
+
const emit = defineEmits(['close'])
|
|
9
|
+
const t = computed(() => chromeTone(props.tone, props.color))
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div :aria-hidden="open ? undefined : 'true'" :style="{ width: (open ? width : 0) + 'px', flexShrink: 0, overflow: 'hidden', height: '100%', transition: 'width var(--dur-slow) var(--ease-out)' }">
|
|
13
|
+
<div :data-theme="t.theme" :data-size="size" :style="{ ...t.vars, width: width + 'px', height: '100%', background: 'var(--sb-bg)', borderRight: '1px solid var(--sb-border)', color: 'var(--sb-text)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', opacity: open ? 1 : 0, transform: open ? 'none' : 'translateX(-12px)', transition: 'opacity var(--dur) var(--ease-out), transform var(--dur-slow) var(--ease-out)', pointerEvents: open ? 'auto' : 'none', ...style }">
|
|
14
|
+
<div style="height:var(--topbar-h);display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 8px 0 16px;box-sizing:border-box;flex-shrink:0">
|
|
15
|
+
<span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading)">{{ title }}</span>
|
|
16
|
+
<div data-size="md" style="display:flex;align-items:center;gap:2px;flex-shrink:0"><slot name="actions" /><IconButton v-if="closable" :icon="closeIcon" muted :label="closeLabel" :title="closeLabel" @click="emit('close')" /></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div style="flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column"><slot /></div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
/**
|
|
3
|
-
* App sidebar — 240px expanded / 64px icon rail.
|
|
3
|
+
* App sidebar — 240px expanded / 64px icon rail. 56px brand head (no rule), a 2px-gap nav list, pinned footer items and a version line.
|
|
4
|
+
* Tones: "recessed" (grey rail, the active item is a white tile with a hairline ring — the default), "flush" (white rail),
|
|
5
|
+
* "contrast" (the rail scoped to the dark theme), "custom" (dark scope on a solid colour; see SIDEBAR_COLORS).
|
|
4
6
|
* @startingPoint section="Chrome" subtitle="240px app sidebar with icon-rail collapse" viewport="240x600"
|
|
5
7
|
*/
|
|
6
8
|
export interface SidebarItem {
|
|
7
|
-
icon
|
|
9
|
+
icon?: string;
|
|
8
10
|
label: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/** Draws a chevron on the right: the item opens a SidePanel */
|
|
12
|
+
trailing?: boolean;
|
|
13
|
+
children?: (SidebarItem | string)[];
|
|
12
14
|
}
|
|
13
15
|
export interface SidebarProps {
|
|
16
|
+
/** Default KEYBRICKS_NAV */
|
|
14
17
|
items?: SidebarItem[];
|
|
18
|
+
/** Default KEYBRICKS_NAV_FOOTER (Management, Knowledge Base) */
|
|
15
19
|
footerItems?: SidebarItem[];
|
|
16
20
|
/** Active item label */
|
|
17
21
|
active?: string;
|
|
18
22
|
onSelect?: (label: string) => void;
|
|
19
23
|
collapsed?: boolean;
|
|
20
|
-
/** Renders the
|
|
24
|
+
/** Renders the Collapse / Expand item at the top of the list. Vue: @toggle-collapse (hide it with :show-toggle="false"). */
|
|
21
25
|
onToggleCollapse?: () => void;
|
|
22
26
|
logo?: string;
|
|
23
27
|
brand?: string;
|
|
28
|
+
/** Version line under the footer items ("Version 2.4.0"); the rail shows "v2.4.0" */
|
|
29
|
+
version?: string;
|
|
30
|
+
tone?: "recessed" | "flush" | "contrast" | "custom";
|
|
31
|
+
/** For tone "custom": a SIDEBAR_COLORS key (slate, navy, indigo, violet, plum, wine, forest, teal, umber, charcoal) or any hex */
|
|
32
|
+
color?: string;
|
|
33
|
+
labels?: { collapse?: string; expand?: string };
|
|
24
34
|
height?: number | string;
|
|
25
35
|
style?: React.CSSProperties;
|
|
26
36
|
}
|
|
27
37
|
export declare function Sidebar(props: SidebarProps): JSX.Element;
|
|
28
|
-
/** The product's
|
|
38
|
+
/** The product's main nav (Home … Task Execution); Documents has trailing: true. */
|
|
29
39
|
export declare const KEYBRICKS_NAV: SidebarItem[];
|
|
40
|
+
/** Pinned footer: Management (trailing) and Knowledge Base. */
|
|
41
|
+
export declare const KEYBRICKS_NAV_FOOTER: SidebarItem[];
|
|
42
|
+
/** Groups for the Management side panel. */
|
|
43
|
+
export declare const KEYBRICKS_MGMT: { icon: string; label: string; children?: string[] }[];
|
|
44
|
+
/** The ten custom rail colours. */
|
|
45
|
+
export declare const SIDEBAR_COLORS: Record<"slate" | "navy" | "indigo" | "violet" | "plum" | "wine" | "forest" | "teal" | "umber" | "charcoal", string>;
|
|
@@ -1,75 +1,57 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT } from "./nav-model.js";
|
|
4
|
+
import { chromeTone, SIDEBAR_COLORS } from "./chrome-tones.js";
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
const LABEL = { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "18px", letterSpacing: "var(--tracking-base)", fontWeight: "var(--weight-medium)" };
|
|
7
|
+
|
|
8
|
+
/* One rail row: 36px min, radius-md, 16px icon, medium label, optional trailing chevron (the item opens a side panel). */
|
|
9
|
+
function NavItem({ icon, label, active, indent, collapsed, trailing, onClick }) {
|
|
5
10
|
const [hover, setHover] = React.useState(false);
|
|
6
11
|
return (
|
|
7
|
-
<div role="button" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
8
|
-
style={{
|
|
9
|
-
<Icon name={icon} size={indent ? 14 : 16}
|
|
10
|
-
{!collapsed ? <span style={
|
|
11
|
-
{!collapsed &&
|
|
12
|
+
<div role="button" tabIndex={0} title={collapsed ? label : undefined} aria-current={active ? "page" : undefined} onClick={onClick} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick && onClick(); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
13
|
+
style={{ minHeight: indent ? 32 : 36, borderRadius: "var(--radius-md)", display: "flex", alignItems: "center", justifyContent: collapsed ? "center" : "flex-start", gap: 10, padding: indent ? "6px 10px" : "8px 10px", boxSizing: "border-box", cursor: "pointer", userSelect: "none", background: active ? "var(--sb-active)" : hover ? "var(--sb-hover)" : "transparent", color: active ? "var(--sb-active-text)" : "var(--sb-text)", boxShadow: active ? "var(--sb-shadow)" : "none", transition: "var(--t-hover)" }}>
|
|
14
|
+
{icon ? <span style={{ display: "inline-flex", flexShrink: 0 }}><Icon name={icon} size={indent ? 14 : 16} /></span> : null}
|
|
15
|
+
{!collapsed ? <span style={LABEL}>{label}</span> : null}
|
|
16
|
+
{!collapsed && trailing ? <span style={{ display: "inline-flex", flexShrink: 0, opacity: 0.6 }}><Icon name="chevron-right" size={14} /></span> : null}
|
|
12
17
|
</div>
|
|
13
18
|
);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
* Keybricks app sidebar — 240px
|
|
18
|
-
*
|
|
22
|
+
* Keybricks app sidebar — 240px, 64px icon rail when collapsed. A 56px brand head (no rule), a 2px-gap list, pinned footer items and a version line.
|
|
23
|
+
* tone: "recessed" (grey, default) | "flush" (white) | "contrast" (dark) | "custom" (+ color: a SIDEBAR_COLORS key or hex).
|
|
24
|
+
* items / footerItems: [{ icon, label, trailing?, children? }] — trailing draws a chevron (the item opens a SidePanel); children make a collapsible group.
|
|
19
25
|
*/
|
|
20
|
-
export function Sidebar({ items =
|
|
21
|
-
const
|
|
22
|
-
const
|
|
26
|
+
export function Sidebar({ items = KEYBRICKS_NAV, footerItems = KEYBRICKS_NAV_FOOTER, active, onSelect, collapsed = false, onToggleCollapse, logo = "assets/keybricks-logo.png", brand = "Keybricks", version, tone = "recessed", color, labels, height = "100%", style }) {
|
|
27
|
+
const L = { collapse: "Collapse", expand: "Expand", ...labels };
|
|
28
|
+
const t = chromeTone(tone, color);
|
|
29
|
+
const [open, setOpen] = React.useState(() => items.filter((i) => i.children && i.children.some((c) => (c.label || c) === active)).map((i) => i.label));
|
|
30
|
+
const toggle = (l) => setOpen(open.includes(l) ? open.filter((x) => x !== l) : [...open, l]);
|
|
31
|
+
const pick = (l) => onSelect && onSelect(l);
|
|
32
|
+
const list = (arr) => arr.map((it) => it.children ? (
|
|
33
|
+
<div key={it.label} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
34
|
+
<NavItem icon={it.icon} label={it.label} collapsed={collapsed} trailing={false} active={collapsed && it.children.some((c) => (c.label || c) === active)} onClick={() => (collapsed ? pick(it.label) : toggle(it.label))} />
|
|
35
|
+
{!collapsed && open.includes(it.label) ? <div style={{ marginLeft: 24, display: "flex", flexDirection: "column", gap: 2 }}>{it.children.map((c) => { const lb = c.label || c; return <NavItem key={lb} icon={c.icon} label={lb} indent active={active === lb} onClick={() => pick(lb)} />; })}</div> : null}
|
|
36
|
+
</div>
|
|
37
|
+
) : <NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} trailing={it.trailing} active={active === it.label} onClick={() => pick(it.label)} />);
|
|
23
38
|
return (
|
|
24
|
-
<aside style={{ width: collapsed ? "var(--rail-w)" : "var(--sidebar-w)", height, background: "var(--bg
|
|
25
|
-
<div style={{ height: "var(--topbar-h)",
|
|
26
|
-
<img src={logo} alt="" style={{ width: 24, height: 24, objectFit: "contain" }} />
|
|
27
|
-
{!collapsed ? <span style={{ fontFamily: "var(--font-sans)",
|
|
39
|
+
<aside data-theme={t.theme} style={{ ...t.vars, width: collapsed ? "var(--rail-w)" : "var(--sidebar-w)", height, background: "var(--sb-bg)", borderRight: "1px solid var(--sb-border)", color: "var(--sb-text)", display: "flex", flexDirection: "column", boxSizing: "border-box", flexShrink: 0, transition: "width .15s ease", ...style }}>
|
|
40
|
+
<div style={{ height: "var(--topbar-h)", display: "flex", alignItems: "center", gap: 10, padding: "0 16px", boxSizing: "border-box", justifyContent: collapsed ? "center" : "flex-start", flexShrink: 0 }}>
|
|
41
|
+
<img src={logo} alt="" style={{ width: 24, height: 24, objectFit: "contain", flexShrink: 0 }} />
|
|
42
|
+
{!collapsed ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-md)", lineHeight: "var(--leading-md)", letterSpacing: "var(--tracking-md)", fontWeight: "var(--weight-semibold)", color: "var(--sb-brand)", whiteSpace: "nowrap" }}>{brand}</span> : null}
|
|
28
43
|
</div>
|
|
29
|
-
<nav style={{ flex: 1, overflowY: "auto", padding: 8, display: "flex", flexDirection: "column", gap:
|
|
30
|
-
{onToggleCollapse ? <NavItem icon="panel-left"
|
|
31
|
-
{items
|
|
32
|
-
<div key={it.label}>
|
|
33
|
-
<div role="button" tabIndex={0} data-wash="" onClick={() => toggleSection(it.label)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleSection(it.label); } }} style={{ background: "var(--wash, transparent)", transition: "var(--t-hover)", height: "var(--h-subnav-item)", display: "flex", alignItems: "center", gap: 10, padding: collapsed ? "6px 10px" : "6px 10px", justifyContent: collapsed ? "center" : "flex-start", cursor: "pointer", color: "var(--text-body)", borderRadius: "var(--radius-md)", boxSizing: "border-box" }}>
|
|
34
|
-
<Icon name={it.icon} size={16} />
|
|
35
|
-
{!collapsed ? <><span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", letterSpacing: "var(--tracking-base)", flex: 1 }}>{it.label}</span><Icon name={openSections.includes(it.label) ? "chevron-up" : "chevron-down"} size={14} /></> : null}
|
|
36
|
-
</div>
|
|
37
|
-
{!collapsed && openSections.includes(it.label) ? (
|
|
38
|
-
<div style={{ marginLeft: 24, display: "flex", flexDirection: "column", gap: 2, paddingTop: 2 }}>
|
|
39
|
-
{it.children.map((c) => <NavItem key={c.label} icon={c.icon} label={c.label} indent active={active === c.label} onClick={() => onSelect && onSelect(c.label)} />)}
|
|
40
|
-
</div>
|
|
41
|
-
) : null}
|
|
42
|
-
</div>
|
|
43
|
-
) : (
|
|
44
|
-
<NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} active={active === it.label} onClick={() => onSelect && onSelect(it.label)} />
|
|
45
|
-
))}
|
|
44
|
+
<nav style={{ flex: 1, overflowY: "auto", overflowX: "hidden", padding: 8, display: "flex", flexDirection: "column", gap: 2 }}>
|
|
45
|
+
{onToggleCollapse ? <NavItem icon={collapsed ? "panel-left" : "chevron-left"} label={collapsed ? L.expand : L.collapse} collapsed={collapsed} onClick={onToggleCollapse} /> : null}
|
|
46
|
+
{list(items)}
|
|
46
47
|
</nav>
|
|
47
|
-
<div style={{
|
|
48
|
-
{footerItems
|
|
48
|
+
<div style={{ padding: 8, display: "flex", flexDirection: "column", gap: 2, flexShrink: 0 }}>
|
|
49
|
+
{list(footerItems)}
|
|
50
|
+
{version ? <span title={version} style={{ padding: collapsed ? "8px 0 4px" : "8px 10px 4px", textAlign: collapsed ? "center" : "left", fontFamily: "var(--font-sans)", fontSize: collapsed ? "var(--text-glyph-min)" : "var(--text-xs)", lineHeight: collapsed ? "var(--leading-2xs)" : "var(--leading-xs)", color: "var(--sb-muted)", whiteSpace: "nowrap", overflow: "hidden" }}>{collapsed ? "v" + String(version).split(" ").pop().replace(/^v/i, "") : version}</span> : null}
|
|
49
51
|
</div>
|
|
50
52
|
</aside>
|
|
51
53
|
);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/**
|
|
55
|
-
export
|
|
56
|
-
{ icon: "house", label: "Home" },
|
|
57
|
-
{ icon: "file-text", label: "Documents" },
|
|
58
|
-
{ icon: "layout-grid", label: "Business Intelligence" },
|
|
59
|
-
{ icon: "database", label: "Data Integration" },
|
|
60
|
-
{ icon: "scale", label: "Reconciliation" },
|
|
61
|
-
{ icon: "network", label: "Consolidation" },
|
|
62
|
-
{
|
|
63
|
-
icon: "folder-cog", label: "Management", children: [
|
|
64
|
-
{ icon: "box", label: "Modeling" },
|
|
65
|
-
{ icon: "workflow", label: "Workflow" },
|
|
66
|
-
{ icon: "shield", label: "Security Management" },
|
|
67
|
-
{ icon: "refresh-cw", label: "System Migration" },
|
|
68
|
-
{ icon: "chart-column", label: "Business Logic" },
|
|
69
|
-
{ icon: "list-todo", label: "Tasks Management" },
|
|
70
|
-
{ icon: "circle-check", label: "Validation Rules" },
|
|
71
|
-
{ icon: "settings-2", label: "Control Center" },
|
|
72
|
-
{ icon: "network", label: "Reconciliation" },
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
];
|
|
56
|
+
/** Nav models and tones — defined in framework-neutral modules, shared with the Vue entry. */
|
|
57
|
+
export { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT, SIDEBAR_COLORS };
|
|
@@ -1,54 +1,50 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const props = defineProps({
|
|
10
|
-
items: { type: Array, default: () => [] },
|
|
11
|
-
footerItems: { type: Array, default: () => [{ icon: 'circle-help', label: 'Help' }, { icon: 'settings', label: 'Settings' }, { icon: 'moon', label: 'Dark Mode' }] },
|
|
12
|
-
active: String, collapsed: { type: Boolean, default: false },
|
|
13
|
-
logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' },
|
|
14
|
-
height: { type: [String, Number], default: '100%' }, style: Object, canCollapse: Boolean,
|
|
15
|
-
})
|
|
4
|
+
import { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER } from './nav-model.js'
|
|
5
|
+
import { chromeTone } from './chrome-tones.js'
|
|
6
|
+
// Keybricks app sidebar — 240px, 64px icon rail when collapsed. 56px brand head (no rule), 2px-gap list, pinned footer, version line.
|
|
7
|
+
// tone: "recessed" (grey, default) | "flush" (white) | "contrast" (dark) | "custom" (+ color: a SIDEBAR_COLORS key or hex).
|
|
8
|
+
// items / footerItems: [{ icon, label, trailing?, children? }] — trailing draws a chevron (the item opens a SidePanel); children make a collapsible group.
|
|
9
|
+
const props = defineProps({ items: { type: Array, default: () => KEYBRICKS_NAV }, footerItems: { type: Array, default: () => KEYBRICKS_NAV_FOOTER }, active: String, collapsed: Boolean, logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' }, version: String, tone: { type: String, default: 'recessed' }, color: String, labels: Object, height: { type: String, default: '100%' }, showToggle: { type: Boolean, default: true }, style: Object })
|
|
16
10
|
const emit = defineEmits(['select', 'toggleCollapse'])
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
const L = computed(() => ({ collapse: 'Collapse', expand: 'Expand', ...(props.labels || {}) }))
|
|
12
|
+
const t = computed(() => chromeTone(props.tone, props.color))
|
|
13
|
+
const hover = ref(null)
|
|
14
|
+
const open = ref(props.items.filter(i => i.children && i.children.some(c => (c.label || c) === props.active)).map(i => i.label))
|
|
15
|
+
const toggle = (l) => { open.value = open.value.includes(l) ? open.value.filter(x => x !== l) : [...open.value, l] }
|
|
16
|
+
const lb = (c) => c.label || c
|
|
17
|
+
const LABEL = { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: '18px', letterSpacing: 'var(--tracking-base)', fontWeight: 'var(--weight-medium)' }
|
|
18
|
+
function row(key, active, indent) { return { minHeight: indent ? '32px' : '36px', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', justifyContent: props.collapsed ? 'center' : 'flex-start', gap: '10px', padding: indent ? '6px 10px' : '8px 10px', boxSizing: 'border-box', cursor: 'pointer', userSelect: 'none', background: active ? 'var(--sb-active)' : hover.value === key ? 'var(--sb-hover)' : 'transparent', color: active ? 'var(--sb-active-text)' : 'var(--sb-text)', boxShadow: active ? 'var(--sb-shadow)' : 'none', transition: 'var(--t-hover)' } }
|
|
19
|
+
function key(e, fn) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fn() } }
|
|
20
|
+
const groups = computed(() => [{ id: 'main', list: props.items }, { id: 'foot', list: props.footerItems }])
|
|
21
|
+
const versionText = computed(() => props.collapsed && props.version ? 'v' + String(props.version).split(' ').pop().replace(/^v/i, '') : props.version)
|
|
20
22
|
</script>
|
|
21
23
|
<template>
|
|
22
|
-
<aside :style="{ width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--bg
|
|
23
|
-
<div :style="{ height: 'var(--topbar-h)',
|
|
24
|
-
<img :src="logo" alt="" style="width:24px;height:24px;object-fit:contain" />
|
|
25
|
-
<span v-if="!collapsed" style="font-family:var(--font-sans);font-
|
|
24
|
+
<aside :data-theme="t.theme" :style="{ ...t.vars, width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--sb-bg)', borderRight: '1px solid var(--sb-border)', color: 'var(--sb-text)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, transition: 'width .15s ease', ...style }">
|
|
25
|
+
<div :style="{ height: 'var(--topbar-h)', display: 'flex', alignItems: 'center', gap: '10px', padding: '0 16px', boxSizing: 'border-box', justifyContent: collapsed ? 'center' : 'flex-start', flexShrink: 0 }">
|
|
26
|
+
<img :src="logo" alt="" style="width:24px;height:24px;object-fit:contain;flex-shrink:0" />
|
|
27
|
+
<span v-if="!collapsed" style="font-family:var(--font-sans);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);font-weight:var(--weight-semibold);color:var(--sb-brand);white-space:nowrap">{{ brand }}</span>
|
|
26
28
|
</div>
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
<template v-for="g in groups" :key="g.id">
|
|
30
|
+
<component :is="g.id === 'main' ? 'nav' : 'div'" :style="g.id === 'main' ? { flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '8px', display: 'flex', flexDirection: 'column', gap: '2px' } : { padding: '8px', display: 'flex', flexDirection: 'column', gap: '2px', flexShrink: 0 }">
|
|
31
|
+
<div v-if="g.id === 'main' && showToggle" role="button" tabindex="0" :title="collapsed ? L.expand : undefined" :style="row('__toggle', false)" @click="emit('toggleCollapse')" @keydown="e => key(e, () => emit('toggleCollapse'))" @mouseenter="hover = '__toggle'" @mouseleave="hover = null">
|
|
32
|
+
<span style="display:inline-flex;flex-shrink:0"><Icon :name="collapsed ? 'panel-left' : 'chevron-left'" :size="16" /></span><span v-if="!collapsed" :style="LABEL">{{ collapsed ? L.expand : L.collapse }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<template v-for="it in g.list" :key="it.label">
|
|
35
|
+
<div role="button" tabindex="0" :title="collapsed ? it.label : undefined" :aria-current="active === it.label ? 'page' : undefined" :style="row(it.label, active === it.label || (collapsed && it.children && it.children.some(c => lb(c) === active)))" @click="it.children && !collapsed ? toggle(it.label) : emit('select', it.label)" @keydown="e => key(e, () => it.children && !collapsed ? toggle(it.label) : emit('select', it.label))" @mouseenter="hover = it.label" @mouseleave="hover = null">
|
|
36
|
+
<span v-if="it.icon" style="display:inline-flex;flex-shrink:0"><Icon :name="it.icon" :size="16" /></span>
|
|
37
|
+
<span v-if="!collapsed" :style="LABEL">{{ it.label }}</span>
|
|
38
|
+
<span v-if="!collapsed && it.trailing" style="display:inline-flex;flex-shrink:0;opacity:.6"><Icon name="chevron-right" :size="14" /></span>
|
|
34
39
|
</div>
|
|
35
|
-
<div v-if="!collapsed &&
|
|
36
|
-
<div v-for="c in it.children" :key="c
|
|
40
|
+
<div v-if="it.children && !collapsed && open.includes(it.label)" style="margin-left:24px;display:flex;flex-direction:column;gap:2px">
|
|
41
|
+
<div v-for="c in it.children" :key="lb(c)" role="button" tabindex="0" :aria-current="active === lb(c) ? 'page' : undefined" :style="row('c:' + lb(c), active === lb(c), true)" @click="emit('select', lb(c))" @keydown="e => key(e, () => emit('select', lb(c)))" @mouseenter="hover = 'c:' + lb(c)" @mouseleave="hover = null">
|
|
42
|
+
<span v-if="c.icon" style="display:inline-flex;flex-shrink:0"><Icon :name="c.icon" :size="14" /></span><span :style="LABEL">{{ lb(c) }}</span>
|
|
43
|
+
</div>
|
|
37
44
|
</div>
|
|
38
|
-
</
|
|
39
|
-
<
|
|
40
|
-
</
|
|
41
|
-
</
|
|
42
|
-
<div style="border-top:1px solid var(--border-card);padding:8px;display:flex;flex-direction:column;flex-shrink:0">
|
|
43
|
-
<div v-for="f in footerItems" :key="f.label" role="button" tabindex="0" @click="emit('select', f.label)" :style="itemStyle(false, false)"><Icon :name="f.icon" :size="16" /><span v-if="!collapsed" style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1">{{ f.label }}</span></div>
|
|
44
|
-
</div>
|
|
45
|
+
</template>
|
|
46
|
+
<span v-if="g.id === 'foot' && version" :title="version" :style="{ padding: collapsed ? '8px 0 4px' : '8px 10px 4px', textAlign: collapsed ? 'center' : 'left', fontFamily: 'var(--font-sans)', fontSize: collapsed ? 'var(--text-glyph-min)' : 'var(--text-xs)', lineHeight: collapsed ? 'var(--leading-2xs)' : 'var(--leading-xs)', color: 'var(--sb-muted)', whiteSpace: 'nowrap', overflow: 'hidden' }">{{ versionText }}</span>
|
|
47
|
+
</component>
|
|
48
|
+
</template>
|
|
45
49
|
</aside>
|
|
46
50
|
</template>
|
|
47
|
-
|
|
48
|
-
<!-- KEYBRICKS_NAV — default Keybricks nav model (Home dashboard), same shape as Sidebar.jsx's export, for reference:
|
|
49
|
-
export const KEYBRICKS_NAV = [
|
|
50
|
-
{ icon: 'house', label: 'Home' }, { icon: 'file-text', label: 'Documents' },
|
|
51
|
-
{ icon: 'layout-grid', label: 'Business Intelligence' }, { icon: 'database', label: 'Data Integration' },
|
|
52
|
-
{ icon: 'scale', label: 'Reconciliation' }, { icon: 'network', label: 'Consolidation' },
|
|
53
|
-
{ icon: 'folder-cog', label: 'Management', children: [ ... ] },
|
|
54
|
-
] -->
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** 56px top bar
|
|
2
|
+
/** 56px top bar: bare search left; dark AI Assistant, notifications, help and the user trigger right. The trigger opens a UserMenu. */
|
|
3
3
|
export interface TopBarProps {
|
|
4
4
|
placeholder?: string;
|
|
5
|
-
/** Replaces the default bell + AI Assistant pair */
|
|
6
|
-
actions?: React.ReactNode;
|
|
7
5
|
/** Receives the new search string */
|
|
8
6
|
onSearch?: (value: string) => void;
|
|
7
|
+
/** Replaces the whole right-hand group */
|
|
8
|
+
actions?: React.ReactNode;
|
|
9
|
+
aiLabel?: string;
|
|
10
|
+
onAi?: () => void;
|
|
11
|
+
onNotifications?: () => void;
|
|
12
|
+
onHelp?: () => void;
|
|
13
|
+
notificationsLabel?: string;
|
|
14
|
+
helpLabel?: string;
|
|
15
|
+
/** Shows the user trigger (name + chevron); omit to hide it */
|
|
16
|
+
user?: { name: string; email?: string };
|
|
17
|
+
/** Content of the menu the trigger opens (default: a UserMenu with name and email) */
|
|
18
|
+
userMenu?: React.ReactNode;
|
|
9
19
|
style?: React.CSSProperties;
|
|
10
20
|
}
|
|
11
21
|
export declare function TopBar(props: TopBarProps): JSX.Element;
|
|
22
|
+
/** Account menu: 288px, avatar + name + email, an Appearance row (icon-only light / dark / system SegmentedControl), then ContextMenu rows. */
|
|
23
|
+
export interface UserMenuProps {
|
|
24
|
+
name?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
/** Label of the selected theme option ("Light" | "Dark" | "System" by default). The Appearance row shows when theme or onThemeChange is set. Vue: v-model:theme. */
|
|
27
|
+
theme?: string;
|
|
28
|
+
onThemeChange?: (label: string) => void;
|
|
29
|
+
themeOptions?: { label: string; icon: string }[];
|
|
30
|
+
appearanceLabel?: string;
|
|
31
|
+
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean }[];
|
|
32
|
+
onSelect?: (label: string) => void;
|
|
33
|
+
style?: React.CSSProperties;
|
|
34
|
+
}
|
|
35
|
+
export declare function UserMenu(props: UserMenuProps): JSX.Element;
|
|
36
|
+
export declare const THEME_OPTIONS: { label: string; icon: string }[];
|
|
12
37
|
/** Editor toolbar: back, 14px/600 title, "• Editing", right actions. */
|
|
13
38
|
export interface PageToolbarProps {
|
|
14
39
|
title?: string;
|
|
@@ -3,17 +3,64 @@ import { Icon } from "../icons/Icon.jsx";
|
|
|
3
3
|
import { IconButton } from "../buttons/IconButton.jsx";
|
|
4
4
|
import { Button } from "../buttons/Button.jsx";
|
|
5
5
|
import { SearchInput } from "../forms/SearchInput.jsx";
|
|
6
|
+
import { Avatar } from "../display/Avatar.jsx";
|
|
7
|
+
import { SegmentedControl } from "../tabs/SegmentedControl.jsx";
|
|
8
|
+
import { ContextMenu } from "../feedback/ContextMenu.jsx";
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
const TXT = { fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)" };
|
|
11
|
+
const ONE = { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
|
|
12
|
+
export const THEME_OPTIONS = [{ label: "Light", icon: "sun" }, { label: "Dark", icon: "moon" }, { label: "System", icon: "monitor" }];
|
|
13
|
+
|
|
14
|
+
/** Account menu (opened from the TopBar's user trigger): avatar + name + email, an Appearance row with a light / dark / system switch, then rows.
|
|
15
|
+
* theme: "Light" | "Dark" | "System" (labels of themeOptions). items: [{ icon, label, danger?, divider? }] → onSelect(label). */
|
|
16
|
+
export function UserMenu({ name, email, theme, onThemeChange, themeOptions = THEME_OPTIONS, appearanceLabel = "Appearance", items = [], onSelect, style }) {
|
|
17
|
+
return (
|
|
18
|
+
<div role="menu" style={{ width: 288, background: "var(--bg-raised)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-menu)", padding: 4, boxSizing: "border-box", ...style }}>
|
|
19
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 12px" }}>
|
|
20
|
+
<Avatar name={name} size={32} />
|
|
21
|
+
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
|
22
|
+
<span style={{ ...TXT, ...ONE, fontWeight: "var(--weight-medium)", color: "var(--text-heading)" }}>{name}</span>
|
|
23
|
+
{email ? <span style={{ ...ONE, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-secondary)" }}>{email}</span> : null}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div style={{ height: 1, background: "var(--border-subtle)", margin: "4px 0" }} />
|
|
27
|
+
{onThemeChange || theme ? <div style={{ height: 40, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "0 12px", boxSizing: "border-box" }}>
|
|
28
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}><span style={{ display: "inline-flex", color: "var(--text-body)" }}><Icon name="sliders" size={16} /></span><span style={{ ...TXT, ...ONE, color: "var(--text-heading)" }}>{appearanceLabel}</span></div>
|
|
29
|
+
<SegmentedControl iconOnly options={themeOptions} value={theme} onChange={onThemeChange} style={{ flexShrink: 0 }} />
|
|
30
|
+
</div> : null}
|
|
31
|
+
{items.length ? <ContextMenu items={items} onSelect={onSelect} style={{ display: "flex", border: "none", boxShadow: "none", padding: 0, minWidth: 0, background: "transparent" }} /> : null}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 56px top bar: bare search on the left; AI Assistant (dark), notifications, help and the user trigger on the right.
|
|
37
|
+
* The user trigger opens a UserMenu below it (closes on outside click). actions replaces the right-hand group entirely. */
|
|
38
|
+
export function TopBar({ placeholder = "Search documents, workflows, data...", onSearch, actions, aiLabel = "AI Assistant", onAi, onNotifications, onHelp, notificationsLabel = "Notifications", helpLabel = "Help", user, userMenu, style }) {
|
|
39
|
+
const [open, setOpen] = React.useState(false);
|
|
40
|
+
const ref = React.useRef(null);
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (!open) return;
|
|
43
|
+
const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
|
44
|
+
const esc = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
45
|
+
document.addEventListener("mousedown", off); document.addEventListener("keydown", esc);
|
|
46
|
+
return () => { document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); };
|
|
47
|
+
}, [open]);
|
|
48
|
+
const [hover, setHover] = React.useState(false);
|
|
9
49
|
return (
|
|
10
|
-
<header style={{ height: "var(--topbar-h)", background: "var(--bg-surface)", borderBottom: "1px solid var(--border-card)", display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 24px", boxSizing: "border-box", flexShrink: 0, ...style }}>
|
|
11
|
-
<SearchInput bare placeholder={placeholder} onChange={onSearch} style={{
|
|
12
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
50
|
+
<header style={{ height: "var(--topbar-h)", background: "var(--bg-surface)", borderBottom: "1px solid var(--border-card)", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, padding: "0 24px", boxSizing: "border-box", flexShrink: 0, minWidth: 0, position: "relative", ...style }}>
|
|
51
|
+
<SearchInput bare placeholder={placeholder} onChange={onSearch} style={{ flex: "0 1 576px", minWidth: 0 }} />
|
|
52
|
+
<div ref={ref} style={{ display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
|
|
13
53
|
{actions || (<>
|
|
14
|
-
<
|
|
15
|
-
<
|
|
54
|
+
<Button variant="dark" icon="ai" onClick={onAi}>{aiLabel}</Button>
|
|
55
|
+
<IconButton icon="bell" label={notificationsLabel} title={notificationsLabel} onClick={onNotifications} />
|
|
56
|
+
<IconButton icon="circle-help" label={helpLabel} title={helpLabel} onClick={onHelp} />
|
|
57
|
+
{user ? <div role="button" tabIndex={0} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(!open)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(!open); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
58
|
+
style={{ height: 32, display: "flex", alignItems: "center", gap: 8, padding: "0 8px 0 10px", borderRadius: "var(--radius-md)", cursor: "pointer", userSelect: "none", background: open ? "var(--bg-active)" : hover ? "var(--bg-hover)" : "transparent", transition: "var(--t-hover)" }}>
|
|
59
|
+
<span style={{ ...TXT, fontWeight: "var(--weight-medium)", color: "var(--text-heading)", whiteSpace: "nowrap" }}>{user.name}</span>
|
|
60
|
+
<span style={{ display: "inline-flex", color: "var(--text-muted)" }}><Icon name="chevron-down" size={16} /></span>
|
|
61
|
+
</div> : null}
|
|
16
62
|
</>)}
|
|
63
|
+
{open && user ? <div style={{ position: "absolute", right: 24, top: 52, zIndex: "var(--z-menu)" }}>{userMenu || <UserMenu name={user.name} email={user.email} />}</div> : null}
|
|
17
64
|
</div>
|
|
18
65
|
</header>
|
|
19
66
|
);
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
2
4
|
import IconButton from '../buttons/IconButton.vue'
|
|
3
5
|
import Button from '../buttons/Button.vue'
|
|
4
6
|
import SearchInput from '../forms/SearchInput.vue'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import UserMenu from './UserMenu.vue'
|
|
8
|
+
// 56px top bar: bare search on the left; AI Assistant (dark), notifications, help and the user trigger on the right.
|
|
9
|
+
// The user trigger opens a UserMenu below it (closes on outside click / Escape); #user-menu slot replaces its content.
|
|
10
|
+
// #actions slot replaces the whole right-hand group.
|
|
11
|
+
const props = defineProps({ placeholder: { type: String, default: 'Search documents, workflows, data...' }, aiLabel: { type: String, default: 'AI Assistant' }, notificationsLabel: { type: String, default: 'Notifications' }, helpLabel: { type: String, default: 'Help' }, user: Object, style: Object })
|
|
12
|
+
const emit = defineEmits(['search', 'ai', 'notifications', 'help'])
|
|
13
|
+
const open = ref(false), hover = ref(false), root = ref(null)
|
|
14
|
+
const off = (e) => { if (open.value && root.value && !root.value.contains(e.target)) open.value = false }
|
|
15
|
+
const esc = (e) => { if (e.key === 'Escape') open.value = false }
|
|
16
|
+
onMounted(() => { document.addEventListener('mousedown', off); document.addEventListener('keydown', esc) })
|
|
17
|
+
onUnmounted(() => { document.removeEventListener('mousedown', off); document.removeEventListener('keydown', esc) })
|
|
18
|
+
function key(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open.value = !open.value } }
|
|
8
19
|
</script>
|
|
9
20
|
<template>
|
|
10
|
-
<header :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, ...style }">
|
|
11
|
-
<SearchInput bare :placeholder="placeholder" @update:modelValue="v => emit('search', v)" style="
|
|
12
|
-
<div style="display:flex;align-items:center;gap:8px">
|
|
21
|
+
<header :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, minWidth: 0, position: 'relative', ...style }">
|
|
22
|
+
<SearchInput bare :placeholder="placeholder" @update:modelValue="v => emit('search', v)" style="flex:0 1 576px;min-width:0" />
|
|
23
|
+
<div ref="root" style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
13
24
|
<slot name="actions">
|
|
14
|
-
<
|
|
15
|
-
<
|
|
25
|
+
<Button variant="dark" icon="ai" @click="emit('ai')">{{ aiLabel }}</Button>
|
|
26
|
+
<IconButton icon="bell" :label="notificationsLabel" :title="notificationsLabel" @click="emit('notifications')" />
|
|
27
|
+
<IconButton icon="circle-help" :label="helpLabel" :title="helpLabel" @click="emit('help')" />
|
|
28
|
+
<div v-if="user" role="button" tabindex="0" aria-haspopup="menu" :aria-expanded="open" @click="open = !open" @keydown="key" @mouseenter="hover = true" @mouseleave="hover = false"
|
|
29
|
+
:style="{ height: '32px', display: 'flex', alignItems: 'center', gap: '8px', padding: '0 8px 0 10px', borderRadius: 'var(--radius-md)', cursor: 'pointer', userSelect: 'none', background: open ? 'var(--bg-active)' : hover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }">
|
|
30
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading);white-space:nowrap">{{ user.name }}</span>
|
|
31
|
+
<span style="display:inline-flex;color:var(--text-muted)"><Icon name="chevron-down" :size="16" /></span>
|
|
32
|
+
</div>
|
|
16
33
|
</slot>
|
|
34
|
+
<div v-if="open && user" style="position:absolute;right:24px;top:52px;z-index:var(--z-menu)"><slot name="user-menu"><UserMenu :name="user.name" :email="user.email" :show-appearance="false" /></slot></div>
|
|
17
35
|
</div>
|
|
18
36
|
</header>
|
|
19
37
|
</template>
|
|
@@ -165,7 +165,7 @@ function Row({ item, state, cfg, api }) {
|
|
|
165
165
|
<Highlight text={titleOf(node)} query={cfg.searchValue} />
|
|
166
166
|
</span>
|
|
167
167
|
{node.tag ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: 1, padding: "2px 5px", borderRadius: "var(--radius-sm)", background: "var(--bg-inset)", color: "var(--text-secondary)", flexShrink: 0 }}>{node.tag}</span> : null}
|
|
168
|
-
{node.count != null ? <span style={{ marginLeft: "auto", fontFamily: "var(--font-
|
|
168
|
+
{node.count != null ? <span style={{ marginLeft: "auto", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", color: "var(--text-muted)", paddingLeft: 8 }}>{node.count}</span> : null}
|
|
169
169
|
{cfg.rowActions && !disabled ? <span style={{ marginLeft: node.count != null ? 6 : "auto", display: "flex", gap: 2, opacity: hover ? 1 : 0, transition: "var(--t-hover)" }} onClick={(e) => e.stopPropagation()}>{cfg.rowActions(node)}</span> : null}
|
|
170
170
|
</div>
|
|
171
171
|
{open && loading && !kids.length ? (
|
|
@@ -9,6 +9,8 @@ const docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', pu
|
|
|
9
9
|
const INDENT = 18
|
|
10
10
|
const props = defineProps({
|
|
11
11
|
items: { type: Array, default: () => [] },
|
|
12
|
+
treeData: Array, // alias of items (same as Tree.jsx)
|
|
13
|
+
expandedKeys: Array, selectedKeys: Array, checkedKeys: Array, // controlled: the parent's value wins whenever it changes
|
|
12
14
|
defaultExpanded: { type: Array, default: () => [] },
|
|
13
15
|
defaultActive: String,
|
|
14
16
|
defaultSelected: { type: Array, default: () => [] },
|
|
@@ -24,12 +26,15 @@ const props = defineProps({
|
|
|
24
26
|
})
|
|
25
27
|
const emit = defineEmits(['select', 'check', 'expand', 'drop'])
|
|
26
28
|
|
|
27
|
-
const data = ref(JSON.parse(JSON.stringify(props.items)))
|
|
29
|
+
const data = ref(JSON.parse(JSON.stringify(props.treeData || props.items)))
|
|
28
30
|
// Compare by value: re-seeding on identity alone would wipe async-loaded children.
|
|
29
|
-
watch(() => JSON.stringify(props.items), (next, prev) => { if (next !== prev) data.value = JSON.parse(next) })
|
|
31
|
+
watch(() => JSON.stringify(props.treeData || props.items), (next, prev) => { if (next !== prev) data.value = JSON.parse(next) })
|
|
30
32
|
const expanded = ref([...props.defaultExpanded])
|
|
31
33
|
const selected = ref(props.defaultActive ? [props.defaultActive] : [...props.defaultSelected])
|
|
32
34
|
const checked = ref([...props.defaultChecked])
|
|
35
|
+
watch(() => props.expandedKeys, (v) => { if (v) expanded.value = [...v] }, { immediate: true })
|
|
36
|
+
watch(() => props.selectedKeys, (v) => { if (v) selected.value = [...v] }, { immediate: true })
|
|
37
|
+
watch(() => props.checkedKeys, (v) => { if (v) checked.value = [...v] }, { immediate: true })
|
|
33
38
|
const loading = ref([]); const loaded = ref([]); const focus = ref(null)
|
|
34
39
|
const dragKey = ref(null); const drop = ref(null)
|
|
35
40
|
|
|
@@ -245,14 +250,14 @@ const dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)
|
|
|
245
250
|
<template v-for="(p, i) in parts(titleOf(row.n))" :key="i"><mark v-if="p.hit" style="background:var(--soft-bg);color:var(--info-fg);font-weight:var(--weight-medium);padding:0;border-radius:2px">{{ p.t }}</mark><template v-else>{{ p.t }}</template></template>
|
|
246
251
|
</span>
|
|
247
252
|
<span v-if="row.n.tag" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:1;padding:2px 5px;border-radius:var(--radius-sm);background:var(--bg-inset);color:var(--text-secondary);flex-shrink:0">{{ row.n.tag }}</span>
|
|
248
|
-
<span v-if="row.n.count != null" style="margin-left:auto;font-family:var(--font-
|
|
253
|
+
<span v-if="row.n.count != null" style="margin-left:auto;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-muted);padding-left:8px">{{ row.n.count }}</span>
|
|
249
254
|
</div>
|
|
250
255
|
<div v-if="!rows.length" style="padding:10px 8px;font-family:var(--font-sans);font-size:var(--text-base);color:var(--text-muted)">No matching items</div>
|
|
251
256
|
</div>
|
|
252
257
|
</template>
|
|
253
258
|
<style scoped>
|
|
254
259
|
.kb-tree-row:not(.is-disabled):hover{background:var(--bg-hover)}
|
|
255
|
-
.kb-tree-row:focus-visible{outline:2px solid var(--focus-
|
|
260
|
+
.kb-tree-row:focus-visible{outline:2px solid var(--focus-border);outline-offset:-2px}
|
|
256
261
|
.kb-tree-spin{animation:kb-tree-spin 900ms linear infinite}
|
|
257
262
|
@keyframes kb-tree-spin{to{transform:rotate(360deg)}}
|
|
258
263
|
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import Icon from '../icons/Icon.vue'
|
|
3
|
+
import Avatar from '../display/Avatar.vue'
|
|
4
|
+
import SegmentedControl from '../tabs/SegmentedControl.vue'
|
|
5
|
+
import ContextMenu from '../feedback/ContextMenu.vue'
|
|
6
|
+
// Account menu (opened from the TopBar's user trigger): avatar + name + email, an Appearance row with a light / dark / system switch
|
|
7
|
+
// (v-model:theme, labels of themeOptions), then rows (items → @select(label)).
|
|
8
|
+
defineProps({ name: String, email: String, theme: String, themeOptions: { type: Array, default: () => [{ label: 'Light', icon: 'sun' }, { label: 'Dark', icon: 'moon' }, { label: 'System', icon: 'monitor' }] }, appearanceLabel: { type: String, default: 'Appearance' }, showAppearance: { type: Boolean, default: true }, items: { type: Array, default: () => [] }, style: Object })
|
|
9
|
+
const emit = defineEmits(['update:theme', 'select'])
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div role="menu" :style="{ width: '288px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '4px', boxSizing: 'border-box', ...style }">
|
|
13
|
+
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px">
|
|
14
|
+
<Avatar :name="name" :size="32" />
|
|
15
|
+
<div style="display:flex;flex-direction:column;min-width:0">
|
|
16
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ name }}</span>
|
|
17
|
+
<span v-if="email" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ email }}</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div style="height:1px;background:var(--border-subtle);margin:4px 0"></div>
|
|
21
|
+
<div v-if="showAppearance" style="height:40px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px;box-sizing:border-box">
|
|
22
|
+
<div style="display:flex;align-items:center;gap:10px;min-width:0"><span style="display:inline-flex;color:var(--text-body)"><Icon name="sliders" :size="16" /></span><span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ appearanceLabel }}</span></div>
|
|
23
|
+
<SegmentedControl icon-only :options="themeOptions" :model-value="theme" @update:model-value="v => emit('update:theme', v)" style="flex-shrink:0" />
|
|
24
|
+
</div>
|
|
25
|
+
<ContextMenu v-if="items.length" :items="items" @select="l => emit('select', l)" :style="{ display: 'flex', border: 'none', boxShadow: 'none', padding: 0, minWidth: 0, background: 'transparent' }" />
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|