mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
Both files were donors for the new package rather than casualties of it: theme.dart was byte-identical to untools' copy (md5 6cf8e82d), and confirm.dart's confirmDestructive() was the ready-made pattern for the ~35 AlertDialogs inlined across the fleet. Keeping local copies after promoting them upstream is exactly how the two would drift apart again, so they are deleted and imported from design_system-v0.1.0 instead. test/confirm_test.dart moves upstream with its subject -- the package covers that behaviour (Escape and the barrier both refuse, Cancel is autofocused so a reflexive Return cannot delete) as part of its own 100% gate. test/theme_test.dart stays as a consumer-side smoke check that the dependency actually resolves to the frozen palette. Verified: flutter analyze clean (bar one pre-existing bin/ style info), 308 tests pass at 100% coverage (1962/1962 -- the drop from 2005 is exactly the promoted lines), and the release APK is installed and running on the Pixel 6a with all 108 notes intact, the gold #B8862E focus ring rendering from the package's inputDecorationTheme, and no crashes in logcat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
117 lines
3.6 KiB
Dart
117 lines
3.6 KiB
Dart
import 'package:design_system/design_system.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// A lightweight, read-only renderer for the note format.
|
|
///
|
|
/// The app's notes use a small, known Markdown subset — an `#` title, `##`
|
|
/// section headings, italic `_guidance_` lines, `-` bullet lists and plain
|
|
/// paragraphs — so a tailored line-based renderer covers everything we emit
|
|
/// without pulling in a full Markdown engine. It keeps the quick-capture app
|
|
/// lean and is trivial to test.
|
|
///
|
|
/// The whole rendered note is wrapped in a [SelectionArea] so it can be copied
|
|
/// out in one go (the note is meant to be pasted into an LLM).
|
|
class MarkdownView extends StatelessWidget {
|
|
/// Creates a [MarkdownView] rendering [text].
|
|
const MarkdownView({required this.text, super.key});
|
|
|
|
/// The raw markdown source to render.
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final widgets = <Widget>[];
|
|
|
|
for (final line in text.split('\n')) {
|
|
final trimmed = line.trim();
|
|
|
|
if (trimmed.isEmpty) {
|
|
widgets.add(const SizedBox(height: 8));
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith('## ')) {
|
|
widgets.add(
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
|
child: Text(
|
|
trimmed.substring(3).trim(),
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith('# ')) {
|
|
widgets.add(
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 4),
|
|
child: Text(
|
|
trimmed.substring(2).trim(),
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
// A whole-line italic marker — used for the section guidance lines.
|
|
if (trimmed.length >= 2 &&
|
|
trimmed.startsWith('_') &&
|
|
trimmed.endsWith('_')) {
|
|
widgets.add(
|
|
Text(
|
|
trimmed.substring(1, trimmed.length - 1),
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
fontStyle: FontStyle.italic,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
widgets.add(
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 4, top: 4),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('• ', style: theme.textTheme.bodyLarge),
|
|
Expanded(
|
|
child: Text(
|
|
trimmed.substring(2),
|
|
style: theme.textTheme.bodyLarge,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
widgets.add(Text(line, style: theme.textTheme.bodyLarge));
|
|
}
|
|
|
|
return SelectionArea(
|
|
child: SingleChildScrollView(
|
|
// Caps the reading column at ~70 chars (rule 21) — the desktop
|
|
// build is an arbitrarily wide Chrome `--app` window, so without
|
|
// this, body text runs the full viewport width.
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: kProseMaxWidth),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: widgets,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|