From ce6e5fa5f0b9ad745b9229ad6c658c47b1838363 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:00:53 +0530 Subject: [PATCH] feat: mobile ui showcase (#25827) * feat: mobile ui showcase * remove showcase from main app * update fonts * update code to be loaded from asset * fix ci --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .github/workflows/static_analysis.yml | 8 + .../pages/dev/ui_showcase.page.dart | 112 ----- mobile/lib/routing/router.dart | 4 +- mobile/lib/routing/router.gr.dart | 16 - mobile/lib/widgets/common/immich_app_bar.dart | 6 - .../widgets/common/immich_sliver_app_bar.dart | 6 - mobile/packages/ui/.gitignore | 5 +- mobile/packages/ui/showcase/.gitignore | 11 + mobile/packages/ui/showcase/.metadata | 30 ++ .../ui/showcase/analysis_options.yaml | 1 + .../ui/showcase/assets/immich-text-dark.png | Bin 0 -> 36878 bytes .../ui/showcase/assets/immich-text-light.png | Bin 0 -> 36839 bytes .../ui/showcase/assets/immich_logo.png | Bin 0 -> 5198 bytes .../showcase/assets/themes/github_dark.json | 339 +++++++++++++++ .../packages/ui/showcase/lib/app_theme.dart | 96 +++++ .../packages/ui/showcase/lib/constants.dart | 16 + mobile/packages/ui/showcase/lib/main.dart | 55 +++ .../pages/components/close_button_page.dart | 41 ++ .../examples/html_text_bold_text.dart | 13 + .../components/examples/html_text_links.dart | 25 ++ .../examples/html_text_nested_tags.dart | 20 + .../lib/pages/components/form_page.dart | 79 ++++ .../lib/pages/components/html_text_page.dart | 40 ++ .../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 | 396 ++++++++++++++++++ .../ui/showcase/lib/pages/home_page.dart | 118 ++++++ 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 | 237 +++++++++++ .../ui/showcase/lib/widgets/page_title.dart | 17 + .../ui/showcase/lib/widgets/shell_layout.dart | 59 +++ .../lib/widgets/sidebar_navigation.dart | 117 ++++++ mobile/packages/ui/showcase/pubspec.lock | 393 +++++++++++++++++ mobile/packages/ui/showcase/pubspec.yaml | 47 +++ 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 ++ 44 files changed, 2764 insertions(+), 144 deletions(-) delete mode 100644 mobile/lib/presentation/pages/dev/ui_showcase.page.dart 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-text-dark.png create mode 100644 mobile/packages/ui/showcase/assets/immich-text-light.png 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/examples/html_text_bold_text.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart create mode 100644 mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.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/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index c0d53388c6..f6063fd343 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -69,6 +69,14 @@ jobs: - name: Install dependencies run: dart pub get + - name: Install dependencies for UI package + run: dart pub get + working-directory: ./mobile/packages/ui + + - name: Install dependencies for UI Showcase + run: dart pub get + working-directory: ./mobile/packages/ui/showcase + - name: Install DCM uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 with: diff --git a/mobile/lib/presentation/pages/dev/ui_showcase.page.dart b/mobile/lib/presentation/pages/dev/ui_showcase.page.dart deleted file mode 100644 index a07c7fc19c..0000000000 --- a/mobile/lib/presentation/pages/dev/ui_showcase.page.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_ui/immich_ui.dart'; - -List _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) { - final children = []; - - final items = [ - (variant: ImmichVariant.filled, title: "Filled Variant"), - (variant: ImmichVariant.ghost, title: "Ghost Variant"), - ]; - - for (final (:variant, :title) in items) { - children.add(Text(title)); - children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)])); - } - - return children; -} - -class _ComponentTitle extends StatelessWidget { - final String title; - - const _ComponentTitle(this.title); - - @override - Widget build(BuildContext context) { - return Text(title, style: context.textTheme.titleLarge); - } -} - -@RoutePage() -class ImmichUIShowcasePage extends StatelessWidget { - const ImmichUIShowcasePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Immich UI Showcase')), - body: Padding( - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Column( - spacing: 10, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _ComponentTitle("IconButton"), - ..._showcaseBuilder( - (variant, color) => - ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onPressed: () {}), - ), - const _ComponentTitle("CloseButton"), - ..._showcaseBuilder( - (variant, color) => ImmichCloseButton(color: color, variant: variant, onPressed: () {}), - ), - const _ComponentTitle("TextButton"), - - ImmichTextButton( - labelText: "Text Button", - onPressed: () {}, - variant: ImmichVariant.filled, - color: ImmichColor.primary, - ), - ImmichTextButton( - labelText: "Text Button", - onPressed: () {}, - variant: ImmichVariant.filled, - color: ImmichColor.primary, - loading: true, - ), - ImmichTextButton( - labelText: "Text Button", - onPressed: () {}, - variant: ImmichVariant.ghost, - color: ImmichColor.primary, - ), - ImmichTextButton( - labelText: "Text Button", - onPressed: () {}, - variant: ImmichVariant.ghost, - color: ImmichColor.primary, - loading: true, - ), - const _ComponentTitle("Form"), - ImmichForm( - onSubmit: () {}, - child: const Column( - spacing: 10, - children: [ImmichTextInput(label: "Title", hintText: "Enter a title")], - ), - ), - const _ComponentTitle("HtmlText"), - ImmichHtmlText( - 'This is an example of HTML text with bold and links.', - linkHandlers: { - 'link': () { - context.showSnackBar(const SnackBar(content: Text('Link tapped!'))); - }, - 'test-link': () { - context.showSnackBar(const SnackBar(content: Text('Test-link tapped!'))); - }, - }, - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 9468b105e5..32cdef8e77 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -78,9 +78,9 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/ui_showcase.page.dart'; import 'package:immich_mobile/presentation/pages/download_info.page.dart'; import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; @@ -88,7 +88,6 @@ import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart'; import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/drift_asset_troubleshoot.page.dart'; -import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; @@ -338,7 +337,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ImmichUIShowcaseRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: CleanupPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index b287d73114..486611bdfd 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1852,22 +1852,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [ImmichUIShowcasePage] -class ImmichUIShowcaseRoute extends PageRouteInfo { - const ImmichUIShowcaseRoute({List? children}) - : super(ImmichUIShowcaseRoute.name, initialChildren: children); - - static const String name = 'ImmichUIShowcaseRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ImmichUIShowcasePage(); - }, - ); -} - /// generated route for /// [LibraryPage] class LibraryRoute extends PageRouteInfo { diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index b3dc04236c..d7116d43b8 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -1,6 +1,5 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -153,11 +152,6 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { actions: [ if (actions != null) ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (kDebugMode || kProfileMode) - IconButton( - icon: const Icon(Icons.palette_rounded), - onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()), - ), if (isCasting) Padding( padding: const EdgeInsets.only(right: 12), diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 95622c1e5a..8e4b2326a8 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -1,6 +1,5 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -76,11 +75,6 @@ class ImmichSliverAppBar extends ConsumerWidget { const _SyncStatusIndicator(), if (actions != null) ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled) - IconButton( - icon: const Icon(Icons.palette_rounded), - onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()), - ), if (showUploadButton && !isReadonlyModeEnabled) const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()), const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()), diff --git a/mobile/packages/ui/.gitignore b/mobile/packages/ui/.gitignore index adf09d9f51..b84f47ac2c 100644 --- a/mobile/packages/ui/.gitignore +++ b/mobile/packages/ui/.gitignore @@ -9,4 +9,7 @@ ios/ .dart_tool/ .packages .flutter-plugins -.flutter-plugins-dependencies \ No newline at end of file +.flutter-plugins-dependencies + +# Fonts copied by build process +fonts/ \ No newline at end of file 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-text-dark.png b/mobile/packages/ui/showcase/assets/immich-text-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..215687af8f9aa89413a7a88935cbb2e82be731bf GIT binary patch literal 36878 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 J&Uu3e{~y~#t)&0} literal 0 HcmV?d00001 diff --git a/mobile/packages/ui/showcase/assets/immich-text-light.png b/mobile/packages/ui/showcase/assets/immich-text-light.png new file mode 100644 index 0000000000000000000000000000000000000000..478158d39c354a06182383e0e098d8583f470494 GIT binary patch literal 36839 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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..995bf3c91e --- /dev/null +++ b/mobile/packages/ui/showcase/lib/app_theme.dart @@ -0,0 +1,96 @@ +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, + fontFamily: 'GoogleSans', + 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, + fontFamily: 'GoogleSans', + 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/examples/html_text_bold_text.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_bold_text.dart new file mode 100644 index 0000000000..af4c87f40e --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_bold_text.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class HtmlTextBoldText extends StatelessWidget { + const HtmlTextBoldText({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichHtmlText( + 'This is bold text and strong text.', + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart new file mode 100644 index 0000000000..a764d7173e --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class HtmlTextLinks extends StatelessWidget { + const HtmlTextLinks({super.key}); + + @override + Widget build(BuildContext context) { + return 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!'))); + }, + }, + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.dart new file mode 100644 index 0000000000..836d949b66 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class HtmlTextNestedTags extends StatelessWidget { + const HtmlTextNestedTags({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichHtmlText( + 'You can combine bold and links together.', + linkHandlers: { + 'link': () { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Nested link clicked!'))); + }, + }, + ); + } +} 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..64dbc70597 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:showcase/pages/components/examples/html_text_bold_text.dart'; +import 'package:showcase/pages/components/examples/html_text_links.dart'; +import 'package:showcase/pages/components/examples/html_text_nested_tags.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: const HtmlTextBoldText(), + code: 'html_text_bold_text.dart', + ), + ExampleCard( + title: 'Links', + preview: const HtmlTextLinks(), + code: 'html_text_links.dart', + ), + ExampleCard( + title: 'Nested Tags', + preview: const HtmlTextNestedTags(), + code: 'html_text_nested_tags.dart', + ), + ], + ), + ); + } +} 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..17de02d80a --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart @@ -0,0 +1,396 @@ +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: [ + const 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), + ], + ), + ), + const 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), + ], + ), + ), + const 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)), + ], + ), + ), + const 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), + ], + ), + ), + const 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), + ], + ), + ), + const 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: 'GoogleSansCode'), + ), + ), + 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: 'GoogleSansCode'), + ), + ), + 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: 'GoogleSansCode', 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..de7af6c26b --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/home_page.dart @@ -0,0 +1,118 @@ +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: const BorderRadius.all(Radius.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..fea561afb6 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/example_card.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.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; + String? code; + + @override + void initState() { + super.initState(); + if (widget.code != null) { + rootBundle + .loadString('lib/pages/components/examples/${widget.code!}') + .then((value) { + setState(() { + code = value; + }); + }); + } + } + + @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 (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: 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: const BorderRadius.all(Radius.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: const BorderRadius.all(Radius.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: 'GoogleSansCode', + fontSize: 13, + color: lineNumberColor, + height: 1.5, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + SelectableText.rich( + _codeHighlighter.highlight(code), + style: const TextStyle( + fontFamily: 'GoogleSansCode', + 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..8bcb687e75 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart @@ -0,0 +1,59 @@ +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), + Image.asset( + isDark + ? 'assets/immich-text-dark.png' + : 'assets/immich-text-light.png', + height: 24, + filterQuality: FilterQuality.none, + isAntiAlias: true, + ), + ], + ), + 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..e45ce07e66 --- /dev/null +++ b/mobile/packages/ui/showcase/pubspec.yaml @@ -0,0 +1,47 @@ +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/ + - lib/pages/components/examples/ + + fonts: + - family: GoogleSans + fonts: + - asset: ../../../fonts/GoogleSans/GoogleSans-Regular.ttf + - asset: ../../../fonts/GoogleSans/GoogleSans-Italic.ttf + style: italic + - asset: ../../../fonts/GoogleSans/GoogleSans-Medium.ttf + weight: 500 + - asset: ../../../fonts/GoogleSans/GoogleSans-SemiBold.ttf + weight: 600 + - asset: ../../../fonts/GoogleSans/GoogleSans-Bold.ttf + weight: 700 + - family: GoogleSansCode + fonts: + - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-Regular.ttf + - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-Medium.ttf + weight: 500 + - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf + weight: 600 \ 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" + } + ] +}