feat(settings): per-field scope tags + scope resolution (T-449)

Each settings field gains a scope tag showing where its value lives — folder
= Project (.clide), globe = Always (~/.clide), circle-dashed = Default/unset —
colour-coded (statusSuccess / statusWarning / muted) with a tooltip. Tapping
opens a menu to move the value between the scopes the key supports, or reset
to default; the tag's menu replaces the interim reset button.

Backs it with scope-explicit SettingsStore access — rawAt / setAt / removeAt /
effectiveLayer / writableLayers — over the two storage files (app ~/.clide,
project .clide). ext.* keys layer project-over-app; app.*/project.* keys live
only in their prefix's file, so their menu offers that one scope + reset.

Tests: store scope ops (layering, reload, guards) and the tag (Default vs
All-clide rendering, menu reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:18:38 +02:00
co-authored by Claude Opus 4.8
parent 6c6b0c731e
commit f56ad88439
11 changed files with 320 additions and 51 deletions
+19 -10
View File
@@ -227,18 +227,27 @@ class _RailRow extends StatelessWidget {
cursor: SystemMouseCursors.click,
onTap: onTap,
builder: (ctx, hovered, _) => Container(
decoration: BoxDecoration(
color: selected ? tokens.sidebarItemSelected : (hovered ? tokens.sidebarItemHover : null),
border: Border(left: BorderSide(color: selected ? tokens.panelActiveBorder : const Color(0x00000000), width: 2)),
),
padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),
// color: null => no paint (shows the modal surface behind the rail).
color: selected ? tokens.sidebarItemSelected : (hovered ? tokens.sidebarItemHover : null),
child: Row(
children: [
if (category.iconName != null) ...[
ClideIcon(PhosphorIcons.byName(category.iconName!), size: 15, color: fg),
const SizedBox(width: 8),
],
Expanded(child: ClideText(category.title, color: fg, maxLines: 1, overflow: TextOverflow.ellipsis)),
// Accent left-stripe; the 2px slot is reserved either way so the
// row never shifts. Painted only when selected — no color literal.
SizedBox(width: 2, child: selected ? ColoredBox(color: tokens.panelActiveBorder) : null),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 7, 12, 7),
child: Row(
children: [
if (category.iconName != null) ...[
ClideIcon(PhosphorIcons.byName(category.iconName!), size: 15, color: fg),
const SizedBox(width: 8),
],
Expanded(child: ClideText(category.title, color: fg, maxLines: 1, overflow: TextOverflow.ellipsis)),
],
),
),
),
],
),
),