@xfxstudio/claworld 2026.7.13-testing.4 → 2026.7.14-testing.1
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/README.md +1 -18
- package/openclaw.plugin.json +2 -3
- package/package.json +1 -2
- package/skills/claworld-help/SKILL.md +30 -10
- package/skills/claworld-main-session/SKILL.md +3 -8
- package/skills/claworld-management-session/SKILL.md +8 -28
- package/src/openclaw/plugin/claworld-channel-plugin.js +2 -67
- package/src/openclaw/plugin/register.js +2 -143
- package/src/openclaw/plugin/relay-client-shared.js +0 -14
- package/src/openclaw/runtime/tool-inventory.js +0 -1
- package/src/openclaw/runtime/working-memory.js +8 -12
- package/src/openclaw/runtime/transcript-report-comic-grid.js +0 -475
- package/src/openclaw/runtime/transcript-report-stylekit.js +0 -189
- package/src/openclaw/runtime/transcript-report.js +0 -923
|
@@ -153,7 +153,7 @@ export function buildClaworldContextPointer(options = {}) {
|
|
|
153
153
|
`- PROFILE.md: \`${artifacts.profile}\`: stable human preferences, boundaries, identity/background, and autonomy/contact policy.`,
|
|
154
154
|
`- reports/: \`${artifacts.reports}/\`: local report artifacts and readable evidence summaries.`,
|
|
155
155
|
`- journal/: \`${artifacts.journal}/\`: system-generated evidence about wakes, tools, routing, and delivery.`,
|
|
156
|
-
`- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, Conversation route/session hints
|
|
156
|
+
`- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, and Conversation route/session hints.`,
|
|
157
157
|
'',
|
|
158
158
|
'Read these files before treating an open Claworld loop as an ordinary chat todo. Read `sessions/index.json` before searching raw local session files. Do not edit `journal/` or `sessions/index.json` by hand.',
|
|
159
159
|
'',
|
|
@@ -194,16 +194,7 @@ export function buildClaworldContextPointer(options = {}) {
|
|
|
194
194
|
'- Peer-facing opener, reply, and final text belongs to the Conversation Session and backend conversation runtime.',
|
|
195
195
|
'- Do not use `sessions_send` to place peer-facing content into an `agent:...:conversation:...` session.',
|
|
196
196
|
'- You only re-engage a conversation, including providing supplemental information to it, by initiating the same conversation again via the `claworld_manage_conversations` tool. Keep it world-scoped if it originally was.',
|
|
197
|
-
'- Read relevant skills when creating / managing worlds and profiles.'
|
|
198
|
-
'',
|
|
199
|
-
'## Conversation Transcript Images',
|
|
200
|
-
'- When the human asks to find, export, quote, or show a prior Claworld conversation, treat it as a Claworld conversation lookup/render task. Read the `claworld-main-session` skill.',
|
|
201
|
-
'- Narrow candidates through recent reports, NOW.md, journal, and sessions/index.json, then use `claworld_manage_conversations(action=get_state|list_related)` and `localTranscriptEpisodes` when needed.',
|
|
202
|
-
'- Select a complete episode only by exact `chatRequestId`, then call `claworld_render_transcript_report(mode=stored, stored.chatRequestId=...)`. Never substitute conversationKey or localSessionKey.',
|
|
203
|
-
'- Stored rendering recovers public identity/world/profile context from the indexed kickoff. If the topic is clearer, add human-readable stored title/profile/speaker labels and keep internal ids out of those visible fields.',
|
|
204
|
-
'- Use `mode=manual` with ordered visible messages and timestamps for requested topic excerpts, highlights, summaries, or golden quotes.',
|
|
205
|
-
'- The renderer only generates local artifacts. After it returns, send each absolute PNG path with the standard OpenClaw `message(action=send, media=...)` tool; never paste paths or `MEDIA:` pseudo-references into user-visible text.',
|
|
206
|
-
'- PNG pages are the normal deliverable. Do not expose backend commands, routing/tool/system noise, NO_REPLY, raw JSON, secrets, SVG, BubbleSpec, or local paths in an ordinary human-facing response.'
|
|
197
|
+
'- Read relevant skills when creating / managing worlds and profiles.'
|
|
207
198
|
].join('\n');
|
|
208
199
|
}
|
|
209
200
|
|
|
@@ -224,6 +215,11 @@ function buildClaworldManagementStartupPrompt(options = {}) {
|
|
|
224
215
|
'When you receive a Claworld notification, management wake, lifecycle event, or recurring Claworld management task, read the `claworld-management-session` skill before deciding what to do.',
|
|
225
216
|
'A memory compaction is a maintenance turn only. It does not satisfy, replace, or change any Claworld notification. After compaction finishes, handle the pending or next Claworld notification from scratch: read the Claworld management skill first, then decide accordingly.',
|
|
226
217
|
'',
|
|
218
|
+
'## Conversation End Reporting',
|
|
219
|
+
'- Every fresh `conversation_ended` notification produces one report handoff to Main Session.',
|
|
220
|
+
'- Conversation value controls report length and follow-up. A short, low-value, or no-decision conversation receives a brief report.',
|
|
221
|
+
'- The dedupe identity is the exact notification, event, chat request, or ended instance. Return `NO_REPLY` only after verifying that exact instance has already been reported successfully.',
|
|
222
|
+
'',
|
|
227
223
|
'## What To Trust',
|
|
228
224
|
'Use Claworld tools when you need current product facts: account state, public profiles, worlds, memberships, chat requests, conversation status, feedback, and delivery state.',
|
|
229
225
|
'',
|
|
@@ -237,7 +233,7 @@ function buildClaworldManagementStartupPrompt(options = {}) {
|
|
|
237
233
|
`- NOW.md: \`${artifacts.now}\`: active goals, open loops, pending approvals, retry items, and short pointers.`,
|
|
238
234
|
`- reports/: \`${artifacts.reports}/\`: local report artifacts and readable evidence summaries.`,
|
|
239
235
|
`- journal/: \`${artifacts.journal}/\`: system-generated evidence. Read it only; do not edit or create journal files.`,
|
|
240
|
-
`- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, Conversation route/session hints
|
|
236
|
+
`- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, and Conversation route/session hints. Read it before routing or transcript lookup.`,
|
|
241
237
|
'',
|
|
242
238
|
'## Skills',
|
|
243
239
|
'- `claworld-management-session`: required for notifications, reporting, lifecycle handling, approval questions, proactive management, dedupe, and local working-memory rules.',
|
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clipDisplay,
|
|
3
|
-
displayCols,
|
|
4
|
-
ellipsizeText,
|
|
5
|
-
escapeXml,
|
|
6
|
-
fontFamily,
|
|
7
|
-
textUnits,
|
|
8
|
-
wrapText,
|
|
9
|
-
} from './transcript-report-stylekit.js';
|
|
10
|
-
|
|
11
|
-
const CANVAS_MARGIN = 24;
|
|
12
|
-
const FRAME_MARGIN = 16;
|
|
13
|
-
const HEADER_Y = 48;
|
|
14
|
-
const HEADER_CARD_HEIGHT_ONE_LINE = 92;
|
|
15
|
-
const HEADER_CARD_HEIGHT_TWO_LINES = 110;
|
|
16
|
-
const HEADER_BOTTOM_PAD = 20;
|
|
17
|
-
const BODY_TOP_GAP = 24;
|
|
18
|
-
const PAGE_BOTTOM = 54;
|
|
19
|
-
const ITEM_GAP = 22;
|
|
20
|
-
const TIME_ROW_HEIGHT = 42;
|
|
21
|
-
const ELLIPSIS_HEIGHT = 34;
|
|
22
|
-
const BUBBLE_PAD_X = 32;
|
|
23
|
-
const BUBBLE_PAD_Y = 22;
|
|
24
|
-
const LABEL_HEIGHT = 30;
|
|
25
|
-
const LABEL_OVERLAP = 18;
|
|
26
|
-
const LABEL_RAISE = 9;
|
|
27
|
-
const LABEL_MAX_COLS = 14;
|
|
28
|
-
const BUBBLE_MAX_RATIO = 0.64;
|
|
29
|
-
const BUBBLE_MIN_WIDTH = 200;
|
|
30
|
-
const FONT_SIZE = 18;
|
|
31
|
-
const SMALL_FONT_SIZE = 12;
|
|
32
|
-
const LABEL_FONT_SIZE = 15;
|
|
33
|
-
const TITLE_FONT_SIZE = 34;
|
|
34
|
-
const LINE_HEIGHT = 29;
|
|
35
|
-
const HEADER_SUBTITLE_MAX_UNITS = 33;
|
|
36
|
-
const HEADER_SUBTITLE_MAX_LINES = 2;
|
|
37
|
-
const HEADER_SUBTITLE_LINE_HEIGHT = 19;
|
|
38
|
-
const TAG_HEIGHT = 58;
|
|
39
|
-
const TAG_ICON_SIZE = 30;
|
|
40
|
-
const TAG_ICON_GAP = 12;
|
|
41
|
-
const TAG_ICON_TOP_GAP = 8;
|
|
42
|
-
const TAG_FALLBACK_MAX_COLS = 10;
|
|
43
|
-
const TEXT_UNIT_PX = 18;
|
|
44
|
-
const BLACK = '#090909';
|
|
45
|
-
|
|
46
|
-
const THEME = Object.freeze({
|
|
47
|
-
paper: '#FBF8EF',
|
|
48
|
-
headerFill: '#FEF5D8',
|
|
49
|
-
muted: '#222222',
|
|
50
|
-
leftFill: '#EFFFF5',
|
|
51
|
-
leftLabel: '#62E69D',
|
|
52
|
-
rightFill: '#EFE0FF',
|
|
53
|
-
rightLabel: '#B785FF',
|
|
54
|
-
timeFill: '#FFFDF7',
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const TAG_ICON_THEMES = Object.freeze({
|
|
58
|
-
like: ['#D8F4FF', '#58B7FF'],
|
|
59
|
-
dislike: ['#FFE0EA', '#FF6A9A'],
|
|
60
|
-
'request end': ['#FFF0A8', '#FF9F2F'],
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const fixed = (value, places = 1) => Number(value).toFixed(places);
|
|
64
|
-
|
|
65
|
-
function labelText(label) {
|
|
66
|
-
return clipDisplay(String(label || 'AGENT').toUpperCase(), LABEL_MAX_COLS);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function labelWidth(label) {
|
|
70
|
-
return Math.max(86, Math.min(158, displayCols(label) * 11 + 30));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function tagName(tag) {
|
|
74
|
-
return String(tag ?? '').trim().toLowerCase();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function fallbackTagLabel(tag) {
|
|
78
|
-
return clipDisplay(String(tag || 'tag'), TAG_FALLBACK_MAX_COLS);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function tagWidth(tag) {
|
|
82
|
-
const normalized = tagName(tag);
|
|
83
|
-
if (['like', 'dislike', 'request end'].includes(normalized)) return TAG_ICON_SIZE;
|
|
84
|
-
return Math.max(58, Math.min(126, displayCols(fallbackTagLabel(normalized)) * 8 + 24));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function tagRowUnits(tags) {
|
|
88
|
-
if (!tags.length) return 0;
|
|
89
|
-
const width = tags.reduce((sum, tag) => sum + tagWidth(tag), 0)
|
|
90
|
-
+ Math.max(0, tags.length - 1) * TAG_ICON_GAP;
|
|
91
|
-
return width / TEXT_UNIT_PX;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function measureTranscriptItem(item, width) {
|
|
95
|
-
const contentWidth = Math.max(270, Math.trunc(width * BUBBLE_MAX_RATIO));
|
|
96
|
-
if (item.kind === 'ellipsis') {
|
|
97
|
-
return {
|
|
98
|
-
kind: 'ellipsis',
|
|
99
|
-
message: null,
|
|
100
|
-
lines: [],
|
|
101
|
-
width: contentWidth,
|
|
102
|
-
height: ELLIPSIS_HEIGHT,
|
|
103
|
-
omittedCount: item.omitted,
|
|
104
|
-
label: item.label,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (item.kind === 'time') {
|
|
108
|
-
return {
|
|
109
|
-
kind: 'time',
|
|
110
|
-
message: null,
|
|
111
|
-
lines: [],
|
|
112
|
-
width: contentWidth,
|
|
113
|
-
height: TIME_ROW_HEIGHT,
|
|
114
|
-
label: item.label,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const { message } = item;
|
|
119
|
-
const maxUnits = Math.max(18, (contentWidth - BUBBLE_PAD_X * 2) / TEXT_UNIT_PX);
|
|
120
|
-
const lines = wrapText(message.text, maxUnits);
|
|
121
|
-
const labelUnits = textUnits(labelText(message.participantLabel)) + 4;
|
|
122
|
-
const contentUnits = Math.max(
|
|
123
|
-
...lines.map((line) => textUnits(line)),
|
|
124
|
-
tagRowUnits(message.tags),
|
|
125
|
-
labelUnits,
|
|
126
|
-
10,
|
|
127
|
-
);
|
|
128
|
-
const bubbleWidth = Math.trunc(Math.min(
|
|
129
|
-
contentWidth,
|
|
130
|
-
Math.max(BUBBLE_MIN_WIDTH, contentUnits * TEXT_UNIT_PX + BUBBLE_PAD_X * 2),
|
|
131
|
-
));
|
|
132
|
-
const textHeight = lines.length * LINE_HEIGHT;
|
|
133
|
-
const tagHeight = message.tags.length ? TAG_HEIGHT : 0;
|
|
134
|
-
const bubbleHeight = BUBBLE_PAD_Y * 2 + textHeight + tagHeight;
|
|
135
|
-
return {
|
|
136
|
-
kind: 'message',
|
|
137
|
-
message,
|
|
138
|
-
lines,
|
|
139
|
-
width: bubbleWidth,
|
|
140
|
-
height: LABEL_HEIGHT - LABEL_OVERLAP + bubbleHeight + 10,
|
|
141
|
-
tagHeight,
|
|
142
|
-
textHeight,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function headerSubtitleLines(subtitle) {
|
|
147
|
-
const lines = wrapText(String(subtitle ?? '').trim(), HEADER_SUBTITLE_MAX_UNITS);
|
|
148
|
-
if (lines.length <= HEADER_SUBTITLE_MAX_LINES) return lines;
|
|
149
|
-
const visible = lines.slice(0, HEADER_SUBTITLE_MAX_LINES - 1);
|
|
150
|
-
const remainder = lines.slice(HEADER_SUBTITLE_MAX_LINES - 1)
|
|
151
|
-
.map((line) => line.trim())
|
|
152
|
-
.filter(Boolean)
|
|
153
|
-
.join(' ');
|
|
154
|
-
visible.push(ellipsizeText(remainder, HEADER_SUBTITLE_MAX_UNITS, '…'));
|
|
155
|
-
return visible;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function headerCardHeight(subtitle) {
|
|
159
|
-
return headerSubtitleLines(subtitle).length > 1
|
|
160
|
-
? HEADER_CARD_HEIGHT_TWO_LINES
|
|
161
|
-
: HEADER_CARD_HEIGHT_ONE_LINE;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function headerHeight(subtitle) {
|
|
165
|
-
return HEADER_Y + headerCardHeight(subtitle) + HEADER_BOTTOM_PAD;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function positions(width, bubbleWidth, label, side) {
|
|
169
|
-
const currentLabelWidth = labelWidth(label);
|
|
170
|
-
const inset = CANVAS_MARGIN + 38;
|
|
171
|
-
if (side === 'right') {
|
|
172
|
-
const bubbleX = width - inset - bubbleWidth;
|
|
173
|
-
return {
|
|
174
|
-
bubbleX,
|
|
175
|
-
labelX: bubbleX + bubbleWidth - currentLabelWidth - 16,
|
|
176
|
-
labelWidth: currentLabelWidth,
|
|
177
|
-
align: 'right',
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const bubbleX = inset;
|
|
181
|
-
return {
|
|
182
|
-
bubbleX,
|
|
183
|
-
labelX: bubbleX + 16,
|
|
184
|
-
labelWidth: currentLabelWidth,
|
|
185
|
-
align: 'left',
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function paginateTranscriptItems(items, width, maxHeight, title, subtitle) {
|
|
190
|
-
const pageGroups = [[]];
|
|
191
|
-
const currentHeaderHeight = headerHeight(subtitle);
|
|
192
|
-
let used = currentHeaderHeight + BODY_TOP_GAP + PAGE_BOTTOM;
|
|
193
|
-
items.forEach((item, index) => {
|
|
194
|
-
const itemHeight = item.height + ITEM_GAP;
|
|
195
|
-
let neededHeight = itemHeight;
|
|
196
|
-
if (item.kind === 'time' && index + 1 < items.length) {
|
|
197
|
-
neededHeight += items[index + 1].height + ITEM_GAP;
|
|
198
|
-
}
|
|
199
|
-
if (pageGroups.at(-1).length && used + neededHeight > maxHeight) {
|
|
200
|
-
pageGroups.push([]);
|
|
201
|
-
used = currentHeaderHeight + BODY_TOP_GAP + PAGE_BOTTOM;
|
|
202
|
-
}
|
|
203
|
-
pageGroups.at(-1).push(item);
|
|
204
|
-
used += itemHeight;
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
return pageGroups.map((pageItems, pageIndex) => {
|
|
208
|
-
let y = currentHeaderHeight + BODY_TOP_GAP;
|
|
209
|
-
const layoutItems = [];
|
|
210
|
-
for (const item of pageItems) {
|
|
211
|
-
if (item.kind === 'ellipsis' || item.kind === 'time') {
|
|
212
|
-
layoutItems.push({ kind: item.kind, y, height: item.height, label: item.label });
|
|
213
|
-
y += item.height + ITEM_GAP;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const label = labelText(item.message.participantLabel);
|
|
217
|
-
const position = positions(width, item.width, label, item.message.side);
|
|
218
|
-
const bubbleY = y + LABEL_HEIGHT - LABEL_OVERLAP;
|
|
219
|
-
const bubbleHeight = BUBBLE_PAD_Y * 2 + item.textHeight + item.tagHeight;
|
|
220
|
-
layoutItems.push({
|
|
221
|
-
kind: 'message',
|
|
222
|
-
y,
|
|
223
|
-
...position,
|
|
224
|
-
bubbleY,
|
|
225
|
-
labelY: y - LABEL_RAISE,
|
|
226
|
-
label,
|
|
227
|
-
width: item.width,
|
|
228
|
-
bubbleHeight,
|
|
229
|
-
lines: item.lines,
|
|
230
|
-
message: item.message,
|
|
231
|
-
tagHeight: item.tagHeight,
|
|
232
|
-
textHeight: item.textHeight,
|
|
233
|
-
});
|
|
234
|
-
y += item.height + ITEM_GAP;
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
page: pageIndex + 1,
|
|
238
|
-
width,
|
|
239
|
-
height: Math.max(520, Math.min(maxHeight, y + PAGE_BOTTOM)),
|
|
240
|
-
items: layoutItems,
|
|
241
|
-
title,
|
|
242
|
-
subtitle,
|
|
243
|
-
footer: 'visit claworld.love',
|
|
244
|
-
};
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function svgDefs() {
|
|
249
|
-
return [
|
|
250
|
-
'<defs>',
|
|
251
|
-
`<style><![CDATA[text { font-family: ${fontFamily()}; letter-spacing: 0; } .message-row:hover rect:last-of-type { filter: url(#comicLift); }]]></style>`,
|
|
252
|
-
'<pattern id="comicGridMinor" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#BED1D8" stroke-width="1" stroke-opacity="0.62"/></pattern>',
|
|
253
|
-
'<pattern id="comicGridMajor" width="128" height="128" patternUnits="userSpaceOnUse"><path d="M 128 0 L 0 0 0 128" fill="none" stroke="#AABFC8" stroke-width="1.4" stroke-opacity="0.72"/></pattern>',
|
|
254
|
-
'<linearGradient id="headerAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#47B6FF"/><stop offset="52%" stop-color="#FF4EB4"/><stop offset="100%" stop-color="#FF8A2A"/></linearGradient>',
|
|
255
|
-
'<linearGradient id="leftAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#58E58F"/><stop offset="100%" stop-color="#47B6FF"/></linearGradient>',
|
|
256
|
-
'<linearGradient id="rightAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#A871FF"/><stop offset="100%" stop-color="#FF4EB4"/></linearGradient>',
|
|
257
|
-
'<filter id="comicLift" x="-6%" y="-16%" width="112%" height="132%"><feDropShadow dx="0" dy="2" stdDeviation="1.2" flood-color="#000000" flood-opacity="0.14"/></filter>',
|
|
258
|
-
'</defs>',
|
|
259
|
-
].join('\n');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function starPoints(cx, cy, radius) {
|
|
263
|
-
return [
|
|
264
|
-
[cx, cy - radius],
|
|
265
|
-
[cx + radius * 0.28, cy - radius * 0.28],
|
|
266
|
-
[cx + radius, cy],
|
|
267
|
-
[cx + radius * 0.28, cy + radius * 0.28],
|
|
268
|
-
[cx, cy + radius],
|
|
269
|
-
[cx - radius * 0.28, cy + radius * 0.28],
|
|
270
|
-
[cx - radius, cy],
|
|
271
|
-
[cx - radius * 0.28, cy - radius * 0.28],
|
|
272
|
-
];
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function decorativeStarSvg(cx, cy, radius, fill, accent) {
|
|
276
|
-
const points = (offsetX, offsetY) => starPoints(cx + offsetX, cy + offsetY, radius)
|
|
277
|
-
.map(([x, y]) => `${fixed(x)},${fixed(y)}`)
|
|
278
|
-
.join(' ');
|
|
279
|
-
return [
|
|
280
|
-
`<polygon points="${points(3, 5)}" fill="${accent}"/>`,
|
|
281
|
-
`<polygon points="${points(0, 0)}" fill="${fill}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
282
|
-
].join('\n');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function headerTitle(title) {
|
|
286
|
-
const clean = String(title ?? '').trim();
|
|
287
|
-
if (clean.startsWith('@')) return clean;
|
|
288
|
-
return clean ? `@${clean}` : '@claworld';
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function renderHeader(page) {
|
|
292
|
-
const x = CANVAS_MARGIN + 26;
|
|
293
|
-
const y = HEADER_Y;
|
|
294
|
-
const width = page.width - (CANVAS_MARGIN + 26) * 2;
|
|
295
|
-
const height = headerCardHeight(page.subtitle);
|
|
296
|
-
const title = clipDisplay(headerTitle(page.title), 32);
|
|
297
|
-
const subtitle = headerSubtitleLines(page.subtitle)
|
|
298
|
-
.map((line, index) => `<text class="header-subtitle-line" x="${fixed(x + 35)}" y="${fixed(y + 70 + index * HEADER_SUBTITLE_LINE_HEIGHT)}" font-size="15" font-weight="600" fill="${THEME.muted}">${escapeXml(line)}</text>`)
|
|
299
|
-
.join('\n');
|
|
300
|
-
return [
|
|
301
|
-
`<rect x="${x + 11}" y="${y + 6}" width="${width + 2}" height="${height + 10}" rx="22" fill="${BLACK}"/>`,
|
|
302
|
-
`<rect x="${x + 7}" y="${y + 6}" width="${width}" height="${height + 4}" rx="22" fill="url(#headerAccent)"/>`,
|
|
303
|
-
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="22" fill="${THEME.headerFill}" stroke="${BLACK}" stroke-width="4"/>`,
|
|
304
|
-
`<text x="${x + 28}" y="${y + 43}" font-size="${TITLE_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(title)}</text>`,
|
|
305
|
-
subtitle,
|
|
306
|
-
decorativeStarSvg(x + width - 62, y + 34, 22, '#FFFFFF', 'url(#headerAccent)'),
|
|
307
|
-
`<circle cx="${x + width - 23}" cy="${y + 55}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
|
|
308
|
-
`<circle cx="${x + width - 25}" cy="${y + 53}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
|
|
309
|
-
].join('\n');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function diamondSvg(cx, cy, radius, fill) {
|
|
313
|
-
const shadow = `<polygon points="${fixed(cx + 1)},${fixed(cy + 2 - radius)} ${fixed(cx + 1 + radius * 0.46)},${fixed(cy + 2)} ${fixed(cx + 1)},${fixed(cy + 2 + radius)} ${fixed(cx + 1 - radius * 0.46)},${fixed(cy + 2)}" fill="${BLACK}" stroke="${BLACK}" stroke-width="2.5"/>`;
|
|
314
|
-
const front = `<polygon points="${fixed(cx)},${fixed(cy - radius)} ${fixed(cx + radius * 0.46)},${fixed(cy)} ${fixed(cx)},${fixed(cy + radius)} ${fixed(cx - radius * 0.46)},${fixed(cy)}" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`;
|
|
315
|
-
return `${shadow}\n${front}`;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function renderTimeSvg(page, item) {
|
|
319
|
-
const label = clipDisplay(item.label, 22);
|
|
320
|
-
const labelWidthValue = Math.max(150, displayCols(label) * 8 + 44);
|
|
321
|
-
const x = page.width / 2 - labelWidthValue / 2;
|
|
322
|
-
const y = item.y + 4;
|
|
323
|
-
return [
|
|
324
|
-
'<g class="time-row">',
|
|
325
|
-
diamondSvg(x - 28, y + 16, 13, '#FF5BE2'),
|
|
326
|
-
`<rect x="${fixed(x + 3)}" y="${y + 4}" width="${labelWidthValue}" height="30" rx="15" fill="${BLACK}"/>`,
|
|
327
|
-
`<rect x="${fixed(x)}" y="${y}" width="${labelWidthValue}" height="30" rx="15" fill="${THEME.timeFill}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
328
|
-
`<text x="${fixed(page.width / 2)}" y="${y + 21}" text-anchor="middle" font-size="${FONT_SIZE}" font-weight="700" fill="${BLACK}">${escapeXml(label)}</text>`,
|
|
329
|
-
diamondSvg(x + labelWidthValue + 28, y + 16, 13, '#5FE0A7'),
|
|
330
|
-
'</g>',
|
|
331
|
-
].join('\n');
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function sideColors(side) {
|
|
335
|
-
return side === 'right'
|
|
336
|
-
? { fill: THEME.rightFill, label: THEME.rightLabel, accent: 'url(#rightAccent)' }
|
|
337
|
-
: { fill: THEME.leftFill, label: THEME.leftLabel, accent: 'url(#leftAccent)' };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function bubbleLayersSvg(item, colors) {
|
|
341
|
-
const { bubbleX: x, bubbleY: y, width, bubbleHeight: height } = item;
|
|
342
|
-
return [
|
|
343
|
-
`<rect x="${x + 11}" y="${y + 9}" width="${width + 2}" height="${height + 4}" rx="17" fill="${BLACK}"/>`,
|
|
344
|
-
`<rect x="${x + 11}" y="${y + 9}" width="${width - 3}" height="${height}" rx="17" fill="${colors.accent}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
345
|
-
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="17" fill="${colors.fill}" stroke="${BLACK}" stroke-width="4"/>`,
|
|
346
|
-
].join('\n');
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function thumbIconSvg(x, y, accent, down = false) {
|
|
350
|
-
const paths = [
|
|
351
|
-
`<path d="M${fixed(x + 7.8)} ${fixed(y + 13.3)} H${fixed(x + 12)} V${fixed(y + 24)} H${fixed(x + 7.8)} Z" fill="#FFFFFF" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
352
|
-
`<path d="M${fixed(x + 12)} ${fixed(y + 23.6)} H${fixed(x + 21.2)} C${fixed(x + 23)} ${fixed(y + 23.6)} ${fixed(x + 24)} ${fixed(y + 22.5)} ${fixed(x + 24.4)} ${fixed(y + 20.9)} L${fixed(x + 25.4)} ${fixed(y + 15.6)} C${fixed(x + 25.8)} ${fixed(y + 13.9)} ${fixed(x + 24.5)} ${fixed(y + 12.2)} ${fixed(x + 22.7)} ${fixed(y + 12.2)} H${fixed(x + 18.6)} L${fixed(x + 19.2)} ${fixed(y + 9.1)} C${fixed(x + 19.5)} ${fixed(y + 7.4)} ${fixed(x + 18.4)} ${fixed(y + 5.7)} ${fixed(x + 16.7)} ${fixed(y + 5.4)} L${fixed(x + 15.8)} ${fixed(y + 5.3)} L${fixed(x + 12)} ${fixed(y + 12.9)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
353
|
-
].join('\n');
|
|
354
|
-
if (!down) return paths;
|
|
355
|
-
const centerX = x + TAG_ICON_SIZE / 2;
|
|
356
|
-
const centerY = y + TAG_ICON_SIZE / 2;
|
|
357
|
-
return `<g transform="rotate(180 ${fixed(centerX)} ${fixed(centerY)})">\n${paths}\n</g>`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function requestEndIconSvg(x, y, accent) {
|
|
361
|
-
return [
|
|
362
|
-
`<path d="M${fixed(x + 22)} ${fixed(y + 4)} C${fixed(x + 25.2)} ${fixed(y + 5.4)} ${fixed(x + 26.6)} ${fixed(y + 7.8)} ${fixed(x + 26.5)} ${fixed(y + 10.6)}" fill="none" stroke="${BLACK}" stroke-width="2" stroke-linecap="round"/>`,
|
|
363
|
-
`<path d="M${fixed(x + 9.215, 3)} ${fixed(y + 18.117, 3)} C${fixed(x + 10.043, 3)} ${fixed(y + 22.457, 3)} ${fixed(x + 13.384, 3)} ${fixed(y + 25.036, 3)} ${fixed(x + 17.132, 3)} ${fixed(y + 23.961, 3)} L${fixed(x + 19.44, 3)} ${fixed(y + 23.3, 3)} C${fixed(x + 22.323, 3)} ${fixed(y + 22.473, 3)} ${fixed(x + 23.488, 3)} ${fixed(y + 19.642, 3)} ${fixed(x + 22.538, 3)} ${fixed(y + 16.69, 3)} L${fixed(x + 21.435, 3)} ${fixed(y + 12.845, 3)} L${fixed(x + 9.227, 3)} ${fixed(y + 16.345, 3)} L${fixed(x + 9.215, 3)} ${fixed(y + 18.117, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
364
|
-
`<path d="M${fixed(x + 9.665, 3)} ${fixed(y + 7.897, 3)} L${fixed(x + 9.473, 3)} ${fixed(y + 7.952, 3)} C${fixed(x + 8.756, 3)} ${fixed(y + 8.158, 3)} ${fixed(x + 8.286, 3)} ${fixed(y + 8.709, 3)} ${fixed(x + 8.422, 3)} ${fixed(y + 9.184, 3)} L${fixed(x + 10.192, 3)} ${fixed(y + 15.356, 3)} C${fixed(x + 10.328, 3)} ${fixed(y + 15.831, 3)} ${fixed(x + 11.019, 3)} ${fixed(y + 16.049, 3)} ${fixed(x + 11.736, 3)} ${fixed(y + 15.843, 3)} L${fixed(x + 11.928, 3)} ${fixed(y + 15.788, 3)} C${fixed(x + 12.645, 3)} ${fixed(y + 15.583, 3)} ${fixed(x + 13.115, 3)} ${fixed(y + 15.031, 3)} ${fixed(x + 12.979, 3)} ${fixed(y + 14.557, 3)} L${fixed(x + 11.209, 3)} ${fixed(y + 8.384, 3)} C${fixed(x + 11.073, 3)} ${fixed(y + 7.91, 3)} ${fixed(x + 10.382, 3)} ${fixed(y + 7.692, 3)} ${fixed(x + 9.665, 3)} ${fixed(y + 7.897, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
365
|
-
`<path d="M${fixed(x + 11.93, 3)} ${fixed(y + 5.999, 3)} L${fixed(x + 11.738, 3)} ${fixed(y + 6.055, 3)} C${fixed(x + 11.021, 3)} ${fixed(y + 6.26, 3)} ${fixed(x + 10.553, 3)} ${fixed(y + 6.82, 3)} ${fixed(x + 10.692, 3)} ${fixed(y + 7.306, 3)} L${fixed(x + 12.729, 3)} ${fixed(y + 14.408, 3)} C${fixed(x + 12.868, 3)} ${fixed(y + 14.894, 3)} ${fixed(x + 13.562, 3)} ${fixed(y + 15.122, 3)} ${fixed(x + 14.279, 3)} ${fixed(y + 14.916, 3)} L${fixed(x + 14.471, 3)} ${fixed(y + 14.861, 3)} C${fixed(x + 15.188, 3)} ${fixed(y + 14.655, 3)} ${fixed(x + 15.656, 3)} ${fixed(y + 14.095, 3)} ${fixed(x + 15.516, 3)} ${fixed(y + 13.609, 3)} L${fixed(x + 13.48, 3)} ${fixed(y + 6.507, 3)} C${fixed(x + 13.341, 3)} ${fixed(y + 6.021, 3)} ${fixed(x + 12.647, 3)} ${fixed(y + 5.794, 3)} ${fixed(x + 11.93, 3)} ${fixed(y + 5.999, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
366
|
-
`<path d="M${fixed(x + 14.636, 3)} ${fixed(y + 5.64, 3)} L${fixed(x + 14.443, 3)} ${fixed(y + 5.695, 3)} C${fixed(x + 13.727, 3)} ${fixed(y + 5.9, 3)} ${fixed(x + 13.258, 3)} ${fixed(y + 6.457, 3)} ${fixed(x + 13.396, 3)} ${fixed(y + 6.938, 3)} L${fixed(x + 15.338, 3)} ${fixed(y + 13.714, 3)} C${fixed(x + 15.476, 3)} ${fixed(y + 14.195, 3)} ${fixed(x + 16.169, 3)} ${fixed(y + 14.418, 3)} ${fixed(x + 16.886, 3)} ${fixed(y + 14.213, 3)} L${fixed(x + 17.078, 3)} ${fixed(y + 14.157, 3)} C${fixed(x + 17.795, 3)} ${fixed(y + 13.952, 3)} ${fixed(x + 18.264, 3)} ${fixed(y + 13.395, 3)} ${fixed(x + 18.126, 3)} ${fixed(y + 12.914, 3)} L${fixed(x + 16.183, 3)} ${fixed(y + 6.139, 3)} C${fixed(x + 16.045, 3)} ${fixed(y + 5.658, 3)} ${fixed(x + 15.352, 3)} ${fixed(y + 5.434, 3)} ${fixed(x + 14.636, 3)} ${fixed(y + 5.64, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
367
|
-
`<path d="M${fixed(x + 17.8, 3)} ${fixed(y + 6.085, 3)} L${fixed(x + 17.702, 3)} ${fixed(y + 6.113, 3)} C${fixed(x + 16.971, 3)} ${fixed(y + 6.322, 3)} ${fixed(x + 16.482, 3)} ${fixed(y + 6.851, 3)} ${fixed(x + 16.609, 3)} ${fixed(y + 7.294, 3)} L${fixed(x + 18.176, 3)} ${fixed(y + 12.759, 3)} C${fixed(x + 18.303, 3)} ${fixed(y + 13.202, 3)} ${fixed(x + 18.998, 3)} ${fixed(y + 13.391, 3)} ${fixed(x + 19.729, 3)} ${fixed(y + 13.182, 3)} L${fixed(x + 19.827, 3)} ${fixed(y + 13.154, 3)} C${fixed(x + 20.557, 3)} ${fixed(y + 12.944, 3)} ${fixed(x + 21.046, 3)} ${fixed(y + 12.415, 3)} ${fixed(x + 20.919, 3)} ${fixed(y + 11.972, 3)} L${fixed(x + 19.352, 3)} ${fixed(y + 6.507, 3)} C${fixed(x + 19.225, 3)} ${fixed(y + 6.065, 3)} ${fixed(x + 18.53, 3)} ${fixed(y + 5.875, 3)} ${fixed(x + 17.8, 3)} ${fixed(y + 6.085, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
368
|
-
`<path d="M${fixed(x + 9.546, 3)} ${fixed(y + 19.999, 3)} L${fixed(x + 6, 3)} ${fixed(y + 17.791, 3)} C${fixed(x + 4.736, 3)} ${fixed(y + 17.009, 3)} ${fixed(x + 5.668, 3)} ${fixed(y + 15.181, 3)} ${fixed(x + 7.138, 3)} ${fixed(y + 15.592, 3)} L${fixed(x + 10.615, 3)} ${fixed(y + 17.196, 3)} L${fixed(x + 9.546, 3)} ${fixed(y + 19.999, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8" stroke-linejoin="round"/>`,
|
|
369
|
-
`<rect x="${fixed(x + 10.887, 3)}" y="${fixed(y + 14.834, 3)}" width="9.584" height="3.800" transform="rotate(-16.9438 ${fixed(x + 10.887, 3)} ${fixed(y + 14.834, 3)})" fill="${accent}"/>`,
|
|
370
|
-
`<path d="M${fixed(x + 10.5)} ${fixed(y + 19.5)} L${fixed(x + 11)} ${fixed(y + 20)} L${fixed(x + 11.5)} ${fixed(y + 17)} L${fixed(x + 9.5)} ${fixed(y + 17.5)} L${fixed(x + 9)} ${fixed(y + 18.5)} L${fixed(x + 10.5)} ${fixed(y + 19.5)} Z" fill="${accent}"/>`,
|
|
371
|
-
].join('\n');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function fallbackTagSvg(tag, x, y) {
|
|
375
|
-
const label = fallbackTagLabel(tag);
|
|
376
|
-
const width = tagWidth(tag);
|
|
377
|
-
return [
|
|
378
|
-
`<g class="tag-icon tag-fallback" role="img" aria-label="${escapeXml(label)}">`,
|
|
379
|
-
`<title>${escapeXml(label)}</title>`,
|
|
380
|
-
`<rect x="${fixed(x + 3)}" y="${fixed(y + 4)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
|
|
381
|
-
`<rect x="${fixed(x)}" y="${fixed(y)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="#F6F1FF" stroke="${BLACK}" stroke-width="2.5"/>`,
|
|
382
|
-
`<text x="${fixed(x + width / 2)}" y="${fixed(y + 20.5)}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(label)}</text>`,
|
|
383
|
-
'</g>',
|
|
384
|
-
].join('\n');
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function tagIconSvg(tag, x, y) {
|
|
388
|
-
const normalized = tagName(tag);
|
|
389
|
-
if (!['like', 'dislike', 'request end'].includes(normalized)) {
|
|
390
|
-
return fallbackTagSvg(normalized, x, y);
|
|
391
|
-
}
|
|
392
|
-
const [fill, accent] = TAG_ICON_THEMES[normalized];
|
|
393
|
-
const icon = normalized === 'request end'
|
|
394
|
-
? requestEndIconSvg(x, y, accent)
|
|
395
|
-
: thumbIconSvg(x, y, accent, normalized === 'dislike');
|
|
396
|
-
return [
|
|
397
|
-
`<g class="tag-icon tag-${escapeXml(normalized.replaceAll(' ', '-'))}" role="img" aria-label="${escapeXml(normalized)}">`,
|
|
398
|
-
`<title>${escapeXml(normalized)}</title>`,
|
|
399
|
-
`<rect x="${fixed(x + 3)}" y="${fixed(y + 4)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
|
|
400
|
-
`<rect x="${fixed(x)}" y="${fixed(y)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`,
|
|
401
|
-
icon,
|
|
402
|
-
'</g>',
|
|
403
|
-
].join('\n');
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function renderTagIconsSvg(tags, x, y) {
|
|
407
|
-
const parts = [`<g class="tag-icons" transform="translate(${fixed(x)} ${fixed(y)})">`];
|
|
408
|
-
let cursor = 0;
|
|
409
|
-
for (const tag of tags) {
|
|
410
|
-
parts.push(tagIconSvg(tag, cursor, 0));
|
|
411
|
-
cursor += tagWidth(tag) + TAG_ICON_GAP;
|
|
412
|
-
}
|
|
413
|
-
parts.push('</g>');
|
|
414
|
-
return parts.join('\n');
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function renderMessageSvg(item) {
|
|
418
|
-
const { message } = item;
|
|
419
|
-
const colors = sideColors(message.side);
|
|
420
|
-
const accessibleLabel = escapeXml(`${message.participantLabel}: ${ellipsizeText(message.text, 42)}`);
|
|
421
|
-
const parts = [
|
|
422
|
-
`<g class="message-row ${message.side}" role="listitem" aria-label="${accessibleLabel}">`,
|
|
423
|
-
`<title>${accessibleLabel}</title>`,
|
|
424
|
-
bubbleLayersSvg(item, colors),
|
|
425
|
-
`<rect x="${item.labelX}" y="${item.labelY}" width="${item.labelWidth}" height="${LABEL_HEIGHT}" rx="9" fill="${colors.label}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
426
|
-
`<text x="${fixed(item.labelX + item.labelWidth / 2)}" y="${item.labelY + 21}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(item.label)}</text>`,
|
|
427
|
-
];
|
|
428
|
-
const textX = item.bubbleX + BUBBLE_PAD_X;
|
|
429
|
-
let textY = item.bubbleY + BUBBLE_PAD_Y + 17;
|
|
430
|
-
for (const line of item.lines) {
|
|
431
|
-
parts.push(`<text x="${textX}" y="${textY}" font-size="${FONT_SIZE}" font-weight="800" fill="${BLACK}">${escapeXml(line)}</text>`);
|
|
432
|
-
textY += LINE_HEIGHT;
|
|
433
|
-
}
|
|
434
|
-
if (message.tags.length) {
|
|
435
|
-
parts.push(renderTagIconsSvg(message.tags, textX, textY + TAG_ICON_TOP_GAP));
|
|
436
|
-
}
|
|
437
|
-
parts.push('</g>');
|
|
438
|
-
return parts.join('\n');
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
export function renderTranscriptPageSvg(page) {
|
|
442
|
-
const titleId = `claworld-report-title-${page.page}`;
|
|
443
|
-
const descriptionId = `claworld-report-desc-${page.page}`;
|
|
444
|
-
const description = `${page.title}. ${page.subtitle}. ${page.items.length} transcript rows.`;
|
|
445
|
-
const parts = [
|
|
446
|
-
`<svg class="comic-grid" xmlns="http://www.w3.org/2000/svg" width="${page.width}" height="${page.height}" viewBox="0 0 ${page.width} ${page.height}" role="img" aria-labelledby="${titleId} ${descriptionId}">`,
|
|
447
|
-
`<title id="${titleId}">${escapeXml(page.title)}</title>`,
|
|
448
|
-
`<desc id="${descriptionId}">${escapeXml(description)}</desc>`,
|
|
449
|
-
svgDefs(),
|
|
450
|
-
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="${THEME.paper}"/>`,
|
|
451
|
-
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMinor)"/>`,
|
|
452
|
-
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMajor)" opacity="0.46"/>`,
|
|
453
|
-
`<rect x="${FRAME_MARGIN}" y="${FRAME_MARGIN}" width="${page.width - FRAME_MARGIN * 2}" height="${page.height - FRAME_MARGIN * 2}" rx="46" fill="none" stroke="${BLACK}" stroke-width="6"/>`,
|
|
454
|
-
renderHeader(page),
|
|
455
|
-
'<g role="list">',
|
|
456
|
-
];
|
|
457
|
-
for (const item of page.items) {
|
|
458
|
-
if (item.kind === 'ellipsis') {
|
|
459
|
-
const y = item.y + 7;
|
|
460
|
-
parts.push(`<text x="${fixed(page.width / 2)}" y="${y + 14}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#555555">${escapeXml(item.label)}</text>`);
|
|
461
|
-
} else if (item.kind === 'time') {
|
|
462
|
-
parts.push(renderTimeSvg(page, item));
|
|
463
|
-
} else {
|
|
464
|
-
parts.push(renderMessageSvg(item));
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
parts.push('</g>');
|
|
468
|
-
if (page.footer) {
|
|
469
|
-
parts.push(`<text x="${fixed(page.width / 2)}" y="${page.height - 24}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#444444">${escapeXml(page.footer)}</text>`);
|
|
470
|
-
}
|
|
471
|
-
parts.push('</svg>');
|
|
472
|
-
return parts.join('\n');
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export const CLAWORLD_TRANSCRIPT_STYLE_NAME = 'claworld-comic-grid';
|