reclaim/app/lib/widgets/norm_text_card.dart
flemming-it 0c536c5b91 fix(store): dash capability IDs + .chain bundles (chain catalog conformance)
chain's catalog charset (naming.rs check_segment, capability-namespaces.yaml)
allows only [a-z0-9-] per dot-segment — underscores are rejected by
`chain validate`. Repack all 8 modules with the dash capability form
(econ.skm-score, law.benefit-score, …) and align the store index, flows,
app strings and docs to match. Bundles now use the .chain extension
(content-addressed; legacy .fai would still install).

- store/store.yaml: name: dash, wasm_url .chain, refreshed sha256
- store/SHA256.txt: 8 new .chain hashes
- flows + app/lib + RUN.md + MACHBARKEITSSTUDIE.md: capability refs _ -> -
- left untouched: text_akoma_normalize.wasm artifact, DB field skm_score,
  backlog module law.skm_score

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-20 00:11:39 +02:00

143 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import '../data/models.dart';
import '../theme/reclaim_tokens.dart';
import 'reclaim_card.dart';
/// Renders the actual paragraphs of a norm right next to the
/// evaluation. Solves the "I want to read the law in place"
/// requirement so a reviewer never has to context-switch to a
/// browser tab just to confirm what the score is about.
///
/// Two side-buttons at the top:
/// - "Im Browser öffnen" — pop the canonical source URL
/// - "URL kopieren" — clipboard, useful for citations
///
/// On macOS the browser-open uses the system `open` command —
/// no extra package, no permission prompt. iOS/Linux/Windows
/// branches would need `url_launcher` later; Phase 0 ships
/// macOS first.
class NormTextCard extends StatelessWidget {
const NormTextCard({super.key, required this.norm});
final Norm norm;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
final paragraphs = norm.paragraphs;
return ReclaimCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Gesetzestext', style: t.headlineSmall),
const SizedBox(height: 4),
Text(
'${norm.jurabk} ${norm.paragraph} — Stand '
'${_isoDate(norm.standDate)}',
style: t.labelSmall?.copyWith(
color: ReclaimColors.mute,
),
),
],
),
),
IconButton(
tooltip: 'URL kopieren',
onPressed: () =>
Clipboard.setData(ClipboardData(text: norm.sourceUrl)),
icon: const Icon(Icons.link),
),
FilledButton.icon(
onPressed: () => _openInBrowser(context, norm.sourceUrl),
icon: const Icon(Icons.open_in_new),
label: const Text('Im Browser öffnen'),
),
],
),
const SizedBox(height: ReclaimSpace.lg),
if (paragraphs.isEmpty)
Text(
'Für diese Norm ist noch kein normalisierter Text '
'eingebunden. Wenn Ch∆In mit `text.akoma-normalize@^0` '
'gegen die Quell-URL läuft, erscheinen die Absätze hier.',
style: t.bodyLarge?.copyWith(color: ReclaimColors.mute),
)
else
for (final p in paragraphs)
Padding(
padding:
const EdgeInsets.only(bottom: ReclaimSpace.md),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 64,
child: Text(
p.number,
style: t.labelLarge?.copyWith(
color: ReclaimColors.signal,
fontFamily: ReclaimTypography.mono,
),
),
),
Expanded(
child: SelectableText(
p.text,
style: t.bodyLarge?.copyWith(height: 1.55),
),
),
],
),
),
const SizedBox(height: ReclaimSpace.sm),
SelectableText(
norm.sourceUrl,
style: t.labelSmall?.copyWith(
color: ReclaimColors.mute,
fontFamily: ReclaimTypography.mono,
),
),
],
),
);
}
String _isoDate(DateTime d) =>
'${d.year.toString().padLeft(4, "0")}-'
'${d.month.toString().padLeft(2, "0")}-'
'${d.day.toString().padLeft(2, "0")}';
Future<void> _openInBrowser(BuildContext context, String url) async {
final messenger = ScaffoldMessenger.of(context);
final uri = Uri.tryParse(url);
if (uri == null) {
messenger.showSnackBar(
SnackBar(content: Text('Ungültige URL: $url')),
);
return;
}
try {
final ok =
await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok) {
messenger.showSnackBar(
const SnackBar(content: Text('Browser-Aufruf abgelehnt.')),
);
}
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('Konnte Browser nicht öffnen: $e')),
);
}
}
}