Halkyon's shared packages
A pub workspace of the Dart packages Halkyon's apps share: halkyon_ui, Halkyon's look (the theme, fonts, layout and shared widgets, with texts in English and German), halkyon_core's low-level primitives and halkyon_interop's cross-app contracts. Dart 3.13 with LeanCode's lints, configured once at the root; apps take a package by git, pinned to a signed commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
commit
80fa7b9628
2 changed files with 6762 additions and 0 deletions
89
packages/halkyon_ui/test/theme/app_fonts_test.dart
Normal file
89
packages/halkyon_ui/test/theme/app_fonts_test.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// How many pixels [text] inks in [family] at [weight]: heavier strokes ink
|
||||
/// more.
|
||||
Future<int> ink(String text, String family, FontWeight weight) async {
|
||||
final painter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
fontFamily: family,
|
||||
fontSize: 40,
|
||||
fontWeight: weight,
|
||||
color: const Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
final recorder = ui.PictureRecorder();
|
||||
painter.paint(Canvas(recorder), Offset.zero);
|
||||
final image = await recorder.endRecording().toImage(
|
||||
painter.width.ceil(),
|
||||
painter.height.ceil(),
|
||||
);
|
||||
final pixels = (await image.toByteData())!;
|
||||
var inked = 0;
|
||||
for (var alpha = 3; alpha < pixels.lengthInBytes; alpha += 4) {
|
||||
if (pixels.getUint8(alpha) > 128) {
|
||||
inked++;
|
||||
}
|
||||
}
|
||||
return inked;
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test("the font is the package's own, as an app finds a package's font", () {
|
||||
expect(appFontFamily, 'packages/halkyon_ui/Noto Sans');
|
||||
});
|
||||
|
||||
test("the theme sets text in Noto Sans, with the apps' CJK fonts for what "
|
||||
'it lacks, named as an app declares them', () {
|
||||
for (final brightness in Brightness.values) {
|
||||
final style = appTheme(brightness).textTheme.bodyLarge!;
|
||||
expect(style.fontFamily, appFontFamily);
|
||||
expect(style.fontFamilyFallback, [
|
||||
'Noto Sans JP',
|
||||
'Noto Sans SC',
|
||||
'Noto Sans TC',
|
||||
'Noto Sans KR',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("a language's own CJK font comes first, so shared Han characters "
|
||||
'take its shapes', () {
|
||||
expect(appFontFallbackFor(null).first, 'Noto Sans JP');
|
||||
expect(appFontFallbackFor('en'), appFontFallback);
|
||||
expect(appFontFallbackFor('ja').first, 'Noto Sans JP');
|
||||
expect(appFontFallbackFor('zh-Hans').first, 'Noto Sans SC');
|
||||
expect(appFontFallbackFor('zh').first, 'Noto Sans SC');
|
||||
expect(appFontFallbackFor('zh-Hant').first, 'Noto Sans TC');
|
||||
expect(appFontFallbackFor('zh-TW').first, 'Noto Sans TC');
|
||||
expect(appFontFallbackFor('ko').first, 'Noto Sans KR');
|
||||
expect(hasOwnFont('ko'), isTrue);
|
||||
expect(hasOwnFont('de'), isFalse);
|
||||
// Every font stays, once.
|
||||
expect(appFontFallbackFor('ko').toSet(), appFontFallback.toSet());
|
||||
expect(appFontFallbackFor('ko'), hasLength(4));
|
||||
});
|
||||
|
||||
test('Noto Sans draws every weight from its one file', () async {
|
||||
// In this package's own tests it is the root project, whose assets have
|
||||
// no `packages/` prefix; an app's test of the name is in example/.
|
||||
final loader = FontLoader(appFontFamily)
|
||||
..addFont(rootBundle.load('assets/fonts/NotoSans-Variable.ttf'));
|
||||
await loader.load();
|
||||
|
||||
const text = 'Ἀθῆναι Halkyon';
|
||||
final regular = await ink(text, appFontFamily, FontWeight.w400);
|
||||
final bold = await ink(text, appFontFamily, FontWeight.w700);
|
||||
expect(bold, greaterThan(regular * 1.2));
|
||||
});
|
||||
}
|
||||
99
packages/halkyon_ui/test/theme/theme_test.dart
Normal file
99
packages/halkyon_ui/test/theme/theme_test.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// WCAG's contrast ratio of two colours, from 1 (none) to 21.
|
||||
double _contrast(Color a, Color b) {
|
||||
final (lighter, darker) = (
|
||||
max(a.computeLuminance(), b.computeLuminance()),
|
||||
min(a.computeLuminance(), b.computeLuminance()),
|
||||
);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
/// An app's own colours, beside the shared ones.
|
||||
final class const _AppColors(final Color special)
|
||||
extends ThemeExtension<_AppColors> {
|
||||
@override
|
||||
_AppColors copyWith({Color? special}) => _AppColors(special ?? this.special);
|
||||
|
||||
@override
|
||||
_AppColors lerp(_AppColors? other, double t) =>
|
||||
other == null ? this : _AppColors(Color.lerp(special, other.special, t)!);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('the picker palette', () {
|
||||
test('has the 20 colours content can be given', () {
|
||||
expect(PaletteColor.values, hasLength(20));
|
||||
});
|
||||
|
||||
for (final color in PaletteColor.values) {
|
||||
test(
|
||||
'${color.name}: its text colour is readable on its fill (WCAG AA)',
|
||||
() {
|
||||
expect(
|
||||
_contrast(color.foreground, color.background),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'finds a colour by its stored name, and nothing for an unknown one',
|
||||
() {
|
||||
expect(PaletteColor.byName('cobalt'), PaletteColor.cobalt);
|
||||
expect(PaletteColor.byName('ultraviolet'), isNull);
|
||||
expect(PaletteColor.byName(null), isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('the theme', () {
|
||||
for (final brightness in Brightness.values) {
|
||||
test('$brightness: Material 3, with the status colours', () {
|
||||
final theme = appTheme(brightness);
|
||||
|
||||
expect(theme.useMaterial3, isTrue);
|
||||
expect(theme.colorScheme.brightness, brightness);
|
||||
expect(theme.extras, isA<ThemeExtras>());
|
||||
});
|
||||
}
|
||||
|
||||
test("takes an app's own extensions beside the shared ones", () {
|
||||
const colors = _AppColors(Color(0xFF123456));
|
||||
|
||||
final theme = appTheme(Brightness.light, extensions: [colors]);
|
||||
|
||||
expect(theme.extension<_AppColors>(), colors);
|
||||
expect(theme.extras, ThemeExtras.light);
|
||||
});
|
||||
|
||||
test('light: cobalt is the primary colour, copper the accent', () {
|
||||
final scheme = appTheme(Brightness.light).colorScheme;
|
||||
|
||||
expect(scheme.primary, Brand.primary);
|
||||
expect(scheme.tertiary, Brand.accent);
|
||||
});
|
||||
|
||||
test("dark: the primary colour's text stays readable", () {
|
||||
final scheme = appTheme(Brightness.dark).colorScheme;
|
||||
|
||||
expect(
|
||||
_contrast(scheme.onPrimary, scheme.primary),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
);
|
||||
expect(
|
||||
_contrast(scheme.onSurface, scheme.surface),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
);
|
||||
});
|
||||
|
||||
test("the navigation bar is Material 3 Expressive's shorter one", () {
|
||||
expect(appTheme(Brightness.light).navigationBarTheme.height, 64);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in a new issue