Skip to main content

Design System

Single barrel: lib/design_system/design_system.dart. Zero feature/contract/network import — enforced by design_system_purity_test.dart.

Foundations

TokenFileHighlights
KxColors (ThemeExtension)foundations/kx_colors.dartsemantic roles textPrimary, textSecondary, surface, accent, danger, focusRing, border — light/dark both present
KxPalettekx_palette.dartraw ramps; tealAccentDecorative=#26BFA8 decorative only (contrast test asserts it never maps to a text/border role)
KxSpacingkx_spacing.dart4-pt rhythm xs=4, sm=8, md=12, lg=16, xl=24, 2xl=32 — all paddings derive
KxRadiuskx_radius.dartcard=12, button=12, sheet=16, pill=999
KxTypographykx_typography.dartRoboto 400/500/700 (700 alias weight 600); styles display, title, heading, body, label, caption
KxBreakpointskx_breakpoints.dartcompact<600, medium 600-839, expanded 840-1439, large >=1440 — one source; allowsTwoColumns ≥840
KxMotionkx_motion.dartshort=150ms, medium=250ms, long=350ms — collapsed to 0 when MediaQuery.disableAnimations
KxElevationkx_elevation.dartlevel1/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:

  • KxHeroAppBar returns a sliver — compose with CustomScrollView, not Scaffold.appBar.
  • KxNavShell rendersNavigationBar compact vs NavigationRail expanded; badge order owned by Material.
  • KxPhotoStrip is the only horizontally scrolling widget (contract no horizontal page scroll).
  • KxImage bounds decode by painted box, shows fallback on null/malformed bytes.
  • KxSkeleton animates forever; tests pump(duration) not pumpAndSettle unless reduced motion on.
  • Product status → KxTone (no domain meaning; feature maps ChargeStatus→KxTone).

Quality gates

GateFileOracle
WCAG AA text 4.5:1, UI 3:1, focus ringcontrast_test.dartevery role both themes
tealAccentDecorative never text/bordercontrast_test.dartexplicit assertion
Breakpoints 599/600/839/840/1439/1440foundations_test.dartboundary table
>48 px targetsper-component testbutton/card/selector/tabs/strip/back
Enter+Space activationper-component testsame set
Focus ring on focuscomponent testshared role color
Semantics (heading, live-region banner, merged card, decorative exclude)per-component testwidget semantics tree
Image fallback + bounded decodekx_image_test.dartnull+bad bytes
Shimmer stops under reduced motionkx_skeleton_test.dartnew harness
No overflow 360/600/840/1440 × 1.0/1.3/2.0 text, RTL, darkresponsive_sweep_test.dartpump at each matrix point
No hardcoded prosedesign_system_purity_test.dartforbids literal outside foundations
No raw color/radiussameforbids Colors.* equivalents
No cycle importssameforbidden 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())