Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cfc47e4
feat: proof-of-concept TUI button integration in Fields
juliacanzani Feb 19, 2026
a0c4084
refactor: first theming pass with centralized wp theme
juliacanzani Feb 19, 2026
fcc2e7e
feat: finish TUI button POC follow-ups and Storybook environment cont…
juliacanzani Feb 21, 2026
7dfe9fd
Remove TUI input-group overrides from text field styles
juliacanzani Feb 24, 2026
6ef998a
Refine Button API, fix import paths, update deps and config
juliacanzani Feb 24, 2026
0ab9713
Add ProseMirror dynamic text editor, settings modal, and utilities
juliacanzani Feb 24, 2026
6a6cb62
Refactor Text and Textarea fields with three-path rendering
juliacanzani Feb 24, 2026
9634e21
Make sure tui styles are available and enqueued
nicolas-jaussaud Mar 3, 2026
5f795b5
feat(utils): add shared isDev() utility
juliacanzani Mar 5, 2026
b11d6cb
fix(Button): replace module-level warning flags with useRef guards an…
juliacanzani Mar 5, 2026
68c7427
fix(Button): move destructive→danger normalisation before branch spli…
juliacanzani Mar 5, 2026
9d766e8
feat(Text): add FieldsTextProps interface and type component signature
juliacanzani Mar 5, 2026
5dd6180
fix(Text): add mountedRef guard to prevent onChange firing on mount
juliacanzani Mar 5, 2026
cd38bb9
feat(Text): extract DynamicTextField sub-component, add forwardRef
juliacanzani Mar 5, 2026
1f0c7df
fix(Text): remove querySelector in inputMaskRef callback, add Field.H…
juliacanzani Mar 5, 2026
795f1e3
test(Button,Text): add TUI render path coverage
juliacanzani Mar 5, 2026
ca57b45
fix(jest): support TUI ESM packages and TypeScript in test suite
juliacanzani Mar 5, 2026
2a74fb4
chore: add TUI migration harness infrastructure
juliacanzani Mar 5, 2026
63dbdcf
feat(Checkbox): migrate to TUI Checkbox with forwardRef and Field com…
juliacanzani Mar 5, 2026
1fc0a42
fix(Checkbox): replace useState/useEffect with TUI controlled pattern
juliacanzani Mar 5, 2026
62f933e
feat(Switch): migrate to TUI Switch with forwardRef and Field compound
juliacanzani Mar 5, 2026
7f6b8ca
fix(Switch): replace useState/useEffect with TUI controlled pattern
juliacanzani Mar 5, 2026
ae3321f
feat(RadioGroup): migrate to TUI RadioGroup with forwardRef and Field…
juliacanzani Mar 5, 2026
0f7be93
feat(Radio): migrate to TUI Radio with forwardRef and FieldsRadioProps
juliacanzani Mar 5, 2026
54ab8f8
feat(Textarea): migrate to TUI Textarea with forwardRef and Field com…
juliacanzani Mar 5, 2026
3ef837e
fix(Textarea): add mountedRef guard to DynamicTextarea to prevent onC…
juliacanzani Mar 5, 2026
2b2ed5a
feat(Notice): migrate to TUI Notice with forwardRef and type→theme ma…
juliacanzani Mar 5, 2026
bb14a51
test(TUI): add TUI render path tests for Checkbox, Switch, Radio, Tex…
juliacanzani Mar 6, 2026
b4b305b
chore: add harness batch 2 specs for Checkbox, Switch, Radio, Textare…
juliacanzani Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .harness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Fields TUI Migration Harness
# Supervised loop for migrating Fields components to TUI wrappers

validation:
commands:
lint: npm run lint 2>/dev/null || true # soft gate — pre-existing failures expected
typecheck: npx tsc -b --noEmit
test: npm run jest:test

state:
plan: .harness/plan.md
progress: .harness/progress.md
complete: .harness/COMPLETE

prompts:
build: .harness/prompts/build.md

specs:
- .harness/specs/wrapper-contract.md
- .harness/specs/migrate-checkbox.md
- .harness/specs/migrate-switch.md
- .harness/specs/migrate-radio.md
- .harness/specs/migrate-textarea.md
- .harness/specs/migrate-notice.md
1 change: 1 addition & 0 deletions .harness/COMPLETE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
All tasks complete.
73 changes: 73 additions & 0 deletions .harness/iterations/iteration-1.json

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions .harness/iterations/iteration-10.json

Large diffs are not rendered by default.

205 changes: 205 additions & 0 deletions .harness/iterations/iteration-11.json

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions .harness/iterations/iteration-2.json

Large diffs are not rendered by default.

239 changes: 239 additions & 0 deletions .harness/iterations/iteration-3.json

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions .harness/iterations/iteration-4.json

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions .harness/iterations/iteration-5.json

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions .harness/iterations/iteration-6.json

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions .harness/iterations/iteration-7.json

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions .harness/iterations/iteration-8.json

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions .harness/iterations/iteration-9.json

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions .harness/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Implementation Plan — Batch 2: Component Migrations

Generated: 2026-03-05T22:55:55Z
Last updated: 2026-03-05T22:55:55Z (iteration 1)

## Tasks

- [x] 1. Migrate Checkbox to TUI — types + forwardRef + TUI component
Context: Replace react-aria `useCheckbox`/`useToggleState` with TUI Checkbox. Add `FieldsCheckboxProps` interface, `forwardRef<HTMLInputElement>`, wrap in `Field` compound. Map `onChange(isSelected)` → TUI `onCheckedChange(checked)`. Add hidden input for PHP form submission.
Spec refs: migrate-checkbox items 1, 2, 5, 6
Done: iteration 1. Replaced react-aria with TUI Checkbox + Field compound. Added FieldsCheckboxProps interface, forwardRef<HTMLInputElement>, hidden input. Used always-controlled TUI Checkbox pattern with local state synced from value prop via useEffect (no onChange call) to preserve repeater bulk-select. Added render-time label warning to maintain a11y parity with react-aria (isDev() returns false in Jest so unconditional warn used).

- [x] 2. Migrate Checkbox — fix onChange mount-fire + controlled value
Context: Add `mountedRef` guard to prevent onChange firing on mount. Replace dangerous `useEffect` on `props.value` with TUI controlled `checked`/`defaultChecked` pattern. Add isDev warning for external value control pattern.
Spec refs: migrate-checkbox items 3, 4, 7
Dependencies: Task 1
Done: iteration 2. Removed useState + useEffect entirely. boolValue derived directly from value prop; controlled (checked=boolValue) when value is provided, defaultChecked=false otherwise. onCheckedChange calls onChange directly. mountedRef guard confirmed no-op — TUI onCheckedChange never fires on mount. isDev warning skipped — no dangerous pattern remains.

- [x] 3. Migrate Switch to TUI — types + forwardRef + TUI component
Context: Replace react-aria `useSwitch`/`useToggleState`/`useFocusRing` with TUI Switch. Add `FieldsSwitchProps` interface, `forwardRef<HTMLButtonElement>`, wrap in `Field` compound. Map `onChange(isSelected)` → TUI `onCheckedChange(checked)`. Add hidden input. Remove custom toggle track/thumb markup.
Spec refs: migrate-switch items 1, 2, 5, 6
Done: iteration 3. Replaced react-aria with TUI Switch + Field compound. Added FieldsSwitchProps interface, forwardRef<HTMLButtonElement>, hidden input. Used useState + useEffect for controlled value sync. Updated FieldGroup.test.tsx to use [role="switch"] selector instead of .tf-switch-label (Switch no longer renders a label wrapper with that class). SwitchField index.tsx wrapper handles 'on'/'off' string conversion — Switch.tsx just handles boolean.

- [x] 4. Migrate Switch — fix onChange mount-fire + controlled value
Context: Add `mountedRef` guard. Replace `useEffect` on `props.value` with TUI controlled pattern.
Spec refs: migrate-switch items 3, 4
Dependencies: Task 3
Done: iteration 4. Removed useState + useEffect. boolValue derived directly from value prop; controlled (checked=boolValue) when value is provided, defaultChecked=false otherwise. onCheckedChange calls onChange directly. mountedRef guard confirmed no-op — TUI onCheckedChange is interaction-only (same as Checkbox in task 2).

- [x] 5. Migrate RadioGroup to TUI — types + TUI component
Context: Replace react-aria `useRadioGroup`/`useRadioGroupState` with TUI RadioGroup. Add `FieldsRadioGroupProps` interface, `forwardRef`. Map `onChange(selectedValue)` → TUI `onValueChange(value)`. Remove `RadioContext`/`createContext` (TUI handles context internally). Add hidden input. Wrap in `Field` compound.
Spec refs: migrate-radio items 1, 2, 5, 6
Done: iteration 5. Replaced react-aria with TUI RadioGroup + Field compound. Added FieldsRadioGroupProps interface, forwardRef<HTMLDivElement> (TUI RadioGroup has no ref support, so ref goes on outer wrapper div). Kept RadioContext stub export (createContext(null)) for Radio.tsx import compatibility — Task 6 will remove it. controlledProps uses value when defined, defaultValue='' otherwise.

- [x] 6. Migrate Radio to TUI — types + TUI component
Context: Replace react-aria `useRadio` with TUI Radio. Add `FieldsRadioProps` interface, `forwardRef`. TUI Radio gets state from RadioGroup context — just pass `value` and `children`. Verify RadioContext isn't imported elsewhere before removing export.
Spec refs: migrate-radio items 3, 7
Dependencies: Task 5
Done: iteration 6. Replaced react-aria useRadio + RadioContext with TUI Radio. Added FieldsRadioProps interface (value, children, isDisabled, className, id), forwardRef<HTMLButtonElement>. TUI Radio uses label prop not children — mapped children→label. Added id to FieldsRadioProps for AlignmentMatrix compat (not forwarded to TUI). Removed RadioContext stub from RadioGroup.tsx.

- [x] 7. Migrate RadioGroup — fix onChange mount-fire
Context: Add `mountedRef` guard to RadioGroup.
Spec refs: migrate-radio item 4
Dependencies: Task 5
Done: iteration 7. No code change needed — TUI onValueChange is interaction-only. The onChange-on-mount bug was eliminated when react-aria was replaced in task 5. No useEffect calling onChange exists in current code. Same outcome as Checkbox task 2 and Switch task 4.

- [x] 8. Migrate Textarea to TUI — types + forwardRef + extract DynamicTextarea
Context: Add `FieldsTextareaProps` interface, `forwardRef<HTMLTextAreaElement>`. Extract `DynamicTextarea` sub-component (same pattern as Text.tsx) to isolate `useTextField`. Replace native `<textarea>` path with TUI Textarea + `Field` compound. TUI Textarea uses native `onChange` — extract `event.target.value` for Fields `onChange(value)`. Preserve `data-identifier` attribute.
Spec refs: migrate-textarea items 1, 2, 3, 5, 6, 7
Done: iteration 8. Extracted DynamicTextarea sub-component (useTextField isolated there). Main component uses forwardRef<HTMLTextAreaElement> + TUI Textarea + Field compound. data-identifier passes through TUI Textarea (extends TextareaHTMLAttributes). TUI onChange maps event.target.value → Fields onChange(value) directly (no useState/useEffect needed in TUI path). DynamicTextarea retains useState + useEffect without mountedRef guard (Task 9 adds it). 19/33 suites passing (baseline preserved).

- [x] 9. Migrate Textarea — fix onChange mount-fire
Context: Add `mountedRef` guard. Remove `useTextField` from main component (now only in DynamicTextarea).
Spec refs: migrate-textarea items 4, 6
Dependencies: Task 8
Done: iteration 9. Added mountedRef guard to DynamicTextarea useEffect — skips onChange call on mount, same pattern as DynamicTextField in Text.tsx. useTextField removal from main component was already done in Task 8. 19/33 suites passing (baseline preserved).

- [x] 10. Migrate Notice to TUI
Context: Replace simple div/p/button markup with TUI Notice compound component. Add `FieldsNoticeProps` interface, `forwardRef`. Map `type` → TUI `theme` (`error` → `danger`). Map `message` → `Notice.Content`. Map `onDismiss` → TUI dismissible pattern.
Spec refs: migrate-notice items 1, 2, 3, 4, 5
Done: iteration 10. Replaced div/p/button with TUI Notice + Notice.Body. Added FieldsNoticeProps interface (message, type, onDismiss, className). type→theme mapping (error→danger, others same). forwardRef<HTMLElement> with `as never` cast since NoticeHandle isn't exported from @tangible/ui root. Added className="tf-notice" for legacy CSS compat. dismissible={!!onDismiss}. 19/33 suites passing (baseline preserved).

- [x] 11. Add/update Jest tests for migrated components
Context: Add TUI render path tests for Checkbox, Switch, Radio, Textarea, Notice. Test: (a) renders TUI component class, (b) onChange doesn't fire on mount, (c) controlled value updates, (d) disabled state passes through. Place in `tests/jest/cases/components/`.
Dependencies: Tasks 2, 4, 7, 9, 10
Done: iteration 11. Added Switch.test.tsx, Radio.test.tsx, Textarea.test.tsx (controls/) and Notice.test.tsx (elements/). Augmented Checkbox.test.tsx with TUI path tests. All 4 new suites pass. Textarea and Radio tested via direct import; Textarea uses fields.render (direct import fails due to base/index.ts→Modal.tsx dependency chain). onChange-on-mount confirmed: Checkbox, Switch, RadioGroup all pass (TUI interaction-only callbacks). Textarea onChange-on-mount not tested — Control.tsx wrapper fires on mount. 23/37 suites passing (was 19/33 with 4 new suites added).

## Discoveries

- **isDev() returns false in Jest** — import.meta.env.DEV is false in Jest/Babel transpilation. Dev warnings gated on isDev() won't fire in tests. Use unconditional console.warn for a11y warnings that tests verify, or use `process.env.NODE_ENV !== 'production'` directly.
- **TUI Checkbox bulk-select compat** — repeater bulk-select sets `value={true}` externally. Must sync value→state via useEffect (without calling onChange) to preserve this behavior. Task 2 will improve this with the proper controlled pattern.
- **onChange-on-mount bug is already fixed** by TUI migration — TUI `onCheckedChange` only fires on user interaction, not on mount. The useEffect calling onChange from the original code is gone. Task 2 item 3 (mountedRef guard) may be a no-op for the TUI path.

- **onChange-on-mount is systemic** — Checkbox, Switch, RadioGroup, Textarea all have the same `useEffect(() => onChange(value), [value])` pattern that fires on mount. All fixed in this batch.
- **Control.tsx also has this bug** (line 37-39) — affects all fields when wrapped by the Fields control layer. Not fixed here — broader change needed.
- **TUI Switch forwards to `<button>`, not `<input>`** — different from Checkbox which forwards to `<input>`. Hidden input is needed for both.
- **RadioContext can likely be removed** — TUI RadioGroup manages context internally. Verify no external imports before deleting.
- **TUI RadioGroup has no forwardRef** — regular function, not forwardRef. Wrapper's forwardRef goes to the outer `<div className="tf-radio-group">` instead. This is fine for the use case.
- **TUI Radio forwards to `HTMLButtonElement`** — spec says `<input>` but actual TUI type is `ForwardRefExoticComponent<RadioProps & RefAttributes<HTMLButtonElement>>`. Task 6 should use `forwardRef<HTMLButtonElement>`.
- **RadioContext stub kept for Radio.tsx** — createContext(null) exported to avoid import error in Radio.tsx until Task 6 replaces it.
- **Notice is trivially simple** (~11 lines) — lowest risk migration in this batch.
- **TUI Textarea uses native `onChange`** — unlike other TUI components that use `onValueChange`/`onCheckedChange`. This is because it extends `TextareaHTMLAttributes`.
- **SwitchField index.tsx is the registered type, not Switch.tsx** — Switch.tsx is the inner component. SwitchField wraps it and converts boolean ↔ 'on'/'off' strings. Switch.tsx only handles boolean value props. Tests that reference switch behavior should account for this two-layer architecture.
- **FieldGroup test used .tf-switch-label class** — updated to `[role="switch"]` in iteration 3. The TUI Switch `<button role="switch">` is the correct semantic click target.
97 changes: 97 additions & 0 deletions .harness/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Fields TUI Migration — Batch 2 Progress Log

## Iteration 11 — 2026-03-05T23:44:19Z
Task: Add/update Jest tests for migrated components
Result: pass
Files changed: tests/jest/cases/components/controls/Checkbox.test.tsx, tests/jest/cases/components/controls/Switch.test.tsx, tests/jest/cases/components/controls/Radio.test.tsx, tests/jest/cases/components/controls/Textarea.test.tsx, tests/jest/cases/components/elements/Notice.test.tsx
Commit: bb14a51
Notes: Checkbox/Switch/RadioGroup tested via direct component import (avoids Control.tsx mount-fire). Textarea tested via fields.render (direct import fails: TextArea.tsx → ../../base → Modal.tsx → undefined getter in base/index.ts). Textarea onChange-on-mount not tested due to Control.tsx wrapper bug. Notice direct import works fine. 23/37 suites passing (4 new suites; all pre-existing failures unchanged). All tasks complete.

## Iteration 10 — 2026-03-05T23:40:07Z
Task: Migrate Notice to TUI
Result: pass
Files changed: assets/src/components/base/notice/Notice.tsx
Commit: 2b2ed5a
Notes: TUI Notice uses Notice.Body (not Notice.Content as spec predicted). NoticeHandle not re-exported from @tangible/ui root — used forwardRef<HTMLElement> with `as never` cast to avoid TS error. type→theme mapping: error→danger, others unchanged. dismissible set from !!onDismiss. Added className="tf-notice" for legacy CSS compat. 19/33 suites passing (baseline preserved). Task 11 (Jest tests for all migrated components) remains.

## Iteration 9 — 2026-03-05T23:38:59Z
Task: Migrate Textarea — fix onChange mount-fire
Result: pass
Files changed: assets/src/components/field/textarea/TextArea.tsx
Commit: 3ef837e
Notes: Added mountedRef guard to DynamicTextarea — same pattern as DynamicTextField in Text.tsx. useTextField removal from main component was already complete from Task 8. 19/33 suites passing (baseline preserved). Tasks 10 and 11 remain.

## Iteration 8 — 2026-03-05T23:36:06Z
Task: Migrate Textarea to TUI — types + forwardRef + extract DynamicTextarea
Result: pass
Files changed: assets/src/components/field/textarea/TextArea.tsx
Commit: 54ab8f8
Notes: TUI Textarea extends TextareaHTMLAttributes so data-identifier passes through naturally. TUI path calls props.onChange(event.target.value) directly in onChange handler — no useState/useEffect needed (no mount-fire bug in this path). DynamicTextarea retains useState + useEffect without mountedRef guard; Task 9 will add that guard. 19/33 suites passing (baseline preserved).

## Iteration 7 — 2026-03-05T23:34:45Z
Task: Migrate RadioGroup — fix onChange mount-fire
Result: pass
Files changed: none
Commit: none (no code change required)
Notes: TUI onValueChange is interaction-only — the onChange-on-mount bug was already eliminated when react-aria was replaced in task 5. mountedRef guard is a no-op, same outcome as Checkbox task 2 and Switch task 4. 19/33 suites passing (baseline preserved).

## Iteration 6 — 2026-03-05T23:32:32Z
Task: Migrate Radio to TUI — types + TUI component
Result: pass
Files changed: assets/src/components/field/radio/Radio.tsx, assets/src/components/field/radio/RadioGroup.tsx
Commit: 0f7be93
Notes: TUI Radio uses `label` prop (not `children`) — children is mapped to label. TUI Radio forwards to HTMLButtonElement (not <input> as spec says). RadioContext stub removed from RadioGroup — was only imported by Radio.tsx. Added `id` to FieldsRadioProps for AlignmentMatrix.tsx compat (AlignmentMatrix passes id to <Radio> — accepted in interface but not forwarded to TUI since TUI Radio doesn't accept it). ButtonOption.tsx has a pre-existing TS error (useRadio state type mismatch) unrelated to this migration. 19/33 suites passing (baseline preserved).

## Iteration 5 — 2026-03-05T23:29:01Z
Task: Migrate RadioGroup to TUI — types + TUI component
Result: pass
Files changed: assets/src/components/field/radio/RadioGroup.tsx
Commit: ae3321f
Notes: TUI RadioGroup has no forwardRef — ref forwarded to outer wrapper div instead. RadioContext kept as createContext(null) stub so Radio.tsx doesn't have an import error (Task 6 will remove it). TUI Radio actually forwards to HTMLButtonElement (not <input> as spec says) — correct this in Task 6. onChange-on-mount bug in RadioGroup is naturally fixed by TUI migration (onValueChange fires only on user interaction). 19/33 suites passing (baseline preserved).

## Iteration 4 — 2026-03-05T23:26:52Z
Task: Migrate Switch — fix onChange mount-fire + controlled value
Result: pass
Files changed: assets/src/components/field/switch/Switch.tsx
Commit: 7f6b8ca
Notes: Same controlled pattern as Checkbox task 2. Removed useState + useEffect entirely. boolValue derived directly from value prop. mountedRef guard is unnecessary — TUI onCheckedChange fires only on user interaction. 19/33 suites passing (baseline preserved). Note: run jest with `--config tests/jest/jest.config.js` not bare `npx jest`.


*Batch 1 (Button + Text fixes) complete. See git history for previous progress.*

## Iteration 3 — 2026-03-05T23:11:52Z
Task: Migrate Switch to TUI — types + forwardRef + TUI component
Result: pass
Files changed: assets/src/components/field/switch/Switch.tsx, tests/jest/cases/components/controls/FieldGroup.test.tsx
Commit: 62f933e
Notes: SwitchField (index.tsx) is the registered type; Switch.tsx is the inner component. SwitchField handles 'on'/'off' string conversion — Switch.tsx deals in booleans. FieldGroup.test.tsx updated to use [role="switch"] instead of .tf-switch-label (TUI Switch renders a <button role="switch"> not a <label class="tf-switch-label">). 19/33 suites passing (baseline preserved).

## Iteration 2 — 2026-03-05T23:08:44Z
Task: Migrate Checkbox — fix onChange mount-fire + controlled value
Result: pass
Files changed: assets/src/components/field/checkbox/Checkbox.tsx
Commit: 1fc0a42
Notes: Removed useState + useEffect. boolValue derived directly from value prop; controlled when value is provided, defaultChecked=false otherwise. mountedRef guard is confirmed unnecessary — TUI onCheckedChange is interaction-only. isDev warning omitted — no dangerous pattern remains in the controlled approach. 19/33 suites passing (baseline unchanged).

## Iteration 1 — 2026-03-05T22:58:07Z
Task: Migrate Checkbox to TUI — types + forwardRef + TUI component
Result: pass
Files changed: assets/src/components/field/checkbox/Checkbox.tsx
Commit: 63dbdcf
Notes: isDev() returns false in Jest (import.meta.env.DEV=false via Babel), so a11y label warning is unconditional. TUI Checkbox is always-controlled (checked={localState}) with value→state sync via useEffect (no onChange call) to preserve repeater bulk-select. The onChange-on-mount bug is naturally fixed — TUI onCheckedChange only fires on user interaction. Task 2 item 3 (mountedRef guard) may be a no-op for TUI path; verify before adding.

## Iteration 1 — 2026-03-05T22:55:55Z
Task: Generate implementation plan
Result: pass
Notes: Refined existing plan with 11 tasks from 5 spec files (migrate-checkbox, migrate-switch, migrate-radio, migrate-textarea, migrate-notice) + wrapper-contract. Plan covers Checkbox, Switch, RadioGroup/Radio, Textarea, Notice migrations to TUI, all sharing the onChange-on-mount bug fix pattern.

## Iteration 0 — 2026-03-05
Task: Generate batch 2 plan and specs
Result: pass
Notes: 5 component migrations (Checkbox, Switch, Radio, Textarea, Notice) across 11 tasks. All have onChange-on-mount bug. Specs written from actual source review. Jest infra fixed in batch 1 (19/33 suites passing). TUI v0.0.4 installed.

**Stashed dirty state from iteration 1:** harness-iteration-1-dirty

**Stashed dirty state from iteration 2:** harness-iteration-2-dirty

**Stashed dirty state from iteration 3:** harness-iteration-3-dirty
Loading
Loading