From e34ec305c9ad22d335d2a44e92f69b2b29f17ef6 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:06:20 +0530 Subject: [PATCH] feat: mobile ui showcase --- mobile/packages/ui/showcase/.gitignore | 11 + mobile/packages/ui/showcase/.metadata | 30 ++ .../ui/showcase/analysis_options.yaml | 1 + .../ui/showcase/assets/immich_logo.png | Bin 0 -> 5198 bytes .../showcase/assets/themes/github_dark.json | 339 +++++++++++++++ .../packages/ui/showcase/lib/app_theme.dart | 94 +++++ .../packages/ui/showcase/lib/constants.dart | 16 + mobile/packages/ui/showcase/lib/main.dart | 55 +++ .../pages/components/close_button_page.dart | 41 ++ .../lib/pages/components/form_page.dart | 79 ++++ .../lib/pages/components/html_text_page.dart | 75 ++++ .../pages/components/icon_button_page.dart | 52 +++ .../pages/components/password_input_page.dart | 39 ++ .../pages/components/text_button_page.dart | 140 +++++++ .../lib/pages/components/text_input_page.dart | 65 +++ .../pages/design_system/constants_page.dart | 390 +++++++++++++++++ .../ui/showcase/lib/pages/home_page.dart | 123 ++++++ mobile/packages/ui/showcase/lib/router.dart | 48 +++ mobile/packages/ui/showcase/lib/routes.dart | 97 +++++ .../lib/widgets/component_examples.dart | 85 ++++ .../ui/showcase/lib/widgets/example_card.dart | 221 ++++++++++ .../ui/showcase/lib/widgets/page_title.dart | 17 + .../ui/showcase/lib/widgets/shell_layout.dart | 58 +++ .../lib/widgets/sidebar_navigation.dart | 117 ++++++ mobile/packages/ui/showcase/pubspec.lock | 393 ++++++++++++++++++ mobile/packages/ui/showcase/pubspec.yaml | 26 ++ mobile/packages/ui/showcase/web/favicon.ico | Bin 0 -> 15086 bytes .../showcase/web/icons/Icon-maskable-192.png | Bin 0 -> 5198 bytes .../showcase/web/icons/Icon-maskable-512.png | Bin 0 -> 13544 bytes .../ui/showcase/web/icons/apple-icon-180.png | Bin 0 -> 6358 bytes mobile/packages/ui/showcase/web/index.html | 38 ++ mobile/packages/ui/showcase/web/manifest.json | 37 ++ 32 files changed, 2687 insertions(+) create mode 100644 mobile/packages/ui/showcase/.gitignore create mode 100644 mobile/packages/ui/showcase/.metadata create mode 100644 mobile/packages/ui/showcase/analysis_options.yaml create mode 100644 mobile/packages/ui/showcase/assets/immich_logo.png create mode 100644 mobile/packages/ui/showcase/assets/themes/github_dark.json create mode 100644 mobile/packages/ui/showcase/lib/app_theme.dart create mode 100644 mobile/packages/ui/showcase/lib/constants.dart create mode 100644 mobile/packages/ui/showcase/lib/main.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/form_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/home_page.dart create mode 100644 mobile/packages/ui/showcase/lib/router.dart create mode 100644 mobile/packages/ui/showcase/lib/routes.dart create mode 100644 mobile/packages/ui/showcase/lib/widgets/component_examples.dart create mode 100644 mobile/packages/ui/showcase/lib/widgets/example_card.dart create mode 100644 mobile/packages/ui/showcase/lib/widgets/page_title.dart create mode 100644 mobile/packages/ui/showcase/lib/widgets/shell_layout.dart create mode 100644 mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart create mode 100644 mobile/packages/ui/showcase/pubspec.lock create mode 100644 mobile/packages/ui/showcase/pubspec.yaml create mode 100644 mobile/packages/ui/showcase/web/favicon.ico create mode 100644 mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png create mode 100644 mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png create mode 100644 mobile/packages/ui/showcase/web/icons/apple-icon-180.png create mode 100644 mobile/packages/ui/showcase/web/index.html create mode 100644 mobile/packages/ui/showcase/web/manifest.json diff --git a/mobile/packages/ui/showcase/.gitignore b/mobile/packages/ui/showcase/.gitignore new file mode 100644 index 0000000000..b285cd608b --- /dev/null +++ b/mobile/packages/ui/showcase/.gitignore @@ -0,0 +1,11 @@ +# Build artifacts +build/ + +# Test cache and generated files +.dart_tool/ +.packages +.flutter-plugins +.flutter-plugins-dependencies + +# IDE-specific files +.vscode/ \ No newline at end of file diff --git a/mobile/packages/ui/showcase/.metadata b/mobile/packages/ui/showcase/.metadata new file mode 100644 index 0000000000..b95fa4d74e --- /dev/null +++ b/mobile/packages/ui/showcase/.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: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: web + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # 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/mobile/packages/ui/showcase/analysis_options.yaml b/mobile/packages/ui/showcase/analysis_options.yaml new file mode 100644 index 0000000000..f9b303465f --- /dev/null +++ b/mobile/packages/ui/showcase/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/mobile/packages/ui/showcase/assets/immich_logo.png b/mobile/packages/ui/showcase/assets/immich_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..49fd3ae2896f8674cc5e4521f343508b1ffa70ee GIT binary patch literal 5198 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 literal 0 HcmV?d00001 diff --git a/mobile/packages/ui/showcase/assets/themes/github_dark.json b/mobile/packages/ui/showcase/assets/themes/github_dark.json new file mode 100644 index 0000000000..bd4801482e --- /dev/null +++ b/mobile/packages/ui/showcase/assets/themes/github_dark.json @@ -0,0 +1,339 @@ +{ + "name": "GitHub Dark", + "settings": [ + { + "settings": { + "foreground": "#e1e4e8", + "background": "#24292e" + } + }, + { + "scope": [ + "comment", + "punctuation.definition.comment", + "string.comment" + ], + "settings": { + "foreground": "#6a737d" + } + }, + { + "scope": [ + "constant", + "entity.name.constant", + "variable.other.constant", + "variable.other.enummember", + "variable.language" + ], + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "entity", + "entity.name" + ], + "settings": { + "foreground": "#b392f0" + } + }, + { + "scope": "variable.parameter.function", + "settings": { + "foreground": "#e1e4e8" + } + }, + { + "scope": "entity.name.tag", + "settings": { + "foreground": "#85e89d" + } + }, + { + "scope": "keyword", + "settings": { + "foreground": "#f97583" + } + }, + { + "scope": [ + "storage", + "storage.type" + ], + "settings": { + "foreground": "#f97583" + } + }, + { + "scope": [ + "storage.modifier.package", + "storage.modifier.import", + "storage.type.java" + ], + "settings": { + "foreground": "#e1e4e8" + } + }, + { + "scope": [ + "string", + "punctuation.definition.string", + "string punctuation.section.embedded source" + ], + "settings": { + "foreground": "#9ecbff" + } + }, + { + "scope": "support", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.property-name", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "variable", + "settings": { + "foreground": "#ffab70" + } + }, + { + "scope": "variable.other", + "settings": { + "foreground": "#e1e4e8" + } + }, + { + "scope": "invalid.broken", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "invalid.deprecated", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "invalid.illegal", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "invalid.unimplemented", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "message.error", + "settings": { + "foreground": "#fdaeb7" + } + }, + { + "scope": "string variable", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "source.regexp", + "string.regexp" + ], + "settings": { + "foreground": "#dbedff" + } + }, + { + "scope": [ + "string.regexp.character-class", + "string.regexp constant.character.escape", + "string.regexp source.ruby.embedded", + "string.regexp string.regexp.arbitrary-repitition" + ], + "settings": { + "foreground": "#dbedff" + } + }, + { + "scope": "string.regexp constant.character.escape", + "settings": { + "fontStyle": "bold", + "foreground": "#85e89d" + } + }, + { + "scope": "support.constant", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "support.variable", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.module-reference", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "punctuation.definition.list.begin.markdown", + "settings": { + "foreground": "#ffab70" + } + }, + { + "scope": [ + "markup.heading", + "markup.heading entity.name" + ], + "settings": { + "fontStyle": "bold", + "foreground": "#79b8ff" + } + }, + { + "scope": "markup.quote", + "settings": { + "foreground": "#85e89d" + } + }, + { + "scope": "markup.italic", + "settings": { + "fontStyle": "italic", + "foreground": "#e1e4e8" + } + }, + { + "scope": "markup.bold", + "settings": { + "fontStyle": "bold", + "foreground": "#e1e4e8" + } + }, + { + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "scope": "markup.inline.raw", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "markup.deleted", + "meta.diff.header.from-file", + "punctuation.definition.deleted" + ], + "settings": { + "foreground": "#fdaeb7" + } + }, + { + "scope": [ + "markup.inserted", + "meta.diff.header.to-file", + "punctuation.definition.inserted" + ], + "settings": { + "foreground": "#85e89d" + } + }, + { + "scope": [ + "markup.changed", + "punctuation.definition.changed" + ], + "settings": { + "foreground": "#ffab70" + } + }, + { + "scope": [ + "markup.ignored", + "markup.untracked" + ], + "settings": { + "foreground": "#2f363d" + } + }, + { + "scope": "meta.diff.range", + "settings": { + "fontStyle": "bold", + "foreground": "#b392f0" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.separator", + "settings": { + "fontStyle": "bold", + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.output", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "brackethighlighter.tag", + "brackethighlighter.curly", + "brackethighlighter.round", + "brackethighlighter.square", + "brackethighlighter.angle", + "brackethighlighter.quote" + ], + "settings": { + "foreground": "#d1d5da" + } + }, + { + "scope": "brackethighlighter.unmatched", + "settings": { + "foreground": "#fdaeb7" + } + }, + { + "scope": [ + "constant.other.reference.link", + "string.other.link" + ], + "settings": { + "fontStyle": "underline", + "foreground": "#dbedff" + } + } + ] +} diff --git a/mobile/packages/ui/showcase/lib/app_theme.dart b/mobile/packages/ui/showcase/lib/app_theme.dart new file mode 100644 index 0000000000..66e64d4645 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/app_theme.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Light theme colors + static const _primary500 = Color(0xFF4250AF); + static const _primary100 = Color(0xFFD4D6F0); + static const _primary900 = Color(0xFF181E44); + static const _danger500 = Color(0xFFE53E3E); + static const _light50 = Color(0xFFFAFAFA); + static const _light300 = Color(0xFFD4D4D4); + static const _light500 = Color(0xFF737373); + + // Dark theme colors + static const _darkPrimary500 = Color(0xFFACCBFA); + static const _darkPrimary300 = Color(0xFF616D94); + static const _darkDanger500 = Color(0xFFE88080); + static const _darkLight50 = Color(0xFF0A0A0A); + static const _darkLight100 = Color(0xFF171717); + static const _darkLight200 = Color(0xFF262626); + + static ThemeData get lightTheme { + return ThemeData( + colorScheme: const ColorScheme.light( + primary: _primary500, + onPrimary: Colors.white, + primaryContainer: _primary100, + onPrimaryContainer: _primary900, + secondary: _light500, + onSecondary: Colors.white, + error: _danger500, + onError: Colors.white, + surface: _light50, + onSurface: Color(0xFF1A1C1E), + surfaceContainerHighest: Color(0xFFE3E4E8), + outline: Color(0xFFD1D3D9), + outlineVariant: _light300, + ), + useMaterial3: true, + scaffoldBackgroundColor: _light50, + cardTheme: const CardThemeData( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: _light300, width: 1), + ), + ), + appBarTheme: const AppBarTheme( + centerTitle: false, + elevation: 0, + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + foregroundColor: Color(0xFF1A1C1E), + ), + ); + } + + static ThemeData get darkTheme { + return ThemeData( + colorScheme: const ColorScheme.dark( + primary: _darkPrimary500, + onPrimary: Color(0xFF0F1433), + primaryContainer: _darkPrimary300, + onPrimaryContainer: _primary100, + secondary: Color(0xFFC4C6D0), + onSecondary: Color(0xFF2E3042), + error: _darkDanger500, + onError: Color(0xFF0F1433), + surface: _darkLight50, + onSurface: Color(0xFFE3E3E6), + surfaceContainerHighest: _darkLight200, + outline: Color(0xFF8E9099), + outlineVariant: Color(0xFF43464F), + ), + useMaterial3: true, + scaffoldBackgroundColor: _darkLight50, + cardTheme: const CardThemeData( + elevation: 0, + color: _darkLight100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: _darkLight200, width: 1), + ), + ), + appBarTheme: const AppBarTheme( + centerTitle: false, + elevation: 0, + backgroundColor: _darkLight50, + surfaceTintColor: Colors.transparent, + foregroundColor: Color(0xFFE3E3E6), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/constants.dart b/mobile/packages/ui/showcase/lib/constants.dart new file mode 100644 index 0000000000..cfca4cfda9 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/constants.dart @@ -0,0 +1,16 @@ +const String appTitle = '@immich/ui'; + +class LayoutConstants { + static const double sidebarWidth = 220.0; + + static const double gridSpacing = 16.0; + static const double gridAspectRatio = 2.5; + + static const double borderRadiusSmall = 6.0; + static const double borderRadiusMedium = 8.0; + static const double borderRadiusLarge = 12.0; + + static const double iconSizeSmall = 16.0; + static const double iconSizeMedium = 18.0; + static const double iconSizeLarge = 20.0; +} diff --git a/mobile/packages/ui/showcase/lib/main.dart b/mobile/packages/ui/showcase/lib/main.dart new file mode 100644 index 0000000000..6cd2df4fe5 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/main.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/app_theme.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/router.dart'; +import 'package:showcase/widgets/example_card.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initializeCodeHighlighter(); + runApp(const ShowcaseApp()); +} + +class ShowcaseApp extends StatefulWidget { + const ShowcaseApp({super.key}); + + @override + State createState() => _ShowcaseAppState(); +} + +class _ShowcaseAppState extends State { + ThemeMode _themeMode = ThemeMode.light; + late final GoRouter _router; + + @override + void initState() { + super.initState(); + _router = AppRouter.createRouter(_toggleTheme); + } + + void _toggleTheme() { + setState(() { + _themeMode = _themeMode == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: appTitle, + themeMode: _themeMode, + routerConfig: _router, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + debugShowCheckedModeBanner: false, + builder: (context, child) => ImmichThemeProvider( + colorScheme: Theme.of(context).colorScheme, + child: child!, + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart new file mode 100644 index 0000000000..1bae98e0a4 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class CloseButtonPage extends StatelessWidget { + const CloseButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.closeButton.name, + child: ComponentExamples( + title: 'ImmichCloseButton', + subtitle: 'Pre-configured close button for dialogs and sheets.', + examples: [ + ExampleCard( + title: 'Default & Custom', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichCloseButton(onPressed: () {}), + ImmichCloseButton( + variant: ImmichVariant.filled, + onPressed: () {}, + ), + ImmichCloseButton( + color: ImmichColor.secondary, + onPressed: () {}, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/form_page.dart b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart new file mode 100644 index 0000000000..14567031de --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class FormPage extends StatefulWidget { + const FormPage({super.key}); + + @override + State createState() => _FormPageState(); +} + +class _FormPageState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + String _result = ''; + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.form.name, + child: ComponentExamples( + title: 'ImmichForm', + subtitle: + 'Form container with built-in validation and submit handling.', + examples: [ + ExampleCard( + title: 'Login Form', + preview: Column( + children: [ + ImmichForm( + submitText: 'Login', + submitIcon: Icons.login, + onSubmit: () async { + await Future.delayed(const Duration(seconds: 1)); + setState(() { + _result = 'Form submitted!'; + }); + }, + child: Column( + spacing: 10, + children: [ + ImmichTextInput( + label: 'Email', + controller: _emailController, + keyboardType: TextInputType.emailAddress, + validator: (value) => + value?.isEmpty ?? true ? 'Required' : null, + ), + ImmichPasswordInput( + label: 'Password', + controller: _passwordController, + validator: (value) => + value?.isEmpty ?? true ? 'Required' : null, + ), + ], + ), + ), + if (_result.isNotEmpty) ...[ + const SizedBox(height: 16), + Text(_result, style: const TextStyle(color: Colors.green)), + ], + ], + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart b/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart new file mode 100644 index 0000000000..1b6c8a39a7 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class HtmlTextPage extends StatelessWidget { + const HtmlTextPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.htmlText.name, + child: ComponentExamples( + title: 'ImmichHtmlText', + subtitle: 'Render text with HTML formatting (bold, links).', + examples: [ + ExampleCard( + title: 'Bold Text', + preview: ImmichHtmlText( + 'This is bold text and strong text.', + ), + code: '''ImmichHtmlText( + 'This is bold text and strong text.', +)''', + ), + ExampleCard( + title: 'Links', + preview: ImmichHtmlText( + 'Read the documentation or visit GitHub.', + linkHandlers: { + 'docs-link': () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Docs link clicked!')), + ); + }, + 'github-link': () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('GitHub link clicked!')), + ); + }, + }, + ), + code: '''ImmichHtmlText( + 'Read the documentation.', + linkHandlers: { + 'docs-link': () => launchUrl(docsUrl), + }, +)''', + ), + ExampleCard( + title: 'Nested Tags', + preview: ImmichHtmlText( + 'You can combine bold and links together.', + linkHandlers: { + 'link': () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Nested link clicked!')), + ); + }, + }, + ), + code: '''ImmichHtmlText( + 'You can combine bold and links.', + linkHandlers: { + 'link': () => handleClick(), + }, +)''', + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart new file mode 100644 index 0000000000..4418b1de4f --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class IconButtonPage extends StatelessWidget { + const IconButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.iconButton.name, + child: ComponentExamples( + title: 'ImmichIconButton', + subtitle: 'Icon-only button with customizable styling.', + examples: [ + ExampleCard( + title: 'Variants & Colors', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichIconButton( + icon: Icons.add, + onPressed: () {}, + variant: ImmichVariant.filled, + ), + ImmichIconButton( + icon: Icons.edit, + onPressed: () {}, + variant: ImmichVariant.ghost, + ), + ImmichIconButton( + icon: Icons.delete, + onPressed: () {}, + color: ImmichColor.secondary, + ), + ImmichIconButton( + icon: Icons.settings, + onPressed: () {}, + disabled: true, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart b/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart new file mode 100644 index 0000000000..772dd7882f --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class PasswordInputPage extends StatelessWidget { + const PasswordInputPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.passwordInput.name, + child: ComponentExamples( + title: 'ImmichPasswordInput', + subtitle: 'Password field with visibility toggle.', + examples: [ + ExampleCard( + title: 'Password Input', + preview: ImmichPasswordInput( + label: 'Password', + hintText: 'Enter your password', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart new file mode 100644 index 0000000000..59e5b86294 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class TextButtonPage extends StatefulWidget { + const TextButtonPage({super.key}); + + @override + State createState() => _TextButtonPageState(); +} + +class _TextButtonPageState extends State { + bool _isLoading = false; + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.textButton.name, + child: ComponentExamples( + title: 'ImmichTextButton', + subtitle: + 'A versatile button component with multiple variants and color options.', + examples: [ + ExampleCard( + title: 'Variants', + description: + 'Filled and ghost variants for different visual hierarchy', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'Filled', + variant: ImmichVariant.filled, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Ghost', + variant: ImmichVariant.ghost, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'Colors', + description: 'Primary and secondary color options', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'Primary', + color: ImmichColor.primary, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Secondary', + color: ImmichColor.secondary, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'With Icons', + description: 'Add leading icons', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'With Icon', + icon: Icons.add, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Download', + icon: Icons.download, + variant: ImmichVariant.ghost, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'Loading State', + description: 'Shows loading indicator during async operations', + preview: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImmichTextButton( + onPressed: () async { + setState(() => _isLoading = true); + await Future.delayed(const Duration(seconds: 2)); + if (mounted) setState(() => _isLoading = false); + }, + labelText: _isLoading ? 'Loading...' : 'Click Me', + loading: _isLoading, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'Disabled State', + description: 'Buttons can be disabled', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'Disabled', + disabled: true, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Disabled Ghost', + variant: ImmichVariant.ghost, + disabled: true, + expanded: false, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart b/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart new file mode 100644 index 0000000000..5a0bfec6cd --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class TextInputPage extends StatefulWidget { + const TextInputPage({super.key}); + + @override + State createState() => _TextInputPageState(); +} + +class _TextInputPageState extends State { + final _controller1 = TextEditingController(); + final _controller2 = TextEditingController(); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.textInput.name, + child: ComponentExamples( + title: 'ImmichTextInput', + subtitle: 'Text field with validation support.', + examples: [ + ExampleCard( + title: 'Basic Usage', + preview: Column( + children: [ + ImmichTextInput( + label: 'Email', + hintText: 'Enter your email', + controller: _controller1, + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + ImmichTextInput( + label: 'Username', + controller: _controller2, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Username is required'; + } + if (value.length < 3) { + return 'Username must be at least 3 characters'; + } + return null; + }, + ), + ], + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller1.dispose(); + _controller2.dispose(); + super.dispose(); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart b/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart new file mode 100644 index 0000000000..c120cc7790 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart @@ -0,0 +1,390 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class ConstantsPage extends StatefulWidget { + const ConstantsPage({super.key}); + + @override + State createState() => _ConstantsPageState(); +} + +class _ConstantsPageState extends State { + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.constants.name, + child: ComponentExamples( + title: 'Constants', + subtitle: 'Consistent spacing, sizing, and styling constants.', + expand: true, + examples: [ + ExampleCard( + title: 'Spacing', + description: 'ImmichSpacing (4.0 → 48.0)', + preview: Column( + children: [ + _SpacingBox(label: 'xs', size: ImmichSpacing.xs), + _SpacingBox(label: 'sm', size: ImmichSpacing.sm), + _SpacingBox(label: 'md', size: ImmichSpacing.md), + _SpacingBox(label: 'lg', size: ImmichSpacing.lg), + _SpacingBox(label: 'xl', size: ImmichSpacing.xl), + _SpacingBox(label: 'xxl', size: ImmichSpacing.xxl), + _SpacingBox(label: 'xxxl', size: ImmichSpacing.xxxl), + ], + ), + ), + ExampleCard( + title: 'Border Radius', + description: 'ImmichRadius (0.0 → 24.0)', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + _RadiusBox(label: 'none', radius: ImmichRadius.none), + _RadiusBox(label: 'xs', radius: ImmichRadius.xs), + _RadiusBox(label: 'sm', radius: ImmichRadius.sm), + _RadiusBox(label: 'md', radius: ImmichRadius.md), + _RadiusBox(label: 'lg', radius: ImmichRadius.lg), + _RadiusBox(label: 'xl', radius: ImmichRadius.xl), + _RadiusBox(label: 'xxl', radius: ImmichRadius.xxl), + ], + ), + ), + ExampleCard( + title: 'Icon Sizes', + description: 'ImmichIconSize (16.0 → 48.0)', + preview: Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.start, + children: [ + _IconSizeBox(label: 'xs', size: ImmichIconSize.xs), + _IconSizeBox(label: 'sm', size: ImmichIconSize.sm), + _IconSizeBox(label: 'md', size: ImmichIconSize.md), + _IconSizeBox(label: 'lg', size: ImmichIconSize.lg), + _IconSizeBox(label: 'xl', size: ImmichIconSize.xl), + _IconSizeBox(label: 'xxl', size: ImmichIconSize.xxl), + ], + ), + ), + const ExampleCard( + title: 'Text Sizes', + description: 'ImmichTextSize (10.0 → 60.0)', + preview: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Caption', + style: TextStyle(fontSize: ImmichTextSize.caption), + ), + Text('Label', style: TextStyle(fontSize: ImmichTextSize.label)), + Text('Body', style: TextStyle(fontSize: ImmichTextSize.body)), + Text('H6', style: TextStyle(fontSize: ImmichTextSize.h6)), + Text('H5', style: TextStyle(fontSize: ImmichTextSize.h5)), + Text('H4', style: TextStyle(fontSize: ImmichTextSize.h4)), + Text('H3', style: TextStyle(fontSize: ImmichTextSize.h3)), + Text('H2', style: TextStyle(fontSize: ImmichTextSize.h2)), + Text('H1', style: TextStyle(fontSize: ImmichTextSize.h1)), + ], + ), + ), + ExampleCard( + title: 'Elevation', + description: 'ImmichElevation (0.0 → 16.0)', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + _ElevationBox(label: 'none', elevation: ImmichElevation.none), + _ElevationBox(label: 'xs', elevation: ImmichElevation.xs), + _ElevationBox(label: 'sm', elevation: ImmichElevation.sm), + _ElevationBox(label: 'md', elevation: ImmichElevation.md), + _ElevationBox(label: 'lg', elevation: ImmichElevation.lg), + _ElevationBox(label: 'xl', elevation: ImmichElevation.xl), + _ElevationBox(label: 'xxl', elevation: ImmichElevation.xxl), + ], + ), + ), + ExampleCard( + title: 'Border Width', + description: 'ImmichBorderWidth (0.5 → 4.0)', + preview: Column( + children: [ + _BorderBox( + label: 'hairline', + borderWidth: ImmichBorderWidth.hairline, + ), + _BorderBox(label: 'base', borderWidth: ImmichBorderWidth.base), + _BorderBox(label: 'md', borderWidth: ImmichBorderWidth.md), + _BorderBox(label: 'lg', borderWidth: ImmichBorderWidth.lg), + _BorderBox(label: 'xl', borderWidth: ImmichBorderWidth.xl), + ], + ), + ), + ExampleCard( + title: 'Animation Durations', + description: 'ImmichDuration (100ms → 700ms)', + preview: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + _AnimatedDurationBox( + label: 'Extra Fast', + duration: ImmichDuration.extraFast, + ), + _AnimatedDurationBox( + label: 'Fast', + duration: ImmichDuration.fast, + ), + _AnimatedDurationBox( + label: 'Normal', + duration: ImmichDuration.normal, + ), + _AnimatedDurationBox( + label: 'Slow', + duration: ImmichDuration.slow, + ), + _AnimatedDurationBox( + label: 'Extra Slow', + duration: ImmichDuration.extraSlow, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SpacingBox extends StatelessWidget { + final String label; + final double size; + + const _SpacingBox({required this.label, required this.size}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 60, + child: Text(label, style: const TextStyle(fontFamily: 'monospace')), + ), + Container( + width: size, + height: 24, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text('${size.toStringAsFixed(1)}px'), + ], + ), + ); + } +} + +class _RadiusBox extends StatelessWidget { + final String label; + final double radius; + + const _RadiusBox({required this.label, required this.radius}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(radius), + ), + ), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12)), + ], + ); + } +} + +class _IconSizeBox extends StatelessWidget { + final String label; + final double size; + + const _IconSizeBox({required this.label, required this.size}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Icon(Icons.palette_rounded, size: size), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12)), + Text( + '${size.toStringAsFixed(0)}px', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ); + } +} + +class _ElevationBox extends StatelessWidget { + final String label; + final double elevation; + + const _ElevationBox({required this.label, required this.elevation}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Material( + elevation: elevation, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Container( + width: 60, + height: 60, + alignment: Alignment.center, + child: Text(label, style: const TextStyle(fontSize: 12)), + ), + ), + const SizedBox(height: 4), + Text( + elevation.toStringAsFixed(1), + style: const TextStyle(fontSize: 10), + ), + ], + ); + } +} + +class _BorderBox extends StatelessWidget { + final String label; + final double borderWidth; + + const _BorderBox({required this.label, required this.borderWidth}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 80, + child: Text(label, style: const TextStyle(fontFamily: 'monospace')), + ), + Expanded( + child: Container( + height: 40, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: borderWidth, + ), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + ), + ), + const SizedBox(width: 8), + Text('${borderWidth.toStringAsFixed(1)}px'), + ], + ), + ); + } +} + +class _AnimatedDurationBox extends StatefulWidget { + final String label; + final Duration duration; + + const _AnimatedDurationBox({required this.label, required this.duration}); + + @override + State<_AnimatedDurationBox> createState() => _AnimatedDurationBoxState(); +} + +class _AnimatedDurationBoxState extends State<_AnimatedDurationBox> { + bool _atEnd = false; + bool _isAnimating = false; + + void _playAnimation() async { + if (_isAnimating) return; + setState(() => _isAnimating = true); + setState(() => _atEnd = true); + await Future.delayed(widget.duration); + if (!mounted) return; + setState(() => _atEnd = false); + await Future.delayed(widget.duration); + if (!mounted) return; + setState(() => _isAnimating = false); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( + children: [ + SizedBox( + width: 90, + child: Text( + widget.label, + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ), + Expanded( + child: Container( + height: 32, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(6), + ), + child: AnimatedAlign( + duration: widget.duration, + curve: Curves.easeInOut, + alignment: _atEnd ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: 60, + height: 28, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: colorScheme.primary, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text( + '${widget.duration.inMilliseconds}ms', + style: TextStyle( + fontSize: 11, + color: colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: _isAnimating ? null : _playAnimation, + icon: Icon( + Icons.play_arrow_rounded, + color: _isAnimating ? colorScheme.outline : colorScheme.primary, + ), + iconSize: 24, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + ], + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/home_page.dart b/mobile/packages/ui/showcase/lib/pages/home_page.dart new file mode 100644 index 0000000000..45bd79a421 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/home_page.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/routes.dart'; + +class HomePage extends StatelessWidget { + final VoidCallback onThemeToggle; + + const HomePage({super.key, required this.onThemeToggle}); + + @override + Widget build(BuildContext context) { + return Title( + title: appTitle, + color: Theme.of(context).colorScheme.primary, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + children: [ + Text( + appTitle, + style: Theme.of(context).textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 12), + Text( + 'A collection of Flutter components that are shared across all Immich projects', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w400, + height: 1.5, + ), + ), + const SizedBox(height: 48), + ...routesByCategory.entries.map((entry) { + if (entry.key == AppRouteCategory.root) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.key.displayName, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 16), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: LayoutConstants.gridSpacing, + mainAxisSpacing: LayoutConstants.gridSpacing, + childAspectRatio: LayoutConstants.gridAspectRatio, + ), + itemCount: entry.value.length, + itemBuilder: (context, index) { + return _ComponentCard(route: entry.value[index]); + }, + ), + const SizedBox(height: 48), + ], + ); + }), + ], + ), + ); + } +} + +class _ComponentCard extends StatelessWidget { + final AppRoute route; + + const _ComponentCard({required this.route}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => context.go(route.path), + borderRadius: BorderRadius.circular(LayoutConstants.borderRadiusLarge), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + route.icon, + size: 32, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + route.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + + const SizedBox(height: 8), + Text( + route.description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.4, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/router.dart b/mobile/packages/ui/showcase/lib/router.dart new file mode 100644 index 0000000000..014de44fd8 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/router.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:showcase/pages/components/close_button_page.dart'; +import 'package:showcase/pages/components/form_page.dart'; +import 'package:showcase/pages/components/html_text_page.dart'; +import 'package:showcase/pages/components/icon_button_page.dart'; +import 'package:showcase/pages/components/password_input_page.dart'; +import 'package:showcase/pages/components/text_button_page.dart'; +import 'package:showcase/pages/components/text_input_page.dart'; +import 'package:showcase/pages/design_system/constants_page.dart'; +import 'package:showcase/pages/home_page.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/shell_layout.dart'; + +class AppRouter { + static GoRouter createRouter(VoidCallback onThemeToggle) { + return GoRouter( + initialLocation: AppRoute.home.path, + routes: [ + ShellRoute( + builder: (context, state, child) => + ShellLayout(onThemeToggle: onThemeToggle, child: child), + routes: AppRoute.values + .map( + (route) => GoRoute( + path: route.path, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: switch (route) { + AppRoute.home => HomePage(onThemeToggle: onThemeToggle), + AppRoute.textButton => const TextButtonPage(), + AppRoute.iconButton => const IconButtonPage(), + AppRoute.closeButton => const CloseButtonPage(), + AppRoute.textInput => const TextInputPage(), + AppRoute.passwordInput => const PasswordInputPage(), + AppRoute.form => const FormPage(), + AppRoute.htmlText => const HtmlTextPage(), + AppRoute.constants => const ConstantsPage(), + }, + ), + ), + ) + .toList(), + ), + ], + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/routes.dart b/mobile/packages/ui/showcase/lib/routes.dart new file mode 100644 index 0000000000..a39fb7bc34 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/routes.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +enum AppRouteCategory { + root(''), + forms('Forms'), + buttons('Buttons'), + designSystem('Design System'); + + final String displayName; + const AppRouteCategory(this.displayName); +} + +enum AppRoute { + home( + name: 'Home', + description: 'Home page', + path: '/', + category: AppRouteCategory.root, + icon: Icons.home_outlined, + ), + textButton( + name: 'Text Button', + description: 'Versatile button with filled and ghost variants', + path: '/text-button', + category: AppRouteCategory.buttons, + icon: Icons.smart_button_rounded, + ), + iconButton( + name: 'Icon Button', + description: 'Icon-only button with customizable styling', + path: '/icon-button', + category: AppRouteCategory.buttons, + icon: Icons.radio_button_unchecked_rounded, + ), + closeButton( + name: 'Close Button', + description: 'Pre-configured close button for dialogs', + path: '/close-button', + category: AppRouteCategory.buttons, + icon: Icons.close_rounded, + ), + textInput( + name: 'Text Input', + description: 'Text field with validation support', + path: '/text-input', + category: AppRouteCategory.forms, + icon: Icons.text_fields_outlined, + ), + passwordInput( + name: 'Password Input', + description: 'Password field with visibility toggle', + path: '/password-input', + category: AppRouteCategory.forms, + icon: Icons.password_outlined, + ), + form( + name: 'Form', + description: 'Form container with built-in validation', + path: '/form', + category: AppRouteCategory.forms, + icon: Icons.description_outlined, + ), + htmlText( + name: 'Html Text', + description: 'Render text with HTML formatting', + path: '/html-text', + category: AppRouteCategory.forms, + icon: Icons.code_rounded, + ), + constants( + name: 'Constants', + description: 'Spacing, colors, typography, and more', + path: '/constants', + category: AppRouteCategory.designSystem, + icon: Icons.palette_outlined, + ); + + final String name; + final String description; + final String path; + final AppRouteCategory category; + final IconData icon; + + const AppRoute({ + required this.name, + required this.description, + required this.path, + required this.category, + required this.icon, + }); +} + +final routesByCategory = AppRoute.values + .fold>>({}, (map, route) { + map.putIfAbsent(route.category, () => []).add(route); + return map; + }); diff --git a/mobile/packages/ui/showcase/lib/widgets/component_examples.dart b/mobile/packages/ui/showcase/lib/widgets/component_examples.dart new file mode 100644 index 0000000000..21e6516079 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/component_examples.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +class ComponentExamples extends StatelessWidget { + final String title; + final String? subtitle; + final List examples; + final bool expand; + + const ComponentExamples({ + super.key, + required this.title, + this.subtitle, + required this.examples, + this.expand = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 24, 24, 24), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: _PageHeader(title: title, subtitle: subtitle), + ), + const SliverPadding(padding: EdgeInsets.only(top: 24)), + if (expand) + SliverList.builder( + itemCount: examples.length, + itemBuilder: (context, index) => examples[index], + ) + else + SliverLayoutBuilder( + builder: (context, constraints) { + return SliverList.builder( + itemCount: examples.length, + itemBuilder: (context, index) => Align( + alignment: Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.crossAxisExtent * 0.6, + maxWidth: constraints.crossAxisExtent, + ), + child: IntrinsicWidth(child: examples[index]), + ), + ), + ); + }, + ), + ], + ), + ); + } +} + +class _PageHeader extends StatelessWidget { + final String title; + final String? subtitle; + + const _PageHeader({required this.title, this.subtitle}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of( + context, + ).textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.bold), + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/example_card.dart b/mobile/packages/ui/showcase/lib/widgets/example_card.dart new file mode 100644 index 0000000000..7e28c738c6 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/example_card.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:showcase/constants.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +late final Highlighter _codeHighlighter; + +Future initializeCodeHighlighter() async { + await Highlighter.initialize(['dart']); + final darkTheme = await HighlighterTheme.loadFromAssets([ + 'assets/themes/github_dark.json', + ], const TextStyle(color: Color(0xFFe1e4e8))); + + _codeHighlighter = Highlighter(language: 'dart', theme: darkTheme); +} + +class ExampleCard extends StatefulWidget { + final String title; + final String? description; + final Widget preview; + final String? code; + + const ExampleCard({ + super.key, + required this.title, + this.description, + required this.preview, + this.code, + }); + + @override + State createState() => _ExampleCardState(); +} + +class _ExampleCardState extends State { + bool _showPreview = true; + + @override + Widget build(BuildContext context) { + return Card( + elevation: 1, + margin: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + if (widget.description != null) + Text( + widget.description!, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + if (widget.code != null) ...[ + const SizedBox(width: 16), + Row( + children: [ + _ToggleButton( + icon: Icons.visibility_rounded, + label: 'Preview', + isSelected: _showPreview, + onTap: () => setState(() => _showPreview = true), + ), + const SizedBox(width: 8), + _ToggleButton( + icon: Icons.code_rounded, + label: 'Code', + isSelected: !_showPreview, + onTap: () => setState(() => _showPreview = false), + ), + ], + ), + ], + ], + ), + ), + const Divider(height: 1), + if (_showPreview) + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox(width: double.infinity, child: widget.preview), + ) + else + Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Color(0xFF24292e), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular( + LayoutConstants.borderRadiusMedium, + ), + bottomRight: Radius.circular( + LayoutConstants.borderRadiusMedium, + ), + ), + ), + child: _CodeCard(code: widget.code!), + ), + ], + ), + ); + } +} + +class _ToggleButton extends StatelessWidget { + final IconData icon; + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _ToggleButton({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(24), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.7) + : Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(24), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 16, + color: Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(width: 6), + Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ], + ), + ), + ); + } +} + +class _CodeCard extends StatelessWidget { + final String code; + + const _CodeCard({required this.code}); + + @override + Widget build(BuildContext context) { + final lines = code.split('\n'); + final lineNumberColor = Colors.white.withValues(alpha: 0.4); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate( + lines.length, + (index) => SizedBox( + height: 20, + child: Text( + '${index + 1}', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: lineNumberColor, + height: 1.5, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + SelectableText.rich( + _codeHighlighter.highlight(code), + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + height: 1.54, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/page_title.dart b/mobile/packages/ui/showcase/lib/widgets/page_title.dart new file mode 100644 index 0000000000..eae3bf6ffb --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/page_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PageTitle extends StatelessWidget { + final String title; + final Widget child; + + const PageTitle({super.key, required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Title( + title: '$title | @immich/ui', + color: Theme.of(context).colorScheme.primary, + child: child, + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart b/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart new file mode 100644 index 0000000000..e71037a88e --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/widgets/sidebar_navigation.dart'; + +class ShellLayout extends StatelessWidget { + final Widget child; + final VoidCallback onThemeToggle; + + const ShellLayout({ + super.key, + required this.child, + required this.onThemeToggle, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/immich_logo.png', height: 32, width: 32), + const SizedBox(width: 8), + Text( + 'immich', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + actions: [ + IconButton( + icon: Icon( + isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined, + size: LayoutConstants.iconSizeLarge, + ), + onPressed: onThemeToggle, + tooltip: 'Toggle theme', + ), + ], + shape: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1), + ), + ), + body: Row( + children: [ + const SidebarNavigation(), + const VerticalDivider(), + Expanded(child: child), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart b/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart new file mode 100644 index 0000000000..10eba170e6 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/routes.dart'; + +class SidebarNavigation extends StatelessWidget { + const SidebarNavigation({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: LayoutConstants.sidebarWidth, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + children: [ + ...routesByCategory.entries.expand((entry) { + final category = entry.key; + final routes = entry.value; + return [ + if (category != AppRouteCategory.root) _CategoryHeader(category), + ...routes.map((route) => _NavItem(route)), + const SizedBox(height: 24), + ]; + }), + ], + ), + ); + } +} + +class _CategoryHeader extends StatelessWidget { + final AppRouteCategory category; + + const _CategoryHeader(this.category); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8), + child: Text( + category.displayName, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + ); + } +} + +class _NavItem extends StatelessWidget { + final AppRoute route; + + const _NavItem(this.route); + + @override + Widget build(BuildContext context) { + final currentRoute = GoRouterState.of(context).uri.toString(); + final isSelected = currentRoute == route.path; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + context.go(route.path); + }, + borderRadius: BorderRadius.circular( + LayoutConstants.borderRadiusMedium, + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? (isDark + ? Colors.white.withValues(alpha: 0.1) + : Theme.of( + context, + ).colorScheme.primaryContainer.withValues(alpha: 0.5)) + : Colors.transparent, + borderRadius: BorderRadius.circular( + LayoutConstants.borderRadiusMedium, + ), + ), + child: Row( + children: [ + Icon( + route.icon, + size: 20, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + route.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/pubspec.lock b/mobile/packages/ui/showcase/pubspec.lock new file mode 100644 index 0000000000..4d8ec62b90 --- /dev/null +++ b/mobile/packages/ui/showcase/pubspec.lock @@ -0,0 +1,393 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a + url: "https://pub.dev" + source: hosted + version: "17.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + immich_ui: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.0" + irondash_engine_context: + dependency: transitive + description: + name: irondash_engine_context + sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" + url: "https://pub.dev" + source: hosted + version: "0.5.5" + irondash_message_channel: + dependency: transitive + description: + name: irondash_message_channel + sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 + url: "https://pub.dev" + source: hosted + version: "0.7.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pixel_snap: + dependency: transitive + description: + name: pixel_snap + sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" + url: "https://pub.dev" + source: hosted + version: "0.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + super_clipboard: + dependency: transitive + description: + name: super_clipboard + sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 + url: "https://pub.dev" + source: hosted + version: "0.9.1" + super_native_extensions: + dependency: transitive + description: + name: super_native_extensions + sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 + url: "https://pub.dev" + source: hosted + version: "0.9.1" + syntax_highlight: + dependency: "direct main" + description: + name: syntax_highlight + sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.0" diff --git a/mobile/packages/ui/showcase/pubspec.yaml b/mobile/packages/ui/showcase/pubspec.yaml new file mode 100644 index 0000000000..2cfd5ff8c3 --- /dev/null +++ b/mobile/packages/ui/showcase/pubspec.yaml @@ -0,0 +1,26 @@ +name: showcase +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 + +dependencies: + flutter: + sdk: flutter + immich_ui: + path: ../ + go_router: ^17.0.1 + syntax_highlight: ^0.5.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true + assets: + - assets/ + - assets/themes/ \ No newline at end of file diff --git a/mobile/packages/ui/showcase/web/favicon.ico b/mobile/packages/ui/showcase/web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7ec34e9e53c53af721fe70e6177fd4f8e75625f0 GIT binary patch literal 15086 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 rx2V}n>sIfhp;D>8Y*wmoJJ=30I)9_=xAE{ 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 literal 0 HcmV?d00001 diff --git a/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png b/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..a7220554bced1f2d0702d9911829b9272716d509 GIT binary patch literal 13544 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 literal 0 HcmV?d00001 diff --git a/mobile/packages/ui/showcase/web/icons/apple-icon-180.png b/mobile/packages/ui/showcase/web/icons/apple-icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..4e642631a3ad61c030b0fca2bb1f2b92efd16d34 GIT binary patch literal 6358 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 Y2jUXJ7`x~Fp8x;=07*qoM6N<$g8y~;Bme*a literal 0 HcmV?d00001 diff --git a/mobile/packages/ui/showcase/web/index.html b/mobile/packages/ui/showcase/web/index.html new file mode 100644 index 0000000000..abf42ad1fd --- /dev/null +++ b/mobile/packages/ui/showcase/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + @immich/ui + + + + + + diff --git a/mobile/packages/ui/showcase/web/manifest.json b/mobile/packages/ui/showcase/web/manifest.json new file mode 100644 index 0000000000..25b44bd1ae --- /dev/null +++ b/mobile/packages/ui/showcase/web/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "@immich/ui Showcase", + "short_name": "@immich/ui", + "start_url": ".", + "display": "standalone", + "background_color": "#FCFCFD", + "theme_color": "#4250AF", + "description": "Immich UI component library showcase and documentation", + "orientation": "landscape", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}