diff --git a/examples/composer/.gitignore b/examples/composer/.gitignore new file mode 100644 index 000000000..3820a95c6 --- /dev/null +++ b/examples/composer/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/examples/composer/.metadata b/examples/composer/.metadata new file mode 100644 index 000000000..e4ea30271 --- /dev/null +++ b/examples/composer/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "44a626f4f0027bc38a46dc68aed5964b05a83c18" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + - platform: macos + create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/composer/README.md b/examples/composer/README.md new file mode 100644 index 000000000..97305c999 --- /dev/null +++ b/examples/composer/README.md @@ -0,0 +1,9 @@ +# GenUI Composer + +An application for experimenting with and building A2UI surfaces. + +The composer allows you to: + +- Generate a UI surface from a text prompt using AI. +- Browse a gallery of pre-designed surfaces. +- View and edit the underlying A2UI JSON for any surface and see a live preview. diff --git a/examples/composer/analysis_options.yaml b/examples/composer/analysis_options.yaml new file mode 100644 index 000000000..6a65e2191 --- /dev/null +++ b/examples/composer/analysis_options.yaml @@ -0,0 +1,5 @@ +# Copyright 2025 The Flutter Authors. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +include: package:flutter_lints/flutter.yaml diff --git a/examples/composer/lib/ai_client.dart b/examples/composer/lib/ai_client.dart new file mode 100644 index 000000000..dfed0dbdf --- /dev/null +++ b/examples/composer/lib/ai_client.dart @@ -0,0 +1,56 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; + +import 'get_api_key.dart'; + +/// An abstract interface for AI clients. +abstract interface class AiClient { + /// Sends a message stream request to the AI service. + Stream sendStream( + String prompt, { + required List history, + }); + + /// Dispose of resources. + void dispose(); +} + +/// An implementation of [AiClient] using `package:dartantic_ai`. +class DartanticAiClient implements AiClient { + DartanticAiClient({String? modelName}) { + final String apiKey = getApiKey(); + _provider = dartantic.GoogleProvider(apiKey: apiKey); + _agent = dartantic.Agent.forProvider( + _provider, + chatModelName: modelName ?? 'gemini-3-flash-preview', + ); + } + + late final dartantic.GoogleProvider _provider; + late final dartantic.Agent _agent; + + @override + Stream sendStream( + String prompt, { + required List history, + }) async* { + final Stream> stream = _agent.sendStream( + prompt, + history: history, + ); + + await for (final result in stream) { + if (result.output.isNotEmpty) { + yield result.output; + } + } + } + + @override + void dispose() {} +} diff --git a/examples/composer/lib/ai_client_transport.dart b/examples/composer/lib/ai_client_transport.dart new file mode 100644 index 000000000..d5367682f --- /dev/null +++ b/examples/composer/lib/ai_client_transport.dart @@ -0,0 +1,74 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; +import 'package:genui/genui.dart'; +import 'package:logging/logging.dart'; + +import 'ai_client.dart'; + +/// A [Transport] that wraps an [AiClient] to communicate with an LLM. +class AiClientTransport implements Transport { + AiClientTransport({required this.aiClient}); + + final AiClient aiClient; + final A2uiTransportAdapter _adapter = A2uiTransportAdapter(); + final List _history = []; + final Logger _logger = Logger('AiClientTransport'); + + @override + Stream get incomingMessages => _adapter.incomingMessages; + + @override + Stream get incomingText => _adapter.incomingText; + + @override + Future sendRequest(ChatMessage message) async { + final buffer = StringBuffer(); + for (final dartantic.StandardPart part in message.parts) { + if (part.isUiInteractionPart) { + buffer.write(part.asUiInteractionPart!.interaction); + } else if (part is TextPart) { + buffer.write(part.text); + } + } + final text = buffer.toString(); + if (text.isEmpty) return; + + _history.add(dartantic.ChatMessage.user(text)); + + try { + final Stream stream = aiClient.sendStream( + text, + history: List.of(_history), + ); + final fullResponseBuffer = StringBuffer(); + + await for (final chunk in stream) { + if (chunk.isNotEmpty) { + fullResponseBuffer.write(chunk); + _adapter.addChunk(chunk); + } + } + + _history.add(dartantic.ChatMessage.model(fullResponseBuffer.toString())); + } catch (e, stack) { + _logger.severe('Error sending request', e, stack); + rethrow; + } + } + + @override + void dispose() { + _adapter.dispose(); + aiClient.dispose(); + } + + /// Adds a system message to the history. + void addSystemMessage(String content) { + _history.add(dartantic.ChatMessage.system(content)); + } +} diff --git a/examples/composer/lib/components_tab.dart b/examples/composer/lib/components_tab.dart new file mode 100644 index 000000000..fa21a6a63 --- /dev/null +++ b/examples/composer/lib/components_tab.dart @@ -0,0 +1,58 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; + +/// The Components tab shows every component in the standard catalog using +/// the built-in [DebugCatalogView]. +class ComponentsTab extends StatefulWidget { + const ComponentsTab({super.key}); + + @override + State createState() => _ComponentsTabState(); +} + +class _ComponentsTabState extends State { + late final Catalog _catalog; + + @override + void initState() { + super.initState(); + _catalog = BasicCatalogItems.asCatalog(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text('Components', style: theme.textTheme.headlineSmall), + ), + const SizedBox(height: 8), + Expanded( + child: DebugCatalogView( + catalog: _catalog, + onSubmit: (message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'User action: ' + '${jsonEncode(message.parts.last)}', + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/examples/composer/lib/create_tab.dart b/examples/composer/lib/create_tab.dart new file mode 100644 index 000000000..cacaaa9a3 --- /dev/null +++ b/examples/composer/lib/create_tab.dart @@ -0,0 +1,227 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:genui/genui.dart'; +import 'package:logging/logging.dart'; + +import 'ai_client.dart'; +import 'ai_client_transport.dart'; + +/// The Create tab. Shows a prompt input and, upon submission, generates a UI +/// surface via AI and transitions to the surface editor. +class CreateTab extends StatefulWidget { + const CreateTab({super.key, required this.onSurfaceCreated}); + + final void Function(String componentsJson, {String? dataJson}) + onSurfaceCreated; + + @override + State createState() => _CreateTabState(); +} + +class _CreateTabState extends State { + static const String _examplePrompt = 'a weather card'; + + final TextEditingController _promptController = TextEditingController(); + final Logger _logger = Logger('CreateTab'); + late final FocusNode _focusNode = FocusNode( + onKeyEvent: (node, event) { + if (!_isGenerating && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.enter && + !HardwareKeyboard.instance.isShiftPressed) { + _generate(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + ); + + bool _isGenerating = false; + bool _disposed = false; + String? _error; + + /// Resources for the current in-flight request, stored so they can be + /// disposed if the widget is torn down mid-generation. + AiClientTransport? _activeTransport; + SurfaceController? _activeController; + Conversation? _activeConversation; + + Future _generate() async { + final String prompt = _promptController.text.trim().isEmpty + ? _examplePrompt + : _promptController.text.trim(); + + setState(() { + _isGenerating = true; + _error = null; + }); + + try { + final AiClient aiClient = DartanticAiClient(); + final transport = _activeTransport = AiClientTransport( + aiClient: aiClient, + ); + + final Catalog catalog = BasicCatalogItems.asCatalog(); + final controller = _activeController = SurfaceController( + catalogs: [catalog], + ); + + final conversation = _activeConversation = Conversation( + controller: controller, + transport: transport, + ); + + final promptBuilder = PromptBuilder.chat( + catalog: catalog, + systemPromptFragments: [ + 'You are a UI generator. The user will describe a UI they want. ' + 'Generate a single A2UI surface that matches their description. ' + 'Be creative and use appropriate components from the catalog.', + ], + ); + transport.addSystemMessage(promptBuilder.systemPromptJoined()); + + final message = ChatMessage.user(prompt); + await conversation.sendRequest(message); + + await Future.delayed(Duration.zero); + + if (_disposed) return; + + final surfaceId = controller.activeSurfaceIds.firstOrNull; + if (surfaceId != null) { + final context = controller.contextFor(surfaceId); + final definition = context.definition.value; + if (definition != null) { + final componentsJson = const JsonEncoder.withIndent(' ').convert( + definition.components.values.map((c) => c.toJson()).toList(), + ); + + final dataModel = context.dataModel; + final data = dataModel.getValue(DataPath.root); + final String? dataJson = + data is Map && data.isNotEmpty + ? const JsonEncoder.withIndent(' ').convert(data) + : null; + + widget.onSurfaceCreated(componentsJson, dataJson: dataJson); + } else { + setState(() { + _error = 'Surface was created but has no definition.'; + }); + } + } else { + setState(() { + _error = + 'No surface was generated. The AI may not have produced ' + 'valid A2UI output. Try a different description.'; + }); + } + } catch (e, stack) { + _logger.severe('Error generating surface', e, stack); + setState(() { + _error = 'Error: $e'; + }); + } finally { + _disposeActiveResources(); + if (mounted) { + setState(() { + _isGenerating = false; + }); + } + } + } + + void _disposeActiveResources() { + _activeConversation?.dispose(); + _activeController?.dispose(); + _activeTransport?.dispose(); + _activeConversation = null; + _activeController = null; + _activeTransport = null; + } + + @override + void dispose() { + _disposed = true; + _disposeActiveResources(); + _focusNode.dispose(); + _promptController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 48.0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'What would you like to build?', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w300, + ), + ), + const SizedBox(height: 24), + TextField( + controller: _promptController, + focusNode: _focusNode, + decoration: InputDecoration( + hintText: 'Describe a UI... (e.g. "$_examplePrompt")', + border: const OutlineInputBorder(), + suffixIcon: _isGenerating + ? const Padding( + padding: EdgeInsets.all(12.0), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : IconButton( + icon: const Icon(Icons.send), + onPressed: _generate, + ), + ), + enabled: !_isGenerating, + maxLines: 3, + minLines: 1, + ), + if (_isGenerating) ...[ + const SizedBox(height: 16), + Text( + 'Generating surface...', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + if (_error != null) ...[ + const SizedBox(height: 16), + Text( + _error!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/examples/composer/lib/gallery_tab.dart b/examples/composer/lib/gallery_tab.dart new file mode 100644 index 000000000..50cc4309d --- /dev/null +++ b/examples/composer/lib/gallery_tab.dart @@ -0,0 +1,326 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:genui/genui.dart'; +import 'package:logging/logging.dart'; + +import 'sample_parser.dart'; +import 'surface_utils.dart'; + +class GalleryTab extends StatefulWidget { + const GalleryTab({super.key, required this.onOpenInEditor}); + + final void Function(String jsonl, {String? dataJson}) onOpenInEditor; + + @override + State createState() => _GalleryTabState(); +} + +class _GalleryTabState extends State + with AutomaticKeepAliveClientMixin { + final Logger _logger = Logger('GalleryTab'); + List<_GallerySampleMetadata> _samples = []; + bool _isLoading = true; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _loadSampleMetadata(); + } + + /// Loads just the metadata (name, description) for each sample, without + /// creating SurfaceControllers or rendering anything. + Future _loadSampleMetadata() async { + try { + final String manifestContent = await rootBundle.loadString( + 'samples/manifest.txt', + ); + final List filenames = + manifestContent + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty && line.endsWith('.sample')) + .toList() + ..sort(); + + final samples = <_GallerySampleMetadata>[]; + + for (final filename in filenames) { + try { + final String content = await rootBundle.loadString( + 'samples/$filename', + ); + final Sample sample = SampleParser.parseString(content); + samples.add( + _GallerySampleMetadata( + name: sample.name, + description: sample.description, + rawContent: content, + rawJsonl: sample.rawJsonl, + ), + ); + } catch (e) { + _logger.warning('Skipping sample $filename: $e'); + } + } + + if (mounted) { + setState(() { + _samples = samples; + _isLoading = false; + }); + } + } catch (e) { + _logger.severe('Error loading sample metadata', e); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + void _openSampleInEditor(_GallerySampleMetadata meta) { + widget.onOpenInEditor(meta.rawJsonl); + } + + @override + Widget build(BuildContext context) { + super.build(context); + final theme = Theme.of(context); + + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (_samples.isEmpty) { + return Center( + child: Text( + 'No samples found.', + textAlign: TextAlign.center, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text('Gallery', style: theme.textTheme.headlineSmall), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final double width = constraints.maxWidth; + int crossAxisCount = 2; + if (width > 1200) { + crossAxisCount = 3; + } + + return SingleChildScrollView( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int col = 0; col < crossAxisCount; col++) ...[ + if (col > 0) const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for ( + int i = col; + i < _samples.length; + i += crossAxisCount + ) ...[ + if (i >= crossAxisCount) + const SizedBox(height: 12), + _GalleryCard( + meta: _samples[i], + onTap: () => _openSampleInEditor(_samples[i]), + ), + ], + ], + ), + ), + ], + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _GallerySampleMetadata { + final String name; + final String description; + final String rawContent; + final String rawJsonl; + + _GallerySampleMetadata({ + required this.name, + required this.description, + required this.rawContent, + required this.rawJsonl, + }); +} + +/// A gallery card that renders a live, sandboxed surface preview. +/// +/// Each card creates its own [SurfaceController] on init, feeds the sample +/// messages into it, and renders the resulting surface scaled down to fit the +/// card. The preview is non-interactive (taps pass through to the card's +/// InkWell) and fully clipped to prevent layout overflow. +class _GalleryCard extends StatefulWidget { + const _GalleryCard({required this.meta, required this.onTap}); + + final _GallerySampleMetadata meta; + final VoidCallback onTap; + + @override + State<_GalleryCard> createState() => _GalleryCardState(); +} + +class _GalleryCardState extends State<_GalleryCard> + with AutomaticKeepAliveClientMixin { + SurfaceController? _controller; + List _surfaceIds = []; + bool _isLoading = true; + bool _hasError = false; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _loadSurface(); + } + + Future _loadSurface() async { + try { + final result = await loadSampleSurface(widget.meta.rawContent); + if (mounted) { + setState(() { + _controller = result.controller; + _surfaceIds = result.surfaceIds; + _isLoading = false; + }); + } else { + result.controller.dispose(); + } + } catch (e) { + if (mounted) { + setState(() { + _hasError = true; + _isLoading = false; + }); + } + } + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + final theme = Theme.of(context); + + return Card( + clipBehavior: Clip.antiAlias, + elevation: 1, + child: InkWell( + onTap: widget.onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 4), + child: Tooltip( + message: widget.meta.description, + child: Text( + widget.meta.name, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ClipRect( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: _buildPreview(theme), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPreview(ThemeData theme) { + if (_isLoading) { + return SizedBox( + height: 100, + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.onSurfaceVariant.withAlpha(100), + ), + ), + ), + ); + } + + if (_hasError || _surfaceIds.isEmpty || _controller == null) { + return SizedBox( + height: 100, + child: Center( + child: Icon( + Icons.widgets_outlined, + size: 32, + color: theme.colorScheme.onSurfaceVariant.withAlpha(60), + ), + ), + ); + } + + final String surfaceId = _surfaceIds.first; + final SurfaceContext surfaceContext = _controller!.contextFor(surfaceId); + + return RepaintBoundary( + child: IgnorePointer( + child: Surface( + key: ValueKey(surfaceId), + surfaceContext: surfaceContext, + ), + ), + ); + } +} diff --git a/examples/composer/lib/get_api_key.dart b/examples/composer/lib/get_api_key.dart new file mode 100644 index 000000000..0c79e9f2d --- /dev/null +++ b/examples/composer/lib/get_api_key.dart @@ -0,0 +1,27 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +const String _geminiApiKey = String.fromEnvironment('GEMINI_API_KEY'); + +String? debugApiKey; + +String getApiKey() { + if (debugApiKey != null) { + return debugApiKey!; + } + String apiKey = _geminiApiKey.isEmpty + ? Platform.environment['GEMINI_API_KEY'] ?? '' + : _geminiApiKey; + if (apiKey.isEmpty) { + throw Exception( + 'Gemini API key is required. Run the app with a GEMINI_API_KEY as a ' + 'Dart environment variable, for example by running with ' + '-D GEMINI_API_KEY=\$GEMINI_API_KEY or set the GEMINI_API_KEY ' + 'environment variable in your shell environment.', + ); + } + return apiKey; +} diff --git a/examples/composer/lib/main.dart b/examples/composer/lib/main.dart new file mode 100644 index 000000000..8bebe9d33 --- /dev/null +++ b/examples/composer/lib/main.dart @@ -0,0 +1,136 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; + +import 'components_tab.dart'; +import 'create_tab.dart'; +import 'gallery_tab.dart'; +import 'surface_editor.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + runApp(const ComposerApp()); +} + +class ComposerApp extends StatelessWidget { + const ComposerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'GenUI Composer', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.light, + ), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.dark, + ), + useMaterial3: true, + ), + home: const ComposerShell(), + ); + } +} + +class ComposerShell extends StatefulWidget { + const ComposerShell({super.key}); + + @override + State createState() => _ComposerShellState(); +} + +class _ComposerShellState extends State { + int _selectedIndex = 0; + + String? _editorJsonl; + String? _editorDataJson; + int _editorKey = 0; + + void _openSurfaceEditor(String jsonl, {String? dataJson}) { + setState(() { + _editorJsonl = jsonl; + _editorDataJson = dataJson; + _editorKey++; + _selectedIndex = 0; // Switch to the Create tab + }); + } + + void _closeSurfaceEditor() { + setState(() { + _editorJsonl = null; + _editorDataJson = null; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: _selectedIndex, + onDestinationSelected: (int index) { + setState(() { + _selectedIndex = index; + }); + }, + labelType: NavigationRailLabelType.all, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.add_circle_outline), + selectedIcon: Icon(Icons.add_circle), + label: Text('Create'), + ), + NavigationRailDestination( + icon: Icon(Icons.grid_view_outlined), + selectedIcon: Icon(Icons.grid_view), + label: Text('Gallery'), + ), + NavigationRailDestination( + icon: Icon(Icons.widgets_outlined), + selectedIcon: Icon(Icons.widgets), + label: Text('Components'), + ), + ], + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: IndexedStack( + index: _selectedIndex, + children: [ + _editorJsonl != null + ? SurfaceEditorView( + key: ValueKey('editor-$_editorKey'), + initialJsonl: _editorJsonl!, + initialDataJson: _editorDataJson, + onClose: _closeSurfaceEditor, + ) + : CreateTab( + onSurfaceCreated: (String jsonl, {String? dataJson}) { + _openSurfaceEditor(jsonl, dataJson: dataJson); + }, + ), + GalleryTab(onOpenInEditor: _openSurfaceEditor), + const ComponentsTab(), + ], + ), + ), + ], + ), + ); + } +} diff --git a/examples/composer/lib/sample_parser.dart b/examples/composer/lib/sample_parser.dart new file mode 100644 index 000000000..ac2e85aa5 --- /dev/null +++ b/examples/composer/lib/sample_parser.dart @@ -0,0 +1,73 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:genui/genui.dart'; +import 'package:yaml/yaml.dart'; + +/// A parsed sample containing metadata and a stream of A2UI messages. +class Sample { + final String name; + final String description; + final String rawJsonl; + final Stream messages; + + Sample({ + required this.name, + required this.description, + required this.rawJsonl, + required this.messages, + }); +} + +/// Parses `.sample` files with a YAML frontmatter header and a JSONL body. +class SampleParser { + static Sample parseString(String content) { + final List lines = const LineSplitter().convert(content); + var startLine = 0; + if (lines.isNotEmpty && lines.first.trim() == '---') { + startLine = 1; + } + + final int separatorIndex = lines.indexOf('---', startLine); + + if (separatorIndex == -1) { + throw const FormatException( + 'Sample file must contain a YAML header and a JSONL body separated ' + 'by "---"', + ); + } + + final String yamlHeader = lines + .sublist(startLine, separatorIndex) + .join('\n'); + final String jsonlBody = lines.sublist(separatorIndex + 1).join('\n'); + + final Object? yamlNode = loadYaml(yamlHeader); + final Map header = (yamlNode is Map) ? yamlNode : {}; + final String name = header['name'] as String? ?? 'Untitled Sample'; + final String description = header['description'] as String? ?? ''; + + final Stream messages = Stream.fromIterable( + const LineSplitter() + .convert(jsonlBody) + .where((line) => line.trim().isNotEmpty) + .map((line) { + final Object? json = jsonDecode(line); + if (json is Map) { + return A2uiMessage.fromJson(json); + } + throw FormatException('Invalid JSON line: $line'); + }), + ); + + return Sample( + name: name, + description: description, + rawJsonl: jsonlBody.trim(), + messages: messages, + ); + } +} diff --git a/examples/composer/lib/surface_editor.dart b/examples/composer/lib/surface_editor.dart new file mode 100644 index 000000000..3fd885159 --- /dev/null +++ b/examples/composer/lib/surface_editor.dart @@ -0,0 +1,475 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_highlight/themes/vs.dart'; +import 'package:flutter_highlight/themes/vs2015.dart'; +import 'package:genui/genui.dart'; +import 'package:highlight/languages/json.dart' as json_lang; + +import 'surface_utils.dart'; + +const _kEditorSurfaceId = 'editor'; +const _kDebounceDuration = Duration(milliseconds: 400); + +/// A surface editor view that shows A2UI JSONL and data model and a live +/// rendered preview. +class SurfaceEditorView extends StatefulWidget { + const SurfaceEditorView({ + super.key, + required this.initialJsonl, + this.initialDataJson, + required this.onClose, + }); + + /// The initial JSON string to load. Can be either: + /// - A JSON array of components (clean format from Create tab) + /// - JSONL with A2UI protocol messages (from Gallery samples) + final String initialJsonl; + + /// Optional initial data model JSON to pre-populate the Data pane. + final String? initialDataJson; + + /// Called when the user wants to close the editor and go back. + final VoidCallback onClose; + + @override + State createState() => _SurfaceEditorViewState(); +} + +class _SurfaceEditorViewState extends State { + late CodeController _jsonController; + late CodeController _dataController; + late SurfaceController _surfaceController; + late final Catalog _catalog; + final List _surfaceIds = []; + StreamSubscription? _surfaceSub; + ValueNotifier? _dataModelNotifier; + Timer? _jsonDebounce; + Timer? _dataDebounce; + String? _parseError; + String? _dataError; + + /// The current JSONL text for display/edit. + late String _currentJson; + + /// The current data model JSON for display/edit. + String _currentDataJson = '{}'; + + /// Flag to suppress listener notifications during programmatic updates. + bool _isInternalUpdate = false; + + @override + void initState() { + super.initState(); + + _catalog = BasicCatalogItems.asCatalog(); + _currentJson = _toJsonl(widget.initialJsonl, widget.initialDataJson); + if (widget.initialDataJson != null && + widget.initialDataJson!.trim().isNotEmpty) { + _currentDataJson = widget.initialDataJson!; + } + _jsonController = CodeController( + text: _currentJson, + language: json_lang.json, + ); + _dataController = CodeController( + text: _currentDataJson, + language: json_lang.json, + ); + + _surfaceController = SurfaceController(catalogs: [_catalog]); + _setupSurfaceListener(); + _applyJson(_currentJson); + + _jsonController.addListener(_onJsonControllerChanged); + _dataController.addListener(_onDataControllerChanged); + } + + /// Converts input to pretty-printed JSONL. If the input is already JSONL, + /// pretty-prints each line. If it's a components array, wraps it in + /// protocol envelopes. + static String _toJsonl(String input, String? dataJson) { + final trimmed = input.trim(); + + // If it's a components array, convert to full JSONL. + if (trimmed.startsWith('[')) { + try { + final parsed = jsonDecode(trimmed); + if (parsed is List) { + return componentsToJsonl( + trimmed, + dataJson: dataJson, + surfaceId: _kEditorSurfaceId, + ); + } + } catch (_) {} + } + + final lines = const LineSplitter() + .convert(trimmed) + .where((line) => line.trim().isNotEmpty); + final formatted = []; + for (final line in lines) { + try { + final parsed = jsonDecode(line.trim()); + formatted.add(const JsonEncoder.withIndent(' ').convert(parsed)); + } catch (_) { + formatted.add(line); + } + } + return formatted.join('\n\n'); + } + + void _setupSurfaceListener() { + _surfaceSub = _surfaceController.surfaceUpdates.listen((update) { + if (update is SurfaceAdded) { + if (!_surfaceIds.contains(update.surfaceId)) { + setState(() { + _surfaceIds.add(update.surfaceId); + }); + _subscribeToDataModel(); + _refreshDataModelDisplay(); + } + } else if (update is SurfaceRemoved) { + setState(() { + _surfaceIds.remove(update.surfaceId); + }); + } else if (update is ComponentsUpdated) { + _refreshDataModelDisplay(); + } + }); + } + + void _subscribeToDataModel() { + _dataModelNotifier?.removeListener(_onDataModelChanged); + if (_surfaceIds.isEmpty) return; + + final dataModel = _surfaceController.store.getDataModel(_surfaceIds.first); + _dataModelNotifier = dataModel.subscribe(DataPath.root); + _dataModelNotifier!.addListener(_onDataModelChanged); + } + + void _onDataModelChanged() { + _refreshDataModelDisplay(); + } + + /// Refreshes the data model display from the current SurfaceController state. + void _refreshDataModelDisplay() { + if (_surfaceIds.isEmpty) return; + + final surfaceId = _surfaceIds.first; + final dataModel = _surfaceController.store.getDataModel(surfaceId); + final data = dataModel.getValue(DataPath.root); + final dataJson = const JsonEncoder.withIndent(' ').convert(data); + + _isInternalUpdate = true; + _dataController.text = dataJson; + _isInternalUpdate = false; + setState(() { + _currentDataJson = dataJson; + }); + } + + void _applyJson(String json) { + _dataModelNotifier?.removeListener(_onDataModelChanged); + _dataModelNotifier = null; + _surfaceSub?.cancel(); + _surfaceController.dispose(); + + _surfaceController = SurfaceController(catalogs: [_catalog]); + _surfaceIds.clear(); + _setupSurfaceListener(); + + setState(() { + _parseError = null; + }); + + try { + final trimmed = json.trim(); + + // Split on blank lines to separate pretty-printed JSON messages. + final chunks = trimmed.split(RegExp(r'\n\s*\n')); + + for (final chunk in chunks) { + final trimmedChunk = chunk.trim(); + if (trimmedChunk.isEmpty || !trimmedChunk.startsWith('{')) continue; + + final obj = jsonDecode(trimmedChunk); + if (obj is Map) { + final message = A2uiMessage.fromJson(obj); + _surfaceController.handleMessage(message); + } + } + _refreshDataModelDisplay(); + } catch (e) { + setState(() { + _parseError = e.toString(); + }); + } + } + + /// Applies data model JSON to the current surface. + void _applyDataModel(String dataJson) { + if (_surfaceIds.isEmpty) return; + + setState(() { + _dataError = null; + }); + + try { + final parsed = jsonDecode(dataJson.trim()); + if (parsed is Map) { + final surfaceId = _surfaceIds.first; + _surfaceController.handleMessage( + A2uiMessage.fromJson({ + 'version': kProtocolVersion, + 'updateDataModel': { + 'surfaceId': surfaceId, + 'path': '/', + 'value': parsed, + }, + }), + ); + } else { + setState(() { + _dataError = 'Data model must be a JSON object'; + }); + } + } catch (e) { + setState(() { + _dataError = e.toString(); + }); + } + } + + void _onJsonControllerChanged() { + final text = _jsonController.text; + if (text == _currentJson) return; + _currentJson = text; + _jsonDebounce?.cancel(); + _jsonDebounce = Timer(_kDebounceDuration, () => _applyJson(text)); + } + + void _onDataControllerChanged() { + if (_isInternalUpdate) return; + final text = _dataController.text; + if (text == _currentDataJson) return; + _currentDataJson = text; + _dataDebounce?.cancel(); + _dataDebounce = Timer(_kDebounceDuration, () => _applyDataModel(text)); + } + + @override + void dispose() { + _jsonDebounce?.cancel(); + _dataDebounce?.cancel(); + _dataModelNotifier?.removeListener(_onDataModelChanged); + _jsonController.removeListener(_onJsonControllerChanged); + _dataController.removeListener(_onDataControllerChanged); + _surfaceSub?.cancel(); + _surfaceController.dispose(); + _jsonController.dispose(); + _dataController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + children: [ + _buildHeaderBar(theme), + Expanded( + child: Row( + children: [ + Expanded(child: _buildEditorPane(theme)), + const VerticalDivider(width: 1), + Expanded(child: _buildPreviewPane(theme)), + ], + ), + ), + ], + ); + } + + Widget _buildHeaderBar(ThemeData theme) { + return Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + border: Border(bottom: BorderSide(color: theme.dividerColor)), + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: widget.onClose, + tooltip: 'Back to Create', + ), + const SizedBox(width: 8), + Text('Surface Editor', style: theme.textTheme.titleMedium), + ], + ), + ); + } + + Widget _buildEditorPane(ThemeData theme) { + return Column( + children: [ + Expanded( + flex: 3, + child: _buildEditorSection( + theme: theme, + label: 'JSONL', + controller: _jsonController, + error: _parseError, + ), + ), + Divider(height: 1, color: theme.dividerColor), + Expanded( + flex: 2, + child: _buildEditorSection( + theme: theme, + label: 'Data', + controller: _dataController, + error: _dataError, + ), + ), + ], + ); + } + + Widget _buildPreviewPane(ThemeData theme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + 'Preview', + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.fromLTRB(4, 0, 8, 8), + decoration: BoxDecoration( + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + color: theme.colorScheme.surfaceContainerLowest, + ), + child: _surfaceIds.isEmpty + ? Center( + child: Text( + _parseError != null + ? 'Fix the JSON to see a preview' + : 'No surfaces to display', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + for (final surfaceId in _surfaceIds) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Surface( + key: ValueKey(surfaceId), + surfaceContext: _surfaceController.contextFor( + surfaceId, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildEditorSection({ + required ThemeData theme, + required String label, + required CodeController controller, + required String? error, + }) { + final codeStyles = theme.brightness == Brightness.dark + ? vs2015Theme + : vsTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Text( + label, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 4, 4), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + color: theme.colorScheme.surfaceContainerLowest, + ), + clipBehavior: Clip.antiAlias, + child: CodeTheme( + data: CodeThemeData(styles: codeStyles), + child: SingleChildScrollView( + child: CodeField( + controller: controller, + gutterStyle: GutterStyle.none, + textStyle: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ), + ), + ), + ), + if (error != null) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 4, 4), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + error, + style: TextStyle( + fontSize: 11, + color: theme.colorScheme.onErrorContainer, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } +} diff --git a/examples/composer/lib/surface_utils.dart b/examples/composer/lib/surface_utils.dart new file mode 100644 index 000000000..ee34f7e77 --- /dev/null +++ b/examples/composer/lib/surface_utils.dart @@ -0,0 +1,191 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:genui/genui.dart'; +import 'package:logging/logging.dart'; + +import 'sample_parser.dart'; + +final _logger = Logger('SurfaceUtils'); + +const kProtocolVersion = 'v0.9'; + +/// Merges a list of component maps by their `id` field. +/// Later entries override earlier ones with the same ID. +Map> mergeComponentsById( + List components, [ + Map>? existing, +]) { + final map = existing ?? >{}; + for (final comp in components) { + if (comp is Map && comp['id'] != null) { + map[comp['id'] as String] = comp; + } + } + return map; +} + +/// Sets a value at a nested path in a data model map. +/// Path format: "/segment1/segment2/..." — leading slashes are stripped. +void setNestedValue(Map model, String path, Object value) { + final segments = path.split('/').where((s) => s.isNotEmpty).toList(); + if (segments.isEmpty) return; + + Map current = model; + for (int i = 0; i < segments.length - 1; i++) { + current.putIfAbsent(segments[i], () => {}); + final next = current[segments[i]]; + if (next is Map) { + current = next; + } else { + return; // Path conflict, skip. + } + } + + current[segments.last] = value; +} + +/// Detects the root component ID from a list of component maps. +/// +/// The root is the component whose ID is not referenced as a child by any +/// other component. Falls back to `'root'` if detection is ambiguous. +String detectRootId(List components) { + final allIds = components + .whereType>() + .map((c) => c['id'] as String?) + .whereType() + .toList(); + + final allIdSet = allIds.toSet(); + + final referencedIds = {}; + for (final comp in components) { + if (comp is Map) { + _collectStringValues(comp, allIdSet, referencedIds); + } + } + + final rootCandidates = allIdSet.difference(referencedIds); + if (rootCandidates.isEmpty) return 'root'; + + // Return the first candidate by list position to ensure deterministic + // ordering when there are multiple unreferenced components. + return allIds.firstWhere(rootCandidates.contains); +} + +/// Recursively walks [obj] and adds any string values that appear in +/// [knownIds] to [result]. Skips the component's own 'id' key. +void _collectStringValues( + Object? obj, + Set knownIds, + Set result, { + String? parentKey, +}) { + if (obj is Map) { + for (final entry in obj.entries) { + _collectStringValues(entry.value, knownIds, result, parentKey: entry.key); + } + } else if (obj is List) { + for (final item in obj) { + _collectStringValues(item, knownIds, result); + } + } else if (obj is String && parentKey != 'id' && knownIds.contains(obj)) { + result.add(obj); + } +} + +/// Reconstructs full A2UI JSONL from a components array and optional data +/// model. Each message is pretty-printed and separated by a blank line. +String componentsToJsonl( + String componentsJson, { + String? dataJson, + String surfaceId = 'editor', +}) { + final encoder = const JsonEncoder.withIndent(' '); + final messages = []; + + // 1. createSurface + messages.add( + encoder.convert({ + 'version': kProtocolVersion, + 'createSurface': { + 'surfaceId': surfaceId, + 'catalogId': basicCatalogId, + 'sendDataModel': true, + }, + }), + ); + + // 2. updateComponents + try { + final parsed = jsonDecode(componentsJson.trim()); + if (parsed is List) { + final rootId = detectRootId(parsed); + messages.add( + encoder.convert({ + 'version': kProtocolVersion, + 'updateComponents': { + 'surfaceId': surfaceId, + 'root': rootId, + 'components': parsed, + }, + }), + ); + } + } catch (e) { + _logger.fine('Could not parse components JSON, skipping', e); + } + + // 3. updateDataModel (optional) + if (dataJson != null && dataJson.trim().isNotEmpty) { + try { + final parsed = jsonDecode(dataJson.trim()); + if (parsed is Map && parsed.isNotEmpty) { + messages.add( + encoder.convert({ + 'version': kProtocolVersion, + 'updateDataModel': { + 'surfaceId': surfaceId, + 'path': '/', + 'value': parsed, + }, + }), + ); + } + } catch (e) { + _logger.fine('Could not parse data model JSON, skipping', e); + } + } + + return messages.join('\n\n'); +} + +/// Creates a [SurfaceController], feeds the parsed sample messages into it, +/// and returns the controller along with the discovered surface IDs. +Future<({SurfaceController controller, List surfaceIds})> +loadSampleSurface(String rawContent) async { + final catalog = BasicCatalogItems.asCatalog(); + final controller = SurfaceController(catalogs: [catalog]); + final surfaceIds = []; + + final sub = controller.surfaceUpdates.listen((update) { + if (update is SurfaceAdded) { + surfaceIds.add(update.surfaceId); + } + }); + + try { + final sample = SampleParser.parseString(rawContent); + await sample.messages.listen(controller.handleMessage).asFuture(); + } catch (e, s) { + _logger.warning('Error loading sample surface', e, s); + } + + await sub.cancel(); + + return (controller: controller, surfaceIds: surfaceIds); +} diff --git a/examples/composer/macos/.gitignore b/examples/composer/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/examples/composer/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/examples/composer/macos/Flutter/Flutter-Debug.xcconfig b/examples/composer/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/examples/composer/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/composer/macos/Flutter/Flutter-Release.xcconfig b/examples/composer/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/examples/composer/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/composer/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/composer/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..8236f5728 --- /dev/null +++ b/examples/composer/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/examples/composer/macos/Podfile b/examples/composer/macos/Podfile new file mode 100644 index 000000000..ff5ddb3b8 --- /dev/null +++ b/examples/composer/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/examples/composer/macos/Podfile.lock b/examples/composer/macos/Podfile.lock new file mode 100644 index 000000000..ed05ac66c --- /dev/null +++ b/examples/composer/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/examples/composer/macos/Runner.xcodeproj/project.pbxproj b/examples/composer/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..06257eb73 --- /dev/null +++ b/examples/composer/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 1809C441DC3E03A54AF9F80E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 19BABBAAF7FDB64B582732EB /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + F10B7C67FEDCDBE6BA85B595 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E59D57BFAECF81DB16C89B0C /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 03C162A914F6236D6A2303A9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 0441694D3AA914C365F13102 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 19BABBAAF7FDB64B582732EB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* composer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = composer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 346E10C61F1FF18B7E548527 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7714234F46E4AB54F5E5FB1C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C5A53B5CC448E03ACB287E02 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + CC74ABB6C9FE534D9E806CE9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E59D57BFAECF81DB16C89B0C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F10B7C67FEDCDBE6BA85B595 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1809C441DC3E03A54AF9F80E /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + EAF8FB022AC19D3F5E5B1F01 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* composer.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 19BABBAAF7FDB64B582732EB /* Pods_Runner.framework */, + E59D57BFAECF81DB16C89B0C /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + EAF8FB022AC19D3F5E5B1F01 /* Pods */ = { + isa = PBXGroup; + children = ( + 7714234F46E4AB54F5E5FB1C /* Pods-Runner.debug.xcconfig */, + 03C162A914F6236D6A2303A9 /* Pods-Runner.release.xcconfig */, + C5A53B5CC448E03ACB287E02 /* Pods-Runner.profile.xcconfig */, + CC74ABB6C9FE534D9E806CE9 /* Pods-RunnerTests.debug.xcconfig */, + 346E10C61F1FF18B7E548527 /* Pods-RunnerTests.release.xcconfig */, + 0441694D3AA914C365F13102 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + F1565797F09375106223156C /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + D0BFA3DEE71FF54CEEB50B12 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 13FB93DEF417658CE9C0121A /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* composer.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 13FB93DEF417658CE9C0121A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + D0BFA3DEE71FF54CEEB50B12 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F1565797F09375106223156C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CC74ABB6C9FE534D9E806CE9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.composer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/composer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/composer"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 346E10C61F1FF18B7E548527 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.composer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/composer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/composer"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0441694D3AA914C365F13102 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.composer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/composer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/composer"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/examples/composer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/composer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/composer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/composer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/composer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..c20c8f579 --- /dev/null +++ b/examples/composer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/composer/macos/Runner.xcworkspace/contents.xcworkspacedata b/examples/composer/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/examples/composer/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/composer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/composer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/composer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/composer/macos/Runner/AppDelegate.swift b/examples/composer/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..43bd41192 --- /dev/null +++ b/examples/composer/macos/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/examples/composer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/examples/composer/macos/Runner/Base.lproj/MainMenu.xib b/examples/composer/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..7469ca2ee --- /dev/null +++ b/examples/composer/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/composer/macos/Runner/Configs/AppInfo.xcconfig b/examples/composer/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..013f83fde --- /dev/null +++ b/examples/composer/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = composer + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.composer + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/examples/composer/macos/Runner/Configs/Debug.xcconfig b/examples/composer/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/examples/composer/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/composer/macos/Runner/Configs/Release.xcconfig b/examples/composer/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/examples/composer/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/composer/macos/Runner/Configs/Warnings.xcconfig b/examples/composer/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/examples/composer/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/examples/composer/macos/Runner/DebugProfile.entitlements b/examples/composer/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..08c3ab17c --- /dev/null +++ b/examples/composer/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/examples/composer/macos/Runner/Info.plist b/examples/composer/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/examples/composer/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/examples/composer/macos/Runner/MainFlutterWindow.swift b/examples/composer/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..7fc0ba7b0 --- /dev/null +++ b/examples/composer/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,30 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } + + override func orderFront(_ sender: Any?) { + super.orderFront(sender) + + // Set the window size after it's fully initialized. + // awakeFromNib is too early — Flutter resets the frame. + let width: CGFloat = 1700 + let height: CGFloat = 1000 + self.setContentSize(NSSize(width: width, height: height)) + self.center() + } +} diff --git a/examples/composer/macos/Runner/Release.entitlements b/examples/composer/macos/Runner/Release.entitlements new file mode 100644 index 000000000..ee95ab7e5 --- /dev/null +++ b/examples/composer/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/examples/composer/macos/RunnerTests/RunnerTests.swift b/examples/composer/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..8b03e329d --- /dev/null +++ b/examples/composer/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,16 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/examples/composer/pubspec.yaml b/examples/composer/pubspec.yaml new file mode 100644 index 000000000..14d960905 --- /dev/null +++ b/examples/composer/pubspec.yaml @@ -0,0 +1,35 @@ +# Copyright 2025 The Flutter Authors. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +name: composer +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ">=3.9.2 <4.0.0" + flutter: ">=3.35.7 <4.0.0" + +resolution: workspace + +dependencies: + dartantic_ai: ^3.0.0 + flutter: + sdk: flutter + flutter_code_editor: ^0.3.5 + flutter_highlight: ^0.7.0 + genui: ^0.7.0 + highlight: ^0.7.0 + logging: ^1.3.0 + yaml: ^3.1.3 + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - samples/ + - ../travel_app/assets/travel_images/ diff --git a/examples/composer/samples/animalKingdomExplorer.sample b/examples/composer/samples/animalKingdomExplorer.sample new file mode 100644 index 000000000..8c25bc7bc --- /dev/null +++ b/examples/composer/samples/animalKingdomExplorer.sample @@ -0,0 +1,42 @@ +--- +description: A simple, explicit UI to display a hierarchy of animals. +name: animalKingdomExplorer +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a simplified UI explorer for the Animal Kingdom. + + The UI must have a main 'Text' (variant 'h1') with the text "Simple Animal Explorer". + + Below the text heading, create a 'Tabs' component with exactly three tabs: "Mammals", "Birds", and "Reptiles". + + Each tab's content should be a 'Column'. The first item in each column must be a 'TextField' with the label "Search...". Below the search field, display the hierarchy for that tab using nested 'Card' components. + + The exact hierarchy to create is as follows: + + **1. "Mammals" Tab:** + - A 'Card' for the Class "Mammalia". + - Inside the "Mammalia" card, create two 'Card's for the following Orders: + - A 'Card' for the Order "Carnivora". Inside this, create 'Card's for these three species: "Lion", "Tiger", "Wolf". + - A 'Card' for the Order "Artiodactyla". Inside this, create 'Card's for these two species: "Giraffe", "Hippopotamus". + + **2. "Birds" Tab:** + - A 'Card' for the Class "Aves". + - Inside the "Aves" card, create three 'Card's for the following Orders: + - A 'Card' for the Order "Accipitriformes". Inside this, create a 'Card' for the species: "Bald Eagle". + - A 'Card' for the Order "Struthioniformes". Inside this, create a 'Card' for the species: "Ostrich". + - A 'Card' for the Order "Sphenisciformes". Inside this, create a 'Card' for the species: "Penguin". + + **3. "Reptiles" Tab:** + - A 'Card' for the Class "Reptilia". + - Inside the "Reptilia" card, create two 'Card's for the following Orders: + - A 'Card' for the Order "Crocodilia". Inside this, create a 'Card' for the species: "Nile Crocodile". + - A 'Card' for the Order "Squamata". Inside this, create 'Card's for these two species: "Komodo Dragon", "Ball Python". + + Each species card must contain a 'Row' with an 'Image' and a 'Text' component for the species name. Do not add any other components. + + Each Class and Order card must contain a 'Column' with a 'Text' component with the name, and then the children cards below. + + IMPORTANT: Do not skip any of the classes, orders, or species above. Include every item that is mentioned. + +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"List","children":["mainHeading","mainTabs"]},{"id":"mainHeading","component":"Text","text":"Simple Animal Explorer","variant":"h1"},{"id":"mainTabs","component":"Tabs","tabs":[{"title":"Mammals","child":"mammalsTabContent"},{"title":"Birds","child":"birdsTabContent"},{"title":"Reptiles","child":"reptilesTabContent"}]},{"id":"mammalsTabContent","component":"Column","children":["mammalsSearchField","mammaliaCard"]},{"id":"mammalsSearchField","component":"TextField","label":"Search...","value":""},{"id":"mammaliaCard","component":"Card","child":"mammaliaColumn"},{"id":"mammaliaColumn","component":"Column","children":["mammaliaText","carnivoraCard","artiodactylaCard"]},{"id":"mammaliaText","component":"Text","text":"Class: Mammalia"},{"id":"carnivoraCard","component":"Card","child":"carnivoraColumn"},{"id":"carnivoraColumn","component":"Column","children":["carnivoraText","lionCard","tigerCard","wolfCard"]},{"id":"carnivoraText","component":"Text","text":"Order: Carnivora"},{"id":"lionCard","component":"Card","child":"lionRow"},{"id":"lionRow","component":"Row","children":["lionImage","lionText"]},{"id":"lionImage","component":"Image","url":"../travel_app/assets/travel_images/gray_wolf.jpg"},{"id":"lionText","component":"Text","text":"Lion"},{"id":"tigerCard","component":"Card","child":"tigerRow"},{"id":"tigerRow","component":"Row","children":["tigerImage","tigerText"]},{"id":"tigerImage","component":"Image","url":"../travel_app/assets/travel_images/desert_iguana_mojave_desert_california.jpg"},{"id":"tigerText","component":"Text","text":"Tiger"},{"id":"wolfCard","component":"Card","child":"wolfRow"},{"id":"wolfRow","component":"Row","children":["wolfImage","wolfText"]},{"id":"wolfImage","component":"Image","url":"../travel_app/assets/travel_images/whitetip_reef_shark_hawaii.jpg"},{"id":"wolfText","component":"Text","text":"Wolf"},{"id":"artiodactylaCard","component":"Card","child":"artiodactylaColumn"},{"id":"artiodactylaColumn","component":"Column","children":["artiodactylaText","giraffeCard","hippopotamusCard"]},{"id":"artiodactylaText","component":"Text","text":"Order: Artiodactyla"},{"id":"giraffeCard","component":"Card","child":"giraffeRow"},{"id":"giraffeRow","component":"Row","children":["giraffeImage","giraffeText"]},{"id":"giraffeImage","component":"Image","url":"../travel_app/assets/travel_images/banded_cleaner_shrimp.jpg"},{"id":"giraffeText","component":"Text","text":"Giraffe"},{"id":"hippopotamusCard","component":"Card","child":"hippopotamusRow"},{"id":"hippopotamusRow","component":"Row","children":["hippopotamusImage","hippopotamusText"]},{"id":"hippopotamusImage","component":"Image","url":"../travel_app/assets/travel_images/crab_on_beach_phuket_thailand.jpg"},{"id":"hippopotamusText","component":"Text","text":"Hippopotamus"},{"id":"birdsTabContent","component":"Column","children":["birdsSearchField","avesCard"]},{"id":"birdsSearchField","component":"TextField","label":"Search...","value":""},{"id":"avesCard","component":"Card","child":"avesColumn"},{"id":"avesColumn","component":"Column","children":["avesText","accipitriformesCard","struthioniformesCard","sphenisciformesCard"]},{"id":"avesText","component":"Text","text":"Class: Aves"},{"id":"accipitriformesCard","component":"Card","child":"accipitriformesColumn"},{"id":"accipitriformesColumn","component":"Column","children":["accipitriformesText","baldEagleCard"]},{"id":"accipitriformesText","component":"Text","text":"Order: Accipitriformes"},{"id":"baldEagleCard","component":"Card","child":"baldEagleRow"},{"id":"baldEagleRow","component":"Row","children":["baldEagleImage","baldEagleText"]},{"id":"baldEagleImage","component":"Image","url":"../travel_app/assets/travel_images/sailing_contender_dinghy.jpg"},{"id":"baldEagleText","component":"Text","text":"Bald Eagle"},{"id":"struthioniformesCard","component":"Card","child":"struthioniformesColumn"},{"id":"struthioniformesColumn","component":"Column","children":["struthioniformesText","ostrichCard"]},{"id":"struthioniformesText","component":"Text","text":"Order: Struthioniformes"},{"id":"ostrichCard","component":"Card","child":"ostrichRow"},{"id":"ostrichRow","component":"Row","children":["ostrichImage","ostrichText"]},{"id":"ostrichImage","component":"Image","url":"../travel_app/assets/travel_images/snorkeling_hanauma_bay_hawaii.jpg"},{"id":"ostrichText","component":"Text","text":"Ostrich"},{"id":"sphenisciformesCard","component":"Card","child":"sphenisciformesColumn"},{"id":"sphenisciformesColumn","component":"Column","children":["sphenisciformesText","penguinCard"]},{"id":"sphenisciformesText","component":"Text","text":"Order: Sphenisciformes"},{"id":"penguinCard","component":"Card","child":"penguinRow"},{"id":"penguinRow","component":"Row","children":["penguinImage","penguinText"]},{"id":"penguinImage","component":"Image","url":"../travel_app/assets/travel_images/caribbean_reef_squid.jpg"},{"id":"penguinText","component":"Text","text":"Penguin"},{"id":"reptilesTabContent","component":"Column","children":["reptilesSearchField","reptiliaCard"]},{"id":"reptilesSearchField","component":"TextField","label":"Search...","value":""},{"id":"reptiliaCard","component":"Card","child":"reptiliaColumn"},{"id":"reptiliaColumn","component":"Column","children":["reptiliaText","crocodiliaCard","squamataCard"]},{"id":"reptiliaText","component":"Text","text":"Class: Reptilia"},{"id":"crocodiliaCard","component":"Card","child":"crocodiliaColumn"},{"id":"crocodiliaColumn","component":"Column","children":["crocodiliaText","nileCrocodileCard"]},{"id":"crocodiliaText","component":"Text","text":"Order: Crocodilia"},{"id":"nileCrocodileCard","component":"Card","child":"nileCrocodileRow"},{"id":"nileCrocodileRow","component":"Row","children":["nileCrocodileImage","nileCrocodileText"]},{"id":"nileCrocodileImage","component":"Image","url":"../travel_app/assets/travel_images/deep_sea_corals_wagner_seamount.jpg"},{"id":"nileCrocodileText","component":"Text","text":"Nile Crocodile"},{"id":"squamataCard","component":"Card","child":"squamataColumn"},{"id":"squamataColumn","component":"Column","children":["squamataText","komodoDragonCard","ballPythonCard"]},{"id":"squamataText","component":"Text","text":"Order: Squamata"},{"id":"komodoDragonCard","component":"Card","child":"komodoDragonRow"},{"id":"komodoDragonRow","component":"Row","children":["komodoDragonImage","komodoDragonText"]},{"id":"komodoDragonImage","component":"Image","url":"../travel_app/assets/travel_images/brain_coral.jpg"},{"id":"komodoDragonText","component":"Text","text":"Komodo Dragon"},{"id":"ballPythonCard","component":"Card","child":"ballPythonRow"},{"id":"ballPythonRow","component":"Row","children":["ballPythonImage","ballPythonText"]},{"id":"ballPythonImage","component":"Image","url":"../travel_app/assets/travel_images/fluorescent_coral_monterey_bay_aquarium.jpg"},{"id":"ballPythonText","component":"Text","text":"Ball Python"}]}} diff --git a/examples/composer/samples/calendarEventCreator.sample b/examples/composer/samples/calendarEventCreator.sample new file mode 100644 index 000000000..3df5df374 --- /dev/null +++ b/examples/composer/samples/calendarEventCreator.sample @@ -0,0 +1,8 @@ +--- +description: A form to create a new calendar event. +name: calendarEventCreator +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a calendar event creation form. It should have a 'Text' (variant 'h1') "New Event". Include a 'TextField' for the "Event Title". Use a 'Row' for two 'DateTimeInput's for "Start Time" and "End Time" (initialize both with a literal empty string value: '' (do not bind to a data path)). Add a 'CheckBox' labeled "All-day event". Finally, a 'Row' with two 'Button's: "Save" and "Cancel". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["newEventTitle","eventTitleField","timeInputsRow","allDayCheckbox","actionButtonsRow"]},{"id":"newEventTitle","component":"Text","text":"New Event","variant":"h1"},{"id":"eventTitleField","component":"TextField","label":"Event Title","value":""},{"id":"timeInputsRow","component":"Row","children":["startTimeInput","endTimeInput"],"justify":"spaceBetween"},{"id":"startTimeInput","component":"DateTimeInput","value":"","enableDate":true,"enableTime":true,"label":"Start Time"},{"id":"endTimeInput","component":"DateTimeInput","value":"","enableDate":true,"enableTime":true,"label":"End Time"},{"id":"allDayCheckbox","component":"CheckBox","label":"All-day event","value":false},{"id":"actionButtonsRow","component":"Row","children":["saveButton","cancelButton"],"justify":"end"},{"id":"saveButton","component":"Button","child":"saveButtonText","action":{"event":{"name":"saveEvent"}},"variant":"primary"},{"id":"saveButtonText","component":"Text","text":"Save"},{"id":"cancelButton","component":"Button","child":"cancelButtonText","action":{"event":{"name":"cancelEvent"}}},{"id":"cancelButtonText","component":"Text","text":"Cancel"}]}} diff --git a/examples/composer/samples/chatRoom.sample b/examples/composer/samples/chatRoom.sample new file mode 100644 index 000000000..ae3d18f4f --- /dev/null +++ b/examples/composer/samples/chatRoom.sample @@ -0,0 +1,8 @@ +--- +description: A chat application interface. +name: chatRoom +prompt: | + Create a chat room interface. It should have a 'Column' for the message history. Inside, include several 'Card's representing messages, each with a 'Text' for the sender and a 'Text' for the message body. Specifically include these messages: "Alice: Hi there!", "Bob: Hello!". At the bottom, a 'Row' with a 'TextField' (label "Type a message...") and a 'Button' labeled "Send". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["messageHistoryColumn","messageInputRow"]},{"id":"messageHistoryColumn","component":"Column","children":["messageCardAlice","messageCardBob"]},{"id":"messageCardAlice","component":"Card","child":"messageContentAlice"},{"id":"messageContentAlice","component":"Column","children":["senderAlice","bodyAlice"]},{"id":"senderAlice","component":"Text","text":"Alice:"},{"id":"bodyAlice","component":"Text","text":"Hi there!"},{"id":"messageCardBob","component":"Card","child":"messageContentBob"},{"id":"messageContentBob","component":"Column","children":["senderBob","bodyBob"]},{"id":"senderBob","component":"Text","text":"Bob:"},{"id":"bodyBob","component":"Text","text":"Hello!"},{"id":"messageInputRow","component":"Row","children":["messageTextField","sendButton"]},{"id":"messageTextField","component":"TextField","label":"Type a message...","value":""},{"id":"sendButton","component":"Button","child":"sendButtonText","action":{"event":{"name":"sendMessage"}}},{"id":"sendButtonText","component":"Text","text":"Send"}]}} diff --git a/examples/composer/samples/checkoutPage.sample b/examples/composer/samples/checkoutPage.sample new file mode 100644 index 000000000..7ebe9278e --- /dev/null +++ b/examples/composer/samples/checkoutPage.sample @@ -0,0 +1,9 @@ +--- +description: A simplified e-commerce checkout page. +name: checkoutPage +prompt: | + Create a simplified e-commerce checkout page. It should have a 'Text' (variant 'h1') "Checkout". A 'Column' for shipping info with 'TextField's for "Name", "Address", "City", "Zip Code". A 'Column' for payment info with 'TextField's for "Card Number", "Expiry Date", "CVV". Finally, a 'Text' "Total: $99.99" and a 'Button' "Place Order". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["checkoutTitle","shippingInfoColumn","paymentInfoColumn","totalText","placeOrderButton"]},{"id":"checkoutTitle","component":"Text","text":"Checkout","variant":"h1"},{"id":"shippingInfoColumn","component":"Column","children":["nameField","addressField","cityField","zipCodeField"]},{"id":"nameField","component":"TextField","label":"Name","value":{"path":"/shipping/name"}},{"id":"addressField","component":"TextField","label":"Address","value":{"path":"/shipping/address"}},{"id":"cityField","component":"TextField","label":"City","value":{"path":"/shipping/city"}},{"id":"zipCodeField","component":"TextField","label":"Zip Code","value":{"path":"/shipping/zipCode"}},{"id":"paymentInfoColumn","component":"Column","children":["cardNumberField","expiryDateField","cvvField"]},{"id":"cardNumberField","component":"TextField","label":"Card Number","value":{"path":"/payment/cardNumber"}},{"id":"expiryDateField","component":"TextField","label":"Expiry Date","value":{"path":"/payment/expiryDate"}},{"id":"cvvField","component":"TextField","label":"CVV","value":{"path":"/payment/cvv"}},{"id":"totalText","component":"Text","text":"Total: $99.99"},{"id":"placeOrderButton","component":"Button","child":"placeOrderButtonText","action":{"event":{"name":"placeOrder"}}},{"id":"placeOrderButtonText","component":"Text","text":"Place Order"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"shipping":{"name":"","address":"","city":"","zipCode":""},"payment":{"cardNumber":"","expiryDate":"","cvv":""}}}} diff --git a/examples/composer/samples/cinemaSeatSelection.sample b/examples/composer/samples/cinemaSeatSelection.sample new file mode 100644 index 000000000..df7f9a30e --- /dev/null +++ b/examples/composer/samples/cinemaSeatSelection.sample @@ -0,0 +1,12 @@ +--- +description: A seat selection grid. +name: cinemaSeatSelection +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for cinema seat selection. 'Text' (h1) "Select Seats". 'Text' "Screen" (centered). 'Column' of 'Row's representing rows of seats. + - Row A: 4 'CheckBox'es. + - Row B: 4 'CheckBox'es. + - Row C: 4 'CheckBox'es. + 'Button' "Confirm Selection". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["titleText","screenText","seatRowA","seatRowB","seatRowC","confirmButton"],"align":"center"},{"id":"titleText","component":"Text","text":"Select Seats","variant":"h1"},{"id":"screenText","component":"Text","text":"Screen","align":"center"},{"id":"seatRowA","component":"Row","children":["seatA1","seatA2","seatA3","seatA4"],"justify":"center"},{"id":"seatA1","component":"CheckBox","label":"A1","value":false},{"id":"seatA2","component":"CheckBox","label":"A2","value":false},{"id":"seatA3","component":"CheckBox","label":"A3","value":false},{"id":"seatA4","component":"CheckBox","label":"A4","value":false},{"id":"seatRowB","component":"Row","children":["seatB1","seatB2","seatB3","seatB4"],"justify":"center"},{"id":"seatB1","component":"CheckBox","label":"B1","value":false},{"id":"seatB2","component":"CheckBox","label":"B2","value":false},{"id":"seatB3","component":"CheckBox","label":"B3","value":false},{"id":"seatB4","component":"CheckBox","label":"B4","value":false},{"id":"seatRowC","component":"Row","children":["seatC1","seatC2","seatC3","seatC4"],"justify":"center"},{"id":"seatC1","component":"CheckBox","label":"C1","value":false},{"id":"seatC2","component":"CheckBox","label":"C2","value":false},{"id":"seatC3","component":"CheckBox","label":"C3","value":false},{"id":"seatC4","component":"CheckBox","label":"C4","value":false},{"id":"confirmButton","component":"Button","child":"confirmButtonText","action":{"event":{"name":"confirmSelection"}}},{"id":"confirmButtonText","component":"Text","text":"Confirm Selection"}]}} diff --git a/examples/composer/samples/clientSideValidation.sample b/examples/composer/samples/clientSideValidation.sample new file mode 100644 index 000000000..b39c503d9 --- /dev/null +++ b/examples/composer/samples/clientSideValidation.sample @@ -0,0 +1,10 @@ +--- +description: A text field with client-side validation requirements. +name: clientSideValidation +prompt: | + Create a 'createSurface' and 'updateComponents' message for a registration form with validation. Surface ID 'main'. + Include a 'TextField' for "Username" that MUST match the regex "^[a-zA-Z0-9]{3,}$". If it fails, show error "Username must be at least 3 alphanumeric characters". + Include a 'Button' labeled "Register". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["usernameTextField","registerButton"]},{"id":"usernameTextField","component":"TextField","label":"Username","value":{"path":"/username"},"checks":[{"condition":{"call":"regex","args":{"value":{"path":"/username"},"pattern":"^[a-zA-Z0-9]{3,}$"}},"message":"Username must be at least 3 alphanumeric characters"}]},{"id":"registerButton","component":"Button","child":"registerButtonText","action":{"event":{"name":"registerUser"}}},{"id":"registerButtonText","component":"Text","text":"Register"}]}} diff --git a/examples/composer/samples/contactCard.sample b/examples/composer/samples/contactCard.sample new file mode 100644 index 000000000..55ce3cbda --- /dev/null +++ b/examples/composer/samples/contactCard.sample @@ -0,0 +1,8 @@ +--- +description: A UI to display contact information. +name: contactCard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a contact card. The root component of the surface must be a 'Card'. This Card should contain a 'Row'. The row contains an 'Image' (as an avatar) and a 'Column'. The column contains a 'Text' for the name "Jane Doe", a 'Text' for the email "jane.doe@example.com", and a 'Text' for the phone number "(123) 456-7890". Below the main row, add a 'Button' labeled "View on Map" (using a child 'Text' component). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"mainColumn"},{"id":"mainColumn","component":"Column","children":["contactRow","mapButton"]},{"id":"contactRow","component":"Row","children":["avatarImage","contactDetailsColumn"],"align":"center"},{"id":"avatarImage","component":"Image","url":"../travel_app/assets/travel_images/santorini_panorama.jpg","variant":"avatar"},{"id":"contactDetailsColumn","component":"Column","children":["nameText","emailText","phoneText"]},{"id":"nameText","component":"Text","text":"Jane Doe","variant":"h5"},{"id":"emailText","component":"Text","text":"jane.doe@example.com","variant":"body"},{"id":"phoneText","component":"Text","text":"(123) 456-7890","variant":"body"},{"id":"mapButton","component":"Button","child":"mapButtonText","action":{"functionCall":{"call":"openUrl","args":{"url":"https://maps.google.com/?q=Jane+Doe+address"},"returnType":"void"}}},{"id":"mapButtonText","component":"Text","text":"View on Map"}]}} diff --git a/examples/composer/samples/courseSyllabus.sample b/examples/composer/samples/courseSyllabus.sample new file mode 100644 index 000000000..33cdb0c1d --- /dev/null +++ b/examples/composer/samples/courseSyllabus.sample @@ -0,0 +1,10 @@ +--- +description: A course syllabus outline. +name: courseSyllabus +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a course syllabus. 'Text' (h1) "Introduction to Computer Science". 'List' of modules. + - For module 1, a 'Card' with 'Text' "Algorithms" and 'List' ("Sorting", "Searching"). + - For module 2, a 'Card' with 'Text' "Data Structures" and 'List' ("Arrays", "Linked Lists"). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["title","modulesList"]},{"id":"title","component":"Text","text":"Introduction to Computer Science","variant":"h1"},{"id":"modulesList","component":"List","children":["module1Card","module2Card"],"direction":"vertical"},{"id":"module1Card","component":"Card","child":"module1Content"},{"id":"module1Content","component":"Column","children":["module1Title","module1Items"]},{"id":"module1Title","component":"Text","text":"Algorithms","variant":"h4"},{"id":"module1Items","component":"List","children":["sortingText","searchingText"],"direction":"vertical"},{"id":"sortingText","component":"Text","text":"Sorting","variant":"body"},{"id":"searchingText","component":"Text","text":"Searching","variant":"body"},{"id":"module2Card","component":"Card","child":"module2Content"},{"id":"module2Content","component":"Column","children":["module2Title","module2Items"]},{"id":"module2Title","component":"Text","text":"Data Structures","variant":"h4"},{"id":"module2Items","component":"List","children":["arraysText","linkedListsText"],"direction":"vertical"},{"id":"arraysText","component":"Text","text":"Arrays","variant":"body"},{"id":"linkedListsText","component":"Text","text":"Linked Lists","variant":"body"}]}} diff --git a/examples/composer/samples/dashboard.sample b/examples/composer/samples/dashboard.sample new file mode 100644 index 000000000..e0595889e --- /dev/null +++ b/examples/composer/samples/dashboard.sample @@ -0,0 +1,8 @@ +--- +description: A simple dashboard with statistics. +name: dashboard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a simple dashboard. It should have a 'Text' (variant 'h1') "Sales Dashboard". Below, a 'Row' containing three 'Card's. The first card has a 'Text' "Revenue" and another 'Text' "$50,000". The second card has "New Customers" and "1,200". The third card has "Conversion Rate" and "4.5%". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dashboardTitle","dashboardCardsRow"]},{"id":"dashboardTitle","component":"Text","text":"Sales Dashboard","variant":"h1"},{"id":"dashboardCardsRow","component":"Row","children":["revenueCard","customersCard","conversionCard"],"justify":"spaceBetween"},{"id":"revenueCard","component":"Card","child":"revenueCardContent"},{"id":"revenueCardContent","component":"Column","children":["revenueLabel","revenueValue"]},{"id":"revenueLabel","component":"Text","text":"Revenue"},{"id":"revenueValue","component":"Text","text":"$50,000","variant":"h3"},{"id":"customersCard","component":"Card","child":"customersCardContent"},{"id":"customersCardContent","component":"Column","children":["customersLabel","customersValue"]},{"id":"customersLabel","component":"Text","text":"New Customers"},{"id":"customersValue","component":"Text","text":"1,200","variant":"h3"},{"id":"conversionCard","component":"Card","child":"conversionCardContent"},{"id":"conversionCardContent","component":"Column","children":["conversionLabel","conversionValue"]},{"id":"conversionLabel","component":"Text","text":"Conversion Rate"},{"id":"conversionValue","component":"Text","text":"4.5%","variant":"h3"}]}} diff --git a/examples/composer/samples/dogBreedGenerator.sample b/examples/composer/samples/dogBreedGenerator.sample new file mode 100644 index 000000000..65226e405 --- /dev/null +++ b/examples/composer/samples/dogBreedGenerator.sample @@ -0,0 +1,26 @@ +--- +description: A prompt to generate a UI for a dog breed information and generator tool. +name: dogBreedGenerator +prompt: | + Use a surfaceId of 'main'. Then, generate a 'createSurface' message followed by 'updateComponents' message to describe the following UI: + + A vertical list with: + - Dog breed information + - Dog generator + + The dog breed information is a card, which contains a title “Famous Dog breeds”, a header image, and a horizontal list of images of different dog breeds (using a 'List' component). The list information should be in the data model at /breeds. + + The dog generator is another card which is a form that generates a fictional dog breed with a description + - Title + - Description text explaining what it is + - Dog breed name (text input) + - Number of legs (number input) + - Button called “Generate” which takes the data above and generates a new dog description + - Skills (ChoicePicker component, variant 'multipleSelection') + - A divider + - A section which shows the generated content + +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"dogBreedName":"","numberOfLegs":0,"selectedSkills":[],"generatedDescription":"","breeds":[{"url":"../travel_app/assets/travel_images/gray_wolf.jpg"},{"url":"../travel_app/assets/travel_images/desert_iguana_mojave_desert_california.jpg"},{"url":"../travel_app/assets/travel_images/banded_cleaner_shrimp.jpg"}],"allSkills":[{"label":"Jumping","value":"jumping"},{"label":"Swimming","value":"swimming"},{"label":"Running","value":"running"},{"label":"Flying","value":"flying"}]}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dogInfoCard","dogGeneratorCard"]},{"id":"dogInfoCard","component":"Card","child":"dogInfoColumn"},{"id":"dogInfoColumn","component":"Column","children":["dogInfoTitle","headerImage","dogBreedList"]},{"id":"dogInfoTitle","component":"Text","text":"Famous Dog breeds","variant":"h3"},{"id":"headerImage","component":"Image","url":"../travel_app/assets/travel_images/maldives_islands.jpg","variant":"header"},{"id":"dogBreedList","component":"List","direction":"horizontal","children":{"componentId":"breedImage","path":"/breeds"}},{"id":"breedImage","component":"Image","url":{"path":"/url"},"variant":"avatar"},{"id":"dogGeneratorCard","component":"Card","child":"dogGeneratorColumn"},{"id":"dogGeneratorColumn","component":"Column","children":["generatorTitle","generatorDescriptionText","dogBreedNameInput","numberOfLegsInput","skillsChoicePicker","generateButton","divider","generatedContent"]},{"id":"generatorTitle","component":"Text","text":"Dog Generator","variant":"h3"},{"id":"generatorDescriptionText","component":"Text","text":"Generate a fictional dog breed with a description.","variant":"body"},{"id":"dogBreedNameInput","component":"TextField","label":"Dog breed name","value":{"path":"/dogBreedName"}},{"id":"numberOfLegsInput","component":"TextField","label":"Number of legs","variant":"number","value":{"path":"/numberOfLegs"}},{"id":"skillsChoicePicker","component":"ChoicePicker","label":"Skills","variant":"multipleSelection","options":{"path":"/allSkills"},"value":{"path":"/selectedSkills"}},{"id":"generateButton","component":"Button","child":"generateButtonText","action":{"event":{"name":"generateDogDescription","context":{"breedName":{"path":"/dogBreedName"},"legs":{"path":"/numberOfLegs"},"skills":{"path":"/selectedSkills"}}}}},{"id":"generateButtonText","component":"Text","text":"Generate"},{"id":"divider","component":"Divider","axis":"horizontal"},{"id":"generatedContent","component":"Column","children":["generatedContentTitle","generatedDescriptionText"]},{"id":"generatedContentTitle","component":"Text","text":"Generated Dog Description:","variant":"h4"},{"id":"generatedDescriptionText","component":"Text","text":{"path":"/generatedDescription"},"variant":"body"}]}} diff --git a/examples/composer/samples/eCommerceProductPage.sample b/examples/composer/samples/eCommerceProductPage.sample new file mode 100644 index 000000000..31da12f08 --- /dev/null +++ b/examples/composer/samples/eCommerceProductPage.sample @@ -0,0 +1,18 @@ +--- +description: A detailed product page for an e-commerce website. +name: eCommerceProductPage +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a product details page. + The main layout should be a 'Row'. + The left side of the row is a 'Column' containing a large main 'Image' of the product, and below it, a 'Row' of three smaller thumbnail 'Image' components. + The right side of the row is another 'Column' for product information: + - A 'Text' (variant 'h1') for the product name, "Premium Leather Jacket". + - A 'Text' component for the price, "$299.99". + - A 'Divider'. + - A 'ChoicePicker' (variant 'mutuallyExclusive') labeled "Select Size" with options "S", "M", "L", "XL". + - A 'ChoicePicker' (variant 'mutuallyExclusive') labeled "Select Color" with options "Black", "Brown", "Red". + - A 'Button' with a 'Text' child "Add to Cart". + - A 'Text' component for the product description below the button. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Row","children":["productImageColumn","productInfoColumn"]},{"id":"productImageColumn","component":"Column","weight":1,"children":["mainProductImage","thumbnailImagesRow"]},{"id":"mainProductImage","component":"Image","url":"../travel_app/assets/travel_images/macba_barcelona.jpg","variant":"largeFeature"},{"id":"thumbnailImagesRow","component":"Row","children":["thumbnailImage1","thumbnailImage2","thumbnailImage3"]},{"id":"thumbnailImage1","component":"Image","url":"../travel_app/assets/travel_images/torre_glories_barcelona.jpg","variant":"smallFeature"},{"id":"thumbnailImage2","component":"Image","url":"../travel_app/assets/travel_images/castelldefels_spain_september.jpg","variant":"smallFeature"},{"id":"thumbnailImage3","component":"Image","url":"../travel_app/assets/travel_images/circuit_de_catalunya_f1.jpg","variant":"smallFeature"},{"id":"productInfoColumn","component":"Column","weight":1,"children":["productName","productPrice","infoDivider","sizePicker","colorPicker","addToCartButton","addToCartButtonText","productDescription"]},{"id":"productName","component":"Text","text":"Premium Leather Jacket","variant":"h1"},{"id":"productPrice","component":"Text","text":"$299.99"},{"id":"infoDivider","component":"Divider"},{"id":"sizePicker","component":"ChoicePicker","label":"Select Size","variant":"mutuallyExclusive","options":[{"label":"S","value":"small"},{"label":"M","value":"medium"},{"label":"L","value":"large"},{"label":"XL","value":"extraLarge"}],"value":{"path":"/selectedSize"}},{"id":"colorPicker","component":"ChoicePicker","label":"Select Color","variant":"mutuallyExclusive","options":[{"label":"Black","value":"black"},{"label":"Brown","value":"brown"},{"label":"Red","value":"red"}],"value":{"path":"/selectedColor"}},{"id":"addToCartButton","component":"Button","child":"addToCartButtonText","action":{"event":{"name":"addToCart","context":{"productId":"jacket123","size":{"path":"/selectedSize"},"color":{"path":"/selectedColor"}}}}},{"id":"addToCartButtonText","component":"Text","text":"Add to Cart"},{"id":"productDescription","component":"Text","text":"Crafted from genuine leather, this jacket offers timeless style and exceptional durability. Perfect for any season."}]}} diff --git a/examples/composer/samples/fileBrowser.sample b/examples/composer/samples/fileBrowser.sample new file mode 100644 index 000000000..89dcc1339 --- /dev/null +++ b/examples/composer/samples/fileBrowser.sample @@ -0,0 +1,8 @@ +--- +description: A file explorer list. +name: fileBrowser +prompt: | + Create a file browser. It should have a 'Text' (variant 'h1') "My Files". A 'List' of 'Row's. Each row has an 'Icon' (folder or attachFile) and a 'Text' (filename). Examples (create these as static rows, not data bound): "Documents", "Images", "Work.txt". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["myFilesTitle","fileList"]},{"id":"myFilesTitle","component":"Text","text":"My Files","variant":"h1"},{"id":"fileList","component":"List","direction":"vertical","children":["row1","row2","row3"]},{"id":"row1","component":"Row","children":["iconFolder1","textDocuments"]},{"id":"iconFolder1","component":"Icon","name":"folder"},{"id":"textDocuments","component":"Text","text":"Documents"},{"id":"row2","component":"Row","children":["iconFolder2","textImages"]},{"id":"iconFolder2","component":"Icon","name":"folder"},{"id":"textImages","component":"Text","text":"Images"},{"id":"row3","component":"Row","children":["iconFile3","textWorkTxt"]},{"id":"iconFile3","component":"Icon","name":"attachFile"},{"id":"textWorkTxt","component":"Text","text":"Work.txt"}]}} diff --git a/examples/composer/samples/fitnessTracker.sample b/examples/composer/samples/fitnessTracker.sample new file mode 100644 index 000000000..aa9e8809a --- /dev/null +++ b/examples/composer/samples/fitnessTracker.sample @@ -0,0 +1,9 @@ +--- +description: A daily activity summary. +name: fitnessTracker +prompt: | + Create a fitness tracker dashboard. It should have a 'Text' (variant 'h1') "Daily Activity", and a 'Row' of 'Card's. Each card should contain a 'Column' with a 'Text' label (e.g. "Steps") and a 'Text' value (e.g. "10,000"). Create cards for "Steps" ("10,000"), "Calories" ("500 kcal"), "Distance" ("5 km"). Below that, a 'Slider' labeled "Daily Goal" (initialize value to 50). Finally, a 'List' of recent workouts. Use 'Text' components for the list items, for example: "Morning Run", "Evening Yoga", "Gym Session". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dailyActivityTitle","activityCardsRow","dailyGoalSlider","workoutsList"]},{"id":"dailyActivityTitle","component":"Text","text":"Daily Activity","variant":"h1"},{"id":"activityCardsRow","component":"Row","children":["stepsCard","caloriesCard","distanceCard"],"justify":"spaceBetween"},{"id":"stepsCard","component":"Card","child":"stepsColumn"},{"id":"stepsColumn","component":"Column","children":["stepsLabel","stepsValue"]},{"id":"stepsLabel","component":"Text","text":"Steps"},{"id":"stepsValue","component":"Text","text":"10,000"},{"id":"caloriesCard","component":"Card","child":"caloriesColumn"},{"id":"caloriesColumn","component":"Column","children":["caloriesLabel","caloriesValue"]},{"id":"caloriesLabel","component":"Text","text":"Calories"},{"id":"caloriesValue","component":"Text","text":"500 kcal"},{"id":"distanceCard","component":"Card","child":"distanceColumn"},{"id":"distanceColumn","component":"Column","children":["distanceLabel","distanceValue"]},{"id":"distanceLabel","component":"Text","text":"Distance"},{"id":"distanceValue","component":"Text","text":"5 km"},{"id":"dailyGoalSlider","component":"Slider","label":"Daily Goal","min":0,"max":100,"value":{"path":"/dailyGoal"}},{"id":"workoutsList","component":"List","children":["workout1","workout2","workout3"],"direction":"vertical"},{"id":"workout1","component":"Text","text":"Morning Run"},{"id":"workout2","component":"Text","text":"Evening Yoga"},{"id":"workout3","component":"Text","text":"Gym Session"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"dailyGoal":50}}} diff --git a/examples/composer/samples/flashcardApp.sample b/examples/composer/samples/flashcardApp.sample new file mode 100644 index 000000000..de01ebbc0 --- /dev/null +++ b/examples/composer/samples/flashcardApp.sample @@ -0,0 +1,8 @@ +--- +description: A language learning flashcard. +name: flashcardApp +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a flashcard app. 'Text' (h1) "Spanish Vocabulary". 'Card' (the flashcard). Inside the card, a 'Column' with 'Text' (h2) "Hola" (Front). 'Divider'. 'Text' "Hello" (Back - conceptually hidden, but rendered here). 'Row' of buttons: "Hard", "Good", "Easy". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["appTitle","flashcard","buttonsRow"],"justify":"center","align":"center"},{"id":"appTitle","component":"Text","text":"Spanish Vocabulary","variant":"h1"},{"id":"flashcard","component":"Card","child":"flashcardContent"},{"id":"flashcardContent","component":"Column","children":["flashcardFront","flashcardDivider","flashcardBack"],"align":"center"},{"id":"flashcardFront","component":"Text","text":"Hola","variant":"h2"},{"id":"flashcardDivider","component":"Divider","axis":"horizontal"},{"id":"flashcardBack","component":"Text","text":"Hello"},{"id":"buttonsRow","component":"Row","children":["hardButton","goodButton","easyButton"],"justify":"spaceBetween"},{"id":"hardButton","component":"Button","child":"hardButtonText","action":{"event":{"name":"rateFlashcard","context":{"rating":"hard"}}}},{"id":"hardButtonText","component":"Text","text":"Hard"},{"id":"goodButton","component":"Button","child":"goodButtonText","action":{"event":{"name":"rateFlashcard","context":{"rating":"good"}}}},{"id":"goodButtonText","component":"Text","text":"Good"},{"id":"easyButton","component":"Button","child":"easyButtonText","action":{"event":{"name":"rateFlashcard","context":{"rating":"easy"}}}},{"id":"easyButtonText","component":"Text","text":"Easy"}]}} diff --git a/examples/composer/samples/flightBooker.sample b/examples/composer/samples/flightBooker.sample new file mode 100644 index 000000000..5d3dd2078 --- /dev/null +++ b/examples/composer/samples/flightBooker.sample @@ -0,0 +1,9 @@ +--- +description: A form to search for flights. +name: flightBooker +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a flight booking form. It should have a 'Text' (variant 'h1') "Book a Flight". Then a 'Row' with two 'TextField's for "Origin" and "Destination". Below that, a 'Row' with two 'DateTimeInput's for "Departure Date" and "Return Date" (initialize with empty values). Add a 'Slider' labeled "Passengers" (min 1, max 10, value 1). Finally, a 'Button' labeled "Search Flights". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["bookFlightTitle","originDestinationRow","datesRow","passengersSlider","searchFlightsButton"]},{"id":"bookFlightTitle","component":"Text","text":"Book a Flight","variant":"h1"},{"id":"originDestinationRow","component":"Row","children":["originTextField","destinationTextField"]},{"id":"originTextField","component":"TextField","label":"Origin","value":{"path":"/origin"}},{"id":"destinationTextField","component":"TextField","label":"Destination","value":{"path":"/destination"}},{"id":"datesRow","component":"Row","children":["departureDateInput","returnDateInput"]},{"id":"departureDateInput","component":"DateTimeInput","label":"Departure Date","value":{"path":"/departureDate"},"enableDate":true},{"id":"returnDateInput","component":"DateTimeInput","label":"Return Date","value":{"path":"/returnDate"},"enableDate":true},{"id":"passengersSlider","component":"Slider","label":"Passengers","min":1,"max":10,"value":{"path":"/passengers"}},{"id":"searchFlightsButton","component":"Button","child":"searchFlightsButtonText","action":{"event":{"name":"searchFlights","context":{"origin":{"path":"/origin"},"destination":{"path":"/destination"},"departureDate":{"path":"/departureDate"},"returnDate":{"path":"/returnDate"},"passengers":{"path":"/passengers"}}}}},{"id":"searchFlightsButtonText","component":"Text","text":"Search Flights"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"origin":"","destination":"","departureDate":"","returnDate":"","passengers":1}}} diff --git a/examples/composer/samples/hello_world.sample b/examples/composer/samples/hello_world.sample new file mode 100644 index 000000000..5d7b80d53 --- /dev/null +++ b/examples/composer/samples/hello_world.sample @@ -0,0 +1,5 @@ +name: Test Sample +description: This is a test sample to verify the parser. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Text","text":"Hello World!","variant":"h1"}]}} diff --git a/examples/composer/samples/hotelSearchResults.sample b/examples/composer/samples/hotelSearchResults.sample new file mode 100644 index 000000000..68a22803f --- /dev/null +++ b/examples/composer/samples/hotelSearchResults.sample @@ -0,0 +1,10 @@ +--- +description: Hotel search results list. +name: hotelSearchResults +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for hotel search results. 'Text' (h1) "Hotels in Tokyo". 'List' of 'Card's. + - Card 1: 'Row' with 'Image', 'Column' ('Text' "Grand Hotel", 'Text' "5 Stars", 'Text' "$200/night"), 'Button' "Book". + - Card 2: 'Row' with 'Image', 'Column' ('Text' "City Inn", 'Text' "3 Stars", 'Text' "$100/night"), 'Button' "Book". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["hotelsTitle","hotelList"]},{"id":"hotelsTitle","component":"Text","text":"Hotels in Tokyo","variant":"h1"},{"id":"hotelList","component":"List","direction":"vertical","children":["card1","card2"]},{"id":"card1","component":"Card","child":"card1Content"},{"id":"card1Content","component":"Row","children":["card1Image","card1Details","card1Button"]},{"id":"card1Image","component":"Image","url":"../travel_app/assets/travel_images/kinkaku_ji_golden_pavilion_kyoto.jpg","variant":"smallFeature"},{"id":"card1Details","component":"Column","children":["card1Name","card1Stars","card1Price"]},{"id":"card1Name","component":"Text","text":"Grand Hotel"},{"id":"card1Stars","component":"Text","text":"5 Stars"},{"id":"card1Price","component":"Text","text":"$200/night"},{"id":"card1Button","component":"Button","child":"card1ButtonText","action":{"event":{"name":"bookHotel","context":{"hotelId":"grandHotel"}}}},{"id":"card1ButtonText","component":"Text","text":"Book"},{"id":"card2","component":"Card","child":"card2Content"},{"id":"card2Content","component":"Row","children":["card2Image","card2Details","card2Button"]},{"id":"card2Image","component":"Image","url":"../travel_app/assets/travel_images/crowded_tram_tokyo.jpg","variant":"smallFeature"},{"id":"card2Details","component":"Column","children":["card2Name","card2Stars","card2Price"]},{"id":"card2Name","component":"Text","text":"City Inn"},{"id":"card2Stars","component":"Text","text":"3 Stars"},{"id":"card2Price","component":"Text","text":"$100/night"},{"id":"card2Button","component":"Button","child":"card2ButtonText","action":{"event":{"name":"bookHotel","context":{"hotelId":"cityInn"}}}},{"id":"card2ButtonText","component":"Text","text":"Book"}]}} diff --git a/examples/composer/samples/interactiveDashboard.sample b/examples/composer/samples/interactiveDashboard.sample new file mode 100644 index 000000000..a763fd72c --- /dev/null +++ b/examples/composer/samples/interactiveDashboard.sample @@ -0,0 +1,17 @@ +--- +description: A dashboard with filters and data cards. +name: interactiveDashboard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for an interactive analytics dashboard. + At the top, a 'Text' (variant 'h1') "Company Dashboard". + Below the text heading, a 'Card' containing a 'Row' of filter controls: + - A 'DateTimeInput' with a label for "Start Date" (initialize with empty value). + - A 'DateTimeInput' with a label for "End Date" (initialize with empty value). + - A 'Button' labeled "Apply Filters". + Below the filters card, a 'Row' containing two 'Card's for key metrics: + - The first 'Card' has a 'Text' (variant 'h2') "Total Revenue" and a 'Text' component showing "$1,234,567". + - The second 'Card' has a 'Text' (variant 'h2') "New Users" and a 'Text' component showing "4,321". + Finally, a large 'Card' at the bottom with a 'Text' (variant 'h2') "Revenue Over Time" and a placeholder 'Image' with a valid URL to represent a line chart. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dashboardTitle","filtersCard","metricsRow","revenueChartCard"]},{"id":"dashboardTitle","component":"Text","text":"Company Dashboard","variant":"h1"},{"id":"filtersCard","component":"Card","child":"filterControlsRow"},{"id":"filterControlsRow","component":"Row","children":["startDateInput","endDateInput","applyFiltersButton"],"justify":"spaceBetween"},{"id":"startDateInput","component":"DateTimeInput","label":"Start Date","value":"","enableDate":true},{"id":"endDateInput","component":"DateTimeInput","label":"End Date","value":"","enableDate":true},{"id":"applyFiltersButton","component":"Button","child":"applyFiltersText","action":{"event":{"name":"applyFilters"}}},{"id":"applyFiltersText","component":"Text","text":"Apply Filters"},{"id":"metricsRow","component":"Row","children":["totalRevenueCard","newUsersCard"],"justify":"spaceEvenly"},{"id":"totalRevenueCard","component":"Card","child":"totalRevenueColumn"},{"id":"totalRevenueColumn","component":"Column","children":["totalRevenueTitle","totalRevenueValue"]},{"id":"totalRevenueTitle","component":"Text","text":"Total Revenue","variant":"h2"},{"id":"totalRevenueValue","component":"Text","text":"$1,234,567"},{"id":"newUsersCard","component":"Card","child":"newUsersColumn"},{"id":"newUsersColumn","component":"Column","children":["newUsersTitle","newUsersValue"]},{"id":"newUsersTitle","component":"Text","text":"New Users","variant":"h2"},{"id":"newUsersValue","component":"Text","text":"4,321"},{"id":"revenueChartCard","component":"Card","child":"revenueChartColumn"},{"id":"revenueChartColumn","component":"Column","children":["revenueChartTitle","revenueChartImage"]},{"id":"revenueChartTitle","component":"Text","text":"Revenue Over Time","variant":"h2"},{"id":"revenueChartImage","component":"Image","url":"assets/images/brooklyn_bridge_new_york.jpg"}]}} diff --git a/examples/composer/samples/jobApplication.sample b/examples/composer/samples/jobApplication.sample new file mode 100644 index 000000000..78f1cda53 --- /dev/null +++ b/examples/composer/samples/jobApplication.sample @@ -0,0 +1,9 @@ +--- +description: A job application form. +name: jobApplication +prompt: | + Create a job application form. It should have 'TextField's for "Name", "Email", "Phone", "Resume URL". A 'ChoicePicker' (variant 'mutuallyExclusive') labeled "Years of Experience" (options: "0-1", "2-5", "5+"). A 'Button' "Submit Application". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"name":"","email":"","phone":"","resumeUrl":"","experience":""}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["nameField","emailField","phoneField","resumeUrlField","experiencePicker","submitButton"],"justify":"start","align":"stretch"},{"id":"nameField","component":"TextField","label":"Name","value":{"path":"/name"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/name"}}},"message":"Name is required."}]},{"id":"emailField","component":"TextField","label":"Email","value":{"path":"/email"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/email"}}},"message":"Email is required."},{"condition":{"call":"email","args":{"value":{"path":"/email"}}},"message":"Invalid email format."}]},{"id":"phoneField","component":"TextField","label":"Phone","value":{"path":"/phone"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/phone"}}},"message":"Phone number is required."},{"condition":{"call":"regex","args":{"value":{"path":"/phone"},"pattern":"^\\+?[1-9]\\d{1,14}$"}},"message":"Invalid phone number format."}]},{"id":"resumeUrlField","component":"TextField","label":"Resume URL","value":{"path":"/resumeUrl"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/resumeUrl"}}},"message":"Resume URL is required."}]},{"id":"experiencePicker","component":"ChoicePicker","label":"Years of Experience","variant":"mutuallyExclusive","options":[{"label":"0-1","value":"0-1"},{"label":"2-5","value":"2-5"},{"label":"5+","value":"5+"}],"value":{"path":"/experience"}},{"id":"submitButton","component":"Button","child":"submitButtonText","action":{"event":{"name":"submitApplication","context":{"name":{"path":"/name"},"email":{"path":"/email"},"phone":{"path":"/phone"},"resumeUrl":{"path":"/resumeUrl"},"experience":{"path":"/experience"}}}}},{"id":"submitButtonText","component":"Text","text":"Submit Application"}]}} diff --git a/examples/composer/samples/kanbanBoard.sample b/examples/composer/samples/kanbanBoard.sample new file mode 100644 index 000000000..0ed4fc85a --- /dev/null +++ b/examples/composer/samples/kanbanBoard.sample @@ -0,0 +1,12 @@ +--- +description: A Kanban-style task tracking board. +name: kanbanBoard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a Kanban board. It should have a 'Text' (variant 'h1') "Project Tasks". Below, a 'Row' containing three 'Column's representing "To Do", "In Progress", and "Done". Each column should have a 'Text' (variant 'h2') header and a list of 'Card's. + - "To Do" column: Card "Research", Card "Design". + - "In Progress" column: Card "Implementation". + - "Done" column: Card "Planning". + Each card should just contain a 'Text' with the task name. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["projectTitle","kanbanBoard"]},{"id":"projectTitle","component":"Text","text":"Project Tasks","variant":"h1"},{"id":"kanbanBoard","component":"Row","children":["toDoColumn","inProgressColumn","doneColumn"],"justify":"spaceEvenly"},{"id":"toDoColumn","component":"Column","children":["toDoHeader","cardResearch","cardDesign"],"align":"start"},{"id":"toDoHeader","component":"Text","text":"To Do","variant":"h2"},{"id":"cardResearch","component":"Card","child":"textResearch"},{"id":"textResearch","component":"Text","text":"Research"},{"id":"cardDesign","component":"Card","child":"textDesign"},{"id":"textDesign","component":"Text","text":"Design"},{"id":"inProgressColumn","component":"Column","children":["inProgressHeader","cardImplementation"],"align":"start"},{"id":"inProgressHeader","component":"Text","text":"In Progress","variant":"h2"},{"id":"cardImplementation","component":"Card","child":"textImplementation"},{"id":"textImplementation","component":"Text","text":"Implementation"},{"id":"doneColumn","component":"Column","children":["doneHeader","cardPlanning"],"align":"start"},{"id":"doneHeader","component":"Text","text":"Done","variant":"h2"},{"id":"cardPlanning","component":"Card","child":"textPlanning"},{"id":"textPlanning","component":"Text","text":"Planning"}]}} diff --git a/examples/composer/samples/loginForm.sample b/examples/composer/samples/loginForm.sample new file mode 100644 index 000000000..6da71b302 --- /dev/null +++ b/examples/composer/samples/loginForm.sample @@ -0,0 +1,8 @@ +--- +description: A simple login form with username, password, a "remember me" checkbox, and a submit button. +name: loginForm +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a login form. It should have a "Login" text (variant 'h1'), two text fields for username and password (bound to /login/username and /login/password), a checkbox for "Remember Me" (bound to /login/rememberMe), and a "Sign In" button. The button's action should have a 'event' property with 'name': 'login', and a 'context' containing the username, password, and rememberMe status. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["loginTitle","usernameField","passwordField","rememberMeCheckbox","signInButton"],"justify":"center","align":"center"},{"id":"loginTitle","component":"Text","text":"Login","variant":"h1"},{"id":"usernameField","component":"TextField","label":"Username","value":{"path":"/login/username"}},{"id":"passwordField","component":"TextField","label":"Password","value":{"path":"/login/password"},"variant":"obscured"},{"id":"rememberMeCheckbox","component":"CheckBox","label":"Remember Me","value":{"path":"/login/rememberMe"}},{"id":"signInButton","component":"Button","child":"signInButtonText","action":{"event":{"name":"login","context":{"username":{"path":"/login/username"},"password":{"path":"/login/password"},"rememberMe":{"path":"/login/rememberMe"}}}}},{"id":"signInButtonText","component":"Text","text":"Sign In"}]}} diff --git a/examples/composer/samples/manifest.txt b/examples/composer/samples/manifest.txt new file mode 100644 index 000000000..bb1e78005 --- /dev/null +++ b/examples/composer/samples/manifest.txt @@ -0,0 +1,44 @@ +animalKingdomExplorer.sample +calendarEventCreator.sample +chatRoom.sample +checkoutPage.sample +cinemaSeatSelection.sample +clientSideValidation.sample +contactCard.sample +courseSyllabus.sample +dashboard.sample +dogBreedGenerator.sample +eCommerceProductPage.sample +fileBrowser.sample +fitnessTracker.sample +flashcardApp.sample +flightBooker.sample +hello_world.sample +hotelSearchResults.sample +interactiveDashboard.sample +jobApplication.sample +kanbanBoard.sample +loginForm.sample +musicPlayer.sample +nestedDataBinding.sample +nestedLayoutRecursive.sample +newsAggregator.sample +notificationCenter.sample +openUrlAction.sample +photoEditor.sample +podcastEpisode.sample +productGallery.sample +profileEditor.sample +recipeCard.sample +restaurantMenu.sample +settingsPage.sample +simpleCalculator.sample +smartHome.sample +socialMediaPost.sample +standardFunctions.sample +stockWatchlist.sample +surveyForm.sample +travelItinerary.sample +triviaQuiz.sample +videoCallInterface.sample +weatherForecast.sample diff --git a/examples/composer/samples/musicPlayer.sample b/examples/composer/samples/musicPlayer.sample new file mode 100644 index 000000000..70f36a32b --- /dev/null +++ b/examples/composer/samples/musicPlayer.sample @@ -0,0 +1,8 @@ +--- +description: A simple music player UI. +name: musicPlayer +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a music player. It should be a 'Card' containing a 'Column'. Inside the column, there's an 'Image' for the album art, a 'Text' for the song title "Bohemian Rhapsody", another 'Text' for the artist "Queen", a 'Slider' labeled "Progress", and a 'Row' with three 'Button' components. Each Button should have a child 'Text' component. The Text components should have the labels "Previous", "Play", and "Next" respectively. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"musicPlayerColumn"},{"id":"musicPlayerColumn","component":"Column","children":["albumArtImage","songTitleText","artistText","progressSlider","controlsRow"]},{"id":"albumArtImage","component":"Image","url":"../travel_app/assets/travel_images/santorini_panorama.jpg","variant":"mediumFeature"},{"id":"songTitleText","component":"Text","text":"Bohemian Rhapsody","variant":"h4"},{"id":"artistText","component":"Text","text":"Queen","variant":"body"},{"id":"progressSlider","component":"Slider","label":"Progress","min":0,"max":100,"value":50},{"id":"controlsRow","component":"Row","justify":"spaceAround","children":["previousButton","playButton","nextButton"]},{"id":"previousButton","component":"Button","child":"previousButtonText","action":{"event":{"name":"previousSong"}}},{"id":"previousButtonText","component":"Text","text":"Previous"},{"id":"playButton","component":"Button","child":"playButtonText","action":{"event":{"name":"playPause"}}},{"id":"playButtonText","component":"Text","text":"Play"},{"id":"nextButton","component":"Button","child":"nextButtonText","action":{"event":{"name":"nextSong"}}},{"id":"nextButtonText","component":"Text","text":"Next"}]}} diff --git a/examples/composer/samples/nestedDataBinding.sample b/examples/composer/samples/nestedDataBinding.sample new file mode 100644 index 000000000..c0d54face --- /dev/null +++ b/examples/composer/samples/nestedDataBinding.sample @@ -0,0 +1,33 @@ +--- +description: A project dashboard with deeply nested data binding. +name: nestedDataBinding +prompt: | + Generate a stream of JSON messages for a Project Management Dashboard. + The output must consist of exactly three JSON objects, one after the other. + + Generate a createSurface message with surfaceId 'main'. + Generate an updateComponents message with surfaceId 'main'. + It should have a 'Text' (variant 'h1') "Project Dashboard". + Then a 'List' of projects bound to '/projects'. + Inside the list template, each item should be a 'Card' containing: + - A 'Text' (variant 'h2') bound to the project 'title'. + - A 'List' of tasks bound to the 'tasks' property of the project. + Inside the tasks list template, each item should be a 'Column' containing: + - A 'Text' bound to the task 'description'. + - A 'Row' for the assignee, containing: + - A 'Text' bound to 'assignee/name'. + - A 'Text' bound to 'assignee/role'. + - A 'List' of subtasks bound to 'subtasks'. + Inside the subtasks list template, each item should be a 'Text' bound to 'title'. + + Then generate an 'updateDataModel' message. + Populate this dashboard with sample data: + - At least one project. + - The project should have a title, and a list of tasks. + - The task should have a description, an assignee object (with name and role), and a list of subtasks. + + Ensure all referenced component IDs (like 'subtaskList') are explicitly defined in the 'components' list. The component with id 'subtaskList' must effectively exist in the output list. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dashboardTitle","projectList"]},{"id":"dashboardTitle","component":"Text","variant":"h1","text":"Project Dashboard"},{"id":"projectList","component":"List","direction":"vertical","children":{"componentId":"projectCardTemplate","path":"/projects"}},{"id":"projectCardTemplate","component":"Card","child":"projectCardContent"},{"id":"projectCardContent","component":"Column","children":["projectTitle","taskList"]},{"id":"projectTitle","component":"Text","variant":"h2","text":{"path":"title"}},{"id":"taskList","component":"List","direction":"vertical","children":{"componentId":"taskItemTemplate","path":"tasks"}},{"id":"taskItemTemplate","component":"Column","children":["taskDescription","assigneeRow","subtaskList"]},{"id":"taskDescription","component":"Text","text":{"path":"description"}},{"id":"assigneeRow","component":"Row","children":["assigneeName","assigneeRole"]},{"id":"assigneeName","component":"Text","text":{"path":"assignee/name"}},{"id":"assigneeRole","component":"Text","text":{"path":"assignee/role"}},{"id":"subtaskList","component":"List","direction":"vertical","children":{"componentId":"subtaskItemTemplate","path":"subtasks"}},{"id":"subtaskItemTemplate","component":"Text","text":{"path":"title"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"projects":[{"title":"Website Redesign","tasks":[{"description":"Design mockups for homepage","assignee":{"name":"Alice Smith","role":"UI Designer"},"subtasks":[{"title":"Gather requirements"},{"title":"Sketch wireframes"},{"title":"Create high-fidelity designs"}]},{"description":"Develop user authentication module","assignee":{"name":"Bob Johnson","role":"Backend Developer"},"subtasks":[{"title":"Set up database"},{"title":"Implement login API"},{"title":"Integrate with frontend"}]}]},{"title":"Mobile App Development","tasks":[{"description":"Plan app features","assignee":{"name":"Charlie Brown","role":"Product Manager"},"subtasks":[{"title":"Market research"},{"title":"User stories"}]}]}]}}} diff --git a/examples/composer/samples/nestedLayoutRecursive.sample b/examples/composer/samples/nestedLayoutRecursive.sample new file mode 100644 index 000000000..b1e24f8bb --- /dev/null +++ b/examples/composer/samples/nestedLayoutRecursive.sample @@ -0,0 +1,17 @@ +--- +description: A deeply nested layout to test component recursion. +name: nestedLayoutRecursive +prompt: | + Create a 'createSurface' and 'updateComponents' message with surfaceId 'main'. + Create a layout with at least 5 levels of depth: + Level 1: Card + Level 2: Column (inside Card) + Level 3: Row (inside Column) + Level 4: List (inside Row) + Level 5: Text (inside List items) + + Use explicit, static components for this structure (no data binding for the list). + Level 5 Text should say "Deep content". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"column1"},{"id":"column1","component":"Column","children":["row1"]},{"id":"row1","component":"Row","children":["list1"]},{"id":"list1","component":"List","children":["text1"],"direction":"vertical"},{"id":"text1","component":"Text","text":"Deep content"}]}} diff --git a/examples/composer/samples/newsAggregator.sample b/examples/composer/samples/newsAggregator.sample new file mode 100644 index 000000000..d90516504 --- /dev/null +++ b/examples/composer/samples/newsAggregator.sample @@ -0,0 +1,8 @@ +--- +description: A news feed with article cards. +name: newsAggregator +prompt: | + Create a news aggregator. The root component should be a 'Column'. Inside this column, place a 'Text' (variant 'h1') "Top Headlines". Below the text, place a 'List' of 'Card's. The 'List' should be a sibling of the 'Text', not a parent. Each card has a 'Column' with an 'Image', a 'Text' (headline), and a 'Text' (summary). Include headlines "Tech Breakthrough" and "Local Sports". Each card should have a 'Button' labeled "Read More". Create these as static components, not data bound. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["headlineText","newsList"]},{"id":"headlineText","component":"Text","text":"Top Headlines","variant":"h1"},{"id":"newsList","component":"List","direction":"vertical","children":["card1","card2"]},{"id":"card1","component":"Card","child":"card1Content"},{"id":"card1Content","component":"Column","children":["image1","headline1","summary1","button1"]},{"id":"image1","component":"Image","url":"../travel_app/assets/travel_images/brooklyn_bridge_new_york.jpg"},{"id":"headline1","component":"Text","text":"Tech Breakthrough"},{"id":"summary1","component":"Text","text":"Scientists announce a major leap forward in AI."},{"id":"button1","component":"Button","child":"button1Label","action":{"event":{"name":"readMore","context":{"articleId":"tech-breakthrough"}}}},{"id":"button1Label","component":"Text","text":"Read More"},{"id":"card2","component":"Card","child":"card2Content"},{"id":"card2Content","component":"Column","children":["image2","headline2","summary2","button2"]},{"id":"image2","component":"Image","url":"../travel_app/assets/travel_images/sydney_harbour_bridge_1932.jpg"},{"id":"headline2","component":"Text","text":"Local Sports"},{"id":"summary2","component":"Text","text":"High school team wins championship title."},{"id":"button2","component":"Button","child":"button2Label","action":{"event":{"name":"readMore","context":{"articleId":"local-sports"}}}},{"id":"button2Label","component":"Text","text":"Read More"}]}} diff --git a/examples/composer/samples/notificationCenter.sample b/examples/composer/samples/notificationCenter.sample new file mode 100644 index 000000000..7255e5e47 --- /dev/null +++ b/examples/composer/samples/notificationCenter.sample @@ -0,0 +1,8 @@ +--- +description: A list of notifications. +name: notificationCenter +prompt: | + Create a notification center. It should have a 'Text' (variant 'h1') "Notifications". A 'List' of 'Card's. Include cards for "New message from Sarah" and "Your order has shipped". Each card should have a 'Button' "Dismiss". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["notificationCenterTitle","notificationList"]},{"id":"notificationCenterTitle","component":"Text","text":"Notifications","variant":"h1"},{"id":"notificationList","component":"List","direction":"vertical","children":["messageNotificationCard","orderShippedNotificationCard"]},{"id":"messageNotificationCard","component":"Card","child":"messageCardContent"},{"id":"messageCardContent","component":"Column","children":["messageText","dismissMessageButton"]},{"id":"messageText","component":"Text","text":"New message from Sarah"},{"id":"dismissMessageButton","component":"Button","child":"dismissMessageButtonText","action":{"event":{"name":"dismissNotification","context":{"notificationId":"messageFromSarah"}}}},{"id":"dismissMessageButtonText","component":"Text","text":"Dismiss"},{"id":"orderShippedNotificationCard","component":"Card","child":"orderShippedCardContent"},{"id":"orderShippedCardContent","component":"Column","children":["orderShippedText","dismissOrderButton"]},{"id":"orderShippedText","component":"Text","text":"Your order has shipped"},{"id":"dismissOrderButton","component":"Button","child":"dismissOrderButtonText","action":{"event":{"name":"dismissNotification","context":{"notificationId":"orderShipped"}}}},{"id":"dismissOrderButtonText","component":"Text","text":"Dismiss"}]}} diff --git a/examples/composer/samples/openUrlAction.sample b/examples/composer/samples/openUrlAction.sample new file mode 100644 index 000000000..0db6b3148 --- /dev/null +++ b/examples/composer/samples/openUrlAction.sample @@ -0,0 +1,10 @@ +--- +description: A button that opens an external URL. +name: openUrlAction +prompt: | + Create a 'createSurface' and 'updateComponents' message. Surface ID 'main'. + Include a 'Button' labeled "Visit Website". + The button's action should be a client-side function call to 'openUrl' with the argument 'url': 'https://a2ui.org'. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["websiteButton"]},{"id":"websiteButton","component":"Button","child":"buttonLabel","action":{"functionCall":{"call":"openUrl","args":{"url":"https://a2ui.org"},"returnType":"void"}}},{"id":"buttonLabel","component":"Text","text":"Visit Website"}]}} diff --git a/examples/composer/samples/photoEditor.sample b/examples/composer/samples/photoEditor.sample new file mode 100644 index 000000000..5900763ce --- /dev/null +++ b/examples/composer/samples/photoEditor.sample @@ -0,0 +1,9 @@ +--- +description: A photo editing interface with sliders. +name: photoEditor +prompt: | + Create a photo editor. It should have a large 'Image' (photo). Below it, a 'Row' of 'Button's (Filters, Crop, Adjust). Below that, a 'Slider' labeled "Intensity" (initialize value to 50). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["photo","buttonRow","intensitySlider"]},{"id":"photo","component":"Image","url":"../travel_app/assets/travel_images/santorini_panorama.jpg","variant":"largeFeature"},{"id":"buttonRow","component":"Row","justify":"spaceBetween","children":["filtersButton","cropButton","adjustButton"]},{"id":"filtersButton","component":"Button","child":"filtersButtonText","action":{"event":{"name":"filters_clicked"}}},{"id":"filtersButtonText","component":"Text","text":"Filters"},{"id":"cropButton","component":"Button","child":"cropButtonText","action":{"event":{"name":"crop_clicked"}}},{"id":"cropButtonText","component":"Text","text":"Crop"},{"id":"adjustButton","component":"Button","child":"adjustButtonText","action":{"event":{"name":"adjust_clicked"}}},{"id":"adjustButtonText","component":"Text","text":"Adjust"},{"id":"intensitySlider","component":"Slider","label":"Intensity","min":0,"max":100,"value":{"path":"/intensity"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"intensity":50}}} diff --git a/examples/composer/samples/podcastEpisode.sample b/examples/composer/samples/podcastEpisode.sample new file mode 100644 index 000000000..8fb19ac1b --- /dev/null +++ b/examples/composer/samples/podcastEpisode.sample @@ -0,0 +1,14 @@ +--- +description: A podcast player interface. +name: podcastEpisode +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a podcast player. 'Card' containing: + - 'Image' (Cover Art). + - 'Text' (h2) "Episode 42: The Future of AI". + - 'Text' "Host: Jane Smith". + - 'Slider' labeled "Progress" (initialize value to 0). + - 'Row' with 'Button' (child 'Text' "1x"), 'Button' (child 'Text' "Play/Pause"), 'Button' (child 'Text' "Share"). + Create these as static components, not data bound. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["podcastCard"]},{"id":"podcastCard","component":"Card","child":"podcastContentColumn"},{"id":"podcastContentColumn","component":"Column","children":["coverArtImage","episodeTitleText","hostText","progressBarSlider","controlsRow"]},{"id":"coverArtImage","component":"Image","url":"../travel_app/assets/travel_images/statue_of_liberty_new_york.jpg","variant":"mediumFeature"},{"id":"episodeTitleText","component":"Text","text":"Episode 42: The Future of AI","variant":"h2"},{"id":"hostText","component":"Text","text":"Host: Jane Smith","variant":"body"},{"id":"progressBarSlider","component":"Slider","label":"Progress","min":0,"max":100,"value":0},{"id":"controlsRow","component":"Row","justify":"spaceBetween","children":["speedButton","playPauseButton","shareButton"]},{"id":"speedButton","component":"Button","child":"speedButtonText","action":{"event":{"name":"changeSpeed"}}},{"id":"speedButtonText","component":"Text","text":"1x"},{"id":"playPauseButton","component":"Button","child":"playPauseButtonText","action":{"event":{"name":"togglePlayPause"}}},{"id":"playPauseButtonText","component":"Text","text":"Play/Pause"},{"id":"shareButton","component":"Button","child":"shareButtonText","action":{"event":{"name":"shareEpisode"}}},{"id":"shareButtonText","component":"Text","text":"Share"}]}} diff --git a/examples/composer/samples/productGallery.sample b/examples/composer/samples/productGallery.sample new file mode 100644 index 000000000..b0c18e376 --- /dev/null +++ b/examples/composer/samples/productGallery.sample @@ -0,0 +1,9 @@ +--- +description: A gallery of products using a list with a template. +name: productGallery +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a product gallery. It should display a list of products from the data model at '/products'. Use a template for the list items. Each item should be a Card containing a Column. The Column should contain an Image (from '/products/item/imageUrl'), a Text component for the product name (from '/products/item/name'), and a Button labeled "Add to Cart". The button's action should have a 'event' with 'name': 'addToCart' and a 'context' with the product ID, for example, 'productId': 'static-id-123' (use this exact literal string). You should create a template component and then a list that uses it. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/products","value":{"product1":{"id":"product1","name":"Super Widget","imageUrl":"../travel_app/assets/travel_images/santorini_from_space.jpg"},"product2":{"id":"product2","name":"Mega Doodad","imageUrl":"../travel_app/assets/travel_images/maldives_islands.jpg"},"product3":{"id":"product3","name":"Ultra Gizmo","imageUrl":"../travel_app/assets/travel_images/sydney_harbour_bridge_1932.jpg"}}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["productList"]},{"id":"productList","component":"List","direction":"vertical","children":{"componentId":"productCardTemplate","path":"/products"}},{"id":"productCardTemplate","component":"Card","child":"productCardColumn"},{"id":"productCardColumn","component":"Column","children":["productImage","productNameText","addToCartButton"]},{"id":"productImage","component":"Image","url":{"path":"imageUrl"}},{"id":"productNameText","component":"Text","text":{"path":"name"},"variant":"h5"},{"id":"addToCartButton","component":"Button","child":"addToCartButtonText","action":{"event":{"name":"addToCart","context":{"productId":"static-id-123"}}}},{"id":"addToCartButtonText","component":"Text","text":"Add to Cart"}]}} diff --git a/examples/composer/samples/profileEditor.sample b/examples/composer/samples/profileEditor.sample new file mode 100644 index 000000000..363c85fd3 --- /dev/null +++ b/examples/composer/samples/profileEditor.sample @@ -0,0 +1,8 @@ +--- +description: A user profile editing form. +name: profileEditor +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for editing a profile. 'Text' (h1) "Edit Profile". 'Image' (Current Avatar). 'Button' "Change Photo". 'TextField' "Display Name". 'TextField' "Bio" (multiline). 'TextField' "Website". 'Button' "Save Changes". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["editProfileTitle","currentAvatarImage","changePhotoButton","displayNameTextField","bioTextField","websiteTextField","saveChangesButton"]},{"id":"editProfileTitle","component":"Text","text":"Edit Profile","variant":"h1"},{"id":"currentAvatarImage","component":"Image","url":"../travel_app/assets/travel_images/maldives_islands.jpg","variant":"avatar"},{"id":"changePhotoButton","component":"Button","child":"changePhotoText","action":{"event":{"name":"changePhoto"}}},{"id":"changePhotoText","component":"Text","text":"Change Photo"},{"id":"displayNameTextField","component":"TextField","label":"Display Name","value":{"path":"/profile/displayName"}},{"id":"bioTextField","component":"TextField","label":"Bio","value":{"path":"/profile/bio"},"variant":"longText"},{"id":"websiteTextField","component":"TextField","label":"Website","value":{"path":"/profile/website"},"checks":[{"condition":{"call":"regex","args":{"value":{"path":"/profile/website"},"pattern":"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"},"returnType":"boolean"},"message":"Please enter a valid URL."}]},{"id":"saveChangesButton","component":"Button","child":"saveChangesText","variant":"primary","action":{"event":{"name":"saveProfileChanges"}}},{"id":"saveChangesText","component":"Text","text":"Save Changes"}]}} diff --git a/examples/composer/samples/recipeCard.sample b/examples/composer/samples/recipeCard.sample new file mode 100644 index 000000000..ef6044733 --- /dev/null +++ b/examples/composer/samples/recipeCard.sample @@ -0,0 +1,8 @@ +--- +description: A UI to display a recipe with ingredients and instructions. +name: recipeCard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a recipe card. It should have a 'Text' (variant 'h1') for the recipe title, "Classic Lasagna". Below the title, an 'Image' of the lasagna. Then, a 'Row' containing two 'Column's. The first column has a 'Text' (variant 'h2') "Ingredients" and a 'List' of ingredients (use 'Text' components for items: "Pasta", "Cheese", "Sauce"). The second column has a 'Text' (variant 'h2') "Instructions" and a 'List' of step-by-step instructions (use 'Text' components: "Boil pasta", "Layer ingredients", "Bake"). Finally, a 'Button' at the bottom labeled "Watch Video Tutorial". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["recipeTitle","recipeImage","contentRow","watchVideoButton"]},{"id":"recipeTitle","component":"Text","text":"Classic Lasagna","variant":"h1"},{"id":"recipeImage","component":"Image","url":"../travel_app/assets/travel_images/brooklyn_bridge_new_york.jpg","fit":"cover"},{"id":"contentRow","component":"Row","children":["ingredientsColumn","instructionsColumn"],"justify":"spaceBetween"},{"id":"ingredientsColumn","component":"Column","weight":1,"children":["ingredientsTitle","ingredientsList"]},{"id":"ingredientsTitle","component":"Text","text":"Ingredients","variant":"h2"},{"id":"ingredientsList","component":"List","children":["ingredient1","ingredient2","ingredient3"],"direction":"vertical"},{"id":"ingredient1","component":"Text","text":"Pasta"},{"id":"ingredient2","component":"Text","text":"Cheese"},{"id":"ingredient3","component":"Text","text":"Sauce"},{"id":"instructionsColumn","component":"Column","weight":1,"children":["instructionsTitle","instructionsList"]},{"id":"instructionsTitle","component":"Text","text":"Instructions","variant":"h2"},{"id":"instructionsList","component":"List","children":["instruction1","instruction2","instruction3"],"direction":"vertical"},{"id":"instruction1","component":"Text","text":"Boil pasta"},{"id":"instruction2","component":"Text","text":"Layer ingredients"},{"id":"instruction3","component":"Text","text":"Bake"},{"id":"watchVideoButton","component":"Button","child":"watchVideoText","action":{"functionCall":{"call":"openUrl","args":{"url":"https://example.com/lasagna-tutorial"},"returnType":"void"}}},{"id":"watchVideoText","component":"Text","text":"Watch Video Tutorial"}]}} diff --git a/examples/composer/samples/restaurantMenu.sample b/examples/composer/samples/restaurantMenu.sample new file mode 100644 index 000000000..ff6a8d086 --- /dev/null +++ b/examples/composer/samples/restaurantMenu.sample @@ -0,0 +1,11 @@ +--- +description: A restaurant menu with tabs. +name: restaurantMenu +prompt: | + Create a restaurant menu with tabs. It should have a 'Text' (variant 'h1') "Gourmet Bistro". A 'Tabs' component with "Starters", "Mains", "Desserts". + - "Starters": 'List' containing IDs of separate 'Row' components (Name, Price). Create rows for "Soup - $8", "Salad - $10". + - "Mains": 'List' containing IDs of separate 'Row' components. Create rows for "Steak - $25", "Pasta - $18". + - "Desserts": 'List' containing IDs of separate 'Row' components. Create rows for "Cake - $8", "Pie - $7". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["restaurantTitle","menuTabs"]},{"id":"restaurantTitle","component":"Text","text":"Gourmet Bistro","variant":"h1"},{"id":"menuTabs","component":"Tabs","tabs":[{"title":"Starters","child":"startersList"},{"title":"Mains","child":"mainsList"},{"title":"Desserts","child":"dessertsList"}]},{"id":"startersList","component":"List","direction":"vertical","children":["soupRow","saladRow"]},{"id":"soupRow","component":"Row","children":["soupText","soupPrice"],"justify":"spaceBetween"},{"id":"soupText","component":"Text","text":"Soup"},{"id":"soupPrice","component":"Text","text":"$8"},{"id":"saladRow","component":"Row","children":["saladText","saladPrice"],"justify":"spaceBetween"},{"id":"saladText","component":"Text","text":"Salad"},{"id":"saladPrice","component":"Text","text":"$10"},{"id":"mainsList","component":"List","direction":"vertical","children":["steakRow","pastaRow"]},{"id":"steakRow","component":"Row","children":["steakText","steakPrice"],"justify":"spaceBetween"},{"id":"steakText","component":"Text","text":"Steak"},{"id":"steakPrice","component":"Text","text":"$25"},{"id":"pastaRow","component":"Row","children":["pastaText","pastaPrice"],"justify":"spaceBetween"},{"id":"pastaText","component":"Text","text":"Pasta"},{"id":"pastaPrice","component":"Text","text":"$18"},{"id":"dessertsList","component":"List","direction":"vertical","children":["cakeRow","pieRow"]},{"id":"cakeRow","component":"Row","children":["cakeText","cakePrice"],"justify":"spaceBetween"},{"id":"cakeText","component":"Text","text":"Cake"},{"id":"cakePrice","component":"Text","text":"$8"},{"id":"pieRow","component":"Row","children":["pieText","piePrice"],"justify":"spaceBetween"},{"id":"pieText","component":"Text","text":"Pie"},{"id":"piePrice","component":"Text","text":"$7"}]}} diff --git a/examples/composer/samples/settingsPage.sample b/examples/composer/samples/settingsPage.sample new file mode 100644 index 000000000..439a46fe9 --- /dev/null +++ b/examples/composer/samples/settingsPage.sample @@ -0,0 +1,9 @@ +--- +description: A settings page with tabs and a modal dialog. +name: settingsPage +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a user settings page. Use a Tabs component with two tabs: "Profile" and "Notifications". The "Profile" tab should contain a simple column with a text field for the user's name. The "Notifications" tab should contain a checkbox for "Enable email notifications". Also, include a Modal component. The modal's trigger should be a button labeled "Delete Account", and its content should be a column with a confirmation text and two buttons: "Confirm Deletion" and "Cancel". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["settingsTabs","deleteAccountModal"]},{"id":"settingsTabs","component":"Tabs","tabs":[{"title":"Profile","child":"profileTabContent"},{"title":"Notifications","child":"notificationsTabContent"}]},{"id":"profileTabContent","component":"Column","children":["userNameTextField"]},{"id":"userNameTextField","component":"TextField","label":"Name","value":{"path":"/user/name"}},{"id":"notificationsTabContent","component":"Column","children":["emailNotificationsCheckbox"]},{"id":"emailNotificationsCheckbox","component":"CheckBox","label":"Enable email notifications","value":{"path":"/user/emailNotificationsEnabled"}},{"id":"deleteAccountModal","component":"Modal","trigger":"deleteAccountButton","content":"deleteConfirmationContent"},{"id":"deleteAccountButton","component":"Button","child":"deleteAccountButtonText","action":{"event":{"name":"openDeleteAccountModal"}}},{"id":"deleteAccountButtonText","component":"Text","text":"Delete Account"},{"id":"deleteConfirmationContent","component":"Column","children":["confirmationText","modalButtonsRow"]},{"id":"confirmationText","component":"Text","text":"Are you sure you want to delete your account? This action cannot be undone."},{"id":"modalButtonsRow","component":"Row","justify":"spaceBetween","children":["confirmDeletionButton","cancelDeletionButton"]},{"id":"confirmDeletionButton","component":"Button","child":"confirmDeletionButtonText","action":{"event":{"name":"confirmAccountDeletion"}}},{"id":"confirmDeletionButtonText","component":"Text","text":"Confirm Deletion"},{"id":"cancelDeletionButton","component":"Button","child":"cancelDeletionButtonText","action":{"event":{"name":"cancelAccountDeletion"}}},{"id":"cancelDeletionButtonText","component":"Text","text":"Cancel"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"user":{"name":"John Doe","emailNotificationsEnabled":true}}}} diff --git a/examples/composer/samples/simpleCalculator.sample b/examples/composer/samples/simpleCalculator.sample new file mode 100644 index 000000000..cc9f3c6ae --- /dev/null +++ b/examples/composer/samples/simpleCalculator.sample @@ -0,0 +1,13 @@ +--- +description: A basic calculator layout. +name: simpleCalculator +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a calculator. It should have a 'Card'. Inside the card, there MUST be a single 'Column' that contains two things: a 'Text' (display) showing "0", and a nested 'Column' of 'Row's for the buttons. + - Row 1: "7", "8", "9", "/" + - Row 2: "4", "5", "6", "*" + - Row 3: "1", "2", "3", "-" + - Row 4: "0", ".", "=", "+" + Each button should be a 'Button' component with a child 'Text' component for the label (e.g. '7', '+'). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"calculatorColumn"},{"id":"calculatorColumn","component":"Column","children":["display","buttonsColumn"]},{"id":"display","component":"Text","text":"0","variant":"h3"},{"id":"buttonsColumn","component":"Column","children":["row1","row2","row3","row4"]},{"id":"row1","component":"Row","children":["button7","button8","button9","buttonDivide"],"justify":"spaceBetween"},{"id":"button7","component":"Button","child":"text7","action":{"event":{"name":"digitPress","context":{"digit":"7"}}}},{"id":"text7","component":"Text","text":"7"},{"id":"button8","component":"Button","child":"text8","action":{"event":{"name":"digitPress","context":{"digit":"8"}}}},{"id":"text8","component":"Text","text":"8"},{"id":"button9","component":"Button","child":"text9","action":{"event":{"name":"digitPress","context":{"digit":"9"}}}},{"id":"text9","component":"Text","text":"9"},{"id":"buttonDivide","component":"Button","child":"textDivide","action":{"event":{"name":"operatorPress","context":{"operator":"/"}}}},{"id":"textDivide","component":"Text","text":"/"},{"id":"row2","component":"Row","children":["button4","button5","button6","buttonMultiply"],"justify":"spaceBetween"},{"id":"button4","component":"Button","child":"text4","action":{"event":{"name":"digitPress","context":{"digit":"4"}}}},{"id":"text4","component":"Text","text":"4"},{"id":"button5","component":"Button","child":"text5","action":{"event":{"name":"digitPress","context":{"digit":"5"}}}},{"id":"text5","component":"Text","text":"5"},{"id":"button6","component":"Button","child":"text6","action":{"event":{"name":"digitPress","context":{"digit":"6"}}}},{"id":"text6","component":"Text","text":"6"},{"id":"buttonMultiply","component":"Button","child":"textMultiply","action":{"event":{"name":"operatorPress","context":{"operator":"*"}}}},{"id":"textMultiply","component":"Text","text":"*"},{"id":"row3","component":"Row","children":["button1","button2","button3","buttonMinus"],"justify":"spaceBetween"},{"id":"button1","component":"Button","child":"text1","action":{"event":{"name":"digitPress","context":{"digit":"1"}}}},{"id":"text1","component":"Text","text":"1"},{"id":"button2","component":"Button","child":"text2","action":{"event":{"name":"digitPress","context":{"digit":"2"}}}},{"id":"text2","component":"Text","text":"2"},{"id":"button3","component":"Button","child":"text3","action":{"event":{"name":"digitPress","context":{"digit":"3"}}}},{"id":"text3","component":"Text","text":"3"},{"id":"buttonMinus","component":"Button","child":"textMinus","action":{"event":{"name":"operatorPress","context":{"operator":"-"}}}},{"id":"textMinus","component":"Text","text":"-"},{"id":"row4","component":"Row","children":["button0","buttonDot","buttonEquals","buttonPlus"],"justify":"spaceBetween"},{"id":"button0","component":"Button","child":"text0","action":{"event":{"name":"digitPress","context":{"digit":"0"}}}},{"id":"text0","component":"Text","text":"0"},{"id":"buttonDot","component":"Button","child":"textDot","action":{"event":{"name":"decimalPress"}}},{"id":"textDot","component":"Text","text":"."},{"id":"buttonEquals","component":"Button","child":"textEquals","action":{"event":{"name":"calculate"}}},{"id":"textEquals","component":"Text","text":"="},{"id":"buttonPlus","component":"Button","child":"textPlus","action":{"event":{"name":"operatorPress","context":{"operator":"+"}}}},{"id":"textPlus","component":"Text","text":"+"}]}} diff --git a/examples/composer/samples/smartHome.sample b/examples/composer/samples/smartHome.sample new file mode 100644 index 000000000..e8e5a4dcd --- /dev/null +++ b/examples/composer/samples/smartHome.sample @@ -0,0 +1,8 @@ +--- +description: A smart home control panel. +name: smartHome +prompt: | + Create a smart home dashboard. It should have a 'Text' (variant 'h1') "Living Room". A 'Grid' of 'Card's. To create the grid, use a 'Column' that contains multiple 'Row's. Each 'Row' should contain 'Card's. Create a row with cards for "Lights" (CheckBox, label "Lights", value true) and "Thermostat" (Slider, label "Thermostat", value 72). Create another row with a card for "Music" (CheckBox, label "Music", value false). Ensure the CheckBox labels are exactly "Lights" and "Music". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["livingRoomTitle","gridColumn"]},{"id":"livingRoomTitle","component":"Text","text":"Living Room","variant":"h1"},{"id":"gridColumn","component":"Column","children":["firstRow","secondRow"]},{"id":"firstRow","component":"Row","children":["lightsCard","thermostatCard"],"justify":"spaceEvenly"},{"id":"lightsCard","component":"Card","child":"lightsCheckbox","weight":1},{"id":"lightsCheckbox","component":"CheckBox","label":"Lights","value":true},{"id":"thermostatCard","component":"Card","child":"thermostatSlider","weight":1},{"id":"thermostatSlider","component":"Slider","label":"Thermostat","min":60,"max":80,"value":72},{"id":"secondRow","component":"Row","children":["musicCard"],"justify":"spaceEvenly"},{"id":"musicCard","component":"Card","child":"musicCheckbox","weight":1},{"id":"musicCheckbox","component":"CheckBox","label":"Music","value":false}]}} diff --git a/examples/composer/samples/socialMediaPost.sample b/examples/composer/samples/socialMediaPost.sample new file mode 100644 index 000000000..6c0caf01c --- /dev/null +++ b/examples/composer/samples/socialMediaPost.sample @@ -0,0 +1,8 @@ +--- +description: A component representing a social media post. +name: socialMediaPost +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a social media post. It should be a 'Card' containing a 'Column'. The first item is a 'Row' with an 'Image' (user avatar) and a 'Text' (username "user123"). Below that, a 'Text' component for the post content: "Enjoying the beautiful weather today!". Then, an 'Image' for the main post picture. Finally, a 'Row' with three 'Button's: "Like", "Comment", and "Share". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"postColumn"},{"id":"postColumn","component":"Column","children":["userInfoRow","postContentText","postImage","actionsRow"]},{"id":"userInfoRow","component":"Row","children":["userAvatar","usernameText"],"align":"center"},{"id":"userAvatar","component":"Image","url":"../travel_app/assets/travel_images/santorini_panorama.jpg","variant":"avatar"},{"id":"usernameText","component":"Text","text":"user123","variant":"h5"},{"id":"postContentText","component":"Text","text":"Enjoying the beautiful weather today!","variant":"body"},{"id":"postImage","component":"Image","url":"../travel_app/assets/travel_images/maldives_islands.jpg","variant":"mediumFeature","fit":"cover"},{"id":"actionsRow","component":"Row","children":["likeButton","commentButton","shareButton"],"justify":"spaceBetween"},{"id":"likeButton","component":"Button","child":"likeButtonText","action":{"event":{"name":"likePost"}},"variant":"borderless"},{"id":"likeButtonText","component":"Text","text":"Like"},{"id":"commentButton","component":"Button","child":"commentButtonText","action":{"event":{"name":"commentPost"}},"variant":"borderless"},{"id":"commentButtonText","component":"Text","text":"Comment"},{"id":"shareButton","component":"Button","child":"shareButtonText","action":{"event":{"name":"sharePost"}},"variant":"borderless"},{"id":"shareButtonText","component":"Text","text":"Share"}]}} diff --git a/examples/composer/samples/standardFunctions.sample b/examples/composer/samples/standardFunctions.sample new file mode 100644 index 000000000..937f2abbc --- /dev/null +++ b/examples/composer/samples/standardFunctions.sample @@ -0,0 +1,15 @@ +--- +description: Usage of pluralize. +name: standardFunctions +prompt: | + Create a 'createSurface' and 'updateComponents' message for a shopping cart summary. Surface ID 'main'. + Display a 'Text' component. + The text value should be a 'pluralize' function call with returnType 'string'. + The pluralize call should use the count from '/cart/count' and provide these options: + 'zero': "No items" + 'one': "One item" + 'other': "${/cart/count} items" +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["cartSummaryText"]},{"id":"cartSummaryText","component":"Text","text":{"call":"pluralize","args":{"count":{"path":"/cart/count"},"zero":"No items","one":"One item","other":"${/cart/count} items"},"returnType":"string"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/cart/count","value":2}} diff --git a/examples/composer/samples/stockWatchlist.sample b/examples/composer/samples/stockWatchlist.sample new file mode 100644 index 000000000..fe242fa51 --- /dev/null +++ b/examples/composer/samples/stockWatchlist.sample @@ -0,0 +1,11 @@ +--- +description: A stock market watchlist. +name: stockWatchlist +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a stock watchlist. 'Text' (h1) "Market Watch". 'List' of 'Row's. + - Row 1: 'Text' "AAPL", 'Text' "$150.00", 'Text' "+1.2%". + - Row 2: 'Text' "GOOGL", 'Text' "$2800.00", 'Text' "-0.5%". + - Row 3: 'Text' "AMZN", 'Text' "$3400.00", 'Text' "+0.8%". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["marketWatchTitle","stockList"]},{"id":"marketWatchTitle","component":"Text","text":"Market Watch","variant":"h1"},{"id":"stockList","component":"List","children":["aaplRow","googlRow","amznRow"]},{"id":"aaplRow","component":"Row","children":["aaplSymbol","aaplPrice","aaplChange"],"justify":"spaceBetween"},{"id":"aaplSymbol","component":"Text","text":"AAPL"},{"id":"aaplPrice","component":"Text","text":"$150.00"},{"id":"aaplChange","component":"Text","text":"+1.2%"},{"id":"googlRow","component":"Row","children":["googlSymbol","googlPrice","googlChange"],"justify":"spaceBetween"},{"id":"googlSymbol","component":"Text","text":"GOOGL"},{"id":"googlPrice","component":"Text","text":"$2800.00"},{"id":"googlChange","component":"Text","text":"-0.5%"},{"id":"amznRow","component":"Row","children":["amznSymbol","amznPrice","amznChange"],"justify":"spaceBetween"},{"id":"amznSymbol","component":"Text","text":"AMZN"},{"id":"amznPrice","component":"Text","text":"$3400.00"},{"id":"amznChange","component":"Text","text":"+0.8%"}]}} diff --git a/examples/composer/samples/surveyForm.sample b/examples/composer/samples/surveyForm.sample new file mode 100644 index 000000000..5188c1253 --- /dev/null +++ b/examples/composer/samples/surveyForm.sample @@ -0,0 +1,9 @@ +--- +description: A customer feedback survey form. +name: surveyForm +prompt: | + Create a customer feedback survey form. It should have a 'Text' (variant 'h1') "Customer Feedback". Then a 'ChoicePicker' (variant 'mutuallyExclusive') with label "How would you rate our service?" and options "Excellent", "Good", "Average", "Poor". Then a 'ChoicePicker' (variant 'multipleSelection') with label "What did you like?" and options "Product Quality", "Price", "Customer Support". Finally, a 'TextField' with the label "Any other comments?" and a 'Button' labeled "Submit Feedback". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"serviceRating":"","likedItems":[],"comments":""}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["customerFeedbackTitle","serviceRatingPicker","likedItemsPicker","commentsTextField","submitButton"]},{"id":"customerFeedbackTitle","component":"Text","text":"Customer Feedback","variant":"h1"},{"id":"serviceRatingPicker","component":"ChoicePicker","label":"How would you rate our service?","variant":"mutuallyExclusive","options":[{"label":"Excellent","value":"excellent"},{"label":"Good","value":"good"},{"label":"Average","value":"average"},{"label":"Poor","value":"poor"}],"value":{"path":"/serviceRating"}},{"id":"likedItemsPicker","component":"ChoicePicker","label":"What did you like?","variant":"multipleSelection","options":[{"label":"Product Quality","value":"product_quality"},{"label":"Price","value":"price"},{"label":"Customer Support","value":"customer_support"}],"value":{"path":"/likedItems"}},{"id":"commentsTextField","component":"TextField","label":"Any other comments?","variant":"longText","value":{"path":"/comments"}},{"id":"submitButton","component":"Button","child":"submitButtonText","action":{"event":{"name":"submitFeedback","context":{"serviceRating":{"path":"/serviceRating"},"likedItems":{"path":"/likedItems"},"comments":{"path":"/comments"}}}}},{"id":"submitButtonText","component":"Text","text":"Submit Feedback"}]}} diff --git a/examples/composer/samples/travelItinerary.sample b/examples/composer/samples/travelItinerary.sample new file mode 100644 index 000000000..bae388f0f --- /dev/null +++ b/examples/composer/samples/travelItinerary.sample @@ -0,0 +1,14 @@ +--- +description: A multi-day travel itinerary display. +name: travelItinerary +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a travel itinerary for a trip to Paris. + It should have a main 'Text' component with variant 'h1' and text "Paris Adventure". + Below, use a 'List' to display three days. Each item in the list should be a 'Card'. + - The first 'Card' (Day 1) should contain a 'Text' (variant 'h2') "Day 1: Arrival & Eiffel Tower", and a 'List' of activities for that day: "Check into hotel", "Lunch at a cafe", "Visit the Eiffel Tower". + - The second 'Card' (Day 2) should contain a 'Text' (variant 'h2') "Day 2: Museums & Culture", and a 'List' of activities: "Visit the Louvre Museum", "Walk through Tuileries Garden", "See the Arc de Triomphe". + - The third 'Card' (Day 3) should contain a 'Text' (variant 'h2') "Day 3: Art & Departure", and a 'List' of activities: "Visit Musée d'Orsay", "Explore Montmartre", "Depart from CDG". + Each activity in the inner lists should be a 'Row' containing a 'CheckBox' (to mark as complete, with an empty label '') and a 'Text' component with the activity description. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["parisAdventureTitle","itineraryList"]},{"id":"parisAdventureTitle","component":"Text","text":"Paris Adventure","variant":"h1"},{"id":"itineraryList","component":"List","children":["day1Card","day2Card","day3Card"],"direction":"vertical"},{"id":"day1Card","component":"Card","child":"day1Content"},{"id":"day1Content","component":"Column","children":["day1Title","day1Activities"]},{"id":"day1Title","component":"Text","text":"Day 1: Arrival & Eiffel Tower","variant":"h2"},{"id":"day1Activities","component":"List","children":["day1Activity1","day1Activity2","day1Activity3"],"direction":"vertical"},{"id":"day1Activity1","component":"Row","children":["day1Checkbox1","day1Text1"]},{"id":"day1Checkbox1","component":"CheckBox","label":"","value":false},{"id":"day1Text1","component":"Text","text":"Check into hotel"},{"id":"day1Activity2","component":"Row","children":["day1Checkbox2","day1Text2"]},{"id":"day1Checkbox2","component":"CheckBox","label":"","value":false},{"id":"day1Text2","component":"Text","text":"Lunch at a cafe"},{"id":"day1Activity3","component":"Row","children":["day1Checkbox3","day1Text3"]},{"id":"day1Checkbox3","component":"CheckBox","label":"","value":false},{"id":"day1Text3","component":"Text","text":"Visit the Eiffel Tower"},{"id":"day2Card","component":"Card","child":"day2Content"},{"id":"day2Content","component":"Column","children":["day2Title","day2Activities"]},{"id":"day2Title","component":"Text","text":"Day 2: Museums & Culture","variant":"h2"},{"id":"day2Activities","component":"List","children":["day2Activity1","day2Activity2","day2Activity3"],"direction":"vertical"},{"id":"day2Activity1","component":"Row","children":["day2Checkbox1","day2Text1"]},{"id":"day2Checkbox1","component":"CheckBox","label":"","value":false},{"id":"day2Text1","component":"Text","text":"Visit the Louvre Museum"},{"id":"day2Activity2","component":"Row","children":["day2Checkbox2","day2Text2"]},{"id":"day2Checkbox2","component":"CheckBox","label":"","value":false},{"id":"day2Text2","component":"Text","text":"Walk through Tuileries Garden"},{"id":"day2Activity3","component":"Row","children":["day2Checkbox3","day2Text3"]},{"id":"day2Checkbox3","component":"CheckBox","label":"","value":false},{"id":"day2Text3","component":"Text","text":"See the Arc de Triomphe"},{"id":"day3Card","component":"Card","child":"day3Content"},{"id":"day3Content","component":"Column","children":["day3Title","day3Activities"]},{"id":"day3Title","component":"Text","text":"Day 3: Art & Departure","variant":"h2"},{"id":"day3Activities","component":"List","children":["day3Activity1","day3Activity2","day3Activity3"],"direction":"vertical"},{"id":"day3Activity1","component":"Row","children":["day3Checkbox1","day3Text1"]},{"id":"day3Checkbox1","component":"CheckBox","label":"","value":false},{"id":"day3Text1","component":"Text","text":"Visit Musée d'Orsay"},{"id":"day3Activity2","component":"Row","children":["day3Checkbox2","day3Text2"]},{"id":"day3Checkbox2","component":"CheckBox","label":"","value":false},{"id":"day3Text2","component":"Text","text":"Explore Montmartre"},{"id":"day3Activity3","component":"Row","children":["day3Checkbox3","day3Text3"]},{"id":"day3Checkbox3","component":"CheckBox","label":"","value":false},{"id":"day3Text3","component":"Text","text":"Depart from CDG"}]}} diff --git a/examples/composer/samples/triviaQuiz.sample b/examples/composer/samples/triviaQuiz.sample new file mode 100644 index 000000000..459c124fc --- /dev/null +++ b/examples/composer/samples/triviaQuiz.sample @@ -0,0 +1,9 @@ +--- +description: A trivia question card. +name: triviaQuiz +prompt: | + Create a trivia quiz. It should have a 'Text' (variant 'h1') "Question 1". A 'Text' "What is the capital of France?". A 'ChoicePicker' (variant 'mutuallyExclusive') for answers (options: "Paris", "London", "Berlin", "Madrid"). A 'Button' "Submit Answer". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["question1Title","question1Text","answerChoicePicker","submitButton"]},{"id":"question1Title","component":"Text","variant":"h1","text":"Question 1"},{"id":"question1Text","component":"Text","text":"What is the capital of France?"},{"id":"answerChoicePicker","component":"ChoicePicker","label":"Select your answer","variant":"mutuallyExclusive","options":[{"label":"Paris","value":"Paris"},{"label":"London","value":"London"},{"label":"Berlin","value":"Berlin"},{"label":"Madrid","value":"Madrid"}],"value":{"path":"/selectedAnswer"}},{"id":"submitButton","component":"Button","child":"submitButtonText","action":{"event":{"name":"submitAnswer","context":{"answer":{"path":"/selectedAnswer"}}}}},{"id":"submitButtonText","component":"Text","text":"Submit Answer"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/","value":{"selectedAnswer":[]}}} diff --git a/examples/composer/samples/videoCallInterface.sample b/examples/composer/samples/videoCallInterface.sample new file mode 100644 index 000000000..735e6089b --- /dev/null +++ b/examples/composer/samples/videoCallInterface.sample @@ -0,0 +1,8 @@ +--- +description: A video conference UI. +name: videoCallInterface +prompt: | + Create a video call interface. It should have a 'Text' (variant 'h1') "Video Call". A 'Video' component with a valid placeholder URL (e.g. 'https://example.com/video.mp4'). Below that, a 'Row' with three 'Button's, each with a child 'Text' component with the text "Mute", "Camera", and "End Call" respectively. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["videoCallTitle","videoDisplay","callControls"],"justify":"spaceBetween","align":"center"},{"id":"videoCallTitle","component":"Text","text":"Video Call","variant":"h1"},{"id":"videoDisplay","component":"Video","url":"https://example.com/video.mp4"},{"id":"callControls","component":"Row","children":["muteButton","cameraButton","endCallButton"],"justify":"spaceEvenly","align":"center"},{"id":"muteButton","component":"Button","child":"muteButtonText","action":{"event":{"name":"muteToggle"}}},{"id":"muteButtonText","component":"Text","text":"Mute"},{"id":"cameraButton","component":"Button","child":"cameraButtonText","action":{"event":{"name":"cameraToggle"}}},{"id":"cameraButtonText","component":"Text","text":"Camera"},{"id":"endCallButton","component":"Button","child":"endCallButtonText","action":{"event":{"name":"endCall"}}},{"id":"endCallButtonText","component":"Text","text":"End Call"}]}} diff --git a/examples/composer/samples/weatherForecast.sample b/examples/composer/samples/weatherForecast.sample new file mode 100644 index 000000000..9d8ca03d7 --- /dev/null +++ b/examples/composer/samples/weatherForecast.sample @@ -0,0 +1,8 @@ +--- +description: A UI to display the weather forecast. +name: weatherForecast +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a weather forecast UI. It should have a 'Text' (variant 'h1') with the city name, "New York". Below it, a 'Row' with the current temperature as a 'Text' component ("68°F") and an 'Image' for the weather icon (e.g., a sun). Below that, a 'Divider'. Then, a 'List' component to display the 5-day forecast. Each item in the list should be a 'Row' with the day, an icon, and high/low temperatures. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["cityName","currentWeatherRow","forecastDivider","forecastList"]},{"id":"cityName","component":"Text","variant":"h1","text":"New York"},{"id":"currentWeatherRow","component":"Row","align":"center","children":["currentTemperature","weatherIcon"]},{"id":"currentTemperature","component":"Text","text":"68°F"},{"id":"weatherIcon","component":"Image","url":"../travel_app/assets/travel_images/santorini_panorama.jpg","variant":"icon"},{"id":"forecastDivider","component":"Divider","axis":"horizontal"},{"id":"forecastList","component":"List","direction":"vertical","children":["day1Forecast","day2Forecast","day3Forecast","day4Forecast","day5Forecast"]},{"id":"day1Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day1Text","day1Icon","day1Temp"]},{"id":"day1Text","component":"Text","text":"Mon"},{"id":"day1Icon","component":"Icon","name":"cloud"},{"id":"day1Temp","component":"Text","text":"H:72° L:58°"},{"id":"day2Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day2Text","day2Icon","day2Temp"]},{"id":"day2Text","component":"Text","text":"Tue"},{"id":"day2Icon","component":"Icon","name":"wbSunny"},{"id":"day2Temp","component":"Text","text":"H:75° L:60°"},{"id":"day3Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day3Text","day3Icon","day3Temp"]},{"id":"day3Text","component":"Text","text":"Wed"},{"id":"day3Icon","component":"Icon","name":"rainy"},{"id":"day3Temp","component":"Text","text":"H:65° L:55°"},{"id":"day4Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day4Text","day4Icon","day4Temp"]},{"id":"day4Text","component":"Text","text":"Thu"},{"id":"day4Icon","component":"Icon","name":"cloud"},{"id":"day4Temp","component":"Text","text":"H:68° L:57°"},{"id":"day5Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day5Text","day5Icon","day5Temp"]},{"id":"day5Text","component":"Text","text":"Fri"},{"id":"day5Icon","component":"Icon","name":"wbSunny"},{"id":"day5Temp","component":"Text","text":"H:70° L:62°"}]}} diff --git a/packages/genui/lib/src/model/data_model.dart b/packages/genui/lib/src/model/data_model.dart index 0333f710a..2aba001ab 100644 --- a/packages/genui/lib/src/model/data_model.dart +++ b/packages/genui/lib/src/model/data_model.dart @@ -487,14 +487,22 @@ class InMemoryDataModel implements DataModel { if (parent == DataPath.root) break; if (!parent.isAbsolute && parent.segments.isEmpty) break; parent = parent.dirname; - if (_subscriptions.containsKey(parent)) { - _subscriptions[parent]!.value = getValue(parent); + final _RefCountedValueNotifier? notifier = + _subscriptions[parent]; + if (notifier != null) { + final Object? newValue = getValue(parent); + if (newValue != notifier.value) { + notifier.value = newValue; + } else { + // _updateValue mutates containers in place, which means + // listeners on this ancestor won't get automatically notified by + // the `ValueNotifier.value` setter. + notifier.forceNotify(); + } } } - if (path != DataPath.root && _subscriptions.containsKey(DataPath.root)) { - _subscriptions[DataPath.root]!.value = getValue(DataPath.root); - } - for (final DataPath p in _subscriptions.keys) { + + for (final DataPath p in _subscriptions.keys.toList()) { if (p.startsWith(path) && p != path) { _subscriptions[p]!.value = getValue(p); } @@ -513,6 +521,10 @@ class _RefCountedValueNotifier extends ValueNotifier { _refCount++; } + void forceNotify() { + notifyListeners(); + } + @override void dispose() { if (_isDisposed) { diff --git a/pubspec.yaml b/pubspec.yaml index 4d780ea9f..eee1b95c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ environment: workspace: - examples/catalog_gallery + - examples/composer - examples/simple_chat - examples/travel_app