From e08cedcd3fa96a3542a53e8d2c7c5563b048eaa0 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 18 Jun 2026 13:42:55 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(brand):=20rename=20product=20to=20Recl?= =?UTF-8?q?=E2=88=86Im=20(Reclaim)=20across=20app,=20bundle,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recl∆Im — the F∆I product line for Pflicht-basierte Wirkungsanalyse und Reform-Vorschlags-Pipelines. Replaces the working title 'Law-Heatmap', which described the demo view rather than the platform. Heatmap, Norms list, Hub view, Methodik are now four panes of the same Recl∆Im app. Renames in this commit: Flutter package lawheatmap_app -> reclaim_app macOS bundle id ai.flemming.lawheatmapApp -> ai.flemming.reclaim macOS PRODUCT_NAME lawheatmap_app -> reclaim_app Theme tokens LawHeatmapColors/Space/Radius/Typography/Theme -> Reclaim* Surface card LawHeatmapCard -> ReclaimCard Top-level widget LawHeatmapApp -> ReclaimApp Theme files lawheatmap_theme.dart/lawheatmap_tokens.dart -> reclaim_theme.dart/reclaim_tokens.dart Widget file lawheatmap_card.dart -> reclaim_card.dart Build script build-macos.sh paths and headline string Docs MACHBARKEITSSTUDIE.md, METHODIK.md, RUN.md, app/README.md, flow YAML UI strings 'F∆I Law-Heatmap' / 'Law-Heatmap' -> 'Recl∆Im' / 'Recl∆Im (F∆I)' for vendor- prefixed contexts flutter analyze: clean. flutter test: 2/2 passing. The local working directory stays fai_lawheatmap/ for now — the dir is referenced by the .claude project metadata; renaming it would break session continuity for no real gain. The Forgejo repo was renamed via API in the same change-set (fai/lawheatmap -> fai/reclaim), with the legacy slug serving a redirect courtesy of Forgejo's built-in rename handling. Signed-off-by: flemming-it --- MACHBARKEITSSTUDIE.md | 2 +- METHODIK.md | 2 +- RUN.md | 4 +- app/README.md | 4 +- app/build-macos.sh | 6 +- app/lib/main.dart | 14 ++-- app/lib/pages/about_page.dart | 38 +++++------ app/lib/pages/heatmap_page.dart | 28 ++++---- app/lib/pages/hub_status_page.dart | 58 ++++++++--------- app/lib/pages/landing_page.dart | 30 ++++----- app/lib/pages/norms_list_page.dart | 26 ++++---- app/lib/pages/shell_page.dart | 10 +-- ...wheatmap_theme.dart => reclaim_theme.dart} | 42 ++++++------ ...eatmap_tokens.dart => reclaim_tokens.dart} | 18 +++--- app/lib/widgets/delta_mark.dart | 4 +- app/lib/widgets/demo_banner.dart | 16 ++--- app/lib/widgets/evidence_sidebar.dart | 26 ++++---- app/lib/widgets/heatmap_grid.dart | 22 +++---- app/lib/widgets/hub_capabilities_card.dart | 20 +++--- ...lawheatmap_card.dart => reclaim_card.dart} | 12 ++-- app/lib/widgets/source_chip.dart | 10 +-- app/lib/widgets/tier_badge.dart | 14 ++-- app/macos/Runner.xcodeproj/project.pbxproj | 18 +++--- .../xcshareddata/xcschemes/Runner.xcscheme | 10 +-- app/macos/Runner/Configs/AppInfo.xcconfig | 4 +- app/pubspec.lock | 64 +++++++++++++++++++ app/pubspec.yaml | 5 +- app/test/widget_test.dart | 8 +-- flows/durchstich-gewo-14.yaml | 2 +- 29 files changed, 291 insertions(+), 226 deletions(-) rename app/lib/theme/{lawheatmap_theme.dart => reclaim_theme.dart} (73%) rename app/lib/theme/{lawheatmap_tokens.dart => reclaim_tokens.dart} (88%) rename app/lib/widgets/{lawheatmap_card.dart => reclaim_card.dart} (83%) diff --git a/MACHBARKEITSSTUDIE.md b/MACHBARKEITSSTUDIE.md index c1603d0..9305180 100644 --- a/MACHBARKEITSSTUDIE.md +++ b/MACHBARKEITSSTUDIE.md @@ -1,4 +1,4 @@ -# F∆I Law-Heatmap — Machbarkeitsstudie & Umsetzungsplan +# Recl∆Im (F∆I) — Machbarkeitsstudie & Umsetzungsplan > Arbeitstitel des Systems: **„Gesetzes-Heatmap" / „Regulierungs-Wirkungsanalyse auf Ch∆In-Basis"** > Auftraggeber-Kontext: MIT AG Digitalisierung & KI-Strategien (Vorsitz Dr. S. Flemming), diff --git a/METHODIK.md b/METHODIK.md index 6afb9a9..bb3d292 100644 --- a/METHODIK.md +++ b/METHODIK.md @@ -1,6 +1,6 @@ # Methodik-Papier — Pflicht-basierte Wirkungsanalyse von Rechtsnormen -> Standalone-Auszug aus der Machbarkeitsstudie F∆I Law-Heatmap, +> Standalone-Auszug aus der Machbarkeitsstudie Recl∆Im (F∆I), > destilliert für AG-Vorlage und Beirats-Konsultation. > > Version v0.1.0 · 2026-06 · Diskussionsgrundlage, AG-intern. diff --git a/RUN.md b/RUN.md index d4c6c08..05501ba 100644 --- a/RUN.md +++ b/RUN.md @@ -1,4 +1,4 @@ -# F∆I Law-Heatmap — Run-Guide (Phase 0) +# Recl∆Im (F∆I) — Run-Guide (Phase 0) > Stand v0.1.0 — Demo-Modus, ohne externe Daten, ohne Hub-Verbindung. @@ -39,7 +39,7 @@ eigene Crate-Verzeichnisse, ABI `chain:platform`, alle Apache 2.0: cd app ./setup.sh # Fonts + flutter pub get ./build-macos.sh # Robust-Build (strippt macOS-xattrs vor CodeSign) -open build/macos/Build/Products/Debug/lawheatmap_app.app +open build/macos/Build/Products/Debug/reclaim_app.app ``` Direkter `flutter run -d macos` funktioniert auch, kann aber bei diff --git a/app/README.md b/app/README.md index 241dc2b..e0c8e2e 100644 --- a/app/README.md +++ b/app/README.md @@ -1,6 +1,6 @@ -# lawheatmap_app +# reclaim_app -F∆I Law-Heatmap client +Recl∆Im (F∆I) client ## Getting Started diff --git a/app/build-macos.sh b/app/build-macos.sh index 87938f6..c4ff804 100755 --- a/app/build-macos.sh +++ b/app/build-macos.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Robust macOS build for the F∆I Law-Heatmap client. +# Robust macOS build for the Recl∆Im (F∆I) client. # # macOS xattrs (especially `com.apple.provenance` on files that # touched iCloud or were downloaded by curl) make Xcode's @@ -30,7 +30,7 @@ flutter build macos --debug \ echo " flutter exited $rc — continuing to xattr+resign step in case the binary linked" } -APP=build/macos/Build/Products/Debug/lawheatmap_app.app +APP=build/macos/Build/Products/Debug/reclaim_app.app if [ ! -d "$APP" ]; then echo "ERROR: $APP does not exist — flutter build failed before linking" exit 1 @@ -45,4 +45,4 @@ echo "[4/4] ad-hoc resign" echo echo "done. Launch with:" echo " open $APP" -echo " # or directly: $APP/Contents/MacOS/lawheatmap_app" +echo " # or directly: $APP/Contents/MacOS/reclaim_app" diff --git a/app/lib/main.dart b/app/lib/main.dart index 82db2aa..733c55c 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -5,13 +5,13 @@ import 'data/hub_endpoint.dart'; import 'data/hub_repository.dart'; import 'data/repository.dart'; import 'pages/landing_page.dart'; -import 'theme/lawheatmap_theme.dart'; +import 'theme/reclaim_theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); final settings = await HubSettings.load(); final repository = await _pickRepository(settings); - runApp(LawHeatmapApp(repository: repository, settings: settings)); + runApp(ReclaimApp(repository: repository, settings: settings)); } /// Decide which repository to start with. @@ -28,8 +28,8 @@ Future _pickRepository(HubSettings settings) async { return const MockRepository(); } -class LawHeatmapApp extends StatelessWidget { - const LawHeatmapApp({ +class ReclaimApp extends StatelessWidget { + const ReclaimApp({ super.key, this.repository = const MockRepository(), this.settings, @@ -41,10 +41,10 @@ class LawHeatmapApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'F∆I Law-Heatmap', + title: 'Recl∆Im', debugShowCheckedModeBanner: false, - theme: LawHeatmapTheme.light(), - darkTheme: LawHeatmapTheme.dark(), + theme: ReclaimTheme.light(), + darkTheme: ReclaimTheme.dark(), themeMode: ThemeMode.dark, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, diff --git a/app/lib/pages/about_page.dart b/app/lib/pages/about_page.dart index e7434f7..cff1356 100644 --- a/app/lib/pages/about_page.dart +++ b/app/lib/pages/about_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/lawheatmap_tokens.dart'; -import '../widgets/lawheatmap_card.dart'; +import '../theme/reclaim_tokens.dart'; +import '../widgets/reclaim_card.dart'; class AboutPage extends StatelessWidget { const AboutPage({super.key}); @@ -10,25 +10,25 @@ class AboutPage extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return SingleChildScrollView( - padding: const EdgeInsets.all(LawHeatmapSpace.xl), + padding: const EdgeInsets.all(ReclaimSpace.xl), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Methodik & Quellen', style: t.displaySmall), - const SizedBox(height: LawHeatmapSpace.xs), + const SizedBox(height: ReclaimSpace.xs), Text( 'Wie das System bewertet, woran es sich messen lässt.', - style: t.bodyLarge?.copyWith(color: LawHeatmapColors.mute), + style: t.bodyLarge?.copyWith(color: ReclaimColors.mute), ), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( accent: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Drei nicht verhandelbare Sätze', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.md), + const SizedBox(height: ReclaimSpace.md), Text( '1. Wir bewerten Pflichten, nicht Gesetze.\n' '2. Jede Zahl trägt die Tier-Stufe ihres schwächsten ' @@ -40,13 +40,13 @@ class AboutPage extends StatelessWidget { ], ), ), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Methodische Vorbilder', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), _bullet(t, 'Standardkostenmodell (SKM) — Nationaler Normenkontrollrat. P × F × T × h.'), _bullet(t, @@ -62,13 +62,13 @@ class AboutPage extends StatelessWidget { ], ), ), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('4-Tier-Evidenz-Modell', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), _bullet(t, 'T1 — amtlich/peer-reviewed: NKR, DESTATIS, ifo, ZEW, BMWK, SVR, IfM Bonn.'), _bullet(t, @@ -80,13 +80,13 @@ class AboutPage extends StatelessWidget { ], ), ), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Plattform', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text( 'Ch∆In v0.16.0 — Vendor: F∆I = Flemming.AI. ' 'Hub. Module. Flow. Deterministisch, WASM-sandboxed, ' @@ -104,7 +104,7 @@ class AboutPage extends StatelessWidget { Widget _bullet(TextTheme t, String text) { return Padding( - padding: const EdgeInsets.only(bottom: LawHeatmapSpace.sm), + padding: const EdgeInsets.only(bottom: ReclaimSpace.sm), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -114,7 +114,7 @@ class AboutPage extends StatelessWidget { width: 5, height: 5, decoration: const BoxDecoration( - color: LawHeatmapColors.signal, + color: ReclaimColors.signal, shape: BoxShape.circle, ), ), diff --git a/app/lib/pages/heatmap_page.dart b/app/lib/pages/heatmap_page.dart index 37dc7f1..9fd69ce 100644 --- a/app/lib/pages/heatmap_page.dart +++ b/app/lib/pages/heatmap_page.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import '../data/models.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; import '../widgets/demo_banner.dart'; import '../widgets/heatmap_grid.dart'; -import '../widgets/lawheatmap_card.dart'; +import '../widgets/reclaim_card.dart'; import '../widgets/tier_badge.dart'; import 'norm_detail_page.dart'; @@ -35,7 +35,7 @@ class _HeatmapPageState extends State { const DemoBanner(), Expanded( child: Padding( - padding: const EdgeInsets.all(LawHeatmapSpace.xl), + padding: const EdgeInsets.all(ReclaimSpace.xl), child: FutureBuilder>( future: _future, builder: (context, snap) { @@ -48,18 +48,18 @@ class _HeatmapPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Heatmap', style: t.displaySmall), - const SizedBox(height: LawHeatmapSpace.xs), + const SizedBox(height: ReclaimSpace.xs), Text( 'X: jährliche Bürokratiekosten (€, log10). ' 'Y: Nutzen-Score 0–5. Punktgröße: Betroffenheit. ' 'Klick auf einen Punkt → Detail mit Pflichten + Quellen.', style: t.bodyLarge?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), Expanded( - child: LawHeatmapCard( + child: ReclaimCard( accent: true, expand: true, child: HeatmapGrid( @@ -77,10 +77,10 @@ class _HeatmapPageState extends State { ), ), ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), Wrap( - spacing: LawHeatmapSpace.md, - runSpacing: LawHeatmapSpace.sm, + spacing: ReclaimSpace.md, + runSpacing: ReclaimSpace.sm, children: [ for (final e in items) InkWell( @@ -94,10 +94,10 @@ class _HeatmapPageState extends State { ), ); }, - child: LawHeatmapCard( + child: ReclaimCard( padding: const EdgeInsets.symmetric( - horizontal: LawHeatmapSpace.md, - vertical: LawHeatmapSpace.sm, + horizontal: ReclaimSpace.md, + vertical: ReclaimSpace.sm, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -105,7 +105,7 @@ class _HeatmapPageState extends State { TierBadge( tier: e.tierLowest, compact: true), const SizedBox( - width: LawHeatmapSpace.sm), + width: ReclaimSpace.sm), Text( '${e.norm.jurabk} ${e.norm.paragraph}', style: t.labelLarge, diff --git a/app/lib/pages/hub_status_page.dart b/app/lib/pages/hub_status_page.dart index 8784ecb..906677f 100644 --- a/app/lib/pages/hub_status_page.dart +++ b/app/lib/pages/hub_status_page.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import '../data/hub_endpoint.dart'; import '../data/hub_repository.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; import '../widgets/hub_capabilities_card.dart'; -import '../widgets/lawheatmap_card.dart'; +import '../widgets/reclaim_card.dart'; /// Hub connection + settings. Read-only summary at the top, then /// an editable form for endpoint host/port/secure/token, then a @@ -109,18 +109,18 @@ class _HubStatusPageState extends State { final repo = widget.repository; final s = _settings; return SingleChildScrollView( - padding: const EdgeInsets.all(LawHeatmapSpace.xl), + padding: const EdgeInsets.all(ReclaimSpace.xl), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Ch∆In Hub', style: t.displaySmall), - const SizedBox(height: LawHeatmapSpace.xs), + const SizedBox(height: ReclaimSpace.xs), Text( 'Status der Verbindung und Konfiguration des lokalen Hubs.', - style: t.bodyLarge?.copyWith(color: LawHeatmapColors.mute), + style: t.bodyLarge?.copyWith(color: ReclaimColors.mute), ), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( accent: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -142,23 +142,23 @@ class _HubStatusPageState extends State { ], ), ), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Verbindung konfigurieren', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text( '`chain serve` läuft standardmäßig auf ' '127.0.0.1:50051 ohne Auth. ' 'Bei aktiver Auth den Bearer-Token aus ' '~/.chain/hub-auth-token hier eintragen.', style: t.labelSmall - ?.copyWith(color: LawHeatmapColors.mute), + ?.copyWith(color: ReclaimColors.mute), ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), Row(children: [ Expanded( flex: 3, @@ -170,7 +170,7 @@ class _HubStatusPageState extends State { ), ), ), - const SizedBox(width: LawHeatmapSpace.md), + const SizedBox(width: ReclaimSpace.md), Expanded( child: TextField( controller: _portCtl, @@ -182,7 +182,7 @@ class _HubStatusPageState extends State { ), ), ]), - const SizedBox(height: LawHeatmapSpace.md), + const SizedBox(height: ReclaimSpace.md), TextField( controller: _tokenCtl, decoration: const InputDecoration( @@ -190,23 +190,23 @@ class _HubStatusPageState extends State { ), obscureText: true, ), - const SizedBox(height: LawHeatmapSpace.md), + const SizedBox(height: ReclaimSpace.md), Row(children: [ Switch( value: _secure, onChanged: (v) => setState(() => _secure = v), ), - const SizedBox(width: LawHeatmapSpace.sm), + const SizedBox(width: ReclaimSpace.sm), const Text('TLS (https)'), - const SizedBox(width: LawHeatmapSpace.xl), + const SizedBox(width: ReclaimSpace.xl), Switch( value: _useHub, onChanged: (v) => setState(() => _useHub = v), ), - const SizedBox(width: LawHeatmapSpace.sm), + const SizedBox(width: ReclaimSpace.sm), const Text('Hub beim Start nutzen'), ]), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), Row(children: [ FilledButton.icon( onPressed: _probing ? null : _probe, @@ -221,7 +221,7 @@ class _HubStatusPageState extends State { : const Icon(Icons.bolt_outlined), label: const Text('Verbindung testen'), ), - const SizedBox(width: LawHeatmapSpace.md), + const SizedBox(width: ReclaimSpace.md), OutlinedButton.icon( onPressed: _save, icon: const Icon(Icons.save_outlined), @@ -229,24 +229,24 @@ class _HubStatusPageState extends State { ), ]), if (_probeMessage != null) ...[ - const SizedBox(height: LawHeatmapSpace.md), + const SizedBox(height: ReclaimSpace.md), Text(_probeMessage!, style: t.labelLarge?.copyWith( - color: LawHeatmapColors.signal, + color: ReclaimColors.signal, )), ], ], ), ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), HubCapabilitiesCard(repository: repo), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Geplanter Live-Flow', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text( '1. `chain serve` auf localhost:50051 starten.\n' '2. Module installieren (`chain install`).\n' @@ -261,7 +261,7 @@ class _HubStatusPageState extends State { 'Evaluation-Objekte übersetzt.', style: t.bodyLarge?.copyWith( height: 1.6, - fontFamily: LawHeatmapTypography.mono, + fontFamily: ReclaimTypography.mono, fontSize: 12, ), ), @@ -282,12 +282,12 @@ class _HubStatusPageState extends State { width: 200, child: Text(label, style: t.labelLarge - ?.copyWith(color: LawHeatmapColors.mute)), + ?.copyWith(color: ReclaimColors.mute)), ), Expanded( child: Text(value, style: t.bodyLarge - ?.copyWith(fontFamily: LawHeatmapTypography.mono)), + ?.copyWith(fontFamily: ReclaimTypography.mono)), ), ], ), diff --git a/app/lib/pages/landing_page.dart b/app/lib/pages/landing_page.dart index a10745d..0f19bac 100644 --- a/app/lib/pages/landing_page.dart +++ b/app/lib/pages/landing_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_theme.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_theme.dart'; +import '../theme/reclaim_tokens.dart'; import '../widgets/delta_mark.dart'; import 'shell_page.dart'; @@ -23,28 +23,28 @@ class LandingPage extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 720), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: LawHeatmapSpace.xxl, - vertical: LawHeatmapSpace.xxxl, + horizontal: ReclaimSpace.xxl, + vertical: ReclaimSpace.xxxl, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ const DeltaMark(size: 96), - const SizedBox(height: LawHeatmapSpace.xxl), - Text('F∆I Law-Heatmap', style: t.displaySmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.xxl), + Text('Recl∆Im', style: t.displaySmall), + const SizedBox(height: ReclaimSpace.sm), Text( 'Pflicht-basierte Wirkungsanalyse von Rechtsnormen — ' 'auf Ch∆In, mit Quellen-Audit by construction.', style: t.bodyLarge - ?.copyWith(color: LawHeatmapColors.mute), + ?.copyWith(color: ReclaimColors.mute), ), - const SizedBox(height: LawHeatmapSpace.xxl), + const SizedBox(height: ReclaimSpace.xxl), Wrap( crossAxisAlignment: WrapCrossAlignment.center, - spacing: LawHeatmapSpace.md, - runSpacing: LawHeatmapSpace.sm, + spacing: ReclaimSpace.md, + runSpacing: ReclaimSpace.sm, children: [ FilledButton.icon( onPressed: () { @@ -61,18 +61,18 @@ class LandingPage extends StatelessWidget { Text( 'Demo: 5 Pilot-Normen, ohne Hub-Verbindung', style: t.labelSmall?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ), ], ), - const SizedBox(height: LawHeatmapSpace.xl), + const SizedBox(height: ReclaimSpace.xl), Text( 'v0.1.0 · Phase 0 · Mock-Modus · ' 'Live-Hub-Anbindung folgt in Woche 1', - style: LawHeatmapTheme.mono( + style: ReclaimTheme.mono( t.labelSmall ?? const TextStyle(), - ).copyWith(color: LawHeatmapColors.mute), + ).copyWith(color: ReclaimColors.mute), ), ], ), diff --git a/app/lib/pages/norms_list_page.dart b/app/lib/pages/norms_list_page.dart index 9f0d9b2..189a1fb 100644 --- a/app/lib/pages/norms_list_page.dart +++ b/app/lib/pages/norms_list_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import '../data/models.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; import '../widgets/demo_banner.dart'; -import '../widgets/lawheatmap_card.dart'; +import '../widgets/reclaim_card.dart'; import '../widgets/tier_badge.dart'; import 'norm_detail_page.dart'; @@ -21,20 +21,20 @@ class NormsListPage extends StatelessWidget { const DemoBanner(), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.all(LawHeatmapSpace.xl), + padding: const EdgeInsets.all(ReclaimSpace.xl), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Normen-Pilotkorpus', style: t.displaySmall), - const SizedBox(height: LawHeatmapSpace.xs), + const SizedBox(height: ReclaimSpace.xs), Text( '5 Normen für den vertikalen Durchstich. ' 'Korpus erweiterbar in Phase 1.', style: t.bodyLarge?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), FutureBuilder>( future: repository.list(), builder: (context, snap) { @@ -47,7 +47,7 @@ class NormsListPage extends StatelessWidget { for (final e in snap.data!) Padding( padding: const EdgeInsets.only( - bottom: LawHeatmapSpace.md), + bottom: ReclaimSpace.md), child: _NormRow( evaluation: e, repository: repository, @@ -86,7 +86,7 @@ class _NormRow extends StatelessWidget { ), ); }, - child: LawHeatmapCard( + child: ReclaimCard( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -103,14 +103,14 @@ class _NormRow extends StatelessWidget { Text( evaluation.norm.eli, style: t.labelSmall?.copyWith( - fontFamily: LawHeatmapTypography.mono, - color: LawHeatmapColors.mute, + fontFamily: ReclaimTypography.mono, + color: ReclaimColors.mute, ), ), ], ), ), - const SizedBox(width: LawHeatmapSpace.lg), + const SizedBox(width: ReclaimSpace.lg), Expanded( child: _MetricColumn( label: 'Schaden / Jahr', @@ -130,7 +130,7 @@ class _NormRow extends StatelessWidget { value: _kmu(evaluation.affectedCount), ), ), - const SizedBox(width: LawHeatmapSpace.md), + const SizedBox(width: ReclaimSpace.md), TierBadge(tier: evaluation.tierLowest), ], ), @@ -166,7 +166,7 @@ class _MetricColumn extends StatelessWidget { children: [ Text( label, - style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + style: t.labelSmall?.copyWith(color: ReclaimColors.mute), ), const SizedBox(height: 2), Text(value, style: t.titleMedium), diff --git a/app/lib/pages/shell_page.dart b/app/lib/pages/shell_page.dart index 75dc952..fcbebde 100644 --- a/app/lib/pages/shell_page.dart +++ b/app/lib/pages/shell_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; import '../widgets/delta_mark.dart'; import 'about_page.dart'; import 'heatmap_page.dart'; @@ -42,18 +42,18 @@ class _ShellPageState extends State { onDestinationSelected: (i) => setState(() => _index = i), leading: Padding( padding: const EdgeInsets.symmetric( - vertical: LawHeatmapSpace.lg, - horizontal: LawHeatmapSpace.md, + vertical: ReclaimSpace.lg, + horizontal: ReclaimSpace.md, ), child: Column( children: [ const DeltaMark(size: 36), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text( 'F∆I', style: t.labelLarge?.copyWith( letterSpacing: 2, - color: LawHeatmapColors.signal, + color: ReclaimColors.signal, ), ), ], diff --git a/app/lib/theme/lawheatmap_theme.dart b/app/lib/theme/reclaim_theme.dart similarity index 73% rename from app/lib/theme/lawheatmap_theme.dart rename to app/lib/theme/reclaim_theme.dart index c894707..012a9b2 100644 --- a/app/lib/theme/lawheatmap_theme.dart +++ b/app/lib/theme/reclaim_theme.dart @@ -1,35 +1,35 @@ import 'package:flutter/material.dart'; -import 'lawheatmap_tokens.dart'; +import 'reclaim_tokens.dart'; -/// ThemeData assembly for the F∆I Law-Heatmap client. +/// ThemeData assembly for the F∆I Recl∆Im client. /// /// Dark mode is the default (consistent with fai_web's /// `color-scheme: dark`). Light mode falls back to the paper /// palette for printing/screenshot work. -class LawHeatmapTheme { - LawHeatmapTheme._(); +class ReclaimTheme { + ReclaimTheme._(); static ThemeData dark() => _build(Brightness.dark); static ThemeData light() => _build(Brightness.light); static ThemeData _build(Brightness brightness) { final isDark = brightness == Brightness.dark; - final canvas = isDark ? LawHeatmapColors.ink : LawHeatmapColors.paper; + final canvas = isDark ? ReclaimColors.ink : ReclaimColors.paper; final surface = - isDark ? LawHeatmapColors.inkRaised : LawHeatmapColors.paperRaised; + isDark ? ReclaimColors.inkRaised : ReclaimColors.paperRaised; final onSurface = isDark ? const Color(0xFFE9E7E2) : const Color(0xFF1A1B1E); final colorScheme = ColorScheme( brightness: brightness, - primary: LawHeatmapColors.signal, - onPrimary: LawHeatmapColors.ink, - secondary: LawHeatmapColors.mute, + primary: ReclaimColors.signal, + onPrimary: ReclaimColors.ink, + secondary: ReclaimColors.mute, onSecondary: onSurface, error: const Color(0xFFD8723A), - onError: LawHeatmapColors.ink, + onError: ReclaimColors.ink, surface: surface, onSurface: onSurface, ); @@ -42,33 +42,33 @@ class LawHeatmapTheme { colorScheme: colorScheme, scaffoldBackgroundColor: canvas, canvasColor: canvas, - fontFamily: LawHeatmapTypography.body, + fontFamily: ReclaimTypography.body, textTheme: textTheme, cardTheme: CardThemeData( color: surface, elevation: 0, margin: EdgeInsets.zero, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(LawHeatmapRadius.md), + borderRadius: BorderRadius.all(ReclaimRadius.md), side: BorderSide(color: Color(0x24FFFFFF)), ), ), dividerTheme: DividerThemeData( - color: onSurface.withValues(alpha: LawHeatmapColors.hairlineDarkOpacity), + color: onSurface.withValues(alpha: ReclaimColors.hairlineDarkOpacity), thickness: 1, space: 1, ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( - backgroundColor: LawHeatmapColors.signal, - foregroundColor: LawHeatmapColors.ink, + backgroundColor: ReclaimColors.signal, + foregroundColor: ReclaimColors.ink, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(LawHeatmapRadius.sm), + borderRadius: BorderRadius.all(ReclaimRadius.sm), ), textStyle: textTheme.labelLarge, padding: const EdgeInsets.symmetric( - horizontal: LawHeatmapSpace.lg, - vertical: LawHeatmapSpace.md, + horizontal: ReclaimSpace.lg, + vertical: ReclaimSpace.md, ), ), ), @@ -76,8 +76,8 @@ class LawHeatmapTheme { } static TextTheme _textTheme(Color onSurface) { - const display = LawHeatmapTypography.display; - const body = LawHeatmapTypography.body; + const display = ReclaimTypography.display; + const body = ReclaimTypography.body; return TextTheme( displaySmall: TextStyle( fontFamily: display, @@ -123,5 +123,5 @@ class LawHeatmapTheme { /// Convenience: JetBrains-Mono variant of an existing style for /// technical content (ELI/CELEX, SHA-256, audit-event IDs). static TextStyle mono(TextStyle base) => - base.copyWith(fontFamily: LawHeatmapTypography.mono); + base.copyWith(fontFamily: ReclaimTypography.mono); } diff --git a/app/lib/theme/lawheatmap_tokens.dart b/app/lib/theme/reclaim_tokens.dart similarity index 88% rename from app/lib/theme/lawheatmap_tokens.dart rename to app/lib/theme/reclaim_tokens.dart index a473a3e..654bde6 100644 --- a/app/lib/theme/lawheatmap_tokens.dart +++ b/app/lib/theme/reclaim_tokens.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -/// Visual tokens for the F∆I Law-Heatmap client, sourced from the +/// Visual tokens for the F∆I Recl∆Im client, sourced from the /// fai_web brand palette (see fai_web/src/styles/global.css:14-20). /// /// Unlike `fai_chain_studio`'s `ChainColors` (Tailwind sky-400 — /// dev-tool look), this palette matches the public-facing brand: /// ink/paper with a petrol "signal" accent that doubles as the eye /// of the `∆` mark. -class LawHeatmapColors { - LawHeatmapColors._(); +class ReclaimColors { + ReclaimColors._(); // Core palette (fai_web/src/styles/global.css) static const ink = Color(0xFF08090A); @@ -40,8 +40,8 @@ class LawHeatmapColors { /// Spacing scale — 4/8/12/16/24/32/48 multiples, matching /// `ChainSpace` from `fai_chain_studio/lib/theme/tokens.dart`. -class LawHeatmapSpace { - LawHeatmapSpace._(); +class ReclaimSpace { + ReclaimSpace._(); static const xs = 4.0; static const sm = 8.0; @@ -52,15 +52,15 @@ class LawHeatmapSpace { static const xxxl = 48.0; } -class LawHeatmapRadius { - LawHeatmapRadius._(); +class ReclaimRadius { + ReclaimRadius._(); static const sm = Radius.circular(6); static const md = Radius.circular(10); } -class LawHeatmapTypography { - LawHeatmapTypography._(); +class ReclaimTypography { + ReclaimTypography._(); static const display = 'SpaceGroteskVariable'; static const body = 'InterVariable'; diff --git a/app/lib/widgets/delta_mark.dart b/app/lib/widgets/delta_mark.dart index 30db44d..fd921cb 100644 --- a/app/lib/widgets/delta_mark.dart +++ b/app/lib/widgets/delta_mark.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; /// The F∆I / Ch∆In brand mark: an upward triangle (the `∆`) with /// a petrol "eye" at the centre. Geometry mirrors fai_web's @@ -89,7 +89,7 @@ class _DeltaPainter extends CustomPainter { // Eye — petrol fill, centred on the triangle's centroid. final eyeCentre = Offset(w / 2, inset + (h - 2 * inset) * 2 / 3); final eyeR = w * 0.06 * pulse; - final eye = Paint()..color = LawHeatmapColors.signal; + final eye = Paint()..color = ReclaimColors.signal; canvas.drawCircle(eyeCentre, eyeR, eye); } diff --git a/app/lib/widgets/demo_banner.dart b/app/lib/widgets/demo_banner.dart index b697bc4..0d41fc0 100644 --- a/app/lib/widgets/demo_banner.dart +++ b/app/lib/widgets/demo_banner.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; /// Permanent banner shown above all data screens in mock mode. /// Carries the political-defensibility caveat: this is demo data, @@ -14,29 +14,29 @@ class DemoBanner extends StatelessWidget { return Container( width: double.infinity, padding: const EdgeInsets.symmetric( - horizontal: LawHeatmapSpace.lg, - vertical: LawHeatmapSpace.sm, + horizontal: ReclaimSpace.lg, + vertical: ReclaimSpace.sm, ), decoration: BoxDecoration( - color: LawHeatmapColors.tierT4.withValues(alpha: 0.12), + color: ReclaimColors.tierT4.withValues(alpha: 0.12), border: Border( bottom: BorderSide( - color: LawHeatmapColors.tierT4.withValues(alpha: 0.55), + color: ReclaimColors.tierT4.withValues(alpha: 0.55), ), ), ), child: Row( children: [ const Icon(Icons.science_outlined, - size: 18, color: LawHeatmapColors.tierT4), - const SizedBox(width: LawHeatmapSpace.sm), + size: 18, color: ReclaimColors.tierT4), + const SizedBox(width: ReclaimSpace.sm), Expanded( child: Text( 'Demo-Daten — Phase 0, ohne Verbands-Erhebung. ' 'Keine Juristen-Approval, keine Beirats-Validierung. ' 'Alle Werte tragen Tier T4, bis T1/T2-Pipeline läuft.', style: t.labelSmall?.copyWith( - color: LawHeatmapColors.tierT4, + color: ReclaimColors.tierT4, ), ), ), diff --git a/app/lib/widgets/evidence_sidebar.dart b/app/lib/widgets/evidence_sidebar.dart index 070f313..ab4aa7a 100644 --- a/app/lib/widgets/evidence_sidebar.dart +++ b/app/lib/widgets/evidence_sidebar.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import '../data/models.dart'; -import '../theme/lawheatmap_tokens.dart'; -import 'lawheatmap_card.dart'; +import '../theme/reclaim_tokens.dart'; +import 'reclaim_card.dart'; import 'source_chip.dart'; /// Sidebar listing the citable sources behind an evaluation. @@ -16,39 +16,39 @@ class EvidenceSidebar extends StatelessWidget { @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; - return LawHeatmapCard( + return ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Quellen', style: t.headlineSmall), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text( 'Jede Zahl trägt die Tier-Stufe ihres schwächsten ' 'Bestandteils (Regel der niedrigsten Stufe, §6.7).', - style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + style: t.labelSmall?.copyWith(color: ReclaimColors.mute), ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), for (final s in evaluation.sources) SourceChip(source: s), - const Divider(height: LawHeatmapSpace.xl), + const Divider(height: ReclaimSpace.xl), Text('Audit-Event', style: t.labelLarge), const SizedBox(height: 4), SelectableText( evaluation.auditEventId, style: t.labelSmall?.copyWith( - fontFamily: LawHeatmapTypography.mono, - color: LawHeatmapColors.mute, + fontFamily: ReclaimTypography.mono, + color: ReclaimColors.mute, ), ), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text( 'Quell-SHA-256:', - style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + style: t.labelSmall?.copyWith(color: ReclaimColors.mute), ), SelectableText( evaluation.norm.sourceSha256, style: t.labelSmall?.copyWith( - fontFamily: LawHeatmapTypography.mono, - color: LawHeatmapColors.mute, + fontFamily: ReclaimTypography.mono, + color: ReclaimColors.mute, fontSize: 10, ), ), diff --git a/app/lib/widgets/heatmap_grid.dart b/app/lib/widgets/heatmap_grid.dart index bf2fa41..3abb1a8 100644 --- a/app/lib/widgets/heatmap_grid.dart +++ b/app/lib/widgets/heatmap_grid.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import '../data/models.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; /// 2D scatter heatmap — x: Schaden (€/Jahr, log-scaled), /// y: Nutzen (0–5), point area: Betroffenheit (sqrt-scaled). @@ -121,7 +121,7 @@ class _HeatmapPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final layout = _Layout(size); final onSurface = theme.colorScheme.onSurface; - const mute = LawHeatmapColors.mute; + const mute = ReclaimColors.mute; // Plot frame. final frame = Paint() @@ -163,7 +163,7 @@ class _HeatmapPainter extends CustomPainter { const axisStyle = TextStyle( color: mute, fontSize: 10, - fontFamily: LawHeatmapTypography.body, + fontFamily: ReclaimTypography.body, ); _drawText( canvas, @@ -205,8 +205,8 @@ class _HeatmapPainter extends CustomPainter { final r = layout.radius(e); final hot = e.skmEurPerYear > 50000000; final color = Color.lerp( - LawHeatmapColors.benefitSaturated, - LawHeatmapColors.harmWarm, + ReclaimColors.benefitSaturated, + ReclaimColors.harmWarm, hot ? 0.5 : 0.0, )!; final body = Paint()..color = color.withValues(alpha: 0.7); @@ -224,7 +224,7 @@ class _HeatmapPainter extends CustomPainter { TextStyle( color: onSurface, fontSize: 11, - fontFamily: LawHeatmapTypography.body, + fontFamily: ReclaimTypography.body, fontWeight: i == hoverIndex ? FontWeight.w600 : FontWeight.w400, ), ); @@ -240,8 +240,8 @@ class _HeatmapPainter extends CustomPainter { ..color = theme.colorScheme.surface.withValues(alpha: 0.92); final boxBorder = Paint() ..style = PaintingStyle.stroke - ..color = LawHeatmapColors.signal.withValues(alpha: 0.6); - final rrect = RRect.fromRectAndRadius(box, LawHeatmapRadius.sm); + ..color = ReclaimColors.signal.withValues(alpha: 0.6); + final rrect = RRect.fromRectAndRadius(box, ReclaimRadius.sm); canvas.drawRRect(rrect, boxPaint); canvas.drawRRect(rrect, boxBorder); _drawText( @@ -252,7 +252,7 @@ class _HeatmapPainter extends CustomPainter { color: onSurface, fontSize: 12, fontWeight: FontWeight.w600, - fontFamily: LawHeatmapTypography.body, + fontFamily: ReclaimTypography.body, ), ); _drawText( @@ -262,7 +262,7 @@ class _HeatmapPainter extends CustomPainter { const TextStyle( color: mute, fontSize: 11, - fontFamily: LawHeatmapTypography.body, + fontFamily: ReclaimTypography.body, ), ); _drawText( @@ -272,7 +272,7 @@ class _HeatmapPainter extends CustomPainter { const TextStyle( color: mute, fontSize: 11, - fontFamily: LawHeatmapTypography.body, + fontFamily: ReclaimTypography.body, ), ); } diff --git a/app/lib/widgets/hub_capabilities_card.dart b/app/lib/widgets/hub_capabilities_card.dart index 42d644e..3ed4d51 100644 --- a/app/lib/widgets/hub_capabilities_card.dart +++ b/app/lib/widgets/hub_capabilities_card.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import '../data/hub_repository.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_tokens.dart'; -import 'lawheatmap_card.dart'; +import '../theme/reclaim_tokens.dart'; +import 'reclaim_card.dart'; /// Lists the capabilities the connected hub has installed. /// Empty / explanatory state when the active repository is not @@ -39,7 +39,7 @@ class _HubCapabilitiesCardState extends State { final repo = widget.repository; final liveHub = repo is HubRepository && repo.isHealthy; - return LawHeatmapCard( + return ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -53,13 +53,13 @@ class _HubCapabilitiesCardState extends State { icon: const Icon(Icons.refresh), ), ]), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), if (!liveHub) Text( 'Sichtbar sobald die App im Hub-Modus startet und ' 'der Hub erreichbar ist (siehe Verbindung oben).', style: t.labelSmall?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ) else @@ -68,14 +68,14 @@ class _HubCapabilitiesCardState extends State { builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Padding( - padding: EdgeInsets.all(LawHeatmapSpace.md), + padding: EdgeInsets.all(ReclaimSpace.md), child: CircularProgressIndicator(), ); } if (snap.hasError) { return Text('Fehler: ${snap.error}', style: t.labelLarge - ?.copyWith(color: LawHeatmapColors.harmWarm)); + ?.copyWith(color: ReclaimColors.harmWarm)); } final items = snap.data ?? const []; if (items.isEmpty) { @@ -83,7 +83,7 @@ class _HubCapabilitiesCardState extends State { 'Hub hat noch keine Capabilities installiert. ' 'Mit `chain install ` hinzufügen.', style: t.bodyLarge?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ); } @@ -101,7 +101,7 @@ class _HubCapabilitiesCardState extends State { child: Text( '${c.capability}@${c.version}', style: t.bodyLarge?.copyWith( - fontFamily: LawHeatmapTypography.mono, + fontFamily: ReclaimTypography.mono, ), ), ), @@ -110,7 +110,7 @@ class _HubCapabilitiesCardState extends State { '${c.moduleName} ${c.moduleVersion}' '${c.kind.isEmpty ? "" : " · ${c.kind}"}', style: t.labelSmall?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ), ), diff --git a/app/lib/widgets/lawheatmap_card.dart b/app/lib/widgets/reclaim_card.dart similarity index 83% rename from app/lib/widgets/lawheatmap_card.dart rename to app/lib/widgets/reclaim_card.dart index 8a84701..891956f 100644 --- a/app/lib/widgets/lawheatmap_card.dart +++ b/app/lib/widgets/reclaim_card.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; /// Surface container — visual equivalent of fai_chain_studio's /// `ChainCard`, retuned to the fai_web palette. -class LawHeatmapCard extends StatelessWidget { - const LawHeatmapCard({ +class ReclaimCard extends StatelessWidget { + const ReclaimCard({ super.key, required this.child, this.padding = - const EdgeInsets.all(LawHeatmapSpace.lg), + const EdgeInsets.all(ReclaimSpace.lg), this.accent = false, this.expand = false, }); @@ -39,9 +39,9 @@ class LawHeatmapCard extends StatelessWidget { Container( height: 2, decoration: const BoxDecoration( - color: LawHeatmapColors.signal, + color: ReclaimColors.signal, borderRadius: BorderRadius.vertical( - top: LawHeatmapRadius.md, + top: ReclaimRadius.md, ), ), ), diff --git a/app/lib/widgets/source_chip.dart b/app/lib/widgets/source_chip.dart index ca81fa7..fdd16c0 100644 --- a/app/lib/widgets/source_chip.dart +++ b/app/lib/widgets/source_chip.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../data/models.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; import 'tier_badge.dart'; /// A citable source line: tier badge + label + (planned) link. @@ -16,7 +16,7 @@ class SourceChip extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return Padding( - padding: const EdgeInsets.only(bottom: LawHeatmapSpace.sm), + padding: const EdgeInsets.only(bottom: ReclaimSpace.sm), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -24,7 +24,7 @@ class SourceChip extends StatelessWidget { padding: const EdgeInsets.only(top: 2), child: TierBadge(tier: source.tier, compact: true), ), - const SizedBox(width: LawHeatmapSpace.sm), + const SizedBox(width: ReclaimSpace.sm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -33,14 +33,14 @@ class SourceChip extends StatelessWidget { Text( source.url, style: t.labelSmall?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, ), ), if (source.note != null) Text( source.note!, style: t.labelSmall?.copyWith( - color: LawHeatmapColors.mute, + color: ReclaimColors.mute, fontStyle: FontStyle.italic, ), ), diff --git a/app/lib/widgets/tier_badge.dart b/app/lib/widgets/tier_badge.dart index b194a6b..282eed6 100644 --- a/app/lib/widgets/tier_badge.dart +++ b/app/lib/widgets/tier_badge.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../data/models.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; /// Tier badge — surfaces the lowest evidence tier in a composed /// value. Every figure in the app carries one of these next to it. @@ -15,22 +15,22 @@ class TierBadge extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context).textTheme; final color = switch (tier) { - EvidenceTier.t1 => LawHeatmapColors.tierT1, - EvidenceTier.t2 => LawHeatmapColors.tierT2, - EvidenceTier.t3 => LawHeatmapColors.tierT3, - EvidenceTier.t4 => LawHeatmapColors.tierT4, + EvidenceTier.t1 => ReclaimColors.tierT1, + EvidenceTier.t2 => ReclaimColors.tierT2, + EvidenceTier.t3 => ReclaimColors.tierT3, + EvidenceTier.t4 => ReclaimColors.tierT4, }; return Tooltip( message: '${tier.short} — ${tier.description}', child: Container( padding: EdgeInsets.symmetric( - horizontal: compact ? 6 : LawHeatmapSpace.sm, + horizontal: compact ? 6 : ReclaimSpace.sm, vertical: compact ? 2 : 4, ), decoration: BoxDecoration( color: color.withValues(alpha: 0.18), border: Border.all(color: color.withValues(alpha: 0.55)), - borderRadius: const BorderRadius.all(LawHeatmapRadius.sm), + borderRadius: const BorderRadius.all(ReclaimRadius.sm), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj index 07f152c..00d1a7d 100644 --- a/app/macos/Runner.xcodeproj/project.pbxproj +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -65,7 +65,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* lawheatmap_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "lawheatmap_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* reclaim_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "reclaim_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -134,7 +134,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* lawheatmap_app.app */, + 33CC10ED2044A3C60003C045 /* reclaim_app.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -224,7 +224,7 @@ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, ); productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* lawheatmap_app.app */; + productReference = 33CC10ED2044A3C60003C045 /* reclaim_app.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -395,10 +395,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.reclaim.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lawheatmap_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lawheatmap_app"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/reclaim_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/reclaim_app"; }; name = Debug; }; @@ -409,10 +409,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.reclaim.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lawheatmap_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lawheatmap_app"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/reclaim_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/reclaim_app"; }; name = Release; }; @@ -423,10 +423,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.reclaim.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lawheatmap_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lawheatmap_app"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/reclaim_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/reclaim_app"; }; name = Profile; }; diff --git a/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 45c3a4d..a74a1e8 100644 --- a/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -33,7 +33,7 @@ @@ -49,7 +49,7 @@ @@ -84,7 +84,7 @@ @@ -101,7 +101,7 @@ diff --git a/app/macos/Runner/Configs/AppInfo.xcconfig b/app/macos/Runner/Configs/AppInfo.xcconfig index 3ad4d22..51f8e2b 100644 --- a/app/macos/Runner/Configs/AppInfo.xcconfig +++ b/app/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = lawheatmap_app +PRODUCT_NAME = reclaim_app // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp +PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.reclaim // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2026 ai.flemming. All rights reserved. diff --git a/app/pubspec.lock b/app/pubspec.lock index 5cb87e4..c077d58 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -409,6 +409,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 0eb45b5..c43960b 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,5 +1,5 @@ -name: lawheatmap_app -description: F∆I Law-Heatmap — Flutter client for the Ch∆In-based law-bureaucracy heatmap. +name: reclaim_app +description: Recl∆Im — Flutter client for the Ch∆In-based duty-level reform-mapping platform (F∆I). publish_to: 'none' version: 0.1.0+1 @@ -14,6 +14,7 @@ dependencies: sdk: flutter intl: ^0.20.2 shared_preferences: ^2.3.0 + url_launcher: ^6.3.0 # Ch∆In gRPC wire — sibling-path dep, same pattern as # fai_chain_studio. Adapt the chain_client_sdk_dart relative diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 873b29f..eb83a95 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lawheatmap_app/main.dart'; +import 'package:reclaim_app/main.dart'; void main() { // Generous surface so NavigationRail + Heatmap fit without @@ -18,9 +18,9 @@ void main() { testWidgets('Landing renders brand mark + CTA', (WidgetTester tester) async { await tester.binding.setSurfaceSize(surfaceSize); - await tester.pumpWidget(const LawHeatmapApp()); + await tester.pumpWidget(const ReclaimApp()); await tester.pump(); - expect(find.text('F∆I Law-Heatmap'), findsOneWidget); + expect(find.text('Recl∆Im'), findsOneWidget); expect( find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'), findsOneWidget, @@ -31,7 +31,7 @@ void main() { testWidgets('Tapping CTA reaches the shell and shows the heatmap', (WidgetTester tester) async { await tester.binding.setSurfaceSize(surfaceSize); - await tester.pumpWidget(const LawHeatmapApp()); + await tester.pumpWidget(const ReclaimApp()); await tester.pump(); await tester.tap( find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'), diff --git a/flows/durchstich-gewo-14.yaml b/flows/durchstich-gewo-14.yaml index d7eb63f..5b2162d 100644 --- a/flows/durchstich-gewo-14.yaml +++ b/flows/durchstich-gewo-14.yaml @@ -122,7 +122,7 @@ steps: - id: review use: system.approval@^0 with: - title: "F∆I Law-Heatmap — Review ${{ normalize.norm.eli }}" + title: "Recl∆Im (F∆I) — Review ${{ normalize.norm.eli }}" payload: norm: ${{ normalize.norm }} skm: ${{ skm.report }} From 116310b65b384ef55e22d93bd38e8acd78bc4503 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 18 Jun 2026 13:43:22 +0200 Subject: [PATCH 2/2] feat(ui): inline the normative text + open-source URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the actual paragraphs of the law right next to the evaluation panel on the norm-detail page. A reviewer no longer has to leave the app to confirm what a score is about; the canonical source URL stays one click away via 'Im Browser öffnen' (url_launcher) and one click away as a clipboard copy. Schema models.dart gains a NormParagraph (number + text). Norm carries a List paragraphs, defaulted to const [] so norms harvested by text.akoma_normalize@^0 in live mode slot in without breaking the Mock path. Demo data fixtures.dart embeds the public source paragraphs verbatim under §5 UrhG (amtliche Werke) for the five pilot norms. Plumbing NormTextCard (lib/widgets/norm_text_card.dart) is the renderer: per-paragraph hanging label, selectable body text, source URL in mono at the bottom, two action buttons. NormDetailPage drops the card between the metric row and the duties card so the eye lands on it during a 'why this score?' read-through. macOS sandbox The Release entitlement file gains com.apple.security.network.client so url_launcher's NSWorkspace call and the HubRepository's gRPC channel can both reach the network. The DebugProfile already had server; client matches the production posture. GeneratedPluginRegistrant.swift was re-generated by flutter pub get after url_launcher was added — no hand edits. flutter analyze: clean. flutter test: 2/2 passing. The macOS build via build-macos.sh launches and the new card renders the GewO § 14, KassenSichV, DSGVO Art. 30, BauO Bln § 60 ff and MiLoG § 17 fixtures with their actual normative text. Signed-off-by: flemming-it --- app/lib/data/fixtures.dart | 135 +++++++++++++++++ app/lib/data/models.dart | 19 ++- app/lib/pages/norm_detail_page.dart | 67 ++++---- app/lib/widgets/norm_text_card.dart | 143 ++++++++++++++++++ .../Flutter/GeneratedPluginRegistrant.swift | 2 + app/macos/Runner/DebugProfile.entitlements | 2 + app/macos/Runner/Release.entitlements | 2 + 7 files changed, 337 insertions(+), 33 deletions(-) create mode 100644 app/lib/widgets/norm_text_card.dart diff --git a/app/lib/data/fixtures.dart b/app/lib/data/fixtures.dart index 57fcd25..18980ee 100644 --- a/app/lib/data/fixtures.dart +++ b/app/lib/data/fixtures.dart @@ -29,6 +29,26 @@ class Fixtures { standDate: DateTime.utc(2025, 12, 1), sourceUrl: 'https://www.gesetze-im-internet.de/gewo/__14.html', sourceSha256: 'demo-${'a' * 60}', + paragraphs: const [ + NormParagraph( + number: '(1)', + text: 'Wer den selbständigen Betrieb eines stehenden Gewerbes, ' + 'einer Zweigniederlassung oder einer unselbständigen ' + 'Zweigstelle anfängt, muss dies der zuständigen Behörde ' + 'gleichzeitig anzeigen. Das Gleiche gilt, wenn ' + '1. der Betrieb verlegt wird, ' + '2. der Gegenstand des Gewerbes gewechselt oder ' + 'auf Waren oder Leistungen ausgedehnt wird, die bei ' + 'Gewerbebetrieben der angemeldeten Art nicht geschäftsüblich ' + 'sind, oder ' + '3. der Betrieb aufgegeben wird.', + ), + NormParagraph( + number: '(2)', + text: 'Die Anzeige hat unverzüglich zu erfolgen. Sie ist nach ' + 'amtlich vorgeschriebenem Vordruck zu erstatten.', + ), + ], ), skmEurPerYear: 2_800_000, benefitScore: 2.5, @@ -78,6 +98,30 @@ class Fixtures { sourceUrl: 'https://www.gesetze-im-internet.de/kassensichv/index.html', sourceSha256: 'demo-${'b' * 60}', + paragraphs: const [ + NormParagraph( + number: '§ 1', + text: 'Diese Verordnung gilt für elektronische ' + 'Aufzeichnungssysteme im Sinne des § 146a Absatz 1 ' + 'Satz 1 der Abgabenordnung, mit denen aufzeichnungspflichtige ' + 'Geschäftsvorfälle oder andere Vorgänge erfasst werden.', + ), + NormParagraph( + number: '§ 2', + text: 'Das elektronische Aufzeichnungssystem muss jeden ' + 'einzelnen aufzeichnungspflichtigen Vorgang vollständig ' + 'aufzeichnen. Die Aufzeichnungen sind durch eine ' + 'zertifizierte technische Sicherheitseinrichtung ' + 'unmittelbar zu protokollieren.', + ), + NormParagraph( + number: '§ 3', + text: 'Den Steuerpflichtigen treffen Mitteilungspflichten ' + 'gegenüber dem zuständigen Finanzamt; der Ausfall einer ' + 'zertifizierten technischen Sicherheitseinrichtung ist ' + 'unverzüglich anzuzeigen.', + ), + ], ), skmEurPerYear: 25_000_000, benefitScore: 3.0, @@ -136,6 +180,48 @@ class Fixtures { standDate: DateTime.utc(2025, 10, 1), sourceUrl: 'https://eur-lex.europa.eu/eli/reg/2016/679', sourceSha256: 'demo-${'c' * 60}', + paragraphs: const [ + NormParagraph( + number: '(1)', + text: 'Jeder Verantwortliche und gegebenenfalls sein Vertreter ' + 'führen ein Verzeichnis aller Verarbeitungstätigkeiten, ' + 'die ihrer Zuständigkeit unterliegen. Dieses Verzeichnis ' + 'enthält sämtliche folgenden Angaben: a) den Namen und ' + 'die Kontaktdaten des Verantwortlichen … b) die Zwecke ' + 'der Verarbeitung; c) eine Beschreibung der Kategorien ' + 'betroffener Personen und der Kategorien personenbezogener ' + 'Daten; d) die Kategorien von Empfängern …', + ), + NormParagraph( + number: '(2)', + text: 'Jeder Auftragsverarbeiter und gegebenenfalls sein ' + 'Vertreter führen ein Verzeichnis zu allen Kategorien von ' + 'im Auftrag eines Verantwortlichen durchgeführten ' + 'Verarbeitungstätigkeiten.', + ), + NormParagraph( + number: '(3)', + text: 'Das in den Absätzen 1 und 2 genannte Verzeichnis ist ' + 'schriftlich zu führen, was auch in einem elektronischen ' + 'Format erfolgen kann.', + ), + NormParagraph( + number: '(4)', + text: 'Der Verantwortliche oder der Auftragsverarbeiter sowie ' + 'gegebenenfalls deren Vertreter stellen das Verzeichnis ' + 'der Aufsichtsbehörde auf Anfrage zur Verfügung.', + ), + NormParagraph( + number: '(5)', + text: 'Die in den Absätzen 1 und 2 genannten Pflichten gelten ' + 'nicht für Unternehmen oder Einrichtungen, die weniger als ' + '250 Mitarbeiter beschäftigen, es sei denn die von ihnen ' + 'vorgenommene Verarbeitung birgt ein Risiko für die Rechte ' + 'und Freiheiten der betroffenen Personen, die Verarbeitung ' + 'erfolgt nicht nur gelegentlich oder umfasst besondere ' + 'Datenkategorien gemäß Artikel 9 Absatz 1 …', + ), + ], ), skmEurPerYear: 450_000_000, benefitScore: 3.8, @@ -185,6 +271,31 @@ class Fixtures { sourceUrl: 'https://gesetze.berlin.de/bsbe/document/jlr-BauOBE2005V25P60', sourceSha256: 'demo-${'d' * 60}', + paragraphs: const [ + NormParagraph( + number: '§ 60 (1)', + text: 'Für die Errichtung, Änderung und Nutzungsänderung ' + 'baulicher Anlagen sowie anderer Anlagen und ' + 'Einrichtungen im Sinne des § 1 Absatz 1 Satz 2 ist die ' + 'Baugenehmigung erforderlich, soweit in den §§ 61 bis 65 ' + 'sowie 76 und 77 nichts anderes bestimmt ist.', + ), + NormParagraph( + number: '§ 60 (2)', + text: 'Bauliche Anlagen und andere Anlagen, an die in dieser ' + 'Bauordnung oder in Vorschriften aufgrund dieser ' + 'Bauordnung Anforderungen gestellt werden, dürfen nur so ' + 'errichtet, geändert und in der Nutzung geändert werden, ' + 'dass sie diesen Anforderungen entsprechen.', + ), + NormParagraph( + number: '§ 63', + text: 'Der Bauantrag ist schriftlich bei der Bauaufsichtsbehörde ' + 'unter Beifügung der für die Beurteilung des Vorhabens und ' + 'die Bearbeitung des Bauantrags erforderlichen Bauvorlagen ' + 'einzureichen.', + ), + ], ), skmEurPerYear: 72_000_000, benefitScore: 3.5, @@ -233,6 +344,30 @@ class Fixtures { standDate: DateTime.utc(2025, 11, 1), sourceUrl: 'https://www.gesetze-im-internet.de/milog/__17.html', sourceSha256: 'demo-${'e' * 60}', + paragraphs: const [ + NormParagraph( + number: '(1)', + text: 'Ein Arbeitgeber, der Arbeitnehmerinnen und Arbeitnehmer ' + 'nach § 8 Absatz 1 des Vierten Buches Sozialgesetzbuch ' + 'geringfügig beschäftigt oder in den in § 2a des ' + 'Schwarzarbeitsbekämpfungsgesetzes genannten ' + 'Wirtschaftsbereichen oder Wirtschaftszweigen Personen ' + 'beschäftigt, ist verpflichtet, Beginn, Ende und Dauer der ' + 'täglichen Arbeitszeit dieser Arbeitnehmerinnen und ' + 'Arbeitnehmer spätestens bis zum Ablauf des siebten auf ' + 'den Tag der Arbeitsleistung folgenden Kalendertages ' + 'aufzuzeichnen und diese Aufzeichnungen mindestens zwei ' + 'Jahre beginnend ab dem für die Aufzeichnung maßgeblichen ' + 'Zeitpunkt aufzubewahren.', + ), + NormParagraph( + number: '(2)', + text: 'Arbeitgeber im Sinne des Absatzes 1 haben die für die ' + 'Kontrolle der Einhaltung der Verpflichtungen nach § 20 ' + 'in Verbindung mit § 2 Absatz 1 erforderlichen Unterlagen ' + 'im Inland in deutscher Sprache bereitzuhalten.', + ), + ], ), skmEurPerYear: 270_000_000, benefitScore: 3.2, diff --git a/app/lib/data/models.dart b/app/lib/data/models.dart index d439635..309835f 100644 --- a/app/lib/data/models.dart +++ b/app/lib/data/models.dart @@ -1,4 +1,4 @@ -// Domain model for the F∆I Law-Heatmap client. +// Domain model for the F∆I Recl∆Im client. // // These types are the lingua franca between the (eventually // gRPC-fed) repository and the UI. Names mirror the Studie's @@ -75,6 +75,16 @@ class Duty { final String? consequence; } +/// One Absatz/paragraph of the actual normative text. +class NormParagraph { + const NormParagraph({required this.number, required this.text}); + + /// Source numbering as it appears, e.g. "(1)", "(2a)". Empty + /// when the source has no Absatz numbering. + final String number; + final String text; +} + /// One norm version — a versioned legal text identified by its /// (synthetic for now) ELI/CELEX. class Norm { @@ -86,6 +96,7 @@ class Norm { required this.standDate, required this.sourceUrl, required this.sourceSha256, + this.paragraphs = const [], }); final String eli; @@ -95,6 +106,12 @@ class Norm { final DateTime standDate; final String sourceUrl; final String sourceSha256; + + /// The actual paragraphs of the norm text, normalised by + /// `text.akoma_normalize` in live mode. In demo mode the + /// fixture quotes the public source verbatim under §5 UrhG + /// (amtliche Werke). + final List paragraphs; } /// A heatmap point: one norm with its computed scores. diff --git a/app/lib/pages/norm_detail_page.dart b/app/lib/pages/norm_detail_page.dart index e94accb..0185ab5 100644 --- a/app/lib/pages/norm_detail_page.dart +++ b/app/lib/pages/norm_detail_page.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import '../data/models.dart'; import '../data/repository.dart'; -import '../theme/lawheatmap_tokens.dart'; +import '../theme/reclaim_tokens.dart'; import '../widgets/demo_banner.dart'; import '../widgets/evidence_sidebar.dart'; -import '../widgets/lawheatmap_card.dart'; +import '../widgets/norm_text_card.dart'; +import '../widgets/reclaim_card.dart'; import '../widgets/tier_badge.dart'; class NormDetailPage extends StatelessWidget { @@ -31,7 +32,7 @@ class NormDetailPage extends StatelessWidget { const DemoBanner(), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.all(LawHeatmapSpace.xl), + padding: const EdgeInsets.all(ReclaimSpace.xl), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -55,8 +56,8 @@ class NormDetailPage extends StatelessWidget { e.norm.eli, style: t.bodyLarge?.copyWith( fontFamily: - LawHeatmapTypography.mono, - color: LawHeatmapColors.mute, + ReclaimTypography.mono, + color: ReclaimColors.mute, ), ), ], @@ -65,34 +66,36 @@ class NormDetailPage extends StatelessWidget { TierBadge(tier: e.tierLowest), ], ), - const SizedBox(height: LawHeatmapSpace.lg), + const SizedBox(height: ReclaimSpace.lg), _MetricsRow(e: e), - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + NormTextCard(norm: e.norm), + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Pflichten', style: t.headlineSmall), const SizedBox( - height: LawHeatmapSpace.sm), + height: ReclaimSpace.sm), Text( 'Atomare deontische Aussagen, extrahiert ' 'aus dem Norm-Text (Phase 1: regex + LLM-' 'Vorschlag, Juristen-Review obligatorisch).', style: t.labelSmall?.copyWith( - color: LawHeatmapColors.mute), + color: ReclaimColors.mute), ), const SizedBox( - height: LawHeatmapSpace.md), + height: ReclaimSpace.md), for (final d in e.duties) _DutyTile(duty: d), ], ), ), if (e.notes != null) ...[ - const SizedBox(height: LawHeatmapSpace.lg), - LawHeatmapCard( + const SizedBox(height: ReclaimSpace.lg), + ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -100,7 +103,7 @@ class NormDetailPage extends StatelessWidget { Text('Methodik-Notiz', style: t.headlineSmall), const SizedBox( - height: LawHeatmapSpace.sm), + height: ReclaimSpace.sm), Text(e.notes!, style: t.bodyLarge), ], ), @@ -109,7 +112,7 @@ class NormDetailPage extends StatelessWidget { ], ), ), - const SizedBox(width: LawHeatmapSpace.xl), + const SizedBox(width: ReclaimSpace.xl), SizedBox( width: 340, child: EvidenceSidebar(evaluation: e), @@ -137,19 +140,19 @@ class _MetricsRow extends StatelessWidget { value: _eur(e.skmEurPerYear), sub: 'SKM P×F×T×h', ), - const SizedBox(width: LawHeatmapSpace.md), + const SizedBox(width: ReclaimSpace.md), _MetricBox( label: 'Nutzen', value: '${e.benefitScore.toStringAsFixed(1)} / 5', sub: '7-dim. Score', ), - const SizedBox(width: LawHeatmapSpace.md), + const SizedBox(width: ReclaimSpace.md), _MetricBox( label: 'Betroffene', value: _kmu(e.affectedCount), sub: 'DESTATIS', ), - const SizedBox(width: LawHeatmapSpace.md), + const SizedBox(width: ReclaimSpace.md), _MetricBox( label: 'Frust', value: '${e.frustScore.toStringAsFixed(1)} / 10', @@ -187,19 +190,19 @@ class _MetricBox extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return Expanded( - child: LawHeatmapCard( + child: ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: t.labelSmall - ?.copyWith(color: LawHeatmapColors.mute)), + ?.copyWith(color: ReclaimColors.mute)), const SizedBox(height: 4), Text(value, style: t.displaySmall), const SizedBox(height: 4), Text(sub, style: t.labelSmall - ?.copyWith(color: LawHeatmapColors.mute)), + ?.copyWith(color: ReclaimColors.mute)), ], ), ), @@ -215,16 +218,16 @@ class _DutyTile extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return Padding( - padding: const EdgeInsets.only(bottom: LawHeatmapSpace.md), + padding: const EdgeInsets.only(bottom: ReclaimSpace.md), child: Container( - padding: const EdgeInsets.all(LawHeatmapSpace.md), + padding: const EdgeInsets.all(ReclaimSpace.md), decoration: BoxDecoration( border: Border.all( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.12)), - borderRadius: const BorderRadius.all(LawHeatmapRadius.sm), + borderRadius: const BorderRadius.all(ReclaimRadius.sm), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -234,26 +237,26 @@ class _DutyTile extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: LawHeatmapColors.signal.withValues(alpha: 0.18), + color: ReclaimColors.signal.withValues(alpha: 0.18), borderRadius: - const BorderRadius.all(LawHeatmapRadius.sm), + const BorderRadius.all(ReclaimRadius.sm), ), child: Text( duty.modality, style: t.labelSmall?.copyWith( - color: LawHeatmapColors.signal, + color: ReclaimColors.signal, fontWeight: FontWeight.w600, ), ), ), - const SizedBox(width: LawHeatmapSpace.sm), + const SizedBox(width: ReclaimSpace.sm), Text(duty.sourceNorm, style: t.labelSmall?.copyWith( - fontFamily: LawHeatmapTypography.mono, - color: LawHeatmapColors.mute, + fontFamily: ReclaimTypography.mono, + color: ReclaimColors.mute, )), ]), - const SizedBox(height: LawHeatmapSpace.sm), + const SizedBox(height: ReclaimSpace.sm), Text(duty.action, style: t.bodyLarge), const SizedBox(height: 4), Text( @@ -261,7 +264,7 @@ class _DutyTile extends StatelessWidget { 'Frequenz: ${duty.frequency}' '${duty.authority != null ? " · Behörde: ${duty.authority}" : ""}' '${duty.consequence != null ? " · Sanktion: ${duty.consequence}" : ""}', - style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + style: t.labelSmall?.copyWith(color: ReclaimColors.mute), ), ], ), diff --git a/app/lib/widgets/norm_text_card.dart b/app/lib/widgets/norm_text_card.dart new file mode 100644 index 0000000..7c98ce2 --- /dev/null +++ b/app/lib/widgets/norm_text_card.dart @@ -0,0 +1,143 @@ +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 _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')), + ); + } + } +} diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..997e35d 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/app/macos/Runner/DebugProfile.entitlements b/app/macos/Runner/DebugProfile.entitlements index dddb8a3..08c3ab1 100644 --- a/app/macos/Runner/DebugProfile.entitlements +++ b/app/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/app/macos/Runner/Release.entitlements b/app/macos/Runner/Release.entitlements index 852fa1a..ee95ab7 100644 --- a/app/macos/Runner/Release.entitlements +++ b/app/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client +