diff --git a/docs/superpowers/specs/2026-07-12-site-builder-asset-picker-design.md b/docs/superpowers/specs/2026-07-12-site-builder-asset-picker-design.md new file mode 100644 index 0000000..35748e9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-site-builder-asset-picker-design.md @@ -0,0 +1,149 @@ +# Site Builder — unified image/asset picker (2026-07-12) + +Wherever an image is entered by URL in the Craft.js site builder +(`/workspace/site-builder/craft/`), the user should also be able to **pick an +image already uploaded to the site builder** (and upload a new one). Today this +"Browse uploaded assets" affordance exists but is copy-pasted into ~6 components +and missing entirely from ~7 other image fields. Scope decisions were made with +the user before writing this spec. + +## Problem + +The "select an uploaded image" capability (a **Browse** button opening an inline +thumbnail grid backed by `?action=list_assets`) is implemented — six independent +times: + +- `craft/src/components/media/ImageBlock.tsx` +- `craft/src/components/basic/Logo.tsx` +- `craft/src/components/basic/Navbar.tsx` (logo) +- `craft/src/panels/right/styles/ImageStylePanel.tsx` +- `craft/src/panels/right/styles/HeroStylePanel.tsx` (`AssetBrowser` sub-component) +- `craft/src/components/media/VideoBlock.tsx` (video assets) + +…and is **absent** (plain URL `` only) from: + +- `craft/src/panels/right/styles/BackgroundSectionStylePanel.tsx` (`bgImage`) +- `craft/src/panels/right/styles/MediaStylePanel.tsx` (two image fields) +- `craft/src/panels/right/styles/FeaturesEditor.tsx` (per-feature image — has + upload + URL, no browse) +- `craft/src/components/sections/FeaturesGrid.tsx` (per-feature image, settings) +- `craft/src/components/sections/Gallery.tsx` (per-image `src`) +- `craft/src/components/sections/ContentSlider.tsx` (per-slide `imageSrc`) +- `craft/src/components/layout/Container.tsx` (background image URL) + +Same capability, inconsistent presence — a UX gap for users and 6 divergent +copies to maintain. + +## Solution — one reusable component, two presentation variants + +### New: `craft/src/ui/AssetPicker.tsx` + +Single source of truth for choosing an image (or, later, video). Lives in +`src/ui/` — the existing neutral home for reusable settings controls +(`BorderControl`, `SpacingInput`, `AnchorIdField`, …), importable by BOTH +`src/components/**` and `src/panels/**`. (No component currently imports from +`panels/right/styles`, so the shared piece must not live there.) + +It composes the three affordances that already exist today: +1. **Upload** a new file (drag-drop zone + button) +2. **Browse** uploaded site assets (inline thumbnail grid) +3. **Paste a URL** (escape hatch for external images) + +```ts +interface AssetPickerProps { + value: string; // current url ('' = none) + onChange: (url: string) => void; + mediaType?: 'image' | 'video' | 'any'; // default 'image' — filters grid + file accept + variant?: 'full' | 'compact'; // default 'full' + placeholder?: string; // URL input placeholder +} +``` + +- **`variant="full"`** — the rich layout: preview thumbnail with remove-✕, dashed + drag-drop zone when empty, Upload + Browse buttons, inline grid, URL apply row. + Mirrors today's `ImageStylePanel` layout. Consumers: **ImageStylePanel, Logo + settings, Navbar logo, BackgroundSectionStylePanel, MediaStylePanel, Hero bg + image, Container bg image**. +- **`variant="compact"`** — one tight row (small thumbnail + Upload + Browse + + inline URL input; grid expands below on Browse) that fits inside array-editor + item cards. Consumers: **FeaturesEditor, FeaturesGrid settings, Gallery rows, + ContentSlider slides**. + +Both variants share ALL logic (upload, browse fetch + grid, URL state, WHP/ +standalone gating); only the markup differs, selected by `variant`. + +### Supporting change: `craft/src/utils/assets.ts` (de-duplicate) + +`uploadToWhp` is duplicated in at least `panels/right/styles/shared.tsx`, +`panels/right/styles/FeaturesEditor.tsx`, and inline in several components; every +Browse re-implements the `list_assets` fetch + `type` filter. Add a neutral util: + +```ts +// src/utils/assets.ts +export async function uploadAsset(file: File): Promise; +export async function listAssets(mediaType?: 'image' | 'video' | 'any'): Promise; +export interface Asset { name: string; url: string; type: string; } +``` + +- `uploadAsset` = the existing `uploadToWhp` body (POST `upload_asset`, blob + fallback when no `WHP_CONFIG`). +- `listAssets` = fetch `list_assets`, return `[]` when no `WHP_CONFIG`, filter by + `mediaType` (`type` startsWith `image`/`video`; `any` = no filter). + +`AssetPicker` uses both. `shared.tsx` keeps re-exporting `uploadToWhp` (thin +wrapper delegating to `uploadAsset`) so nothing else breaks; local copies in +components are removed as each is migrated to `AssetPicker`. + +## Behavior details + +- **Standalone mode** (no `window.WHP_CONFIG`): Browse is hidden (no assets API + to call); Upload still works via `blob:` fallback. Matches today's behavior. +- **Video-ready**: `mediaType` drives both the grid filter and the file-input + `accept`. Image fields are wired now with the default `'image'`. VideoBlock / + Hero bg-video may adopt the same component with `mediaType="video"` — done in + this work if cheap, otherwise a clean follow-up. +- **No `toHtml` changes.** This is purely settings-panel input UX; published HTML + is byte-for-byte unaffected, so all existing `*.toHtml.test.ts` stay green. +- **Prop wiring unchanged.** Each consumer keeps writing to its own prop + (`src`, `imageSrc`, `logoImage`, `bgImage`, per-item `image`/`imageSrc`) — only + the input UI is swapped. `AssetPicker` is controlled via `value`/`onChange`. + +## Migration order (each independently verifiable) + +1. `utils/assets.ts` + `AssetPicker.tsx` + unit test (no consumers yet). +2. Migrate the "full" panel fields: ImageStylePanel, MediaStylePanel, + BackgroundSectionStylePanel, HeroStylePanel (image), Container bg. +3. Migrate the "full" component settings: Logo, Navbar logo. +4. Migrate ImageBlock to the shared component. +5. Migrate the "compact" array editors: FeaturesEditor, FeaturesGrid, Gallery, + ContentSlider. +6. Remove now-dead duplicated upload/browse state and local `uploadToWhp` copies. + +## Testing + +- New `craft/src/ui/AssetPicker.test.tsx`: + - renders the current `value` (thumbnail/URL shown), + - `onChange` fires with the asset URL on grid-select and on URL "Apply", + - grid requests + shows only assets matching `mediaType`, + - Browse hidden when `window.WHP_CONFIG` is absent + (mock `fetch` + toggle `window.WHP_CONFIG`). +- New `craft/src/utils/assets.test.ts`: `uploadAsset` blob fallback + POST shape; + `listAssets` filter + empty-without-config. +- Regression: full `cd craft && npx vitest run` and `npm run build` (tsc) green. + +## Out of scope / follow-ups + +- Rewriting the asset grid UX itself (search, pagination, folders) — this reuses + the current grid look, just makes it universal. +- Migrating VideoBlock/Hero video pickers is optional in this pass (component is + built video-ready; wire if cheap, else follow-up). +- A separate, parallel Fable-agent audit of the whole site builder may surface + additional improvements; those are tracked independently, not in this spec. + +## Deploy + +Ships inside the WHP release, per the standard site-builder pipeline +(build craft bundle → copy `dist/{index.html,js,css}` into +`whp/web-files/site-builder/` → `build-release.sh` → `download-update.sh` across +the fleet). Handled at ship time via the `whp-deploy-release` skill, after +implementation and review.