Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -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 `<input>` 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<string | null>;
|
||||
export async function listAssets(mediaType?: 'image' | 'video' | 'any'): Promise<Asset[]>;
|
||||
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.
|
||||
Reference in New Issue
Block a user