Files
clide/app/lib/kernel/src/theme/loader.dart
T
jpmschweitzerandClaude b13483e2d5 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>
2026-04-21 15:39:41 +02:00

107 lines
3.0 KiB
Dart

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;
}