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
+151
View File
@@ -0,0 +1,151 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:clide_app/kernel/src/theme/tokens.dart';
import 'package:flutter/foundation.dart';
/// A foreground/background token pair the a11y contrast suite walks.
@immutable
class ContrastPair {
const ContrastPair({
required this.name,
required this.foreground,
required this.background,
this.largeText = false,
});
final String name;
final Color foreground;
final Color background;
/// WCAG AA threshold for "large text" (18pt, or 14pt bold) is 3:1;
/// normal text is 4.5:1. Mark a pair as [largeText] when the rendered
/// typography qualifies.
final bool largeText;
}
/// Compute the WCAG 2.x relative-luminance ratio between two colors.
///
/// Alpha is pre-composited against a neutral grey so semi-transparent
/// tokens don't spuriously pass. Returns a value in `[1, 21]`.
double contrastRatio(Color a, Color b, {Color onto = const Color(0xFF808080)}) {
final la = _relativeLuminance(_composite(a, onto));
final lb = _relativeLuminance(_composite(b, onto));
final brighter = math.max(la, lb);
final darker = math.min(la, lb);
return (brighter + 0.05) / (darker + 0.05);
}
/// Minimum ratio required for this pair per WCAG AA.
double minimumRatio(ContrastPair pair) => pair.largeText ? 3.0 : 4.5;
/// Canonical set of token pairs each bundled theme must honour.
///
/// The a11y contrast test walks this list per-theme.
List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
ContrastPair(
name: 'global.text_on_background',
foreground: s.globalForeground,
background: s.globalBackground,
),
ContrastPair(
name: 'panel.header_foreground_on_panel',
foreground: s.panelHeaderForeground,
background: s.panelHeader,
),
ContrastPair(
name: 'sidebar.foreground_on_sidebar',
foreground: s.sidebarForeground,
background: s.sidebarBackground,
),
ContrastPair(
name: 'statusbar.foreground_on_statusbar',
foreground: s.statusBarForeground,
background: s.statusBarBackground,
),
ContrastPair(
name: 'tab.active_text_on_active_bg',
foreground: s.tabActiveForeground,
background: s.tabActive,
),
ContrastPair(
name: 'tab.inactive_text_on_inactive_bg',
foreground: s.tabInactiveForeground,
background: s.tabInactive,
),
ContrastPair(
name: 'button.text_on_button',
foreground: s.buttonForeground,
background: s.buttonBackground,
),
ContrastPair(
name: 'listItem.selected_text_on_selected_bg',
foreground: s.listItemSelectedForeground,
background: s.listItemSelectedBackground,
),
ContrastPair(
name: 'listItem.text_on_list',
foreground: s.listItemForeground,
background: s.listItemBackground,
),
ContrastPair(
name: 'tooltip.text_on_tooltip',
foreground: s.tooltipForeground,
background: s.tooltipBackground,
),
ContrastPair(
name: 'dropdown.text_on_dropdown',
foreground: s.dropdownForeground,
background: s.dropdownBackground,
),
];
/// Convenience for tests: returns the list of pairs that fail WCAG AA.
List<ContrastFailure> failingPairs(SurfaceTokens tokens) {
final out = <ContrastFailure>[];
for (final p in canonicalPairs(tokens)) {
final ratio = contrastRatio(p.foreground, p.background);
final need = minimumRatio(p);
if (ratio < need) {
out.add(ContrastFailure(pair: p, ratio: ratio, minimum: need));
}
}
return out;
}
@immutable
class ContrastFailure {
const ContrastFailure({
required this.pair,
required this.ratio,
required this.minimum,
});
final ContrastPair pair;
final double ratio;
final double minimum;
@override
String toString() => 'contrast ${pair.name}: ${ratio.toStringAsFixed(2)} < '
'${minimum.toStringAsFixed(1)}';
}
// -- internals ---------------------------------------------------------------
Color _composite(Color src, Color dst) {
final a = src.a;
if (a >= 0.999) return src;
double mix(double s, double d) => s * a + d * (1 - a);
return Color.from(
alpha: 1.0,
red: mix(src.r, dst.r),
green: mix(src.g, dst.g),
blue: mix(src.b, dst.b),
);
}
double _relativeLuminance(Color c) {
double chan(double v) =>
v <= 0.03928 ? v / 12.92 : math.pow((v + 0.055) / 1.055, 2.4).toDouble();
return 0.2126 * chan(c.r) + 0.7152 * chan(c.g) + 0.0722 * chan(c.b);
}
+106
View File
@@ -0,0 +1,106 @@
import 'package:clide_app/kernel/src/theme/loader.dart';
import 'package:clide_app/kernel/src/theme/resolver.dart';
import 'package:clide_app/kernel/src/theme/tokens.dart';
import 'package:flutter/widgets.dart';
@immutable
class ClideThemeData {
const ClideThemeData({
required this.name,
required this.displayName,
required this.dark,
required this.surface,
});
final String name;
final String displayName;
final bool dark;
final SurfaceTokens surface;
}
class ThemeController extends ChangeNotifier {
ThemeController({
required List<ThemeDefinition> bundled,
ThemeResolver resolver = const ThemeResolver(),
String? initialName,
}) : _resolver = resolver,
_defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) {
final first = initialName != null && _defs.containsKey(initialName)
? initialName
: bundled.first.name;
_currentName = first;
_current = _build(first);
}
final ThemeResolver _resolver;
final Map<String, ThemeDefinition> _defs;
late String _currentName;
late ClideThemeData _current;
ClideThemeData get current => _current;
String get currentName => _currentName;
List<ThemeDefinition> get available => _defs.values.toList(growable: false);
void select(String name) {
if (!_defs.containsKey(name)) {
throw ArgumentError('Unknown theme: $name');
}
if (name == _currentName) return;
_currentName = name;
_current = _build(name);
notifyListeners();
}
void registerTheme(ThemeDefinition def) {
_defs[def.name] = def;
// If the user re-imported the current theme, rebuild so overrides
// take effect without a select().
if (def.name == _currentName) {
_current = _build(def.name);
notifyListeners();
}
}
ClideThemeData _build(String name) {
final def = _defs[name]!;
final tokens = _resolver.resolve(
palette: def.palette,
semanticOverride: def.semanticOverride,
surfaceOverride: def.surfaceOverride,
extensionOverride: def.extensionOverride,
);
return ClideThemeData(
name: def.name,
displayName: def.displayName,
dark: def.dark,
surface: tokens,
);
}
}
class ClideTheme extends InheritedNotifier<ThemeController> {
const ClideTheme({
super.key,
required ThemeController controller,
required super.child,
}) : super(notifier: controller);
static ClideThemeData of(BuildContext context) {
final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>();
if (w == null) {
throw FlutterError(
'ClideTheme.of() called with a context that is not a descendant of a ClideTheme.');
}
return w.notifier!.current;
}
static ThemeController controllerOf(BuildContext context) {
final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>();
if (w == null) {
throw FlutterError(
'ClideTheme.controllerOf() called with a context that is not a descendant of a ClideTheme.');
}
return w.notifier!;
}
}
+106
View File
@@ -0,0 +1,106 @@
import 'dart:io';
import 'package:clide_app/kernel/src/theme/palette.dart';
import 'package:clide_app/kernel/src/theme/semantic.dart';
import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart';
class ThemeDefinition {
const ThemeDefinition({
required this.name,
required this.displayName,
required this.dark,
required this.palette,
this.semanticOverride,
this.surfaceOverride,
this.extensionOverride,
});
final String name;
final String displayName;
final bool dark;
final Palette palette;
final SemanticRoles? semanticOverride;
final Map<String, String>? surfaceOverride;
final Map<String, String>? extensionOverride;
}
class ThemeLoader {
const ThemeLoader();
ThemeDefinition fromYamlString(String text, {String? fallbackName}) {
final doc = loadYaml(text);
if (doc is! Map) {
throw FormatException('Theme root is not a map');
}
final name = (doc['name'] as String?) ?? fallbackName;
if (name == null || name.isEmpty) {
throw const FormatException('Theme missing `name`');
}
final displayName = (doc['display_name'] as String?) ?? name;
final dark = (doc['dark'] as bool?) ?? true;
final paletteYaml = doc['palette'];
if (paletteYaml is! Map) {
throw const FormatException('Theme missing `palette`');
}
final palette = _parsePalette(paletteYaml);
final semantic = doc['semantic'];
final surface = doc['surface'];
final extension = doc['extension'];
return ThemeDefinition(
name: name,
displayName: displayName,
dark: dark,
palette: palette,
semanticOverride:
semantic is Map ? _parseSemantic(semantic, palette) : null,
surfaceOverride: surface is Map ? _parseRefMap(surface) : null,
extensionOverride: extension is Map ? _parseRefMap(extension) : null,
);
}
Future<ThemeDefinition> fromAsset(
AssetBundle bundle, String assetPath) async {
final txt = await bundle.loadString(assetPath);
final fallback = assetPath.split('/').last.replaceAll('.yaml', '');
return fromYamlString(txt, fallbackName: fallback);
}
Future<ThemeDefinition> fromFile(File f) async {
final txt = await f.readAsString();
final fallback = f.uri.pathSegments.last.replaceAll('.yaml', '');
return fromYamlString(txt, fallbackName: fallback);
}
}
Palette _parsePalette(Map src) {
final colors = <String, Color>{};
src.forEach((k, v) {
if (v is! String) return;
final c = Palette.parseHex(v);
if (c != null) colors['$k'] = c;
});
return Palette(colors);
}
SemanticRoles _parseSemantic(Map src, Palette palette) {
final roles = <String, Color>{};
src.forEach((k, v) {
if (v is! String) return;
final resolved =
v.startsWith('#') ? Palette.parseHex(v) : palette.lookup(v);
if (resolved != null) roles['$k'] = resolved;
});
return SemanticRoles(roles);
}
Map<String, String> _parseRefMap(Map src) {
final out = <String, String>{};
src.forEach((k, v) {
if (v is String) out['$k'] = v;
});
return out;
}
+22
View File
@@ -0,0 +1,22 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
@immutable
class Palette {
const Palette(this._colors);
final Map<String, Color> _colors;
Color? lookup(String name) => _colors[name];
Iterable<String> get names => _colors.keys;
static Color? parseHex(String s) {
var v = s.trim();
if (v.startsWith('#')) v = v.substring(1);
if (v.length == 6) v = 'FF$v';
if (v.length != 8) return null;
final n = int.tryParse(v, radix: 16);
if (n == null) return null;
return Color(n);
}
}
+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',
};
+40
View File
@@ -0,0 +1,40 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
@immutable
class SemanticRoles {
const SemanticRoles(this._roles);
final Map<String, Color> _roles;
Color? lookup(String role) => _roles[role];
Iterable<String> get roles => _roles.keys;
}
abstract class SemanticKeys {
static const mainchrome = 'mainchrome';
static const calltoaction = 'calltoaction';
static const focus = 'focus';
static const background = 'background';
static const surface = 'surface';
static const text = 'text';
static const textMuted = 'text_muted';
static const success = 'success';
static const warning = 'warning';
static const error = 'error';
static const info = 'info';
static const all = <String>[
mainchrome,
calltoaction,
focus,
background,
surface,
text,
textMuted,
success,
warning,
error,
info,
];
}
@@ -0,0 +1,29 @@
# Summer Night — ported from legacy clide v1.2.0.
# Palette-only; the three-tier resolver fills semantic + surface from
# defaults. Override sections land here as the token surface grows.
name: summer-night
display_name: Summer Night
dark: true
palette:
# Accents (legacy names: primary/secondary/accent)
primary: "#00a3d2" # cyan
secondary: "#00a9b9" # teal
accent: "#fa5f8b" # pink
# Backgrounds
background: "#21262f"
surface: "#393e48"
panel: "#292e38"
# Text. `muted` is WCAG-AA-calibrated against `panel` — don't darken
# without re-running the a11y/contrast suite.
foreground: "#e2e8f5"
muted: "#a6adbb"
# Status
success: "#00ab9a"
warning: "#d08447"
error: "#f06c6f"
info: "#00a3d2"
+293
View File
@@ -0,0 +1,293 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
/// Resolved surface tokens — the only thing widgets consume.
///
/// The token surface grows as features need more of it. Every token
/// declared here must have a default resolution in
/// [DefaultSurfaceMap] so legacy palette-only themes produce a complete
/// SurfaceTokens without declaring the full surface.
@immutable
class SurfaceTokens {
const SurfaceTokens({
// global
required this.globalForeground,
required this.globalBackground,
required this.globalBorder,
required this.globalFocus,
required this.globalTextMuted,
// panel
required this.panelBackground,
required this.panelBorder,
required this.panelActiveBorder,
required this.panelHeader,
required this.panelHeaderForeground,
// sidebar
required this.sidebarBackground,
required this.sidebarForeground,
required this.sidebarItemHover,
required this.sidebarItemSelected,
required this.sidebarSectionHeader,
// statusbar
required this.statusBarBackground,
required this.statusBarForeground,
required this.statusBarItemActiveBackground,
required this.statusBarItemHoverBackground,
// tabs
required this.tabBarBackground,
required this.tabActive,
required this.tabInactive,
required this.tabActiveForeground,
required this.tabInactiveForeground,
required this.tabActiveBorder,
required this.tabCloseHover,
// buttons
required this.buttonBackground,
required this.buttonForeground,
required this.buttonHoverBackground,
required this.buttonActiveBackground,
required this.buttonBorder,
// list items
required this.listItemBackground,
required this.listItemForeground,
required this.listItemHoverBackground,
required this.listItemSelectedBackground,
required this.listItemSelectedForeground,
// scrollbar
required this.scrollbarSlider,
required this.scrollbarSliderHover,
required this.scrollbarTrack,
// tooltip
required this.tooltipBackground,
required this.tooltipForeground,
required this.tooltipBorder,
// dropdown
required this.dropdownBackground,
required this.dropdownForeground,
required this.dropdownBorder,
// modal
required this.modalOverlayBackground,
required this.modalSurfaceBackground,
required this.modalSurfaceBorder,
// divider
required this.dividerColor,
// status
required this.statusSuccess,
required this.statusWarning,
required this.statusError,
required this.statusInfo,
required this.extensionTokens,
});
final Color globalForeground;
final Color globalBackground;
final Color globalBorder;
final Color globalFocus;
final Color globalTextMuted;
final Color panelBackground;
final Color panelBorder;
final Color panelActiveBorder;
final Color panelHeader;
final Color panelHeaderForeground;
final Color sidebarBackground;
final Color sidebarForeground;
final Color sidebarItemHover;
final Color sidebarItemSelected;
final Color sidebarSectionHeader;
final Color statusBarBackground;
final Color statusBarForeground;
final Color statusBarItemActiveBackground;
final Color statusBarItemHoverBackground;
final Color tabBarBackground;
final Color tabActive;
final Color tabInactive;
final Color tabActiveForeground;
final Color tabInactiveForeground;
final Color tabActiveBorder;
final Color tabCloseHover;
final Color buttonBackground;
final Color buttonForeground;
final Color buttonHoverBackground;
final Color buttonActiveBackground;
final Color buttonBorder;
final Color listItemBackground;
final Color listItemForeground;
final Color listItemHoverBackground;
final Color listItemSelectedBackground;
final Color listItemSelectedForeground;
final Color scrollbarSlider;
final Color scrollbarSliderHover;
final Color scrollbarTrack;
final Color tooltipBackground;
final Color tooltipForeground;
final Color tooltipBorder;
final Color dropdownBackground;
final Color dropdownForeground;
final Color dropdownBorder;
final Color modalOverlayBackground;
final Color modalSurfaceBackground;
final Color modalSurfaceBorder;
final Color dividerColor;
final Color statusSuccess;
final Color statusWarning;
final Color statusError;
final Color statusInfo;
/// Extension-declared tokens keyed by their dotted path
/// (e.g. `ext.sqlite.table.background`).
final Map<String, Color> extensionTokens;
}
/// Canonical surface-token keys as they appear in YAML.
///
/// Keeping them in one place lets the loader, the resolver, and the
/// default map reference the same strings without typos.
abstract class TokenKeys {
// global
static const globalForeground = 'global.foreground';
static const globalBackground = 'global.background';
static const globalBorder = 'global.border';
static const globalFocus = 'global.focus';
static const globalTextMuted = 'global.textMuted';
// panel
static const panelBackground = 'panel.background';
static const panelBorder = 'panel.border';
static const panelActiveBorder = 'panel.activeBorder';
static const panelHeader = 'panel.header';
static const panelHeaderForeground = 'panel.headerForeground';
// sidebar
static const sidebarBackground = 'sidebar.background';
static const sidebarForeground = 'sidebar.foreground';
static const sidebarItemHover = 'sidebar.itemHover';
static const sidebarItemSelected = 'sidebar.itemSelected';
static const sidebarSectionHeader = 'sidebar.sectionHeader';
// statusbar
static const statusBarBackground = 'statusBar.background';
static const statusBarForeground = 'statusBar.foreground';
static const statusBarItemActiveBackground = 'statusBar.itemActiveBackground';
static const statusBarItemHoverBackground = 'statusBar.itemHoverBackground';
// tabs
static const tabBarBackground = 'tabBar.background';
static const tabActive = 'tabBar.tabActive';
static const tabInactive = 'tabBar.tabInactive';
static const tabActiveForeground = 'tabBar.tabActiveForeground';
static const tabInactiveForeground = 'tabBar.tabInactiveForeground';
static const tabActiveBorder = 'tabBar.tabActiveBorder';
static const tabCloseHover = 'tabBar.tabCloseHover';
// buttons
static const buttonBackground = 'button.background';
static const buttonForeground = 'button.foreground';
static const buttonHoverBackground = 'button.hoverBackground';
static const buttonActiveBackground = 'button.activeBackground';
static const buttonBorder = 'button.border';
// list items
static const listItemBackground = 'listItem.background';
static const listItemForeground = 'listItem.foreground';
static const listItemHoverBackground = 'listItem.hoverBackground';
static const listItemSelectedBackground = 'listItem.selectedBackground';
static const listItemSelectedForeground = 'listItem.selectedForeground';
// scrollbar
static const scrollbarSlider = 'scrollbar.slider';
static const scrollbarSliderHover = 'scrollbar.sliderHover';
static const scrollbarTrack = 'scrollbar.track';
// tooltip
static const tooltipBackground = 'tooltip.background';
static const tooltipForeground = 'tooltip.foreground';
static const tooltipBorder = 'tooltip.border';
// dropdown
static const dropdownBackground = 'dropdown.background';
static const dropdownForeground = 'dropdown.foreground';
static const dropdownBorder = 'dropdown.border';
// modal
static const modalOverlayBackground = 'modal.overlayBackground';
static const modalSurfaceBackground = 'modal.surfaceBackground';
static const modalSurfaceBorder = 'modal.surfaceBorder';
// divider
static const dividerColor = 'divider.color';
// status
static const statusSuccess = 'status.success';
static const statusWarning = 'status.warning';
static const statusError = 'status.error';
static const statusInfo = 'status.info';
static const all = <String>[
globalForeground,
globalBackground,
globalBorder,
globalFocus,
globalTextMuted,
panelBackground,
panelBorder,
panelActiveBorder,
panelHeader,
panelHeaderForeground,
sidebarBackground,
sidebarForeground,
sidebarItemHover,
sidebarItemSelected,
sidebarSectionHeader,
statusBarBackground,
statusBarForeground,
statusBarItemActiveBackground,
statusBarItemHoverBackground,
tabBarBackground,
tabActive,
tabInactive,
tabActiveForeground,
tabInactiveForeground,
tabActiveBorder,
tabCloseHover,
buttonBackground,
buttonForeground,
buttonHoverBackground,
buttonActiveBackground,
buttonBorder,
listItemBackground,
listItemForeground,
listItemHoverBackground,
listItemSelectedBackground,
listItemSelectedForeground,
scrollbarSlider,
scrollbarSliderHover,
scrollbarTrack,
tooltipBackground,
tooltipForeground,
tooltipBorder,
dropdownBackground,
dropdownForeground,
dropdownBorder,
modalOverlayBackground,
modalSurfaceBackground,
modalSurfaceBorder,
dividerColor,
statusSuccess,
statusWarning,
statusError,
statusInfo,
];
}