@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.
- package/LICENSE.md +135 -0
- package/package.json +68 -0
- package/src/app-bridge-protocol.ts +115 -0
- package/src/app-presenter.tsx +25 -0
- package/src/app-sandbox.tsx +552 -0
- package/src/appearance.ts +192 -0
- package/src/avatar.test.ts +229 -0
- package/src/avatar.ts +731 -0
- package/src/backgrounds.test.ts +119 -0
- package/src/backgrounds.ts +118 -0
- package/src/draw-presenter.tsx +39 -0
- package/src/event-presenter.tsx +62 -0
- package/src/file-presenter.tsx +76 -0
- package/src/formula-calculator.tsx +209 -0
- package/src/formula-presenter.test.ts +128 -0
- package/src/formula-presenter.tsx +301 -0
- package/src/help-topics.ts +104 -0
- package/src/lib/ink-audit.test.ts +314 -0
- package/src/lib/theme-css-blocks.ts +26 -0
- package/src/lib/theme-generator.test.ts +179 -0
- package/src/lib/theme-registry.gen.ts +352 -0
- package/src/lib/themes.test.ts +308 -0
- package/src/lib/themes.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/nav-items.ts +225 -0
- package/src/note-presenter.tsx +14 -0
- package/src/page-outline.tsx +127 -0
- package/src/table-presenter.tsx +226 -0
- package/src/task-presenter.tsx +60 -0
- package/src/ui/button.tsx +50 -0
- package/src/ui/input.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/view-payload.ts +82 -0
- package/styles/app.css +1098 -0
- package/styles/themes.css +6198 -0
- package/themes/generate.d.mts +11 -0
- package/themes/generate.mjs +618 -0
- package/themes/model.d.mts +24 -0
- package/themes/model.mjs +213 -0
- package/themes/preview.html +145 -0
- package/themes/seeds.d.mts +16 -0
- package/themes/seeds.mjs +3694 -0
- package/tsconfig.json +15 -0
- 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
|
+
}
|