@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 +178 -5
- package/README.md +54 -8
- package/dist/index.cjs +187 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -4
- package/dist/index.d.ts +57 -4
- package/dist/index.js +185 -6
- package/dist/index.js.map +1 -1
- package/dist/parts.d.cts +1 -1
- package/dist/parts.d.ts +1 -1
- package/dist/{types-CF0rfKly.d.cts → types-BtX6LIyn.d.cts} +20 -1
- package/dist/{types-CF0rfKly.d.ts → types-BtX6LIyn.d.ts} +20 -1
- package/package.json +1 -1
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
|
|
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
|
|
42
|
-
- `AppOptions` renamed `
|
|
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
|
|
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.
|
|
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
|
-
| `
|
|
50
|
-
| `user.
|
|
51
|
-
|
|
|
52
|
-
| `
|
|
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
|
|
1404
|
-
const
|
|
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
|
|
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
|