@kb-labs/studio-app 0.6.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.
Files changed (168) hide show
  1. package/.env.example +10 -0
  2. package/README.md +74 -0
  3. package/UI-KIT-COMPONENTS.md +783 -0
  4. package/UI-KIT-PLAN.md +326 -0
  5. package/eslint-rules/no-direct-antd-import.js +151 -0
  6. package/eslint.config.ts +27 -0
  7. package/index.html +17 -0
  8. package/package.json +72 -0
  9. package/postcss.config.js +7 -0
  10. package/public/favicon.ico +0 -0
  11. package/public/favicon.svg +7 -0
  12. package/rspack.config.mjs +118 -0
  13. package/src/App.tsx +46 -0
  14. package/src/components/devtools/devtools-log-feed.module.css +165 -0
  15. package/src/components/devtools/devtools-log-feed.tsx +300 -0
  16. package/src/components/devtools/devtools-panel.module.css +121 -0
  17. package/src/components/devtools/devtools-panel.tsx +115 -0
  18. package/src/components/empty-state.tsx +28 -0
  19. package/src/components/error-boundary.tsx +210 -0
  20. package/src/components/health-banner.module.css +103 -0
  21. package/src/components/health-banner.tsx +134 -0
  22. package/src/components/health-indicator.tsx +12 -0
  23. package/src/components/page-header.tsx +49 -0
  24. package/src/components/page-transition.tsx +24 -0
  25. package/src/components/role-switcher.tsx +148 -0
  26. package/src/components/run-badge.tsx +13 -0
  27. package/src/components/ui/icons.tsx +509 -0
  28. package/src/components/ui/index.ts +25 -0
  29. package/src/components/ui/kb-config-provider.tsx +130 -0
  30. package/src/components/ui/kb-header.module.css +51 -0
  31. package/src/components/ui/kb-header.tsx +112 -0
  32. package/src/components/ui/kb-notification-bell.module.css +103 -0
  33. package/src/components/ui/kb-notification-bell.tsx +199 -0
  34. package/src/components/ui/kb-page-layout.tsx +66 -0
  35. package/src/components/ui/kb-quick-search.module.css +86 -0
  36. package/src/components/ui/kb-quick-search.tsx +196 -0
  37. package/src/components/ui/kb-sidebar.module.css +47 -0
  38. package/src/components/ui/kb-sidebar.tsx +196 -0
  39. package/src/components/ui/kb-stat-card.module.css +21 -0
  40. package/src/components/ui/kb-stat-card.tsx +22 -0
  41. package/src/components/ui/kb-stat-grid.tsx +22 -0
  42. package/src/components/ui/kb-status-bar.module.css +49 -0
  43. package/src/components/ui/kb-status-bar.tsx +145 -0
  44. package/src/components/ui/kb-system-health-indicator.tsx +229 -0
  45. package/src/components/ui/kb-theme-toggle.tsx +42 -0
  46. package/src/components/ui/theme-adapter.ts +812 -0
  47. package/src/components/ui/theme-transition-overlay.tsx +70 -0
  48. package/src/components/ui/utils.ts +6 -0
  49. package/src/components/widget-error-boundary.tsx +172 -0
  50. package/src/config/env.ts +5 -0
  51. package/src/config/feature-flags.ts +482 -0
  52. package/src/config/studio.config.schema.ts +88 -0
  53. package/src/config/studio.config.ts +84 -0
  54. package/src/env.d.ts +23 -0
  55. package/src/hooks/use-command-palette.ts +6 -0
  56. package/src/hooks/use-devtools-store.ts +25 -0
  57. package/src/hooks/use-feature-flags.ts +101 -0
  58. package/src/index.css +10 -0
  59. package/src/layouts/dashboard-grid.tsx +122 -0
  60. package/src/layouts/index.ts +12 -0
  61. package/src/layouts/two-pane.tsx +134 -0
  62. package/src/main.tsx +15 -0
  63. package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +257 -0
  64. package/src/modules/analytics/components/analytics-date-range-picker.tsx +121 -0
  65. package/src/modules/analytics/pages/analytics-cache-page.tsx +285 -0
  66. package/src/modules/analytics/pages/analytics-embeddings-page.tsx +274 -0
  67. package/src/modules/analytics/pages/analytics-events-page.tsx +425 -0
  68. package/src/modules/analytics/pages/analytics-llm-page.tsx +434 -0
  69. package/src/modules/analytics/pages/analytics-overview-page.tsx +336 -0
  70. package/src/modules/analytics/pages/analytics-page.tsx +20 -0
  71. package/src/modules/analytics/pages/analytics-storage-page.tsx +344 -0
  72. package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +279 -0
  73. package/src/modules/analytics/routes/index.tsx +133 -0
  74. package/src/modules/analytics/utils/formatters.test.ts +148 -0
  75. package/src/modules/analytics/utils/formatters.ts +100 -0
  76. package/src/modules/auth/pages/login-page.module.css +113 -0
  77. package/src/modules/auth/pages/login-page.tsx +80 -0
  78. package/src/modules/auth/routes/index.tsx +24 -0
  79. package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +199 -0
  80. package/src/modules/dashboard/components/AiInsightsWidget.tsx +358 -0
  81. package/src/modules/dashboard/components/AnimatedMetricCard.css +88 -0
  82. package/src/modules/dashboard/components/AnimatedMetricCard.tsx +125 -0
  83. package/src/modules/dashboard/components/HeroMetricCard.css +114 -0
  84. package/src/modules/dashboard/components/HeroMetricCard.tsx +152 -0
  85. package/src/modules/dashboard/components/HeroMetricsWidget.tsx +146 -0
  86. package/src/modules/dashboard/components/SystemResourcesWidget.tsx +197 -0
  87. package/src/modules/dashboard/components/dashboard-empty.tsx +22 -0
  88. package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +59 -0
  89. package/src/modules/dashboard/components/dashboard-kpis.tsx +13 -0
  90. package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +85 -0
  91. package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +78 -0
  92. package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +67 -0
  93. package/src/modules/dashboard/components/dashboard-requests-widget.tsx +62 -0
  94. package/src/modules/dashboard/layouts/dashboard-layout.tsx +16 -0
  95. package/src/modules/dashboard/pages/ai-insights-page.tsx +563 -0
  96. package/src/modules/dashboard/pages/dashboard-animations.css +317 -0
  97. package/src/modules/dashboard/pages/dashboard-page.tsx +13 -0
  98. package/src/modules/dashboard/routes/index.tsx +70 -0
  99. package/src/modules/observability/pages/devkit-page.tsx +263 -0
  100. package/src/modules/observability/pages/incident-detail-page.tsx +571 -0
  101. package/src/modules/observability/pages/incidents-page.tsx +370 -0
  102. package/src/modules/observability/pages/log-detail-page.tsx +508 -0
  103. package/src/modules/observability/pages/logs-page.tsx +1129 -0
  104. package/src/modules/observability/pages/prometheus-metrics-page.tsx +486 -0
  105. package/src/modules/observability/pages/state-broker-page.tsx +229 -0
  106. package/src/modules/observability/pages/system-events-page.tsx +255 -0
  107. package/src/modules/observability/routes/index.tsx +133 -0
  108. package/src/modules/plugins/components/marketplace-card.tsx +153 -0
  109. package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +374 -0
  110. package/src/modules/plugins/components/plugin-card.tsx +136 -0
  111. package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +101 -0
  112. package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +101 -0
  113. package/src/modules/plugins/components/plugin-detail/index.ts +7 -0
  114. package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +64 -0
  115. package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +30 -0
  116. package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +68 -0
  117. package/src/modules/plugins/components/plugin-detail/permissions/index.ts +6 -0
  118. package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +43 -0
  119. package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +242 -0
  120. package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +96 -0
  121. package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +55 -0
  122. package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +47 -0
  123. package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +169 -0
  124. package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +159 -0
  125. package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +40 -0
  126. package/src/modules/plugins/components/plugin-grid.tsx +20 -0
  127. package/src/modules/plugins/components/plugin-table.tsx +178 -0
  128. package/src/modules/plugins/pages/marketplace-page.tsx +200 -0
  129. package/src/modules/plugins/pages/plugin-detail-page.tsx +1145 -0
  130. package/src/modules/plugins/routes/index.tsx +48 -0
  131. package/src/modules/plugins/utils/index.ts +1 -0
  132. package/src/modules/plugins/utils/schema-helpers.ts +21 -0
  133. package/src/modules/settings/components/api-routes-viewer.tsx +415 -0
  134. package/src/modules/settings/components/appearance-settings.tsx +200 -0
  135. package/src/modules/settings/components/configuration-settings.tsx +321 -0
  136. package/src/modules/settings/components/data-privacy-settings.tsx +111 -0
  137. package/src/modules/settings/components/experimental-settings.tsx +243 -0
  138. package/src/modules/settings/components/navigation-settings.tsx +198 -0
  139. package/src/modules/settings/components/notifications-settings.tsx +40 -0
  140. package/src/modules/settings/pages/settings-page.tsx +298 -0
  141. package/src/modules/settings/routes/index.tsx +47 -0
  142. package/src/pages/README-assistant.md +91 -0
  143. package/src/pages/login-page.tsx +177 -0
  144. package/src/pages/not-found-page.tsx +85 -0
  145. package/src/providers/auth-provider.tsx +40 -0
  146. package/src/providers/command-palette-provider.tsx +193 -0
  147. package/src/providers/data-sources-provider.tsx +27 -0
  148. package/src/providers/devtools-provider.tsx +72 -0
  149. package/src/providers/navigation-items-provider.tsx +38 -0
  150. package/src/providers/registry-v2-provider.tsx +93 -0
  151. package/src/providers/settings-provider.tsx +158 -0
  152. package/src/router.tsx +302 -0
  153. package/src/routes/helpers.tsx +25 -0
  154. package/src/routes/plugin-page-v2.tsx +52 -0
  155. package/src/routes/types.ts +18 -0
  156. package/src/styles/antd-overrides.css +211 -0
  157. package/src/styles/base.css +190 -0
  158. package/src/styles/settings.css +75 -0
  159. package/src/styles/variables.css +94 -0
  160. package/src/utils/analytics.ts +183 -0
  161. package/src/utils/apply-navigation-categories.ts +83 -0
  162. package/src/utils/commands.ts +252 -0
  163. package/src/utils/export.ts +174 -0
  164. package/src/utils/format-date.ts +166 -0
  165. package/src/utils/logger.ts +110 -0
  166. package/tailwind.config.ts +13 -0
  167. package/tsconfig.json +13 -0
  168. package/vitest-setup.ts +1 -0
@@ -0,0 +1,196 @@
1
+ import * as React from 'react';
2
+ import { Modal, Input, List, Empty, Badge } from 'antd';
3
+ import { Search, FileText, Box, Settings, BarChart, Activity, Navigation, Zap } from 'lucide-react';
4
+ import styles from './kb-quick-search.module.css';
5
+
6
+ export interface SearchableItem {
7
+ id: string;
8
+ title: string;
9
+ description?: string;
10
+ category: 'page' | 'plugin' | 'widget' | 'workflow' | 'setting' | 'navigation' | 'action';
11
+ path: string;
12
+ icon?: React.ReactNode;
13
+ badge?: string;
14
+ }
15
+
16
+ export interface KBQuickSearchProps {
17
+ open: boolean;
18
+ onClose: () => void;
19
+ items: SearchableItem[];
20
+ onNavigate: (path: string) => void;
21
+ placeholder?: string;
22
+ }
23
+
24
+ const CATEGORY_CONFIG = {
25
+ page: { icon: FileText, color: '#1890ff', label: 'Page' },
26
+ navigation: { icon: Navigation, color: '#13c2c2', label: 'Navigation' },
27
+ action: { icon: Zap, color: '#fa8c16', label: 'Action' },
28
+ plugin: { icon: Box, color: '#52c41a', label: 'Plugin' },
29
+ widget: { icon: Activity, color: '#722ed1', label: 'Widget' },
30
+ workflow: { icon: BarChart, color: '#faad14', label: 'Workflow' },
31
+ setting: { icon: Settings, color: '#8c8c8c', label: 'Setting' },
32
+ } as const;
33
+
34
+ export function KBQuickSearch({
35
+ open,
36
+ onClose,
37
+ items,
38
+ onNavigate,
39
+ placeholder = 'Search pages, plugins, widgets...',
40
+ }: KBQuickSearchProps) {
41
+ const [searchQuery, setSearchQuery] = React.useState('');
42
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
43
+ const inputRef = React.useRef<any>(null);
44
+
45
+ const filteredItems = React.useMemo(() => {
46
+ if (!searchQuery.trim()) { return items.slice(0, 10); }
47
+ const query = searchQuery.toLowerCase();
48
+ return items
49
+ .filter((item) => {
50
+ return (item.title?.toLowerCase().includes(query) ?? false)
51
+ || (item.description?.toLowerCase().includes(query) ?? false)
52
+ || (item.path?.toLowerCase().includes(query) ?? false);
53
+ })
54
+ .slice(0, 10);
55
+ }, [items, searchQuery]);
56
+
57
+ React.useEffect(() => { setSelectedIndex(0); }, [filteredItems]);
58
+
59
+ React.useEffect(() => {
60
+ if (open) {
61
+ setTimeout(() => { inputRef.current?.focus(); }, 100);
62
+ } else {
63
+ setSearchQuery('');
64
+ setSelectedIndex(0);
65
+ }
66
+ }, [open]);
67
+
68
+ const handleKeyDown = React.useCallback(
69
+ (e: React.KeyboardEvent) => {
70
+ if (e.key === 'ArrowDown') {
71
+ e.preventDefault();
72
+ setSelectedIndex((prev) => Math.min(prev + 1, filteredItems.length - 1));
73
+ } else if (e.key === 'ArrowUp') {
74
+ e.preventDefault();
75
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
76
+ } else if (e.key === 'Enter') {
77
+ e.preventDefault();
78
+ const item = filteredItems[selectedIndex];
79
+ if (item) { onNavigate(item.path); onClose(); }
80
+ } else if (e.key === 'Escape') {
81
+ e.preventDefault();
82
+ onClose();
83
+ }
84
+ },
85
+ [filteredItems, selectedIndex, onNavigate, onClose]
86
+ );
87
+
88
+ const handleItemClick = (item: SearchableItem) => {
89
+ onNavigate(item.path);
90
+ onClose();
91
+ };
92
+
93
+ return (
94
+ <Modal
95
+ open={open}
96
+ onCancel={onClose}
97
+ footer={null}
98
+ width={600}
99
+ centered
100
+ closable={false}
101
+ styles={{ body: { padding: 0 } }}
102
+ >
103
+ <div className={styles.searchWrap}>
104
+ <Input
105
+ ref={inputRef}
106
+ size="large"
107
+ placeholder={placeholder}
108
+ prefix={<Search size={18} style={{ color: '#8c8c8c' }} />}
109
+ suffix={
110
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
111
+ <kbd>ESC</kbd>
112
+ </div>
113
+ }
114
+ value={searchQuery}
115
+ onChange={(e) => setSearchQuery(e.target.value)}
116
+ onKeyDown={handleKeyDown}
117
+ className={styles.searchInput}
118
+ />
119
+ </div>
120
+
121
+ <div className={styles.list}>
122
+ {filteredItems.length === 0 ? (
123
+ <Empty
124
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
125
+ description="No results found"
126
+ className={styles.emptyState}
127
+ />
128
+ ) : (
129
+ <List
130
+ dataSource={filteredItems}
131
+ renderItem={(item, index) => {
132
+ const config = CATEGORY_CONFIG[item.category];
133
+ const Icon = config.icon;
134
+ const isSelected = index === selectedIndex;
135
+
136
+ return (
137
+ <List.Item
138
+ key={item.id}
139
+ onClick={() => handleItemClick(item)}
140
+ className={`${styles.resultItem} ${isSelected ? styles.selected : ''}`}
141
+ onMouseEnter={() => setSelectedIndex(index)}
142
+ >
143
+ <List.Item.Meta
144
+ avatar={
145
+ <div
146
+ className={styles.categoryIcon}
147
+ style={{ backgroundColor: `${config.color}15` }}
148
+ >
149
+ {item.icon || <Icon size={16} color={config.color} />}
150
+ </div>
151
+ }
152
+ title={
153
+ <div className={styles.itemTitle}>
154
+ <span>{item.title}</span>
155
+ {item.badge && (
156
+ <Badge
157
+ count={item.badge}
158
+ style={{ backgroundColor: config.color, fontSize: 11, height: 18, lineHeight: '18px' }}
159
+ />
160
+ )}
161
+ </div>
162
+ }
163
+ description={
164
+ <div className={styles.itemDesc}>
165
+ <span style={{ fontSize: 11, color: config.color }}>{config.label}</span>
166
+ {item.description && (
167
+ <>
168
+ <span style={{ color: 'var(--text-tertiary)' }}>•</span>
169
+ <span style={{ fontSize: 12 }}>{item.description}</span>
170
+ </>
171
+ )}
172
+ </div>
173
+ }
174
+ />
175
+ {isSelected && (
176
+ <div className={styles.enterHint}>
177
+ <kbd>↵</kbd>
178
+ </div>
179
+ )}
180
+ </List.Item>
181
+ );
182
+ }}
183
+ />
184
+ )}
185
+ </div>
186
+
187
+ <div className={styles.footer}>
188
+ <div className={styles.footerLeft}>
189
+ <div className={styles.footerHint}><kbd>↑↓</kbd> Navigate</div>
190
+ <div className={styles.footerHint}><kbd>↵</kbd> Select</div>
191
+ </div>
192
+ <div>{filteredItems.length} result{filteredItems.length !== 1 ? 's' : ''}</div>
193
+ </div>
194
+ </Modal>
195
+ );
196
+ }
@@ -0,0 +1,47 @@
1
+ .sider {
2
+ overflow: hidden;
3
+ height: calc(100vh - 28px);
4
+ position: fixed;
5
+ left: 0;
6
+ top: 0;
7
+ bottom: 28px;
8
+ background-color: var(--bg-sidebar);
9
+ border-right: none;
10
+ box-shadow: 1px 0 0 var(--border-primary), 2px 0 8px rgba(0, 0, 0, 0.03);
11
+ z-index: var(--z-sidebar);
12
+ display: flex;
13
+ flex-direction: column;
14
+ }
15
+
16
+ .header {
17
+ height: 64px;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ border-bottom: 1px solid var(--border-primary);
22
+ flex-shrink: 0;
23
+ }
24
+
25
+ .scrollArea {
26
+ position: absolute;
27
+ top: 64px;
28
+ left: 0;
29
+ right: 0;
30
+ bottom: 48px;
31
+ overflow-y: auto;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ .menu {
36
+ border-right: 0 !important;
37
+ padding: 4px 8px;
38
+ }
39
+
40
+ .settingsArea {
41
+ position: absolute;
42
+ bottom: 0;
43
+ left: 0;
44
+ right: 0;
45
+ border-top: 1px solid var(--border-primary);
46
+ background-color: var(--bg-sidebar);
47
+ }
@@ -0,0 +1,196 @@
1
+ import * as React from 'react';
2
+ import { Layout, Menu } from 'antd';
3
+ import type { MenuProps } from 'antd';
4
+ import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
5
+ import { UIButton } from '@kb-labs/studio-ui-kit';
6
+ import styles from './kb-sidebar.module.css';
7
+
8
+ const { Sider: AntSider } = Layout;
9
+
10
+ export interface NavigationItem {
11
+ key: string;
12
+ label: string;
13
+ icon?: React.ReactNode;
14
+ path?: string;
15
+ children?: NavigationItem[];
16
+ type?: 'group' | 'divider';
17
+ }
18
+
19
+ export interface KBSidebarProps {
20
+ items: NavigationItem[];
21
+ collapsed?: boolean;
22
+ onCollapse?: (collapsed: boolean) => void;
23
+ width?: number;
24
+ collapsedWidth?: number;
25
+ currentPath?: string;
26
+ onNavigate?: (path: string) => void;
27
+ }
28
+
29
+ export function KBSidebar({
30
+ items,
31
+ collapsed: controlledCollapsed,
32
+ onCollapse,
33
+ width = 220,
34
+ collapsedWidth = 64,
35
+ currentPath,
36
+ onNavigate,
37
+ }: KBSidebarProps) {
38
+ const [internalCollapsed, setInternalCollapsed] = React.useState(false);
39
+
40
+ const collapsed = controlledCollapsed !== undefined ? controlledCollapsed : internalCollapsed;
41
+ const setCollapsed = React.useCallback(
42
+ (newCollapsed: boolean) => {
43
+ if (controlledCollapsed === undefined) {
44
+ setInternalCollapsed(newCollapsed);
45
+ }
46
+ onCollapse?.(newCollapsed);
47
+ },
48
+ [controlledCollapsed, onCollapse]
49
+ );
50
+
51
+ const keyToPathMap = React.useMemo(() => {
52
+ const map = new Map<string, string>();
53
+ const buildMap = (navItems: NavigationItem[]) => {
54
+ for (const item of navItems) {
55
+ if (item.path && item.key) {
56
+ map.set(item.key, item.path);
57
+ }
58
+ if (item.children) {
59
+ buildMap(item.children);
60
+ }
61
+ }
62
+ };
63
+ buildMap(items);
64
+ return map;
65
+ }, [items]);
66
+
67
+ const menuItems: MenuProps['items'] = React.useMemo(() => {
68
+ const convertItems = (navItems: NavigationItem[]): MenuProps['items'] => {
69
+ return navItems.map((item) => {
70
+ const hasChildren = item.children && item.children.length > 0;
71
+
72
+ if (item.type === 'divider') {
73
+ return { type: 'divider' as const, key: item.key };
74
+ }
75
+
76
+ if (item.type === 'group' && hasChildren) {
77
+ return {
78
+ type: 'group' as const,
79
+ key: item.key,
80
+ label: item.label,
81
+ children: convertItems(item.children!),
82
+ };
83
+ }
84
+
85
+ const menuItem: any = {
86
+ key: item.key,
87
+ icon: item.icon,
88
+ label: item.label,
89
+ };
90
+
91
+ if (hasChildren && item.children) {
92
+ menuItem.children = convertItems(item.children);
93
+ }
94
+
95
+ return menuItem;
96
+ });
97
+ };
98
+
99
+ return convertItems(items);
100
+ }, [items]);
101
+
102
+ const selectedKeys = React.useMemo(() => {
103
+ if (!currentPath) { return []; }
104
+
105
+ const findKeys = (path: string, navItems: NavigationItem[]): string[] => {
106
+ for (const item of navItems) {
107
+ if (item.path === path) {
108
+ return [item.key || item.path || `item-${Math.random()}`];
109
+ }
110
+ if (item.children?.length) {
111
+ const childKeys = findKeys(path, item.children);
112
+ if (childKeys.length > 0) {return childKeys;}
113
+ }
114
+ }
115
+ for (const item of navItems) {
116
+ if (item.path && !item.children?.length && path.startsWith(item.path + '/')) {
117
+ return [item.key || item.path || `item-${Math.random()}`];
118
+ }
119
+ }
120
+ return [];
121
+ };
122
+
123
+ return findKeys(currentPath, items);
124
+ }, [currentPath, items]);
125
+
126
+ const openKeys = React.useMemo(() => {
127
+ if (!currentPath) { return []; }
128
+
129
+ const findOpenKeys = (path: string, navItems: NavigationItem[]): string[] => {
130
+ for (const item of navItems) {
131
+ if (item.children) {
132
+ const childKeys = findOpenKeys(path, item.children);
133
+ if (childKeys.length > 0) {
134
+ return [item.key || item.path || `item-${Math.random()}`, ...childKeys];
135
+ }
136
+ }
137
+ }
138
+ return [];
139
+ };
140
+
141
+ return findOpenKeys(currentPath, items);
142
+ }, [currentPath, items]);
143
+
144
+ const handleMenuClick = (e: { key: string }) => {
145
+ const path = keyToPathMap.get(e.key);
146
+ if (path && onNavigate) {
147
+ onNavigate(path);
148
+ }
149
+ };
150
+
151
+ const mainItems = (menuItems ?? []).filter((item: any) => item?.key !== 'settings');
152
+ const settingsItems = (menuItems ?? []).filter((item: any) => item?.key === 'settings');
153
+
154
+ return (
155
+ <AntSider
156
+ collapsible
157
+ collapsed={collapsed}
158
+ width={width}
159
+ collapsedWidth={collapsedWidth}
160
+ trigger={null}
161
+ className={styles.sider}
162
+ theme="light"
163
+ >
164
+ <div className={styles.header}>
165
+ <UIButton
166
+ variant="text"
167
+ icon={collapsed ? <PanelLeftOpen size={16} /> : <PanelLeftClose size={16} />}
168
+ onClick={() => setCollapsed(!collapsed)}
169
+ />
170
+ </div>
171
+
172
+ <div className={styles.scrollArea}>
173
+ <Menu
174
+ mode="inline"
175
+ selectedKeys={selectedKeys}
176
+ defaultOpenKeys={openKeys}
177
+ items={mainItems}
178
+ onClick={handleMenuClick}
179
+ className={styles.menu}
180
+ />
181
+ </div>
182
+
183
+ {settingsItems.length > 0 && (
184
+ <div className={styles.settingsArea}>
185
+ <Menu
186
+ mode="inline"
187
+ selectedKeys={selectedKeys}
188
+ items={settingsItems}
189
+ onClick={handleMenuClick}
190
+ className={styles.menu}
191
+ />
192
+ </div>
193
+ )}
194
+ </AntSider>
195
+ );
196
+ }
@@ -0,0 +1,21 @@
1
+ .label {
2
+ font-size: 11px;
3
+ font-weight: 600;
4
+ color: var(--text-tertiary);
5
+ text-transform: uppercase;
6
+ letter-spacing: 0.06em;
7
+ margin-bottom: 10px;
8
+ }
9
+
10
+ .value {
11
+ font-size: 28px;
12
+ font-weight: 700;
13
+ line-height: 1.1;
14
+ letter-spacing: -0.02em;
15
+ font-family: var(--font-heading);
16
+ color: var(--text-primary);
17
+ }
18
+
19
+ .value.positive { color: #52c41a; }
20
+ .value.negative { color: #ff4d4f; }
21
+ .value.warning { color: #faad14; }
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ import { UICard } from '@kb-labs/studio-ui-kit';
3
+ import styles from './kb-stat-card.module.css';
4
+
5
+ export interface KBStatCardProps {
6
+ label: string;
7
+ value: React.ReactNode;
8
+ variant?: 'positive' | 'negative' | 'warning' | 'default';
9
+ }
10
+
11
+ export function KBStatCard({ label, value, variant = 'default' }: KBStatCardProps) {
12
+ return (
13
+ <UICard>
14
+ <div>
15
+ <div className={styles.label}>{label}</div>
16
+ <div className={`${styles.value} ${variant !== 'default' ? styles[variant] : ''}`}>
17
+ {value}
18
+ </div>
19
+ </div>
20
+ </UICard>
21
+ );
22
+ }
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ import { UIRow, UICol } from '@kb-labs/studio-ui-kit';
3
+
4
+ export interface KBStatGridProps {
5
+ columns?: number;
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ export function KBStatGrid({ columns = 4, children }: KBStatGridProps) {
10
+ const span = 24 / columns;
11
+
12
+ return (
13
+ <UIRow gutter={[16, 16]}>
14
+ {React.Children.map(children, (child, index) => (
15
+ <UICol span={span} key={index}>
16
+ {child}
17
+ </UICol>
18
+ ))}
19
+ </UIRow>
20
+ );
21
+ }
22
+
@@ -0,0 +1,49 @@
1
+ .bar {
2
+ position: fixed;
3
+ bottom: 0;
4
+ left: 0;
5
+ right: 0;
6
+ height: 28px;
7
+ background-color: var(--bg-secondary);
8
+ border-top: 1px solid var(--border-primary);
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ padding: 0 8px;
13
+ z-index: var(--z-sticky);
14
+ font-size: 12px;
15
+ transition: background-color 150ms ease-in-out, border-color 150ms ease-in-out;
16
+ }
17
+
18
+ .side {
19
+ display: flex;
20
+ align-items: center;
21
+ }
22
+
23
+ .item {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 6px;
27
+ padding: 0 12px;
28
+ height: 28px;
29
+ font-size: 12px;
30
+ cursor: default;
31
+ color: var(--text-secondary);
32
+ transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
33
+ border-radius: 4px;
34
+ user-select: none;
35
+ }
36
+
37
+ .item.clickable {
38
+ cursor: pointer;
39
+ }
40
+
41
+ .item.clickable:hover {
42
+ background-color: var(--bg-hover);
43
+ color: var(--text-primary);
44
+ }
45
+
46
+ .itemIcon {
47
+ display: flex;
48
+ align-items: center;
49
+ }
@@ -0,0 +1,145 @@
1
+ import * as React from 'react';
2
+ import { Space, Tooltip, Badge } from 'antd';
3
+ import {
4
+ ApiOutlined,
5
+ CloudOutlined,
6
+ UserOutlined,
7
+ SettingOutlined,
8
+ QuestionCircleOutlined,
9
+ ThunderboltOutlined,
10
+ } from '@ant-design/icons';
11
+ import styles from './kb-status-bar.module.css';
12
+
13
+ export interface StatusBarItemProps {
14
+ icon?: React.ReactNode;
15
+ label: string;
16
+ tooltip?: string;
17
+ status?: 'success' | 'error' | 'warning' | 'processing' | 'default';
18
+ onClick?: () => void;
19
+ clickable?: boolean;
20
+ }
21
+
22
+ export function StatusBarItem({
23
+ icon,
24
+ label,
25
+ tooltip,
26
+ status,
27
+ onClick,
28
+ clickable = !!onClick,
29
+ }: StatusBarItemProps) {
30
+ const content = (
31
+ <div
32
+ onClick={onClick}
33
+ className={`${styles.item} ${clickable ? styles.clickable : ''}`}
34
+ >
35
+ {status && <Badge status={status} />}
36
+ {icon && <span className={styles.itemIcon}>{icon}</span>}
37
+ <span>{label}</span>
38
+ </div>
39
+ );
40
+
41
+ if (tooltip) {
42
+ return <Tooltip title={tooltip}>{content}</Tooltip>;
43
+ }
44
+
45
+ return content;
46
+ }
47
+
48
+ export interface KBStatusBarProps {
49
+ leftItems?: React.ReactNode;
50
+ rightItems?: React.ReactNode;
51
+ children?: React.ReactNode;
52
+ }
53
+
54
+ export function KBStatusBar({ leftItems, rightItems, children }: KBStatusBarProps) {
55
+ return (
56
+ <div className={styles.bar}>
57
+ {children ? (
58
+ children
59
+ ) : (
60
+ <>
61
+ <div className={styles.side}>
62
+ <Space size={0}>{leftItems}</Space>
63
+ </div>
64
+ <div className={styles.side}>
65
+ <Space size={0}>{rightItems}</Space>
66
+ </div>
67
+ </>
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export const StatusBarPresets = {
74
+ ApiConnection: ({ connected, onClick }: { connected: boolean; onClick?: () => void }) => (
75
+ <StatusBarItem
76
+ icon={<ApiOutlined />}
77
+ label={connected ? 'Connected' : 'Disconnected'}
78
+ status={connected ? 'success' : 'error'}
79
+ tooltip={connected ? 'API connection active' : 'API connection lost'}
80
+ onClick={onClick}
81
+ clickable={!!onClick}
82
+ />
83
+ ),
84
+
85
+ CloudStatus: ({ synced, onClick }: { synced: boolean; onClick?: () => void }) => (
86
+ <StatusBarItem
87
+ icon={<CloudOutlined />}
88
+ label={synced ? 'Synced' : 'Syncing...'}
89
+ status={synced ? 'success' : 'processing'}
90
+ tooltip={synced ? 'All changes synced' : 'Syncing changes...'}
91
+ onClick={onClick}
92
+ clickable={!!onClick}
93
+ />
94
+ ),
95
+
96
+ User: ({ email, onClick }: { email: string; onClick?: () => void }) => (
97
+ <StatusBarItem
98
+ icon={<UserOutlined />}
99
+ label={email}
100
+ tooltip="Current user"
101
+ onClick={onClick}
102
+ clickable={!!onClick}
103
+ />
104
+ ),
105
+
106
+ Version: ({ version, onClick }: { version: string; onClick?: () => void }) => (
107
+ <StatusBarItem
108
+ label={`v${version}`}
109
+ tooltip={`Platform version ${version}`}
110
+ onClick={onClick}
111
+ clickable={!!onClick}
112
+ />
113
+ ),
114
+
115
+ Settings: ({ onClick }: { onClick: () => void }) => (
116
+ <StatusBarItem
117
+ icon={<SettingOutlined />}
118
+ label="Settings"
119
+ tooltip="Open settings"
120
+ onClick={onClick}
121
+ clickable
122
+ />
123
+ ),
124
+
125
+ Help: ({ onClick }: { onClick: () => void }) => (
126
+ <StatusBarItem
127
+ icon={<QuestionCircleOutlined />}
128
+ label="Help"
129
+ tooltip="Get help"
130
+ onClick={onClick}
131
+ clickable
132
+ />
133
+ ),
134
+
135
+ BackgroundJobs: ({ count, onClick }: { count: number; onClick?: () => void }) => (
136
+ <StatusBarItem
137
+ icon={<ThunderboltOutlined />}
138
+ label={count === 1 ? '1 job running' : `${count} jobs running`}
139
+ status="processing"
140
+ tooltip="View background jobs"
141
+ onClick={onClick}
142
+ clickable={!!onClick}
143
+ />
144
+ ),
145
+ };