217 lines
14 KiB
Markdown
217 lines
14 KiB
Markdown
# Subframe-Migration: Komponenten-Mapping
|
||
|
||
Vertrag für die Migration der shadcn/Radix-UI (`apps/web/app/components/ui/*`) auf das
|
||
Subframe-Designsystem „Basedesign" (`apps/web/app/ui/*`, project `f023064a5ca0`).
|
||
|
||
## Grundsätze
|
||
|
||
- **Theme:** Subframe ist die alleinige Quelle (`apps/web/app/ui/theme.css`, eingebunden in
|
||
`globals.css`). Der Brand ist grayscale und deckt sich faktisch mit dem bisherigen
|
||
zinc-Primary. Die shadcn-Tokens (`--primary`, `--background` …) bleiben aktiv, bis ihre
|
||
Consumer migriert sind, und werden am Ende entfernt (samt JS-`@config` → beseitigt die
|
||
22 transitorischen CSS-Optimierungs-Warnungen).
|
||
- **Mechanismus:** Strangler-Shim. Pro Primitive wird die Implementierung in
|
||
`components/ui/<x>.tsx` durch einen Wrapper über die Subframe-Komponente ersetzt; die
|
||
bestehende API bleibt erhalten, Call-Sites ändern sich nicht. Schwer abbildbare Spezialfälle
|
||
bleiben vorerst auf dem Legacy-Pfad und werden später auf native Subframe-Komponenten
|
||
(LinkButton, IconButton …) umgestellt.
|
||
- **Verifikation pro Schritt:** `pnpm --filter @toolctrl/web typecheck` + `build` grün,
|
||
Komponenten-Tests grün, danach `pnpm verify:a11y-coverage`.
|
||
- **Generierte Dateien** in `app/ui/` gehören Subframe. Lokale Fixes nur mit
|
||
`// @subframe/sync-disable` (sonst überschreibt der nächste `pnpm subframe:sync`).
|
||
|
||
## Button ✅ migriert (Shim)
|
||
|
||
`components/ui/button.tsx` rendert für den Normalfall die Subframe-`Button`.
|
||
|
||
Prop-Mapping:
|
||
|
||
- `variant`: default→`primary`, destructive→`destructive`, outline→`tertiary`,
|
||
secondary→`secondary`, ghost→`ghost`, link→`ghost`
|
||
- `size`: default→`medium`, sm→`small`, xs→`small`, lg→`large`, icon→Legacy
|
||
- `isLoading`→`loading`; `dirty`→eigene Klasse (bleibt); `type`→Default `submit` erzwungen
|
||
(Subframe würde sonst `button` erzwingen und Formular-Buttons brechen)
|
||
|
||
Legacy-Pfad (noch shadcn, später nativ):
|
||
- `asChild` (46 Stellen) → Ziel: Subframe `LinkButton`
|
||
- `size="icon"` (16 Stellen) → Ziel: Subframe `IconButton`
|
||
- `buttonVariants`-Export (Consumer: alert-dialog, calendar, pagination, json/ShowModalButton)
|
||
|
||
## Formularfelder (`components/ui/input/*`) — der eigentliche Hebel
|
||
|
||
Die bare `input.tsx` wird kaum genutzt (9×); die Frequenz liegt in den Feld-Komponenten.
|
||
Subframes `TextInput`/`TextArea` sind `<label>`-Container (Label + Box + `*.Input`) und passen
|
||
hier — nicht auf die bare `input.tsx`. Beide haben eine `max-w`-Begrenzung, die für volle
|
||
Feldbreite mit `className="w-full max-w-none"` überschrieben werden muss.
|
||
|
||
- **InputText (129×) ✅ migriert** — volle Subframe `TextInput`/`TextArea` inkl. Label (rich
|
||
Label-Content als ReactNode), `error`, controlled+uncontrolled, type-Mapping. Editor-Branches
|
||
(monaco/wysiwyg) bleiben unverändert (in der Praxis ungenutzt). Label-Assoziation jetzt
|
||
**implizit** (Input im `<label>`) statt `for`/`id` → a11y-Test entsprechend angepasst.
|
||
Hinweis: `type="password"`/`"search"` zeigen jetzt Subframes eingebautes Lock-/Such-Icon.
|
||
- **Welle 2 ✅ migriert** (Shim → Subframe, typecheck/build/a11y grün): InputNumber→NumberInput,
|
||
InputSearch/InputSearchWithURL→TextInput(type=search), InputSearchButton→Button,
|
||
InputSelect/InputSelector→Select, InputCheckbox/Inline/WithDescription→Checkbox,
|
||
InputCheckboxCards→CheckboxCard, InputRadioGroup→RadioGroup, InputRadioGroupCards→RadioCardGroup,
|
||
InputChoicebox→RadioCardGroup, Toggle→Switch. InputResponsiveSelector erbt via InputSelector.
|
||
app/ui-Patch: NumberInput (hardcodiertes `value=""` entfernt, sync-disable).
|
||
Trade-offs: InputNumber `icon`-Prop jetzt visuell No-op; InputCheckboxWithDescription Layout-Shift.
|
||
|
||
## Welle 3 (Buttons-Folder / Banners / Atome)
|
||
|
||
- **Migriert ✅** (typecheck/build/a11y grün): `buttons/ButtonShare` + `buttons/ButtonFlyout` →
|
||
via Button-Shim (Rest des buttons/-Ordners lief schon über den Shim); `banners/*` (Error/Warning/
|
||
Success/Info) → BannerAlert (type-Mapping; `onDismiss` als Overlay erhalten, da BannerAlerts
|
||
dismiss-Button ein Mock ohne onClick ist); `progress.tsx` → Progress; `skeleton.tsx` → SkeletonText.
|
||
|
||
### ⚠️ Subframe-Komponenten, die NICHT passen (Mock oder Struktur-Mismatch) → bleiben shadcn
|
||
|
||
Rein präsentational (kein onChange/State/Navigation) oder strukturell inkompatibel — bewusst NICHT migriert:
|
||
- **Combobox / MultiSelect** — `<input value="">` hartkodiert, kein onChange → InputCombobox bleibt shadcn.
|
||
- **DatePicker** — hartkodierte Tage/Monate, Pfeile ohne onClick → InputDate/InputDatePicker bleiben shadcn (funktionaler `Calendar` wrappt react-day-picker und bleibt).
|
||
- **SearchInput** — value hartkodiert; echte Suchfelder nutzen `TextInput type=search`.
|
||
- **Tooltip** — statischer `<div>`-Mock (kein Radix-Trigger/Content/Hover) → `tooltip.tsx` bleibt shadcn.
|
||
- **Alert** — Subframe ist prop-basiert (title/description), shadcn ist Compound (Alert/AlertTitle/AlertDescription) → bleibt shadcn.
|
||
- **Avatar** — Subframe hat keine AvatarImage/AvatarFallback-Subkomponenten + keine Fallback-bei-Ladefehler-Logik → bleibt shadcn.
|
||
- **Badge** (+ badges/*) — BadgeFeedback erzwingt einen führenden Dot, ist `<div>` statt `<span>`, kein `asChild`, keine freien Farben (Badge nutzt 8+ freie Farben) → bleibt shadcn.
|
||
|
||
Damit diese migrierbar werden, müsste das Subframe-Design die Interaktionen/Struktur liefern (oder
|
||
wir behalten shadcn). Bei jedem Sync prüfen, ob upstream funktionale/passende Varianten kommen.
|
||
|
||
## Welle 4 (funktionale Compounds) — Decke erreicht
|
||
|
||
Nur **`slider.tsx` → Subframe Slider** migriert (funktional, gleicher Radix; im Repo aktuell ungenutzt;
|
||
ref nicht weitergereicht, da span vs. div). Alle anderen Compounds **gestoppt** — die Subframe-
|
||
Komponenten sind zwar teils Radix-funktional, aber **kompositorisch unvollständig**:
|
||
- **Dialog** — Subframe exportiert nur Root+Content (kein Trigger/Close/Portal/Overlay), Root hardcodiert
|
||
Portal+Overlay+Title → würde an jeder Modal-Stelle doppeltes Overlay erzeugen. Bleibt shadcn.
|
||
(Korrektur: `components/ui/dialog` wird direkt nur in 3 Dateien importiert; die Modal-Frequenz läuft
|
||
transitiv über `modals/DialogSticky`/`CommandDialog`/`ModalBlank`, ~57 Dateien.)
|
||
- **DropdownMenu** — Subframe ist nur Item-/Surface-Styling; das funktionale Radix ist DASSELBE Paket
|
||
wie bei shadcn (kein Gewinn, hohes Risiko). Bleibt shadcn.
|
||
- **Drawer / Sheet** — Subframe Drawer ist right-only, kein Trigger/Close, hardcodiertes Content-Styling;
|
||
`sheet` braucht `side`-Prop; `drawer` ist ein vaul-Drag-Drawer. Bleiben shadcn.
|
||
- **Accordion** — Subframe basiert auf Collapsible (kein Multi-Item `type`/`value`). Bleibt shadcn.
|
||
|
||
**Fazit:** Die sauber migrierbare Fläche ist mit dem aktuellen „Basedesign"-Stand weitgehend
|
||
ausgeschöpft. Compounds brauchen in Subframe **komponierbare Teile** (Trigger/Close/Portal/Content +
|
||
Sub-Items), Mocks brauchen **Funktionalität**. Weiterer Fortschritt = Subframe-Design-Arbeit, nicht Code.
|
||
|
||
## Welle 5 (Rest-Sweep „alles Migrierbare")
|
||
|
||
- **Migriert ✅** (typecheck/build/a11y grün): `switch`→Switch, `checkbox`→Checkbox, `radio-group`→RadioGroup
|
||
(alle mit controlled/uncontrolled-Shim + null→undefined-Coercion), `calendar`→Subframe Calendar
|
||
(= dasselbe react-day-picker, kein Feature-Verlust), `button-group`/`split-button`→ButtonGroup,
|
||
`loaders/Loading`+`FloatingLoader`→Loader, `steps/ProgressBar`→StepperHorizontal.
|
||
- **Gestoppt** (Struktur/Compound/Semantik-Mismatch, bleiben shadcn): `breadcrumb`(+breadcrumbs/) — Subframe-Items sind keine Links;
|
||
`empty`(+emptyState/) — Subframe-Button ist Mock ohne onClick; `stepper` — Subframe ist rein präsentational (keine
|
||
Step-State/Trigger); `toggle`/`toggle-group` — Subframe ist single-only + bricht `toggleVariants`-Export;
|
||
`links/` — URL-Filter-Toggle, kein Link-Button; `errors/ErrorPage`+`ServerError` — Full-Page-Layout vs. Inline-Karte,
|
||
Retry-Button ist Mock; `headers/HeaderFlyoutItem` — Popover-Flyout vs. Top-Bar; `stats/*` — StatsCard erzwingt
|
||
Trend-Icon + Pointer + bricht `<dl>`/sr-only-a11y; `select` — Compound (10 Exporte, kompositionale Inversion);
|
||
`table` — Subframe Table nicht komponierbar (keine Header/Body/Footer/Caption-Teile).
|
||
|
||
**Endstand der Code-Migration:** Was mit dem aktuellen Subframe-Projekt sauber ging, ist migriert. Alles Weitere
|
||
ist durch die Vollständigkeit von „Basedesign" blockiert (siehe Mock-/Compound-Listen oben).
|
||
|
||
## Direkte 1:1-Ziele (gleiche Reihenfolge wie Migrationswellen)
|
||
|
||
| shadcn | Subframe | Hinweis |
|
||
|---|---|---|
|
||
| input | TextInput | placeholder=string |
|
||
| textarea | TextArea | |
|
||
| select | Select | gepatcht (sync-disable) |
|
||
| checkbox | Checkbox | |
|
||
| radio-group | RadioGroup | |
|
||
| switch | Switch | |
|
||
| badge | BadgeFeedback / Tag_Label | kein generisches Badge |
|
||
| avatar | Avatar | + AvatarGroup |
|
||
| tooltip | Tooltip | |
|
||
| tabs | Tabs | |
|
||
| accordion | Accordion | |
|
||
| breadcrumb | Breadcrumbs | |
|
||
| progress | Progress | + ProgressCircle |
|
||
| slider | Slider | |
|
||
| skeleton | SkeletonText / SkeletonCircle | |
|
||
| table | Table | |
|
||
| dialog | Dialog | + ModalBlank/ConfirmDialog/ResultDialog |
|
||
| drawer / sheet | Drawer | |
|
||
| dropdown-menu | DropdownMenu | |
|
||
| context-menu | ContextMenu | |
|
||
| sidebar | SidebarWithSections | |
|
||
| stepper | StepperHorizontal/Vertical | |
|
||
| toggle-group / toggle | ToggleGroup | |
|
||
| input-otp | InputOtp | |
|
||
| command | Combobox / SearchInput | keine Command-Palette; gepatcht |
|
||
| multi-select | MultiSelect | + MultiSelectTags |
|
||
| calendar | Calendar | + DatePicker |
|
||
| chart | AreaChart/BarChart/LineChart/PieChart | konkrete Typen statt Wrapper |
|
||
| card | CardIcon/CardImage/StatsCard | kein generisches Card |
|
||
| empty | EmptyState | |
|
||
| alert | Alert | |
|
||
| toast/toaster/sonner | Toast | |
|
||
|
||
## Lücken — bleiben shadcn (kein Subframe-Pendant)
|
||
|
||
`aspect-ratio`, `carousel`, `collapsible`, `hover-card`, `input-group`, `label`,
|
||
`pagination`, `popover`, `resizable`, `scroll-area`, `separator`.
|
||
|
||
## Bekannte Generierungs-Bugs (gepatcht, sync-disabled)
|
||
|
||
- `Select`: undefined `checked`/`highlighted` → entfernt (Styling via data-state)
|
||
- `Combobox`, `SearchInput`: `placeholder` ReactNode → `string`
|
||
|
||
## Token-Zuordnung: shadcn → Subframe (für „Logik=Radix, Look=Subframe")
|
||
|
||
Strategie für alle noch nicht migrierten Komponenten: die bestehende Radix/shadcn-Komponente
|
||
BEHALTEN (Logik bleibt), NUR die Farb-/Stil-Klassen auf Subframe-Tokens umstellen. Keine
|
||
Struktur-/Import-/Export-Änderung — nur className. Sync-sicher (alles in components/ui; die
|
||
Subframe-Tokens kommen global aus app/ui/theme.css). Subframe-`app/ui`-Mock-Komponenten werden
|
||
für diese NICHT verwendet.
|
||
|
||
| shadcn-Token | Subframe-Token |
|
||
|---|---|
|
||
| `bg-background` | `bg-surface-neutral-primary` |
|
||
| `bg-card`, `bg-popover` | `bg-surface-neutral-primary` (Overlays/Modal: `bg-surface-neutral-modal`) |
|
||
| `text-foreground`, `text-card-foreground` | `text-text-neutral-primary` |
|
||
| `text-muted-foreground` | `text-text-neutral-tertiary` |
|
||
| `placeholder:text-muted-foreground` | `placeholder:text-text-neutral-subtle` |
|
||
| `bg-primary` (hover `/90`) | `bg-brand-brand-surface-solid-default` (hover `-hover`) |
|
||
| `text-primary-foreground` | `text-text-neutral-on-solid` |
|
||
| `bg-secondary`, `bg-muted`, `bg-accent` | `bg-surface-neutral-secondary` |
|
||
| `text-secondary-foreground`, `text-accent-foreground` | `text-text-neutral-primary` |
|
||
| `hover:bg-accent` | `hover:bg-surface-neutral-secondary` (aktiv: `-tertiary`) |
|
||
| `bg-destructive` (hover `/90`) | `bg-surface-feedback-solid-negative-default` (hover `-hover`) |
|
||
| `text-destructive` | `text-text-feedback-negative` |
|
||
| `border`, `border-border` | `border-border-default` |
|
||
| `border-input` | `border-border-strong` |
|
||
| Fokus: `ring-ring`, `ring-[3px]`, `focus-visible:ring-*` | `shadow-focus` (+ `border-brand-brand-border-solid-default`) |
|
||
| disabled: `opacity-50`/muted | `bg-surface-neutral-disabled`, `text-text-neutral-disabled`, `border-border-disabled` |
|
||
|
||
Regel: nur diese Art Klassen ersetzen. Layout-/Spacing-/Radius-Utilities (flex, gap, px, rounded …)
|
||
unverändert lassen. Im Zweifel an Button.tsx / TextInput.tsx orientieren (echte Subframe-Tokens).
|
||
|
||
## Welle 6+7 — „Logik=Radix, Look=Subframe" (Restyle aller restlichen Komponenten)
|
||
|
||
Strategie-Wechsel (vom Designer bestätigt: es geht um Logik, nicht um neue Subframe-Komponenten):
|
||
Die zuvor als „blockiert/Mock" eingestuften Komponenten werden NICHT durch Subframe-Mock-
|
||
Komponenten ersetzt — stattdessen behalten sie ihre funktionierende Radix/shadcn-Logik und
|
||
bekommen nur den Subframe-LOOK per Token-Swap (className). Sync-sicher (nur components/ui,
|
||
Tokens global aus theme.css). Carbon nicht nötig — Radix ist die headless Logik-Basis.
|
||
|
||
Restyled (typecheck/build/a11y/axe grün): dialog, alert-dialog, modals/*, dropdown-menu,
|
||
context-menu, dropdowns/*, tabs (+tabs/*), accordion, collapsible, tooltip, popover, hover-card,
|
||
tooltips/*, table (+tables/*), select, drawer, sheet, sidebar, slideOvers/*, card, alert, avatar,
|
||
badge (+badges/*, freie Farben bewusst erhalten), breadcrumb (+breadcrumbs/*), empty (+emptyState/*),
|
||
stepper, stats/*, separator, scroll-area, resizable, input-group, input, textarea, choicebox,
|
||
dividers/*, command (+commandPalettes/*), toast, notifications/*, errors/*, headers/*, chart
|
||
(nur Tooltip/Legende), carousel (erbt via Button).
|
||
|
||
Bewusst NICHT umgestellt: freie Farben (Badge-Paletten, Trend-grün/rot, Status-Icons),
|
||
Overlay-Backdrops (`bg-black/50`), Sidebar-eigener `sidebar-*`-Token-Namespace, recharts-Datenfarben,
|
||
sowie Komponenten ohne Farb-Tokens (label, pagination, aspect-ratio — Look kommt via Button/Tokens).
|
||
|
||
Langer Schwanz (noch offen, meist Spezial/Utility, geringe Sichtbarkeit): lists/, sort/, selectors/,
|
||
misc/, json/, code/, docs/, layouts/, media/, images/, sections/, wizards/, poll/, pdf/ + Spezial-
|
||
Inputs (InputColorSelector/MultiText/Image/Media/RichTextEditor). Icons (111) bleiben außen vor.
|