WIP: feat(ui): Migrate to new UI implementation
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user