@mandujs/core 0.9.41 → 0.9.42
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.
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -73
- package/src/bundler/dev.ts +21 -14
- package/src/client/globals.ts +44 -0
- package/src/client/index.ts +5 -4
- package/src/client/island.ts +8 -13
- package/src/client/router.ts +33 -41
- package/src/client/runtime.ts +23 -51
- package/src/client/window-state.ts +101 -0
- package/src/config/index.ts +1 -0
- package/src/config/mandu.ts +45 -9
- package/src/config/validate.ts +158 -0
- package/src/constants.ts +25 -0
- package/src/contract/client.ts +4 -3
- package/src/contract/define.ts +459 -0
- package/src/devtools/ai/context-builder.ts +375 -0
- package/src/devtools/ai/index.ts +25 -0
- package/src/devtools/ai/mcp-connector.ts +465 -0
- package/src/devtools/client/catchers/error-catcher.ts +327 -0
- package/src/devtools/client/catchers/index.ts +18 -0
- package/src/devtools/client/catchers/network-proxy.ts +363 -0
- package/src/devtools/client/components/index.ts +39 -0
- package/src/devtools/client/components/kitchen-root.tsx +362 -0
- package/src/devtools/client/components/mandu-character.tsx +241 -0
- package/src/devtools/client/components/overlay.tsx +368 -0
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
- package/src/devtools/client/components/panel/index.ts +32 -0
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
- package/src/devtools/client/components/panel/network-panel.tsx +292 -0
- package/src/devtools/client/components/panel/panel-container.tsx +259 -0
- package/src/devtools/client/filters/context-filters.ts +282 -0
- package/src/devtools/client/filters/index.ts +16 -0
- package/src/devtools/client/index.ts +63 -0
- package/src/devtools/client/persistence.ts +335 -0
- package/src/devtools/client/state-manager.ts +478 -0
- package/src/devtools/design-tokens.ts +263 -0
- package/src/devtools/hook/create-hook.ts +207 -0
- package/src/devtools/hook/index.ts +13 -0
- package/src/devtools/index.ts +439 -0
- package/src/devtools/init.ts +266 -0
- package/src/devtools/protocol.ts +237 -0
- package/src/devtools/server/index.ts +17 -0
- package/src/devtools/server/source-context.ts +444 -0
- package/src/devtools/types.ts +319 -0
- package/src/devtools/worker/index.ts +25 -0
- package/src/devtools/worker/redaction-worker.ts +222 -0
- package/src/devtools/worker/worker-manager.ts +409 -0
- package/src/error/formatter.ts +28 -24
- package/src/error/index.ts +13 -9
- package/src/error/result.ts +46 -0
- package/src/error/types.ts +6 -4
- package/src/filling/filling.ts +6 -5
- package/src/guard/check.ts +60 -56
- package/src/guard/types.ts +3 -1
- package/src/guard/watcher.ts +10 -1
- package/src/index.ts +81 -0
- package/src/intent/index.ts +310 -0
- package/src/island/index.ts +304 -0
- package/src/router/fs-patterns.ts +7 -0
- package/src/router/fs-routes.ts +20 -8
- package/src/router/fs-scanner.ts +117 -133
- package/src/runtime/server.ts +261 -201
- package/src/runtime/ssr.ts +5 -4
- package/src/runtime/streaming-ssr.ts +5 -4
- package/src/utils/bun.ts +8 -0
- package/src/utils/lru-cache.ts +75 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Kitchen DevTools - Components Module
|
|
3
|
+
* @version 1.0.3
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ManduCharacter,
|
|
8
|
+
ManduBadge,
|
|
9
|
+
type ManduCharacterProps,
|
|
10
|
+
type ManduBadgeProps,
|
|
11
|
+
} from './mandu-character';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
ErrorOverlay,
|
|
15
|
+
type ErrorOverlayProps,
|
|
16
|
+
} from './overlay';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
mountKitchen,
|
|
20
|
+
unmountKitchen,
|
|
21
|
+
isKitchenMounted,
|
|
22
|
+
} from './kitchen-root';
|
|
23
|
+
|
|
24
|
+
// Panel Components
|
|
25
|
+
export {
|
|
26
|
+
PanelContainer,
|
|
27
|
+
ErrorsPanel,
|
|
28
|
+
IslandsPanel,
|
|
29
|
+
NetworkPanel,
|
|
30
|
+
GuardPanel,
|
|
31
|
+
TABS,
|
|
32
|
+
type TabId,
|
|
33
|
+
type TabDefinition,
|
|
34
|
+
type PanelContainerProps,
|
|
35
|
+
type ErrorsPanelProps,
|
|
36
|
+
type IslandsPanelProps,
|
|
37
|
+
type NetworkPanelProps,
|
|
38
|
+
type GuardPanelProps,
|
|
39
|
+
} from './panel';
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Kitchen DevTools - Root Component
|
|
3
|
+
* @version 1.0.3
|
|
4
|
+
*
|
|
5
|
+
* Shadow DOM을 사용하여 앱의 CSS와 격리된 DevTools 루트
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
9
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
10
|
+
import type { NormalizedError, DevToolsConfig, IslandSnapshot, NetworkRequest, GuardViolation } from '../../types';
|
|
11
|
+
import { generateCSSVariables, testIds, zIndex } from '../../design-tokens';
|
|
12
|
+
import { getStateManager, type KitchenState } from '../state-manager';
|
|
13
|
+
import { getOrCreateHook } from '../../hook';
|
|
14
|
+
import { ErrorOverlay } from './overlay';
|
|
15
|
+
import { ManduBadge } from './mandu-character';
|
|
16
|
+
import {
|
|
17
|
+
PanelContainer,
|
|
18
|
+
ErrorsPanel,
|
|
19
|
+
IslandsPanel,
|
|
20
|
+
NetworkPanel,
|
|
21
|
+
GuardPanel,
|
|
22
|
+
type TabId,
|
|
23
|
+
} from './panel';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Base Styles
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const baseStyles = `
|
|
30
|
+
${generateCSSVariables()}
|
|
31
|
+
|
|
32
|
+
* {
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:host {
|
|
39
|
+
all: initial;
|
|
40
|
+
font-family: var(--mk-font-sans);
|
|
41
|
+
color: var(--mk-color-text-primary);
|
|
42
|
+
font-size: var(--mk-font-size-md);
|
|
43
|
+
line-height: 1.5;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.mk-badge-container {
|
|
47
|
+
position: fixed;
|
|
48
|
+
z-index: ${zIndex.devtools};
|
|
49
|
+
transition: all 0.3s ease;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.mk-badge-container.bottom-right {
|
|
53
|
+
bottom: 16px;
|
|
54
|
+
right: 16px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.mk-badge-container.bottom-left {
|
|
58
|
+
bottom: 16px;
|
|
59
|
+
left: 16px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mk-badge-container.top-right {
|
|
63
|
+
top: 16px;
|
|
64
|
+
right: 16px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.mk-badge-container.top-left {
|
|
68
|
+
top: 16px;
|
|
69
|
+
left: 16px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.mk-badge-container.panel-open {
|
|
73
|
+
opacity: 0;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Kitchen App Component
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
interface KitchenAppProps {
|
|
83
|
+
config: DevToolsConfig;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
|
|
87
|
+
const [state, setState] = useState<KitchenState>(() => getStateManager().getState());
|
|
88
|
+
|
|
89
|
+
// Subscribe to state changes
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const stateManager = getStateManager();
|
|
92
|
+
const unsubscribe = stateManager.subscribe((newState) => {
|
|
93
|
+
setState(newState);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Connect to hook
|
|
97
|
+
const hook = getOrCreateHook();
|
|
98
|
+
hook.connect((event) => {
|
|
99
|
+
stateManager.handleEvent(event);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
unsubscribe();
|
|
104
|
+
hook.disconnect();
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// Keyboard shortcuts
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
111
|
+
// Ctrl+Shift+M: Toggle panel
|
|
112
|
+
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
getStateManager().toggle();
|
|
115
|
+
}
|
|
116
|
+
// Ctrl+Shift+E: Open errors tab
|
|
117
|
+
if (e.ctrlKey && e.shiftKey && e.key === 'E') {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
getStateManager().setActiveTab('errors');
|
|
120
|
+
getStateManager().open();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
125
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
// Handlers
|
|
129
|
+
const handleOverlayClose = useCallback(() => {
|
|
130
|
+
getStateManager().hideOverlay();
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
const handleOverlayIgnore = useCallback(() => {
|
|
134
|
+
if (state.overlayError) {
|
|
135
|
+
getStateManager().ignoreError(state.overlayError.id);
|
|
136
|
+
}
|
|
137
|
+
}, [state.overlayError]);
|
|
138
|
+
|
|
139
|
+
const handleOverlayCopy = useCallback(async () => {
|
|
140
|
+
if (!state.overlayError) return;
|
|
141
|
+
|
|
142
|
+
const errorInfo = formatErrorForCopy(state.overlayError);
|
|
143
|
+
try {
|
|
144
|
+
await navigator.clipboard.writeText(errorInfo);
|
|
145
|
+
} catch {
|
|
146
|
+
// Fallback for older browsers
|
|
147
|
+
const textarea = document.createElement('textarea');
|
|
148
|
+
textarea.value = errorInfo;
|
|
149
|
+
document.body.appendChild(textarea);
|
|
150
|
+
textarea.select();
|
|
151
|
+
document.execCommand('copy');
|
|
152
|
+
document.body.removeChild(textarea);
|
|
153
|
+
}
|
|
154
|
+
}, [state.overlayError]);
|
|
155
|
+
|
|
156
|
+
const handleBadgeClick = useCallback(() => {
|
|
157
|
+
getStateManager().toggle();
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const handlePanelClose = useCallback(() => {
|
|
161
|
+
getStateManager().close();
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const handleTabChange = useCallback((tab: TabId) => {
|
|
165
|
+
getStateManager().setActiveTab(tab);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
const handleErrorClick = useCallback((error: NormalizedError) => {
|
|
169
|
+
getStateManager().showOverlay(error);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const handleErrorIgnore = useCallback((id: string) => {
|
|
173
|
+
getStateManager().ignoreError(id);
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
const handleClearErrors = useCallback(() => {
|
|
177
|
+
getStateManager().clearErrors();
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
const handleClearGuard = useCallback(() => {
|
|
181
|
+
getStateManager().clearGuardViolations();
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
// Calculate error count
|
|
185
|
+
const errorCount = useMemo(() => {
|
|
186
|
+
return state.errors.filter(
|
|
187
|
+
(e) => e.severity === 'error' || e.severity === 'critical'
|
|
188
|
+
).length;
|
|
189
|
+
}, [state.errors]);
|
|
190
|
+
|
|
191
|
+
// Convert Maps to Arrays for panels
|
|
192
|
+
const islandsArray = useMemo(
|
|
193
|
+
() => Array.from(state.islands.values()),
|
|
194
|
+
[state.islands]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const networkArray = useMemo(
|
|
198
|
+
() => Array.from(state.networkRequests.values()),
|
|
199
|
+
[state.networkRequests]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const position = config.position ?? 'bottom-right';
|
|
203
|
+
|
|
204
|
+
// Don't render if disabled
|
|
205
|
+
if (config.enabled === false) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Render active panel content
|
|
210
|
+
const renderPanelContent = () => {
|
|
211
|
+
switch (state.activeTab) {
|
|
212
|
+
case 'errors':
|
|
213
|
+
return (
|
|
214
|
+
<ErrorsPanel
|
|
215
|
+
errors={state.errors}
|
|
216
|
+
onErrorClick={handleErrorClick}
|
|
217
|
+
onErrorIgnore={handleErrorIgnore}
|
|
218
|
+
onClearAll={handleClearErrors}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
case 'islands':
|
|
222
|
+
return <IslandsPanel islands={islandsArray} />;
|
|
223
|
+
case 'network':
|
|
224
|
+
return <NetworkPanel requests={networkArray} />;
|
|
225
|
+
case 'guard':
|
|
226
|
+
return (
|
|
227
|
+
<GuardPanel
|
|
228
|
+
violations={state.guardViolations}
|
|
229
|
+
onClear={handleClearGuard}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
default:
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
{/* Badge (hidden when panel is open) */}
|
|
240
|
+
<div className={`mk-badge-container ${position}${state.isOpen ? ' panel-open' : ''}`}>
|
|
241
|
+
<ManduBadge
|
|
242
|
+
state={state.manduState}
|
|
243
|
+
count={errorCount}
|
|
244
|
+
onClick={handleBadgeClick}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Panel */}
|
|
249
|
+
{state.isOpen && (
|
|
250
|
+
<PanelContainer
|
|
251
|
+
state={state}
|
|
252
|
+
activeTab={state.activeTab}
|
|
253
|
+
onTabChange={handleTabChange}
|
|
254
|
+
onClose={handlePanelClose}
|
|
255
|
+
position={position}
|
|
256
|
+
>
|
|
257
|
+
{renderPanelContent()}
|
|
258
|
+
</PanelContainer>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{/* Overlay */}
|
|
262
|
+
{state.overlayError && config.features?.errorOverlay !== false && (
|
|
263
|
+
<ErrorOverlay
|
|
264
|
+
error={state.overlayError}
|
|
265
|
+
onClose={handleOverlayClose}
|
|
266
|
+
onIgnore={handleOverlayIgnore}
|
|
267
|
+
onCopy={handleOverlayCopy}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Helper Functions
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
function formatErrorForCopy(error: NormalizedError): string {
|
|
279
|
+
const lines = [
|
|
280
|
+
`[${error.severity.toUpperCase()}] ${error.type}`,
|
|
281
|
+
`Message: ${error.message}`,
|
|
282
|
+
`Time: ${new Date(error.timestamp).toISOString()}`,
|
|
283
|
+
`URL: ${error.url}`,
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
if (error.source) {
|
|
287
|
+
lines.push(`Source: ${error.source}:${error.line ?? '?'}:${error.column ?? '?'}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (error.stack) {
|
|
291
|
+
lines.push('', 'Stack Trace:', error.stack);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (error.componentStack) {
|
|
295
|
+
lines.push('', 'Component Stack:', error.componentStack);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return lines.join('\n');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Mount Function
|
|
303
|
+
// ============================================================================
|
|
304
|
+
|
|
305
|
+
let kitchenRoot: Root | null = null;
|
|
306
|
+
let hostElement: HTMLElement | null = null;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Mandu Kitchen DevTools 마운트
|
|
310
|
+
*/
|
|
311
|
+
export function mountKitchen(config: DevToolsConfig = {}): void {
|
|
312
|
+
if (typeof window === 'undefined') return;
|
|
313
|
+
if (hostElement) return; // Already mounted
|
|
314
|
+
|
|
315
|
+
// Create host element
|
|
316
|
+
hostElement = document.createElement('div');
|
|
317
|
+
hostElement.setAttribute('data-testid', testIds.host);
|
|
318
|
+
hostElement.setAttribute('id', 'mandu-kitchen-host');
|
|
319
|
+
document.body.appendChild(hostElement);
|
|
320
|
+
|
|
321
|
+
// Create shadow root
|
|
322
|
+
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
|
|
323
|
+
|
|
324
|
+
// Inject base styles
|
|
325
|
+
const styleElement = document.createElement('style');
|
|
326
|
+
styleElement.textContent = baseStyles;
|
|
327
|
+
shadowRoot.appendChild(styleElement);
|
|
328
|
+
|
|
329
|
+
// Create render container
|
|
330
|
+
const container = document.createElement('div');
|
|
331
|
+
container.setAttribute('data-testid', testIds.root);
|
|
332
|
+
shadowRoot.appendChild(container);
|
|
333
|
+
|
|
334
|
+
// Mount React
|
|
335
|
+
kitchenRoot = createRoot(container);
|
|
336
|
+
kitchenRoot.render(<KitchenApp config={config} />);
|
|
337
|
+
|
|
338
|
+
// Initialize state manager with config
|
|
339
|
+
getStateManager(config);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Mandu Kitchen DevTools 언마운트
|
|
344
|
+
*/
|
|
345
|
+
export function unmountKitchen(): void {
|
|
346
|
+
if (kitchenRoot) {
|
|
347
|
+
kitchenRoot.unmount();
|
|
348
|
+
kitchenRoot = null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (hostElement) {
|
|
352
|
+
hostElement.remove();
|
|
353
|
+
hostElement = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* DevTools 상태 확인
|
|
359
|
+
*/
|
|
360
|
+
export function isKitchenMounted(): boolean {
|
|
361
|
+
return hostElement !== null;
|
|
362
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Kitchen DevTools - Mandu Character Component
|
|
3
|
+
* @version 1.0.3
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import type { ManduState } from '../../types';
|
|
8
|
+
import { MANDU_CHARACTERS } from '../../types';
|
|
9
|
+
import { colors, animation, testIds } from '../../design-tokens';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Styles
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
const styles = {
|
|
16
|
+
container: {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
gap: '12px',
|
|
20
|
+
padding: '12px 16px',
|
|
21
|
+
borderRadius: '12px',
|
|
22
|
+
backgroundColor: colors.background.medium,
|
|
23
|
+
transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
|
|
24
|
+
},
|
|
25
|
+
emoji: {
|
|
26
|
+
fontSize: '32px',
|
|
27
|
+
lineHeight: 1,
|
|
28
|
+
userSelect: 'none' as const,
|
|
29
|
+
},
|
|
30
|
+
content: {
|
|
31
|
+
display: 'flex',
|
|
32
|
+
flexDirection: 'column' as const,
|
|
33
|
+
gap: '2px',
|
|
34
|
+
},
|
|
35
|
+
message: {
|
|
36
|
+
fontSize: '14px',
|
|
37
|
+
color: colors.text.primary,
|
|
38
|
+
fontWeight: 500,
|
|
39
|
+
},
|
|
40
|
+
status: {
|
|
41
|
+
fontSize: '12px',
|
|
42
|
+
color: colors.text.secondary,
|
|
43
|
+
},
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
const stateColors: Record<ManduState, string> = {
|
|
47
|
+
normal: colors.semantic.success,
|
|
48
|
+
warning: colors.semantic.warning,
|
|
49
|
+
error: colors.semantic.error,
|
|
50
|
+
loading: colors.semantic.info,
|
|
51
|
+
hmr: colors.brand.accent,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Animation Keyframes (inline)
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
const bounceAnimation = `
|
|
59
|
+
@keyframes mk-bounce {
|
|
60
|
+
0%, 100% { transform: translateY(0); }
|
|
61
|
+
50% { transform: translateY(-4px); }
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const pulseAnimation = `
|
|
66
|
+
@keyframes mk-pulse {
|
|
67
|
+
0%, 100% { opacity: 1; }
|
|
68
|
+
50% { opacity: 0.6; }
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const shakeAnimation = `
|
|
73
|
+
@keyframes mk-shake {
|
|
74
|
+
0%, 100% { transform: translateX(0); }
|
|
75
|
+
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
|
|
76
|
+
20%, 40%, 60%, 80% { transform: translateX(2px); }
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const sparkleAnimation = `
|
|
81
|
+
@keyframes mk-sparkle {
|
|
82
|
+
0% { transform: scale(1); }
|
|
83
|
+
50% { transform: scale(1.1); }
|
|
84
|
+
100% { transform: scale(1); }
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Props
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
export interface ManduCharacterProps {
|
|
93
|
+
state: ManduState;
|
|
94
|
+
errorCount?: number;
|
|
95
|
+
className?: string;
|
|
96
|
+
compact?: boolean;
|
|
97
|
+
onClick?: () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Component
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
export function ManduCharacter({
|
|
105
|
+
state,
|
|
106
|
+
errorCount = 0,
|
|
107
|
+
className,
|
|
108
|
+
compact = false,
|
|
109
|
+
onClick,
|
|
110
|
+
}: ManduCharacterProps): React.ReactElement {
|
|
111
|
+
const character = MANDU_CHARACTERS[state];
|
|
112
|
+
const stateColor = stateColors[state];
|
|
113
|
+
|
|
114
|
+
const getAnimation = (): string => {
|
|
115
|
+
switch (state) {
|
|
116
|
+
case 'loading':
|
|
117
|
+
return 'mk-bounce 1s ease-in-out infinite';
|
|
118
|
+
case 'error':
|
|
119
|
+
return 'mk-shake 0.5s ease-in-out';
|
|
120
|
+
case 'hmr':
|
|
121
|
+
return 'mk-sparkle 0.6s ease-in-out';
|
|
122
|
+
case 'warning':
|
|
123
|
+
return 'mk-pulse 2s ease-in-out infinite';
|
|
124
|
+
default:
|
|
125
|
+
return 'none';
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const containerStyle = {
|
|
130
|
+
...styles.container,
|
|
131
|
+
borderLeft: `4px solid ${stateColor}`,
|
|
132
|
+
cursor: onClick ? 'pointer' : 'default',
|
|
133
|
+
...(compact && {
|
|
134
|
+
padding: '8px 12px',
|
|
135
|
+
gap: '8px',
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const emojiStyle = {
|
|
140
|
+
...styles.emoji,
|
|
141
|
+
animation: getAnimation(),
|
|
142
|
+
...(compact && { fontSize: '24px' }),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<>
|
|
147
|
+
{/* Inject keyframes */}
|
|
148
|
+
<style>
|
|
149
|
+
{bounceAnimation}
|
|
150
|
+
{pulseAnimation}
|
|
151
|
+
{shakeAnimation}
|
|
152
|
+
{sparkleAnimation}
|
|
153
|
+
</style>
|
|
154
|
+
|
|
155
|
+
<div
|
|
156
|
+
data-testid={testIds.mandu}
|
|
157
|
+
className={className}
|
|
158
|
+
style={containerStyle}
|
|
159
|
+
onClick={onClick}
|
|
160
|
+
role={onClick ? 'button' : undefined}
|
|
161
|
+
tabIndex={onClick ? 0 : undefined}
|
|
162
|
+
onKeyDown={(e) => {
|
|
163
|
+
if (onClick && (e.key === 'Enter' || e.key === ' ')) {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
onClick();
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<span style={emojiStyle} aria-hidden="true">
|
|
170
|
+
{character.emoji}
|
|
171
|
+
</span>
|
|
172
|
+
|
|
173
|
+
{!compact && (
|
|
174
|
+
<div style={styles.content}>
|
|
175
|
+
<span style={styles.message}>{character.message}</span>
|
|
176
|
+
{errorCount > 0 && (
|
|
177
|
+
<span style={styles.status}>
|
|
178
|
+
{errorCount}개의 {state === 'error' ? '에러' : '경고'}가 있어요
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Badge Component (for mini display)
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
export interface ManduBadgeProps {
|
|
193
|
+
state: ManduState;
|
|
194
|
+
count?: number;
|
|
195
|
+
onClick?: () => void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function ManduBadge({
|
|
199
|
+
state,
|
|
200
|
+
count = 0,
|
|
201
|
+
onClick,
|
|
202
|
+
}: ManduBadgeProps): React.ReactElement {
|
|
203
|
+
const character = MANDU_CHARACTERS[state];
|
|
204
|
+
const stateColor = stateColors[state];
|
|
205
|
+
|
|
206
|
+
const badgeStyle: React.CSSProperties = {
|
|
207
|
+
display: 'flex',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
justifyContent: 'center',
|
|
210
|
+
gap: '6px',
|
|
211
|
+
padding: '8px 12px',
|
|
212
|
+
borderRadius: '9999px',
|
|
213
|
+
backgroundColor: colors.background.dark,
|
|
214
|
+
border: `2px solid ${stateColor}`,
|
|
215
|
+
cursor: 'pointer',
|
|
216
|
+
transition: `all ${animation.duration.fast} ${animation.easing.easeOut}`,
|
|
217
|
+
boxShadow: colors.background.overlay,
|
|
218
|
+
fontSize: '16px',
|
|
219
|
+
userSelect: 'none',
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const countStyle: React.CSSProperties = {
|
|
223
|
+
fontSize: '12px',
|
|
224
|
+
fontWeight: 600,
|
|
225
|
+
color: stateColor,
|
|
226
|
+
minWidth: '18px',
|
|
227
|
+
textAlign: 'center',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<button
|
|
232
|
+
data-testid={testIds.badge}
|
|
233
|
+
style={badgeStyle}
|
|
234
|
+
onClick={onClick}
|
|
235
|
+
aria-label={`Mandu Kitchen: ${character.message}${count > 0 ? `, ${count} issues` : ''}`}
|
|
236
|
+
>
|
|
237
|
+
<span aria-hidden="true">{character.emoji}</span>
|
|
238
|
+
{count > 0 && <span style={countStyle}>{count}</span>}
|
|
239
|
+
</button>
|
|
240
|
+
);
|
|
241
|
+
}
|