diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index d3c188f..fcacbf7 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -1,8 +1,8 @@ # Third-party notices -NestQuest bundles glyph data from the icon sets below. The curated names are listed in -`tools/icons/curated-icons.json`; `tools/icons/generate.mjs` reads each glyph's SVG path -data from the locally installed packages and generates it into both bundles +NestQuest bundles glyph data from the icon sets below. The curated Lucide names and the +Font Awesome quick-pick names are listed in `tools/icons/curated-icons.json`; +`tools/icons/generate.mjs` reads each glyph's SVG path data from the locally installed packages and generates it into both bundles (`admin/src/icons/registry.generated.ts` and `frontend/src/icons/registry.generated.ts`). Nothing is loaded from a CDN or fetched at runtime. The package versions used are recorded in the header of each generated file. @@ -15,8 +15,8 @@ in the header of each generated file. - Icons: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - Fonts: [SIL OFL 1.1](https://openfontlicense.org) - Code: [MIT](https://opensource.org/licenses/MIT) -- Use in NestQuest: a curated subset of the Free **solid** icon set, embedded as SVG path - data. No fonts are bundled. +- Use in NestQuest: the full Free **solid** icon set, embedded as SVG path data (a curated + subset doubles as the admin picker's quick picks). No fonts are bundled. Font Awesome Free by @fontawesome — https://fontawesome.com — icons licensed under CC BY 4.0. diff --git a/admin/src/definitions/DefinitionsTab.test.tsx b/admin/src/definitions/DefinitionsTab.test.tsx index aede057..34a272d 100644 --- a/admin/src/definitions/DefinitionsTab.test.tsx +++ b/admin/src/definitions/DefinitionsTab.test.tsx @@ -1355,7 +1355,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => { fireEvent.click(within(s).getByRole("button", { name: "Morning" })); } - it("ships the curated Font Awesome subset, each drawn as a filled glyph", () => { + it("ships the curated Font Awesome quick picks, each drawn as a filled glyph", () => { expect(FA_DEFINITION_ICONS.length).toBeGreaterThan(0); for (const entry of FA_DEFINITION_ICONS) { expect(entry.kind).toBe("fa"); diff --git a/admin/src/definitions/definitionIcons.tsx b/admin/src/definitions/definitionIcons.tsx index b326b05..ce82ddf 100644 --- a/admin/src/definitions/definitionIcons.tsx +++ b/admin/src/definitions/definitionIcons.tsx @@ -1,11 +1,14 @@ /** * The icons a definition's `icon` may name (ADMIN-SPEC §3.3): the curated - * Lucide set, the curated Font Awesome Free solid subset, or an emoji. The - * names, labels and path data come from the shared icon registry - * (`src/icons/registry.generated.ts`, generated from - * `tools/icons/curated-icons.json`) — no CDN, no runtime fetch. The stored - * value is namespaced — `lucide:`, `fa:` or `emoji:` — - * and a legacy bare `` still names the same Lucide entry. + * Lucide set, any icon in the full Font Awesome Free Solid set, or an emoji. + * The Font Awesome quick picks (`FA_DEFINITION_ICONS`) are only a curated + * shortcut list; `fa:` resolves against the full set + * (`FA_SOLID_ICONS`). The names, labels and path data come from the shared + * icon registry (`src/icons/registry.generated.ts`, generated by + * `tools/icons/generate.mjs` from `tools/icons/curated-icons.json` and the + * installed Free Solid package) — no CDN, no runtime fetch. The stored value + * is namespaced — `lucide:`, `fa:` or `emoji:` — and a + * legacy bare `` still names the same Lucide entry. */ import { GlyphSvg } from "../icons/GlyphSvg"; import { @@ -105,8 +108,9 @@ export function emojiIconText(value: string | null): string { } /** - * The curated glyph (Lucide or Font Awesome) an icon value resolves to via - * the shared parser; emoji, unknown and null values give null. + * The glyph an icon value resolves to via the shared parser: a curated + * Lucide entry or any Font Awesome Free Solid icon (quick pick or not); + * emoji, unknown and null values give null. */ export function findDefinitionIcon(value: string | null): DefinitionIconEntry | null { const icon = resolveIcon(value); diff --git a/admin/src/icons/registry.test.ts b/admin/src/icons/registry.test.ts index acc972f..d4aef43 100644 --- a/admin/src/icons/registry.test.ts +++ b/admin/src/icons/registry.test.ts @@ -157,7 +157,7 @@ describe("offline bundling", () => { } }); - it("no bundled source imports an icon package — only the curated generated data ships", () => { + it("no bundled source imports an icon package — only the generated registry data ships", () => { const sources = [join(REPO_ROOT, "admin", "src"), join(REPO_ROOT, "frontend", "src")] .flatMap(filesUnder) .filter((path) => /\.(ts|tsx)$/.test(path) && !path.endsWith(".test.ts") && !path.endsWith(".test.tsx")); diff --git a/design/ADMIN-SPEC.md b/design/ADMIN-SPEC.md index b337c07..35d1fc4 100644 --- a/design/ADMIN-SPEC.md +++ b/design/ADMIN-SPEC.md @@ -105,14 +105,25 @@ Each definition carries one optional icon, shown on both surfaces. The picker of sources: - **Lucide** — a grid of the curated Lucide glyphs, drawn stroked. -- **Font Awesome** — a grid of a curated subset of the Font Awesome Free *solid* set, - drawn filled. +- **Font Awesome** — a search over the **full** Font Awesome Free *solid* set (every + glyph in `@fortawesome/free-solid-svg-icons`), plus a curated quick-pick grid, all drawn + filled. With the search field empty the quick-pick grid shows; typing filters the full + set by name or label (an exact name ranks first, then name prefixes, then the rest), + shown 40 results per page with *Previous* / *Show more*. + **Free-text rule:** an admin may type a Free Solid icon name directly (trimmed, + case-insensitive — `Dog` stores `fa:dog`). Text that is not the exact name of a Free + Solid icon — a partial search, a misspelling, or a Pro or Brands name — is rejected in + the picker with an inline error and cannot be saved until a result is selected or the + full name is typed. - **Emoji** — a text input; the typed emoji is stored as entered (not trimmed) and must be 1–8 code points with no whitespace, control characters, `<`, `>` or `&`. -Both curated lists come from `tools/icons/curated-icons.json`; their glyph path data is -generated into the admin and panel bundles by `tools/icons/generate.mjs` (no CDN, no -runtime fetch). See `THIRD-PARTY-NOTICES.md` for licences. +The curated Lucide set and the Font Awesome quick-pick list come from +`tools/icons/curated-icons.json`; `tools/icons/generate.mjs` generates their glyph path +data, together with the full Font Awesome Free Solid set, into the admin and panel bundles +(no CDN, no runtime fetch). Embedding the full set makes both bundles heavier — the panel +bundle (`nestquest-cards.js`) is about 1 MB raw / 276 kB gzip. See +`THIRD-PARTY-NOTICES.md` for licences. The stored value is a **namespaced key**: diff --git a/design/DESIGN-HANDOFF.md b/design/DESIGN-HANDOFF.md index 8e053e4..a3e160f 100644 --- a/design/DESIGN-HANDOFF.md +++ b/design/DESIGN-HANDOFF.md @@ -78,6 +78,6 @@ The panel speaks tabletop; the admin speaks plainly. Both sit on the same `quest decide before shipping the card. - No gamification is in scope. XP, levels, streaks and rarity were explicitly deferred, but the append-only event log already supports computing them later. -- Icon per quest definition is chosen by the parent from curated Lucide and Font Awesome - Free sets or as an emoji, stored as a namespaced key (`lucide:`/`fa:`/`emoji:`) — +- Icon per quest definition is chosen by the parent from the curated Lucide set, the full + Font Awesome Free Solid set (searchable, with curated quick picks) or as an emoji, stored as a namespaced key (`lucide:`/`fa:`/`emoji:`) — resolved 2026-09-26, see `ADMIN-SPEC.md` §3.3. diff --git a/design/PANEL-SPEC.md b/design/PANEL-SPEC.md index ccef96a..f457a5e 100644 --- a/design/PANEL-SPEC.md +++ b/design/PANEL-SPEC.md @@ -106,7 +106,8 @@ collapse the column, the three-column rhythm is the layout. 1. **Icon tile** 64×64, radius 10px, `--nq-p-icon-tile`, glyph 34px white — the definition's icon, resolved from its namespaced key (`ADMIN-SPEC.md` §3.3): `lucide:` draws the curated Lucide glyph **stroked** (fill none, stroke-width 2, - round caps and joins); `fa:` draws the curated Font Awesome glyph **filled**; + round caps and joins); `fa:` draws the Font Awesome Free Solid glyph **filled** (any icon in the full + set, not only the quick picks); `emoji:` renders the emoji **as text**. A legacy bare name resolves as `lucide:`. No icon, or any key that does not resolve (unknown namespace or name, malformed emoji), falls back to the **star** glyph. Glyph data is bundled from the diff --git a/frontend/src/icons/render.ts b/frontend/src/icons/render.ts index 49f3dad..3f273b4 100644 --- a/frontend/src/icons/render.ts +++ b/frontend/src/icons/render.ts @@ -1,7 +1,8 @@ /** * Lit rendering for the shared icon registry (`registry.generated.ts`, - * generated from tools/icons/curated-icons.json). The glyph path data is - * bundled — no CDN and no runtime fetch. + * generated by tools/icons/generate.mjs: the curated Lucide set plus the full + * Font Awesome Free Solid set). The glyph path data is bundled — no CDN and + * no runtime fetch. */ import { html, svg, type TemplateResult } from "lit"; diff --git a/tools/icons/generate.mjs b/tools/icons/generate.mjs index 16b7fe7..2fd81f4 100644 --- a/tools/icons/generate.mjs +++ b/tools/icons/generate.mjs @@ -16,13 +16,15 @@ * Lucide circles, rects and lines are converted to path `d` strings so every * glyph is plain `{ viewBox, paths }` data any renderer can draw. * - * Besides the curated sets, the module embeds the complete Font Awesome Free - * Solid set (`FA_SOLID_ICONS`). Its source is the installed + * Besides the curated sets (the Lucide set and the Font Awesome quick picks), + * the module embeds the complete Font Awesome Free Solid set (`FA_SOLID_ICONS`), + * which is what `fa:` resolves against. Its source is the installed * @fortawesome/free-solid-svg-icons package itself: every exported icon * definition, de-duplicated by canonical `iconName` (the package also exports * each legacy alias, e.g. faHome for `house`, as the same definition). The * package carries no human labels, so each label is the kebab name in Title - * Case (`spray-can-sparkles` → "Spray Can Sparkles"). + * Case (`spray-can-sparkles` → "Spray Can Sparkles"). Embedding the full set + * makes both bundles heavier: the panel bundle is about 1 MB raw / 276 kB gzip. */ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { createRequire } from "node:module";