WIP: feat(ui): Migrate to new UI implementation

This commit is contained in:
2026-06-22 06:47:16 +04:00
parent 873aa8d7b5
commit bb4334f68d
109 changed files with 13499 additions and 1257 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"flutter": "stable"
}
+48
View File
@@ -0,0 +1,48 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# FVM Version Cache
.fvm/
+33
View File
@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "44a626f4f0027bc38a46dc68aed5964b05a83c18"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
- platform: macos
create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
- platform: windows
create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+3
View File
@@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".fvm/versions/stable"
}
+11
View File
@@ -0,0 +1,11 @@
# MoonWell Widgetbook
Standalone development catalog for MoonWell Launcher components.
```powershell
fvm flutter pub get
fvm dart run build_runner build
fvm flutter run -d windows
```
Use cases must follow [`../docs/widgetbook.md`](../docs/widgetbook.md).
+1
View File
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+65
View File
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'main.directories.g.dart';
void main() => runApp(const MoonWellWidgetbook());
@widgetbook.App()
class MoonWellWidgetbook extends StatelessWidget {
const MoonWellWidgetbook({super.key});
@override
Widget build(BuildContext context) {
return Widgetbook.material(
initialRoute: '/?path=compositions/mwlaunchershell/ready',
directories: directories,
addons: [
MaterialThemeAddon(
themes: [
WidgetbookTheme(
name: 'Forest',
data: MoonWellTheme.create(
MoonWellThemeVariant.forest,
fontPackage: 'moonwell_launcher',
),
),
WidgetbookTheme(
name: 'Temple',
data: MoonWellTheme.create(
MoonWellThemeVariant.temple,
fontPackage: 'moonwell_launcher',
),
),
],
),
ViewportAddon(const [
ViewportData(
name: 'Minimum desktop · 1024×640',
width: 1024,
height: 640,
pixelRatio: 1,
platform: TargetPlatform.windows,
),
ViewportData(
name: 'Target desktop · 1280×720',
width: 1280,
height: 720,
pixelRatio: 1,
platform: TargetPlatform.windows,
),
ViewportData(
name: 'Large desktop · 1600×900',
width: 1600,
height: 900,
pixelRatio: 1,
platform: TargetPlatform.windows,
),
]),
TextScaleAddon(min: 1, max: 2, divisions: 4, initialScale: 1),
],
);
}
}
+413
View File
@@ -0,0 +1,413 @@
// dart format width=80
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_import, prefer_relative_imports, directives_ordering
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AppGenerator
// **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:widgetbook/widgetbook.dart' as _widgetbook;
import 'package:widgetbook_workspace/stories/authentication_stories.dart'
as _widgetbook_workspace_stories_authentication_stories;
import 'package:widgetbook_workspace/stories/foundations_stories.dart'
as _widgetbook_workspace_stories_foundations_stories;
import 'package:widgetbook_workspace/stories/launcher_stories.dart'
as _widgetbook_workspace_stories_launcher_stories;
import 'package:widgetbook_workspace/stories/primitives_stories.dart'
as _widgetbook_workspace_stories_primitives_stories;
import 'package:widgetbook_workspace/stories/shell_stories.dart'
as _widgetbook_workspace_stories_shell_stories;
import 'package:widgetbook_workspace/stories/window_chrome_stories.dart'
as _widgetbook_workspace_stories_window_chrome_stories;
final directories = <_widgetbook.WidgetbookNode>[
_widgetbook.WidgetbookCategory(
name: 'Authentication',
children: [
_widgetbook.WidgetbookComponent(
name: 'MwAuthenticationForm',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'api_error',
builder: _widgetbook_workspace_stories_authentication_stories
.buildMwAuthenticationFormApiErrorUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_authentication_stories
.buildMwAuthenticationFormUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'loading',
builder: _widgetbook_workspace_stories_authentication_stories
.buildMwAuthenticationFormLoadingUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'long_russian_copy',
builder: _widgetbook_workspace_stories_authentication_stories
.buildMwAuthenticationFormLongRussianCopyUseCase,
),
],
),
],
),
_widgetbook.WidgetbookCategory(
name: 'Compositions',
children: [
_widgetbook.WidgetbookComponent(
name: 'MwLauncherShell',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'downloading',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLauncherShellDownloadingUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'empty_news',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLauncherShellEmptyNewsUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'failure',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLauncherShellFailureUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'paused',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLauncherShellPausedUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'ready',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLauncherShellReadyUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'verifying',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLauncherShellVerifyingUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwLoginShell',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'api_error',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLoginShellApiErrorUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLoginShellUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'restored_session_loading',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwLoginShellRestoredSessionLoadingUseCase,
),
],
),
],
),
_widgetbook.WidgetbookCategory(
name: 'Foundations',
children: [
_widgetbook.WidgetbookComponent(
name: 'ArtworkSlot',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_foundations_stories
.buildArtworkSlotUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'FoundationsGallery',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_foundations_stories
.buildFoundationsGalleryUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwArtworkBackground',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwArtworkBackgroundUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwBrandMark',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_shell_stories
.buildMwBrandMarkUseCase,
),
],
),
],
),
_widgetbook.WidgetbookCategory(
name: 'Launcher',
children: [
_widgetbook.WidgetbookComponent(
name: 'MwAccountAffordance',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwAccountAffordanceUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwGameRail',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'compact',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwGameRailCompactUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'full',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwGameRailFullUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwGameTile',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwGameTileInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwLauncherStatusBar',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'metadata',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwLauncherStatusBarMetadataUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwNewsItem',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'image_failure',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwNewsItemImageFailureUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'text_only',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwNewsItemTextOnlyUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwNewsList',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'empty',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwNewsListEmptyUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'failure',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwNewsListFailureUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'loading',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwNewsListLoadingUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'populated',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwNewsListPopulatedUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwSettingsPanel',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwSettingsPanelInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwSyncActionArea',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_launcher_stories
.buildMwSyncActionAreaInteractiveUseCase,
),
],
),
],
),
_widgetbook.WidgetbookCategory(
name: 'Primitives',
children: [
_widgetbook.WidgetbookComponent(
name: 'MwBadge',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwBadgeInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwButton',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'disabled',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwButtonDisabledUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwButtonInteractiveUseCase,
),
_widgetbook.WidgetbookUseCase(
name: 'loading',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwButtonLoadingUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwCheckbox',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwCheckboxInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwDisabledFeature',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'default',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwDisabledFeatureUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwIconButton',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwIconButtonInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwPanel',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwPanelInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwProgressBar',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwProgressBarInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwStateView',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwStateViewInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwStatusText',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwStatusTextInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwTabs',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwTabsInteractiveUseCase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'MwTextField',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_primitives_stories
.buildMwTextFieldInteractiveUseCase,
),
],
),
],
),
_widgetbook.WidgetbookCategory(
name: 'Window Chrome',
children: [
_widgetbook.WidgetbookComponent(
name: 'MwWindowChrome',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'interactive',
builder: _widgetbook_workspace_stories_window_chrome_stories
.buildMwWindowChromeInteractiveUseCase,
),
],
),
],
),
];
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_authentication.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
@widgetbook.UseCase(
name: 'default',
type: MwAuthenticationForm,
path: '[Authentication]',
)
Widget buildMwAuthenticationFormUseCase(BuildContext context) {
return SizedBox(
width: 420,
child: MwAuthenticationForm(
usernameController: TextEditingController(
text: context.knobs.string(label: 'username', initialValue: ''),
),
passwordController: TextEditingController(
text: context.knobs.string(label: 'password', initialValue: ''),
),
onSubmit: () => debugPrint('Authentication form submitted'),
),
);
}
@widgetbook.UseCase(
name: 'loading',
type: MwAuthenticationForm,
path: '[Authentication]',
)
Widget buildMwAuthenticationFormLoadingUseCase(BuildContext context) {
return SizedBox(
width: 420,
child: MwAuthenticationForm(
usernameController: TextEditingController(text: 'moonwalker'),
passwordController: TextEditingController(text: 'password'),
loading: true,
onSubmit: () => debugPrint('Loading authentication form submitted'),
),
);
}
@widgetbook.UseCase(
name: 'api_error',
type: MwAuthenticationForm,
path: '[Authentication]',
)
Widget buildMwAuthenticationFormApiErrorUseCase(BuildContext context) {
return SizedBox(
width: 420,
child: MwAuthenticationForm(
usernameController: TextEditingController(text: 'moonwalker'),
passwordController: TextEditingController(),
error: context.knobs.string(
label: 'error',
initialValue: 'Неверный логин или пароль.',
),
onSubmit: () => debugPrint('Errored authentication form submitted'),
),
);
}
@widgetbook.UseCase(
name: 'long_russian_copy',
type: MwAuthenticationForm,
path: '[Authentication]',
)
Widget buildMwAuthenticationFormLongRussianCopyUseCase(BuildContext context) {
return SizedBox(
width: 420,
child: MwAuthenticationForm(
usernameController: TextEditingController(),
passwordController: TextEditingController(),
error:
'Не удалось связаться с сервером авторизации. Проверьте подключение '
'к интернету и повторите попытку через несколько минут.',
onSubmit: () => debugPrint('Long-copy authentication form submitted'),
),
);
}
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_primitives.dart';
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
class FoundationsGallery extends StatelessWidget {
const FoundationsGallery({super.key});
@override
Widget build(BuildContext context) {
final tokens = MoonWellDesignTokens.of(context);
final colors = <String, Color>{
'Background': tokens.background,
'Surface low': tokens.surfaceLow,
'Surface': tokens.surface,
'Surface high': tokens.surfaceHigh,
'Accent': tokens.accent,
'Accent muted': tokens.accentMuted,
'Success': tokens.success,
'Warning': tokens.warning,
'Error': Theme.of(context).colorScheme.error,
};
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Semantic colors',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
for (final entry in colors.entries)
SizedBox(
width: 144,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 72,
decoration: BoxDecoration(
color: entry.value,
border: Border.all(color: Colors.white24),
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 6),
Text(entry.key),
MwStatusText(
text:
'#${entry.value.toARGB32().toRadixString(16).substring(2).toUpperCase()}',
monospace: true,
),
],
),
),
],
),
const SizedBox(height: 32),
Text('Typography', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Text(
'MoonWell',
style: TextStyle(
fontFamily: tokens.brandFontFamily,
fontSize: 32,
letterSpacing: 2,
),
),
Text(
'Лунный колодец зовёт героев Нордскола',
style: Theme.of(context).textTheme.headlineMedium,
),
const Text('Интерфейс, формы, навигация и рабочие статусы'),
MwStatusText(
text: 'build a93fd1c2 · 12.4 MB/s · 00:01:42',
monospace: true,
),
const SizedBox(height: 32),
Text(
'Surfaces & radii',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
const Row(
children: [
Expanded(
child: MwPanel(level: MwPanelLevel.low, child: Text('Low')),
),
SizedBox(width: 12),
Expanded(child: MwPanel(child: Text('Regular'))),
SizedBox(width: 12),
Expanded(
child: MwPanel(level: MwPanelLevel.high, child: Text('High')),
),
],
),
],
),
);
}
}
@widgetbook.UseCase(
name: 'default',
type: FoundationsGallery,
path: '[Foundations]',
)
Widget buildFoundationsGalleryUseCase(BuildContext context) {
return const FoundationsGallery();
}
class ArtworkSlot extends StatelessWidget {
const ArtworkSlot({super.key});
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
Image.asset(
'assets/background.png',
package: 'moonwell_launcher',
fit: BoxFit.cover,
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0x22000000), Color(0xEE000000)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
const Center(child: MwBrandMark(width: 240)),
],
);
}
}
@widgetbook.UseCase(name: 'default', type: ArtworkSlot, path: '[Foundations]')
Widget buildArtworkSlotUseCase(BuildContext context) {
return const ArtworkSlot();
}
@@ -0,0 +1,230 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart';
import 'package:moonwell_launcher/core/moonwell_theme_variant.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
@widgetbook.UseCase(name: 'interactive', type: MwGameTile, path: '[Launcher]')
Widget buildMwGameTileInteractiveUseCase(BuildContext context) {
return SizedBox(
width: context.knobs.double.slider(
label: 'width',
initialValue: 240,
min: 72,
max: 360,
divisions: 36,
),
child: MwGameTile(
title: context.knobs.string(label: 'title', initialValue: 'MoonWell'),
subtitle: context.knobs.stringOrNull(
label: 'subtitle',
initialValue: 'Wrath of the Lich King',
),
icon: Icons.nights_stay_outlined,
selected: context.knobs.boolean(label: 'selected', initialValue: true),
enabled: context.knobs.boolean(label: 'enabled', initialValue: true),
compact: context.knobs.boolean(label: 'compact', initialValue: false),
onPressed: () => debugPrint('Game tile selected'),
),
);
}
@widgetbook.UseCase(name: 'full', type: MwGameRail, path: '[Launcher]')
Widget buildMwGameRailFullUseCase(BuildContext context) {
return const SizedBox(width: 240, height: 560, child: MwGameRail());
}
@widgetbook.UseCase(name: 'compact', type: MwGameRail, path: '[Launcher]')
Widget buildMwGameRailCompactUseCase(BuildContext context) {
return const SizedBox(
width: 72,
height: 560,
child: MwGameRail(compact: true),
);
}
@widgetbook.UseCase(name: 'text_only', type: MwNewsItem, path: '[Launcher]')
Widget buildMwNewsItemTextOnlyUseCase(BuildContext context) {
return SizedBox(
width: 620,
child: MwNewsItem(
item: MwNewsItemData(
title: context.knobs.string(
label: 'title',
initialValue: 'Добро пожаловать в MoonWell',
),
body: context.knobs.string(
label: 'body',
initialValue: 'Приключение в Нордсколе начинается здесь.',
),
createdAt: DateTime(2026, 6, 22),
),
),
);
}
@widgetbook.UseCase(name: 'image_failure', type: MwNewsItem, path: '[Launcher]')
Widget buildMwNewsItemImageFailureUseCase(BuildContext context) {
return const SizedBox(
width: 620,
child: MwNewsItem(
item: MwNewsItemData(
title: 'Обновление игрового мира',
body: 'Изображение недоступно, но публикация остаётся читаемой.',
imageUrl: 'https://invalid.moonwell.local/news.jpg',
),
),
);
}
@widgetbook.UseCase(name: 'loading', type: MwNewsList, path: '[Launcher]')
Widget buildMwNewsListLoadingUseCase(BuildContext context) {
return const MwNewsList(state: MwNewsListState.loading);
}
@widgetbook.UseCase(name: 'empty', type: MwNewsList, path: '[Launcher]')
Widget buildMwNewsListEmptyUseCase(BuildContext context) {
return const MwNewsList(state: MwNewsListState.empty);
}
@widgetbook.UseCase(name: 'failure', type: MwNewsList, path: '[Launcher]')
Widget buildMwNewsListFailureUseCase(BuildContext context) {
return MwNewsList(
state: MwNewsListState.failure,
error: context.knobs.string(
label: 'error',
initialValue: 'Сервер новостей временно недоступен.',
),
onRetry: () => debugPrint('News reload requested'),
);
}
@widgetbook.UseCase(name: 'populated', type: MwNewsList, path: '[Launcher]')
Widget buildMwNewsListPopulatedUseCase(BuildContext context) {
final count = context.knobs.int.slider(
label: 'itemCount',
initialValue: 8,
min: 1,
max: 50,
divisions: 49,
);
return MwNewsList(
state: MwNewsListState.populated,
items: List.generate(
count,
(index) => MwNewsItemData(
title: 'Новость ${index + 1}',
body:
'Реалистичный текст публикации о событиях и обновлениях MoonWell.',
createdAt: DateTime(2026, 6, 22).subtract(Duration(days: index)),
),
),
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwSyncActionArea,
path: '[Launcher]',
)
Widget buildMwSyncActionAreaInteractiveUseCase(BuildContext context) {
final state = context.knobs.object.dropdown<MwSyncPresentation>(
label: 'state',
initialOption: MwSyncPresentation.downloading,
options: MwSyncPresentation.values,
labelBuilder: (value) => value.name,
);
return SizedBox(
width: 420,
child: MwSyncActionArea(
state: state,
status: context.knobs.string(
label: 'status',
initialValue: 'Загружаем обновление клиента…',
),
progress: context.knobs.double.slider(
label: 'progress',
initialValue: .46,
min: 0,
max: 1,
divisions: 100,
),
currentPath: context.knobs.stringOrNull(
label: 'currentPath',
initialValue: r'Data\ruRU\patch-ruRU.MPQ',
),
error: state == MwSyncPresentation.failure
? context.knobs.string(
label: 'error',
initialValue: 'Проверка SHA-256 не пройдена.',
)
: null,
onPrimary: () => debugPrint('Primary sync action requested: $state'),
onPause: state == MwSyncPresentation.downloading
? () => debugPrint('Sync pause requested')
: null,
),
);
}
@widgetbook.UseCase(
name: 'metadata',
type: MwLauncherStatusBar,
path: '[Launcher]',
)
Widget buildMwLauncherStatusBarMetadataUseCase(BuildContext context) {
return MwLauncherStatusBar(
status: context.knobs.string(
label: 'status',
initialValue: 'Загрузка игровых файлов',
),
fileCount: 'Файлы 124/480',
speed: '12.4 MB/s',
eta: 'ETA 00:01:42',
buildHash: 'a93fd1c2',
);
}
@widgetbook.UseCase(
name: 'default',
type: MwAccountAffordance,
path: '[Launcher]',
)
Widget buildMwAccountAffordanceUseCase(BuildContext context) {
return MwAccountAffordance(
onSettings: () => debugPrint('Settings requested'),
onLogout: () => debugPrint('Logout requested'),
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwSettingsPanel,
path: '[Launcher]',
)
Widget buildMwSettingsPanelInteractiveUseCase(BuildContext context) {
final variant = context.knobs.object.dropdown<MoonWellThemeVariant>(
label: 'themeVariant',
initialOption: MoonWellThemeVariant.forest,
options: MoonWellThemeVariant.values,
labelBuilder: (value) => value.name,
);
return SizedBox(
width: 420,
child: MwSettingsPanel(
installationPath: context.knobs.stringOrNull(
label: 'installationPath',
initialValue: r'C:\Games\MoonWell',
),
themeVariant: variant,
syncActive: context.knobs.boolean(
label: 'syncActive',
initialValue: false,
),
onChooseDirectory: () => debugPrint('Directory selection requested'),
onThemeChanged: (value) => debugPrint('Theme changed: ${value.name}'),
onVerify: () => debugPrint('Manual verification requested'),
onLogout: () => debugPrint('Logout requested'),
),
);
}
@@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_primitives.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
@widgetbook.UseCase(name: 'interactive', type: MwButton, path: '[Primitives]')
Widget buildMwButtonInteractiveUseCase(BuildContext context) {
final variant = context.knobs.object.dropdown<MwButtonVariant>(
label: 'variant',
initialOption: MwButtonVariant.primary,
options: MwButtonVariant.values,
labelBuilder: (value) => value.name,
);
return MwButton(
label: context.knobs.string(label: 'label', initialValue: 'Играть'),
icon: context.knobs.boolean(label: 'showIcon', initialValue: true)
? Icons.play_arrow
: null,
variant: variant,
loading: context.knobs.boolean(label: 'loading', initialValue: false),
expand: context.knobs.boolean(label: 'expand', initialValue: false),
onPressed: context.knobs.boolean(label: 'enabled', initialValue: true)
? () => debugPrint('MwButton pressed')
: null,
);
}
@widgetbook.UseCase(name: 'loading', type: MwButton, path: '[Primitives]')
Widget buildMwButtonLoadingUseCase(BuildContext context) {
return MwButton(
label: context.knobs.string(label: 'label', initialValue: 'Загрузка'),
loading: true,
onPressed: () => debugPrint('Loading MwButton pressed'),
);
}
@widgetbook.UseCase(name: 'disabled', type: MwButton, path: '[Primitives]')
Widget buildMwButtonDisabledUseCase(BuildContext context) {
return MwButton(
label: context.knobs.string(label: 'label', initialValue: 'Недоступно'),
onPressed: null,
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwIconButton,
path: '[Primitives]',
)
Widget buildMwIconButtonInteractiveUseCase(BuildContext context) {
return MwIconButton(
icon: Icons.settings,
tooltip: context.knobs.string(label: 'tooltip', initialValue: 'Настройки'),
selected: context.knobs.boolean(label: 'selected', initialValue: false),
destructive: context.knobs.boolean(
label: 'destructive',
initialValue: false,
),
onPressed: context.knobs.boolean(label: 'enabled', initialValue: true)
? () => debugPrint('MwIconButton pressed')
: null,
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwTextField,
path: '[Primitives]',
)
Widget buildMwTextFieldInteractiveUseCase(BuildContext context) {
return SizedBox(
width: 380,
child: MwTextField(
label: context.knobs.string(label: 'label', initialValue: 'Логин'),
hint: context.knobs.stringOrNull(
label: 'hint',
initialValue: 'Введите логин',
),
errorText: context.knobs.stringOrNull(
label: 'errorText',
initialValue: null,
),
enabled: context.knobs.boolean(label: 'enabled', initialValue: true),
obscureText: context.knobs.boolean(
label: 'obscureText',
initialValue: false,
),
prefixIcon: context.knobs.boolean(label: 'showIcon', initialValue: true)
? Icons.person_outline
: null,
onSubmitted: (value) => debugPrint('MwTextField submitted: $value'),
),
);
}
@widgetbook.UseCase(name: 'interactive', type: MwCheckbox, path: '[Primitives]')
Widget buildMwCheckboxInteractiveUseCase(BuildContext context) {
return MwCheckbox(
label: context.knobs.string(label: 'label', initialValue: 'Запомнить меня'),
value: context.knobs.boolean(label: 'value', initialValue: false),
onChanged: context.knobs.boolean(label: 'enabled', initialValue: true)
? (value) => debugPrint('MwCheckbox changed: $value')
: null,
);
}
@widgetbook.UseCase(name: 'interactive', type: MwTabs, path: '[Primitives]')
Widget buildMwTabsInteractiveUseCase(BuildContext context) {
return MwTabs(
labels: const ['Новости', 'Обновления', 'Очень длинный раздел'],
selectedIndex: context.knobs.int.slider(
label: 'selectedIndex',
initialValue: 0,
min: 0,
max: 2,
divisions: 2,
),
disabledIndices:
context.knobs.boolean(label: 'disableFutureTabs', initialValue: true)
? const {1, 2}
: const {},
onSelected: (index) => debugPrint('MwTabs selected: $index'),
);
}
@widgetbook.UseCase(name: 'interactive', type: MwBadge, path: '[Primitives]')
Widget buildMwBadgeInteractiveUseCase(BuildContext context) {
return MwBadge(
label: context.knobs.string(label: 'label', initialValue: 'Готово'),
tone: context.knobs.object.dropdown<MwBadgeTone>(
label: 'tone',
initialOption: MwBadgeTone.success,
options: MwBadgeTone.values,
labelBuilder: (value) => value.name,
),
);
}
@widgetbook.UseCase(name: 'interactive', type: MwPanel, path: '[Primitives]')
Widget buildMwPanelInteractiveUseCase(BuildContext context) {
return SizedBox(
width: 420,
child: MwPanel(
level: context.knobs.object.dropdown<MwPanelLevel>(
label: 'level',
initialOption: MwPanelLevel.regular,
options: MwPanelLevel.values,
labelBuilder: (value) => value.name,
),
child: Text(
context.knobs.string(
label: 'content',
initialValue: 'Содержимое панели MoonWell',
),
),
),
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwProgressBar,
path: '[Primitives]',
)
Widget buildMwProgressBarInteractiveUseCase(BuildContext context) {
return SizedBox(
width: 440,
child: MwProgressBar(
value: context.knobs.boolean(label: 'indeterminate', initialValue: false)
? null
: context.knobs.double.slider(
label: 'value',
initialValue: .46,
min: 0,
max: 1,
divisions: 100,
),
label: context.knobs.stringOrNull(
label: 'label',
initialValue: 'Загрузка файлов',
),
state: context.knobs.object.dropdown<MwProgressState>(
label: 'state',
initialOption: MwProgressState.active,
options: MwProgressState.values,
labelBuilder: (value) => value.name,
),
),
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwStatusText,
path: '[Primitives]',
)
Widget buildMwStatusTextInteractiveUseCase(BuildContext context) {
return MwStatusText(
text: context.knobs.string(
label: 'text',
initialValue: 'Проверяем локальные файлы…',
),
tone: context.knobs.object.dropdown<MwStatusTone>(
label: 'tone',
initialOption: MwStatusTone.operational,
options: MwStatusTone.values,
labelBuilder: (value) => value.name,
),
monospace: context.knobs.boolean(label: 'monospace', initialValue: false),
);
}
@widgetbook.UseCase(
name: 'interactive',
type: MwStateView,
path: '[Primitives]',
)
Widget buildMwStateViewInteractiveUseCase(BuildContext context) {
final kind = context.knobs.object.dropdown<MwStateKind>(
label: 'kind',
initialOption: MwStateKind.empty,
options: MwStateKind.values,
labelBuilder: (value) => value.name,
);
return MwStateView(
kind: kind,
title: context.knobs.string(
label: 'title',
initialValue: 'Новостей пока нет',
),
description: context.knobs.stringOrNull(
label: 'description',
initialValue: 'Новые публикации появятся здесь.',
),
onRetry: kind == MwStateKind.error
? () => debugPrint('MwStateView retry pressed')
: null,
);
}
@widgetbook.UseCase(
name: 'default',
type: MwDisabledFeature,
path: '[Primitives]',
)
Widget buildMwDisabledFeatureUseCase(BuildContext context) {
return MwDisabledFeature(
message: context.knobs.string(
label: 'message',
initialValue: 'Регистрация появится позже',
),
child: const MwButton(
label: 'Регистрация · скоро',
onPressed: null,
variant: MwButtonVariant.secondary,
),
);
}
+246
View File
@@ -0,0 +1,246 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_authentication.dart';
import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart';
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
VoidCallback _windowCallback(String action) {
return () => debugPrint('Window action requested: $action');
}
@widgetbook.UseCase(name: 'default', type: MwBrandMark, path: '[Foundations]')
Widget buildMwBrandMarkUseCase(BuildContext context) {
return MwBrandMark(
width: context.knobs.double.slider(
label: 'width',
initialValue: 220,
min: 96,
max: 360,
divisions: 33,
),
);
}
@widgetbook.UseCase(
name: 'default',
type: MwArtworkBackground,
path: '[Foundations]',
)
Widget buildMwArtworkBackgroundUseCase(BuildContext context) {
return const MwArtworkBackground(
assetPackage: 'moonwell_launcher',
child: Center(child: Text('Safe content area')),
);
}
@widgetbook.UseCase(name: 'default', type: MwLoginShell, path: '[Compositions]')
Widget buildMwLoginShellUseCase(BuildContext context) {
return MwLoginShell(
assetPackage: 'moonwell_launcher',
form: MwAuthenticationForm(
usernameController: TextEditingController(),
passwordController: TextEditingController(),
onSubmit: () => debugPrint('Login submitted'),
),
onDrag: _windowCallback('drag'),
onDoubleTap: _windowCallback('double_tap'),
onMinimize: _windowCallback('minimize'),
onMaximizeRestore: _windowCallback('maximize_restore'),
onClose: _windowCallback('close'),
);
}
@widgetbook.UseCase(
name: 'api_error',
type: MwLoginShell,
path: '[Compositions]',
)
Widget buildMwLoginShellApiErrorUseCase(BuildContext context) {
return MwLoginShell(
assetPackage: 'moonwell_launcher',
form: MwAuthenticationForm(
usernameController: TextEditingController(text: 'moonwalker'),
passwordController: TextEditingController(),
error: context.knobs.string(
label: 'error',
initialValue: 'Неверный логин или пароль.',
),
onSubmit: () => debugPrint('Login submitted after API error'),
),
onDrag: _windowCallback('drag'),
onDoubleTap: _windowCallback('double_tap'),
onMinimize: _windowCallback('minimize'),
onMaximizeRestore: _windowCallback('maximize_restore'),
onClose: _windowCallback('close'),
);
}
@widgetbook.UseCase(
name: 'restored_session_loading',
type: MwLoginShell,
path: '[Compositions]',
)
Widget buildMwLoginShellRestoredSessionLoadingUseCase(BuildContext context) {
return MwLoginShell(
assetPackage: 'moonwell_launcher',
form: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Восстанавливаем защищённую сессию…'),
],
),
),
onDrag: _windowCallback('drag'),
onDoubleTap: _windowCallback('double_tap'),
onMinimize: _windowCallback('minimize'),
onMaximizeRestore: _windowCallback('maximize_restore'),
onClose: _windowCallback('close'),
);
}
MwLauncherShell _launcherShell({
required MwNewsListState newsState,
required MwSyncPresentation syncState,
required String status,
double? progress,
String? error,
}) {
return MwLauncherShell(
assetPackage: 'moonwell_launcher',
news: MwNewsList(
state: newsState,
error: newsState == MwNewsListState.failure
? 'Сервер новостей временно недоступен.'
: null,
items: newsState == MwNewsListState.populated
? List.generate(
8,
(index) => MwNewsItemData(
title: 'Новость ${index + 1}',
body: 'События игрового мира и важные обновления MoonWell.',
createdAt: DateTime(2026, 6, 22 - index),
),
)
: const [],
onRetry: () => debugPrint('News reload requested'),
),
syncActions: MwSyncActionArea(
state: syncState,
status: status,
progress: progress,
currentPath: syncState == MwSyncPresentation.downloading
? r'Data\ruRU\patch-ruRU.MPQ'
: null,
error: error,
onPrimary: () => debugPrint('Primary sync action requested'),
onPause: syncState == MwSyncPresentation.downloading
? () => debugPrint('Sync pause requested')
: null,
),
statusBar: const MwLauncherStatusBar(
status: 'MoonWell Launcher · локальная установка',
fileCount: '124/480',
buildHash: 'a93fd1c2',
),
account: MwAccountAffordance(
onSettings: () => debugPrint('Settings requested'),
onLogout: () => debugPrint('Logout requested'),
),
onDrag: _windowCallback('drag'),
onDoubleTap: _windowCallback('double_tap'),
onMinimize: _windowCallback('minimize'),
onMaximizeRestore: _windowCallback('maximize_restore'),
onClose: _windowCallback('close'),
);
}
@widgetbook.UseCase(
name: 'ready',
type: MwLauncherShell,
path: '[Compositions]',
)
Widget buildMwLauncherShellReadyUseCase(BuildContext context) {
return _launcherShell(
newsState: MwNewsListState.populated,
syncState: MwSyncPresentation.ready,
status: 'Клиент готов к запуску.',
);
}
@widgetbook.UseCase(
name: 'downloading',
type: MwLauncherShell,
path: '[Compositions]',
)
Widget buildMwLauncherShellDownloadingUseCase(BuildContext context) {
return _launcherShell(
newsState: MwNewsListState.populated,
syncState: MwSyncPresentation.downloading,
status: 'Загружаем обновление клиента…',
progress: context.knobs.double.slider(
label: 'progress',
initialValue: .46,
min: 0,
max: 1,
divisions: 100,
),
);
}
@widgetbook.UseCase(
name: 'paused',
type: MwLauncherShell,
path: '[Compositions]',
)
Widget buildMwLauncherShellPausedUseCase(BuildContext context) {
return _launcherShell(
newsState: MwNewsListState.populated,
syncState: MwSyncPresentation.paused,
status: 'Обновление приостановлено.',
progress: .46,
);
}
@widgetbook.UseCase(
name: 'verifying',
type: MwLauncherShell,
path: '[Compositions]',
)
Widget buildMwLauncherShellVerifyingUseCase(BuildContext context) {
return _launcherShell(
newsState: MwNewsListState.populated,
syncState: MwSyncPresentation.verifying,
status: 'Проверяем целостность установки…',
);
}
@widgetbook.UseCase(
name: 'failure',
type: MwLauncherShell,
path: '[Compositions]',
)
Widget buildMwLauncherShellFailureUseCase(BuildContext context) {
return _launcherShell(
newsState: MwNewsListState.failure,
syncState: MwSyncPresentation.failure,
status: 'Не удалось завершить обновление.',
error: 'Проверка SHA-256 не пройдена.',
);
}
@widgetbook.UseCase(
name: 'empty_news',
type: MwLauncherShell,
path: '[Compositions]',
)
Widget buildMwLauncherShellEmptyNewsUseCase(BuildContext context) {
return _launcherShell(
newsState: MwNewsListState.empty,
syncState: MwSyncPresentation.ready,
status: 'Клиент готов к запуску.',
);
}
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_window_chrome.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
@widgetbook.UseCase(
name: 'interactive',
type: MwWindowChrome,
path: '[Window Chrome]',
)
Widget buildMwWindowChromeInteractiveUseCase(BuildContext context) {
return MwWindowChrome(
onDrag: () => debugPrint('Window drag requested'),
onDoubleTap: () => debugPrint('Window maximize/restore requested'),
onMinimize: () => debugPrint('Window minimize requested'),
onMaximizeRestore: () => debugPrint('Window maximize/restore requested'),
onClose: () => debugPrint('Window close requested'),
showMaximize: context.knobs.boolean(label: 'Show maximize button'),
);
}
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,24 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import auto_updater_macos
import file_picker
import flutter_secure_storage_darwin
import screen_retriever_macos
import shared_preferences_foundation
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}
+42
View File
@@ -0,0 +1,42 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = F