# Design-System Components

- [Introduction](#introduction)
- [Semantic Token Layer](#semantic-token-layer)
  - [MagicStarterTokens](#magicstartertokens)
  - [Wiring into WindThemeData](#wiring-into-windthemedata)
  - [Token Reference](#token-reference)
- [Atomic Folder Shape](#atomic-folder-shape)
- [Component Families](#component-families)
  - [Form Controls](#form-controls)
  - [Display and Feedback](#display-and-feedback)
  - [Selection and Navigation](#selection-and-navigation)
  - [Overlay](#overlay)
  - [Composition and App Chrome](#composition-and-app-chrome)
- [Import Collisions](#import-collisions)
- [design.md.stub](#designmdstub)

## Introduction

Magic Starter ships a complete atomic design-system component library under `lib/src/ui/components/`. Every component is driven by a `WindRecipe` that resolves colors through the `MagicStarterTokens` semantic alias layer, so a single brand configuration flows into all built-in screens and every component your host app composes directly.

All 46 components are exported from `package:magic_starter/magic_starter.dart`.

## Semantic Token Layer

### MagicStarterTokens

`MagicStarterTokens` is a single `const` class with one public member: `defaultAliases`. It is a `Map<String, String>` of 17 semantic roles to light+dark Wind className pairs following the `bg-/text-/border-color-` prefix convention:

```dart
final aliases = MagicStarterTokens.defaultAliases;
// {
//   'surface':                  'bg-white dark:bg-gray-950',
//   'surface-container':        'bg-gray-50 dark:bg-gray-900',
//   'surface-container-high':   'bg-gray-100 dark:bg-gray-800',
//   'fg':                       'text-gray-900 dark:text-gray-50',
//   'fg-muted':                 'text-gray-500 dark:text-gray-400',
//   'fg-disabled':              'text-gray-300 dark:text-gray-600',
//   'primary':                  'bg-primary-600 dark:bg-primary-500',
//   'on-primary':               'text-white dark:text-white',
//   'primary-container':        'bg-primary-50 dark:bg-primary-950',
//   'accent':                   'bg-accent-600 dark:bg-accent-500',
//   'border':                   'border-gray-200 dark:border-gray-800',
//   'border-subtle':            'border-gray-100 dark:border-gray-900',
//   'destructive':              'bg-red-600 dark:bg-red-500',
//   'on-destructive':           'text-white dark:text-white',
//   'destructive-container':    'bg-red-50 dark:bg-red-950',
//   'success':                  'bg-green-600 dark:bg-green-500',
//   'warning':                  'bg-amber-500 dark:bg-amber-400',
// }
```

### Wiring into WindThemeData

Pass `MagicStarterTokens.defaultAliases` as the `aliases` argument when constructing your `WindThemeData`:

```dart
WindApp(
  theme: WindThemeData(
    aliases: MagicStarterTokens.defaultAliases,
  ),
  child: const MyApp(),
)
```

Components that reference a semantic role (e.g. `bg-surface`, `text-fg`, `bg-primary`) will resolve through this map at render time. Without the aliases wired in, the semantic class names are silently skipped by Wind, so your brand color and surface tokens will not apply.

### Token Reference

| Role | Semantic meaning | Maps to (default) |
|------|-----------------|-------------------|
| `surface` | Page/card background | `bg-white dark:bg-gray-950` |
| `surface-container` | Subtle recessed area | `bg-gray-50 dark:bg-gray-900` |
| `surface-container-high` | Elevated container | `bg-gray-100 dark:bg-gray-800` |
| `fg` | Primary text | `text-gray-900 dark:text-gray-50` |
| `fg-muted` | Secondary/supporting text | `text-gray-500 dark:text-gray-400` |
| `fg-disabled` | Disabled state text | `text-gray-300 dark:text-gray-600` |
| `primary` | Brand primary fill | `bg-primary-600 dark:bg-primary-500` |
| `on-primary` | Text on primary fill | `text-white dark:text-white` |
| `primary-container` | Tinted primary area | `bg-primary-50 dark:bg-primary-950` |
| `accent` | Secondary brand fill | `bg-accent-600 dark:bg-accent-500` |
| `border` | Standard border | `border-gray-200 dark:border-gray-800` |
| `border-subtle` | Faint separator | `border-gray-100 dark:border-gray-900` |
| `destructive` | Danger / delete fill | `bg-red-600 dark:bg-red-500` |
| `on-destructive` | Text on danger fill | `text-white dark:text-white` |
| `destructive-container` | Tinted danger area | `bg-red-50 dark:bg-red-950` |
| `success` | Positive / confirmed | `bg-green-600 dark:bg-green-500` |
| `warning` | Caution / review | `bg-amber-500 dark:bg-amber-400` |
| `bg-ai-soft` | Soft container for AI-authored content (replies, summaries) | `bg-violet-100 dark:bg-violet-900` |
| `text-ai-soft-foreground` | Text or icon on top of `bg-ai-soft` | `text-violet-700 dark:text-violet-200` |
| `border-ai` | Accent edge of AI-authored content | `border-violet-500 dark:border-violet-400` |
| `bg-warning-soft` | Soft amber container for a quiet status pill | `bg-amber-100 dark:bg-amber-900` |
| `text-warning-soft-foreground` | Text or icon on top of `bg-warning-soft` | `text-amber-800 dark:text-amber-200` |
| `bg-success-soft` | Soft green container for a quiet status pill | `bg-green-100 dark:bg-green-900` |
| `text-success-soft-foreground` | Text or icon on top of `bg-success-soft` | `text-green-800 dark:text-green-200` |

These keys are written as the full class they replace (`bg-ai-soft`, `border-ai`), not as a bare role. The support screens use the AI pair and `border-ai` for the assistant's avatar, its bubble (a 2px left edge) and its chip, and the soft warning and success pairs for the "Team answering" and "Resolved" pills. A host theme that defines the same keys supplies its own map and wins.

To override individual roles, spread `defaultAliases` and replace specific keys:

```dart
WindThemeData(
  aliases: {
    ...MagicStarterTokens.defaultAliases,
    'primary': 'bg-indigo-600 dark:bg-indigo-500',
    'on-primary': 'text-white dark:text-white',
  },
)
```

## Atomic Folder Shape

Every component lives in a 4-file atomic folder:

```
lib/src/ui/components/<name>/
  <name>.dart          # The widget class (StatelessWidget or StatefulWidget)
  <name>.recipe.dart   # WindRecipe function: resolves className from theme tokens
  <name>.preview.dart  # Standalone preview widget for interactive inspection
  index.dart           # Barrel: re-exports the public API for this component
```

The `.recipe.dart` file contains a top-level function (e.g. `buildButtonRecipe`, `buildInputRecipe`, `cardRecipe`) that accepts variant/state arguments and returns a Wind className string. The widget delegates its className derivation to the recipe so the visual contract is inspectable and testable in isolation.

## Component Families

### Form Controls

| Component | Enums / Helpers | Notes |
|-----------|----------------|-------|
| `MSButton` | `ButtonIntent` (primary/secondary/ghost/destructive), `ButtonSize` (sm/md/lg), `buildButtonRecipe` | Use `ButtonIntent.destructive` for delete / danger actions; `fullWidth: true` fills the parent width (wraps in `SizedBox`, defaults `false`). Shape, the three sizes and the secondary border come from `MagicStarterButtonTheme` (`MagicStarter.useButtonTheme`). Below `lg`, every size keeps its painted size but takes a 44x44 hit area |
| `MSInput` | `InputState` (idle/focused/error/disabled), `buildInputRecipe` | Pairs with `MSFormField` for label + error display; `fullWidth: true` fills the parent width (defaults `false`). A single-line field is 44px tall below `lg` and 40px from `lg` up (`py-3 lg:py-2.5` over a `leading-5` line); the padding and type of the text, placeholder and value come from `MagicStarterFieldTheme.inputSizeClassName` (`MagicStarter.useFieldTheme`) |
| `MSTextarea` | `TextareaState`, `textareaRecipe` | Multi-line input, same state model as `MSInput`; `fullWidth: true` fills the parent width (defaults `false`). On iOS a focused field carries a Done toolbar above the keyboard, since a multiline field's Return key inserts a newline and would otherwise leave the keyboard with no way out. One consequence for consumer TESTS: while that toolbar is up it schedules a frame callback every frame, so `pumpAndSettle` under `debugDefaultTargetPlatformOverride = TargetPlatform.iOS` never settles; pump explicitly instead |
| `MSCheckbox` | | Boolean toggle; Wind-only, no Material dependency |
| `MSSwitch` | | Toggle; replaces Flutter's `MSSwitch` in Wind layouts. An optional `label` renders beside the track and is part of the tap target; below `lg` the hit area is at least 44 px tall. Shows a focus ring on keyboard focus |
| `MSRadio` | | Single-select option |
| `MSSelect` | `selectRecipe` | Dropdown single-select backed by an item list; the trigger is the same height as `MSInput` (44px below `lg`, 40px from `lg` up), and its padding, minimum height and type come from `MagicStarterFieldTheme.selectSizeClassName` |
| `MSCombobox` | `comboboxRecipe` | Searchable single-select with filter input; the trigger is the same height as `MSInput` (44px below `lg`, 40px from `lg` up) and shares `MagicStarterFieldTheme.selectSizeClassName` with `MSSelect`. The search field's placeholder defaults to `trans('common.search_placeholder')` |
| `MSSwitchRow` | `switchRowRecipe` | An `MSSwitch` with its `label` beside it; the label doubles as the switch's accessibility name |
| `MSKeyValueEditor` | `MSKeyValueRow`, `keyValueEditorRecipe` | Controlled editor for an ordered list of key/value pairs (e.g. HTTP headers); every mutation emits a fresh list |
| `MSStringValueList` | `StringValueListTone`, `stringValueListRecipe` | Controlled editor for a short list of distinct strings rendered as chips; commits on Enter/IME-done, trims and de-duplicates |

### Display and Feedback

| Component | Enums / Helpers | Notes |
|-----------|----------------|-------|
| `MSBadge` | `BadgeTone` (neutral/primary/success/warning/destructive) | Inline label chip |
| `MSTypography` | `TypographyVariant` (h1-h6/body/caption/overline) | Semantic text wrapper |
| `MSSkeleton` | `SkeletonShape` (line/rect/circle) | Placeholder loading block |
| `MSToast` | `ToastVariant` (info/success/warning/error) | Transient feedback banner (the message is `text-sm font-medium`; `messageClassName` is appended after it, so `text-[15px] leading-5 font-normal` sets another body type, with or without an action); presents nothing itself, so the host shows it (through its own `OverlayEntry`) and decides how long it stays. `Magic.toast(String)` cannot carry an action and its overlay ignores pointers, so an action needs the host's own overlay. `actionLabel` and `onAction` (both or neither) add a trailing text action such as Undo: a cell at least 44 px tall and wide at every width (it grows with the label at a large text size), named by `actionLabel` for assistive technology, with a ring inside the toast on keyboard focus. A long message wraps beside it and the action keeps its size; a one line toast is as tall with an action as without |
| `MSTooltip` | | Hover/long-press hint bubble |
| `MSEmptyState` | | Illustrated empty-list placeholder |
| `MSErrorState` | | Full-screen or inline error with retry |
| `MSDataTable` | `MSDataColumn`, `dataTable*ClassName` | Header plus rows, with the columns passed in. Two modes: the default renders `rows` eagerly (right for a short, complete list); `MSDataTable.paginated` hands the body to magic's `MagicPaginatedListView` inside a bounded box, so a long collection costs the viewport rather than the result and reaching the tail asks the paginator for its next page. The header stays outside the scrolling body. `bodyHeight` is what bounds it, since a `ListView` needs a bound and `shrinkWrap: true` would build every row anyway. Column labels and `loadingLabel` are ALREADY TRANSLATED strings, not keys: half the callers render a label that is not a key at all (a currency code, a region name) |

### Selection and Navigation

| Component | Enums / Helpers | Notes |
|-----------|----------------|-------|
| `MSSegmentedControl` | `SegmentedControlSize`, `segmentedControlRecipe` | Inline tab switcher; below `lg` each segment is at least 44 px tall to tap, the painted pill unchanged. The pill's padding and type per size and the root's padding come from `MagicStarterFieldTheme` (`segmentedSmClassName`, `segmentedMdClassName`, `segmentedPaddingClassName`); the hit cell is not themed. In a horizontal scroller (the `overflow-x-auto` root override, or your own scroll view that holds nothing but the control) it scrolls the selected segment into view on mount and when `selectedIndex` changes; a page view, a vertical list or a strip with padding or other content around the control is left alone, and the page's vertical scroll never moves |
| `MSTabs` | `tabsRecipe` | Full tab bar with content panels |
| `MSAccordion` | `MSAccordionItem`, `accordionRecipe` | Collapsible section list |
| `MSNavbar` | | Horizontal top navigation bar |
| `MSDropdownMenu` | `MSDropdownMenuItem` | Contextual action menu |

### Overlay

| Component | Notes |
|-----------|-------|
| `MSDialog` | Modal dialog shell; reads `MagicStarterModalTheme` tokens. `scrollable: false` hands a body that scrolls itself the bounded height, as on `MSBottomSheet`. With the modal theme's `presentation: MagicStarterModalPresentation.sheet`, `MSDialog.show` opens the same content as a bottom sheet, and the widget draws the sheet panel inside a bottom sheet route |
| `MSBottomSheet` | Slide-up sheet; reads `MagicStarterModalTheme` tokens. `MSBottomSheet.show(..., dialogBreakpoint: 'lg')` presents an `MSDialog` from that Wind breakpoint up, with the same title, description, body, footer and `scrollable`, so one call serves a phone and a desktop; the width is read when the call is made. Null (the default) always presents a sheet, and so does the `sheet` presentation, which ignores the breakpoint; the `adaptive` presentation ignores it too and swaps at `MagicStarterModalTheme.dialogBreakpoint` for every call. Built in a dialog route (an overlay route under `adaptive`), the widget draws the dialog shell. `canDismiss` (a `ValueListenable<bool>`, read at the gesture) holds a sheet with unsaved input open: while false a drag down is off (the handle stays drawn), and a scrim tap or its semantics dismiss, Back and Escape leave it open and call `onDismissBlocked(sheetContext)`, where the app asks the person and, on a confirmed discard, closes it with `Navigator.of(sheetContext).pop()` (a `pop` skips the guard; the opener's context would pop the wrong navigator, since the sheet sits on the root one); the dialog from `dialogBreakpoint` is held the same way |

### Composition and App Chrome

| Component | Notes |
|-----------|-------|
| `MSFormField` | Label + input + hint + error layout wrapper |
| `MSCard` | Surface/inset/elevated variants |
| `MSPageHeader` | Full-width responsive header (title, subtitle, leading, actions). Stacked below `sm`, a row above it; `inlineActions` (or `MagicStarterPageHeaderTheme.inlineActions`) makes it a row at every width AND lets a long title shrink instead of overflowing. `size` picks `MSPageHeaderSize.regular`, `.large` (the theme's `largeTitleClassName`, display size on a phone and one step smaller from `lg`) or `.compact` (a 44 pt bar below the top safe area: leading control, centred one-line title, actions, over a translucent fill and a backdrop blur that turn opaque under high contrast). Null follows `MagicStarterPageHeaderTheme.largeTitle`. `className` is appended to the container after the theme's container class, for a page under a bar of its own (`pt-0` or `lg:pt-0` takes the header's top padding off); it styles the regular and the large header, and the compact bar ignores it. Under `MagicStarter.useHostPageChrome`, where the predicate answers true, it draws only its actions and reports its title, subtitle and back control to `MagicStarter.pageChrome` ([host-owned page chrome](https://anilcancakir.com/magic/docs/starter/v1.0/architecture/manager.md#host-page-chrome)) |
| `MSSocialDivider` | "Or continue with" separator for auth forms |
| `MSUserProfileDropdown` | Avatar menu with profile links, theme toggle and logout |
| `MSTeamSelector` | Current-team switcher; requires a registered team resolver |
| `MSPageContainer` | Shared page geometry: width cap, edge margins, vertical rhythm, horizontal safe area. Reads `MagicStarter.manager.pageContainerClassName` |
| `MSPageScaffold` | Full page treatment: page surface + own scroll + `MSPageContainer` + `MSPageHeader` + `gap-6` sections column. `footer` docks a widget below the scroll area, outside it, in the same width and edge margins; it clears the bottom safe area and rides above the on-screen keyboard (a chat composer is the use). A docked layout needs a bounded height, as the app shell provides. `scrollController` hands the scroll area to the page, to keep a thread on its newest message. `containerClassName` is appended to the page container after the host's geometry, for a page under a bar of its own (`pt-4` replaces the host's top padding). `headerClassName` is handed to the regular and the large header the same way (`lg:pt-0` takes the header's own top padding off from `lg` up); the compact bar is built without it. `largeTitle: true` (or `MagicStarterPageHeaderTheme.largeTitle`, which also reaches the starter's own pages) renders a large header that collapses into the compact bar pinned over the page once it has scrolled under it, fading in 200 ms (a swap under Reduce Motion). Under host-owned page chrome it builds neither the large title nor the bar |
| `MSMasterDetail` | A list and its selected item side by side from `breakpoint` (`lg` by default): `master` in a fixed `w-96` column, `detail` (or `placeholder` while it is null) in the rest. Below the breakpoint it renders `detail ?? master`, which is push navigation when the selection lives in the route. Owns no page geometry or scroll, so it goes inside `MSPageScaffold`; `MSMasterDetail.isSplit(context)` gives a page the same answer for its header |
| `MSFormActions` | Cancel/submit footer row for a form; `cancelLabel`/`onCancel` are optional (omitting both renders no cancel button) |
| `MSHeaderAction` | Page-header action that collapses to an icon-only glyph below `lg` and a labelled `MSButton` at `lg` and up; `label` becomes the icon form's accessibility label |

> [!NOTE]
> The bell-icon dropdown is not in this table. It moved to `magic_notifications` as `NotificationDropdown`; see [Notifications](https://anilcancakir.com/magic/docs/starter/v1.0/basics/notifications.md).

## Import Collisions

Every component carries an `MS` prefix (`MSButton`, `MSDialog`, `MSSwitch`, ...),
so none of the barrel's component exports collide with
`package:material_ui/material_ui.dart`. You can import both without a `hide` clause:

```dart
import 'package:material_ui/material_ui.dart';
import 'package:magic_starter/magic_starter.dart';

// No ambiguity: MSSwitch is the component, Switch is Material's.
```

> Earlier releases exported unprefixed names (`Switch`, `Dialog`, ...) and
> required a `hide` clause. See the migration table in the package README to
> update from the pre-`MS` names.

Or hide the specific colliding names:

```dart
import 'package:material_ui/material_ui.dart' hide MSSwitch, MSDialog, MSCheckbox;
import 'package:magic_starter/magic_starter.dart';
```

Widget tests that need both Material and magic_starter types must do the same.

## design.md.stub

`assets/stubs/design.md.stub` is a `DESIGN.md` template that covers all 17 semantic roles, typography on the 4px logical scale, rounded/spacing scales, and key component entries with `{{ placeholder }}` tokens.

Consumers copy it into their project root:

```bash
cp $(flutter pub cache list magic_starter)/assets/stubs/design.md.stub DESIGN.md
```

Then fill in brand hex values and fonts, and run the CLI commands:

```bash
# Validate your DESIGN.md against the schema
dart run magic:dowel design:lint

# Generate the Wind theme from your DESIGN.md
dart run magic:dowel design:sync
```

`design:sync` regenerates the `WindThemeData(aliases: ...)` map, replacing `MagicStarterTokens.defaultAliases` with your brand-specific values so every component reflects your brand automatically.
