@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,121 @@
1
+ /* ── Toggle button ───────────────────────────────────────────────────── */
2
+
3
+ .toggleBtn {
4
+ position: fixed;
5
+ left: 50%;
6
+ transform: translateX(-50%);
7
+ z-index: 9999;
8
+ transition: bottom 0.2s ease;
9
+ width: 96px;
10
+ height: 22px;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ gap: 6px;
15
+ padding: 0 10px;
16
+ background: var(--bg-spotlight, #1f1f1f);
17
+ color: var(--text-inverse);
18
+ border: none;
19
+ cursor: pointer;
20
+ font-size: 11px;
21
+ font-weight: 500;
22
+ letter-spacing: 0.02em;
23
+ box-shadow: 0 -2px 8px var(--shadow-elevated);
24
+ user-select: none;
25
+ border-radius: 6px 6px 0 0;
26
+ }
27
+
28
+ .toggleBtn.open {
29
+ border-radius: 0 0 6px 6px;
30
+ }
31
+
32
+ .toggleArrow {
33
+ width: 0;
34
+ height: 0;
35
+ border-left: 5px solid transparent;
36
+ border-right: 5px solid transparent;
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .toggleArrow.arrowUp {
41
+ border-bottom: 5px solid var(--text-inverse);
42
+ }
43
+
44
+ .toggleArrow.arrowDown {
45
+ border-top: 5px solid var(--text-inverse);
46
+ }
47
+
48
+ /* ── Floating panel ──────────────────────────────────────────────────── */
49
+
50
+ .panel {
51
+ position: fixed;
52
+ bottom: 0;
53
+ left: 0;
54
+ right: 0;
55
+ z-index: 9998;
56
+ background: var(--bg-secondary);
57
+ border-top: 1px solid var(--border-primary);
58
+ box-shadow: 0 -4px 20px var(--shadow-elevated);
59
+ display: flex;
60
+ flex-direction: column;
61
+ overflow: hidden;
62
+ }
63
+
64
+ /* ── Drag handle ─────────────────────────────────────────────────────── */
65
+
66
+ .dragHandle {
67
+ height: 6px;
68
+ cursor: ns-resize;
69
+ background: transparent;
70
+ flex-shrink: 0;
71
+ position: relative;
72
+ }
73
+
74
+ .dragGrip {
75
+ position: absolute;
76
+ top: 2px;
77
+ left: 50%;
78
+ transform: translateX(-50%);
79
+ width: 36px;
80
+ height: 3px;
81
+ border-radius: 2px;
82
+ background: var(--border-secondary);
83
+ pointer-events: none;
84
+ }
85
+
86
+ /* ── Top bar ─────────────────────────────────────────────────────────── */
87
+
88
+ .topBar {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: flex-end;
92
+ gap: 2px;
93
+ padding: 0 8px;
94
+ height: 28px;
95
+ border-bottom: 1px solid var(--border-primary);
96
+ flex-shrink: 0;
97
+ }
98
+
99
+ .iconBtn {
100
+ background: none;
101
+ border: none;
102
+ cursor: pointer;
103
+ padding: 2px 6px;
104
+ font-size: 13px;
105
+ color: var(--text-secondary);
106
+ border-radius: 4px;
107
+ line-height: 1;
108
+ transition: background 0.15s, color 0.15s;
109
+ }
110
+
111
+ .iconBtn:hover {
112
+ background: var(--bg-hover);
113
+ color: var(--text-primary);
114
+ }
115
+
116
+ /* ── Content ─────────────────────────────────────────────────────────── */
117
+
118
+ .content {
119
+ flex: 1;
120
+ overflow: hidden;
121
+ }
@@ -0,0 +1,115 @@
1
+ import * as React from 'react';
2
+ import { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { UIBadge } from '@kb-labs/studio-ui-kit';
4
+ import { useDevToolsStore } from '@/hooks/use-devtools-store';
5
+ import { useFeatureFlags } from '@/hooks/use-feature-flags';
6
+ import { DevToolsLogFeed } from './devtools-log-feed';
7
+ import styles from './devtools-panel.module.css';
8
+
9
+ const MIN_HEIGHT = 120;
10
+ const DEFAULT_HEIGHT = 280;
11
+
12
+ export function DevToolsPanel() {
13
+ const { isEnabled } = useFeatureFlags();
14
+ if (!isEnabled('devtools-panel')) {return null;}
15
+ return <DevToolsPanelInner />;
16
+ }
17
+
18
+ function DevToolsPanelInner() {
19
+ const { channels } = useDevToolsStore();
20
+ const [open, setOpen] = useState(false);
21
+ const [maximized, setMaximized] = useState(false);
22
+ const [height, setHeight] = useState(DEFAULT_HEIGHT);
23
+
24
+ const errorCount = React.useMemo(() => {
25
+ let count = 0;
26
+ channels.forEach((ch) => {
27
+ (ch.getEvents() as Array<{ status?: string }>).forEach((e) => {
28
+ if (e.status === 'error' || e.status === 'warning') {count++;}
29
+ });
30
+ });
31
+ return count;
32
+ }, [channels]);
33
+
34
+ // ── Drag to resize ──────────────────────────────────────────────────────
35
+ const dragRef = useRef<{ startY: number; startH: number } | null>(null);
36
+
37
+ const onResizeMouseDown = useCallback((e: React.MouseEvent) => {
38
+ e.preventDefault();
39
+ dragRef.current = { startY: e.clientY, startH: height };
40
+
41
+ const onMove = (ev: MouseEvent) => {
42
+ if (!dragRef.current) {return;}
43
+ const delta = dragRef.current.startY - ev.clientY;
44
+ setHeight(Math.max(MIN_HEIGHT, dragRef.current.startH + delta));
45
+ setMaximized(false);
46
+ };
47
+ const onUp = () => {
48
+ dragRef.current = null;
49
+ window.removeEventListener('mousemove', onMove);
50
+ window.removeEventListener('mouseup', onUp);
51
+ };
52
+ window.addEventListener('mousemove', onMove);
53
+ window.addEventListener('mouseup', onUp);
54
+ }, [height]);
55
+
56
+ // ── Keyboard shortcut: Ctrl+Shift+D ────────────────────────────────────
57
+ useEffect(() => {
58
+ const handler = (e: KeyboardEvent) => {
59
+ if (e.ctrlKey && e.shiftKey && e.key === 'D') {
60
+ e.preventDefault();
61
+ setOpen((v) => !v);
62
+ }
63
+ };
64
+ window.addEventListener('keydown', handler);
65
+ return () => window.removeEventListener('keydown', handler);
66
+ }, []);
67
+
68
+ const panelHeight = maximized ? 'calc(100vh - 48px)' : height;
69
+ const toggleBottom = open ? (maximized ? 'calc(100vh - 48px)' : height) : 0;
70
+
71
+ return (
72
+ <>
73
+ {/* Toggle button */}
74
+ <button
75
+ className={`${styles.toggleBtn} ${open ? styles.open : ''}`}
76
+ style={{ bottom: toggleBottom }}
77
+ onClick={() => setOpen((v) => !v)}
78
+ >
79
+ <span className={`${styles.toggleArrow} ${open ? styles.arrowDown : styles.arrowUp}`} />
80
+ <span>DevTools</span>
81
+ {errorCount > 0 && <UIBadge count={errorCount} size="small" style={{ marginLeft: 2 }} />}
82
+ </button>
83
+
84
+ {/* Floating panel */}
85
+ {open && (
86
+ <div className={styles.panel} style={{ height: panelHeight }}>
87
+ <div className={styles.dragHandle} onMouseDown={onResizeMouseDown}>
88
+ <div className={styles.dragGrip} />
89
+ </div>
90
+
91
+ <div className={styles.topBar}>
92
+ <button
93
+ className={styles.iconBtn}
94
+ onClick={() => setMaximized((v) => !v)}
95
+ title={maximized ? 'Restore' : 'Maximize'}
96
+ >
97
+ {maximized ? '⊡' : '⊞'}
98
+ </button>
99
+ <button
100
+ className={styles.iconBtn}
101
+ onClick={() => setOpen(false)}
102
+ title="Close (Ctrl+Shift+D)"
103
+ >
104
+ ✕
105
+ </button>
106
+ </div>
107
+
108
+ <div className={styles.content}>
109
+ <DevToolsLogFeed />
110
+ </div>
111
+ </div>
112
+ )}
113
+ </>
114
+ );
115
+ }
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+ import { UIEmptyState as UIEmptyStateBase, UIButton } from '@kb-labs/studio-ui-kit';
3
+
4
+ export interface EmptyStateProps {
5
+ icon?: React.ReactNode;
6
+ title: string;
7
+ description: string;
8
+ action?: { label: string; onClick: () => void };
9
+ }
10
+
11
+ export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
12
+ // Convert icon component to React element if it's a component
13
+ const iconElement = React.isValidElement(icon)
14
+ ? icon
15
+ : icon && typeof icon === 'function'
16
+ ? React.createElement(icon as React.ComponentType)
17
+ : icon;
18
+
19
+ return (
20
+ <UIEmptyStateBase
21
+ icon={iconElement}
22
+ title={title}
23
+ description={description}
24
+ action={action ? <UIButton onClick={action.onClick}>{action.label}</UIButton> : undefined}
25
+ />
26
+ );
27
+ }
28
+
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @module @kb-labs/studio-app/components/ErrorBoundary
3
+ * Custom error boundary for React Router with friendly UI
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import { useRouteError, isRouteErrorResponse, Link } from 'react-router-dom';
8
+ import {
9
+ UIResult, UIButton, UICard, UITypographyText, UITypographyParagraph,
10
+ UISpace, UIAccordion, UITag, UIMessage, UIIcon,
11
+ } from '@kb-labs/studio-ui-kit';
12
+
13
+ export function ErrorBoundary() {
14
+ const error = useRouteError();
15
+ const [copied, setCopied] = React.useState(false);
16
+
17
+ let title = 'Упс! Что-то пошло не так';
18
+ let description = 'Произошла непредвиденная ошибка';
19
+ let status: 'error' | 'warning' | '404' | '500' | 'info' = 'error';
20
+ let suggestion = 'Попробуйте перезагрузить страницу или вернуться на главную';
21
+
22
+ if (isRouteErrorResponse(error)) {
23
+ // React Router error response
24
+ if (error.status === 404) {
25
+ status = '404';
26
+ title = 'Страница не найдена';
27
+ description = 'К сожалению, запрошенная страница не существует';
28
+ suggestion = 'Проверьте адрес или вернитесь на главную страницу';
29
+ } else {
30
+ status = '500';
31
+ title = `Ошибка ${error.status}`;
32
+ description = error.statusText || error.data?.message || description;
33
+ }
34
+ } else if (error instanceof Error) {
35
+ // JavaScript error
36
+ description = error.message;
37
+
38
+ // Friendly message for common errors
39
+ if (error.message.includes('Unsupported data source')) {
40
+ title = 'Ошибка конфигурации виджета';
41
+ description = 'Виджет имеет некорректную настройку источника данных';
42
+ suggestion = 'Это ошибка в манифесте плагина. Обратитесь к разработчику.';
43
+ status = 'warning';
44
+ } else if (error.message.includes('not found') || error.message.includes('404')) {
45
+ title = 'Ресурс не найден';
46
+ description = 'Запрошенный ресурс не существует';
47
+ suggestion = 'Проверьте настройки плагина или обратитесь к администратору';
48
+ status = '404';
49
+ } else if (error.message.includes('Failed to fetch')) {
50
+ title = 'Ошибка сети';
51
+ description = 'Не удалось подключиться к серверу';
52
+ suggestion = 'Проверьте подключение к интернету и попробуйте снова';
53
+ status = 'warning';
54
+ }
55
+ }
56
+
57
+ // Extract error details
58
+ const errorDetails = error instanceof Error ? {
59
+ name: error.name,
60
+ message: error.message,
61
+ stack: error.stack,
62
+ } : null;
63
+
64
+ return (
65
+ <div style={{
66
+ display: 'flex',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ minHeight: '100vh',
70
+ padding: '2rem',
71
+ backgroundColor: 'var(--bg-tertiary)',
72
+ }}>
73
+ <UICard
74
+ style={{
75
+ maxWidth: '800px',
76
+ width: '100%',
77
+ borderRadius: '16px',
78
+ boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
79
+ backgroundColor: 'var(--bg-secondary)',
80
+ }}
81
+ styles={{ body: { padding: '3rem' } }}
82
+ >
83
+ <UIResult
84
+ status={status}
85
+ title={<span style={{ fontSize: '1.5rem', fontWeight: 600 }}>{title}</span>}
86
+ subTitle={
87
+ <UISpace direction="vertical" size="middle" style={{ width: '100%', marginTop: '1rem' }}>
88
+ <UITypographyParagraph style={{ fontSize: '1rem', color: 'var(--text-secondary)', margin: 0 }}>
89
+ {description}
90
+ </UITypographyParagraph>
91
+ <UITypographyText type="secondary" style={{ fontSize: '0.875rem' }}>
92
+ 💡 {suggestion}
93
+ </UITypographyText>
94
+ </UISpace>
95
+ }
96
+ extra={
97
+ <UISpace direction="vertical" size="large" style={{ width: '100%', marginTop: '1.5rem' }}>
98
+ <UISpace size="middle">
99
+ <Link to="/">
100
+ <UIButton variant="primary" size="large" icon={<UIIcon name="HomeOutlined" />}>
101
+ На главную
102
+ </UIButton>
103
+ </Link>
104
+ <UIButton
105
+ size="large"
106
+ icon={<UIIcon name="ReloadOutlined" />}
107
+ onClick={() => window.location.reload()}
108
+ >
109
+ Перезагрузить
110
+ </UIButton>
111
+ </UISpace>
112
+
113
+ {/* Error Details Accordion */}
114
+ {errorDetails && (
115
+ <UIAccordion
116
+ ghost
117
+ style={{ width: '100%', textAlign: 'left' }}
118
+ items={[
119
+ {
120
+ key: 'details',
121
+ label: 'Детали ошибки (для разработчиков)',
122
+ children: (
123
+ <UISpace direction="vertical" size="middle" style={{ width: '100%' }}>
124
+ {/* Error Type */}
125
+ <div>
126
+ <UITypographyText type="secondary" style={{ fontSize: '0.75rem' }}>Тип ошибки:</UITypographyText>
127
+ <div style={{ marginTop: 4 }}>
128
+ <UITag variant="error" icon={<UIIcon name="CodeOutlined" />}>
129
+ {errorDetails.name}
130
+ </UITag>
131
+ </div>
132
+ </div>
133
+
134
+ {/* Error Message */}
135
+ <div>
136
+ <UITypographyText type="secondary" style={{ fontSize: '0.75rem' }}>Сообщение:</UITypographyText>
137
+ <div
138
+ style={{
139
+ marginTop: 4,
140
+ padding: '12px',
141
+ background: 'var(--bg-secondary)',
142
+ borderRadius: '8px',
143
+ fontFamily: 'monospace',
144
+ fontSize: '0.875rem',
145
+ wordBreak: 'break-word',
146
+ }}
147
+ >
148
+ <UITypographyText code>{errorDetails.message}</UITypographyText>
149
+ </div>
150
+ </div>
151
+
152
+ {/* Stack Trace */}
153
+ {errorDetails.stack && (
154
+ <div>
155
+ <UITypographyText type="secondary" style={{ fontSize: '0.75rem' }}>Stack Trace:</UITypographyText>
156
+ <div
157
+ style={{
158
+ marginTop: 4,
159
+ padding: '12px',
160
+ background: '#1e1e1e',
161
+ borderRadius: '8px',
162
+ maxHeight: '300px',
163
+ overflow: 'auto',
164
+ }}
165
+ >
166
+ <pre
167
+ style={{
168
+ margin: 0,
169
+ fontFamily: 'Monaco, Menlo, "Courier New", monospace',
170
+ fontSize: '0.75rem',
171
+ lineHeight: 1.5,
172
+ color: '#d4d4d4',
173
+ whiteSpace: 'pre-wrap',
174
+ wordBreak: 'break-word',
175
+ }}
176
+ >
177
+ {errorDetails.stack}
178
+ </pre>
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {/* Copy to Clipboard */}
184
+ <UIButton
185
+ size="small"
186
+ variant={copied ? 'primary' : 'default'}
187
+ icon={copied ? <UIIcon name="CheckOutlined" /> : <UIIcon name="CopyOutlined" />}
188
+ onClick={() => {
189
+ const text = `${errorDetails.name}: ${errorDetails.message}\n\n${errorDetails.stack || 'No stack trace'}`;
190
+ navigator.clipboard.writeText(text);
191
+ setCopied(true);
192
+ UIMessage.success('Ошибка скопирована в буфер обмена');
193
+ setTimeout(() => setCopied(false), 2000);
194
+ }}
195
+ >
196
+ {copied ? 'Скопировано!' : 'Скопировать в буфер обмена'}
197
+ </UIButton>
198
+ </UISpace>
199
+ ),
200
+ },
201
+ ]}
202
+ />
203
+ )}
204
+ </UISpace>
205
+ }
206
+ />
207
+ </UICard>
208
+ </div>
209
+ );
210
+ }
@@ -0,0 +1,103 @@
1
+ /* ── Health banner — compact top strip ───────────────────────────────── */
2
+
3
+ .strip {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 8px;
7
+ padding: 6px 16px;
8
+ font-size: 12px;
9
+ line-height: 1.4;
10
+ border-bottom: 1px solid transparent;
11
+ }
12
+
13
+ .stripError {
14
+ background: rgba(220, 38, 38, 0.07);
15
+ border-color: rgba(220, 38, 38, 0.2);
16
+ color: var(--error);
17
+ }
18
+
19
+ .stripWarning {
20
+ background: rgba(245, 158, 11, 0.07);
21
+ border-color: rgba(245, 158, 11, 0.2);
22
+ color: var(--warning);
23
+ }
24
+
25
+ .stripInfo {
26
+ background: rgba(14, 165, 233, 0.07);
27
+ border-color: rgba(14, 165, 233, 0.2);
28
+ color: var(--info);
29
+ }
30
+
31
+ .icon {
32
+ flex-shrink: 0;
33
+ display: flex;
34
+ align-items: center;
35
+ }
36
+
37
+ .message {
38
+ font-weight: 600;
39
+ flex-shrink: 0;
40
+ }
41
+
42
+ .description {
43
+ color: var(--text-secondary);
44
+ flex: 1;
45
+ overflow: hidden;
46
+ text-overflow: ellipsis;
47
+ white-space: nowrap;
48
+ }
49
+
50
+ .detailsBtn {
51
+ background: none;
52
+ border: none;
53
+ cursor: pointer;
54
+ font-size: 12px;
55
+ color: inherit;
56
+ opacity: 0.75;
57
+ padding: 0 4px;
58
+ flex-shrink: 0;
59
+ text-decoration: underline;
60
+ text-underline-offset: 2px;
61
+ }
62
+
63
+ .detailsBtn:hover {
64
+ opacity: 1;
65
+ }
66
+
67
+ .retryBtn {
68
+ background: none;
69
+ border: none;
70
+ cursor: pointer;
71
+ padding: 2px;
72
+ display: flex;
73
+ align-items: center;
74
+ color: inherit;
75
+ opacity: 0.6;
76
+ flex-shrink: 0;
77
+ border-radius: 3px;
78
+ }
79
+
80
+ .retryBtn:hover {
81
+ opacity: 1;
82
+ background: rgba(0, 0, 0, 0.06);
83
+ }
84
+
85
+ /* ── Expanded details panel ──────────────────────────────────────────── */
86
+
87
+ .details {
88
+ padding: 8px 16px 10px 40px;
89
+ font-size: 12px;
90
+ border-bottom: 1px solid var(--border-primary);
91
+ background: var(--bg-tertiary);
92
+ }
93
+
94
+ .detailsList {
95
+ margin: 4px 0 0 12px;
96
+ padding: 0;
97
+ list-style: disc;
98
+ color: var(--text-secondary);
99
+ }
100
+
101
+ .detailsList li {
102
+ margin-bottom: 2px;
103
+ }