# Modifier Migration Plan — bloated inits → idiomatic SwiftUI modifiers

**PHASE 1 (inventory + plan).** Goal: convert bloated `init`s into SwiftUI's chainable modifier
pattern, **without breaking the public API** (deprecate old init + forward). Solid (already
lean) APIs are left untouched.

## Inventory (evidence)

- **103 components**, averaging **5.2** init parameters.
- **26 components ≥ 8 parameters** (anti-pattern targets). **51 components ≤ 4** (clean → untouched).
- **Precedent:** the button family has already been converted (#96–#98): `isContentWidth`→`block`,
  `isEnabled`→`.disabled()`, label `.lineLimit(1)`. This plan spreads the same pattern.

| Bloat | Components (init param count) |
|---|---|
| 23 | TextInput |
| 12 | ThemeButton✅(done), MultiSelect, DateField |
| 11 | Slider, SearchBar, RadioButton, Badge, Accordion |
| 10 | Rating, RangeSlider, ProgressBar, InputNumber, Chip, Checkbox |
| 9 | VideoPlayerView, SelectBox, Pagination, MultiLineTextInput, Carousel |
| 8 | Stat |

## Decision framework (summary)

- **STAYS (init):** required content (`title/value/items/url`), `@Binding` (`selection/text/isOn`),
  required closures (`action/onSubmit`), `@ViewBuilder` slots (`content/label`). Also items considered
  *domain data* — `range/bounds`, `options`, `infoMessages` (validation to display) and
  `placeholder` → **stay** (content/data, not appearance).
- **MODIFIER (mechanism):**
  - **(M-disabled)** `isEnabled: Bool` → SwiftUI native **`.disabled(_:)`** (not custom; read from the environment). *Highest leverage, ~20 components.*
  - **(M-a11y)** `accessibilityID: String?` → self-returning **`.a11yID(_:)`** (forwards to the kit's existing `.a11y()` infrastructure).
  - **(M-style/3)** semantic variant enums (`style/variant/type/selectionStyle`) → **self-returning** `.componentStyle(_:)` (no custom *render* needed; just semantic color/emphasis). Where explicit render extension is required, a Style protocol already exists (Card/Stat/Select).
  - **(M-size/3)** `size`/`customSize`/`height` → self-returning `.size(_:)`, or for those using `ControlSize`, native **`.controlSize()`** + `@Environment(\.controlSize)`.
  - **(M-flag/3)** Bool config flags with defaults (`allowClear/showCount/isSecure/showPercentage/gradient/loop/autoplay/searchable…`) → self-returning flag modifiers.
  - **(M-color/3)** style colors (`textColor/strokeColor/trailColor/backgroundColor/gradient`) → self-returning style modifiers.
  - **(M-env/2)** cross-cutting config that makes sense to cascade into the subtree (density) → EnvironmentKey modifier (e.g. `.controlSize`, and `.fieldDensity` in the future).

> Mechanism choice: multi-variant explicit-render → Style protocol (1); cascade → Environment (2);
> single-instance one-off → self-returning (3). Most cases in this library are **(3)** or native env.

---

## Highest leverage: cross-cutting migrations

These simplify DOZENS of components with a SINGLE modifier — **do these first**:

### X1 — `isEnabled: Bool` → `.disabled(_:)` (native)
Affected components (removed from init and read via `@Environment(\.isEnabled)`; old param deprecated+forwarded):
`Chip, Rating, Checkbox, RadioButton, Pagination, DateField, MultiSelect, RangeSlider, SearchBar, SelectBox, Slider, InputNumber, MultiLineTextInput, TextInput` (+ButtonGroup already ✅).
**~14 components, one pattern.** Mapping: `isEnabled: $x` → `.disabled(!x)`; `isEnabled: false` → `.disabled(true)`.

### X2 — `accessibilityID: String?` → `.a11yID(_:)` (self-returning)
Affected: `Checkbox, RadioButton, DateField, MultiSelect, SearchBar, SelectBox, Slider, RangeSlider, InputNumber, MultiLineTextInput, TextInput, Pagination`. **~12 components.** Old param deprecated+forwarded to `.a11yID(id)`.

### X3 — `size:` → native `.controlSize()` (those using ControlSize)
`Checkbox`, `RadioButton` already take `ControlSize` → `@Environment(\.controlSize)` + native `.controlSize()`. Those with a custom size enum (TextInput/Badge/Chip/…) get a self-returning `.size(_:)`.

---

## Per-component plan (26 bloated)

Legend: **STAYS** | **→.disabled** | **→.a11yID** | **→style(3)** | **→size** | **→flag(3)** | **→color(3)**

### TextInput (23 → target ~6 init)
| Param | Decision | Modifier |
|---|---|---|
| `label`, `text`(Binding), `onSubmit` | **STAYS** | — |
| `placeholder`, `infoMessages` | **STAYS** (content/data) | — |
| `isEnabled` | →.disabled | `.disabled(_:)` |
| `accessibilityID` | →.a11yID | `.a11yID(_:)` |
| `size` | →size | `.size(_:)` (TextInputSize) |
| `isSecure, allowClear, showCount, hardLimit, autocorrectionDisabled` | →flag | `.secure() .clearable() .characterCount(limit:style:) …` |
| `maxLength, countStyle` | →flag (grouped) | `.characterCount(_ max:style:)` |
| `leadingSystemImage, suffixSystemImage, addonBefore, addonAfter` | →style/slot | `.icon(leading:trailing:)` / `.addons(before:after:)` |
| `keyboardType, textContentType, submitLabel, autocapitalization, formatter` | →flag (grouped) | `.keyboard(_:contentType:submit:caps:)` + `.formatter(_:)` |
**Note:** TextInput already carries a `TextInputModel` init variant — the model-based init is preserved; the bloated init is split into `.modifier`s.

### Badge (11 → ~3 init)
| `text`, `action` | **STAYS** |
| `style, variant, size, shape` | →style/size | `.badgeStyle(_:) .badgeVariant(_:) .size(_:) .shape(_:)` (or a single `.badgeStyle(_, variant:, size:, shape:)`) |
| `leadingSystemImage, trailingSystemImage` | →style | `.icon(leading:trailing:)` |
| `textColor, gradient` | →color | `.tint(_:)` / `.gradient(_:)` (⚠ avoid clashing with SwiftUI's `.tint` → `.badgeColor`) |
| `highlighted` | →flag | `.highlighted(_:)` |

### Chip (10 → ~3)
| `title`, `isSelected`(Binding) | **STAYS** |
| `size, selectionStyle` | →style/size | `.size(_:) .chipStyle(_:)` |
| `leadingSystemImage, rating` | →style | `.icon(_:) .rating(_:)` |
| `isExist, isInteractive, expandsHorizontally` | →flag | `.interactive(_:) .strikethrough(_:) .expands(_:)` |
| `isEnabled` | →.disabled | `.disabled(_:)` |

### Rating (10 → ~3)
| `value`, `onRate, onReviewTap` | **STAYS** |
| `maxValue, size, layout, allowHalf, systemImage` | →style/flag | `.scale(max:) .size(_:) .layout(_:) .allowHalf(_:) .symbol(_:)` |
| `isEnabled` | →.disabled |
| `countLabel, sentiment` | →flag | `.caption(count:sentiment:)` |

### Checkbox / RadioButton (10–11 → ~3)
| `label`, `isChecked/isSelected`(Binding), `infoMessages` | **STAYS** |
| `size`(ControlSize), `customSize` | →controlSize | native `.controlSize()` + `.size(custom:)` |
| `type, style, padding, alignment, backgroundColor` | →style | `.checkboxStyle(_:)` / `.radioStyle(_:)` (type+style+padding+align in one protocol/modifier) |
| `isIndeterminate` | →flag | `.indeterminate(_:)` |
| `isEnabled` | →.disabled · `accessibilityID` | →.a11yID |

### ProgressBar (10 → ~2)
| `value` | **STAYS** |
| `height, gradient, strokeColor, trailColor` | →color/size | `.barHeight(_:) .gradient(_:) .colors(stroke:trail:)` |
| `showPercentage, status, steps, successSegment, format, accessibilityLabel` | →flag | `.percentage(_:) .status(_:) .steps(_:) .successAt(_:) .valueFormat(_:)` |

### Pagination (9 → ~3)
| `current`(Binding), `total` | **STAYS** |
| `simple, siblingCount, boundaryCount, showJumper, jumperTitle, showTotal` | →flag | `.simple(_:) .window(sibling:boundary:) .jumper(title:) .total(_:)` |
| `isEnabled` | →.disabled |

### Accordion (11 → ~3)
| `title`, `content`(@ViewBuilder) | **STAYS** |
| `subtitle, number, leadingSystemImage` | →style/content | `.subtitle(_:) .number(_:) .icon(_:)` (or subtitle/number STAYS — content) |
| `indicator, titleSize, paddingSize` | →style/size | `.indicator(_:) .titleSize(_:) .density(_:)` |
| `truncateSubtitle, initiallyExpanded, showDivider` | →flag | `.truncateSubtitle(_:) .expanded(_:) .divider(_:)` |

### Stat (8 → ~3)
| `title, value`, `trend` | **STAYS** (StatStyle layout already exists ✅) |
| `prefix, suffix, description, systemImage` | →content/style | most STAY (content); `systemImage` →`.icon(_:)` |
| `isLoading` | →flag | `.loading(_:)` |

### DateField / SelectBox / MultiSelect / SearchBar / Slider / RangeSlider / InputNumber / MultiLineTextInput
Common pattern (form/input family):
- **STAYS:** binding (`date/selection/text/value/lowerValue/upperValue`), `options`, `range/bounds`, `optionTitle/isOptionEnabled`, `onChange*/onSubmit/onSearch`, `placeholder`, `infoMessages`, `marks`.
- **→.disabled:** `isEnabled` (all). **→.a11yID:** `accessibilityID` (all).
- **→flag(3):** `allowClear, searchable, isLoading, showInputs, showJumper, showValueTooltip, editable, showBackButton, showMuteToggle, loop, autoplay, muted, debounce, maxTagCount, maxResults, step` → each a fluent flag modifier (`.clearable() .searchable() .loading() .step(_:) .debounce(_:) …`).
- **→style/size(3):** `style, size, axis, minHeight, verticalHeight, leadingSystemImage, hint, errorText` → style/size modifiers (hint/errorText can fold into `infoMessages`).

### Carousel / VideoPlayerView (media)
- **STAYS:** `items/url`, `content`(@ViewBuilder), `currentIndex/progress/isMuted`(Binding), `onTap`.
- **→flag(3):** `autoplay, showsArrows, showsDots, loop, fade, dotPosition, muted, showMuteToggle, tapToToggle` → fluent flags (`.autoplay(_:) .arrows(_:) .dots(_:) .loop(_:) .muted(_:)`).

---

## Clean components (≤4 params) — UNTOUCHED
51 components (Avatar, Tag, Kbd, Spinner, Divider, Icon, StatusDot, Skeleton, Toast, Card, Hero, EmptyState, … + the 6 new ones in this plan: Join/Mask/TextRotate/Gauge/ShareButton/ColorField). These are already lean along the content+binding+action axis; **no unnecessary changes** (audit rule: don't touch Solid).

---

## Migration safety

> **Note — the repo is not public yet.** The task says "deprecate+forward" (library rule); but
> the owner previously preferred "not public → drop deprecated, clean break." **Two paths:**
> **(A) deprecate+forward** (rule-compliant, source-compatible) — old init `@available(*, deprecated, message: "Use .x() modifier")` + internally forwards to the new API.
> **(B) clean break** (since it's not public) — remove the old init, update all call sites (as we did for the button family).
> **Recommendation:** use **(B)** until public release (cleaner, less boilerplate); switch to **(A)** at 1.0 once the API freezes. Owner's call.

- Modifier defaults behave **identically** to the old param defaults → no visual change.
- public component → public modifier. After each component: update call sites + #Preview + gallery registry + snapshot + DocC; don't move to the next component until `swift build`+`test`+Demo are green.
- Avoid names that clash with SwiftUI: use the natives `.tint/.font/.controlSize`; use a separate name like `.badgeColor` for semantic color.

## Suggested order (PHASE 2)
1. **X1 `isEnabled`→`.disabled()`** (14 components, one pattern — highest leverage, lowest risk).
2. **X2 `accessibilityID`→`.a11yID()`** (12 components).
3. **X3 `size`→`controlSize`/`.size()`**.
4. Then component-by-component, **bloated first**: TextInput → MultiSelect/DateField → Badge/Chip/RadioButton/Checkbox → ProgressBar/Rating/Pagination/Accordion/Stat → form/media family.
5. Each component: apply plan → call sites+preview+gallery+test+doc → build/test/Demo green → record the "old→new" mapping line in MODIFIERS_PLAN.md.

---

## PHASE 2 — implementation log (old → new mapping)
_(Recorded here as each component is converted.)_

- **Button family** ✅ (#96–#98): `isContentWidth: true` → (remove, default content-width) · `isContentWidth: false` → `block: true` · `isEnabled: $x` → `.disabled(!x)`.
- **X1 `isEnabled`→`.disabled()`** ✅ (13 components): Chip, Rating, Checkbox, RadioButton, Pagination, DateField, MultiSelect, RangeSlider, SearchBar, SelectBox, Slider, InputNumber, MultiLineTextInput → `@Environment(\.isEnabled)`. Mapping: `isEnabled: $x`→`.disabled(!x)` · `isEnabled: false`→`.disabled(true)`. **TextInput** (model-based) deferred to its own refactor; **RadioButtonGroup/CheckboxGroup/SegmentedControl/ThemeToggle** out of X1 scope (round 2).
- **TextInput** ✅ (X1+X2 deferral closed): `isEnabled`→`@Environment(\.isEnabled)` (native `.disabled()`), `accessibilityID`→`.a11yID()` modifier. Both removed from `TextInputModel` + the flat init; the View holds `@Environment(\.isEnabled)` + `private var accessibilityID` (6× `model.isEnabled`→`isEnabled`, 6× `model.accessibilityID`→`accessibilityID`). Call sites: MoleculeDemos (7 models, per-mode `demoA11yID` switch + `.a11yID()`), MoreDemos (2 form fields), TextInput #Preview (2) + Accessibility.swift doc example. **Full 23→6 teardown DATA-DRIVEN REJECTED:** **11 of the 23 call sites use `TextInputModel(...)`** config bundles (solid escape hatch), flat init params are sparse at call sites (at most 3, mostly 1), `isEnabled` 0 usages. Splitting 15 style/flag params into 15 modifiers would break the 11 model call sites + marginal benefit → per "don't break Solid / don't make unnecessary changes" the model was preserved and only cross-cut consistency was applied.
- **VideoPlayerView** ✅ (10→5 init): playback flags (`autoplay/loop/muted/showMuteToggle/tapToToggle`) → modifiers; `url` (content) + bindings (`progress/isMuted/isActive`) + `onTap` stay in init. Modifiers: `.autoplay(_:) .loop(_:) .muted(_:) .muteToggle(_:) .tapToToggle(_:)` (autoplay/loop/muted default `true` preserved — inline auto-play video). Call sites: MoreDemos demo, ComponentRegistry usage.
- **Carousel** ✅ (9→4 init ×2, data-driven): 2 inits (content + activeContent). `items` (content) + `loop`+`currentIndex` (**seeds the @State `selection` → must stay in init**, like Accordion) + content stay in init. `autoplay`(5)/`showsArrows`(3)/`showsDots`(0)/`fade`(1)/`dotPosition`(1) → modifiers: `.autoplay(_:) .arrows(_:) .dots(_:position:) .fade(_:)` (showsDots+dotPosition → grouped into `.dots`). Call sites: OrganismDemos demo, ComponentRegistry usage, #Preview. (PagingCarousel is a separate component, unaffected.)
- **RangeSlider** ✅ (9→4 init, data-driven): 5 call sites → `lowerValue/upperValue/bounds/step` stay in init; `marks`(4)/`onChangeEnd`(3)/`valueLabel`(2)/`showInputs`+`inputTitles`(1) → modifiers: `.marks(_:) .inputs(_:titles:) .onChangeEnd(_:) .valueLabel(_:)` (showInputs+inputTitles → grouped into `.inputs`). Call sites: MoleculeDemos demo (inputs+marks variants), ComponentRegistry usage, ScreenshotGenerator, #Preview.
- **Slider** ✅ (9→4 init, data-driven): 8 call sites → `value/bounds/step/label` (step+label common) stay in init; `marks` 4×, `showValueTooltip` 4×, `onChangeEnd` 2×, `axis`/`verticalHeight` 1× → modifiers: `.marks(_:) .axis(_:height:) .showsValueTooltip(_:) .onChangeEnd(_:)` (axis+verticalHeight → grouped into `.axis(_:height:)`). Call sites: MoleculeDemos demo (vertical+horizontal), ComponentRegistry usage, ScreenshotGenerator (gallery shot), #Preview.
- **Pagination** ✅ (8→2 init, data-driven): 9 call sites → all config params sparse (siblingCount/showJumper 3×, simple/showTotal 2×, jumperTitle 1×, boundaryCount 0×) → all to modifiers; `current`/`total` (content/data) stay in init. Modifiers: `.simple(_:) .window(sibling:boundary:) .jumper(_:title:) .showTotal(_:)` (siblingCount+boundaryCount → `.window`, showJumper+jumperTitle → grouped into `.jumper`). Call sites: MoleculeDemos demo (4 modifiers), ComponentRegistry usage, #Preview.
- **InputNumber** ✅ (11→8 init, data-driven — minimal): 8 call sites → most params are **actually used** (label 9×, range 7×, unit 6×, step 5×, large 4×, hint 3×) → STAY in init ("don't make unnecessary changes"). Only sparse/dead: `editable` 2×, `hasInfo` 0×, `onChange` 0× → modifiers: `.editable(_:) .hasInfo(_:) .onValueChange(_:)` (`.onValueChange` to avoid confusion with SwiftUI `.onChange(of:)`). Call sites: MoleculeDemos demo (2× `.editable`).
- **MultiSelect** ✅ (11→7 init, data-driven): config flags sparse (isLoading 3×, searchable/allowClear/maxTagCount 1×) → modifiers: `.searchable(_:) .clearable(_:) .maxTags(_:) .loading(_:)`. `label/options/selection/placeholder/infoMessages/isOptionEnabled/optionTitle` (content/data) stay in init. Call sites: MoreDemos demo (4 modifiers), ComponentRegistry usage.
- **DateField / SelectBox / Stat** ⏭️ **(skipped — data-driven)**: DateField params are mostly used (style 6×, allowClear 6×, leadingSystemImage 4×) → teardown would be an "unnecessary change." SelectBox ~7 params, most content (label/options/selection/placeholder/hint/errorText/optionTitle). Stat already uses the StatStyle protocol + most of its 8 params are display content. All three left as-is per "don't break Solid / don't make unnecessary changes."
- **Autocomplete** ✅ (10→5 init ×2 + X1): hadn't been in X1 → `isEnabled`→`@Environment(\.isEnabled)`. 2 inits (static + async). `label/text/suggestions/suggest/placeholder/onSelect` (content/data/primary callback) stay in init; `maxResults`(0)/`debounce`(0)/`isSuggestionEnabled`(2)/`onSearch`(0) → modifiers: `.maxResults(_:) .debounce(_:) .suggestionEnabled(_:) .onSearch(_:)`. Async init `debounce=0.3` baseline in the init body (like SearchBar). Call sites: MoreDemos demo (static+async, `.suggestionEnabled`).
- **SearchBar** ✅ (14→8 init ×2, data-driven + cautious): 2 inits (classic + async `suggest`). `text/placeholder/suggestions/recent` (content/data) + **interaction callbacks** (`onSearch/onSelect/onSubmit/onClearRecent`) STAY in init (the component's contract; also `onSubmit` would clash with SwiftUI native `.onSubmit`). **Chrome+tuning** → modifiers: `.backButton(_:action:)` (showBackButton+onBack), `.trailingIcon(_:action:)` (trailingSystemImage+onTrailing), `.debounce(_:)`, `.maxResults(_:)`. Async init sets the `debounce=0.3` baseline in the init body (`.debounce(_:)` can override it) — per-init default preserved. Call sites: MoreDemos demo (`.backButton/.trailingIcon`), 2 #Preview.
- **Accordion** ✅ (11→4 init, data-driven): 7 call sites → `initiallyExpanded` 5× (+ seeds `@State expanded`, must stay in init), `leadingSystemImage` 3× **stays in init** (+ title/content @ViewBuilder); `subtitle`/`truncateSubtitle`/`showDivider` 0×, `number`/`indicator`/`titleSize`/`paddingSize` 1× → modifiers: `.subtitle(_:) .number(_:) .indicator(_:) .titleSize(_:) .density(_:) .truncateSubtitle(_:) .divider(_:)` (`.density` = paddingSize; avoided clashing with SwiftUI `.padding`). The single call site (OrganismDemos demo) converted to modifiers.
- **Rating** ✅ (10→3 init, data-driven): 9 call sites → `layout` 5×, `countLabel` 6× **stay in init** (+ content `value`); `allowHalf` 3×, `size`/`onReviewTap` 2×, `systemImage`/`onRate` 1×, `maxValue`/`sentiment` 0× → modifiers: `.maxValue(_:) .starSize(_:) .allowHalf(_:) .symbol(_:) .sentiment(_:) .onRate(_:) .onReviewTap(_:)`. Optional callbacks (`onRate/onReviewTap`) moved to idiomatic `.onRate{} .onReviewTap{}` modifiers (trailing closure preserved). Call sites: AtomDemos demo, ComponentRegistry usage string, #Preview.
- **Chip** ✅ (10→4 init, data-driven): X1 had added `@Environment(\.isEnabled)` to Chip but left a **dead `isEnabled` param** in the init (it wasn't assigned, silently ignored — bug risk) → removed. 16 call sites: `selectionStyle` 6×, `size` 3× **stay in init** (+ title/isSelected); `leadingSystemImage/rating/isExist/expandsHorizontally` 1×, `isInteractive` 0× → modifiers: `.icon(_:) .rating(_:) .exists(_:) .interactive(_:) .expands(_:)`. The single call site (AtomDemos Chip demo) converted to a modifier showcase + #Preview (`.icon`).
- **ProgressBar** ✅ (11→3 init, data-driven): 17 call sites measured → `showPercentage` 12×, `status` 5× **stay in init** (+ content `value`); `height` 3×, `gradient` 3×, `steps` 2×, `strokeColor`/`trailColor`/`successSegment` 1×, `format`/`accessibilityLabel` 0× → modifiers: `.barHeight(_:) .gradient(_:) .steps(_:) .colors(fill:track:) .successSegment(_:) .valueFormat(_:) .progressLabel(_:)`. (strokeColor+trailColor grouped into `.colors(fill:track:)`; the successSegment clamp moved into the modifier.) Call sites: Upload (`barHeight`), AtomDemos demo (5-modifier showcase), DisplaySnapshotTests (`.gradient()` — visually identical, snapshot valid), #Preview.
- **Badge** ✅ (11→6 init, data-driven): call-site density measured (36 sites) → `style` 33×, `leadingSystemImage` 11×, `size` 10×, `variant` 4× **stay in init** (real usage); `shape` 2×, `textColor` 1×, `trailingSystemImage`/`gradient`/`highlighted` **0×** → moved to self-returning modifiers: `.badgeShape(_:) .trailingIcon(_:) .badgeColor(_:) .gradient(_:) .highlighted(_:)`. (`.badgeColor` is a separate name to avoid clashing with SwiftUI `.tint/.foregroundColor`.) The single call site (AtomDemos Badge demo, exercising the whole long tail via knobs) converted to a modifier showcase + #Preview. Churn ~2 sites.
- **X3 `size`→`.controlSize()`** ✅ (3 components — the ControlSize trio): Checkbox, RadioButton, ThemeToggle. ThemeKit's `public enum ControlSize` (small/medium) custom enum was **shadowing** SwiftUI's `ControlSize` (collision); removed. All three moved to the native `ControlSize` + `@Environment(\.controlSize)` + native `.controlSize(_:)` cascade. The `size` init param was removed. Metric: `extension ControlSize { var checkboxSide }` (`.mini/.small`→20, default `.regular`→24) in Checkbox+RadioButton; ThemeToggle track uses `isCompact` (32×20 / 40×24). Mapping: `size: .small`→`.controlSize(.small)` · `size: .medium`→(remove, native default `.regular`=old `.medium`=24, visually identical) · `size: small ? .small : .medium`→`.controlSize(small ? .small : .regular)`. `customSize: CGFloat?` (Checkbox pixel escape hatch) stayed in init. Call sites: TreeSelect, MultiSelect, MoleculeDemos(Checkbox/RadioButton/ThemeToggle demo). **Remaining size params** (Avatar/Badge/Chip/Divider/ListRow/ProgressIndicator/SegmentedControl/Select/Rating/RadialProgress + button/TextInput) are component-specific enums → to be handled in the component-by-component phase (not a single mechanical cross-cut).
- **X1 round-2 (containers)** ✅ (4 components — X1 FULLY CLOSED): CheckboxGroup, RadioGroup (+ RadioButtonGroup 2nd struct), SegmentedControl, Select → control-level `isEnabled` moved to `@Environment(\.isEnabled)`. Multi-init forwards (convenience→designated) were dropped; in SegmentedControl the **per-item `SegmentItem.isEnabled` was preserved** (a separate concept); in Select the `SelectStyleConfiguration(isEnabled:)` internal forward was preserved (reads from env). `.disabled()` cascades natively to children. Call sites: SegmentedControl ×2, RadioGroup ×1, RadioButtonGroup ×2, CheckboxGroup ×1 → `.disabled(!enabled)`. **No component now carries `isEnabled: Bool` in its init.**
- **X1 round-2 (leaves)** ✅ (5 components): leaves not covered by the original X1 → `isEnabled: Bool` init param moved to `@Environment(\.isEnabled)` (native `.disabled()`): ThemeToggle, OTPInput, QuantityStepper, FileInput, TreeSelect. The OTPInput main struct reads from env and preserves the `isEnabled:` forward to the internal `OTPDigitBox`. Call site: ThemeToggle demo+#Preview (`.disabled()`); the other 4 have no call sites (default true). **Remaining X1 round-2 = containers** (CheckboxGroup/RadioGroup/SegmentedControl/Select) — to be handled separately/carefully due to the `isEnabled` forward to children + the per-item `SegmentItem.isEnabled`.
- **X2 `accessibilityID`→`.a11yID()`** ✅ (21 components): SelectBox, Autocomplete, CheckboxGroup, RadioGroup, SegmentedControl, RangeSlider, SearchBar, Select, RadioButton, ToggleGroup, Slider, ThemeToggle, OTPInput, InputNumber, Checkbox, DateField, MultiSelect, MultiLineTextInput, QuantityStepper, Swap, SegmentedTabBar. Mechanism: stored `accessibilityID` → `private var … = nil` (namespace stays hidden), init param removed, self-returning `func a11yID(_ id: String?) -> Self` modifier added (`var copy = self; copy.accessibilityID = …; return copy`). Mapping: `accessibilityID: "x"` → trailing `.a11yID("x")`. Call sites: DateField/Select/Checkbox demos migrated. **TextInput** (model-based) + **button family** (`ButtonConfiguration` intermediary; accessibilityID init param preserved) are out of X2 scope.

## PHASE 3 — strict R1–R7 modifier refactor (COMPONENT_REFACTOR_RULES)

Stricter ruleset adopted: init = `content + action` (≤2 params, R1); every
appearance/state axis is a chainable, order-free modifier from the R5 vocabulary
(`.variant/.size/.loading/.fullWidth/.icon/.shape`) routed through a single
copy-on-write `copy(_:)` helper (R2); `disabled` is native (R3); colors/metrics
from tokens (R4); **clean break** (owner's call — old inits removed, not
deprecated; recorded in `.api-breakage-allowlist.txt` + CHANGELOG). Scope:
Tier A+B (≥6-param components) first.

- **ThemeButton** ✅ (12→2 init — the canon). Init now `ThemeButton(_ title:action:)`. Moved to modifiers: `color:`→`.color(_:)`, `variant:`→`.variant(_:)`, `size:`→`.size(_:)`, `shape:`→`.shape(_:)`, `block:`→`.fullWidth(_:)`, `isLoading: Binding<Bool>`→`.loading(_ on: Bool = true)` (plain Bool — the button only ever *read* the binding), `systemImage:`+`iconPosition:`→`.icon(leading:trailing:)` (two slots replace the single image + position enum; `ButtonIconPosition` removed), `accessibilityID:`→`.a11yID(_:)`. `isEnabled: Binding<Bool>`→native `@Environment(\.isEnabled)` + `.disabled(_:)` (R3). Single `copy(_:)` mutation point (R2). Icon-only rendering hardened: the old code dropped the glyph for icon-only + `.trailing`; `content` now renders `leadingSystemImage ?? trailingSystemImage` for circle/square. Call sites migrated (≈40): Sources (Feedback, Dialog, Tour, ResultView, Popconfirm), Demo (ThemeButtonDemo knob, MoreDemos ×9, ThemesView ×3, ComponentRegistry usage string), Tests (ButtonSnapshotTests full matrix, ScreenshotGenerator, GifGenerator) + #Preview ×2. Skill docs (SKILL.md, components.md) updated. Snapshots unchanged (modifier defaults == old param defaults → visually identical). **Preset family** (PrimaryButton/…/Buttons.swift, `ButtonConfiguration` intermediary) is a separate ergonomic API — already native-`.disabled()` — and is a later queue item, not part of this change.
- **DateField** ✅ (10→2 init). Init now `DateField(_ label:date:)`. The PHASE 2 "skip" (data-driven) is **superseded** by PHASE 3's strict R1 clean break. Content (`label`) + the `date` Binding stay in init; the 8 appearance/config params → modifiers: `placeholder:`→`.placeholder(_:)`, `range:`→`.range(_:)` (sparse/0×, not required data → modifier per ListRow precedent), `style:`→`.style(_:)`, `locale:`→`.locale(_:)`, `components:`→`.components(_:)`, `infoMessages:`→`.infoMessages(_:)`, `allowClear:`→`.clearable(_ on: Bool = true)` (R5 flag vocabulary, matches MultiSelect), `leadingSystemImage:`→`.icon(_:)` (R5, matches ListRow). `accessibilityID` already `.a11yID()` (X2) — folded into the single `copy(_:)` helper (R2). `isEnabled` already native env (X1/R3). Stored config flipped `private let`→`private var` with defaults. The `var style`/`func style(_:)` and `var range`/`func range(_:)` property-vs-method pairs coexist (legal Swift, intended). Call sites migrated (5): MoreDemos DateFieldDemo knob (6 modifiers + `.a11yID` + `.disabled`), HotelSearchView ×2, ComponentRegistry usage string, ScreenshotGenerator gallery shot + #Preview ×3. Skill `components.md` updated. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **ListRow** ✅ (14→2 init). Init now `ListRow(_ title:action:)`. 12 params → modifiers: `subtitle:`→`.subtitle`, `number:`→`.number`, `size:`→`.size`, `leadingSystemImage:`→`.icon`, `leadingImageURL:`→`.leadingImage`, `leadingSelection:`→`.leadingSelection`, `alertCount:`→`.alertCount`, `badge:`→`.badge`, `meta:`→`.meta`, `infos:`→`.infos`, `isSelected:`→`.selected`, `multilineTitle:`→`.multilineTitle`, `infoAction:`→`.onInfo`, `trailing:`→`.trailing`. Single `copy(_:)` (R2). `action` kept optional in init (rows can be display-only). Call sites migrated (23): ListView, Drawer, MenuCard, MoreDemos (ListRowDemo knob + Drawer demos ×4 + List demo), ComponentRegistry usage, GifGenerator ×3, ScreenshotGenerator ×3, #Preview ×6. No dedicated snapshot test.
- **TreeSelect** ✅ (9→4 init). Init now `TreeSelect(_ label:nodes:selection:initiallyExpanded:)`. Content (`label`) + required DATA (`nodes`) + the `selection` Binding stay in init; `initiallyExpanded` **stays in init** (seeds the `@State expanded`, like Accordion/Carousel). 5 config params → modifiers: `placeholder:`→`.placeholder(_:)`, `cascade:`→`.cascade(_ on: Bool = true)`, `searchable:`→`.searchable(_ on: Bool = true)` (R5 flag), `isLoading:`→`.loading(_ on: Bool = true)` (R5 flag), `isNodeEnabled:`→`.nodeEnabled(_:)` (per-node predicate). Single `copy(_:)` (R2). `isEnabled` already native env (X1 round-2/R3). Stored config flipped `private let`→`private var` with defaults. The `var isNodeEnabled` stored property, the `func nodeEnabled(_:)` modifier, and the private `func nodeEnabled(_ node:)` helper coexist (distinct signatures, legal Swift). Call sites migrated (4): MoreDemos TreeSelectDemo knob (4 modifiers), ComponentRegistry usage string, ScreenshotGenerator gallery shot, #Preview. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **RadialProgress** ✅ (8→1 init). Init now `RadialProgress(_ value:)` (display atom — no action). Required DATA (`value`, clamped 0…1) stays in init; the 7 appearance/config params → modifiers: `size:`→`.size(_:)` (R5), `lineWidth:`→`.lineWidth(_:)`, `showLabel:`→`.showsLabel(_ on: Bool = true)` (R5 flag), `status:`→`.status(_:)`, `dashboard:`→`.dashboard(_ on: Bool = true)` (R5 flag), `tint:`→`.ringColor(_:)` (separate name to avoid clashing with SwiftUI `.tint`), `accessibilityLabel:`→`.a11yLabel(_:)` (separate name to avoid clashing with SwiftUI `.accessibilityLabel`). Single `copy(_:)` (R2). Stored config flipped `private let`→`private var` with defaults. The `var status`/`func status(_:)` and `var size`/`func size(_:)` property-vs-method pairs coexist (legal Swift, intended). Call sites migrated (6): MoreDemos RadialProgressDemo knob (5 modifiers), ComponentRegistry usage string, DisplaySnapshotTests (`.dashboard()`/`.status()` — visually identical, snapshot valid), ScreenshotGenerator ×2, #Preview. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **EmptyState** ✅ (11/9/9→1/2/2 init). EmptyState was listed "clean (≤4)/untouched" in PHASE 1, but its 3 inits each carried 8–10 params — PHASE 3's strict R1 applies. The 3 inits now key on the **media variant** (the distinguishing content): `EmptyState(_ title:)` (default SF Symbol), `EmptyState(image:title:)`, `EmptyState(animatedURL:title:)` — `title` (content) stays in init; media stored as a private `enum Media { case symbol/image/animated }`. The 9 other params → modifiers: `systemImage:`→`.icon(_:)` (R5; re-points the symbol media), `message:`→`.message(_:)`, `imageMaxHeight:`→`.imageMaxHeight(_:)`, `iconForeground:`→`.iconForeground(_:)`, `iconBackground:`→`.iconBackground(_:)`, `iconCircleSize:`→`.iconCircleSize(_:)`, `buttonTitle:`+`action:`→`.primaryAction(_ title:action:)` (paired title+handler), `secondaryTitle:`+`onSecondary:`→`.secondaryAction(_ title:action:)`. Single `copy(_:)` (R2); stored config `private var` with token defaults (R4). Call sites migrated (6): MoreDemos EmptyStateDemo knob (3 media branches → modifier chains), HotelFavoritesView, ComponentRegistry usage string, DisplaySnapshotTests, ScreenshotGenerator + #Preview. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Stat** ✅ (8→2 init ×2). Init now `Stat(title:value:)` (String + Int overloads). The PHASE 2 "skip" (data-driven — "most params are display content") is **superseded** by PHASE 3's strict R1 clean break. Content (`title` + `value`) stays in init; the 6 other params → modifiers: `prefix:`→`.prefix(_:)`, `suffix:`→`.suffix(_:)`, `isLoading:`→`.loading(_ on: Bool = true)` (R5 flag), `description:`→`.description(_:)`, `systemImage:`→`.icon(_:)` (R5), `trend:`→`.trend(_:)`. Single `copy(_:)` (R2). The private `Value`-taking designated init was removed (the two public inits now set `value` directly); stored config flipped `private let`→`private var` with defaults. The `var prefix/suffix/description/trend` stored props and the `func prefix/suffix/description/trend(_:)` modifiers coexist (legal Swift; `Stat` isn't a Sequence/CustomStringConvertible so no native clash). The `StatStyle` protocol + `.statStyle(_:)` are unchanged (orthogonal layout hook). Call sites migrated (8): MoreDemos StatDemo knob (2 branches, ×5 modifiers), ThemeInjectionDemo, HotelDetailView ×2 (`.icon`), ComponentRegistry usage string, DisplaySnapshotTests, ScreenshotGenerator ×2 + #Preview ×2 (Default/States). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **RadioButton** ✅ (8→3 init [+ tag-based convenience init 7→3]). Init now `RadioButton(_ label:isSelected:infoMessages:)`. Content (`label`) + the `isSelected` Binding + required validation DATA (`infoMessages`, default `[]`) stay in init; the 5 appearance params → modifiers: `type:`→`.type(_:)`, `style:`→`.radioStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match `RadioButtonStyle`), `padding:`→`.gap(_:)` (renamed to avoid clashing with SwiftUI's `.padding`; it's the radio↔label gap), `backgroundColor:`→`.fillColor(_:)` (renamed to avoid clashing with SwiftUI's `.backgroundColor`/`.background`), `verticalAlignment:`→`.alignment(_:)`. The already-present `.a11yID()` (X2) was **rewritten from inline `var copy = self` to route through the single `copy(_:)` helper** (R2 — one mutation point). `size`(ControlSize) already native `.controlSize()` (X3); `isEnabled` already native env (X1/R3). Stored config flipped `private let`→`private var` with defaults. The tag-based convenience init was trimmed to `RadioButton(tag:selection:type:infoMessages:)` (dropped `style`/`padding`/`backgroundColor` — 0 call sites; it forwards to the new init then sets `type`). Call sites migrated (1 with moved params): MoleculeDemos RadioButtonDemo knob (`.type/.radioStyle/.gap`). Internal users (ListRow `leadingView`, RadioGroup, SelectionCards `RadioCard`), ComponentRegistry usage, FormControlSnapshotTests ×3, ScreenshotGenerator, #Preview use only `label`+`isSelected`(+native `.disabled`) — unchanged. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **InputNumber** ✅ (8→3 init). Init now `InputNumber(_ label:value:range:)`. The PHASE 2 "minimal/data-driven" entry is **superseded** by PHASE 3's strict R1 clean break. Content (`label`) + the `value` Binding + required DATA (`range`, default 0...99) stay in init; the 5 remaining init params → modifiers: `step:`→`.step(_:)`, `unit:`→`.unit(_:)`, `hint:`→`.hint(_:)`, `errorText:`→`.errorText(_:)`, `large:`→`.large(_ on: Bool = true)` (binary height flag, R5 domain-flag naming). The already-present `.editable/.hasInfo/.onValueChange/.a11yID` modifiers were **rewritten from inline `var copy = self` to route through the single `copy(_:)` helper** (R2 — one mutation point). `isEnabled` already native env (X1/R3); `accessibilityID` already `.a11yID()` (X2). Stored config flipped `private let`→`private var` with defaults; `height` became a computed `large ? 48 : 40`. The `var step/unit/hint/errorText`/`func …(_:)` property-vs-method pairs coexist (legal Swift, intended). Call sites migrated (8): MoleculeDemos InputNumberDemo knob (2 branches, ×5 modifiers + `.editable`), HotelSearchView (`.large()`), ComponentRegistry usage string, ScreenshotGenerator gallery shot, #Preview ×3. InputNumberTests (static `clamp`/`parse`) unaffected. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Checkbox** ✅ (7→3 init, RadioButton sibling): `label`+`isChecked` Binding+`infoMessages` (required validation data) stay in init. `customSize`→`.customSize(_:)`, `type`→`.type(_:)`, `isIndeterminate`→`.indeterminate(_ on: Bool = true)`, `alignment`→`.alignment(_:)`. Size already native `.controlSize` (KEPT, no `.size`), `disabled` already native, `.a11yID(_:)` rerouted through the shared `copy(_:)` helper (was inline `var copy = self`). Call sites: CheckboxGroup (select-all master `isIndeterminate`→`.indeterminate`), TreeSelect (`isIndeterminate`→`.indeterminate`), MoleculeDemos (full knob set → 4 modifiers), FormControlSnapshotTests (`isIndeterminate: true`→`.indeterminate()`), #Preview. Untouched (only pass `isChecked`/`infoMessages`/label, valid new API): SelectionCards, ListRow `.checkbox`, MultiSelect, Fieldset, MoreDemos ×2, HotelCheckoutView, ScreenshotGenerator, ComponentRegistry usage.
- **MultiLineTextInput** ✅ (7→2 init): `label`+`text` Binding (content + binding) stay in init. `placeholder`→`.placeholder(_:)`, `characterLimit`→`.characterLimit(_:)`, `errorText`→`.errorText(_:)`, `infoMessages`→`.infoMessages(_:)`, `minHeight`→`.minHeight(_:)`. The init's `errorText`+`infoMessages`→`messages` merge moved to a computed `messages` property (`errorText` appended as `.error` `InfoMessage`). `disabled` already native, `.a11yID(_:)` rerouted through the shared `copy(_:)` helper (was inline `var copy = self`). Call sites: MoreDemos demo (placeholder/characterLimit/errorText → 3 modifiers), ComponentRegistry `usage:` string, #Preview. Untouched (label+text only): ScreenshotGenerator.
- **ProgressIndicator** ✅ (7→3 init): `variant` (core kind) + `current`/`total` (required data) stay in init. `size`→`.size(_:)`, `videoProgress`→`.videoProgress(_:)`, `stepText`→`.stepText(_:)`, `cornerRadius`→`.cornerRadius(_ on: Bool = true)`. `current`/`total` clamping kept in init; `videoProgress` 0…1 clamp moved to point-of-use in `fillFor`. No prior `.a11yID`/copy helper — added the single shared `copy(_:)`. Call sites: MoreDemos demo (videoProgress/stepText → 2 modifiers), ComponentRegistry `usage:` string (`stepText:`→`.stepText`), #Preview (4). Untouched (variant+current+total only): ScreenshotGenerator.
- **AlertToast** ✅ (7→1 init): `title` (content) stays in init; the 6 other params → modifiers: `message:`→`.message(_:)`, `type:`→`.variant(_:)` (R5; status drives fill+icon), `systemImage:`→`.icon(_:)` (R5), `isLoading:`→`.loading(_ on: Bool = true)` (R5 flag), `action: ToastAction?`→`.action(_:)`, `onClose:`→`.onClose(_:)`. Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (8): Feedback.swift `ToastBanner` (all 6 modifiers from the `ToastItem` model), Toast.swift `.toast` modifier (message/variant/onClose), OrganismDemos AlertToastDemo knob (message/variant/onClose), ComponentRegistry usage string, GifGenerator, ScreenshotGenerator + #Preview. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Avatar** ✅ (6/6→1 init ×1): both inits (size-tier + numeric `dimension:`) collapsed to a single `Avatar(_ content:)` — `content` (the `AvatarContent` source) is the only init param. The 5 config params → modifiers: `size:`→`.size(_:)` (R5), `dimension:`→`.dimension(_:)` (the numeric size escape hatch, now a modifier instead of a second init), `background:`→`.fillColor(_:)` (renamed to avoid clashing with SwiftUI `.background`, R5 precedent), `shape:`→`.shape(_:)` (R5), `presence:`+`presencePulse:`→`.presence(_ kind:pulse:)` (the dot + its pulse grouped into one modifier). Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 tokens kept). **AvatarGroup** (sibling struct) left as-is per scope — its internal `Avatar(...)` builders migrated to the new modifier API. Call sites migrated (7 + AvatarGroup internals): ChatBubble (`.size/.fillColor`), AtomDemos AvatarDemo knob (2 branches: `.dimension`/`.size` + `.fillColor/.shape/.presence`), ComponentRegistry usage string, DisplaySnapshotTests (`.size/.fillColor/.shape`), ScreenshotGenerator (`.size/.presence`) + #Preview ×2. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Badge (PHASE 3 finish)** ✅ (6→2 init): the PHASE 2 partial entry above is **superseded** by PHASE 3's strict R1 — the init was still 6 params. Now `Badge(_ text:action:)`: `text` (content) + `action` (optional tap handler — rows can be display-only) stay in init. The 4 remaining appearance params → modifiers: `style:`→`.badgeStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match `BadgeStyle`, R5 precedent like `.radioStyle`), `variant:`→`.variant(_:)` (R5), `size:`→`.size(_:)` (R5), `leadingSystemImage:`→`.icon(_:)` (R5, folds the leading glyph into the standard icon vocabulary). The 5 pre-existing modifiers (`.badgeShape/.trailingIcon/.badgeColor/.gradient/.highlighted`) were **rerouted from inline `var copy = self` to the single shared `copy(_:)` helper** (R2 — one mutation point). Stored config flipped the moved `private let`s → `private var` with defaults (R4 tokens kept). Call sites migrated (~22 real `Badge(` sites): Sources — PreviewMatrix ×3, ListRow `centerView` badge, Indicator; Demo — AtomDemos BadgeDemo knob (`text`+`action` in init, `.badgeStyle/.variant/.size/.icon` + long-tail modifiers), MoreDemos indicator, ThemeInjectionDemo, ComponentRegistry usage string, DesignPreviewSheet, ThemeConfiguratorView, ThemesView ×3, Hotel{Results,Search,Detail}View; Tests — ComponentSnapshotTests ×9, ThemePresetRenderProof ×3, ScreenshotGenerator ×4 + #Preview. `Badge("x")`-only sites (RenderSmokeTests ×2, ComponentSnapshotTests neutral) already valid new API — untouched. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **FileInput** ✅ (7→2 init): `label` (content) + `onPick` (primary callback) stay in init (positional `FileInput(_ label:onPick:)`). The 5 remaining params → modifiers: `fileName:`→`.fileName(_:)` (the bound display value), `buttonTitle:`→`.buttonTitle(_:)`, `placeholder:`→`.placeholder(_:)`, `infoMessages:`→`.infoMessages(_:)`, `onClear:`→`.onClear(_:)` (the optional trailing clear handler). Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 tokens kept). `isEnabled` already native env (X1 round-2/R3). Call sites migrated (3): MoreDemos FileInputDemo knob (`.fileName/.infoMessages/.onClear`), ComponentRegistry usage string, ScreenshotGenerator gallery shot (`.fileName/.onClear`) + #Preview ×2. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **OTPInput** ✅ (8→2 init): `code` (Binding) + `onComplete` (primary callback) stay in init (`OTPInput(code:onComplete:)`). The 6 remaining params → modifiers: `digitCount:`→`.digitCount(_:)`, `isSecure:`→`.secure(_ on: Bool = true)` (R5 domain flag), `errorText:`→`.errorText(_:)`, `infoMessages:`→`.infoMessages(_:)`, `resendInterval:`+`onResend:`→`.resend(interval:onResend:)` (the countdown+resend feature grouped into one modifier). The init's `errorText`+`infoMessages`→`messages` merge moved to a computed `messages` property (`errorText` appended as `.error`). The already-present `.a11yID()` (X2) was **rewritten from inline `var copy = self` to route through the single shared `copy(_:)` helper** (R2 — one mutation point). `isEnabled` already native env (X1 round-2/R3) — the internal `isEnabled:` forward to the private `OTPDigitBox` subview is preserved. Stored config flipped `private let`→`private var` with defaults. Call sites migrated (3): MoreDemos OTPDemo knob (`.digitCount/.secure/.errorText`, conditional `.resend`), ComponentRegistry usage string, ScreenshotGenerator gallery shot (`.digitCount`) + #Preview ×3. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Callout** ✅ (7→1 init): `text` (content) is the only init param (`Callout(_ text:)`). The 6 other params → modifiers: `type:`→`.variant(_:)` (R5; status drives accent+icon), `style:`→`.calloutStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match `CalloutStyle`, R5 precedent like `.badgeStyle`), `showIcon:`→`.showsIcon(_ on: Bool = true)` (R5 flag — toggles the type's leading icon), `actionTitle:`+`onAction:`→`.action(_ title:onAction:)` (the inline action button's title+handler grouped, like EmptyState's `.primaryAction`), `onClose:`→`.onClose(_:)` (the dismiss × handler). Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (≈14 real `Callout(` sites): Sources — Upload `.done/.failed` cases (`.variant`); Demo — OrganismDemos CalloutDemo knob (`.variant/.calloutStyle/.showsIcon/.onClose`, conditional `.action`), ComponentRegistry usage string, HotelResultsView, HotelCheckoutView ×2; Tests — DisplaySnapshotTests ×4, ScreenshotGenerator ×3 (`.calloutStyle` on soft), RenderSmokeTests + #Preview ×5. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **FloatingActionButton** ✅ (6→3 init): `systemImage` (content, default `"plus"`) + `actions:` (speed-dial DATA array) + `action:` (primary tap callback for the no-speed-dial mode) stay in init — the FAB has two modes (single action vs an `actions:` menu) and both the content glyph and its action(s) are its contract. The 3 appearance/state params → modifiers: `shape:`→`.shape(_:)` (R5; circle/square), `color:`→`.color(_:)` (R5 precedent for `tint:`/`color:`; the main button's semantic fill), `badge:`→`.badge(_:)` (the optional count bubble, `Int?`). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). `FABAction` (per-item struct) is data — its init left alone. Call sites migrated (1 real): MoreDemos FABDemo knob (`.shape/.color/.badge`). ComponentRegistry usage string + ScreenshotGenerator gallery shot use only `systemImage:`/`actions:` (still in init) — untouched; #Preview untouched. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **PromoBanner** ✅ (6→2 init): `title` (content) + `action:` (optional primary CTA callback — banners can be display-only) stay in init (`PromoBanner(_ title:action:)`). The 4 other params → modifiers: `subtitle:`→`.subtitle(_:)`, `systemImage:`→`.icon(_:)` (R5; folds the leading glyph into the standard icon vocabulary), `ctaTitle:`→`.ctaTitle(_:)` (the trailing button label; renders only when paired with the init `action`), `tint:`→`.color(_:)` (R5 precedent for `tint:`→`.color`; the `PromoBannerTint` treatment). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (4 real): OrganismDemos PromoBannerDemo knob (`.subtitle/.icon/.ctaTitle/.color`, conditional `ctaTitle`/`action`), HotelSearchView (`.subtitle/.icon/.ctaTitle`), ScreenshotGenerator gallery shot (`.subtitle/.icon/.ctaTitle`), ComponentRegistry usage string (`.icon/.ctaTitle`) + #Preview ×2. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Swap** ✅ (5→1 init): the `isOn` Binding is the only init param (`Swap(isOn:)`). Per the per-component directive the two glyphs + the appearance/state params → modifiers: `on:`/`off:`→`.symbols(on:off:)` (the two SF Symbols swapped between, grouped into one modifier; sensible defaults `"xmark"`/`"line.3.horizontal"`), `size:`→`.size(_:)` (R5; glyph point size), `rotate:`→`.rotate(_ on: Bool = true)` (R5 flag — the rotate-in/out transition). The already-present `.a11yID()` (X2) was **rewritten from inline `var copy = self` to route through the single shared `copy(_:)` helper** (R2 — one mutation point). Stored config flipped `private let`→`private var` with defaults. The `var size`/`func size(_:)` and `var rotate`/`func rotate(_:)` property-vs-method pairs coexist (legal Swift, intended). Call sites migrated (4 real `Swap(` sites): Demo — MoreDemos SwapDemo knob (2 branches: `.symbols(...).size(32)`), ComponentRegistry usage string (`Swap(isOn: $on).symbols(on: "xmark", off: "line.3.horizontal")`); Tests — ScreenshotGenerator gallery shot (`.symbols(...)`), #Preview ×2 (`.symbols(...)`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **RemoteImage** ✅ (5/5/5→1/2/2 init ×3): `url` (the content source) is the only param of the base init (`RemoteImage(_ url:)`); the two **data overloads** that carry a genuine aspect-ratio source are preserved — `RemoteImage(_ url:, ratio: String)` (API string like "16:9") and `RemoteImage(_ url:, ratio: RemoteImageRatio)` (named constant) — now set only the parsed ratio (they forward to the base init then assign `aspectRatio`). The 4 appearance/state params → modifiers: `aspectRatio:`→`.ratio(_:)` (**renamed** to avoid clashing with SwiftUI's native `.aspectRatio(_:contentMode:)`; numeric width/height), `contentMode:`→`.contentMode(_:)`, `cornerRadius:`→`.cornerRadius(_:)` (no clash on this type), `circle:`→`.circle(_ on: Bool = true)` (R5 flag — clip to a circle vs a rounded rect). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (8 real `RemoteImage(` sites — used in rows/chips/cards): Sources — ListRow `leadingView` (`.ratio(1).cornerRadius`), Chips ×2 (`.ratio(_:).cornerRadius` + `.contentMode(.fit)`), ImageCollage (`.contentMode(.fill).cornerRadius`); Demo — MoreDemos RemoteImageDemo knob (3 branches: gif `ratio:` overload + `.cornerRadius`, `.ratio(1).circle()`, `ratio:` overload + `.cornerRadius`), ComponentRegistry usage string (`RemoteImage(url, ratio: "16:9").cornerRadius(12)`); Tests/#Preview — base `#Preview` (`ratio:` overload + `.cornerRadius`, `.ratio(1).cornerRadius`). The `var aspectRatio`/`func ratio(_:)` (distinct names), `var contentMode`/`func contentMode(_:)`, `var cornerRadius`/`func cornerRadius(_:)`, `var circle`/`func circle(_:)` property-vs-method pairs coexist (legal Swift, intended). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **GaugeView** ✅ (5→3 init): required DATA (`value`) + content/data (`range`, default `0...1`; `label`, the optional caption) stay in init (`GaugeView(value:in:label:)`). The 2 appearance/state params → modifiers: `style:`→`.gaugeStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match the nested `GaugeView.Style`, R5 precedent like `.badgeStyle`/`.calloutStyle`), `showsValue:`→`.showsValue(_ on: Bool = true)` (R5 flag — toggles the inline percentage readout). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). The `var style`/`func gaugeStyle(_:)` (distinct names) and `var showsValue`/`func showsValue(_:)` property-vs-method pair coexist (legal Swift, intended). Call sites migrated (3 with moved params): ComponentRegistry static entry (`.gaugeStyle(.circular)` usage string + the linear demo), ScreenshotGenerator gallery shot (`.gaugeStyle(.linear)`), #Preview (`.gaugeStyle(.linear)`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **DividerView** ✅ (5→1 init): `title` (the optional inline content label) is the only init param (`DividerView(_ title:)`). The 4 appearance/state params → modifiers: `size:`→`.size(_:)` (R5; the non-titled/non-dashed thickness tier), `axis:`→`.axis(_:)` (horizontal/vertical orientation), `dashed:`→`.dashed(_ on: Bool = true)` (R5 flag), `titleAlign:`→`.titleAlign(_:)` (leading/center/trailing placement of the inline title). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (~30 real `DividerView(` sites — divider is used widely as a separator): Sources — Join (`.axis(.vertical)`), TreeSelect, CheckboxGroup, Autocomplete, MultiSelect ×2, SearchBar, Select, MenuCard, ListRow `#Preview` ×5, KeyValueTable, Card, Upload, Footer, Accordion, ListView ×4, Dialog, ButtonDock, AccordionGroup (all `.size(.small)`); Demo — MoreDemos DividerDemo knob (`title` in init, `.dashed/.titleAlign/.axis`) + List demo, TypographyView, ComponentRegistry usage string (`DividerView("OR").dashed().titleAlign(.center)`); Tests — ScreenshotGenerator gallery shot (`DividerView("OR").dashed().titleAlign(.center)`) + separator + #Preview. The `var size`/`func size(_:)`, `var axis`/`func axis(_:)`, `var dashed`/`func dashed(_:)`, `var titleAlign`/`func titleAlign(_:)` property-vs-method pairs coexist (legal Swift, intended). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Timeline** ✅ (5→1 init): `items` (the required `[Timeline.Item]` DATA array) is the only init param (`Timeline(_ items:)`). The 4 layout/state params → modifiers: `axis:`→`.axis(_:)` (rail orientation vertical/horizontal), `mode:`→`.mode(_:)` (content placement left/right/alternate around the rail, vertical only), `reverse:`→`.reversed(_ on: Bool = true)` (R5 domain flag — flips item order), `pending:`→`.pending(_:)` (the trailing loading node label). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). **`Timeline.Item`** (per-item data struct) init left untouched per scope — only the `Timeline` view init was refactored. Call sites migrated (2 real): MoreDemos TimelineDemo knob (`.axis/.mode/.reversed/.pending`), ScreenshotGenerator gallery shot (`.pending`), ComponentRegistry usage string (`.pending`) + #Preview ×2 (`.pending`, `.mode(.alternate)`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **ImageCollage** ✅ (5→2 init): `urls` (the required `[URL]` DATA array) + `onTap` (the per-tile tap callback) stay in init (`ImageCollage(_ urls:onTap:)`). The 3 layout/appearance params → modifiers: `height:`→`.height(_:)`, `spacing:`→`.spacing(_:)`, `cornerRadius:`→`.cornerRadius(_:)`. Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 — defaults 220/4/12 preserved). The `var height/spacing/cornerRadius` stored props and the `func height/spacing/cornerRadius(_:)` modifiers coexist (legal Swift, intended). Call sites migrated (3 real): Demo — MoreDemos ImageCollageDemo knob (`urls`+`onTap` in init, `.height(220)`), ComponentRegistry usage string (`.height(220)`); Tests/#Preview — #Preview ×2 (`.height(180)`/`.height(220)`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **ChatBubble** ✅ (5→3 init): `text` (content) + `author`/`time` (read as message content — kept in init per the per-component directive) stay in init (`ChatBubble(_ text:author:time:)`). Only the 2 appearance params → modifiers: `side:`→`.side(_:)` (the conversation direction incoming/outgoing — a clear domain name, not overloading `.variant`), `avatarSystemImage:`→`.icon(_:)` (R5; the avatar glyph, positioned by `side`). Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (≈7 real `ChatBubble(` sites): Demo — MoreDemos ChatBubbleDemo knob (`.side/.icon`), ComponentRegistry usage string (`.side/.icon`), HotelDetailView ×2 (`.side/.icon`); Tests — ScreenshotGenerator ×2 (`.side`) + #Preview ×2 (`.side/.icon`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Steps** ✅ (5→2 init): `steps` (the required `[Steps.Step]` DATA array) + `onSelect` (the tap-to-navigate callback) stay in init (`Steps(_ steps:onSelect:)`). The 3 appearance/layout params → modifiers: `axis:`→`.axis(_:)` (horizontal/vertical, precedent DividerView/Timeline), `small:`→`.small(_ on: Bool = true)` (binary compact flag, R5 domain-flag naming like InputNumber's `.large`), `progressDot:`→`.progressDot(_ on: Bool = true)` (Ant `progressDot` — minimal dots vs numbered markers). Single shared `copy(_:)` helper (R2); stored config flipped `private let`→`private var` with defaults (R4 tokens kept). **`Steps.Step`** (per-step data struct: title/description/systemImage/state/percent — all item content/data) init left untouched per scope. Call sites migrated (1 with moved params): MoreDemos StepsDemo knob (`.axis/.progressDot`). `Steps([...]) { … }`/data-only sites (ComponentRegistry usage string, HotelCheckoutView, ScreenshotGenerator gallery shot, #Preview horizontal) already valid new API — untouched; #Preview vertical migrated to `.axis(.vertical)`. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Tag** ✅ (5→2 init): `text` (content) + `onRemove` (optional removal callback) stay in init (`Tag(_ text:onRemove:)`). The 3 appearance params → modifiers: `leadingSystemImage:`→`.icon(_:)` (R5; folds the leading glyph into the standard icon vocabulary), `style:`→`.tagStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match `BadgeStyle`, R5 precedent like `.badgeStyle`/`.radioStyle`), `variant:`→`.variant(_:)` (R5; `FillVariant` soft/solid/outline/ghost). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (≈9 real `Tag(` sites): Sources — PageHeader `headerView` (`.tagStyle`); Demo — AtomDemos TagDemo knob (`text`+`onRemove` in init, `.icon/.tagStyle/.variant`), ComponentRegistry usage string, HotelResultsView (`.icon`), HotelDetailView (`.icon`); Tests — ComponentSnapshotTests ×2 (`.icon`), ScreenshotGenerator (`.tagStyle/.variant`) + #Preview ×2 (Default + States). `Tag("x")`/`Tag("x", onRemove:)`-only sites (MultiSelect ×2, ThemeInjectionDemo, ScreenshotGenerator plain rows) already valid new API — untouched. PreviewMatrix doc comment updated. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **SegmentedTabBar** ✅ (6→2 init ×2): both inits (`[TabItem]` + `[String]` overloads) collapse to `SegmentedTabBar(_ items:selection:onClose:onAdd:)` — `items` (required DATA) + `selection` (Binding) + the optional `onClose`/`onAdd` callbacks (the close-tab / add-tab interaction contract, card mode) stay in init. The 2 appearance params → modifiers: `scrollable:`→`.scrollable(_ on: Bool = true)` (R5 flag — horizontal scroll vs even distribution), `style:`→`.tabStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match `BadgeStyle`/`SegmentedTabBarStyle`, R5 precedent like `.badgeStyle`/`.calloutStyle`; underline/card). The already-present `.a11yID()` (X2) was **rewritten from inline `var copy = self` to route through the single shared `copy(_:)` helper** (R2 — one mutation point). Per-item `TabItem.isEnabled` is item data — **left as-is**. Stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (2 with moved params): OrganismDemos SegmentedTabBarDemo knob (card branch `style: .card`→`.tabStyle(.card)`, default branch `scrollable:`→`.scrollable(scrollable)`). `SegmentedTabBar([...], selection:)`-only sites (ComponentRegistry usage string, ScreenshotGenerator gallery shot, captions/rich demo branches) already valid new API — untouched; #Preview scrollable migrated to `.scrollable()`. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **SelectionCards** ✅ (R3 — `isEnabled` removed, no other change): the two public structs **RadioCard** (`RadioCard(_ title:description:isSelected:action:)`) and **CheckboxCard** (`CheckboxCard(_ title:description:isChecked:action:)`) each carried a component-level `isEnabled: Bool = true` in their init — **removed** per R3; the disabled state is now native (`.disabled()` cascades through the inner `Button`, no custom Bool). `title` (content) + `description` (content) + `isSelected`/`isChecked` (selection data) + `action` (required callback) stay in init. The private `SelectionCard` helper view also dropped its `isEnabled` field + `.disabled(!isEnabled)` (the styling only ever depended on `isSelected`; `.disabled()` is inherited from the environment). No modifiers added (nothing left to move). Call sites migrated (0 — no site ever passed `isEnabled:`, so dropping the defaulted param is source-compatible for callers; selector removed for the API-breakage check): HotelCheckoutView ×3, OrganismDemos SelectionCardsDemo ×3, ScreenshotGenerator ×2, ComponentRegistry usage string, #Preview ×3 — all already valid. Migration if any caller had used it: `RadioCard(…, isEnabled: x) { … }` → `RadioCard(…) { … }.disabled(!x)`. Snapshots unchanged.
- **Upload** ✅ (7→5 init): `prompt` (content, default copy) + `files` (DATA array) + the three callbacks `onPick`/`onRemove`/`onRetry` (the pick/remove/retry interaction contract) stay in init. The 2 appearance/config params → modifiers: `buttonTitle:`→`.buttonTitle(_:)`, `maxCount:`→`.maxCount(_:)` (the file cap — `nil` = unlimited; gates the picker `.disabled` + the count readout). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (the localized `"Upload Photo"` button default + `nil` cap preserved). The sibling **`UploadList`** struct (controller-bound wrapper) is a separate component — its own `prompt`/`buttonTitle` params stay; its internal `Upload(...)` builder migrated to `.buttonTitle(buttonTitle)`. Call sites migrated (2 with moved params): MoreDemos UploadDemo (`.buttonTitle("Add photo").maxCount(3)`), ScreenshotGenerator gallery shot (`.buttonTitle("Add photo").maxCount(3)`). ComponentRegistry usage string (uses `UploadList`), static `#Preview`s (`Upload(files:…, onRetry:)` only) already valid — untouched. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Chip (PHASE 3 finish)** ✅ (4→2 init): the PHASE 2 entry above (which kept `size`/`selectionStyle` in init "data-driven") is **superseded** by PHASE 3's strict R1 — init is now `Chip(_ title:isSelected:)`. `title` (content) + the `isSelected` Binding stay in init; the 2 remaining params → modifiers: `size:`→`.size(_:)` (R5; small/large), `selectionStyle:`→`.chipStyle(_:)` (renamed from the bare `selectionStyle` to avoid the generic clash + match `BadgeStyle`/`.badgeStyle`/`.tagStyle`, R5 precedent; tonal/solid). The 5 pre-existing modifiers (`.icon/.rating/.exists/.interactive/.expands`) were **rerouted from inline `var copy = self` to the single shared `copy(_:)` helper** (R2 — one mutation point). `isEnabled` already native env (X1/R3 — no param to remove). Stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). The `var size`/`func size(_:)` property-vs-method pair coexists (legal Swift, intended). Call sites migrated (≈6 with moved params): Sources — Chips.swift `FilterChip` (`.chipStyle(selectionStyle)`), FilterGroup (`.chipStyle(.solid)`); Demo — AtomDemos ChipDemo knob (`.size(...).chipStyle(...)` + the long-tail modifiers), ThemeConfiguratorView (`.chipStyle(.solid)`), ComponentRegistry usage string (`.chipStyle(.tonal)`); Tests — #Preview (`.chipStyle(.tonal/.solid)`, `.size(.large)`). `Chip("x", isSelected:)`-only sites (ScreenshotGenerator ×2, ComponentSnapshotTests ×2, ThemePresetRenderProof ×2) already valid new API — untouched. (`ImageChip` is a separate Molecule, unaffected.) Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **InputLabel** ✅ (4→1 init): `text` (content) stays in init (`InputLabel(_ text:)`). The 3 appearance/state params → modifiers: `isRequired:`→`.required(_ on: Bool = true)` (R5-style flag — the trailing asterisk), `hasInfo:`→`.hasInfo(_ on: Bool = true)` (the info glyph), `hasError:`→`.hasError(_ on: Bool = true)` (error-color treatment). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated: Sources — DateField (`InputLabel(label, hasError: hasError)`→`InputLabel(label).hasError(hasError)`); Demo — MoreDemos InputLabelDemo knob (`.required(required).hasInfo(info).hasError(error)`), ComponentRegistry usage string (`.required().hasInfo()`); Tests — ScreenshotGenerator (`.required().hasInfo()`) + #Preview ×2. `InputLabel(label)`-only sites (MultiSelect, Autocomplete, FileInput, TreeSelect) already valid new API — untouched. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **RollingNumber** ✅ (4→1 init): `value` (content) stays in init (`RollingNumber(_ value:)`). The 3 appearance params → modifiers: `size:`→`.size(_:)` (R5; the CGFloat digit point size), `weight:`→`.weight(_:)` (R5 precedent — `Font.Weight`), `color:`→`.color(_:)` (R5 precedent — optional override, `nil` = `textPrimary`). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated: Sources — Stat `.number` case (`.size(28).weight(.semibold).color(...)`); Demo — MoreDemos RollingNumberDemo knob (`.size(size).color(...)`), ComponentRegistry usage string (`.size(40)`); Tests — ScreenshotGenerator (`.size(40)`) + #Preview (`.size(40)`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **StatusDot** ✅ (4→2 init): `kind` (content — the status) + `label` (content — the visible/spoken text) stay in init (`StatusDot(_ kind:label:)`). The 2 appearance/state params → modifiers: `size:`→`.size(_:)` (R5; the CGFloat dot diameter), `pulse:`→`.pulse(_ on: Bool = true)` (R5-style flag — the animated pulse ring). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (3 with moved params): Sources — Avatar presence dot (`StatusDot(presence).size(dot).pulse(presencePulse)`); Demo — MoreDemos StatusDotDemo knob (`StatusDot(kind, label:).size(14).pulse(pulse)`), ComponentRegistry usage string (`.pulse()`); Tests — RenderSmokeTests (`.pulse()`) + #Preview (`.pulse()`). `StatusDot(kind, label:)`-only sites (ComponentSnapshotTests ×4, ScreenshotGenerator ×3) already valid new API — untouched. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Chips (ImageChip)** ✅ (4→2 init; R3): the spec's "Chips" target is the `ImageChip` struct in `Chips.swift` — the only chip with a component-level `isEnabled: Bool` in its init (signature `isSelected:url:size:isEnabled:`). `isSelected` (Binding) + `url` (DATA) stay in init (`ImageChip(isSelected:url:)`). `size:`→`.size(_:)` (R5; ImageChipSize small/medium/large). The component-level `isEnabled: Bool` was **REMOVED per R3** — disabled state is now native (`@Environment(\.isEnabled)`, read for the `.opacity`/tap gate; `.disabled(_:)` cascades). Single shared `copy(_:)` helper (R2); the moved `private let size`→`private var` with default. The sibling chip structs (CompactChip, ChoseChip, FilterChip, ChipGroup) and the singular `Chip` were left untouched. Call sites migrated (2): MoreDemos image branch (`ImageChip(isSelected:url:).size(.medium)` ×2). No call site ever passed `isEnabled:`, so the R3 migration (`isEnabled: x`→`.disabled(!x)`) had no real sites; documented for completeness. Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **Coupon** ✅ (4→3 init): `code` (content) + `label` (content, default `"Kupon Kodu:"`) + `onCopy` (the copy-action callback, default `{}`) stay in init (`Coupon(code:label:onCopy:)`). The 1 appearance param → modifier: `style:`→`.couponStyle(_:)` (renamed from the bare `style` to avoid the generic clash + match `BadgeStyle`/`.badgeStyle`/`.tagStyle`, R5 precedent; filled/outlined/plain). Single shared `copy(_:)` helper (R2); the moved `private let style`→`private var` with `.outlined` default (R4 tokens kept). Call sites migrated: Demo — OrganismDemos CouponDemo knob (`.couponStyle(style)`), ComponentRegistry usage string (`.couponStyle(.outlined)`); Tests — ScreenshotGenerator (`.couponStyle(.outlined)`) + #Preview ×3 (`.couponStyle(.filled/.outlined/.plain)`). Snapshots unchanged (modifier defaults == old param defaults → visually identical).
- **SegmentedControl** ✅ (4→2 init): two overloads keep their content/data only — `items` (`[SegmentItem]` or `[String]` DATA) + `selection` (Binding). The 2 appearance params moved to modifiers: `block:`→`.fullWidth(_ on: Bool = true)` (R5 — Ant Segmented `block`, default `true` preserves the old default), `size:`→`.size(_:)` (`SegmentedSize`). The pre-existing `.a11yID` modifier was **rewritten from inline `var copy = self` to route through the single `copy(_:)` helper** (R2). Stored config flipped `private let`→`private var` with defaults (`isFullWidth = true`, `size = .medium`). `@Environment(\.isEnabled)` + native `.disabled()` already in place (R3); per-item `SegmentItem.isEnabled` is ITEM DATA — left untouched. Call sites migrated (1 with moved params): MoleculeDemos SegmentedControlDemo knob (2 branches → `.fullWidth(block).size(size)`). The other 5 call sites (MoreDemos, HotelResultsView ×2, FormControlSnapshotTests, ScreenshotGenerator) and ComponentRegistry `usage:` used only `items`+`selection` defaults → already valid. Snapshots unchanged (modifier defaults == old param defaults).
- **ThemeToggle** ✅ (4→1 init): `isOn` (Binding) is the only init param. The 3 appearance/state params moved to modifiers: `isLoading:`→`.loading(_ on: Bool = true)` (R5), `onSystemImage:`+`offSystemImage:`→`.symbols(on:off:)` (paired knob glyphs for the on/off states — kept as one modifier since they're a matched pair). The pre-existing `.a11yID` modifier was **rewritten from inline `var copy = self` to route through the single `copy(_:)` helper** (R2). Stored config flipped `private let`→`private var` with defaults (`isLoading = false`, glyphs `nil`). `@Environment(\.isEnabled)` + native `.disabled()` already in place (R3). Call sites migrated (2 with moved params): MoleculeDemos ToggleDemo knob (`.loading(loading).symbols(on:off:)`), ComponentRegistry `usage:` string. The other 6 call sites (ListRow trailing toggle, ToggleGroup, MoreDemos, ScreenshotGenerator ×3) used only `isOn` → already valid. Snapshots unchanged (modifier defaults == old param defaults).
- **InfoBanner** ✅ (9→3 init): content/data stays in init — `message` (content) + `title` (optional content) + `links` (inline-link DATA), i.e. `InfoBanner(_ message:title:links:)`. The 6 appearance/state/callback params moved to modifiers: `type:`→`.variant(_:)` (R5; `InfoBannerType` drives surface/accent/border/icon, like Callout/AlertToast), `showIcon:`→`.showsIcon(_ on: Bool = true)` (R5 flag, Callout precedent), `banner:`→`.fullWidth(_ on: Bool = true)` (R5; edge-to-edge full-width + no corners/border, Ant Alert `banner`), `actionTitle:`+`onAction:`→`.action(_ title:onAction:)` (the trailing inline button title+handler grouped, Callout/EmptyState precedent), `onDismiss:`→`.onDismiss(_:)` (the × handler). Single shared `copy(_:)` helper (R2); stored config flipped the moved `private let`→`private var` with defaults (R4 tokens kept). Call sites migrated (≈9 real): OrganismDemos InfoBannerDemo knob (`.variant/.showsIcon/.fullWidth/.action/.onDismiss`), ComponentRegistry `usage:` string, MoreDemos, ThemeInjectionDemo, DesignPreviewSheet ×2, ThemeConfiguratorView ×4, ScreenshotGenerator ×2 (`.variant`) + #Preview ×5. Snapshots unchanged (modifier defaults == old param defaults → visually identical).

- **Full-library sweep** ✅ (2026-07-02, 58 components — PHASE 3 CLOSED): the entire remaining surface converted to R1–R7 in one coordinated pass. Buttons (Primary/Secondary/Outline/Ghost 9→2 ×2 inits, Link 4→2; `isLoading` Binding→plain `.loading()`, `textStyle:`→`.titleTextStyle` to avoid the kit-wide `.textStyle` clash, per-mode `confirmsSuccess` default preserved via `Bool?`); TextInput (26→2; model init kept as the supported second entry point; `onSubmit:`→`.onCommit`); Select 11→4 ×2 / SelectBox / MultiSelect / Autocomplete ×2 / SearchBar (callbacks→`.onSearch/.onSelect/.onCommit`); CheckboxGroup/RadioGroup/RadioButtonGroup/ToggleGroup/Checkbox/RadioButton/ColorField/Fieldset; ChoseChip/CompactChip/FilterChip/ChipGroup (`selectionStyle:`→`.chipStyle`); Slider/RangeSlider/QuantityStepper (`step:`→`.step`); Accordion (`leadingSystemImage:`→`.icon`)/AccordionGroup (`.mode`); Card 9→3 (`padding:`→`.contentPadding` — literal 16 kept, `sp-md` varies per theme)/ListView/DataTable/NotificationCard (`type:`→`.variant`)/ResultView/Hero (dark stored as background factory so `.dark()` stays order-free)/BlogCard/MenuCard/PageHeader; Gallery/PagingCarousel/RatingSummary/RadioCard+CheckboxCard/KeyValueTable/Diff (`aspectRatio:`→`.aspect`)/UploadList/Title/InlineText (`style:`→`.inlineStyle`); atoms AnimatedImage/AvatarGroup/Icon (~94 sites)/ProgressBar/Rating/Ribbon/Skeleton/Spinner/TextLink; Lottie add-on LottieEmptyState (media-keyed inits, `.primaryAction` per EmptyState precedent)+LottieIllustration (`.loop`). Clean break (owner's call): old inits deleted, all recorded in `.api-breakage-allowlist.txt`; ~300 call sites migrated across Sources previews/internals, Demo (gallery+examples+registry usage strings), Tests (snapshot names unchanged), and DocC samples. Verified textually (tree-sitter parse of all 237 Swift files + removed-param greps); no Swift toolchain on the Linux worker, so `swift build` runs on CI/macOS.
