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:
1 parent
ef65a40213
commit
d9318053e8
46 files changed
+703
-1
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# SXCHNGE frontend
|
||||
|
||||
Home for the consumer application and its Flutter packages. The existing public Astro website lives in [`../website/`](../website/README.md).
|
||||
|
||||
The first package, [`sxchnge_theme`](packages/sxchnge_theme/README.md), provides installable light/dark Material themes and bundled Onest fonts. Its `example/` folder demonstrates installation and component styling. The consumer application in [`app/`](app/README.md) now includes a responsive login/sign-up preview; backend authentication is not connected.
|
||||
|
||||
The application will provide consumer registration, sign-in, onboarding and verification screens, exchange flows, and transaction history through the sxchnge backend. Eligibility and remaining limits are projections of Sendico's authoritative decisions.
|
||||
|
||||
Flutter is anticipated by the existing website handoff. Optional brand references are in [`../website/design/`](../website/design/README.md) and [`../website/flutter/brand_palette.dart`](../website/flutter/brand_palette.dart); these do not prescribe the application's component styling.
|
||||
|
||||
Define the first customer journey and BFF contract before scaffolding the application. Sendico integration credentials belong in the backend.
|
||||
|
||||
Shared authentication: [`sxchnge_auth`](packages/sxchnge_auth/README.md) provides reusable login/signup providers, service contracts, and dummy implementations for both web and future mobile clients.
|
||||
@@ -0,0 +1,5 @@
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
build/
|
||||
pubspec.lock
|
||||
example/pubspec.lock
|
||||
@@ -0,0 +1,5 @@
|
||||
## 0.1.0
|
||||
|
||||
- Introduce light and dark Material themes derived from the SXCHNGE website.
|
||||
- Bundle Onest with its SIL Open Font License.
|
||||
- Export brand colors and theme factories.
|
||||
@@ -0,0 +1,58 @@
|
||||
# sxchnge_theme
|
||||
|
||||
Installable SXCHNGE theme package for Flutter 3.47+ / Dart 3.13+. It provides light and dark Material 3 themes, website brand colors, and bundled Onest typography with Latin and Cyrillic support. No runtime network requests or third-party packages are needed.
|
||||
|
||||
## Install
|
||||
|
||||
From an application at `frontend/app`, add:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
sxchnge_theme:
|
||||
path: ../packages/sxchnge_theme
|
||||
```
|
||||
|
||||
Adjust the path relative to your app's `pubspec.yaml`, then run `flutter pub get`. This is a private package (`publish_to: none`), ready for local use; it has not been published to pub.dev.
|
||||
|
||||
## Apply
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sxchnge_theme/sxchnge_theme.dart';
|
||||
|
||||
MaterialApp(
|
||||
theme: SxchngeTheme.light(),
|
||||
darkTheme: SxchngeTheme.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
home: const Scaffold(body: Center(child: Text('Hello, sxchnge'))),
|
||||
);
|
||||
```
|
||||
|
||||
Onest is declared by the package and the theme uses its package-qualified font family, so the consuming app needs no additional font declarations. The bundled SIL Open Font License is in `assets/fonts/OFL.txt`. Follow Flutter's [TextStyle package font guidance](https://api.flutter.dev/flutter/painting/TextStyle/package.html) when creating standalone text styles outside the theme.
|
||||
|
||||
Use `Theme.of(context).colorScheme` and `textTheme` for normal application widgets. `SxchngeBrand` exposes the original palette for intentional brand treatments. Customize locally with `SxchngeTheme.light().copyWith(...)`.
|
||||
|
||||
## Design mapping
|
||||
|
||||
- Ink, mint, lime, paper, and forest match `website/design/tokens.json` version 1.
|
||||
- Light mode uses ink actions on paper; dark mode uses mint actions on ink.
|
||||
- Mint and lime containers use ink foregrounds. Lime is a brand accent, not a success or transaction status.
|
||||
- Onest body text starts at 16 logical pixels, with 14-pixel secondary text. Text scaling is preserved.
|
||||
- Cards and dialogs use 24-pixel corners, fields use 14-pixel corners, and buttons use pill shapes with at least 48-pixel minimum height.
|
||||
- Error colors remain Material semantic roles. A future product status system should define success, warning, and information roles separately.
|
||||
|
||||
The palette is a package-owned snapshot: website builds do not modify this package. Deliberate brand updates should update the snapshot and package version together. Campaign layout, rotation, marquee, and decorative motion are not part of the application theme.
|
||||
|
||||
## Example and validation
|
||||
|
||||
`example/lib/main.dart` is a minimal consumer gallery with English/Russian text, cards, inputs, validation errors, and enabled/disabled buttons. To launch it, generate the platform runner you need from `example/`, for example `flutter create --platforms=web .`, then `flutter run -d chrome`. Platform runners are not included in this theme-only package.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make test-frontend
|
||||
```
|
||||
|
||||
From this package and its `example/` folder, run `flutter analyze`. Tests cover core brand synchronization in this repository, foreground contrast in both modes, package font/license bundling in an independent consumer, and rendering/input on a narrow screen at 200% text scale.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 The Onest Project Authors (https://github.com/googlefonts/onest)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Brand snapshot from website/design/tokens.json (version 1).
|
||||
// Keep these values aligned when deliberately updating the package's brand.
|
||||
import 'dart:ui';
|
||||
|
||||
abstract final class SxchngeBrand {
|
||||
static const ink = Color(0xFF14221D);
|
||||
static const mint = Color(0xFFC4F9CE);
|
||||
static const lime = Color(0xFFD7FF5F);
|
||||
static const paper = Color(0xFFF9FAF6);
|
||||
static const muted = Color(0xFF637168);
|
||||
static const line = Color(0xFFD9E0D8);
|
||||
static const forest = Color(0xFF396A4B);
|
||||
static const focus = Color(0xFF168850);
|
||||
static const darkField = Color(0xFF25372E);
|
||||
static const darkReceive = Color(0xFF203D2D);
|
||||
static const sageSurface = Color(0xFFEAF0E6);
|
||||
static const blueSurface = Color(0xFFEDF0F9);
|
||||
static const fontFamily = 'Onest';
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'brand.dart';
|
||||
|
||||
/// Install with `MaterialApp(theme: SxchngeTheme.light(),
|
||||
/// darkTheme: SxchngeTheme.dark())`.
|
||||
abstract final class SxchngeTheme {
|
||||
static const packageName = 'sxchnge_theme';
|
||||
|
||||
static ThemeData light() => _build(Brightness.light);
|
||||
static ThemeData dark() => _build(Brightness.dark);
|
||||
|
||||
static ThemeData _build(Brightness brightness) {
|
||||
final dark = brightness == Brightness.dark;
|
||||
// Seed supplies complete Material roles; explicit overrides preserve brand
|
||||
// colors and readable foregrounds rather than approximating the palette.
|
||||
final scheme =
|
||||
ColorScheme.fromSeed(
|
||||
seedColor: SxchngeBrand.forest,
|
||||
brightness: brightness,
|
||||
).copyWith(
|
||||
primary: dark ? SxchngeBrand.mint : SxchngeBrand.ink,
|
||||
onPrimary: dark ? SxchngeBrand.ink : SxchngeBrand.mint,
|
||||
primaryContainer: SxchngeBrand.mint,
|
||||
onPrimaryContainer: SxchngeBrand.ink,
|
||||
secondary: dark ? SxchngeBrand.mint : SxchngeBrand.forest,
|
||||
onSecondary: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
||||
secondaryContainer: dark
|
||||
? SxchngeBrand.darkReceive
|
||||
: SxchngeBrand.sageSurface,
|
||||
onSecondaryContainer: dark ? SxchngeBrand.mint : SxchngeBrand.ink,
|
||||
tertiary: dark ? SxchngeBrand.lime : SxchngeBrand.forest,
|
||||
onTertiary: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
||||
tertiaryContainer: SxchngeBrand.lime,
|
||||
onTertiaryContainer: SxchngeBrand.ink,
|
||||
surface: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
||||
onSurface: dark ? SxchngeBrand.paper : SxchngeBrand.ink,
|
||||
onSurfaceVariant: dark ? const Color(0xFFB6C6B9) : SxchngeBrand.muted,
|
||||
surfaceContainerLowest: dark ? const Color(0xFF101B17) : Colors.white,
|
||||
surfaceContainerLow: dark
|
||||
? const Color(0xFF1B2B23)
|
||||
: SxchngeBrand.paper,
|
||||
surfaceContainer: dark
|
||||
? SxchngeBrand.darkField
|
||||
: SxchngeBrand.sageSurface,
|
||||
surfaceContainerHigh: dark
|
||||
? SxchngeBrand.darkReceive
|
||||
: SxchngeBrand.line,
|
||||
surfaceContainerHighest: dark
|
||||
? const Color(0xFF324A3C)
|
||||
: const Color(0xFFCFD9CE),
|
||||
outline: dark ? const Color(0xFF8CA291) : SxchngeBrand.muted,
|
||||
outlineVariant: dark ? const Color(0xFF405A48) : SxchngeBrand.line,
|
||||
inverseSurface: dark ? SxchngeBrand.paper : SxchngeBrand.ink,
|
||||
onInverseSurface: dark ? SxchngeBrand.ink : SxchngeBrand.paper,
|
||||
inversePrimary: dark ? SxchngeBrand.forest : SxchngeBrand.mint,
|
||||
surfaceTint: Colors.transparent,
|
||||
);
|
||||
final base = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
fontFamily: SxchngeBrand.fontFamily,
|
||||
package: packageName,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
visualDensity: VisualDensity.standard,
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
);
|
||||
final text = base.textTheme.copyWith(
|
||||
displayLarge: base.textTheme.displayLarge!.copyWith(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -2,
|
||||
height: 1.1,
|
||||
),
|
||||
displayMedium: base.textTheme.displayMedium!.copyWith(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -1.5,
|
||||
height: 1.15,
|
||||
),
|
||||
displaySmall: base.textTheme.displaySmall!.copyWith(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -1,
|
||||
height: 1.2,
|
||||
),
|
||||
headlineLarge: base.textTheme.headlineLarge!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -1,
|
||||
),
|
||||
headlineMedium: base.textTheme.headlineMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
headlineSmall: base.textTheme.headlineSmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
bodyLarge: base.textTheme.bodyLarge!.copyWith(fontSize: 16, height: 1.65),
|
||||
bodyMedium: base.textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
),
|
||||
bodySmall: base.textTheme.bodySmall!.copyWith(fontSize: 14, height: 1.5),
|
||||
labelLarge: base.textTheme.labelLarge!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
labelMedium: base.textTheme.labelMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
labelSmall: base.textTheme.labelSmall!.copyWith(fontSize: 14),
|
||||
);
|
||||
final border = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(color: scheme.outline),
|
||||
);
|
||||
return base.copyWith(
|
||||
textTheme: text,
|
||||
appBarTheme: AppBarThemeData(
|
||||
backgroundColor: scheme.surface,
|
||||
foregroundColor: scheme.onSurface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: text.titleLarge,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: scheme.surfaceContainer,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(64, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: text.labelLarge,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(64, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: text.labelLarge,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(64, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: text.labelLarge,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(48, 48),
|
||||
textStyle: text.labelLarge,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationThemeData(
|
||||
filled: true,
|
||||
fillColor: scheme.surfaceContainerLowest,
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
border: border,
|
||||
enabledBorder: border,
|
||||
focusedBorder: border.copyWith(
|
||||
borderSide: BorderSide(
|
||||
color: dark ? SxchngeBrand.mint : SxchngeBrand.focus,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorBorder: border.copyWith(
|
||||
borderSide: BorderSide(color: scheme.error),
|
||||
),
|
||||
focusedErrorBorder: border.copyWith(
|
||||
borderSide: BorderSide(color: scheme.error, width: 2),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: scheme.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
thickness: 1,
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: scheme.surface,
|
||||
indicatorColor: scheme.primaryContainer,
|
||||
),
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: scheme.primary,
|
||||
selectionColor: SxchngeBrand.lime.withValues(alpha: dark ? 0.3 : 0.5),
|
||||
selectionHandleColor: scheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// SXCHNGE brand colors, typography, and Material themes.
|
||||
library;
|
||||
|
||||
export 'src/brand.dart';
|
||||
export 'src/theme.dart';
|
||||
@@ -0,0 +1,24 @@
|
||||
name: sxchnge_theme
|
||||
description: SXCHNGE Material theme and bundled Onest typography for Flutter apps.
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.13.0 <4.0.0'
|
||||
flutter: '>=3.47.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- assets/fonts/OFL.txt
|
||||
fonts:
|
||||
- family: Onest
|
||||
fonts:
|
||||
- asset: assets/fonts/Onest-Variable.ttf
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:sxchnge_theme/sxchnge_theme.dart';
|
||||
|
||||
double contrast(Color a, Color b) {
|
||||
final x = a.computeLuminance();
|
||||
final y = b.computeLuminance();
|
||||
return (math.max(x, y) + 0.05) / (math.min(x, y) + 0.05);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('website core colors stay aligned with the package snapshot', () {
|
||||
final source = File('../../../website/design/tokens.json');
|
||||
if (!source.existsSync())
|
||||
return; // Also allow testing outside this monorepo.
|
||||
final colors =
|
||||
(jsonDecode(source.readAsStringSync()) as Map)['colors'] as Map;
|
||||
for (final entry in {
|
||||
'ink': SxchngeBrand.ink,
|
||||
'mint': SxchngeBrand.mint,
|
||||
'lime': SxchngeBrand.lime,
|
||||
'paper': SxchngeBrand.paper,
|
||||
'forest': SxchngeBrand.forest,
|
||||
}.entries) {
|
||||
expect(
|
||||
entry.value.toARGB32(),
|
||||
int.parse('ff${(colors[entry.key] as String).substring(1)}', radix: 16),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (final dark in [false, true]) {
|
||||
final theme = dark ? SxchngeTheme.dark() : SxchngeTheme.light();
|
||||
test(
|
||||
'${dark ? "dark" : "light"} foreground pairs meet normal text contrast',
|
||||
() {
|
||||
final c = theme.colorScheme;
|
||||
for (final pair in [
|
||||
(c.primary, c.onPrimary),
|
||||
(c.primaryContainer, c.onPrimaryContainer),
|
||||
(c.secondary, c.onSecondary),
|
||||
(c.secondaryContainer, c.onSecondaryContainer),
|
||||
(c.tertiary, c.onTertiary),
|
||||
(c.tertiaryContainer, c.onTertiaryContainer),
|
||||
(c.surface, c.onSurface),
|
||||
(c.surface, c.onSurfaceVariant),
|
||||
(c.error, c.onError),
|
||||
(c.errorContainer, c.onErrorContainer),
|
||||
]) {
|
||||
expect(contrast(pair.$1, pair.$2), greaterThanOrEqualTo(4.5));
|
||||
}
|
||||
expect(
|
||||
theme.textTheme.bodyMedium!.fontFamily,
|
||||
'packages/sxchnge_theme/Onest',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user