refine T-313: multi-icon labelled glyph card + multi-size + flow

Broadens the Phosphor glyph card from a single-glyph preview to a
list of entries, each glyph + optional label + optional
description, driven by `clide icon show` (terse positional for
bare previews, JSON payload for labelled/described entries).

- Multi-size: each icon renders at a 48px hero (new named
  constant) plus a sample at every inline type token (badge 11 ->
  body 15) so legibility at real UI sizes is visible.
- Selection model (decided): display card stays display-only
  (D-78); selection happens in the interaction zone via a
  Claude-offered choice list whose options reuse the card's
  per-icon labels — labels are the bridge for the show-then-pick
  flow.
- Notes name->codepoint resolution already exists
  (phosphor_glyphs.g.dart, PhosphorIcons.byName), retiring the
  old 'generate the full set' caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:43:10 +02:00
co-authored by Claude Opus 4.8
parent c72eb1eb5d
commit 7ac29f59d1
3 changed files with 252 additions and 0 deletions
+144
View File
@@ -2587,3 +2587,147 @@ Uses the already-bundled Phosphor.ttf via PhosphorIconPainter.
- The card is display-only (D-78); a click could copy the codepoint/name. - The card is display-only (D-78); a click could copy the codepoint/name.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302).', NULL, '2026-06-10 10:21:09', '2026-06-10 10:21:09', '2026-06-10 10:21:09', NULL, '1ea34a81269e81267f69a993eabf4002', 2) ON CONFLICT(hash) DO NOTHING; - Surfaced 2026-06-10 while choosing settings scope icons (T-302).', NULL, '2026-06-10 10:21:09', '2026-06-10 10:21:09', '2026-06-10 10:21:09', NULL, '1ea34a81269e81267f69a993eabf4002', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'status', 'ready', 'done', NULL, '2026-06-10 10:21:48', '2026-06-10 10:21:48', '2026-06-10 10:21:48', NULL, '37b28095fb8c93b3e3319402eae91173', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'status', 'ready', 'done', NULL, '2026-06-10 10:21:48', '2026-06-10 10:21:48', '2026-06-10 10:21:48', NULL, '37b28095fb8c93b3e3319402eae91173', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:29:08', '2026-06-10 10:29:08', '2026-06-10 10:29:08', NULL, '7c78be9c5f8508b9e8ec788b0f34d95a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:38:06', '2026-06-10 10:38:06', '2026-06-10 10:38:06', NULL, '9ea7fb36b62a72a630cbe9c58a54521d', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders Phosphor glyphs by name/codepoint, so icons can be previewed and picked in the live pane.
## Why
Picking icons (e.g. the settings scope tags, T-302) needs to SEE real glyphs.
Frame0 can''t render Phosphor (private-use codepoints, no font) and golden tests
render the font as Ahem boxes (only painter_bold_metrics loads a real font), so
preview only works where the app has the font a native card is the vehicle.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a
grid of Phosphor glyphs with their name + codepoint, driven by the clide CLI
e.g. `clide icon show <name|0xNNNN ...>` or `clide glyphs [filter]` (D-6 parity).
Uses the already-bundled Phosphor.ttf via PhosphorIconPainter.
## Notes / scope
- Every glyph already renders via `PhosphorIconPainter(0xNNNN)`; the 49 named
consts in lib/widgets/src/icons/phosphor.dart are curated sugar. We do NOT
need to bulk-add all ~1512 consts for availability.
- OPTIONAL behind this card: generate the full namecodepoint set (from
.claude/skills/ui-design/references/phosphor-glyphs.md, 1512 entries) so glyphs
are discoverable by name but only if the card makes them browsable; weigh
against clide''s curated/minimal philosophy.
- The card is display-only (D-78); a click could copy the codepoint/name.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302).', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
## Why
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
### Multi-size rendering (per entry)
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant scale is the inline type scale in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive sizing off those tokens (not bare numbers) so the row tracks the scale if it changes; the hero size can be a named large constant. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
### CLI shape
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
## Display-only vs. selectable (D-78 key boundary)
The card itself is a CONVERSATION WIDGET and stays display-only (D-78): it shows the labelled icon options and a click may copy the codepoint/name, but it does NOT resolve a selection inline. When the offer needs to be an actual PICK that returns a choice, that selection belongs in the interaction zone (AskUserQuestion-style option list that replaces the composer), NOT inline in the transcript see the interaction-zone rule. So scope this ticket to the display/offer card; an interaction-zone icon picker (or feeding these entries into an AskUserQuestion option list) is a follow-up, not part of this card. Flag which path we want before building the selectable variant.
## Notes / scope
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
## Acceptance
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
- Each icon renders at multiple sizes: a large hero plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
- The card is display-only (no inline selection); the description records that a selectable picker routes through the interaction zone as a separate follow-up.
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', NULL, '2026-06-10 10:41:56', '2026-06-10 10:41:56', '2026-06-10 10:41:56', NULL, 'cd2ddb517ae86b0e0751a599a99c3acd', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'title', 'Native Phosphor glyph preview card (clide icon show)', 'Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)', NULL, '2026-06-10 10:41:56', '2026-06-10 10:41:56', '2026-06-10 10:41:56', NULL, 'fd46733997b9f01126da8e45a3f2a52a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
## Why
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
### Multi-size rendering (per entry)
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant scale is the inline type scale in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive sizing off those tokens (not bare numbers) so the row tracks the scale if it changes; the hero size can be a named large constant. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
### CLI shape
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
## Display-only vs. selectable (D-78 key boundary)
The card itself is a CONVERSATION WIDGET and stays display-only (D-78): it shows the labelled icon options and a click may copy the codepoint/name, but it does NOT resolve a selection inline. When the offer needs to be an actual PICK that returns a choice, that selection belongs in the interaction zone (AskUserQuestion-style option list that replaces the composer), NOT inline in the transcript see the interaction-zone rule. So scope this ticket to the display/offer card; an interaction-zone icon picker (or feeding these entries into an AskUserQuestion option list) is a follow-up, not part of this card. Flag which path we want before building the selectable variant.
## Notes / scope
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
## Acceptance
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
- Each icon renders at multiple sizes: a large hero plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
- The card is display-only (no inline selection); the description records that a selectable picker routes through the interaction zone as a separate follow-up.
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
## Why
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
### Multi-size rendering (per entry)
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering at 48px so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. The 48px hero is larger than any existing type token (welcome banner is 52, dialog title 16), so add a named preview constant for it (e.g. clideIconPreviewHero = 48) rather than a bare 48 keep the scale coherent. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
### CLI shape
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
## Display-only card + interaction-zone selection (D-78 decided)
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
## Notes / scope
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
## Acceptance
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
- Each icon renders at multiple sizes: a 48px hero (new named constant, e.g. clideIconPreviewHero) plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', NULL, '2026-06-10 10:43:01', '2026-06-10 10:43:01', '2026-06-10 10:43:01', NULL, 'fdb996091e37ea8551ea0b002afb0291', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -138,3 +138,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'T-311', '2026-06-10 09:55:55', '2026-06-10 09:55:55', NULL, 'ed03464125d09ff5f23913e6247064e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'T-311', '2026-06-10 09:55:55', '2026-06-10 09:55:55', NULL, 'ed03464125d09ff5f23913e6247064e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'T-312', '2026-06-10 10:04:53', '2026-06-10 10:04:53', NULL, 'dd22af12b333c13376b23aa5381ea2d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'T-312', '2026-06-10 10:04:53', '2026-06-10 10:04:53', NULL, 'dd22af12b333c13376b23aa5381ea2d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'T-313', '2026-06-10 10:20:54', '2026-06-10 10:20:54', NULL, '9b7abb73ea56cfdad2039303a8935897', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'T-313', '2026-06-10 10:20:54', '2026-06-10 10:20:54', NULL, '9b7abb73ea56cfdad2039303a8935897', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'T-314', '2026-06-10 10:24:29', '2026-06-10 10:24:29', NULL, 'f8b89421a13f620c2a0e76bd780db5a2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+107
View File
@@ -1568,3 +1568,110 @@ the real .clide vs ~/.clide files), gear (collides with "settings"). Note that
every Phosphor glyph already renders via PhosphorIconPainter(0xNNNN); only named every Phosphor glyph already renders via PhosphorIconPainter(0xNNNN); only named
consts need adding. Preview/picking is blocked on a native glyph card (see the consts need adding. Preview/picking is blocked on a native glyph card (see the
new ticket) goldens render the font as Ahem boxes.', 'done', 'medium', NULL, NULL, NULL, '2026-06-09 20:24:54', '2026-06-10 10:21:48', NULL, '0ab1ccc1bfc22b98f08f6e0c58116f2e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); new ticket) goldens render the font as Ahem boxes.', 'done', 'medium', NULL, NULL, NULL, '2026-06-09 20:24:54', '2026-06-10 10:21:48', NULL, '0ab1ccc1bfc22b98f08f6e0c58116f2e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Phosphor icons: single label→codepoint map; reference glyphs by name, not codepoint', 'Replace the 49 hand-maintained named consts in lib/widgets/src/icons/phosphor.dart with ONE generated const Map<String,int> of all 1512 Phosphor glyphs (exact kebab-case upstream names → codepoint). Code references glyphs via PhosphorIcons.byName(''folder'') — no raw 0xNNNN in feature code; codepoints live only in the generated map. Migrate the ~32 call sites (by codepoint, not guessed names). Add a test that scans lib/ for byName(''...'') literals and asserts each exists in the map, recovering the compile-time typo safety the named consts gave. Map is regeneratable from .claude/skills/ui-design/references/phosphor-glyphs.md. Foundation for the glyph preview card (T-313).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:24:29', '2026-06-10 10:24:29', NULL, 'f47e0bdac48ba76f91043659a1d25a97', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Phosphor icons: single label→codepoint map; reference glyphs by name, not codepoint', 'Replace the 49 hand-maintained named consts in lib/widgets/src/icons/phosphor.dart with ONE generated const Map<String,int> of all 1512 Phosphor glyphs (exact kebab-case upstream names → codepoint). Code references glyphs via PhosphorIcons.byName(''folder'') — no raw 0xNNNN in feature code; codepoints live only in the generated map. Migrate the ~32 call sites (by codepoint, not guessed names). Add a test that scans lib/ for byName(''...'') literals and asserts each exists in the map, recovering the compile-time typo safety the named consts gave. Map is regeneratable from .claude/skills/ui-design/references/phosphor-glyphs.md. Foundation for the glyph preview card (T-313).', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:24:29', '2026-06-10 10:29:08', NULL, '6616d70e7c621b0f204336ed29d40bf0', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Native Phosphor glyph preview card (clide icon show)', 'A conversation-pane card that renders Phosphor glyphs by name/codepoint, so icons can be previewed and picked in the live pane.
## Why
Picking icons (e.g. the settings scope tags, T-302) needs to SEE real glyphs.
Frame0 can''t render Phosphor (private-use codepoints, no font) and golden tests
render the font as Ahem boxes (only painter_bold_metrics loads a real font), so
preview only works where the app has the font a native card is the vehicle.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a
grid of Phosphor glyphs with their name + codepoint, driven by the clide CLI
e.g. `clide icon show <name|0xNNNN ...>` or `clide glyphs [filter]` (D-6 parity).
Uses the already-bundled Phosphor.ttf via PhosphorIconPainter.
## Notes / scope
- Every glyph already renders via `PhosphorIconPainter(0xNNNN)`; the 49 named
consts in lib/widgets/src/icons/phosphor.dart are curated sugar. We do NOT
need to bulk-add all ~1512 consts for availability.
- OPTIONAL behind this card: generate the full namecodepoint set (from
.claude/skills/ui-design/references/phosphor-glyphs.md, 1512 entries) so glyphs
are discoverable by name but only if the card makes them browsable; weigh
against clide''s curated/minimal philosophy.
- The card is display-only (D-78); a click could copy the codepoint/name.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302).', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 10:38:06', NULL, '3852ea0ab0ba8510fd4fbf1fa6d20239', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
## Why
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
### Multi-size rendering (per entry)
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant scale is the inline type scale in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive sizing off those tokens (not bare numbers) so the row tracks the scale if it changes; the hero size can be a named large constant. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
### CLI shape
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
## Display-only vs. selectable (D-78 key boundary)
The card itself is a CONVERSATION WIDGET and stays display-only (D-78): it shows the labelled icon options and a click may copy the codepoint/name, but it does NOT resolve a selection inline. When the offer needs to be an actual PICK that returns a choice, that selection belongs in the interaction zone (AskUserQuestion-style option list that replaces the composer), NOT inline in the transcript see the interaction-zone rule. So scope this ticket to the display/offer card; an interaction-zone icon picker (or feeding these entries into an AskUserQuestion option list) is a follow-up, not part of this card. Flag which path we want before building the selectable variant.
## Notes / scope
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
## Acceptance
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
- Each icon renders at multiple sizes: a large hero plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
- The card is display-only (no inline selection); the description records that a selectable picker routes through the interaction zone as a separate follow-up.
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 10:41:56', NULL, '6b462c12d7c4b839c4b5affd6e1546b1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
## Why
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
## Deliverable
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
### Multi-size rendering (per entry)
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering at 48px so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. The 48px hero is larger than any existing type token (welcome banner is 52, dialog title 16), so add a named preview constant for it (e.g. clideIconPreviewHero = 48) rather than a bare 48 keep the scale coherent. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
### CLI shape
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
## Display-only card + interaction-zone selection (D-78 decided)
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
## Notes / scope
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
## Acceptance
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
- Each icon renders at multiple sizes: a 48px hero (new named constant, e.g. clideIconPreviewHero) plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 10:43:01', NULL, '04e889f18335733517fba43971eb67f4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);