@keyfunc/keybricks-ds 2.3.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 +10 -4
- package/DESIGN.md +44 -1
- package/INTEGRATION.md +24 -6
- package/_adherence.oxlintrc.json +28 -13
- 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/app-chrome.card.html +33 -21
- package/components/_cards/context-menu.card.html +7 -2
- package/components/_cards/icons-0-usage.card.html +88 -0
- package/components/_cards/icons-4-datasource.card.html +5 -0
- package/components/_cards/segmented-control.card.html +9 -3
- package/components/_cards/tabs.card.html +5 -2
- package/components/_shared/src-feedback.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 +4 -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/feedback.prompt.md +1 -1
- package/components/index.d.ts +1 -0
- package/components/index.js +4 -3
- 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 -37
- package/components/navigation/Sidebar.vue +40 -39
- 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/UserMenu.vue +27 -0
- package/components/navigation/chrome-tones.js +22 -0
- package/components/navigation/nav-model.js +21 -18
- package/components/navigation/navigation.prompt.md +2 -0
- package/components/tabs/CountBadge.vue +12 -0
- package/components/tabs/PanelTabs.vue +7 -3
- package/components/tabs/PillTabs.vue +7 -3
- package/components/tabs/SegmentedControl.d.ts +4 -2
- package/components/tabs/SegmentedControl.jsx +5 -3
- 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/tabs.prompt.md +2 -2
- package/package.json +2 -2
- package/readme.md +4 -4
- package/readme.zh-CN.md +2 -2
- package/tokens/layout.css +4 -3
- package/tokens/tokens.d.ts +2 -0
- package/tokens/tokens.js +4 -1
- package/tokens/tokens.json +8 -1
|
@@ -76,7 +76,7 @@ function mount(rootSelector, files){
|
|
|
76
76
|
var btn = document.createElement('button');
|
|
77
77
|
btn.type = 'button';
|
|
78
78
|
btn.innerHTML = iconSvg() + '<span style="margin-left:6px">View Code</span>';
|
|
79
|
-
btn.style.cssText = 'display:inline-flex;align-items:center;height:28px;padding:0
|
|
79
|
+
btn.style.cssText = 'display:inline-flex;align-items:center;height:28px;box-sizing:border-box;padding:0 8px;border-radius:var(--radius-md,8px);border:1px solid var(--border-control,#e5e5e5);background:var(--bg-surface,#fff);color:var(--text-secondary,#737373);font-size:var(--text-xs,12px);line-height:var(--leading-xs,16px);font-family:var(--font-sans,sans-serif);cursor:pointer;';
|
|
80
80
|
var panel = document.createElement('div');
|
|
81
81
|
panel.style.cssText = 'display:none;margin-top:10px;border:1px solid var(--border-control,#e5e5e5);border-radius:8px;overflow:hidden;background:var(--bg-inset,#fafafa);';
|
|
82
82
|
var headRow = document.createElement('div');
|
|
@@ -84,19 +84,19 @@ function mount(rootSelector, files){
|
|
|
84
84
|
var tabBar = document.createElement('div');
|
|
85
85
|
tabBar.style.cssText = 'display:flex;overflow-x:auto;flex:1;';
|
|
86
86
|
var fwBar = document.createElement('div');
|
|
87
|
-
fwBar.style.cssText = 'display:flex;gap:2px;padding:
|
|
87
|
+
fwBar.style.cssText = 'display:flex;align-items:center;flex-shrink:0;height:28px;box-sizing:border-box;gap:2px;padding:2px;margin:0 8px;border-radius:var(--radius-md,8px);background:var(--bg-inset,#fafafa);border:1px solid var(--border-control,#e5e5e5);';
|
|
88
88
|
var framework = 'react';
|
|
89
89
|
['React','Vue 3'].forEach(function(fw){
|
|
90
90
|
var fb = document.createElement('button');
|
|
91
91
|
fb.type = 'button'; fb.textContent = fw;
|
|
92
|
-
fb.style.cssText = 'font-size:12px;font-family:var(--font-sans,sans-serif);padding:
|
|
92
|
+
fb.style.cssText = 'display:inline-flex;align-items:center;height:22px;font-size:var(--text-xs,12px);line-height:var(--leading-xs,16px);font-weight:var(--weight-medium,500);font-family:var(--font-sans,sans-serif);padding:0 6px;border-radius:var(--radius-sm,4px);border:none;background:transparent;color:var(--text-secondary,#737373);cursor:pointer;white-space:nowrap;';
|
|
93
93
|
fb.onclick = function(){
|
|
94
94
|
framework = fw === 'Vue 3' ? 'vue' : 'react';
|
|
95
95
|
Array.prototype.forEach.call(fwBar.children, function(el){
|
|
96
96
|
var on = el.textContent === fw;
|
|
97
|
-
el.style.background = on ? 'var(--
|
|
98
|
-
el.style.color = on ? 'var(--
|
|
99
|
-
|
|
97
|
+
el.style.background = on ? 'var(--bg-surface,#fff)' : 'transparent'; el.style.boxShadow = on ? '0 1px 2px rgba(0,0,0,.08)' : 'none';
|
|
98
|
+
el.style.color = on ? 'var(--text-heading,#0a0a0a)' : 'var(--text-secondary,#737373)';
|
|
99
|
+
|
|
100
100
|
});
|
|
101
101
|
Array.prototype.forEach.call(tabBar.children, function(el, i){
|
|
102
102
|
if (tabs[i].html) return;
|
|
@@ -108,9 +108,9 @@ function mount(rootSelector, files){
|
|
|
108
108
|
};
|
|
109
109
|
fwBar.appendChild(fb);
|
|
110
110
|
});
|
|
111
|
-
fwBar.firstChild.style.background = 'var(--
|
|
112
|
-
fwBar.firstChild.style.color = 'var(--
|
|
113
|
-
|
|
111
|
+
fwBar.firstChild.style.background = 'var(--bg-surface,#fff)'; fwBar.firstChild.style.boxShadow = '0 1px 2px rgba(0,0,0,.08)';
|
|
112
|
+
fwBar.firstChild.style.color = 'var(--text-heading,#0a0a0a)';
|
|
113
|
+
|
|
114
114
|
headRow.appendChild(tabBar); headRow.appendChild(fwBar);
|
|
115
115
|
var body = document.createElement('div');
|
|
116
116
|
var tabs = files.slice();
|
|
@@ -119,11 +119,11 @@ function mount(rootSelector, files){
|
|
|
119
119
|
var box = document.createElement('div');
|
|
120
120
|
box.style.position = 'relative';
|
|
121
121
|
var pre = document.createElement('pre');
|
|
122
|
-
pre.style.cssText = 'margin:0;padding:14px
|
|
122
|
+
pre.style.cssText = 'margin:0;padding:14px 70px 14px 16px;overflow:auto;max-height:380px;font-family:var(--font-mono,monospace);font-size:var(--text-code,12px);line-height:var(--leading-code,18px);color:var(--text-body,#525252);white-space:pre;';
|
|
123
123
|
var copyBtn = document.createElement('button');
|
|
124
124
|
copyBtn.type = 'button';
|
|
125
125
|
copyBtn.innerHTML = copySvg() + '<span style="margin-left:4px">Copy</span>';
|
|
126
|
-
copyBtn.style.cssText = 'position:absolute;top:8px;right:10px;display:inline-flex;align-items:center;height:
|
|
126
|
+
copyBtn.style.cssText = 'position:absolute;top:8px;right:10px;display:inline-flex;align-items:center;height:28px;box-sizing:border-box;padding:0 8px;border-radius:var(--radius-md,8px);background:var(--bg-surface,#fff);color:var(--text-secondary,#737373);font-size:var(--text-xs,12px);line-height:var(--leading-xs,16px);font-family:var(--font-sans,sans-serif);cursor:pointer;border:1px solid var(--border-control,#e5e5e5);';
|
|
127
127
|
function setText(text){
|
|
128
128
|
pre.textContent = text;
|
|
129
129
|
copyBtn.onclick = function(){
|
|
@@ -169,7 +169,7 @@ function mount(rootSelector, files){
|
|
|
169
169
|
tb.type = 'button';
|
|
170
170
|
tb.textContent = t.name;
|
|
171
171
|
if (t.vueOnly) tb.style.display = 'none';
|
|
172
|
-
tb.style.cssText = 'padding:8px 12px;border:none;background:transparent;font-size:12px;font-family:var(--font-sans,sans-serif);cursor:pointer;white-space:nowrap;margin-bottom:-1px;border-bottom:2px solid transparent;';
|
|
172
|
+
tb.style.cssText = 'padding:8px 12px;border:none;background:transparent;font-size:var(--text-xs,12px);line-height:var(--leading-xs,16px);font-family:var(--font-sans,sans-serif);cursor:pointer;white-space:nowrap;margin-bottom:-1px;border-bottom:2px solid transparent;';
|
|
173
173
|
tb.onclick = function(){ selectTab(i); };
|
|
174
174
|
tabBar.appendChild(tb);
|
|
175
175
|
});
|
|
@@ -114,10 +114,12 @@ export { default as Breadcrumb } from "../navigation/Breadcrumb.vue";
|
|
|
114
114
|
export { default as DocTree } from "../navigation/DocTree.vue";
|
|
115
115
|
export { default as PageHeading } from "../navigation/PageHeading.vue";
|
|
116
116
|
export { default as PageToolbar } from "../navigation/PageToolbar.vue";
|
|
117
|
+
export { default as SidePanel } from "../navigation/SidePanel.vue";
|
|
117
118
|
export { default as Sidebar } from "../navigation/Sidebar.vue";
|
|
118
119
|
export { default as ThemeToggle } from "../navigation/ThemeToggle.vue";
|
|
119
120
|
export { default as TopBar } from "../navigation/TopBar.vue";
|
|
120
121
|
export { default as Tree } from "../navigation/Tree.vue";
|
|
122
|
+
export { default as UserMenu } from "../navigation/UserMenu.vue";
|
|
121
123
|
|
|
122
124
|
/* pipeline/ */
|
|
123
125
|
export { default as ConfigPanel } from "../pipeline/ConfigPanel.vue";
|
|
@@ -132,6 +134,7 @@ export { default as PortLabel } from "../pipeline/PortLabel.vue";
|
|
|
132
134
|
export { default as ZoomControls } from "../pipeline/ZoomControls.vue";
|
|
133
135
|
|
|
134
136
|
/* tabs/ */
|
|
137
|
+
export { default as CountBadge } from "../tabs/CountBadge.vue";
|
|
135
138
|
export { default as PanelTabs } from "../tabs/PanelTabs.vue";
|
|
136
139
|
export { default as PillTabs } from "../tabs/PillTabs.vue";
|
|
137
140
|
export { default as SegmentedControl } from "../tabs/SegmentedControl.vue";
|
|
@@ -139,7 +142,7 @@ export { default as Tabs } from "../tabs/Tabs.vue";
|
|
|
139
142
|
export { default as ViewToggle } from "../tabs/ViewToggle.vue";
|
|
140
143
|
|
|
141
144
|
/* shared values */
|
|
142
|
-
export { KEYBRICKS_NAV } from "../navigation/nav-model.js";
|
|
145
|
+
export { KEYBRICKS_MGMT, KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER } from "../navigation/nav-model.js";
|
|
143
146
|
export { applyStoredTheme } from "../navigation/theme-store.js";
|
|
144
147
|
export { PIPELINE_CATALOG } from "../pipeline/pipeline-catalog.js";
|
|
145
148
|
export { VUE_ICON_PATHS } from "../icons/icon-paths.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** Floating context menu — r10, soft shadow, control-height rows at the region's tier (32px / 14px at md), red danger row.
|
|
2
|
+
/** Floating context menu — r10, soft shadow, control-height rows at the region's tier (32px / 14px at md), red danger row.
|
|
3
|
+
* Action menus keep rows flush; a single-choice menu (any item `selected`) spaces rows 2px apart. Dividers keep 4px above and below. */
|
|
3
4
|
export interface ContextMenuProps {
|
|
4
|
-
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean }[];
|
|
5
|
+
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
5
6
|
onSelect?: (label: string) => void;
|
|
6
7
|
/** Size tier of the rows. Omit to inherit the tier of the control that opened the menu (the default). */
|
|
7
8
|
size?: "sm" | "md" | "lg";
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
3
|
|
|
4
|
-
/** Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
4
|
+
/** Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
5
|
+
* selected: true marks the current choice of a single-choice menu (section switcher, language picker): --bg-hover fill, --weight-medium
|
|
6
|
+
* --text-heading, no checkmark. A menu with any selected item spaces its rows 2px apart (like Sidebar / Tree / DocTree) so a hovered
|
|
7
|
+
* fill never touches the selected one; an action menu (nothing selected) keeps its rows flush. Dividers keep 4px above and below. */
|
|
5
8
|
export function ContextMenu({ items = [], onSelect, size, style }) {
|
|
6
9
|
const [hover, setHover] = React.useState(-1);
|
|
10
|
+
const choice = items.some((it) => it && it.selected);
|
|
7
11
|
return (
|
|
8
|
-
<div role="menu" data-size={size} style={{ background: "var(--bg-surface)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-menu)", padding: 4, minWidth: 148, display: "inline-flex", flexDirection: "column", ...style }}>
|
|
9
|
-
{items.map((it, i) => it.divider ? <div key={i} style={{ height: 1, background: "var(--border-subtle)", margin: "4px 0" }} /> : (
|
|
12
|
+
<div role="menu" data-size={size} style={{ background: "var(--bg-surface)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-menu)", padding: 4, minWidth: 148, display: "inline-flex", flexDirection: "column", gap: choice ? 2 : 0, ...style }}>
|
|
13
|
+
{items.map((it, i) => it.divider ? <div key={i} role="separator" style={{ height: 1, flexShrink: 0, background: "var(--border-subtle)", margin: "4px 0" }} /> : (
|
|
10
14
|
<button key={i} type="button" onClick={() => onSelect && onSelect(it.label)}
|
|
11
15
|
onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(-1)}
|
|
12
|
-
role="menuitem" style={{ display: "flex", alignItems: "center", gap: 10, height: "var(--control-h)", padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", background: hover === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: it.danger ? "var(--danger)" : "var(--text-heading)", textAlign: "left", whiteSpace: "nowrap" }}>
|
|
16
|
+
role={choice ? "menuitemradio" : "menuitem"} aria-checked={choice ? !!it.selected : undefined} style={{ display: "flex", alignItems: "center", gap: 10, height: "var(--control-h)", flexShrink: 0, padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", background: it.selected || hover === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontWeight: it.selected ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: it.danger ? "var(--danger)" : "var(--text-heading)", textAlign: "left", whiteSpace: "nowrap" }}>
|
|
13
17
|
{it.icon ? <Icon name={it.icon} size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)" }} /> : null}{it.label}
|
|
14
18
|
</button>
|
|
15
19
|
))}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
|
-
// Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
5
|
-
|
|
4
|
+
// Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
5
|
+
// selected: true marks the current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. A menu with any
|
|
6
|
+
// selected item spaces its rows 2px apart (like Sidebar / Tree / DocTree); an action menu keeps its rows flush. Dividers keep 4px above and below.
|
|
7
|
+
const props = defineProps({ items: { type: Array, default: () => [] }, size: String, style: Object })
|
|
8
|
+
const choice = computed(() => props.items.some(it => it && it.selected))
|
|
6
9
|
const emit = defineEmits(['select'])
|
|
7
10
|
const hover = ref(-1)
|
|
8
11
|
</script>
|
|
9
12
|
<template>
|
|
10
|
-
<div role="menu" :data-size="size" :style="{ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', display: 'inline-flex', flexDirection: 'column', ...style }">
|
|
13
|
+
<div role="menu" :data-size="size" :style="{ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', display: 'inline-flex', flexDirection: 'column', gap: choice ? '2px' : 0, ...style }">
|
|
11
14
|
<template v-for="(it, i) in items" :key="i">
|
|
12
|
-
<div v-if="it.divider" style="height:1px
|
|
15
|
+
<div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
|
|
13
16
|
<button v-else type="button" @click="emit('select', it.label)" @mouseenter="hover = i" @mouseleave="hover = -1"
|
|
14
|
-
role="menuitem" :style="{ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: hover === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap' }">
|
|
17
|
+
:role="choice ? 'menuitemradio' : 'menuitem'" :aria-checked="choice ? !!it.selected : undefined" :style="{ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', flexShrink: 0, padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: it.selected || hover === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontWeight: it.selected ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap' }">
|
|
15
18
|
<Icon v-if="it.icon" :name="it.icon" :size="16" :style="{ width: 'var(--control-icon)', height: 'var(--control-icon)' }" />{{ it.label }}
|
|
16
19
|
</button>
|
|
17
20
|
</template>
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
/** Trigger-anchored dropdown menu (reuses ContextMenu's look). Addition — for "Show ⌄"-style buttons and user menus. */
|
|
3
3
|
export interface MenuProps {
|
|
4
4
|
trigger: React.ReactNode;
|
|
5
|
-
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean }[];
|
|
5
|
+
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
6
6
|
onSelect?: (label: string) => void;
|
|
7
7
|
align?: "start" | "end";
|
|
8
8
|
style?: React.CSSProperties;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Feedback & flow: `Stepper` (wizard circles), `ContextMenu
|
|
1
|
+
Feedback & flow: `Stepper` (wizard circles), `ContextMenu` / `Menu` (action menus flush; `selected: true` on one item makes a single-choice menu with 2px row gaps, no checkmark), `ActivityItem`/`DotActivity` (feeds), `LogViewer` (mono logs).
|
|
2
2
|
|
|
3
3
|
```jsx
|
|
4
4
|
<Stepper current={1} steps={[{label:"Upload & Parse",icon:"upload"},{label:"Comparison",icon:"file-text"},{label:"Confirmation",icon:"check"},{label:"Execution",icon:"play"},{label:"Results",icon:"eye"}]}/>
|
package/components/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export * from "./icons/Icon";
|
|
|
73
73
|
export * from "./icons/Unicon";
|
|
74
74
|
export * from "./navigation/Breadcrumb";
|
|
75
75
|
export * from "./navigation/DocTree";
|
|
76
|
+
export * from "./navigation/SidePanel";
|
|
76
77
|
export * from "./navigation/Sidebar";
|
|
77
78
|
export * from "./navigation/TopBar";
|
|
78
79
|
export * from "./navigation/Tree";
|
package/components/index.js
CHANGED
|
@@ -92,8 +92,9 @@ export { Unicon } from "./icons/Unicon.jsx";
|
|
|
92
92
|
/* navigation/ */
|
|
93
93
|
export { Breadcrumb, ThemeToggle, applyStoredTheme } from "./navigation/Breadcrumb.jsx";
|
|
94
94
|
export { DocTree } from "./navigation/DocTree.jsx";
|
|
95
|
-
export {
|
|
96
|
-
export {
|
|
95
|
+
export { SidePanel } from "./navigation/SidePanel.jsx";
|
|
96
|
+
export { KEYBRICKS_MGMT, KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, SIDEBAR_COLORS, Sidebar } from "./navigation/Sidebar.jsx";
|
|
97
|
+
export { PageHeading, PageToolbar, THEME_OPTIONS, TopBar, UserMenu } from "./navigation/TopBar.jsx";
|
|
97
98
|
export { Tree } from "./navigation/Tree.jsx";
|
|
98
99
|
|
|
99
100
|
/* pipeline/ */
|
|
@@ -106,5 +107,5 @@ export { ZoomControls } from "./pipeline/ZoomControls.jsx";
|
|
|
106
107
|
|
|
107
108
|
/* tabs/ */
|
|
108
109
|
export { SegmentedControl } from "./tabs/SegmentedControl.jsx";
|
|
109
|
-
export { PanelTabs, PillTabs, Tabs } from "./tabs/Tabs.jsx";
|
|
110
|
+
export { CountBadge, PanelTabs, PillTabs, Tabs } from "./tabs/Tabs.jsx";
|
|
110
111
|
export { ViewToggle } from "./tabs/ViewToggle.jsx";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Side panel docked beside the Sidebar (Documents, Management): 240px, same tone as the rail, grows open from 0 and fades its content in.
|
|
3
|
+
* 56px header: title + actions + close. The body scrolls. Vue: #actions slot, @close. */
|
|
4
|
+
export interface SidePanelProps {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
title?: React.ReactNode;
|
|
7
|
+
/** Extra header buttons left of close (e.g. a muted IconButton "maximize-2" to open the full page) */
|
|
8
|
+
actions?: React.ReactNode;
|
|
9
|
+
/** Shows the close button. Vue: @close (hide it with :closable="false"). */
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
closeLabel?: string;
|
|
12
|
+
closeIcon?: string;
|
|
13
|
+
/** Same values as Sidebar — keep the two in step */
|
|
14
|
+
tone?: "recessed" | "flush" | "contrast" | "custom";
|
|
15
|
+
color?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
/** Size tier of the panel body ("sm" for compact document lists) */
|
|
18
|
+
size?: "sm" | "md" | "lg";
|
|
19
|
+
children?: React.ReactNode;
|
|
20
|
+
style?: React.CSSProperties;
|
|
21
|
+
}
|
|
22
|
+
export declare function SidePanel(props: SidePanelProps): JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { IconButton } from "../buttons/IconButton.jsx";
|
|
3
|
+
import { chromeTone } from "./chrome-tones.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Side panel docked beside the Sidebar (Documents, Management). 240px, same tone as the rail, opens by growing from 0
|
|
7
|
+
* (--dur-slow, --ease-out) while its content fades in from 12px left. 56px header: title + actions + close.
|
|
8
|
+
* Body scrolls; put a SegmentedControl, SearchInput, Tree or rows inside. size="sm" makes its controls compact.
|
|
9
|
+
*/
|
|
10
|
+
export function SidePanel({ open = true, title, actions, onClose, closeLabel = "Close", closeIcon = "x", tone = "recessed", color, width = 240, size, children, style }) {
|
|
11
|
+
const t = chromeTone(tone, color);
|
|
12
|
+
return (
|
|
13
|
+
<div aria-hidden={open ? undefined : "true"} style={{ width: open ? width : 0, flexShrink: 0, overflow: "hidden", height: "100%", transition: "width var(--dur-slow) var(--ease-out)" }}>
|
|
14
|
+
<div data-theme={t.theme} data-size={size} style={{ ...t.vars, width, 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 }}>
|
|
15
|
+
<div style={{ height: "var(--topbar-h)", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, padding: "0 8px 0 16px", boxSizing: "border-box", flexShrink: 0 }}>
|
|
16
|
+
<span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", fontWeight: "var(--weight-medium)", color: "var(--text-heading)" }}>{title}</span>
|
|
17
|
+
<div data-size="md" style={{ display: "flex", alignItems: "center", gap: 2, flexShrink: 0 }}>{actions}{onClose ? <IconButton icon={closeIcon} muted label={closeLabel} title={closeLabel} onClick={onClose} /> : null}</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div style={{ flex: 1, minHeight: 0, overflow: "auto", display: "flex", flexDirection: "column" }}>{children}</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -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,56 +1,57 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
-
import { KEYBRICKS_NAV } from "./nav-model.js";
|
|
3
|
+
import { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT } from "./nav-model.js";
|
|
4
|
+
import { chromeTone, SIDEBAR_COLORS } from "./chrome-tones.js";
|
|
4
5
|
|
|
5
|
-
|
|
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 }) {
|
|
6
10
|
const [hover, setHover] = React.useState(false);
|
|
7
11
|
return (
|
|
8
|
-
<div role="button" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
9
|
-
style={{
|
|
10
|
-
<Icon name={icon} size={indent ? 14 : 16}
|
|
11
|
-
{!collapsed ? <span style={
|
|
12
|
-
{!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}
|
|
13
17
|
</div>
|
|
14
18
|
);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
* Keybricks app sidebar — 240px
|
|
19
|
-
*
|
|
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.
|
|
20
25
|
*/
|
|
21
|
-
export function Sidebar({ items =
|
|
22
|
-
const
|
|
23
|
-
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)} />);
|
|
24
38
|
return (
|
|
25
|
-
<aside style={{ width: collapsed ? "var(--rail-w)" : "var(--sidebar-w)", height, background: "var(--bg
|
|
26
|
-
<div style={{ height: "var(--topbar-h)",
|
|
27
|
-
<img src={logo} alt="" style={{ width: 24, height: 24, objectFit: "contain" }} />
|
|
28
|
-
{!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}
|
|
29
43
|
</div>
|
|
30
|
-
<nav style={{ flex: 1, overflowY: "auto", padding: 8, display: "flex", flexDirection: "column", gap:
|
|
31
|
-
{onToggleCollapse ? <NavItem icon="panel-left"
|
|
32
|
-
{items
|
|
33
|
-
<div key={it.label}>
|
|
34
|
-
<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" }}>
|
|
35
|
-
<Icon name={it.icon} size={16} />
|
|
36
|
-
{!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}
|
|
37
|
-
</div>
|
|
38
|
-
{!collapsed && openSections.includes(it.label) ? (
|
|
39
|
-
<div style={{ marginLeft: 24, display: "flex", flexDirection: "column", gap: 2, paddingTop: 2 }}>
|
|
40
|
-
{it.children.map((c) => <NavItem key={c.label} icon={c.icon} label={c.label} indent active={active === c.label} onClick={() => onSelect && onSelect(c.label)} />)}
|
|
41
|
-
</div>
|
|
42
|
-
) : null}
|
|
43
|
-
</div>
|
|
44
|
-
) : (
|
|
45
|
-
<NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} active={active === it.label} onClick={() => onSelect && onSelect(it.label)} />
|
|
46
|
-
))}
|
|
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)}
|
|
47
47
|
</nav>
|
|
48
|
-
<div style={{
|
|
49
|
-
{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}
|
|
50
51
|
</div>
|
|
51
52
|
</aside>
|
|
52
53
|
);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
export { KEYBRICKS_NAV };
|
|
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,49 +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
|
-
<!-- The product's nav model is KEYBRICKS_NAV in ./nav-model.js — also exported from the Vue entry:
|
|
49
|
-
import { Sidebar, KEYBRICKS_NAV } from '@keyfunc/keybricks-ds/vue' -->
|