Commit Graph
265 Commits
Author SHA1 Message Date
shadowdao 74df8a3b70 Merge issue-34 (Refs #34) 2026-10-07 11:41:22 -07:00
shadowdao d756a723de Merge issue-43 (Refs #43)
# Conflicts:
#	craft/src/editor/CanvasOverlays.tsx
2026-10-07 11:41:22 -07:00
shadowdao 817028df89 Merge issue-41 (Refs #41) 2026-10-07 11:40:54 -07:00
shadowdao 4fbe109bac Merge issue-37 (Refs #37) 2026-10-07 11:40:54 -07:00
shadowdaoandClaude Sonnet 5.5 fad34f3e71 feat(site-builder): actionable empty canvas and inline add for empty containers
Refs #41

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:39:34 -07:00
shadowdao 22c59eb970 feat(site-builder): tablet/mobile canvas emulates published hide, stacking and full-width
Frame gets data-device; RenderNode mirrors hide flags and selection onto the
dom; editor.css emulates the exported breakpoints inside the narrowed frame.
Also: data-wsb-overridden counts only set values, responsive scope ids are
zone-prefixed (h-/b-/f-) so ROOT no longer collides, and comment delimiters
are stripped from responsive declarations.

Refs #37
2026-10-07 11:39:25 -07:00
shadowdaoandClaude Sonnet 5.5 af460f23e6 feat(site-builder): precise drop indicator with target glow and empty-container label
Restyle Craft's insertion bar via the Editor indicator prop and mirror the
live placement onto the DOM (data-drop-target/-error/-empty, is-dragging).

Refs #43

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:38:22 -07:00
shadowdaoandClaude Sonnet 5.5 2a6055d863 feat(site-builder): selection breadcrumb, Esc selects parent, Tab cycles siblings
Refs #34

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:36:40 -07:00
shadowdao ce3db23506 Merge W0-tokens-defaults (Refs #45 #37 #44) 2026-10-07 11:33:02 -07:00
shadowdao 8216248107 Merge W0-breakpoints (Refs #45 #37 #44) 2026-10-07 11:33:02 -07:00
shadowdaoandClaude Sonnet 5.5 705c602a67 test: flush #40 debounce in Container padding seed test (still one undo step)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:31:51 -07:00
shadowdao 136e3ce6ea fix(site-builder): restore closing brace lost in #36/#38 editor.css merge 2026-10-07 11:31:10 -07:00
shadowdao 1f5cac2cca Merge issue-38 (Refs #38)
# Conflicts:
#	craft/src/styles/editor.css
2026-10-07 11:30:20 -07:00
shadowdao 536b6c6b8c Merge issue-36 (Refs #36) 2026-10-07 11:30:16 -07:00
shadowdao bee052833c Merge issue-35 (Refs #35) 2026-10-07 11:30:16 -07:00
shadowdao 5db1879c63 Merge issue-40 (Refs #40) 2026-10-07 11:30:16 -07:00
shadowdao ec071c4c39 feat(site-builder): debounce style inputs and stop Layers recomputing on hover
Text/number/hex inputs keep local state and commit on 150ms idle, blur or
Enter through the continuous setters; native colour pickers get an
onContinuous path; ButtonStylePanel colour routes through the shared
setters. LayersPanel collectors return structural signatures/primitives
so hover and unrelated prop edits no longer rebuild the tree.

Refs #40
2026-10-07 11:28:47 -07:00
shadowdaoandClaude Opus 5.5 5f99df23a7 feat(site-builder): per-breakpoint style model, device context and responsive export
- utils/breakpoints.ts: single source for 767/768/991/992; head
  VISIBILITY/RESPONSIVE CSS built from it (flex-stacking rule moved from
  768 to the 767 mobile boundary).
- utils/responsive-style.ts: styleTablet/styleMobile partial overrides,
  cascade resolution, pure setters, scoped @media CSS per node.
- state/DeviceContext.tsx: real DeviceProvider (owns the one-time mobile
  forcing from EditorShell); TopBar, overflow menu, ReportIssueModal and
  Canvas read useDevice() instead of props.
- editor/RenderNode.tsx: resolves the effective style for the current
  device in one place and marks overridden nodes with data-wsb-overridden.
  Craft passes an opaque <DefaultRender/> to onRender, so the component
  element is rebuilt with the resolved style (cloneElement cannot work).
- utils/html-export.ts: hidden nodes skipped; overrides emitted as one
  body-level <style data-wsb-responsive> block + data-wsb-r scope attr;
  injectAttrs skips leading <style> elements.
- test-utils/editorHarness.tsx: optional onRender/wrap options.

Refs #45
Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:28:44 -07:00
shadowdaoandClaude Sonnet 5.5 4a11a9e344 feat(site-builder): open Spacing/Colours by default, remember collapse state, single container padding control
CollapsibleSection persists open/closed per section title via local-prefs.
Container/Section/Columns drop the shorthand padding preset grid; the
per-side Padding control expands a legacy shorthand and writes all four
longhands in one undo step (new SpacingControl onChangeAll).

Refs #35

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:25:30 -07:00
shadowdao e5119174e7 feat(site-builder): Element/Site tabs in the right panel
Refs #36
2026-10-07 11:25:28 -07:00
shadowdaoandClaude Sonnet 5.5 5006089a4a test(site-builder): real-editor test that new blocks serialize token defaults
Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:24:52 -07:00
shadowdaoandClaude Sonnet 5.5 045643d570 feat(site-builder): Section background and dividers default to bg token
Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:24:52 -07:00
shadowdaoandClaude Sonnet 5.5 488f208262 feat(site-builder): section blocks default to palette tokens
Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:24:52 -07:00
shadowdaoandClaude Sonnet 5.5 af43af7dc1 feat(site-builder): form blocks default to palette tokens
Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:24:52 -07:00
shadowdaoandClaude Sonnet 5.5 d70f4933af feat(site-builder): basic blocks default to palette tokens
Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:24:52 -07:00
shadowdao 7b082c8d2e feat(site-builder): searchable blocks panel, remembered collapse state and left tab
Refs #38
2026-10-07 11:23:00 -07:00
shadowdao 2180f9a776 Merge W0-registry-notices (Refs #38 #41 #49 #48 #57 #58)
# Conflicts:
#	craft/src/state/NoticeContext.tsx
#	craft/src/styles/editor.css
2026-10-07 11:20:30 -07:00
shadowdao ae871c9cdc Merge W0-commands (Refs #39 #49 #34)
# Conflicts:
#	craft/src/styles/editor.css
2026-10-07 11:20:25 -07:00
shadowdao 42700f911f Merge W0-tokens-core (Refs #44) 2026-10-07 11:20:21 -07:00
shadowdaoandClaude Opus 5.5 f8ac1fbe42 feat(site-builder): token module, canvas token variables, palette swatch row
Make var(--wsb-*) a first-class style value in the editor:

- utils/tokens.ts: TokenKey, TOKEN_VARS (moved from html-export.ts, same
  order/content as the backend), COLOR_TOKENS palette order, tokenRef,
  isTokenRef, tokenKeyOf, resolveTokenValue and buildCanvasTokenCss, which
  defines the 16 vars + token base CSS scoped to .canvas-device-frame
  (never :root) with every value through cssValue().
- editor/CanvasTokenStyle.tsx: <style data-wsb-canvas-tokens> driven by
  useSiteDesign(), mounted in App inside SiteDesignProvider, so design
  edits recolour token-backed values on the canvas instantly.
- panels/right/styles/TokenSwatchRow.tsx: palette-first swatch row that
  emits token references; .token-swatch styles appended to editor.css.
- html-export.ts imports TOKEN_VARS; export output pinned byte-identical
  to the pre-move baseline by hash in html-export.test.ts.

Refs #44

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:16:29 -07:00
shadowdaoandClaude Sonnet 5.5 73aa35ac96 feat(site-builder): command registry, shortcut matcher, tree walkers and top-bar command menu
Library only; nothing is mounted. Adds minimal DeviceContext/NoticeContext
stubs (owned by W0-breakpoints / W0-registry-notices) that expose the
agreed interfaces.

Refs #39 #49 #34

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:14:52 -07:00
shadowdaoandClaude Sonnet 5.5 21c2776c88 feat(site-builder): block registry, notices, local prefs, project extras, overlay slot
Add an exported block registry (keywords, search, insertBlock) consumed by
BlocksPanel, a NoticeProvider/NoticeHost toast stack, a guarded localStorage
prefs helper, a ProjectExtras store persisted as project.json 'extras', and a
CanvasOverlays mount point.

Refs #38
Refs #41
Refs #49
Refs #48
Refs #57
Refs #58

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 11:14:51 -07:00
shadowdaoandClaude Opus 5.5 97ccbee461 fix(site-builder): serialise all saves; honest pill for conflicts and new sites
A manual Save during Publish sent a second save with the same base_rev; the
loser 409'd against its own tab (and could abort the publish).

- useWhpApi: every save runs through one in-flight promise (SaveGuard
  saveInFlight). A save requested while one is out waits, then sends only
  if still dirty, with the rev the first returned; payload is read from the
  editor at send time. Forced (empty/overwrite) saves always send.
- Save buttons are disabled while publishing.
- Pill: a conflict reads 'Unsaved changes' (modal open) from Save and from
  Publish alike; a never-saved site (load rev null) reads 'Not saved yet'.
- Autosave interval reads runSave through a ref, so editing can't keep
  restarting it and starve autosave.
- Fix stale 'Throws like exportBodyHtml' comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:35:11 -07:00
shadowdaoandClaude Opus 5.5 572e736f1f fix(site-builder): Undo can no longer cross the load or a page switch
Craft records actions.deserialize as an undoable step and nothing cleared
history, so Undo right after load returned the canvas to the blank pre-load
Frame (autosave would then save it), and Undo after a page switch put the
previous page's content into the current slot.

- clear history after the load-time deserialize and after every page/
  header/footer switch (in-place AI replacement of the same slot keeps it)
- continuousSetProp opens a burst only when its first call actually
  changed something, so it never merges into an entry it did not create
- the empty-save banner's Undo is disabled when there is nothing to undo

Real-editor tests: load, page/header switch, no-op-first burst, banner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:32:15 -07:00
shadowdao eb134a4743 Merge polish/save-pipeline: load gate, honest save state, idle autosave, conflict handling (Refs #28 #30 #31 #33) 2026-10-07 10:21:51 -07:00
shadowdaoandClaude Sonnet 5.5 34c932fc67 fix(site-builder): disarm Keep mine on empty-banner Undo; autosaveDue honours conflict
Refs #31

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:20:45 -07:00
shadowdaoandClaude Sonnet 5.5 a6db5438b2 feat(site-builder): detect two-tab save conflicts; Reload latest / Keep mine modal
Refs #31

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:19:07 -07:00
shadowdaoandClaude Sonnet 5.5 a20c9bae28 fix(site-builder): don't cache swallowed export failures; load-bearing autosave gate test
Refs #33

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:15:24 -07:00
shadowdaoandClaude Sonnet 5.5 c863767283 fix(site-builder): autosave only when dirty; memoise per-state page export
Refs #33

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:12:31 -07:00
shadowdaoandClaude Sonnet 5.5 86b82ed154 fix(site-builder): publish racing an edit stays a draft; no autosave during publish
Refs #30

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:09:26 -07:00
shadowdaoandClaude Sonnet 5.5 9418444916 fix(site-builder): honest save state (dirty tracking, status pill, beforeunload, error notice)
Track dirty from Craft node changes and page/design/header/footer state, cleared
only by a successful save. Persistent Unsaved/Saving/Saved/Published/Save failed
pill, beforeunload prompt while dirty or saving, one persistent error notice,
resp.ok/non-JSON handling for save and publish, and publish refuses to run
after a failed save. Draft badge no longer flips on a no-op autosave.

Refs #30

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:07:03 -07:00
shadowdao 8537cacd01 Merge polish/undo-merging: one undo step per edit (Refs #32) 2026-10-07 10:01:59 -07:00
shadowdaoandClaude Sonnet 5.5 2c85e33b38 fix(site-builder): continuous-edit bursts via history.merge; plain setters are single steps
Refs #32

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 10:00:29 -07:00
shadowdao a406ff8548 fix(site-builder): fail load on unreadable page state; disable editing controls until loaded
Refs #28
2026-10-07 10:00:11 -07:00
shadowdao 66e669fa01 fix(site-builder): never save before a successful load; guard empty overwrites
Refs #28
2026-10-07 09:57:11 -07:00
shadowdaoandClaude Sonnet 5.5 ac490215ce fix(site-builder): one undo step per edit (throttle continuous inputs, single setProp for multi-prop toggles)
Refs #32

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 09:56:12 -07:00
shadowdaoandClaude Opus 5 9ab01021be chore: pin the shared-memory project key
Ties every collaborator's Claude Code to the same shared-memory project
automatically, instead of each machine inferring a key from the directory
name. Read by project.identify at session start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:09:04 -07:00
jknapp a684d51996 Merge PR #27: webhook destination controls for contact forms
Adds the webhook destination option to the contact form. Legacy markers stay byte-identical; the raw secret never reaches a Craft prop.

Pairs with the whp-side branch of the same name.
2026-08-11 00:42:02 +00:00
shadowdaoandClaude Opus 5 422697acec fix(site-builder): review findings on the webhook destination controls
I-1: WebhookSecretField now mounts with key={selectedId}. GuidedStyles renders
FormStylePanel with no key, so a selection change re-rendered rather than
remounted it and React kept `draft`/`status`. Since a failed store deliberately
retains the draft, clicking a second contact form showed node A's raw secret in
node B's field, and the next blur assigned the returned secret_id to the wrong
form (burning a slot against the per-site cap). The status banner leaked the
same way. Both pinned by the reviewer's repro sequence.

I-2: the button is "Clear", not "Remove", and says so -- nothing deletes a
stored key file, so the 50-per-site cap counts stores-ever. Labelling it Remove
told the customer they had reclaimed a slot right up until the 429 that said
otherwise. Actually deleting the file is Task 8's territory.

M-1: inline warning when the webhook URL is blank or not absolute https. The
publish step does refuse these, but into an error_log the customer never reads.
Warning only -- isHttpsWebhookUrl never edits the value or blocks the publish,
since either would trade a loud server-side refusal for a silently inert form.

M-2: the BYTE-IDENTITY test now compares against a literal marker captured by
executing the emitter at 071f3447, not against another head-revision output. The
self-comparison could only catch a drift affecting one side; a uniform one
passed it. Verified: a uniform `<!-- WHP-FORM` drift now fails this assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:57:38 -07:00
shadowdaoandClaude Opus 5 d1c57db967 feat(site-builder): webhook destination controls on the contact form
ContactForm gains four craft props (destinationType, webhookUrl,
webhookSecretId, webhookAuthMode), all present in craft.props defaults so
FormStylePanel's `nodeProps.X !== undefined` gates actually render their
controls. The controls live in FormStylePanel (RightPanel renders only
GuidedStyles, so related.settings would be dead UI).

relayFormWiring widens the marker to optionally carry type/url/secret/
authmode BETWEEN `id` and `recipient`, which is where FormRelayRewrite.php's
parser looks. A marker with no type is byte-identical to what shipped before
-- pinned by a test that diffs an explicit-email form against one with no
destination props at all, since every already-published site depends on that
shape continuing to provision an email endpoint.

Every optional attribute value goes through one escaping site (markerAttr ->
escapeAttr); type and authmode are additionally allowlisted, so a case-drifted
"Bearer" reaches the relay as the exact literal it compares against instead of
being silently downgraded to unsigned.

The raw shared secret is never a prop: it is held in WebhookSecretField's
local state, POSTed to /api/form-webhook-secret.php on blur, and only the
returned opaque id is persisted. The field is write-only (set / replace /
remove, never view) because the endpoint has no read route, and the endpoint's
429 cap message is surfaced verbatim so a customer can act on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:45:05 -07:00