T-302: settings-modal wireframe + scope-icon decision

Frame0 wireframe for the app settings UI (design step under T-8). A
centered modal with a left sidebar of sections (Claude is one), inline
per-field scope (no header toggle): each field shows its EFFECTIVE value
+ a source tag, edits default to Project, and a field's tag promotes it
to Always (global, all clide) or resets. Editor category fully rendered
(number/select/toggle/external-file + override shadow).

Scope tag becomes an icon: folder (project) / globe (always) /
circle-dashed (default), recorded on T-302. Filed T-313 (native Phosphor
glyph preview card — the vehicle to pick icons in-pane) and T-312 (image
card caches by path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:22:16 +02:00
co-authored by Claude Opus 4.8
parent b680df2b24
commit c72eb1eb5d
5 changed files with 307 additions and 0 deletions
+89
View File
@@ -2498,3 +2498,92 @@ ACCEPTANCE
CLARIFICATION (the TOOLTIP line above lost a word to shell escaping): ClideButton already exposes a tooltip parameter (clide_button.dart:17,26,42) pass tooltip on the new button for the mouseover; no widget change needed.', NULL, '2026-06-10 09:56:24', '2026-06-10 09:56:24', '2026-06-10 09:56:24', NULL, 'f4fa8bc5799ef68d14512e128c03d1ba', 2) ON CONFLICT(hash) DO NOTHING; CLARIFICATION (the TOOLTIP line above lost a word to shell escaping): ClideButton already exposes a tooltip parameter (clide_button.dart:17,26,42) pass tooltip on the new button for the mouseover; no widget change needed.', NULL, '2026-06-10 09:56:24', '2026-06-10 09:56:24', '2026-06-10 09:56:24', NULL, 'f4fa8bc5799ef68d14512e128c03d1ba', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:01:09', '2026-06-10 10:01:09', '2026-06-10 10:01:09', NULL, '7ed94c6ae98bb9f7a2e3a843512c47bd', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:01:09', '2026-06-10 10:01:09', '2026-06-10 10:01:09', NULL, '7ed94c6ae98bb9f7a2e3a843512c47bd', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'ready', 'done', NULL, '2026-06-10 10:07:19', '2026-06-10 10:07:19', '2026-06-10 10:07:19', NULL, '15b320f72bf2a7bec603482d4b980bf6', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'ready', 'done', NULL, '2026-06-10 10:07:19', '2026-06-10 10:07:19', '2026-06-10 10:07:19', NULL, '15b320f72bf2a7bec603482d4b980bf6', 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 ('06FB1ZFJK6J2GSV4SA69QF730C', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:08:59', '2026-06-10 10:08:59', '2026-06-10 10:08:59', NULL, 'e240ed5c41e787235e004fc193df9163', 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 ('06FB16YM1Y910T07Y1Y22MGTEM', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:09:11', '2026-06-10 10:09:11', '2026-06-10 10:09:11', NULL, '4cc7018105565dc1ab774296d7bdf4ac', 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', 'description', 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
**Deliverable**
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
**Scope to frame (from T-8)**
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
**Open design questions for the wireframe to answer**
- Settings as a full-screen view, a pane/tab, or a modal?
- Per-project (`.clide/settings.yaml`) vs. user-global scope shown together or switched?
- Search/filter across all settings.
- How schema-driven fields render labels, help text, defaults, and reset.
**Constraints**
- Follow clide visual language pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
This is the design step; implementation of the actual settings UI is separate child work under T-8.', 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
**Deliverable**
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
**Scope to frame (from T-8)**
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
**Open design questions for the wireframe to answer**
- Settings as a full-screen view, a pane/tab, or a modal?
- Per-project (`.clide/settings.yaml`) vs. user-global scope shown together or switched?
- Search/filter across all settings.
- How schema-driven fields render labels, help text, defaults, and reset.
**Constraints**
- Follow clide visual language pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
This is the design step; implementation of the actual settings UI is separate child work under T-8.
## Scope-tag icon decision (review, 2026-06-10)
The per-field scope tag is an ICON, not a text label (space + cognitive load).
Chosen trio (locationglobal reach ladder), keeping the colour coding + a
tooltip with the word for clarity/a11y; tapping opens the scope menu:
- Project (this repo) = `folder` 0xe24a (teal) already wired
- Always (all clide) = `globe` 0xe288 (amber) add const
- Default (unset) = `circle-dashed` 0xe602 (grey) add const
Alternatives considered: house (warmer homeworld pair), user-circle (mirrors
the real .clide vs ~/.clide files), gear (collides with "settings"). Note that
every Phosphor glyph already renders via PhosphorIconPainter(0xNNNN); only named
consts need adding. Preview/picking is blocked on a native glyph card (see the
new ticket) goldens render the font as Ahem boxes.', NULL, '2026-06-10 10:20:48', '2026-06-10 10:20:48', '2026-06-10 10:20:48', NULL, '14f95d56a8f037f285dc4cbeb1ae7504', 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.', '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).', 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;
+1
View File
@@ -137,3 +137,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 ('06FB1SJKA7KRCQMZE41SPQG780', 'T-310', '2026-06-10 09:39:05', '2026-06-10 09:39:05', NULL, '9088122bf9d5e04ca8a7fee3bf031a53', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'T-310', '2026-06-10 09:39:05', '2026-06-10 09:39:05', NULL, '9088122bf9d5e04ca8a7fee3bf031a53', 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 ('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);
+122
View File
@@ -1446,3 +1446,125 @@ Root cause: lib/builtin/claude/src/prompt_card.dart:125-135 defines _digitKeys a
Fix: extend _digitKeys (or the match in _onKey) to also map LogicalKeyboardKey.numpad1..numpad9 to the same 1-9 selection. Keep the hasPrimaryFocus / modifier guards as-is so digits still type into note fields. Fix: extend _digitKeys (or the match in _onKey) to also map LogicalKeyboardKey.numpad1..numpad9 to the same 1-9 selection. Keep the hasPrimaryFocus / modifier guards as-is so digits still type into note fields.
Acceptance: with the prompt card focused, numpad 1-9 selects the corresponding option/button exactly like the number-row digits; numpad keys still type normally when a note field has focus.', 'done', 'low', NULL, NULL, NULL, '2026-06-10 09:39:05', '2026-06-10 10:07:19', NULL, '229d4e82ae8b66e249475f49d69bacdb', 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); Acceptance: with the prompt card focused, numpad 1-9 selects the corresponding option/button exactly like the number-row digits; numpad keys still type normally when a note field has focus.', 'done', 'low', NULL, NULL, NULL, '2026-06-10 09:39:05', '2026-06-10 10:07:19', NULL, '229d4e82ae8b66e249475f49d69bacdb', 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 ('06FB1ZFJK6J2GSV4SA69QF730C', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Image card caches by path — overwritten file shows stale render', 'clide image show (and the conversation image viewer card) appears to cache by file path: re-showing a file after its bytes change at the SAME path renders the previous image, not the new one. Hit live while iterating on a Frame0 wireframe (re-exported settings-modal.png in place; the pane kept showing the prior version until exported under a new filename).
Expected: showing a path always reflects the current file bytes.
Explore / options:
- Cache-key on mtime + size (or a content hash) instead of path alone; invalidate when they change.
- A force-refresh path for clide image show (re-decode, bypass cache).
- Check where the cache lives: the image card widget (Image.file / ImageProvider cache Flutter''s imageCache keys by path+scale and won''t refresh on overwrite), the IPC image.show handler, or the pane render layer.
- Likely Flutter imageCache: evict the key (or use a FileImage with a key that includes mtime) before showing.
Note: surfaced via clide image show (T-249 / T-252 lightbox share the same provider). Repro: export an image, show it, overwrite the same path with different content, show again -> stale.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:04:53', '2026-06-10 10:08:59', NULL, '92b12d703dccdb13e19da14a9d051e6b', 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 ('06FB16YM1Y910T07Y1Y22MGTEM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Card the ''context'' conversation block like the rest of the cards', 'The injected "context" block (skill-load / command-expansion / system-reminder messages) renders as a frameless `ConversationCardVariant.bare` card in `lib/builtin/claude/src/conversation_view.dart:410-419`, so it reads as a bare `> context …` row wedged between the fully-framed Skill and Bash tool cards (see pasted screenshot). It looks unfinished next to the carded tool calls.
Make the context block follow the standard collapser-card pattern established for all group/tool cards in the `collapser-card` wireframe (docs/design/wireframes/cards/): a proper card frame with the chevron toggle hard against the left edge, label, and the fixed-width right-aligned counter/status slot so it is visually consistent with the Skill/Bash/Activity cards around it.
Keep the de-emphasis that D-78 calls for (muted accent, collapsed-by-default, first-line summary) this is about giving it the same *frame* as the other cards, not the blue "you" accent. The same treatment likely applies to the sibling bare cards that share this branch (agent prompt, thinking) for consistency; scope to context first and note whether thinking/agent-prompt should follow.
Acceptance: the context row sits in a framed card matching the collapser-card wireframe geometry, still muted and collapsed by default, with the toggle and counter/status slot aligned to the other cards. Update/extend the relevant golden(s).', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 08:17:42', '2026-06-10 10:09:11', NULL, '647c5ca8b442dd1f86336889e054f779', 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 ('06FB0TNQM7H8JWCP59WQVD0FW4', 'task', '06FB0TNQM6T6580D8ABDVTMNZW', 'Frame0 wireframe: settings screen design', 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
**Deliverable**
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
**Scope to frame (from T-8)**
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
**Open design questions for the wireframe to answer**
- Settings as a full-screen view, a pane/tab, or a modal?
- Per-project (`.clide/settings.yaml`) vs. user-global scope shown together or switched?
- Search/filter across all settings.
- How schema-driven fields render labels, help text, defaults, and reset.
**Constraints**
- Follow clide visual language pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
This is the design step; implementation of the actual settings UI is separate child work under T-8.
## Scope-tag icon decision (review, 2026-06-10)
The per-field scope tag is an ICON, not a text label (space + cognitive load).
Chosen trio (locationglobal reach ladder), keeping the colour coding + a
tooltip with the word for clarity/a11y; tapping opens the scope menu:
- Project (this repo) = `folder` 0xe24a (teal) already wired
- Always (all clide) = `globe` 0xe288 (amber) add const
- Default (unset) = `circle-dashed` 0xe602 (grey) add const
Alternatives considered: house (warmer homeworld pair), user-circle (mirrors
the real .clide vs ~/.clide files), gear (collides with "settings"). Note that
every Phosphor glyph already renders via PhosphorIconPainter(0xNNNN); only named
consts need adding. Preview/picking is blocked on a native glyph card (see the
new ticket) goldens render the font as Ahem boxes.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-09 20:24:54', '2026-06-10 10:20:48', NULL, '267a1e8c9bab093fa443f7d8272525b9', 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.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 10:20:54', NULL, '645c474b9333b748ea355cb0d8191fd9', 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).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 10:21:09', NULL, '00bfb44c7719b117d224dfe778a5bffe', 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 ('06FB0TNQM7H8JWCP59WQVD0FW4', 'task', '06FB0TNQM6T6580D8ABDVTMNZW', 'Frame0 wireframe: settings screen design', 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
**Deliverable**
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
**Scope to frame (from T-8)**
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
**Open design questions for the wireframe to answer**
- Settings as a full-screen view, a pane/tab, or a modal?
- Per-project (`.clide/settings.yaml`) vs. user-global scope shown together or switched?
- Search/filter across all settings.
- How schema-driven fields render labels, help text, defaults, and reset.
**Constraints**
- Follow clide visual language pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
This is the design step; implementation of the actual settings UI is separate child work under T-8.
## Scope-tag icon decision (review, 2026-06-10)
The per-field scope tag is an ICON, not a text label (space + cognitive load).
Chosen trio (locationglobal reach ladder), keeping the colour coding + a
tooltip with the word for clarity/a11y; tapping opens the scope menu:
- Project (this repo) = `folder` 0xe24a (teal) already wired
- Always (all clide) = `globe` 0xe288 (amber) add const
- Default (unset) = `circle-dashed` 0xe602 (grey) add const
Alternatives considered: house (warmer homeworld pair), user-circle (mirrors
the real .clide vs ~/.clide files), gear (collides with "settings"). Note that
every Phosphor glyph already renders via PhosphorIconPainter(0xNNNN); only named
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);
@@ -0,0 +1,95 @@
{
"name": "Settings Modal",
"shapes": {
"backdrop": { "type": "Rectangle", "left": 0, "top": 0, "width": 1120, "height": 720, "fillColor": "#0B0D11", "strokeColor": "#0B0D11" },
"modal": { "type": "Rectangle", "left": 140, "top": 70, "width": 840, "height": 580, "fillColor": "#21262F", "strokeColor": "#3A4150", "corners": [8, 8, 8, 8] },
"title": { "type": "Text", "parent": "modal", "left": 164, "top": 90, "text": "Settings", "fontColor": "#E2E8F5", "fontSize": 18 },
"close": { "type": "Text", "parent": "modal", "left": 944, "top": 90, "text": "✕", "fontColor": "#6A7280", "fontSize": 16 },
"header-rule": { "type": "Line", "parent": "modal", "left": 140, "top": 134, "width": 840, "height": 1, "strokeColor": "#393E48" },
"sidebar": { "type": "Rectangle", "parent": "modal", "left": 140, "top": 135, "width": 220, "height": 467, "fillColor": "#1C2128", "strokeColor": "#1C2128" },
"sidebar-rule": { "type": "Line", "parent": "modal", "left": 360, "top": 135, "width": 1, "height": 467, "strokeColor": "#393E48" },
"search": { "type": "Rectangle", "parent": "sidebar", "left": 156, "top": 150, "width": 188, "height": 30, "fillColor": "#14181E", "strokeColor": "#393E48", "corners": [6, 6, 6, 6] },
"search-t": { "type": "Text", "parent": "search", "left": 168, "top": 158, "text": "Search settings…", "fontColor": "#5A626E", "fontSize": 12 },
"cat-appearance": { "type": "Text", "parent": "sidebar", "left": 172, "top": 204, "text": "Appearance", "fontColor": "#9DA5B4", "fontSize": 13 },
"cat-editor-bg": { "type": "Rectangle", "parent": "sidebar", "left": 152, "top": 232, "width": 196, "height": 32, "fillColor": "#2A3340", "strokeColor": "#2A3340", "corners": [6, 6, 6, 6] },
"cat-editor": { "type": "Text", "parent": "cat-editor-bg", "left": 172, "top": 240, "text": "Editor", "fontColor": "#E2E8F5", "fontSize": 13 },
"cat-keymap": { "type": "Text", "parent": "sidebar", "left": 172, "top": 280, "text": "Keymap", "fontColor": "#9DA5B4", "fontSize": 13 },
"cat-files": { "type": "Text", "parent": "sidebar", "left": 172, "top": 316, "text": "Files", "fontColor": "#9DA5B4", "fontSize": 13 },
"cat-claude": { "type": "Text", "parent": "sidebar", "left": 172, "top": 352, "text": "Claude", "fontColor": "#9DA5B4", "fontSize": 13 },
"cat-advanced": { "type": "Text", "parent": "sidebar", "left": 172, "top": 388, "text": "Advanced", "fontColor": "#9DA5B4", "fontSize": 13 },
"panel-heading": { "type": "Text", "parent": "modal", "left": 384, "top": 148, "text": "Editor", "fontColor": "#E2E8F5", "fontSize": 16 },
"panel-legend": { "type": "Text", "parent": "modal", "left": 384, "top": 174, "text": "Edits save to this Project by default. Each field's tag is its scope — tap it to make it Always (all clide) or reset. Overridden layers show shadowed inline.", "fontColor": "#6A7280", "fontSize": 11 },
"panel-rule": { "type": "Line", "parent": "modal", "left": 384, "top": 198, "width": 572, "height": 1, "strokeColor": "#30363F" },
"f1-label": { "type": "Text", "parent": "modal", "left": 384, "top": 216, "text": "Tab width", "fontColor": "#E2E8F5", "fontSize": 13 },
"f1-help": { "type": "Text", "parent": "modal", "left": 384, "top": 236, "text": "Spaces per indent level.", "fontColor": "#6A7280", "fontSize": 11 },
"f1-num": { "type": "Rectangle", "parent": "modal", "left": 824, "top": 214, "width": 70, "height": 28, "fillColor": "#14181E", "strokeColor": "#393E48", "corners": [5, 5, 5, 5] },
"f1-num-t": { "type": "Text", "parent": "f1-num", "left": 836, "top": 221, "text": "2", "fontColor": "#E2E8F5", "fontSize": 13 },
"f1-num-step": { "type": "Text", "parent": "f1-num", "left": 874, "top": 220, "text": "⌃⌄", "fontColor": "#6A7280", "fontSize": 11 },
"f1-tag": { "type": "Rectangle", "parent": "modal", "left": 902, "top": 219, "width": 54, "height": 18, "fillColor": "#0E2A26", "strokeColor": "#1A5", "corners": [9, 9, 9, 9] },
"f1-tag-t": { "type": "Text", "parent": "f1-tag", "left": 909, "top": 222, "text": "Project", "fontColor": "#33C6B4", "fontSize": 10 },
"f1-tag-c": { "type": "Text", "parent": "f1-tag", "left": 947, "top": 222, "text": "▾", "fontColor": "#33C6B4", "fontSize": 9 },
"f1-rule": { "type": "Line", "parent": "modal", "left": 384, "top": 262, "width": 572, "height": 1, "strokeColor": "#262B33" },
"f2-label": { "type": "Text", "parent": "modal", "left": 384, "top": 278, "text": "End of line", "fontColor": "#E2E8F5", "fontSize": 13 },
"f2-help": { "type": "Text", "parent": "modal", "left": 384, "top": 298, "text": "Line-ending written on save.", "fontColor": "#6A7280", "fontSize": 11 },
"f2-sel": { "type": "Rectangle", "parent": "modal", "left": 784, "top": 276, "width": 110, "height": 28, "fillColor": "#14181E", "strokeColor": "#393E48", "corners": [5, 5, 5, 5] },
"f2-sel-t": { "type": "Text", "parent": "f2-sel", "left": 796, "top": 283, "text": "LF", "fontColor": "#E2E8F5", "fontSize": 13 },
"f2-sel-c": { "type": "Text", "parent": "f2-sel", "left": 874, "top": 283, "text": "▾", "fontColor": "#6A7280", "fontSize": 12 },
"f2-tag": { "type": "Rectangle", "parent": "modal", "left": 902, "top": 281, "width": 54, "height": 18, "fillColor": "#262B33", "strokeColor": "#30363F", "corners": [9, 9, 9, 9] },
"f2-tag-t": { "type": "Text", "parent": "f2-tag", "left": 912, "top": 284, "text": "Default", "fontColor": "#6A7280", "fontSize": 10 },
"f2-rule": { "type": "Line", "parent": "modal", "left": 384, "top": 324, "width": 572, "height": 1, "strokeColor": "#262B33" },
"f3-label": { "type": "Text", "parent": "modal", "left": 384, "top": 340, "text": "Insert final newline", "fontColor": "#E2E8F5", "fontSize": 13 },
"f3-help": { "type": "Text", "parent": "modal", "left": 384, "top": 360, "text": "Ensure files end with a newline on save.", "fontColor": "#6A7280", "fontSize": 11 },
"f3-tog": { "type": "Rectangle", "parent": "modal", "left": 858, "top": 342, "width": 36, "height": 20, "fillColor": "#00A3D2", "strokeColor": "#00A3D2", "corners": [10, 10, 10, 10] },
"f3-knob": { "type": "Ellipse", "parent": "f3-tog", "left": 876, "top": 344, "width": 16, "height": 16, "fillColor": "#0B0D11", "strokeColor": "#0B0D11" },
"f3-tag": { "type": "Rectangle", "parent": "modal", "left": 902, "top": 343, "width": 54, "height": 18, "fillColor": "#0E2A26", "strokeColor": "#1A5", "corners": [9, 9, 9, 9] },
"f3-tag-t": { "type": "Text", "parent": "f3-tag", "left": 909, "top": 346, "text": "Project", "fontColor": "#33C6B4", "fontSize": 10 },
"f3-rule": { "type": "Line", "parent": "modal", "left": 384, "top": 388, "width": 572, "height": 1, "strokeColor": "#262B33" },
"f4-label": { "type": "Text", "parent": "modal", "left": 384, "top": 404, "text": "Trim trailing whitespace", "fontColor": "#E2E8F5", "fontSize": 13 },
"f4-help": { "type": "Text", "parent": "modal", "left": 384, "top": 424, "text": "Strip trailing spaces from each line on save.", "fontColor": "#6A7280", "fontSize": 11 },
"f4-tog": { "type": "Rectangle", "parent": "modal", "left": 858, "top": 406, "width": 36, "height": 20, "fillColor": "#00A3D2", "strokeColor": "#00A3D2", "corners": [10, 10, 10, 10] },
"f4-knob": { "type": "Ellipse", "parent": "f4-tog", "left": 876, "top": 408, "width": 16, "height": 16, "fillColor": "#0B0D11", "strokeColor": "#0B0D11" },
"f4-tag": { "type": "Rectangle", "parent": "modal", "left": 896, "top": 407, "width": 60, "height": 18, "fillColor": "#2A2410", "strokeColor": "#6E5A1C", "corners": [9, 9, 9, 9] },
"f4-tag-t": { "type": "Text", "parent": "f4-tag", "left": 906, "top": 410, "text": "Always", "fontColor": "#E0A458", "fontSize": 10 },
"f4-rule": { "type": "Line", "parent": "modal", "left": 384, "top": 452, "width": 572, "height": 1, "strokeColor": "#262B33" },
"f5-label": { "type": "Text", "parent": "modal", "left": 384, "top": 468, "text": "Max line length", "fontColor": "#E2E8F5", "fontSize": 13 },
"f5-help": { "type": "Text", "parent": "modal", "left": 384, "top": 488, "text": "Column for the editor ruler. 0 disables it.", "fontColor": "#6A7280", "fontSize": 11 },
"f5-num": { "type": "Rectangle", "parent": "modal", "left": 824, "top": 466, "width": 70, "height": 28, "fillColor": "#14181E", "strokeColor": "#00AB9A", "corners": [5, 5, 5, 5] },
"f5-num-t": { "type": "Text", "parent": "f5-num", "left": 834, "top": 473, "text": "100", "fontColor": "#E2E8F5", "fontSize": 13 },
"f5-num-step": { "type": "Text", "parent": "f5-num", "left": 874, "top": 472, "text": "⌃⌄", "fontColor": "#6A7280", "fontSize": 11 },
"f5-tag": { "type": "Rectangle", "parent": "modal", "left": 902, "top": 471, "width": 54, "height": 18, "fillColor": "#0E2A26", "strokeColor": "#1A5", "corners": [9, 9, 9, 9] },
"f5-tag-t": { "type": "Text", "parent": "f5-tag", "left": 910, "top": 474, "text": "Project", "fontColor": "#33C6B4", "fontSize": 10 },
"f5-shadow": { "type": "Text", "parent": "modal", "left": 384, "top": 506, "text": "↳ overrides Always 80 · Default 0", "fontColor": "#5A626E", "fontSize": 11 },
"f5-rule": { "type": "Line", "parent": "modal", "left": 384, "top": 528, "width": 572, "height": 1, "strokeColor": "#262B33" },
"f6-label": { "type": "Text", "parent": "modal", "left": 384, "top": 544, "text": "Project .editorconfig", "fontColor": "#E2E8F5", "fontSize": 13 },
"f6-help": { "type": "Text", "parent": "modal", "left": 384, "top": 564, "text": "Per-file rules layered on top of these defaults (T-290).", "fontColor": "#6A7280", "fontSize": 11 },
"f6-btn": { "type": "Rectangle", "parent": "modal", "left": 824, "top": 542, "width": 132, "height": 28, "fillColor": "#1A1E24", "strokeColor": "#3A4150", "corners": [5, 5, 5, 5] },
"f6-btn-t": { "type": "Text", "parent": "f6-btn", "left": 838, "top": 549, "text": "Edit file ↗", "fontColor": "#9DA5B4", "fontSize": 12 },
"footer-rule": { "type": "Line", "parent": "modal", "left": 140, "top": 602, "width": 840, "height": 1, "strokeColor": "#393E48" },
"footer-hint": { "type": "Text", "parent": "modal", "left": 384, "top": 620, "text": "Changes apply immediately. Project ▸ Always ▸ Default precedence; tags: Project teal · Always amber · Default grey.", "fontColor": "#6A7280", "fontSize": 11 },
"footer-done": { "type": "Rectangle", "parent": "modal", "left": 884, "top": 614, "width": 72, "height": 28, "fillColor": "#2A3340", "strokeColor": "#3A4150", "corners": [5, 5, 5, 5] },
"footer-done-t": { "type": "Text", "parent": "footer-done", "left": 908, "top": 621, "text": "Done", "fontColor": "#E2E8F5", "fontSize": 13 },
"menu": { "type": "Rectangle", "left": 800, "top": 242, "width": 166, "height": 84, "fillColor": "#1A1E24", "strokeColor": "#3A4150", "corners": [6, 6, 6, 6] },
"menu-r1-bg": { "type": "Rectangle", "parent": "menu", "left": 804, "top": 246, "width": 158, "height": 22, "fillColor": "#0E2A26", "strokeColor": "#0E2A26", "corners": [4, 4, 4, 4] },
"menu-r1": { "type": "Text", "parent": "menu-r1-bg", "left": 812, "top": 251, "text": "✓ This project", "fontColor": "#33C6B4", "fontSize": 11 },
"menu-r2": { "type": "Text", "parent": "menu", "left": 812, "top": 277, "text": "Always — all clide", "fontColor": "#E0A458", "fontSize": 11 },
"menu-div": { "type": "Line", "parent": "menu", "left": 806, "top": 300, "width": 154, "height": 1, "strokeColor": "#30363F" },
"menu-r3": { "type": "Text", "parent": "menu", "left": 812, "top": 307, "text": "Reset to default", "fontColor": "#9DA5B4", "fontSize": 11 },
"note": { "type": "Text", "left": 140, "top": 662, "text": "Settings modal (T-302) — no header toggle: scope lives in the per-line tag. Edits default to PROJECT (.clide/settings.yaml). Tap a tag to flip it to ALWAYS (global, all clide) or Reset. Effective value + source tag always shown; overridden layers shadowed inline. Precedence Project ▸ Always ▸ Default.", "fontColor": "#5A626E", "fontSize": 11 }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB