@mandujs/core 0.18.22 β†’ 0.19.2

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 (91) hide show
  1. package/README.ko.md +0 -14
  2. package/package.json +4 -1
  3. package/src/brain/architecture/analyzer.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +18 -14
  5. package/src/bundler/build.test.ts +127 -0
  6. package/src/bundler/build.ts +291 -113
  7. package/src/bundler/css.ts +20 -5
  8. package/src/bundler/dev.ts +55 -2
  9. package/src/bundler/prerender.ts +195 -0
  10. package/src/change/snapshot.ts +4 -23
  11. package/src/change/types.ts +2 -3
  12. package/src/client/Form.tsx +105 -0
  13. package/src/client/__tests__/use-sse.test.ts +153 -0
  14. package/src/client/hooks.ts +105 -6
  15. package/src/client/index.ts +35 -6
  16. package/src/client/router.ts +670 -433
  17. package/src/client/rpc.ts +140 -0
  18. package/src/client/runtime.ts +24 -21
  19. package/src/client/use-fetch.ts +239 -0
  20. package/src/client/use-head.ts +197 -0
  21. package/src/client/use-sse.ts +378 -0
  22. package/src/components/Image.tsx +162 -0
  23. package/src/config/mandu.ts +5 -0
  24. package/src/config/validate.ts +34 -0
  25. package/src/content/index.ts +5 -1
  26. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  27. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  28. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  29. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  30. package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
  31. package/src/devtools/client/components/panel/index.ts +45 -32
  32. package/src/devtools/client/components/panel/panel-container.tsx +332 -312
  33. package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
  34. package/src/devtools/client/state-manager.ts +535 -478
  35. package/src/devtools/design-tokens.ts +265 -264
  36. package/src/devtools/types.ts +345 -319
  37. package/src/filling/context.ts +65 -0
  38. package/src/filling/filling.ts +336 -14
  39. package/src/filling/index.ts +5 -1
  40. package/src/filling/session.ts +216 -0
  41. package/src/filling/ws.ts +78 -0
  42. package/src/generator/generate.ts +2 -2
  43. package/src/guard/auto-correct.ts +0 -29
  44. package/src/guard/check.ts +14 -31
  45. package/src/guard/presets/index.ts +296 -294
  46. package/src/guard/rules.ts +15 -19
  47. package/src/guard/validator.ts +834 -834
  48. package/src/index.ts +5 -1
  49. package/src/island/index.ts +373 -304
  50. package/src/kitchen/api/contract-api.ts +225 -0
  51. package/src/kitchen/api/diff-parser.ts +108 -0
  52. package/src/kitchen/api/file-api.ts +273 -0
  53. package/src/kitchen/api/guard-api.ts +83 -0
  54. package/src/kitchen/api/guard-decisions.ts +100 -0
  55. package/src/kitchen/api/routes-api.ts +50 -0
  56. package/src/kitchen/index.ts +21 -0
  57. package/src/kitchen/kitchen-handler.ts +256 -0
  58. package/src/kitchen/kitchen-ui.ts +1732 -0
  59. package/src/kitchen/stream/activity-sse.ts +145 -0
  60. package/src/kitchen/stream/file-tailer.ts +99 -0
  61. package/src/middleware/compress.ts +62 -0
  62. package/src/middleware/cors.ts +47 -0
  63. package/src/middleware/index.ts +10 -0
  64. package/src/middleware/jwt.ts +134 -0
  65. package/src/middleware/logger.ts +58 -0
  66. package/src/middleware/timeout.ts +55 -0
  67. package/src/paths.ts +0 -4
  68. package/src/plugins/hooks.ts +64 -0
  69. package/src/plugins/index.ts +3 -0
  70. package/src/plugins/types.ts +5 -0
  71. package/src/report/build.ts +0 -6
  72. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  73. package/src/router/fs-patterns.ts +11 -1
  74. package/src/router/fs-routes.ts +78 -14
  75. package/src/router/fs-scanner.ts +2 -2
  76. package/src/router/fs-types.ts +2 -1
  77. package/src/runtime/adapter-bun.ts +62 -0
  78. package/src/runtime/adapter.ts +47 -0
  79. package/src/runtime/cache.ts +310 -0
  80. package/src/runtime/handler.ts +65 -0
  81. package/src/runtime/image-handler.ts +195 -0
  82. package/src/runtime/index.ts +12 -0
  83. package/src/runtime/middleware.ts +263 -0
  84. package/src/runtime/server.ts +686 -92
  85. package/src/runtime/ssr.ts +55 -29
  86. package/src/runtime/streaming-ssr.ts +106 -82
  87. package/src/spec/index.ts +0 -1
  88. package/src/spec/schema.ts +1 -0
  89. package/src/testing/index.ts +144 -0
  90. package/src/watcher/watcher.ts +27 -1
  91. package/src/spec/lock.ts +0 -56
@@ -1,312 +1,332 @@
1
- /**
2
- * Mandu Kitchen DevTools - Panel Container
3
- * @version 1.0.3
4
- */
5
-
6
- import React, { useState, useCallback } from 'react';
7
- import { colors, typography, spacing, borderRadius, shadows, zIndex, animation, testIds } from '../../../design-tokens';
8
- import type { KitchenState } from '../../state-manager';
9
-
10
- // ============================================================================
11
- // Types
12
- // ============================================================================
13
-
14
- export type TabId = 'errors' | 'islands' | 'network' | 'guard';
15
-
16
- export interface TabDefinition {
17
- id: TabId;
18
- label: string;
19
- icon: string;
20
- testId: string;
21
- }
22
-
23
- export interface PanelContainerProps {
24
- state: KitchenState;
25
- activeTab: TabId;
26
- onTabChange: (tab: TabId) => void;
27
- onClose: () => void;
28
- onRestart?: () => void;
29
- position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
30
- children: React.ReactNode;
31
- }
32
-
33
- // ============================================================================
34
- // Constants
35
- // ============================================================================
36
-
37
- export const TABS: TabDefinition[] = [
38
- { id: 'errors', label: 'μ—λŸ¬', icon: 'πŸ”₯', testId: testIds.tabErrors },
39
- { id: 'islands', label: 'Islands', icon: '🏝️', testId: testIds.tabIslands },
40
- { id: 'network', label: 'Network', icon: 'πŸ“‘', testId: testIds.tabNetwork },
41
- { id: 'guard', label: 'Guard', icon: 'πŸ›‘οΈ', testId: testIds.tabGuard },
42
- ];
43
-
44
- // ============================================================================
45
- // Styles
46
- // ============================================================================
47
-
48
- const styles = {
49
- container: {
50
- position: 'fixed' as const,
51
- width: '420px',
52
- maxHeight: '70vh',
53
- backgroundColor: colors.background.dark,
54
- borderRadius: borderRadius.lg,
55
- boxShadow: shadows.xl,
56
- display: 'flex',
57
- flexDirection: 'column' as const,
58
- overflow: 'hidden',
59
- zIndex: zIndex.devtools,
60
- fontFamily: typography.fontFamily.sans,
61
- transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
62
- },
63
- header: {
64
- display: 'flex',
65
- alignItems: 'center',
66
- justifyContent: 'space-between',
67
- padding: `${spacing.sm} ${spacing.md}`,
68
- borderBottom: `1px solid ${colors.background.light}`,
69
- backgroundColor: colors.background.medium,
70
- },
71
- title: {
72
- display: 'flex',
73
- alignItems: 'center',
74
- gap: spacing.sm,
75
- fontSize: typography.fontSize.sm,
76
- fontWeight: typography.fontWeight.semibold,
77
- color: colors.text.primary,
78
- },
79
- logo: {
80
- fontSize: typography.fontSize.lg,
81
- },
82
- headerActions: {
83
- display: 'flex',
84
- alignItems: 'center',
85
- gap: spacing.xs,
86
- },
87
- restartButton: {
88
- background: 'transparent',
89
- border: 'none',
90
- color: colors.text.secondary,
91
- fontSize: typography.fontSize.md,
92
- cursor: 'pointer',
93
- padding: spacing.xs,
94
- borderRadius: borderRadius.sm,
95
- lineHeight: 1,
96
- transition: `all ${animation.duration.fast}`,
97
- },
98
- closeButton: {
99
- background: 'transparent',
100
- border: 'none',
101
- color: colors.text.secondary,
102
- fontSize: typography.fontSize.lg,
103
- cursor: 'pointer',
104
- padding: spacing.xs,
105
- borderRadius: borderRadius.sm,
106
- lineHeight: 1,
107
- transition: `color ${animation.duration.fast}`,
108
- },
109
- tabs: {
110
- display: 'flex',
111
- borderBottom: `1px solid ${colors.background.light}`,
112
- backgroundColor: colors.background.dark,
113
- },
114
- tab: {
115
- flex: 1,
116
- display: 'flex',
117
- alignItems: 'center',
118
- justifyContent: 'center',
119
- gap: spacing.xs,
120
- padding: `${spacing.sm} ${spacing.md}`,
121
- background: 'transparent',
122
- border: 'none',
123
- borderBottom: '2px solid transparent',
124
- color: colors.text.secondary,
125
- fontSize: typography.fontSize.sm,
126
- cursor: 'pointer',
127
- transition: `all ${animation.duration.fast}`,
128
- },
129
- tabActive: {
130
- color: colors.brand.accent,
131
- borderBottomColor: colors.brand.accent,
132
- backgroundColor: 'rgba(232, 150, 122, 0.12)',
133
- },
134
- tabIcon: {
135
- fontSize: typography.fontSize.md,
136
- },
137
- tabBadge: {
138
- minWidth: '18px',
139
- height: '18px',
140
- padding: '0 4px',
141
- borderRadius: borderRadius.full,
142
- backgroundColor: colors.semantic.error,
143
- color: colors.text.primary,
144
- fontSize: typography.fontSize.xs,
145
- fontWeight: typography.fontWeight.semibold,
146
- display: 'flex',
147
- alignItems: 'center',
148
- justifyContent: 'center',
149
- },
150
- content: {
151
- flex: 1,
152
- overflow: 'auto',
153
- minHeight: '200px',
154
- maxHeight: '50vh',
155
- },
156
- resizeHandle: {
157
- position: 'absolute' as const,
158
- width: '100%',
159
- height: '4px',
160
- cursor: 'ns-resize',
161
- backgroundColor: 'transparent',
162
- },
163
- };
164
-
165
- const positionStyles: Record<string, React.CSSProperties> = {
166
- 'bottom-right': { bottom: '80px', right: '16px' },
167
- 'bottom-left': { bottom: '80px', left: '16px' },
168
- 'top-right': { top: '16px', right: '16px' },
169
- 'top-left': { top: '16px', left: '16px' },
170
- };
171
-
172
- // ============================================================================
173
- // Component
174
- // ============================================================================
175
-
176
- export function PanelContainer({
177
- state,
178
- activeTab,
179
- onTabChange,
180
- onClose,
181
- onRestart,
182
- position,
183
- children,
184
- }: PanelContainerProps): React.ReactElement {
185
- const [isResizing, setIsResizing] = useState(false);
186
- const [height, setHeight] = useState(400);
187
- const [hoveredTab, setHoveredTab] = useState<TabId | null>(null);
188
- const [isCloseHovered, setIsCloseHovered] = useState(false);
189
- const [isRestartHovered, setIsRestartHovered] = useState(false);
190
-
191
- const getTabBadgeCount = useCallback((tabId: TabId): number => {
192
- switch (tabId) {
193
- case 'errors':
194
- return state.errors.filter(e => e.severity === 'error' || e.severity === 'critical').length;
195
- case 'islands':
196
- return Array.from(state.islands.values()).filter(i => i.status === 'error').length;
197
- case 'guard':
198
- return state.guardViolations.length;
199
- default:
200
- return 0;
201
- }
202
- }, [state]);
203
-
204
- const containerStyle: React.CSSProperties = {
205
- ...styles.container,
206
- ...positionStyles[position],
207
- height: `${height}px`,
208
- };
209
-
210
- return (
211
- <div
212
- data-testid={testIds.panel}
213
- style={containerStyle}
214
- >
215
- {/* Resize Handle (top) */}
216
- {position.startsWith('bottom') && (
217
- <div
218
- style={{ ...styles.resizeHandle, top: 0 }}
219
- onMouseDown={() => setIsResizing(true)}
220
- />
221
- )}
222
-
223
- {/* Header */}
224
- <div style={styles.header}>
225
- <div style={styles.title}>
226
- <span style={styles.logo}>πŸ₯Ÿ</span>
227
- <span>Mandu Kitchen</span>
228
- </div>
229
- <div style={styles.headerActions}>
230
- {onRestart && (
231
- <button
232
- data-testid={testIds.restartButton}
233
- style={{
234
- ...styles.restartButton,
235
- ...(isRestartHovered ? {
236
- color: colors.semantic.warning,
237
- backgroundColor: `${colors.semantic.warning}18`,
238
- } : {}),
239
- }}
240
- onClick={onRestart}
241
- onMouseEnter={() => setIsRestartHovered(true)}
242
- onMouseLeave={() => setIsRestartHovered(false)}
243
- aria-label="μΊμ‹œ μ§€μš°κ³  μ™„μ „ μž¬μ‹œμž‘"
244
- title="μΊμ‹œ μ§€μš°κ³  μ™„μ „ μž¬μ‹œμž‘"
245
- >
246
- πŸ”„
247
- </button>
248
- )}
249
- <button
250
- style={{
251
- ...styles.closeButton,
252
- ...(isCloseHovered ? { color: colors.text.primary, backgroundColor: 'rgba(255, 255, 255, 0.08)' } : {}),
253
- }}
254
- onClick={onClose}
255
- onMouseEnter={() => setIsCloseHovered(true)}
256
- onMouseLeave={() => setIsCloseHovered(false)}
257
- aria-label="νŒ¨λ„ λ‹«κΈ°"
258
- >
259
- Γ—
260
- </button>
261
- </div>
262
- </div>
263
-
264
- {/* Tabs */}
265
- <div style={styles.tabs} role="tablist">
266
- {TABS.map((tab) => {
267
- const isActive = activeTab === tab.id;
268
- const badgeCount = getTabBadgeCount(tab.id);
269
-
270
- return (
271
- <button
272
- key={tab.id}
273
- data-testid={tab.testId}
274
- role="tab"
275
- aria-selected={isActive}
276
- style={{
277
- ...styles.tab,
278
- ...(isActive ? styles.tabActive : {}),
279
- ...(!isActive && hoveredTab === tab.id ? {
280
- color: colors.text.primary,
281
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
282
- } : {}),
283
- }}
284
- onClick={() => onTabChange(tab.id)}
285
- onMouseEnter={() => setHoveredTab(tab.id)}
286
- onMouseLeave={() => setHoveredTab(null)}
287
- >
288
- <span style={styles.tabIcon}>{tab.icon}</span>
289
- <span>{tab.label}</span>
290
- {badgeCount > 0 && (
291
- <span style={styles.tabBadge}>{badgeCount > 99 ? '99+' : badgeCount}</span>
292
- )}
293
- </button>
294
- );
295
- })}
296
- </div>
297
-
298
- {/* Content */}
299
- <div style={styles.content} role="tabpanel">
300
- {children}
301
- </div>
302
-
303
- {/* Resize Handle (bottom) */}
304
- {position.startsWith('top') && (
305
- <div
306
- style={{ ...styles.resizeHandle, bottom: 0 }}
307
- onMouseDown={() => setIsResizing(true)}
308
- />
309
- )}
310
- </div>
311
- );
312
- }
1
+ /**
2
+ * Mandu Kitchen DevTools - Panel Container
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React, { useState, useCallback } from 'react';
7
+ import { colors, typography, spacing, borderRadius, shadows, zIndex, animation, testIds } from '../../../design-tokens';
8
+ import type { KitchenState } from '../../state-manager';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type TabId = 'errors' | 'islands' | 'network' | 'guard' | 'preview';
15
+
16
+ export interface TabDefinition {
17
+ id: TabId;
18
+ label: string;
19
+ icon: string;
20
+ testId: string;
21
+ }
22
+
23
+ export interface PanelContainerProps {
24
+ state: KitchenState;
25
+ activeTab: TabId;
26
+ onTabChange: (tab: TabId) => void;
27
+ onClose: () => void;
28
+ onRestart?: () => void;
29
+ position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
30
+ children: React.ReactNode;
31
+ }
32
+
33
+ // ============================================================================
34
+ // Constants
35
+ // ============================================================================
36
+
37
+ export const TABS: TabDefinition[] = [
38
+ { id: 'errors', label: 'μ—λŸ¬', icon: 'πŸ”₯', testId: testIds.tabErrors },
39
+ { id: 'islands', label: 'Islands', icon: '🏝️', testId: testIds.tabIslands },
40
+ { id: 'network', label: 'Network', icon: 'πŸ“‘', testId: testIds.tabNetwork },
41
+ { id: 'guard', label: 'Guard', icon: 'πŸ›‘οΈ', testId: testIds.tabGuard },
42
+ { id: 'preview', label: 'Preview', icon: 'πŸ“', testId: testIds.tabPreview },
43
+ ];
44
+
45
+ // ============================================================================
46
+ // Styles
47
+ // ============================================================================
48
+
49
+ const styles = {
50
+ container: {
51
+ position: 'fixed' as const,
52
+ width: '420px',
53
+ maxHeight: '70vh',
54
+ backgroundColor: colors.background.dark,
55
+ borderRadius: borderRadius.lg,
56
+ boxShadow: shadows.xl,
57
+ display: 'flex',
58
+ flexDirection: 'column' as const,
59
+ overflow: 'hidden',
60
+ zIndex: zIndex.devtools,
61
+ fontFamily: typography.fontFamily.sans,
62
+ transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
63
+ },
64
+ header: {
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ justifyContent: 'space-between',
68
+ padding: `${spacing.sm} ${spacing.md}`,
69
+ borderBottom: `1px solid ${colors.background.light}`,
70
+ backgroundColor: colors.background.medium,
71
+ },
72
+ title: {
73
+ display: 'flex',
74
+ alignItems: 'center',
75
+ gap: spacing.sm,
76
+ fontSize: typography.fontSize.sm,
77
+ fontWeight: typography.fontWeight.semibold,
78
+ color: colors.text.primary,
79
+ },
80
+ logo: {
81
+ fontSize: typography.fontSize.lg,
82
+ },
83
+ headerActions: {
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ gap: spacing.xs,
87
+ },
88
+ restartButton: {
89
+ background: 'transparent',
90
+ border: 'none',
91
+ color: colors.text.secondary,
92
+ fontSize: typography.fontSize.md,
93
+ cursor: 'pointer',
94
+ padding: spacing.xs,
95
+ borderRadius: borderRadius.sm,
96
+ lineHeight: 1,
97
+ transition: `all ${animation.duration.fast}`,
98
+ },
99
+ closeButton: {
100
+ background: 'transparent',
101
+ border: 'none',
102
+ color: colors.text.secondary,
103
+ fontSize: typography.fontSize.lg,
104
+ cursor: 'pointer',
105
+ padding: spacing.xs,
106
+ borderRadius: borderRadius.sm,
107
+ lineHeight: 1,
108
+ transition: `color ${animation.duration.fast}`,
109
+ },
110
+ tabs: {
111
+ display: 'flex',
112
+ borderBottom: `1px solid ${colors.background.light}`,
113
+ backgroundColor: colors.background.dark,
114
+ },
115
+ tab: {
116
+ flex: 1,
117
+ display: 'flex',
118
+ alignItems: 'center',
119
+ justifyContent: 'center',
120
+ gap: spacing.xs,
121
+ padding: `${spacing.sm} ${spacing.md}`,
122
+ background: 'transparent',
123
+ border: 'none',
124
+ borderBottom: '2px solid transparent',
125
+ color: colors.text.secondary,
126
+ fontSize: typography.fontSize.sm,
127
+ cursor: 'pointer',
128
+ transition: `all ${animation.duration.fast}`,
129
+ },
130
+ tabActive: {
131
+ color: colors.brand.accent,
132
+ borderBottomColor: colors.brand.accent,
133
+ backgroundColor: 'rgba(232, 150, 122, 0.12)',
134
+ },
135
+ tabIcon: {
136
+ fontSize: typography.fontSize.md,
137
+ },
138
+ tabBadge: {
139
+ minWidth: '18px',
140
+ height: '18px',
141
+ padding: '0 4px',
142
+ borderRadius: borderRadius.full,
143
+ backgroundColor: colors.semantic.error,
144
+ color: colors.text.primary,
145
+ fontSize: typography.fontSize.xs,
146
+ fontWeight: typography.fontWeight.semibold,
147
+ display: 'flex',
148
+ alignItems: 'center',
149
+ justifyContent: 'center',
150
+ },
151
+ content: {
152
+ flex: 1,
153
+ overflow: 'auto',
154
+ minHeight: '200px',
155
+ maxHeight: '50vh',
156
+ },
157
+ resizeHandle: {
158
+ position: 'absolute' as const,
159
+ width: '100%',
160
+ height: '4px',
161
+ cursor: 'ns-resize',
162
+ backgroundColor: 'transparent',
163
+ },
164
+ };
165
+
166
+ const positionStyles: Record<string, React.CSSProperties> = {
167
+ 'bottom-right': { bottom: '80px', right: '16px' },
168
+ 'bottom-left': { bottom: '80px', left: '16px' },
169
+ 'top-right': { top: '16px', right: '16px' },
170
+ 'top-left': { top: '16px', left: '16px' },
171
+ };
172
+
173
+ // ============================================================================
174
+ // Component
175
+ // ============================================================================
176
+
177
+ export function PanelContainer({
178
+ state,
179
+ activeTab,
180
+ onTabChange,
181
+ onClose,
182
+ onRestart,
183
+ position,
184
+ children,
185
+ }: PanelContainerProps): React.ReactElement {
186
+ const [isResizing, setIsResizing] = useState(false);
187
+ const [height, setHeight] = useState(400);
188
+ const [hoveredTab, setHoveredTab] = useState<TabId | null>(null);
189
+ const [isCloseHovered, setIsCloseHovered] = useState(false);
190
+ const [isRestartHovered, setIsRestartHovered] = useState(false);
191
+ const [isExpandHovered, setIsExpandHovered] = useState(false);
192
+
193
+ const getTabBadgeCount = useCallback((tabId: TabId): number => {
194
+ switch (tabId) {
195
+ case 'errors':
196
+ return state.errors.filter(e => e.severity === 'error' || e.severity === 'critical').length;
197
+ case 'islands':
198
+ return Array.from(state.islands.values()).filter(i => i.status === 'error').length;
199
+ case 'guard':
200
+ return state.guardViolations.length;
201
+ case 'preview':
202
+ return state.recentChanges?.length ?? 0;
203
+ default:
204
+ return 0;
205
+ }
206
+ }, [state]);
207
+
208
+ const containerStyle: React.CSSProperties = {
209
+ ...styles.container,
210
+ ...positionStyles[position],
211
+ height: `${height}px`,
212
+ };
213
+
214
+ return (
215
+ <div
216
+ data-testid={testIds.panel}
217
+ style={containerStyle}
218
+ >
219
+ {/* Resize Handle (top) */}
220
+ {position.startsWith('bottom') && (
221
+ <div
222
+ style={{ ...styles.resizeHandle, top: 0 }}
223
+ onMouseDown={() => setIsResizing(true)}
224
+ />
225
+ )}
226
+
227
+ {/* Header */}
228
+ <div style={styles.header}>
229
+ <div style={styles.title}>
230
+ <span style={styles.logo}>πŸ₯Ÿ</span>
231
+ <span>Mandu Kitchen</span>
232
+ </div>
233
+ <div style={styles.headerActions}>
234
+ {onRestart && (
235
+ <button
236
+ data-testid={testIds.restartButton}
237
+ style={{
238
+ ...styles.restartButton,
239
+ ...(isRestartHovered ? {
240
+ color: colors.semantic.warning,
241
+ backgroundColor: `${colors.semantic.warning}18`,
242
+ } : {}),
243
+ }}
244
+ onClick={onRestart}
245
+ onMouseEnter={() => setIsRestartHovered(true)}
246
+ onMouseLeave={() => setIsRestartHovered(false)}
247
+ aria-label="μΊμ‹œ μ§€μš°κ³  μ™„μ „ μž¬μ‹œμž‘"
248
+ title="μΊμ‹œ μ§€μš°κ³  μ™„μ „ μž¬μ‹œμž‘"
249
+ >
250
+ πŸ”„
251
+ </button>
252
+ )}
253
+ <button
254
+ style={{
255
+ ...styles.restartButton,
256
+ ...(isExpandHovered ? {
257
+ color: colors.brand.accent,
258
+ backgroundColor: `${colors.brand.accent}18`,
259
+ } : {}),
260
+ }}
261
+ onClick={() => window.open('/__kitchen', '_blank')}
262
+ onMouseEnter={() => setIsExpandHovered(true)}
263
+ onMouseLeave={() => setIsExpandHovered(false)}
264
+ aria-label="ν’€ νŽ˜μ΄μ§€λ‘œ μ—΄κΈ°"
265
+ title="ν’€ νŽ˜μ΄μ§€λ‘œ μ—΄κΈ°"
266
+ >
267
+ πŸ”²
268
+ </button>
269
+ <button
270
+ style={{
271
+ ...styles.closeButton,
272
+ ...(isCloseHovered ? { color: colors.text.primary, backgroundColor: 'rgba(255, 255, 255, 0.08)' } : {}),
273
+ }}
274
+ onClick={onClose}
275
+ onMouseEnter={() => setIsCloseHovered(true)}
276
+ onMouseLeave={() => setIsCloseHovered(false)}
277
+ aria-label="νŒ¨λ„ λ‹«κΈ°"
278
+ >
279
+ Γ—
280
+ </button>
281
+ </div>
282
+ </div>
283
+
284
+ {/* Tabs */}
285
+ <div style={styles.tabs} role="tablist">
286
+ {TABS.map((tab) => {
287
+ const isActive = activeTab === tab.id;
288
+ const badgeCount = getTabBadgeCount(tab.id);
289
+
290
+ return (
291
+ <button
292
+ key={tab.id}
293
+ data-testid={tab.testId}
294
+ role="tab"
295
+ aria-selected={isActive}
296
+ style={{
297
+ ...styles.tab,
298
+ ...(isActive ? styles.tabActive : {}),
299
+ ...(!isActive && hoveredTab === tab.id ? {
300
+ color: colors.text.primary,
301
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
302
+ } : {}),
303
+ }}
304
+ onClick={() => onTabChange(tab.id)}
305
+ onMouseEnter={() => setHoveredTab(tab.id)}
306
+ onMouseLeave={() => setHoveredTab(null)}
307
+ >
308
+ <span style={styles.tabIcon}>{tab.icon}</span>
309
+ <span>{tab.label}</span>
310
+ {badgeCount > 0 && (
311
+ <span style={styles.tabBadge}>{badgeCount > 99 ? '99+' : badgeCount}</span>
312
+ )}
313
+ </button>
314
+ );
315
+ })}
316
+ </div>
317
+
318
+ {/* Content */}
319
+ <div style={styles.content} role="tabpanel">
320
+ {children}
321
+ </div>
322
+
323
+ {/* Resize Handle (bottom) */}
324
+ {position.startsWith('top') && (
325
+ <div
326
+ style={{ ...styles.resizeHandle, bottom: 0 }}
327
+ onMouseDown={() => setIsResizing(true)}
328
+ />
329
+ )}
330
+ </div>
331
+ );
332
+ }