Design System
Single barrel: lib/design_system/design_system.dart. Zero feature/contract/network import — enforced by design_system_purity_test.dart.
Foundations
| Token | File | Highlights |
|---|---|---|
KxColors (ThemeExtension) | foundations/kx_colors.dart | semantic roles textPrimary, textSecondary, surface, accent, danger, focusRing, border — light/dark both present |
KxPalette | kx_palette.dart | raw ramps; tealAccentDecorative=#26BFA8 decorative only (contrast test asserts it never maps to a text/border role) |
KxSpacing | kx_spacing.dart | 4-pt rhythm xs=4, sm=8, md=12, lg=16, xl=24, 2xl=32 — all paddings derive |
KxRadius | kx_radius.dart | card=12, button=12, sheet=16, pill=999 |
KxTypography | kx_typography.dart | Roboto 400/500/700 (700 alias weight 600); styles display, title, heading, body, label, caption |
KxBreakpoints | kx_breakpoints.dart | compact<600, medium 600-839, expanded 840-1439, large >=1440 — one source; allowsTwoColumns ≥840 |
KxMotion | kx_motion.dart | short=150ms, medium=250ms, long=350ms — collapsed to 0 when MediaQuery.disableAnimations |
KxElevation | kx_elevation.dart | level1/2/3 + KxTouchTarget.min=48 |
Theme construction: KxTheme.light() + dark() map tokens to ThemeData for app bar, card, divider, input, nav bar/rail, tab bar, checkbox/radio/switch, scrollbar, progress, dialog, snackbar, tooltip and page transitions. Mode itself is owned by ThemeCubit.
Components (18)
kx_button (KxButton.filled/outlined/text), kx_status_pill, kx_banner (info/success/warning), kx_card, kx_section_header, kx_icon_text, kx_readonly_field, kx_selector_field, kx_tabs/KxTabItem, kx_photo_strip/KxPhotoItem, kx_image, kx_skeleton+KxSkeletonBlock, kx_message_state (empty/error), kx_content_container, kx_hero_app_bar (sliver), kx_nav_shell, kx_focus_ring, kx_progress_indicator.
Rules:
KxHeroAppBarreturns a sliver — compose withCustomScrollView, notScaffold.appBar.KxNavShellrendersNavigationBarcompact vsNavigationRailexpanded; badge order owned by Material.KxPhotoStripis the only horizontally scrolling widget (contractno horizontal page scroll).KxImagebounds decode by painted box, shows fallback on null/malformed bytes.KxSkeletonanimates forever; testspump(duration)notpumpAndSettleunless reduced motion on.- Product status →
KxTone(no domain meaning; feature mapsChargeStatus→KxTone).
Quality gates
| Gate | File | Oracle |
|---|---|---|
| WCAG AA text 4.5:1, UI 3:1, focus ring | contrast_test.dart | every role both themes |
tealAccentDecorative never text/border | contrast_test.dart | explicit assertion |
| Breakpoints 599/600/839/840/1439/1440 | foundations_test.dart | boundary table |
| >48 px targets | per-component test | button/card/selector/tabs/strip/back |
| Enter+Space activation | per-component test | same set |
| Focus ring on focus | component test | shared role color |
| Semantics (heading, live-region banner, merged card, decorative exclude) | per-component test | widget semantics tree |
| Image fallback + bounded decode | kx_image_test.dart | null+bad bytes |
| Shimmer stops under reduced motion | kx_skeleton_test.dart | new harness |
| No overflow 360/600/840/1440 × 1.0/1.3/2.0 text, RTL, dark | responsive_sweep_test.dart | pump at each matrix point |
| No hardcoded prose | design_system_purity_test.dart | forbids literal outside foundations |
| No raw color/radius | same | forbids Colors.* equivalents |
| No cycle imports | same | forbidden import assert |
Ten goldens in test/golden/design_system/goldens/ — deterministic harness ( DPR 1.0, surface, text scale, NoSplash, Roboto), macOS 25.5/Flutter 3.41.9 baseline; Linux CI must regenerate once (part-02-shared-files.md R-02-05) because font rasterization differs.
Fonts & assets
assets/fonts/Roboto-{Regular,Medium,Bold}.ttf committed with ASSET_PROVENANCE.md hashes; pubspec.yaml declares them as family Roboto (weight 600 resolves to Medium). assets/images/ intentionally empty aside from placeholder; flutter.assets lists fixtures + vectors + media + images + fonts.
Using the kit
import 'package:kxinspect_frontend_flutter/design_system/design_system.dart';
// tokens directly
final pad = KxSpacing.lg; // 16
final tone = KxTone.success; // maps to KxColors.success semantics
// theme
ThemeData theme = KxTheme.light(); // or .dark()
// components compose without business knowledge
KxBanner.info(title: 'Outstanding', action: 'View charge', onAction: () => goCharge())
KxPhotoStrip(items: photos.map(Photo->KxPhotoItem).toList())