@usenavii/core 0.5.0 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -6,6 +6,175 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.26.1] - 2026-05-31
10
+
11
+ ### Changed (docs)
12
+
13
+ - Removed Gravatar comparisons from user-facing docs (README, package READMEs, `/docs/recipes`, `/docs/sdk-core`). Functionality unchanged — `seedFromEmail()` still hashes `sha256(email.trim().toLowerCase())`, so cross-product seed parity still holds for any caller using the same scheme.
14
+
15
+ ## [0.26.0] - 2026-05-30
16
+
17
+ ### Added (`@usenavii/core` 0.7.0)
18
+
19
+ - **`seedFromEmail(email)`** — Gravatar-compatible seed helper. Returns `sha256` hex of the trimmed + lowercased email so the raw address never reaches URLs, server access logs, `Referer` headers, browser history, CDN cache keys, or analytics pixels. Two services hashing the same email produce the same seed → drop-in compatible with Gravatar's lookup scheme.
20
+ - **`normalizeEmail(email)`** — exported canonicalization step (trim + lowercase + NFC) for callers who need to reproduce the form before hashing.
21
+ - **`sha256Hex(input)`** — sync SHA-256 (FIPS 180-4) primitive used by `seedFromEmail`. Pure JS, no deps; available for callers that want to hash other inputs in the same scheme.
22
+ - **`SeedOptions`** type + `hashEmail` option on `Navii.seed()`.
23
+
24
+ ### Changed (`@usenavii/core` 0.7.0) — breaking
25
+
26
+ - `Navii.seed({ email })` now returns `sha256(normalizeEmail(email))` instead of the raw address. **Migration:** if your existing avatars were keyed on raw emails and you need them to stay stable, pass `{ hashEmail: false }` until you can re-key. New deployments should leave the default.
27
+ - `Navii.seedFromEmail` exposed on the `Navii` namespace.
28
+
29
+ ### Added (`@usenavii/react` 0.8.0)
30
+
31
+ - Re-exports `seed`, `seedFromEmail`, `normalizeEmail`, `SeedFields`, `SeedOptions` from `@usenavii/core` so `<Navii seed={seedFromEmail(user.email)} />` works without a second import.
32
+
33
+ ### Added (API host)
34
+
35
+ - `/avatar/:seed` sets `x-navii-warning: plaintext-email-seed; hash with seedFromEmail()` when the seed matches an email pattern. Render still succeeds — the header is a client-side nudge. Also logged at `warn` level for ops visibility.
36
+
37
+ ### Changed (API host)
38
+
39
+ - `/docs/recipes` gets a new "Using emails as seeds (Gravatar-style)" section.
40
+ - `/docs/sdk-core#seed` documents `seedFromEmail`, `normalizeEmail`, and the `hashEmail` option on `Navii.seed()`.
41
+ - `/docs/http-api#headers` documents the new `x-navii-warning` response header.
42
+
43
+ ## [0.25.2] - 2026-05-28
44
+
45
+ ### Changed (API host)
46
+ - Landing hero CTAs reworked. New triple: `Install Navii →` (primary, scrolls to install snippets), `Get Figma Plugin →` (secondary, opens Figma community), `Try the Builder` (tertiary text link → `/builder`). Old single-purpose pair (`Try it` + `Customize a face`) replaced — plugin is now visible from the hero.
47
+ - `/docs/sdk-core` SDK options table picks up `packs` + `style` rows (matches the engine surface that's been live since v0.23.0).
48
+
49
+ ## [0.25.1] - 2026-05-27
50
+
51
+ ### Added (API host)
52
+ - **`/avatar/:seed?packs=…&style=…`** — `/avatar/:seed` now parses `packs` (comma-separated pack ids) and `style` (`masc | femme | neutral`) query params. Unknown pack ids are silently skipped (engine ignores them). Pack order does not affect cached output — the PNG cache key normalizes by sorting.
53
+
54
+ ### Fixed (API host)
55
+ - PNG cache key extended to include `packs` + `style` so cached renders no longer collide across different pack/style combinations of the same seed.
56
+
57
+ ### Changed (API host)
58
+ - `/docs/http-api` documents the new `packs` and `style` rows in the `/avatar/:seed` query table, plus new example URLs (`?packs=halloween`, `?packs=office,mono&style=neutral`).
59
+ - `/random` inherits the new params (it forwards every `/avatar/:seed` query through unchanged).
60
+
61
+ ### Notes
62
+ - Required by the upcoming Figma plugin update (fixes fill-mode rendering — plugin was sending the right options to the main thread but `buildUrl()` was stripping `packs`/`mood`/`style` before the HTTP request).
63
+ - Backward compatible. Existing `<img src="https://api.navii.dev/avatar/alice">` URLs unchanged.
64
+
65
+ ## [0.25.0] - 2026-05-27
66
+
67
+ ### Added (`@usenavii/react` 0.7.0)
68
+ - **`<NaviiGroup>`** — overlapping avatar stack, thin React wrapper around `@usenavii/core`'s `renderGroup()`. Props: `seeds`, `size`, `overlap`, `max` (overflow → `+N` counter tile), `ring`, `tileBg`, `counterFill`, `counterInk`, plus all per-tile options (`paletteId`, `palette`, `mood`, `background`, `animated`, `styleHint`). `<img>` width is computed from `size + overlap + max` so layout is stable on load.
69
+ - `renderGroup` + `GroupOptions` re-exported from `@usenavii/react`.
70
+
71
+ ### Changed (`@usenavii/core`)
72
+ - No source changes. Stays at `0.6.0` — react `0.7.0` ships independently. Lockstep convention relaxed when only one package has source changes.
73
+
74
+ ### Changed (tooling)
75
+ - `scripts/release-audit.mjs` — core/react version mismatch downgraded from `error` to `warn`. Lockstep stays the default expectation, but the audit no longer forces a no-op publish on the unchanged package.
76
+
77
+ ### Added (API host)
78
+ - `/docs/sdk-react` documents `<NaviiGroup>` props + behavior, plus the new `renderGroup`/`GroupOptions` re-exports.
79
+
80
+ ## [0.24.2] - 2026-05-27
81
+
82
+ ### Added (API host)
83
+ - The Skill Club and ForYu logos in the landing "built with navii" wall.
84
+
85
+ ## [0.24.1] - 2026-05-27
86
+
87
+ ### Added (Figma plugin)
88
+ - **Sign out of Pro** — Pro pill in header now opens upgrade modal even when Pro. Modal footer shows the signed-in email + a Sign-out button that clears the cached license via `license-clear`. Lets users test the free-tier flow on the same device without losing access (the underlying Polar license is unchanged).
89
+
90
+ ### Changed (Figma plugin)
91
+ - Pro pill click now always opens the upgrade modal (was: free-only). Pro view exposes account info + sign-out.
92
+ - Footer (Insert / Fill random) hidden on **Packs** and **Mascots** tabs — those are browsing surfaces with their own inline actions (Enable button in pack-modal, card-click action modal in Mascots). Footer remains on Seed + Build where Insert is the primary CTA.
93
+
94
+ ### Fixed (Figma plugin)
95
+ - Pro user's usage chip stayed stuck at "10 of 10 left today" on plugin open due to a race between `usage-get` (UI request) and `doLicenseRestore` (main thread state). Restore now pushes a fresh usage snapshot via `doUsageGet()` after setting `cachedLicenseOk`, so the chip flips to "Pro · Unlimited" reliably.
96
+
97
+ ## [0.24.0] - 2026-05-27
98
+
99
+ ### Added (API host)
100
+ - **Per-release OG cards** — `GET /og/blog/v<X.Y.Z>.png` composes a 1200×630 card for each minor+ release: dark radial background, hero avatar (deterministic from `navii <version>` seed + `mood: happy`, transparent so the mascot floats on the gradient), version pill, headline parsed from CHANGELOG, date, and `navii.dev/blog` brand mark. Cached per version. `/blog/v<X.Y.Z>` now sets `og:image` + `twitter:image` to this URL so social previews show the release-specific card instead of the generic landing OG image.
101
+
102
+ ## [0.23.6] - 2026-05-27
103
+
104
+ ### Removed (API host)
105
+ - Ghana Duty logo from the landing "built with navii" wall (asset + markup). Wall keeps Elorm UI, Golly Express, Fleetlinq.
106
+
107
+ ## [0.23.5] - 2026-05-27
108
+
109
+ ### Added (API host)
110
+ - Ghana Duty logo (PNG) in the landing "built with navii" wall, alongside Elorm UI.
111
+
112
+ ## [0.23.4] - 2026-05-26
113
+
114
+ ### Fixed (API tests)
115
+ - `license/verify` "route not mounted" test expected `404` but the app's catchall `notFound` handler returns `302 → /`. Test updated to match actual behavior. v0.23.3 release pipeline failed on this stale expectation; v0.23.4 reships the `/blog` timeline with the fix.
116
+
117
+ ## [0.23.3] - 2026-05-26 (yanked — test pipeline failed)
118
+
119
+ ### Added (API host)
120
+ - **`/blog` — release timeline.** New page parses `CHANGELOG.md` at request time, surfaces minor+ releases (`x.y.0`) only, links to GitHub release notes per entry. Per-release permalinks at `/blog/v<x.y.z>`. Hero avatar per release derived from the version string. Sitemap includes every release URL. Linked from landing + docs nav.
121
+
122
+ ## [0.23.2] - 2026-05-26
123
+
124
+ ### Changed (docs)
125
+ - Public READMEs (root, `@usenavii/core`, `@usenavii/react`) and the live API reference at `/docs/http-api` + `/docs/sdk-core` now document `AvatarOptions.mood`, the new `Palette` object override on `build()`, and the `?mood=` query param on `/avatar/:seed`. React README memo-deps line corrected to match source (`mood`, `palette`, `styleHint` in; stale `tileBg` claim out).
126
+ - Chore release — image rebuild deploys the updated landing page and `/docs/*` pages to `api.navii.dev`. No npm package contents changed (publish step no-ops on existing versions).
127
+
128
+ ## [0.23.1] - 2026-05-26
129
+
130
+ ### Fixed (release)
131
+ - `@usenavii/react` workspace dep on `@usenavii/core` updated to `^0.6.0` — v0.23.0 release workflow failed pnpm install with `ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE` because the workspace spec was stuck at `^0.5.0`. v0.23.1 reships the v0.23.0 feature set (mood overlay + runtime palette injection) with the dep bump.
132
+
133
+ ## [0.23.0] - 2026-05-26 (yanked — release pipeline failed)
134
+
135
+ ### Added (`@usenavii/core` 0.6.0, `@usenavii/react` 0.6.0)
136
+ - **`AvatarOptions.mood`** — new `MoodId = 'neutral' | 'happy' | 'serious' | 'sleepy' | 'wink'`. Overrides seed-derived eyes + mouth with a curated pair: `happy` → wide + smile, `serious` → squint + flat, `sleepy` → sleepy + dot, `wink` → wink + smirk. Same seed + same mood = byte-identical render. Different mood on the same seed shares body / palette / topper. Bypasses pack pick constraints by design (the mood IS the override). `neutral` (or undefined) preserves prior behavior.
137
+ - **Runtime palette injection in `build()`** — `options.palette` (Palette object) now wins over `spec.palette` (id). Lets callers pass a brand or runtime-built palette without registering it in `PALETTES`. Fall-through: `options.palette` → `spec.palette` id → `PALETTES[0]`.
138
+ - **React `<Navii>`** forwards new `mood` and `palette` props through to the engine; `MoodId` re-exported from the React package.
139
+
140
+ ### Added (API host)
141
+ - `GET /avatar/:seed?mood=happy|serious|sleepy|wink|neutral` — server-side mood overlay. PNG cache key extended with `m=` so moods don't collide.
142
+ - Elorm UI logo (inline SVG, `currentColor`) in the landing "built with navii" wall, sized via new `.logo svg.lg` rule.
143
+
144
+ ## [0.22.4] - 2026-05-26
145
+
146
+ ### Fixed (Figma plugin)
147
+ - **Missing `permissions: ["teamlibrary"]`** in `manifest.json`. Without it, `figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync()` throws and Brand mode silently returns an empty palette list for every Pro user. Added.
148
+ - **`innerHTML` interpolation** of `pack.name` and `pack.emoji` replaced with `createElement` + `textContent` (ui.ts, active-packs chip + pack-card title). XSS surface closed even though current pack data is static.
149
+ - **`doInsertBuild` new-node path** now wrapped in try/catch — uncaught throw from `figma.createNodeFromSvg` would otherwise kill the plugin's main thread.
150
+
151
+ ### Removed (Figma plugin)
152
+ - **Cmd+Shift+P dev-bypass shortcut** that unlocked Pro features for free. Violated Figma's review policy (hidden functionality circumventing fees). Deleted entirely; only the Cmd+Enter primary-action shortcut remains.
153
+
154
+ ### Changed (Figma plugin)
155
+ - **License key no longer crosses the iframe boundary.** Main thread sends a sanitized `publicLicenseView()` over `figma.ui.postMessage`. The raw key stays in the main thread (needed for re-verify) + `figma.clientStorage` only. UI never sees or stores the key string.
156
+
157
+ ### Fixed (build pipeline)
158
+ - **`scripts/build.mjs`** stopped escaping `<script` (without `/`). Per HTML5 only `</script` ends a script block; the extra replace could corrupt minified regex/string literals containing the substring. Only `</script` is escaped now.
159
+
160
+ ## [0.22.3] - 2026-05-26
161
+
162
+ ### Fixed (API host)
163
+ - `/license/verify` blocked by CORS when called from the Figma plugin iframe (`Origin: null` preflight). Added permissive CORS middleware (route only accepts a license key — no cookies/auth — so wide-open CORS is safe) plus an `OPTIONS` preflight handler. Plugin verify flow now reaches the API end-to-end.
164
+
165
+ ## [0.22.2] - 2026-05-26
166
+
167
+ ### Added (API host)
168
+ - `GET /thanks` — post-purchase confirmation page Polar redirects buyers to after successful checkout. Confirms payment, points them to their email for the license key, and links to the Polar customer portal via the one-time `customer_session_token` query param.
169
+
170
+ ### Fixed (API host)
171
+ - Buyers hit a 404 at `https://navii.dev/thanks` after paying because no route existed yet.
172
+
173
+ ## [0.22.1] - 2026-05-26
174
+
175
+ ### Changed (API host)
176
+ - Support email switched from `tsormed@gmail.com` to `support@navii.dev` (ImprovMX free-tier forward → tsormed@gmail.com).
177
+
9
178
  ## [0.22.0] - 2026-05-26
10
179
 
11
180
  ### Added (`@usenavii/core` 0.5.0)
@@ -29,7 +198,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
29
198
  - Mono switched to full-bleed squircle (was contained orb) for editorial tile look.
30
199
 
31
200
  ### Added (API host)
32
- - **Polar.sh license verification** — `POST /license/verify` now proxies to Polar's `/v1/customer-portal/license-keys/validate` (replaces Gumroad). Validates `status === 'granted'`, expiry, and optional benefit-id match.
201
+ - **Polar.sh license verification** — `POST /license/verify` now proxies to Polar's `/v1/customer-portal/license-keys/validate` (replaces Polar.sh). Validates `status === 'granted'`, expiry, and optional benefit-id match.
33
202
  - **`GET /checkout`** — redirects to Polar checkout w/ configured product preselected. Powered by `@polar-sh/hono`.
34
203
  - **`GET /portal`** — Polar customer portal proxy (license re-fetch, refund request).
35
204
  - **`POST /polar/webhooks`** — signature-verified webhook receiver, logs events.
@@ -38,12 +207,12 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
38
207
  - 8 new license unit tests w/ fetch mock covering granted/revoked/expired/wrong-product/upstream-error paths.
39
208
 
40
209
  ### Changed (API host)
41
- - Privacy page swapped Gumroad references for Polar.sh.
42
- - `AppOptions` renamed `gumroadProductPermalink` → `polarOrganizationId` + related Polar fields.
210
+ - Privacy page swapped Polar.sh references for Polar.sh.
211
+ - `AppOptions` renamed `Polar.shProductPermalink` → `polarOrganizationId` + related Polar fields.
43
212
 
44
213
  ### Added (Figma plugin)
45
214
  - **Style hint pill row** in Packs panel — Auto / Masc / Femme / Neutral toggle. Persisted via `navii.style` localStorage. Disabled when no pack active.
46
- - Plugin checkout URL now points at `${API_BASE}/checkout` (instead of hardcoded Gumroad link), letting us swap payment providers without re-publishing.
215
+ - Plugin checkout URL now points at `${API_BASE}/checkout` (instead of hardcoded Polar.sh link), letting us swap payment providers without re-publishing.
47
216
 
48
217
  ### Fixed (Figma plugin)
49
218
  - Left column in Packs panel was not scrollable when content overflowed (e.g. with new Style hint section). Added `overflow-y: auto` + `min-height: 0` to `.col-left`.
@@ -119,7 +288,11 @@ Deployment-only release. No changes to `@usenavii/core` or `@usenavii/react` (bo
119
288
  - React binding: `<Navii seed="..." />`.
120
289
  - Dual ESM/CJS build via tsup. TypeScript types included.
121
290
 
122
- [Unreleased]: https://github.com/uxderrick/navii/compare/v0.22.0...HEAD
291
+ [Unreleased]: https://github.com/uxderrick/navii/compare/v0.22.4...HEAD
292
+ [0.22.4]: https://github.com/uxderrick/navii/compare/v0.22.3...v0.22.4
293
+ [0.22.3]: https://github.com/uxderrick/navii/compare/v0.22.2...v0.22.3
294
+ [0.22.2]: https://github.com/uxderrick/navii/compare/v0.22.1...v0.22.2
295
+ [0.22.1]: https://github.com/uxderrick/navii/compare/v0.22.0...v0.22.1
123
296
  [0.22.0]: https://github.com/uxderrick/navii/compare/v0.21.2...v0.22.0
124
297
  [0.21.2]: https://github.com/uxderrick/navii/compare/v0.21.1...v0.21.2
125
298
  [0.21.1]: https://github.com/uxderrick/navii/compare/v0.21.0...v0.21.1
package/README.md CHANGED
@@ -43,13 +43,14 @@ Navii.group([user1.id, user2.id, user3.id]);
43
43
 
44
44
  Same seed always produces the same avatar — that's the contract.
45
45
 
46
- | Pass | Result |
47
- | -------------------------- | ------------------------------------------------------- |
48
- | `user.id` / UUID | ✅ Best. Stable and globally unique. |
49
- | `user.email` | ✅ Good. Stable, unique per user. |
50
- | `user.name` alone | ⚠️ Names collide. Two "Alice"s get the same avatar. |
51
- | `${name}-${createdAt}` | Fine fallback if no ID exists. Bake at signup. |
52
- | `Date.now()` at render | **Don't.** Breaks determinism changes on reload. |
46
+ | Pass | Result |
47
+ | -------------------------- | ----------------------------------------------------------- |
48
+ | `user.id` / UUID | ✅ Best. Stable and globally unique. |
49
+ | `seedFromEmail(email)` | ✅ Good. Hashed email — stable, unique, no PII on the wire. |
50
+ | `user.email` (raw) | ⚠️ Leaks email into URLs, logs, Referer headers. Hash it. |
51
+ | `user.name` alone | ⚠️ Names collide. Two "Alice"s get the same avatar. |
52
+ | `${name}-${createdAt}` | Fine fallback if no ID exists. Bake at signup. |
53
+ | `Date.now()` at render | ❌ **Don't.** Breaks determinism — changes on reload. |
53
54
 
54
55
  If your shape is uncertain, use the helper:
55
56
 
@@ -58,7 +59,20 @@ const s = Navii.seed({ id: user.id, email: user.email, name: user.name, createdA
58
59
  const svg = Navii.create(s);
59
60
  ```
60
61
 
61
- It picks the most unique field automatically: `id` → `email` → `name + createdAt` → `name`.
62
+ It picks the most unique field automatically: `id` → `email` → `name + createdAt` → `name`. When the email branch is used it hashes via `seedFromEmail()` by default — pass `{ hashEmail: false }` only to preserve existing raw-email seeds during a migration.
63
+
64
+ ### Using emails as seeds
65
+
66
+ Raw emails in URLs leak through server logs, Referer headers, browser history, and CDN cache keys. Hash the email — `sha256` of the trimmed + lowercased address:
67
+
68
+ ```ts
69
+ import { seedFromEmail, createAvatar } from '@usenavii/core';
70
+
71
+ const s = seedFromEmail(user.email); // sha256 hex, 64 chars
72
+ createAvatar(s);
73
+ ```
74
+
75
+ Two services that both hash with `seedFromEmail()` get the same seed for the same person, so avatars stay consistent across products.
62
76
 
63
77
  ## API
64
78
 
@@ -81,11 +95,43 @@ Navii.{ create, random, render, select, group, seed, build }
81
95
  | ------------ | ----------------------------------------------------- | ------------ |
82
96
  | `size` | `number` (px) | `96` |
83
97
  | `paletteId` | known palette id (e.g. `'mint'`) | seed-derived |
98
+ | `palette` | `Palette` object — runtime/brand palette, no registration in `PALETTES` needed. Wins over `paletteId`. | none |
84
99
  | `background` | `'none' \| 'solid' \| 'ring'` or `{ color }` | seed-derived |
100
+ | `mood` | `'neutral' \| 'happy' \| 'serious' \| 'sleepy' \| 'wink'` — overrides seed-derived eyes + mouth with a curated pair. Same seed + same mood = byte-identical. Bypasses pack eye/mouth constraints by design. | `'neutral'` |
85
101
  | `tileBg` | CSS color or `'auto'` (palette accent) | none |
86
102
  | `title` | accessible label (sets `<title>` + `aria-label`) | none |
87
103
  | `animated` | `boolean` — idle float / blink / sway / pulse / twinkle | `false` |
88
104
 
105
+ #### Mood overlay
106
+
107
+ Same seed, four expressions — body / palette / topper stay identical:
108
+
109
+ ```ts
110
+ import { createAvatar } from '@usenavii/core';
111
+
112
+ createAvatar('alice'); // neutral (seed-derived)
113
+ createAvatar('alice', { mood: 'happy' }); // wide eyes + smile
114
+ createAvatar('alice', { mood: 'serious' }); // squint + flat mouth
115
+ createAvatar('alice', { mood: 'sleepy' }); // sleepy + dot
116
+ createAvatar('alice', { mood: 'wink' }); // wink + smirk
117
+ ```
118
+
119
+ #### Runtime palette injection (`build()` only)
120
+
121
+ Pass a `Palette` object (e.g. pulled from Figma variables or design tokens) without registering it in the global `PALETTES` lookup:
122
+
123
+ ```ts
124
+ import { build } from '@usenavii/core';
125
+
126
+ const acmePalette = {
127
+ id: 'acme', name: 'Acme Brand',
128
+ bodyFrom: '#FF5722', bodyTo: '#FFA726',
129
+ feature: '#1A1A1A', background: '#FFF8F0',
130
+ };
131
+
132
+ build({ body: 'tall', eyes: 'wide' }, { palette: acmePalette, size: 128 });
133
+ ```
134
+
89
135
  ### `random()` — pick a seed for you
90
136
 
91
137
  For "spin again" UX, onboarding before the user picks an avatar, dev/demo seeding. Returns the chosen seed so you can persist it:
package/dist/index.cjs CHANGED
@@ -1339,6 +1339,18 @@ function resolvePacks(ids) {
1339
1339
  }
1340
1340
 
1341
1341
  // src/select.ts
1342
+ var MOOD_EYES = {
1343
+ happy: "wide",
1344
+ serious: "squint",
1345
+ sleepy: "sleepy",
1346
+ wink: "wink"
1347
+ };
1348
+ var MOOD_MOUTH = {
1349
+ happy: "smile",
1350
+ serious: "flat",
1351
+ sleepy: "dot",
1352
+ wink: "smirk"
1353
+ };
1342
1354
  function applyStyleHint(pool, packs, hint, partKey) {
1343
1355
  for (const pack of packs) {
1344
1356
  const subset = pack.styleHints?.[hint]?.[partKey];
@@ -1400,10 +1412,13 @@ function selectAvatar(seed2, options = {}) {
1400
1412
  topperPool = applyStyleHint(topperPool, enabledPacks, styleHint, "topper");
1401
1413
  }
1402
1414
  const body = rng.pick(bodyPool);
1403
- const eyes = rng.pick(eyesPool);
1404
- const mouth = rng.pick(mouthPool);
1415
+ const eyesPicked = rng.pick(eyesPool);
1416
+ const mouthPicked = rng.pick(mouthPool);
1405
1417
  const antenna = rng.pick(antennaPool);
1406
1418
  const accessory = rng.pick(accessoryPool);
1419
+ const mood = options.mood;
1420
+ const eyes = mood && mood !== "neutral" ? MOOD_EYES[mood] : eyesPicked;
1421
+ const mouth = mood && mood !== "neutral" ? MOOD_MOUTH[mood] : mouthPicked;
1407
1422
  let background;
1408
1423
  if (typeof options.background === "string") {
1409
1424
  background = options.background;
@@ -1653,13 +1668,176 @@ function clamp(n, lo, hi) {
1653
1668
  return Math.max(lo, Math.min(hi, n));
1654
1669
  }
1655
1670
 
1671
+ // src/sha256.ts
1672
+ var K = new Uint32Array([
1673
+ 1116352408,
1674
+ 1899447441,
1675
+ 3049323471,
1676
+ 3921009573,
1677
+ 961987163,
1678
+ 1508970993,
1679
+ 2453635748,
1680
+ 2870763221,
1681
+ 3624381080,
1682
+ 310598401,
1683
+ 607225278,
1684
+ 1426881987,
1685
+ 1925078388,
1686
+ 2162078206,
1687
+ 2614888103,
1688
+ 3248222580,
1689
+ 3835390401,
1690
+ 4022224774,
1691
+ 264347078,
1692
+ 604807628,
1693
+ 770255983,
1694
+ 1249150122,
1695
+ 1555081692,
1696
+ 1996064986,
1697
+ 2554220882,
1698
+ 2821834349,
1699
+ 2952996808,
1700
+ 3210313671,
1701
+ 3336571891,
1702
+ 3584528711,
1703
+ 113926993,
1704
+ 338241895,
1705
+ 666307205,
1706
+ 773529912,
1707
+ 1294757372,
1708
+ 1396182291,
1709
+ 1695183700,
1710
+ 1986661051,
1711
+ 2177026350,
1712
+ 2456956037,
1713
+ 2730485921,
1714
+ 2820302411,
1715
+ 3259730800,
1716
+ 3345764771,
1717
+ 3516065817,
1718
+ 3600352804,
1719
+ 4094571909,
1720
+ 275423344,
1721
+ 430227734,
1722
+ 506948616,
1723
+ 659060556,
1724
+ 883997877,
1725
+ 958139571,
1726
+ 1322822218,
1727
+ 1537002063,
1728
+ 1747873779,
1729
+ 1955562222,
1730
+ 2024104815,
1731
+ 2227730452,
1732
+ 2361852424,
1733
+ 2428436474,
1734
+ 2756734187,
1735
+ 3204031479,
1736
+ 3329325298
1737
+ ]);
1738
+ function rotr(x, n) {
1739
+ return (x >>> n | x << 32 - n) >>> 0;
1740
+ }
1741
+ function utf8Encode(str) {
1742
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(str);
1743
+ const out = [];
1744
+ for (let i = 0; i < str.length; i++) {
1745
+ let c = str.charCodeAt(i);
1746
+ if (c < 128) out.push(c);
1747
+ else if (c < 2048) {
1748
+ out.push(192 | c >> 6, 128 | c & 63);
1749
+ } else if (c < 55296 || c >= 57344) {
1750
+ out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
1751
+ } else {
1752
+ i++;
1753
+ c = 65536 + ((c & 1023) << 10 | str.charCodeAt(i) & 1023);
1754
+ out.push(
1755
+ 240 | c >> 18,
1756
+ 128 | c >> 12 & 63,
1757
+ 128 | c >> 6 & 63,
1758
+ 128 | c & 63
1759
+ );
1760
+ }
1761
+ }
1762
+ return new Uint8Array(out);
1763
+ }
1764
+ function sha256Hex(input) {
1765
+ const msg = utf8Encode(input);
1766
+ const bitLen = msg.length * 8;
1767
+ const padLen = (msg.length + 9 + 63 & -64) - msg.length;
1768
+ const buf = new Uint8Array(msg.length + padLen);
1769
+ buf.set(msg);
1770
+ buf[msg.length] = 128;
1771
+ const dv = new DataView(buf.buffer);
1772
+ dv.setUint32(buf.length - 4, bitLen >>> 0, false);
1773
+ dv.setUint32(buf.length - 8, Math.floor(bitLen / 4294967296), false);
1774
+ const H = new Uint32Array([
1775
+ 1779033703,
1776
+ 3144134277,
1777
+ 1013904242,
1778
+ 2773480762,
1779
+ 1359893119,
1780
+ 2600822924,
1781
+ 528734635,
1782
+ 1541459225
1783
+ ]);
1784
+ const W = new Uint32Array(64);
1785
+ for (let chunk = 0; chunk < buf.length; chunk += 64) {
1786
+ for (let i = 0; i < 16; i++) W[i] = dv.getUint32(chunk + i * 4, false);
1787
+ for (let i = 16; i < 64; i++) {
1788
+ const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ W[i - 15] >>> 3;
1789
+ const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ W[i - 2] >>> 10;
1790
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
1791
+ }
1792
+ let a = H[0], b = H[1], c = H[2], d = H[3];
1793
+ let e = H[4], f = H[5], g = H[6], h = H[7];
1794
+ for (let i = 0; i < 64; i++) {
1795
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1796
+ const ch = e & f ^ ~e & g;
1797
+ const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
1798
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
1799
+ const mj = a & b ^ a & c ^ b & c;
1800
+ const t2 = S0 + mj >>> 0;
1801
+ h = g;
1802
+ g = f;
1803
+ f = e;
1804
+ e = d + t1 >>> 0;
1805
+ d = c;
1806
+ c = b;
1807
+ b = a;
1808
+ a = t1 + t2 >>> 0;
1809
+ }
1810
+ H[0] = H[0] + a >>> 0;
1811
+ H[1] = H[1] + b >>> 0;
1812
+ H[2] = H[2] + c >>> 0;
1813
+ H[3] = H[3] + d >>> 0;
1814
+ H[4] = H[4] + e >>> 0;
1815
+ H[5] = H[5] + f >>> 0;
1816
+ H[6] = H[6] + g >>> 0;
1817
+ H[7] = H[7] + h >>> 0;
1818
+ }
1819
+ let out = "";
1820
+ for (let i = 0; i < 8; i++) out += H[i].toString(16).padStart(8, "0");
1821
+ return out;
1822
+ }
1823
+
1656
1824
  // src/seed.ts
1657
- function seed(fields) {
1825
+ function normalizeEmail(email) {
1826
+ return email.trim().toLowerCase().normalize("NFC");
1827
+ }
1828
+ function seedFromEmail(email) {
1829
+ if (typeof email !== "string" || email.length === 0) {
1830
+ throw new Error("navii: seedFromEmail() requires a non-empty string");
1831
+ }
1832
+ return sha256Hex(normalizeEmail(email));
1833
+ }
1834
+ function seed(fields, options = {}) {
1835
+ const hashEmail = options.hashEmail ?? true;
1658
1836
  if (fields.id !== null && fields.id !== void 0 && String(fields.id).length > 0) {
1659
1837
  return String(fields.id);
1660
1838
  }
1661
1839
  if (fields.email && fields.email.length > 0) {
1662
- return fields.email;
1840
+ return hashEmail ? seedFromEmail(fields.email) : fields.email;
1663
1841
  }
1664
1842
  if (fields.name && fields.name.length > 0) {
1665
1843
  if (fields.createdAt !== null && fields.createdAt !== void 0) {
@@ -1674,7 +1852,7 @@ function seed(fields) {
1674
1852
 
1675
1853
  // src/build.ts
1676
1854
  function build(spec = {}, options = {}) {
1677
- const palette = spec.palette ? PALETTE_BY_ID[spec.palette] ?? PALETTES[0] : PALETTES[0];
1855
+ const palette = options.palette ?? (spec.palette ? PALETTE_BY_ID[spec.palette] ?? PALETTES[0] : PALETTES[0]);
1678
1856
  const resolved = {
1679
1857
  seed: "__build__",
1680
1858
  palette,
@@ -1718,6 +1896,7 @@ var Navii = {
1718
1896
  select: selectAvatar,
1719
1897
  group: renderGroup,
1720
1898
  seed,
1899
+ seedFromEmail,
1721
1900
  build
1722
1901
  };
1723
1902
 
@@ -1728,12 +1907,15 @@ exports.build = build;
1728
1907
  exports.createAvatar = createAvatar;
1729
1908
  exports.createRng = createRng;
1730
1909
  exports.cyrb53 = cyrb53;
1910
+ exports.normalizeEmail = normalizeEmail;
1731
1911
  exports.random = random;
1732
1912
  exports.renderAvatar = renderAvatar;
1733
1913
  exports.renderAvatarInner = renderAvatarInner;
1734
1914
  exports.renderGroup = renderGroup;
1735
1915
  exports.resolvePacks = resolvePacks;
1736
1916
  exports.seed = seed;
1917
+ exports.seedFromEmail = seedFromEmail;
1737
1918
  exports.selectAvatar = selectAvatar;
1919
+ exports.sha256Hex = sha256Hex;
1738
1920
  //# sourceMappingURL=index.cjs.map
1739
1921
  //# sourceMappingURL=index.cjs.map