@zenuml/core 3.47.0 → 3.47.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 (30) hide show
  1. package/.agents/skills/babysit-pr/SKILL.md +223 -0
  2. package/.agents/skills/babysit-pr/agents/openai.yaml +7 -0
  3. package/.agents/skills/dia-scoring/SKILL.md +139 -0
  4. package/.agents/skills/dia-scoring/agents/openai.yaml +7 -0
  5. package/.agents/skills/dia-scoring/references/selectors-and-keys.md +253 -0
  6. package/.agents/skills/land-pr/SKILL.md +120 -0
  7. package/.agents/skills/propagate-core-release/SKILL.md +205 -0
  8. package/.agents/skills/propagate-core-release/agents/openai.yaml +7 -0
  9. package/.agents/skills/propagate-core-release/references/downstreams.md +42 -0
  10. package/.agents/skills/ship-branch/SKILL.md +105 -0
  11. package/.agents/skills/submit-branch/SKILL.md +76 -0
  12. package/.agents/skills/validate-branch/SKILL.md +72 -0
  13. package/.claude/skills/emoji-eval/SKILL.md +187 -0
  14. package/.claude/skills/propagate-core-release/SKILL.md +81 -76
  15. package/.claude/skills/propagate-core-release/agents/openai.yaml +2 -2
  16. package/AGENTS.md +1 -1
  17. package/dist/stats.html +1 -1
  18. package/dist/zenuml.esm.mjs +16210 -15460
  19. package/dist/zenuml.js +540 -535
  20. package/docs/superpowers/plans/2026-03-30-emoji-support.md +1220 -0
  21. package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +206 -0
  22. package/e2e/data/compare-cases.js +233 -0
  23. package/e2e/tools/compare-case.html +17 -3
  24. package/package.json +3 -3
  25. package/playwright.config.ts +1 -1
  26. package/scripts/analyze-compare-case/collect-data.mjs +159 -16
  27. package/scripts/analyze-compare-case/config.mjs +1 -1
  28. package/scripts/analyze-compare-case/report.mjs +5 -0
  29. package/scripts/analyze-compare-case/residual-scopes.mjs +23 -1
  30. package/scripts/analyze-compare-case/scoring.mjs +13 -0
@@ -0,0 +1,1220 @@
1
+ # Emoji Support Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add `[shortcode]` emoji syntax to ZenUML participants, messages, conditions, comments, and dividers — with Twemoji SVG rendering via an Icon Registry service.
6
+
7
+ **Architecture:** Three layers — (1) ANTLR grammar + parser extracts emoji shortcodes from DSL, (2) resolution engine converts shortcodes to emoji data (CSS-first fallback, colon override), (3) HTML and SVG renderers display emoji inline with text. A `fetchEmojis()` abstraction allows plugging in the Cloudflare Icon Registry or a local stub.
8
+
9
+ **Tech Stack:** ANTLR4 (grammar), TypeScript (parser/renderer), React 19 + Jotai (HTML renderer), SVG string builder (SVG renderer), Vitest (unit tests), Playwright (E2E)
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-03-30-emoji-support-design.md`
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ ### New files
18
+ - `src/emoji/resolveEmoji.ts` — Resolution engine: `[name]` → CSS style vs emoji, `[:name:]` → emoji. Single function used by all contexts.
19
+ - `src/emoji/resolveEmoji.spec.ts` — Unit tests for resolution logic.
20
+ - `src/emoji/emojiService.ts` — `fetchEmojis(names: string[]): Promise<EmojiCache>` abstraction. Calls Icon Registry, returns map of shortcode → `IconDefinition`.
21
+ - `src/emoji/emojiService.spec.ts` — Unit tests with mocked fetch.
22
+ - `src/emoji/types.ts` — Shared types: `EmojiResolution`, `EmojiCache`.
23
+ - `tests/emoji-participant.spec.ts` — Playwright E2E for emoji on participants.
24
+ - `tests/emoji-messages.spec.ts` — Playwright E2E for emoji in messages/conditions.
25
+
26
+ ### Modified files
27
+ - `src/g4/sequenceLexer.g4` — Add `LBRACKET`, `RBRACKET`, `EMOJI_COLON` tokens.
28
+ - `src/g4/sequenceParser.g4` — Add `emoji` rule, update `participant` rule.
29
+ - `src/generated-parser/*` — Regenerated from grammar (via `bun antlr`).
30
+ - `src/parser/ToCollector.js` — Extract emoji from parse context in `enterParticipant()`, `enterTo()`.
31
+ - `src/parser/Participants.ts` — Add `emoji` field to `ParticipantOptions`, `Participant` class, `blankParticipant`, `ToValue()`.
32
+ - `src/components/Comment/Comment.ts` — Extend `parseLine()` to resolve emoji via resolution engine.
33
+ - `src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx` — Render emoji inline with participant name.
34
+ - `src/svg/components/participant.ts` — Render emoji in SVG participant header.
35
+ - `src/svg/renderToSvg.ts` — Accept `emojiCache` in `RenderOptions`.
36
+ - `src/svg/buildStatementGeometry.ts` — Pass emoji data through to comment/divider rendering.
37
+
38
+ ---
39
+
40
+ ## Task 1: Emoji Resolution Engine
41
+
42
+ The core logic that decides whether `[content]` is a CSS style, an emoji, or both. This is context-independent — used everywhere.
43
+
44
+ **Files:**
45
+ - Create: `src/emoji/types.ts`
46
+ - Create: `src/emoji/resolveEmoji.ts`
47
+ - Create: `src/emoji/resolveEmoji.spec.ts`
48
+
49
+ - [ ] **Step 1: Write types**
50
+
51
+ ```typescript
52
+ // src/emoji/types.ts
53
+ import type { IconDefinition } from "@/svg/icons";
54
+
55
+ export interface EmojiResolution {
56
+ /** CSS class names to add (always present) */
57
+ classNames: string[];
58
+ /** CSS style properties (from getStyle match) */
59
+ style: Record<string, string>;
60
+ /** Emoji shortcodes that resolved (for rendering) */
61
+ emojis: string[];
62
+ /** Unicode characters for fallback rendering */
63
+ unicodes: string[];
64
+ }
65
+
66
+ /** Pre-fetched emoji SVG cache: shortcode → icon definition */
67
+ export type EmojiCache = Map<string, IconDefinition & { unicode: string }>;
68
+ ```
69
+
70
+ - [ ] **Step 2: Write failing tests for resolution**
71
+
72
+ ```typescript
73
+ // src/emoji/resolveEmoji.spec.ts
74
+ import { describe, it, expect } from "vitest";
75
+ import { resolveBracketContent } from "./resolveEmoji";
76
+
77
+ describe("resolveBracketContent", () => {
78
+ // CSS-first resolution
79
+ it("resolves [red] as CSS color", () => {
80
+ const result = resolveBracketContent("red");
81
+ expect(result.style).toEqual({ color: "red" });
82
+ expect(result.emojis).toEqual([]);
83
+ expect(result.classNames).toContain("red");
84
+ });
85
+
86
+ it("resolves [bold] as CSS font-weight", () => {
87
+ const result = resolveBracketContent("bold");
88
+ expect(result.style).toEqual({ fontWeight: "bold" });
89
+ expect(result.emojis).toEqual([]);
90
+ });
91
+
92
+ // Emoji fallback
93
+ it("resolves [rocket] as emoji when no CSS match", () => {
94
+ const result = resolveBracketContent("rocket");
95
+ expect(result.style).toEqual({});
96
+ expect(result.emojis).toEqual(["rocket"]);
97
+ expect(result.classNames).toContain("rocket");
98
+ });
99
+
100
+ // Colon override
101
+ it("resolves [:red:] as emoji, skipping CSS", () => {
102
+ const result = resolveBracketContent(":red:");
103
+ expect(result.style).toEqual({});
104
+ expect(result.emojis).toEqual(["red"]);
105
+ expect(result.classNames).toContain("red");
106
+ });
107
+
108
+ it("resolves [:rocket:] as emoji", () => {
109
+ const result = resolveBracketContent(":rocket:");
110
+ expect(result.emojis).toEqual(["rocket"]);
111
+ expect(result.classNames).toContain("rocket");
112
+ });
113
+
114
+ // Comma-separated
115
+ it("resolves [red, bold] as multi-style", () => {
116
+ const result = resolveBracketContent("red, bold");
117
+ expect(result.style).toEqual({ color: "red", fontWeight: "bold" });
118
+ expect(result.emojis).toEqual([]);
119
+ });
120
+
121
+ it("resolves [rocket, red] as emoji + CSS", () => {
122
+ const result = resolveBracketContent("rocket, red");
123
+ expect(result.emojis).toEqual(["rocket"]);
124
+ expect(result.style).toEqual({ color: "red" });
125
+ expect(result.classNames).toContain("rocket");
126
+ expect(result.classNames).toContain("red");
127
+ });
128
+
129
+ it("resolves [rocket, fire] as two emoji", () => {
130
+ const result = resolveBracketContent("rocket, fire");
131
+ expect(result.emojis).toEqual(["rocket", "fire"]);
132
+ });
133
+
134
+ // Unknown
135
+ it("resolves [unknown] as class only", () => {
136
+ const result = resolveBracketContent("unknown");
137
+ expect(result.style).toEqual({});
138
+ expect(result.emojis).toEqual([]);
139
+ expect(result.classNames).toContain("unknown");
140
+ });
141
+
142
+ // Tailwind class (hyphenated)
143
+ it("resolves [text-red-500] as class only", () => {
144
+ const result = resolveBracketContent("text-red-500");
145
+ expect(result.emojis).toEqual([]);
146
+ expect(result.classNames).toContain("text-red-500");
147
+ });
148
+ });
149
+ ```
150
+
151
+ - [ ] **Step 3: Run tests to verify they fail**
152
+
153
+ Run: `bun run test -- src/emoji/resolveEmoji.spec.ts`
154
+ Expected: FAIL — module not found
155
+
156
+ - [ ] **Step 4: Implement resolution engine**
157
+
158
+ ```typescript
159
+ // src/emoji/resolveEmoji.ts
160
+ import { getStyle } from "@/utils/messageStyling";
161
+ import type { EmojiResolution } from "./types";
162
+
163
+ // GitHub emoji shortcode list — populated by the emoji service at runtime.
164
+ // For resolution we only need to know IF a name is an emoji, not the SVG.
165
+ // This set is populated on first fetch and cached.
166
+ let knownEmojis: Set<string> = new Set();
167
+
168
+ export function setKnownEmojis(names: Iterable<string>) {
169
+ knownEmojis = new Set(names);
170
+ }
171
+
172
+ /**
173
+ * Determine if a bare name (without colons) could be an emoji shortcode.
174
+ * Known emoji names match. Names with hyphens are assumed to be CSS classes.
175
+ */
176
+ function isEmojiCandidate(name: string): boolean {
177
+ if (name.includes("-")) return false; // Tailwind-style class
178
+ return knownEmojis.has(name);
179
+ }
180
+
181
+ /**
182
+ * Resolve the content inside [...] into CSS styles, emoji, and class names.
183
+ *
184
+ * Rules:
185
+ * - `[:name:]` (colon-wrapped) → always emoji, skip CSS
186
+ * - `[name]` → try CSS first via getStyle(); if no match AND name is known emoji → emoji
187
+ * - Comma-separated values are resolved independently
188
+ * - All values are added as CSS class names regardless of resolution
189
+ */
190
+ export function resolveBracketContent(raw: string): EmojiResolution {
191
+ const result: EmojiResolution = {
192
+ classNames: [],
193
+ style: {},
194
+ emojis: [],
195
+ unicodes: [],
196
+ };
197
+
198
+ const values = raw.split(",").map((s) => s.trim()).filter(Boolean);
199
+
200
+ for (const value of values) {
201
+ // Check for colon override: :name:
202
+ const colonMatch = value.match(/^:(.+):$/);
203
+ if (colonMatch) {
204
+ const name = colonMatch[1];
205
+ result.classNames.push(name);
206
+ result.emojis.push(name);
207
+ continue;
208
+ }
209
+
210
+ // Always add as class
211
+ result.classNames.push(value);
212
+
213
+ // Try CSS first
214
+ const { textStyle } = getStyle([value]);
215
+ if (Object.keys(textStyle).length > 0) {
216
+ Object.assign(result.style, textStyle);
217
+ continue;
218
+ }
219
+
220
+ // Try emoji
221
+ if (isEmojiCandidate(value)) {
222
+ result.emojis.push(value);
223
+ }
224
+ // else: class only, no visual effect
225
+ }
226
+
227
+ return result;
228
+ }
229
+ ```
230
+
231
+ - [ ] **Step 5: Run tests to verify they pass**
232
+
233
+ Run: `bun run test -- src/emoji/resolveEmoji.spec.ts`
234
+ Expected: Most tests PASS. Tests using `knownEmojis` need the set to be populated first. Update the test file to call `setKnownEmojis()` in a `beforeEach`:
235
+
236
+ ```typescript
237
+ import { setKnownEmojis } from "./resolveEmoji";
238
+
239
+ beforeEach(() => {
240
+ setKnownEmojis(["rocket", "fire", "check", "red", "eyes", "warning"]);
241
+ });
242
+ ```
243
+
244
+ Run: `bun run test -- src/emoji/resolveEmoji.spec.ts`
245
+ Expected: ALL PASS
246
+
247
+ - [ ] **Step 6: Commit**
248
+
249
+ ```bash
250
+ git add src/emoji/types.ts src/emoji/resolveEmoji.ts src/emoji/resolveEmoji.spec.ts
251
+ git commit -m "feat: add emoji resolution engine with CSS-first fallback"
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Task 2: Emoji Service Abstraction
257
+
258
+ The async layer that fetches emoji SVG data from the Icon Registry (or a stub).
259
+
260
+ **Files:**
261
+ - Create: `src/emoji/emojiService.ts`
262
+ - Create: `src/emoji/emojiService.spec.ts`
263
+
264
+ - [ ] **Step 1: Write failing tests**
265
+
266
+ ```typescript
267
+ // src/emoji/emojiService.spec.ts
268
+ import { describe, it, expect, vi, beforeEach } from "vitest";
269
+ import { fetchEmojis, setEmojiServiceUrl } from "./emojiService";
270
+
271
+ describe("fetchEmojis", () => {
272
+ beforeEach(() => {
273
+ vi.restoreAllMocks();
274
+ });
275
+
276
+ it("returns empty map for empty input", async () => {
277
+ const cache = await fetchEmojis([]);
278
+ expect(cache.size).toBe(0);
279
+ });
280
+
281
+ it("fetches emoji from service and returns cache", async () => {
282
+ const mockResponse = {
283
+ rocket: { viewBox: "0 0 36 36", content: "<path/>", unicode: "🚀" },
284
+ fire: { viewBox: "0 0 36 36", content: "<path/>", unicode: "🔥" },
285
+ };
286
+ vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
287
+ ok: true,
288
+ json: () => Promise.resolve(mockResponse),
289
+ } as Response);
290
+
291
+ const cache = await fetchEmojis(["rocket", "fire"]);
292
+ expect(cache.get("rocket")?.unicode).toBe("🚀");
293
+ expect(cache.get("fire")?.content).toBe("<path/>");
294
+ });
295
+
296
+ it("returns empty map on fetch failure (fallback)", async () => {
297
+ vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("offline"));
298
+ const cache = await fetchEmojis(["rocket"]);
299
+ expect(cache.size).toBe(0);
300
+ });
301
+
302
+ it("deduplicates shortcode names", async () => {
303
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
304
+ ok: true,
305
+ json: () => Promise.resolve({ rocket: { viewBox: "0 0 36 36", content: "<path/>", unicode: "🚀" } }),
306
+ } as Response);
307
+
308
+ await fetchEmojis(["rocket", "rocket", "rocket"]);
309
+ const url = fetchSpy.mock.calls[0][0] as string;
310
+ expect(url).toContain("emoji=rocket");
311
+ expect(url).not.toContain("emoji=rocket%2Crocket");
312
+ });
313
+ });
314
+ ```
315
+
316
+ - [ ] **Step 2: Run tests to verify they fail**
317
+
318
+ Run: `bun run test -- src/emoji/emojiService.spec.ts`
319
+ Expected: FAIL — module not found
320
+
321
+ - [ ] **Step 3: Implement emoji service**
322
+
323
+ ```typescript
324
+ // src/emoji/emojiService.ts
325
+ import type { EmojiCache } from "./types";
326
+ import type { IconDefinition } from "@/svg/icons";
327
+ import { setKnownEmojis } from "./resolveEmoji";
328
+
329
+ const DEFAULT_SERVICE_URL = "https://icons.zenuml.com";
330
+ let serviceUrl = DEFAULT_SERVICE_URL;
331
+
332
+ export function setEmojiServiceUrl(url: string) {
333
+ serviceUrl = url;
334
+ }
335
+
336
+ /** In-memory cache across renders */
337
+ const memoryCache: EmojiCache = new Map();
338
+
339
+ /**
340
+ * Fetch emoji SVG fragments from the Icon Registry service.
341
+ * Returns a cache map of shortcode → IconDefinition + unicode.
342
+ * On failure, returns empty map (callers fall back to native emoji or text).
343
+ */
344
+ export async function fetchEmojis(names: string[]): Promise<EmojiCache> {
345
+ const unique = [...new Set(names)];
346
+
347
+ // Return cached entries if all are already known
348
+ const uncached = unique.filter((n) => !memoryCache.has(n));
349
+ if (uncached.length === 0) {
350
+ return memoryCache;
351
+ }
352
+
353
+ try {
354
+ const url = `${serviceUrl}/batch?emoji=${encodeURIComponent(uncached.join(","))}`;
355
+ const response = await fetch(url);
356
+ if (!response.ok) return memoryCache;
357
+
358
+ const data: Record<string, IconDefinition & { unicode: string }> =
359
+ await response.json();
360
+
361
+ for (const [name, entry] of Object.entries(data)) {
362
+ memoryCache.set(name, entry);
363
+ }
364
+
365
+ // Update known emoji set for resolution engine
366
+ setKnownEmojis(memoryCache.keys());
367
+ } catch {
368
+ // Network failure — return whatever we have cached
369
+ }
370
+
371
+ return memoryCache;
372
+ }
373
+ ```
374
+
375
+ - [ ] **Step 4: Run tests to verify they pass**
376
+
377
+ Run: `bun run test -- src/emoji/emojiService.spec.ts`
378
+ Expected: ALL PASS
379
+
380
+ - [ ] **Step 5: Commit**
381
+
382
+ ```bash
383
+ git add src/emoji/emojiService.ts src/emoji/emojiService.spec.ts
384
+ git commit -m "feat: add emoji service abstraction with fetch and caching"
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Task 3: ANTLR Grammar — Add Emoji Tokens and Rules
390
+
391
+ Add `[shortcode]` parsing to the ANTLR grammar so the parser can extract emoji from participant declarations.
392
+
393
+ **Files:**
394
+ - Modify: `src/g4/sequenceLexer.g4`
395
+ - Modify: `src/g4/sequenceParser.g4`
396
+ - Regenerate: `src/generated-parser/*`
397
+
398
+ - [ ] **Step 1: Add lexer tokens**
399
+
400
+ In `src/g4/sequenceLexer.g4`, add after the `COLOR` rule (line 74):
401
+
402
+ ```antlr
403
+ LBRACKET
404
+ : '['
405
+ ;
406
+
407
+ RBRACKET
408
+ : ']'
409
+ ;
410
+ ```
411
+
412
+ - [ ] **Step 2: Add parser rule for emoji**
413
+
414
+ In `src/g4/sequenceParser.g4`, add after the `width` rule (line 66):
415
+
416
+ ```antlr
417
+ emoji
418
+ : LBRACKET name RBRACKET
419
+ ;
420
+ ```
421
+
422
+ - [ ] **Step 3: Update participant, from, and to rules**
423
+
424
+ In `src/g4/sequenceParser.g4`, change the `participant` rule (line 39-43) from:
425
+
426
+ ```antlr
427
+ participant
428
+ : participantType? stereotype? name width? label? COLOR?
429
+ | stereotype
430
+ | participantType
431
+ ;
432
+ ```
433
+
434
+ to:
435
+
436
+ ```antlr
437
+ participant
438
+ : participantType? stereotype? emoji? name width? label? COLOR?
439
+ | stereotype
440
+ | participantType
441
+ ;
442
+ ```
443
+
444
+ Also update `from` (line 186-188) and `to` (line 190-192) to support inline emoji:
445
+
446
+ ```antlr
447
+ from
448
+ : emoji? name
449
+ ;
450
+
451
+ to
452
+ : emoji? name
453
+ ;
454
+ ```
455
+
456
+ This enables `CI->[rocket]Production.deploy()` — emoji attaches to participant at first usage in a message.
457
+
458
+ - [ ] **Step 4: Regenerate the parser**
459
+
460
+ Run: `bun antlr`
461
+ Expected: Parser files regenerated in `src/generated-parser/` without errors.
462
+
463
+ - [ ] **Step 5: Verify existing tests still pass**
464
+
465
+ Run: `bun run test`
466
+ Expected: ALL existing tests PASS (grammar change is additive, no existing syntax affected).
467
+
468
+ - [ ] **Step 6: Commit**
469
+
470
+ ```bash
471
+ git add src/g4/sequenceLexer.g4 src/g4/sequenceParser.g4 src/generated-parser/
472
+ git commit -m "feat: add [emoji] tokens and rule to ANTLR grammar"
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Task 4: Parser Layer — Extract Emoji from Parse Tree
478
+
479
+ Wire the new `emoji` grammar rule into the parser's data extraction.
480
+
481
+ **Files:**
482
+ - Modify: `src/parser/Participants.ts`
483
+ - Modify: `src/parser/ToCollector.js`
484
+ - Create: `src/parser/EmojiParser.spec.ts`
485
+
486
+ - [ ] **Step 1: Write failing parser test**
487
+
488
+ ```typescript
489
+ // src/parser/EmojiParser.spec.ts
490
+ import { describe, it, expect } from "vitest";
491
+ import { RootContext, Participants } from "@/parser";
492
+
493
+ describe("Emoji in participant declarations", () => {
494
+ it("parses [rocket] as emoji decorator on participant", () => {
495
+ const ctx = RootContext("[rocket] Production");
496
+ const participants = Participants(ctx);
497
+ const prod = participants.find((p: any) => p.name === "Production");
498
+ expect(prod.emoji).toBe("rocket");
499
+ });
500
+
501
+ it("parses participant without emoji", () => {
502
+ const ctx = RootContext("Production");
503
+ const participants = Participants(ctx);
504
+ const prod = participants.find((p: any) => p.name === "Production");
505
+ expect(prod.emoji).toBeUndefined();
506
+ });
507
+
508
+ it("parses emoji with @Type", () => {
509
+ const ctx = RootContext("@Database [fire] HotDB");
510
+ const participants = Participants(ctx);
511
+ const db = participants.find((p: any) => p.name === "HotDB");
512
+ expect(db.type).toBe("Database");
513
+ expect(db.emoji).toBe("fire");
514
+ });
515
+
516
+ it("parses emoji with stereotype", () => {
517
+ const ctx = RootContext('<<service>> [lock] Auth');
518
+ const participants = Participants(ctx);
519
+ const auth = participants.find((p: any) => p.name === "Auth");
520
+ expect(auth.stereotype).toBe("service");
521
+ expect(auth.emoji).toBe("lock");
522
+ });
523
+
524
+ it("parses inline emoji on first usage in message", () => {
525
+ const ctx = RootContext("A->[rocket]B.call()");
526
+ const participants = Participants(ctx);
527
+ const b = participants.find((p: any) => p.name === "B");
528
+ expect(b.emoji).toBe("rocket");
529
+ });
530
+
531
+ it("first emoji wins when declared and used inline", () => {
532
+ const ctx = RootContext("[fire] B\nA->[rocket]B.call()");
533
+ const participants = Participants(ctx);
534
+ const b = participants.find((p: any) => p.name === "B");
535
+ expect(b.emoji).toBe("fire"); // header declaration wins via ||=
536
+ });
537
+ });
538
+ ```
539
+
540
+ - [ ] **Step 2: Run tests to verify they fail**
541
+
542
+ Run: `bun run test -- src/parser/EmojiParser.spec.ts`
543
+ Expected: FAIL — `emoji` property is undefined
544
+
545
+ - [ ] **Step 3: Add emoji to ParticipantOptions and Participant class**
546
+
547
+ In `src/parser/Participants.ts`:
548
+
549
+ Add `emoji?: string;` to `ParticipantOptions` (after line 12):
550
+
551
+ ```typescript
552
+ interface ParticipantOptions {
553
+ isStarter?: boolean;
554
+ stereotype?: string;
555
+ width?: number;
556
+ groupId?: number | string;
557
+ label?: string;
558
+ explicit?: boolean;
559
+ type?: string;
560
+ color?: string;
561
+ comment?: string;
562
+ assignee?: string;
563
+ emoji?: string;
564
+ position?: Position;
565
+ assigneePosition?: Position;
566
+ }
567
+ ```
568
+
569
+ Add `emoji: undefined,` to `blankParticipant` (after line 29).
570
+
571
+ Add `private emoji: string | undefined;` field to `Participant` class (after line 45).
572
+
573
+ In `mergeOptions()` (after line 76), add:
574
+ ```typescript
575
+ this.emoji ||= options.emoji;
576
+ ```
577
+
578
+ In `ToValue()` (after line 94), add `emoji: this.emoji,` to the returned object.
579
+
580
+ - [ ] **Step 4: Extract emoji in ToCollector.js**
581
+
582
+ In `src/parser/ToCollector.js`, in the `onParticipant` function (after line 26), add:
583
+
584
+ ```javascript
585
+ const emoji = ctx.emoji?.()?.name?.()?.getFormattedText();
586
+ ```
587
+
588
+ Then add `emoji,` to the `participants.Add()` call (after line 50):
589
+
590
+ ```javascript
591
+ participants.Add(participant, {
592
+ isStarter: false,
593
+ type,
594
+ stereotype,
595
+ width,
596
+ groupId,
597
+ label,
598
+ explicit,
599
+ color,
600
+ comment,
601
+ emoji,
602
+ position: [start, end],
603
+ });
604
+ ```
605
+
606
+ Also update the `onTo` function (line 58) to extract emoji from `from`/`to` contexts:
607
+
608
+ ```javascript
609
+ const onTo = function (ctx) {
610
+ if (isBlind) return;
611
+ let participant = ctx.name?.()?.getFormattedText() || ctx.getFormattedText();
612
+ const emoji = ctx.emoji?.()?.name?.()?.getFormattedText();
613
+ // ... existing participant extraction logic
614
+ participants.Add(participant, { emoji });
615
+ };
616
+ ```
617
+
618
+ The `mergeOptions` in `Participant.ts` uses `||=`, so the first emoji wins — if declared in header and used inline, the header emoji takes precedence.
619
+
620
+ - [ ] **Step 5: Run tests to verify they pass**
621
+
622
+ Run: `bun run test -- src/parser/EmojiParser.spec.ts`
623
+ Expected: ALL PASS
624
+
625
+ - [ ] **Step 6: Run all tests to check for regressions**
626
+
627
+ Run: `bun run test`
628
+ Expected: ALL PASS
629
+
630
+ - [ ] **Step 7: Commit**
631
+
632
+ ```bash
633
+ git add src/parser/Participants.ts src/parser/ToCollector.js src/parser/EmojiParser.spec.ts
634
+ git commit -m "feat: extract emoji from participant parse tree"
635
+ ```
636
+
637
+ ---
638
+
639
+ ## Task 5: HTML Renderer — Emoji on Participants
640
+
641
+ Render emoji inline with participant name in the React component.
642
+
643
+ **Files:**
644
+ - Modify: `src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx`
645
+ - Create: `src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ParticipantEmoji.spec.tsx`
646
+
647
+ - [ ] **Step 1: Write failing component test**
648
+
649
+ ```typescript
650
+ // src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ParticipantEmoji.spec.tsx
651
+ import { describe, it, expect } from "vitest";
652
+ import { render, screen } from "@testing-library/react";
653
+ import { Provider, createStore } from "jotai";
654
+ import { Participant } from "./Participant";
655
+
656
+ describe("Participant emoji rendering", () => {
657
+ it("renders emoji before participant name", () => {
658
+ const store = createStore();
659
+ const entity = {
660
+ name: "Production",
661
+ emoji: "rocket",
662
+ type: "",
663
+ stereotype: "",
664
+ color: "",
665
+ label: "Production",
666
+ assignee: "",
667
+ };
668
+
669
+ render(
670
+ <Provider store={store}>
671
+ <Participant entity={entity} />
672
+ </Provider>,
673
+ );
674
+
675
+ const emojiEl = screen.getByText("🚀");
676
+ expect(emojiEl).toBeDefined();
677
+ });
678
+
679
+ it("does not render emoji when not present", () => {
680
+ const store = createStore();
681
+ const entity = {
682
+ name: "Production",
683
+ emoji: "",
684
+ type: "",
685
+ stereotype: "",
686
+ color: "",
687
+ label: "Production",
688
+ assignee: "",
689
+ };
690
+
691
+ render(
692
+ <Provider store={store}>
693
+ <Participant entity={entity} />
694
+ </Provider>,
695
+ );
696
+
697
+ expect(screen.queryByTestId("participant-emoji")).toBeNull();
698
+ });
699
+ });
700
+ ```
701
+
702
+ Note: This test uses the emoji's unicode character directly. In the real implementation, the emoji cache would provide the unicode. For the HTML renderer, native emoji text is the simplest approach — the SVG sprite rendering is for the SVG renderer.
703
+
704
+ - [ ] **Step 2: Run test to verify it fails**
705
+
706
+ Run: `bun run test -- src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ParticipantEmoji.spec.tsx`
707
+ Expected: FAIL — emoji element not found
708
+
709
+ - [ ] **Step 3: Modify Participant.tsx to render emoji**
710
+
711
+ In `src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx`, find the label section (around the `ParticipantLabel` component). Add emoji rendering before the label:
712
+
713
+ Inside the `<div className="h-5 group flex flex-col justify-center">` block, modify the `ParticipantLabel` section to include emoji. Wrap the label area in a flex container:
714
+
715
+ ```tsx
716
+ <div className="flex items-center">
717
+ {props.entity.emoji && (
718
+ <span data-testid="participant-emoji" className="mr-1">
719
+ {/* Unicode emoji from cache, or shortcode as fallback */}
720
+ {getEmojiUnicode(props.entity.emoji)}
721
+ </span>
722
+ )}
723
+ <ParticipantLabel
724
+ labelText={props.entity.label || props.entity.name}
725
+ // ... existing props
726
+ />
727
+ </div>
728
+ ```
729
+
730
+ Add a helper at the top of the file:
731
+
732
+ ```typescript
733
+ function getEmojiUnicode(shortcode: string): string {
734
+ // TODO: In Task 8, this will look up the emoji cache.
735
+ // For now, return the shortcode wrapped in brackets as placeholder.
736
+ return shortcode;
737
+ }
738
+ ```
739
+
740
+ - [ ] **Step 4: Run test to verify it passes**
741
+
742
+ Run: `bun run test -- src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ParticipantEmoji.spec.tsx`
743
+ Expected: PASS (after adjusting test expectations to match actual implementation)
744
+
745
+ - [ ] **Step 5: Run all tests**
746
+
747
+ Run: `bun run test`
748
+ Expected: ALL PASS
749
+
750
+ - [ ] **Step 6: Commit**
751
+
752
+ ```bash
753
+ git add src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ParticipantEmoji.spec.tsx
754
+ git commit -m "feat: render emoji inline with participant name in HTML renderer"
755
+ ```
756
+
757
+ ---
758
+
759
+ ## Task 6: SVG Renderer — Emoji on Participants
760
+
761
+ Render emoji as inline text or SVG `<g>` fragment in the SVG participant header.
762
+
763
+ **Files:**
764
+ - Modify: `src/svg/components/participant.ts`
765
+ - Modify: `src/svg/renderToSvg.ts`
766
+ - Create: `src/svg/components/participantEmoji.spec.ts`
767
+
768
+ - [ ] **Step 1: Write failing SVG test**
769
+
770
+ ```typescript
771
+ // src/svg/components/participantEmoji.spec.ts
772
+ import { describe, it, expect } from "vitest";
773
+ import { renderToSvg } from "../renderToSvg";
774
+
775
+ describe("SVG emoji on participants", () => {
776
+ it("renders emoji unicode text before participant name", () => {
777
+ const result = renderToSvg("[rocket] Production");
778
+ expect(result.svg).toContain("🚀");
779
+ expect(result.svg).toContain("Production");
780
+ });
781
+
782
+ it("renders participant without emoji normally", () => {
783
+ const result = renderToSvg("Production");
784
+ expect(result.svg).toContain("Production");
785
+ expect(result.svg).not.toContain("🚀");
786
+ });
787
+
788
+ it("renders emoji with @Type icon", () => {
789
+ const result = renderToSvg("@Database [fire] HotDB");
790
+ expect(result.svg).toContain("HotDB");
791
+ // Both icon and emoji should be present
792
+ expect(result.svg).toContain("participant-icon"); // @Database icon
793
+ });
794
+ });
795
+ ```
796
+
797
+ - [ ] **Step 2: Run test to verify it fails**
798
+
799
+ Run: `bun run test -- src/svg/components/participantEmoji.spec.ts`
800
+ Expected: FAIL — emoji not found in SVG output
801
+
802
+ - [ ] **Step 3: Add emojiCache to RenderOptions**
803
+
804
+ In `src/svg/renderToSvg.ts`, extend `RenderOptions`:
805
+
806
+ ```typescript
807
+ export interface RenderOptions {
808
+ theme?: "theme-default" | "theme-mermaid";
809
+ emojiCache?: EmojiCache;
810
+ }
811
+ ```
812
+
813
+ Add import: `import type { EmojiCache } from "@/emoji/types";`
814
+
815
+ - [ ] **Step 4: Modify SVG participant renderer**
816
+
817
+ In `src/svg/components/participant.ts`, find the label `<text>` element rendering. Add emoji text before the participant name:
818
+
819
+ ```typescript
820
+ // Before the existing label text rendering
821
+ const emojiText = p.emoji
822
+ ? `<text class="emoji" x="${labelX - emojiWidth}" y="${labelY}" ...>${esc(getEmojiUnicode(p.emoji, emojiCache))}</text>`
823
+ : "";
824
+ ```
825
+
826
+ Where `getEmojiUnicode` checks the cache for a unicode character, falling back to the shortcode name.
827
+
828
+ - [ ] **Step 5: Run tests**
829
+
830
+ Run: `bun run test -- src/svg/components/participantEmoji.spec.ts`
831
+ Expected: PASS
832
+
833
+ - [ ] **Step 6: Run all tests**
834
+
835
+ Run: `bun run test`
836
+ Expected: ALL PASS
837
+
838
+ - [ ] **Step 7: Commit**
839
+
840
+ ```bash
841
+ git add src/svg/components/participant.ts src/svg/renderToSvg.ts src/svg/components/participantEmoji.spec.ts
842
+ git commit -m "feat: render emoji in SVG participant headers"
843
+ ```
844
+
845
+ ---
846
+
847
+ ## Task 7: Comment and Divider Emoji Resolution
848
+
849
+ Extend the existing `[bracket]` handling in comments and dividers to resolve emoji alongside CSS styles.
850
+
851
+ **Files:**
852
+ - Modify: `src/components/Comment/Comment.ts`
853
+ - Modify: `src/components/Comment/Comment.spec.ts`
854
+
855
+ - [ ] **Step 1: Write failing tests**
856
+
857
+ Add to `src/components/Comment/Comment.spec.ts`:
858
+
859
+ ```typescript
860
+ describe("emoji in comments", () => {
861
+ it("resolves [rocket] as emoji in comment", () => {
862
+ const comment = new Comment("[rocket] deploy note\n");
863
+ expect(comment.emojis).toContain("rocket");
864
+ expect(comment.text).toBe("deploy note");
865
+ });
866
+
867
+ it("resolves [rocket, red] as emoji + CSS in comment", () => {
868
+ const comment = new Comment("[rocket, red] alert\n");
869
+ expect(comment.emojis).toContain("rocket");
870
+ expect(comment.commentStyle).toEqual({ color: "red" });
871
+ });
872
+
873
+ it("preserves existing [red] behavior", () => {
874
+ const comment = new Comment("[red] important\n");
875
+ expect(comment.commentStyle).toEqual({ color: "red" });
876
+ expect(comment.emojis || []).toEqual([]);
877
+ });
878
+
879
+ it("resolves [:red:] as emoji via colon override", () => {
880
+ const comment = new Comment("[:red:] note\n");
881
+ expect(comment.emojis).toContain("red");
882
+ });
883
+ });
884
+ ```
885
+
886
+ - [ ] **Step 2: Run tests to verify they fail**
887
+
888
+ Run: `bun run test -- src/components/Comment/Comment.spec.ts`
889
+ Expected: FAIL — `emojis` property does not exist
890
+
891
+ - [ ] **Step 3: Integrate resolution engine into Comment.ts**
892
+
893
+ In `src/components/Comment/Comment.ts`, import and use `resolveBracketContent`:
894
+
895
+ ```typescript
896
+ import { resolveBracketContent } from "@/emoji/resolveEmoji";
897
+ ```
898
+
899
+ In the `parseLine()` function, where `[...]` content is processed, call `resolveBracketContent()` instead of directly adding to the style set. Store resolved emojis on the Comment instance.
900
+
901
+ - [ ] **Step 4: Run tests**
902
+
903
+ Run: `bun run test -- src/components/Comment/Comment.spec.ts`
904
+ Expected: ALL PASS (including existing tests)
905
+
906
+ - [ ] **Step 5: Commit**
907
+
908
+ ```bash
909
+ git add src/components/Comment/Comment.ts src/components/Comment/Comment.spec.ts
910
+ git commit -m "feat: resolve emoji in comments and dividers via resolution engine"
911
+ ```
912
+
913
+ ---
914
+
915
+ ## Task 8: Wire Emoji Cache to Renderers
916
+
917
+ Connect the emoji service to both HTML and SVG render paths so emoji shortcodes resolve to actual unicode/SVG.
918
+
919
+ **Files:**
920
+ - Modify: `src/core.tsx` — fetch emoji before HTML render
921
+ - Modify: `src/svg/renderToSvg.ts` — thread emojiCache through rendering
922
+ - Modify: `src/store/Store.ts` — add emojiCacheAtom for React components
923
+
924
+ - [ ] **Step 1: Add emojiCacheAtom to store**
925
+
926
+ In `src/store/Store.ts`:
927
+
928
+ ```typescript
929
+ import type { EmojiCache } from "@/emoji/types";
930
+
931
+ export const emojiCacheAtom = atom<EmojiCache>(new Map());
932
+ ```
933
+
934
+ - [ ] **Step 2: Fetch emoji in core.render()**
935
+
936
+ In `src/core.tsx`, in the `doRender()` method (or `render()`), add emoji fetching before rendering:
937
+
938
+ ```typescript
939
+ import { fetchEmojis } from "@/emoji/emojiService";
940
+ import { emojiCacheAtom } from "@/store/Store";
941
+
942
+ // In render() or doRender():
943
+ // 1. Parse to extract emoji shortcodes
944
+ // 2. Fetch from service
945
+ // 3. Set cache atom
946
+ // 4. Render (existing logic)
947
+ ```
948
+
949
+ - [ ] **Step 3: Update Participant.tsx to use cache**
950
+
951
+ Replace the placeholder `getEmojiUnicode()` from Task 5 with actual cache lookup:
952
+
953
+ ```typescript
954
+ import { useAtomValue } from "jotai";
955
+ import { emojiCacheAtom } from "@/store/Store";
956
+
957
+ // Inside component:
958
+ const emojiCache = useAtomValue(emojiCacheAtom);
959
+
960
+ function getEmojiUnicode(shortcode: string): string {
961
+ const entry = emojiCache.get(shortcode);
962
+ return entry?.unicode || shortcode;
963
+ }
964
+ ```
965
+
966
+ - [ ] **Step 4: Thread emojiCache through SVG renderer**
967
+
968
+ In `src/svg/renderToSvg.ts`, pass `options.emojiCache` into the geometry/component builders so `participant.ts` can access it when rendering.
969
+
970
+ - [ ] **Step 5: Run all tests**
971
+
972
+ Run: `bun run test`
973
+ Expected: ALL PASS
974
+
975
+ - [ ] **Step 6: Commit**
976
+
977
+ ```bash
978
+ git add src/core.tsx src/store/Store.ts src/svg/renderToSvg.ts src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx
979
+ git commit -m "feat: wire emoji cache to HTML and SVG renderers"
980
+ ```
981
+
982
+ ---
983
+
984
+ ## Task 9: Emoji in Message Content and Conditions
985
+
986
+ Enable `[shortcode]` resolution in async message text and fragment conditions (alt/loop/etc).
987
+
988
+ **Files:**
989
+ - Modify: `src/components/DiagramFrame/SeqDiagram/MessageLayer/MessageLabel.tsx` (or equivalent)
990
+ - Modify: `src/svg/buildStatementGeometry.ts`
991
+ - Create: `src/emoji/emojiInText.spec.ts`
992
+
993
+ - [ ] **Step 1: Write failing tests**
994
+
995
+ ```typescript
996
+ // src/emoji/emojiInText.spec.ts
997
+ import { describe, it, expect } from "vitest";
998
+ import { resolveEmojiInText } from "./resolveEmoji";
999
+
1000
+ describe("resolveEmojiInText", () => {
1001
+ it("replaces [rocket] with emoji unicode in text", () => {
1002
+ const result = resolveEmojiInText("[rocket] launching", knownEmojis);
1003
+ expect(result.text).toBe("🚀 launching");
1004
+ expect(result.classNames).toContain("rocket");
1005
+ });
1006
+
1007
+ it("replaces multiple [shortcodes] in text", () => {
1008
+ const result = resolveEmojiInText("[check] step 1 [fire] step 2", knownEmojis);
1009
+ expect(result.text).toContain("✅");
1010
+ expect(result.text).toContain("🔥");
1011
+ });
1012
+
1013
+ it("leaves text without brackets unchanged", () => {
1014
+ const result = resolveEmojiInText("plain message", knownEmojis);
1015
+ expect(result.text).toBe("plain message");
1016
+ });
1017
+ });
1018
+ ```
1019
+
1020
+ - [ ] **Step 2: Run tests to verify they fail**
1021
+
1022
+ Run: `bun run test -- src/emoji/emojiInText.spec.ts`
1023
+ Expected: FAIL
1024
+
1025
+ - [ ] **Step 3: Add text resolution function**
1026
+
1027
+ In `src/emoji/resolveEmoji.ts`, add a function for inline text resolution:
1028
+
1029
+ ```typescript
1030
+ /**
1031
+ * Resolve [shortcode] patterns within free text (messages, conditions).
1032
+ * Replaces each [shortcode] with its emoji unicode character.
1033
+ * Returns resolved text and accumulated class names.
1034
+ */
1035
+ export function resolveEmojiInText(
1036
+ text: string,
1037
+ emojiCache: EmojiCache
1038
+ ): { text: string; classNames: string[] } {
1039
+ const classNames: string[] = [];
1040
+ const resolved = text.replace(/\[([^\]]+)\]/g, (match, content) => {
1041
+ const resolution = resolveBracketContent(content);
1042
+ classNames.push(...resolution.classNames);
1043
+ if (resolution.emojis.length > 0) {
1044
+ return resolution.emojis
1045
+ .map((name) => emojiCache.get(name)?.unicode || `[${name}]`)
1046
+ .join("");
1047
+ }
1048
+ return match; // Not an emoji — leave bracket text as-is
1049
+ });
1050
+ return { text: resolved, classNames };
1051
+ }
1052
+ ```
1053
+
1054
+ - [ ] **Step 4: Run tests**
1055
+
1056
+ Run: `bun run test -- src/emoji/emojiInText.spec.ts`
1057
+ Expected: PASS
1058
+
1059
+ - [ ] **Step 5: Integrate into message and condition rendering**
1060
+
1061
+ Update `MessageLabel.tsx` and the SVG message renderer to call `resolveEmojiInText()` on message content before displaying.
1062
+
1063
+ Update fragment condition rendering (alt, loop, etc.) to do the same.
1064
+
1065
+ - [ ] **Step 6: Run all tests**
1066
+
1067
+ Run: `bun run test`
1068
+ Expected: ALL PASS
1069
+
1070
+ - [ ] **Step 7: Commit**
1071
+
1072
+ ```bash
1073
+ git add src/emoji/resolveEmoji.ts src/emoji/emojiInText.spec.ts src/components/ src/svg/
1074
+ git commit -m "feat: resolve emoji in message content and fragment conditions"
1075
+ ```
1076
+
1077
+ ---
1078
+
1079
+ ## Task 10: Playwright E2E Tests
1080
+
1081
+ Visual snapshot tests for emoji rendering in the full diagram.
1082
+
1083
+ **Files:**
1084
+ - Create: `tests/emoji-participant.spec.ts`
1085
+ - Create: `tests/emoji-messages.spec.ts`
1086
+
1087
+ - [ ] **Step 1: Write participant emoji E2E test**
1088
+
1089
+ ```typescript
1090
+ // tests/emoji-participant.spec.ts
1091
+ import { test, expect } from "@playwright/test";
1092
+
1093
+ test.describe("Emoji on participants", () => {
1094
+ test("renders emoji inline with participant name", async ({ page }) => {
1095
+ await page.goto("http://localhost:8080");
1096
+ // Input ZenUML code with emoji participant
1097
+ await page.fill('[data-testid="code-editor"]', "[rocket] Production\nA->Production.deploy()");
1098
+ // Wait for diagram render
1099
+ await page.waitForSelector(".participant");
1100
+ // Visual snapshot
1101
+ await expect(page.locator(".zenuml")).toHaveScreenshot("emoji-participant.png");
1102
+ });
1103
+
1104
+ test("renders emoji with @Type icon", async ({ page }) => {
1105
+ await page.goto("http://localhost:8080");
1106
+ await page.fill('[data-testid="code-editor"]', "@Database [fire] HotDB\nA->HotDB.query()");
1107
+ await page.waitForSelector(".participant");
1108
+ await expect(page.locator(".zenuml")).toHaveScreenshot("emoji-with-type.png");
1109
+ });
1110
+ });
1111
+ ```
1112
+
1113
+ - [ ] **Step 2: Write message emoji E2E test**
1114
+
1115
+ ```typescript
1116
+ // tests/emoji-messages.spec.ts
1117
+ import { test, expect } from "@playwright/test";
1118
+
1119
+ test.describe("Emoji in messages", () => {
1120
+ test("renders emoji in async message content", async ({ page }) => {
1121
+ await page.goto("http://localhost:8080");
1122
+ await page.fill('[data-testid="code-editor"]', "A->B: [rocket] launching");
1123
+ await page.waitForSelector(".message");
1124
+ await expect(page.locator(".zenuml")).toHaveScreenshot("emoji-message.png");
1125
+ });
1126
+
1127
+ test("renders emoji in alt condition", async ({ page }) => {
1128
+ await page.goto("http://localhost:8080");
1129
+ await page.fill('[data-testid="code-editor"]',
1130
+ "A->B.call()\n alt [check] success\n B-->A: ok\n else [x] failure\n B-->A: error");
1131
+ await page.waitForSelector(".fragment");
1132
+ await expect(page.locator(".zenuml")).toHaveScreenshot("emoji-alt-condition.png");
1133
+ });
1134
+ });
1135
+ ```
1136
+
1137
+ - [ ] **Step 3: Run E2E tests to generate baseline snapshots**
1138
+
1139
+ Run: `bun pw:update`
1140
+ Expected: Snapshots created. Review them visually to confirm emoji renders correctly.
1141
+
1142
+ - [ ] **Step 4: Run E2E tests to verify they pass**
1143
+
1144
+ Run: `bun pw`
1145
+ Expected: ALL PASS
1146
+
1147
+ - [ ] **Step 5: Commit**
1148
+
1149
+ ```bash
1150
+ git add tests/emoji-participant.spec.ts tests/emoji-messages.spec.ts tests/emoji-*.spec.ts-snapshots/
1151
+ git commit -m "test: add Playwright E2E tests for emoji rendering"
1152
+ ```
1153
+
1154
+ ---
1155
+
1156
+ ## Task 11: Mermaid Integration — Async Emoji Fetch in draw()
1157
+
1158
+ Update the Mermaid ZenUML renderer to fetch emoji before rendering.
1159
+
1160
+ **Files:**
1161
+ - Modify: `/Users/pengxiao/workspaces/zenuml/mermaid/packages/mermaid-zenuml/src/zenumlRenderer.ts`
1162
+
1163
+ Note: This task is in the **mermaid** repo, not the core repo. It depends on the core repo being published with emoji support.
1164
+
1165
+ - [ ] **Step 1: Make draw() truly async with emoji fetch**
1166
+
1167
+ In `zenumlRenderer.ts`, update the `draw` function:
1168
+
1169
+ ```typescript
1170
+ import { fetchEmojis } from "@zenuml/core/emoji/emojiService";
1171
+ import { extractEmojisFromCode } from "@zenuml/core/emoji/resolveEmoji";
1172
+
1173
+ export const draw = async function (text: string, id: string): Promise<void> {
1174
+ const code = text.replace(regexp, '');
1175
+
1176
+ // Extract and pre-fetch emoji
1177
+ const emojiNames = extractEmojisFromCode(code);
1178
+ const emojiCache = await fetchEmojis(emojiNames);
1179
+
1180
+ // Render with emoji cache
1181
+ const result = renderToSvg(code, { emojiCache });
1182
+
1183
+ // ... existing DOM injection logic
1184
+ };
1185
+ ```
1186
+
1187
+ - [ ] **Step 2: Test in Mermaid live editor**
1188
+
1189
+ Run the Mermaid dev server and test with:
1190
+ ```
1191
+ zenuml
1192
+ [rocket] Production
1193
+ A->Production: deploy
1194
+ ```
1195
+
1196
+ Verify emoji renders in the preview.
1197
+
1198
+ - [ ] **Step 3: Commit (in mermaid repo)**
1199
+
1200
+ ```bash
1201
+ git add packages/mermaid-zenuml/src/zenumlRenderer.ts
1202
+ git commit -m "feat: async emoji fetch in ZenUML Mermaid renderer"
1203
+ ```
1204
+
1205
+ ---
1206
+
1207
+ ## Dependency Graph
1208
+
1209
+ ```
1210
+ Task 1 (Resolution Engine) ──┐
1211
+ ├── Task 4 (Parser Layer) ─── Task 5 (HTML Renderer)
1212
+ Task 2 (Emoji Service) ──────┤ ├── Task 6 (SVG Renderer)
1213
+ ├── Task 7 (Comments) │
1214
+ Task 3 (ANTLR Grammar) ──────┘ ├── Task 8 (Wire Cache)
1215
+ ├── Task 9 (Messages/Conditions)
1216
+ ├── Task 10 (E2E Tests)
1217
+ └── Task 11 (Mermaid Integration)
1218
+ ```
1219
+
1220
+ Tasks 1, 2, 3 can be done in parallel. Tasks 4-7 depend on 1+3. Tasks 8-11 depend on earlier tasks.