@crossworks/share-ui 0.230.43

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 (44) hide show
  1. package/LICENSE.md +135 -0
  2. package/package.json +68 -0
  3. package/src/app-bridge-protocol.ts +115 -0
  4. package/src/app-presenter.tsx +25 -0
  5. package/src/app-sandbox.tsx +552 -0
  6. package/src/appearance.ts +192 -0
  7. package/src/avatar.test.ts +229 -0
  8. package/src/avatar.ts +731 -0
  9. package/src/backgrounds.test.ts +119 -0
  10. package/src/backgrounds.ts +118 -0
  11. package/src/draw-presenter.tsx +39 -0
  12. package/src/event-presenter.tsx +62 -0
  13. package/src/file-presenter.tsx +76 -0
  14. package/src/formula-calculator.tsx +209 -0
  15. package/src/formula-presenter.test.ts +128 -0
  16. package/src/formula-presenter.tsx +301 -0
  17. package/src/help-topics.ts +104 -0
  18. package/src/lib/ink-audit.test.ts +314 -0
  19. package/src/lib/theme-css-blocks.ts +26 -0
  20. package/src/lib/theme-generator.test.ts +179 -0
  21. package/src/lib/theme-registry.gen.ts +352 -0
  22. package/src/lib/themes.test.ts +308 -0
  23. package/src/lib/themes.ts +75 -0
  24. package/src/lib/utils.ts +6 -0
  25. package/src/nav-items.ts +225 -0
  26. package/src/note-presenter.tsx +14 -0
  27. package/src/page-outline.tsx +127 -0
  28. package/src/table-presenter.tsx +226 -0
  29. package/src/task-presenter.tsx +60 -0
  30. package/src/ui/button.tsx +50 -0
  31. package/src/ui/input.tsx +18 -0
  32. package/src/ui/label.tsx +20 -0
  33. package/src/view-payload.ts +82 -0
  34. package/styles/app.css +1098 -0
  35. package/styles/themes.css +6198 -0
  36. package/themes/generate.d.mts +11 -0
  37. package/themes/generate.mjs +618 -0
  38. package/themes/model.d.mts +24 -0
  39. package/themes/model.mjs +213 -0
  40. package/themes/preview.html +145 -0
  41. package/themes/seeds.d.mts +16 -0
  42. package/themes/seeds.mjs +3694 -0
  43. package/tsconfig.json +15 -0
  44. package/tsconfig.tsbuildinfo +1 -0
package/src/avatar.ts ADDED
@@ -0,0 +1,731 @@
1
+ /**
2
+ * Generated avatars — the one generator both tiers use.
3
+ *
4
+ * DiceBear v10 (`@dicebear/core` + one JSON per style from `@dicebear/styles`)
5
+ * is plain data + plain functions: no React, no `useId()`. That is the whole
6
+ * reason it replaced boring-avatars here. The old library's components crash
7
+ * under `react-dom/server` inside a route handler (the route's bundled React
8
+ * and a dynamically-imported `react-dom/server` are two instances, so the hook
9
+ * dispatcher is null), which forced a 300-line hand-port of its geometry just
10
+ * to serve `/api/agents/[id]/avatar`. This module renders identically in a
11
+ * client component and in a route handler, so that port is gone.
12
+ *
13
+ * ONE STYLE, MANY SEEDS. The style is a brain-level appearance choice (see
14
+ * Settings → Appearance); each agent and the owner differ only by seed. A
15
+ * single family reads as one product while still telling every entity apart —
16
+ * six unrelated styles at once just read as noise.
17
+ *
18
+ * COLOUR is a setting — see `AvatarTint`. The default, `mixed`, tints only the
19
+ * BACKGROUND and leaves the artwork its native palette; `native` themes
20
+ * nothing and `theme` repaints every colour group a style exposes. Why `mixed`
21
+ * is the default: boring-avatars' variants were colour-driven (marble, sunset
22
+ * and ring are pure colour blends), so forcing one 5-colour ramp through them
23
+ * collapsed every seed into the same mush — the "blends in, no distinction"
24
+ * problem this replaced. Tinting the background alone keeps a brain on-theme
25
+ * while the artwork goes on carrying the identity.
26
+ *
27
+ * WHY THE JSON IS LAZY. All 50 styles are 2.47 MB of JSON, and an avatar
28
+ * renders in the app shell on EVERY screen — statically importing the set
29
+ * would put all of it in the first load of every page to draw one 32px circle.
30
+ * So each style is a `() => import()` and lands in its own chunk: the app
31
+ * fetches the ONE style the brain uses, once, and the picker (a single
32
+ * settings screen) is the only place that pulls more. The arrow functions are
33
+ * written out per style rather than built from a template literal so both
34
+ * bundlers can see every target statically.
35
+ *
36
+ * LICENCES. Not all styles are CC0 — 14 are CC BY 4.0, which REQUIRES
37
+ * attribution. Every style therefore carries its creator and licence here,
38
+ * the picker shows them at the point of choice, and docs/avatar-styles.md is
39
+ * the human-readable credit list. DiceBear also embeds an RDF credit block in
40
+ * each SVG, which is left intact. `avatar.test.ts` checks this table against
41
+ * the installed package so a version bump cannot silently restate a licence.
42
+ */
43
+
44
+ import { Avatar, Style } from '@dicebear/core';
45
+
46
+ /**
47
+ * What a style is FOR.
48
+ *
49
+ * The catalogue used to be split by look (minimalist / characters / scenes),
50
+ * which described the artwork but not the job. Since the same generator now
51
+ * draws both 32px avatars and full-panel backgrounds (see backdrop.ts), the
52
+ * useful split is by PURPOSE; a portrait makes a poor wallpaper, and a field
53
+ * of waves makes an unrecognisable avatar.
54
+ *
55
+ * `initials`, `initial-face`, `glyphs` and `icons` sit under `avatars` despite
56
+ * being minimal: they encode an identity, which is an avatar's whole job and
57
+ * meaningless spread across a sidebar.
58
+ */
59
+ export type AvatarStyleCategory = 'avatars' | 'backgrounds';
60
+
61
+ export type AvatarStyleMeta = {
62
+ id: string;
63
+ label: string;
64
+ category: AvatarStyleCategory;
65
+ /** Designer, for attribution. Mirrors the style JSON's `meta.creator.name`. */
66
+ creator: string;
67
+ /** SPDX-ish licence name. Mirrors the style JSON's `meta.license.name`. */
68
+ license: string;
69
+ /** Dynamic import of the style's JSON — one chunk per style. */
70
+ load: () => Promise<unknown>;
71
+ };
72
+
73
+ /** Human-readable category names, in picker order. */
74
+ export const AVATAR_CATEGORIES: Array<{ id: AvatarStyleCategory; label: string }> = [
75
+ { id: 'avatars', label: 'Avatars' },
76
+ { id: 'backgrounds', label: 'Backgrounds' },
77
+ ];
78
+
79
+ /** Licences that impose no attribution duty. Anything else is credited in the
80
+ * picker and in docs/avatar-styles.md. */
81
+ const NO_ATTRIBUTION_REQUIRED = new Set(['CC0 1.0', 'MIT']);
82
+
83
+ /** Whether picking this style obliges us to credit its designer. */
84
+ export function requiresAttribution(style: AvatarStyleMeta): boolean {
85
+ return !NO_ATTRIBUTION_REQUIRED.has(style.license);
86
+ }
87
+
88
+ export const AVATAR_STYLES: AvatarStyleMeta[] = [
89
+ // ── avatars ─────────────────────────────────────────────────────
90
+ {
91
+ id: 'glyphs',
92
+ label: 'Glyphs',
93
+ category: 'avatars',
94
+ creator: 'Matt Houser',
95
+ license: 'CC BY 4.0',
96
+ load: () => import('@dicebear/styles/glyphs.json'),
97
+ },
98
+ {
99
+ id: 'icons',
100
+ label: 'Icons',
101
+ category: 'avatars',
102
+ creator: 'The Bootstrap Authors',
103
+ license: 'MIT',
104
+ load: () => import('@dicebear/styles/icons.json'),
105
+ },
106
+ {
107
+ id: 'initial-face',
108
+ label: 'Initial Face',
109
+ category: 'avatars',
110
+ creator: 'DiceBear',
111
+ license: 'CC0 1.0',
112
+ load: () => import('@dicebear/styles/initial-face.json'),
113
+ },
114
+ {
115
+ id: 'initials',
116
+ label: 'Initials',
117
+ category: 'avatars',
118
+ creator: 'DiceBear',
119
+ license: 'CC0 1.0',
120
+ load: () => import('@dicebear/styles/initials.json'),
121
+ },
122
+ {
123
+ id: 'thumbs',
124
+ label: 'Thumbs',
125
+ category: 'avatars',
126
+ creator: 'DiceBear',
127
+ license: 'CC0 1.0',
128
+ load: () => import('@dicebear/styles/thumbs.json'),
129
+ },
130
+ {
131
+ id: 'notionists',
132
+ label: 'Notionists',
133
+ category: 'avatars',
134
+ creator: 'Zoish',
135
+ license: 'CC0 1.0',
136
+ load: () => import('@dicebear/styles/notionists.json'),
137
+ },
138
+ {
139
+ id: 'notionists-neutral',
140
+ label: 'Notionists Neutral',
141
+ category: 'avatars',
142
+ creator: 'Zoish',
143
+ license: 'CC0 1.0',
144
+ load: () => import('@dicebear/styles/notionists-neutral.json'),
145
+ },
146
+ {
147
+ id: 'lorelei',
148
+ label: 'Lorelei',
149
+ category: 'avatars',
150
+ creator: 'Lisa Wischofsky',
151
+ license: 'CC0 1.0',
152
+ load: () => import('@dicebear/styles/lorelei.json'),
153
+ },
154
+ {
155
+ id: 'lorelei-neutral',
156
+ label: 'Lorelei Neutral',
157
+ category: 'avatars',
158
+ creator: 'Lisa Wischofsky',
159
+ license: 'CC0 1.0',
160
+ load: () => import('@dicebear/styles/lorelei-neutral.json'),
161
+ },
162
+ {
163
+ id: 'open-peeps',
164
+ label: 'Open Peeps',
165
+ category: 'avatars',
166
+ creator: 'Pablo Stanley',
167
+ license: 'CC0 1.0',
168
+ load: () => import('@dicebear/styles/open-peeps.json'),
169
+ },
170
+ {
171
+ id: 'pixel-art',
172
+ label: 'Pixel Art',
173
+ category: 'avatars',
174
+ creator: 'DiceBear',
175
+ license: 'CC0 1.0',
176
+ load: () => import('@dicebear/styles/pixel-art.json'),
177
+ },
178
+ {
179
+ id: 'pixel-art-neutral',
180
+ label: 'Pixel Art Neutral',
181
+ category: 'avatars',
182
+ creator: 'DiceBear',
183
+ license: 'CC0 1.0',
184
+ load: () => import('@dicebear/styles/pixel-art-neutral.json'),
185
+ },
186
+ {
187
+ id: 'pixelbot',
188
+ label: 'Pixelbot',
189
+ category: 'avatars',
190
+ creator: 'DiceBear',
191
+ license: 'CC0 1.0',
192
+ load: () => import('@dicebear/styles/pixelbot.json'),
193
+ },
194
+ {
195
+ id: 'bottts',
196
+ label: 'Bottts',
197
+ category: 'avatars',
198
+ creator: 'Pablo Stanley',
199
+ license: 'Free for personal and commercial use',
200
+ load: () => import('@dicebear/styles/bottts.json'),
201
+ },
202
+ {
203
+ id: 'bottts-neutral',
204
+ label: 'Bottts Neutral',
205
+ category: 'avatars',
206
+ creator: 'Pablo Stanley',
207
+ license: 'Free for personal and commercial use',
208
+ load: () => import('@dicebear/styles/bottts-neutral.json'),
209
+ },
210
+ {
211
+ id: 'avataaars',
212
+ label: 'Avataaars',
213
+ category: 'avatars',
214
+ creator: 'Pablo Stanley',
215
+ license: 'Free for personal and commercial use',
216
+ load: () => import('@dicebear/styles/avataaars.json'),
217
+ },
218
+ {
219
+ id: 'avataaars-neutral',
220
+ label: 'Avataaars Neutral',
221
+ category: 'avatars',
222
+ creator: 'Pablo Stanley',
223
+ license: 'Free for personal and commercial use',
224
+ load: () => import('@dicebear/styles/avataaars-neutral.json'),
225
+ },
226
+ {
227
+ id: 'adventurer',
228
+ label: 'Adventurer',
229
+ category: 'avatars',
230
+ creator: 'Lisa Wischofsky',
231
+ license: 'CC BY 4.0',
232
+ load: () => import('@dicebear/styles/adventurer.json'),
233
+ },
234
+ {
235
+ id: 'adventurer-neutral',
236
+ label: 'Adventurer Neutral',
237
+ category: 'avatars',
238
+ creator: 'Lisa Wischofsky',
239
+ license: 'CC BY 4.0',
240
+ load: () => import('@dicebear/styles/adventurer-neutral.json'),
241
+ },
242
+ {
243
+ id: 'big-ears',
244
+ label: 'Big Ears',
245
+ category: 'avatars',
246
+ creator: 'The Visual Team',
247
+ license: 'CC BY 4.0',
248
+ load: () => import('@dicebear/styles/big-ears.json'),
249
+ },
250
+ {
251
+ id: 'big-ears-neutral',
252
+ label: 'Big Ears Neutral',
253
+ category: 'avatars',
254
+ creator: 'The Visual Team',
255
+ license: 'CC BY 4.0',
256
+ load: () => import('@dicebear/styles/big-ears-neutral.json'),
257
+ },
258
+ {
259
+ id: 'big-smile',
260
+ label: 'Big Smile',
261
+ category: 'avatars',
262
+ creator: 'Ashley Seo',
263
+ license: 'CC BY 4.0',
264
+ load: () => import('@dicebear/styles/big-smile.json'),
265
+ },
266
+ {
267
+ id: 'clay',
268
+ label: 'Clay',
269
+ category: 'avatars',
270
+ creator: 'DiceBear',
271
+ license: 'CC0 1.0',
272
+ load: () => import('@dicebear/styles/clay.json'),
273
+ },
274
+ {
275
+ id: 'critters',
276
+ label: 'Critters',
277
+ category: 'avatars',
278
+ creator: 'DiceBear',
279
+ license: 'CC0 1.0',
280
+ load: () => import('@dicebear/styles/critters.json'),
281
+ },
282
+ {
283
+ id: 'croodles',
284
+ label: 'Croodles',
285
+ category: 'avatars',
286
+ creator: 'vijay verma',
287
+ license: 'CC BY 4.0',
288
+ load: () => import('@dicebear/styles/croodles.json'),
289
+ },
290
+ {
291
+ id: 'croodles-neutral',
292
+ label: 'Croodles Neutral',
293
+ category: 'avatars',
294
+ creator: 'vijay verma',
295
+ license: 'CC BY 4.0',
296
+ load: () => import('@dicebear/styles/croodles-neutral.json'),
297
+ },
298
+ {
299
+ id: 'dylan',
300
+ label: 'Dylan',
301
+ category: 'avatars',
302
+ creator: 'Natalia Spivak',
303
+ license: 'CC BY 4.0',
304
+ load: () => import('@dicebear/styles/dylan.json'),
305
+ },
306
+ {
307
+ id: 'fun-emoji',
308
+ label: 'Fun Emoji',
309
+ category: 'avatars',
310
+ creator: 'Davis Uche',
311
+ license: 'CC BY 4.0',
312
+ load: () => import('@dicebear/styles/fun-emoji.json'),
313
+ },
314
+ {
315
+ id: 'micah',
316
+ label: 'Micah',
317
+ category: 'avatars',
318
+ creator: 'Micah Lanier',
319
+ license: 'CC BY 4.0',
320
+ load: () => import('@dicebear/styles/micah.json'),
321
+ },
322
+ {
323
+ id: 'miniavs',
324
+ label: 'Miniavs',
325
+ category: 'avatars',
326
+ creator: 'Webpixels',
327
+ license: 'CC BY 4.0',
328
+ load: () => import('@dicebear/styles/miniavs.json'),
329
+ },
330
+ {
331
+ id: 'moods',
332
+ label: 'Moods',
333
+ category: 'avatars',
334
+ creator: 'DiceBear',
335
+ license: 'CC0 1.0',
336
+ load: () => import('@dicebear/styles/moods.json'),
337
+ },
338
+ {
339
+ id: 'personas',
340
+ label: 'Personas',
341
+ category: 'avatars',
342
+ creator: 'Draftbit - draftbit.com',
343
+ license: 'CC BY 4.0',
344
+ load: () => import('@dicebear/styles/personas.json'),
345
+ },
346
+ {
347
+ id: 'sprouts',
348
+ label: 'Sprouts',
349
+ category: 'avatars',
350
+ creator: 'DiceBear',
351
+ license: 'CC0 1.0',
352
+ load: () => import('@dicebear/styles/sprouts.json'),
353
+ },
354
+ {
355
+ id: 'toon-head',
356
+ label: 'Toon Head',
357
+ category: 'avatars',
358
+ creator: 'Johan Melin',
359
+ license: 'CC BY 4.0',
360
+ load: () => import('@dicebear/styles/toon-head.json'),
361
+ },
362
+ // ── backgrounds ─────────────────────────────────────────────────
363
+ {
364
+ id: 'shapes',
365
+ label: 'Shapes',
366
+ category: 'backgrounds',
367
+ creator: 'DiceBear',
368
+ license: 'CC0 1.0',
369
+ load: () => import('@dicebear/styles/shapes.json'),
370
+ },
371
+ {
372
+ id: 'identicon',
373
+ label: 'Identicon',
374
+ category: 'backgrounds',
375
+ creator: 'DiceBear',
376
+ license: 'CC0 1.0',
377
+ load: () => import('@dicebear/styles/identicon.json'),
378
+ },
379
+ {
380
+ id: 'loops',
381
+ label: 'Loops',
382
+ category: 'backgrounds',
383
+ creator: 'DiceBear',
384
+ license: 'CC0 1.0',
385
+ load: () => import('@dicebear/styles/loops.json'),
386
+ },
387
+ {
388
+ id: 'rings',
389
+ label: 'Rings',
390
+ category: 'backgrounds',
391
+ creator: 'DiceBear',
392
+ license: 'CC0 1.0',
393
+ load: () => import('@dicebear/styles/rings.json'),
394
+ },
395
+ {
396
+ id: 'squircles',
397
+ label: 'Squircles',
398
+ category: 'backgrounds',
399
+ creator: 'DiceBear',
400
+ license: 'CC0 1.0',
401
+ load: () => import('@dicebear/styles/squircles.json'),
402
+ },
403
+ {
404
+ id: 'shape-grid',
405
+ label: 'Shape Grid',
406
+ category: 'backgrounds',
407
+ creator: 'DiceBear',
408
+ license: 'CC0 1.0',
409
+ load: () => import('@dicebear/styles/shape-grid.json'),
410
+ },
411
+ {
412
+ id: 'glass',
413
+ label: 'Glass',
414
+ category: 'backgrounds',
415
+ creator: 'DiceBear',
416
+ license: 'CC0 1.0',
417
+ load: () => import('@dicebear/styles/glass.json'),
418
+ },
419
+ {
420
+ id: 'blobs',
421
+ label: 'Blobs',
422
+ category: 'backgrounds',
423
+ creator: 'DiceBear',
424
+ license: 'CC0 1.0',
425
+ load: () => import('@dicebear/styles/blobs.json'),
426
+ },
427
+ {
428
+ id: 'disco',
429
+ label: 'Disco',
430
+ category: 'backgrounds',
431
+ creator: 'DiceBear',
432
+ license: 'CC0 1.0',
433
+ load: () => import('@dicebear/styles/disco.json'),
434
+ },
435
+ {
436
+ id: 'stripes',
437
+ label: 'Stripes',
438
+ category: 'backgrounds',
439
+ creator: 'DiceBear',
440
+ license: 'CC0 1.0',
441
+ load: () => import('@dicebear/styles/stripes.json'),
442
+ },
443
+ {
444
+ id: 'triangles',
445
+ label: 'Triangles',
446
+ category: 'backgrounds',
447
+ creator: 'DiceBear',
448
+ license: 'CC0 1.0',
449
+ load: () => import('@dicebear/styles/triangles.json'),
450
+ },
451
+ {
452
+ id: 'waves',
453
+ label: 'Waves',
454
+ category: 'backgrounds',
455
+ creator: 'DiceBear',
456
+ license: 'CC0 1.0',
457
+ load: () => import('@dicebear/styles/waves.json'),
458
+ },
459
+ {
460
+ id: 'weave',
461
+ label: 'Weave',
462
+ category: 'backgrounds',
463
+ creator: 'DiceBear',
464
+ license: 'CC0 1.0',
465
+ load: () => import('@dicebear/styles/weave.json'),
466
+ },
467
+ {
468
+ id: 'constellation',
469
+ label: 'Constellation',
470
+ category: 'backgrounds',
471
+ creator: 'DiceBear',
472
+ license: 'CC0 1.0',
473
+ load: () => import('@dicebear/styles/constellation.json'),
474
+ },
475
+ {
476
+ id: 'landscape',
477
+ label: 'Landscape',
478
+ category: 'backgrounds',
479
+ creator: 'DiceBear',
480
+ license: 'CC0 1.0',
481
+ load: () => import('@dicebear/styles/landscape.json'),
482
+ },
483
+ {
484
+ id: 'planets',
485
+ label: 'Planets',
486
+ category: 'backgrounds',
487
+ creator: 'DiceBear',
488
+ license: 'CC0 1.0',
489
+ load: () => import('@dicebear/styles/planets.json'),
490
+ },
491
+ ];
492
+ export const AVATAR_STYLE_IDS: readonly string[] = AVATAR_STYLES.map((s) => s.id);
493
+
494
+ /** The styles the AVATAR picker offers. */
495
+ export const AVATAR_PICKER_STYLES: AvatarStyleMeta[] = AVATAR_STYLES.filter(
496
+ (s) => s.category === 'avatars',
497
+ );
498
+
499
+ /** The styles a BACKGROUND picker offers. */
500
+ export const BACKGROUND_STYLES: AvatarStyleMeta[] = AVATAR_STYLES.filter(
501
+ (s) => s.category === 'backgrounds',
502
+ );
503
+
504
+ /**
505
+ * Was `shapes`, which is now a background. Nothing was migrated: a brain that
506
+ * explicitly saved a style keeps it, and `resolveAvatarStyle` still resolves
507
+ * every id in the registry regardless of category, rendering never cared about
508
+ * the split. Only a brain that never chose one moves, and it moves to a style
509
+ * that is actually an avatar.
510
+ */
511
+ export const DEFAULT_AVATAR_STYLE = 'thumbs';
512
+
513
+ /**
514
+ * How much of the theme an avatar takes on.
515
+ *
516
+ * - `native` — the style's own palette, untouched. Loudest and most
517
+ * distinguishable, but ignores the brain's theme entirely.
518
+ * - `mixed` — the theme tints the BACKGROUND; the artwork keeps its native
519
+ * colours. On-theme without flattening the artwork, which is why it is the
520
+ * default: it is the setting that made seeds tell apart again.
521
+ * - `theme` — the ramp is pushed into every colour group the style exposes.
522
+ * Most on-theme, least varied; character styles expose no groups, so for
523
+ * them this is identical to `mixed`.
524
+ */
525
+ export type AvatarTint = 'native' | 'mixed' | 'theme';
526
+
527
+ export const AVATAR_TINTS: Array<{ id: AvatarTint; label: string; hint: string }> = [
528
+ { id: 'native', label: 'Original', hint: 'The style’s own colours' },
529
+ { id: 'mixed', label: 'Mixed', hint: 'Themed background, original artwork' },
530
+ { id: 'theme', label: 'Theme', hint: 'Theme colours throughout' },
531
+ ];
532
+
533
+ export const DEFAULT_AVATAR_TINT: AvatarTint = 'mixed';
534
+
535
+ export function resolveAvatarTint(v: string | null | undefined): AvatarTint {
536
+ return v === 'native' || v === 'theme' || v === 'mixed' ? v : DEFAULT_AVATAR_TINT;
537
+ }
538
+
539
+ /** boring-avatars variant → nearest shipped style. Stored avatars carry the old
540
+ * ids, so they are translated on READ rather than migrated: nobody's avatar
541
+ * vanishes, and re-saving is not required to get a valid one. */
542
+ const LEGACY_STYLES: Record<string, string> = {
543
+ beam: 'thumbs',
544
+ bauhaus: 'shapes',
545
+ geometric: 'thumbs', // deprecated boring-avatars alias
546
+ abstract: 'shapes', // deprecated boring-avatars alias
547
+ marble: 'glass',
548
+ sunset: 'glass',
549
+ pixel: 'identicon',
550
+ ring: 'rings',
551
+ };
552
+
553
+ /** Resolve any stored style id — current, legacy, empty or unknown — to a
554
+ * style that exists. Never throws; unknown ids fall back to the default. */
555
+ export function resolveAvatarStyle(id: string | null | undefined): string {
556
+ if (!id) return DEFAULT_AVATAR_STYLE;
557
+ if (AVATAR_STYLE_IDS.includes(id)) return id;
558
+ return LEGACY_STYLES[id] ?? DEFAULT_AVATAR_STYLE;
559
+ }
560
+
561
+ export function avatarStyleMeta(id: string): AvatarStyleMeta {
562
+ const resolved = resolveAvatarStyle(id);
563
+ return AVATAR_STYLES.find((s) => s.id === resolved) ?? AVATAR_STYLES[0]!;
564
+ }
565
+
566
+ /** A parsed style, the colour groups `theme` tint may safely override, and the
567
+ * component variants the style declares. */
568
+ export type Loaded = {
569
+ style: Style;
570
+ tintGroups: string[];
571
+ /** component name → the variant names it offers, e.g. `rotation` →
572
+ * `['quarter', 'none', 'free']`. Empty for styles that declare none. */
573
+ variants: Record<string, string[]>;
574
+ };
575
+
576
+ /**
577
+ * Which of a style's declared colour groups the `theme` tint is allowed to
578
+ * repaint.
579
+ *
580
+ * Groups carrying `contrastTo` are EXCLUDED, and that exclusion is the whole
581
+ * reason this function exists. Those groups are not decoration — they are the
582
+ * legible part drawn ON another group, and DiceBear solves them to black or
583
+ * white for contrast: `initials` text on its background, the `icons` glyph,
584
+ * `thumbs`' eyes and mouth. Painting them from the same 5-colour ramp as the
585
+ * surface behind them is a coin-flip on whether the avatar still has a face.
586
+ *
587
+ * `background` is excluded too, but only because it is already covered by the
588
+ * core `backgroundColor` option every tint above `native` passes.
589
+ */
590
+ function tintableGroups(json: unknown): string[] {
591
+ const colors = (json as { colors?: Record<string, unknown> } | null)?.colors;
592
+ if (!colors) return [];
593
+ return Object.entries(colors)
594
+ .filter(([name, spec]) => {
595
+ if (name === 'background') return false;
596
+ return !(spec && typeof spec === 'object' && 'contrastTo' in spec);
597
+ })
598
+ .map(([name]) => name);
599
+ }
600
+
601
+ /**
602
+ * The component variants a style declares, as `{ component: [variant, …] }`.
603
+ *
604
+ * DiceBear validates options against a fixed set of patterns and THROWS on an
605
+ * unknown key, so `rotationVariant` may only be passed to a style that actually
606
+ * has a `rotation` component. Reading the declaration is the only way to know;
607
+ * guessing by style id would break the moment a style is added or renamed.
608
+ */
609
+ function componentVariants(json: unknown): Record<string, string[]> {
610
+ const components = (json as { components?: Record<string, unknown> } | null)?.components;
611
+ if (!components) return {};
612
+ const out: Record<string, string[]> = {};
613
+ for (const [name, spec] of Object.entries(components)) {
614
+ const variants = (spec as { variants?: Record<string, unknown> } | null)?.variants;
615
+ if (variants) out[name] = Object.keys(variants);
616
+ }
617
+ return out;
618
+ }
619
+
620
+ // `Style` parses and validates the JSON once; building one per render would
621
+ // re-do that for every avatar in a list. Populated by loadAvatarStyle and kept
622
+ // for the life of the process — a style is a few hundred KB at most and the
623
+ // app realistically touches one.
624
+ const STYLES = new Map<string, Loaded>();
625
+ const INFLIGHT = new Map<string, Promise<Loaded>>();
626
+
627
+ /** Whether `renderAvatarSvgSync` can draw this style right now (its JSON chunk
628
+ * has already been fetched). React uses this to decide whether to await. */
629
+ export function isAvatarStyleReady(id: string | null | undefined): boolean {
630
+ return STYLES.has(resolveAvatarStyle(id));
631
+ }
632
+
633
+ /** The parsed style, if its JSON chunk has already been fetched. Lets callers
634
+ * that render something other than an avatar (the backdrop) reuse this
635
+ * module's one cache rather than parsing the same JSON a second time. */
636
+ export function loadedAvatarStyle(id: string | null | undefined): Loaded | null {
637
+ return STYLES.get(resolveAvatarStyle(id)) ?? null;
638
+ }
639
+
640
+ /** Fetch and parse a style's JSON. Idempotent, and concurrent calls for the
641
+ * same style share one request. Rejects only if the chunk itself fails to
642
+ * load, which callers treat as "draw nothing" rather than an error state. */
643
+ export function loadAvatarStyle(id: string | null | undefined): Promise<Loaded> {
644
+ const key = resolveAvatarStyle(id);
645
+ const ready = STYLES.get(key);
646
+ if (ready) return Promise.resolve(ready);
647
+ const pending = INFLIGHT.get(key);
648
+ if (pending) return pending;
649
+ const p = avatarStyleMeta(key)
650
+ .load()
651
+ .then((mod) => {
652
+ // A JSON module is the object itself under CJS interop and under
653
+ // `.default` as ESM; both bundlers here have produced each at times.
654
+ const json = (mod as { default?: unknown }).default ?? mod;
655
+ const loaded: Loaded = {
656
+ style: new Style(json as ConstructorParameters<typeof Style>[0]),
657
+ tintGroups: tintableGroups(json),
658
+ variants: componentVariants(json),
659
+ };
660
+ STYLES.set(key, loaded);
661
+ INFLIGHT.delete(key);
662
+ return loaded;
663
+ })
664
+ .catch((err) => {
665
+ INFLIGHT.delete(key);
666
+ throw err;
667
+ });
668
+ INFLIGHT.set(key, p);
669
+ return p;
670
+ }
671
+
672
+ /** DiceBear validates colours as hex and REJECTS anything else (it throws on
673
+ * `oklch(...)`). Every token in themes.css is authored/emitted as hex, so the
674
+ * theme ramp passes straight through — but a caller reading a live CSS custom
675
+ * property is one theme edit away from handing us something else, and an
676
+ * avatar must never be the thing that throws. Drop what wouldn't validate. */
677
+ const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
678
+ function hexOnly(colors: readonly string[] | undefined): string[] | undefined {
679
+ if (!colors?.length) return undefined;
680
+ const ok = colors.map((c) => c.trim()).filter((c) => HEX.test(c));
681
+ return ok.length ? ok : undefined;
682
+ }
683
+
684
+ export type RenderAvatarOptions = {
685
+ /** Stored style id; legacy and unknown ids are resolved, not rejected. */
686
+ style?: string | null;
687
+ /** Stable per-entity seed — the same seed always yields the same avatar. */
688
+ seed: string;
689
+ /** Rendered px. Sets the root svg width/height; the viewBox scales. */
690
+ size?: number;
691
+ /** The theme's chart ramp, as hex. Ignored when tint is `native`. */
692
+ ramp?: readonly string[];
693
+ /** Defaults to `mixed`. */
694
+ tint?: AvatarTint;
695
+ };
696
+
697
+ function draw(loaded: Loaded, { seed, size = 40, ramp, tint }: RenderAvatarOptions): string {
698
+ const colors = hexOnly(ramp);
699
+ const opts: Record<string, unknown> = { seed: seed || 'mantle', size };
700
+ const mode = resolveAvatarTint(tint);
701
+ if (colors && mode !== 'native') {
702
+ // `backgroundColor` is a CORE option, so it lands on every style — including
703
+ // the character styles, which declare no colour groups at all. Without it,
704
+ // theming would be a silent no-op for most of the set.
705
+ opts.backgroundColor = colors;
706
+ if (mode === 'theme') {
707
+ for (const g of loaded.tintGroups) opts[`${g}Color`] = colors;
708
+ }
709
+ }
710
+ return new Avatar(loaded.style, opts).toString();
711
+ }
712
+
713
+ /** Render an avatar SYNCHRONOUSLY. Returns null when the style's JSON has not
714
+ * been fetched yet — call `loadAvatarStyle` and render again. This shape
715
+ * exists for React, which cannot await inside a render. */
716
+ export function renderAvatarSvgSync(opts: RenderAvatarOptions): string | null {
717
+ const loaded = STYLES.get(resolveAvatarStyle(opts.style));
718
+ return loaded ? draw(loaded, opts) : null;
719
+ }
720
+
721
+ /** Render an avatar, fetching the style first if needed. The natural form
722
+ * outside React — used by the agent-avatar route. Pure and deterministic:
723
+ * same inputs, same bytes, in the browser and on the server alike. */
724
+ export async function renderAvatarSvg(opts: RenderAvatarOptions): Promise<string> {
725
+ return draw(await loadAvatarStyle(opts.style), opts);
726
+ }
727
+
728
+ /** A short, stable seed for a brand-new avatar. */
729
+ export function randomAvatarSeed(): string {
730
+ return Math.random().toString(36).slice(2, 10);
731
+ }