refactor: rename internal Fai* design system + fai_ helpers to chain
Some checks failed
Security / Security check (push) Failing after 2s

The Studio design system, widgets and helpers carried a Fai* / fai_
prefix (FaiSpace, FaiColors, FaiTheme, FaiLog, 17 fai_*.dart files, the
faiBinary* l10n keys). Studio is the Ch∆In product, so rename them to
Chain* / chain_ — carefully preserving English fail/failure/failed.
Also fix stale references: the 'fai' binary in l10n strings -> 'chain',
FAI_* env vars (FAI_BIN/DATA_DIR/MODULES_DIR/TODAY/BOOTSTRAP_TOKEN) ->
CHAIN_*, fai_platform -> fai_chain, fai_hub -> chain_hub. Vendor
security-hook tooling (FAI_BANNED_TERMS_FILE) + the .fai bundle ext left.
flutter analyze + test: clean (20 passed).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-16 17:53:17 +02:00
parent 68d23ab7dd
commit 891acd2ba2
52 changed files with 1225 additions and 1225 deletions

View file

@ -38,7 +38,7 @@ flutter run -d macos
Will be published as `fai/studio` on Forgejo (`git.flemming.ws`). Will be published as `fai/studio` on Forgejo (`git.flemming.ws`).
The local directory `chain_studio/` follows the established The local directory `chain_studio/` follows the established
`fai_platform/` layout convention. `fai_chain/` layout convention.
## Why "Studio" and not "Stage" ## Why "Studio" and not "Stage"

View file

@ -14,7 +14,7 @@ connected and lets you enrol new ones.
2. The hub issues a single-use **bootstrap token** and bundles its 2. The hub issues a single-use **bootstrap token** and bundles its
**CA certificate**. Copy the shown config into the satellite's **CA certificate**. Copy the shown config into the satellite's
`config.yaml` (one paste) and export the token as `config.yaml` (one paste) and export the token as
`FAI_BOOTSTRAP_TOKEN`. `CHAIN_BOOTSTRAP_TOKEN`.
3. Start the satellite. It dials the primary, the CA pin makes the 3. Start the satellite. It dials the primary, the CA pin makes the
first connect tamper-proof, and the primary signs it a short-lived first connect tamper-proof, and the primary signs it a short-lived
certificate. It then appears in this list. certificate. It then appears in this list.

View file

@ -16,7 +16,7 @@ verbundenen Satelliten und richtet neue ein.
2. Der Hub stellt ein einmaliges **Bootstrap-Token** aus und legt sein 2. Der Hub stellt ein einmaliges **Bootstrap-Token** aus und legt sein
**CA-Zertifikat** bei. Kopiere die gezeigte Konfiguration in die **CA-Zertifikat** bei. Kopiere die gezeigte Konfiguration in die
`config.yaml` des Satelliten (ein Einfügen) und exportiere das `config.yaml` des Satelliten (ein Einfügen) und exportiere das
Token als `FAI_BOOTSTRAP_TOKEN`. Token als `CHAIN_BOOTSTRAP_TOKEN`.
3. Starte den Satelliten. Er wählt sich beim Primary ein, der CA-Pin 3. Starte den Satelliten. Er wählt sich beim Primary ein, der CA-Pin
macht den ersten Connect manipulationssicher, und der Primary macht den ersten Connect manipulationssicher, und der Primary
signiert ihm ein kurzlebiges Zertifikat. Danach erscheint er in signiert ihm ein kurzlebiges Zertifikat. Danach erscheint er in

View file

@ -102,7 +102,7 @@ Instead a `_DocReaderSheet` (modal bottom-sheet, same shape as
existing `flutter_markdown` package — same one the Store uses existing `flutter_markdown` package — same one the Store uses
for module READMEs. The doc source is bundled with Studio as for module READMEs. The doc source is bundled with Studio as
`assets/docs/*.md`, kept in sync via a build step that copies `assets/docs/*.md`, kept in sync via a build step that copies
the relevant files from `fai_platform/docs/`. the relevant files from `fai_chain/docs/`.
Pros: Pros:
- Air-gap-tauglich (no network call to read docs) - Air-gap-tauglich (no network call to read docs)
@ -158,7 +158,7 @@ phased PRs:
content with hard-coded strings (with i18n). Lands content with hard-coded strings (with i18n). Lands
without the embedded-doc reader. without the embedded-doc reader.
2. **Phase B** — bundled markdown docs + `_DocReaderSheet`. 2. **Phase B** — bundled markdown docs + `_DocReaderSheet`.
Pick 4-5 source docs from `fai_platform/docs/architecture/` Pick 4-5 source docs from `fai_chain/docs/architecture/`
that map to operator concerns (security, host-services, that map to operator concerns (security, host-services,
flow-experience, compliance-gaps). Add a build script that flow-experience, compliance-gaps). Add a build script that
syncs them into `assets/docs/`. syncs them into `assets/docs/`.

View file

@ -26,10 +26,10 @@ import 'dart:io';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
class FaiLog { class ChainLog {
FaiLog._(); ChainLog._();
static final FaiLog instance = FaiLog._(); static final ChainLog instance = ChainLog._();
// Serialise concurrent writes Future-based mutex so a burst // Serialise concurrent writes Future-based mutex so a burst
// of errors (rare but happens during failed install retries) // of errors (rare but happens during failed install retries)

View file

@ -16,9 +16,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/fai_error_box.dart'; import '../widgets/chain_error_box.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
import 'fai_log.dart'; import 'chain_log.dart';
/// Show [error] as a SnackBar with a copy affordance. /// Show [error] as a SnackBar with a copy affordance.
/// ///
@ -41,7 +41,7 @@ void showFaiErrorSnack(
}) { }) {
// Fire-and-forget log write. If the disk's full or the dir // Fire-and-forget log write. If the disk's full or the dir
// is read-only the UI still surfaces the error. // is read-only the UI still surfaces the error.
FaiLog.instance.error(source, error, context: title); ChainLog.instance.error(source, error, context: title);
final messenger = ScaffoldMessenger.maybeOf(context); final messenger = ScaffoldMessenger.maybeOf(context);
if (messenger == null) { if (messenger == null) {
// No Scaffold above the caller is likely showing the // No Scaffold above the caller is likely showing the
@ -60,8 +60,8 @@ void showFaiErrorSnack(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
content: Padding( content: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.sm, horizontal: ChainSpace.sm,
vertical: FaiSpace.xs, vertical: ChainSpace.xs,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -75,9 +75,9 @@ void showFaiErrorSnack(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
], ],
FaiErrorBox(error: error, isError: true), ChainErrorBox(error: error, isError: true),
], ],
), ),
), ),
@ -94,14 +94,14 @@ Future<void> showFaiErrorDialog(
Object error, { Object error, {
String? title, String? title,
}) async { }) async {
FaiLog.instance.error(source, error); ChainLog.instance.error(source, error);
return showDialog<void>( return showDialog<void>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: title == null ? null : Text(title), title: title == null ? null : Text(title),
content: SizedBox( content: SizedBox(
width: 480, width: 480,
child: FaiErrorBox(error: error, isError: true, maxHeight: 240), child: ChainErrorBox(error: error, isError: true, maxHeight: 240),
), ),
actions: [ actions: [
TextButton( TextButton(

View file

@ -161,7 +161,7 @@ class StudioFlowRunDriver implements editor.FlowRunDriver {
/// (text / json / bytes only). File outputs degrade to /// (text / json / bytes only). File outputs degrade to
/// JSON containing the URI string the editor's run-tab /// JSON containing the URI string the editor's run-tab
/// renderer is read-only so the operator can still see /// renderer is read-only so the operator can still see
/// where the file went; Studio's richer FaiFlowOutput /// where the file went; Studio's richer ChainFlowOutput
/// widget keeps file-open affordances available elsewhere. /// widget keeps file-open affordances available elsewhere.
editor.FlowOutputValue _convertOutput(FlowOutput out) { editor.FlowOutputValue _convertOutput(FlowOutput out) {
return switch (out) { return switch (out) {

View file

@ -1,4 +1,4 @@
// HubService singleton wrapper around fai_dart_sdk's HubClient. // HubService singleton wrapper around chain_dart_sdk's HubClient.
// //
// Centralises the gRPC connection so each page just imports // Centralises the gRPC connection so each page just imports
// `HubService.instance` instead of constructing its own client. // `HubService.instance` instead of constructing its own client.
@ -1282,7 +1282,7 @@ class SatelliteEnrollment {
return 'federation:\n' return 'federation:\n'
' upstream: https://<this-primary-host>:<federation-port>\n' ' upstream: https://<this-primary-host>:<federation-port>\n'
' hub_name: $name\n' ' hub_name: $name\n'
' bootstrap_token_env: FAI_BOOTSTRAP_TOKEN$ca'; ' bootstrap_token_env: CHAIN_BOOTSTRAP_TOKEN$ca';
} }
} }
@ -1553,7 +1553,7 @@ class HardwareSnapshot {
} }
/// One curated model with editorial metadata. Wire-faithful /// One curated model with editorial metadata. Wire-faithful
/// with `crates/fai_hub/system-ai/models.yaml`. /// with `crates/chain_hub/system-ai/models.yaml`.
class CuratedModelInfo { class CuratedModelInfo {
final String id; final String id;
final String family; final String family;

View file

@ -4,7 +4,7 @@ import 'package:path/path.dart' as p;
/// Operator-managed registry auth token, kept at /// Operator-managed registry auth token, kept at
/// `~/.chain/registry-token` (mode 0600 on Unix). The hub reads /// `~/.chain/registry-token` (mode 0600 on Unix). The hub reads
/// this file at install time when `CHAIN_REGISTRY_TOKEN` is unset /// this file at install time when `CHAIN_REGISTRY_TOKEN` is unset
/// see `download_to_temp` in `crates/fai_hub/src/lib.rs`. /// see `download_to_temp` in `crates/chain_hub/src/lib.rs`.
/// ///
/// Studio writes the file directly so a fresh install never /// Studio writes the file directly so a fresh install never
/// requires the operator to fiddle with shell env vars. /// requires the operator to fiddle with shell env vars.

View file

@ -22,7 +22,7 @@ import 'package:shared_preferences/shared_preferences.dart';
/// Sentinel returned in `_runFai(...).stderr` when no `fai` /// Sentinel returned in `_runFai(...).stderr` when no `fai`
/// binary could be located. UI layers should not display this /// binary could be located. UI layers should not display this
/// verbatim they check [SystemActions.faiBinaryExists] first /// verbatim they check [SystemActions.chainBinaryExists] first
/// and render a localized, actionable message instead. /// and render a localized, actionable message instead.
const String kFaiBinaryNotFound = 'fai-binary-not-found'; const String kFaiBinaryNotFound = 'fai-binary-not-found';
@ -37,7 +37,7 @@ class SystemActions {
/// SharedPreferences key for an operator-chosen `fai` binary /// SharedPreferences key for an operator-chosen `fai` binary
/// path (set via the file picker when auto-detection fails). /// path (set via the file picker when auto-detection fails).
static const String _kFaiBinaryPrefKey = 'system.fai_binary_path'; static const String _kFaiBinaryPrefKey = 'system.chain_binary_path';
/// In-memory cache of the operator override, loaded once at /// In-memory cache of the operator override, loaded once at
/// startup by [loadFaiBinaryOverride]. Null = no override. /// startup by [loadFaiBinaryOverride]. Null = no override.
@ -71,7 +71,7 @@ class SystemActions {
/// the canonical install dir). UI uses this to decide between /// the canonical install dir). UI uses this to decide between
/// the normal daemon-control affordances and the "locate the /// the normal daemon-control affordances and the "locate the
/// binary / read the install guide" recovery path. /// binary / read the install guide" recovery path.
static bool faiBinaryExists() => _faiExecutable() != null; static bool chainBinaryExists() => _faiExecutable() != null;
/// Ask the OS to open [path] in the default handler. On macOS /// Ask the OS to open [path] in the default handler. On macOS
/// this opens text files in TextEdit, configs in the registered /// this opens text files in TextEdit, configs in the registered
@ -137,7 +137,7 @@ class SystemActions {
/// Run a `chain daemon ...` subcommand and surface the captured /// Run a `chain daemon ...` subcommand and surface the captured
/// output. Used by Doctor's "Restart daemon" button. /// output. Used by Doctor's "Restart daemon" button.
static Future<({bool ok, String stdout, String stderr})> faiDaemon( static Future<({bool ok, String stdout, String stderr})> chainDaemon(
List<String> args, List<String> args,
) async { ) async {
return _runFai(['daemon', ...args]); return _runFai(['daemon', ...args]);
@ -145,7 +145,7 @@ class SystemActions {
/// Run `chain update apply --channel <c>`. Long-running on a slow /// Run `chain update apply --channel <c>`. Long-running on a slow
/// network caller should show a spinner. /// network caller should show a spinner.
static Future<({bool ok, String stdout, String stderr})> faiUpdateApply( static Future<({bool ok, String stdout, String stderr})> chainUpdateApply(
String channel, String channel,
) async { ) async {
return _runFai(['update', 'apply', '--channel', channel]); return _runFai(['update', 'apply', '--channel', channel]);
@ -154,7 +154,7 @@ class SystemActions {
/// Switch the active channel pointer at `~/.chain/current-channel`. /// Switch the active channel pointer at `~/.chain/current-channel`.
/// The CLI also restarts the daemon for the new channel, so the /// The CLI also restarts the daemon for the new channel, so the
/// caller does not need a follow-up restart. /// caller does not need a follow-up restart.
static Future<({bool ok, String stdout, String stderr})> faiChannelSwitch( static Future<({bool ok, String stdout, String stderr})> chainChannelSwitch(
String channel, String channel,
) async { ) async {
return _runFai(['channel', 'switch', channel]); return _runFai(['channel', 'switch', channel]);
@ -164,14 +164,14 @@ class SystemActions {
/// (launchd plist on macOS, systemd-user unit on Linux). On /// (launchd plist on macOS, systemd-user unit on Linux). On
/// Windows the platform CLI returns a "not supported" exit /// Windows the platform CLI returns a "not supported" exit
/// status that the caller surfaces as an error message. /// status that the caller surfaces as an error message.
static Future<({bool ok, String stdout, String stderr})> faiDaemonEnable( static Future<({bool ok, String stdout, String stderr})> chainDaemonEnable(
String channel, String channel,
) async { ) async {
return _runFai(['daemon', 'enable', '--channel', channel]); return _runFai(['daemon', 'enable', '--channel', channel]);
} }
/// Remove the autostart unit installed by [faiDaemonEnable]. /// Remove the autostart unit installed by [chainDaemonEnable].
static Future<({bool ok, String stdout, String stderr})> faiDaemonDisable( static Future<({bool ok, String stdout, String stderr})> chainDaemonDisable(
String channel, String channel,
) async { ) async {
return _runFai(['daemon', 'disable', '--channel', channel]); return _runFai(['daemon', 'disable', '--channel', channel]);
@ -186,7 +186,7 @@ class SystemActions {
/// ///
/// Studio's gRPC channel goes down for ~1s while the daemon /// Studio's gRPC channel goes down for ~1s while the daemon
/// restarts caller should follow up with a reconnect. /// restarts caller should follow up with a reconnect.
static Future<({bool ok, String stdout, String stderr})> faiReset({ static Future<({bool ok, String stdout, String stderr})> chainReset({
bool keepModules = false, bool keepModules = false,
bool keepData = false, bool keepData = false,
}) async { }) async {
@ -202,9 +202,9 @@ class SystemActions {
final exe = _faiExecutable(); final exe = _faiExecutable();
if (exe == null) { if (exe == null) {
// Sentinel, not a user-facing string. Callers detect this // Sentinel, not a user-facing string. Callers detect this
// via `faiBinaryExists()` and render a localized, // via `chainBinaryExists()` and render a localized,
// actionable recovery message (locate binary / install // actionable recovery message (locate binary / install
// guide) instead of CLI jargon like "set FAI_BIN". // guide) instead of CLI jargon like "set CHAIN_BIN".
return (ok: false, stdout: '', stderr: kFaiBinaryNotFound); return (ok: false, stdout: '', stderr: kFaiBinaryNotFound);
} }
try { try {
@ -226,7 +226,7 @@ class SystemActions {
} }
/// Locate the `fai` binary. Order: operator override (set via /// Locate the `fai` binary. Order: operator override (set via
/// the file picker), $FAI_BIN, PATH, fallback to the canonical /// the file picker), $CHAIN_BIN, PATH, fallback to the canonical
/// install location under the user's home dir. /// install location under the user's home dir.
static String? _faiExecutable() { static String? _faiExecutable() {
final override = _faiBinaryOverride; final override = _faiBinaryOverride;
@ -236,7 +236,7 @@ class SystemActions {
return override; return override;
} }
final fromEnv = Platform.environment['FAI_BIN']; final fromEnv = Platform.environment['CHAIN_BIN'];
if (fromEnv != null && fromEnv.isNotEmpty && File(fromEnv).existsSync()) { if (fromEnv != null && fromEnv.isNotEmpty && File(fromEnv).existsSync()) {
return fromEnv; return fromEnv;
} }

View file

@ -16,7 +16,7 @@ import 'hub.dart';
/// SharedPreferences key for the operator's preferred theme /// SharedPreferences key for the operator's preferred theme
/// plugin. `null` (or absent) means "use Studio's built-in /// plugin. `null` (or absent) means "use Studio's built-in
/// default" (FaiTheme.light / .dark). /// default" (ChainTheme.light / .dark).
const _kThemePluginKey = 'theme_plugin_capability'; const _kThemePluginKey = 'theme_plugin_capability';
/// One ColorScheme pair as built from a theme-plugin's /// One ColorScheme pair as built from a theme-plugin's
@ -96,14 +96,14 @@ Future<ThemePluginSchemes> loadThemePluginSchemes(String capability) async {
} }
/// Build a `ThemeData` from a plugin-supplied `ColorScheme`. /// Build a `ThemeData` from a plugin-supplied `ColorScheme`.
/// Delegates to [FaiTheme.fromColorScheme] so plugin-themed /// Delegates to [ChainTheme.fromColorScheme] so plugin-themed
/// builds and built-in themes share one structural shape same /// builds and built-in themes share one structural shape same
/// textTheme cascade, same component themes, same `inherit` /// textTheme cascade, same component themes, same `inherit`
/// values on every TextStyle. Without that, Flutter's /// values on every TextStyle. Without that, Flutter's
/// AnimatedDefaultTextStyle lerp blows up when swapping between /// AnimatedDefaultTextStyle lerp blows up when swapping between
/// the two paths. /// the two paths.
ThemeData themeDataFromScheme(ColorScheme scheme) => ThemeData themeDataFromScheme(ColorScheme scheme) =>
FaiTheme.fromColorScheme(scheme); ChainTheme.fromColorScheme(scheme);
/// Translate the plugin's 14 ARGB tokens into a `ColorScheme`. /// Translate the plugin's 14 ARGB tokens into a `ColorScheme`.
/// Order matches the plugin contract: /// Order matches the plugin contract:

View file

@ -1539,13 +1539,13 @@
"detail": {"type": "String"} "detail": {"type": "String"}
} }
}, },
"faiBinaryNotFound": "Das Programm `fai` wurde auf diesem Rechner nicht gefunden.", "chainBinaryNotFound": "Das Programm `chain` wurde auf diesem Rechner nicht gefunden.",
"faiBinaryNotFoundHint": "Installiere die Ch∆In-Plattform oder verweise Studio auf eine vorhandene `fai`-Datei.", "chainBinaryNotFoundHint": "Installiere die Ch∆In-Plattform oder verweise Studio auf eine vorhandene `chain`-Datei.",
"faiBinaryLocateButton": "`fai`-Programm suchen …", "chainBinaryLocateButton": "`chain`-Programm suchen …",
"faiBinaryInstallDocsButton": "Installationsanleitung öffnen", "chainBinaryInstallDocsButton": "Installationsanleitung öffnen",
"faiBinaryPickerTitle": "`fai`-Programm auswählen", "chainBinaryPickerTitle": "`chain`-Programm auswählen",
"faiBinarySetOk": "Verwende `fai` unter {path}", "chainBinarySetOk": "Verwende `chain` unter {path}",
"@faiBinarySetOk": { "@chainBinarySetOk": {
"placeholders": { "placeholders": {
"path": {"type": "String"} "path": {"type": "String"}
} }

View file

@ -1542,13 +1542,13 @@
"detail": {"type": "String"} "detail": {"type": "String"}
} }
}, },
"faiBinaryNotFound": "Could not find the `fai` program on this machine.", "chainBinaryNotFound": "Could not find the `chain` program on this machine.",
"faiBinaryNotFoundHint": "Install the Ch∆In platform, or point Studio at an existing `fai` binary.", "chainBinaryNotFoundHint": "Install the Ch∆In platform, or point Studio at an existing `chain` binary.",
"faiBinaryLocateButton": "Locate `fai` binary…", "chainBinaryLocateButton": "Locate `chain` binary…",
"faiBinaryInstallDocsButton": "Open install guide", "chainBinaryInstallDocsButton": "Open install guide",
"faiBinaryPickerTitle": "Select the `fai` binary", "chainBinaryPickerTitle": "Select the `chain` binary",
"faiBinarySetOk": "Using `fai` binary at {path}", "chainBinarySetOk": "Using `chain` binary at {path}",
"@faiBinarySetOk": { "@chainBinarySetOk": {
"placeholders": { "placeholders": {
"path": {"type": "String"} "path": {"type": "String"}
} }

View file

@ -4328,41 +4328,41 @@ abstract class AppLocalizations {
/// **'Could not start daemon: {detail}'** /// **'Could not start daemon: {detail}'**
String daemonStartFailed(String detail); String daemonStartFailed(String detail);
/// No description provided for @faiBinaryNotFound. /// No description provided for @chainBinaryNotFound.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not find the `fai` program on this machine.'** /// **'Could not find the `chain` program on this machine.'**
String get faiBinaryNotFound; String get chainBinaryNotFound;
/// No description provided for @faiBinaryNotFoundHint. /// No description provided for @chainBinaryNotFoundHint.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Install the Ch∆In platform, or point Studio at an existing `fai` binary.'** /// **'Install the Ch∆In platform, or point Studio at an existing `chain` binary.'**
String get faiBinaryNotFoundHint; String get chainBinaryNotFoundHint;
/// No description provided for @faiBinaryLocateButton. /// No description provided for @chainBinaryLocateButton.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Locate `fai` binary…'** /// **'Locate `chain` binary…'**
String get faiBinaryLocateButton; String get chainBinaryLocateButton;
/// No description provided for @faiBinaryInstallDocsButton. /// No description provided for @chainBinaryInstallDocsButton.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Open install guide'** /// **'Open install guide'**
String get faiBinaryInstallDocsButton; String get chainBinaryInstallDocsButton;
/// No description provided for @faiBinaryPickerTitle. /// No description provided for @chainBinaryPickerTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select the `fai` binary'** /// **'Select the `chain` binary'**
String get faiBinaryPickerTitle; String get chainBinaryPickerTitle;
/// No description provided for @faiBinarySetOk. /// No description provided for @chainBinarySetOk.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Using `fai` binary at {path}'** /// **'Using `chain` binary at {path}'**
String faiBinarySetOk(String path); String chainBinarySetOk(String path);
/// No description provided for @sysAiFixParse. /// No description provided for @sysAiFixParse.
/// ///

View file

@ -2535,25 +2535,25 @@ class AppLocalizationsDe extends AppLocalizations {
} }
@override @override
String get faiBinaryNotFound => String get chainBinaryNotFound =>
'Das Programm `fai` wurde auf diesem Rechner nicht gefunden.'; 'Das Programm `chain` wurde auf diesem Rechner nicht gefunden.';
@override @override
String get faiBinaryNotFoundHint => String get chainBinaryNotFoundHint =>
'Installiere die Ch∆In-Plattform oder verweise Studio auf eine vorhandene `fai`-Datei.'; 'Installiere die Ch∆In-Plattform oder verweise Studio auf eine vorhandene `chain`-Datei.';
@override @override
String get faiBinaryLocateButton => '`fai`-Programm suchen …'; String get chainBinaryLocateButton => '`chain`-Programm suchen …';
@override @override
String get faiBinaryInstallDocsButton => 'Installationsanleitung öffnen'; String get chainBinaryInstallDocsButton => 'Installationsanleitung öffnen';
@override @override
String get faiBinaryPickerTitle => '`fai`-Programm auswählen'; String get chainBinaryPickerTitle => '`chain`-Programm auswählen';
@override @override
String faiBinarySetOk(String path) { String chainBinarySetOk(String path) {
return 'Verwende `fai` unter $path'; return 'Verwende `chain` unter $path';
} }
@override @override

View file

@ -2538,25 +2538,25 @@ class AppLocalizationsEn extends AppLocalizations {
} }
@override @override
String get faiBinaryNotFound => String get chainBinaryNotFound =>
'Could not find the `fai` program on this machine.'; 'Could not find the `chain` program on this machine.';
@override @override
String get faiBinaryNotFoundHint => String get chainBinaryNotFoundHint =>
'Install the Ch∆In platform, or point Studio at an existing `fai` binary.'; 'Install the Ch∆In platform, or point Studio at an existing `chain` binary.';
@override @override
String get faiBinaryLocateButton => 'Locate `fai` binary…'; String get chainBinaryLocateButton => 'Locate `chain` binary…';
@override @override
String get faiBinaryInstallDocsButton => 'Open install guide'; String get chainBinaryInstallDocsButton => 'Open install guide';
@override @override
String get faiBinaryPickerTitle => 'Select the `fai` binary'; String get chainBinaryPickerTitle => 'Select the `chain` binary';
@override @override
String faiBinarySetOk(String path) { String chainBinarySetOk(String path) {
return 'Using `fai` binary at $path'; return 'Using `chain` binary at $path';
} }
@override @override

View file

@ -8,7 +8,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'data/fai_log.dart'; import 'data/chain_log.dart';
import 'data/hub.dart'; import 'data/hub.dart';
import 'data/system_actions.dart'; import 'data/system_actions.dart';
import 'data/theme_plugin.dart'; import 'data/theme_plugin.dart';
@ -22,7 +22,7 @@ import 'pages/store.dart';
import 'pages/welcome.dart'; import 'pages/welcome.dart';
import 'theme/theme.dart'; import 'theme/theme.dart';
import 'theme/tokens.dart'; import 'theme/tokens.dart';
import 'widgets/fai_search_palette.dart'; import 'widgets/chain_search_palette.dart';
import 'widgets/widgets.dart'; import 'widgets/widgets.dart';
/// Studio's own build version. Bump on every UI commit so the /// Studio's own build version. Bump on every UI commit so the
@ -55,7 +55,7 @@ class StudioApp extends StatefulWidget {
/// Capability name of the active theme plugin (e.g. /// Capability name of the active theme plugin (e.g.
/// `studio.theme.solarized`) or `null` for the built-in /// `studio.theme.solarized`) or `null` for the built-in
/// FaiTheme. /// ChainTheme.
final String? initialThemePlugin; final String? initialThemePlugin;
const StudioApp({ const StudioApp({
@ -89,7 +89,7 @@ class StudioAppState extends State<StudioApp> {
/// Capability name of the active `studio.theme.*` plugin /// Capability name of the active `studio.theme.*` plugin
/// (e.g. `studio.theme.solarized`) or `null` for /// (e.g. `studio.theme.solarized`) or `null` for
/// FaiTheme's built-in palette. Flipping this triggers a /// ChainTheme's built-in palette. Flipping this triggers a
/// re-fetch of the plugin's ColorSchemes; the MaterialApp /// re-fetch of the plugin's ColorSchemes; the MaterialApp
/// rebuilds with the new themes. /// rebuilds with the new themes.
late final ValueNotifier<String?> themePluginNotifier; late final ValueNotifier<String?> themePluginNotifier;
@ -188,7 +188,7 @@ class StudioAppState extends State<StudioApp> {
// own "applied" snackbar is replaced upstream by the // own "applied" snackbar is replaced upstream by the
// friendlier failure message via [themePluginFailed]. // friendlier failure message via [themePluginFailed].
// ignore: discarded_futures // ignore: discarded_futures
FaiLog.instance.error( ChainLog.instance.error(
'theme.plugin.load', 'theme.plugin.load',
e, e,
context: 'capability=$capability', context: 'capability=$capability',
@ -217,8 +217,8 @@ class StudioAppState extends State<StudioApp> {
return MaterialApp( return MaterialApp(
title: 'Ch∆In Studio', title: 'Ch∆In Studio',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: p.light ?? FaiTheme.light(), theme: p.light ?? ChainTheme.light(),
darkTheme: p.dark ?? FaiTheme.dark(), darkTheme: p.dark ?? ChainTheme.dark(),
themeMode: _flutterMode(mode), themeMode: _flutterMode(mode),
locale: locale, locale: locale,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
@ -268,7 +268,7 @@ class StudioShellState extends State<StudioShell> {
/// row and the WelcomePage hero CTA. /// row and the WelcomePage hero CTA.
Future<void> startDaemon() async { Future<void> startDaemon() async {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final r = await SystemActions.faiDaemon(['start']); final r = await SystemActions.chainDaemon(['start']);
if (!mounted) return; if (!mounted) return;
final detail = r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim(); final detail = r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -325,7 +325,7 @@ class StudioShellState extends State<StudioShell> {
// (permissions, on-disk path, uninstall) now lives inside // (permissions, on-disk path, uninstall) now lives inside
// the Store's detail sheet, and the Store with the // the Store's detail sheet, and the Store with the
// "Installed" filter covers the listing role. Cmd+K still // "Installed" filter covers the listing role. Cmd+K still
// opens FaiModuleSheet for quick info. // opens ChainModuleSheet for quick info.
// Flows destination IS the editor as of 0.52.0 the // Flows destination IS the editor as of 0.52.0 the
// graph / text / run tabs all live behind one icon. The // graph / text / run tabs all live behind one icon. The
// separate "flow-editor" destination is gone; the Cmd+K // separate "flow-editor" destination is gone; the Cmd+K
@ -413,9 +413,9 @@ class StudioShellState extends State<StudioShell> {
void _openSearchPalette() { void _openSearchPalette() {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final pagesGroup = l.searchGroupPages; final pagesGroup = l.searchGroupPages;
final hits = <FaiSearchHit>[ final hits = <ChainSearchHit>[
for (var i = 0; i < _pages.length; i++) for (var i = 0; i < _pages.length; i++)
FaiSearchHit( ChainSearchHit(
label: _pages[i].labelOf(context), label: _pages[i].labelOf(context),
hint: l.searchPageHint(i + 1), hint: l.searchPageHint(i + 1),
icon: _pages[i].icon, icon: _pages[i].icon,
@ -425,18 +425,18 @@ class StudioShellState extends State<StudioShell> {
setState(() => _selectedIndex = i); setState(() => _selectedIndex = i);
}, },
), ),
FaiSearchHit( ChainSearchHit(
label: l.searchSettingsLabel, label: l.searchSettingsLabel,
hint: l.searchSettingsHint, hint: l.searchSettingsHint,
icon: Icons.settings_outlined, icon: Icons.settings_outlined,
group: pagesGroup, group: pagesGroup,
onSelect: () { onSelect: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
FaiSettingsDialog.show(context); ChainSettingsDialog.show(context);
}, },
), ),
]; ];
FaiSearchPalette.show(context, staticHits: hits); ChainSearchPalette.show(context, staticHits: hits);
} }
@override @override
@ -481,7 +481,7 @@ class StudioShellState extends State<StudioShell> {
), ),
_OpenSettingsIntent: CallbackAction<_OpenSettingsIntent>( _OpenSettingsIntent: CallbackAction<_OpenSettingsIntent>(
onInvoke: (_) { onInvoke: (_) {
FaiSettingsDialog.show(context); ChainSettingsDialog.show(context);
return null; return null;
}, },
), ),
@ -513,13 +513,13 @@ class StudioShellState extends State<StudioShell> {
if (_hubUnreachable) if (_hubUnreachable)
_HubUnreachableBanner( _HubUnreachableBanner(
endpoint: HubService.instance.endpointLabel, endpoint: HubService.instance.endpointLabel,
onOpenSettings: () => FaiSettingsDialog.show(context), onOpenSettings: () => ChainSettingsDialog.show(context),
), ),
Expanded( Expanded(
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: FaiMotion.base, duration: ChainMotion.base,
switchInCurve: FaiMotion.easing, switchInCurve: ChainMotion.easing,
switchOutCurve: FaiMotion.easing, switchOutCurve: ChainMotion.easing,
transitionBuilder: (child, anim) => FadeTransition( transitionBuilder: (child, anim) => FadeTransition(
opacity: anim, opacity: anim,
child: SlideTransition( child: SlideTransition(
@ -569,8 +569,8 @@ class _HubUnreachableBanner extends StatelessWidget {
color: theme.colorScheme.errorContainer, color: theme.colorScheme.errorContainer,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.lg, horizontal: ChainSpace.lg,
vertical: FaiSpace.sm, vertical: ChainSpace.sm,
), ),
child: Row( child: Row(
children: [ children: [
@ -579,7 +579,7 @@ class _HubUnreachableBanner extends StatelessWidget {
size: 18, size: 18,
color: theme.colorScheme.onErrorContainer, color: theme.colorScheme.onErrorContainer,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
l.hubUnreachableBanner(endpoint), l.hubUnreachableBanner(endpoint),
@ -588,7 +588,7 @@ class _HubUnreachableBanner extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
TextButton( TextButton(
onPressed: onOpenSettings, onPressed: onOpenSettings,
child: Text(l.hubUnreachableOpenSettings), child: Text(l.hubUnreachableOpenSettings),
@ -674,8 +674,8 @@ class _SidebarState extends State<_Sidebar>
// reveal is a deliberate, eased gesture, not a flicker. // reveal is a deliberate, eased gesture, not a flicker.
// The collapse runs faster (reverseDuration) so dismissing // The collapse runs faster (reverseDuration) so dismissing
// the rail feels crisp rather than sluggish. // the rail feels crisp rather than sluggish.
duration: FaiMotion.slow, duration: ChainMotion.slow,
reverseDuration: FaiMotion.base, reverseDuration: ChainMotion.base,
value: 0, value: 0,
); );
} }
@ -688,7 +688,7 @@ class _SidebarState extends State<_Sidebar>
Future<void> _startDaemon(BuildContext context) async { Future<void> _startDaemon(BuildContext context) async {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final r = await SystemActions.faiDaemon(['start']); final r = await SystemActions.chainDaemon(['start']);
if (!context.mounted) return; if (!context.mounted) return;
final detail = r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim(); final detail = r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -704,8 +704,8 @@ class _SidebarState extends State<_Sidebar>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mode = widget.connected == true final mode = widget.connected == true
? FaiDeltaMode.live ? ChainDeltaMode.live
: FaiDeltaMode.idle; : ChainDeltaMode.idle;
final hasChannel = final hasChannel =
widget.activeChannel != null && widget.activeChannel!.isNotEmpty; widget.activeChannel != null && widget.activeChannel!.isNotEmpty;
return MouseRegion( return MouseRegion(
@ -721,7 +721,7 @@ class _SidebarState extends State<_Sidebar>
return Container( return Container(
width: width, width: width,
color: theme.colorScheme.surfaceContainerLow, color: theme.colorScheme.surfaceContainerLow,
padding: const EdgeInsets.symmetric(vertical: FaiSpace.xl), padding: const EdgeInsets.symmetric(vertical: ChainSpace.xl),
child: ClipRect( child: ClipRect(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -730,7 +730,7 @@ class _SidebarState extends State<_Sidebar>
SizedBox( SizedBox(
height: _brandRowH, height: _brandRowH,
child: _sidebarRow( child: _sidebarRow(
leading: FaiDeltaMark( leading: ChainDeltaMark(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
mode: mode, mode: mode,
), ),
@ -796,7 +796,7 @@ class _SidebarState extends State<_Sidebar>
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Expanded( Expanded(
child: ListView( child: ListView(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -868,7 +868,7 @@ class _SidebarState extends State<_Sidebar>
child: Opacity( child: Opacity(
opacity: t, opacity: t,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: FaiSpace.md), padding: const EdgeInsets.only(right: ChainSpace.md),
child: label, child: label,
), ),
), ),
@ -889,7 +889,7 @@ class _SidebarState extends State<_Sidebar>
} }
Future<void> _openSettings(BuildContext context) async { Future<void> _openSettings(BuildContext context) async {
final saved = await FaiSettingsDialog.show(context); final saved = await ChainSettingsDialog.show(context);
if (saved && context.mounted) { if (saved && context.mounted) {
// Force a sidebar rebuild so the new endpoint shows in // Force a sidebar rebuild so the new endpoint shows in
// the connection pill. // the connection pill.
@ -911,7 +911,7 @@ class _ChannelPill extends StatelessWidget {
case 'production': case 'production':
return cs.error; return cs.error;
case 'beta': case 'beta':
return FaiColors.warning; return ChainColors.warning;
case 'dev': case 'dev':
return cs.primary; return cs.primary;
case 'local': case 'local':
@ -943,17 +943,17 @@ class _ChannelPill extends StatelessWidget {
message: l.sidebarChannelTooltip, message: l.sidebarChannelTooltip,
child: Material( child: Material(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
onTap: () => FaiSettingsDialog.show(context), onTap: () => ChainSettingsDialog.show(context),
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.sm, horizontal: ChainSpace.sm,
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: accent.withValues(alpha: 0.4)), border: Border.all(color: accent.withValues(alpha: 0.4)),
), ),
child: Row( child: Row(
@ -963,7 +963,7 @@ class _ChannelPill extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
channel, channel,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
weight: FontWeight.w600, weight: FontWeight.w600,
color: accent, color: accent,
@ -980,7 +980,7 @@ class _ChannelPill extends StatelessWidget {
/// Mini-version of [_ConnectionLabel] used when the sidebar is /// Mini-version of [_ConnectionLabel] used when the sidebar is
/// collapsed. A 10px dot in the same green/red/amber tonality /// collapsed. A 10px dot in the same green/red/amber tonality
/// the FaiDeltaMark uses for live / idle / unknown. /// the ChainDeltaMark uses for live / idle / unknown.
class _CollapsedConnectionDot extends StatelessWidget { class _CollapsedConnectionDot extends StatelessWidget {
final bool? connected; final bool? connected;
const _CollapsedConnectionDot({required this.connected}); const _CollapsedConnectionDot({required this.connected});
@ -989,10 +989,10 @@ class _CollapsedConnectionDot extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final color = connected == true final color = connected == true
? FaiColors.success ? ChainColors.success
: connected == false : connected == false
? theme.colorScheme.error ? theme.colorScheme.error
: FaiColors.warning; : ChainColors.warning;
return Container( return Container(
width: 10, width: 10,
height: 10, height: 10,
@ -1025,7 +1025,7 @@ class _CollapsedChannelChip extends StatelessWidget {
case 'production': case 'production':
return cs.error; return cs.error;
case 'beta': case 'beta':
return FaiColors.warning; return ChainColors.warning;
case 'dev': case 'dev':
return cs.primary; return cs.primary;
case 'local': case 'local':
@ -1053,7 +1053,7 @@ class _CollapsedChannelChip extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
_letter, _letter,
style: FaiTheme.mono(size: 10, weight: FontWeight.w700, color: accent), style: ChainTheme.mono(size: 10, weight: FontWeight.w700, color: accent),
), ),
); );
} }
@ -1080,7 +1080,7 @@ class _BrandLabel extends StatelessWidget {
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'v$kStudioVersion', 'v$kStudioVersion',
style: FaiTheme.mono(size: 10, color: theme.colorScheme.primary), style: ChainTheme.mono(size: 10, color: theme.colorScheme.primary),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -1133,7 +1133,7 @@ class _ConnectionLabel extends StatelessWidget {
endpoint, endpoint,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -1251,7 +1251,7 @@ class _SidebarItemState extends State<_SidebarItem> {
child: Opacity( child: Opacity(
opacity: widget.t, opacity: widget.t,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: FaiSpace.md), padding: const EdgeInsets.only(right: ChainSpace.md),
child: Text( child: Text(
label, label,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
@ -1278,19 +1278,19 @@ class _SidebarItemState extends State<_SidebarItem> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: widget.onTap, onTap: widget.onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: FaiMotion.fast, duration: ChainMotion.fast,
// No horizontal padding the icon column anchors // No horizontal padding the icon column anchors
// the left. The bg highlight spans the rail's // the left. The bg highlight spans the rail's
// current width (typical nav-rail behaviour). // current width (typical nav-rail behaviour).
padding: const EdgeInsets.symmetric(vertical: FaiSpace.md), padding: const EdgeInsets.symmetric(vertical: ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
// Lift only the selected item a whisper of depth // Lift only the selected item a whisper of depth
// marks "you are here" without competing with the // marks "you are here" without competing with the
// accent fill. // accent fill.
boxShadow: widget.selected boxShadow: widget.selected
? FaiElevation.low(theme.brightness) ? ChainElevation.low(theme.brightness)
: null, : null,
), ),
child: content, child: content,
@ -1348,7 +1348,7 @@ class _Footer extends StatelessWidget {
child: Opacity( child: Opacity(
opacity: t, opacity: t,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: FaiSpace.sm), padding: const EdgeInsets.only(right: ChainSpace.sm),
child: Row( child: Row(
children: [ children: [
_ThemeToggle(), _ThemeToggle(),
@ -1515,7 +1515,7 @@ class _SidebarClockState extends State<_SidebarClock> {
message: tooltip, message: tooltip,
child: Text( child: Text(
'$hh:$mm:$ss', '$hh:$mm:$ss',
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.85), color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.85),
), ),
@ -1553,7 +1553,7 @@ class _LanguageToggle extends StatelessWidget {
), ),
child: Text( child: Text(
isDe ? 'DE' : 'EN', isDe ? 'DE' : 'EN',
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
weight: FontWeight.w600, weight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,

View file

@ -224,7 +224,7 @@ class _ApprovalsPageState extends State<ApprovalsPage>
tooltip: AppLocalizations.of(context)!.approvalsReloadTooltip, tooltip: AppLocalizations.of(context)!.approvalsReloadTooltip,
onPressed: _refresh, onPressed: _refresh,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
], ],
), ),
body: TabBarView( body: TabBarView(
@ -288,7 +288,7 @@ class _PendingList extends StatelessWidget {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.cloud_off_outlined, icon: Icons.cloud_off_outlined,
iconColor: theme.colorScheme.error, iconColor: theme.colorScheme.error,
title: l.hubUnreachable, title: l.hubUnreachable,
@ -301,7 +301,7 @@ class _PendingList extends StatelessWidget {
} }
final pending = snapshot.data ?? []; final pending = snapshot.data ?? [];
if (pending.isEmpty) { if (pending.isEmpty) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.task_alt_outlined, icon: Icons.task_alt_outlined,
title: l.approvalsInboxZero, title: l.approvalsInboxZero,
hint: l.approvalsInboxHint, hint: l.approvalsInboxHint,
@ -314,15 +314,15 @@ class _PendingList extends StatelessWidget {
children: [ children: [
ListView.separated( ListView.separated(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
FaiSpace.xl, ChainSpace.xl,
FaiSpace.xl, ChainSpace.xl,
FaiSpace.xl, ChainSpace.xl,
// Keep the last card clear of the floating // Keep the last card clear of the floating
// batch action bar. // batch action bar.
selected.isEmpty ? FaiSpace.xl : 96.0, selected.isEmpty ? ChainSpace.xl : 96.0,
), ),
itemCount: pending.length, itemCount: pending.length,
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.md), separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.md),
itemBuilder: (context, i) { itemBuilder: (context, i) {
final a = pending[i]; final a = pending[i];
final isSelected = selectedIds.contains(a.id); final isSelected = selectedIds.contains(a.id);
@ -337,9 +337,9 @@ class _PendingList extends StatelessWidget {
), ),
if (selected.isNotEmpty) if (selected.isNotEmpty)
Positioned( Positioned(
left: FaiSpace.xl, left: ChainSpace.xl,
right: FaiSpace.xl, right: ChainSpace.xl,
bottom: FaiSpace.lg, bottom: ChainSpace.lg,
child: _BatchActionBar( child: _BatchActionBar(
selectedCount: selected.length, selectedCount: selected.length,
totalCount: pending.length, totalCount: pending.length,
@ -387,15 +387,15 @@ class _BatchActionBar extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Material( return Material(
elevation: 4, elevation: 4,
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.lg, horizontal: ChainSpace.lg,
vertical: FaiSpace.md, vertical: ChainSpace.md,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Row( child: Row(
@ -405,14 +405,14 @@ class _BatchActionBar extends StatelessWidget {
size: 18, size: 18,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
l.approvalsBatchSelected(selectedCount), l.approvalsBatchSelected(selectedCount),
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
if (selectedCount < totalCount) if (selectedCount < totalCount)
TextButton( TextButton(
onPressed: inFlight ? null : onSelectAll, onPressed: inFlight ? null : onSelectAll,
@ -434,7 +434,7 @@ class _BatchActionBar extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FilledButton.icon( FilledButton.icon(
onPressed: inFlight ? null : onApprove, onPressed: inFlight ? null : onApprove,
icon: inFlight icon: inFlight
@ -470,7 +470,7 @@ class _HistoryList extends StatelessWidget {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.cloud_off_outlined, icon: Icons.cloud_off_outlined,
iconColor: theme.colorScheme.error, iconColor: theme.colorScheme.error,
title: l.hubUnreachable, title: l.hubUnreachable,
@ -483,16 +483,16 @@ class _HistoryList extends StatelessWidget {
} }
final decided = snapshot.data ?? []; final decided = snapshot.data ?? [];
if (decided.isEmpty) { if (decided.isEmpty) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.history, icon: Icons.history,
title: l.approvalsHistoryEmpty, title: l.approvalsHistoryEmpty,
hint: l.approvalsHistoryEmptyHint, hint: l.approvalsHistoryEmptyHint,
); );
} }
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
itemCount: decided.length, itemCount: decided.length,
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.xs), separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.xs),
itemBuilder: (context, i) { itemBuilder: (context, i) {
final a = decided[i]; final a = decided[i];
return _HistoryRow(record: a); return _HistoryRow(record: a);
@ -522,7 +522,7 @@ class _ApprovalCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return FaiCard( return ChainCard(
accentTop: true, accentTop: true,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -534,13 +534,13 @@ class _ApprovalCard extends StatelessWidget {
onChanged: (_) => onToggleSelected(), onChanged: (_) => onToggleSelected(),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
), ),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
FaiPill( ChainPill(
label: l.approvalsPillPending, label: l.approvalsPillPending,
tone: FaiPillTone.warning, tone: ChainPillTone.warning,
icon: Icons.pending_outlined, icon: Icons.pending_outlined,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
'${approval.flowName} ${approval.stepId}', '${approval.flowName} ${approval.stepId}',
@ -550,17 +550,17 @@ class _ApprovalCard extends StatelessWidget {
), ),
), ),
if (approval.expiresAt != null) if (approval.expiresAt != null)
FaiPill( ChainPill(
label: _expiresInLabel(context, approval.expiresAt!), label: _expiresInLabel(context, approval.expiresAt!),
tone: FaiPillTone.neutral, tone: ChainPillTone.neutral,
icon: Icons.schedule, icon: Icons.schedule,
), ),
], ],
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
Text(approval.prompt, style: theme.textTheme.bodyLarge), Text(approval.prompt, style: theme.textTheme.bodyLarge),
if (approval.payloadPreview != null) ...[ if (approval.payloadPreview != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 4), padding: const EdgeInsets.only(bottom: 4),
child: Text( child: Text(
@ -574,27 +574,27 @@ class _ApprovalCard extends StatelessWidget {
), ),
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText( child: SelectableText(
_prettyPreview(approval.payloadPreview!), _prettyPreview(approval.payloadPreview!),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
), ),
), ),
], ],
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
Row( Row(
children: [ children: [
Text( Text(
approval.id, approval.id,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -611,7 +611,7 @@ class _ApprovalCard extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FilledButton.icon( FilledButton.icon(
onPressed: onApprove, onPressed: onApprove,
icon: const Icon(Icons.check, size: 16), icon: const Icon(Icons.check, size: 16),
@ -663,21 +663,21 @@ class _HistoryRow extends StatelessWidget {
context: context, context: context,
builder: (_) => _HistoryDialog(record: record), builder: (_) => _HistoryDialog(record: record),
), ),
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.sm, horizontal: ChainSpace.sm,
vertical: FaiSpace.sm, vertical: ChainSpace.sm,
), ),
child: Row( child: Row(
children: [ children: [
FaiPill(label: label, tone: tone, icon: icon), ChainPill(label: label, tone: tone, icon: icon),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
SizedBox( SizedBox(
width: 150, width: 150,
child: Text( child: Text(
timeStr, timeStr,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -693,7 +693,7 @@ class _HistoryRow extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
if (record.decidedBy.isNotEmpty) if (record.decidedBy.isNotEmpty)
Text( Text(
record.decidedBy, record.decidedBy,
@ -707,24 +707,24 @@ class _HistoryRow extends StatelessWidget {
); );
} }
static (FaiPillTone, IconData, String) _statusPresentation( static (ChainPillTone, IconData, String) _statusPresentation(
BuildContext context, BuildContext context,
String status, String status,
) { ) {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
switch (status) { switch (status) {
case 'approved': case 'approved':
return (FaiPillTone.success, Icons.check, l.approvalsPillApproved); return (ChainPillTone.success, Icons.check, l.approvalsPillApproved);
case 'rejected': case 'rejected':
return (FaiPillTone.danger, Icons.close, l.approvalsPillRejected); return (ChainPillTone.danger, Icons.close, l.approvalsPillRejected);
case 'expired': case 'expired':
return ( return (
FaiPillTone.neutral, ChainPillTone.neutral,
Icons.timer_off_outlined, Icons.timer_off_outlined,
l.approvalsPillExpired, l.approvalsPillExpired,
); );
default: default:
return (FaiPillTone.neutral, Icons.help_outline, status); return (ChainPillTone.neutral, Icons.help_outline, status);
} }
} }
} }
@ -743,8 +743,8 @@ class _HistoryDialog extends StatelessWidget {
return AlertDialog( return AlertDialog(
title: Row( title: Row(
children: [ children: [
FaiPill(label: label, tone: tone, icon: icon), ChainPill(label: label, tone: tone, icon: icon),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
'${record.flowName} ${record.stepId}', '${record.flowName} ${record.stepId}',
@ -779,7 +779,7 @@ class _HistoryDialog extends StatelessWidget {
AppLocalizations.of(context)!.approvalsDialogReason, AppLocalizations.of(context)!.approvalsDialogReason,
record.reason, record.reason,
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
AppLocalizations.of(context)!.approvalsDialogPrompt, AppLocalizations.of(context)!.approvalsDialogPrompt,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -790,7 +790,7 @@ class _HistoryDialog extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
SelectableText(record.prompt, style: theme.textTheme.bodyMedium), SelectableText(record.prompt, style: theme.textTheme.bodyMedium),
if (record.payloadPreview != null) ...[ if (record.payloadPreview != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
AppLocalizations.of(context)!.approvalsPayloadPreview, AppLocalizations.of(context)!.approvalsPayloadPreview,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -801,22 +801,22 @@ class _HistoryDialog extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.sm), padding: const EdgeInsets.all(ChainSpace.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText( child: SelectableText(
record.payloadPreview!, record.payloadPreview!,
style: FaiTheme.mono(size: 11), style: ChainTheme.mono(size: 11),
), ),
), ),
], ],
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
record.id, record.id,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),

View file

@ -97,7 +97,7 @@ class _AuditPageState extends State<AuditPage> {
title: Text(AppLocalizations.of(context)!.auditTitle), title: Text(AppLocalizations.of(context)!.auditTitle),
actions: [ actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: FaiSpace.lg), padding: const EdgeInsets.only(right: ChainSpace.lg),
child: _FilterChips( child: _FilterChips(
value: _typeFilter, value: _typeFilter,
onChanged: (v) => setState(() => _typeFilter = v), onChanged: (v) => setState(() => _typeFilter = v),
@ -113,7 +113,7 @@ class _AuditPageState extends State<AuditPage> {
tooltip: AppLocalizations.of(context)!.auditClearLogTooltip, tooltip: AppLocalizations.of(context)!.auditClearLogTooltip,
onPressed: _onClearPressed, onPressed: _onClearPressed,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
], ],
), ),
body: Column( body: Column(
@ -123,14 +123,14 @@ class _AuditPageState extends State<AuditPage> {
child: !_initialLoaded child: !_initialLoaded
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _error != null && _events.isEmpty : _error != null && _events.isEmpty
? FaiEmptyState( ? ChainEmptyState(
icon: Icons.cloud_off_outlined, icon: Icons.cloud_off_outlined,
iconColor: theme.colorScheme.error, iconColor: theme.colorScheme.error,
title: AppLocalizations.of(context)!.hubUnreachable, title: AppLocalizations.of(context)!.hubUnreachable,
hint: AppLocalizations.of(context)!.hubUnreachableHint, hint: AppLocalizations.of(context)!.hubUnreachableHint,
) )
: filtered.isEmpty : filtered.isEmpty
? FaiEmptyState( ? ChainEmptyState(
icon: Icons.timeline_outlined, icon: Icons.timeline_outlined,
title: AppLocalizations.of(context)!.auditNoEvents, title: AppLocalizations.of(context)!.auditNoEvents,
hint: AppLocalizations.of(context)!.auditNoEventsHint, hint: AppLocalizations.of(context)!.auditNoEventsHint,
@ -187,7 +187,7 @@ class _AuditPageState extends State<AuditPage> {
Color _toneFor(String type, ThemeData theme) { Color _toneFor(String type, ThemeData theme) {
if (type.endsWith('.failed')) return theme.colorScheme.error; if (type.endsWith('.failed')) return theme.colorScheme.error;
if (type.endsWith('.completed')) return theme.colorScheme.primary; if (type.endsWith('.completed')) return theme.colorScheme.primary;
if (type.endsWith('.started')) return FaiColors.warning; if (type.endsWith('.started')) return ChainColors.warning;
return theme.colorScheme.outline; return theme.colorScheme.outline;
} }
} }
@ -220,15 +220,15 @@ class _GroupedEventList extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final items = _itemsWithHeaders(events, l); final items = _itemsWithHeaders(events, l);
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final item = items[i]; final item = items[i];
if (item is _GroupHeader) { if (item is _GroupHeader) {
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: i == 0 ? 0 : FaiSpace.lg, top: i == 0 ? 0 : ChainSpace.lg,
bottom: FaiSpace.sm, bottom: ChainSpace.sm,
), ),
child: Text( child: Text(
item.label, item.label,
@ -242,8 +242,8 @@ class _GroupedEventList extends StatelessWidget {
} }
final e = (item as _EventItem).event; final e = (item as _EventItem).event;
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: FaiSpace.xs), padding: const EdgeInsets.only(bottom: ChainSpace.xs),
child: FaiDataRow( child: ChainDataRow(
accent: toneFor(e.type), accent: toneFor(e.type),
leading: formatTime(e.timestamp), leading: formatTime(e.timestamp),
title: e.type, title: e.type,
@ -328,7 +328,7 @@ class _FilterChips extends StatelessWidget {
children: [ children: [
for (final (v, label) in items) for (final (v, label) in items)
Padding( Padding(
padding: const EdgeInsets.only(left: FaiSpace.xs), padding: const EdgeInsets.only(left: ChainSpace.xs),
child: _ChipButton( child: _ChipButton(
label: label, label: label,
selected: value == v, selected: value == v,
@ -378,14 +378,14 @@ class _ChipButtonState extends State<_ChipButton> {
child: GestureDetector( child: GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: FaiMotion.fast, duration: ChainMotion.fast,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.md, horizontal: ChainSpace.md,
vertical: 6, vertical: 6,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all( border: Border.all(
color: widget.selected color: widget.selected
? theme.colorScheme.primary.withValues(alpha: 0.3) ? theme.colorScheme.primary.withValues(alpha: 0.3)
@ -394,7 +394,7 @@ class _ChipButtonState extends State<_ChipButton> {
), ),
child: Text( child: Text(
widget.label, widget.label,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
weight: widget.selected ? FontWeight.w500 : FontWeight.w400, weight: widget.selected ? FontWeight.w500 : FontWeight.w400,
color: fg, color: fg,
@ -420,8 +420,8 @@ class _LiveStatusBar extends StatelessWidget {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.xl, horizontal: ChainSpace.xl,
vertical: FaiSpace.sm, vertical: ChainSpace.sm,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
@ -431,11 +431,11 @@ class _LiveStatusBar extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
FaiStatusDot( ChainStatusDot(
color: live ? FaiColors.success : FaiColors.danger, color: live ? ChainColors.success : ChainColors.danger,
pulsing: live, pulsing: live,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
live ? l.auditLiveStatus(eventCount) : l.auditDisconnected(error!), live ? l.auditLiveStatus(eventCount) : l.auditDisconnected(error!),
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -569,7 +569,7 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
return AlertDialog( return AlertDialog(
title: Text(event.type), title: Text(event.type),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
), ),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640, maxHeight: 560), constraints: const BoxConstraints(maxWidth: 640, maxHeight: 560),
@ -616,7 +616,7 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
valueColor: theme.colorScheme.error, valueColor: theme.colorScheme.error,
), ),
if (detail.isNotEmpty) ...[ if (detail.isNotEmpty) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
l.auditDetailHeader, l.auditDetailHeader,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -627,17 +627,17 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
const SizedBox(height: 4), const SizedBox(height: 4),
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText(detail, style: FaiTheme.mono(size: 11)), child: SelectableText(detail, style: ChainTheme.mono(size: 11)),
), ),
], ],
if (_explanation != null || _explaining) ...[ if (_explanation != null || _explaining) ...[
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_ExplanationPanel( _ExplanationPanel(
explaining: _explaining, explaining: _explaining,
result: _explanation, result: _explanation,
@ -729,7 +729,7 @@ class _FlowRunDialog extends StatelessWidget {
return AlertDialog( return AlertDialog(
title: Text(l.auditFlowRunDialogTitle(flowName)), title: Text(l.auditFlowRunDialogTitle(flowName)),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
), ),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640, maxHeight: maxHeight), constraints: BoxConstraints(maxWidth: 640, maxHeight: maxHeight),
@ -743,15 +743,15 @@ class _FlowRunDialog extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Flexible( Flexible(
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemCount: related.length, itemCount: related.length,
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.xs), separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.xs),
itemBuilder: (context, i) { itemBuilder: (context, i) {
final e = related[i]; final e = related[i];
return FaiDataRow( return ChainDataRow(
accent: _toneFor(e.type, theme), accent: _toneFor(e.type, theme),
leading: _formatRelativeTime(e.timestamp), leading: _formatRelativeTime(e.timestamp),
title: e.type, title: e.type,
@ -792,7 +792,7 @@ class _FlowRunDialog extends StatelessWidget {
Color _toneFor(String type, ThemeData theme) { Color _toneFor(String type, ThemeData theme) {
if (type.endsWith('.failed')) return theme.colorScheme.error; if (type.endsWith('.failed')) return theme.colorScheme.error;
if (type.endsWith('.completed')) return theme.colorScheme.primary; if (type.endsWith('.completed')) return theme.colorScheme.primary;
if (type.endsWith('.started')) return FaiColors.warning; if (type.endsWith('.started')) return ChainColors.warning;
return theme.colorScheme.outline; return theme.colorScheme.outline;
} }
} }
@ -831,7 +831,7 @@ class _Field extends StatelessWidget {
child: SelectableText( child: SelectableText(
value, value,
style: mono style: mono
? FaiTheme.mono( ? ChainTheme.mono(
size: 11, size: 11,
color: valueColor ?? theme.colorScheme.onSurface, color: valueColor ?? theme.colorScheme.onSurface,
) )
@ -875,10 +875,10 @@ class _ExplanationPanel extends StatelessWidget {
: theme.colorScheme.error; : theme.colorScheme.error;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: color.withValues(alpha: 0.3)), border: Border.all(color: color.withValues(alpha: 0.3)),
), ),
child: Column( child: Column(
@ -887,7 +887,7 @@ class _ExplanationPanel extends StatelessWidget {
Row( Row(
children: [ children: [
Icon(Icons.auto_awesome, size: 14, color: color), Icon(Icons.auto_awesome, size: 14, color: color),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
Text( Text(
l.auditSystemAi, l.auditSystemAi,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -895,15 +895,15 @@ class _ExplanationPanel extends StatelessWidget {
letterSpacing: 0.6, letterSpacing: 0.6,
), ),
), ),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
FaiPill( ChainPill(
label: privacyMode, label: privacyMode,
tone: privacyMode == 'full' tone: privacyMode == 'full'
? FaiPillTone.warning ? ChainPillTone.warning
: FaiPillTone.neutral, : ChainPillTone.neutral,
), ),
if (result != null && result!.isSuccess && result!.cached) ...[ if (result != null && result!.isSuccess && result!.cached) ...[
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
Tooltip( Tooltip(
message: () { message: () {
final hits = result!.cacheHits > 1 final hits = result!.cacheHits > 1
@ -913,9 +913,9 @@ class _ExplanationPanel extends StatelessWidget {
? l.auditCachedTooltipUnknown(hits) ? l.auditCachedTooltipUnknown(hits)
: l.auditCachedTooltipKnown(result!.cachedAt, hits); : l.auditCachedTooltipKnown(result!.cachedAt, hits);
}(), }(),
child: FaiPill( child: ChainPill(
label: l.auditCachedPill, label: l.auditCachedPill,
tone: FaiPillTone.success, tone: ChainPillTone.success,
icon: Icons.bolt, icon: Icons.bolt,
), ),
), ),
@ -926,7 +926,7 @@ class _ExplanationPanel extends StatelessWidget {
result!.cached result!.cached
? l.auditOriginalLatency(result!.latencyMs) ? l.auditOriginalLatency(result!.latencyMs)
: l.auditLatency(result!.latencyMs), : l.auditLatency(result!.latencyMs),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -934,7 +934,7 @@ class _ExplanationPanel extends StatelessWidget {
if (result != null && if (result != null &&
result!.isSuccess && result!.isSuccess &&
onRegenerate != null) ...[ onRegenerate != null) ...[
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Tooltip( Tooltip(
message: l.auditRegenerateTooltip, message: l.auditRegenerateTooltip,
child: IconButton( child: IconButton(
@ -946,7 +946,7 @@ class _ExplanationPanel extends StatelessWidget {
], ],
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (explaining) if (explaining)
Row( Row(
children: [ children: [
@ -955,7 +955,7 @@ class _ExplanationPanel extends StatelessWidget {
height: 14, height: 14,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
l.auditAskingFull, l.auditAskingFull,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -974,7 +974,7 @@ class _ExplanationPanel extends StatelessWidget {
), ),
), ),
if (result!.fixHint(l).isNotEmpty) ...[ if (result!.fixHint(l).isNotEmpty) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
l.auditFixLabel, l.auditFixLabel,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -1063,7 +1063,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
TextField( TextField(
controller: _reviewer, controller: _reviewer,
decoration: InputDecoration( decoration: InputDecoration(
@ -1072,7 +1072,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
ValueListenableBuilder<TextEditingValue>( ValueListenableBuilder<TextEditingValue>(
valueListenable: _reason, valueListenable: _reason,
builder: (_, _, _) => TextField( builder: (_, _, _) => TextField(

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../data/error_presentation.dart'; import '../data/error_presentation.dart';
import '../data/fai_log.dart'; import '../data/chain_log.dart';
import '../data/hub.dart'; import '../data/hub.dart';
import '../data/system_actions.dart'; import '../data/system_actions.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
@ -47,7 +47,7 @@ class _DoctorPageState extends State<DoctorPage> {
tooltip: AppLocalizations.of(context)!.doctorRecheckTooltip, tooltip: AppLocalizations.of(context)!.doctorRecheckTooltip,
onPressed: _refresh, onPressed: _refresh,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
], ],
), ),
body: FutureBuilder<DoctorSnapshot>( body: FutureBuilder<DoctorSnapshot>(
@ -58,7 +58,7 @@ class _DoctorPageState extends State<DoctorPage> {
} }
if (snapshot.hasError) { if (snapshot.hasError) {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return FaiEmptyState( return ChainEmptyState(
icon: Icons.cloud_off_outlined, icon: Icons.cloud_off_outlined,
iconColor: Theme.of(context).colorScheme.error, iconColor: Theme.of(context).colorScheme.error,
title: l.hubUnreachable, title: l.hubUnreachable,
@ -74,34 +74,34 @@ class _DoctorPageState extends State<DoctorPage> {
s.update.updateAvailable || s.update.updateAvailable ||
(!s.update.manifestReachable && s.update.localVersion.isNotEmpty); (!s.update.manifestReachable && s.update.localVersion.isNotEmpty);
return ListView( return ListView(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
children: [ children: [
if (showUpdate) _UpdateBanner(status: s.update), if (showUpdate) _UpdateBanner(status: s.update),
if (showUpdate) const SizedBox(height: FaiSpace.lg), if (showUpdate) const SizedBox(height: ChainSpace.lg),
_SummaryStrip(snapshot: s), _SummaryStrip(snapshot: s),
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
_Section( _Section(
title: AppLocalizations.of(context)!.doctorEventLogSection, title: AppLocalizations.of(context)!.doctorEventLogSection,
child: _EventLogPanel(snapshot: s, onRefresh: _refresh), child: _EventLogPanel(snapshot: s, onRefresh: _refresh),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_Section( _Section(
title: AppLocalizations.of( title: AppLocalizations.of(
context, context,
)!.doctorModulesApprovalsSection, )!.doctorModulesApprovalsSection,
child: _ModulesPanel(snapshot: s), child: _ModulesPanel(snapshot: s),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_Section( _Section(
title: AppLocalizations.of(context)!.doctorHostServicesSection, title: AppLocalizations.of(context)!.doctorHostServicesSection,
child: _ServicesPanel(snapshot: s), child: _ServicesPanel(snapshot: s),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_Section( _Section(
title: AppLocalizations.of(context)!.doctorDaemonFilesSection, title: AppLocalizations.of(context)!.doctorDaemonFilesSection,
child: _DaemonPathsPanel(paths: s.paths), child: _DaemonPathsPanel(paths: s.paths),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_Section( _Section(
title: AppLocalizations.of(context)!.doctorDaemonControlSection, title: AppLocalizations.of(context)!.doctorDaemonControlSection,
child: _DaemonActionsCard(), child: _DaemonActionsCard(),
@ -128,8 +128,8 @@ class _Section extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: FaiSpace.xs, left: ChainSpace.xs,
bottom: FaiSpace.sm, bottom: ChainSpace.sm,
), ),
child: Text( child: Text(
title.toUpperCase(), title.toUpperCase(),
@ -162,17 +162,17 @@ class _SummaryStrip extends StatelessWidget {
subtitle: l.doctorSummaryCapabilities(snapshot.capabilityCount), subtitle: l.doctorSummaryCapabilities(snapshot.capabilityCount),
icon: Icons.extension_outlined, icon: Icons.extension_outlined,
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
_StatTile( _StatTile(
label: l.doctorSummaryApprovals, label: l.doctorSummaryApprovals,
value: snapshot.pendingApprovals.toString(), value: snapshot.pendingApprovals.toString(),
subtitle: l.doctorSummaryPending, subtitle: l.doctorSummaryPending,
icon: Icons.inbox_outlined, icon: Icons.inbox_outlined,
tone: snapshot.pendingApprovals > 0 tone: snapshot.pendingApprovals > 0
? FaiPillTone.warning ? ChainPillTone.warning
: FaiPillTone.neutral, : ChainPillTone.neutral,
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
_StatTile( _StatTile(
label: l.doctorSummaryAudit, label: l.doctorSummaryAudit,
value: snapshot.chainHealthy ? '' : '', value: snapshot.chainHealthy ? '' : '',
@ -182,10 +182,10 @@ class _SummaryStrip extends StatelessWidget {
), ),
icon: Icons.shield_outlined, icon: Icons.shield_outlined,
tone: snapshot.chainHealthy tone: snapshot.chainHealthy
? FaiPillTone.success ? ChainPillTone.success
: FaiPillTone.danger, : ChainPillTone.danger,
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
_StatTile( _StatTile(
label: l.doctorSummaryServices, label: l.doctorSummaryServices,
value: snapshot.services.length.toString(), value: snapshot.services.length.toString(),
@ -202,36 +202,36 @@ class _StatTile extends StatelessWidget {
final String value; final String value;
final String subtitle; final String subtitle;
final IconData icon; final IconData icon;
final FaiPillTone tone; final ChainPillTone tone;
const _StatTile({ const _StatTile({
required this.label, required this.label,
required this.value, required this.value,
required this.subtitle, required this.subtitle,
required this.icon, required this.icon,
this.tone = FaiPillTone.neutral, this.tone = ChainPillTone.neutral,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final accentColor = switch (tone) { final accentColor = switch (tone) {
FaiPillTone.success => FaiColors.success, ChainPillTone.success => ChainColors.success,
FaiPillTone.warning => FaiColors.warning, ChainPillTone.warning => ChainColors.warning,
FaiPillTone.danger => theme.colorScheme.error, ChainPillTone.danger => theme.colorScheme.error,
_ => theme.colorScheme.primary, _ => theme.colorScheme.primary,
}; };
return Expanded( return Expanded(
child: FaiCard( child: ChainCard(
accentLeft: accentColor, accentLeft: accentColor,
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Icon(icon, size: 14, color: theme.colorScheme.onSurfaceVariant), Icon(icon, size: 14, color: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
Flexible( Flexible(
child: Text( child: Text(
label.toUpperCase(), label.toUpperCase(),
@ -245,7 +245,7 @@ class _StatTile extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
value, value,
style: theme.textTheme.displaySmall?.copyWith( style: theme.textTheme.displaySmall?.copyWith(
@ -282,15 +282,15 @@ class _EventLogPanel extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final healthy = snapshot.chainHealthy; final healthy = snapshot.chainHealthy;
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return FaiCard( return ChainCard(
child: Row( child: Row(
children: [ children: [
Icon( Icon(
healthy ? Icons.verified_outlined : Icons.gpp_bad_outlined, healthy ? Icons.verified_outlined : Icons.gpp_bad_outlined,
size: 24, size: 24,
color: healthy ? FaiColors.success : theme.colorScheme.error, color: healthy ? ChainColors.success : theme.colorScheme.error,
), ),
const SizedBox(width: FaiSpace.lg), const SizedBox(width: ChainSpace.lg),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -322,10 +322,10 @@ class _EventLogPanel extends StatelessWidget {
icon: const Icon(Icons.fact_check_outlined, size: 16), icon: const Icon(Icons.fact_check_outlined, size: 16),
label: Text(l.doctorVerifyNow), label: Text(l.doctorVerifyNow),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: healthy ? l.doctorChainPillOk : l.doctorChainPillTamper, label: healthy ? l.doctorChainPillOk : l.doctorChainPillTamper,
tone: healthy ? FaiPillTone.success : FaiPillTone.danger, tone: healthy ? ChainPillTone.success : ChainPillTone.danger,
), ),
], ],
), ),
@ -353,7 +353,7 @@ class _DaemonPathsPanel extends StatelessWidget {
(l.doctorPathLog, paths.logPath, Icons.description_outlined, false), (l.doctorPathLog, paths.logPath, Icons.description_outlined, false),
( (
l.doctorPathStudioErrors, l.doctorPathStudioErrors,
FaiLog.instance.path, ChainLog.instance.path,
Icons.report_problem_outlined, Icons.report_problem_outlined,
false, false,
), ),
@ -365,7 +365,7 @@ class _DaemonPathsPanel extends StatelessWidget {
].where((e) => e.$2.isNotEmpty).toList(); ].where((e) => e.$2.isNotEmpty).toList();
if (entries.isEmpty) { if (entries.isEmpty) {
return FaiCard( return ChainCard(
child: Text( child: Text(
l.doctorPathsEmpty, l.doctorPathsEmpty,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
@ -373,7 +373,7 @@ class _DaemonPathsPanel extends StatelessWidget {
); );
} }
return FaiCard( return ChainCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -416,7 +416,7 @@ class _PathRow extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 16, color: theme.colorScheme.onSurfaceVariant), Icon(icon, size: 16, color: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
SizedBox( SizedBox(
width: 90, width: 90,
child: Text( child: Text(
@ -429,13 +429,13 @@ class _PathRow extends StatelessWidget {
Expanded( Expanded(
child: SelectableText( child: SelectableText(
path, path,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
if (isLog || isConfig) ...[ if (isLog || isConfig) ...[
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => isConfig onPressed: () => isConfig
@ -447,7 +447,7 @@ class _PathRow extends StatelessWidget {
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
), ),
), ),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
], ],
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () async { onPressed: () async {
@ -494,7 +494,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
/// True when the last daemon action failed because no `fai` /// True when the last daemon action failed because no `fai`
/// binary could be located. Swaps the raw output line for the /// binary could be located. Swaps the raw output line for the
/// actionable [FaiBinaryRecovery] block. /// actionable [ChainBinaryRecovery] block.
bool _binaryMissing = false; bool _binaryMissing = false;
ChannelStatusSnapshot? _channels; ChannelStatusSnapshot? _channels;
@ -557,7 +557,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
} }
} }
} }
return FaiCard( return ChainCard(
// Row + Expanded forces the card to take the full width // Row + Expanded forces the card to take the full width
// its parent offers. Without this, the card hugs the // its parent offers. Without this, the card hugs the
// intrinsic width of the longest button row and ends up // intrinsic width of the longest button row and ends up
@ -575,10 +575,10 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
: Icons.power_off_outlined, : Icons.power_off_outlined,
size: 18, size: 18,
color: active?.running == true color: active?.running == true
? FaiColors.success ? ChainColors.success
: theme.colorScheme.onSurfaceVariant, : theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
active == null active == null
@ -593,36 +593,36 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
), ),
), ),
if (active != null) ...[ if (active != null) ...[
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: active.running label: active.running
? l.doctorPillRunning ? l.doctorPillRunning
: l.doctorPillStopped, : l.doctorPillStopped,
tone: active.running tone: active.running
? FaiPillTone.success ? ChainPillTone.success
: FaiPillTone.neutral, : ChainPillTone.neutral,
), ),
], ],
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
l.doctorDaemonControlHint, l.doctorDaemonControlHint,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Wrap( Wrap(
spacing: FaiSpace.sm, spacing: ChainSpace.sm,
runSpacing: FaiSpace.sm, runSpacing: ChainSpace.sm,
children: [ children: [
FilledButton.tonalIcon( FilledButton.tonalIcon(
onPressed: _busy onPressed: _busy
? null ? null
: () => _run( : () => _run(
l.daemonActionRestart, l.daemonActionRestart,
() => SystemActions.faiDaemon(['restart']), () => SystemActions.chainDaemon(['restart']),
), ),
icon: const Icon(Icons.restart_alt, size: 16), icon: const Icon(Icons.restart_alt, size: 16),
label: Text(l.doctorRestart), label: Text(l.doctorRestart),
@ -633,7 +633,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
? null ? null
: () => _run( : () => _run(
l.daemonActionStart, l.daemonActionStart,
() => SystemActions.faiDaemon(['start']), () => SystemActions.chainDaemon(['start']),
), ),
icon: const Icon(Icons.play_arrow, size: 16), icon: const Icon(Icons.play_arrow, size: 16),
label: Text(l.doctorStart), label: Text(l.doctorStart),
@ -643,7 +643,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
? null ? null
: () => _run( : () => _run(
l.daemonActionStop, l.daemonActionStop,
() => SystemActions.faiDaemon(['stop']), () => SystemActions.chainDaemon(['stop']),
), ),
icon: const Icon(Icons.stop_circle_outlined, size: 16), icon: const Icon(Icons.stop_circle_outlined, size: 16),
label: Text(l.doctorStop), label: Text(l.doctorStop),
@ -653,7 +653,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
? null ? null
: () => _run( : () => _run(
l.daemonActionStatus, l.daemonActionStatus,
() => SystemActions.faiDaemon(['status']), () => SystemActions.chainDaemon(['status']),
), ),
icon: const Icon(Icons.health_and_safety_outlined, size: 16), icon: const Icon(Icons.health_and_safety_outlined, size: 16),
label: Text(l.doctorStatusAction), label: Text(l.doctorStatusAction),
@ -661,7 +661,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
], ],
), ),
if (_busy) ...[ if (_busy) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Row( Row(
children: [ children: [
const SizedBox( const SizedBox(
@ -669,7 +669,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
height: 14, height: 14,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
l.doctorDaemonControlWorking, l.doctorDaemonControlWorking,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -680,27 +680,27 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
), ),
], ],
if (_binaryMissing) ...[ if (_binaryMissing) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
FaiBinaryRecovery( ChainBinaryRecovery(
onLocated: () => _run( onLocated: () => _run(
l.daemonActionStatus, l.daemonActionStatus,
() => SystemActions.faiDaemon(['status']), () => SystemActions.chainDaemon(['status']),
), ),
), ),
], ],
if (_output != null) ...[ if (_output != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.sm), padding: const EdgeInsets.all(ChainSpace.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText( child: SelectableText(
_output!, _output!,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
@ -732,7 +732,7 @@ class _ModulesPanel extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return FaiCard( return ChainCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -743,7 +743,7 @@ class _ModulesPanel extends StatelessWidget {
size: 18, size: 18,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -767,17 +767,17 @@ class _ModulesPanel extends StatelessWidget {
], ],
), ),
), ),
FaiPill( ChainPill(
label: snapshot.moduleCount > 0 label: snapshot.moduleCount > 0
? l.doctorPillLoaded ? l.doctorPillLoaded
: l.doctorPillEmpty, : l.doctorPillEmpty,
tone: snapshot.moduleCount > 0 tone: snapshot.moduleCount > 0
? FaiPillTone.success ? ChainPillTone.success
: FaiPillTone.neutral, : ChainPillTone.neutral,
), ),
], ],
), ),
const Divider(height: FaiSpace.xl), const Divider(height: ChainSpace.xl),
Row( Row(
children: [ children: [
Icon( Icon(
@ -785,7 +785,7 @@ class _ModulesPanel extends StatelessWidget {
size: 18, size: 18,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
snapshot.pendingApprovals == 0 snapshot.pendingApprovals == 0
? l.doctorApprovalsNone ? l.doctorApprovalsNone
@ -794,9 +794,9 @@ class _ModulesPanel extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
if (snapshot.pendingApprovals > 0) if (snapshot.pendingApprovals > 0)
FaiPill( ChainPill(
label: l.doctorApprovalsAttentionPill, label: l.doctorApprovalsAttentionPill,
tone: FaiPillTone.warning, tone: ChainPillTone.warning,
), ),
], ],
), ),
@ -819,10 +819,10 @@ class _ServicesPanel extends StatelessWidget {
// Wrap on narrow windows so the mono-spaced hint // Wrap on narrow windows so the mono-spaced hint
// (`add to ~/.chain/config.yaml under services:`) does not // (`add to ~/.chain/config.yaml under services:`) does not
// overflow horizontally past the card's right edge. // overflow horizontally past the card's right edge.
return FaiCard( return ChainCard(
child: Wrap( child: Wrap(
spacing: FaiSpace.sm, spacing: ChainSpace.sm,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
Row( Row(
@ -833,7 +833,7 @@ class _ServicesPanel extends StatelessWidget {
size: 18, size: 18,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
l.doctorServicesEmpty, l.doctorServicesEmpty,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
@ -844,7 +844,7 @@ class _ServicesPanel extends StatelessWidget {
), ),
Text( Text(
l.doctorServicesEmptyHint, l.doctorServicesEmptyHint,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -853,7 +853,7 @@ class _ServicesPanel extends StatelessWidget {
), ),
); );
} }
return FaiCard( return ChainCard(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
@ -861,7 +861,7 @@ class _ServicesPanel extends StatelessWidget {
if (i > 0) if (i > 0)
Divider(height: 1, color: theme.colorScheme.outlineVariant), Divider(height: 1, color: theme.colorScheme.outlineVariant),
Padding( Padding(
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
@ -869,25 +869,25 @@ class _ServicesPanel extends StatelessWidget {
size: 16, size: 16,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
snapshot.services[i].name, snapshot.services[i].name,
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
Text( Text(
snapshot.services[i].endpoint, snapshot.services[i].endpoint,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const Spacer(), const Spacer(),
for (final tag in snapshot.services[i].tags) ...[ for (final tag in snapshot.services[i].tags) ...[
FaiPill(label: tag, tone: FaiPillTone.neutral), ChainPill(label: tag, tone: ChainPillTone.neutral),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
], ],
], ],
), ),
@ -917,7 +917,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
_applying = true; _applying = true;
_applyOutput = null; _applyOutput = null;
}); });
final r = await SystemActions.faiUpdateApply(widget.status.channel); final r = await SystemActions.chainUpdateApply(widget.status.channel);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_applying = false; _applying = false;
@ -945,7 +945,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
final body = available final body = available
? l.doctorUpdateBody(status.channel, status.latestVersion) ? l.doctorUpdateBody(status.channel, status.latestVersion)
: (status.reason ?? l.doctorUpdateUnreachableBody(status.channel)); : (status.reason ?? l.doctorUpdateUnreachableBody(status.channel));
return FaiCard( return ChainCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -953,7 +953,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(iconData, size: 20, color: iconColor), Icon(iconData, size: 20, color: iconColor),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -967,7 +967,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
body, body,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -976,7 +976,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
l.doctorReleaseNotes(status.releaseNotesUrl!), l.doctorReleaseNotes(status.releaseNotesUrl!),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
@ -999,16 +999,16 @@ class _UpdateBannerState extends State<_UpdateBanner> {
_applying ? l.doctorApplying : l.doctorApplyUpdate, _applying ? l.doctorApplying : l.doctorApplyUpdate,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: available ? l.doctorPillNew : l.doctorPillOffline, label: available ? l.doctorPillNew : l.doctorPillOffline,
tone: available ? FaiPillTone.success : FaiPillTone.neutral, tone: available ? ChainPillTone.success : ChainPillTone.neutral,
), ),
], ],
), ),
if (_applyOutput != null) ...[ if (_applyOutput != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
FaiErrorBox(text: _applyOutput!, maxHeight: 240), ChainErrorBox(text: _applyOutput!, maxHeight: 240),
], ],
], ],
), ),

View file

@ -103,7 +103,7 @@ class _FederationPageState extends State<FederationPage> {
tooltip: l.federationReloadTooltip, tooltip: l.federationReloadTooltip,
onPressed: _refresh, onPressed: _refresh,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
], ],
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
@ -118,7 +118,7 @@ class _FederationPageState extends State<FederationPage> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.cloud_off_outlined, icon: Icons.cloud_off_outlined,
iconColor: theme.colorScheme.error, iconColor: theme.colorScheme.error,
title: l.hubUnreachable, title: l.hubUnreachable,
@ -131,7 +131,7 @@ class _FederationPageState extends State<FederationPage> {
} }
final sats = snapshot.data ?? []; final sats = snapshot.data ?? [];
if (sats.isEmpty) { if (sats.isEmpty) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.hub_outlined, icon: Icons.hub_outlined,
title: l.federationEmptyTitle, title: l.federationEmptyTitle,
hint: l.federationEmptyHint, hint: l.federationEmptyHint,
@ -143,9 +143,9 @@ class _FederationPageState extends State<FederationPage> {
); );
} }
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
itemCount: sats.length, itemCount: sats.length,
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.md), separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.md),
itemBuilder: (context, i) => _SatelliteCard(satellite: sats[i]), itemBuilder: (context, i) => _SatelliteCard(satellite: sats[i]),
); );
}, },
@ -162,19 +162,19 @@ class _SatelliteCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return FaiCard( return ChainCard(
accentTop: true, accentTop: true,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
FaiPill( ChainPill(
label: l.federationPillConnected, label: l.federationPillConnected,
tone: FaiPillTone.success, tone: ChainPillTone.success,
icon: Icons.link, icon: Icons.link,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
satellite.displayName, satellite.displayName,
@ -183,19 +183,19 @@ class _SatelliteCard extends StatelessWidget {
), ),
), ),
), ),
FaiPill( ChainPill(
label: satellite.region.isEmpty label: satellite.region.isEmpty
? l.federationRegionNone ? l.federationRegionNone
: satellite.region, : satellite.region,
tone: FaiPillTone.neutral, tone: ChainPillTone.neutral,
icon: Icons.public, icon: Icons.public,
), ),
], ],
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Wrap( Wrap(
spacing: FaiSpace.md, spacing: ChainSpace.md,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
children: [ children: [
_meta(theme, 'v${satellite.hubVersion}'), _meta(theme, 'v${satellite.hubVersion}'),
_meta(theme, 'wire ${satellite.wireVersion}'), _meta(theme, 'wire ${satellite.wireVersion}'),
@ -203,7 +203,7 @@ class _SatelliteCard extends StatelessWidget {
], ],
), ),
if (satellite.capabilities.isNotEmpty) ...[ if (satellite.capabilities.isNotEmpty) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
l.federationCapabilities(satellite.capabilities.length), l.federationCapabilities(satellite.capabilities.length),
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -213,13 +213,13 @@ class _SatelliteCard extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Wrap( Wrap(
spacing: FaiSpace.xs, spacing: ChainSpace.xs,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
children: satellite.capabilities children: satellite.capabilities
.map( .map(
(c) => FaiPill( (c) => ChainPill(
label: c, label: c,
tone: FaiPillTone.accent, tone: ChainPillTone.accent,
icon: Icons.extension_outlined, icon: Icons.extension_outlined,
), ),
) )
@ -233,7 +233,7 @@ class _SatelliteCard extends StatelessWidget {
Widget _meta(ThemeData theme, String text) => Text( Widget _meta(ThemeData theme, String text) => Text(
text, text,
style: FaiTheme.mono(size: 10, color: theme.colorScheme.onSurfaceVariant), style: ChainTheme.mono(size: 10, color: theme.colorScheme.onSurfaceVariant),
); );
} }
@ -261,11 +261,11 @@ class _EnrollmentDialog extends StatelessWidget {
Text(l.federationTokenLabel, style: theme.textTheme.labelSmall), Text(l.federationTokenLabel, style: theme.textTheme.labelSmall),
const SizedBox(height: 4), const SizedBox(height: 4),
_CopyableBlock(text: enrollment.token, copiedMsg: l.federationCopied), _CopyableBlock(text: enrollment.token, copiedMsg: l.federationCopied),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text(l.federationConfigLabel, style: theme.textTheme.labelSmall), Text(l.federationConfigLabel, style: theme.textTheme.labelSmall),
const SizedBox(height: 4), const SizedBox(height: 4),
_CopyableBlock(text: config, copiedMsg: l.federationCopied), _CopyableBlock(text: config, copiedMsg: l.federationCopied),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
l.federationEnrollmentHint, l.federationEnrollmentHint,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -299,19 +299,19 @@ class _CopyableBlock extends StatelessWidget {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.md, ChainSpace.md,
FaiSpace.md, ChainSpace.md,
40, 40,
FaiSpace.md, ChainSpace.md,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText( child: SelectableText(
text, text,
style: FaiTheme.mono(size: 11, color: theme.colorScheme.onSurface), style: ChainTheme.mono(size: 11, color: theme.colorScheme.onSurface),
), ),
), ),
Positioned( Positioned(

View file

@ -48,7 +48,7 @@ class _ModulesPageState extends State<ModulesPage> {
tooltip: AppLocalizations.of(context)!.modulesReloadTooltip, tooltip: AppLocalizations.of(context)!.modulesReloadTooltip,
onPressed: _refresh, onPressed: _refresh,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
], ],
), ),
body: FutureBuilder<List<ModuleSummary>>( body: FutureBuilder<List<ModuleSummary>>(
@ -59,7 +59,7 @@ class _ModulesPageState extends State<ModulesPage> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.cloud_off_outlined, icon: Icons.cloud_off_outlined,
iconColor: Theme.of(context).colorScheme.error, iconColor: Theme.of(context).colorScheme.error,
title: l.hubUnreachable, title: l.hubUnreachable,
@ -72,22 +72,22 @@ class _ModulesPageState extends State<ModulesPage> {
} }
final modules = snapshot.data ?? []; final modules = snapshot.data ?? [];
if (modules.isEmpty) { if (modules.isEmpty) {
return FaiEmptyState( return ChainEmptyState(
icon: Icons.extension_outlined, icon: Icons.extension_outlined,
title: l.modulesNoneTitle, title: l.modulesNoneTitle,
hint: l.modulesNoneHint, hint: l.modulesNoneHint,
); );
} }
return ListView( return ListView(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
children: [ children: [
_RecentActivityPanel(future: _historyFuture), _RecentActivityPanel(future: _historyFuture),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
for (var i = 0; i < modules.length; i++) ...[ for (var i = 0; i < modules.length; i++) ...[
if (i > 0) const SizedBox(height: FaiSpace.md), if (i > 0) const SizedBox(height: ChainSpace.md),
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
final uninstalled = await FaiModuleSheet.show( final uninstalled = await ChainModuleSheet.show(
context, context,
modules[i].name, modules[i].name,
); );
@ -116,7 +116,7 @@ class _ModuleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return FaiCard( return ChainCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -128,15 +128,15 @@ class _ModuleCard extends StatelessWidget {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: 'v${module.version}', label: 'v${module.version}',
tone: FaiPillTone.neutral, tone: ChainPillTone.neutral,
monospace: true, monospace: true,
), ),
], ],
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -152,13 +152,13 @@ class _ModuleCard extends StatelessWidget {
), ),
Expanded( Expanded(
child: Wrap( child: Wrap(
spacing: FaiSpace.xs, spacing: ChainSpace.xs,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
children: module.capabilities children: module.capabilities
.map( .map(
(c) => FaiPill( (c) => ChainPill(
label: c, label: c,
tone: FaiPillTone.accent, tone: ChainPillTone.accent,
monospace: true, monospace: true,
), ),
) )
@ -192,12 +192,12 @@ class _RecentActivityPanel extends StatelessWidget {
if (events.isEmpty) return const SizedBox.shrink(); if (events.isEmpty) return const SizedBox.shrink();
return Container( return Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.md, horizontal: ChainSpace.md,
vertical: FaiSpace.sm, vertical: ChainSpace.sm,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Column( child: Column(
@ -219,7 +219,7 @@ class _RecentActivityPanel extends StatelessWidget {
fontSize: 10, fontSize: 10,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
AppLocalizations.of( AppLocalizations.of(
context, context,
@ -230,7 +230,7 @@ class _RecentActivityPanel extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
for (final e in events) _ActivityRow(event: e), for (final e in events) _ActivityRow(event: e),
], ],
), ),
@ -248,7 +248,7 @@ class _ActivityRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final installed = event.type == 'module.installed'; final installed = event.type == 'module.installed';
final accent = installed ? FaiColors.success : FaiColors.warning; final accent = installed ? ChainColors.success : ChainColors.warning;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 2), padding: const EdgeInsets.symmetric(vertical: 2),
child: Row( child: Row(
@ -263,7 +263,7 @@ class _ActivityRow extends StatelessWidget {
width: 150, width: 150,
child: Text( child: Text(
_formatTimestamp(event.timestamp.toLocal()), _formatTimestamp(event.timestamp.toLocal()),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),

File diff suppressed because it is too large Load diff

View file

@ -35,14 +35,14 @@ class WelcomePage extends StatelessWidget {
final hubDown = StudioShellState.of(context)?.connected == false; final hubDown = StudioShellState.of(context)?.connected == false;
return Scaffold( return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(titleSpacing: FaiSpace.xl, title: Text(l.navWelcome)), appBar: AppBar(titleSpacing: ChainSpace.xl, title: Text(l.navWelcome)),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.xl, ChainSpace.xl,
FaiSpace.lg, ChainSpace.lg,
FaiSpace.xl, ChainSpace.xl,
FaiSpace.xxl, ChainSpace.xxl,
), ),
// Center the 960-px content column on wide windows // Center the 960-px content column on wide windows
// so the right margin matches the left rather than // so the right margin matches the left rather than
@ -59,24 +59,24 @@ class WelcomePage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const _Hero(), const _Hero(),
const SizedBox(height: FaiSpace.xxl), const SizedBox(height: ChainSpace.xxl),
// When the hub is down the onboarding checklist // When the hub is down the onboarding checklist
// would render dead (all-unchecked, nothing to // would render dead (all-unchecked, nothing to
// probe). Lead with a "start the hub" card // probe). Lead with a "start the hub" card
// instead; restore the checklist once connected. // instead; restore the checklist once connected.
if (hubDown) ...[ if (hubDown) ...[
const _HubDownHero(), const _HubDownHero(),
const SizedBox(height: FaiSpace.xxl), const SizedBox(height: ChainSpace.xxl),
] else ] else
const _OnboardingChecklist(), const _OnboardingChecklist(),
const _PillarRow(), const _PillarRow(),
const SizedBox(height: FaiSpace.xxl), const SizedBox(height: ChainSpace.xxl),
const _SectionLabel(textKey: _SectionLabelKey.trust), const _SectionLabel(textKey: _SectionLabelKey.trust),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
const _TrustPosture(), const _TrustPosture(),
const SizedBox(height: FaiSpace.xxl), const SizedBox(height: ChainSpace.xxl),
const _SectionLabel(textKey: _SectionLabelKey.docs), const _SectionLabel(textKey: _SectionLabelKey.docs),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
const _DocsRow(), const _DocsRow(),
], ],
), ),
@ -100,9 +100,9 @@ class _Hero extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.xxl), padding: const EdgeInsets.all(ChainSpace.xxl),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
theme.colorScheme.primary.withValues(alpha: 0.18), theme.colorScheme.primary.withValues(alpha: 0.18),
@ -123,7 +123,7 @@ class _Hero extends StatelessWidget {
Row( Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.14), color: theme.colorScheme.primary.withValues(alpha: 0.14),
shape: BoxShape.circle, shape: BoxShape.circle,
@ -134,7 +134,7 @@ class _Hero extends StatelessWidget {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
), ),
const SizedBox(width: FaiSpace.lg), const SizedBox(width: ChainSpace.lg),
Expanded( Expanded(
child: Text( child: Text(
l.welcomeHeroTitle, l.welcomeHeroTitle,
@ -145,7 +145,7 @@ class _Hero extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
Text( Text(
l.welcomeHeroSubtitle, l.welcomeHeroSubtitle,
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
@ -189,13 +189,13 @@ class _HubDownHeroState extends State<_HubDownHero> {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
// No `fai` binary "Start hub" cannot work; lead the // No `fai` binary "Start hub" cannot work; lead the
// operator to the install guide instead. // operator to the install guide instead.
final canStart = SystemActions.faiBinaryExists(); final canStart = SystemActions.chainBinaryExists();
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.errorContainer.withValues(alpha: 0.35), color: theme.colorScheme.errorContainer.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all( border: Border.all(
color: theme.colorScheme.error.withValues(alpha: 0.4), color: theme.colorScheme.error.withValues(alpha: 0.4),
), ),
@ -210,7 +210,7 @@ class _HubDownHeroState extends State<_HubDownHero> {
size: 24, size: 24,
color: theme.colorScheme.error, color: theme.colorScheme.error,
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
Expanded( Expanded(
child: Text( child: Text(
l.welcomeHubDownTitle, l.welcomeHubDownTitle,
@ -221,7 +221,7 @@ class _HubDownHeroState extends State<_HubDownHero> {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
l.welcomeHubDownBody, l.welcomeHubDownBody,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
@ -229,15 +229,15 @@ class _HubDownHeroState extends State<_HubDownHero> {
height: 1.45, height: 1.45,
), ),
), ),
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
Text( Text(
l.welcomeHubDownEndpoint(HubService.instance.endpointLabel), l.welcomeHubDownEndpoint(HubService.instance.endpointLabel),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
if (canStart) if (canStart)
FilledButton.icon( FilledButton.icon(
onPressed: _starting ? null : _start, onPressed: _starting ? null : _start,
@ -253,8 +253,8 @@ class _HubDownHeroState extends State<_HubDownHero> {
), ),
) )
else else
const FaiBinaryRecovery(), const ChainBinaryRecovery(),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.open_in_new, size: 16), icon: const Icon(Icons.open_in_new, size: 16),
label: Text(l.welcomeHubDownDocsLink), label: Text(l.welcomeHubDownDocsLink),
@ -323,7 +323,7 @@ class _PillarRow extends StatelessWidget {
children: [ children: [
for (final p in pillars) ...[ for (final p in pillars) ...[
_Pillar(icon: p.$1, title: p.$2, body: p.$3), _Pillar(icon: p.$1, title: p.$2, body: p.$3),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
], ],
], ],
); );
@ -339,7 +339,7 @@ class _PillarRow extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
for (var i = 0; i < pillars.length; i++) ...[ for (var i = 0; i < pillars.length; i++) ...[
if (i > 0) const SizedBox(width: FaiSpace.md), if (i > 0) const SizedBox(width: ChainSpace.md),
Expanded( Expanded(
child: _Pillar( child: _Pillar(
icon: pillars[i].$1, icon: pillars[i].$1,
@ -367,24 +367,24 @@ class _Pillar extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Container( return Container(
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(icon, size: 28, color: theme.colorScheme.primary), Icon(icon, size: 28, color: theme.colorScheme.primary),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
title, title,
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
body, body,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
@ -427,7 +427,7 @@ class _TrustPosture extends StatelessWidget {
return Column( return Column(
children: [ children: [
for (var i = 0; i < rows.length; i++) ...[ for (var i = 0; i < rows.length; i++) ...[
if (i > 0) const SizedBox(height: FaiSpace.sm), if (i > 0) const SizedBox(height: ChainSpace.sm),
_TrustRow(icon: rows[i].$1, title: rows[i].$2, body: rows[i].$3), _TrustRow(icon: rows[i].$1, title: rows[i].$2, body: rows[i].$3),
], ],
], ],
@ -450,17 +450,17 @@ class _TrustRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Container( return Container(
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(icon, size: 22, color: theme.colorScheme.primary), Icon(icon, size: 22, color: theme.colorScheme.primary),
const SizedBox(width: FaiSpace.lg), const SizedBox(width: ChainSpace.lg),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -601,13 +601,13 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
_aiOk, _aiOk,
l.welcomeChecklistAi, l.welcomeChecklistAi,
l.welcomeChecklistAiHint, l.welcomeChecklistAiHint,
() => FaiSettingsDialog.show(context), () => ChainSettingsDialog.show(context),
), ),
( (
_mcpOk, _mcpOk,
l.welcomeChecklistMcp, l.welcomeChecklistMcp,
l.welcomeChecklistMcpHint, l.welcomeChecklistMcpHint,
() => FaiSettingsDialog.show(context), () => ChainSettingsDialog.show(context),
), ),
( (
_moduleOk, _moduleOk,
@ -623,7 +623,7 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
), ),
]; ];
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: FaiSpace.xxl), padding: const EdgeInsets.only(bottom: ChainSpace.xxl),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -661,11 +661,11 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Column( child: Column(
@ -684,7 +684,7 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
), ),
), ),
if (allDone) ...[ if (allDone) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
_AllDoneCelebration(onDismiss: _dismiss), _AllDoneCelebration(onDismiss: _dismiss),
], ],
], ],
@ -707,19 +707,19 @@ class _AllDoneCelebration extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Container( return Container(
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
color: FaiColors.success.withValues(alpha: 0.08), color: ChainColors.success.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: FaiColors.success.withValues(alpha: 0.35)), border: Border.all(color: ChainColors.success.withValues(alpha: 0.35)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Icon(Icons.celebration_outlined, color: FaiColors.success), Icon(Icons.celebration_outlined, color: ChainColors.success),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
l.welcomeChecklistAllSetTitle, l.welcomeChecklistAllSetTitle,
@ -741,7 +741,7 @@ class _AllDoneCelebration extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
_NextStepRow( _NextStepRow(
icon: Icons.timeline_outlined, icon: Icons.timeline_outlined,
title: l.welcomeChecklistNextAuditTitle, title: l.welcomeChecklistNextAuditTitle,
@ -797,12 +797,12 @@ class _NextStepRow extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final canActivate = doc != null && onOpenDoc != null; final canActivate = doc != null && onOpenDoc != null;
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: FaiSpace.sm), padding: const EdgeInsets.only(bottom: ChainSpace.sm),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(icon, size: 18, color: theme.colorScheme.primary), Icon(icon, size: 18, color: theme.colorScheme.primary),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -823,7 +823,7 @@ class _NextStepRow extends StatelessWidget {
], ],
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
OutlinedButton( OutlinedButton(
onPressed: canActivate ? () => onOpenDoc!(doc!) : null, onPressed: canActivate ? () => onOpenDoc!(doc!) : null,
child: Text(buttonLabel), child: Text(buttonLabel),
@ -856,16 +856,16 @@ class _ChecklistRow extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final row = Padding( final row = Padding(
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon( Icon(
done ? Icons.check_circle : Icons.radio_button_unchecked, done ? Icons.check_circle : Icons.radio_button_unchecked,
size: 20, size: 20,
color: done ? FaiColors.success : theme.colorScheme.outline, color: done ? ChainColors.success : theme.colorScheme.outline,
), ),
const SizedBox(width: FaiSpace.lg), const SizedBox(width: ChainSpace.lg),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -890,10 +890,10 @@ class _ChecklistRow extends StatelessWidget {
], ],
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
FaiPill( ChainPill(
label: done ? l.welcomeChecklistDone : l.welcomeChecklistTodo, label: done ? l.welcomeChecklistDone : l.welcomeChecklistTodo,
tone: done ? FaiPillTone.success : FaiPillTone.neutral, tone: done ? ChainPillTone.success : ChainPillTone.neutral,
), ),
], ],
), ),
@ -985,7 +985,7 @@ class _DocsRow extends StatelessWidget {
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final twoCols = constraints.maxWidth >= 640; final twoCols = constraints.maxWidth >= 640;
@ -997,16 +997,16 @@ class _DocsRow extends StatelessWidget {
return Column( return Column(
children: [ children: [
for (var i = 0; i < _kDocs.length; i++) ...[ for (var i = 0; i < _kDocs.length; i++) ...[
if (i > 0) const SizedBox(height: FaiSpace.md), if (i > 0) const SizedBox(height: ChainSpace.md),
_DocCard(entry: _kDocs[i]), _DocCard(entry: _kDocs[i]),
], ],
], ],
); );
} }
final cardWidth = (constraints.maxWidth - FaiSpace.md) / 2; final cardWidth = (constraints.maxWidth - ChainSpace.md) / 2;
return Wrap( return Wrap(
spacing: FaiSpace.md, spacing: ChainSpace.md,
runSpacing: FaiSpace.md, runSpacing: ChainSpace.md,
children: [ children: [
for (final d in _kDocs) for (final d in _kDocs)
SizedBox( SizedBox(
@ -1045,30 +1045,30 @@ class _DocCardState extends State<_DocCard> {
onEnter: (_) => setState(() => _hovered = true), onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false), onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer( child: AnimatedContainer(
duration: FaiMotion.base, duration: ChainMotion.base,
curve: FaiMotion.easing, curve: ChainMotion.easing,
transform: Matrix4.translationValues(0, _hovered ? -2 : 0, 0), transform: Matrix4.translationValues(0, _hovered ? -2 : 0, 0),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
boxShadow: _hovered ? FaiElevation.medium(theme.brightness) : null, boxShadow: _hovered ? ChainElevation.medium(theme.brightness) : null,
), ),
child: Material( child: Material(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
child: InkWell( child: InkWell(
onTap: () => _DocReaderSheet.show(context, entry), onTap: () => _DocReaderSheet.show(context, entry),
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
child: Container( child: Container(
padding: const EdgeInsets.all(FaiSpace.lg), padding: const EdgeInsets.all(ChainSpace.lg),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(entry.icon, size: 22, color: theme.colorScheme.primary), Icon(entry.icon, size: 22, color: theme.colorScheme.primary),
const SizedBox(width: FaiSpace.lg), const SizedBox(width: ChainSpace.lg),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1159,7 +1159,7 @@ class _DocReaderSheet extends StatefulWidget {
backgroundColor: Theme.of(context).colorScheme.surfaceContainer, backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
elevation: 8, elevation: 8,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(FaiRadius.md)), borderRadius: BorderRadius.vertical(top: Radius.circular(ChainRadius.md)),
), ),
builder: (_) => _DocReaderSheet(entry: entry), builder: (_) => _DocReaderSheet(entry: entry),
); );
@ -1191,7 +1191,7 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
// the .dart_tool asset manifest can be inconsistent with // the .dart_tool asset manifest can be inconsistent with
// the compiled binary surface "rebuild Studio" as a // the compiled binary surface "rebuild Studio" as a
// first-class hint rather than just dumping the raw // first-class hint rather than just dumping the raw
// PlatformException string into FaiErrorBox. // PlatformException string into ChainErrorBox.
try { try {
return await rootBundle.loadString(localised); return await rootBundle.loadString(localised);
} catch (firstErr) { } catch (firstErr) {
@ -1219,7 +1219,7 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
child: Container( child: Container(
width: 40, width: 40,
height: 4, height: 4,
@ -1231,10 +1231,10 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.xxl, ChainSpace.xxl,
FaiSpace.sm, ChainSpace.sm,
FaiSpace.lg, ChainSpace.lg,
FaiSpace.sm, ChainSpace.sm,
), ),
child: Row( child: Row(
children: [ children: [
@ -1243,7 +1243,7 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
size: 20, size: 20,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Text( child: Text(
widget.entry.title(l), widget.entry.title(l),
@ -1268,7 +1268,7 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
builder: (context, snap) { builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) { if (snap.connectionState == ConnectionState.waiting) {
return const Padding( return const Padding(
padding: EdgeInsets.all(FaiSpace.xxl), padding: EdgeInsets.all(ChainSpace.xxl),
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
); );
} }
@ -1276,7 +1276,7 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
final err = snap.error; final err = snap.error;
final structured = err is _DocReaderError ? err : null; final structured = err is _DocReaderError ? err : null;
return Padding( return Padding(
padding: const EdgeInsets.all(FaiSpace.xxl), padding: const EdgeInsets.all(ChainSpace.xxl),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -1286,21 +1286,21 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
color: theme.colorScheme.error, color: theme.colorScheme.error,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (structured != null) if (structured != null)
FaiErrorBox( ChainErrorBox(
text: structured.localizedBody(l), text: structured.localizedBody(l),
isError: true, isError: true,
maxHeight: 200, maxHeight: 200,
) )
else else
FaiErrorBox( ChainErrorBox(
error: err, error: err,
isError: true, isError: true,
maxHeight: 200, maxHeight: 200,
), ),
if (structured != null) ...[ if (structured != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.open_in_new, size: 16), icon: const Icon(Icons.open_in_new, size: 16),
label: Text(structured.forgejoUrl), label: Text(structured.forgejoUrl),
@ -1314,14 +1314,14 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
} }
return Markdown( return Markdown(
data: snap.data ?? '', data: snap.data ?? '',
padding: const EdgeInsets.all(FaiSpace.xxl), padding: const EdgeInsets.all(ChainSpace.xxl),
selectable: true, selectable: true,
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
if (href != null && href.isNotEmpty) { if (href != null && href.isNotEmpty) {
await SystemActions.openInOs(href); await SystemActions.openInOs(href);
} }
}, },
styleSheet: FaiTheme.markdownStyle(theme), styleSheet: ChainTheme.markdownStyle(theme),
); );
}, },
), ),

View file

@ -5,8 +5,8 @@ import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'tokens.dart'; import 'tokens.dart';
class FaiTheme { class ChainTheme {
FaiTheme._(); ChainTheme._();
/// Bundled font families. Declared in pubspec.yaml `fonts:` and /// Bundled font families. Declared in pubspec.yaml `fonts:` and
/// shipped as TTF assets under `assets/fonts/` NOT fetched at /// shipped as TTF assets under `assets/fonts/` NOT fetched at
@ -79,7 +79,7 @@ class FaiTheme {
} }
/// JetBrains Mono for monospaced technical content. Used by /// JetBrains Mono for monospaced technical content. Used by
/// FaiMono helper. Only Regular (400) and Medium (500) weights /// ChainMono helper. Only Regular (400) and Medium (500) weights
/// are bundled heavier requests fall back to the nearest. /// are bundled heavier requests fall back to the nearest.
static TextStyle mono({ static TextStyle mono({
double size = 12, double size = 12,
@ -125,25 +125,25 @@ class FaiTheme {
).copyWith(backgroundColor: Colors.transparent), ).copyWith(backgroundColor: Colors.transparent),
codeblockDecoration: BoxDecoration( codeblockDecoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
codeblockPadding: const EdgeInsets.all(FaiSpace.md), codeblockPadding: const EdgeInsets.all(ChainSpace.md),
blockquote: theme.textTheme.bodyMedium?.copyWith( blockquote: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
blockquoteDecoration: BoxDecoration( blockquoteDecoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border( border: Border(
left: BorderSide(color: theme.colorScheme.primary, width: 3), left: BorderSide(color: theme.colorScheme.primary, width: 3),
), ),
), ),
blockquotePadding: const EdgeInsets.fromLTRB( blockquotePadding: const EdgeInsets.fromLTRB(
FaiSpace.md, ChainSpace.md,
FaiSpace.sm, ChainSpace.sm,
FaiSpace.sm, ChainSpace.sm,
FaiSpace.sm, ChainSpace.sm,
), ),
tableCellsDecoration: BoxDecoration( tableCellsDecoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
@ -164,32 +164,32 @@ class FaiTheme {
static ThemeData dark() { static ThemeData dark() {
final scheme = ColorScheme( final scheme = ColorScheme(
brightness: Brightness.dark, brightness: Brightness.dark,
primary: FaiColors.accent, primary: ChainColors.accent,
onPrimary: FaiColors.black, onPrimary: ChainColors.black,
primaryContainer: FaiColors.accentMuted, primaryContainer: ChainColors.accentMuted,
onPrimaryContainer: FaiColors.textStrong, onPrimaryContainer: ChainColors.textStrong,
secondary: FaiColors.accentDeep, secondary: ChainColors.accentDeep,
onSecondary: FaiColors.black, onSecondary: ChainColors.black,
secondaryContainer: FaiColors.surfaceHigh, secondaryContainer: ChainColors.surfaceHigh,
onSecondaryContainer: FaiColors.text, onSecondaryContainer: ChainColors.text,
tertiary: FaiColors.accent, tertiary: ChainColors.accent,
onTertiary: FaiColors.black, onTertiary: ChainColors.black,
tertiaryContainer: FaiColors.surfaceHigh, tertiaryContainer: ChainColors.surfaceHigh,
onTertiaryContainer: FaiColors.text, onTertiaryContainer: ChainColors.text,
error: FaiColors.danger, error: ChainColors.danger,
onError: FaiColors.textStrong, onError: ChainColors.textStrong,
errorContainer: const Color(0xFF3F1518), errorContainer: const Color(0xFF3F1518),
onErrorContainer: const Color(0xFFFCA5A5), onErrorContainer: const Color(0xFFFCA5A5),
surface: FaiColors.black, surface: ChainColors.black,
onSurface: FaiColors.text, onSurface: ChainColors.text,
onSurfaceVariant: FaiColors.muted, onSurfaceVariant: ChainColors.muted,
outline: FaiColors.border, outline: ChainColors.border,
outlineVariant: FaiColors.border, outlineVariant: ChainColors.border,
surfaceContainerLowest: FaiColors.black, surfaceContainerLowest: ChainColors.black,
surfaceContainerLow: FaiColors.surface, surfaceContainerLow: ChainColors.surface,
surfaceContainer: FaiColors.surface, surfaceContainer: ChainColors.surface,
surfaceContainerHigh: FaiColors.surfaceHigh, surfaceContainerHigh: ChainColors.surfaceHigh,
surfaceContainerHighest: FaiColors.surfaceHigh, surfaceContainerHighest: ChainColors.surfaceHigh,
); );
return _build(scheme); return _build(scheme);
} }
@ -197,32 +197,32 @@ class FaiTheme {
static ThemeData light() { static ThemeData light() {
final scheme = ColorScheme( final scheme = ColorScheme(
brightness: Brightness.light, brightness: Brightness.light,
primary: FaiColors.accentDeep, primary: ChainColors.accentDeep,
onPrimary: FaiColors.lightSurface, onPrimary: ChainColors.lightSurface,
primaryContainer: const Color(0xFFE0F2FE), primaryContainer: const Color(0xFFE0F2FE),
onPrimaryContainer: FaiColors.accentMuted, onPrimaryContainer: ChainColors.accentMuted,
secondary: FaiColors.accentMuted, secondary: ChainColors.accentMuted,
onSecondary: FaiColors.lightSurface, onSecondary: ChainColors.lightSurface,
secondaryContainer: FaiColors.lightSurfaceHigh, secondaryContainer: ChainColors.lightSurfaceHigh,
onSecondaryContainer: FaiColors.lightText, onSecondaryContainer: ChainColors.lightText,
tertiary: FaiColors.accentDeep, tertiary: ChainColors.accentDeep,
onTertiary: FaiColors.lightSurface, onTertiary: ChainColors.lightSurface,
tertiaryContainer: const Color(0xFFE0F2FE), tertiaryContainer: const Color(0xFFE0F2FE),
onTertiaryContainer: FaiColors.accentMuted, onTertiaryContainer: ChainColors.accentMuted,
error: FaiColors.danger, error: ChainColors.danger,
onError: FaiColors.lightSurface, onError: ChainColors.lightSurface,
errorContainer: const Color(0xFFFEE2E2), errorContainer: const Color(0xFFFEE2E2),
onErrorContainer: const Color(0xFF7F1D1D), onErrorContainer: const Color(0xFF7F1D1D),
surface: FaiColors.lightCanvas, surface: ChainColors.lightCanvas,
onSurface: FaiColors.lightText, onSurface: ChainColors.lightText,
onSurfaceVariant: FaiColors.lightMuted, onSurfaceVariant: ChainColors.lightMuted,
outline: FaiColors.lightBorder, outline: ChainColors.lightBorder,
outlineVariant: FaiColors.lightBorder, outlineVariant: ChainColors.lightBorder,
surfaceContainerLowest: FaiColors.lightCanvas, surfaceContainerLowest: ChainColors.lightCanvas,
surfaceContainerLow: FaiColors.lightSurface, surfaceContainerLow: ChainColors.lightSurface,
surfaceContainer: FaiColors.lightSurface, surfaceContainer: ChainColors.lightSurface,
surfaceContainerHigh: FaiColors.lightSurfaceHigh, surfaceContainerHigh: ChainColors.lightSurfaceHigh,
surfaceContainerHighest: FaiColors.lightSurfaceHigh, surfaceContainerHighest: ChainColors.lightSurfaceHigh,
); );
return _build(scheme); return _build(scheme);
} }
@ -256,7 +256,7 @@ class FaiTheme {
color: scheme.surfaceContainer, color: scheme.surfaceContainer,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
side: BorderSide(color: scheme.outlineVariant), side: BorderSide(color: scheme.outlineVariant),
), ),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@ -266,14 +266,14 @@ class FaiTheme {
backgroundColor: scheme.primary, backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary, foregroundColor: scheme.onPrimary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.lg, horizontal: ChainSpace.lg,
vertical: FaiSpace.md, vertical: ChainSpace.md,
), ),
textStyle: textTheme.labelMedium, textStyle: textTheme.labelMedium,
animationDuration: FaiMotion.fast, animationDuration: ChainMotion.fast,
), ),
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
@ -281,14 +281,14 @@ class FaiTheme {
foregroundColor: scheme.onSurface, foregroundColor: scheme.onSurface,
side: BorderSide(color: scheme.outline), side: BorderSide(color: scheme.outline),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.lg, horizontal: ChainSpace.lg,
vertical: FaiSpace.md, vertical: ChainSpace.md,
), ),
textStyle: textTheme.labelMedium, textStyle: textTheme.labelMedium,
animationDuration: FaiMotion.fast, animationDuration: ChainMotion.fast,
), ),
), ),
iconButtonTheme: IconButtonThemeData( iconButtonTheme: IconButtonThemeData(
@ -323,7 +323,7 @@ class FaiTheme {
), ),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
), ),
), ),
); );

View file

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
/// Colour palette. Single strong accent (ChIn Cyan) plus neutral /// Colour palette. Single strong accent (ChIn Cyan) plus neutral
/// greys. Status colours stay restrained no candy palette. /// greys. Status colours stay restrained no candy palette.
class FaiColors { class ChainColors {
FaiColors._(); ChainColors._();
// Brand accent. Slightly cooler / more saturated than the // Brand accent. Slightly cooler / more saturated than the
// initial #1E3A8A this reads more as "deep tech, alive" // initial #1E3A8A this reads more as "deep tech, alive"
@ -43,8 +43,8 @@ class FaiColors {
/// Spacing scale. Stick to multiples of 4. Keep the vocabulary /// Spacing scale. Stick to multiples of 4. Keep the vocabulary
/// small six steps cover everything. /// small six steps cover everything.
class FaiSpace { class ChainSpace {
FaiSpace._(); ChainSpace._();
static const xs = 4.0; static const xs = 4.0;
static const sm = 8.0; static const sm = 8.0;
static const md = 12.0; static const md = 12.0;
@ -55,8 +55,8 @@ class FaiSpace {
} }
/// Border radii. Two sizes only. /// Border radii. Two sizes only.
class FaiRadius { class ChainRadius {
FaiRadius._(); ChainRadius._();
static const sm = 6.0; static const sm = 6.0;
static const md = 10.0; static const md = 10.0;
} }
@ -66,8 +66,8 @@ class FaiRadius {
/// Brightness-aware: dark themes lean on deeper, lower-alpha /// Brightness-aware: dark themes lean on deeper, lower-alpha
/// shadows (they read as ambient occlusion against near-black /// shadows (they read as ambient occlusion against near-black
/// surfaces); light themes use softer, slightly larger spreads. /// surfaces); light themes use softer, slightly larger spreads.
class FaiElevation { class ChainElevation {
FaiElevation._(); ChainElevation._();
/// Resting elevation for cards and sheets. Just enough to lift /// Resting elevation for cards and sheets. Just enough to lift
/// a surface off the canvas without announcing itself. /// a surface off the canvas without announcing itself.
@ -106,8 +106,8 @@ class FaiElevation {
} }
/// Animation durations. Keep them under 300ms for power-user feel. /// Animation durations. Keep them under 300ms for power-user feel.
class FaiMotion { class ChainMotion {
FaiMotion._(); ChainMotion._();
static const fast = Duration(milliseconds: 120); static const fast = Duration(milliseconds: 120);
static const base = Duration(milliseconds: 200); static const base = Duration(milliseconds: 200);
static const slow = Duration(milliseconds: 320); static const slow = Duration(milliseconds: 320);

View file

@ -1,6 +1,6 @@
// Recovery affordance shown when Studio cannot locate the `fai` // Recovery affordance shown when Studio cannot locate the `fai`
// binary on the machine. Replaces the old CLI-jargon dead-end // binary on the machine. Replaces the old CLI-jargon dead-end
// ("set FAI_BIN to its full path") with two acts a non-CLI // ("set CHAIN_BIN to its full path") with two acts a non-CLI
// operator can actually take: // operator can actually take:
// //
// 1. Locate the binary with a native file picker, persisting // 1. Locate the binary with a native file picker, persisting
@ -14,26 +14,26 @@ import '../data/system_actions.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiBinaryRecovery extends StatelessWidget { class ChainBinaryRecovery extends StatelessWidget {
/// Fired after the operator successfully picks a `fai` binary, /// Fired after the operator successfully picks a `fai` binary,
/// so the host can retry whatever action hit the missing /// so the host can retry whatever action hit the missing
/// binary (e.g. re-run the daemon start / status probe). /// binary (e.g. re-run the daemon start / status probe).
final VoidCallback? onLocated; final VoidCallback? onLocated;
const FaiBinaryRecovery({super.key, this.onLocated}); const ChainBinaryRecovery({super.key, this.onLocated});
Future<void> _locate(BuildContext context) async { Future<void> _locate(BuildContext context) async {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
final picked = await FilePicker.pickFiles( final picked = await FilePicker.pickFiles(
dialogTitle: l.faiBinaryPickerTitle, dialogTitle: l.chainBinaryPickerTitle,
); );
final path = picked?.files.single.path; final path = picked?.files.single.path;
if (path == null) return; if (path == null) return;
final ok = await SystemActions.setFaiBinaryPath(path); final ok = await SystemActions.setFaiBinaryPath(path);
messenger.showSnackBar( messenger.showSnackBar(
SnackBar( SnackBar(
content: Text(ok ? l.faiBinarySetOk(path) : l.faiBinaryNotFound), content: Text(ok ? l.chainBinarySetOk(path) : l.chainBinaryNotFound),
), ),
); );
if (ok) onLocated?.call(); if (ok) onLocated?.call();
@ -45,10 +45,10 @@ class FaiBinaryRecovery extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.errorContainer.withValues(alpha: 0.4), color: theme.colorScheme.errorContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all( border: Border.all(
color: theme.colorScheme.error.withValues(alpha: 0.4), color: theme.colorScheme.error.withValues(alpha: 0.4),
), ),
@ -57,34 +57,34 @@ class FaiBinaryRecovery extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
l.faiBinaryNotFound, l.chainBinaryNotFound,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: theme.colorScheme.error, color: theme.colorScheme.error,
), ),
), ),
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
Text( Text(
l.faiBinaryNotFoundHint, l.chainBinaryNotFoundHint,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Wrap( Wrap(
spacing: FaiSpace.sm, spacing: ChainSpace.sm,
runSpacing: FaiSpace.sm, runSpacing: ChainSpace.sm,
children: [ children: [
FilledButton.tonalIcon( FilledButton.tonalIcon(
onPressed: () => _locate(context), onPressed: () => _locate(context),
icon: const Icon(Icons.folder_open, size: 16), icon: const Icon(Icons.folder_open, size: 16),
label: Text(l.faiBinaryLocateButton), label: Text(l.chainBinaryLocateButton),
), ),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => onPressed: () =>
SystemActions.openInOs(kFaiInstallDocsUrl), SystemActions.openInOs(kFaiInstallDocsUrl),
icon: const Icon(Icons.open_in_new, size: 16), icon: const Icon(Icons.open_in_new, size: 16),
label: Text(l.faiBinaryInstallDocsButton), label: Text(l.chainBinaryInstallDocsButton),
), ),
], ],
), ),

View file

@ -1,11 +1,11 @@
// FaiCard flat card with subtle accent gradient on the top // ChainCard flat card with subtle accent gradient on the top
// border. Replaces Material's default Card for the Ch∆In look. // border. Replaces Material's default Card for the Ch∆In look.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiCard extends StatelessWidget { class ChainCard extends StatelessWidget {
/// Card body. Padding is applied internally; pass plain content. /// Card body. Padding is applied internally; pass plain content.
final Widget child; final Widget child;
@ -18,15 +18,15 @@ class FaiCard extends StatelessWidget {
/// Used by status rows (live event, healthy service). /// Used by status rows (live event, healthy service).
final Color? accentLeft; final Color? accentLeft;
/// Internal padding. Defaults to [FaiSpace.lg]. /// Internal padding. Defaults to [ChainSpace.lg].
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
const FaiCard({ const ChainCard({
super.key, super.key,
required this.child, required this.child,
this.accentTop = false, this.accentTop = false,
this.accentLeft, this.accentLeft,
this.padding = const EdgeInsets.all(FaiSpace.lg), this.padding = const EdgeInsets.all(ChainSpace.lg),
}); });
@override @override
@ -35,7 +35,7 @@ class FaiCard extends StatelessWidget {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
@ -61,8 +61,8 @@ class FaiCard extends StatelessWidget {
), ),
if (accentLeft != null) if (accentLeft != null)
Positioned( Positioned(
top: FaiSpace.md, top: ChainSpace.md,
bottom: FaiSpace.md, bottom: ChainSpace.md,
left: 0, left: 0,
width: 3, width: 3,
child: DecoratedBox( child: DecoratedBox(

View file

@ -1,4 +1,4 @@
// FaiDataRow Linear-style dense list row used by the audit // ChainDataRow Linear-style dense list row used by the audit
// stream. Hover-elevation, accent-coloured leading stripe, mono // stream. Hover-elevation, accent-coloured leading stripe, mono
// timestamp, expressive type badge. // timestamp, expressive type badge.
@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiDataRow extends StatefulWidget { class ChainDataRow extends StatefulWidget {
/// Coloured leading stripe (4px). Used to encode event type /// Coloured leading stripe (4px). Used to encode event type
/// (success / warning / failure) at a glance. /// (success / warning / failure) at a glance.
final Color accent; final Color accent;
@ -27,7 +27,7 @@ class FaiDataRow extends StatefulWidget {
/// Optional tap handler. /// Optional tap handler.
final VoidCallback? onTap; final VoidCallback? onTap;
const FaiDataRow({ const ChainDataRow({
super.key, super.key,
required this.accent, required this.accent,
required this.leading, required this.leading,
@ -38,10 +38,10 @@ class FaiDataRow extends StatefulWidget {
}); });
@override @override
State<FaiDataRow> createState() => _FaiDataRowState(); State<ChainDataRow> createState() => _FaiDataRowState();
} }
class _FaiDataRowState extends State<FaiDataRow> { class _FaiDataRowState extends State<ChainDataRow> {
bool _hovered = false; bool _hovered = false;
@override @override
@ -57,12 +57,12 @@ class _FaiDataRowState extends State<FaiDataRow> {
onTap: widget.onTap, onTap: widget.onTap,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: AnimatedContainer( child: AnimatedContainer(
duration: FaiMotion.fast, duration: ChainMotion.fast,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _hovered color: _hovered
? theme.colorScheme.surfaceContainerHigh ? theme.colorScheme.surfaceContainerHigh
: theme.colorScheme.surfaceContainer, : theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all( border: Border.all(
color: _hovered color: _hovered
? theme.colorScheme.outline ? theme.colorScheme.outline
@ -70,8 +70,8 @@ class _FaiDataRowState extends State<FaiDataRow> {
), ),
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.md, horizontal: ChainSpace.md,
vertical: FaiSpace.sm, vertical: ChainSpace.sm,
), ),
child: Row( child: Row(
children: [ children: [
@ -83,12 +83,12 @@ class _FaiDataRowState extends State<FaiDataRow> {
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
SizedBox( SizedBox(
width: 88, width: 88,
child: Text( child: Text(
widget.leading, widget.leading,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -98,7 +98,7 @@ class _FaiDataRowState extends State<FaiDataRow> {
width: 200, width: 200,
child: Text( child: Text(
widget.title, widget.title,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 12, size: 12,
weight: FontWeight.w500, weight: FontWeight.w500,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
@ -116,10 +116,10 @@ class _FaiDataRowState extends State<FaiDataRow> {
), ),
if (widget.trailing != null) if (widget.trailing != null)
Padding( Padding(
padding: const EdgeInsets.only(left: FaiSpace.md), padding: const EdgeInsets.only(left: ChainSpace.md),
child: Text( child: Text(
widget.trailing!, widget.trailing!,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),

View file

@ -1,4 +1,4 @@
// FaiDeltaMark the ChIn signature element. A precise triangle // ChainDeltaMark the ChIn signature element. A precise triangle
// () drawn from primitives, not a font glyph. Three states: // () drawn from primitives, not a font glyph. Three states:
// //
// - idle: static, accent colour, soft glow // - idle: static, accent colour, soft glow
@ -12,25 +12,25 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum FaiDeltaMode { idle, live, busy } enum ChainDeltaMode { idle, live, busy }
class FaiDeltaMark extends StatefulWidget { class ChainDeltaMark extends StatefulWidget {
final FaiDeltaMode mode; final ChainDeltaMode mode;
final double size; final double size;
final Color color; final Color color;
const FaiDeltaMark({ const ChainDeltaMark({
super.key, super.key,
required this.color, required this.color,
this.mode = FaiDeltaMode.idle, this.mode = ChainDeltaMode.idle,
this.size = 36, this.size = 36,
}); });
@override @override
State<FaiDeltaMark> createState() => _FaiDeltaMarkState(); State<ChainDeltaMark> createState() => _FaiDeltaMarkState();
} }
class _FaiDeltaMarkState extends State<FaiDeltaMark> class _FaiDeltaMarkState extends State<ChainDeltaMark>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final AnimationController _ctrl; late final AnimationController _ctrl;
@ -44,13 +44,13 @@ class _FaiDeltaMarkState extends State<FaiDeltaMark>
_restart(); _restart();
} }
Duration _durationFor(FaiDeltaMode m) { Duration _durationFor(ChainDeltaMode m) {
switch (m) { switch (m) {
case FaiDeltaMode.idle: case ChainDeltaMode.idle:
return const Duration(seconds: 1); return const Duration(seconds: 1);
case FaiDeltaMode.live: case ChainDeltaMode.live:
return const Duration(milliseconds: 1600); return const Duration(milliseconds: 1600);
case FaiDeltaMode.busy: case ChainDeltaMode.busy:
return const Duration(seconds: 4); return const Duration(seconds: 4);
} }
} }
@ -58,21 +58,21 @@ class _FaiDeltaMarkState extends State<FaiDeltaMark>
void _restart() { void _restart() {
_ctrl.duration = _durationFor(widget.mode); _ctrl.duration = _durationFor(widget.mode);
switch (widget.mode) { switch (widget.mode) {
case FaiDeltaMode.idle: case ChainDeltaMode.idle:
_ctrl.stop(); _ctrl.stop();
_ctrl.value = 0; _ctrl.value = 0;
break; break;
case FaiDeltaMode.live: case ChainDeltaMode.live:
_ctrl.repeat(reverse: true); _ctrl.repeat(reverse: true);
break; break;
case FaiDeltaMode.busy: case ChainDeltaMode.busy:
_ctrl.repeat(); _ctrl.repeat();
break; break;
} }
} }
@override @override
void didUpdateWidget(covariant FaiDeltaMark old) { void didUpdateWidget(covariant ChainDeltaMark old) {
super.didUpdateWidget(old); super.didUpdateWidget(old);
if (old.mode != widget.mode) _restart(); if (old.mode != widget.mode) _restart();
} }
@ -103,7 +103,7 @@ class _FaiDeltaMarkState extends State<FaiDeltaMark>
class _DeltaPainter extends CustomPainter { class _DeltaPainter extends CustomPainter {
final Color color; final Color color;
final FaiDeltaMode mode; final ChainDeltaMode mode;
final double t; final double t;
_DeltaPainter({required this.color, required this.mode, required this.t}); _DeltaPainter({required this.color, required this.mode, required this.t});
@ -115,12 +115,12 @@ class _DeltaPainter extends CustomPainter {
// In live mode: scale the whole mark between 0.85 and 1.15 // In live mode: scale the whole mark between 0.85 and 1.15
// so the pulse is visible without staring. Idle stays at 1.0. // so the pulse is visible without staring. Idle stays at 1.0.
final scale = mode == FaiDeltaMode.live ? 0.85 + 0.30 * t : 1.0; final scale = mode == ChainDeltaMode.live ? 0.85 + 0.30 * t : 1.0;
final r = size.width * 0.36 * scale; final r = size.width * 0.36 * scale;
canvas.save(); canvas.save();
canvas.translate(cx, cy); canvas.translate(cx, cy);
if (mode == FaiDeltaMode.busy) { if (mode == ChainDeltaMode.busy) {
canvas.rotate(t * 2 * pi); canvas.rotate(t * 2 * pi);
} }
@ -133,8 +133,8 @@ class _DeltaPainter extends CustomPainter {
// Glow halo. In live mode the glow ring grows with the pulse // Glow halo. In live mode the glow ring grows with the pulse
// and the alpha breathes harder. // and the alpha breathes harder.
if (mode != FaiDeltaMode.idle) { if (mode != ChainDeltaMode.idle) {
final pulseStrength = mode == FaiDeltaMode.live ? t : 1.0; final pulseStrength = mode == ChainDeltaMode.live ? t : 1.0;
final glow = Paint() final glow = Paint()
..color = color.withValues(alpha: 0.30 + 0.40 * pulseStrength) ..color = color.withValues(alpha: 0.30 + 0.40 * pulseStrength)
..maskFilter = MaskFilter.blur(BlurStyle.normal, 8 + 14 * pulseStrength) ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8 + 14 * pulseStrength)
@ -143,7 +143,7 @@ class _DeltaPainter extends CustomPainter {
} }
// Filled triangle, brighter on the up-beat for live mode. // Filled triangle, brighter on the up-beat for live mode.
final fillAlpha = mode == FaiDeltaMode.live ? 0.15 + 0.20 * t : 0.18; final fillAlpha = mode == ChainDeltaMode.live ? 0.15 + 0.20 * t : 0.18;
final fill = Paint() final fill = Paint()
..color = color.withValues(alpha: fillAlpha) ..color = color.withValues(alpha: fillAlpha)
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
@ -158,7 +158,7 @@ class _DeltaPainter extends CustomPainter {
canvas.drawPath(path, stroke); canvas.drawPath(path, stroke);
// Inner accent dot. // Inner accent dot.
final dotAlpha = mode == FaiDeltaMode.live ? 0.55 + 0.45 * t : 1.0; final dotAlpha = mode == ChainDeltaMode.live ? 0.55 + 0.45 * t : 1.0;
final dot = Paint() final dot = Paint()
..color = color.withValues(alpha: dotAlpha) ..color = color.withValues(alpha: dotAlpha)
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;

View file

@ -1,4 +1,4 @@
// FaiEmptyState gracious empty / loading / error placeholder. // ChainEmptyState gracious empty / loading / error placeholder.
// Replaces "(no data)" strings with one tone, one icon, one tip. // Replaces "(no data)" strings with one tone, one icon, one tip.
// //
// The blank surface is where an app most easily feels unfinished, // The blank surface is where an app most easily feels unfinished,
@ -11,9 +11,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
import 'fai_delta_mark.dart'; import 'chain_delta_mark.dart';
class FaiEmptyState extends StatelessWidget { class ChainEmptyState extends StatelessWidget {
final IconData icon; final IconData icon;
final String title; final String title;
final String? hint; final String? hint;
@ -23,7 +23,7 @@ class FaiEmptyState extends StatelessWidget {
/// for connection-failure variants. /// for connection-failure variants.
final Color? iconColor; final Color? iconColor;
const FaiEmptyState({ const ChainEmptyState({
super.key, super.key,
required this.icon, required this.icon,
required this.title, required this.title,
@ -40,7 +40,7 @@ class FaiEmptyState extends StatelessWidget {
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440), constraints: const BoxConstraints(maxWidth: 440),
child: Padding( child: Padding(
padding: const EdgeInsets.all(FaiSpace.xxl), padding: const EdgeInsets.all(ChainSpace.xxl),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -57,7 +57,7 @@ class FaiEmptyState extends StatelessWidget {
children: [ children: [
Opacity( Opacity(
opacity: 0.10, opacity: 0.10,
child: FaiDeltaMark( child: ChainDeltaMark(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
size: 112, size: 112,
), ),
@ -71,14 +71,14 @@ class FaiEmptyState extends StatelessWidget {
border: Border.all( border: Border.all(
color: color.withValues(alpha: 0.18), color: color.withValues(alpha: 0.18),
), ),
boxShadow: FaiElevation.low(theme.brightness), boxShadow: ChainElevation.low(theme.brightness),
), ),
child: Icon(icon, size: 26, color: color), child: Icon(icon, size: 26, color: color),
), ),
], ],
), ),
), ),
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
Text( Text(
title, title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -87,7 +87,7 @@ class FaiEmptyState extends StatelessWidget {
), ),
), ),
if (hint != null) ...[ if (hint != null) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
hint!, hint!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -98,7 +98,7 @@ class FaiEmptyState extends StatelessWidget {
), ),
], ],
if (action != null) ...[ if (action != null) ...[
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
action!, action!,
], ],
], ],

View file

@ -1,4 +1,4 @@
// FaiEnBadge small "[EN]" pill shown next to text that came // ChainEnBadge small "[EN]" pill shown next to text that came
// from outside Studio's localization pipeline (MCP server tool // from outside Studio's localization pipeline (MCP server tool
// names, n8n endpoint descriptions, native LLM responses). // names, n8n endpoint descriptions, native LLM responses).
// //
@ -14,21 +14,21 @@ import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiEnBadge extends StatelessWidget { class ChainEnBadge extends StatelessWidget {
/// When true, the badge renders. When false (e.g. the active /// When true, the badge renders. When false (e.g. the active
/// locale already is English), it returns /// locale already is English), it returns
/// `SizedBox.shrink()` so callers can drop it inline without /// `SizedBox.shrink()` so callers can drop it inline without
/// guarding visibility themselves. /// guarding visibility themselves.
final bool visible; final bool visible;
const FaiEnBadge({super.key, required this.visible}); const ChainEnBadge({super.key, required this.visible});
/// Convenience constructor: derives `visible` from the active /// Convenience constructor: derives `visible` from the active
/// Localizations locale. Use this from inside a Build method /// Localizations locale. Use this from inside a Build method
/// where you already have a BuildContext. /// where you already have a BuildContext.
factory FaiEnBadge.forContext(BuildContext context) { factory ChainEnBadge.forContext(BuildContext context) {
final lang = Localizations.localeOf(context).languageCode; final lang = Localizations.localeOf(context).languageCode;
return FaiEnBadge(visible: lang != 'en'); return ChainEnBadge(visible: lang != 'en');
} }
@override @override
@ -42,7 +42,7 @@ class FaiEnBadge extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest, color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Text( child: Text(

View file

@ -1,4 +1,4 @@
// FaiErrorBox selectable monospace error / output block with // ChainErrorBox selectable monospace error / output block with
// a small copy-to-clipboard button in the top-right corner. // a small copy-to-clipboard button in the top-right corner.
// //
// Used wherever the operator might want to paste daemon stderr // Used wherever the operator might want to paste daemon stderr
@ -15,7 +15,7 @@ import '../l10n/app_localizations.dart';
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiErrorBox extends StatefulWidget { class ChainErrorBox extends StatefulWidget {
/// The text the operator wants to read (and copy). Rendered /// The text the operator wants to read (and copy). Rendered
/// monospace, selectable, multi-line. Ignored when [error] is /// monospace, selectable, multi-line. Ignored when [error] is
/// supplied. /// supplied.
@ -41,7 +41,7 @@ class FaiErrorBox extends StatefulWidget {
/// trailers otherwise. /// trailers otherwise.
final Object? error; final Object? error;
const FaiErrorBox({ const ChainErrorBox({
super.key, super.key,
this.text = '', this.text = '',
this.isError = false, this.isError = false,
@ -49,14 +49,14 @@ class FaiErrorBox extends StatefulWidget {
this.error, this.error,
}) : assert( }) : assert(
text != '' || error != null, text != '' || error != null,
'FaiErrorBox needs either text or error', 'ChainErrorBox needs either text or error',
); );
@override @override
State<FaiErrorBox> createState() => _FaiErrorBoxState(); State<ChainErrorBox> createState() => _FaiErrorBoxState();
} }
class _FaiErrorBoxState extends State<FaiErrorBox> { class _FaiErrorBoxState extends State<ChainErrorBox> {
bool _justCopied = false; bool _justCopied = false;
bool _detailExpanded = false; bool _detailExpanded = false;
@ -86,14 +86,14 @@ class _FaiErrorBoxState extends State<FaiErrorBox> {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.sm, ChainSpace.sm,
FaiSpace.xs, ChainSpace.xs,
FaiSpace.xs, ChainSpace.xs,
FaiSpace.sm, ChainSpace.sm,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: accent.withValues(alpha: 0.4)), border: Border.all(color: accent.withValues(alpha: 0.4)),
), ),
child: Column( child: Column(
@ -133,7 +133,7 @@ class _FaiErrorBoxState extends State<FaiErrorBox> {
), ),
], ],
if (friendly.detail.isNotEmpty) ...[ if (friendly.detail.isNotEmpty) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
InkWell( InkWell(
onTap: () => setState(() { onTap: () => setState(() {
_detailExpanded = !_detailExpanded; _detailExpanded = !_detailExpanded;
@ -159,7 +159,7 @@ class _FaiErrorBoxState extends State<FaiErrorBox> {
), ),
), ),
if (_detailExpanded) ...[ if (_detailExpanded) ...[
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: widget.maxHeight ?? double.infinity, maxHeight: widget.maxHeight ?? double.infinity,
@ -168,7 +168,7 @@ class _FaiErrorBoxState extends State<FaiErrorBox> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: SelectableText( child: SelectableText(
friendly.detail, friendly.detail,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -187,7 +187,7 @@ class _FaiErrorBoxState extends State<FaiErrorBox> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: SelectableText( child: SelectableText(
widget.text, widget.text,
style: FaiTheme.mono(size: 11, color: fg), style: ChainTheme.mono(size: 11, color: fg),
), ),
), ),
), ),

View file

@ -1,4 +1,4 @@
// FaiFlowOutput type-aware renderer for one entry in // ChainFlowOutput type-aware renderer for one entry in
// `runSavedFlow`'s output map. Reads `FlowOutput` and dispatches // `runSavedFlow`'s output map. Reads `FlowOutput` and dispatches
// to the right widget: // to the right widget:
// //
@ -27,10 +27,10 @@ import '../l10n/app_localizations.dart';
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiFlowOutput extends StatelessWidget { class ChainFlowOutput extends StatelessWidget {
final FlowOutput output; final FlowOutput output;
const FaiFlowOutput({super.key, required this.output}); const ChainFlowOutput({super.key, required this.output});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -75,10 +75,10 @@ class _TextView extends StatelessWidget {
if (_looksLikeMarkdown) { if (_looksLikeMarkdown) {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: MarkdownBody( child: MarkdownBody(
@ -89,16 +89,16 @@ class _TextView extends StatelessWidget {
await SystemActions.openInOs(href); await SystemActions.openInOs(href);
} }
}, },
styleSheet: FaiTheme.markdownStyle(theme), styleSheet: ChainTheme.markdownStyle(theme),
), ),
); );
} }
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText( child: SelectableText(
@ -119,15 +119,15 @@ class _CodeBlock extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: SelectableText( child: SelectableText(
text, text,
style: FaiTheme.mono(size: 12, color: theme.colorScheme.onSurface), style: ChainTheme.mono(size: 12, color: theme.colorScheme.onSurface),
), ),
); );
} }
@ -183,10 +183,10 @@ class _BytesView extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Column( child: Column(
@ -196,11 +196,11 @@ class _BytesView extends StatelessWidget {
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240), constraints: const BoxConstraints(maxHeight: 240),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: Image.memory(bytes, fit: BoxFit.contain), child: Image.memory(bytes, fit: BoxFit.contain),
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
], ],
Text( Text(
'${mimeType.isEmpty ? l.flowsOutputBytesUnknownMime : mimeType}' '${mimeType.isEmpty ? l.flowsOutputBytesUnknownMime : mimeType}'
@ -209,7 +209,7 @@ class _BytesView extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
OutlinedButton.icon( OutlinedButton.icon(
icon: const Icon(Icons.save_alt, size: 16), icon: const Icon(Icons.save_alt, size: 16),
label: Text(l.flowsOutputSaveAs), label: Text(l.flowsOutputSaveAs),
@ -247,10 +247,10 @@ class _FileView extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Column( child: Column(
@ -258,7 +258,7 @@ class _FileView extends StatelessWidget {
children: [ children: [
SelectableText( SelectableText(
uri, uri,
style: FaiTheme.mono(size: 11, color: theme.colorScheme.primary), style: ChainTheme.mono(size: 11, color: theme.colorScheme.primary),
), ),
if (mimeType.isNotEmpty) ...[ if (mimeType.isNotEmpty) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
@ -269,7 +269,7 @@ class _FileView extends StatelessWidget {
), ),
), ),
], ],
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
OutlinedButton.icon( OutlinedButton.icon(
icon: const Icon(Icons.open_in_new, size: 16), icon: const Icon(Icons.open_in_new, size: 16),
label: Text(l.flowsOutputOpen), label: Text(l.flowsOutputOpen),

View file

@ -1,4 +1,4 @@
// FaiLogViewer modal sheet that renders a log file inline with // ChainLogViewer modal sheet that renders a log file inline with
// line numbers, basic syntax colouring and a Copy-all button. // line numbers, basic syntax colouring and a Copy-all button.
// //
// Use [showFaiLogViewer] to open it. Two log shapes are supported // Use [showFaiLogViewer] to open it. Two log shapes are supported
@ -9,7 +9,7 @@
// anything else renders neutral. // anything else renders neutral.
// - tracing text (the daemon's `~/.chain/run/<channel>.log`): // - tracing text (the daemon's `~/.chain/run/<channel>.log`):
// `[INFO]`, `[WARN]`, `[ERROR]`, `[DEBUG]` substrings get the // `[INFO]`, `[WARN]`, `[ERROR]`, `[DEBUG]` substrings get the
// matching tone from the FaiTheme palette. // matching tone from the ChainTheme palette.
// //
// The viewer reads the *last* N lines (default 500) to keep // The viewer reads the *last* N lines (default 500) to keep
// rendering snappy on huge log files. A Refresh button re-reads // rendering snappy on huge log files. A Refresh button re-reads
@ -42,7 +42,7 @@ Future<void> showFaiLogViewer(
return showDialog<void>( return showDialog<void>(
context: context, context: context,
barrierDismissible: true, barrierDismissible: true,
builder: (_) => FaiLogViewer( builder: (_) => ChainLogViewer(
path: path, path: path,
title: title ?? path.split(Platform.pathSeparator).last, title: title ?? path.split(Platform.pathSeparator).last,
tailLines: tailLines, tailLines: tailLines,
@ -72,7 +72,7 @@ Future<void> showFaiConfigViewer(
); );
} }
class FaiLogViewer extends StatefulWidget { class ChainLogViewer extends StatefulWidget {
final String path; final String path;
final String title; final String title;
final int tailLines; final int tailLines;
@ -84,7 +84,7 @@ class FaiLogViewer extends StatefulWidget {
/// (for config / plain-text files). /// (for config / plain-text files).
final bool plain; final bool plain;
const FaiLogViewer({ const ChainLogViewer({
super.key, super.key,
required this.path, required this.path,
required this.title, required this.title,
@ -94,10 +94,10 @@ class FaiLogViewer extends StatefulWidget {
}); });
@override @override
State<FaiLogViewer> createState() => _FaiLogViewerState(); State<ChainLogViewer> createState() => _FaiLogViewerState();
} }
class _FaiLogViewerState extends State<FaiLogViewer> { class _FaiLogViewerState extends State<ChainLogViewer> {
List<String> _lines = const <String>[]; List<String> _lines = const <String>[];
bool _loading = true; bool _loading = true;
bool _justCopied = false; bool _justCopied = false;
@ -141,8 +141,8 @@ class _FaiLogViewerState extends State<FaiLogViewer> {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Dialog( return Dialog(
insetPadding: const EdgeInsets.symmetric( insetPadding: const EdgeInsets.symmetric(
horizontal: FaiSpace.xl, horizontal: ChainSpace.xl,
vertical: FaiSpace.xl, vertical: ChainSpace.xl,
), ),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 980, maxHeight: 720), constraints: const BoxConstraints(maxWidth: 980, maxHeight: 720),
@ -169,7 +169,7 @@ class _FaiLogViewerState extends State<FaiLogViewer> {
: _lines.isEmpty : _lines.isEmpty
? Center( ? Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
child: Text( child: Text(
l.logViewerEmpty, l.logViewerEmpty,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -218,10 +218,10 @@ class _Header extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.lg, ChainSpace.lg,
FaiSpace.md, ChainSpace.md,
FaiSpace.sm, ChainSpace.sm,
FaiSpace.md, ChainSpace.md,
), ),
child: Row( child: Row(
children: [ children: [
@ -230,7 +230,7 @@ class _Header extends StatelessWidget {
size: 18, size: 18,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -246,7 +246,7 @@ class _Header extends StatelessWidget {
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'$path · ${l.logViewerLineCount(lineCount)}', '$path · ${l.logViewerLineCount(lineCount)}',
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -296,7 +296,7 @@ class _LogBody extends StatelessWidget {
final gutterWidth = 12.0 + 8.0 * lines.length.toString().length; final gutterWidth = 12.0 + 8.0 * lines.length.toString().length;
return Scrollbar( return Scrollbar(
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
itemCount: lines.length, itemCount: lines.length,
itemBuilder: (context, i) => _LogLine( itemBuilder: (context, i) => _LogLine(
lineNumber: i + 1, lineNumber: i + 1,
@ -328,7 +328,7 @@ class _LogLine extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 1),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -337,7 +337,7 @@ class _LogLine extends StatelessWidget {
child: Text( child: Text(
'$lineNumber', '$lineNumber',
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant.withValues( color: theme.colorScheme.onSurfaceVariant.withValues(
alpha: 0.6, alpha: 0.6,
@ -345,7 +345,7 @@ class _LogLine extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: SelectableText.rich( child: SelectableText.rich(
plain plain
@ -354,7 +354,7 @@ class _LogLine extends StatelessWidget {
style: TextStyle(color: theme.colorScheme.onSurface), style: TextStyle(color: theme.colorScheme.onSurface),
) )
: _highlightLine(text, theme), : _highlightLine(text, theme),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
@ -388,7 +388,7 @@ TextSpan _highlightJsonLine(String line, ThemeData theme) {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
final stringStyle = TextStyle(color: FaiColors.success); final stringStyle = TextStyle(color: ChainColors.success);
final errorStringStyle = TextStyle(color: theme.colorScheme.error); final errorStringStyle = TextStyle(color: theme.colorScheme.error);
final keyRe = RegExp(r'"(ts|level|source|error|context)"\s*:'); final keyRe = RegExp(r'"(ts|level|source|error|context)"\s*:');
@ -440,7 +440,7 @@ TextSpan _highlightTraceLine(String line, ThemeData theme) {
break; break;
case 'WARN': case 'WARN':
case 'WARNING': case 'WARNING':
tone = FaiColors.warning; tone = ChainColors.warning;
break; break;
case 'INFO': case 'INFO':
tone = theme.colorScheme.primary; tone = theme.colorScheme.primary;

View file

@ -1,4 +1,4 @@
// FaiModuleSheet modal bottom-sheet showing detailed module // ChainModuleSheet modal bottom-sheet showing detailed module
// info: full capability list, declared permissions, on-disk // info: full capability list, declared permissions, on-disk
// directory. Opened by tapping a module card on the Modules // directory. Opened by tapping a module card on the Modules
// page. // page.
@ -11,12 +11,12 @@ import '../l10n/app_localizations.dart';
import '../pages/welcome.dart' show showFaiDoc; import '../pages/welcome.dart' show showFaiDoc;
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
import 'fai_pill.dart'; import 'chain_pill.dart';
class FaiModuleSheet extends StatefulWidget { class ChainModuleSheet extends StatefulWidget {
final String moduleName; final String moduleName;
const FaiModuleSheet({super.key, required this.moduleName}); const ChainModuleSheet({super.key, required this.moduleName});
/// Convenience launcher used from the Modules list. Resolves /// Convenience launcher used from the Modules list. Resolves
/// to `true` when the operator uninstalled the module the /// to `true` when the operator uninstalled the module the
@ -28,18 +28,18 @@ class FaiModuleSheet extends StatefulWidget {
isScrollControlled: true, isScrollControlled: true,
elevation: 8, elevation: 8,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(FaiRadius.md)), borderRadius: BorderRadius.vertical(top: Radius.circular(ChainRadius.md)),
), ),
builder: (_) => FaiModuleSheet(moduleName: name), builder: (_) => ChainModuleSheet(moduleName: name),
); );
return r ?? false; return r ?? false;
} }
@override @override
State<FaiModuleSheet> createState() => _FaiModuleSheetState(); State<ChainModuleSheet> createState() => _FaiModuleSheetState();
} }
class _FaiModuleSheetState extends State<FaiModuleSheet> { class _FaiModuleSheetState extends State<ChainModuleSheet> {
late final Future<ModuleDetail> _future; late final Future<ModuleDetail> _future;
bool _uninstalling = false; bool _uninstalling = false;
@ -127,12 +127,12 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
_Handle(), _Handle(),
if (snapshot.connectionState == ConnectionState.waiting) if (snapshot.connectionState == ConnectionState.waiting)
const Padding( const Padding(
padding: EdgeInsets.all(FaiSpace.xxxl), padding: EdgeInsets.all(ChainSpace.xxxl),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
else if (snapshot.hasError) else if (snapshot.hasError)
Padding( Padding(
padding: const EdgeInsets.all(FaiSpace.xl), padding: const EdgeInsets.all(ChainSpace.xl),
child: Text( child: Text(
AppLocalizations.of( AppLocalizations.of(
context, context,
@ -163,7 +163,7 @@ class _Handle extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
child: Container( child: Container(
width: 40, width: 40,
height: 4, height: 4,
@ -193,10 +193,10 @@ class _Body extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return ListView( return ListView(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.xxl, ChainSpace.xxl,
FaiSpace.md, ChainSpace.md,
FaiSpace.xxl, ChainSpace.xxl,
FaiSpace.xxl, ChainSpace.xxl,
), ),
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
@ -208,39 +208,39 @@ class _Body extends StatelessWidget {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
FaiPill( ChainPill(
label: 'v${detail.version}', label: 'v${detail.version}',
tone: FaiPillTone.accent, tone: ChainPillTone.accent,
monospace: true, monospace: true,
), ),
], ],
), ),
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
SelectableText( SelectableText(
detail.directory, detail.directory,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
_SectionHeader(l.moduleSheetCapabilities), _SectionHeader(l.moduleSheetCapabilities),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Wrap( Wrap(
spacing: FaiSpace.xs, spacing: ChainSpace.xs,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
children: detail.capabilities children: detail.capabilities
.map( .map(
(c) => FaiPill( (c) => ChainPill(
label: c, label: c,
tone: FaiPillTone.accent, tone: ChainPillTone.accent,
monospace: true, monospace: true,
), ),
) )
.toList(), .toList(),
), ),
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
Row( Row(
children: [ children: [
_SectionHeader(l.moduleSheetPermissions), _SectionHeader(l.moduleSheetPermissions),
@ -259,7 +259,7 @@ class _Body extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (detail.permissions.isEmpty) if (detail.permissions.isEmpty)
Text( Text(
l.moduleSheetNoPermissions, l.moduleSheetNoPermissions,
@ -282,10 +282,10 @@ class _Body extends StatelessWidget {
size: 14, size: 14,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
SelectableText( SelectableText(
p, p,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 12, size: 12,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
@ -296,7 +296,7 @@ class _Body extends StatelessWidget {
) )
.toList(), .toList(),
), ),
const SizedBox(height: FaiSpace.xxl), const SizedBox(height: ChainSpace.xxl),
Row( Row(
children: [ children: [
const Spacer(), const Spacer(),
@ -378,7 +378,7 @@ class _UninstallVersionPickerDialogState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(l.uninstallVersionPickerBody(widget.moduleName)), Text(l.uninstallVersionPickerBody(widget.moduleName)),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
RadioGroup<String>( RadioGroup<String>(
groupValue: _picked, groupValue: _picked,
onChanged: (val) { onChanged: (val) {

View file

@ -1,4 +1,4 @@
// FaiPill small inline label. Used for capabilities, versions, // ChainPill small inline label. Used for capabilities, versions,
// permission scopes, status text. Replaces ad-hoc Container chips. // permission scopes, status text. Replaces ad-hoc Container chips.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -6,18 +6,18 @@ import 'package:flutter/material.dart';
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
enum FaiPillTone { neutral, accent, success, warning, danger } enum ChainPillTone { neutral, accent, success, warning, danger }
class FaiPill extends StatelessWidget { class ChainPill extends StatelessWidget {
final String label; final String label;
final FaiPillTone tone; final ChainPillTone tone;
final IconData? icon; final IconData? icon;
final bool monospace; final bool monospace;
const FaiPill({ const ChainPill({
super.key, super.key,
required this.label, required this.label,
this.tone = FaiPillTone.neutral, this.tone = ChainPillTone.neutral,
this.icon, this.icon,
this.monospace = false, this.monospace = false,
}); });
@ -27,13 +27,13 @@ class FaiPill extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final (bg, fg) = _colors(theme.colorScheme); final (bg, fg) = _colors(theme.colorScheme);
final textStyle = monospace final textStyle = monospace
? FaiTheme.mono(size: 11, weight: FontWeight.w500, color: fg) ? ChainTheme.mono(size: 11, weight: FontWeight.w500, color: fg)
: theme.textTheme.labelSmall?.copyWith(color: fg); : theme.textTheme.labelSmall?.copyWith(color: fg);
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -50,25 +50,25 @@ class FaiPill extends StatelessWidget {
(Color, Color) _colors(ColorScheme scheme) { (Color, Color) _colors(ColorScheme scheme) {
switch (tone) { switch (tone) {
case FaiPillTone.neutral: case ChainPillTone.neutral:
return (scheme.surfaceContainerHighest, scheme.onSurfaceVariant); return (scheme.surfaceContainerHighest, scheme.onSurfaceVariant);
case FaiPillTone.accent: case ChainPillTone.accent:
return (scheme.primary.withValues(alpha: 0.15), scheme.primary); return (scheme.primary.withValues(alpha: 0.15), scheme.primary);
case FaiPillTone.success: case ChainPillTone.success:
return ( return (
FaiColors.success.withValues(alpha: 0.15), ChainColors.success.withValues(alpha: 0.15),
scheme.brightness == Brightness.dark scheme.brightness == Brightness.dark
? FaiColors.success ? ChainColors.success
: const Color(0xFF15803D), : const Color(0xFF15803D),
); );
case FaiPillTone.warning: case ChainPillTone.warning:
return ( return (
FaiColors.warning.withValues(alpha: 0.15), ChainColors.warning.withValues(alpha: 0.15),
scheme.brightness == Brightness.dark scheme.brightness == Brightness.dark
? FaiColors.warning ? ChainColors.warning
: const Color(0xFFB45309), : const Color(0xFFB45309),
); );
case FaiPillTone.danger: case ChainPillTone.danger:
return (scheme.errorContainer, scheme.onErrorContainer); return (scheme.errorContainer, scheme.onErrorContainer);
} }
} }

View file

@ -1,4 +1,4 @@
// FaiSearchPalette global Cmd+K command bar. // ChainSearchPalette global Cmd+K command bar.
// //
// Indexes: nav destinations, installed modules, store entries, // Indexes: nav destinations, installed modules, store entries,
// saved flows. Filters client-side as the operator types. // saved flows. Filters client-side as the operator types.
@ -12,12 +12,12 @@ import 'package:flutter/services.dart';
import '../data/hub.dart'; import '../data/hub.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import 'fai_module_sheet.dart'; import 'chain_module_sheet.dart';
/// One row in the palette. Carries everything needed to render /// One row in the palette. Carries everything needed to render
/// + dispatch keep it serializable-shaped so we can persist /// + dispatch keep it serializable-shaped so we can persist
/// recent picks later. /// recent picks later.
class FaiSearchHit { class ChainSearchHit {
final String label; final String label;
final String hint; final String hint;
final IconData icon; final IconData icon;
@ -33,7 +33,7 @@ class FaiSearchHit {
/// Action to run when this hit is selected. /// Action to run when this hit is selected.
final VoidCallback onSelect; final VoidCallback onSelect;
FaiSearchHit({ ChainSearchHit({
required this.label, required this.label,
required this.hint, required this.hint,
required this.icon, required this.icon,
@ -42,33 +42,33 @@ class FaiSearchHit {
}) : haystack = '$label $hint $group'.toLowerCase(); }) : haystack = '$label $hint $group'.toLowerCase();
} }
class FaiSearchPalette extends StatefulWidget { class ChainSearchPalette extends StatefulWidget {
/// Pages the operator can navigate to. Each entry maps to a /// Pages the operator can navigate to. Each entry maps to a
/// callback that selects the right tab back in the shell. /// callback that selects the right tab back in the shell.
final List<FaiSearchHit> staticHits; final List<ChainSearchHit> staticHits;
const FaiSearchPalette({super.key, required this.staticHits}); const ChainSearchPalette({super.key, required this.staticHits});
/// Convenience launcher used from the Cmd+K shortcut. /// Convenience launcher used from the Cmd+K shortcut.
static Future<void> show( static Future<void> show(
BuildContext context, { BuildContext context, {
required List<FaiSearchHit> staticHits, required List<ChainSearchHit> staticHits,
}) { }) {
return showDialog<void>( return showDialog<void>(
context: context, context: context,
barrierColor: Colors.black54, barrierColor: Colors.black54,
builder: (_) => FaiSearchPalette(staticHits: staticHits), builder: (_) => ChainSearchPalette(staticHits: staticHits),
); );
} }
@override @override
State<FaiSearchPalette> createState() => _FaiSearchPaletteState(); State<ChainSearchPalette> createState() => _FaiSearchPaletteState();
} }
class _FaiSearchPaletteState extends State<FaiSearchPalette> { class _FaiSearchPaletteState extends State<ChainSearchPalette> {
final _query = TextEditingController(); final _query = TextEditingController();
final _focus = FocusNode(); final _focus = FocusNode();
Future<List<FaiSearchHit>>? _dynamicFuture; Future<List<ChainSearchHit>>? _dynamicFuture;
int _highlight = 0; int _highlight = 0;
@override @override
@ -92,7 +92,7 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
/// Pull modules / store / flows in parallel so the palette /// Pull modules / store / flows in parallel so the palette
/// is searchable as soon as the operator finishes typing the /// is searchable as soon as the operator finishes typing the
/// first character. /// first character.
Future<List<FaiSearchHit>> _loadDynamic() async { Future<List<ChainSearchHit>> _loadDynamic() async {
final svc = HubService.instance; final svc = HubService.instance;
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final results = await Future.wait([ final results = await Future.wait([
@ -104,17 +104,17 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
final store = results[1] as List<StoreItem>; final store = results[1] as List<StoreItem>;
final flows = results[2] as List<SavedFlow>; final flows = results[2] as List<SavedFlow>;
final hits = <FaiSearchHit>[]; final hits = <ChainSearchHit>[];
for (final m in modules) { for (final m in modules) {
hits.add( hits.add(
FaiSearchHit( ChainSearchHit(
label: m.name, label: m.name,
hint: l.searchInstalledModuleHint(m.version), hint: l.searchInstalledModuleHint(m.version),
icon: Icons.extension, icon: Icons.extension,
group: l.searchGroupModules, group: l.searchGroupModules,
onSelect: () { onSelect: () {
Navigator.pop(context); Navigator.pop(context);
FaiModuleSheet.show(context, m.name); ChainModuleSheet.show(context, m.name);
}, },
), ),
); );
@ -123,7 +123,7 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
final pagesGroup = l.searchGroupPages; final pagesGroup = l.searchGroupPages;
for (final s in store) { for (final s in store) {
hits.add( hits.add(
FaiSearchHit( ChainSearchHit(
label: s.name, label: s.name,
hint: s.taglineEn.isEmpty hint: s.taglineEn.isEmpty
? l.searchStoreHintWithCategory( ? l.searchStoreHintWithCategory(
@ -151,7 +151,7 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
final flowsGroup = l.searchGroupFlows; final flowsGroup = l.searchGroupFlows;
for (final f in flows) { for (final f in flows) {
hits.add( hits.add(
FaiSearchHit( ChainSearchHit(
label: f.name, label: f.name,
hint: l.searchSavedFlowHint, hint: l.searchSavedFlowHint,
icon: Icons.account_tree_outlined, icon: Icons.account_tree_outlined,
@ -174,11 +174,11 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
/// Filtered + grouped result list. Empty query returns the /// Filtered + grouped result list. Empty query returns the
/// pages first then everything else, capped to 50 rows so the /// pages first then everything else, capped to 50 rows so the
/// dialog stays scrollable. /// dialog stays scrollable.
List<FaiSearchHit> _resultsFor(List<FaiSearchHit> dynamic_, String raw) { List<ChainSearchHit> _resultsFor(List<ChainSearchHit> dynamic_, String raw) {
final query = raw.trim().toLowerCase(); final query = raw.trim().toLowerCase();
final all = [...widget.staticHits, ...dynamic_]; final all = [...widget.staticHits, ...dynamic_];
if (query.isEmpty) return all.take(50).toList(); if (query.isEmpty) return all.take(50).toList();
final scored = <(int, FaiSearchHit)>[]; final scored = <(int, ChainSearchHit)>[];
for (final h in all) { for (final h in all) {
final i = h.haystack.indexOf(query); final i = h.haystack.indexOf(query);
if (i < 0) continue; if (i < 0) continue;
@ -224,10 +224,10 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640, maxHeight: 540), constraints: const BoxConstraints(maxWidth: 640, maxHeight: 540),
child: FutureBuilder<List<FaiSearchHit>>( child: FutureBuilder<List<ChainSearchHit>>(
future: _dynamicFuture, future: _dynamicFuture,
builder: (context, snap) { builder: (context, snap) {
final dyn = snap.data ?? const <FaiSearchHit>[]; final dyn = snap.data ?? const <ChainSearchHit>[];
return ValueListenableBuilder<TextEditingValue>( return ValueListenableBuilder<TextEditingValue>(
valueListenable: _query, valueListenable: _query,
builder: (_, value, _) { builder: (_, value, _) {

View file

@ -1,4 +1,4 @@
// FaiSettingsDialog modal for changing the hub endpoint. // ChainSettingsDialog modal for changing the hub endpoint.
// Reads current values from HubService, persists on save via // Reads current values from HubService, persists on save via
// HubService.reconnect. // HubService.reconnect.
@ -14,25 +14,25 @@ import '../l10n/app_localizations.dart';
import '../pages/welcome.dart' show showFaiDoc; import '../pages/welcome.dart' show showFaiDoc;
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
import 'fai_error_box.dart'; import 'chain_error_box.dart';
import 'fai_pill.dart'; import 'chain_pill.dart';
import 'fai_system_ai_editor.dart'; import 'chain_system_ai_editor.dart';
import 'theme_picker_grid.dart'; import 'theme_picker_grid.dart';
class FaiSettingsDialog extends StatefulWidget { class ChainSettingsDialog extends StatefulWidget {
const FaiSettingsDialog({super.key}); const ChainSettingsDialog({super.key});
/// Convenience launcher used from the sidebar gear icon. /// Convenience launcher used from the sidebar gear icon.
static Future<bool> show(BuildContext context) async { static Future<bool> show(BuildContext context) async {
final ok = await showDialog<bool>( final ok = await showDialog<bool>(
context: context, context: context,
builder: (_) => const FaiSettingsDialog(), builder: (_) => const ChainSettingsDialog(),
); );
return ok ?? false; return ok ?? false;
} }
@override @override
State<FaiSettingsDialog> createState() => _FaiSettingsDialogState(); State<ChainSettingsDialog> createState() => _FaiSettingsDialogState();
} }
/// Sidebar categories every Settings panel belongs to exactly /// Sidebar categories every Settings panel belongs to exactly
@ -40,7 +40,7 @@ class FaiSettingsDialog extends StatefulWidget {
/// first-opened category. /// first-opened category.
enum _Category { general, appearance, ai, integrations, security, maintenance } enum _Category { general, appearance, ai, integrations, security, maintenance }
class _FaiSettingsDialogState extends State<FaiSettingsDialog> { class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
late final TextEditingController _host; late final TextEditingController _host;
late final TextEditingController _port; late final TextEditingController _port;
bool _secure = false; bool _secure = false;
@ -291,7 +291,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
_saving = true; _saving = true;
_channelToast = null; _channelToast = null;
}); });
final r = await SystemActions.faiChannelSwitch(name); final r = await SystemActions.chainChannelSwitch(name);
if (!mounted) return; if (!mounted) return;
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
setState(() { setState(() {
@ -384,10 +384,10 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
}; };
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
FaiSpace.xl, ChainSpace.xl,
FaiSpace.xl, ChainSpace.xl,
FaiSpace.xl, ChainSpace.xl,
FaiSpace.lg, ChainSpace.lg,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -403,7 +403,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
String? docSlug, String? docSlug,
}) { }) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: FaiSpace.lg), padding: const EdgeInsets.only(bottom: ChainSpace.lg),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -455,7 +455,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
), ),
autofocus: true, autofocus: true,
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -470,7 +470,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
), ),
const SizedBox(width: FaiSpace.md), const SizedBox(width: ChainSpace.md),
Expanded( Expanded(
flex: 3, flex: 3,
child: SwitchListTile( child: SwitchListTile(
@ -490,7 +490,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
], ],
), ),
if (_error != null) ...[ if (_error != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
_error!, _error!,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -498,12 +498,12 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
), ),
), ),
], ],
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Container( Container(
padding: const EdgeInsets.all(FaiSpace.sm), padding: const EdgeInsets.all(ChainSpace.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
), ),
child: Row( child: Row(
children: [ children: [
@ -512,10 +512,10 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
size: 14, size: 14,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
_previewUrl(), _previewUrl(),
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
@ -524,7 +524,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
), ),
), ),
if (_channels != null) ...[ if (_channels != null) ...[
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
Text( Text(
l.channelsHeader, l.channelsHeader,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
@ -540,7 +540,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
for (final ch in _channels!.channels) for (final ch in _channels!.channels)
_ChannelRow( _ChannelRow(
channel: ch, channel: ch,
@ -551,21 +551,21 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
? null ? null
: () => _runDaemon( : () => _runDaemon(
l.daemonActionEnableAutostart, l.daemonActionEnableAutostart,
() => SystemActions.faiDaemonEnable(ch.name), () => SystemActions.chainDaemonEnable(ch.name),
), ),
onDisableAutostart: _saving onDisableAutostart: _saving
? null ? null
: () => _runDaemon( : () => _runDaemon(
l.daemonActionDisableAutostart, l.daemonActionDisableAutostart,
() => SystemActions.faiDaemonDisable(ch.name), () => SystemActions.chainDaemonDisable(ch.name),
), ),
), ),
if (_channelToast != null) ...[ if (_channelToast != null) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
FaiErrorBox(text: _channelToast!, maxHeight: 200), ChainErrorBox(text: _channelToast!, maxHeight: 200),
], ],
], ],
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
const _DefaultScopePanel(), const _DefaultScopePanel(),
]; ];
} }
@ -594,7 +594,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
_SystemAiPanel( _SystemAiPanel(
status: _aiStatus!, status: _aiStatus!,
onEdit: () async { onEdit: () async {
final updated = await FaiSystemAiEditor.show(context, _aiStatus!); final updated = await ChainSystemAiEditor.show(context, _aiStatus!);
if (updated != null && mounted) { if (updated != null && mounted) {
setState(() => _aiStatus = updated); setState(() => _aiStatus = updated);
} }
@ -612,7 +612,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
), ),
if (_mcpClients == null) if (_mcpClients == null)
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: FaiSpace.md), padding: EdgeInsets.symmetric(vertical: ChainSpace.md),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)), child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
) )
else else
@ -622,7 +622,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
onRemove: _removeMcpClient, onRemove: _removeMcpClient,
onRefresh: _refreshMcpClients, onRefresh: _refreshMcpClients,
), ),
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
if (_n8nEndpoints != null) if (_n8nEndpoints != null)
_N8nEndpointsPanel( _N8nEndpointsPanel(
endpoints: _n8nEndpoints!, endpoints: _n8nEndpoints!,
@ -646,7 +646,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
onSave: _saveRegistryToken, onSave: _saveRegistryToken,
onClear: _clearRegistryToken, onClear: _clearRegistryToken,
), ),
const SizedBox(height: FaiSpace.xl), const SizedBox(height: ChainSpace.xl),
_HubAuthTokenPanel( _HubAuthTokenPanel(
configuredChars: _hubAuthTokenChars, configuredChars: _hubAuthTokenChars,
onSave: _saveHubAuthToken, onSave: _saveHubAuthToken,
@ -686,7 +686,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
return AlertDialog( return AlertDialog(
title: Text(l.settingsTitle), title: Text(l.settingsTitle),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
), ),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
content: ConstrainedBox( content: ConstrainedBox(
@ -770,15 +770,15 @@ class _ChannelRow extends StatelessWidget {
channel.running ? Icons.circle : Icons.circle_outlined, channel.running ? Icons.circle : Icons.circle_outlined,
size: 10, size: 10,
color: channel.running color: channel.running
? FaiColors.success ? ChainColors.success
: theme.colorScheme.outline, : theme.colorScheme.outline,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
SizedBox( SizedBox(
width: 88, width: 88,
child: Text( child: Text(
channel.name, channel.name,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
weight: active ? FontWeight.w600 : FontWeight.w400, weight: active ? FontWeight.w600 : FontWeight.w400,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
@ -789,7 +789,7 @@ class _ChannelRow extends StatelessWidget {
width: 64, width: 64,
child: Text( child: Text(
':${channel.port}', ':${channel.port}',
style: FaiTheme.mono( style: ChainTheme.mono(
size: 11, size: 11,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -805,10 +805,10 @@ class _ChannelRow extends StatelessWidget {
), ),
if (active) if (active)
Padding( Padding(
padding: const EdgeInsets.only(right: FaiSpace.sm), padding: const EdgeInsets.only(right: ChainSpace.sm),
child: FaiPill( child: ChainPill(
label: l.channelsActive, label: l.channelsActive,
tone: FaiPillTone.success, tone: ChainPillTone.success,
), ),
), ),
PopupMenuButton<String>( PopupMenuButton<String>(
@ -876,7 +876,7 @@ class _MenuRow extends StatelessWidget {
return Row( return Row(
children: [ children: [
Icon(icon, size: 14), Icon(icon, size: 14),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text(text), Text(text),
], ],
); );
@ -906,18 +906,18 @@ class _SystemAiPanel extends StatelessWidget {
fontSize: 10, fontSize: 10,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: status.enabled ? l.systemAiEnabled : l.systemAiOff, label: status.enabled ? l.systemAiEnabled : l.systemAiOff,
tone: status.enabled ? FaiPillTone.success : FaiPillTone.neutral, tone: status.enabled ? ChainPillTone.success : ChainPillTone.neutral,
), ),
if (status.enabled) ...[ if (status.enabled) ...[
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
FaiPill( ChainPill(
label: status.privacyMode, label: status.privacyMode,
tone: status.privacyMode == 'full' tone: status.privacyMode == 'full'
? FaiPillTone.warning ? ChainPillTone.warning
: FaiPillTone.neutral, : ChainPillTone.neutral,
), ),
], ],
const Spacer(), const Spacer(),
@ -985,7 +985,7 @@ class _StatRow extends StatelessWidget {
child: Text( child: Text(
value.isEmpty ? '' : value, value.isEmpty ? '' : value,
style: mono style: mono
? FaiTheme.mono(size: 11, color: theme.colorScheme.onSurface) ? ChainTheme.mono(size: 11, color: theme.colorScheme.onSurface)
: theme.textTheme.bodySmall, : theme.textTheme.bodySmall,
), ),
), ),
@ -1045,10 +1045,10 @@ class _McpClientsPanel extends StatelessWidget {
fontSize: 10, fontSize: 10,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: l.mcpServersCount(clients.length), label: l.mcpServersCount(clients.length),
tone: FaiPillTone.neutral, tone: ChainPillTone.neutral,
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
@ -1075,10 +1075,10 @@ class _McpClientsPanel extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (clients.isEmpty) if (clients.isEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
child: Text( child: Text(
l.mcpEmpty, l.mcpEmpty,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -1105,7 +1105,7 @@ class _McpClientRow extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final ok = client.healthy; final ok = client.healthy;
final dotColor = client.errorKind.isEmpty final dotColor = client.errorKind.isEmpty
? (ok ? FaiColors.success : FaiColors.muted) ? (ok ? ChainColors.success : ChainColors.muted)
: theme.colorScheme.error; : theme.colorScheme.error;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
@ -1116,7 +1116,7 @@ class _McpClientRow extends StatelessWidget {
padding: const EdgeInsets.only(top: 6), padding: const EdgeInsets.only(top: 6),
child: Icon(Icons.circle, size: 8, color: dotColor), child: Icon(Icons.circle, size: 8, color: dotColor),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1125,28 +1125,28 @@ class _McpClientRow extends StatelessWidget {
children: [ children: [
Text( Text(
client.name, client.name,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 12, size: 12,
weight: FontWeight.w600, weight: FontWeight.w600,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
if (client.toolCount > 0) if (client.toolCount > 0)
FaiPill( ChainPill(
label: l.mcpToolsCount(client.toolCount), label: l.mcpToolsCount(client.toolCount),
tone: FaiPillTone.success, tone: ChainPillTone.success,
) )
else if (client.errorKind.isNotEmpty) else if (client.errorKind.isNotEmpty)
FaiPill( ChainPill(
label: client.errorKind, label: client.errorKind,
tone: FaiPillTone.danger, tone: ChainPillTone.danger,
), ),
], ],
), ),
Text( Text(
client.endpoint, client.endpoint,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -1234,7 +1234,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
// Curated suggestions from the official Anthropic // Curated suggestions from the official Anthropic
// MCP servers + a few community staples. Click a // MCP servers + a few community staples. Click a
// chip to pre-fill the form. The operator still // chip to pre-fill the form. The operator still
@ -1249,8 +1249,8 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Wrap( Wrap(
spacing: FaiSpace.xs, spacing: ChainSpace.xs,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
children: [ children: [
for (final s in _kMcpSuggestions) for (final s in _kMcpSuggestions)
ActionChip( ActionChip(
@ -1260,7 +1260,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
), ),
], ],
), ),
const Divider(height: FaiSpace.xl), const Divider(height: ChainSpace.xl),
TextField( TextField(
controller: _name, controller: _name,
autofocus: true, autofocus: true,
@ -1271,7 +1271,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextField( TextField(
controller: _endpoint, controller: _endpoint,
decoration: InputDecoration( decoration: InputDecoration(
@ -1281,7 +1281,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextField( TextField(
controller: _apiKey, controller: _apiKey,
decoration: InputDecoration( decoration: InputDecoration(
@ -1292,7 +1292,7 @@ class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextField( TextField(
controller: _desc, controller: _desc,
decoration: InputDecoration( decoration: InputDecoration(
@ -1377,10 +1377,10 @@ class _N8nEndpointsPanel extends StatelessWidget {
fontSize: 10, fontSize: 10,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FaiPill( ChainPill(
label: l.n8nEndpointsCount(endpoints.length), label: l.n8nEndpointsCount(endpoints.length),
tone: FaiPillTone.neutral, tone: ChainPillTone.neutral,
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
@ -1407,10 +1407,10 @@ class _N8nEndpointsPanel extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (endpoints.isEmpty) if (endpoints.isEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
child: Text( child: Text(
l.n8nEmpty, l.n8nEmpty,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -1437,7 +1437,7 @@ class _N8nEndpointRow extends StatelessWidget {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final ok = endpoint.healthy; final ok = endpoint.healthy;
final dotColor = endpoint.errorKind.isEmpty final dotColor = endpoint.errorKind.isEmpty
? (ok ? FaiColors.success : FaiColors.muted) ? (ok ? ChainColors.success : ChainColors.muted)
: theme.colorScheme.error; : theme.colorScheme.error;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
@ -1448,7 +1448,7 @@ class _N8nEndpointRow extends StatelessWidget {
padding: const EdgeInsets.only(top: 6), padding: const EdgeInsets.only(top: 6),
child: Icon(Icons.circle, size: 8, color: dotColor), child: Icon(Icons.circle, size: 8, color: dotColor),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1457,28 +1457,28 @@ class _N8nEndpointRow extends StatelessWidget {
children: [ children: [
Text( Text(
endpoint.name, endpoint.name,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 12, size: 12,
weight: FontWeight.w600, weight: FontWeight.w600,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
if (endpoint.workflowCount > 0) if (endpoint.workflowCount > 0)
FaiPill( ChainPill(
label: l.n8nWorkflowsCount(endpoint.workflowCount), label: l.n8nWorkflowsCount(endpoint.workflowCount),
tone: FaiPillTone.success, tone: ChainPillTone.success,
) )
else if (endpoint.errorKind.isNotEmpty) else if (endpoint.errorKind.isNotEmpty)
FaiPill( ChainPill(
label: endpoint.errorKind, label: endpoint.errorKind,
tone: FaiPillTone.danger, tone: ChainPillTone.danger,
), ),
], ],
), ),
Text( Text(
endpoint.baseUrl, endpoint.baseUrl,
style: FaiTheme.mono( style: ChainTheme.mono(
size: 10, size: 10,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -1554,7 +1554,7 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
TextField( TextField(
controller: _name, controller: _name,
autofocus: true, autofocus: true,
@ -1565,7 +1565,7 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextField( TextField(
controller: _baseUrl, controller: _baseUrl,
decoration: InputDecoration( decoration: InputDecoration(
@ -1575,7 +1575,7 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextField( TextField(
controller: _apiKey, controller: _apiKey,
decoration: InputDecoration( decoration: InputDecoration(
@ -1586,7 +1586,7 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
TextField( TextField(
controller: _desc, controller: _desc,
decoration: InputDecoration( decoration: InputDecoration(
@ -1718,7 +1718,7 @@ class _MaintenancePanelState extends State<_MaintenancePanel> {
_resetting = true; _resetting = true;
_resultText = null; _resultText = null;
}); });
final r = await SystemActions.faiReset( final r = await SystemActions.chainReset(
keepModules: _keepModules, keepModules: _keepModules,
keepData: _keepData, keepData: _keepData,
); );
@ -1755,7 +1755,7 @@ class _MaintenancePanelState extends State<_MaintenancePanel> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
CheckboxListTile( CheckboxListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
dense: true, dense: true,
@ -1788,7 +1788,7 @@ class _MaintenancePanelState extends State<_MaintenancePanel> {
), ),
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Row( Row(
children: [ children: [
FilledButton.icon( FilledButton.icon(
@ -1813,8 +1813,8 @@ class _MaintenancePanelState extends State<_MaintenancePanel> {
], ],
), ),
if (_resultText != null) ...[ if (_resultText != null) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
FaiErrorBox(text: _resultText!, isError: !_resultOk, maxHeight: 240), ChainErrorBox(text: _resultText!, isError: !_resultOk, maxHeight: 240),
], ],
], ],
); );
@ -1891,7 +1891,7 @@ class _RegistryCredentialsPanelState extends State<_RegistryCredentialsPanel> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Row( Row(
children: [ children: [
Icon( Icon(
@ -1901,7 +1901,7 @@ class _RegistryCredentialsPanelState extends State<_RegistryCredentialsPanel> {
? theme.colorScheme.primary ? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant, : theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
Text( Text(
isConfigured isConfigured
? l.registryTokenStatusConfigured(widget.configuredChars!) ? l.registryTokenStatusConfigured(widget.configuredChars!)
@ -1925,7 +1925,7 @@ class _RegistryCredentialsPanelState extends State<_RegistryCredentialsPanel> {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -1947,7 +1947,7 @@ class _RegistryCredentialsPanelState extends State<_RegistryCredentialsPanel> {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FilledButton.icon( FilledButton.icon(
icon: const Icon(Icons.save_outlined, size: 14), icon: const Icon(Icons.save_outlined, size: 14),
label: Text(l.registryTokenSaveButton), label: Text(l.registryTokenSaveButton),
@ -2040,7 +2040,7 @@ class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Row( Row(
children: [ children: [
Icon( Icon(
@ -2050,7 +2050,7 @@ class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
? theme.colorScheme.primary ? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant, : theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
Text( Text(
isConfigured isConfigured
? l.hubAuthTokenStatusConfigured(widget.configuredChars!) ? l.hubAuthTokenStatusConfigured(widget.configuredChars!)
@ -2074,7 +2074,7 @@ class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -2096,7 +2096,7 @@ class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FilledButton.icon( FilledButton.icon(
icon: const Icon(Icons.save_outlined, size: 14), icon: const Icon(Icons.save_outlined, size: 14),
label: Text(l.hubAuthTokenSaveButton), label: Text(l.hubAuthTokenSaveButton),
@ -2410,10 +2410,10 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (_loading) if (_loading)
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: EdgeInsets.symmetric(vertical: ChainSpace.sm),
child: SizedBox( child: SizedBox(
height: 16, height: 16,
width: 16, width: 16,
@ -2438,7 +2438,7 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
onDown: () => _moveDown(i), onDown: () => _moveDown(i),
onRemove: () => _removeAt(i), onRemove: () => _removeAt(i),
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -2453,7 +2453,7 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
onSubmitted: _saving ? null : _addEntry, onSubmitted: _saving ? null : _addEntry,
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
FilledButton.icon( FilledButton.icon(
icon: const Icon(Icons.add, size: 14), icon: const Icon(Icons.add, size: 14),
label: Text(l.defaultScopeAddButton), label: Text(l.defaultScopeAddButton),
@ -2464,7 +2464,7 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
], ],
), ),
if (unusedSuggestions.isNotEmpty) ...[ if (unusedSuggestions.isNotEmpty) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
l.defaultScopeSuggestions, l.defaultScopeSuggestions,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -2474,7 +2474,7 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Wrap( Wrap(
spacing: FaiSpace.xs, spacing: ChainSpace.xs,
runSpacing: 4, runSpacing: 4,
children: [ children: [
for (final s in unusedSuggestions) for (final s in unusedSuggestions)
@ -2489,7 +2489,7 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
), ),
], ],
if (_error != null) ...[ if (_error != null) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
_error!, _error!,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -2603,7 +2603,7 @@ class _Sidebar extends StatelessWidget {
child: Container( child: Container(
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(vertical: FaiSpace.lg), padding: const EdgeInsets.symmetric(vertical: ChainSpace.lg),
children: [ children: [
for (final (cat, icon, label) in items) for (final (cat, icon, label) in items)
_SidebarRow( _SidebarRow(
@ -2639,24 +2639,24 @@ class _SidebarRow extends StatelessWidget {
? theme.colorScheme.onSurface ? theme.colorScheme.onSurface
: theme.colorScheme.onSurfaceVariant; : theme.colorScheme.onSurfaceVariant;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 2),
child: Material( child: Material(
color: selected color: selected
? theme.colorScheme.primary.withValues(alpha: 0.14) ? theme.colorScheme.primary.withValues(alpha: 0.14)
: Colors.transparent, : Colors.transparent,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.md, horizontal: ChainSpace.md,
vertical: 8, vertical: 8,
), ),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 16, color: fg), Icon(icon, size: 16, color: fg),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
Text( Text(
label, label,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(

View file

@ -1,11 +1,11 @@
// FaiStatusDot small pulsing dot used as a "live" indicator. // ChainStatusDot small pulsing dot used as a "live" indicator.
// Goes still when [pulsing] is false, breathes gently when true. // Goes still when [pulsing] is false, breathes gently when true.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class FaiStatusDot extends StatefulWidget { class ChainStatusDot extends StatefulWidget {
final Color color; final Color color;
/// When true, the dot fades between full and 30% opacity. /// When true, the dot fades between full and 30% opacity.
@ -14,7 +14,7 @@ class FaiStatusDot extends StatefulWidget {
final double size; final double size;
const FaiStatusDot({ const ChainStatusDot({
super.key, super.key,
required this.color, required this.color,
this.pulsing = false, this.pulsing = false,
@ -22,10 +22,10 @@ class FaiStatusDot extends StatefulWidget {
}); });
@override @override
State<FaiStatusDot> createState() => _FaiStatusDotState(); State<ChainStatusDot> createState() => _FaiStatusDotState();
} }
class _FaiStatusDotState extends State<FaiStatusDot> class _FaiStatusDotState extends State<ChainStatusDot>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final AnimationController _ctrl; late final AnimationController _ctrl;
@ -40,7 +40,7 @@ class _FaiStatusDotState extends State<FaiStatusDot>
} }
@override @override
void didUpdateWidget(covariant FaiStatusDot old) { void didUpdateWidget(covariant ChainStatusDot old) {
super.didUpdateWidget(old); super.didUpdateWidget(old);
if (widget.pulsing && !_ctrl.isAnimating) { if (widget.pulsing && !_ctrl.isAnimating) {
_ctrl.repeat(reverse: true); _ctrl.repeat(reverse: true);
@ -86,10 +86,10 @@ class _FaiStatusDotState extends State<FaiStatusDot>
} }
/// Convenience presets. /// Convenience presets.
class FaiStatusDots { class ChainStatusDots {
FaiStatusDots._(); ChainStatusDots._();
static Widget live() => static Widget live() =>
const FaiStatusDot(color: FaiColors.success, pulsing: true); const ChainStatusDot(color: ChainColors.success, pulsing: true);
static Widget idle() => const FaiStatusDot(color: FaiColors.muted); static Widget idle() => const ChainStatusDot(color: ChainColors.muted);
static Widget down() => const FaiStatusDot(color: FaiColors.danger); static Widget down() => const ChainStatusDot(color: ChainColors.danger);
} }

View file

@ -1,4 +1,4 @@
// FaiSystemAiEditor modal for configuring the hub-internal // ChainSystemAiEditor modal for configuring the hub-internal
// System AI from inside Studio. Mirrors the YAML block but with // System AI from inside Studio. Mirrors the YAML block but with
// provider presets, in-place explainers, and a "Test connection" // provider presets, in-place explainers, and a "Test connection"
// button. On save, calls HubAdmin.UpdateSystemAi which both // button. On save, calls HubAdmin.UpdateSystemAi which both
@ -11,10 +11,10 @@ import '../data/hub.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../theme/theme.dart'; import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
import 'fai_pill.dart'; import 'chain_pill.dart';
/// Provider preset metadata kept in sync with /// Provider preset metadata kept in sync with
/// `fai_hub::operator_config::SystemLlmProvider`. Anything that /// `chain_hub::operator_config::SystemLlmProvider`. Anything that
/// is operator-visible (label, description, default endpoint, /// is operator-visible (label, description, default endpoint,
/// suggested env-var) lives here so the dropdown self-explains /// suggested env-var) lives here so the dropdown self-explains
/// without needing to read the source. /// without needing to read the source.
@ -108,10 +108,10 @@ class _ProviderPreset {
} }
} }
class FaiSystemAiEditor extends StatefulWidget { class ChainSystemAiEditor extends StatefulWidget {
final SystemAiStatus initial; final SystemAiStatus initial;
const FaiSystemAiEditor({super.key, required this.initial}); const ChainSystemAiEditor({super.key, required this.initial});
/// Convenience launcher. Returns the new status when the /// Convenience launcher. Returns the new status when the
/// operator saved, null on cancel. /// operator saved, null on cancel.
@ -121,15 +121,15 @@ class FaiSystemAiEditor extends StatefulWidget {
) { ) {
return showDialog<SystemAiStatus>( return showDialog<SystemAiStatus>(
context: context, context: context,
builder: (_) => FaiSystemAiEditor(initial: initial), builder: (_) => ChainSystemAiEditor(initial: initial),
); );
} }
@override @override
State<FaiSystemAiEditor> createState() => _FaiSystemAiEditorState(); State<ChainSystemAiEditor> createState() => _FaiSystemAiEditorState();
} }
class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> { class _FaiSystemAiEditorState extends State<ChainSystemAiEditor> {
late _ProviderPreset _preset; late _ProviderPreset _preset;
late final TextEditingController _endpoint; late final TextEditingController _endpoint;
late final TextEditingController _model; late final TextEditingController _model;
@ -365,11 +365,11 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
return AlertDialog( return AlertDialog(
title: Text(l.systemAiTitle), title: Text(l.systemAiTitle),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(FaiRadius.md), borderRadius: BorderRadius.circular(ChainRadius.md),
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: FaiSpace.xl, horizontal: ChainSpace.xl,
vertical: FaiSpace.lg, vertical: ChainSpace.lg,
), ),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520, maxHeight: 600), constraints: const BoxConstraints(maxWidth: 520, maxHeight: 600),
@ -385,10 +385,10 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
), ),
), ),
if (_hw != null) ...[ if (_hw != null) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
_HardwareBanner(hw: _hw!, lastReviewed: _curated?.lastReviewed), _HardwareBanner(hw: _hw!, lastReviewed: _curated?.lastReviewed),
], ],
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_ProviderDropdown(value: _preset, onChanged: _onProviderChanged), _ProviderDropdown(value: _preset, onChanged: _onProviderChanged),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
@ -397,17 +397,17 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
TextField( TextField(
controller: _endpoint, controller: _endpoint,
style: FaiTheme.mono(size: 12), style: ChainTheme.mono(size: 12),
decoration: InputDecoration( decoration: InputDecoration(
labelText: l.systemAiEndpointLabel, labelText: l.systemAiEndpointLabel,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
), ),
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
_ModelPicker( _ModelPicker(
controller: _model, controller: _model,
preset: _preset, preset: _preset,
@ -424,10 +424,10 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
? null ? null
: _pullModel, : _pullModel,
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
TextField( TextField(
controller: _apiKeyEnv, controller: _apiKeyEnv,
style: FaiTheme.mono(size: 12), style: ChainTheme.mono(size: 12),
decoration: InputDecoration( decoration: InputDecoration(
labelText: _preset.wire == 'openai' labelText: _preset.wire == 'openai'
? l.systemAiApiKeyRequired ? l.systemAiApiKeyRequired
@ -445,20 +445,20 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: FaiSpace.lg), const SizedBox(height: ChainSpace.lg),
_PrivacyModeChips( _PrivacyModeChips(
value: _privacyMode, value: _privacyMode,
onChanged: (v) => setState(() => _privacyMode = v), onChanged: (v) => setState(() => _privacyMode = v),
), ),
if (widget.initial.cacheCount > 0) ...[ if (widget.initial.cacheCount > 0) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
_CacheStatusRow( _CacheStatusRow(
cacheCount: widget.initial.cacheCount, cacheCount: widget.initial.cacheCount,
onClear: _saving || _testing ? null : _clearCache, onClear: _saving || _testing ? null : _clearCache,
), ),
], ],
if (_error != null) ...[ if (_error != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text( Text(
_error!, _error!,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -467,7 +467,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
), ),
], ],
if (_testResult != null) ...[ if (_testResult != null) ...[
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
_TestResultPanel(result: _testResult!), _TestResultPanel(result: _testResult!),
], ],
], ],
@ -573,11 +573,11 @@ class _PrivacyModeChips extends StatelessWidget {
for (final (wire, label, desc) in modes) for (final (wire, label, desc) in modes)
InkWell( InkWell(
onTap: () => onChanged(wire), onTap: () => onChanged(wire),
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: FaiSpace.sm, vertical: ChainSpace.sm,
horizontal: FaiSpace.sm, horizontal: ChainSpace.sm,
), ),
child: Row( child: Row(
children: [ children: [
@ -616,13 +616,13 @@ class _TestResultPanel extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final ok = result.isSuccess; final ok = result.isSuccess;
final color = ok ? FaiColors.success : theme.colorScheme.error; final color = ok ? ChainColors.success : theme.colorScheme.error;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(FaiSpace.md), padding: const EdgeInsets.all(ChainSpace.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: color.withValues(alpha: 0.3)), border: Border.all(color: color.withValues(alpha: 0.3)),
), ),
child: Column( child: Column(
@ -635,16 +635,16 @@ class _TestResultPanel extends StatelessWidget {
size: 14, size: 14,
color: color, color: color,
), ),
const SizedBox(width: FaiSpace.xs), const SizedBox(width: ChainSpace.xs),
Text( Text(
ok ? l.systemAiConnectionOk : l.systemAiConnectionFailed, ok ? l.systemAiConnectionOk : l.systemAiConnectionFailed,
style: theme.textTheme.bodyMedium?.copyWith(color: color), style: theme.textTheme.bodyMedium?.copyWith(color: color),
), ),
const Spacer(), const Spacer(),
if (ok && result.latencyMs > 0) if (ok && result.latencyMs > 0)
FaiPill( ChainPill(
label: '${result.latencyMs} ms', label: '${result.latencyMs} ms',
tone: FaiPillTone.neutral, tone: ChainPillTone.neutral,
monospace: true, monospace: true,
), ),
], ],
@ -652,10 +652,10 @@ class _TestResultPanel extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
SelectableText( SelectableText(
ok ? l.systemAiReplyPrefix(result.text) : result.text, ok ? l.systemAiReplyPrefix(result.text) : result.text,
style: FaiTheme.mono(size: 11, color: theme.colorScheme.onSurface), style: ChainTheme.mono(size: 11, color: theme.colorScheme.onSurface),
), ),
if (!ok && result.fixHint(l).isNotEmpty) ...[ if (!ok && result.fixHint(l).isNotEmpty) ...[
const SizedBox(height: FaiSpace.xs), const SizedBox(height: ChainSpace.xs),
Text( Text(
result.fixHint(l), result.fixHint(l),
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -720,7 +720,7 @@ class _ModelPicker extends StatelessWidget {
valueListenable: controller, valueListenable: controller,
builder: (_, _, _) => TextField( builder: (_, _, _) => TextField(
controller: controller, controller: controller,
style: FaiTheme.mono(size: 12), style: ChainTheme.mono(size: 12),
decoration: InputDecoration( decoration: InputDecoration(
labelText: l.systemAiModelLabel, labelText: l.systemAiModelLabel,
hintText: preset.modelHint.isEmpty hintText: preset.modelHint.isEmpty
@ -738,7 +738,7 @@ class _ModelPicker extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: onRefresh, onPressed: onRefresh,
icon: loading icon: loading
@ -751,7 +751,7 @@ class _ModelPicker extends StatelessWidget {
label: Text(l.systemAiRefresh), label: Text(l.systemAiRefresh),
), ),
if (_isOllama) ...[ if (_isOllama) ...[
const SizedBox(width: FaiSpace.sm), const SizedBox(width: ChainSpace.sm),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: onPull, onPressed: onPull,
icon: pulling icon: pulling
@ -776,7 +776,7 @@ class _ModelPicker extends StatelessWidget {
), ),
], ],
if (models != null) ...[ if (models != null) ...[
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
if (models!.isEmpty) if (models!.isEmpty)
Text( Text(
_isOllama ? l.systemAiNoModelsOllama : l.systemAiNoModelsGeneric, _isOllama ? l.systemAiNoModelsOllama : l.systemAiNoModelsGeneric,
@ -786,8 +786,8 @@ class _ModelPicker extends StatelessWidget {
) )
else ...[ else ...[
Wrap( Wrap(
spacing: FaiSpace.xs, spacing: ChainSpace.xs,
runSpacing: FaiSpace.xs, runSpacing: ChainSpace.xs,
children: [ children: [
for (final id in _sortedBySuitability( for (final id in _sortedBySuitability(
models!, models!,
@ -850,13 +850,13 @@ extension _SuitabilityCopy on _Suitability {
Color color(ColorScheme cs) { Color color(ColorScheme cs) {
switch (this) { switch (this) {
case _Suitability.recommended: case _Suitability.recommended:
return FaiColors.success; return ChainColors.success;
case _Suitability.balanced: case _Suitability.balanced:
return cs.primary; return cs.primary;
case _Suitability.small: case _Suitability.small:
return FaiColors.warning; return ChainColors.warning;
case _Suitability.large: case _Suitability.large:
return FaiColors.warning; return ChainColors.warning;
case _Suitability.huge: case _Suitability.huge:
return cs.error; return cs.error;
case _Suitability.unknown: case _Suitability.unknown:
@ -1005,7 +1005,7 @@ class _ModelChip extends StatelessWidget {
size: suitability == _Suitability.recommended ? 14 : 9, size: suitability == _Suitability.recommended ? 14 : 9,
color: color, color: color,
), ),
label: Text(id, style: FaiTheme.mono(size: 11)), label: Text(id, style: ChainTheme.mono(size: 11)),
side: BorderSide(color: color.withValues(alpha: 0.4)), side: BorderSide(color: color.withValues(alpha: 0.4)),
onPressed: onTap, onPressed: onTap,
), ),
@ -1030,10 +1030,10 @@ class _HardwareBanner extends StatelessWidget {
? l.systemAiHwReviewed(lastReviewed!) ? l.systemAiHwReviewed(lastReviewed!)
: ''; : '';
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Row( child: Row(
@ -1067,7 +1067,7 @@ class _SuitabilityLegend extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
Widget dot(Color c, String label) => Padding( Widget dot(Color c, String label) => Padding(
padding: const EdgeInsets.only(right: FaiSpace.md), padding: const EdgeInsets.only(right: ChainSpace.md),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -1090,11 +1090,11 @@ class _SuitabilityLegend extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(right: FaiSpace.md), padding: const EdgeInsets.only(right: ChainSpace.md),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.star, size: 11, color: FaiColors.success), Icon(Icons.star, size: 11, color: ChainColors.success),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
l.systemAiLegendRecommended(tierTag), l.systemAiLegendRecommended(tierTag),
@ -1107,7 +1107,7 @@ class _SuitabilityLegend extends StatelessWidget {
), ),
), ),
dot(theme.colorScheme.primary, l.systemAiLegendBalanced), dot(theme.colorScheme.primary, l.systemAiLegendBalanced),
dot(FaiColors.warning, l.systemAiLegendSmallLarge), dot(ChainColors.warning, l.systemAiLegendSmallLarge),
dot(theme.colorScheme.error, l.systemAiLegendHuge), dot(theme.colorScheme.error, l.systemAiLegendHuge),
dot(theme.colorScheme.outline, l.systemAiLegendUnknown), dot(theme.colorScheme.outline, l.systemAiLegendUnknown),
], ],
@ -1131,10 +1131,10 @@ class _CacheStatusRow extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
child: Row( child: Row(

View file

@ -55,7 +55,7 @@ class _ThemePickerGridState extends State<ThemePickerGrid> {
builder: (context, snap) { builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) { if (snap.connectionState != ConnectionState.done) {
return const Padding( return const Padding(
padding: EdgeInsets.symmetric(vertical: FaiSpace.sm), padding: EdgeInsets.symmetric(vertical: ChainSpace.sm),
child: SizedBox( child: SizedBox(
width: 14, width: 14,
height: 14, height: 14,
@ -85,7 +85,7 @@ class _ThemePickerGridState extends State<ThemePickerGrid> {
_customTile(theme, l, active), _customTile(theme, l, active),
], ],
), ),
const SizedBox(height: FaiSpace.sm), const SizedBox(height: ChainSpace.sm),
Text( Text(
l.themePluginHint, l.themePluginHint,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
@ -248,13 +248,13 @@ class _Tile extends StatelessWidget {
height: 76, height: 76,
child: Material( child: Material(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.sm), borderRadius: BorderRadius.circular(ChainRadius.sm),
border: Border.all( border: Border.all(
color: selected color: selected
? theme.colorScheme.primary ? theme.colorScheme.primary
@ -432,7 +432,7 @@ class _ColorPickerDialogState extends State<_ColorPickerDialog> {
), ),
], ],
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
TextField( TextField(
controller: _hex, controller: _hex,
decoration: InputDecoration( decoration: InputDecoration(
@ -444,7 +444,7 @@ class _ColorPickerDialogState extends State<_ColorPickerDialog> {
onChanged: _commitHex, onChanged: _commitHex,
onSubmitted: _commitHex, onSubmitted: _commitHex,
), ),
const SizedBox(height: FaiSpace.md), const SizedBox(height: ChainSpace.md),
Text(l.themePluginCustomPreview, style: theme.textTheme.labelSmall), Text(l.themePluginCustomPreview, style: theme.textTheme.labelSmall),
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(

View file

@ -4,17 +4,17 @@
// primitives directly no Material `Card` / generic `Container` // primitives directly no Material `Card` / generic `Container`
// soup in page code. // soup in page code.
export 'fai_binary_recovery.dart'; export 'chain_binary_recovery.dart';
export 'fai_card.dart'; export 'chain_card.dart';
export 'fai_data_row.dart'; export 'chain_data_row.dart';
export 'fai_delta_mark.dart'; export 'chain_delta_mark.dart';
export 'fai_empty_state.dart'; export 'chain_empty_state.dart';
export 'fai_en_badge.dart'; export 'chain_en_badge.dart';
export 'fai_error_box.dart'; export 'chain_error_box.dart';
export 'fai_flow_output.dart'; export 'chain_flow_output.dart';
export 'fai_log_viewer.dart'; export 'chain_log_viewer.dart';
export 'fai_module_sheet.dart'; export 'chain_module_sheet.dart';
export 'fai_pill.dart'; export 'chain_pill.dart';
export 'fai_settings_dialog.dart'; export 'chain_settings_dialog.dart';
export 'fai_status_dot.dart'; export 'chain_status_dot.dart';
export 'fai_system_ai_editor.dart'; export 'chain_system_ai_editor.dart';

View file

@ -1,36 +1,36 @@
// Unit tests for FaiLog the central error log that // Unit tests for ChainLog the central error log that
// `~/.fai/logs/studio-errors.log` is the operator-visible // `~/.fai/logs/studio-errors.log` is the operator-visible
// surface of. The tests redirect the singleton at a temp file // surface of. The tests redirect the singleton at a temp file
// via `FaiLog.testPathOverride` so the real log under HOME is // via `ChainLog.testPathOverride` so the real log under HOME is
// never touched. // never touched.
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:chain_studio/data/fai_log.dart'; import 'package:chain_studio/data/chain_log.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
group('FaiLog', () { group('ChainLog', () {
late Directory tmp; late Directory tmp;
late File logFile; late File logFile;
setUp(() { setUp(() {
tmp = Directory.systemTemp.createTempSync('fai_log_test_'); tmp = Directory.systemTemp.createTempSync('chain_log_test_');
logFile = File('${tmp.path}/studio-errors.log'); logFile = File('${tmp.path}/studio-errors.log');
FaiLog.testPathOverride = logFile.path; ChainLog.testPathOverride = logFile.path;
}); });
tearDown(() { tearDown(() {
FaiLog.testPathOverride = null; ChainLog.testPathOverride = null;
try { try {
tmp.deleteSync(recursive: true); tmp.deleteSync(recursive: true);
} catch (_) {/* best-effort */} } catch (_) {/* best-effort */}
}); });
test('append writes one JSON-shaped line per event', () async { test('append writes one JSON-shaped line per event', () async {
await FaiLog.instance.error('flows.run', 'boom'); await ChainLog.instance.error('flows.run', 'boom');
final lines = await FaiLog.instance.tail(); final lines = await ChainLog.instance.tail();
expect(lines, hasLength(1)); expect(lines, hasLength(1));
final entry = jsonDecode(lines.single) as Map<String, Object?>; final entry = jsonDecode(lines.single) as Map<String, Object?>;
expect(entry['source'], 'flows.run'); expect(entry['source'], 'flows.run');
@ -40,22 +40,22 @@ void main() {
}); });
test('context field is preserved when supplied', () async { test('context field is preserved when supplied', () async {
await FaiLog.instance.error( await ChainLog.instance.error(
'theme.plugin.load', 'theme.plugin.load',
Exception('endpoint unreachable'), Exception('endpoint unreachable'),
context: 'capability=studio.theme.space', context: 'capability=studio.theme.space',
); );
final lines = await FaiLog.instance.tail(); final lines = await ChainLog.instance.tail();
final entry = jsonDecode(lines.single) as Map<String, Object?>; final entry = jsonDecode(lines.single) as Map<String, Object?>;
expect(entry['context'], 'capability=studio.theme.space'); expect(entry['context'], 'capability=studio.theme.space');
expect(entry['error'], contains('endpoint unreachable')); expect(entry['error'], contains('endpoint unreachable'));
}); });
test('tail returns oldest first', () async { test('tail returns oldest first', () async {
await FaiLog.instance.error('a', '1'); await ChainLog.instance.error('a', '1');
await FaiLog.instance.error('b', '2'); await ChainLog.instance.error('b', '2');
await FaiLog.instance.error('c', '3'); await ChainLog.instance.error('c', '3');
final lines = await FaiLog.instance.tail(); final lines = await ChainLog.instance.tail();
expect(lines, hasLength(3)); expect(lines, hasLength(3));
final sources = lines.map((l) => jsonDecode(l)['source']).toList(); final sources = lines.map((l) => jsonDecode(l)['source']).toList();
expect(sources, ['a', 'b', 'c']); expect(sources, ['a', 'b', 'c']);
@ -63,9 +63,9 @@ void main() {
test('tail caps output at maxLines and keeps the newest', () async { test('tail caps output at maxLines and keeps the newest', () async {
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
await FaiLog.instance.error('burst', 'event-$i'); await ChainLog.instance.error('burst', 'event-$i');
} }
final lines = await FaiLog.instance.tail(maxLines: 5); final lines = await ChainLog.instance.tail(maxLines: 5);
expect(lines, hasLength(5)); expect(lines, hasLength(5));
final last = jsonDecode(lines.last) as Map<String, Object?>; final last = jsonDecode(lines.last) as Map<String, Object?>;
expect(last['error'], 'event-19'); expect(last['error'], 'event-19');
@ -77,7 +77,7 @@ void main() {
// `.log.1` and a short fresh `.log`. // `.log.1` and a short fresh `.log`.
logFile.parent.createSync(recursive: true); logFile.parent.createSync(recursive: true);
logFile.writeAsBytesSync(List.filled(257 * 1024, 0x20)); logFile.writeAsBytesSync(List.filled(257 * 1024, 0x20));
await FaiLog.instance.error('rotation', 'after-cap'); await ChainLog.instance.error('rotation', 'after-cap');
expect(File('${logFile.path}.1').existsSync(), isTrue); expect(File('${logFile.path}.1').existsSync(), isTrue);
final newSize = logFile.lengthSync(); final newSize = logFile.lengthSync();
expect(newSize, lessThan(2 * 1024)); expect(newSize, lessThan(2 * 1024));
@ -86,16 +86,16 @@ void main() {
}); });
test('path getter returns the override when set', () { test('path getter returns the override when set', () {
expect(FaiLog.instance.path, logFile.path); expect(ChainLog.instance.path, logFile.path);
}); });
test('writes failing silently does not throw', () async { test('writes failing silently does not throw', () async {
// Redirect to a deliberately impossible path. The error() // Redirect to a deliberately impossible path. The error()
// call must complete cleanly; the operator UI must never // call must complete cleanly; the operator UI must never
// crash because the disk is full / read-only / missing. // crash because the disk is full / read-only / missing.
FaiLog.testPathOverride = '/this/path/does/not/exist/ever/x.log'; ChainLog.testPathOverride = '/this/path/does/not/exist/ever/x.log';
await expectLater( await expectLater(
FaiLog.instance.error('bad.path', 'boom'), ChainLog.instance.error('bad.path', 'boom'),
completes, completes,
); );
}); });

View file

@ -35,14 +35,14 @@ flutter test test/integration/
``` ```
Prereq: a `fai` binary on PATH or at Prereq: a `fai` binary on PATH or at
`../fai_platform/target/release/fai`. The harness skips with a `../fai_chain/target/release/chain`. The harness skips with a
clear message when neither exists, so this command does not clear message when neither exists, so this command does not
fail on a fresh checkout — it just reports skipped tests. fail on a fresh checkout — it just reports skipped tests.
To get the binary: To get the binary:
```bash ```bash
cd ../fai_platform cd ../fai_chain
cargo build --release --bin fai cargo build --release --bin fai
``` ```
@ -52,11 +52,11 @@ A cold `chain serve` spends its first ~30s building the
curated-model database and initialising SQLite migrations. curated-model database and initialising SQLite migrations.
`HubFixture.start()` waits up to 60s by default; if your local `HubFixture.start()` waits up to 60s by default; if your local
hub takes longer the first time, run `chain serve` once by hand hub takes longer the first time, run `chain serve` once by hand
against any temp `FAI_DATA_DIR` to warm the per-user cargo / against any temp `CHAIN_DATA_DIR` to warm the per-user cargo /
SBOM caches: SBOM caches:
```bash ```bash
FAI_DATA_DIR=/tmp/fai_warmup chain serve --bind 127.0.0.1:0 CHAIN_DATA_DIR=/tmp/chain_warmup chain serve --bind 127.0.0.1:0
# wait for "hub started", Ctrl-C # wait for "hub started", Ctrl-C
``` ```
@ -66,7 +66,7 @@ Subsequent integration-test runs are quick.
`.forgejo/workflows/ci.yml` doesn't run these yet. Adding them `.forgejo/workflows/ci.yml` doesn't run these yet. Adding them
needs an artifact-passing pattern: the platform build job needs an artifact-passing pattern: the platform build job
publishes `target/release/fai` as a CI artifact; the studio publishes `target/release/chain` as a CI artifact; the studio
test job consumes it. Pattern is straightforward once we want test job consumes it. Pattern is straightforward once we want
it; it's deferred because we don't yet have enough integration it; it's deferred because we don't yet have enough integration
tests to justify the CI runtime. tests to justify the CI runtime.

View file

@ -22,7 +22,7 @@
// flutter test test/integration/ // flutter test test/integration/
// //
// CI does not yet run these by default they need a built `fai` // CI does not yet run these by default they need a built `fai`
// binary in PATH or in ../fai_platform/target/release/. Wire that // binary in PATH or in ../chain_platform/target/release/. Wire that
// in via .forgejo/workflows/ci.yml once the platform CI publishes // in via .forgejo/workflows/ci.yml once the platform CI publishes
// a Linux binary as an artifact for downstream jobs to consume. // a Linux binary as an artifact for downstream jobs to consume.

View file

@ -72,7 +72,7 @@ class HubFixture {
if (skipIfBinaryMissing) { if (skipIfBinaryMissing) {
markTestSkipped( markTestSkipped(
'Hub fixture skipped: no `fai` binary found on PATH or in ' 'Hub fixture skipped: no `fai` binary found on PATH or in '
'../fai_platform/target/release/fai. Build it with ' '../chain_platform/target/release/fai. Build it with '
'`cargo build --release` and re-run.', '`cargo build --release` and re-run.',
); );
return null; return null;
@ -86,15 +86,15 @@ class HubFixture {
// Run as a child process. `serve --bind` lets us avoid the // Run as a child process. `serve --bind` lets us avoid the
// default 50051 (and any local daemon the developer happens // default 50051 (and any local daemon the developer happens
// to be running). FAI_DATA_DIR keeps the temp footprint // to be running). CHAIN_DATA_DIR keeps the temp footprint
// contained no state leaks into ~/.fai. // contained no state leaks into ~/.fai.
final process = await Process.start( final process = await Process.start(
binary, binary,
['serve', '--bind', addr], ['serve', '--bind', addr],
environment: { environment: {
...Platform.environment, ...Platform.environment,
'FAI_DATA_DIR': tempDir.path, 'CHAIN_DATA_DIR': tempDir.path,
'FAI_MODULES_DIR': '${tempDir.path}/modules', 'CHAIN_MODULES_DIR': '${tempDir.path}/modules',
// Suppress info logs without a CLI flag the binary // Suppress info logs without a CLI flag the binary
// respects RUST_LOG for tracing-subscriber. // respects RUST_LOG for tracing-subscriber.
'RUST_LOG': 'warn', 'RUST_LOG': 'warn',
@ -136,7 +136,7 @@ class HubFixture {
await tempDir.delete(recursive: true); await tempDir.delete(recursive: true);
throw StateError( throw StateError(
'Hub did not become healthy within $readyTimeout. ' 'Hub did not become healthy within $readyTimeout. '
'Check `fai serve` works manually with FAI_DATA_DIR=$tempDir.', 'Check `fai serve` works manually with CHAIN_DATA_DIR=$tempDir.',
); );
} }
@ -172,8 +172,8 @@ class HubFixture {
} }
final candidate = Platform.isWindows final candidate = Platform.isWindows
? '../fai_platform/target/release/fai.exe' ? '../chain_platform/target/release/fai.exe'
: '../fai_platform/target/release/fai'; : '../chain_platform/target/release/fai';
final f = File(candidate); final f = File(candidate);
if (await f.exists()) { if (await f.exists()) {
return f.absolute.path; return f.absolute.path;

View file

@ -10,9 +10,9 @@
// flutter test test/integration/plugin_theme_test.dart // flutter test test/integration/plugin_theme_test.dart
// //
// Pre-reqs (handled by HubFixture's skip-when-missing path): // Pre-reqs (handled by HubFixture's skip-when-missing path):
// - fai binary on PATH or at ../fai_platform/target/release/fai // - fai binary on PATH or at ../chain_platform/target/release/fai
// - studio-theme-solarized plugin built to module.wasm in its // - studio-theme-solarized plugin built to module.wasm in its
// fai_plugins dir. // chain_plugins dir.
import 'dart:io'; import 'dart:io';
@ -33,7 +33,7 @@ void main() {
// add network + bundle-fetch latency that doesn't // add network + bundle-fetch latency that doesn't
// belong in an in-process round-trip test. // belong in an in-process round-trip test.
final pluginSrc = Directory( final pluginSrc = Directory(
'../fai_plugins/studio-theme-solarized', '../chain_plugins/studio-theme-solarized',
); );
if (!await pluginSrc.exists()) { if (!await pluginSrc.exists()) {
markTestSkipped('studio-theme-solarized source not available'); markTestSkipped('studio-theme-solarized source not available');

View file

@ -7,7 +7,7 @@ import 'package:chain_studio/l10n/app_localizations.dart';
import 'package:chain_studio/theme/theme.dart'; import 'package:chain_studio/theme/theme.dart';
/// Recreates the exact DocReaderSheet code path that /// Recreates the exact DocReaderSheet code path that
/// welcome.dart uses, including FaiTheme.markdownStyle, so a /// welcome.dart uses, including ChainTheme.markdownStyle, so a
/// regression in either rootBundle, the markdown parser, or /// regression in either rootBundle, the markdown parser, or
/// the style sheet surfaces here instead of only at runtime /// the style sheet surfaces here instead of only at runtime
/// when an operator clicks a doc card. /// when an operator clicks a doc card.
@ -16,7 +16,7 @@ void main() {
testWidgets('DocReaderSheet renders all four german + english docs', testWidgets('DocReaderSheet renders all four german + english docs',
(tester) async { (tester) async {
final theme = FaiTheme.light(); final theme = ChainTheme.light();
for (final slug in ['architecture', 'security', 'audit', 'flows']) { for (final slug in ['architecture', 'security', 'audit', 'flows']) {
for (final locale in ['de', 'en']) { for (final locale in ['de', 'en']) {
final path = locale == 'de' final path = locale == 'de'
@ -39,7 +39,7 @@ void main() {
data: text, data: text,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
selectable: true, selectable: true,
styleSheet: FaiTheme.markdownStyle(theme), styleSheet: ChainTheme.markdownStyle(theme),
), ),
), ),
)); ));

View file

@ -51,10 +51,10 @@ A starter plist lives at `launchd/ai.flemming.chain.today.plist`.
| Variable | Default | Example | | Variable | Default | Example |
|-------------------|----------------------------------|-------------------------------------------------| |-------------------|----------------------------------|-------------------------------------------------|
| `FAI_TODAY_API` | `http://127.0.0.1:11434/api/generate` | `https://api.openai.com/v1/chat/completions` | | `CHAIN_TODAY_API` | `http://127.0.0.1:11434/api/generate` | `https://api.openai.com/v1/chat/completions` |
| `FAI_TODAY_MODEL` | `gemma3:4b` | `gpt-4o-mini` | | `CHAIN_TODAY_MODEL` | `gemma3:4b` | `gpt-4o-mini` |
| `FAI_TODAY_KEY` | (unset) | `$OPENAI_API_KEY` | | `CHAIN_TODAY_KEY` | (unset) | `$OPENAI_API_KEY` |
| `FAI_TODAY_N` | `3` | `1` (single shot) | | `CHAIN_TODAY_N` | `3` | `1` (single shot) |
OpenAI-compatible endpoints will work as long as they speak `chat/completions` OpenAI-compatible endpoints will work as long as they speak `chat/completions`
or `generate`; the script auto-detects from the URL path. or `generate`; the script auto-detects from the URL path.

View file

@ -9,7 +9,7 @@
# Does not read the audit log without --with-audit. # Does not read the audit log without --with-audit.
set -euo pipefail set -euo pipefail
SINCE="${FAI_TODAY_SINCE:-24 hours ago}" SINCE="${CHAIN_TODAY_SINCE:-24 hours ago}"
# Walk every fai-* directory next to chain_studio so the collector # Walk every fai-* directory next to chain_studio so the collector
# works from any of the sibling checkouts. Adjust here if the # works from any of the sibling checkouts. Adjust here if the
@ -31,10 +31,10 @@ for repo in "$ROOT"/fai_*/; do
done done
emit_section "STORE INDEX SEED CHANGES" emit_section "STORE INDEX SEED CHANGES"
seed="$ROOT/fai_platform/crates/fai_hub/store-index/seed.yaml" seed="$ROOT/fai_chain/crates/chain_hub/store-index/seed.yaml"
if [ -f "$seed" ] && [ -d "$ROOT/fai_platform/.git" ]; then if [ -f "$seed" ] && [ -d "$ROOT/fai_chain/.git" ]; then
diff=$(git -C "$ROOT/fai_platform" log --since="$SINCE" --oneline -- \ diff=$(git -C "$ROOT/fai_chain" log --since="$SINCE" --oneline -- \
crates/fai_hub/store-index/seed.yaml 2>/dev/null || true) crates/chain_hub/store-index/seed.yaml 2>/dev/null || true)
if [ -n "$diff" ]; then if [ -n "$diff" ]; then
printf '%s\n' "$diff" printf '%s\n' "$diff"
else else
@ -43,9 +43,9 @@ if [ -f "$seed" ] && [ -d "$ROOT/fai_platform/.git" ]; then
fi fi
emit_section "ARCHITECTURE / SYSTEM-GAPS CHANGES" emit_section "ARCHITECTURE / SYSTEM-GAPS CHANGES"
arch_dir="$ROOT/fai_platform/docs" arch_dir="$ROOT/fai_chain/docs"
if [ -d "$arch_dir" ] && [ -d "$ROOT/fai_platform/.git" ]; then if [ -d "$arch_dir" ] && [ -d "$ROOT/fai_chain/.git" ]; then
changes=$(git -C "$ROOT/fai_platform" log --since="$SINCE" --oneline -- \ changes=$(git -C "$ROOT/fai_chain" log --since="$SINCE" --oneline -- \
docs/architecture docs/advanced 2>/dev/null || true) docs/architecture docs/advanced 2>/dev/null || true)
if [ -n "$changes" ]; then if [ -n "$changes" ]; then
printf '%s\n' "$changes" printf '%s\n' "$changes"
@ -67,9 +67,9 @@ if [ -d "$studio/.git" ]; then
fi fi
emit_section "AUDIT-LOG HIGHLIGHTS" emit_section "AUDIT-LOG HIGHLIGHTS"
if [ "${FAI_TODAY_WITH_AUDIT:-0}" = "1" ] && command -v fai >/dev/null 2>&1; then if [ "${CHAIN_TODAY_WITH_AUDIT:-0}" = "1" ] && command -v fai >/dev/null 2>&1; then
fai audit recent --limit 20 2>/dev/null | head -40 || \ fai audit recent --limit 20 2>/dev/null | head -40 || \
printf '(audit query failed; daemon may be stopped)\n' printf '(audit query failed; daemon may be stopped)\n'
else else
printf '(audit pull disabled — set FAI_TODAY_WITH_AUDIT=1 to opt in)\n' printf '(audit pull disabled — set CHAIN_TODAY_WITH_AUDIT=1 to opt in)\n'
fi fi

View file

@ -5,17 +5,17 @@
# Defaults to a local Ollama at 127.0.0.1:11434 with gemma3:4b. # Defaults to a local Ollama at 127.0.0.1:11434 with gemma3:4b.
# See README.md for env-var overrides. # See README.md for env-var overrides.
# #
# Output: ~/.fai/today/proposals/<ISO-DATE>/candidate-<n>.yaml # Output: ~/.chain/today/proposals/<ISO-DATE>/candidate-<n>.yaml
# Operator then runs accept.sh with the chosen file. # Operator then runs accept.sh with the chosen file.
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
DATE="$(date -u +%Y-%m-%d)" DATE="$(date -u +%Y-%m-%d)"
OUT_DIR="${FAI_TODAY_OUT:-$HOME/.fai/today/proposals/$DATE}" OUT_DIR="${CHAIN_TODAY_OUT:-$HOME/.chain/today/proposals/$DATE}"
N="${FAI_TODAY_N:-3}" N="${CHAIN_TODAY_N:-3}"
API="${FAI_TODAY_API:-http://127.0.0.1:11434/api/generate}" API="${CHAIN_TODAY_API:-http://127.0.0.1:11434/api/generate}"
MODEL="${FAI_TODAY_MODEL:-gemma3:4b}" MODEL="${CHAIN_TODAY_MODEL:-gemma3:4b}"
KEY="${FAI_TODAY_KEY:-}" KEY="${CHAIN_TODAY_KEY:-}"
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"