Skip to content

Commit af31ee8

Browse files
marcoaciernoclaude
andcommitted
Migrate custom_admin Schedule Builder to Radix UI
Replace the schedule builder's ad-hoc UI with @radix-ui/themes, matching the invitation-letter document builder pattern. - Custom div Modal -> Radix Dialog (guard content on data so the close animation never dereferences null after the data is cleared) - Native <input>/<select>/<button> -> TextField / Select / Button - Raw <h1>/<strong>/<ul><li> markup -> Heading / Text / Card / Badge / DataList - Pending-items basket emoji scroll controls -> IconButton + lucide chevrons - Remove now-unused shared/modal.tsx and the .btn utility class - Drop a stray console.log and an empty else in search-event CSS-grid calendar layout, react-dnd drag-and-drop, GraphQL, and the speaker availability badges/tooltips are unchanged. Spec: backend/custom_admin/SPEC-schedule-builder-radix.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5d8f9c3 commit af31ee8

14 files changed

Lines changed: 538 additions & 292 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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

Comments
 (0)