diff --git a/app/lib/data/fixtures.dart b/app/lib/data/fixtures.dart index 3afdea3..abf5685 100644 --- a/app/lib/data/fixtures.dart +++ b/app/lib/data/fixtures.dart @@ -90,6 +90,12 @@ class Fixtures { 'T (Tarif) T1 — DESTATIS Verdiensterhebung kaufm. Tätigkeit ~30 €/h; ' 'h (Zeitaufwand) T3 — eigene Schätzung ~30 min, NKR-Validierung ausstehend. ' 'Regel der niedrigsten Stufe: Gesamt → T3 (in der Demo konservativ als T4 markiert).', + componentSummary: const [ + 'P · T1 — DESTATIS Gewerbeanzeigen', + 'F · T1 — anlassbezogen, je Vorgang', + 'T · T1 — DESTATIS Verdiensterhebung', + 'h · T3 — eigene Schätzung ~30 min', + ], ); static final _kassenSichV = Evaluation( @@ -177,6 +183,12 @@ class Fixtures { 'Laufende Folgekosten (TSE-Ausfallmeldungen, Hardware-Erneuerung): T2 ' 'aus DIHK-Bürokratie-Index + DEHOGA-Gastronomie-Stichprobe. ' 'Hoher Frust-Score wegen Pluralität TSE-Hersteller × Finanzamt-Vollzug.', + componentSummary: const [ + 'P · T1 — Kassen-pflichtige Betriebe', + 'F · T1 — kontinuierlich + Ausfälle', + 'T · T2 — DIHK Bürokratie-Index', + 'h · T2 — DEHOGA-Stichprobe', + ], ); static final _dsgvoArt30 = Evaluation( @@ -272,6 +284,13 @@ class Fixtures { 'pro Unternehmen liegt im Streit zwischen BfDI-Position (gering, weil ' '<250-MA-Ausnahme) und DIHK-Position (hoch, weil 5-Abs.-Ausnahme ' 'praktisch nicht greift). Tier T2 für Magnitude.', + componentSummary: const [ + 'P · T1 — DESTATIS Unternehmensregister', + 'F · T1 — jährlich überprüfen', + 'T · T2 — DIHK-Position', + 'h · T2 — Streit BfDI ↔ DIHK', + 'EU-determiniert · Reform-Hebel Brüssel', + ], ); static final _berlinBauO60 = Evaluation( @@ -351,6 +370,13 @@ class Fixtures { '→ Genehmigung sind im Land getrennt geregelt). Höchster Frust-Score ' 'aus Verweistiefe + Behörden-Pluralität (Bezirksamt × SenStadt × ' 'Beteiligungsverfahren).', + componentSummary: const [ + 'P · T2 — Handwerkskammer Berlin', + 'F · T1 — pro Bauvorhaben', + 'T · T2 — Bauwirtschaft Berlin', + 'h · T3 — Verfahren mehrstufig', + 'Frust · T2 — Bezirk × Senat × Beteiligung', + ], ); static final _miLoG17 = Evaluation( @@ -428,5 +454,12 @@ class Fixtures { 'dokumentierte den initial-Erfüllungsaufwand; aktuelle Zahlen ' 'aus ZDH Bürokratiekosten-Handwerk. Bürokratieentlastungs-' 'gesetz IV adressierte den § 17 — Reform-Bewegung ist im Lauf.', + componentSummary: const [ + 'P · T1 — DESTATIS MiLoG-pflichtig', + 'F · T1 — arbeitstäglich', + 'T · T1 — DESTATIS Tariflohn', + 'h · T1 — NKR-Stellungnahme 2014', + 'Reform · BEG IV in Lauf', + ], ); } diff --git a/app/lib/data/models.dart b/app/lib/data/models.dart index ae7caf2..d75f950 100644 --- a/app/lib/data/models.dart +++ b/app/lib/data/models.dart @@ -142,6 +142,7 @@ class Evaluation { required this.sources, required this.auditEventId, this.notes, + this.componentSummary = const [], }); final Norm norm; @@ -172,4 +173,10 @@ class Evaluation { final String auditEventId; final String? notes; + + /// Compact lines for at-a-glance reading on list views. + /// One line per SKM-Komponente (P / F / T / h) plus optional + /// extras — each carries its tier-tag inline. Empty list ⇒ + /// fall back to the notes field for the list view. + final List componentSummary; } diff --git a/app/lib/pages/landing_page.dart b/app/lib/pages/landing_page.dart index 0f19bac..50fba50 100644 --- a/app/lib/pages/landing_page.dart +++ b/app/lib/pages/landing_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../data/repository.dart'; import '../theme/reclaim_theme.dart'; import '../theme/reclaim_tokens.dart'; +import '../widgets/brand_wordmark.dart'; import '../widgets/delta_mark.dart'; import 'shell_page.dart'; @@ -20,7 +21,7 @@ class LandingPage extends StatelessWidget { body: SafeArea( child: Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 720), + constraints: const BoxConstraints(maxWidth: 820), child: Padding( padding: const EdgeInsets.symmetric( horizontal: ReclaimSpace.xxl, @@ -30,9 +31,18 @@ class LandingPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const DeltaMark(size: 96), - const SizedBox(height: ReclaimSpace.xxl), - Text('Recl∆Im', style: t.displaySmall), + const Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DeltaMark(size: 96), + SizedBox(width: ReclaimSpace.xl), + BrandWordmark( + size: 56, + letterSpacing: -1.0, + ), + ], + ), + const SizedBox(height: ReclaimSpace.xl), const SizedBox(height: ReclaimSpace.sm), Text( 'Pflicht-basierte Wirkungsanalyse von Rechtsnormen — ' diff --git a/app/lib/pages/norms_list_page.dart b/app/lib/pages/norms_list_page.dart index 189a1fb..d6156a4 100644 --- a/app/lib/pages/norms_list_page.dart +++ b/app/lib/pages/norms_list_page.dart @@ -107,31 +107,42 @@ class _NormRow extends StatelessWidget { color: ReclaimColors.mute, ), ), + const SizedBox(height: ReclaimSpace.sm), + TierBadge(tier: evaluation.tierLowest), ], ), ), const SizedBox(width: ReclaimSpace.lg), Expanded( - child: _MetricColumn( - label: 'Schaden / Jahr', - value: _eur(evaluation.skmEurPerYear), + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _MetricColumn( + label: 'Schaden / Jahr', + value: _eur(evaluation.skmEurPerYear), + ), + const SizedBox(height: ReclaimSpace.sm), + _MetricColumn( + label: 'Nutzen', + value: + '${evaluation.benefitScore.toStringAsFixed(1)} / 5', + ), + const SizedBox(height: ReclaimSpace.sm), + _MetricColumn( + label: 'Betroffene', + value: _kmu(evaluation.affectedCount), + ), + ], ), ), + const SizedBox(width: ReclaimSpace.lg), Expanded( - child: _MetricColumn( - label: 'Nutzen', - value: - '${evaluation.benefitScore.toStringAsFixed(1)} / 5', + flex: 3, + child: _ComponentSummary( + summary: evaluation.componentSummary, ), ), - Expanded( - child: _MetricColumn( - label: 'Betroffene', - value: _kmu(evaluation.affectedCount), - ), - ), - const SizedBox(width: ReclaimSpace.md), - TierBadge(tier: evaluation.tierLowest), ], ), ), @@ -152,6 +163,44 @@ class _NormRow extends StatelessWidget { } } +class _ComponentSummary extends StatelessWidget { + const _ComponentSummary({required this.summary}); + + final List summary; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + if (summary.isEmpty) { + return Text( + '—', + style: t.labelSmall?.copyWith(color: ReclaimColors.mute), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Komponenten', + style: t.labelSmall?.copyWith(color: ReclaimColors.mute), + ), + const SizedBox(height: 4), + for (final line in summary) + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + line, + style: t.bodyLarge?.copyWith( + height: 1.35, + fontSize: 12.5, + ), + ), + ), + ], + ); + } +} + class _MetricColumn extends StatelessWidget { const _MetricColumn({required this.label, required this.value}); diff --git a/app/lib/pages/shell_page.dart b/app/lib/pages/shell_page.dart index 5cf3da4..2f7b51a 100644 --- a/app/lib/pages/shell_page.dart +++ b/app/lib/pages/shell_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../data/repository.dart'; import '../theme/reclaim_tokens.dart'; +import '../widgets/brand_wordmark.dart'; import '../widgets/delta_mark.dart'; import 'about_page.dart'; import 'heatmap_page.dart'; @@ -51,14 +52,7 @@ class _ShellPageState extends State { children: [ const DeltaMark(size: 36), const SizedBox(height: ReclaimSpace.sm), - Text( - 'Recl∆Im', - style: t.labelLarge?.copyWith( - letterSpacing: 1.5, - color: ReclaimColors.signal, - fontWeight: FontWeight.w600, - ), - ), + const BrandWordmark(size: 18), Text( 'F∆I', style: t.labelSmall?.copyWith( diff --git a/app/lib/widgets/brand_wordmark.dart b/app/lib/widgets/brand_wordmark.dart new file mode 100644 index 0000000..14d6783 --- /dev/null +++ b/app/lib/widgets/brand_wordmark.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import '../theme/reclaim_tokens.dart'; + +/// "Recl∆Im" wordmark with the ∆I glyphs petrol-coloured — the +/// brand rule of thumb in the fai_web universe: wherever the name +/// stands as a brand, the ∆I tinted petrol carries the F∆I-family +/// identity. As plain text it would all share the surrounding ink +/// colour. +/// +/// Pass [size] for the base font size; the ∆I scales relative. +class BrandWordmark extends StatelessWidget { + const BrandWordmark({ + super.key, + this.size = 24, + this.fontWeight = FontWeight.w600, + this.color, + this.letterSpacing = -0.5, + }); + + final double size; + final FontWeight fontWeight; + + /// Color of the non-∆I letters. Defaults to the surface's + /// onSurface so the wordmark reads against whichever ink/paper + /// backdrop it sits on. + final Color? color; + final double letterSpacing; + + @override + Widget build(BuildContext context) { + final base = color ?? Theme.of(context).colorScheme.onSurface; + final style = TextStyle( + fontSize: size, + fontWeight: fontWeight, + fontFamily: ReclaimTypography.display, + color: base, + letterSpacing: letterSpacing, + height: 1.0, + ); + return RichText( + text: TextSpan( + style: style, + children: [ + const TextSpan(text: 'Recl'), + TextSpan( + text: '∆I', + style: style.copyWith(color: ReclaimColors.signal), + ), + const TextSpan(text: 'm'), + ], + ), + ); + } +} diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index eb83a95..30b5b41 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:reclaim_app/main.dart'; +import 'package:reclaim_app/widgets/brand_wordmark.dart'; void main() { // Generous surface so NavigationRail + Heatmap fit without @@ -20,7 +21,7 @@ void main() { await tester.binding.setSurfaceSize(surfaceSize); await tester.pumpWidget(const ReclaimApp()); await tester.pump(); - expect(find.text('Recl∆Im'), findsOneWidget); + expect(find.byType(BrandWordmark), findsWidgets); expect( find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'), findsOneWidget,