feat(studio): rich theme picker (presets + custom colour) + editor 0.11.0
Some checks failed
Security / Security check (push) Failing after 1s

Settings dialog's Theme Plugin section is now a grid of
swatched tiles:

- Built-in (none) — falls back to FaiTheme.light/.dark
- One tile per installed studio.theme.* plugin, each
  showing the plugin's primary/secondary/tertiary as
  live colour dots. Tile loads its preview lazily so a
  dozen installed themes don't block the picker.
- Custom — opens a colour-picker dialog with 12 curated
  Material presets + a hex input + live preview. Selecting
  applies ColorScheme.fromSeed for both brightnesses.

main.dart's _pluginThemes parses a 'custom:#RRGGBB' sigil
in the same notifier slot as plugin capability ids, so the
existing persistence + restoration paths cover the custom
case with no new state.

Bumps editor to 0.11.0 (type-checked port connections +
dynamic card width fix + card-height border allowance) and
Studio to 0.58.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-02 01:29:34 +02:00
parent 69864da934
commit c1c60d5434
30 changed files with 1280 additions and 846 deletions

View file

@ -21,6 +21,7 @@ class _ApprovalsPageState extends State<ApprovalsPage>
late final TabController _tab;
late Future<List<ApprovalRecord>> _pendingFuture;
late Future<List<ApprovalRecord>> _historyFuture;
/// IDs the operator currently has multi-selected for a
/// batch operation. Empty set hides the batch action bar
/// and falls back to per-row Approve / Reject buttons.
@ -32,7 +33,8 @@ class _ApprovalsPageState extends State<ApprovalsPage>
late final String _reviewer = _defaultReviewer();
static String _defaultReviewer() {
final user = Platform.environment['USER'] ??
final user =
Platform.environment['USER'] ??
Platform.environment['USERNAME'] ??
'studio';
return '$user@studio';
@ -124,9 +126,11 @@ class _ApprovalsPageState extends State<ApprovalsPage>
}
if (!mounted) return;
setState(() => _batchInFlight = false);
_toast(failed == 0
? l.approvalsBatchApproveDoneToast(ok)
: l.approvalsBatchPartialFailure(ok, failed));
_toast(
failed == 0
? l.approvalsBatchApproveDoneToast(ok)
: l.approvalsBatchPartialFailure(ok, failed),
);
_refresh();
}
@ -148,9 +152,11 @@ class _ApprovalsPageState extends State<ApprovalsPage>
}
if (!mounted) return;
setState(() => _batchInFlight = false);
_toast(failed == 0
? l.approvalsBatchRejectDoneToast(ok)
: l.approvalsBatchPartialFailure(ok, failed));
_toast(
failed == 0
? l.approvalsBatchRejectDoneToast(ok)
: l.approvalsBatchPartialFailure(ok, failed),
);
_refresh();
}
@ -231,10 +237,7 @@ class _ApprovalsPageState extends State<ApprovalsPage>
onBatchReject: _batchReject,
onRetry: _refresh,
),
_HistoryList(
future: _historyFuture,
onRetry: _refresh,
),
_HistoryList(future: _historyFuture, onRetry: _refresh),
],
),
);
@ -391,8 +394,11 @@ class _BatchActionBar extends StatelessWidget {
),
child: Row(
children: [
Icon(Icons.check_box_outlined,
size: 18, color: theme.colorScheme.primary),
Icon(
Icons.check_box_outlined,
size: 18,
color: theme.colorScheme.primary,
),
const SizedBox(width: FaiSpace.sm),
Text(
l.approvalsBatchSelected(selectedCount),
@ -546,10 +552,7 @@ class _ApprovalCard extends StatelessWidget {
],
),
const SizedBox(height: FaiSpace.lg),
Text(
approval.prompt,
style: theme.textTheme.bodyLarge,
),
Text(approval.prompt, style: theme.textTheme.bodyLarge),
if (approval.payloadPreview != null) ...[
const SizedBox(height: FaiSpace.md),
Padding(
@ -648,8 +651,7 @@ class _HistoryRow extends StatelessWidget {
final theme = Theme.of(context);
final (tone, icon, label) = _statusPresentation(context, record.status);
final decided = record.decidedAt;
final timeStr =
decided == null ? '' : _formatTimestamp(decided.toLocal());
final timeStr = decided == null ? '' : _formatTimestamp(decided.toLocal());
return InkWell(
onTap: () => showDialog<void>(
context: context,
@ -728,8 +730,10 @@ class _HistoryDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final (tone, icon, label) =
_HistoryRow._statusPresentation(context, record.status);
final (tone, icon, label) = _HistoryRow._statusPresentation(
context,
record.status,
);
return AlertDialog(
title: Row(
children: [
@ -752,14 +756,23 @@ class _HistoryDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (record.decidedAt != null)
_kv(theme, AppLocalizations.of(context)!.approvalsDialogDecided,
'${_formatTimestamp(record.decidedAt!.toLocal())} '
'· ${record.decidedBy.isEmpty ? "(unknown)" : record.decidedBy}'),
_kv(theme, AppLocalizations.of(context)!.approvalsDialogCreated,
_formatTimestamp(record.createdAt.toLocal())),
_kv(
theme,
AppLocalizations.of(context)!.approvalsDialogDecided,
'${_formatTimestamp(record.decidedAt!.toLocal())} '
'· ${record.decidedBy.isEmpty ? "(unknown)" : record.decidedBy}',
),
_kv(
theme,
AppLocalizations.of(context)!.approvalsDialogCreated,
_formatTimestamp(record.createdAt.toLocal()),
),
if (record.reason.isNotEmpty)
_kv(theme, AppLocalizations.of(context)!.approvalsDialogReason,
record.reason),
_kv(
theme,
AppLocalizations.of(context)!.approvalsDialogReason,
record.reason,
),
const SizedBox(height: FaiSpace.md),
Text(
AppLocalizations.of(context)!.approvalsDialogPrompt,
@ -769,10 +782,7 @@ class _HistoryDialog extends StatelessWidget {
),
),
const SizedBox(height: 4),
SelectableText(
record.prompt,
style: theme.textTheme.bodyMedium,
),
SelectableText(record.prompt, style: theme.textTheme.bodyMedium),
if (record.payloadPreview != null) ...[
const SizedBox(height: FaiSpace.md),
Text(
@ -834,12 +844,7 @@ class _HistoryDialog extends StatelessWidget {
),
),
),
Expanded(
child: SelectableText(
v,
style: theme.textTheme.bodySmall,
),
),
Expanded(child: SelectableText(v, style: theme.textTheme.bodySmall)),
],
),
);