@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,48 @@
1
+ /**
2
+ * @module @kb-labs/studio-app/modules/plugins/routes
3
+ * Plugins module routing configuration
4
+ */
5
+
6
+ import type { RouteObject } from 'react-router-dom';
7
+ import { PluginsPage } from '../pages/marketplace-page';
8
+ import { PluginDetailPage } from '../pages/plugin-detail-page';
9
+ import { ErrorBoundary } from '../../../components/error-boundary';
10
+ import { renderIcon } from '../../../routes/helpers';
11
+ import type { NavigationItem } from '@/components/ui';
12
+
13
+ // Route paths
14
+ const PATHS = {
15
+ ROOT: '/marketplace',
16
+ DETAIL: '/marketplace/:pluginId',
17
+ } as const;
18
+
19
+ // Route keys
20
+ const KEYS = {
21
+ MODULE: 'plugins',
22
+ } as const;
23
+
24
+ /**
25
+ * Plugins routes
26
+ */
27
+ export const pluginsRoutes: RouteObject[] = [
28
+ {
29
+ path: PATHS.ROOT,
30
+ element: <PluginsPage />,
31
+ errorElement: <ErrorBoundary />,
32
+ },
33
+ {
34
+ path: PATHS.DETAIL,
35
+ element: <PluginDetailPage />,
36
+ errorElement: <ErrorBoundary />,
37
+ },
38
+ ];
39
+
40
+ /**
41
+ * Plugins navigation item for sidebar
42
+ */
43
+ export const pluginsNavigation: NavigationItem = {
44
+ key: KEYS.MODULE,
45
+ label: 'Marketplace',
46
+ icon: renderIcon('AppstoreOutlined'),
47
+ path: PATHS.ROOT,
48
+ };
@@ -0,0 +1 @@
1
+ export { renderSchema } from './schema-helpers';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Helper to safely render schema objects
3
+ */
4
+ export function renderSchema(schema: any): string {
5
+ if (!schema) {
6
+ return '—';
7
+ }
8
+ if (typeof schema === 'string') {
9
+ return schema;
10
+ }
11
+ if (typeof schema === 'object') {
12
+ if ('$ref' in schema) {
13
+ return schema.$ref;
14
+ }
15
+ if ('zod' in schema) {
16
+ return schema.zod;
17
+ }
18
+ return JSON.stringify(schema);
19
+ }
20
+ return String(schema);
21
+ }
@@ -0,0 +1,415 @@
1
+ import * as React from 'react';
2
+ import { useState, useEffect, useMemo, useCallback } from 'react';
3
+ import {
4
+ UIInput,
5
+ UITag,
6
+ UISpace,
7
+ UIButton,
8
+ UITypographyText,
9
+ UITitle,
10
+ UITypographyParagraph,
11
+ UIAlert,
12
+ UISpin,
13
+ UIEmptyState,
14
+ UITooltip,
15
+ UISelect,
16
+ UITree,
17
+ UISegmented,
18
+ UIIcon,
19
+ } from '@kb-labs/studio-ui-kit';
20
+ import type { DataNode } from 'antd/es/tree';
21
+ import { useDataSources } from '../../../providers/data-sources-provider';
22
+ import type { RoutesResponse, RouteInfo } from '@kb-labs/studio-data-client';
23
+
24
+ const METHOD_COLORS: Record<string, string> = {
25
+ GET: 'green',
26
+ POST: 'blue',
27
+ PUT: 'orange',
28
+ PATCH: 'purple',
29
+ DELETE: 'red',
30
+ HEAD: 'cyan',
31
+ OPTIONS: 'default',
32
+ };
33
+
34
+ type ViewMode = 'tree' | 'list';
35
+
36
+ interface RouteTreeNode {
37
+ segment: string;
38
+ fullPath: string;
39
+ routes: RouteInfo[];
40
+ children: Map<string, RouteTreeNode>;
41
+ }
42
+
43
+ function buildRouteTree(routes: RouteInfo[]): RouteTreeNode {
44
+ const root: RouteTreeNode = {
45
+ segment: '',
46
+ fullPath: '',
47
+ routes: [],
48
+ children: new Map(),
49
+ };
50
+
51
+ for (const route of routes) {
52
+ // Split URL into segments, e.g., /api/v1/plugins/:id -> ['api', 'v1', 'plugins', ':id']
53
+ const segments = route.url.split('/').filter(Boolean);
54
+ let current = root;
55
+ let pathSoFar = '';
56
+
57
+ for (const segment of segments) {
58
+ pathSoFar += '/' + segment;
59
+
60
+ if (!current.children.has(segment)) {
61
+ current.children.set(segment, {
62
+ segment,
63
+ fullPath: pathSoFar,
64
+ routes: [],
65
+ children: new Map(),
66
+ });
67
+ }
68
+ current = current.children.get(segment)!;
69
+ }
70
+
71
+ // Add route to the leaf node
72
+ current.routes.push(route);
73
+ }
74
+
75
+ return root;
76
+ }
77
+
78
+ function treeNodeToAntdNode(
79
+ node: RouteTreeNode,
80
+ baseUrl: string,
81
+ copiedUrl: string | null,
82
+ onCopy: (url: string) => void
83
+ ): DataNode[] {
84
+ const results: DataNode[] = [];
85
+
86
+ // Sort children alphabetically
87
+ const sortedChildren = Array.from(node.children.entries()).sort((a, b) =>
88
+ a[0].localeCompare(b[0])
89
+ );
90
+
91
+ for (const [segment, child] of sortedChildren) {
92
+ const hasChildren = child.children.size > 0;
93
+ const hasRoutes = child.routes.length > 0;
94
+
95
+ // Determine if this is a parameter segment
96
+ const isParam = segment.startsWith(':') || segment.startsWith('*');
97
+
98
+ const childNodes = treeNodeToAntdNode(child, baseUrl, copiedUrl, onCopy);
99
+
100
+ // Create route entries for this node
101
+ const routeNodes: DataNode[] = child.routes.map((route) => {
102
+ const fullUrl = `${baseUrl}${route.url}`;
103
+
104
+ return {
105
+ key: `${route.method}-${route.url}`,
106
+ title: (
107
+ <UISpace size="small">
108
+ {route.method.split(',').map((m) => (
109
+ <UITag key={m} color={METHOD_COLORS[m.trim()] || 'default'} style={{ margin: 0 }}>
110
+ {m.trim()}
111
+ </UITag>
112
+ ))}
113
+ <UITypographyText code style={{ fontSize: 12 }}>
114
+ {route.url}
115
+ </UITypographyText>
116
+ <UITooltip title={copiedUrl === fullUrl ? 'Copied!' : 'Copy full URL'}>
117
+ <UIButton
118
+ variant="text"
119
+ size="small"
120
+ icon={
121
+ copiedUrl === fullUrl ? (
122
+ <UIIcon name="CheckOutlined" style={{ color: '#52c41a' }} />
123
+ ) : (
124
+ <UIIcon name="CopyOutlined" />
125
+ )
126
+ }
127
+ onClick={(e) => {
128
+ e.stopPropagation();
129
+ onCopy(fullUrl);
130
+ }}
131
+ />
132
+ </UITooltip>
133
+ </UISpace>
134
+ ),
135
+ isLeaf: true,
136
+ icon: <UIIcon name="FileOutlined" />,
137
+ };
138
+ });
139
+
140
+ if (hasChildren || hasRoutes) {
141
+ results.push({
142
+ key: child.fullPath,
143
+ title: (
144
+ <UISpace size="small">
145
+ <UITypographyText strong={hasChildren} type={isParam ? 'warning' : undefined}>
146
+ {segment}
147
+ </UITypographyText>
148
+ {hasRoutes && !hasChildren && (
149
+ <UITypographyText type="secondary" style={{ fontSize: 11 }}>
150
+ ({child.routes.length})
151
+ </UITypographyText>
152
+ )}
153
+ </UISpace>
154
+ ),
155
+ icon: hasChildren ? <UIIcon name="FolderOutlined" /> : undefined,
156
+ children: [...routeNodes, ...childNodes],
157
+ isLeaf: !hasChildren && routeNodes.length === 0,
158
+ });
159
+ }
160
+ }
161
+
162
+ return results;
163
+ }
164
+
165
+ export function ApiRoutesViewer() {
166
+ const { system } = useDataSources();
167
+ const [loading, setLoading] = useState(false);
168
+ const [error, setError] = useState<string | null>(null);
169
+ const [data, setData] = useState<RoutesResponse | null>(null);
170
+ const [searchQuery, setSearchQuery] = useState('');
171
+ const [methodFilter, setMethodFilter] = useState<string | null>(null);
172
+ const [copiedUrl, setCopiedUrl] = useState<string | null>(null);
173
+ const [viewMode, setViewMode] = useState<ViewMode>('tree');
174
+ const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
175
+
176
+ const baseUrl = system.getBaseUrl();
177
+
178
+ const fetchRoutes = useCallback(async () => {
179
+ setLoading(true);
180
+ setError(null);
181
+ try {
182
+ const result = await system.getRoutes();
183
+ setData(result);
184
+
185
+ // Auto-expand first two levels
186
+ if (result.routes.length > 0) {
187
+ const tree = buildRouteTree(result.routes);
188
+ const keysToExpand: string[] = [];
189
+ for (const [, child] of tree.children) {
190
+ keysToExpand.push(child.fullPath);
191
+ for (const [, grandChild] of child.children) {
192
+ keysToExpand.push(grandChild.fullPath);
193
+ }
194
+ }
195
+ setExpandedKeys(keysToExpand);
196
+ }
197
+ } catch (err) {
198
+ setError(err instanceof Error ? err.message : String(err));
199
+ } finally {
200
+ setLoading(false);
201
+ }
202
+ }, [system]);
203
+
204
+ useEffect(() => {
205
+ fetchRoutes();
206
+ }, [fetchRoutes]);
207
+
208
+ const handleCopyUrl = (url: string) => {
209
+ navigator.clipboard.writeText(url);
210
+ setCopiedUrl(url);
211
+ setTimeout(() => setCopiedUrl(null), 2000);
212
+ };
213
+
214
+ // Get unique methods for filter
215
+ const availableMethods = useMemo(() => {
216
+ if (!data?.routes) {return [];}
217
+ const methods = new Set<string>();
218
+ data.routes.forEach((route) => {
219
+ route.method.split(',').forEach((m) => methods.add(m.trim()));
220
+ });
221
+ return Array.from(methods).sort();
222
+ }, [data]);
223
+
224
+ // Filter routes
225
+ const filteredRoutes = useMemo(() => {
226
+ if (!data?.routes) {return [];}
227
+ return data.routes.filter((route) => {
228
+ // Search filter
229
+ const matchesSearch =
230
+ !searchQuery ||
231
+ route.url.toLowerCase().includes(searchQuery.toLowerCase()) ||
232
+ route.method.toLowerCase().includes(searchQuery.toLowerCase());
233
+
234
+ // Method filter
235
+ const matchesMethod = !methodFilter || route.method.includes(methodFilter);
236
+
237
+ return matchesSearch && matchesMethod;
238
+ });
239
+ }, [data, searchQuery, methodFilter]);
240
+
241
+ // Build tree structure
242
+ const treeData = useMemo(() => {
243
+ if (filteredRoutes.length === 0) {return [];}
244
+ const tree = buildRouteTree(filteredRoutes);
245
+ return treeNodeToAntdNode(tree, baseUrl, copiedUrl, handleCopyUrl);
246
+ }, [filteredRoutes, baseUrl, copiedUrl]);
247
+
248
+ // Flat list for list view
249
+ const sortedRoutes = useMemo(() => {
250
+ return [...filteredRoutes].sort((a, b) => {
251
+ const urlCompare = a.url.localeCompare(b.url);
252
+ if (urlCompare !== 0) {return urlCompare;}
253
+ return a.method.localeCompare(b.method);
254
+ });
255
+ }, [filteredRoutes]);
256
+
257
+ return (
258
+ <UISpace direction="vertical" style={{ width: '100%' }} size="middle">
259
+ <div>
260
+ <UITitle level={5}>
261
+ <UIIcon name="ApiOutlined" /> API Routes Explorer
262
+ </UITitle>
263
+ <UITypographyParagraph type="secondary">
264
+ Browse all available REST API endpoints from the backend. Use search and filters to find
265
+ specific routes.
266
+ </UITypographyParagraph>
267
+ </div>
268
+
269
+ {/* Controls */}
270
+ <UISpace wrap>
271
+ <UIInput
272
+ placeholder="Search routes..."
273
+ prefix={<UIIcon name="SearchOutlined" />}
274
+ value={searchQuery}
275
+ onChange={(value) => setSearchQuery(value)}
276
+ style={{ width: 300 }}
277
+ allowClear
278
+ />
279
+ <UISelect
280
+ placeholder="Filter by method"
281
+ allowClear
282
+ style={{ width: 150 }}
283
+ value={methodFilter ?? undefined}
284
+ onChange={(v) => setMethodFilter(v as string | null)}
285
+ options={availableMethods.map((m) => ({ label: m, value: m }))}
286
+ />
287
+ <UISegmented
288
+ value={viewMode}
289
+ onChange={(v) => setViewMode(v as ViewMode)}
290
+ options={[
291
+ { label: 'Tree', value: 'tree', icon: <UIIcon name="ApartmentOutlined" /> },
292
+ { label: 'List', value: 'list', icon: <UIIcon name="UnorderedListOutlined" /> },
293
+ ]}
294
+ />
295
+ <UIButton icon={<UIIcon name="ReloadOutlined" />} onClick={fetchRoutes} loading={loading}>
296
+ Refresh
297
+ </UIButton>
298
+ </UISpace>
299
+
300
+ {/* Stats */}
301
+ {data && (
302
+ <UIAlert
303
+ variant="info"
304
+ showIcon={false}
305
+ message={
306
+ <UISpace split={<span style={{ color: '#d9d9d9' }}>|</span>}>
307
+ <span>
308
+ <UITypographyText strong>{data.count}</UITypographyText> total routes
309
+ </span>
310
+ <span>
311
+ <UITypographyText strong>{filteredRoutes.length}</UITypographyText> shown
312
+ </span>
313
+ <span>
314
+ <UITypographyText type="secondary">
315
+ Last fetched: {new Date(data.ts).toLocaleTimeString()}
316
+ </UITypographyText>
317
+ </span>
318
+ <span>
319
+ <UITypographyText type="secondary">Base: {baseUrl}</UITypographyText>
320
+ </span>
321
+ </UISpace>
322
+ }
323
+ />
324
+ )}
325
+
326
+ {/* Error */}
327
+ {error && (
328
+ <UIAlert variant="error" message="Failed to load routes" description={error} showIcon />
329
+ )}
330
+
331
+ {/* Loading */}
332
+ {loading && !data && (
333
+ <div style={{ textAlign: 'center', padding: 40 }}>
334
+ <UISpin size="large" />
335
+ </div>
336
+ )}
337
+
338
+ {/* Tree View */}
339
+ {data && viewMode === 'tree' && treeData.length > 0 && (
340
+ <UITree
341
+ treeData={treeData}
342
+ showIcon
343
+ showLine={{ showLeafIcon: false }}
344
+ expandedKeys={expandedKeys}
345
+ onExpand={(keys) => setExpandedKeys(keys)}
346
+ style={{
347
+ background: 'transparent',
348
+ padding: '8px 0',
349
+ }}
350
+ />
351
+ )}
352
+
353
+ {/* List View */}
354
+ {data && viewMode === 'list' && (
355
+ <div style={{ maxHeight: 500, overflow: 'auto' }}>
356
+ <UISpace direction="vertical" size="small" style={{ width: '100%' }}>
357
+ {sortedRoutes.map((route) => {
358
+ const fullUrl = route.url.startsWith('/api')
359
+ ? `http://localhost:5050${route.url}`
360
+ : `${baseUrl}${route.url}`;
361
+
362
+ return (
363
+ <div
364
+ key={`${route.method}-${route.url}`}
365
+ style={{
366
+ display: 'flex',
367
+ alignItems: 'center',
368
+ gap: 8,
369
+ padding: '4px 8px',
370
+ borderRadius: 4,
371
+ background: 'rgba(0,0,0,0.02)',
372
+ }}
373
+ >
374
+ <UISpace size={4}>
375
+ {route.method.split(',').map((m) => (
376
+ <UITag
377
+ key={m}
378
+ color={METHOD_COLORS[m.trim()] || 'default'}
379
+ style={{ margin: 0, minWidth: 50, textAlign: 'center' }}
380
+ >
381
+ {m.trim()}
382
+ </UITag>
383
+ ))}
384
+ </UISpace>
385
+ <UITypographyText code style={{ fontSize: 12, flex: 1 }}>
386
+ {route.url}
387
+ </UITypographyText>
388
+ <UITooltip title={copiedUrl === fullUrl ? 'Copied!' : 'Copy full URL'}>
389
+ <UIButton
390
+ variant="text"
391
+ size="small"
392
+ icon={
393
+ copiedUrl === fullUrl ? (
394
+ <UIIcon name="CheckOutlined" style={{ color: '#52c41a' }} />
395
+ ) : (
396
+ <UIIcon name="CopyOutlined" />
397
+ )
398
+ }
399
+ onClick={() => handleCopyUrl(fullUrl)}
400
+ />
401
+ </UITooltip>
402
+ </div>
403
+ );
404
+ })}
405
+ </UISpace>
406
+ </div>
407
+ )}
408
+
409
+ {/* Empty */}
410
+ {!loading && !error && filteredRoutes.length === 0 && data && (
411
+ <UIEmptyState description="No routes match your search" />
412
+ )}
413
+ </UISpace>
414
+ );
415
+ }
@@ -0,0 +1,200 @@
1
+ import * as React from 'react';
2
+ import {
3
+ UIRadio,
4
+ UIRadioGroup,
5
+ UISwitch,
6
+ UISpace,
7
+ UICard,
8
+ UITypographyText,
9
+ UITypographyParagraph,
10
+ } from '@kb-labs/studio-ui-kit';
11
+ import { Sun, Moon, Monitor } from 'lucide-react';
12
+ import { useSettings } from '@/providers/settings-provider';
13
+ import { useKBTheme } from '@/components/ui';
14
+
15
+ const FONT_SIZES = [
16
+ { value: 'small' as const, label: 'Small', description: '13px - Compact view' },
17
+ { value: 'medium' as const, label: 'Medium', description: '14px - Default' },
18
+ { value: 'large' as const, label: 'Large', description: '16px - Better readability' },
19
+ ];
20
+
21
+ export function AppearanceSettings() {
22
+ const { theme, setTheme } = useKBTheme();
23
+ const { settings, updateSettings } = useSettings();
24
+
25
+ const handleThemeChange = (newTheme: 'light' | 'dark' | 'auto') => {
26
+ setTheme(newTheme);
27
+ updateSettings({
28
+ appearance: {
29
+ ...settings.appearance,
30
+ theme: newTheme,
31
+ },
32
+ });
33
+ };
34
+
35
+ const handleCompactModeChange = (checked: boolean) => {
36
+ updateSettings({
37
+ appearance: {
38
+ ...settings.appearance,
39
+ compactMode: checked,
40
+ },
41
+ });
42
+ };
43
+
44
+ const handleFontSizeChange = (e: any) => {
45
+ updateSettings({
46
+ appearance: {
47
+ ...settings.appearance,
48
+ fontSize: e.target.value,
49
+ },
50
+ });
51
+ };
52
+
53
+ return (
54
+ <UISpace direction="vertical" size="large" style={{ width: '100%' }}>
55
+ {/* Theme Selection */}
56
+ <div>
57
+ <UITypographyText strong style={{ fontSize: 16, display: 'block', marginBottom: 12 }}>
58
+ Theme
59
+ </UITypographyText>
60
+ <UITypographyParagraph type="secondary" style={{ marginBottom: 16 }}>
61
+ Choose how KB Studio looks to you. Select a single theme, or sync with your system.
62
+ </UITypographyParagraph>
63
+
64
+ <UISpace direction="horizontal" size="middle" style={{ width: '100%' }}>
65
+ <UICard
66
+ hoverable
67
+ onClick={() => handleThemeChange('light')}
68
+ style={{
69
+ width: 200,
70
+ border: theme === 'light' ? '2px solid var(--link)' : '1px solid var(--border-primary)',
71
+ cursor: 'pointer',
72
+ }}
73
+ styles={{ body: { padding: 16 } }}
74
+ >
75
+ <div style={{ textAlign: 'center' }}>
76
+ <Sun size={32} style={{ marginBottom: 8, color: '#F59E0B' }} />
77
+ <div style={{ fontWeight: 600, marginBottom: 4 }}>Light</div>
78
+ <UITypographyText type="secondary" style={{ fontSize: 12 }}>
79
+ Bright and clean
80
+ </UITypographyText>
81
+ </div>
82
+ <div
83
+ style={{
84
+ marginTop: 12,
85
+ height: 40,
86
+ background: 'linear-gradient(to right, #F9FAFB, #FFFFFF)',
87
+ border: '1px solid #E5E7EB',
88
+ borderRadius: 4,
89
+ }}
90
+ />
91
+ </UICard>
92
+
93
+ <UICard
94
+ hoverable
95
+ onClick={() => handleThemeChange('dark')}
96
+ style={{
97
+ width: 200,
98
+ border: theme === 'dark' ? '2px solid var(--link)' : '1px solid var(--border-primary)',
99
+ cursor: 'pointer',
100
+ }}
101
+ styles={{ body: { padding: 16 } }}
102
+ >
103
+ <div style={{ textAlign: 'center' }}>
104
+ <Moon size={32} style={{ marginBottom: 8, color: '#6366F1' }} />
105
+ <div style={{ fontWeight: 600, marginBottom: 4 }}>Dark</div>
106
+ <UITypographyText type="secondary" style={{ fontSize: 12 }}>
107
+ Easy on the eyes
108
+ </UITypographyText>
109
+ </div>
110
+ <div
111
+ style={{
112
+ marginTop: 12,
113
+ height: 40,
114
+ background: 'linear-gradient(to right, #0D0D0F, #1A1A1C)',
115
+ border: '1px solid #3F3F46',
116
+ borderRadius: 4,
117
+ }}
118
+ />
119
+ </UICard>
120
+
121
+ <UICard
122
+ hoverable
123
+ onClick={() => handleThemeChange('auto')}
124
+ style={{
125
+ width: 200,
126
+ border: theme === 'auto' ? '2px solid var(--link)' : '1px solid var(--border-primary)',
127
+ cursor: 'pointer',
128
+ }}
129
+ styles={{ body: { padding: 16 } }}
130
+ >
131
+ <div style={{ textAlign: 'center' }}>
132
+ <Monitor size={32} style={{ marginBottom: 8, color: '#8B5CF6' }} />
133
+ <div style={{ fontWeight: 600, marginBottom: 4 }}>Auto</div>
134
+ <UITypographyText type="secondary" style={{ fontSize: 12 }}>
135
+ Sync with system
136
+ </UITypographyText>
137
+ </div>
138
+ <div
139
+ style={{
140
+ marginTop: 12,
141
+ height: 40,
142
+ background: 'linear-gradient(to right, #F9FAFB 50%, #1A1A1C 50%)',
143
+ border: '1px solid var(--border-primary)',
144
+ borderRadius: 4,
145
+ }}
146
+ />
147
+ </UICard>
148
+ </UISpace>
149
+ </div>
150
+
151
+ {/* Compact Mode */}
152
+ <div>
153
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
154
+ <div>
155
+ <UITypographyText strong style={{ fontSize: 16, display: 'block' }}>
156
+ Compact Mode
157
+ </UITypographyText>
158
+ <UITypographyParagraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
159
+ Reduce spacing and padding for a denser layout
160
+ </UITypographyParagraph>
161
+ </div>
162
+ <UISwitch
163
+ checked={settings.appearance.compactMode}
164
+ onChange={handleCompactModeChange}
165
+ />
166
+ </div>
167
+ </div>
168
+
169
+ {/* Font Size */}
170
+ <div>
171
+ <UITypographyText strong style={{ fontSize: 16, display: 'block', marginBottom: 12 }}>
172
+ Font Size
173
+ </UITypographyText>
174
+ <UITypographyParagraph type="secondary" style={{ marginBottom: 16 }}>
175
+ Adjust the base font size across the application
176
+ </UITypographyParagraph>
177
+
178
+ <UIRadioGroup
179
+ value={settings.appearance.fontSize}
180
+ onChange={handleFontSizeChange}
181
+ style={{ width: '100%' }}
182
+ >
183
+ <UISpace direction="vertical" style={{ width: '100%' }}>
184
+ {FONT_SIZES.map((size) => (
185
+ <UIRadio key={size.value} value={size.value} style={{ display: 'flex', alignItems: 'center' }}>
186
+ <div>
187
+ <UITypographyText strong>{size.label}</UITypographyText>
188
+ <br />
189
+ <UITypographyText type="secondary" style={{ fontSize: 12 }}>
190
+ {size.description}
191
+ </UITypographyText>
192
+ </div>
193
+ </UIRadio>
194
+ ))}
195
+ </UISpace>
196
+ </UIRadioGroup>
197
+ </div>
198
+ </UISpace>
199
+ );
200
+ }