scaffold Flutter app — kernel, extensions, widgets, Tier 0 built-ins

First real content under app/. Lays the whole Tier 0 foundation in one
commit because the pieces depend on each other circularly (kernel →
extension → widgets → built-ins all reference types from the layer
below); splitting would leave intermediate commits that don't compile.

Key shapes:

  * bare WidgetsApp root — no Material, no Cupertino, no Scaffold.
    ClideTheme InheritedWidget is the only source of tokens.
  * ClideKernel InheritedWidget aggregates 18 services (settings,
    project, extensions, theme, panels, events, ipc, commands +
    palette + keybindings, clipboard, files, notify, dialog, tray,
    secrets, os, net, focus, log, i18n). ExtensionContext exposes
    them through a stable interface.
  * ClideExtension + sealed ContributionPoint hierarchy (Tab,
    StatusItem, Toolbar, Command, TrayItem, LayoutPreset). One
    manifest ships N contributions into kernel slots.
  * Three-tier theme pipeline: palette (named colors) → semantic
    roles → ~60 surface tokens. Defaults at each layer so legacy
    palette-only themes produce a complete SurfaceTokens.
  * A11y baked in from day one — every interactive primitive wraps
    in Semantics(label:, hint:, button:); ensureSemantics() at boot;
    theme/contrast.dart helper exposes token pairs for the WCAG
    gate in the a11y test suite.
  * i18n ported from fframe's L10n pattern (text-driven, namespaced
    JSON catalogs, caller-supplied placeholders) with a proper
    locale fallback chain (exact → language → default → placeholder)
    fframe lacks.
  * Four Tier-0 built-ins live (default-layout, welcome, ipc-status,
    theme-picker) plus 17 id-reserving stubs so later tiers fill in
    without rename churn.

.gitignore extended to cover app/ sub-package artefacts and the
Playwright harness scratch dirs.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-21 15:39:41 +02:00
co-authored by Claude
parent 235cbcc046
commit b13483e2d5
167 changed files with 8270 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
import 'dart:ui';
import 'package:clide_app/kernel/src/theme/palette.dart';
import 'package:clide_app/kernel/src/theme/semantic.dart';
import 'package:clide_app/kernel/src/theme/tokens.dart';
/// Three-tier theme resolution.
///
/// palette (raw colors)
/// ↓ (ref-chain; defaults inherited)
/// semantic (role → palette)
/// ↓ (ref-chain; defaults inherited)
/// surface (token → semantic|palette|literal)
///
/// References take the form:
/// `semantic.<role>` — look up in [semantic]
/// `#rrggbb` / `#aarrggbb` — literal hex
/// bare name — palette lookup
class ThemeResolver {
const ThemeResolver();
SurfaceTokens resolve({
required Palette palette,
SemanticRoles? semanticOverride,
Map<String, String>? surfaceOverride,
Map<String, String>? extensionOverride,
}) {
final semantic = _buildSemantic(palette, semanticOverride);
final surface = <String, Color>{};
for (final key in TokenKeys.all) {
surface[key] = _resolveSurface(
key: key,
palette: palette,
semantic: semantic,
surfaceOverride: surfaceOverride,
);
}
final extTokens = <String, Color>{};
if (extensionOverride != null) {
for (final entry in extensionOverride.entries) {
final resolved = _resolveRef(entry.value, palette, semantic);
if (resolved != null) extTokens[entry.key] = resolved;
}
}
return SurfaceTokens(
globalForeground: surface[TokenKeys.globalForeground]!,
globalBackground: surface[TokenKeys.globalBackground]!,
globalBorder: surface[TokenKeys.globalBorder]!,
globalFocus: surface[TokenKeys.globalFocus]!,
globalTextMuted: surface[TokenKeys.globalTextMuted]!,
panelBackground: surface[TokenKeys.panelBackground]!,
panelBorder: surface[TokenKeys.panelBorder]!,
panelActiveBorder: surface[TokenKeys.panelActiveBorder]!,
panelHeader: surface[TokenKeys.panelHeader]!,
panelHeaderForeground: surface[TokenKeys.panelHeaderForeground]!,
sidebarBackground: surface[TokenKeys.sidebarBackground]!,
sidebarForeground: surface[TokenKeys.sidebarForeground]!,
sidebarItemHover: surface[TokenKeys.sidebarItemHover]!,
sidebarItemSelected: surface[TokenKeys.sidebarItemSelected]!,
sidebarSectionHeader: surface[TokenKeys.sidebarSectionHeader]!,
statusBarBackground: surface[TokenKeys.statusBarBackground]!,
statusBarForeground: surface[TokenKeys.statusBarForeground]!,
statusBarItemActiveBackground:
surface[TokenKeys.statusBarItemActiveBackground]!,
statusBarItemHoverBackground:
surface[TokenKeys.statusBarItemHoverBackground]!,
tabBarBackground: surface[TokenKeys.tabBarBackground]!,
tabActive: surface[TokenKeys.tabActive]!,
tabInactive: surface[TokenKeys.tabInactive]!,
tabActiveForeground: surface[TokenKeys.tabActiveForeground]!,
tabInactiveForeground: surface[TokenKeys.tabInactiveForeground]!,
tabActiveBorder: surface[TokenKeys.tabActiveBorder]!,
tabCloseHover: surface[TokenKeys.tabCloseHover]!,
buttonBackground: surface[TokenKeys.buttonBackground]!,
buttonForeground: surface[TokenKeys.buttonForeground]!,
buttonHoverBackground: surface[TokenKeys.buttonHoverBackground]!,
buttonActiveBackground: surface[TokenKeys.buttonActiveBackground]!,
buttonBorder: surface[TokenKeys.buttonBorder]!,
listItemBackground: surface[TokenKeys.listItemBackground]!,
listItemForeground: surface[TokenKeys.listItemForeground]!,
listItemHoverBackground: surface[TokenKeys.listItemHoverBackground]!,
listItemSelectedBackground:
surface[TokenKeys.listItemSelectedBackground]!,
listItemSelectedForeground:
surface[TokenKeys.listItemSelectedForeground]!,
scrollbarSlider: surface[TokenKeys.scrollbarSlider]!,
scrollbarSliderHover: surface[TokenKeys.scrollbarSliderHover]!,
scrollbarTrack: surface[TokenKeys.scrollbarTrack]!,
tooltipBackground: surface[TokenKeys.tooltipBackground]!,
tooltipForeground: surface[TokenKeys.tooltipForeground]!,
tooltipBorder: surface[TokenKeys.tooltipBorder]!,
dropdownBackground: surface[TokenKeys.dropdownBackground]!,
dropdownForeground: surface[TokenKeys.dropdownForeground]!,
dropdownBorder: surface[TokenKeys.dropdownBorder]!,
modalOverlayBackground: surface[TokenKeys.modalOverlayBackground]!,
modalSurfaceBackground: surface[TokenKeys.modalSurfaceBackground]!,
modalSurfaceBorder: surface[TokenKeys.modalSurfaceBorder]!,
dividerColor: surface[TokenKeys.dividerColor]!,
statusSuccess: surface[TokenKeys.statusSuccess]!,
statusWarning: surface[TokenKeys.statusWarning]!,
statusError: surface[TokenKeys.statusError]!,
statusInfo: surface[TokenKeys.statusInfo]!,
extensionTokens: extTokens,
);
}
SemanticRoles _buildSemantic(Palette palette, SemanticRoles? override) {
final roles = <String, Color>{};
for (final role in SemanticKeys.all) {
final fromOverride = override?.lookup(role);
if (fromOverride != null) {
roles[role] = fromOverride;
continue;
}
for (final candidate in _defaultSemanticFallbacks[role] ?? [role]) {
final fromPalette = palette.lookup(candidate);
if (fromPalette != null) {
roles[role] = fromPalette;
break;
}
}
// If still unresolved, fall back to foreground/background so the
// theme never has a null surface color. Themes that omit these
// will land readable if uninspired.
roles.putIfAbsent(role, () {
return palette.lookup('foreground') ??
palette.lookup('background') ??
const Color(0xFFFFFFFF);
});
}
return SemanticRoles(roles);
}
Color _resolveSurface({
required String key,
required Palette palette,
required SemanticRoles semantic,
Map<String, String>? surfaceOverride,
}) {
final override = surfaceOverride?[key];
if (override != null) {
final resolved = _resolveRef(override, palette, semantic);
if (resolved != null) return resolved;
}
final defaultRef = _defaultSurfaceMap[key];
if (defaultRef != null) {
final resolved = _resolveRef(defaultRef, palette, semantic);
if (resolved != null) return resolved;
}
// Last-ditch: something has to render. Fall back to semantic text.
return semantic.lookup(SemanticKeys.text) ?? const Color(0xFFFFFFFF);
}
Color? _resolveRef(String ref, Palette palette, SemanticRoles semantic) {
if (ref.startsWith('#')) return Palette.parseHex(ref);
if (ref.startsWith('semantic.')) {
return semantic.lookup(ref.substring('semantic.'.length));
}
return palette.lookup(ref);
}
}
/// Default palette names a semantic role will try, in order, when the
/// theme doesn't override the role explicitly.
const Map<String, List<String>> _defaultSemanticFallbacks = {
SemanticKeys.mainchrome: ['panel', 'surface', 'background'],
SemanticKeys.calltoaction: ['accent', 'primary'],
SemanticKeys.focus: ['primary', 'accent'],
SemanticKeys.background: ['background'],
SemanticKeys.surface: ['surface', 'panel'],
SemanticKeys.text: ['foreground'],
SemanticKeys.textMuted: ['muted', 'secondary', 'foreground'],
SemanticKeys.success: ['success'],
SemanticKeys.warning: ['warning'],
SemanticKeys.error: ['error'],
SemanticKeys.info: ['info', 'primary'],
};
/// Default surface map. Every entry resolves through the semantic layer
/// where it makes sense; raw palette refs are used only where the
/// semantic layer doesn't have a role that fits.
const Map<String, String> _defaultSurfaceMap = {
TokenKeys.globalForeground: 'semantic.text',
TokenKeys.globalBackground: 'semantic.background',
TokenKeys.globalBorder: 'semantic.surface',
TokenKeys.globalFocus: 'semantic.focus',
TokenKeys.globalTextMuted: 'semantic.text_muted',
TokenKeys.panelBackground: 'semantic.mainchrome',
TokenKeys.panelBorder: 'semantic.surface',
TokenKeys.panelActiveBorder: 'semantic.focus',
TokenKeys.panelHeader: 'semantic.mainchrome',
TokenKeys.panelHeaderForeground: 'semantic.text',
TokenKeys.sidebarBackground: 'semantic.mainchrome',
TokenKeys.sidebarForeground: 'semantic.text',
TokenKeys.sidebarItemHover: 'semantic.surface',
TokenKeys.sidebarItemSelected: 'semantic.focus',
TokenKeys.sidebarSectionHeader: 'semantic.text_muted',
TokenKeys.statusBarBackground: 'semantic.mainchrome',
TokenKeys.statusBarForeground: 'semantic.text',
TokenKeys.statusBarItemActiveBackground: 'semantic.focus',
TokenKeys.statusBarItemHoverBackground: 'semantic.surface',
TokenKeys.tabBarBackground: 'semantic.mainchrome',
TokenKeys.tabActive: 'semantic.background',
TokenKeys.tabInactive: 'semantic.mainchrome',
TokenKeys.tabActiveForeground: 'semantic.text',
TokenKeys.tabInactiveForeground: 'semantic.text_muted',
TokenKeys.tabActiveBorder: 'semantic.focus',
TokenKeys.tabCloseHover: 'semantic.error',
TokenKeys.buttonBackground: 'semantic.surface',
TokenKeys.buttonForeground: 'semantic.text',
TokenKeys.buttonHoverBackground: 'semantic.mainchrome',
TokenKeys.buttonActiveBackground: 'semantic.focus',
TokenKeys.buttonBorder: 'semantic.surface',
TokenKeys.listItemBackground: 'semantic.background',
TokenKeys.listItemForeground: 'semantic.text',
TokenKeys.listItemHoverBackground: 'semantic.surface',
TokenKeys.listItemSelectedBackground: 'semantic.focus',
TokenKeys.listItemSelectedForeground: 'semantic.background',
TokenKeys.scrollbarSlider: 'semantic.surface',
TokenKeys.scrollbarSliderHover: 'semantic.text_muted',
TokenKeys.scrollbarTrack: 'semantic.mainchrome',
TokenKeys.tooltipBackground: 'semantic.surface',
TokenKeys.tooltipForeground: 'semantic.text',
TokenKeys.tooltipBorder: 'semantic.mainchrome',
TokenKeys.dropdownBackground: 'semantic.surface',
TokenKeys.dropdownForeground: 'semantic.text',
TokenKeys.dropdownBorder: 'semantic.mainchrome',
TokenKeys.modalOverlayBackground: '#C0000000',
TokenKeys.modalSurfaceBackground: 'semantic.mainchrome',
TokenKeys.modalSurfaceBorder: 'semantic.focus',
TokenKeys.dividerColor: 'semantic.surface',
TokenKeys.statusSuccess: 'semantic.success',
TokenKeys.statusWarning: 'semantic.warning',
TokenKeys.statusError: 'semantic.error',
TokenKeys.statusInfo: 'semantic.info',
};