mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 18:13:26 +00:00
c9f16a04ea
* feat(web): add key/label split + slugification to collection modals
Closes TASK-595 in PLAN-593.
Replace the Create modal's stripped-down row form with the shared
FieldEditor component from TASK-594 and introduce a proper key/label
split with auto-slugification, duplicate detection, and reserved-key
validation.
New UI
- FieldEditor shows a muted monospace Key input under the Label input
for new (unsaved) fields only. The key auto-syncs from the label
via slugify(). Once the user edits the key manually (keyTouched),
auto-sync stops and an inline hint explains that keys are immutable
after save.
- Inline error message + red outline on the key input when the key is
invalid or collides with another field. Create/Save buttons are
disabled (with a reason tooltip) while any field has a blocking
error.
Shared helpers in field-editor-types.ts
- slugifyKey(): lowercase, strip non-[a-z0-9_\s-], collapse whitespace
and hyphens to underscores, trim, max 40 chars.
- RESERVED_FIELD_KEYS: UI-side reserved list mirroring top-level Item
JSON fields (id, slug, ref, title, content, created_at, etc.) that
would shadow core item properties and cause confusion.
- validateFieldKey(): structural validation (non-empty, starts with a
letter, only lowercase+digits+underscore, not reserved, length).
- fieldFromDef(): hydrate an EditableField from a FieldDef preserving
the key verbatim — used when loading templates or existing fields
so slugify doesn't overwrite already-valid keys.
CreateCollectionModal
- Drops the { key, type, options: string } row form.
- Now renders fields via FieldEditor bound to EditableField[].
- Template selection marks loaded fields keyTouched=true so template
keys stay intact. Duplicate / reserved / empty keys disable Create
with a hover tooltip reason.
- Save uses field.key directly (no longer derives key from label at
submit time).
EditCollectionModal
- New fields gain the same key/label split + validation. Duplicate
detection runs against existing field keys + other new-field keys.
- hasNewFieldBlockingErrors gates the Save button with a tooltip.
- Existing-field behavior is unchanged: label editable, key frozen,
no key row rendered.
Behavior / regression notes
- User-visible outcome for a typed-and-submitted collection is now:
label "Target Date" -> key "target_date" (was "Target Date" in
pre-T2 behavior). This is the intended fix.
- Templates continue to ship with their existing keys verbatim.
- Alignment of the type dropdown against the taller new-field card
will be revisited in TASK-598 (visual redesign pass).
* fix(web): persist terminal_options on newly-created status fields
Codex P2 (PR #134): the Create modal and EditCollectionModal's new-
fields serialization paths copied key/label/type/options into FieldDef
but not terminal_options. Users could toggle terminal markings on a
status field in the FieldEditor, click Create (or Save), and have
those choices silently dropped — making terminal-dependent behavior
fall back to defaults and misclassify statuses.
Mirror the existing-fields branch that already handles this in
EditCollectionModal: when the field key is "status" and terminalOptions
is non-empty, filter to the options that survived in the saved set and
write them to def.terminal_options.
Key === "status" gating matches the current FieldEditor UI. T4
(TASK-597) will generalize terminal options to any select field and
the gate can be removed there.
Pad Web UI
SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.
Development
npm install
npm run dev # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build # Production build to build/
npm run check # Type checking with svelte-check
When developing, run the Go backend separately with make dev from the project root.
Building for Production
Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.
Stack
- Svelte 5 with runes (
$state,$derived,$effect) - SvelteKit 2 with
adapter-static(SPA mode) - Tiptap block editor with markdown round-trip
- svelte-dnd-action for drag-and-drop in board/list views
- SSE for real-time updates
- TypeScript throughout
Structure
src/
routes/ SvelteKit pages
+layout.svelte App shell (sidebar + main)
+page.svelte Landing/redirect
[workspace]/
+page.svelte Dashboard (collections, phases, activity)
+layout.svelte SSE connection per workspace
[collection]/
+page.svelte Collection view (board/list)
[collection]/[item]/
+page.svelte Item detail + editor
conventions/ Purpose-built conventions page
playbooks/ Purpose-built playbooks page
settings/ Workspace settings
lib/
api/client.ts HTTP API client
components/
layout/ Sidebar, navigation
editor/ Tiptap editor, raw markdown editor
fields/ FieldEditor, relation picker
items/ ItemCard, ItemDetail
collections/ BoardView, ListView
common/ StatusBadge, badges, modals
search/ CommandPalette
activity/ ActivityFeed
stores/ Svelte 5 reactive stores
workspace.svelte.ts Workspace state
collections.svelte.ts Collection + item state
ui.svelte.ts Sidebar, mobile state
types/index.ts TypeScript types and constants
app.css Global styles and design tokens