diff --git a/playground/lib/main.dart b/playground/lib/main.dart index 2b23b3c..5fc9707 100644 --- a/playground/lib/main.dart +++ b/playground/lib/main.dart @@ -38,8 +38,8 @@ class _PlaygroundAppState extends State { title: 'Flow UI Playground · Flutter UI library for AI Chat Interfaces', debugShowCheckedModeBanner: false, themeMode: _mode, - // The chrome paints itself from ShellPalette; the FlowTheme - // extensions are for the component demos on the stage. + // Chrome and stage demos alike paint from the FlowTheme extensions, + // so the workbench and the exhibit flip together. theme: ThemeData( brightness: Brightness.light, extensions: [FlowTheme.light()], diff --git a/playground/lib/src/code_panel.dart b/playground/lib/src/code_panel.dart index 0c48333..66c0392 100644 --- a/playground/lib/src/code_panel.dart +++ b/playground/lib/src/code_panel.dart @@ -13,7 +13,7 @@ import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; import 'demo_registry.dart'; import 'playground_item.dart'; -import 'shell_palette.dart'; +import 'shell_text.dart'; /// The slide-in code panel, resizable by its left edge. The snippet is /// typeset in the package's code block style — the built-in highlighter @@ -85,7 +85,7 @@ class _CodePanelState extends State { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; final code = snippetFor(widget.item, variant: widget.variant); return AnimatedContainer( @@ -96,9 +96,9 @@ class _CodePanelState extends State { width: widget.open ? _width : 0, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: shell.codeBg, + color: colors.surfaceBright, border: widget.open - ? Border(left: BorderSide(color: shell.border)) + ? Border(left: BorderSide(color: colors.outlineVariant)) : const Border(), ), // Fixed-width inner pane so the content doesn't reflow while the @@ -112,7 +112,7 @@ class _CodePanelState extends State { maxWidth: _width, child: Stack( children: [ - Positioned.fill(child: _pane(shell, code)), + Positioned.fill(child: _pane(code)), // The resize handle rides the pane's left edge: drag to // widen between the min and max. PositionedDirectional( @@ -149,11 +149,7 @@ class _CodePanelState extends State { } /// The header's 24px action chip — copy and close share the form. - Widget _headerChip( - ShellPalette shell, { - required VoidCallback onTap, - required Widget child, - }) { + Widget _headerChip({required VoidCallback onTap, required Widget child}) { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( @@ -163,7 +159,7 @@ class _CodePanelState extends State { height: 24, alignment: Alignment.center, decoration: BoxDecoration( - color: shell.codeChip, + color: context.flowColors.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(6)), ), child: child, @@ -172,7 +168,8 @@ class _CodePanelState extends State { ); } - Widget _pane(ShellPalette shell, String code) { + Widget _pane(String code) { + final colors = context.flowColors; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -186,7 +183,7 @@ class _CodePanelState extends State { size: 12, weight: FontWeight.w600, letterSpacing: 0.5, - color: shell.codeHeaderText, + color: colors.onSurfaceMuted, ), ), const Spacer(), @@ -196,25 +193,21 @@ class _CodePanelState extends State { Tooltip( message: 'Copy code', child: _headerChip( - shell, onTap: () => _copy(code), child: Icon( _copied ? Icons.check : Icons.copy_outlined, size: 14, - color: _copied - ? context.flowColors.primary - : shell.codeHeaderText, + color: _copied ? colors.primary : colors.onSurfaceMuted, ), ), ), const SizedBox(width: 8), _headerChip( - shell, onTap: widget.onClose, child: Icon( PhosphorIconsRegular.x, size: 14, - color: shell.codeHeaderText, + color: colors.onSurfaceMuted, ), ), ], diff --git a/playground/lib/src/shell_palette.dart b/playground/lib/src/shell_palette.dart deleted file mode 100644 index b9bc6b8..0000000 --- a/playground/lib/src/shell_palette.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:material_ui/material_ui.dart'; - -/// The playground's accent — the design's pink, drawn on the logo tile and -/// the active sidebar icon. -const Color shellAccent = Color(0xFFED74A9); - -/// A chrome text style in Figtree (bundled by flow_ui). -TextStyle shellText({ - double? size, - FontWeight? weight, - Color? color, - double? letterSpacing, - double? height, -}) { - return TextStyle( - fontFamily: 'Figtree', - package: 'flow_ui', - fontSize: size, - fontWeight: weight, - color: color, - letterSpacing: letterSpacing, - height: height, - ); -} - -/// The playground chrome's own palette, straight from the Claude Design -/// prototype — deliberately separate from flow_ui's tokens: the chrome is -/// the workbench, not the exhibit. -@immutable -class ShellPalette { - const ShellPalette({ - required this.topBarBg, - required this.sidebarBg, - required this.border, - required this.chip, - required this.segmentActiveBg, - required this.segmentShadow, - required this.text, - required this.sectionLabel, - required this.iconRest, - required this.segmentRest, - required this.navText, - required this.navHover, - required this.navActiveBg, - required this.canvas, - required this.stageBg, - required this.pillBorder, - required this.codeBg, - required this.codeHeaderBg, - required this.codeBorder, - required this.codeHeaderText, - required this.codeText, - required this.codeChip, - }); - - final Color topBarBg; - final Color sidebarBg; - final Color border; - final Color chip; - final Color segmentActiveBg; - final Color segmentShadow; - final Color text; - final Color sectionLabel; - final Color iconRest; - final Color segmentRest; - final Color navText; - final Color navHover; - final Color navActiveBg; - final Color canvas; - final Color stageBg; - final Color pillBorder; - final Color codeBg; - final Color codeHeaderBg; - final Color codeBorder; - final Color codeHeaderText; - final Color codeText; - final Color codeChip; - - static const ShellPalette light = ShellPalette( - topBarBg: Color(0xFFFFFFFF), - sidebarBg: Color(0xFFFCFBFA), - border: Color(0xFFECEBE7), - chip: Color(0xFFF2F1ED), - segmentActiveBg: Color(0xFFFFFFFF), - segmentShadow: Color(0x1A000000), - text: Color(0xFF1F1E1B), - sectionLabel: Color(0xFFABA9A2), - iconRest: Color(0xFFABA9A2), - segmentRest: Color(0xFF76746E), - navText: Color(0xFF55534E), - navHover: Color(0xFFF4F3EF), - navActiveBg: Color(0xFFF1F0EC), - canvas: Color(0xFFF0EFEA), - stageBg: Color(0xFFF5F4F1), - pillBorder: Color(0xFFE7E6E1), - codeBg: Color(0xFFFFFFFF), - codeHeaderBg: Color(0xFFFBFAF8), - codeBorder: Color(0xFFEFEEE9), - codeHeaderText: Color(0xFF8E8C86), - codeText: Color(0xFF3B3A35), - codeChip: Color(0xFFF2F1ED), - ); - - static const ShellPalette dark = ShellPalette( - topBarBg: Color(0xFF1E1D1B), - sidebarBg: Color(0xFF1A1917), - border: Color(0xFF31302C), - chip: Color(0xFF2C2B29), - segmentActiveBg: Color(0xFF403E3A), - segmentShadow: Color(0x59000000), - text: Color(0xFFEDECE8), - sectionLabel: Color(0xFF787670), - iconRest: Color(0xFF8A8881), - segmentRest: Color(0xFF9C9A93), - navText: Color(0xFFC6C4BE), - navHover: Color(0xFF2A2927), - navActiveBg: Color(0xFF2E2D2A), - canvas: Color(0xFF131211), - stageBg: Color(0xFF1A1918), - pillBorder: Color(0xFF3B3936), - codeBg: Color(0xFF181715), - codeHeaderBg: Color(0x05FFFFFF), - codeBorder: Color(0x12FFFFFF), - codeHeaderText: Color(0xFF8F8D86), - codeText: Color(0xFFD8D6D0), - codeChip: Color(0x17FFFFFF), - ); - - static ShellPalette of(BuildContext context) { - return Theme.of(context).brightness == Brightness.dark ? dark : light; - } -} diff --git a/playground/lib/src/shell_text.dart b/playground/lib/src/shell_text.dart new file mode 100644 index 0000000..06fb937 --- /dev/null +++ b/playground/lib/src/shell_text.dart @@ -0,0 +1,22 @@ +import 'package:material_ui/material_ui.dart'; + +/// A chrome text style in Figtree (bundled by flow_ui). The chrome keeps +/// its own bespoke sizes from the Claude Design prototype, but draws them +/// in the package's face and colors them from [FlowColors] tokens. +TextStyle shellText({ + double? size, + FontWeight? weight, + Color? color, + double? letterSpacing, + double? height, +}) { + return TextStyle( + fontFamily: 'Figtree', + package: 'flow_ui', + fontSize: size, + fontWeight: weight, + color: color, + letterSpacing: letterSpacing, + height: height, + ); +} diff --git a/playground/lib/src/sidebar.dart b/playground/lib/src/sidebar.dart index 795b849..e30fb87 100644 --- a/playground/lib/src/sidebar.dart +++ b/playground/lib/src/sidebar.dart @@ -1,10 +1,11 @@ +import 'package:flow_ui/flow_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:material_ui/material_ui.dart'; import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; import 'open_docs.dart'; import 'playground_item.dart'; -import 'shell_palette.dart'; +import 'shell_text.dart'; /// The 214px navigation rail: an Examples section, then the component /// list, and — on the web, where the docs share the origin — a Docs link @@ -17,15 +18,15 @@ class Sidebar extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; final examples = PlaygroundItem.values.where((i) => i.isExample); final components = PlaygroundItem.values.where((i) => !i.isExample); return Container( width: 214, decoration: BoxDecoration( - color: shell.sidebarBg, - border: Border(right: BorderSide(color: shell.border)), + color: colors.surfaceBright, + border: Border(right: BorderSide(color: colors.outlineVariant)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -56,7 +57,7 @@ class Sidebar extends StatelessWidget { if (kIsWeb) Container( decoration: BoxDecoration( - border: Border(top: BorderSide(color: shell.border)), + border: Border(top: BorderSide(color: colors.outlineVariant)), ), padding: const EdgeInsets.fromLTRB(10, 9, 10, 13), child: Column( @@ -105,7 +106,7 @@ class _FooterLinkState extends State<_FooterLink> { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; return MouseRegion( cursor: SystemMouseCursors.click, @@ -116,7 +117,7 @@ class _FooterLinkState extends State<_FooterLink> { child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), decoration: BoxDecoration( - color: _hovered ? shell.navHover : Colors.transparent, + color: _hovered ? colors.surfaceContainerLow : Colors.transparent, borderRadius: const BorderRadius.all(Radius.circular(8)), ), child: Row( @@ -124,7 +125,7 @@ class _FooterLinkState extends State<_FooterLink> { Icon( widget.icon, size: 15, - color: _hovered ? shellAccent : shell.iconRest, + color: _hovered ? colors.primary : colors.onSurfaceMuted, ), const SizedBox(width: 9), Expanded( @@ -135,14 +136,16 @@ class _FooterLinkState extends State<_FooterLink> { style: shellText( size: 13.5, weight: FontWeight.w500, - color: _hovered ? shell.text : shell.navText, + color: _hovered + ? colors.onSurface + : colors.onSurfaceVariant, ), ), ), Icon( PhosphorIconsRegular.arrowUpRight, size: 13, - color: shell.iconRest, + color: colors.onSurfaceMuted, ), ], ), @@ -160,8 +163,6 @@ class _SectionLabel extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); - return Padding( padding: EdgeInsetsDirectional.fromSTEB(10, first ? 0 : 16, 10, 6), child: Text( @@ -170,7 +171,7 @@ class _SectionLabel extends StatelessWidget { size: 10.5, weight: FontWeight.w600, letterSpacing: 0.84, - color: shell.sectionLabel, + color: context.flowColors.onSurfaceMuted, ), ), ); @@ -197,13 +198,13 @@ class _NavRowState extends State<_NavRow> { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; final Color background; if (widget.active) { - background = shell.navActiveBg; + background = colors.surfaceContainer; } else if (_hovered) { - background = shell.navHover; + background = colors.surfaceContainerLow; } else { background = Colors.transparent; } @@ -226,7 +227,7 @@ class _NavRowState extends State<_NavRow> { Icon( widget.item.icon, size: 15, - color: widget.active ? shellAccent : shell.iconRest, + color: widget.active ? colors.primary : colors.onSurfaceMuted, ), const SizedBox(width: 9), Expanded( @@ -237,7 +238,9 @@ class _NavRowState extends State<_NavRow> { style: shellText( size: 13.5, weight: FontWeight.w500, - color: widget.active ? shell.text : shell.navText, + color: widget.active + ? colors.onSurface + : colors.onSurfaceVariant, ), ), ), diff --git a/playground/lib/src/stage.dart b/playground/lib/src/stage.dart index e364cdb..809c727 100644 --- a/playground/lib/src/stage.dart +++ b/playground/lib/src/stage.dart @@ -1,17 +1,18 @@ import 'dart:math' as math; +import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; import 'demo_registry.dart'; import 'playground_item.dart'; -import 'shell_palette.dart'; +import 'shell_text.dart'; /// Which frame the canvas stages the demo in. enum StageDevice { web, mobile } -/// The canvas: the warm-grey work area with the demo stage centred on it — +/// The canvas: the theme's page ground with the demo stage centred on it — /// a fluid rail in web mode, a phone mock in mobile mode — and, for items /// with variants, the floating pill switcher over the top edge. class Stage extends StatelessWidget { @@ -30,7 +31,6 @@ class Stage extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); final demo = demoFor(item, variant: variant); final variants = variantsFor(item); @@ -79,7 +79,7 @@ class Stage extends StatelessWidget { } return ColoredBox( - color: shell.canvas, + color: context.flowColors.surface, child: Stack( children: [ Positioned.fill(child: content), @@ -116,13 +116,13 @@ class _VariantPills extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; return Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: shell.topBarBg, - border: Border.all(color: shell.pillBorder), + color: colors.surfaceBright, + border: Border.all(color: colors.outline), borderRadius: const BorderRadius.all(Radius.circular(9)), boxShadow: const [ BoxShadow( @@ -147,7 +147,9 @@ class _VariantPills extends StatelessWidget { vertical: 4, ), decoration: BoxDecoration( - color: id == selected ? shell.chip : Colors.transparent, + color: id == selected + ? colors.surfaceContainer + : Colors.transparent, borderRadius: const BorderRadius.all(Radius.circular(7)), ), child: Text( @@ -155,7 +157,9 @@ class _VariantPills extends StatelessWidget { style: shellText( size: 12.5, weight: FontWeight.w500, - color: id == selected ? shell.text : shell.segmentRest, + color: id == selected + ? colors.onSurface + : colors.onSurfaceMuted, ), ), ), @@ -186,7 +190,7 @@ class _PhoneStage extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; final dark = Theme.of(context).brightness == Brightness.dark; final screenHeight = math .min(_screenHeight, math.max(320, maxHeight - _bezel * 2)) @@ -245,7 +249,7 @@ class _PhoneStage extends StatelessWidget { child: Container( width: _screenWidth, height: screenHeight, - color: shell.stageBg, + color: colors.surface, child: Stack( children: [ // The screen reports a phone: its size override makes @@ -273,11 +277,11 @@ class _PhoneStage extends StatelessWidget { // The navigator generates this route once and // keeps its builder for the route's lifetime, // so nothing per-build may be captured here: - // the demo and palette are read through + // the demo and colors are read through // `context` so component, variant, and theme // changes reach the screen. builder: (context) => Material( - color: ShellPalette.of(context).stageBg, + color: context.flowColors.surface, child: Column( children: [ const _StatusBar(), @@ -321,7 +325,7 @@ class _PhoneStage extends StatelessWidget { width: 118, height: 4, decoration: BoxDecoration( - color: shell.text.withValues(alpha: 0.9), + color: colors.onSurface.withValues(alpha: 0.9), borderRadius: const BorderRadius.all( Radius.circular(3), ), @@ -364,7 +368,7 @@ class _StatusBar extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final ink = context.flowColors.onSurface; return Padding( padding: const EdgeInsets.fromLTRB(26, 16, 26, 4), @@ -377,7 +381,7 @@ class _StatusBar extends StatelessWidget { size: 14.5, weight: FontWeight.w600, letterSpacing: 0.15, - color: shell.text, + color: ink, ), ), Row( @@ -389,12 +393,12 @@ class _StatusBar extends StatelessWidget { height: height, margin: const EdgeInsetsDirectional.only(start: 1.5), decoration: BoxDecoration( - color: shell.text, + color: ink, borderRadius: const BorderRadius.all(Radius.circular(1)), ), ), const SizedBox(width: 6), - Icon(PhosphorIconsRegular.wifiHigh, size: 15, color: shell.text), + Icon(PhosphorIconsRegular.wifiHigh, size: 15, color: ink), const SizedBox(width: 6), // Battery. Container( @@ -402,12 +406,12 @@ class _StatusBar extends StatelessWidget { height: 11, padding: const EdgeInsets.all(1), decoration: BoxDecoration( - border: Border.all(color: shell.text, width: 1.5), + border: Border.all(color: ink, width: 1.5), borderRadius: const BorderRadius.all(Radius.circular(3.5)), ), child: DecoratedBox( decoration: BoxDecoration( - color: shell.text, + color: ink, borderRadius: const BorderRadius.all(Radius.circular(1.5)), ), ), @@ -425,27 +429,35 @@ class _MobileNav extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; return Container( padding: const EdgeInsets.fromLTRB(18, 10, 18, 12), decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: shell.border)), + border: Border(bottom: BorderSide(color: colors.outlineVariant)), ), child: Row( children: [ - Icon(PhosphorIconsRegular.arrowLeft, size: 18, color: shell.navText), + Icon( + PhosphorIconsRegular.arrowLeft, + size: 18, + color: colors.onSurfaceVariant, + ), const SizedBox(width: 12), Text( 'AI Chat', style: shellText( size: 16, weight: FontWeight.w600, - color: shell.text, + color: colors.onSurface, ), ), const Spacer(), - Icon(PhosphorIconsRegular.gearSix, size: 17, color: shell.navText), + Icon( + PhosphorIconsRegular.gearSix, + size: 17, + color: colors.onSurfaceVariant, + ), ], ), ); diff --git a/playground/lib/src/top_bar.dart b/playground/lib/src/top_bar.dart index e8f22c2..08cf28c 100644 --- a/playground/lib/src/top_bar.dart +++ b/playground/lib/src/top_bar.dart @@ -1,9 +1,10 @@ +import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; import 'flow_logo.dart'; -import 'shell_palette.dart'; +import 'shell_text.dart'; import 'stage.dart'; /// The 54px chrome bar: logo cluster on the left, then the Light/Dark and @@ -26,14 +27,14 @@ class TopBar extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; return Container( height: 54, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: shell.topBarBg, - border: Border(bottom: BorderSide(color: shell.border)), + color: colors.surfaceBright, + border: Border(bottom: BorderSide(color: colors.outlineVariant)), ), child: Row( children: [ @@ -45,7 +46,7 @@ class TopBar extends StatelessWidget { size: 15, weight: FontWeight.w700, letterSpacing: -0.15, - color: shell.text, + color: colors.onSurface, ), ), const SizedBox(width: 9), @@ -56,7 +57,7 @@ class TopBar extends StatelessWidget { style: shellText( size: 11.5, weight: FontWeight.w500, - color: shell.sectionLabel, + color: colors.onSurfaceMuted, ), ), ), @@ -106,12 +107,10 @@ class ShellSegmented extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); - return Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: shell.chip, + color: context.flowColors.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(9)), ), child: Row( @@ -144,7 +143,8 @@ class _Segment extends StatelessWidget { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; + final dark = Theme.of(context).brightness == Brightness.dark; return MouseRegion( cursor: SystemMouseCursors.click, @@ -153,12 +153,20 @@ class _Segment extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( - color: active ? shell.segmentActiveBg : Colors.transparent, + // The pill must lift off the track in both themes, and no one + // token does that: in light it is the raised white card, while + // in dark surfaceBright would sink below the track's ink wash, + // so it lifts with the strongest wash instead. + color: active + ? (dark ? colors.surfaceContainerHighest : colors.surfaceBright) + : Colors.transparent, borderRadius: const BorderRadius.all(Radius.circular(7)), boxShadow: active ? [ BoxShadow( - color: shell.segmentShadow, + color: dark + ? const Color(0x59000000) + : const Color(0x1A000000), offset: const Offset(0, 1), blurRadius: 2, ), @@ -170,7 +178,7 @@ class _Segment extends StatelessWidget { style: shellText( size: 12.5, weight: FontWeight.w500, - color: active ? shell.text : shell.segmentRest, + color: active ? colors.onSurface : colors.onSurfaceMuted, ), ), ), @@ -194,7 +202,7 @@ class _CodeToggleState extends State<_CodeToggle> { @override Widget build(BuildContext context) { - final shell = ShellPalette.of(context); + final colors = context.flowColors; return MouseRegion( cursor: SystemMouseCursors.click, @@ -207,13 +215,15 @@ class _CodeToggleState extends State<_CodeToggle> { height: 30, alignment: Alignment.center, decoration: BoxDecoration( - color: widget.active || _hovered ? shell.chip : Colors.transparent, + color: widget.active || _hovered + ? colors.surfaceContainer + : Colors.transparent, borderRadius: const BorderRadius.all(Radius.circular(8)), ), child: Icon( PhosphorIconsRegular.code, size: 17, - color: widget.active ? shell.text : shell.segmentRest, + color: widget.active ? colors.onSurface : colors.onSurfaceMuted, ), ), ),