Prepare product layout and reusable Flutter theme

Relocate the existing Astro website and preserve its assets and hosting configuration. Add the consumer/backend scaffold and themed Flutter package with consumer tests. Related to #1.
This commit is contained in:
Stephan D committed 2026-09-11 15:36:58 +02:00
1 parent ef65a40213
commit d9318053e8
46 files changed
+703 -1

No files matched your search

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:sxchnge_theme/sxchnge_theme.dart';
void main() => runApp(const ThemeExample());
class ThemeExample extends StatelessWidget {
const ThemeExample({super.key, this.mode = ThemeMode.system});
final ThemeMode mode;
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
theme: SxchngeTheme.light(),
darkTheme: SxchngeTheme.dark(),
themeMode: mode,
home: const _Gallery(),
);
}
class _Gallery extends StatelessWidget {
const _Gallery();
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('sxchnge')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Your next exchange',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 16),
const Text(
'Обмен без лишних шагов.\nA preview of the SXCHNGE theme.',
),
const SizedBox(height: 24),
const Card(
child: Padding(
padding: EdgeInsets.all(24),
child: Text('Clear amounts. Clear choices.'),
),
),
const SizedBox(height: 24),
const TextField(
decoration: InputDecoration(
labelText: 'Amount',
hintText: '0.00',
),
),
const SizedBox(height: 16),
const TextField(
decoration: InputDecoration(
labelText: 'Email',
errorText: 'Enter a valid email address',
),
),
const SizedBox(height: 24),
FilledButton(onPressed: () {}, child: const Text('Continue')),
const SizedBox(height: 12),
OutlinedButton(onPressed: () {}, child: const Text('Back')),
const SizedBox(height: 12),
const FilledButton(onPressed: null, child: Text('Unavailable')),
],
),
),
),
),
);
}
@@ -0,0 +1,15 @@
name: sxchnge_theme_example
description: Minimal consumer and component gallery for the SXCHNGE theme.
publish_to: none
environment:
sdk: '>=3.13.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
sxchnge_theme:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
@@ -0,0 +1,51 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sxchnge_theme_example/main.dart';
void main() {
testWidgets('consumer bundles the package font and its license', (
tester,
) async {
await tester.pumpWidget(const ThemeExample());
final manifest =
jsonDecode(await rootBundle.loadString('FontManifest.json')) as List;
final family = manifest.cast<Map>().singleWhere(
(font) => font['family'] == 'packages/sxchnge_theme/Onest',
);
final asset = (family['fonts'] as List).first['asset'] as String;
expect((await rootBundle.load(asset)).lengthInBytes, greaterThan(1000));
expect(
await rootBundle.loadString(
'packages/sxchnge_theme/assets/fonts/OFL.txt',
),
contains('SIL OPEN FONT LICENSE'),
);
});
for (final mode in [ThemeMode.light, ThemeMode.dark]) {
testWidgets(
'$mode renders on a narrow screen with large text and accepts input',
(tester) async {
tester.view.physicalSize = const Size(320, 900);
tester.view.devicePixelRatio = 1;
tester.platformDispatcher.textScaleFactorTestValue = 2;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await tester.pumpWidget(ThemeExample(mode: mode));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
await tester.enterText(find.byType(TextField).first, '125.50');
expect(find.text('125.50'), findsOneWidget);
await tester.ensureVisible(find.text('Continue'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
},
);
}
}