@mandujs/core 0.19.2 → 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.
- package/package.json +1 -1
- package/src/bundler/build.ts +94 -2
- package/src/bundler/types.ts +20 -0
- package/src/devtools/client/components/mandu-character.tsx +77 -53
- package/src/devtools/client/components/panel/errors-panel.tsx +2 -2
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -31
- package/src/devtools/client/components/panel/islands-panel.tsx +30 -14
- package/src/devtools/client/components/panel/network-panel.tsx +2 -3
- package/src/devtools/client/components/panel/panel-container.tsx +485 -332
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -22
- package/src/devtools/init.ts +1 -1
- package/src/devtools/types.ts +35 -35
- package/src/filling/filling.ts +66 -66
- package/src/index.ts +1 -0
- package/src/kitchen/kitchen-handler.ts +80 -1
- package/src/observability/event-bus.ts +79 -0
- package/src/observability/index.ts +8 -0
- package/src/observability/logger-adapter.ts +36 -0
- package/src/runtime/index.ts +9 -8
- package/src/runtime/ppr.ts +74 -0
- package/src/runtime/server.ts +203 -147
- package/src/runtime/ssr.ts +17 -4
- package/src/runtime/streaming-ssr.ts +55 -36
- package/src/testing/index.ts +45 -0
package/package.json
CHANGED
package/src/bundler/build.ts
CHANGED
|
@@ -11,11 +11,67 @@ import type {
|
|
|
11
11
|
BundleManifest,
|
|
12
12
|
BundleStats,
|
|
13
13
|
BundlerOptions,
|
|
14
|
+
IslandFileEntry,
|
|
14
15
|
} from "./types";
|
|
15
16
|
import { HYDRATION } from "../constants";
|
|
16
17
|
import path from "path";
|
|
17
18
|
import fs from "fs/promises";
|
|
18
19
|
|
|
20
|
+
/** Scan for *.island.tsx / *.island.ts files across hydrated route directories. */
|
|
21
|
+
async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<IslandFileEntry[]> {
|
|
22
|
+
const entries: IslandFileEntry[] = [];
|
|
23
|
+
const seenDirs = new Set<string>();
|
|
24
|
+
|
|
25
|
+
for (const route of routes) {
|
|
26
|
+
const dir = path.dirname(path.join(rootDir, route.componentModule ?? route.module));
|
|
27
|
+
if (seenDirs.has(dir)) continue;
|
|
28
|
+
seenDirs.add(dir);
|
|
29
|
+
|
|
30
|
+
let files: string[];
|
|
31
|
+
try { files = await fs.readdir(dir); } catch { continue; }
|
|
32
|
+
|
|
33
|
+
const priority = getRouteHydration(route)?.priority || HYDRATION.DEFAULT_PRIORITY;
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (/\.island\.tsx?$/.test(file)) {
|
|
36
|
+
entries.push({
|
|
37
|
+
name: file.replace(/\.island\.tsx?$/, ""),
|
|
38
|
+
filePath: path.join(dir, file),
|
|
39
|
+
routeId: route.id,
|
|
40
|
+
priority,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build a single per-island bundle. */
|
|
49
|
+
async function buildPerIslandBundle(
|
|
50
|
+
entry: IslandFileEntry, outDir: string, options: BundlerOptions
|
|
51
|
+
): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
|
|
52
|
+
const entryPath = path.join(outDir, `_entry_island_${entry.name}.js`);
|
|
53
|
+
const outputName = `${entry.name}.island.js`;
|
|
54
|
+
try {
|
|
55
|
+
await Bun.write(entryPath, generateIslandEntry(entry.name, entry.filePath));
|
|
56
|
+
const result = await Bun.build({
|
|
57
|
+
entrypoints: [entryPath],
|
|
58
|
+
outdir: outDir,
|
|
59
|
+
naming: outputName,
|
|
60
|
+
minify: options.minify ?? process.env.NODE_ENV === "production",
|
|
61
|
+
sourcemap: options.sourcemap ? "external" : "none",
|
|
62
|
+
target: "browser",
|
|
63
|
+
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
64
|
+
define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), ...options.define },
|
|
65
|
+
});
|
|
66
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
67
|
+
if (!result.success) throw new Error(result.logs.map((l) => l.message).join("\n"));
|
|
68
|
+
return { name: entry.name, js: `/.mandu/client/${outputName}`, route: entry.routeId, priority: entry.priority };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
19
75
|
/**
|
|
20
76
|
* 빈 매니페스트 생성
|
|
21
77
|
*/
|
|
@@ -1292,7 +1348,8 @@ function createBundleManifest(
|
|
|
1292
1348
|
runtimePath: string,
|
|
1293
1349
|
vendorResult: VendorBuildResult,
|
|
1294
1350
|
routerPath: string,
|
|
1295
|
-
env: "development" | "production"
|
|
1351
|
+
env: "development" | "production",
|
|
1352
|
+
islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>
|
|
1296
1353
|
): BundleManifest {
|
|
1297
1354
|
const bundles: BundleManifest["bundles"] = {};
|
|
1298
1355
|
|
|
@@ -1307,11 +1364,25 @@ function createBundleManifest(
|
|
|
1307
1364
|
};
|
|
1308
1365
|
}
|
|
1309
1366
|
|
|
1367
|
+
// Per-island bundles (code splitting)
|
|
1368
|
+
let islands: BundleManifest["islands"];
|
|
1369
|
+
if (islandBundles && islandBundles.length > 0) {
|
|
1370
|
+
islands = {};
|
|
1371
|
+
for (const ib of islandBundles) {
|
|
1372
|
+
islands[ib.name] = {
|
|
1373
|
+
js: ib.js,
|
|
1374
|
+
route: ib.route,
|
|
1375
|
+
priority: ib.priority,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1310
1380
|
return {
|
|
1311
1381
|
version: 1,
|
|
1312
1382
|
buildTime: new Date().toISOString(),
|
|
1313
1383
|
env,
|
|
1314
1384
|
bundles,
|
|
1385
|
+
...(islands ? { islands } : {}),
|
|
1315
1386
|
shared: {
|
|
1316
1387
|
runtime: runtimePath,
|
|
1317
1388
|
vendor: vendorResult.react, // primary vendor for backwards compatibility
|
|
@@ -1544,6 +1615,26 @@ export async function buildClientBundles(
|
|
|
1544
1615
|
}
|
|
1545
1616
|
}
|
|
1546
1617
|
|
|
1618
|
+
// 5.5. Per-island code splitting: scan and build individual island bundles
|
|
1619
|
+
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
1620
|
+
const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
|
|
1621
|
+
|
|
1622
|
+
if (islandFiles.length > 0) {
|
|
1623
|
+
const islandResults = await Promise.all(
|
|
1624
|
+
islandFiles.map(async (entry) => {
|
|
1625
|
+
try {
|
|
1626
|
+
return await buildPerIslandBundle(entry, outDir, options);
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
errors.push(`[island:${entry.name}] ${String(error)}`);
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
})
|
|
1632
|
+
);
|
|
1633
|
+
for (const result of islandResults) {
|
|
1634
|
+
if (result) islandBundles.push(result);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1547
1638
|
// 6. 번들 매니페스트 생성
|
|
1548
1639
|
const bundleManifest = createBundleManifest(
|
|
1549
1640
|
outputs,
|
|
@@ -1551,7 +1642,8 @@ export async function buildClientBundles(
|
|
|
1551
1642
|
runtimeResult.outputPath,
|
|
1552
1643
|
vendorResult,
|
|
1553
1644
|
routerResult.outputPath,
|
|
1554
|
-
env
|
|
1645
|
+
env,
|
|
1646
|
+
islandBundles
|
|
1555
1647
|
);
|
|
1556
1648
|
|
|
1557
1649
|
await fs.writeFile(
|
package/src/bundler/types.ts
CHANGED
|
@@ -53,6 +53,18 @@ export interface BundleManifest {
|
|
|
53
53
|
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
54
54
|
}
|
|
55
55
|
>;
|
|
56
|
+
/** Per-island bundles (code splitting: each island file gets its own JS bundle) */
|
|
57
|
+
islands?: Record<
|
|
58
|
+
string,
|
|
59
|
+
{
|
|
60
|
+
/** JavaScript bundle path */
|
|
61
|
+
js: string;
|
|
62
|
+
/** Route that owns this island */
|
|
63
|
+
route: string;
|
|
64
|
+
/** Hydration priority */
|
|
65
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
66
|
+
}
|
|
67
|
+
>;
|
|
56
68
|
/** 공유 청크 */
|
|
57
69
|
shared: {
|
|
58
70
|
/** Hydration 런타임 */
|
|
@@ -87,6 +99,14 @@ export interface BundleStats {
|
|
|
87
99
|
bundleCount: number;
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
/** Per-island code splitting entry (used by scanIslandFiles) */
|
|
103
|
+
export interface IslandFileEntry {
|
|
104
|
+
name: string;
|
|
105
|
+
filePath: string;
|
|
106
|
+
routeId: string;
|
|
107
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
/**
|
|
91
111
|
* 번들러 옵션
|
|
92
112
|
*/
|
|
@@ -8,24 +8,34 @@ import type { ManduState } from '../../types';
|
|
|
8
8
|
import { MANDU_CHARACTERS } from '../../types';
|
|
9
9
|
import { colors, typography, animation, testIds } from '../../design-tokens';
|
|
10
10
|
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Styles
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
11
|
const styles = {
|
|
16
12
|
container: {
|
|
17
13
|
display: 'flex',
|
|
18
14
|
alignItems: 'center',
|
|
19
15
|
gap: '12px',
|
|
20
16
|
padding: '12px 16px',
|
|
21
|
-
borderRadius: '
|
|
22
|
-
|
|
17
|
+
borderRadius: '16px',
|
|
18
|
+
background: `linear-gradient(180deg, ${colors.background.medium}, ${colors.background.dark})`,
|
|
19
|
+
border: `1px solid ${colors.background.light}`,
|
|
20
|
+
boxShadow: '0 14px 32px rgba(7, 7, 12, 0.28)',
|
|
23
21
|
transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
|
|
24
22
|
},
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
mark: {
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
minWidth: '44px',
|
|
28
|
+
height: '44px',
|
|
29
|
+
padding: '0 12px',
|
|
30
|
+
borderRadius: '9999px',
|
|
31
|
+
fontSize: '11px',
|
|
27
32
|
lineHeight: 1,
|
|
33
|
+
letterSpacing: '0.12em',
|
|
34
|
+
fontWeight: 700,
|
|
28
35
|
userSelect: 'none' as const,
|
|
36
|
+
fontFamily: typography.fontFamily.mono,
|
|
37
|
+
textTransform: 'uppercase' as const,
|
|
38
|
+
border: `1px solid ${colors.background.light}`,
|
|
29
39
|
},
|
|
30
40
|
content: {
|
|
31
41
|
display: 'flex',
|
|
@@ -35,7 +45,7 @@ const styles = {
|
|
|
35
45
|
message: {
|
|
36
46
|
fontSize: '14px',
|
|
37
47
|
color: colors.text.primary,
|
|
38
|
-
fontWeight:
|
|
48
|
+
fontWeight: 600,
|
|
39
49
|
},
|
|
40
50
|
status: {
|
|
41
51
|
fontSize: '12px',
|
|
@@ -51,10 +61,6 @@ const stateColors: Record<ManduState, string> = {
|
|
|
51
61
|
hmr: colors.brand.accent,
|
|
52
62
|
};
|
|
53
63
|
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// Animation Keyframes (inline)
|
|
56
|
-
// ============================================================================
|
|
57
|
-
|
|
58
64
|
const bounceAnimation = `
|
|
59
65
|
@keyframes mk-bounce {
|
|
60
66
|
0%, 100% { transform: translateY(0); }
|
|
@@ -80,15 +86,11 @@ const shakeAnimation = `
|
|
|
80
86
|
const sparkleAnimation = `
|
|
81
87
|
@keyframes mk-sparkle {
|
|
82
88
|
0% { transform: scale(1); }
|
|
83
|
-
50% { transform: scale(1.
|
|
89
|
+
50% { transform: scale(1.08); }
|
|
84
90
|
100% { transform: scale(1); }
|
|
85
91
|
}
|
|
86
92
|
`;
|
|
87
93
|
|
|
88
|
-
// ============================================================================
|
|
89
|
-
// Props
|
|
90
|
-
// ============================================================================
|
|
91
|
-
|
|
92
94
|
export interface ManduCharacterProps {
|
|
93
95
|
state: ManduState;
|
|
94
96
|
errorCount?: number;
|
|
@@ -97,10 +99,6 @@ export interface ManduCharacterProps {
|
|
|
97
99
|
onClick?: () => void;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
// ============================================================================
|
|
101
|
-
// Component
|
|
102
|
-
// ============================================================================
|
|
103
|
-
|
|
104
102
|
export function ManduCharacter({
|
|
105
103
|
state,
|
|
106
104
|
errorCount = 0,
|
|
@@ -126,25 +124,34 @@ export function ManduCharacter({
|
|
|
126
124
|
}
|
|
127
125
|
};
|
|
128
126
|
|
|
129
|
-
const containerStyle = {
|
|
127
|
+
const containerStyle: React.CSSProperties = {
|
|
130
128
|
...styles.container,
|
|
131
129
|
borderLeft: `4px solid ${stateColor}`,
|
|
132
130
|
cursor: onClick ? 'pointer' : 'default',
|
|
133
131
|
...(compact && {
|
|
134
132
|
padding: '8px 12px',
|
|
135
133
|
gap: '8px',
|
|
134
|
+
borderRadius: '14px',
|
|
136
135
|
}),
|
|
137
136
|
};
|
|
138
137
|
|
|
139
|
-
const
|
|
140
|
-
...styles.
|
|
138
|
+
const markStyle: React.CSSProperties = {
|
|
139
|
+
...styles.mark,
|
|
141
140
|
animation: getAnimation(),
|
|
142
|
-
|
|
141
|
+
color: stateColor,
|
|
142
|
+
backgroundColor: `${stateColor}18`,
|
|
143
|
+
borderColor: `${stateColor}45`,
|
|
144
|
+
boxShadow: `inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 0 0 1px ${stateColor}10`,
|
|
145
|
+
...(compact && {
|
|
146
|
+
minWidth: '36px',
|
|
147
|
+
height: '36px',
|
|
148
|
+
padding: '0 10px',
|
|
149
|
+
fontSize: '10px',
|
|
150
|
+
}),
|
|
143
151
|
};
|
|
144
152
|
|
|
145
153
|
return (
|
|
146
154
|
<>
|
|
147
|
-
{/* Inject keyframes */}
|
|
148
155
|
<style>
|
|
149
156
|
{bounceAnimation}
|
|
150
157
|
{pulseAnimation}
|
|
@@ -166,8 +173,8 @@ export function ManduCharacter({
|
|
|
166
173
|
}
|
|
167
174
|
}}
|
|
168
175
|
>
|
|
169
|
-
<span style={
|
|
170
|
-
{character.
|
|
176
|
+
<span style={markStyle} aria-hidden="true">
|
|
177
|
+
{character.mark}
|
|
171
178
|
</span>
|
|
172
179
|
|
|
173
180
|
{!compact && (
|
|
@@ -175,7 +182,7 @@ export function ManduCharacter({
|
|
|
175
182
|
<span style={styles.message}>{character.message}</span>
|
|
176
183
|
{errorCount > 0 && (
|
|
177
184
|
<span style={styles.status}>
|
|
178
|
-
{errorCount}개의 {state === 'error' ? '에러' : '경고'}가
|
|
185
|
+
{errorCount}개의 {state === 'error' ? '에러' : '경고'}가 있습니다
|
|
179
186
|
</span>
|
|
180
187
|
)}
|
|
181
188
|
</div>
|
|
@@ -185,10 +192,6 @@ export function ManduCharacter({
|
|
|
185
192
|
);
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
// ============================================================================
|
|
189
|
-
// Badge Component (for mini display)
|
|
190
|
-
// ============================================================================
|
|
191
|
-
|
|
192
195
|
export interface ManduBadgeProps {
|
|
193
196
|
state: ManduState;
|
|
194
197
|
count?: number;
|
|
@@ -210,28 +213,28 @@ export function ManduBadge({
|
|
|
210
213
|
display: 'flex',
|
|
211
214
|
alignItems: 'center',
|
|
212
215
|
justifyContent: 'center',
|
|
213
|
-
width: '
|
|
214
|
-
height: '
|
|
215
|
-
padding: 0,
|
|
216
|
-
borderRadius: '
|
|
217
|
-
|
|
218
|
-
? colors.background.light
|
|
216
|
+
width: '72px',
|
|
217
|
+
height: '56px',
|
|
218
|
+
padding: '0 14px',
|
|
219
|
+
borderRadius: '18px',
|
|
220
|
+
background: isPressed
|
|
221
|
+
? `linear-gradient(180deg, ${colors.background.light}, ${colors.background.medium})`
|
|
219
222
|
: isHovered
|
|
220
|
-
? colors.background.medium
|
|
221
|
-
: colors.background.dark,
|
|
222
|
-
border: `
|
|
223
|
+
? `linear-gradient(180deg, ${colors.background.medium}, ${colors.background.dark})`
|
|
224
|
+
: `linear-gradient(180deg, ${colors.background.dark}, ${colors.background.medium})`,
|
|
225
|
+
border: `1px solid ${isPressed ? stateColor : isHovered ? colors.brand.accent : `${stateColor}80`}`,
|
|
223
226
|
cursor: 'pointer',
|
|
224
227
|
transition: `all 200ms ${animation.easing.spring}`,
|
|
225
228
|
boxShadow: isPressed
|
|
226
|
-
?
|
|
229
|
+
? '0 8px 18px rgba(8, 6, 18, 0.36), inset 0 2px 4px rgba(0, 0, 0, 0.12)'
|
|
227
230
|
: isHovered
|
|
228
|
-
? `0
|
|
229
|
-
: `0
|
|
231
|
+
? `0 18px 36px rgba(8, 6, 18, 0.42), 0 0 0 4px ${stateColor}18`
|
|
232
|
+
: `0 10px 22px rgba(8, 6, 18, 0.3), 0 0 0 1px ${stateColor}14`,
|
|
230
233
|
userSelect: 'none',
|
|
231
234
|
transform: isPressed
|
|
232
235
|
? 'scale(0.92) translateY(1px)'
|
|
233
236
|
: isHovered
|
|
234
|
-
? 'scale(1.
|
|
237
|
+
? 'scale(1.04) translateY(-2px)'
|
|
235
238
|
: 'scale(1) translateY(0px)',
|
|
236
239
|
outline: 'none',
|
|
237
240
|
lineHeight: 1,
|
|
@@ -247,17 +250,29 @@ export function ManduBadge({
|
|
|
247
250
|
};
|
|
248
251
|
|
|
249
252
|
const textStyle: React.CSSProperties = {
|
|
253
|
+
display: 'flex',
|
|
254
|
+
flexDirection: 'column',
|
|
255
|
+
alignItems: 'flex-start',
|
|
256
|
+
gap: '3px',
|
|
250
257
|
fontFamily: typography.fontFamily.sans,
|
|
251
258
|
color: colors.brand.accent,
|
|
252
|
-
fontSize: '
|
|
259
|
+
fontSize: '13px',
|
|
253
260
|
fontWeight: typography.fontWeight.bold,
|
|
254
261
|
lineHeight: 1,
|
|
255
|
-
letterSpacing: '0.
|
|
262
|
+
letterSpacing: '0.08em',
|
|
256
263
|
transition: `transform 200ms ${animation.easing.spring}`,
|
|
257
|
-
transform: isHovered ? '
|
|
264
|
+
transform: isHovered ? 'translateY(-1px)' : 'translateY(0px)',
|
|
258
265
|
userSelect: 'none',
|
|
259
266
|
};
|
|
260
267
|
|
|
268
|
+
const subtextStyle: React.CSSProperties = {
|
|
269
|
+
fontSize: '9px',
|
|
270
|
+
fontWeight: typography.fontWeight.medium,
|
|
271
|
+
letterSpacing: '0.16em',
|
|
272
|
+
color: colors.text.secondary,
|
|
273
|
+
textTransform: 'uppercase',
|
|
274
|
+
};
|
|
275
|
+
|
|
261
276
|
const countBubbleStyle: React.CSSProperties = {
|
|
262
277
|
position: 'absolute',
|
|
263
278
|
top: '-4px',
|
|
@@ -286,14 +301,23 @@ export function ManduBadge({
|
|
|
286
301
|
style={badgeStyle}
|
|
287
302
|
onClick={onClick}
|
|
288
303
|
onMouseEnter={() => setIsHovered(true)}
|
|
289
|
-
onMouseLeave={() => {
|
|
304
|
+
onMouseLeave={() => {
|
|
305
|
+
setIsHovered(false);
|
|
306
|
+
setIsPressed(false);
|
|
307
|
+
}}
|
|
290
308
|
onMouseDown={() => setIsPressed(true)}
|
|
291
309
|
onMouseUp={() => setIsPressed(false)}
|
|
292
310
|
onFocus={() => setIsHovered(true)}
|
|
293
|
-
onBlur={() => {
|
|
311
|
+
onBlur={() => {
|
|
312
|
+
setIsHovered(false);
|
|
313
|
+
setIsPressed(false);
|
|
314
|
+
}}
|
|
294
315
|
aria-label={`Mandu Kitchen: ${character.message}${count > 0 ? `, ${count} issues` : ''}`}
|
|
295
316
|
>
|
|
296
|
-
<span aria-hidden="true" style={textStyle}>
|
|
317
|
+
<span aria-hidden="true" style={textStyle}>
|
|
318
|
+
<span>MK</span>
|
|
319
|
+
<span style={subtextStyle}>Dev</span>
|
|
320
|
+
</span>
|
|
297
321
|
{count > 0 && (
|
|
298
322
|
<span style={countBubbleStyle}>
|
|
299
323
|
{count > 99 ? '99+' : count}
|
|
@@ -176,8 +176,8 @@ export function ErrorsPanel({
|
|
|
176
176
|
<div style={styles.emptyState}>
|
|
177
177
|
<ManduCharacter state="normal" compact />
|
|
178
178
|
<p style={styles.emptyMessage}>
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
활성 오류가 없습니다.<br />
|
|
180
|
+
새 이슈가 생기면 여기에 표시됩니다.
|
|
181
181
|
</p>
|
|
182
182
|
</div>
|
|
183
183
|
</div>
|
|
@@ -204,12 +204,11 @@ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.Reac
|
|
|
204
204
|
if (violations.length === 0) {
|
|
205
205
|
return (
|
|
206
206
|
<div style={styles.container}>
|
|
207
|
-
<div style={styles.emptyState}>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</p>
|
|
207
|
+
<div style={styles.emptyState}>
|
|
208
|
+
<p>
|
|
209
|
+
현재 아키텍처 위반이 없습니다.<br />
|
|
210
|
+
새 스캔 결과가 생기면 여기에 표시됩니다.
|
|
211
|
+
</p>
|
|
213
212
|
<button
|
|
214
213
|
style={{
|
|
215
214
|
...styles.clearButton,
|
|
@@ -219,16 +218,16 @@ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.Reac
|
|
|
219
218
|
}}
|
|
220
219
|
onClick={handleScan}
|
|
221
220
|
disabled={scanning}
|
|
222
|
-
>
|
|
223
|
-
{scanning ? '스캔 중...' : '
|
|
224
|
-
</button>
|
|
225
|
-
{scanResult && (
|
|
226
|
-
<p style={{ marginTop: spacing.sm, fontSize: typography.fontSize.xs }}>
|
|
227
|
-
스캔 결과: {scanResult.total === 0
|
|
228
|
-
? '위반 없음
|
|
229
|
-
: `${scanResult.total}개 위반 (에러 ${scanResult.errors}, 경고 ${scanResult.warnings})`
|
|
230
|
-
}
|
|
231
|
-
</p>
|
|
221
|
+
>
|
|
222
|
+
{scanning ? '스캔 중...' : '전체 스캔'}
|
|
223
|
+
</button>
|
|
224
|
+
{scanResult && (
|
|
225
|
+
<p style={{ marginTop: spacing.sm, fontSize: typography.fontSize.xs }}>
|
|
226
|
+
스캔 결과: {scanResult.total === 0
|
|
227
|
+
? '위반 없음'
|
|
228
|
+
: `${scanResult.total}개 위반 (에러 ${scanResult.errors}, 경고 ${scanResult.warnings})`
|
|
229
|
+
}
|
|
230
|
+
</p>
|
|
232
231
|
)}
|
|
233
232
|
</div>
|
|
234
233
|
</div>
|
|
@@ -253,9 +252,9 @@ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.Reac
|
|
|
253
252
|
style={{ ...styles.clearButton, opacity: scanning ? 0.5 : 1 }}
|
|
254
253
|
onClick={handleScan}
|
|
255
254
|
disabled={scanning}
|
|
256
|
-
>
|
|
257
|
-
{scanning ? '스캔 중...' : '
|
|
258
|
-
</button>
|
|
255
|
+
>
|
|
256
|
+
{scanning ? '스캔 중...' : '스캔'}
|
|
257
|
+
</button>
|
|
259
258
|
<button style={styles.clearButton} onClick={onClear}>
|
|
260
259
|
모두 지우기
|
|
261
260
|
</button>
|
|
@@ -323,9 +322,9 @@ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.Reac
|
|
|
323
322
|
color: isApproved ? colors.semantic.success : colors.text.secondary,
|
|
324
323
|
}}
|
|
325
324
|
onClick={() => handleDecision(violation.ruleId, violation.source.file, 'approve')}
|
|
326
|
-
>
|
|
327
|
-
|
|
328
|
-
</button>
|
|
325
|
+
>
|
|
326
|
+
허용
|
|
327
|
+
</button>
|
|
329
328
|
<button
|
|
330
329
|
style={{
|
|
331
330
|
...styles.clearButton,
|
|
@@ -335,9 +334,9 @@ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.Reac
|
|
|
335
334
|
color: decision === 'reject' ? colors.semantic.error : colors.text.secondary,
|
|
336
335
|
}}
|
|
337
336
|
onClick={() => handleDecision(violation.ruleId, violation.source.file, 'reject')}
|
|
338
|
-
>
|
|
339
|
-
|
|
340
|
-
</button>
|
|
337
|
+
>
|
|
338
|
+
차단
|
|
339
|
+
</button>
|
|
341
340
|
</div>
|
|
342
341
|
</div>
|
|
343
342
|
|
|
@@ -359,12 +358,12 @@ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.Reac
|
|
|
359
358
|
)}
|
|
360
359
|
</div>
|
|
361
360
|
|
|
362
|
-
{violation.suggestion && (
|
|
363
|
-
<div style={styles.suggestion}>
|
|
364
|
-
<div style={styles.suggestionLabel}
|
|
365
|
-
{violation.suggestion}
|
|
366
|
-
</div>
|
|
367
|
-
)}
|
|
361
|
+
{violation.suggestion && (
|
|
362
|
+
<div style={styles.suggestion}>
|
|
363
|
+
<div style={styles.suggestionLabel}>제안</div>
|
|
364
|
+
{violation.suggestion}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
368
367
|
</div>
|
|
369
368
|
);
|
|
370
369
|
})}
|
|
@@ -68,9 +68,18 @@ const styles = {
|
|
|
68
68
|
transition: `all ${animation.duration.fast}`,
|
|
69
69
|
},
|
|
70
70
|
islandIcon: {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
display: 'inline-flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
justifyContent: 'center',
|
|
74
|
+
minWidth: '48px',
|
|
75
|
+
height: '28px',
|
|
76
|
+
padding: '0 8px',
|
|
77
|
+
borderRadius: borderRadius.full,
|
|
73
78
|
textAlign: 'center' as const,
|
|
79
|
+
fontSize: typography.fontSize.xs,
|
|
80
|
+
fontWeight: typography.fontWeight.bold,
|
|
81
|
+
fontFamily: typography.fontFamily.mono,
|
|
82
|
+
letterSpacing: '0.08em',
|
|
74
83
|
},
|
|
75
84
|
islandInfo: {
|
|
76
85
|
flex: 1,
|
|
@@ -118,11 +127,11 @@ const styles = {
|
|
|
118
127
|
};
|
|
119
128
|
|
|
120
129
|
const statusConfig: Record<string, { icon: string; color: string; bg: string }> = {
|
|
121
|
-
ssr: { icon: '
|
|
122
|
-
pending: { icon: '
|
|
123
|
-
hydrating: { icon: '
|
|
124
|
-
hydrated: { icon: '
|
|
125
|
-
error: { icon: '
|
|
130
|
+
ssr: { icon: 'SSR', color: colors.text.muted, bg: colors.background.light },
|
|
131
|
+
pending: { icon: 'WAIT', color: colors.semantic.warning, bg: `${colors.semantic.warning}20` },
|
|
132
|
+
hydrating: { icon: 'HYDR', color: colors.semantic.info, bg: `${colors.semantic.info}20` },
|
|
133
|
+
hydrated: { icon: 'READY', color: colors.semantic.success, bg: `${colors.semantic.success}20` },
|
|
134
|
+
error: { icon: 'ERR', color: colors.semantic.error, bg: `${colors.semantic.error}20` },
|
|
126
135
|
};
|
|
127
136
|
|
|
128
137
|
const strategyLabels: Record<string, string> = {
|
|
@@ -178,10 +187,9 @@ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement
|
|
|
178
187
|
return (
|
|
179
188
|
<div style={styles.container}>
|
|
180
189
|
<div style={styles.emptyState}>
|
|
181
|
-
🏝️
|
|
182
190
|
<p>
|
|
183
|
-
아직 등록된 Island가
|
|
184
|
-
Island 컴포넌트를 사용하면 여기에 표시됩니다.
|
|
191
|
+
아직 등록된 Island가 없습니다.<br />
|
|
192
|
+
Island 컴포넌트를 사용하면 여기에 상태가 표시됩니다.
|
|
185
193
|
</p>
|
|
186
194
|
</div>
|
|
187
195
|
</div>
|
|
@@ -198,18 +206,18 @@ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement
|
|
|
198
206
|
<span style={styles.statValue}>{stats.total}</span>
|
|
199
207
|
</div>
|
|
200
208
|
<div style={styles.stat}>
|
|
201
|
-
<span
|
|
209
|
+
<span>Ready</span>
|
|
202
210
|
<span style={styles.statValue}>{stats.hydrated}</span>
|
|
203
211
|
</div>
|
|
204
212
|
{stats.pending > 0 && (
|
|
205
213
|
<div style={styles.stat}>
|
|
206
|
-
<span
|
|
214
|
+
<span>Pending</span>
|
|
207
215
|
<span style={styles.statValue}>{stats.pending}</span>
|
|
208
216
|
</div>
|
|
209
217
|
)}
|
|
210
218
|
{stats.errors > 0 && (
|
|
211
219
|
<div style={styles.stat}>
|
|
212
|
-
<span
|
|
220
|
+
<span>Error</span>
|
|
213
221
|
<span style={{ ...styles.statValue, color: colors.semantic.error }}>
|
|
214
222
|
{stats.errors}
|
|
215
223
|
</span>
|
|
@@ -234,7 +242,15 @@ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement
|
|
|
234
242
|
|
|
235
243
|
return (
|
|
236
244
|
<div key={island.id} style={styles.islandItem}>
|
|
237
|
-
<span
|
|
245
|
+
<span
|
|
246
|
+
style={{
|
|
247
|
+
...styles.islandIcon,
|
|
248
|
+
backgroundColor: status.bg,
|
|
249
|
+
color: status.color,
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
{status.icon}
|
|
253
|
+
</span>
|
|
238
254
|
|
|
239
255
|
<div style={styles.islandInfo}>
|
|
240
256
|
<div style={styles.islandName}>{island.name}</div>
|
|
@@ -213,10 +213,9 @@ export function NetworkPanel({ requests }: NetworkPanelProps): React.ReactElemen
|
|
|
213
213
|
return (
|
|
214
214
|
<div style={styles.container}>
|
|
215
215
|
<div style={styles.emptyState}>
|
|
216
|
-
📡
|
|
217
216
|
<p>
|
|
218
|
-
아직 네트워크 요청이
|
|
219
|
-
|
|
217
|
+
아직 네트워크 요청이 없습니다.<br />
|
|
218
|
+
요청이 발생하면 여기에 기록됩니다.
|
|
220
219
|
</p>
|
|
221
220
|
</div>
|
|
222
221
|
</div>
|