|
| 1 | +# Spec: Schedule Builder — Radix UI Migration |
| 2 | + |
| 3 | +## Objective |
| 4 | + |
| 5 | +Migrate the **custom_admin Schedule Builder** from its current ad-hoc UI (custom |
| 6 | +`div` modal, native `<input>`/`<select>`/`<button>`, raw `<h1>`/`<strong>`/`<ul>` |
| 7 | +markup, emoji controls) to **Radix UI** — matching the pattern already used by the |
| 8 | +**Invitation Letter Document Builder** in the same app. |
| 9 | + |
| 10 | +Scope decision (confirmed with maintainer): **Full overhaul.** |
| 11 | +- Replace every custom UI primitive with `@radix-ui/themes` components. |
| 12 | +- Redesign the visual presentation of the schedule item card, the add-item modal, |
| 13 | + and the pending-items basket using Radix `Card` / `Badge` / `IconButton` / typography |
| 14 | + for a clean look consistent with the invitation-letter editor. |
| 15 | +- Rethink calendar grid presentation spacing and interaction affordances where it |
| 16 | + improves clarity — but **the grid-positioning mechanism (CSS grid + inline |
| 17 | + `gridColumnStart`/`gridRowStart`) and the drag-and-drop logic (`react-dnd`) stay |
| 18 | + functionally identical.** |
| 19 | +- The event-type picker (`talk`/`keynote`/`break`/…) becomes a `@radix-ui/themes` |
| 20 | + `Select`. |
| 21 | + |
| 22 | +**Out of scope:** GraphQL queries/mutations, the data model, `react-dnd` wiring, |
| 23 | +the Apollo layer, the Astro page entry, and any code outside |
| 24 | +`src/components/schedule-builder/` (plus the now-unused `src/components/shared/modal.tsx`). |
| 25 | + |
| 26 | +**User:** PyCon Italia organizers building the conference schedule in the Django admin. |
| 27 | + |
| 28 | +**Success looks like:** identical behavior (create slot, search proposal/keynote, |
| 29 | +add custom event, drag items between slots/rooms, unassign to basket, edit in admin), |
| 30 | +but the UI is rendered with Radix components, visually consistent with the invitation |
| 31 | +letter editor, with no native form controls or the custom modal remaining. |
| 32 | + |
| 33 | +## Tech Stack |
| 34 | + |
| 35 | +- **Framework:** Astro 5 + React 19 (islands), components in `src/components/**` |
| 36 | +- **UI:** `@radix-ui/themes` ^3.1.6 (primary) — already installed and wired via |
| 37 | + `<Theme>` in `src/components/shared/base.tsx` |
| 38 | +- **Icons:** `lucide-react` ^0.468.0 (e.g. `ChevronLeft`, `ChevronRight`, `Plus`, `Pencil`) |
| 39 | +- **Styling:** Tailwind 3.4 — **only** for grid positioning / one-off layout, not for |
| 40 | + component look (follow invitation-letter convention) |
| 41 | +- **Utilities:** `clsx`, shared `Spacer` (`src/components/shared/spacer.tsx`) |
| 42 | +- **DnD:** `react-dnd` + `react-dnd-html5-backend` (unchanged) |
| 43 | +- **Data:** Apollo Client, codegen'd hooks (unchanged) |
| 44 | + |
| 45 | +No new dependencies required — `Select` ships with `@radix-ui/themes`; chevron icons |
| 46 | +ship with `lucide-react`. |
| 47 | + |
| 48 | +## Commands |
| 49 | + |
| 50 | +Run inside the custom_admin workspace (`backend/custom_admin`): |
| 51 | + |
| 52 | +``` |
| 53 | +Build: pnpm build # astro build — primary verification gate |
| 54 | +Dev: pnpm dev # astro dev on :3002 + codegen watch + ws proxy |
| 55 | +Codegen: pnpm codegen # GraphQL types (not needed unless .graphql changes) |
| 56 | +Lint/fmt: npx @biomejs/biome check src/components/schedule-builder |
| 57 | + npx @biomejs/biome format --write src/components/schedule-builder |
| 58 | +``` |
| 59 | + |
| 60 | +There is no unit-test runner for custom_admin. Verification = clean `pnpm build` + |
| 61 | +Biome clean + manual smoke test in the running admin (via `docker-compose up`, |
| 62 | +schedule-builder page). |
| 63 | + |
| 64 | +## Project Structure |
| 65 | + |
| 66 | +``` |
| 67 | +backend/custom_admin/src/components/ |
| 68 | +├── schedule-builder/ ← migrate everything here |
| 69 | +│ ├── root.tsx already Radix (Flex, Spinner) — leave |
| 70 | +│ ├── calendar.tsx raw <h1>/<button> → Heading/Button(ghost) |
| 71 | +│ ├── item.tsx ScheduleItemCard <ul><li> → Card/Badge/Text; keep DnD+Tooltip |
| 72 | +│ ├── placeholder.tsx drop zone — keep DnD; text → Text |
| 73 | +│ ├── slot-creation.tsx already Button — tidy/group only |
| 74 | +│ ├── pending-items-basket/index.tsx emoji scroll → IconButton+lucide; Card/Text |
| 75 | +│ └── add-item-modal/ |
| 76 | +│ ├── index.tsx custom Modal → Dialog |
| 77 | +│ ├── search-event.tsx <input> → TextField; remove console.log + empty else |
| 78 | +│ ├── add-custom-event.tsx <select>→Select, <input>→TextField, list→Cards, .btn→Button |
| 79 | +│ ├── proposal-preview.tsx .btn → Button; <li>/<strong> → Card/Text/Badge |
| 80 | +│ ├── keynote-preview.tsx same as proposal-preview |
| 81 | +│ └── info-recap.tsx grid+strong/span → Text (or DataList) |
| 82 | +├── shared/ |
| 83 | +│ ├── modal.tsx DELETE (only schedule-builder imports it) |
| 84 | +│ ├── spacer.tsx reuse |
| 85 | +│ └── base.tsx <Theme> wrapper — leave |
| 86 | +└── ... |
| 87 | +src/custom-styles.css remove `.btn` rule once no longer referenced |
| 88 | +``` |
| 89 | + |
| 90 | +## Code Style |
| 91 | + |
| 92 | +Follow the invitation-letter editor (`src/components/invitation-letter-document-builder/`). |
| 93 | +Components from `@radix-ui/themes`, semantic props over Tailwind, `lucide-react` icons, |
| 94 | +compound Dialog/AlertDialog pattern. Example (the reference modal pattern from |
| 95 | +`editor-section.tsx`): |
| 96 | + |
| 97 | +```tsx |
| 98 | +import { Dialog, Button, Flex, Text, TextField } from "@radix-ui/themes"; |
| 99 | +import { Pencil } from "lucide-react"; |
| 100 | + |
| 101 | +<Dialog.Root open={isOpen} onOpenChange={(o) => !o && close()}> |
| 102 | + <Dialog.Content maxWidth="768px"> |
| 103 | + <Dialog.Title>Add event to schedule</Dialog.Title> |
| 104 | + <Flex direction="column" gap="4"> |
| 105 | + <SearchEvent /> |
| 106 | + <AddCustomEvent /> |
| 107 | + </Flex> |
| 108 | + </Dialog.Content> |
| 109 | +</Dialog.Root> |
| 110 | +``` |
| 111 | + |
| 112 | +Select pattern (replacing the native `<select>`): |
| 113 | + |
| 114 | +```tsx |
| 115 | +import { Select } from "@radix-ui/themes"; |
| 116 | + |
| 117 | +<Select.Root value={type} onValueChange={setType}> |
| 118 | + <Select.Trigger placeholder="Choose one" /> |
| 119 | + <Select.Content> |
| 120 | + <Select.Item value="talk">Talk</Select.Item> |
| 121 | + <Select.Item value="keynote">Keynote</Select.Item> |
| 122 | + {/* … */} |
| 123 | + </Select.Content> |
| 124 | +</Select.Root> |
| 125 | +``` |
| 126 | + |
| 127 | +Conventions: |
| 128 | +- `Button variant="ghost"` for icon/inline actions, `variant="soft"` / `color="gray"` |
| 129 | + for secondary, solid default for primary; `color="crimson"` for destructive. |
| 130 | +- Typography: `Heading` / `Text` instead of `<h1>`/`<strong>`/`<span>`. |
| 131 | +- Spacing: `Flex`/`Grid` props (`gap`, `p`, `mt`) and the shared `Spacer`, not Tailwind margins. |
| 132 | +- Keep Tailwind **only** where it positions grid cells or does layout Radix props can't express. |
| 133 | +- Keep existing `react-dnd` `useDrag`/`useDrop` hooks exactly; only the rendered markup changes. |
| 134 | + |
| 135 | +## Testing Strategy |
| 136 | + |
| 137 | +- **No automated tests exist** for custom_admin; do not add a test framework as part |
| 138 | + of this migration (out of scope — flag separately if desired). |
| 139 | +- **Primary gate:** `pnpm build` succeeds with no TypeScript errors. |
| 140 | +- **Lint gate:** Biome check clean on `src/components/schedule-builder`. |
| 141 | +- **Manual smoke test** in the running admin after each task (see Boundaries → Always): |
| 142 | + 1. Slot creation buttons add slots of each duration/type. |
| 143 | + 2. Drag an item from one placeholder to another slot/room → persists. |
| 144 | + 3. Drag an item to the basket → unassigns; basket scroll buttons appear & work. |
| 145 | + 4. Click an empty placeholder → Dialog opens; search returns proposals/keynotes; |
| 146 | + "Add to schedule in <lang>" creates item & closes dialog. |
| 147 | + 5. Add-custom-event: list options create items; "create by hand" with Select type |
| 148 | + + title creates an item. |
| 149 | + 6. "Edit" on a card and "Edit day in admin" open the Django admin editor modal. |
| 150 | + 7. Speaker-availability badges/tooltips still render. |
| 151 | + |
| 152 | +## Boundaries |
| 153 | + |
| 154 | +- **Always:** |
| 155 | + - Run `pnpm build` + Biome after each task; fix before moving on. |
| 156 | + - Preserve every `react-dnd` hook, GraphQL call, and prop contract. |
| 157 | + - Keep behavior identical — this is a UI swap, not a feature change. |
| 158 | + - Migrate one file (or one tightly-coupled pair) per commit. |
| 159 | +- **Ask first:** |
| 160 | + - Adding any new dependency (none expected). |
| 161 | + - Changing GraphQL `.graphql` files or codegen output. |
| 162 | + - Changing the grid-positioning math in `calendar.tsx` / `item.tsx`. |
| 163 | + - Any visible behavior change beyond appearance. |
| 164 | +- **Never:** |
| 165 | + - Touch code outside `schedule-builder/` except deleting `shared/modal.tsx` and |
| 166 | + removing the dead `.btn` rule. |
| 167 | + - Remove the availability-badge / tooltip logic. |
| 168 | + - Leave native `<input>`/`<select>`/`<button className="btn">` or the custom `Modal`. |
| 169 | + - Commit the stray `console.log` (remove it during the search-event task). |
| 170 | + |
| 171 | +## Success Criteria |
| 172 | + |
| 173 | +- [ ] `grep -rn "shared/modal\|className=\"btn\|<select\|<input " src/components/schedule-builder` |
| 174 | + returns nothing (no native controls / custom modal left). |
| 175 | +- [ ] `src/components/shared/modal.tsx` deleted; `.btn` rule removed from `custom-styles.css`. |
| 176 | +- [ ] Add-item modal renders via Radix `Dialog`; event-type via Radix `Select`. |
| 177 | +- [ ] Schedule item card, basket, previews use Radix `Card`/`Badge`/`Text`/`IconButton`. |
| 178 | +- [ ] `pnpm build` passes; Biome check clean. |
| 179 | +- [ ] All 7 manual smoke-test flows pass with unchanged behavior. |
| 180 | +- [ ] Visual style is consistent with the invitation-letter editor. |
| 181 | + |
| 182 | +## Plan (implementation order) |
| 183 | + |
| 184 | +Bottom-up: shared/leaf pieces first so parents compose cleanly, modal last. |
| 185 | + |
| 186 | +1. **info-recap.tsx** (leaf, pure presentation) → Radix `Text`/`DataList`. |
| 187 | +2. **item.tsx → ScheduleItemCard** redesign with Radix `Card`/`Badge`/`Text`/`Button`; |
| 188 | + keep DnD `useDrag`, availability badge + `Tooltip`. (`Item` grid wrapper math unchanged.) |
| 189 | +3. **proposal-preview.tsx** + **keynote-preview.tsx** → `Card`/`Badge`/`Text`, `.btn`→`Button`. |
| 190 | +4. **search-event.tsx** → `TextField`; remove `console.log` + empty `else`. |
| 191 | +5. **add-custom-event.tsx** → Radix `Select`, `TextField`, `Button`; option list → Cards/Buttons. |
| 192 | +6. **add-item-modal/index.tsx** → Radix `Dialog`; then **delete `shared/modal.tsx`**. |
| 193 | +7. **calendar.tsx** → `Heading` + ghost `Button` for "Edit day in admin"; keep grid. |
| 194 | +8. **placeholder.tsx** → `Text` for labels; keep DnD + grid positioning. |
| 195 | +9. **pending-items-basket/index.tsx** → `Card`, `IconButton` + lucide chevrons (replace |
| 196 | + 👈/👉), `Text`; keep DnD + scroll logic. |
| 197 | +10. **slot-creation.tsx** → tidy grouping (already `Button`). |
| 198 | +11. **Cleanup:** remove `.btn` from `custom-styles.css`; final `pnpm build` + Biome + full smoke test. |
| 199 | + |
| 200 | +Risk notes: |
| 201 | +- DnD drag preview uses the rendered node — verify drag still grabs the card after the |
| 202 | + `Card` redesign (item.tsx task). |
| 203 | +- Radix `Dialog` traps focus/scroll; the old modal set `body.overflow` manually — Dialog |
| 204 | + handles this, so confirm background scroll-lock still works and remove the manual logic. |
| 205 | +- Radix `Select` is portal-rendered inside the Dialog — confirm it layers above the |
| 206 | + Dialog overlay (z-index) when open. |
| 207 | + |
| 208 | +## Tasks |
| 209 | + |
| 210 | +- [ ] **T1 — info-recap** → Radix typography. |
| 211 | + - Acceptance: label/value pairs render via `Text`; no raw `<strong>`/`<span>`. |
| 212 | + - Verify: `pnpm build`; modal preview shows recap unchanged. |
| 213 | + - Files: `add-item-modal/info-recap.tsx` |
| 214 | + |
| 215 | +- [ ] **T2 — ScheduleItemCard redesign**. |
| 216 | + - Acceptance: card uses `Card`/`Badge`/`Text`/`Button`; type+duration+status+title+ |
| 217 | + speakers+TM+Edit all present; availability warning badge + `Tooltip` intact; `useDrag` unchanged. |
| 218 | + - Verify: `pnpm build`; drag still works; card readable. |
| 219 | + - Files: `item.tsx` |
| 220 | + |
| 221 | +- [ ] **T3 — proposal & keynote previews**. |
| 222 | + - Acceptance: `.btn`→`Button`; `<li>`/`<strong>`→`Card`/`Text`; availability chip→`Badge`. |
| 223 | + - Verify: `pnpm build`; search results render; add buttons work. |
| 224 | + - Files: `add-item-modal/proposal-preview.tsx`, `add-item-modal/keynote-preview.tsx` |
| 225 | + |
| 226 | +- [ ] **T4 — search-event**. |
| 227 | + - Acceptance: `<input>`→`TextField` (autofocus kept); `console.log` + empty `else` removed. |
| 228 | + - Verify: `pnpm build`; typing searches. |
| 229 | + - Files: `add-item-modal/search-event.tsx` |
| 230 | + |
| 231 | +- [ ] **T5 — add-custom-event**. |
| 232 | + - Acceptance: native `<select>`→Radix `Select`; `<input>`→`TextField`; `.btn`→`Button`; |
| 233 | + quick-option list → Radix Cards/Buttons; create-by-hand validation unchanged. |
| 234 | + - Verify: `pnpm build`; both create paths work; Select layers above Dialog. |
| 235 | + - Files: `add-item-modal/add-custom-event.tsx` |
| 236 | + |
| 237 | +- [ ] **T6 — modal → Dialog + delete custom Modal**. |
| 238 | + - Acceptance: `index.tsx` uses `Dialog.Root/Content/Title`; `shared/modal.tsx` deleted; |
| 239 | + no manual `body.overflow` code remains; open/close still driven by `useAddItemModal`. |
| 240 | + - Verify: `pnpm build`; `grep -rn shared/modal src` empty; dialog opens/closes/scroll-locks. |
| 241 | + - Files: `add-item-modal/index.tsx`, **delete** `shared/modal.tsx` |
| 242 | + |
| 243 | +- [ ] **T7 — calendar header**. |
| 244 | + - Acceptance: `<h1>`→`Heading`; "Edit day in admin" `<button>`→ ghost `Button`; grid untouched. |
| 245 | + - Verify: `pnpm build`; day header renders; edit-in-admin opens. |
| 246 | + - Files: `calendar.tsx` |
| 247 | + |
| 248 | +- [ ] **T8 — placeholder**. |
| 249 | + - Acceptance: labels via `Text`; DnD `useDrop` + grid inline styles unchanged. |
| 250 | + - Verify: `pnpm build`; drop targets + add-on-click work. |
| 251 | + - Files: `placeholder.tsx` |
| 252 | + |
| 253 | +- [ ] **T9 — pending-items basket**. |
| 254 | + - Acceptance: container→`Card`; 👈/👉→`IconButton` + lucide `ChevronLeft/Right`; |
| 255 | + labels→`Text`; DnD + scroll logic unchanged. |
| 256 | + - Verify: `pnpm build`; unassign drop + scroll buttons work. |
| 257 | + - Files: `pending-items-basket/index.tsx` |
| 258 | + |
| 259 | +- [ ] **T10 — slot-creation tidy**. |
| 260 | + - Acceptance: grouped/labeled with Radix layout; behavior identical. |
| 261 | + - Verify: `pnpm build`; each slot button creates a slot. |
| 262 | + - Files: `slot-creation.tsx` |
| 263 | + |
| 264 | +- [ ] **T11 — cleanup + final verify**. |
| 265 | + - Acceptance: `.btn` removed from `custom-styles.css`; all success-criteria greps empty. |
| 266 | + - Verify: `pnpm build` + Biome clean + full 7-flow smoke test. |
| 267 | + - Files: `src/custom-styles.css` |
| 268 | + |
| 269 | +## Open Questions |
| 270 | + |
| 271 | +- None blocking. (Note for later, not this migration: custom_admin has no automated |
| 272 | + tests — worth adding component tests for the schedule builder in a follow-up.) |
0 commit comments