@mandujs/core 0.54.28 → 0.54.30

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.54.28",
3
+ "version": "0.54.30",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -362,6 +362,40 @@ describe("buildClientBundles vendor shims", () => {
362
362
  expect((globalThis as typeof globalThis & { __MANDU_TEST_DEFAULT_PROPS__?: unknown }).__MANDU_TEST_DEFAULT_PROPS__).toBeUndefined();
363
363
  });
364
364
 
365
+ test("surfaces an island hydration failure to the DevTools hook (#324)", async () => {
366
+ const modulePath = path.join(rootDir, ".mandu", "client", "runtime-failing-boundary.js");
367
+ // A module that throws at import time → loadAndHydrate's catch runs.
368
+ await writeFile(modulePath, `throw new Error("boom at import");`, "utf-8");
369
+
370
+ const emitted: Array<{ type?: string; data?: { message?: string; islandId?: string } }> = [];
371
+ (window as unknown as { __MANDU_DEVTOOLS_HOOK__?: { emit: (e: unknown) => void } }).__MANDU_DEVTOOLS_HOOK__ = {
372
+ emit: (e: unknown) => {
373
+ emitted.push(e as { type?: string });
374
+ },
375
+ };
376
+
377
+ document.body.innerHTML = `
378
+ <div
379
+ data-mandu-island="failing--0"
380
+ data-mandu-src="${pathToFileURL(modulePath).href}?t=${Date.now()}"
381
+ data-hydrate="load"
382
+ ></div>
383
+ `;
384
+ (window as typeof window & { __MANDU_ROOTS__?: Map<string, unknown> }).__MANDU_ROOTS__ = new Map();
385
+
386
+ try {
387
+ const runtime = await evaluateGeneratedHydrationRuntime();
388
+ runtime.hydrateIslands();
389
+
390
+ await waitForRuntimeAssertion(() => emitted.some((e) => e.type === "error"));
391
+ const errorEvent = emitted.find((e) => e.type === "error");
392
+ expect(errorEvent?.data?.islandId).toBe("failing--0");
393
+ expect(errorEvent?.data?.message).toContain("boom at import");
394
+ } finally {
395
+ delete (window as unknown as { __MANDU_DEVTOOLS_HOOK__?: unknown }).__MANDU_DEVTOOLS_HOOK__;
396
+ }
397
+ });
398
+
365
399
  test("runtime hydrates default client boundary from boundary-local props", async () => {
366
400
  const modulePath = path.join(rootDir, ".mandu", "client", "runtime-default-boundary.js");
367
401
  await writeFile(
@@ -160,6 +160,7 @@ class IslandErrorBoundary extends Component<IslandErrorBoundaryProps, IslandErro
160
160
 
161
161
  componentDidCatch(error: Error, errorInfo: unknown): void {
162
162
  console.error("[Mandu] Island error:", this.props.islandId, error, errorInfo);
163
+ emitIslandHydrationError(this.props.islandId, error);
163
164
  }
164
165
 
165
166
  reset = (): void => {
@@ -371,6 +372,35 @@ function isManduIslandExport(value: unknown): value is ManduIslandExport {
371
372
  "definition" in value;
372
373
  }
373
374
 
375
+ // #324: surface island hydration/render failures to the in-page DevTools so
376
+ // its health badge and Issues counter reflect them instead of staying
377
+ // "HEALTHY". Render-time errors are swallowed by IslandErrorBoundary and never
378
+ // reach the global error listener, so we emit them to the devtools hook here.
379
+ function emitIslandHydrationError(islandId: string, error: unknown): void {
380
+ const devtoolsHook = (window as Window & {
381
+ __MANDU_DEVTOOLS_HOOK__?: RuntimeDevtoolsHook;
382
+ }).__MANDU_DEVTOOLS_HOOK__;
383
+ if (!devtoolsHook) return;
384
+ try {
385
+ devtoolsHook.emit({
386
+ type: "error",
387
+ timestamp: Date.now(),
388
+ data: {
389
+ id: "island-" + islandId + "-" + Date.now(),
390
+ type: "runtime",
391
+ severity: "error",
392
+ message: error instanceof Error ? error.message : String(error),
393
+ stack: error instanceof Error ? error.stack : undefined,
394
+ timestamp: Date.now(),
395
+ url: location.href,
396
+ islandId,
397
+ },
398
+ });
399
+ } catch {
400
+ // DevTools must never break hydration.
401
+ }
402
+ }
403
+
374
404
  async function loadAndHydrate(element: HTMLElement, src: string): Promise<void> {
375
405
  const id = element.getAttribute("data-mandu-island");
376
406
  if (!id) {
@@ -490,6 +520,7 @@ async function loadAndHydrate(element: HTMLElement, src: string): Promise<void>
490
520
  } catch (error) {
491
521
  console.error("[Mandu] Hydration failed for", islandId, error);
492
522
  element.setAttribute("data-mandu-error", "true");
523
+ emitIslandHydrationError(islandId, error);
493
524
 
494
525
  element.dispatchEvent(new CustomEvent("mandu:hydration-error", {
495
526
  bubbles: true,
@@ -7,28 +7,31 @@
7
7
  // Color Tokens
8
8
  // ============================================================================
9
9
 
10
+ // Aligned with mandujs.com brand tokens (app/globals.css + styles/tokens.css):
11
+ // primary peach #FF8C66, secondary dark-brown #4A3222, warm-brown dark surfaces
12
+ // (#1F1B16/#2A2520/#3E3028), cream text, and the docs-grade semantic palette.
10
13
  export const colors = {
11
14
  brand: {
12
- primary: '#F5E6D3', // 만두피 베이지
13
- secondary: '#8B4513', // 구운 갈색
14
- accent: '#E8967A', // 새우 만두 분홍
15
+ primary: '#F5EDE0', // 만두피 크림 (mandujs cream surface)
16
+ secondary: '#4A3222', // 구운 다크 브라운 (mandujs --color-secondary)
17
+ accent: '#FF8C66', // 만두 시그니처 피치 오렌지 (mandujs --color-primary)
15
18
  },
16
19
  semantic: {
17
- success: '#7EC8A0', // 세이지 그린 (따뜻한 )
18
- warning: '#E8B84D', // 앰버 (부드러운 경고)
19
- error: '#E86464', // 소프트 레드
20
- info: '#7AAFC8', // 스틸 블루 (차분한 정보)
20
+ success: '#6B9E47', // 올리브 그린 (mandujs --color-success)
21
+ warning: '#E8A93A', // 앰버 (mandujs --color-warn)
22
+ error: '#C85450', // 머티드 레드-브라운 (mandujs --color-danger)
23
+ info: '#4A90C2', // 차분한 블루 (mandujs --color-info)
21
24
  },
22
25
  background: {
23
- dark: '#1C1B2E', // 따뜻한 다크 인디고
24
- medium: '#2A2940', // 따뜻한 미디엄
25
- light: '#3A3854', // 따뜻한 라이트
26
- overlay: 'rgba(10, 8, 20, 0.88)',
26
+ dark: '#1F1B16', // 브라운 (mandujs --color-background-dark)
27
+ medium: '#2A2520', // 서피스 (mandujs --color-surface-dark)
28
+ light: '#3E3028', // 라이트 서피스 (mandujs --color-dark-surface)
29
+ overlay: 'rgba(20, 14, 8, 0.88)',
27
30
  },
28
31
  text: {
29
- primary: '#F0EDE8', // 크림 화이트 (따뜻한 )
30
- secondary: '#A8A4A0', // 따뜻한 그레이
31
- muted: '#6B6866', // 따뜻한 뮤트
32
+ primary: '#FFF0E6', // 크림 화이트 (mandujs --color-dark-text)
33
+ secondary: '#C9BCAD', // 베이지 그레이
34
+ muted: '#7A6B5D', // 뮤트 브라운 (mandujs --color-dark-muted)
32
35
  },
33
36
  } as const;
34
37
 
@@ -38,8 +41,9 @@ export const colors = {
38
41
 
39
42
  export const typography = {
40
43
  fontFamily: {
41
- mono: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
42
- sans: "'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
44
+ // Aligned with mandujs.com (app/globals.css --font-mono / --font-sans).
45
+ mono: "'Consolas', 'Monaco', 'Ubuntu Mono', 'JetBrains Mono', ui-monospace, monospace",
46
+ sans: "'Pretendard Variable', 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
43
47
  },
44
48
  fontSize: {
45
49
  xs: '10px',
@@ -99,11 +103,12 @@ export const borderWidth = {
99
103
  // ============================================================================
100
104
 
101
105
  export const shadows = {
102
- sm: '0 1px 3px rgba(8, 6, 18, 0.12)',
103
- md: '0 4px 8px rgba(8, 6, 18, 0.18)',
104
- lg: '0 10px 20px rgba(8, 6, 18, 0.24)',
105
- xl: '0 20px 30px rgba(8, 6, 18, 0.32)',
106
- overlay: '0 25px 50px rgba(8, 6, 18, 0.5)',
106
+ // Warm-tinted shadows to match the brown-toned dark surfaces.
107
+ sm: '0 1px 3px rgba(20, 12, 4, 0.14)',
108
+ md: '0 4px 8px rgba(20, 12, 4, 0.20)',
109
+ lg: '0 10px 20px rgba(20, 12, 4, 0.26)',
110
+ xl: '0 20px 30px rgba(20, 12, 4, 0.34)',
111
+ overlay: '0 25px 50px rgba(20, 12, 4, 0.5)',
107
112
  } as const;
108
113
 
109
114
  // ============================================================================