@mandujs/core 0.19.0 → 0.20.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 (100) 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 +385 -115
  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/bundler/types.ts +20 -0
  11. package/src/change/snapshot.ts +4 -23
  12. package/src/change/types.ts +2 -3
  13. package/src/client/Form.tsx +105 -0
  14. package/src/client/__tests__/use-sse.test.ts +153 -0
  15. package/src/client/hooks.ts +105 -6
  16. package/src/client/index.ts +35 -6
  17. package/src/client/router.ts +670 -433
  18. package/src/client/rpc.ts +140 -0
  19. package/src/client/runtime.ts +24 -21
  20. package/src/client/use-fetch.ts +239 -0
  21. package/src/client/use-head.ts +197 -0
  22. package/src/client/use-sse.ts +378 -0
  23. package/src/components/Image.tsx +162 -0
  24. package/src/config/mandu.ts +5 -0
  25. package/src/config/validate.ts +34 -0
  26. package/src/content/index.ts +5 -1
  27. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  28. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  29. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  30. package/src/devtools/client/components/mandu-character.tsx +77 -53
  31. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  32. package/src/devtools/client/components/panel/errors-panel.tsx +2 -2
  33. package/src/devtools/client/components/panel/guard-panel.tsx +363 -234
  34. package/src/devtools/client/components/panel/index.ts +45 -32
  35. package/src/devtools/client/components/panel/islands-panel.tsx +30 -14
  36. package/src/devtools/client/components/panel/network-panel.tsx +2 -3
  37. package/src/devtools/client/components/panel/panel-container.tsx +273 -100
  38. package/src/devtools/client/components/panel/preview-panel.tsx +212 -0
  39. package/src/devtools/client/state-manager.ts +535 -478
  40. package/src/devtools/design-tokens.ts +265 -264
  41. package/src/devtools/init.ts +1 -1
  42. package/src/devtools/types.ts +321 -295
  43. package/src/filling/filling.ts +328 -6
  44. package/src/filling/index.ts +5 -1
  45. package/src/filling/session.ts +216 -0
  46. package/src/filling/ws.ts +78 -0
  47. package/src/generator/generate.ts +2 -2
  48. package/src/guard/auto-correct.ts +0 -29
  49. package/src/guard/check.ts +14 -31
  50. package/src/guard/presets/index.ts +296 -294
  51. package/src/guard/rules.ts +15 -19
  52. package/src/guard/validator.ts +834 -834
  53. package/src/index.ts +6 -1
  54. package/src/island/index.ts +373 -304
  55. package/src/kitchen/api/contract-api.ts +225 -0
  56. package/src/kitchen/api/diff-parser.ts +108 -0
  57. package/src/kitchen/api/file-api.ts +273 -0
  58. package/src/kitchen/api/guard-api.ts +83 -0
  59. package/src/kitchen/api/guard-decisions.ts +100 -0
  60. package/src/kitchen/api/routes-api.ts +50 -0
  61. package/src/kitchen/index.ts +21 -0
  62. package/src/kitchen/kitchen-handler.ts +335 -0
  63. package/src/kitchen/kitchen-ui.ts +1732 -0
  64. package/src/kitchen/stream/activity-sse.ts +145 -0
  65. package/src/kitchen/stream/file-tailer.ts +99 -0
  66. package/src/middleware/compress.ts +62 -0
  67. package/src/middleware/cors.ts +47 -0
  68. package/src/middleware/index.ts +10 -0
  69. package/src/middleware/jwt.ts +134 -0
  70. package/src/middleware/logger.ts +58 -0
  71. package/src/middleware/timeout.ts +55 -0
  72. package/src/observability/event-bus.ts +79 -0
  73. package/src/observability/index.ts +8 -0
  74. package/src/observability/logger-adapter.ts +36 -0
  75. package/src/paths.ts +0 -4
  76. package/src/plugins/hooks.ts +64 -0
  77. package/src/plugins/index.ts +3 -0
  78. package/src/plugins/types.ts +5 -0
  79. package/src/report/build.ts +0 -6
  80. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  81. package/src/router/fs-patterns.ts +11 -1
  82. package/src/router/fs-routes.ts +78 -14
  83. package/src/router/fs-scanner.ts +2 -2
  84. package/src/router/fs-types.ts +2 -1
  85. package/src/runtime/adapter-bun.ts +62 -0
  86. package/src/runtime/adapter.ts +47 -0
  87. package/src/runtime/cache.ts +310 -0
  88. package/src/runtime/handler.ts +65 -0
  89. package/src/runtime/image-handler.ts +195 -0
  90. package/src/runtime/index.ts +13 -0
  91. package/src/runtime/middleware.ts +263 -0
  92. package/src/runtime/ppr.ts +74 -0
  93. package/src/runtime/server.ts +706 -71
  94. package/src/runtime/ssr.ts +70 -31
  95. package/src/runtime/streaming-ssr.ts +121 -78
  96. package/src/spec/index.ts +0 -1
  97. package/src/spec/schema.ts +1 -0
  98. package/src/testing/index.ts +189 -0
  99. package/src/watcher/watcher.ts +27 -1
  100. package/src/spec/lock.ts +0 -56
@@ -7,11 +7,7 @@ import React, { useState, useCallback } from 'react';
7
7
  import { colors, typography, spacing, borderRadius, shadows, zIndex, animation, testIds } from '../../../design-tokens';
8
8
  import type { KitchenState } from '../../state-manager';
9
9
 
10
- // ============================================================================
11
- // Types
12
- // ============================================================================
13
-
14
- export type TabId = 'errors' | 'islands' | 'network' | 'guard';
10
+ export type TabId = 'errors' | 'islands' | 'network' | 'guard' | 'preview';
15
11
 
16
12
  export interface TabDefinition {
17
13
  id: TabId;
@@ -30,28 +26,23 @@ export interface PanelContainerProps {
30
26
  children: React.ReactNode;
31
27
  }
32
28
 
33
- // ============================================================================
34
- // Constants
35
- // ============================================================================
36
-
37
29
  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 },
30
+ { id: 'errors', label: 'Issues', icon: 'ERR', testId: testIds.tabErrors },
31
+ { id: 'islands', label: 'Islands', icon: 'ISL', testId: testIds.tabIslands },
32
+ { id: 'network', label: 'Network', icon: 'NET', testId: testIds.tabNetwork },
33
+ { id: 'guard', label: 'Guard', icon: 'GRD', testId: testIds.tabGuard },
34
+ { id: 'preview', label: 'Changes', icon: 'CHG', testId: testIds.tabPreview },
42
35
  ];
43
36
 
44
- // ============================================================================
45
- // Styles
46
- // ============================================================================
47
-
48
37
  const styles = {
49
38
  container: {
50
39
  position: 'fixed' as const,
51
- width: '420px',
52
- maxHeight: '70vh',
53
- backgroundColor: colors.background.dark,
54
- borderRadius: borderRadius.lg,
40
+ width: '468px',
41
+ maxWidth: 'calc(100vw - 32px)',
42
+ maxHeight: '76vh',
43
+ background: `linear-gradient(180deg, ${colors.background.medium}, ${colors.background.dark})`,
44
+ border: `1px solid ${colors.background.light}`,
45
+ borderRadius: '20px',
55
46
  boxShadow: shadows.xl,
56
47
  display: 'flex',
57
48
  flexDirection: 'column' as const,
@@ -62,54 +53,134 @@ const styles = {
62
53
  },
63
54
  header: {
64
55
  display: 'flex',
65
- alignItems: 'center',
66
- justifyContent: 'space-between',
67
- padding: `${spacing.sm} ${spacing.md}`,
56
+ flexDirection: 'column' as const,
57
+ gap: spacing.md,
58
+ padding: `${spacing.md} ${spacing.md} ${spacing.sm}`,
68
59
  borderBottom: `1px solid ${colors.background.light}`,
69
- backgroundColor: colors.background.medium,
60
+ background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0))',
61
+ },
62
+ headerTop: {
63
+ display: 'flex',
64
+ alignItems: 'flex-start',
65
+ justifyContent: 'space-between',
66
+ gap: spacing.md,
70
67
  },
71
68
  title: {
72
69
  display: 'flex',
73
70
  alignItems: 'center',
74
- gap: spacing.sm,
75
- fontSize: typography.fontSize.sm,
71
+ gap: spacing.md,
72
+ minWidth: 0,
73
+ },
74
+ logo: {
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ justifyContent: 'center',
78
+ minWidth: '42px',
79
+ height: '42px',
80
+ padding: '0 10px',
81
+ borderRadius: borderRadius.full,
82
+ backgroundColor: `${colors.brand.accent}15`,
83
+ border: `1px solid ${colors.brand.accent}40`,
84
+ color: colors.brand.accent,
85
+ fontFamily: typography.fontFamily.mono,
86
+ fontSize: typography.fontSize.xs,
87
+ fontWeight: typography.fontWeight.bold,
88
+ letterSpacing: '0.14em',
89
+ },
90
+ titleGroup: {
91
+ display: 'flex',
92
+ flexDirection: 'column' as const,
93
+ gap: '4px',
94
+ minWidth: 0,
95
+ },
96
+ titleText: {
97
+ fontSize: typography.fontSize.md,
76
98
  fontWeight: typography.fontWeight.semibold,
77
99
  color: colors.text.primary,
100
+ whiteSpace: 'nowrap' as const,
101
+ overflow: 'hidden',
102
+ textOverflow: 'ellipsis',
78
103
  },
79
- logo: {
80
- fontSize: typography.fontSize.lg,
104
+ subtitle: {
105
+ fontSize: typography.fontSize.xs,
106
+ color: colors.text.secondary,
81
107
  },
82
108
  headerActions: {
83
109
  display: 'flex',
84
110
  alignItems: 'center',
85
- gap: spacing.xs,
111
+ gap: spacing.sm,
112
+ flexWrap: 'wrap' as const,
113
+ justifyContent: 'flex-end',
114
+ },
115
+ statusBadge: {
116
+ padding: `${spacing.xs} ${spacing.sm}`,
117
+ borderRadius: borderRadius.full,
118
+ fontSize: typography.fontSize.xs,
119
+ fontWeight: typography.fontWeight.semibold,
120
+ letterSpacing: '0.08em',
121
+ textTransform: 'uppercase' as const,
122
+ border: `1px solid ${colors.background.light}`,
123
+ },
124
+ headerButton: {
125
+ padding: `${spacing.xs} ${spacing.sm}`,
126
+ borderRadius: borderRadius.full,
127
+ backgroundColor: colors.background.light,
128
+ border: `1px solid transparent`,
129
+ color: colors.text.secondary,
130
+ fontSize: typography.fontSize.xs,
131
+ fontWeight: typography.fontWeight.medium,
132
+ cursor: 'pointer',
133
+ transition: `all ${animation.duration.fast}`,
86
134
  },
87
135
  restartButton: {
88
- background: 'transparent',
89
- border: 'none',
136
+ backgroundColor: colors.background.light,
137
+ border: `1px solid transparent`,
90
138
  color: colors.text.secondary,
91
- fontSize: typography.fontSize.md,
139
+ fontSize: typography.fontSize.xs,
140
+ fontWeight: typography.fontWeight.medium,
92
141
  cursor: 'pointer',
93
- padding: spacing.xs,
94
- borderRadius: borderRadius.sm,
142
+ padding: `${spacing.xs} ${spacing.sm}`,
143
+ borderRadius: borderRadius.full,
95
144
  lineHeight: 1,
96
145
  transition: `all ${animation.duration.fast}`,
97
146
  },
98
147
  closeButton: {
99
- background: 'transparent',
100
- border: 'none',
148
+ backgroundColor: 'transparent',
149
+ border: `1px solid transparent`,
101
150
  color: colors.text.secondary,
102
- fontSize: typography.fontSize.lg,
151
+ fontSize: typography.fontSize.md,
103
152
  cursor: 'pointer',
104
- padding: spacing.xs,
105
- borderRadius: borderRadius.sm,
153
+ padding: `${spacing.xs} ${spacing.sm}`,
154
+ borderRadius: borderRadius.full,
106
155
  lineHeight: 1,
107
- transition: `color ${animation.duration.fast}`,
156
+ transition: `all ${animation.duration.fast}`,
157
+ },
158
+ summaryRow: {
159
+ display: 'flex',
160
+ gap: spacing.xs,
161
+ flexWrap: 'wrap' as const,
162
+ },
163
+ summaryChip: {
164
+ display: 'inline-flex',
165
+ alignItems: 'center',
166
+ gap: spacing.xs,
167
+ padding: `${spacing.xs} ${spacing.sm}`,
168
+ borderRadius: borderRadius.full,
169
+ backgroundColor: 'rgba(255, 255, 255, 0.04)',
170
+ border: `1px solid rgba(255, 255, 255, 0.06)`,
171
+ fontSize: typography.fontSize.xs,
172
+ color: colors.text.secondary,
173
+ },
174
+ summaryValue: {
175
+ color: colors.text.primary,
176
+ fontWeight: typography.fontWeight.semibold,
108
177
  },
109
178
  tabs: {
110
179
  display: 'flex',
180
+ gap: spacing.xs,
181
+ padding: `0 ${spacing.md} ${spacing.md}`,
111
182
  borderBottom: `1px solid ${colors.background.light}`,
112
- backgroundColor: colors.background.dark,
183
+ backgroundColor: 'transparent',
113
184
  },
114
185
  tab: {
115
186
  flex: 1,
@@ -117,22 +188,31 @@ const styles = {
117
188
  alignItems: 'center',
118
189
  justifyContent: 'center',
119
190
  gap: spacing.xs,
120
- padding: `${spacing.sm} ${spacing.md}`,
121
- background: 'transparent',
122
- border: 'none',
123
- borderBottom: '2px solid transparent',
191
+ padding: `${spacing.sm} ${spacing.sm}`,
192
+ backgroundColor: 'rgba(255, 255, 255, 0.02)',
193
+ border: `1px solid rgba(255, 255, 255, 0.04)`,
194
+ borderRadius: borderRadius.md,
124
195
  color: colors.text.secondary,
125
- fontSize: typography.fontSize.sm,
196
+ fontSize: typography.fontSize.xs,
197
+ fontWeight: typography.fontWeight.medium,
126
198
  cursor: 'pointer',
127
199
  transition: `all ${animation.duration.fast}`,
128
200
  },
129
201
  tabActive: {
130
- color: colors.brand.accent,
131
- borderBottomColor: colors.brand.accent,
132
- backgroundColor: 'rgba(232, 150, 122, 0.12)',
202
+ color: colors.text.primary,
203
+ borderColor: `${colors.brand.accent}55`,
204
+ backgroundColor: 'rgba(232, 150, 122, 0.14)',
205
+ boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.04)',
133
206
  },
134
207
  tabIcon: {
135
- fontSize: typography.fontSize.md,
208
+ padding: '2px 6px',
209
+ borderRadius: borderRadius.full,
210
+ backgroundColor: 'rgba(255, 255, 255, 0.06)',
211
+ fontSize: '10px',
212
+ fontWeight: typography.fontWeight.bold,
213
+ letterSpacing: '0.08em',
214
+ fontFamily: typography.fontFamily.mono,
215
+ color: colors.text.secondary,
136
216
  },
137
217
  tabBadge: {
138
218
  minWidth: '18px',
@@ -150,8 +230,8 @@ const styles = {
150
230
  content: {
151
231
  flex: 1,
152
232
  overflow: 'auto',
153
- minHeight: '200px',
154
- maxHeight: '50vh',
233
+ minHeight: '240px',
234
+ maxHeight: '56vh',
155
235
  },
156
236
  resizeHandle: {
157
237
  position: 'absolute' as const,
@@ -160,7 +240,7 @@ const styles = {
160
240
  cursor: 'ns-resize',
161
241
  backgroundColor: 'transparent',
162
242
  },
163
- };
243
+ } as const;
164
244
 
165
245
  const positionStyles: Record<string, React.CSSProperties> = {
166
246
  'bottom-right': { bottom: '80px', right: '16px' },
@@ -169,10 +249,6 @@ const positionStyles: Record<string, React.CSSProperties> = {
169
249
  'top-left': { top: '16px', left: '16px' },
170
250
  };
171
251
 
172
- // ============================================================================
173
- // Component
174
- // ============================================================================
175
-
176
252
  export function PanelContainer({
177
253
  state,
178
254
  activeTab,
@@ -182,11 +258,12 @@ export function PanelContainer({
182
258
  position,
183
259
  children,
184
260
  }: PanelContainerProps): React.ReactElement {
185
- const [isResizing, setIsResizing] = useState(false);
186
- const [height, setHeight] = useState(400);
261
+ const [, setIsResizing] = useState(false);
262
+ const [height] = useState(420);
187
263
  const [hoveredTab, setHoveredTab] = useState<TabId | null>(null);
188
264
  const [isCloseHovered, setIsCloseHovered] = useState(false);
189
265
  const [isRestartHovered, setIsRestartHovered] = useState(false);
266
+ const [isExpandHovered, setIsExpandHovered] = useState(false);
190
267
 
191
268
  const getTabBadgeCount = useCallback((tabId: TabId): number => {
192
269
  switch (tabId) {
@@ -196,11 +273,48 @@ export function PanelContainer({
196
273
  return Array.from(state.islands.values()).filter(i => i.status === 'error').length;
197
274
  case 'guard':
198
275
  return state.guardViolations.length;
276
+ case 'preview':
277
+ return state.recentChanges?.length ?? 0;
199
278
  default:
200
279
  return 0;
201
280
  }
202
281
  }, [state]);
203
282
 
283
+ const issueCount = getTabBadgeCount('errors');
284
+ const islandsCount = state.islands.size;
285
+ const networkCount = state.networkRequests.size;
286
+ const guardCount = state.guardViolations.length;
287
+ const changeCount = state.recentChanges?.length ?? 0;
288
+
289
+ const statusToneMap: Record<KitchenState['manduState'], { label: string; color: string; background: string }> = {
290
+ normal: {
291
+ label: 'Healthy',
292
+ color: colors.semantic.success,
293
+ background: `${colors.semantic.success}14`,
294
+ },
295
+ warning: {
296
+ label: 'Attention',
297
+ color: colors.semantic.warning,
298
+ background: `${colors.semantic.warning}14`,
299
+ },
300
+ error: {
301
+ label: 'Action',
302
+ color: colors.semantic.error,
303
+ background: `${colors.semantic.error}14`,
304
+ },
305
+ loading: {
306
+ label: 'Syncing',
307
+ color: colors.semantic.info,
308
+ background: `${colors.semantic.info}14`,
309
+ },
310
+ hmr: {
311
+ label: 'Updated',
312
+ color: colors.brand.accent,
313
+ background: `${colors.brand.accent}14`,
314
+ },
315
+ };
316
+ const statusTone = statusToneMap[state.manduState];
317
+
204
318
  const containerStyle: React.CSSProperties = {
205
319
  ...styles.container,
206
320
  ...positionStyles[position],
@@ -208,11 +322,7 @@ export function PanelContainer({
208
322
  };
209
323
 
210
324
  return (
211
- <div
212
- data-testid={testIds.panel}
213
- style={containerStyle}
214
- >
215
- {/* Resize Handle (top) */}
325
+ <div data-testid={testIds.panel} style={containerStyle}>
216
326
  {position.startsWith('bottom') && (
217
327
  <div
218
328
  style={{ ...styles.resizeHandle, top: 0 }}
@@ -220,48 +330,102 @@ export function PanelContainer({
220
330
  />
221
331
  )}
222
332
 
223
- {/* Header */}
224
333
  <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 && (
334
+ <div style={styles.headerTop}>
335
+ <div style={styles.title}>
336
+ <span style={styles.logo}>MK</span>
337
+ <div style={styles.titleGroup}>
338
+ <span style={styles.titleText}>Mandu Dev Console</span>
339
+ <span style={styles.subtitle}>
340
+ {state.hmrConnected ? 'Live runtime diagnostics' : 'Runtime diagnostics'}
341
+ </span>
342
+ </div>
343
+ </div>
344
+ <div style={styles.headerActions}>
345
+ <span
346
+ style={{
347
+ ...styles.statusBadge,
348
+ color: statusTone.color,
349
+ backgroundColor: statusTone.background,
350
+ borderColor: `${statusTone.color}45`,
351
+ }}
352
+ >
353
+ {statusTone.label}
354
+ </span>
355
+ {onRestart && (
356
+ <button
357
+ data-testid={testIds.restartButton}
358
+ style={{
359
+ ...styles.restartButton,
360
+ ...(isRestartHovered ? {
361
+ color: colors.semantic.warning,
362
+ borderColor: `${colors.semantic.warning}40`,
363
+ backgroundColor: `${colors.semantic.warning}16`,
364
+ } : {}),
365
+ }}
366
+ onClick={onRestart}
367
+ onMouseEnter={() => setIsRestartHovered(true)}
368
+ onMouseLeave={() => setIsRestartHovered(false)}
369
+ aria-label="캐시 지우고 완전 재시작"
370
+ title="캐시 지우고 완전 재시작"
371
+ >
372
+ Reset
373
+ </button>
374
+ )}
375
+ <button
376
+ style={{
377
+ ...styles.headerButton,
378
+ ...(isExpandHovered ? {
379
+ color: colors.brand.accent,
380
+ borderColor: `${colors.brand.accent}35`,
381
+ backgroundColor: `${colors.brand.accent}12`,
382
+ } : {}),
383
+ }}
384
+ onClick={() => window.open('/__kitchen', '_blank')}
385
+ onMouseEnter={() => setIsExpandHovered(true)}
386
+ onMouseLeave={() => setIsExpandHovered(false)}
387
+ aria-label="풀 페이지로 열기"
388
+ title="풀 페이지로 열기"
389
+ >
390
+ Open
391
+ </button>
231
392
  <button
232
- data-testid={testIds.restartButton}
233
393
  style={{
234
- ...styles.restartButton,
235
- ...(isRestartHovered ? {
236
- color: colors.semantic.warning,
237
- backgroundColor: `${colors.semantic.warning}18`,
394
+ ...styles.closeButton,
395
+ ...(isCloseHovered ? {
396
+ color: colors.text.primary,
397
+ borderColor: 'rgba(255, 255, 255, 0.12)',
398
+ backgroundColor: 'rgba(255, 255, 255, 0.06)',
238
399
  } : {}),
239
400
  }}
240
- onClick={onRestart}
241
- onMouseEnter={() => setIsRestartHovered(true)}
242
- onMouseLeave={() => setIsRestartHovered(false)}
243
- aria-label="캐시 지우고 완전 재시작"
244
- title="캐시 지우고 완전 재시작"
401
+ onClick={onClose}
402
+ onMouseEnter={() => setIsCloseHovered(true)}
403
+ onMouseLeave={() => setIsCloseHovered(false)}
404
+ aria-label="패널 닫기"
245
405
  >
246
- 🔄
406
+ ×
247
407
  </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>
408
+ </div>
409
+ </div>
410
+ <div style={styles.summaryRow}>
411
+ <span style={styles.summaryChip}>
412
+ Issues <span style={styles.summaryValue}>{issueCount}</span>
413
+ </span>
414
+ <span style={styles.summaryChip}>
415
+ Islands <span style={styles.summaryValue}>{islandsCount}</span>
416
+ </span>
417
+ <span style={styles.summaryChip}>
418
+ Requests <span style={styles.summaryValue}>{networkCount}</span>
419
+ </span>
420
+ <span style={styles.summaryChip}>
421
+ Guard <span style={styles.summaryValue}>{guardCount}</span>
422
+ </span>
423
+ <span style={styles.summaryChip}>
424
+ Changes <span style={styles.summaryValue}>{changeCount}</span>
425
+ </span>
261
426
  </div>
262
427
  </div>
263
428
 
264
- {/* Tabs */}
265
429
  <div style={styles.tabs} role="tablist">
266
430
  {TABS.map((tab) => {
267
431
  const isActive = activeTab === tab.id;
@@ -278,6 +442,7 @@ export function PanelContainer({
278
442
  ...(isActive ? styles.tabActive : {}),
279
443
  ...(!isActive && hoveredTab === tab.id ? {
280
444
  color: colors.text.primary,
445
+ borderColor: 'rgba(255, 255, 255, 0.09)',
281
446
  backgroundColor: 'rgba(255, 255, 255, 0.05)',
282
447
  } : {}),
283
448
  }}
@@ -285,7 +450,17 @@ export function PanelContainer({
285
450
  onMouseEnter={() => setHoveredTab(tab.id)}
286
451
  onMouseLeave={() => setHoveredTab(null)}
287
452
  >
288
- <span style={styles.tabIcon}>{tab.icon}</span>
453
+ <span
454
+ style={{
455
+ ...styles.tabIcon,
456
+ ...(isActive ? {
457
+ color: colors.text.primary,
458
+ backgroundColor: 'rgba(255, 255, 255, 0.14)',
459
+ } : {}),
460
+ }}
461
+ >
462
+ {tab.icon}
463
+ </span>
289
464
  <span>{tab.label}</span>
290
465
  {badgeCount > 0 && (
291
466
  <span style={styles.tabBadge}>{badgeCount > 99 ? '99+' : badgeCount}</span>
@@ -295,12 +470,10 @@ export function PanelContainer({
295
470
  })}
296
471
  </div>
297
472
 
298
- {/* Content */}
299
473
  <div style={styles.content} role="tabpanel">
300
474
  {children}
301
475
  </div>
302
476
 
303
- {/* Resize Handle (bottom) */}
304
477
  {position.startsWith('top') && (
305
478
  <div
306
479
  style={{ ...styles.resizeHandle, bottom: 0 }}