|
| 1 | +# Headless Library Plan |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Decouple all Radix UI (and other visual) dependencies from the library core, making it fully headless. Consumers bring their own UI components. All current default/example implementations move to an `examples/` folder. |
| 6 | + |
| 7 | +## Current State |
| 8 | + |
| 9 | +### Radix packages in use (11 total) |
| 10 | + |
| 11 | +- `@radix-ui/react-accordion` — `ui/accordion.tsx` |
| 12 | +- `@radix-ui/react-checkbox` — `ui/checkbox.tsx` |
| 13 | +- `@radix-ui/react-collapsible` — `ui/collapsible.tsx` |
| 14 | +- `@radix-ui/react-dialog` — `ui/dialog.tsx` |
| 15 | +- `@radix-ui/react-label` — `ui/label.tsx`, `ui/form.tsx` |
| 16 | +- `@radix-ui/react-popover` — `ui/popover.tsx` |
| 17 | +- `@radix-ui/react-radio-group` — `ui/radio-group.tsx` |
| 18 | +- `@radix-ui/react-scroll-area` — `ui/scroll-area.tsx` |
| 19 | +- `@radix-ui/react-select` — `ui/select.tsx` |
| 20 | +- `@radix-ui/react-slot` — `ui/form.tsx` (`FormControl`) |
| 21 | +- `@radix-ui/react-tabs` — `ui/tabs.tsx` |
| 22 | + |
| 23 | +### Visual components embedded in flows/shared (must be made overridable or moved) |
| 24 | + |
| 25 | +- `EstimationResults` — uses `Card`, `Accordion`, `BasicTooltip`, `ActionsDropdown` |
| 26 | +- `SummaryResults` — uses `Card`, `Accordion` |
| 27 | +- `Termination/PaidTimeOff` — uses `Button` |
| 28 | +- `ContractorOnboarding/ContractPreviewStatement` — uses `Alert` |
| 29 | +- `ActionsDropdown` — uses `Button` from `ui/button` |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Phases |
| 34 | + |
| 35 | +### Phase 1 — Simplify `src/components/ui/form.tsx` |
| 36 | + |
| 37 | +Remove the two Radix dependencies while keeping the helper pattern intact. |
| 38 | + |
| 39 | +**Changes:** |
| 40 | + |
| 41 | +- `FormLabel`: replace `@radix-ui/react-label` + `Label` with a plain `<label>` element. |
| 42 | +- `FormControl`: replace `@radix-ui/react-slot` `Slot` with a lightweight wrapper that clones the child and merges `id` + `aria-*` props using `React.cloneElement`, keeping accessibility intact. |
| 43 | +- Remove imports: `@radix-ui/react-label`, `@radix-ui/react-slot`, `Label`. |
| 44 | + |
| 45 | +`FormField`, `FormItem`, `FormDescription`, `FormMessage` require no changes (no Radix). |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +### Phase 2 — Move all Default components to `examples/` |
| 50 | + |
| 51 | +All files in `src/components/form/fields/default/` are reference implementations that depend on Radix-backed `ui/` components. Move them to `examples/fields/`. |
| 52 | + |
| 53 | +**Files to move:** |
| 54 | + |
| 55 | +``` |
| 56 | +src/components/form/fields/default/ButtonDefault.tsx |
| 57 | +src/components/form/fields/default/CheckboxFieldDefault.tsx |
| 58 | +src/components/form/fields/default/CountryFieldDefault.tsx |
| 59 | +src/components/form/fields/default/DatePickerFieldDefault.tsx |
| 60 | +src/components/form/fields/default/EmailFieldDefault.tsx |
| 61 | +src/components/form/fields/default/FieldsetToggleButtonDefault.tsx |
| 62 | +src/components/form/fields/default/FileUploadFieldDefault.tsx |
| 63 | +src/components/form/fields/default/MultiSelectFieldDefault.tsx |
| 64 | +src/components/form/fields/default/NumberFieldDefault.tsx |
| 65 | +src/components/form/fields/default/RadioGroupFieldDefault.tsx |
| 66 | +src/components/form/fields/default/SelectFieldDefault.tsx |
| 67 | +src/components/form/fields/default/StatementDefault.tsx |
| 68 | +src/components/form/fields/default/TextAreaFieldDefault.tsx |
| 69 | +src/components/form/fields/default/TextFieldDefault.tsx |
| 70 | +src/components/form/fields/default/WorkScheduleFieldDefault.tsx |
| 71 | +``` |
| 72 | + |
| 73 | +Also move shared defaults: |
| 74 | + |
| 75 | +``` |
| 76 | +src/components/shared/drawer/DrawerDefault.tsx |
| 77 | +src/components/shared/zendesk-drawer/ZendeskDrawerDefault.tsx |
| 78 | +src/components/shared/pdf-preview/PDFPreviewDefault.tsx |
| 79 | +src/components/shared/table/TableFieldDefault.tsx |
| 80 | +``` |
| 81 | + |
| 82 | +Target structure: |
| 83 | + |
| 84 | +``` |
| 85 | +examples/ |
| 86 | + fields/ |
| 87 | + CheckboxFieldDefault.tsx |
| 88 | + TextFieldDefault.tsx |
| 89 | + ... (all field defaults) |
| 90 | + shared/ |
| 91 | + DrawerDefault.tsx |
| 92 | + ZendeskDrawerDefault.tsx |
| 93 | + PDFPreviewDefault.tsx |
| 94 | + TableFieldDefault.tsx |
| 95 | + ui/ |
| 96 | + accordion.tsx (moved from src/components/ui/) |
| 97 | + button.tsx |
| 98 | + checkbox.tsx |
| 99 | + ... (all Radix-backed ui primitives) |
| 100 | +``` |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +### Phase 3 — Remove `src/components/ui/` Radix components from the library |
| 105 | + |
| 106 | +Move all Radix-backed UI primitives to `examples/ui/`. These become reference implementations consumers can copy or replace. |
| 107 | + |
| 108 | +**Files to move to `examples/ui/`:** |
| 109 | + |
| 110 | +``` |
| 111 | +src/components/ui/accordion.tsx |
| 112 | +src/components/ui/badge.tsx |
| 113 | +src/components/ui/basic-tooltip.tsx |
| 114 | +src/components/ui/button.tsx |
| 115 | +src/components/ui/calendar.tsx |
| 116 | +src/components/ui/checkbox.tsx |
| 117 | +src/components/ui/collapsible.tsx |
| 118 | +src/components/ui/command.tsx |
| 119 | +src/components/ui/dialog.tsx |
| 120 | +src/components/ui/drawer.tsx |
| 121 | +src/components/ui/file-uploader.tsx |
| 122 | +src/components/ui/label.tsx |
| 123 | +src/components/ui/multi-select.tsx |
| 124 | +src/components/ui/popover.tsx |
| 125 | +src/components/ui/radio-group.tsx |
| 126 | +src/components/ui/scroll-area.tsx |
| 127 | +src/components/ui/select.tsx |
| 128 | +src/components/ui/tabs.tsx |
| 129 | +``` |
| 130 | + |
| 131 | +**Files that use only native HTML (no Radix) — can stay or move to examples:** |
| 132 | + |
| 133 | +``` |
| 134 | +src/components/ui/alert.tsx — pure HTML, no Radix |
| 135 | +src/components/ui/card.tsx — pure HTML, no Radix |
| 136 | +src/components/ui/input.tsx — pure HTML, no Radix |
| 137 | +src/components/ui/table.tsx — pure HTML, no Radix |
| 138 | +src/components/ui/textarea.tsx — pure HTML, no Radix |
| 139 | +``` |
| 140 | + |
| 141 | +These pure-HTML components have no Radix dependency but are still visual. Move them to `examples/ui/` as well so `src/components/ui/` is fully removed from core. |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +### Phase 4 — Make flows with embedded visual components headless |
| 146 | + |
| 147 | +For each flow component that directly renders visual elements (Card, Button, Alert, Accordion), replace the hardcoded component with either: |
| 148 | + |
| 149 | +- **Component override slot** via a `components` prop (preferred for public-facing output components). |
| 150 | +- **Native HTML equivalent** where the component is simple enough (e.g., `Button` → `<button>`, `Card` → `<div>`). |
| 151 | + |
| 152 | +#### 4a. `EstimationResults` |
| 153 | + |
| 154 | +Currently accepts a partial `components` prop. Extend it to cover all internal visual dependencies: |
| 155 | + |
| 156 | +```ts |
| 157 | +type EstimationResultsComponents = { |
| 158 | + Accordion?: React.ComponentType<...>; |
| 159 | + Tooltip?: React.ComponentType<{ content: React.ReactNode; children: React.ReactNode }>; |
| 160 | + ActionsDropdown?: React.ComponentType<ActionsDropdownProps>; |
| 161 | + HiringSection?: React.ComponentType<...>; // already exists |
| 162 | + OnboardingTimeline?: React.ComponentType<...>; // already exists |
| 163 | + Header?: React.ComponentType<...>; // already exists |
| 164 | + Footer?: React.ComponentType; // already exists |
| 165 | +}; |
| 166 | +``` |
| 167 | + |
| 168 | +The default for each slot is a plain HTML fallback (no Radix). |
| 169 | + |
| 170 | +#### 4b. `SummaryResults` |
| 171 | + |
| 172 | +Same approach — accept `Card` and `Accordion` overrides. Provide plain HTML defaults. |
| 173 | + |
| 174 | +#### 4d. `Termination/PaidTimeOff` |
| 175 | + |
| 176 | +Replace `Button` import with a `components?.button` override or a plain `<button>`. |
| 177 | + |
| 178 | +#### 4f. `ActionsDropdown` |
| 179 | + |
| 180 | +Replace `Button` from `ui/button` with a native `<button>` element directly. `ActionsDropdown` is already a plain dropdown with no Radix — only the `Button` wrapper is Radix-backed. |
| 181 | + |
| 182 | +--- |
| 183 | + |
| 184 | +### Phase 5 — Remove all `@radix-ui/*` from `package.json` |
| 185 | + |
| 186 | +After phases 1–4, verify no file in `src/` imports from `@radix-ui/*`. Then remove all 11 Radix entries from `package.json` dependencies. |
| 187 | + |
| 188 | +Run a final audit: |
| 189 | + |
| 190 | +```bash |
| 191 | +grep -r "@radix-ui" src/ |
| 192 | +``` |
| 193 | + |
| 194 | +Expected result: zero matches. |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +## File structure after completion |
| 199 | + |
| 200 | +``` |
| 201 | +src/ |
| 202 | + components/ |
| 203 | + form/ |
| 204 | + fields/ # field controllers (no Radix — use react-hook-form only) |
| 205 | + CheckBoxField.tsx |
| 206 | + TextField.tsx |
| 207 | + ... |
| 208 | + ui/ |
| 209 | + form.tsx # simplified — no Radix (FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage) |
| 210 | + shared/ |
| 211 | + actions-dropdown/ |
| 212 | + ActionsDropdown.tsx # uses native <button>, no Radix |
| 213 | + drawer/ |
| 214 | + Drawer.tsx # headless — delegates to components context |
| 215 | + table/ |
| 216 | + Table.tsx |
| 217 | + zendesk-drawer/ |
| 218 | + ZendeskDrawer.tsx |
| 219 | +
|
| 220 | +examples/ |
| 221 | + ui/ |
| 222 | + accordion.tsx |
| 223 | + button.tsx |
| 224 | + card.tsx |
| 225 | + checkbox.tsx |
| 226 | + ... |
| 227 | + fields/ |
| 228 | + CheckboxFieldDefault.tsx |
| 229 | + TextFieldDefault.tsx |
| 230 | + ... |
| 231 | + shared/ |
| 232 | + DrawerDefault.tsx |
| 233 | + ZendeskDrawerDefault.tsx |
| 234 | + TableFieldDefault.tsx |
| 235 | + PDFPreviewDefault.tsx |
| 236 | +``` |
| 237 | + |
| 238 | +--- |
| 239 | + |
| 240 | +## What does NOT change |
| 241 | + |
| 242 | +- react-hook-form is kept — it has no visual opinion and is core to how fields work. |
| 243 | +- `FormField` (Controller wrapper) stays in `form.tsx`. |
| 244 | +- All hooks, contexts, API client code, and type definitions stay untouched. |
| 245 | +- The `components` prop pattern on field controllers stays — consumers still pass their own implementations the same way. |
| 246 | +- CSS class names (`RemoteFlows__*`) stay on all components. |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +## Open questions before starting |
| 251 | + |
| 252 | +1. **`form.tsx` `FormControl` replacement**: Use `React.cloneElement` or remove entirely? Since Default components move to examples and those are the only `FormControl` consumers, we could remove `FormControl` from the core export and let examples bring their own version. |
| 253 | + |
| 254 | +2. **`EstimationResults` plain HTML defaults**: When no `Card`/`Accordion` override is given, should the component render plain `<div>` fallbacks (keeping it functional but unstyled), or should it throw (forcing the consumer to provide all slots)? Recommendation: plain `<div>` fallbacks so the component works out-of-the-box without any Radix. |
| 255 | + |
| 256 | +3. **Naming the examples folder**: `examples/` vs `packages/ui/` vs keeping it in the repo as a separate package. TBD with team. |
0 commit comments