feat: each app's own colour, errors a true red, part of Halkyon Days (ADR 0045, D18, D19)

halkyone_ui's theme takes the app's own colour as its primary
(AppPrimary, appTheme(primary:)): Halkyon, Train and Meditate cobalt as
before, Myrmex strawberry once it asks for it. Filled and floating
buttons fill with its deep shade in both modes, white on it, and the
first start and the operator's screen wash in it. Errors become a true
red in every app, #B3261E light and #FF6B5E dark, far in hue from
Myrmex's strawberry, which Material's crimson stood beside (D19).

An app with a name of its own shows it alone in its wordmark, in its
colour (BrandWordmark.alone, LaunchScreen.alone, AppFrame.productAlone).
About says the app is part of Halkyon Days, as does the foot of the
instance's pages: profiles, privacy, invitations (D1). A secret's file
keeps an instance's domain whole where the app is named as it
(halkyon.li, not li).

ADR 0045 records D18 and D19, the names step 4 keeps, and step 3 built.

Whats-New: Errors show in a clearer red, and About says the app is part of Halkyon Days.
Whats-New-De: Fehler erscheinen in einem klareren Rot, und „Über“ nennt Halkyon Days, zu dem die App gehört.
Signed-off-by: Tizian 「ティツィアーン」 <c.3a8bz@simplelogin.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-13 00:34:39 +01:00
commit e5958689a8
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
30 changed files with 505 additions and 80 deletions

View file

@ -8,7 +8,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, Learn's content in pure Dart shared with its agent, `halkyone_halkyon_core`, Halkyon's ADR 0044, D49, and the agent, `halkyone_halkyon_agent`, an MCP server for Claude Code that suggests within the studysets shared with it, D51, the scripts writing Learn's assets in `apps/halkyon/tool/`, run from `apps/halkyon`; the apps' icons are the root's `tool/app_icons.py`, ADR 0031). Learn is the app that runs; Train, Todo and Meditate run too (ADR 0030, ADR 0033, ADR 0038), Todo and Meditate not released yet.
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
- `halkyone_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the navigation bar flush with the foot with its fade, and the rail, a banner above every tab, `NavigationFrame.banner`, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
- `halkyone_ui`: Halkyon's look, the theme around cobalt and copper, each app's own colour as its primary (`AppPrimary`, Myrmex's strawberry; ADR 0045, D18), Noto Sans bundled, the window-size classes and the widgets every screen is built of (the navigation bar flush with the foot with its fade, and the rail, a banner above every tab, `NavigationFrame.banner`, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
- `halkyone_base_server`, `halkyone_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyone_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), people's files, kept by space and SHA-256 in the server's private storage with the room each may take and the pruning, for a host that keeps them (`HalkyonBase.files`, `FileStore`: which of its rows name a file and what it takes; Halkyon's ADR 0043; Learn's `asset` rows, Todo's attachments), the spaces people share, with their members, roles (Owner, Editor, Viewer) and invitations, kept in step with the host's sync engine through its `SpaceGrants` (`HalkyonBase.spaces`, Halkyon's ADR 0044, step 2: Learn's `SyncSpaceGrants`; handed on as an Owner's account goes), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
- `halkyone_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, held back on a metered network as the device's `MobileDataGate` allows (ADR 0042: Android's flag through halkyone_app's own Android plugin, `HalkyonAppPlugin.java`, in Java so that it applies no Kotlin Gradle Plugin, `FakeNetworkWatch` in `testing.dart`; `mobileDataBannerOf` above every tab in each app's frame; the setting in `SyncStatusCard`; an app's files ask `SyncService.filesAllowed`), the account section of an app's settings (`AccountTiles`), every app's Profile (`HalkyonProfile`, decided 2026-11-02: the person's own tab, the same in every app, with their account, the ways they sign in and their public profile, one for all apps, then the app's own tabs) and people's public profiles (`PublicProfileRepository`, `PublicProfileSection`, `showProfileOf`), the wait screen of an account waiting for approval and the operator's card, badge and screen (ADR 0021), the assistants of an app's settings (`AssistantsCard`: granted, by provider, agreed to, what is left of the person's budget, used, and the person's own keys) and the operator's grants from that screen (`AssistantGrantsScreen`: several assistants to several people at once, each with one budget, D57 to D63), with the repositories asking them through the module, at an effort the operator allows, or a person's own key directly (step 2) and what an app's warning before a private studyset goes names (`providerTermsOf`, `OwnAssistant.onThisMachine`, step 3), About with the instance's source link (`showAppAbout`), people's files on the device and the module's store (`FileCache`, `FileServerGateway`, `SealedFile`, `HLKF`, the format every app seals a file in), the keys of spaces people share, sealed to each member on the device and held to the public keys it pinned on first sight, with a safety code to compare them by (`SharedKeys`, `KeyPins`, `FakeSharedKeysServer` in `testing.dart`; Halkyon's ADR 0044, D3, D18) and a file opened in the system's app (`FileOpener`, `SystemFileOpener`: on Android through `HalkyonAppPlugin.java` and its own FileProvider class, `HalkyonFileProvider`; Halkyon's ADR 0043), the device's own speech recognition (`SpeechInput`, `DeviceSpeechInput`: on Android the voice input app handling `RECOGNIZE_SPEECH` where one is installed, `VoiceInputApp` on each app's `VoiceInput.kt`, `one.halky/voice_input`, FUTO Voice Input for one, which records itself and returns text, else `speech_to_text`; `FakeSpeechInput` in `testing.dart`; none on Linux; Meditate's ADR 0038, D18) and dictation into a text at its cursor, listening again after each pause (`Dictation`, Meditate's ADR 0038, D15; an app that listens declares the microphone and its usage texts itself), what's new after an update (`WhatsNew`, `WhatsNewOnUpdate`, `WhatsNewTile`, from the app's `assets/whats_new/`, Train's ADR 0030, D61), the device's appearance and language, chosen in Settings and applied before anyone signs in (`Look`, `FileLookStore`, `LookCubit`, `DeviceLookCard`; Learn keeps them among its own device settings and shows `LookSettingsCard`; ADR 0014's amendment of 4 Nov 2026), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
- `halkyone_editor` (ADR 0033, D37, 6 Nov 2026): rich text, the general parts of Learn's editor on our fork of `appflowy_editor`: `RichContent` (the editor's document JSON, which Learn and Todo store), `RichContentEditor`, `RichContentView`, the format bar, links, the clipboard keeping formats, superscript and subscript, and `EditorBlocks`, through which an app adds blocks, marks within lines and their buttons (`basicBlocks` without any), with texts of its own (English, German). What is Learn's alone stays in Learn on top of it.

View file

@ -8,13 +8,20 @@
same day by the developer: step 2 waits until halkyon.li opens to the public, which
also moves it off the host at home (an open question below); the snap names
`halkyon` and `myrmex` are registered, the Snap Store account's publisher named
Halkyon Days (`halkyon-days`).
Halkyon Days (`halkyon-days`). Step 3 built 13 Nov 2026. Amended on 13 Nov 2026
by the developer for step 4: D18 and D19 (the agent's recommendations, which they
chose), and the names step 4 keeps: Train and Meditate stay "Halkyon Train" and
"Halkyon Meditate" for now, "Halkyon" over their word in their wordmark, cobalt over
copper; the Proton Pass items of Halkyon's and Myrmex's keystores renamed by the
developer after the apps (`Halkyon – Android release keystore`, `Myrmex – …`), as
the release tool finds an app's item by its name.
- **Builds on:** ADR 0015 (one repository), ADR 0016 (one account across an instance),
ADR 0017 (a password and an authenticator), ADR 0020 (SimpleLogin), ADR 0031 (the
apps' icons), ADR 0039 (halkyon.li at home).
- **Amends:** ADR 0039, whose halkyon.li was the family's instance alone (D6); ADR
0016, for Halkyon and Myrmex (D7); ADR 0020, whose SimpleLogin goes out of use (D8);
ADR 0031, D3, D6 and D11, for Halkyon's icon (D14).
ADR 0031, D3, D6 and D11, for Halkyon's icon (D14); halkyone_ui's theme, every
app's around cobalt (D18, D19).
## Context
@ -160,6 +167,29 @@ gives people calm from prying, surveillance-capitalist eyes.
`halkyon-learn` and `halkyon-todo` are closed, their one user moving over. Both
registered 12 Nov 2026 under the account that publishes the others, its publisher
shown as Halkyon Days.
- **D18 Each app has a colour of its own, Myrmex strawberry** (the agent's
recommendation, which the developer chose on 13 Nov 2026): an app with a name of its
own shows that name alone in its wordmark, in its own colour, Halkyon in cobalt and
Myrmex in strawberry (`BrandWordmark.alone`, `AppFrame.productAlone`), where Train and
Meditate keep "Halkyon" over their word. The colour is the theme's primary role
(halkyone_ui's `AppPrimary`, `appTheme(primary:)`): buttons, links, switches, the
add and voice buttons, filled buttons and the first start's wash; neutrals, slate
blue and copper stay every app's. Myrmex's strawberry is deeper than the palette's
(#EC3E7C, too light for white on it or as text on white): #C21E5C, white on it at
5.8:1, the fill in both modes as cobalt's deep shade is; #FFB1C9 its text and marks
in dark mode; containers #FFD9E3 on #3F0018 light and #5C1130 under #FFD9E3 dark.
Halkyon, Train and Meditate keep cobalt. Alternatives were the palette's strawberry
as it is (too light for text, 3.6:1), a darker berry nearer purple (losing the
strawberry), and cobalt for every app with strawberry only on the navigation bar, as
before (the developer wanted Myrmex's own colour on its buttons).
- **D19 Errors are a true red in every app** (the agent's recommendation after looking
at Proton's apps, which the developer chose on 13 Nov 2026): Myrmex's strawberry
would be read as an error beside Material's default error colour, a crimson; so
every app, Halkyon too, takes a red far from it in hue: #B3261E light, white on it,
container #F9DEDC with #410E0B; dark #FF6B5E, saturated rather than Material's pastel
#FFB4AB, with #3B0905 on it, container #5C1712 with #FFDAD5. Proton's apps keep
strong reds in their dark themes too (Mail's #F5385A, Wallet's #FB7878), but theirs
are crimson (349°), 12° from our strawberry (337°); ours sits at 3°.
## Steps
@ -185,7 +215,8 @@ gives people calm from prying, surveillance-capitalist eyes.
configuration, tags and history; the migrations' module names (D11).
4. **The names people see** (D2, D3, D14): Halkyon's and Myrmex's texts, the launcher's
labels, windows, mails, the privacy and profile pages, the listings; Halkyon's
kingfisher icon; "part of Halkyon Days" in About.
kingfisher icon; "part of Halkyon Days" in About. Built 13 Nov 2026 with D18 and
D19, "part of Halkyon Days" at the foot of the instances' pages too.
5. **The deploy** (D7, D9, D13, D15, D16): the variables and compose files renamed;
Myrmex, Train and Meditate on registers of their own, the developer's account copied
into Myrmex's (and Train's and Meditate's, as their data is named by it too);

View file

@ -38,6 +38,7 @@
"accountDeleteTitle": "Dein Konto löschen?",
"accountSignOutFailed": "Abmelden auf diesem Gerät hat nicht geklappt. Versuch es noch einmal.",
"accountDeleteFailed": "Dein Konto wurde nicht gelöscht: Die Instanz war nicht erreichbar. Versuch es noch einmal.",
"aboutPartOf": "Teil von Halkyon Days",
"aboutSourceCode": "Der Quellcode dessen, was diese Instanz ausführt:",
"aboutSourceUnreachable": "Diese Instanz war nicht erreichbar, um nach ihrem Quellcode zu fragen.",
"sessionUnreachable": "Der Server war nicht erreichbar.",

View file

@ -141,6 +141,10 @@
"accountDeleteTitle": "Delete your account?",
"accountSignOutFailed": "Couldn't sign out on this device. Try again.",
"accountDeleteFailed": "Couldn't delete your account: the instance wasn't reached. Try again.",
"aboutPartOf": "Part of Halkyon Days",
"@aboutPartOf": {
"description": "About names the project every app is part of (Halkyon Days' ADR 0045, D1). Halkyon Days is a name: never translated."
},
"aboutSourceCode": "The source of what this instance runs:",
"aboutSourceUnreachable": "Couldn't reach this instance to ask where its source is.",
"sessionUnreachable": "Couldn't reach the server.",

View file

@ -331,6 +331,12 @@ abstract class HalkyonAppLocalizations {
/// **'Couldn\'t delete your account: the instance wasn\'t reached. Try again.'**
String get accountDeleteFailed;
/// About names the project every app is part of (Halkyon Days' ADR 0045, D1). Halkyon Days is a name: never translated.
///
/// In en, this message translates to:
/// **'Part of Halkyon Days'**
String get aboutPartOf;
/// No description provided for @aboutSourceCode.
///
/// In en, this message translates to:

View file

@ -152,6 +152,9 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
String get accountDeleteFailed =>
'Dein Konto wurde nicht gelöscht: Die Instanz war nicht erreichbar. Versuch es noch einmal.';
@override
String get aboutPartOf => 'Teil von Halkyon Days';
@override
String get aboutSourceCode =>
'Der Quellcode dessen, was diese Instanz ausführt:';

View file

@ -152,6 +152,9 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
String get accountDeleteFailed =>
'Couldn\'t delete your account: the instance wasn\'t reached. Try again.';
@override
String get aboutPartOf => 'Part of Halkyon Days';
@override
String get aboutSourceCode => 'The source of what this instance runs:';

View file

@ -3,8 +3,9 @@ import 'package:halkyone_app/src/app/app_frame.dart';
import 'package:halkyone_app/src/app/app_scope.dart';
import 'package:material_ui/material_ui.dart';
/// Shows what the app is: its name (the frame's), and as the app gives them
/// its [version], [icon] and [legalese]; where the source of what the
/// Shows what the app is: its name (the frame's), part of Halkyon Days (the
/// project, Halkyon Days' ADR 0045, D1), and as the app gives them its
/// [version], [icon] and [legalese]; where the source of what the
/// instance runs is ([InstanceSourceCode]), which both licences require
/// offering to the people who use the instance; the app's own parts
/// ([children], such as its credits); and Flutter's "View licenses", every
@ -22,7 +23,11 @@ void showAppAbout(
applicationVersion: version,
applicationIcon: icon,
applicationLegalese: legalese,
children: [const InstanceSourceCode(), ...children],
children: [
Text(HalkyonAppLocalizations.of(context).aboutPartOf),
const InstanceSourceCode(),
...children,
],
);
/// Where the source of what the instance in use runs is, as it names it

View file

@ -9,20 +9,25 @@ typedef LookSettings = ({ThemeMode themeMode, Locale? locale});
/// hands it down ([AppFrameScope]) to every screen beneath, the person's
/// app included.
final class const AppFrame({
/// The app's name in the language of the context it is asked in ("Halkyon
/// Learn"): the window's title, and where this package's texts name the
/// The app's name in the language of the context it is asked in
/// ("Myrmex", "Halkyon Train"): the window's title, and where this package's texts name the
/// app.
required final String Function(BuildContext context) name,
/// The app's word in its wordmark, under "Halkyon" ("Learn").
/// The app's word in its wordmark, under "Halkyon" ("Train"), or its
/// whole name where it has one of its own ([productAlone]: "Myrmex").
required final String Function(BuildContext context) product,
/// Whether the wordmark is [product] alone, in the app's colour, without
/// "Halkyon" above it (Halkyon Days' ADR 0045: Halkyon, Myrmex).
final bool productAlone = false,
/// The app's icon, as the first start shows it on the tile of the
/// instance the build names.
required final ImageProvider icon,
/// An instance's address as the address field's hint shows it
/// (`learn.example.org`).
/// (`myrmex.example.org`).
required final String exampleAddress,
/// What the first start says under the wordmark: what the instance chosen

View file

@ -36,7 +36,8 @@ class _FirstStartScreenState() extends State<FirstStartScreen> {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: TopGradient.overlayStyle(context),
child: TopGradient(
color: Brand.primary,
// The app's own colour (ADR 0045, D18).
color: AppPrimary.of(context).deep,
background: theme.colorScheme.surface,
extent: 320,
child: Scaffold(
@ -54,7 +55,10 @@ class _FirstStartScreenState() extends State<FirstStartScreen> {
Semantics(
header: true,
label: l10n.instanceWelcome(frame.name(context)),
child: BrandWordmark(product: frame.product(context)),
child: BrandWordmark(
product: frame.product(context),
alone: frame.productAlone,
),
),
const SizedBox(height: 16),
Text(

View file

@ -168,7 +168,7 @@ class _OperatorPageState() extends State<OperatorPage> {
WidgetsBinding.instance.addPostFrameCallback((_) => _check());
}
return TopGradient(
color: Brand.primary,
color: AppPrimary.of(context).deep,
background: theme.colorScheme.surfaceContainerLow,
child: Scaffold(
backgroundColor: Colors.transparent,

View file

@ -332,13 +332,17 @@ class const SheetProblem(final String text, {super.key})
/// How the file of a secret names the instance called [name] (its host, with
/// the port if it has one), for the app called [product]: without the app's
/// word, as an operator's apps share one account (`learn.halkyon.eu`,
/// `todo.halkyon.eu`: `halkyon.eu`), and safe in a file's name
/// (`localhost:8280`: `localhost-8280`).
/// `train.halkyon.eu`: `halkyon.eu`), but whole where the word is the
/// domain's own (`halkyon.li` stays, not `li`; Halkyon Days' ADR 0045, D5),
/// and safe in a file's name (`localhost:8280`: `localhost-8280`).
String instanceFileLabel(String name, {required String product}) {
final host = name.toLowerCase();
final prefix = '${product.toLowerCase()}.';
return (host.startsWith(prefix) ? host.substring(prefix.length) : host)
.replaceAll(RegExp('[^a-z0-9.-]+'), '-');
final rest = host.startsWith(prefix) ? host.substring(prefix.length) : '';
return (rest.contains('.') ? rest : host).replaceAll(
RegExp('[^a-z0-9.-]+'),
'-',
);
}
/// Save for [text], in a file named after the instance and [fileKind].

View file

@ -43,6 +43,8 @@ void main() {
await open(tester);
expect(find.text('Halkyon Test'), findsWidgets);
// The project every app is part of (Halkyon Days' ADR 0045, D1).
expect(find.text('Part of Halkyon Days'), findsOneWidget);
expect(find.text('The source of what this instance runs:'), findsOneWidget);
expect(find.text('https://code.example.org/halkyon'), findsOneWidget);
// Flutter's own button to every package's notice.
@ -76,6 +78,7 @@ void main() {
testWidgets('speaks German', (tester) async {
await open(tester, locale: const Locale('de'));
expect(find.text('Teil von Halkyon Days'), findsOneWidget);
expect(
find.text('Der Quellcode dessen, was diese Instanz ausführt:'),
findsOneWidget,

View file

@ -2,6 +2,7 @@ import 'package:flag/flag.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyone_app/halkyone_app.dart';
import 'package:halkyone_app/testing.dart';
import 'package:halkyone_ui/halkyone_ui.dart';
import 'package:material_ui/material_ui.dart';
/// A signed-in person's app as a test makes one: who it is for, and the
@ -53,6 +54,7 @@ void main() {
Set<Instance> answering = const {},
Map<Instance, InstanceLocations> locations = const {},
SubdivisionNames subdivisionNames = const MemorySubdivisionNames({}),
AppFrame? frame,
}) async {
tester.view.physicalSize = const Size(412, 900);
tester.view.devicePixelRatio = 1;
@ -66,7 +68,7 @@ void main() {
await choice.load(usedBefore: usedBefore);
await tester.pumpWidget(
InstanceGate(
frame: testFrame(),
frame: frame ?? testFrame(),
choice: choice,
checker: FakeInstanceChecker(answering, locations),
countryNames: MemoryCountryNames(const {
@ -97,6 +99,31 @@ void main() {
return choice;
}
testWidgets('an app with a name of its own greets with that name alone, '
"in its own colour (Halkyon Days' ADR 0045, D18)", (tester) async {
final strawberry = AppFrame(
name: (context) => 'Myrmex',
product: (context) => 'Myrmex',
productAlone: true,
icon: testIcon,
exampleAddress: 'myrmex.example.org',
firstStartSubtitle: (context) => 'Choose the instance.',
theme: (brightness) =>
appTheme(brightness, primary: AppPrimary.strawberry),
localizationsDelegates: testLocalizationsDelegates,
supportedLocales: HalkyonAppLocalizations.supportedLocales,
launchScreen: const SizedBox(),
);
await start(tester, signedOut: true, frame: strawberry);
expect(find.bySemanticsLabel('Welcome to Myrmex'), findsOneWidget);
expect(find.text('Halkyon'), findsNothing);
expect(
tester.widget<Text>(find.text('Myrmex')).style?.color,
AppPrimary.strawberry.deep,
);
});
Future<void> openSwitcher(WidgetTester tester) async {
await tester.tap(find.text('Switch account'));
await tester.pumpAndSettle();

View file

@ -97,6 +97,10 @@ void main() {
expect(label('notes.example.org', product: 'Learn'), 'notes.example.org');
expect(label('learn.example.org', product: 'Todo'), 'learn.example.org');
expect(label('localhost:8280', product: 'Meditate'), 'localhost-8280');
// An app named as its instance's domain keeps the domain whole
// (Halkyon Days' ADR 0045, D5).
expect(label('halkyon.li', product: 'Halkyon'), 'halkyon.li');
expect(label('myrmex.li', product: 'Myrmex'), 'myrmex.li');
});
testWidgets("a saved phrase's file names the instance", (tester) async {

View file

@ -35,4 +35,10 @@ const pageStyle =
'.address,.app{color:var(--muted)}.address{margin:0 0 1rem}'
'.links{padding:0;list-style:none}.links li{margin:.25rem 0}'
'a{color:var(--accent)}.verified{color:var(--muted);font-size:.875rem}'
'.app{margin-top:2rem;font-size:.875rem}';
'.app{margin-top:2rem;font-size:.875rem}'
'.project{margin-top:2rem;color:var(--muted);font-size:.75rem}';
/// The line at the foot of every page: the project every app is part of
/// (Halkyon Days' ADR 0045, D1), a name never translated.
String projectFoot({required bool german}) =>
'<p class="project">${german ? 'Teil von' : 'Part of'} Halkyon Days</p>';

View file

@ -97,6 +97,7 @@ String privacyPage(
'<a href="${_attribute.convert(sourceCodeUrl(pod))}">'
'${_text.convert(sourceCodeUrl(pod))}</a></p>',
)
..writeln(projectFoot(german: german))
..writeln('</main>')
..writeln('</body>')
..writeln('</html>');

View file

@ -178,6 +178,7 @@ String _page(
);
}
page
..writeln(projectFoot(german: german))
..writeln('</main>')
..writeln('</body>')
..writeln('</html>');

View file

@ -159,6 +159,7 @@ String _page(String title, List<String> paragraphs, {required bool german}) {
page.writeln('<p>${_text.convert(paragraph)}</p>');
}
page
..writeln(projectFoot(german: german))
..writeln('</main>')
..writeln('</body>')
..writeln('</html>');

View file

@ -121,7 +121,13 @@ void main() {
expect(text, contains('Datenschutz'));
expect(text, contains('Deine Lernsets werden zum Abgleich'));
expect(text, contains('Claude Sonnet'));
expect(text, contains('<p class="project">Teil von Halkyon Days</p>'));
expect(page(), contains('<html lang="en">'));
// The project every app is part of (Halkyon Days' ADR 0045, D1).
expect(
page(),
contains('<p class="project">Part of Halkyon Days</p>'),
);
});
test("escapes what the operator's settings hold", () {

Some files were not shown because too many files have changed in this diff Show more