# Migrations

<!-- AUTO-GENERATED by `pnpm nx build codemods` — do not edit.
     Source: packages/codemods/src/migrations/<id>/entry.md -->

Entries are sorted by version, newest first. Find the version you are coming
from and work your way up.

Catching up across several versions? One command bumps every Flow dependency and
runs the codemod of every migration up to the version it lands on:

```shell
npx @mittwald/flow-codemods@latest upgrade
```

It is not the whole migration. Most entries below have no codemod and have to be
done by hand — the command prints those when it is done. It lists migrations
older than your current version too: nothing records which ones you already
performed, and re-running a codemod is a no-op.

It refuses to run on a dirty working tree (`--allow-dirty` overrides that) and
rewrites files in place, so review the diff afterwards.

Every command on this page is written with `npx`, because a generated file
cannot know your package manager. Substitute the equivalent if yours differs:
`pnpm dlx`, `yarn dlx` (Yarn Classic has no `dlx` — it uses `npx`), or `bun x`.
The CLI's own output does detect it and prints the right form.

---

<a id="image-rounded-corners-default"></a>

## Image: rounded corners by default, withBorder draws only the border

**Since `1.4.0`** · migration · no code change needed · also applies to
`@mittwald/flow-remote-react-components`

`Image` now has rounded corners by default, with or without a border. Previously
only `withBorder` rounded them. `withBorder` now draws only the border, and the
new `withRoundedCorners` prop (default `true`) controls the corner radius on its
own. The border color is also more muted.

Keep square corners where an image needs them:

```diff
- <Image src={src} alt="" />
+ <Image src={src} alt="" withRoundedCorners={false} />
```

**Apply:** No code change required. To keep square corners on an `Image` without
`withBorder`, pass `withRoundedCorners={false}`.

---

<a id="popover-open-state-props"></a>

## Popover: `defaultOpen` renamed, `isOpen` and `onOpenChange` now work

**Since `1.1.47`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

`Popover`'s open state props now behave the way their names say. This covers
`ContextualHelp` and `ContextMenu` too, which inherit them.

#### `defaultOpen` is now `isDefaultOpen`

The name matches `Modal` and the other overlays.

```diff
- <ContextualHelp defaultOpen>
+ <ContextualHelp isDefaultOpen>
```

`defaultOpen` keeps working and logs a deprecation warning. A codemod renames
it, on `Popover`, `ContextualHelp` and `ContextMenu` only — the identically
named react-aria prop on `Select`, `MenuTrigger`, `Tooltip`, `TooltipTrigger`,
`DialogTrigger`, `DatePicker` and `DateRangePicker` is untouched.

#### `onOpenChange` reports instead of taking over

Passing `onOpenChange` used to switch the popover into a controlled mode that
had no `isOpen` to control it with: the handler fired, and the popover stopped
closing. It is now a notification that fires on every path — react-aria's
dismissal, and a close performed through the controller — and never performs or
suppresses the change.

```diff
- <Popover onOpenChange={(isOpen) => controller.setOpen(isOpen)}>
+ <Popover onOpenChange={(isOpen) => track(isOpen)}>
```

Two things to check:

- A handler that performed the close itself can drop that call. Leaving it in is
  harmless — the controller ignores a state it is already applying.
- A handler that relied on the prop to _block_ a close no longer blocks it. Use
  the controller for that: a handler registered through `useOverlayController`'s
  `onClose` still aborts by returning `false`.

#### `isOpen` controls the popover

`isOpen` was inherited from react-aria but silently overridden. It is now the
open state whenever it is set.

```tsx
const [isOpen, setIsOpen] = useState(false);

<Popover isOpen={isOpen} onOpenChange={setIsOpen}>
  …
</Popover>;
```

If you pass `isOpen` without updating it from `onOpenChange` — for instance by
spreading props that happen to carry it — the popover no longer opens. Drop the
prop, or wire up the state.

A controller stays the third option and needs neither prop: with
`controller={controller}`, `onOpenChange` is a pure monitor.

**Apply:** Rename `defaultOpen` to `isDefaultOpen` on `Popover`,
`ContextualHelp` and `ContextMenu` — a codemod does it, scoped to those three.
Leave `defaultOpen` alone on `Select`, `MenuTrigger`, `Tooltip`,
`TooltipTrigger`, `DialogTrigger`, `DatePicker` and `DateRangePicker`, where it
is react-aria's own prop and unchanged. Then check every `isOpen` and
`onOpenChange` passed to those three by hand, because both changed behaviour and
neither is mechanically decidable. `isOpen` used to be ignored and now controls
the popover: a value that is not kept up to date through `onOpenChange` keeps
the popover closed. `onOpenChange` used to take over the open state and now only
reports it, so a handler that performed the close itself —
`onOpenChange={(open) => controller.setOpen(open)}` — can drop that call, and a
handler that relied on the prop to _suppress_ the close no longer does; use the
controller's `onClose` for that.

```shell
npx @mittwald/flow-codemods@latest popover-open-state-props src
```

---

<a id="tabler-icons-no-longer-transitive"></a>

## @tabler/icons-react is no longer installed alongside Flow

**Since `1.1.40`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

Flow no longer depends on `@tabler/icons-react`. `@mittwald/flow-icons` carries
the path data of the 123 icons it uses directly, so a Flow install stops pulling
in 74 MB and ~24,000 files it never needed.

The icons themselves do not change: same names, same exports, same rendered
`<svg>` including the `tabler-icon` and `tabler-icon-<name>` classes.

The one thing that breaks is code which imported `@tabler/icons-react` without
declaring it, and got it because npm hoisted Flow's copy into the project's
`node_modules`. That copy is gone. Declare the package where you use it:

```shell
npm i @tabler/icons-react
```

```tsx
// unchanged — it just needs the package in your own dependencies now
import { IconStar } from "@tabler/icons-react";

<Icon>
  <IconStar />
</Icon>;
```

**Apply:** Only affects code that imports from `@tabler/icons-react` — typically
a `<Icon><IconSomething /></Icon>` using an icon that is not in Flow's own set.
Add the package to your own dependencies (`npm i @tabler/icons-react`); the
imports themselves stay unchanged. Flow's own `Icon*` components are unaffected,
keep their names, and render exactly as before.

---

<a id="option-value-inferred-from-mixed-children"></a>

## Option: value is inferred from mixed children

**Since `1.1.12`** · migration · no code change needed · also applies to
`@mittwald/flow-remote-react-components`

An `Option` used to infer its `textValue` — and, through it, its `value` — only
when its children were exactly one text node. Text next to any element left both
undefined, so react-aria assigned the option a key off a render-order counter:

```tsx
<Select>
  <Option>
    Millennium Falcon <Badge>Latest</Badge>
  </Option>
  <Option>X-Wing</Option>
</Select>
```

| Option                        | key before     | key now             |
| ----------------------------- | -------------- | ------------------- |
| `Millennium Falcon` + `Badge` | `react-aria-1` | `Millennium Falcon` |
| `X-Wing`                      | `X-Wing`       | `X-Wing`            |

The key is what the field reports as its selected value and what `defaultValue`
has to match, so the old key made the field submit a meaningless string, could
not be preselected, and shifted when unrelated markup around it changed.

Only text that is a child itself contributes — text inside an element child does
not, so the option above is `"Millennium Falcon"` and not
`"Millennium Falcon Latest"`.

An `Option` with no text among its children at all still has no key to infer,
and now says so on the console instead of failing silently:

```tsx
// logs: An <Option> has no 'value' and none could be inferred from its children…
<Option>
  <Text>Millennium Falcon</Text>
</Option>
```

Give those options a `value`:

```diff
- <Option>
+ <Option value="millennium-falcon">
    <Text>Millennium Falcon</Text>
  </Option>
```

**Apply:** No code change required for the option itself. An `Option` whose
children are text plus an element — text and a `Badge`, text and an icon — now
carries that text as its key, where it previously fell back to react-aria's
generated `react-aria-N`. Check anywhere such a key was read back: a stored or
server-side selection, a `defaultValue`/`value` matched against it, or a test
asserting on it. Pass an explicit `value` to pin the key to something other than
the display text.

---

<a id="use-design-tokens-build-metadata-removed"></a>

## useDesignTokens(): no longer returns style-dictionary build metadata

**Since `1.0.16`** · migration · manual change

The hook imported the design tokens' full style-dictionary output, which carries
`filePath`, `isSource`, `original`, `name`, `attributes` and `key` on every one
of the 1538 tokens. That metadata is 94 % of the payload and meant nothing in a
browser — it put 1.37 MB of JavaScript into every bundle that touched the
package. The hook now reads a runtime build of the same tokens.

Every token keeps `value` and `path`, unchanged and with identical values, so
the common access patterns are untouched:

```tsx
const tokens = useDesignTokens();
tokens["loading-spinner"]["transition-duration"].value; // still works
tokens.axis.color.path; // still works
```

Only the build metadata is gone. If you read one of those fields, take it from
`@mittwald/flow-design-tokens/json/all-light.json` (or `all-dark.json`) directly
— that artifact is unchanged. Do not import it into browser code; it is the
reason this change exists.

```diff
- const source = useDesignTokens().button["corner-radius"].filePath;
+ import tokens from "@mittwald/flow-design-tokens/json/all-light.json";
+ const source = tokens.button["corner-radius"].filePath;
```

**Apply:** Only affects code reading `filePath`, `isSource`, `original`, `name`,
`attributes` or `key` off a token returned by `useDesignTokens()`. Those fields
are gone; read them from `@mittwald/flow-design-tokens/json/all-light.json` (or
`all-dark.json`) instead, and never from browser code. `value` and `path` are
unchanged — code using only those needs no change.

---

<a id="segmented-control-deprecated"></a>

## SegmentedControl deprecated

**Since `0.2.0-alpha.1056`** · deprecation · manual change · also applies to
`@mittwald/flow-remote-react-components`

`SegmentedControl` and `Segment` are deprecated. The component covered two
different jobs, and each already has its own component in Flow. Which
replacement is right depends on what the control does at that place.

**Switching content.** The selection swaps what is shown below it — a content
switcher. That is what `Tabs` are for: each `Segment` becomes a `Tab` with its
title in `TabTitle` and its content inside the tab. The label and the state that
tracked the selection both go away.

```diff
- <SegmentedControl defaultValue="app">
-   <Label>Verbindung</Label>
-   <Segment value="app">Mit App verbinden</Segment>
-   <Segment value="container">Mit Container verbinden</Segment>
- </SegmentedControl>
- {connection === "app" ? <AppSettings /> : <ContainerSettings />}
+ <Tabs>
+   <Tab>
+     <TabTitle>Mit App verbinden</TabTitle>
+     <AppSettings />
+   </Tab>
+   <Tab>
+     <TabTitle>Mit Container verbinden</TabTitle>
+     <ContainerSettings />
+   </Tab>
+ </Tabs>
```

**Setting a value.** The selection feeds a form or a setting. Use a
`RadioGroup`: each `Segment` becomes a `RadioButton`, everything else — `Label`,
`FieldDescription`, `FieldError`, `value`/`defaultValue`/`onChange`, the React
Hook Form binding — stays as it is.

This direction is a rename, not a restructure. The types line up:
`SegmentedControlProps` and `RadioGroupProps` both extend
`Omit<Aria.RadioGroupProps, "children">`, and `SegmentProps` and
`RadioButtonProps` are the same type (`RadioButtonProps` is declared as
`RadioProps`, which is identical to `SegmentProps`). Only
`containerBreakpointSize` has no counterpart — see the note at the end.

```diff
- <SegmentedControl defaultValue="debit">
+ <RadioGroup defaultValue="debit">
    <Label>Zahlungsart</Label>
-   <Segment value="debit">Lastschrift</Segment>
-   <Segment value="invoice">Rechnung</Segment>
+   <RadioButton value="debit">Lastschrift</RadioButton>
+   <RadioButton value="invoice">Rechnung</RadioButton>
    <FieldDescription>Jederzeit änderbar</FieldDescription>
- </SegmentedControl>
+ </RadioGroup>
```

**Always `RadioButton`, not `Radio`.** A `RadioGroup` also accepts a plain
`Radio`, and it takes the same props, so both would compile — but `RadioButton`
is the replacement for a `Segment`. The appearance still changes: `RadioButton`
does not reproduce the segmented control's joined row, which came from
`Segment`'s own `flex: 1` and collapsed borders. See the
[RadioGroup docs](https://flow.mittwald.de/components/form-controls/radio-group).

#### Three things the Tabs direction changes

**The controlled-state props are named differently.** `SegmentedControl` takes
`value` / `defaultValue` / `onChange` (it is a radio group); `Tabs` takes
`selectedKey` / `defaultSelectedKey` / `onSelectionChange`. Nothing carries over
by name here, unlike the `RadioGroup` direction.

**The group `Label`.** `SegmentedControl` is a form field — it wraps
react-aria's `RadioGroup` — so a `<Label>` inside it is a field label. `Tabs`
has no equivalent slot, and a tab list labels itself through its tab titles. The
`Label` therefore cannot move into `Tabs`, but its text has somewhere to go:

- into the surrounding `Section`'s `Heading`, when the group wants a visible
  title
- into `aria-label` on `Tabs`, when it does not — that names the tab list for
  screen readers without showing the name
- nowhere, when the surrounding heading already names the group

A `Label` on its own is **not** a signal that the usage was a `RadioGroup` case
— plenty of content switchers were authored with one, because the component
required a field label.

**Panels that register form fields.** Moving the switched-in content into a
`Tab` does not change when it mounts. `Tab` renders its panel with
`shouldForceMount` and wraps `Content` and `Section` in
`<Activity isActive={…}>`, so every tab's subtree is mounted and keeps its state
— an inactive tab is hidden, not unmounted. React Hook Form fields inside a tab
stay registered across a tab switch, exactly as they did as siblings below a
`SegmentedControl`.

There is no codemod, and the reason is the choice rather than the edit. Which
replacement is right cannot be decided from the source: a value-setting usage
and a content switcher look alike at the call site, and only the surrounding
intention separates them. The `RadioGroup` direction would be mechanical **once
that decision is made** — if you have many usages that all go that way, a
find-and-replace of `SegmentedControl`/`Segment` gets you most of it.

`SegmentedControl` and `Segment` (and the `flr-segmented-control` /
`flr-segment` remote elements) keep working unchanged and will be removed in a
future major version. `SegmentedControl` logs a deprecation warning at runtime;
`Segment` relies on it, since it only renders inside a `SegmentedControl`.

The `containerBreakpointSize` property has no counterpart: `Tabs` collapse on
their own when the available width runs out, and a `RadioGroup` stacks its
options anyway.

**Apply:** Replace `SegmentedControl` with `Tabs` when the selection switches
displayed content, or with `RadioGroup` when it sets a value. Pick per usage.
The two directions cost very different amounts of work. Towards `RadioGroup` it
is a prop-compatible rename: `SegmentedControl` → `RadioGroup` and `Segment` →
`RadioButton` (always `RadioButton`, not `Radio`; it takes exactly `Segment`'s
props), with `value`/`defaultValue`/`onChange` and a `Label` child all carrying
over. Only `containerBreakpointSize` has no counterpart, and the joined row is
not reproduced. Towards `Tabs` it is structural: the state props are
`selectedKey`/`defaultSelectedKey` rather than `value`/`defaultValue`, there is
no `Label` slot (the group label moves to the surrounding `Heading`, or to
`aria-label` on `Tabs` when it should not be visible, or goes away), and the
switched panels move inside the tabs — where they stay mounted, so form fields
in them keep their registration.

---

<a id="align-to-combine"></a>

## Align renamed to Combine

**Since `0.2.0-alpha.1047`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

`Align` is now called `Combine`. The old name suggested a generic alignment tool
and clashed with the `align` property of `Flex`; the component actually gives
known combinations of components a fixed, correct arrangement.

```diff
- <Align>
+ <Combine>
    <Avatar />
    <Text>Max Mustermann</Text>
- </Align>
+ </Combine>
```

`Align` (and the `flr-align` remote element) keeps working unchanged, logs a
deprecation warning at runtime, and will be removed in a future major version.

The component tokens were renamed along with the component
(`--align--avatar-text--spacing` is now `--combine--avatar-text--spacing`), and
so were the CSS class names (`.flow--align` is now `.flow--combine`). Both are
internal and not covered by Semantic Versioning.

**Apply:** Rename `Align` to `Combine` and `AlignProps` to `CombineProps`, for
named, aliased and namespace imports from a Flow package.

```shell
npx @mittwald/flow-codemods@latest align-to-combine src
```

---

<a id="button-color-accent-to-success"></a>

## Button: color `accent` renamed to `success`

**Since `0.2.0-alpha.1046`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

The button color `accent` is now called `success` — the name says what the color
means instead of how it looks.

```diff
- <Button color="accent">Speichern</Button>
+ <Button color="success">Speichern</Button>
```

`color="accent"` keeps working and logs a deprecation warning, but will be
removed in a future release. A codemod rewrites it on `Button` and
`SubmitButton`.

The CSS class and the design tokens are renamed **without** a fallback — update
them by hand if you use `@mittwald/flow-stylesheet` or the token CSS variables
directly:

```diff
- <button class="flow--button flow--button--accent">
+ <button class="flow--button flow--button--success">
```

```diff
- var(--button--accent-solid-background-color--default)
+ var(--button--success-solid-background-color--default)
```

The same rename applies to every `--button--accent-{solid,plain,soft,outline}-*`
variable. `Chat`'s internal class `flow--chat--accent-button` became
`flow--chat--success-button`.

**Apply:** Rename `color="accent"` to `color="success"` on `Button` and
`SubmitButton`. Also update the CSS class `flow--button--accent` to
`flow--button--success` and every
`--button--accent-{solid,plain,soft,outline}-*` design token to its `success`
counterpart if you use `@mittwald/flow-stylesheet` or the token CSS variables
directly — those have no fallback.

```shell
npx @mittwald/flow-codemods@latest button-color-accent-to-success src
```

---

<a id="tooltip-trigger-delay-type"></a>

## TooltipTrigger changed delay type

**Since `0.2.0-alpha.1016`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

Numeric delay values are no longer accepted; only the string literals
`"default"` (400ms) and `"long"` (1500ms) are valid. Pick by what the element
needs, not by rounding the old number to the nearest preset — a former
`delay={500}` is closer to `"long"` numerically, but if the element is an
icon-only button that cannot be understood without the tooltip, `"default"` is
still the right choice.

```diff
- <TooltipTrigger delay={300} />
+ <TooltipTrigger delay="default" />
```

For an icon-only button that needs the tooltip to be understood.

```diff
- <TooltipTrigger delay={2000} />
+ <TooltipTrigger delay="long" />
```

For supplementary information on an element that is already labeled.

**Apply:** Replace every numeric `delay` value on `TooltipTrigger` with one of
the two string literals: `"default"` (400ms) or `"long"` (1500ms). Pick by
intent, not by which number is closer — `"default"` is for elements that cannot
be understood without the tooltip (icon-only buttons), `"long"` is for
supplementary information on elements that are already labeled. A previous
numeric value is not a reliable guide: `delay={500}` maps to `"default"`
(400ms), not `"long"` (1500ms), if the element still needs the tooltip to be
understood.

---

<a id="flags-to-component-defaults-provider"></a>

## `flags` is replaced by the ComponentDefaultsProvider

**Since `0.2.0-alpha.1005`** · deprecation · manual change

The global `flags` object is deprecated. Application-wide defaults are defined
with the `<ComponentDefaultsProvider />` instead, which additionally works per
subtree:

```diff
- import { flags } from "@mittwald/flow-react-components";
-
- flags.requireCloseModalConfirmationOnUnsavedChanges = false;
- flags.disableInitialListSuspenseBoundaries = true;
+ import { ComponentDefaultsProvider } from "@mittwald/flow-react-components";
+
+ <ComponentDefaultsProvider
+   defaults={{
+     Form: { confirmModalCloseOnUnsavedChanges: false },
+     List: { disableInitialSuspenseBoundary: true },
+   }}
+ >
+   <App />
+ </ComponentDefaultsProvider>
```

Assigning a flag keeps working — it acts as the application-wide default below
the provider and logs a deprecation warning — but the flags will be removed in a
future release.

**Apply:** Replace assignments to the global `flags` object with an equivalent
`<ComponentDefaultsProvider defaults={{ ... }} />` wrapping the app, or the
subtree the default should apply to.

---

<a id="modal-unsaved-changes-confirmation"></a>

## Closing a Modal with unsaved changes is confirmed by default

**Since `0.2.0-alpha.1005`** · migration · no code change needed · also applies
to `@mittwald/flow-remote-react-components`

A `Modal` that contains a react-hook-form `<Form>` now asks for confirmation
before it closes while the form is _dirty_ — previously this required the
`requireCloseModalConfirmationOnUnsavedChanges` flag. After a successful submit
or a `form.reset()` the modal closes right away, and actions in the
`<ActionGroup />` as well as the close button in the heading still close it
immediately.

Nothing to do if you had the flag enabled. To keep the previous behavior, opt
out per form:

```diff
- <Form form={form} onSubmit={onSubmit}>
+ <Form form={form} onSubmit={onSubmit} confirmModalCloseOnUnsavedChanges={false}>
```

Or switch the default off for the whole app with the
`<ComponentDefaultsProvider />` (see below).

**Apply:** No code change required — if you had
`requireCloseModalConfirmationOnUnsavedChanges` enabled, the new default matches
it. To keep the previous behaviour (closing without confirmation), pass
`confirmModalCloseOnUnsavedChanges={false}` to `Form` — this also works in
remote apps (`@mittwald/flow-remote-react-components/react-hook-form`). To
switch it off app-wide, set `Form: { confirmModalCloseOnUnsavedChanges: false }`
via `<ComponentDefaultsProvider />`, or outside of remote apps its deprecated
equivalent, the `flags.requireCloseModalConfirmationOnUnsavedChanges = false`
assignment.

---

<a id="password-tools-subpath-renamed"></a>

## `password-tools` entry renamed to `mittwald-password-tools-js`

**Since `0.2.0-alpha.1000`** · migration · codemod available

The subpath export carrying the `@mittwald/password-tools-js` integration is
called `@mittwald/flow-react-components/mittwald-password-tools-js`. Between
`0.2.0-alpha.913` and `0.2.0-alpha.999` it was called
`@mittwald/flow-react-components/password-tools`; `0.2.0-alpha.1000` reverted
that name.

```diff
- import { Rule } from "@mittwald/flow-react-components/password-tools";
+ import { Rule } from "@mittwald/flow-react-components/mittwald-password-tools-js";
```

Only code written against a version inside that window is affected. There is no
deprecation path and no fallback: a subpath export that no longer exists is a
hard `TS2307` ("Cannot find module") and fails the build.

**Apply:** Replace the import path
`@mittwald/flow-react-components/password-tools` with
`@mittwald/flow-react-components/mittwald-password-tools-js`.

```shell
npx @mittwald/flow-codemods@latest password-tools-subpath-renamed src
```

---

<a id="tabs-navigation-usage-to-tab-navigation"></a>

## Tabs restyled; navigation usage moves to TabNavigation

**Since `0.2.0-alpha.977`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

`Tabs` restyled: the tab list now fills the available width with equal-size tabs
and a shared, animated indicator that slides between them, instead of the
previous fit-content tabs that each carried their own hover/pressed/selected
background. Nothing else about `Tabs` changed — same props, same panels, same
collapsing behavior. If `Tabs` switches content within a page, there is nothing
to do; the new look applies on upgrade.

**Real navigation has a proper home now.** React Aria's `Tab` (which `TabTitle`
extends) accepts the same routing props as a plain link — `href`, `target`,
`routerOptions`, … — so a `Tabs` could always double as a navigation control:
give a `TabTitle` an `href` and selecting it pushed a real route change while
the row still looked and behaved like tabs otherwise. That usage only reached
for `Tabs` because nothing else looked right for it. Now there is
`TabNavigation`, introduced in the same version specifically for this case — and
it keeps the visual language the old `Tabs` used to have.

```diff
- <Tabs selectedKey={pathname}>
-   <Tab id="/apps">
-     <TabTitle href="/apps">Apps</TabTitle>
-   </Tab>
-   <Tab id="/container">
-     <TabTitle href="/container">Container</TabTitle>
-   </Tab>
- </Tabs>
+ <TabNavigation aria-label="Projekt-Navigation">
+   <Link href="/apps" aria-current={pathname === "/apps" ? "page" : undefined}>
+     Apps
+   </Link>
+   <Link
+     href="/container"
+     aria-current={pathname === "/container" ? "page" : undefined}
+   >
+     Container
+   </Link>
+ </TabNavigation>
```

#### What moves and what doesn't

**The children.** `Tab` / `TabTitle` / the panel disappear; `TabNavigation`
takes plain `Link` children directly. There is no panel, because the destination
page's content lives on that route, not inside the component.

**The active state.** `selectedKey` / `defaultSelectedKey` / `onSelectionChange`
have no counterpart — `TabNavigation` does not track selection itself. Each
`Link` marks itself current with `aria-current="page"`, which the app's router
already knows how to derive (e.g. from the current pathname), the same way it
would for any other navigation link.

**Status icons.** An `AlertIcon` moves from inside `TabTitle` to inside the
`Link`.

**Collapsing.** The two do this differently. `Tabs` collapses all at once: as
soon as any tab stops fitting, the whole tab list turns into a single button
showing the active tab, which opens every tab in a context menu. `TabNavigation`
collapses incrementally: links are hidden one by one as space runs out, and only
those that no longer fit move into the "More" button's menu — the rest stay
visible as ordinary links.

There is no codemod: only the surrounding code knows whether a given `Tabs`
usage was real navigation (an `href` on `TabTitle`, or a route push from
`onSelectionChange`) or a genuine content switcher that merely looked similar
before this change — the two are indistinguishable from the source alone.

**Apply:** Tabs' rendering changed: the tab list now fills the available width
with equal-size tabs and a shared, animated indicator that slides between them,
replacing the previous fit-content tabs that each carried their own
hover/pressed/selected background. Nothing else about `Tabs` changed, and a
`Tabs` that switches content within a page needs no code change — the new look
applies on upgrade. Where `Tabs` was used to fake real navigation instead — a
`TabTitle` given an `href` (`Aria.Tab`'s routing props: `href`, `target`,
`routerOptions`, …) so selecting it pushed a real route change — replace it with
`TabNavigation`: plain `Link` children instead of `Tab`/`TabTitle`/panels,
`aria-current="page"` on the active `Link` instead of
`selectedKey`/`defaultSelectedKey`/`onSelectionChange`. `TabNavigation` keeps
the visual language the old `Tabs` used to have, because it now owns that use
case.

---

<a id="table-column-width-props"></a>

## TableColumn: `maxWidth` removed, `width` and `minWidth` retyped

**Since `0.2.0-alpha.956`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

`maxWidth` has been removed. `width` and `minWidth` are now typed as
`number | string`: they no longer accept `null`, and the previous
template-literal typing (`` `${number}%` ``, `` `${number}fr` ``) is replaced by
a plain `string`.

```diff
- <TableColumn width="50%" minWidth={null} maxWidth={400} />
+ <TableColumn width="50%" />
```

Percentage, pixel and `fr` values keep working as strings or numbers
(`width="50%"`, `width="200fr"`, `width={300}`). Where you passed `null` to mean
"no explicit width", omit the prop instead.

A codemod removes `maxWidth` — the prop is gone from the type, so an explicit
attribute is wrong at any value, an expression included — and removes a `width`
or `minWidth` written as the literal `null`.

It declines what it cannot decide from the source: `width={maybeNull}` could be
anything at runtime, and a spread's contents are invisible. Both keep their
props and need a look by hand.

**Apply:** Remove `maxWidth` from every `TableColumn`. Where `width` or
`minWidth` was `null`, omit the prop instead — the type no longer accepts
`null`, only `number | string`. A codemod does both for the cases it can decide
from the source. Two it declines: a `width`/`minWidth` whose value is an
expression (`width={maybeNull}`), and a spread that might carry `maxWidth`
(`<TableColumn {...props} />`). Check those by hand.

```shell
npx @mittwald/flow-codemods@latest table-column-width-props src
```

---

<a id="table-render-prop-removed"></a>

## Table: `render` prop removed

**Since `0.2.0-alpha.866`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

The `render` escape hatch on `Table` has been removed. Compose the table from
`TableHeader`, `TableColumn`, `TableBody`, `TableRow` and `TableCell` instead.

**Apply:** Replace the `render` escape hatch on `Table` with a composition of
`TableHeader`, `TableColumn`, `TableBody`, `TableRow` and `TableCell`.

---

<a id="color-primary-to-default"></a>

## Breadcrumb, HeaderNavigation, Heading, IllustratedMessage, and Link: color property "primary" renamed to "default"

**Since `0.2.0-alpha.846`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

The `color="primary"` property has been renamed to `color="default"`.

```diff
- <Link color="primary">
+ <Link color="default" />
```

A codemod rewrites `color="primary"` to `color="default"` on these five
components (and leaves other components such as `Button`, where `"primary"` is
still valid, untouched).

**Apply:** Rewrite `color="primary"` to `color="default"` on `Breadcrumb`,
`HeaderNavigation`, `Heading`, `IllustratedMessage`, and `Link` only — leave
`Button` (and any other component where `"primary"` is still valid) untouched.

```shell
npx @mittwald/flow-codemods@latest color-primary-to-default src
```

---

<a id="table-cell-render-prop-removed"></a>

## TableCell: `render` prop removed

**Since `0.2.0-alpha.846`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

The `render` escape hatch on `TableCell` has been removed. Provide the cell
content as children instead.

```diff
- <TableCell render={(cell) => <CustomCell {...cell} />} />
+ <TableCell>
+  <CustomCell />
+ </TableCell>
```

**Apply:** Provide the cell content as children of `TableCell` instead of a
`render` function.

---

<a id="password-tools-rule"></a>

## password-tools: `AsyncRule` and `SyncRule` replaced by `Rule`

**Since `0.2.0-alpha.802`** · migration · codemod available

The `@mittwald/flow-react-components/mittwald-password-tools-js` entry no longer
exports `AsyncRule` and `SyncRule`. The underlying `@mittwald/password-tools-js`
merged both into a single abstract `Rule`.

```diff
- import { AsyncRule } from "@mittwald/flow-react-components/mittwald-password-tools-js";
+ import { Rule } from "@mittwald/flow-react-components/mittwald-password-tools-js";
```

A custom rule extends `Rule` and may return its result synchronously or as a
promise — the distinction the two classes used to encode is gone.

A codemod replaces both names. A file that imported both — or one of them next
to `Rule` — ends up with a single import.

**Apply:** Replace `AsyncRule` and `SyncRule` imports from
`@mittwald/flow-react-components/mittwald-password-tools-js` with `Rule`. Update
custom rule classes to extend `Rule` instead.

```shell
npx @mittwald/flow-codemods@latest password-tools-rule src
```

---

<a id="accent-box-color-to-background-color"></a>

## AccentBox.color is now a declaration for foreground

**Since `0.2.0-alpha.786`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

The `color` property now controls foreground colors. Use the `backgroundColor`
property to set the background color instead.

```diff
- <AccentBox color="gradient">
+ <AccentBox backgroundColor="gradient" />
```

`color` did not go away, it changed meaning — so this is not a rename. It used
to accept `"blue" | "green" | "gradient" | "neutral"` and now accepts
`"default" | "dark" | "light" | "dark-static" | "light-static"`.

A codemod decides per value: a value from the new foreground union stays on
`color`, every other literal moves to `backgroundColor`. Two cases stay
untouched, because neither can be decided from the value alone — check them by
hand:

- `color={expression}`, where the same expression means the background in old
  code and the foreground in new code.
- An element that already carries `backgroundColor`, where moving `color` there
  would overwrite the explicit value.

One value changes what you see: the runtime fallback maps `"neutral"`,
`"gradient"` and `"green"` onto the background, but not `"blue"`, so
`<AccentBox color="blue">` currently renders the neutral background. The codemod
turns it into `backgroundColor="blue"`, which restores the blue one.

**Apply:** Move every `color` value that is not one of
`"default" | "dark" | "light" | "dark-static" | "light-static"` to
`backgroundColor` instead. Review `color={expression}` and any element that
already has `backgroundColor` by hand — neither can be decided from the value
alone.

```shell
npx @mittwald/flow-codemods@latest accent-box-color-to-background-color src
```

---

<a id="cartesian-chart-restructured"></a>

## CartesianChart

**Since `0.2.0-alpha.780`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

> If you're using a _Function_ in the `dataKey` - you need to define a
> `dataKeyLabel` as well.

The `dataKeyLabel` is required to identify the RowData in the formatter
functions. If a `string` is used - this will automatically be used as the
`dataKeyLabel`.

```diff
- <XAxis dataKey={() => 1337} />
+ <XAxis dataKey={() => 1337} dataKeyLabel={"leet"} />
```

---

> The Data Types from the CartesianChart have changed from `any` to `unknown`.

**Before (With type `any`):**

```tsx
const data = [
  {
    amount: 1,
    time: new Date("2026-08-11"),
  }
];

<CartesianChart data={data}>>
  <XAxis
    dataKey="time"
    tickFormatter={(date) =>
      // date is typeof any
      Intl.DateTimeFormat("de", {
        month: "short",
        day: "2-digit",
      }).format(date)
    }
  />
</CartesianChart>
```

**Now:** now you need to check the type explicit e.g.

```tsx
<XAxis
  dataKey="time"
  tickFormatter={(date) => {
    // date is typeof unknown
    if (date instanceof Date) {
      return Intl.DateTimeFormat("de", {
        month: "short",
        day: "2-digit",
      }).format(date);
    }
  }}
/>
```

or use the **new** `typedCartesianChart` which infers the type automatically

```tsx
interface ChartData {
  amount: number;
  time: Date,
};

const data: ChartData[] = [
  {
    amount: 1,
    time: new Date("2026-08-11"),
  }
];

const ExampleChart = typedCartesianChart<ChartData>();

<ExampleChart.Chart data={data}>>
  <ExampleChart.XAxis
    dataKey="time"
    tickFormatter={(date) =>
      // date is typeof Date
      Intl.DateTimeFormat("de", {
        month: "short",
        day: "2-digit",
      }).format(date)
    }
  />
</ExampleChart.Chart>
```

**Apply:** Add `dataKeyLabel` wherever a `dataKey` is a function — a string
`dataKey` already sets it automatically. Where a `tickFormatter` or other
callback relied on the argument being `any`, add an explicit type check (for
example `instanceof Date`), or switch to `typedCartesianChart<T>()` for a chart
whose callbacks are typed from your own data shape.

---

<a id="code-block-syntax-highlighter-removed"></a>

## Removed the underlying react-syntax-highlighter library from CodeBlock

**Since `0.2.0-alpha.756`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

We've replaced the `react-syntax-highlighter` library, which means many
properties have been removed and the remaining ones have been simplified. See
the
[CodeBlock documentation](https://flow.mittwald.de/components/content/code-block)
for details on what's now supported.

**Apply:** Check every `CodeBlock` usage against the current props (see the
[CodeBlock documentation](https://flow.mittwald.de/components/content/code-block))
and remove or replace props the new implementation does not support.

---

<a id="muted-action-error-to-abort-action-error"></a>

## `MutedActionError` renamed to `AbortActionError`

**Since `0.2.0-alpha.712`** · migration · codemod available

The error that aborts an `Action` without reporting a failure is now called
`AbortActionError`. Its static helpers were renamed along with it.

```diff
- import { MutedActionError } from "@mittwald/flow-react-components";
+ import { AbortActionError } from "@mittwald/flow-react-components";

- throw new MutedActionError();
+ throw new AbortActionError();

- MutedActionError.isMutedActionError(error);
+ AbortActionError.isAbortActionError(error);

- MutedActionError.rethrowIfNotMuted(error);
+ AbortActionError.rethrowIfNotAborted(error);
```

There is no alias for the old name. The thrown error's `name` changed from
`"MutedActionError"` to `"AbortActionError"` as well — update any code that
matches on it.

A codemod renames the class and both static helpers. It also rewrites an
`error.name === "MutedActionError"` comparison, but only in a file that imports
the class — a check living anywhere else cannot be recognised, so grep for the
string once when you are done.

**Apply:** Rename `MutedActionError` to `AbortActionError`, `isMutedActionError`
to `isAbortActionError`, and `rethrowIfNotMuted` to `rethrowIfNotAborted`.
Update any `error.name === "MutedActionError"` comparison to
`"AbortActionError"`.

```shell
npx @mittwald/flow-codemods@latest muted-action-error-to-abort-action-error src
```

---

<a id="overlay-controller-add-on-close-return-type"></a>

## OverlayController.addOnClose / addOnOpen return type changed

**Since `0.2.0-alpha.696`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

The return type changed from `() => void` to `() => unknown`

**Apply:** No type change needed: the return type widened from `() => void` to
`() => unknown`, and a `() => void` callback stays assignable. Instead, check
every callback passed to `addOnClose`/`addOnOpen` for one that can return
`false` — for example an arrow function whose body is an expression evaluating
to `false`. `executeHandlers` now treats any handler returning `false` as a veto
and cancels the close/open. A callback that returned `false` incidentally, with
no intent to block anything, now silently cancels closes.

---

<a id="form-resets-after-modal-close"></a>

## Form: resets itself after the surrounding modal closes

**Since `0.2.0-alpha.694`** · migration · no code change needed · also applies
to `@mittwald/flow-remote-react-components`

A react-hook-form `<Form>` inside a `Modal` now resets to its default values
once the modal has closed. Previously it kept what the user had entered, so
reopening the modal showed the abandoned input.

Opt out per form with the new `autoReset` prop:

```diff
- <Form form={form} onSubmit={onSubmit}>
+ <Form form={form} onSubmit={onSubmit} autoReset={false}>
```

`autoReset` also takes an object (`autoReset={{ onAfterModalClose: false }}`) so
further reset triggers can be added without another prop.

**Apply:** No code change required. To keep the previous behaviour (the form
keeping what the user entered), pass `autoReset={false}` (or
`autoReset={{ onAfterModalClose: false }}`) to `Form`.

---

<a id="cartesian-chart-empty-view"></a>

## CartesianChart.emptyView changed

**Since `0.2.0-alpha.676`** · migration · manual change · also applies to
`@mittwald/flow-remote-react-components`

Component references are no longer accepted for `emptyView` - must be a rendered
element now.

```diff
- <CartesianChart emptyView={EmptyState} />
+ <CartesianChart emptyView={<EmptyState />} />
```

**Apply:** Wrap the `emptyView` value in JSX — `emptyView={<EmptyState />}`
instead of `emptyView={EmptyState}`.

---

<a id="action-prop-to-on-action"></a>

## Action: `action` renamed to `onAction`

**Since `0.2.0-alpha.646`** · migration · codemod available · also applies to
`@mittwald/flow-remote-react-components`

`Action`'s `action` prop is now called `onAction`, which matches the naming of
every other event prop in Flow.

```diff
- <Action action={createOrganization}>
+ <Action onAction={createOrganization}>
    <Button>Organisation anlegen</Button>
  </Action>
```

`action` was removed from `ActionProps`, so TypeScript reports it as an unknown
prop. At runtime the old prop still works: `Action` maps it to `onAction` and
logs a deprecation warning, and an explicit `onAction` wins. The fallback will
be removed in a future major version.

#### The type changed too

`onAction` is typed `ActionFn`, which is `(...args: unknown[]) => unknown`. A
function that declares a parameter of any narrower type is not assignable to it,
because the parameter would receive `unknown`:

```
Type '(options?: CloseOptions | undefined) => void' is not assignable to type 'ActionFn'.
  Types of parameters 'options' and 'args' are incompatible.
    Type 'unknown' is not assignable to type 'CloseOptions | undefined'.
```

Wrap the call instead of passing the reference:

```diff
- <Action onAction={controller.close}>
+ <Action onAction={() => controller.close()}>
```

Only function **references** are affected. An inline arrow
(`onAction={() => …}`), a zero-parameter function, and anything already
accepting `unknown` are all fine.

A codemod renames the prop and wraps the reference.

Whether a reference _needs_ wrapping is not decidable from the source — that
needs type information. Performing the wrap does not need it: `() => fn()` calls
what `Action` would have called, and `onAction` takes no arguments. So the wrap
fixes the reference that needed it and changes nothing for the rest, which makes
the decision unnecessary.

Wrapped: a plain identifier and a member expression (`close`,
`controller.close`, `this.handleSave`). Left alone: an arrow function and a
function expression, which already are the handler; a call (`makeHandler()`,
`close.bind(controller)`), which produces it; and anything that is not one
reference (`isOpen ? close : open`, `onClose ?? noop`, `controller?.close`).

Two wraps are worth a look afterwards. A handler that read the event `Action`
forwards — undocumented, but it does forward the trigger's event — stops
receiving it. And `onAction={props.onAction}`, where the reference may be
`undefined`: passing it was fine, calling it is not, so TypeScript now reports
the call and wants a guard.

**Apply:** Rename the `action` prop on `Action` to `onAction`. Not only a
rename: the new prop is typed `ActionFn` (`(...args: unknown[]) => unknown`), so
a function _reference_ that declares a parameter no longer type-checks and needs
wrapping — `onAction={() => controller.close()}` rather than
`onAction={controller.close}`. A codemod does both. It wraps every bare
reference (`close`, `controller.close`), because the wrap is a no-op for a
reference that did not need it. It leaves a value that already is the handler or
produces one — an arrow function, a function expression, a call like
`makeHandler()` or `close.bind(controller)` — and anything that is not one
reference, such as `isOpen ? close : open` or `controller?.close`. Two wraps to
look at afterwards: a handler that read the event `Action` forwards stops
receiving it, and a possibly-undefined reference (`onAction={props.onAction}`)
becomes a call TypeScript rejects — add the guard it asks for.

```shell
npx @mittwald/flow-codemods@latest action-prop-to-on-action src
```

---

<a id="button-props-interfaces"></a>

## Removed ResetButton and SubmitButton Interfaces

**Since `0.2.0-alpha.646`** · migration · codemod available

The `RemoteButtonElementProps`, `ResetButtonProps`, and `SubmitButtonProps`
interfaces have been removed. Use `ButtonProps` instead.

```diff
- import type { SubmitButtonProps } from "@mittwald/flow-react-components/react-hook-form";
+ import type { ButtonProps } from "@mittwald/flow-react-components";
```

Note the entry: `ButtonProps` lives in the package root, while the removed names
came from `react-hook-form`. A codemod moves the import along with the name. It
covers `@mittwald/flow-react-components` only.
`@mittwald/flow-remote-react-components` does not export `ButtonProps`, so there
is nothing to move a remote import onto — pick your own source for the type
there. `RemoteButtonElementProps` is left alone as well:
`@mittwald/flow-remote-elements` still exports that name.

**Apply:** Replace `ResetButtonProps`/`SubmitButtonProps` imports from
`@mittwald/flow-react-components/react-hook-form` with `ButtonProps` from the
package root. Leave `RemoteButtonElementProps` (from
`@mittwald/flow-remote-elements`) alone.

```shell
npx @mittwald/flow-codemods@latest button-props-interfaces src
```

---

<a id="imports-to-package-root"></a>

## Subpath imports moved to the package root

**Since `0.2.0-alpha.28`** · migration · codemod available

With the latest update to `@mittwald/flow-react-components`, the way package
exports are handled has changed. You no longer need to specify subdirectories
explicitly when importing components and utilities. Instead, imports are now
structured in a more streamlined way.

### Changes in Imports

Previously, you had to import components and utilities from specific
subdirectories, like this:

```javascript
import Button from "@mittwald/flow-react-components/Button";
import { useOverlayController } from "@mittwald/flow-react-components/controller";
import Field from "@mittwald/flow-react-components/react-hook-form/Field";
import { Link } from "@mittwald/flow-react-components/react-hook-form/nextjs";
```

With the new package structure, the same imports should be rewritten as follows:

```javascript
import { Button } from "@mittwald/flow-react-components";
import { useOverlayController } from "@mittwald/flow-react-components";
import { Field } from "@mittwald/flow-react-components/react-hook-form";
import { Link } from "@mittwald/flow-react-components/nextjs";
```

### Stylesheets stay stylesheet imports

A CSS export is a file, not a JS module, so it never moves onto the package
root. A bundler query addressing it (`?url`, `?inline`, `?raw`) is part of how
the file is requested, not part of the subpath — both of these stay exactly as
they are:

```javascript
import "@mittwald/flow-react-components/all.css";
import flowStyles from "@mittwald/flow-react-components/all.css?url";
```

Only a stale `global.css` or `globals.css` is rewritten to `all.css`, query
included.

### `tsconfig.json`

Set `"module": "esnext"` in your `tsconfig.json`, if you have trouble with
missing module exports.

### Migration Steps

#### Do it manually

1. **Update all import statements** in your project according to the new
   structure.
2. **Remove unnecessary subdirectory paths** from imports.
3. **Verify your application still compiles and runs correctly.**
4. **Run your test suite** to ensure no regressions were introduced by the
   migration.

This change shipped in `0.2.0-alpha.28`: `0.2.0-alpha.27` still published one
export entry per component, `alpha.28` published the flat set. Neither `0.1.0`
nor `0.2.0` was ever released as a stable version — both are alpha-only lines,
and the first stable release of Flow is `1.0.0`.

**Apply:** Rewrite every subdirectory import from
`@mittwald/flow-react-components` to the package root, except `react-hook-form`
and `nextjs`, which move to `@mittwald/flow-react-components/react-hook-form`
and `@mittwald/flow-react-components/nextjs`. Asset imports are not part of
this: a CSS specifier stays a CSS specifier — `all.css` and `all-layered.css`
unchanged, a stale `global.css` or `globals.css` rewritten to `all.css` — and
keeps any bundler query it carries (`?url`, `?inline`, `?raw`). It never becomes
a named import from the package root, which has no JS binding behind a
stylesheet. If you hit missing module errors, set `"module": "esnext"` in
`tsconfig.json`.

```shell
npx @mittwald/flow-codemods@latest imports-to-package-root src
```

---

<a id="renamed-css-export"></a>

## Renamed CSS export

**Since `0.1.0-alpha.292`** · migration · codemod available

The CSS export `@mittwald/flow-react-components/styles` has renamed to the more
precise name `@mittwald/flow-react-components/all.css`, because the file
contains the CSS of all components, and now there are CSS exports per component
as well. A documentation on how to use them is planned.

```diff
// main.js
- import "@mittwald/flow-react-components/styles";
+ import "@mittwald/flow-react-components/all.css";
```

A codemod rewrites the specifier in every JavaScript and TypeScript form that
names a module. It cannot reach a `.css` or `.scss` file, so an `@import` of the
old path there needs a manual search.

**Apply:** Replace the import `@mittwald/flow-react-components/styles` with
`@mittwald/flow-react-components/all.css`, keeping any bundler query the
specifier carries (`.../styles?url` becomes `.../all.css?url`). A codemod does
this for JavaScript and TypeScript files. An `@import` of the old path inside a
`.css` or `.scss` file is not covered — search for it by hand.

```shell
npx @mittwald/flow-codemods@latest renamed-css-export src
```
