diff --git a/playground/lib/main.dart b/playground/lib/main.dart index ffe8085..2b23b3c 100644 --- a/playground/lib/main.dart +++ b/playground/lib/main.dart @@ -1,7 +1,6 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -import 'src/code_panel.dart'; import 'src/embed.dart'; import 'src/playground_shell.dart'; @@ -16,9 +15,6 @@ Future main() async { runApp(EmbedApp(request: embed)); return; } - // Loads the Dart grammar and both highlight themes once, so the code - // panel renders synchronously. - await CodeHighlighting.init(); runApp(const PlaygroundApp()); } diff --git a/playground/lib/src/code_panel.dart b/playground/lib/src/code_panel.dart index 6949c44..0c48333 100644 --- a/playground/lib/src/code_panel.dart +++ b/playground/lib/src/code_panel.dart @@ -1,73 +1,103 @@ import 'dart:async'; +import 'package:flow_ui/flow_ui.dart'; +// The built-in highlighter isn't part of the public barrel; the +// playground lives beside the package and reaches in for it so the +// panel can typeset its snippets in the code block's own style without +// the package growing panel-only API. +// ignore: implementation_imports +import 'package:flow_ui/src/utils/flow_syntax_highlighter.dart'; import 'package:flutter/services.dart'; import 'package:material_ui/material_ui.dart'; - import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; -import 'package:syntax_highlight/syntax_highlight.dart'; import 'demo_registry.dart'; import 'playground_item.dart'; import 'shell_palette.dart'; -/// The playground's Dart highlighters, initialized once at startup. -/// -/// `syntax_highlight` loads its TextMate grammar and themes from bundled -/// assets, so setup is async; `main` awaits [init] before running the app -/// and the panel then highlights synchronously. Plain text is the fallback -/// if init hasn't run. -class CodeHighlighting { - CodeHighlighting._(); - - static Highlighter? _light; - static Highlighter? _dark; - - static Future init() async { - await Highlighter.initialize(['dart']); - _light = Highlighter( - language: 'dart', - theme: await HighlighterTheme.loadLightTheme(), - ); - _dark = Highlighter( - language: 'dart', - theme: await HighlighterTheme.loadDarkTheme(), - ); - } - - static Highlighter? of(Brightness brightness) => - brightness == Brightness.dark ? _dark : _light; -} - -/// The slide-in code panel: a 400px pane with a filename header and a -/// snippet body. Snippets arrive with the demos — for now the body holds -/// a placeholder comment, but the header already tracks the selection. -class CodePanel extends StatelessWidget { +/// 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 +/// over the theme's syntax tokens and mono face — directly on the +/// panel's ground, with copy and close as header chips. It follows the +/// stage: switching the variant pills swaps the code to match what's +/// being shown. +class CodePanel extends StatefulWidget { const CodePanel({ super.key, required this.open, required this.item, + this.variant, required this.onClose, }); final bool open; final PlaygroundItem item; + + /// The stage's active variant; null renders the item's default form. + final String? variant; + final VoidCallback onClose; - static const double _width = 400; + @override + State createState() => _CodePanelState(); +} + +class _CodePanelState extends State { + /// The pane resizes by its left edge, between the design's default + /// width and a cap that keeps the stage usable. + static const double _minWidth = 400; + static const double _maxWidth = 720; static const Duration _slide = Duration(milliseconds: 250); + double _width = _minWidth; + bool _dragging = false; + + /// The copy confirmation, per the package's contract: the block + /// reports intent, the panel owns the clipboard and the timing. + bool _copied = false; + Timer? _reset; + + @override + void didUpdateWidget(CodePanel oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.item != oldWidget.item || widget.variant != oldWidget.variant) { + _reset?.cancel(); + _reset = null; + _copied = false; + } + } + + @override + void dispose() { + _reset?.cancel(); + super.dispose(); + } + + Future _copy(String code) async { + await Clipboard.setData(ClipboardData(text: code)); + if (!mounted) return; + setState(() => _copied = true); + _reset?.cancel(); + _reset = Timer(const Duration(milliseconds: 1500), () { + if (mounted) setState(() => _copied = false); + }); + } + @override Widget build(BuildContext context) { final shell = ShellPalette.of(context); + final code = snippetFor(widget.item, variant: widget.variant); return AnimatedContainer( - duration: _slide, + // Zero while dragging: the width must track the pointer, not ease + // after it — the slide animates only opening and closing. + duration: _dragging ? Duration.zero : _slide, curve: Curves.ease, - width: open ? _width : 0, + width: widget.open ? _width : 0, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: shell.codeBg, - border: open + border: widget.open ? Border(left: BorderSide(color: shell.border)) : const Border(), ), @@ -75,92 +105,39 @@ class CodePanel extends StatelessWidget { // panel animates — it slides, per the design. child: AnimatedOpacity( duration: const Duration(milliseconds: 200), - opacity: open ? 1 : 0, + opacity: widget.open ? 1 : 0, child: OverflowBox( alignment: Alignment.centerLeft, minWidth: _width, maxWidth: _width, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Stack( children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 10, - ), - decoration: BoxDecoration( - color: shell.codeHeaderBg, - border: Border(bottom: BorderSide(color: shell.codeBorder)), - ), - child: Row( - children: [ - Text( - item.codeFile, - style: _mono(size: 12, color: shell.codeHeaderText), - ), - const SizedBox(width: 10), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - border: Border.all(color: shell.codeBorder), - borderRadius: const BorderRadius.all( - Radius.circular(5), - ), - ), - child: Text( - 'DART', - style: shellText( - size: 10, - weight: FontWeight.w600, - letterSpacing: 0.5, - color: shell.codeHeaderText, - ), - ), - ), - const Spacer(), - _CopyButton(code: snippetFor(item)), - const SizedBox(width: 10), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onClose, - child: Container( - width: 24, - height: 24, - alignment: Alignment.center, - decoration: BoxDecoration( - color: shell.codeChip, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - child: Icon( - PhosphorIconsRegular.x, - size: 14, - color: shell.codeHeaderText, - ), - ), - ), - ), - ], - ), - ), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 18), - child: Align( - alignment: Alignment.topLeft, - child: _HighlightedCode( - code: snippetFor(item), - style: _mono( - size: 12, - height: 1.7, - color: shell.codeText, - ), - ), + Positioned.fill(child: _pane(shell, code)), + // The resize handle rides the pane's left edge: drag to + // widen between the min and max. + PositionedDirectional( + start: 0, + top: 0, + bottom: 0, + width: 8, + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onHorizontalDragStart: (_) => + setState(() => _dragging = true), + onHorizontalDragUpdate: (details) => setState(() { + _width = (_width - details.delta.dx).clamp( + _minWidth, + _maxWidth, + ); + }), + onHorizontalDragEnd: (_) => + setState(() => _dragging = false), + onHorizontalDragCancel: () => + setState(() => _dragging = false), + // Double-tap snaps back to the default width. + onDoubleTap: () => setState(() => _width = _minWidth), ), ), ), @@ -171,106 +148,97 @@ class CodePanel extends StatelessWidget { ); } - /// The design sets code in Geist; the playground doesn't bundle it, so - /// this leans on the platform's monospace stack. - static TextStyle _mono({double? size, double? height, Color? color}) { - return TextStyle( - fontFamily: 'monospace', - fontFamilyFallback: const ['Menlo', 'Consolas', 'Courier'], - fontSize: size, - height: height, - color: color, - ); - } -} - -/// The header's copy chip: puts the snippet on the clipboard and reads -/// "Copied!" for a beat, per the design. -class _CopyButton extends StatefulWidget { - const _CopyButton({required this.code}); - - final String code; - - @override - State<_CopyButton> createState() => _CopyButtonState(); -} - -class _CopyButtonState extends State<_CopyButton> { - bool _copied = false; - Timer? _reset; - - @override - void dispose() { - _reset?.cancel(); - super.dispose(); - } - - Future _copy() async { - await Clipboard.setData(ClipboardData(text: widget.code)); - if (!mounted) return; - setState(() => _copied = true); - _reset?.cancel(); - _reset = Timer(const Duration(milliseconds: 1500), () { - if (mounted) setState(() => _copied = false); - }); - } - - @override - Widget build(BuildContext context) { - final shell = ShellPalette.of(context); - + /// The header's 24px action chip — copy and close share the form. + Widget _headerChip( + ShellPalette shell, { + required VoidCallback onTap, + required Widget child, + }) { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( - onTap: _copy, + onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + width: 24, + height: 24, + alignment: Alignment.center, decoration: BoxDecoration( color: shell.codeChip, borderRadius: const BorderRadius.all(Radius.circular(6)), ), + child: child, + ), + ), + ); + } + + Widget _pane(ShellPalette shell, String code) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsetsDirectional.fromSTEB(16, 12, 16, 0), child: Row( - mainAxisSize: MainAxisSize.min, children: [ - Icon( - _copied - ? PhosphorIconsRegular.check - : PhosphorIconsRegular.copy, - size: 13, - color: shell.text, - ), - const SizedBox(width: 6), Text( - _copied ? 'Copied!' : 'Copy', + 'Code', style: shellText( size: 12, - weight: FontWeight.w500, - color: shell.text, + weight: FontWeight.w600, + letterSpacing: 0.5, + color: shell.codeHeaderText, + ), + ), + const Spacer(), + // The copy affordance, always visible beside the close + // chip: copy, then a primary-tinted check while the + // confirmation lasts. + 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, + ), + ), + ), + const SizedBox(width: 8), + _headerChip( + shell, + onTap: widget.onClose, + child: Icon( + PhosphorIconsRegular.x, + size: 14, + color: shell.codeHeaderText, ), ), ], ), ), - ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsetsDirectional.fromSTEB(16, 8, 16, 16), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SelectableText.rich( + FlowSyntaxHighlighter.highlight( + code, + language: FlowCodeLanguage.find('dart'), + style: context.flowTypography.code.copyWith( + color: context.flowColors.onSurface, + ), + colors: context.flowSyntaxColors, + ), + ), + ), + ), + ), + ], ); } } - -/// The snippet through the VS Code Dart grammar, in the theme matching the -/// ambient brightness; the base style keeps the mono font and line height, -/// the spans carry per-token color. -class _HighlightedCode extends StatelessWidget { - const _HighlightedCode({required this.code, required this.style}); - - final String code; - final TextStyle style; - - @override - Widget build(BuildContext context) { - final highlighter = CodeHighlighting.of(Theme.of(context).brightness); - final content = highlighter == null - ? TextSpan(text: code, style: style) - : TextSpan(style: style, children: [highlighter.highlight(code)]); - return SelectableText.rich(content); - } -} diff --git a/playground/lib/src/demo_registry.dart b/playground/lib/src/demo_registry.dart index 650dd06..05dcaa3 100644 --- a/playground/lib/src/demo_registry.dart +++ b/playground/lib/src/demo_registry.dart @@ -139,27 +139,28 @@ List<(String, String)> variantsFor(PlaygroundItem item) { bool demoFillsStage(PlaygroundItem item) => item == PlaygroundItem.fullChat; /// The code panel's snippet for [item] — the real flow_ui usage, not the -/// demo's plumbing. -String snippetFor(PlaygroundItem item) { +/// demo's plumbing. [variant] is the stage's active pill, so the code +/// follows what the demo is showing. +String snippetFor(PlaygroundItem item, {String? variant}) { return switch (item) { PlaygroundItem.fullChat => _fullChatSnippet, - PlaygroundItem.composer => composerSnippet, + PlaygroundItem.composer => composerSnippet(variant), PlaygroundItem.modalSelector => modelSelectorSnippet, - PlaygroundItem.message => messageSnippet, - PlaygroundItem.streamingMessage => streamingMessageSnippet, - PlaygroundItem.codeBlock => codeBlockSnippet, - PlaygroundItem.markdown => markdownSnippet, - PlaygroundItem.errorState => errorStateSnippet, + PlaygroundItem.message => messageSnippet(variant), + PlaygroundItem.streamingMessage => streamingMessageSnippet(variant), + PlaygroundItem.codeBlock => codeBlockSnippet(variant), + PlaygroundItem.markdown => markdownSnippet(variant), + PlaygroundItem.errorState => errorStateSnippet(variant), PlaygroundItem.addToChat => addToChatSnippet, - PlaygroundItem.pill => pillSnippet, - PlaygroundItem.attachments => attachmentsSnippet, - PlaygroundItem.thread => threadSnippet, + PlaygroundItem.pill => pillSnippet(variant), + PlaygroundItem.attachments => attachmentsSnippet(variant), + PlaygroundItem.thread => threadSnippet(variant), PlaygroundItem.messageActions => messageActionsSnippet, - PlaygroundItem.streamingText => streamingTextSnippet, - PlaygroundItem.shimmerText => shimmerTextSnippet, - PlaygroundItem.thinkingIndicator => thinkingIndicatorSnippet, - PlaygroundItem.suggestions => suggestionsSnippet, - PlaygroundItem.greeting => greetingSnippet, + PlaygroundItem.streamingText => streamingTextSnippet(variant), + PlaygroundItem.shimmerText => shimmerTextSnippet(variant), + PlaygroundItem.thinkingIndicator => thinkingIndicatorSnippet(variant), + PlaygroundItem.suggestions => suggestionsSnippet(variant), + PlaygroundItem.greeting => greetingSnippet(variant), }; } diff --git a/playground/lib/src/demos/attachments_demo.dart b/playground/lib/src/demos/attachments_demo.dart index 60420c7..d68c813 100644 --- a/playground/lib/src/demos/attachments_demo.dart +++ b/playground/lib/src/demos/attachments_demo.dart @@ -4,7 +4,12 @@ import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; import 'demo_content.dart'; -const String attachmentsSnippet = ''' +String attachmentsSnippet([String? variant]) => switch (variant) { + 'tiles' => _tilesSnip, + _ => _composerSnip, +}; + +const String _composerSnip = ''' FlowComposer( controller: input, attachments: [ @@ -15,9 +20,10 @@ FlowComposer( onRemoveAttachment: remove, removeAttachmentTooltip: 'Remove', onSend: send, -) +)'''; -// Or the strip on its own: +const String _tilesSnip = ''' +// The strip on its own, outside a composer. FlowAttachmentGroup( attachments: attachments, onRemove: remove, diff --git a/playground/lib/src/demos/code_block_demo.dart b/playground/lib/src/demos/code_block_demo.dart index a8a083a..8cc7c19 100644 --- a/playground/lib/src/demos/code_block_demo.dart +++ b/playground/lib/src/demos/code_block_demo.dart @@ -4,7 +4,56 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:flutter/services.dart'; import 'package:material_ui/material_ui.dart'; -const String codeBlockSnippet = r''' +String codeBlockSnippet([String? variant]) => switch (variant) { + 'json' => _blockFor('json', 'screen.json'), + 'yaml' => _blockFor('yaml', 'deploy.yml'), + 'html' => _blockFor('html', 'index.html'), + 'css' => _blockFor('css', 'stage.css'), + 'sql' => _blockFor('sql', 'threads.sql'), + 'plain' => _plain, + 'streaming' => _streaming, + _ => _dart, +}; + +String _blockFor(String language, String filename) => + ''' +// Highlighting is built in and synchronous — $language is one of the +// nine bundled languages. +FlowCodeBlock( + code: source, + language: '$language', + filename: '$filename', + copyTooltip: 'Copy code', + copied: copied, + onCopy: copy, +)'''; + +const String _plain = r''' +// No registered language: the block renders plain. Hosts register +// their own rules: +FlowCodeBlock( + code: buildLog, + filename: 'build.log', +) + +FlowCodeLanguage.register( + const FlowCodeLanguage( + id: 'lisp', + rules: [FlowSyntaxRule(FlowSyntaxToken.comment, r';[^\n]*')], + ), +);'''; + +const String _streaming = ''' +// A fence still arriving: the copy affordance stays hidden until the +// code settles, exactly like a streaming FlowCodePart in a thread. +FlowCodeBlock( + code: arrivedSoFar, // grows as chunks land + language: 'dart', + filename: 'point.dart', + isStreaming: true, +)'''; + +const String _dart = r''' // The block reports intent; the host owns the clipboard and the // confirmation's timing. FlowCodeBlock( diff --git a/playground/lib/src/demos/composer_demo.dart b/playground/lib/src/demos/composer_demo.dart index 544c615..88dc8de 100644 --- a/playground/lib/src/demos/composer_demo.dart +++ b/playground/lib/src/demos/composer_demo.dart @@ -4,7 +4,22 @@ import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; import 'demo_content.dart'; -const String composerSnippet = ''' +String composerSnippet([String? variant]) => switch (variant) { + 'streaming' => _streaming, + _ => _default, +}; + +const String _streaming = ''' +// While a reply streams the send disc reads as stop — onStop is the +// only intent it reports; sending resumes when the stream settles. +FlowComposer( + controller: input, + isStreaming: true, + onSend: send, + onStop: stop, +)'''; + +const String _default = ''' FlowComposer( controller: input, placeholder: 'How can I help you today?', diff --git a/playground/lib/src/demos/error_state_demo.dart b/playground/lib/src/demos/error_state_demo.dart index b0ee5c8..d1b31a7 100644 --- a/playground/lib/src/demos/error_state_demo.dart +++ b/playground/lib/src/demos/error_state_demo.dart @@ -3,7 +3,13 @@ import 'dart:async'; import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -const String errorStateSnippet = ''' +String errorStateSnippet([String? variant]) => switch (variant) { + 'minimal' => _minimalSnip, + 'thread' => _threadSnip, + _ => _cardSnip, +}; + +const String _cardSnip = ''' // The card renders state and reports one intent; what retry means — // re-run the turn, refetch, reconnect — is the host's business. FlowErrorState( @@ -11,8 +17,16 @@ FlowErrorState( message: 'The API is overloaded right now. Retry in a moment.', retryLabel: 'Retry', onRetry: resend, -) +)'''; + +const String _minimalSnip = ''' +// The message-only form: no title, no retry pill — for failures +// retrying can't fix. +FlowErrorState( + message: 'The API is overloaded right now. Retry in a moment.', +)'''; +const String _threadSnip = ''' // In a thread the card renders on its own: parts a failed turn already // delivered keep their ink, and its FlowErrorPart closes the turn. FlowThread( @@ -22,8 +36,6 @@ FlowThread( onRetry: (message) => rerun(message), ) -// retryable: false suppresses the pill — for failures retrying -// can't fix. FlowMessageData( id: 'a2', role: FlowMessageRole.assistant, diff --git a/playground/lib/src/demos/greeting_demo.dart b/playground/lib/src/demos/greeting_demo.dart index ea22ae2..d7da74d 100644 --- a/playground/lib/src/demos/greeting_demo.dart +++ b/playground/lib/src/demos/greeting_demo.dart @@ -2,14 +2,25 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; -const String greetingSnippet = ''' +String greetingSnippet([String? variant]) => switch (variant) { + 'text' => _textSnip, + _ => _iconSnip, +}; + +const String _iconSnip = ''' FlowGreeting( icon: PhosphorIconsRegular.sunHorizon, text: 'Good Afternoon, Divyanshu', ) -// Below 600px of available width it restacks: icon above 21px text. +// Below 600px of available width it restacks: icon above the text. // The host supplies the whole string — nothing is derived.'''; +const String _textSnip = ''' +// Text-only: leave the icon out. +FlowGreeting( + text: 'Good Afternoon, Divyanshu', +)'''; + /// The zero state's headline. On the phone stage it restacks into the /// compact form by itself — the switch is width-based. class GreetingDemo extends StatelessWidget { diff --git a/playground/lib/src/demos/markdown_demo.dart b/playground/lib/src/demos/markdown_demo.dart index ca4ef14..f6ee806 100644 --- a/playground/lib/src/demos/markdown_demo.dart +++ b/playground/lib/src/demos/markdown_demo.dart @@ -5,7 +5,50 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:flutter/services.dart'; import 'package:material_ui/material_ui.dart'; -const String markdownSnippet = ''' +String markdownSnippet([String? variant]) => switch (variant) { + 'streaming' => _streamingSnip, + 'tables' => _tablesSnip, + 'links' => _linksSnip, + _ => _documentSnip, +}; + +const String _streamingSnip = ''' +// Streaming is data: rebuild with a longer text and the trailing +// paragraph reveals character by character — fences, tables and rules +// ease in whole when the frontier reaches them. Input that ends +// mid-construct renders gracefully. +FlowMarkdown( + text: receivedSoFar, + isStreaming: generating, // false settles the reveal + charactersPerSecond: 300, +)'''; + +const String _tablesSnip = ''' +// Column alignment comes from the delimiter row; a table wider than +// its column scrolls horizontally inside the message rather than +// wrapping the page. +// +// | Model | Context | Strength | +// |:--------|--------:|:--------------:| +// | Fable 5 | 400K | Deep reasoning | +FlowMarkdown(text: reply)'''; + +const String _linksSnip = ''' +// Links report intent — the package never launches URLs, and a null +// onLinkTap renders them as plain prose. Bare https:// and www. URLs +// autolink with GFM's trimming rules. +FlowMarkdown( + text: reply, + onLinkTap: (href) => openInBrowser(href), +) + +// In a thread the callback carries the message too: +FlowThread( + messages: messages, + onLinkTap: (message, href) => openInBrowser(href), +)'''; + +const String _documentSnip = ''' // Assistant text parts render markdown by default — pass // markdown: false on FlowThread/FlowMessage for literal text. FlowThread( diff --git a/playground/lib/src/demos/message_demo.dart b/playground/lib/src/demos/message_demo.dart index 218cfd6..6f1f8cd 100644 --- a/playground/lib/src/demos/message_demo.dart +++ b/playground/lib/src/demos/message_demo.dart @@ -1,7 +1,47 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -const String messageSnippet = ''' +String messageSnippet([String? variant]) => switch (variant) { + 'ai' => _assistant, + 'user' => _user, + _ => _pair, +}; + +const String _user = ''' +// The user turn: an ink-wash bubble, trailing-aligned. +FlowMessage( + FlowMessageData.text( + id: 'u1', + role: FlowMessageRole.user, + text: 'Can you tell me more about what you can do?', + ), +)'''; + +const String _assistant = ''' +// The assistant turn: plain on the page. FlowCustomPart is the +// host-content seam — the builder renders this one as the heading. +FlowMessage( + const FlowMessageData( + id: 'a1', + role: FlowMessageRole.assistant, + parts: [ + FlowCustomPart(type: 'heading', data: 'Hello! I am AI chat :)'), + FlowTextPart('I can assist you with most tasks across...'), + ], + ), + customPartBuilder: (context, message, part) => + part.type == 'heading' ? Heading(part.data as String) : null, + footer: FlowMessageActions( + actions: [ + FlowMessageAction.copy(onPressed: copy), + FlowMessageAction.thumbUp(selected: liked, onPressed: like), + FlowMessageAction.thumbDown(selected: disliked, onPressed: dislike), + FlowMessageAction.regenerate(onPressed: retry), + ], + ), +)'''; + +const String _pair = ''' Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/playground/lib/src/demos/pill_demo.dart b/playground/lib/src/demos/pill_demo.dart index fc72bbc..ff91c79 100644 --- a/playground/lib/src/demos/pill_demo.dart +++ b/playground/lib/src/demos/pill_demo.dart @@ -2,7 +2,58 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; -const String pillSnippet = ''' +String pillSnippet([String? variant]) => switch (variant) { + 'icon' => _iconSnip, + 'static' => _staticSnip, + 'composer' => _composerSnip, + _ => _defaultSnip, +}; + +const String _defaultSnip = ''' +// Presence is the state: the host renders a pill while its tool is on, +// and the X only reports intent — removal is the host's move. +if (researchOn) + FlowPill( + icon: PhosphorIconsRegular.graduationCap, + label: 'Research', + removeTooltip: 'Turn off Research', + onRemove: () => setResearch(false), + ), +if (webSearchOn) + FlowPill( + icon: PhosphorIconsRegular.globe, + label: 'Web Search', + removeTooltip: 'Turn off Web Search', + onRemove: () => setWebSearch(false), + ),'''; + +const String _iconSnip = ''' +// On phones the label auto-drops to the design's icon-only form; +// showLabel forces either. +FlowPill( + icon: PhosphorIconsRegular.globe, + label: 'Web Search', + showLabel: false, + removeTooltip: 'Turn off Web Search', + onRemove: () => setWebSearch(false), +)'''; + +const String _staticSnip = ''' +// No onRemove renders a static pill; enabled: false mutes one. +FlowPill( + icon: PhosphorIconsRegular.graduationCap, + label: 'Research', +) + +FlowPill( + icon: PhosphorIconsRegular.globe, + label: 'Web Search', + enabled: false, + removeTooltip: 'Turn off Web Search', + onRemove: turnOff, +)'''; + +const String _composerSnip = ''' // Presence is the state: the host renders a pill while its tool is on, // and the X only reports intent — removal is the host's move. FlowComposer( @@ -22,16 +73,6 @@ FlowComposer( onRemove: () => setResearch(false), ), ], -) - -// On phones the label auto-drops to the design's icon-only form; -// showLabel forces either. No onRemove renders a static pill. -FlowPill( - icon: PhosphorIconsRegular.globe, - label: 'Web Search', - showLabel: false, - removeTooltip: 'Turn off Web Search', - onRemove: () => setWebSearch(false), )'''; /// Stage demo for `FlowPill` — removable tool pills, the forced icon-only diff --git a/playground/lib/src/demos/shimmer_text_demo.dart b/playground/lib/src/demos/shimmer_text_demo.dart index 5449cc1..33d87da 100644 --- a/playground/lib/src/demos/shimmer_text_demo.dart +++ b/playground/lib/src/demos/shimmer_text_demo.dart @@ -1,10 +1,21 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -const String shimmerTextSnippet = ''' +String shimmerTextSnippet([String? variant]) => switch (variant) { + 'settled' => _settledSnip, + _ => _activeSnip, +}; + +const String _activeSnip = ''' +FlowShimmerText( + text: 'Searching the web..', + enabled: true, // the sweeping highlight, while work is under way +)'''; + +const String _settledSnip = ''' FlowShimmerText( text: 'Searching the web..', - enabled: waiting, // false parks the text in the muted base ink + enabled: false, // parks the text in the muted base ink )'''; /// The sweeping highlight on waiting text — the thinking indicator's diff --git a/playground/lib/src/demos/streaming_message_demo.dart b/playground/lib/src/demos/streaming_message_demo.dart index d0d66e8..6700a04 100644 --- a/playground/lib/src/demos/streaming_message_demo.dart +++ b/playground/lib/src/demos/streaming_message_demo.dart @@ -1,7 +1,33 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -const String streamingMessageSnippet = ''' +String streamingMessageSnippet([String? variant]) => switch (variant) { + 'static' => _static, + _ => _animated, +}; + +const String _static = ''' +Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FlowMessage( + FlowMessageData.text( + id: 'u1', + role: FlowMessageRole.user, + text: 'Can you tell me more about what you can do?', + ), + ), + const SizedBox(height: 30), + // Settled: active: false parks the asterisk upright and stills + // the shimmering label — the waiting moment, at rest. + FlowThinkingIndicator( + label: 'thinking..', + active: false, + ), + ], +)'''; + +const String _animated = ''' Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/playground/lib/src/demos/streaming_text_demo.dart b/playground/lib/src/demos/streaming_text_demo.dart index e9f2c79..fa45886 100644 --- a/playground/lib/src/demos/streaming_text_demo.dart +++ b/playground/lib/src/demos/streaming_text_demo.dart @@ -1,13 +1,24 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -const String streamingTextSnippet = ''' +String streamingTextSnippet([String? variant]) => switch (variant) { + 'instant' => _instantSnip, + _ => _animatedSnip, +}; + +const String _animatedSnip = ''' FlowStreamingText( text: streamedSoFar, // append as chunks arrive - isStreaming: generating, // false settles the reveal instantly + isStreaming: true, charactersPerSecond: 300, )'''; +const String _instantSnip = ''' +FlowStreamingText( + text: fullReply, + isStreaming: false, // settles the reveal instantly — history at rest +)'''; + const String _passage = 'FlowStreamingText reveals its text at a steady character rate, so a ' 'reply arriving in uneven network chunks still reads as one calm, ' diff --git a/playground/lib/src/demos/suggestions_demo.dart b/playground/lib/src/demos/suggestions_demo.dart index 4464489..3fddda6 100644 --- a/playground/lib/src/demos/suggestions_demo.dart +++ b/playground/lib/src/demos/suggestions_demo.dart @@ -2,12 +2,21 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; import 'package:phosphoricons_flutter/phosphoricons_flutter.dart'; -const String suggestionsSnippet = ''' +String suggestionsSnippet([String? variant]) { + final layout = switch (variant) { + 'scroll' => 'scroll', + 'wrap' => 'wrap', + _ => 'column', + }; + return _suggestionsSnip.replaceFirst('LAYOUT', layout); +} + +const String _suggestionsSnip = ''' FlowSuggestionGroup( // column: full-width rows, the zero state's form. - // scroll (default): one strip, no scrollbar — above a composer. + // scroll: one strip, no scrollbar — above a composer. // wrap: as many lines as needed. - layout: FlowSuggestionLayout.column, + layout: FlowSuggestionLayout.LAYOUT, suggestions: [ for (final starter in starters) FlowSuggestion( diff --git a/playground/lib/src/demos/thinking_indicator_demo.dart b/playground/lib/src/demos/thinking_indicator_demo.dart index d88a6b8..ab8eb94 100644 --- a/playground/lib/src/demos/thinking_indicator_demo.dart +++ b/playground/lib/src/demos/thinking_indicator_demo.dart @@ -1,10 +1,21 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:material_ui/material_ui.dart'; -const String thinkingIndicatorSnippet = ''' +String thinkingIndicatorSnippet([String? variant]) => switch (variant) { + 'settled' => _settledSnip, + _ => _activeSnip, +}; + +const String _activeSnip = ''' +FlowThinkingIndicator( + label: 'thinking..', + active: true, // the turning, breathing asterisk and shimmer label +)'''; + +const String _settledSnip = ''' FlowThinkingIndicator( label: 'thinking..', - active: generating, // false settles the asterisk upright + active: false, // settles the asterisk upright, stills the label )'''; /// The turning, breathing asterisk with its shimmering label, on its own. diff --git a/playground/lib/src/demos/thread_demo.dart b/playground/lib/src/demos/thread_demo.dart index c59d05b..0b59ca5 100644 --- a/playground/lib/src/demos/thread_demo.dart +++ b/playground/lib/src/demos/thread_demo.dart @@ -4,7 +4,38 @@ import 'package:flow_ui/flow_ui.dart'; import 'package:flutter/services.dart'; import 'package:material_ui/material_ui.dart'; -const String threadSnippet = ''' +String threadSnippet([String? variant]) => switch (variant) { + 'streaming' => _streaming, + 'short' => _short, + _ => _default, +}; + +const String _streaming = ''' +// Streaming is data: rebuild with the newest message grown and the +// thread stays anchored to it. Status walks +// pending → streaming → complete. +FlowThread( + messages: [ + ...history, + FlowMessageData( + id: 'reply', + role: FlowMessageRole.assistant, + parts: [FlowTextPart(streamedSoFar)], + status: FlowMessageStatus.streaming, + ), + ], +)'''; + +const String _short = ''' +// A conversation that still fits its viewport reads from the top, the +// AI-app convention; the thread flips to bottom-anchored only once it +// outgrows the viewport. +SizedBox( + height: 480, + child: FlowThread(messages: fewMessages), +)'''; + +const String _default = ''' // A scrollable conversation — reads from the top, anchoring to the // newest message once it outgrows the viewport. Give it // bounded height; inside FlowChatView that comes for free. diff --git a/playground/lib/src/playground_shell.dart b/playground/lib/src/playground_shell.dart index 5256632..da09555 100644 --- a/playground/lib/src/playground_shell.dart +++ b/playground/lib/src/playground_shell.dart @@ -66,6 +66,7 @@ class _PlaygroundShellState extends State { CodePanel( open: _codeOpen, item: _selected, + variant: _variant, onClose: () => setState(() => _codeOpen = false), ), ], diff --git a/playground/linux/flutter/generated_plugin_registrant.cc b/playground/linux/flutter/generated_plugin_registrant.cc index 819251b..e71a16d 100644 --- a/playground/linux/flutter/generated_plugin_registrant.cc +++ b/playground/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,6 @@ #include "generated_plugin_registrant.h" -#include -#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); - irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); - g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); - super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); } diff --git a/playground/linux/flutter/generated_plugins.cmake b/playground/linux/flutter/generated_plugins.cmake index cd11e59..2e1de87 100644 --- a/playground/linux/flutter/generated_plugins.cmake +++ b/playground/linux/flutter/generated_plugins.cmake @@ -3,8 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - irondash_engine_context - super_native_extensions ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/playground/macos/Flutter/GeneratedPluginRegistrant.swift b/playground/macos/Flutter/GeneratedPluginRegistrant.swift index 0facfeb..cccf817 100644 --- a/playground/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/playground/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,6 @@ import FlutterMacOS import Foundation -import device_info_plus -import irondash_engine_context -import super_native_extensions func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) - IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) - SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) } diff --git a/playground/pubspec.lock b/playground/pubspec.lock index 299d679..b6d0ec5 100644 --- a/playground/pubspec.lock +++ b/playground/pubspec.lock @@ -49,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -73,22 +65,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - device_info_plus: - dependency: transitive - description: - name: device_info_plus - sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" - url: "https://pub.dev" - source: hosted - version: "11.5.0" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" - source: hosted - version: "7.0.3" fake_async: dependency: transitive description: @@ -97,30 +73,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" flow_ui: dependency: "direct main" description: @@ -159,11 +111,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" http: dependency: transitive description: @@ -188,22 +135,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.3" - irondash_engine_context: - dependency: transitive - description: - name: irondash_engine_context - sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" - url: "https://pub.dev" - source: hosted - version: "0.5.5" - irondash_message_channel: - dependency: transitive - description: - name: irondash_message_channel - sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 - url: "https://pub.dev" - source: hosted - version: "0.7.0" leak_tracker: dependency: transitive description: @@ -300,22 +231,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - pixel_snap: - dependency: transitive - description: - name: pixel_snap - sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" - url: "https://pub.dev" - source: hosted - version: "0.1.5" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -353,30 +268,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - super_clipboard: - dependency: transitive - description: - name: super_clipboard - sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 - url: "https://pub.dev" - source: hosted - version: "0.9.1" - super_native_extensions: - dependency: transitive - description: - name: super_native_extensions - sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 - url: "https://pub.dev" - source: hosted - version: "0.9.1" - syntax_highlight: - dependency: "direct main" - description: - name: syntax_highlight - sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138" - url: "https://pub.dev" - source: hosted - version: "0.5.0" term_glyph: dependency: transitive description: @@ -401,14 +292,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" - url: "https://pub.dev" - source: hosted - version: "4.6.0" vector_graphics: dependency: transitive description: @@ -457,22 +340,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" - url: "https://pub.dev" - source: hosted - version: "2.1.0" xml: dependency: transitive description: diff --git a/playground/pubspec.yaml b/playground/pubspec.yaml index ea92b75..6094e9e 100644 --- a/playground/pubspec.yaml +++ b/playground/pubspec.yaml @@ -40,8 +40,6 @@ dependencies: flutter_svg: ^2.0.10 # The icon set the design is drawn in. phosphoricons_flutter: ^1.0.0 - # Dart syntax highlighting for the code panel (same as the gallery). - syntax_highlight: ^0.5.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/playground/windows/flutter/generated_plugin_registrant.cc b/playground/windows/flutter/generated_plugin_registrant.cc index 94564b6..8b6d468 100644 --- a/playground/windows/flutter/generated_plugin_registrant.cc +++ b/playground/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,6 @@ #include "generated_plugin_registrant.h" -#include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { - IrondashEngineContextPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); - SuperNativeExtensionsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); } diff --git a/playground/windows/flutter/generated_plugins.cmake b/playground/windows/flutter/generated_plugins.cmake index 607cf1f..b93c4c3 100644 --- a/playground/windows/flutter/generated_plugins.cmake +++ b/playground/windows/flutter/generated_plugins.cmake @@ -3,8 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - irondash_engine_context - super_native_extensions ) list(APPEND FLUTTER_FFI_PLUGIN_LIST