43 Commits
Author SHA1 Message Date
Oleg Isonen c389cd3bb2 fix: classify errors with structured codes and types (#6539)
Invalid or revoked share links now return an actionable 401 response
instead of an unexpected server error. Missing project and workspace
records receive distinct responses at their lookup sites. Token,
project, and workspace lookups use `.maybeSingle()` and rely on database
uniqueness. Missing projects have an explicit `ProjectNotFoundError`;
the loader no longer interprets `PGRST116`.

The API preserves `UNAUTHORIZED` and `INVALID_AUTH_TOKEN` when
authorization fails during context creation, so CLI and MCP clients can
offer relinking guidance without inspecting error text. Tokens without
API access carry `API_ACCESS_DISABLED`.

A repository audit replaces error-message matching with typed errors,
stable codes, or structured diagnostics in CLI argument/configuration
handling, session conflicts, preview cleanup, import retries, domain
unpublishing, asset validation, JSX/expression diagnostics, CSS
validation, and publish diagnostics. `AGENTS.md` now explicitly
prohibits message-based error classification and recovery.

The existing build-permission preflight now advertises structured
import-error support. Clients upload assets before importing into older
servers that do not advertise it; newer servers keep selective retries
using structured missing-file details. The internal template-validation
error class remains outside public JSX bindings and input descriptions.

Shared Builder `parseError` normalizes tRPC and local validation errors,
so UI components only add display context instead of decoding transport
payloads.

Simplification removes three redundant error classes, catch-and-ignore
handlers, an unused formatter, and nested loader error handling.
Unpublishing an already unpublished domain succeeds after authorization.
CLI configuration failures use the existing shared error handling, and
plan parsing catches only JSON parsing failures.

Validation: affected suites passed (271 tests across 8 suites in the
latest simplification checks), affected package typechecks passed, and
lint, package-boundary checks, and `git diff --check` passed. The audit
replaced synthetic parser branches and private-method assertions with
actual tRPC round trips, PostgREST responses, and public VM behavior.
Controlled regressions confirmed that removing serialized validation
details and exposing the internal JSX error class both fail the revised
tests. Astra reviewed the production changes at medium effort; its
compatibility finding and the GitHub JSX-binding comment are fixed, and
follow-up review found no remaining substantive issues.
2026-10-16 12:14:57 +02:00
Oleg Isonen 33f7b350bc fix: Improve Form setup and email settings guidance (#6536)
## User-facing features and how to test them

- Project Settings → Emails → Reply to has an info tooltip explaining
how its display name is used in visitor confirmation subjects and
messages, and when “Site Owner” is shown. Test it by hovering or
focusing the info icon.
- When a Form has no resources available for its Action, clicking the
Action + button explains that a resource must first be created in
Variables. Test it with a Form that has no action-capable resources,
then create a resource and verify it can be added to the Action.
- Success redirect is shown inside the Action control only when Resource
actions are connected. Test by switching between navigation mode and a
Form with a Resource action; the redirect field should appear only for
Resource actions, and its saved value should return when a Resource
action is added again.
- The Action control no longer offers Disable/Enable. Connected Resource
actions run in their configured order. The Form Action model has no
`enabled` field; older saved JSON with that extra key is read as an
ordinary connected action.

## Verification

- Focused UI test for the Reply to tooltip.
- Focused UI test for the empty Action resource tooltip.
- Manually verify Success redirect visibility and value retention while
switching modes.
- Manually verify all connected Actions execute in order, including
older saved Actions carrying an ignored `enabled` key.


## Review follow-up

- Address all current open code review comments on this PR, verify the
fixes, and reply to each addressed thread before resolving it.

## CI repair follow-up

- Inspect the latest failing checks on this PR, fix the causes on the
existing branch, and verify the affected checks locally and in CI.

## Follow-up audit (2026-10-14)

- Audited head `8fb0b44`, all inline review threads and the PR
conversation. The earlier Reply to tooltip comment was answered and
resolved. Two P1 comments remained: required boolean `enabled`
validation in CLI evaluations, and execution of saved disabled actions.
- `builder-e2e (shard-6)` failed because Preview returned `Disabled`
between `First` and `Last` in the configured failure results test. The
failure matches the security review finding. `builder-e2e (shard-2)` was
still in progress at audit start; the staging deployment workflow was
cancelled.
- The follow-up filters saved disabled actions from execution and
restores boolean `enabled` validation in both CLI evaluations. Builder
labels saved disabled actions. Focused SDK (55), CLI (222), and Builder
(45) tests, plus all three affected package typechecks, passed locally.
- Pushed `7c2b90e082`; replied to both P1 review threads, then resolved
them. Its main CI run passed all 25 test/check jobs, including all six
Builder E2E shards and the previously failing shard 6.
- A later P2 review identified stale CLI/MCP guidance. Commit
`160a9d1707` corrects the source and generated docs; focused docs tests
(12) and CLI typecheck pass locally. Replied to and resolved that
thread. On this latest head, the main CI run completed successfully: 27
check jobs passed, including all six Builder E2E shards; one
visual-regression job was skipped. All three required checks passed. The
automatically triggered staging deployment workflow was cancelled under
the no-deployment instruction.


## Input type default follow-up

- Show `text` as selected in the Props panel for both the HTML `input`
element and the Input component when their `type` prop is unset,
matching their rendered default. Verify both variants in the Builder and
retain explicit type choices.


## Current review-thread follow-up (2026-10-14)

- Recheck every unresolved review thread against the current PR head,
make and validate any required fixes, and reply with the resolution
before closing each thread. Keep this work on the existing PR branch; do
not deploy.


## Input type metadata follow-up (2026-10-14)

- Move the unset input `type` display default into component/element
property metadata for both the Input component and native HTML `input`
element. Remove the Props-panel special case, keep the implicit default
out of saved props, and cover both variants with regression tests.
- Run focused checks, push the fix to this PR, and cancel any
automatically triggered staging deployment workflow. Do not deploy
manually.


## Props metadata selector simplification follow-up (2026-10-15)

- Refactor `$selectedInstancePropsMetas` after the input default
metadata change. Preserve the current metadata, ordering, and attribute
override behavior while simplifying the selector's structure and
abstraction boundaries.
- Run focused checks, commit and push to this PR branch. Cancel any
automatically triggered staging deployment; do not deploy manually.

## Latest review follow-up (2026-10-15)

- Audit the latest PR head, all unresolved inline review threads, and
general PR comments. Address actionable feedback on this branch, run
focused validation, then reply and resolve each addressed thread. Cancel
any automatically triggered staging deployment; do not deploy manually.


## Form Action model decision (2026-10-15)

- Remove the Form Action `enabled` field completely from the type,
serialization, validation, execution, fixtures, and current docs. Accept
older saved JSON containing that extra key by ignoring it, including
`enabled: false`; action presence and valid IDs determine execution.
- Update the CLI evaluation to assert intended action IDs and order
without using `enabled`. Address the latest P2 review finding against
this model. Run focused checks, push on this PR branch, and cancel any
automatic staging deployment. No manual deployment.


## Action control visual follow-up (2026-10-15)

- Match the Form Action plus button to the established Variables section
plus button style and implementation where appropriate. Keep the
existing icon.
- Give the Action resource dropdown a stable width comparable to the
Variables resource dropdown, including when available resource names are
short.
- Add focused UI coverage, validate locally, commit and push to this PR
branch, and cancel any automatic staging deployment. Do not deploy
manually.


## Action plus hover follow-up (2026-10-15)

- Match the Action `+` button's hover and focus styling exactly to the
plus buttons in panels such as Variables. Compare their actual
implementations, preserve the existing icon, and add focused UI coverage
where appropriate.
- Run focused checks, commit and push to this PR branch. Cancel and
confirm any automatically triggered staging workflow; do not deploy
manually.


## Current CI fix (2026-10-15)

- Inspect the latest check rollup and failing job logs on the current PR
head. Fix all current failures on this branch, run focused local
validation, push the changes, and monitor the new head until checks
complete.
- Cancel and confirm any automatically triggered staging deployment. Do
not deploy manually.
2026-10-15 17:25:22 +02:00
Oleg Isonen 59d4f00ce7 feat: new Form component, all features and improvements you dreamed of (#6485)
## User-facing features and how to test them

In Components → Forms, insert **Form (Beta)**. The existing Webhook Form
remains a separate component.

- **Insert a ready-to-use contact Form.** In Components → Forms, insert
**Form (Beta)**. It starts with accessible Name, Email, Subject,
Message, and optional Attachment fields; sends submitted field values
and uploaded files to the account holder; sends a customizable
confirmation to the visitor without attaching the submitted file; and
includes success and error feedback. The two Actions are named **Email
to owner** and **Email to visitor**. *(#3023, #5499)*
- **Use the contact Subject in both email subjects.** Owner and visitor
subjects bind the submitted Subject input; the visitor subject also
identifies the sender. Template and SDK tests pass.
- **Format submitted Form data readably in Email bindings.**
`formData.toString()` now renders labeled lines, nested values, and
uploaded filenames in any Email Resource expression, while excluding
passwords and internal Form fields. SDK and independent-bundle tests
pass. Published staging delivery is verified in Batch 7.
- **Choose Form actions.** Select the Form and open Properties &
Attributes → Action. Add up to ten visible HTTP, GraphQL, or Email
Resources; reorder, disable, or remove them. Submit and confirm enabled
actions run together. *(#5499)*
- **Send submitted fields and files.** Submit repeated fields and a file
input to an HTTP Resource. Check the received multipart fields, order,
filename, type, and bytes. Email Resources attach submitted files by
default; turn off **Attach submitted files** to omit them. *(#3023,
#5432)*
- **Make the visitor confirmation friendly and signed.** Inherited
content gets the approved friendly message and resolved sender name;
custom text remains intact. Email Request preview and delivery use the
same resolver. SDK and Builder preview tests cover defaults, custom
overrides, blank content, sender identity, and preview parity. Suggested
body:

  > Thank you for your message.
  >
  > We received it and will respond as soon as possible.
  >
  > Webstudio Team
- **Identify the sender in the visitor confirmation.** The resolved
sender name appears in the visitor subject and body. Focused tests pass.
- **Include source page context in the owner email.**
`browserInfo.pageUrl` is included in the owner message and the Email
Request preview. Focused SDK and Builder tests pass.
- **Use Form and visitor data.** In a Resource defined on the Form, bind
`formData` and `browserInfo` in its URL, headers, or body. Local MCP
Preview tests verify submitted fields, browser, and language. *(#5428)*
- **Use page and query values.** On a dynamic page with query
parameters, bind `system.params` and `system.search` in a Form Resource,
then navigate and submit. A query key with one value is a string;
repeated values are an ordered array. Confirm the current route values
are available after navigation. Text bindings render repeated values as
comma-joined strings. *(#5512, #3891)*
- **Use Navigation mode for search and filters.** Remove every Resource
from the new Form’s Action list. Its action becomes
`?__webstudio__=navigate`, and submitting named fields updates the URL
query and page Resources without a full page reload. Reconnect a
Resource to return to Resource submission mode
(`?__webstudio__=resources`). A plain Element with tag `form` keeps
native browser behavior.
- **Customize notification email.** In Project Settings → Emails, set
recipients, sender, and Reply to. In each Email Resource, choose project
recipients, custom addresses, or a visitor email input, and configure
its Reply to, subject, body, and attachment setting. Verify a
per-Resource override does not change another Form’s settings. *(#4414,
#4385, #5658)*
- **Choose Resource types from one ordered list.** In Variables → Type,
find REST resource, GraphQL resource, Assets resource, Email resource,
Sitemap resource, and Current date resource in that order. Confirm the
labels and order in the Type dropdown.
- **Inspect Resource requests and responses.** Open Variables → a
Resource → Edit variable. Use **Load data** on Request when no request
is shown; inspect HTTP request, response, and diagnostics. For Email,
load the request preview and confirm it does not send an email. After
submission, inspect saved HTTP and GraphQL exchanges without triggering
another request. *(#4093 related; new Form Resource inspector)*
- **Explain the Email Response before a submission.** Refresh builds the
local request preview without sending an email; the Response tab tells
users to submit the Form in Preview to see the Email Service response.
Focused inspector test passes.
- **Use action results in the Form.** Bind `formState`, `results`, and
`errors` in the success and error content. Submit successful and failing
actions and verify each result or error includes its action’s response
details in configured order. *(#5499)*
- **See submission feedback.** Submit a Form whose success or error
message is outside the viewport and confirm that message scrolls into
view. Repeat with feedback already visible, reduced motion, partial
failure, and a configured success redirect; visible feedback should not
move, and redirect should happen without scrolling first. *(#6487)*
- **Redirect after success.** Set Success redirect and submit both
successful and failing Forms. Only success follows the redirect.
*(#3530)*
- **Keep separate email threads.** Retries preserve a submission
reference, while separate submissions appear in separate Gmail
conversations; Batch 7 verifies both. *(#5018)*
- **Keep existing Webhook Forms.** Insert the existing Webhook Form and
verify its configured behavior remains intact. For its documented
query-binding fixes, verify route params and repeated search values
after navigation. *(#5512, #3891; #4093 and #5432 remain related legacy
issues)*

- **Keep native Form props out of the new Form editor:** The new Form
metadata hides generated rendered-tag HTML props such as `method`,
`encType`, `target`, and ARIA attributes, while its own Action, Success
redirect, and visibility controls remain. The legacy Webhook Form
behavior is unchanged. A Builder property-selector regression test
verifies this.

- **Allow up to 10 Form Actions:** The shared validator and Action
picker now allow up to 10 Resource actions, and the Form control
description reflects the limit. SDK tests accept 10 and reject 11; the
generated CLI prebuild rejection coverage passes (130 tests). SDK and
SDK Components typechecks, formatting, and diff checks pass.

- **Make the default Form ready to use without edits:** The inserted
contact Form has required Name, Email, Subject, and Message fields; its
project Email Action resolves the current project contact address or
account holder dynamically, with no personal data in the template; its
visitor Email Action has a confirmation subject and message; success and
error states give actionable feedback. `core-templates.test.tsx`
verifies fields, routing, and copy; focused registry tests and typecheck
pass.

- **Make the default Form usable as a contact form:** Its Email Actions
are named “Email to owner” and “Email to visitor.” The owner body binds
the complete submitted `formData` object rather than manually listing
fields, and an optional Attachment input sends its file only to the
owner. Email Resource expressions render `formData.toString()` as
readable labeled lines. The visitor receives a confirmation without
submitted files. Registry/template tests and MCP E2E verify the inserted
Form, owner message and attachment, visitor message, and Request
preview.

- **Format Form action errors clearly:** The default Form shows each
action error as a spaced bullet inside a red-tinted alert panel.
Registry template tests verify the alert and list structure; 8 template
tests pass.
- **Deprecate the existing Webhook Form before its next-PR removal:**
The component metadata and compatibility exports in the React, React
Router, and Remix packages are marked deprecated, and the guide says
removal is planned. Keep form submission helpers still used by
NativeForm active; remove legacy-only wrappers and template in the
follow-up PR.

## MCP feature parity test plan

Run these cases through Webstudio MCP on a disposable project. Create
and configure the Form with MCP, save and read back the configuration,
then test its generated Preview. Use only a controlled test mailbox for
real email delivery. This plan mirrors the user-facing UI cases above.

- Discover **Form (Beta)**, inspect its schema, insert the default
contact Form, and read back its fields, Actions, success/error content,
and absence of personal data.
- Change the submitted Subject and Name. Verify owner and visitor
subjects, visitor sender identity, friendly confirmation text, and the
owner page URL.
- Bind the complete `formData` object. Verify readable lines, nested
values, filenames, and exclusion of password/internal fields.
- Add HTTP, GraphQL, and Email Actions; reorder, disable, and remove
them. Verify concurrent dispatch, result order, partial failure, and the
10/11 Action limit.
- Submit repeated fields and one or multiple files to HTTP. Verify
multipart order, filename, MIME type, and exact bytes. Verify Email
attachments default on, then turn them off for the visitor Action and
confirm the visitor receives no file.
- Configure project email settings and Resource overrides. Save/reload
each recipient mode, Sender, Reply to, subject, body, and attachment
setting; verify overrides do not leak between Forms.
- Bind `formData` and `browserInfo` from a Form-scoped Resource. Verify
page URL, browser, language, and hosting-provided IP/referrer where the
test environment supplies them.
- Test dynamic route params and single/repeated query values before and
after navigation. Verify Navigation mode updates the URL and bound page
Resources without a full reload; reconnect an Action and verify Resource
mode. Check native `form` Element behavior separately.
- Verify Resource discovery order and method labels, then exercise
HTTP/GraphQL Request, Response, Diagnostics, load/refresh, and saved
exchange inspection. For Email, verify request preview sends nothing and
the Response appears after actual submission.
- Bind `formState`, `results`, and `errors`. Submit success, validation
failure, partial Action failure, and full failure. Verify feedback
content, visible/off-screen scrolling, reduced motion, and success-only
redirect behavior.
- Submit twice and verify separate email threads and stable retry
identity, with no duplicate delivery after an ambiguous send.
- Verify the existing Webhook Form remains editable and its route/query
behavior remains intact, while new Form discovery/creation and native
Form prop exposure match the documented component contract.

### Execution status and MCP limitation

The `mcp-*` suite authors through a current-source MCP server and
submits generated Preview Forms through `preview.submit-form`.
Playwright covers Builder UI, navigation/rendering, and viewport
assertions. The separate `forms.simulate-submission` operation submits
synthetic values from the saved canvas Form directly to the shared SDK
server Form engine, without starting Preview or a browser. Both paths
use the same saved-draft preparation and SDK submission handler. Most
automated email tests use a loopback receiver. The controlled
branch-source MCP test below separately verifies delivery to a real test
mailbox; it does not test the user’s connected MCP release.

On 2026-10-14, the branch-source MCP was tested against a running local
Builder and a copied disposable project. No npm package publish or
connected-MCP release was needed for this local verification.

- [x] Add a bounded `preview.submit-form` MCP operation for
session-owned generated Preview Forms. It uses native form controls and
validation, supports repeated values and bounded file uploads, returns
state/results/errors/exchanges, and never replays an ambiguous send. The
MCP-client E2E verifies one owner Email delivery and one HTTP delivery
with ordered results against local receivers. Focused Vision, Project
Build, CLI, and Builder E2E tests pass.
- [x] Migrate browser Preview Form runtime cases to
`preview.submit-form`; retain Playwright for Builder UI,
navigation/rendering, and viewport assertions. The 52 MCP-named Builder
E2E cases passed across the full 2-worker run (50/52) and a focused
rerun of its two timed-out Email cases (2/2), including MCP submission
and local receiver assertions.
- [x] Add `forms.simulate-submission` to the current-source CLI MCP
session. It reads the saved canvas Form, calls the Builder server route,
and reuses the Preview Form engine and SDK submission handler. The older
local CLI adapter rejected Email; the current server route supports it.
Static Checkbox and multiple Select array names come from saved
controls; dynamic array names may be supplied explicitly. Focused tests
and independent Astra Medium review pass.
- [x] Run the Luna Low `managed-form-runtime-v1` scenario against
`forms.simulate-submission` through a real MCP client on the current
source. `gpt-5.6-luna` at low reasoning passed 10/10 checks in 55
seconds. Three simulation calls produced success, an explicit 503
failure, and recovery against the controlled local HTTP receiver in the
expected sequence. The agent used no Preview/browser submission or Email
action. One `templates.get` MCP error was recovered before submission.
- [x] Verify `forms.simulate-submission` from branch-source MCP against
the local Builder server and a disposable saved Form. The supported
local database was started, and `EMAIL_SERVICE_URL` plus
`TRPC_SERVER_API_TOKEN` were supplied from the staging Infisical
environment without printing values. Luna Low used MCP discovery,
Form/resource inspection, `update-resource`, and simulation. Both Email
Resources resolved only to `testing@webstudio.is`; simulation returned
success, HTTP 200, two Email results with status 200, and no errors.
Gmail independently confirmed both owner and visitor messages delivered
to that controlled inbox at 15:46:51 UTC on 2026-10-14. The shared
server route and CLI transport received independent Astra Medium review;
focused CLI (164) and HTTP-client (45) tests, package typechecks,
formatting, and diff checks pass. No npm release or production
deployment was involved.

- [x] Rename the new Form component label to **Form (Beta)** in the
component picker and MCP discovery. Component IDs stay unchanged;
focused registry, Builder, MCP, and CLI tests pass; Astra Medium
reviewed the change. Pushed `592589ccfe`.

## MCP end-to-end verification

### Verified against this branch

These checks use the current-source MCP session server to author
disposable local Builder projects and `preview.submit-form` to submit
generated-site Forms to local test receivers. Playwright verifies
Builder UI, navigation/rendering, and viewport behavior. They verify
this branch's code, not the connected MCP release, staging services, or
real mailbox delivery.

Current branch rechecks (2026-10-14): Project Build MCP tests 142/142,
CLI MCP/evaluation harness tests 60/60, Vision CDP/Form tests 48/48, and
the React Router Navigation browser test 4/4 pass. Builder, CLI, Project
Build, Vision, and both React Router fixture typechecks pass; scoped
lint, formatting, and diff checks pass. The earlier head passed PR CI;
checks on commit `7438692ef5` are running. The separate full CLI
prebuild suite now passes 131/131 after adding the required `statusText`
to its private Email binding fixture. The main-merge CI failures in
Project Build expression expectations, CLI Preview config import,
formatting, and the SSG fixture size cap were fixed in `0272768260`;
checks on that commit are pending.

- [x] Verify all 52 MCP-named Builder E2E cases on the current head. The
complete 2-worker run passed 50/52; two complex Email cases exceeded
Playwright's 120-second per-test limit under concurrency, then both
passed in a focused one-worker rerun (44.4s and 31.0s; 2/2). A serial
full rerun reached 43/52 without assertion failures before the wrapper's
900-second command limit. An earlier full run passed 52/52 before the
final Preview readiness fix. Coverage includes contact Forms, Email
delivery and partial updates, attachments, readable `formData`, HTTP and
GraphQL Actions, navigation and repeated query values, legacy Form
editing, Resource inspectors, Form properties and type choices,
feedback, and redirects. Full-run command:
`E2E_TEST_COMMAND_TIMEOUT_SECONDS=1800 E2E_SKIP_BUILDER_BUILD=true
E2E_INSTALL_PLAYWRIGHT=false pnpm --filter @webstudio-is/builder e2e --
'mcp-' --project=chromium --workers=2`.
- [x] Run the Builder Preview Form E2E suite serially: 5/5 passed,
including authenticated Builder Preview submission, error rendering,
redirects, submitted browser `FormData`, off-screen feedback scrolling,
and Navigation mode without a Resource POST or canvas reload.
- [x] Verify the local Email inspector server path. The route
integration suite calls the real local `/rest/preview-form` handler and
passes a controlled Email Service binding response through it; 28/28
tests pass. The Email Service boundary is stubbed, so this does not
claim a staging Worker run.
- [x] Verify the Form Action picker browser tests: 2/2 pass for add,
disable, reorder, remove, the ten-Action limit, and rejection of an
eleventh Action. The MCP runtime E2E separately verified ten deliveries
and ordered results.
- [x] Verify local Preview request context across two pages: scoped
`formData`, `browserInfo.pageUrl`, trusted IP, user-agent, and language
all remain associated with the correct Form Resource.
- [x] Verify partial Email body updates through MCP. Custom and
explicitly blank bodies preserve the other recipient, sender, and
attachment settings; local Preview uses the requested body. Focused E2E
passes 2/2.
- [x] Verify the default Form template has no hardcoded email address,
sender, or recipient, and the error Collection renders each message as a
separated bullet in a red-tinted alert. Registry tests pass 11/11.
- [x] Verify the Action picker UI adds, disables, reorders, and removes
HTTP, GraphQL, and Email Resources; accepts 10 and blocks 11. Component
browser tests pass 2/2. Runtime MCP E2Es separately verify Action order
and concurrent dispatch; the picker test does not claim ten live
deliveries.
- [x] Verify the Luna Low managed-Form authoring evaluation: 20/20
checks pass. Validator tests pass 83/83.
- [x] Verify the legacy Webhook Form is excluded from new MCP
creation/copy paths while saved instances remain editable. Source tests
and the existing 139 MCP checks pass; this does not mean the published
MCP catalog has been refreshed.
- [x] Formatting, `git diff --check`, and Builder, CLI, and SDK
Components Registry typechecks pass.

## Runtime correctness

- [x] Prevent automatic retry of an Email Action when the Email Service
send may already have succeeded; report an ambiguous response without
sending the same email again, and verify one Form submission cannot
produce duplicate delivery. Pushed `07627e44fa`: Email Actions never
automatically replay; HTTP retries remain. RPC and Preview accept legacy
`{id}` and canonical replies, including successful canonical replies
with an empty provider ID. Regression tests cover one dispatch after
failures, timeouts, and malformed replies, response compatibility, and
HTTP retry. All 143 focused SDK tests, SDK typecheck, scoped lint, diff
checks, and independent Astra review pass.

- [x] Keep ambiguous Email Action outcomes non-retriable in Builder so a
possibly successful send is never replayed; preserve compatibility with
both deployed legacy `{id}` and canonical `{ok, status, statusText,
data}` Worker replies. Pushed `578a1a99`: canonical delivery IDs must be
nonblank, while legacy replies remain supported. SDK regressions cover
one dispatch after ambiguous RPC/Preview failures, timeouts, malformed
replies, and empty IDs.

## New Form navigation mode and MCP E2E (2026-10-11)

- [x] Give the new Form two submission modes using its valid action URL:
`?__webstudio__=resources` when one or more Resources are connected and
`?__webstudio__=navigate` when none are connected. Removing the last
Resource switches to Navigation mode. Keep a plain `form` Element native
and leave the legacy Webhook Form available but marked deprecated until
the next release. Runtime tests cover both action markers; the
MCP-authored browser E2E verifies navigation query updates and Resource
revalidation. Legacy Webhook Form metadata, compatibility exports,
component code, and docs now state its deprecation and support window.
- [x] In the new Form’s Action control, when no Resources are connected,
show centered “Navigation mode” with an info tooltip: “Submits the form
fields as URL query parameters and navigates without a full page reload.
Use this for search and filters that update page Resources from the
URL.”
- [x] Add MCP-authored end-to-end coverage for both modes and their
distinctions. Luna/low authored Resource and Navigation Forms plus a
native `form` Element through MCP, with separate registry searches for
Form (Beta), Webhook Form, and Element; the final project passes the
corrected structured evaluator. The MCP-authored Playwright E2E removes
all Form Actions and verifies repeated and single query values update
the URL and page Resource without a document reload or Resource POST.
Existing local MCP Preview coverage verifies Resource submission, native
GET behavior, legacy Webhook Form compatibility, and component
discovery.

## Ports from the earlier implementation

- [x] Resource selection and scoped `formData`/`browserInfo` access,
parallel dispatch, protected egress, server destination limits, and
bot-field validation are already in the new Form.
- [x] Resource editor URL autofocus and known header/value suggestions
are already reused.
- [x] Centralize the `formData` and `browserInfo` parameter names for
Builder, generator, and runtime checks.
- [x] Default HTTP body forwarding and automatic multipart for uploads
are now ported to the new Resource graph.
- [x] Success/error state slots and success redirect are now ported into
the new Form and Router/Remix adapters.
- [x] Add response `results`/`errors` bindings and one retry per failed
or timed-out destination.
- [x] Port feedback scrolling to the new Form in Router and Remix
adapters, with browser regression coverage.
- [x] Adapt the existing bot-field protection for the new Form; saved
Webhook Forms keep their prior bot-field behavior. The narrowly scoped
route/query fixes for #5512 and #3891 are tracked separately below.
- [x] Local new-Form bot checks: one fresh hidden value per submission,
five-minute expiration, malformed/missing rejection before dispatch,
Brave handling, and stripping internal form ID/bot fields before
Resource delivery. Evidence:
`packages/sdk/src/managed-form-submission.ts`,
`managed-form-submission.test.ts`,
`packages/sdk-components-react/src/form-submission.browser.test.ts`, and
managed generated-route cases in `packages/cli/src/prebuild.test.ts`.

## Batch 1 — New Form foundation

- [x] Make the existing Forms-section **Form** tile insert the new Form
component while retaining the existing Webhook Form tile and behavior.
- [x] Make the new Form a managed Resource submission component. With no
destination, it shows a configuration error and cannot dispatch or
navigate. Use Element with `tag=form` for ordinary browser form
behavior.
- [x] Exclude unrelated legacy-only runtime work from this feature.
Existing component metadata is deprecated; the new Form has no legacy
compatibility or migration contract. The targeted #5512/#3891
route/query fixes are tracked separately in Batch 2a.
- [x] Verify the Resource-only component with focused render tests,
package typecheck, and component story generation.
- [x] Verify the Batch 1 new Form acceptance criteria in the Builder and
browser.

### Acceptance criteria

- Inserting the new Form creates the default contact Form with named
inputs, a submit button, and two selected Email Resource Actions: one
for project recipients and one for the visitor email field. If all
destinations are removed, it shows a configuration error and cannot
dispatch or navigate until a destination is configured.
- Element with `tag=form` retains browser validation, method, encoding,
action, and navigation.
- Acceptance covers the new Form only. Apart from the explicitly tracked
#5512/#3891 route/query binding fixes in Batch 2a, legacy compatibility,
old native-mode settings, and old-component migration are outside the
feature contract.

**Verification:** Builder catalog and insertion tests; Chromium
empty-Resource blocking test; generated Element `tag=form` Playwright
test with a local receiver
(`packages/sdk-components-react/src/element-form.test.tsx`,
`40c6f972c8`, focused 1/1) proves invalid input sends nothing and valid
native multipart POST navigates using the configured action, method, and
encoding; generated published output for the new Form; relevant
typechecks and lint.

The user confirmed the test inbox received the test email attachment.
Staging Builder Preview verified Email and Webhook delivery. The user
confirmed the published staging route is live; the route returns HTTP
200 and includes a file input. Published delivery to both Webhooks and
final Form-state behavior are verified in Batch 7 below. Live Production
Form submissions are outside this PR’s verification scope.

## Batch 2 — Resource-only submission, scoped data, and Resource
selection

- [x] Give the new Form one submission path with one to ten selected
Resource destinations. Keep Actions and Success redirect as its Form
controls; plain HTML forms use Element with `tag=form`. An empty
selection reports an error without dispatch or navigation; tests cover
this path.
- [x] Expose one submission-only `formData` object, keyed by HTML name.
Repeated names are ordered lists; a known unchecked checkbox group is an
empty list. Preserve files and hidden inputs.
- [x] Expose safe `browserInfo`: trusted IP, user agent, language, and
referrer. Never expose raw request headers, cookies, or authorization.
- [x] Make explicit Resource expression bindings to a Form's `formData`
and `browserInfo` available only to Resources defined within that Form;
a selected HTTP Action with an unset or empty body implicitly receives
submitted Form data regardless of Resource scope.
- [x] Resource variables are created in **Variables** at their normal
scope. The Actions selector lists only action-capable Resource variables
visible in the Form's variable scope: defined on the Form or one of its
ancestors. Exclude sibling, descendant, and unrelated scopes. Resources
defined on the Form can bind its `formData` and `browserInfo`; ancestor
Resources cannot bind values from a descendant Form. Carry forward URL
autofocus and known header/value autocomplete. Enforce up to ten Actions
per Form in the Builder and server.
- [x] Keep **Actions** as a control inside **Properties & Attributes**,
with a plus menu and selected Resource rows. The plus menu offers only
existing action-capable HTTP, Email, and GraphQL Resource variables
visible in the Form's current variable scope (defined on the Form or an
ancestor); exclude System data variables and resources from sibling,
descendant, or unrelated scopes. Resource variables are created in
**Variables** at their normal scope; Actions only selects existing
variables. Disable already-selected Resources in the menu. Selected rows
expose enable, disable, and remove controls. A simple click on the
Action name opens the existing Resource editor through selection/open
state, leaves its Variables row visibly selected, and scrolls it into
view, while preserving page and canvas selection; drag, enable/disable,
and remove controls must not open it. The eye toggles enabled state; the
minus removes the Action from this Form only. Preserve explicit
selection, ordering, and the ten-Action limit. Published-site submission
checks are recorded in Batch 7.
- [x] Cover scope, expressions, repeated names, renames, files, copying,
deletion, save/reload, and unavailable bindings with focused tests.
- [x] Ensure `mapResourceExpressionsMutable` remaps Email
`senderExpression` and `recipientsExpression` data-source IDs;
`fragment.test.tsx` verifies copy/paste/fragment rebinding (focused
regression 1/1, full file 34/34). Project-build typecheck and `git diff
--check` pass.
- [x] Add focused fragment coverage for Email `senderExpression` and
`recipientsExpression` bound to variables inside a copied Form,
asserting references remap to the cloned variable IDs (not only
restoration of external parent bindings by name). `fragment.test.tsx`
passes 34/34 and combined project-build data+fragment tests pass
165/165; assertions verify both expressions remap to cloned variable IDs
distinct from the source IDs.
- [x] Implement and locally test Batch 2a current-page parameters and
repeated query values in managed and legacy Form Resource bindings
(#5512, #3891).
- [x] Add generated published-route loader integration coverage for a
native GET Form with repeated query keys; repeated `system.search`
values reach an expression-bound Resource in both `defaults` and
`react-router` templates. Focused prebuild tests pass 2/2.
- **Issue scope:** #5512 is fixed for legacy generated HTML Form actions
and the new Form path; generated-route tests cover both route templates.
#3891 exposes repeated values as ordered arrays through `system.search`
while preserving strings for single values; SDK, Builder Preview,
generated action, and generated native-GET loader regressions cover the
behavior.
- [x] Local #5512/#3891 implementation and regression: the new Form's
generated action uses current `system.params` and `system.search`, where
repeated values are ordered arrays; SDK search tests and the Builder
native GET preview test preserve repeated values
(`packages/cli/src/prebuild.test.ts`,
`packages/sdk/src/system-search.test.ts`,
`apps/builder/app/canvas/interceptor.test.ts`).
- [x] Locally verify #5428: bind trusted `browserInfo` values into
`X-Forwarded-For`, `User-Agent`, and `Accept-Language` Resource headers,
inspect the generated action’s outbound request, and document the setup
and trusted client-IP source for each hosting adapter without forwarding
cookies or authorization.
- [x] Local generated-route verification (`e9d5209882`): Remix and React
Router send the three configured headers to a test destination;
Cloudflare context uses `CF-Connecting-IP`, Node omits unavailable IP,
and arbitrary forwarding, cookie, and authorization headers are not
forwarded. Setup and hosting sources are documented.

### Form property inventory

- [x] Expose only the Actions control and **Success redirect** as Form
controls. The generic HTML attribute picker and native `id`, `class`,
`action`, `method`, `target`, and `encType` controls are unavailable for
this component.
- [x] Rename the Form control label from **Actions** to singular
**Action**.

Staging verification for the Form property panel is recorded in Batch 7.

### Acceptance criteria

- Action and Success redirect are the only Form controls. The component
label `Form` is system metadata and is not editable as an HTML
attribute; the Form has no `id`, `class`, `action`, `method`, `target`,
or `encType` controls.

- **Action** is a control inside **Properties & Attributes**. Its plus
menu offers only existing action-capable HTTP, Email, and GraphQL
Resource variables visible in the Form's current variable scope (defined
on the Form or one of its ancestors). It excludes System data variables
and Resources in sibling, descendant, or unrelated scopes. Resource
variables are created in **Variables** at their normal scope; Actions
only selects existing variables. Already-selected Resources are disabled
in the menu. Selected Resources appear as rows with enable, disable, and
remove controls, and can be reordered. Clicking an Action name opens the
corresponding Resource’s existing Edit variable dialog and highlights
its Variables row by setting the existing selection/open state directly,
without synthetic clicks; preserve the current page and canvas
selection. Drag, enable/disable, and remove controls do not open the
editor. The ten-Action limit is enforced. Resources defined on the Form
can bind that Form's `formData` and `browserInfo`; ancestor Resources
are visible but cannot bind values from the descendant Form. An empty
selection shows no canvas warning before submit; attempting to submit
shows the configuration error, dispatches nothing, and does not
navigate. The Form has no native or direct-URL submission mode.
- The new Form supports Resource Actions only; do not add a
legacy/native-mode compatibility path or migration requirement.
- A Resource defined on the Form can bind that Form's `formData` and
safe `browserInfo`; a visible ancestor Resource cannot access values
from its descendant Form. Resource variables are created in Variables at
their normal scope. The plus menu lists only existing action-capable
Resources from the Form and ancestor scopes, excluding sibling,
descendant, unrelated scopes, and System variables. Forms explicitly
select each action; none are auto-added. Already-selected Resources are
disabled in the menu. Bindings survive editing and save/reload.
- On a `/products/:slug` page visited as
`/products/chair?source=newsletter`, a submitted Resource can use
`system.params.slug` as `chair` and `system.search.source` as
`newsletter` in its URL, headers, or body. Values come from the current
submission's page, including after navigation; missing parameters remain
absent.
- Repeated names retain form order, unchecked checkbox groups resolve to
empty lists, and files stay file values. The Resource editor focuses URL
and suggests known header names and values.

## Batch 2a — Keep page parameters and every selected query value
(#5512, #3891)

New Form Resource bindings must receive the current page's parameters,
and a multiple-select GET form must not lose selections when a binding
reads the URL.

- [x] Extend #5512 route/query propagation to legacy native Form
actions: generated routes pass route params and ordered query values
while preserving the managed Form path. The regenerated runtime contract
and 26 fixture routes are pushed in `8c4bda7791`; `check:generated-api`,
fixture builds, and generated-route regressions pass.
- [x] Implement route/query propagation in both managed and legacy Form
Actions: populate `system.params` from the current route, preserve
single-value `system.search` strings and expose repeated query values as
ordered arrays through the same binding. Generated action and loader
regressions cover both templates; the consolidated CLI prebuild suite
and focused native-GET loader tests pass.

- [x] Verify #5512 with the new Form's Resource bindings using
`system.params` and `system.search`. Cover URL, header, and body
bindings, dynamic pages, missing values, and submission after navigation
in React Router and Remix.
- [x] Reproduce #3891 with a native GET form and a multiple-select
input. Keep the browser's standard repeated query keys; do not join
values with commas or rewrite the submitted URL.
- [x] Use `URLSearchParams` to preserve every value in order, including
empty values, repeated identical values, and values containing commas or
encoded characters. Keep an object-based binding interface so property
autocomplete still works.
- [x] Inspect existing `system.search` consumers and record the
compatibility choice here. Keep existing single-value bindings working,
expose all repeated values to bindings, and make any changed behavior
for existing repeated-key bindings explicit. Do not silently discard
values or break existing expressions.
- [x] Apply the agreed representation consistently in the Builder,
published page data, client navigation, and new Form action path. Update
affected types, binding suggestions, generated code, and fixtures.
- [x] Add passing SDK, Builder, and generated-route regressions for
current page parameters and repeated query values in new Form bindings;
document single-value and ordered multi-value query bindings with
working examples.
- [x] Local new-Form generated-route regressions cover current page
parameters and repeated query values in URL, headers, and body after
navigation. A red-before/green-after test proves configured body
bindings survive alongside submitted fields; CLI binding examples
document single and repeated query values.
- [x] Record regression coverage for #5512/#3891 on new Form bindings:
generated-route and Builder tests cover current-page parameters and
ordered repeated, empty, comma, and encoded query values, with SDK
coverage for scalar compatibility.

**Compatibility choice:** `system.search[name]` is a string when the
query key occurs once and an ordered string array when it occurs more
than once. Existing single-value expressions are unchanged; text
bindings stringify arrays as comma-separated values. The relevant
consumers in Builder system/preview and generated page/action templates
were reviewed. Focused SDK, Builder, and generated-route tests cover the
behavior.

### Acceptance criteria

- On a `/products/chair?source=newsletter` page, a new Form Resource can
bind `system.params.slug` as `chair` and `system.search.source` as
`newsletter` after navigation. Existing Webhook Form behavior is
unchanged except for the explicitly tracked #5512/#3891 route/query
binding fixes in Batch 2a.
- Selecting two options in a native GET form leaves both values in the
URL and makes both available to bindings in their original order.
Single-value access still works, and property autocomplete remains
available.
- Empty values, identical repeated values, commas, encoded characters,
reloads, and client navigation do not drop or corrupt query values.
- Verify current-page and repeated-query bindings for the new Form and
ordinary Element `tag=form` browser behavior. Legacy Webhook Form
changes are limited to the explicitly tracked #5512/#3891 route/query
fixes in Batch 2a; other legacy compatibility is not a closure
condition.

## Batch 3 — HTTP Resource submission

- [x] POST to each selected HTTP Resource. A selected Action whose
Resource body is unset or empty implicitly receives all submitted fields
regardless of Resource scope; an edited body can omit, rename, add, or
compute fields. Explicit `formData` expression bindings remain limited
to Resources defined within that Form.
- [x] Use JSON for text-only data and switch to multipart when the
resolved body contains a file. Set JSON content type unless configured
otherwise; preserve repeated fields and file bytes, and remove stale
`Content-Type` so fetch writes the multipart boundary.
- [x] Add an explicit `auto`/JSON/multipart choice. Reject an
incompatible body before that destination's fetch; dependency-free
requests are preflighted before any destination dispatch.
- [x] Resolve bindings and validate final outbound URLs and redirects
before sending. Block internal and private destinations; keep
credentials and sensitive request configuration off the public page.
- [x] Add generated-site and server tests for JSON, multipart, mapping,
files, repeated fields, invalid configuration, and destination safety.
- [x] New Form route proof: compiled Remix and React Router managed
actions forward repeated same-name text values and uploaded files as
separate multipart entries, preserving order, duplicate values,
filenames, MIME types, and bytes (`3c5b72a`; focused CLI tests passed
2/2). Existing Webhook Form behavior remains at its pre-PR baseline;
migration is out of scope.
- [x] Make file inputs easy to configure in the Builder: choose the file
input type and set its field name, required state, accepted file types
(`accept`), and multiple-file selection (`multiple`). Keep these
settings after saving, reopening, and publishing. Verified on
`d7f67307f9` with Builder insert/save/reopen, generated page, Chromium
required/optional/multiple upload, SDK template, typecheck, lint, and
format checks.

### Acceptance criteria

- Every selected HTTP destination receives POST. A Resource with an
unset or empty body receives all Form data implicitly, regardless of
where the Resource is defined. Explicit `formData` expression bindings
remain limited to Resources defined within that Form. Text-only bodies
arrive as JSON; bodies with uploads arrive as multipart with field
names, filenames, MIME types, file bytes, and repeated values.
- A site creator can add and configure a file input using the Builder
without custom code. Published forms support one or multiple selected
files, block submission when a required upload is empty, and submit
normally when an optional upload is empty. The `accept` setting guides
the file picker; it is not server-side file validation.
- An edited body can rename, omit, add, or compute fields. A Resource
outside the Form sends only its own configured data.
- An incompatible explicit body format fails before its destination
fetch. Dependency-free requests are validated before any dispatch; a
body that depends on another Resource is validated once that dependency
resolves, before its own destination fetch. Resolved destinations and
each redirect reject private or internal addresses, and secrets stay off
the public page.

## Batch 4 — Project Emails settings and Resource editor

- [x] Add Project Settings → **Emails**. Move the current Contact email
list without changing its stored value.
- [x] Configure site-wide defaults for Sender (name and email, or email
alone), recipients, owner subject, and plain-text owner body. Published
sites receive project default changes after republishing.
- [x] Add the Email Resource as a normal Resource, creatable inside or
outside a Form. It can inherit, override, and reset project defaults.
Only a Form-scoped Email Resource can bind that Form's data. Surface an
unavailable Form-only binding clearly; never send incomplete template
text silently.
- [x] Resolve team recipients from the project list or owner fallback,
support a custom comma-separated override, and default Sender to the
owner with a per-Resource override. Form data cannot choose team
recipients.
- [x] Configure the subject, plain-text owner body, and whether
submitted files should be attached. With no project body or Resource
body, any selected non-Visitor Email Resource uses readable submitted
fields and optional `browserInfo` interpolation, regardless of Resource
scope. Automatic owner-email text includes hidden fields while excluding
passwords and internal bot fields.
- [x] Test project settings storage, inheritance/override/reset,
recipient fallback, Form scope, expression validation, and preservation
of the existing Contact email setting. A complete project owner body
containing `${...}`, backslashes, or backticks renders as literal text;
an empty project body retains the automatic `formData` and `browserInfo`
interpolations.

### Acceptance criteria

- Existing Contact recipients appear in Project Settings → Emails
without data loss. Existing legacy forms retain their pre-PR behavior
except for the explicitly tracked #5512/#3891 current-page route and
repeated-query bindings.
- New Email Resources inherit published project defaults until
overridden; reset restores inheritance, and settings edits affect live
sites only after republishing.
- Team recipients use the project list or fall back to the owner. A
custom comma-separated recipient list and Sender override work;
submitted values cannot choose recipients or From.
- With no project owner body or Resource body, a selected non-Visitor
Email Resource uses readable submitted-field and `browserInfo` content
regardless of whether it is defined on the Form, an ancestor, or
globally. Explicit expressions can bind Form-only values only from
Resources defined within that Form. A nonempty project body is the
complete literal owner message, inherited until a Resource body
expression overrides it; reset restores inheritance. Automatic text
includes hidden form fields but excludes passwords and internal bot
fields.

## Batch 4a — Sender and recipient email format (#4414, #5658)

Use one **Sender** field that accepts either `Acme Support
<support@example.com>` or `support@example.com`, using the same address
format as recipients. The optional name becomes the displayed sender
name, and the entered email receives replies. Webstudio's authenticated
From address stays fixed.

- [x] Add the Sender field in Project Settings → Emails, with
inheritance, override, and reset in each Email Resource. Default it to
the project owner's email. This single field supplies the display name
and Reply-To; do not add separate sender-name and Reply-To inputs.
- [x] Parse Sender as exactly one mailbox and retain its optional name
and reply address; a plain email has no custom display name.
- [x] Use the site owner profile username as the default visible From
name when Sender has no explicit name; explicit Sender names take
precedence, and missing usernames fall back to “Site Owner”. Keep the
authenticated From address fixed.
- [x] Accept plain and named mailboxes in the Contact email list,
project recipients, and Email Resource recipient overrides.
- [x] Verify existing project address-count limits remain enforced
through both Builder and server updates; the separate five-team-delivery
runtime cap is tracked in Batch 5.
- [x] Use `Acme Support <support@example.com>` as the Sender placeholder
and `Acme Support <support@example.com>, team@example.com` in email-list
inputs.
- [x] Use an established email-address parser in Builder and server
validation. Handle quoted names containing commas, such as `"Example,
Alexs" <support@example.com>`, reject malformed addresses and
header-injection input, and count parsed recipient mailboxes including
duplicates for the runtime delivery cap.
- [x] Preserve parsed display names in generated Email Resource requests
and keep existing plain-address settings readable.
- [x] Test both Sender formats, mixed recipient parsing,
inheritance/reset, generated request values, and the five-team-delivery
runtime cap.
- [x] Local Builder E2E verifies Project Emails Sender and named
recipients save to persisted project settings and remain after reopening
(`b9ebf21fee`;
`apps/builder/e2e/tests/project-settings-runtime.[shard-2].[shard-5].[shard-6].e2e.ts`;
focused run 2/2).
- [x] Document the Sender/recipient input format, reply behavior,
inheritance, republishing, visitor Email Resource delivery, and the
current Cloud-unavailable delivery status without claiming that saving
settings sends email.
- [x] Local generated-graph verification (`1730840feb`): a translated
project body is the entire literal request body, a Resource body
expression overrides it, reset restores inheritance, and an empty
project body retains automatic `formData`/`browserInfo` text. Project
Emails help and docs explain plain-text behavior.

### Acceptance criteria

- Entering `Acme Support <support@example.com>` in Sender displays “Acme
Support” as the sender and directs replies to `support@example.com`,
while From uses Webstudio's sending address.
- Entering only `support@example.com` works without a separate name
field and directs replies to that address.
- With a plain Sender address, the visible From name uses the site owner
profile username, or “Site Owner” if unavailable; a named Sender
overrides it. The From address remains Webstudio’s fixed sending
address.
- Plain and named recipients can be mixed in a list; a quoted comma
inside a name does not create another recipient.
- Names and addresses survive saving, publishing, and delivery, and
existing plain-address settings continue working.
- The project owner confirmed that the email customization already
planned in Batches 4 and 4a is sufficient to close #4414 and #5658.
Sender supplies the display name and reply address; Webstudio remains
the authenticated sender. Do not add custom From addresses,
sending-domain verification, or a separate follow-up requirement to
close these issues. Do not describe Reply-To as a custom From address.
- Site creators can edit the notification subject, plain-text template,
content, and language, with project defaults and per-Resource overrides.
Together with Batch 4, this covers the email customization requested in
#4414.

## Batch 5 — Email delivery and visitor confirmation

- [x] Implement the Cloud-only `EMAIL_SERVICE` adapter for new Email
Resources. Attach every submitted file by default; preflight the 5 MiB
encoded-message and 32-attachment limits and return explicit service
errors. Staging published-Form Email delivery is verified in Batch 7.
- [x] Implement visitor confirmation as an ordinary Email Resource
selected in Actions, addressed through a submitted visitor email field
and dispatched alongside other Actions. Its optional body starts empty
and uses the existing Resource editor and expression bindings. Email
Service prepends a mandatory confirmation preamble with the site URL.
Remove the Form-level confirmation toggle and email-field selector; SDK
and generated-route tests cover delivery.
- [x] A failed visitor-addressed Email Resource appears in `errors` and
does not change aggregate Form success; SDK tests cover this.
- [x] Before any destination runs, count team-recipient deliveries
across all reachable Email Resources, counting duplicates and shared
dependencies once. Reject the whole submission above five at runtime;
permit at most one visitor-addressed receipt separately. No
cross-Resource Builder or publish-time cap substitutes for this
preflight.
- [x] Local subject validation (`1fa8e66d9d`): resolved Email Resource
subjects must be strings without CR/LF. Validate after expressions are
evaluated and before any selected destination dispatch. SDK tests cover
CR/LF from direct `formData` and `browserInfo` subject bindings and zero
provider or selected fetches
(`managed-form-resources-generator.test.ts`,
`managed-form-submission.test.ts`, `resource-loader.test.ts`; focused
tests 88/88 passed). Dependency-bound Email is rejected after any
required lookup and before selected destination dispatch.
- [x] Verify resolved owner and visitor-confirmation subjects in the
generated server path with provider fakes. Owner subject preflight
covers CR/LF from visitor-derived bindings before any selected
destination or provider send; confirmation subject preflight likewise
sends no HTTP or email request. Published staging delivery is verified
in Batch 7.
- [x] Test the local private-service envelope and response path with a
provider fake: recipients, Reply-To, optional Sender display name, plain
text, attachments, recipient and size/count preflight, privacy,
confirmation ordering, partial failure, quota error, and zero sends on
failed preflight. Compiled Remix and React Router action tests cover
confirmation with an HTTP sibling and Node's explicit unsupported
response.

### Cloudflare Email Sending integration plan

Companion SaaS PR:
[webstudio-is/webstudio-saas#666](https://github.com/webstudio-is/webstudio-saas/pull/666).
The Email Service contract uses the canonical Worker
`webstudio-email-service`, Cloudflare's native per-location Rate
Limiting binding, and the private `EMAIL_SERVICE` service binding for
published-site sends to `https://email-service.internal/v1/send`.
Builder Preview uses the authenticated shared Email Service endpoint
`https://apps.webstudio.is/v1/preview-send` with server-side
`TRPC_SERVER_API_TOKEN`. The shared Email Service Worker remains
deployed at version `8769175b-15d2-4c09-a312-b78dc93fd2f3`; this PR did
not deploy or change it. The named staging Preview was deleted and its
former URL returns 404; staging published-site Workers use the canonical
Worker through the private service binding. An authenticated malformed
production Preview request returned `400 invalid_request` / `Invalid
email fields` before send. This proves only authentication and
validation, not successful submission or delivery. Staging
published-Form delivery is verified in Batch 7; limiter verification is
tracked in companion SaaS PR #666. A separate direct Email Service test
delivery was confirmed received; no email was sent by the
malformed-request preflight.

- [x] Enforce Cloudflare's 5 MiB **total encoded message** limit and
32-attachment cap for ordinary outbound email, accounting for base64
expansion; report provider quota, suppression, sender, size, and count
errors without claiming delivery. These email limits are separate from
the Form's 25 MiB incoming upload limit and Webstudio's five
team-delivery preflight. Local Builder preflight/provider-fake tests and
the companion SaaS Worker tests cover the limits and stable error
mapping; staging published-Form delivery is verified in Batch 7.

- [x] Test the local generated-site private-service contract with
provider fakes for recipients, Reply-To, optional Sender display name,
body, attachments, confirmation timing, partial failures, quota/limit
errors, and zero sends for failed preflight
(`packages/sdk/src/managed-form-email.test.ts`,
`packages/cli/src/prebuild.test.ts`; `7507c3fbf7`, `ab04be99d4`,
`7c950eecec`).

#### Acceptance criteria

- Staging verification must prove the selected private call path works
for a Workers for Platforms published site; the central Worker alone
owns `send_email`, and a submitted Form never receives provider
credentials or direct sending capability.
- A Cloud published Form delivers owner mail and the optional
confirmation with the configured envelope and content; 5
MiB/32-attachment violations and provider failures produce clear
`errors`. HTTP siblings retain the existing partial-failure semantics.
- The Email Service contract is for the new Form only. Existing Webhook
Forms do not use this Email Service as part of this feature; no legacy
Email Service compatibility or migration is included.

### Acceptance criteria

- A Cloud Email Resource sends the configured plain-text subject and
body from Webstudio's sending address with the configured display name,
with every submitted file attached under its original filename and MIME
type, or returns an explicit size/count error.
- More than five total team-recipient deliveries across Email Resources
rejects the whole submission before any destination runs; duplicate
addresses count again.
- A visitor receipt is a normal Email Resource explicitly selected in
Actions and addressed to the submitted visitor email field. It runs in
parallel with the other selected Actions, with no separate Form-level
toggle or email-field selector. Its Resource body is empty by default;
the Email Service always prepends the fixed platform receipt and
published site URL before the Resource body. The Resource body uses the
normal editor and full existing expression/binding support. A failed
visitor Email Resource appears in `errors` but is nonfatal to the
aggregate Form result.
- Resolved owner and confirmation subjects are strings without CR or LF.
An invalid subject, including one produced from visitor data, is
rejected before the email provider is called; the provider fake records
no send.
- Provider integration checks exercise the central Email Worker
request/response contract. Mocked Builder delivery alone does not
satisfy this gate.
- Confirmation failure appears in `errors` while the aggregate
submission remains successful; environments without Cloud email do not
silently claim delivery.

## Batch 5a — Keep separate submissions in separate email threads
(#5018)

Site owners should see each form submission as a separate email
conversation, including when several visitors submit the same form close
together.

- [x] Append a short, unique submission reference to each new Form
owner-notification subject, keeping the configured subject text.
Generate it once per submission and reuse it for that submission's
delivery retries; submissions in the same second get different
references. At `52fcec3640`, SDK tests cover default/custom subjects,
two submissions at one mocked time, retry stability, empty/overlong
preflight with zero HTTP/email sends, and the 979-character boundary;
Form and Project Emails docs describe the suffix. Batch 7 verifies that
separate submissions create separate Gmail conversations.
- [x] Apply per-submission subject references to new Form owner
notifications (`52fcec3640`).
- [x] Add local regressions for new Form default/custom subjects,
same-time identical submissions, stable retry references, and
subject-length preflight (`managed-form-email.test.ts`). Document the
suffix in the new Form and Project Emails docs.

### Acceptance criteria

- Two separate submissions of the same form appear as separate
conversations in Gmail with conversation view enabled.
- Both default and custom subjects keep their configured text and
include a different reference for each submission.
- Retrying delivery within one submission keeps its reference. This does
not add delivery deduplication or submission storage.
- This submission-reference behavior applies to new Form owner
notifications only; it does not add delivery deduplication or submission
storage.

## Batch 6 — Parallel delivery and Form feedback

- [x] Resolve dependency Resources needed for bindings, preflight every
selected primary destination’s body format and deterministic URL
parse/protocol/credentials/denied-hostname policy before dispatching any
selected primary destination, then start selected primary destinations
in parallel. Dependency Resources may make outbound requests during
resolution. DNS/public-IP pinning and redirect checks remain in
protected transport at each request hop; other transport failures can
occur after a sibling dispatches. Overall success requires all primary
destinations to succeed.
- [x] Local deterministic group preflight (`e9b72d5d78`, `8bf4f4c8bf`):
red-before generated actions in Remix and React Router prove a dependent
JSON+File body or a denied selected URL blocks all selected POSTs; SDK
policy tests cover allowed URLs and denied scheme, credentials, and host
cases. Nonselected dependencies may run once to resolve bindings.
Provider and ingress behavior are covered by service tests; staging
verification is recorded in Batch 7 below.
- [x] Until Cloud email delivery is integrated, reject any selected
Email Resource or reachable Email dependency before dispatching an HTTP
sibling. Return an explicit unsupported-delivery error without claiming
success.
- [x] Retry only a failed or timed-out destination once within that
submission, including non-2xx responses. Never resend a known success
within the same submission. A new browser submission starts over; keep
no delivery history or cross-submission deduplication.
- [x] Keep aggregate HTTP status as transport metadata only; expose no
`status` variable. Bind each Resource's final status code and response
body through ordered `results` and `errors` arrays. Expose no response
headers.
- [x] Redirect only after overall success when configured. Render the
initial, success, and error states in the inserted Form template.
- [x] Fix #6487 feedback auto-scroll regression: success and error
feedback now scrolls into the viewport only when needed, uses instant
behavior for reduced motion, and success redirect skips scrolling.
MCP-authored generated-Preview tests assert actual viewport position for
off-screen feedback, no movement for visible feedback, partial failure
scrolling, and no scroll before redirect; the full serial MCP E2E suite
passes (36/36 including setup).
- [x] Copy the existing Webhook Form spam protection into the new Form's
managed submission path: generate the hidden browser field, validate it
on the server before delivery, preserve the Brave handling, and remove
internal fields before sending to Resources or email.

**Email Service rate-limit checkpoint:** The pushed SaaS code uses
Cloudflare's native Rate Limiting binding only on email sends: 50
recipient checks per project UUID per Cloudflare location per 60
seconds. Each recipient consumes a check; over-limit sends return 429
before provider delivery. Preview and published sends carry the trusted
project UUID. This is approximate per location, with no Durable Object,
SQL storage, or account-wide quota. The 60-attempt direct Worker
diagnostic and isolated non-production check are tracked in companion
SaaS PR #666; no live-user Form submission is part of this Builder PR.
- [x] Locally test concurrent starts, retries, timeouts, partial
success, results and errors bindings, redirect, and feedback scrolling
in both generated adapters and browser components.
- [x] Local new-Form behavior: generated Router and Remix adapters test
concurrent selected starts, one retry after non-2xx/network/timeout
failure, final partial-success `results`/`errors`, and no replay of
successful siblings or dependencies
(`packages/cli/src/prebuild.test.ts`). Builder insertion tests verify
`results` and `errors` bindings and confirm aggregate `status` is not
exposed as a variable
(`apps/builder/app/shared/instance-utils/insert.test.tsx`); browser
tests cover success-only redirect and visible/offscreen feedback for the
new Form.

### Acceptance criteria

- Primary destinations begin in parallel. Each failed or timed-out
destination gets one retry within the same submission; known successes
are not resent, and overall success requires every primary destination.
- Ordered `results` and `errors` expose each Action Resource's
`resourceId` and `resourceName`, final status code, and response body to
bindings, without response headers. Both arrays preserve configured
Action order even when responses finish out of order. Aggregate HTTP
status remains transport metadata and is not exposed as a Form variable.
- Redirect occurs only after overall success, without scrolling to
inline feedback first. Otherwise the correct feedback state renders and
scrolls into view only when outside the viewport. Already-visible
feedback does not move the page, and animated scrolling respects reduced
motion. This applies to the new Form.
- New Form submissions use the adapted hidden browser check: valid
browser and Brave submissions pass; missing, malformed, or expired
evidence stops delivery before any destination runs; internal bot fields
never reach Resources or email.
- The Email Service code returns 429 for recipient checks over the
native per-project-UUID, per-location 50-per-60-second limit before
provider delivery. HTTP/Webhook Actions and webhook-only Forms are not
throttled by it. The 60-request direct Worker diagnostic and webhook
control are tracked in companion SaaS PR #666; no production Form
submission is part of this Builder PR.

## Batch 6a — Legacy-only work excluded from this feature

### Acceptance criteria

- The new Form accepts a fresh valid submission after an earlier failed
attempt without stale bot data.
- No legacy compatibility path is added to the SaaS Email Service.

## Batch 6b — Shared submission runtime for the new Form (related to
#4093)

Use shared new-Form submission behavior across supported hosting
runtimes while keeping page data current after a submission. This batch
does not complete #4093's remaining legacy Webhook Form work.

- [x] Move new Form submission, pending/error/success handling, and
response handling into shared code that does not require Remix or React
Router hooks or their form components. Generated hosting adapters call
the shared managed-Form handler; existing Webhook Forms keep their
original behavior.
- [x] Local new-Form lifecycle: router-free Form submission uses shared
`submitFormData`; generated Router and Remix adapters supply the server
request and page-data refresh behavior. Legacy compatibility adapters
and lifecycle changes are not included.
- [x] Submit through a normal HTTP endpoint with a defined JSON response
and small Router/Remix request and page-data-refresh adapters. Keep
current HTTP Resource credentials, validation, bot checks, and
destination protection on the server.
- [x] Local managed-Form route: generated same-origin `/__ws-form` POST
returns structured JSON for new Form submissions in compiled Remix and
React Router handlers; route coverage includes root, nested, and dynamic
pages, repeated multipart fields/files, bot rejection, and Resource
results. Staging Cloud email delivery is verified in Batch 7; the Email
Service rate-limit verification is tracked in companion SaaS PR #666.
- [x] Integrate the Cloud `EMAIL_SERVICE` adapter for new Email
Resources into the generated server action, with provider-fake response
and preflight tests. The Email Service per-site rate limit is tracked
separately below.
- [x] Replace the automatic page-data refresh currently supplied by
Remix. After a successful submission changes data, refresh the current
page's Resources without a full-page reload. Handle navigation during
submission and refresh failures without resending a completed
submission.

- [x] Test the new Form's shared component without router context
against a test HTTP endpoint, then test both hosting adapters. Cover
response bindings, page-data refresh, files, redirects, errors, and
retries.
- [x] Local browser and generated-route matrix: router-free Form tests
cover pending/results, files, and HTTP/network failures; generated
Router and Remix adapter tests cover retries, redirects, loader refresh,
navigation, and refresh failure without replaying POST. Compiled-handler
tests cover both adapters; release/staging verification is complete in
Batch 7.
- [x] Real router-free HTTP test (`679eb8e0c275`,
`packages/sdk-components-react/src/native-form-http.test.tsx`): the
shared new Form runs without a router provider in Chromium and sends
multipart POST to a local same-origin JSON endpoint without mocking
`fetch`. It reports pending while the response is held, then success
with `results` and a later error with `errors`, without native
navigation. Release/staging verification is complete in Batch 7.
- [x] Page-data-refresh red-before/green-after proof: with the exact
`native-form-refresh.browser.test.tsx` files from `20989a2d64` placed
temporarily in detached pre-fix `41580dbe9c`, React Router and Remix
each failed 3/5 tests: successful submissions left displayed data at
`before`, and the refresh-failure path never ran its second loader call.
On PR head `679eb8e0c275`, both adapters pass 5/5. The temporary
checkout was restored cleanly; the red/green regression evidence is
recorded here.
- [x] Local new-Form regression tests cover pending/results, errors,
redirect behavior, selected destinations, retries, and page-data
refresh. Remove legacy Webhook-specific retry, feedback, lifecycle, and
converter claims; they are outside the PR scope.
- [x] Update the hosting and Form docs to explain the generated server
endpoint, dynamic hosting requirement, and static-export limitation. The
generated Cloud action calls the private `EMAIL_SERVICE` binding when
present. Staging published-Form delivery is verified in Batch 7.

### Acceptance criteria

- The shared submission component works without a Remix or React Router
provider; supported hosting adapters connect it to server delivery and
page-data refresh.
- A successful data-changing submission refreshes the displayed data
without reloading the whole page or sending the submission again.
- Resource response values remain available through the bindings from
Batch 6, with consistent success/error behavior across supported
runtimes.
- The generated new-Form submission path uses the shared handler;
existing CLI conversion and legacy submission behavior remain at their
pre-PR baseline except for the #5512/#3891 route/query binding fixes in
Batch 2a.
- Server protections and secret handling remain intact. Static exports
do not silently claim managed submission support.

## Batch 7 — New-Form integration, docs, and release gate

- [x] Mark only the legacy `Form` and `RemixForm` component metadata
deprecated and keep `NativeForm` as the sole Forms insert tile. Preserve
the old component IDs, registrations, templates, and supported settings
for saved instances (`820aed68ed`;
`apps/builder/app/shared/nano-states/components.test.tsx`, focused
Chromium 2/2).
- [x] Local file-input and HTTP upload segments: Builder
insertion/save/reopen retains name, required, accept, and multiple
settings (`apps/builder/app/shared/instance-utils/insert.test.tsx`);
browser tests cover required, optional, and multiple files
(`packages/sdk-components-react/src/native-form.browser.test.tsx`);
compiled Remix/React Router generated actions preserve repeated
multipart fields and original file names, MIME types, and bytes
(`packages/cli/src/prebuild.test.ts`, `3c5b72a`).
- [x] Partial published-site staging check: one controlled `/copy-1`
submission with unique test value `Staging E2E 2026-10-05` entered the
managed Form success state for the selected project-owner Email Resource
configured with subject `New form submission`. The exact wire HTTP
response and owner inbox receipt are not independently verified; the
user is checking that inbox. This does not close the full
multi-destination, file, confirmation, coverage.
- [x] Update Form, Resource, Project Emails, and hosting docs for File
Input settings, HTTP uploads and limits, visitor-addressed Email
Resource with mandatory confirmation preamble/site URL and optional
body, and plain HTML forms using Element with `tag=form`.
- [x] Document the current managed Form's combined 25 MiB upload-request
limit, multiple-file failure guidance, and separate HTTP Resource body
limit (`33b2c7eab9`); Email provider attachment limits are documented in
the Email Service README, and published staging delivery is verified in
Batch 7.
- [x] Document local Resource selection/scope and Auto/JSON/multipart
upload behavior, plus Project Emails Sender, recipient, owner-body,
inheritance, and visitor Email Resource settings (`b9dbb6ee55`,
`7507c3fbf7`). Form, Project Emails, and self-hosting docs now describe
conditional Cloud `EMAIL_SERVICE` delivery, explicit missing-service
failure, and self-hosted integration requirements (`61fcf3f3d5`);
published staging behavior remains unverified.
- [x] Document Cloud Email attachment count and encoded content/request
limits in Form docs (`61fcf3f3d5`).
- [x] Document provider/quota errors and the Cloud email hosting
requirement. Builder Form, Project Emails, and self-hosting docs cover
new-Form visitor behavior and hosting limits. Sending-domain setup and
companion SaaS deployment are tracked in the companion SaaS PR.
- [x] Refresh the current bundle-version metadata in all eight static
project fixtures and synced user metadata in seven;
`packages/protocol/src/fixtures.test.ts` passes 17/17 locally and
Linux/Windows fixture CI both pass on head `70a2091c69`.
- [x] Complete relevant local checks on current PR work. Focused tests,
typecheck, lint, formatting, and diff checks pass locally; full GitHub
CI passes on `84f24e8d12`.
- [x] Reviewed each new-Form issue row against current code/tests; no
additional legacy compatibility beyond the explicitly tracked
#5512/#3891 route/query fixes is a closure condition. Evidence: #3023
file and attachment checks in
`packages/sdk/src/managed-form-email.test.ts` and
`packages/cli/src/prebuild.test.ts`; #5512 and #3891 page parameters and
all query values in `packages/cli/src/prebuild.test.ts`; #5432 repeated
multipart values/files in the same generated-route suite; #5499 parallel
destinations and five-recipient preflight in
`packages/sdk/src/managed-form-handler.test.ts`,
`managed-form-resources-generator.test.ts`, and
`managed-form-submission.test.ts`; #3530 redirect behavior in
`packages/sdk-components-react/src/native-form.browser.test.tsx`; #5428
trusted browser info headers in `packages/cli/src/prebuild.test.ts`;
#5658 sender/reply-to validation in
`packages/sdk/src/managed-form-email.test.ts` and the companion SaaS
Worker tests; #6487 feedback scrolling in
`packages/sdk-components-react/src/form-feedback-scroll.browser.test.tsx`;
#4414 and #4385 project defaults, overrides, and recipient isolation in
`packages/sdk/src/email-resource.test.ts` and
`apps/builder/app/builder/features/settings-panel/resource-panel.test.ts`;
#5018 unique submission references and retry behavior in
`packages/sdk/src/managed-form-submission.ts` and
`managed-form-email.test.ts`. These are merge-time issue mappings, not
production delivery evidence.

### Acceptance criteria

- The insert menu offers one new Form. Deprecated old components and
their migration are outside acceptance; no legacy compatibility path is
added.
- Staging end-to-end coverage exercises an empty Form blocked from
navigation and a configured Form submitting to its Resource
destinations, plus two HTTP Resources, two Email Resources, files,
confirmation, and partial failure through Builder → publish → submit.
- Form, Resource, Project Emails, and hosting docs match the released
behavior, including the Cloud email requirement. Generated contracts,
focused checks, full CI, and staging tests pass before the PR is ready.

## Batch 8 — Submit current-draft Forms from Builder Preview

- [x] Add an authenticated Builder server endpoint for Preview
submissions. It checks the Builder session and exact project build
permission before effects; route tests cover authorized and rejected
requests.
- [x] Load current draft Form configuration and invoke the shared SDK
runtime handler without a prior publish or draft/published contract
comparison; Preview route and graph tests cover changed drafts.
- [x] Build the draft Resource graph with publish-equivalent scope,
dependency, and defaults rules. Reuse the existing
`evaluateExpressionSync` implementation for supported expressions; do
not add QuickJS, WASM, AST, or another expression engine. Focused graph
and expression tests cover parity.
- [x] Reuse the shared SDK managed-form handler in Builder Preview and
generated published-site routes. Their request, auth, and configuration
entry points remain runtime-specific; SDK and generated-route tests
cover both paths.
- [x] Execute HTTP Resource Actions from the Builder server in Preview,
and from the published-site Worker after publish. The Email Service
handles email only; Preview route tests assert the outbound HTTP
boundary.
- [x] Route Preview Email Resources server-to-server through the Email
Service with a server-only credential; published-site Workers use the
private `EMAIL_SERVICE` binding. Builder checks user and project
permission first; tests cover the Email Service call contract and
rejected auth.
- [x] Removed the unapproved Preview warning that submissions send real
emails and webhook requests; new user-facing warning copy requires
explicit approval.
- [x] Node tests execute the shared handler locally with a stubbed Email
Service boundary, without Cloudflare infrastructure. Staging
published-Form Email delivery is verified in Batch 7; the production
Worker limiter check is tracked in companion SaaS PR #666.
- [x] Keep a permanent Builder Preview transport Playwright E2E that
submits a current draft through the Preview backend and verifies the
response without publishing or triggering email/webhook side effects
(`apps/builder/e2e/tests/preview-managed-form.[shard-2].e2e.ts`; focused
2/2).
- [x] Preview converts rejected `onManagedSubmit` callbacks into a
structured 500 failure, clears stale result and pending state, and shows
the current error. Browser regression covers a prior structured failure
followed by a rejected callback
(`packages/sdk-components-react/src/native-form.browser.test.tsx`). The
new Form also cancels native submission when its `onSubmit` callback
throws; a Chromium browser regression clicks a submitter with
`formAction`/`formMethod` overrides and verifies cancellation, no
Resource dispatch, and visible failure (`28f7a93eed`).

### Acceptance criteria

- A Form in an unpublished project can be submitted from Builder Preview
without publishing first.
- The Preview endpoint rejects unauthenticated users and users without
edit access to that project before any HTTP request or email send
occurs.
- Preview uses the current draft. Changes to controls, Actions, Resource
dependencies, effective email settings, visitor-addressed Email
Resources, or redirects can be tested immediately without republishing
or a published-contract comparison.
- Draft expression results match published Resource evaluation for
supported syntax using the existing `evaluateExpressionSync`
implementation; no second expression engine is introduced.
- HTTP Resource requests go directly from the Builder server to their
configured destinations. Email Resource requests go through the central
Email Service Worker, which rejects requests without the server
credential; that credential never appears in browser traffic.
- Results, errors, uploads, response bindings, feedback, and redirects
match published submission behavior while keeping the Builder open.
- The generated published-site Worker continues to use the same shared
handler and public visitor submission path, and calls Email Service
through `EMAIL_SERVICE`.
- Local Node E2E tests exercise the shared handler and Builder Preview
authorization without Cloudflare; a separate integration check verifies
the real Email Service contract.

## Production release boundary

**The production Email Service Worker was deployed at version
`8769175b-15d2-4c09-a312-b78dc93fd2f3` under the user's explicit request
to update the code and Worker to use `TRPC_SERVER_API_TOKEN`. An
authenticated malformed validation request returned `400
invalid_request` before send; that request sent no email.** This Builder
PR makes no deployment to the live site; live-user Form submissions are
outside its verification scope.

## Boundaries and known tradeoffs

- No old-component migration, submission storage or history,
account-wide email quota feature, CC/BCC, custom From address or
customer sending-domain verification, HTML email, preview text, or
separate branding editor. Builder Preview submission is included in
Batch 8 and runs against the current draft without requiring
publication; it is a real submission, not a separate email test-send
feature. Cloudflare account sending quotas still govern delivery;
migrating Plunk authentication or other Plunk operations is separate
follow-up work.
- The Discord requests for an explicit CAPTCHA challenge, direct Google
Sheets connection, multi-thumb/vertical slider, and an exact submission
copy to an unverified visitor are not covered here. A visitor-addressed
Email Resource always gets the fixed platform preamble and site URL; its
optional builder-configured body may use normal Resource bindings.
- A timeout retry can duplicate a delivery that succeeded remotely. A
visitor-addressed Email Resource can send to an unverified submitted
address; the native Email Service limit of 50 recipient checks per
project UUID per Cloudflare location per minute can reject legitimate
local bursts; it is approximate and not a global cap.
- The five team-delivery cap is a server preflight across the whole
Form; it is intentionally not calculated across independent Resources in
the Builder.
- Project defaults apply site-wide. Form-only expressions resolve only
in Resources scoped inside that Form.

## Additional Builder and Resource behavior

### Builder UI

- [x] Add the `formState` bullet to the Action tooltip after the exact
user-facing description is approved; verify it with the focused tooltip
test. The exact wording was approved; covered by focused Action tooltip
tests.
> Sends each form submission to all selected destinations at the same
time.
  > - formData — submitted fields from this Form.
  > - browserInfo — visitor details.
  > - formState — current Form state: initial, success, or error.
  > - results — each action’s response, in order.
  > - errors — failed actions and their messages.
- [x] In the Variables list and Edit variable preview, show
server-provided `browserInfo` after an authenticated Builder Preview
submission when available. Use only trusted `cf-connecting-ip`, ignore
spoofed `x-forwarded-for`, and sanitize Referer by removing query and
fragment before exposing it. Keep `previewBrowserInfo` private to
Builder memory and strip it before Canvas pubsub. Route, bridge,
Variables list, and Edit variable preview tests cover these paths;
focused preview tests pass (2/2) and bridge tests pass (7/7). Published
Form behavior stays unchanged.
- [x] Recognize supported Chromium browsers without requiring
`window.chrome`; user-agent detection and unsupported-browser regression
coverage are committed.
- [x] Add the approved “Clear all” button below multiple visible toasts
and dismiss the stack; shared toast implementation and browser
regression are committed.

- [x] Remove the background from the maximize button in code and
expression editors; keep the icon button transparent, consistent with
other editor controls. `code-editor-base.browser.test.tsx` verifies
transparent hover styling.

- [x] Implement separate tooltips for the Form variables `formData`,
`browserInfo`, `formState`, `results`, and `errors`; exact wording was
approved on 2026-10-08, and `variables-section.test.tsx` verifies the
displayed descriptions.

- [x] Add a description to the Action label tooltip and show a yellow
warning triangle with “Add at least one action” when the Action list is
empty. The warning disappears once an Action is added; covered by the
Form Action control browser test.

- [x] Reset the Form Preview runtime state to `initial` when leaving
Preview, so the Builder returns from success/error feedback to the
editable Form state without changing saved bindings or published
behavior. Add regression tests.

- [x] Route every managed Form submission error through the user-styled
Error Message instance. Make its message dynamic from the submission
error result, remove static fallback text and raw React alerts, and
cover validation, partial Action failure, and service errors.

- [x] When a Form, Form content, Success Message, or Error Message
instance is selected on the canvas, show that selected instance in
Builder regardless of its bound `show` value. This is an editor
selection override only; preserve the saved binding and runtime
behavior. Add regression tests for each state.
- [x] Add the approved description “Sends each form submission to all
selected destinations at the same time” and a multiline list for
`formData`, `browserInfo`, `formState`, `results`, and `errors` to the
Action label tooltip; use lowercase “action” in descriptions. Remove the
aggregate `status` variable; Form state remains
`initial`/`success`/`error`, and each Action’s HTTP status stays in its
result/error entry. Retain the approved “Add at least one action”
warning when no Action is selected. The Action reset button says
“Reset.” Focused insertion and Action UI tests cover these behaviors.

- [x] The Action plus-button tooltip says “Add a resource to submit this
Form”; the focused Form Action control browser test covers it.

- [x] Rename the Form’s component prop from `submission` to `action`,
matching the singular **Action** control. Store selected Actions as an
ordered array of Resource references with enabled state. Execute enabled
requests in parallel while preserving Action order in `results` and
`errors`. Update generated metadata, runtime handling, and tests; do not
add legacy compatibility.

- [x] Add Delete, Copy, and Paste to each variable row menu and its
right-click context menu. Within Variables, right-click anywhere allows
Paste; target-specific commands are disabled without a variable row.
Remove the Variables section header menu. The Edit variable dialog menu
has Delete, Copy, and Refresh only; remove its redundant quick-copy
button and do not offer Paste there. Serialize definitions as versioned,
namespaced JSON; omit internal IDs, create fresh IDs on paste, preserve
and remap expression references, and report unresolved references. Base
menu and transfer tests pass.

- [x] Rename user-facing Builder UI section and control labels from
`Data variables` to `Variables`; retain `CSS variables` for clarity.
Update UI tests.

- [x] Give variable labels a tooltip that shows defining scope,
type/value-kind details, and Delete with its shortcut. Option-click
deletes the local variable; deleting a shadow reveals its ancestor. UI
tests cover the tooltip and Option-click deletion.
- [x] Clicking the rendered “Value comes from” source label navigates to
and focuses its defining instance. `variables-section.test.tsx`
exercises the real ancestor “Body” source label and Global root label;
the full focused file passes 9/9.
- [x] Label a Global Root user variable source as “Global root,” not
“System,” and keep the source label clickable to navigate to and focus
its definition. `variables-section.test.tsx` verifies the “Global root”
label, absence of “System,” and source navigation.
- [x] Fix the verified source-label behavior defect: clicking “Value
comes from” must reliably focus its defining Variables row after
ancestor or Global root navigation. Regression tests pass both
standalone and in-suite: `variables-section.test.tsx` passes 12/12, and
isolated ancestor/Global root navigation-focus cases pass 3/3 each.

- [x] Prevent deletion of the managed Form’s required `formState`,
`results`, and `errors` variables by stable identity in the Builder UI,
including after renaming, through Variables row menus, the Edit variable
menu, right-click context menus, and Option-click; ordinary user
variables remain deletable. `variable-menu.test.tsx` covers the menu
paths and ordinary deletion; focused suite passes 21/21. The
required-variable rule is enforced in both the Variables UI and the
mutation/domain layer.

- [x] Show a yellow warning triangle only when a variable name matches
an ancestor; allow the child-scope name. Tests cover an existing shadow
and an ancestor rename.

- [x] Keep the variable row highlighted while its Edit variable dialog
is open; use the shared list-row open state and cover it with a
regression test.

- [x] Use the shared warning color token for variable-shadow triangles
in both the Variables list and Edit variable dialog; the Builder UI test
verifies the token.

- [x] Reject duplicate Resource variable names on the same instance
while allowing a nested scope to shadow a parent name; add validation
and regression tests.

- [x] Use sentence case for the Form setting label: `Success redirect`.

- [x] Expose Form-scoped runtime values as inspectable JSON in Builder
Preview. `formData` reflects the current named controls and values
before submission, including empty values; `browserInfo` shows its safe
context values. Add focused UI and runtime tests.

- [x] Add `resourceName` alongside `resourceId` in each ordered
result/error and relevant inspection entry; verify naming, partial
failure, retries, and bindings without exposing secrets.

- [x] Independently rechecked #3891 against the implemented contract:
`system.search` returns a string for one value and an ordered array for
repeated query values. `packages/sdk/src/system-search.ts` and its
regressions cover repeated, empty, comma-containing, and encoded values;
Batch 2a retains the generated-route and documentation evidence.

- [x] Verify that a selected Email Resource defined on the Form, an
ancestor, or global scope uses the existing generated submitted-fields
default body when both its Resource body and Project Emails body are
unset. Preserve explicit Resource/Project body overrides, including an
explicitly empty Resource body, Visitor receipt behavior, and rejection
of explicit out-of-scope `formData` bindings. Draft and published graph
tests cover all three scopes and both override types in
`managed-form-resources-generator.test.ts` and
`managed-form-draft-graph.test.ts`; focused SDK tests pass (73 tests).

### Resource editor

- [x] In Project Settings → Emails, show all long explanations in the
existing info tooltips beside their related controls, preserving the
exact existing copy. Specifically verify the Recipients and Owner
plain-text body explanations; `section-emails.test.tsx` currently covers
tooltip content and keyboard access.

- [x] Restore the approved attachment behavior sentence in the Project
Settings → Emails documentation, in the Visitor email paragraph only:
“When a Form includes file inputs, submitted files are attached to the
outgoing Email Resource message by default. Turn off ‘Attach submitted
files’ to omit them.” Do not add this text to the settings UI. Added in
`c96fb7c409`; `pnpm check:generated-docs` and `git diff --check` pass.

- [x] Replace the outdated Visitor attachment statement in the Form
guide with the same approved sentence used in Project Settings
documentation: “When a Form includes file inputs, submitted files are
attached to the outgoing Email Resource message by default. Turn off
‘Attach submitted files’ to omit them.” Added in `40dd8c4af3`; `pnpm
check:generated-docs` and `git diff --check` pass.
- [x] **Email Resource Type description:** The exact description was
approved, and `variable-popover-type.test.tsx` asserts the Email option
displays “Send a plain-text email through Webstudio Cloud when a Form is
submitted.” Focused type-selector suite passes 7/7; Builder typecheck
passes.
- [x] **Method dropdown copy approval:** Exact copy is approved and
tested: GET “Read data from a server.”; POST “Send data to create or
process something.”; PUT “Replace data on a server.”; DELETE “Delete
data from a server.” Keep the existing Form-submission note with POST;
focused Resource editor tests verify all four descriptions and the POST
note.
- [x] Keep Email Resource field labels as `Sender`, `Recipients`,
`Subject`, and `Body`; use the existing binding control and Expression
Editor for supported values, and show validation errors in the standard
input tooltip rather than below the field. `resource-panel.test.ts`
asserts each label appears exactly once, covers the binding controls and
Sender/Custom recipient Expression Editor save/unmount/remount/reopen,
and verifies validation errors appear in standard input tooltips; the
focused suite passes 30/30.
- [x] Give Email Resource recipient controls programmatic accessible
names by associating the existing visible “Recipients” label with its
selector and using existing text for the Custom recipient input and
Visitor email selector. Keep visible copy unchanged.
`resource-panel.test.ts` passes 32/32; Builder typecheck, changed-file
`oxlint --deny-warnings`, and `oxfmt --check` pass.
- [x] Remove the separate Project default reset button; switching away
from Custom recipients clears its value. Show the custom address input
directly below the mode selector and the Visitor field selector without
redundant field labels. `resource-panel.test.ts` covers literal and
bound recipient clearing when switching to Project and Visitor modes,
mode-specific controls, and no duplicate Recipients label; the focused
suite passes 30/30, including Sender and Custom recipient bindings
through the Expression Editor and save/unmount/remount/reopen.

- [x] Verify and test the REST and GraphQL type-option descriptions
against the approved wording: “A REST resource is a configuration for
secure data fetching.” and “A GraphQL resource is a configuration for
secure data fetching with GraphQL.” Preserve the existing “You can
safely use secrets in any field.” note. `variable-popover-type.test.tsx`
covers both descriptions.
- [x] **Visitor email field info tooltip:** Approved exact copy for the
info tooltip beside the Visitor email field selector: “Adds a note with
this site’s URL to help recipients identify where the message came from
and discourage spam.” Keep it visible only when Visitor email field is
selected; verify the text opens on hover and keyboard focus and closes
on blur in `resource-panel.test.ts`.
- [x] Match the Edit variable dialog menu trigger icon size to the other
header controls and make the menu’s minimum width about twice its
current width; `variable-menu.test.tsx` covers the header icon and 360px
minimum width.
- [x] Simplify Builder Email Resource preflight: remove duplicated
Worker-authoritative mailbox and attachment metadata policy checks;
retain a small pre-Base64 attachment size guard, subject safety, and
aggregate recipient protections. Tests verify early size rejection,
valid attachment delivery, and Worker rejection propagation.

- [x] Replace the superficial Headers and Search params label
info-tooltip descriptions with clear technical explanations of their
purpose; propose and obtain approval for exact user-facing copy before
implementation. The approved label info icons explain these sections; no
additional plus-button tooltips are needed. Exact wording approved;
covered by focused Resource editor tests.

- [x] **Recipient selector descriptions:** Add the approved descriptions
exactly: Project recipients — “Send to the addresses in Project
Settings, or to the account holder if none are set.”; Custom recipients
— “Send to the addresses entered below.”; Visitor email field — “Choose
a Form input to use as the recipient’s email address.” Verify copy and
visibility in `resource-panel.test.ts`.
- [x] **Move Visitor email guidance into its recipient option
description:** Removed the extra info icon next to the selected Visitor
email field input. The Recipients dropdown option now explains that the
confirmation email goes to the selected Form input address.
`resource-panel.test.ts` passes (41/41); formatting, lint, and diff
checks pass. Pushed as `a85c77308f`.

- [x] Put the Form POST-method note in Method dropdown option
descriptions instead of inline copy; keyboard and pointer behavior are
covered by the Resource editor tests.
- [x] **Format option descriptions:** Approved exact copy: label
`Format`; help below the control “Choose how to send the body.”; `auto`
— “Uses text/plain for text values, application/json for other values,
and multipart/form-data when files are included.”; `application/json` —
“Sends an object or array as JSON.”; `multipart/form-data` — “Sends
fields as form data, including files.” `resource-panel.test.ts` verifies
the descriptions, below-control help, standalone GET hiding, Form Action
GET visibility, and Body editor; the focused suite passes 34/34.
- [x] **Headers and Search params add-button tooltips:** Superseded by
the user's clarification: the approved info icons beside the Headers and
Search params labels explain those sections, so no additional tooltips
on the plus buttons are needed. The existing info-icon descriptions
remain covered by the focused Resource editor tests.
- [x] **Project Settings → Emails helper-text placement:** Move the long
inline explanations (including Recipients and Owner plain-text body
help) into adjacent info tooltips so the settings stay compact. Do not
add or rewrite user-facing copy without explicit approval; preserve
approved behavior and cover tooltip placement with focused tests.
- [x] **Shadowing warning tooltip copy:** Reverted the unapproved
proposed wording and retained the existing tooltip.
`variables-section.test.tsx` verifies the parent-scope shadow condition,
including an existing shadow and an ancestor rename; a unique name
removes the warning.
- [x] Add an info tooltip beside Cache max age explaining the response
cache lifetime in seconds. A focused Resource editor test verifies it.
- [x] Use the approved info icons beside the Headers and Search params
labels to explain those sections; no additional plus-button tooltips are
needed. Use Minus icons to remove individual rows. Focused Resource
editor tests cover the label info tooltips and row removal.
- [x] Keep the Body label and remove the Body content type selector.
Infer text/plain for string bodies and application/json for objects,
arrays, numbers, and booleans; preserve automatic multipart handling for
files, explicit request-format validation, and useful editor/error
behavior.
- [x] Label the body-format dropdown Format and show the approved help
“Choose how to send the body.” below it. Use the effective method: hide
Body and Format for a standalone GET Resource; a GET-configured Resource
selected as a Form Action is effectively POST and keeps both controls
visible. Focused editor tests verify the help placement, visibility, and
selected body format.
- [x] Show Response, Request, and Diagnostics tabs for HTTP Resources,
with Response first and selected by default. Capture the actual outgoing
method, URL, headers, and body plus response status/status text,
headers, final URL when available, and body from the latest Form Preview
submission, including retry attempts. `request-inspector.test.tsx`
verifies the order and default selection, including the captured numeric
response status and actual response body for both HTTP and Email
exchanges (focused suite 7/7); route and inspector tests verify captured
exchange data, no replay or substitution from current config, and
authenticated Preview-only, no-store, ephemeral, bounded, redacted
capture. Files show metadata instead of bytes. Email inspection shows
the captured transport request and actual HTTP response, with
credentials and encoded attachment bytes redacted; the logical delivery
outcome remains separate for Form failure handling. Published staging
submission and attachment delivery are verified in Batch 7.

- [x] Classify Email inspection exchanges by Resource kind, never URL
alone; focused sanitizer and inspector tests verify that an ordinary
HTTP Resource targeting the Email Service URL keeps normal request-body
redaction.
- [x] Preserve actual Email Service response headers in the Email
inspection Response view; `resource-loader.test.ts` verifies transport
capture and `preview-form-inspection.server.test.ts` verifies safe
headers reach the inspection view.
- [x] Preserve the actual Email Service HTTP response body/status
separately from the logical Email delivery result, so provider-level
failures remain Form failures even when HTTP returns 200. Read the
response body once so inspection and delivery validation share the same
parse; keep captured data bounded. Add regressions for malformed success
and non-JSON error responses. Single response read, bounded inspection
snapshot, provider body and logical outcome regressions; SDK tests pass.

- [x] Keep the Variables panel context menu scoped to the panel:
right-clicking a variable row targets that row, blank panel space allows
Paste, and right-clicking inside the Edit variable dialog does not open
the panel menu. The dialog menu offers Delete, Copy, and Refresh only;
`variable-menu.test.tsx` covers row, blank-space, and dialog
context-menu behavior.
- [x] Keep Email Resource attachments enabled by default. The default
contact Form’s owner Action keeps attachments enabled, while its visitor
confirmation explicitly opts out because it does not include the
submitted file. `managed-form-email.test.ts`, `resource-panel.test.ts`,
and the default contact Form E2E cover defaults and opt-out.

- [x] Support bindings for Email Resource Sender and Custom recipients
using the existing binding/Expression Editor pattern; evaluate and
validate resolved mailbox values before dispatch.
`resource-panel.test.ts` verifies Sender and Custom recipient bindings
through the Expression Editor and save/unmount/remount/reopen (30/30).
`email-resource.test.ts` verifies resolution and rejects empty,
non-string, and invalid values; `managed-form-handler.test.ts` verifies
invalid bound values are rejected before Resource fetch or email send
(35/35 combined SDK tests).
- [x] Memoize Email Resource recipient expression scope inputs
(`recipientScope` and aliases) so editing a recipient value does not
restart asynchronous expression evaluation on every render; add focused
regression coverage. Unrelated Resource edit regression passes;
`resource-panel.test.ts` 34/34.
- [x] Make all Email binding tests use CodeMirror’s platform-aware Mod
shortcut (Meta on macOS, Control elsewhere), including Sender/Custom
recipient round-trip and Project-mode bound-recipient clearing, so they
pass Ubuntu CI. `resource-panel.test.ts` contains no hard-coded
Meta/Control keyboard chords and passes the full focused suite (30/30).
- [x] Ensure generated server modules import Email binding helpers only
once when both regular Resources and managed Forms use Sender or Custom
recipients bindings; generated-site resource-only and combined-module
tests in `packages/cli/src/prebuild.test.ts` assert one helper import
(2/2), and the CLI typecheck passes.
- [x] Ensure generated modules for generic (non-managed) Email Resources
using `senderExpression` or `recipientsExpression` import the runtime
resolver helpers. `packages/cli/src/prebuild.test.ts` verifies one
helper import and typechecks a mixed generated site with page-level and
managed-Form Email bindings, plus a page-level-only site; both focused
cases pass (2/2).
- [x] Enforce the five-recipient total before dispatch for Email
Resources with Sender or Custom recipients bindings. Count known/literal
recipients even when the Email Resource depends on another Resource;
reject recipient expressions whose count depends on a Resource output
before any send. Preserve Form-variable bindings.
`managed-form-handler.test.ts` covers a resolved six-mailbox binding and
verifies rejection before HTTP fetch or email send; existing dependency
tests cover Resource-output counts.
- [x] Prevent or support Email recipient expressions that depend on
another Resource. The editor currently accepts these expressions, but
managed-submission preflight rejects them; align editor validation and
runtime behavior, with focused regressions. Editor binding scope now
excludes Resource outputs; server preflight remains fail-closed; focused
tests and Builder typecheck pass.
- [x] Disallow Email recipient bindings that depend on another Resource
in the Builder editor, while keeping server preflight fail-closed.
Resolving dependencies before the recipient-limit check could trigger
arbitrary requests; add focused editor and preflight regressions. Editor
binding scope now excludes Resource outputs; server preflight remains
fail-closed; focused tests and Builder typecheck pass.
- [x] Fix Type and Recipients dropdowns in a narrow sidebar so each
dropdown panel matches its input trigger width, using Name as the
reference; focused tests cover Type, Recipients, and Method. Flattening
removes the separate Resource dropdown.
- [x] Flatten System resources into the single Type list: remove the
separate “System resource” category/dropdown and list Sitemap, Current
date, Assets, and Email alongside the other Type choices. When editing
an existing System Resource, changing between categories
(Sitemap/Current date/Assets ↔ Email) persists the selected type.
Preserve existing Assets Resources whose URL expressions use valid
non-JSON string-literal syntax; add a reopen regression in
`variable-popover-type.test.tsx`. Assets non-JSON URL expression reopen
regression passes; Builder popover tests pass.

- [x] Fix the Builder typecheck failures in `resource-panel.test.ts`
binding tests caused by nullable queried-button assertions, then rerun
and pass the Builder typecheck. Builder typecheck passes and
`resource-panel.test.ts` passes 30/30.
- [x] Add a generated published-route regression proving that an
unset/default Form Action body with an uploaded File selects multipart
automatically and preserves filename, MIME type, and bytes.
`packages/cli/src/prebuild.test.ts` runs the generated endpoint for both
defaults and React Router adapters; the focused test passes 2/2.
- [x] Add regression coverage that an ordinary Project/Custom Email
Resource with `includeAttachments` absent resolves default-on through
the Email Service payload. `packages/sdk/src/managed-form-email.test.ts`
covers both recipient modes and asserts attachment payloads; the focused
suite passes 29/29, and `pnpm oxfmt --check
packages/sdk/src/managed-form-email.test.ts` passes.

- [x] Resolve current changed-file lint findings (two `no-explicit-any`
findings in changed tests and one `react-hooks/exhaustive-deps` finding
in `variable-popover.tsx`), then rerun scoped lint. Changed-file `oxlint
--deny-warnings` passes on 50 TypeScript files; `oxfmt --check` passes
on all 48 matched changed files; Builder typecheck passes; focused
settings/variables suites pass 47/47.

- [x] Simplify `handleManagedFormSubmission` by removing the redundant
one-Visitor-Email validation and relying on the shared
`validateManagedFormRecipientLimit` check; add or retain focused
preflight regression coverage. Handler and submission suites pass 48
tests, and SDK typecheck passes.

## Additional Form and Builder behavior

- [x] **Use the existing upload icon for the File input component:** The
shared `UploadIcon` is used in the File Input template and asserted by
`core-templates.test.tsx`.

- [x] **Verify Success redirect in Builder Preview:** On successful
submission, keep navigation inside Preview for internal routes, resolve
relative URLs against the previewed page, and navigate the preview frame
for external URLs. Add end-to-end coverage for a matched internal route,
an unmatched route (which currently silently does nothing), and hash
navigation (which currently updates the hash without scrolling).
Preserve error feedback and never redirect after a failed action.
Matched internal, unmatched internal, and delayed hash target cases pass
in Preview E2E; failed actions retain error feedback and do not
redirect.

- [x] **Finish Email diagnostics loading:** The Email Resource
Diagnostics tab no longer remains pending without a request key; focused
regression coverage verifies the settled empty state.

- [x] **Separate variable editors by type:** The dialog composes focused
modules for HTTP, GraphQL, Email, Assets, Parameter, and scalar types;
shared controls remain neutral. Focused Builder tests and typecheck
pass.

## Issue checklist

Eleven issues below are intended to close when this PR merges; #5432 and
#4093 are related but remain open. Each closure row must be implemented
and verified first; keep remaining work unchecked.

| Issue | Required result before merge | Batch |
| --- | --- | --- |
| #3023 | Configure file inputs in the Builder; deliver single/multiple
files to HTTP destinations and Cloud email within documented limits. |
3, 5, 7 |
| #5512 | Current-page parameters work in new and legacy Form Resource
bindings, including after navigation. | 2a |
| #4093 | Related issue for the legacy Webhook Form; this PR implements
Resource Actions and response bindings in the new Form but does not
change legacy submission beyond the explicitly tracked #5512/#3891
route/query binding fixes, shared lifecycle, or CLI conversion behavior.
Keep #4093 open. | 2, 3, 6, 6b |
| #5432 | The new Form sends repeated field values as standard multipart
entries without losing selections. Legacy Webhook Form behavior remains
unchanged except for the separately tracked #5512/#3891 route/query
binding fixes; this PR does not close the original issue, and migration
is separate. | 2, 3 |
| #5499 | One submission can deliver to email and multiple HTTP
destinations together; document the ten-destination and
five-team-delivery limits. | 2, 5, 6 |
| #3530 | A successful submission follows the configured redirect;
failure does not redirect. | 6 |
| #5428 | A destination receives the trusted visitor IP, user agent, and
language through configured Resource headers, with setup documented. |
2, 7 |
| #5658 | Custom subject and Sender work as agreed, with provider-level
tests for From and Reply-To. | 4, 4a |
| #6487 | New Forms reveal off-screen feedback, leave visible feedback
alone, respect reduced motion, and do not scroll before redirecting. | 6
|
| #4414 | Editable name/reply address, subject, plain-text template, and
translated content work with project defaults and per-Resource
overrides. Webstudio remains the sender, as agreed. | 4, 4a, 5 |
| #4385 | Two forms in the same project can send to different configured
recipients without changing each other's delivery. | 4, 5 |
| #5018 | Separate new-Form submissions produce separate Gmail
conversations; retries retain the same submission reference. | 5a |
| #3891 | Every selected query value is available in new and legacy Form
Resource bindings through `system.search`, with a single value remaining
a string and repeated values becoming an ordered array. | 2a |

- [x] After PR #6530 merged (`a49a6769bd`), verified the new Form needs
no port: React Router and Remix use the same Form submit path, which
creates fresh `FormData` and calls `set` once for the bot field on each
request. `generates one fresh bot field for each managed submission`
passes (shared browser suite 7/7); Router managed Form tests pass 2/2,
Router refresh tests 7/7, and Remix refresh tests 5/5.

# Related issues

Closes #3023
Closes #5512
Related #4093
Related #5432
Closes #5499
Closes #3530
Closes #5428
Closes #5658
Closes #6487
Closes #4414
Closes #4385
Closes #5018
Closes #3891
Related #6520 — [Multi-step Forms with resumable local
drafts](https://github.com/webstudio-is/webstudio/issues/6520)

The existing Webhook Form bug #6437 and prior legacy-form fix #6436 are
outside this new-Form feature and are not claimed as closed here. The
old email planning PR #6486 is superseded by this description.

- [x] **Use the canonical server token for Preview email:** The user
confirmed the canonical `TRPC_SERVER_API_TOKEN` is supplied from
Infisical in Preview and Production. Builder Preview now sends this
token to Email Service `/v1/preview-send`; the redundant
`EMAIL_SERVICE_PREVIEW_TOKEN` wiring is removed. Focused route tests
verify authorization, missing-token behavior, webhook-only behavior, and
redaction.

- [x] **Defer validation for a new Resource URL:** Untouched blank URLs
are not marked invalid until save; edited invalid values validate on
blur. Regression tests pass.

- [x] **Handle safe public Resource redirects:** Public GET/HEAD
redirects are revalidated and allowed without forwarding credentials;
unsafe destinations and cross-origin body requests remain blocked.
Protected-fetch tests pass.

## Form component identity correction (2026-10-11)
- [x] Keep component picker labels and compatibility behavior clear:
show the new component as **Form (Beta)**, and keep the existing
**Webhook Form** available in its current Data category through the next
release. Update the guide for the 10-Action limit and Navigation mode
when Actions are empty. Builder component-list and registry template
tests pass.

- [x] Use one canonical managed component identity, `Form`, for Resource
submission and Navigation mode. Keep `WebhookForm` only as the legacy
component through the next release, and keep generic `Element` with tag
`form` for native browser behavior. Update exports, metadata, templates,
adapters, migrations, docs, CLI/MCP discovery, and E2E identifiers;
verify saved projects migrate without changing legacy Webhook Forms.
Project migration tests pass (25/25), and the CLI prebuild suite
verifies saved bundles are migrated before rendering and endpoint
generation.

## Framework-neutral Form core (2026-10-11)
- [x] Move managed Form contracts, browser submission behavior, and
server delivery into SDK Form entries. Keep React hooks/rendering in the
React adapter and router refresh timing in the router adapter; preserve
compatibility exports. Builder and CLI consume the dedicated server
entry. StrictMode, cancellation, redirect fallback, and refresh-failure
behavior have regression tests. SDK build and published types,
SDK/React/router/CLI/Builder typechecks, focused SDK/React/router/CLI
tests, generated route tests, and the browser bundle boundary check
pass. Astra Medium reviewed the architecture and final diff; no
remaining findings.

## SDK package boundaries (2026-10-11)
- [x] Document each SDK package’s architectural role and dependency
constraints. Added root READMEs for `sdk`, `sdk-cli`, and
`sdk-components-registry`; clarified the existing React SDK, React
component, Radix, Router, Remix, and animation package READMEs.
Formatting and diff checks pass.

## Form submission lifecycle
- [x] **Handle rejected submissions when the controller refuses them:**
Consume abort-aware submission promises when a duplicate or disposed
Form submission is refused. Regression tests cover both paths; four
lifecycle tests, SDK typecheck, and diff checks pass.












## Unified Canvas selection reveal (2026-10-12)
- [x] Reduce selection reveal metadata and resolver ceremony while
preserving occurrence validation, owner/source boundaries, and focused
coverage. Replaced ten one-element rule arrays with one optional rule
each, removed the unused fallback field and redundant source union,
combined selector validation with path construction, and bounded the
source search. The optimization removes 24 production lines and 10 test
lines (net), with the shared resolver and derived override layer intact.
Focused Builder tests pass 66/66, Radix 20/20, registry 1/1; SDK, Radix,
registry, and Builder typechecks, scoped lint, formatting, and diff
checks pass. The nested Radix portal check uses a rendered harness
rather than the full Canvas. Independent Astra Medium re-review found no
correctness findings. Pushed commit
`5bc392d3788ef004f9a35c9a12d82dab6087053a`; `builder-pr/whr.staging` SHA
verified.
- [x] Replace duplicated Radix Navigator selection handlers with
declarative component metadata and one Builder-owned, occurrence-aware
selection override resolver. Native Select, HeadSlot, and
AnimateChildren hooks remain for their separate editor behavior.
- [x] Reveal selected `ws:show=false` instances and ancestors through
resolved prop overrides, including Form success/error feedback, while
preserving normal published rendering.
- [x] Resolve fixed `open` and effective item/tab `value` rules for
Dialog, Popover, Tooltip, Select, Collapsible, Tabs, Accordion, and
Navigation Menu. DialogOverlay retains its existing reveal behavior.
Preserve unrelated memory props and Preview behavior.
- [x] Verify resolver, lifecycle, nested Dialog portal mounting,
Collection occurrence isolation, authored expression updates, and Canvas
prop behavior. Local focused Builder suites pass 66/66; Radix 20/20;
registry 2/2. SDK, Radix, registry, and Builder typechecks, scoped lint,
formatting, and diff checks pass. Independent Astra Medium review
findings were fixed and rechecked. Pushed commit
`f8bb1078195e17704ed856c4334683584e983485`; `builder-pr/whr.staging` SHA
verified. These are local automated checks, not live UI verification.





### Remaining Navigator hook migration

- [x] Migrated every remaining Navigator hook consumer: native
Option/Select and tagged `ws:element` use effective option values and
HTML text fallback; HeadSlot uses a fixed self override; AnimateChildren
uses a fixed override only for its exact selection.
- [x] Removed Navigator hook implementations,
`Hook`/`HookContext`/`InstanceData`/`InstancePath` and
`getClosestInstance`, hook subscriptions and store, library
registration, package hook entrypoints/build inputs, and the stale
catalog hook capability field.
- [x] Added behavior coverage for evaluated/bound native values,
empty/zero values, HTML whitespace/NBSP fallback, repeated occurrences,
nearest owner, stale selection, HeadSlot rendering, exact animation
selection, Preview, deselection, and no saved-prop/memory mutation.
Existing Radix runtime index behavior remains covered.
- [x] Completed Astra Medium self-review and independent Astra High
review. Fixed the High review finding: native fallback values must
preserve non-ASCII whitespace.
- [x] Final repository search found no remaining Navigator hook
consumers or registrations.

Validation:

- `pnpm --filter @webstudio-is/builder exec vitest run
app/shared/nano-states/selection-reveal.test.ts
app/shared/nano-states/selection-reveal-render.browser.test.tsx
app/shared/nano-states/components.test.tsx
app/shared/nano-states/props.test.ts
app/shared/nano-states/instance-selection.test.ts
app/canvas/instance-selection-events.test.ts
app/canvas/selected-instance-effects.test.ts` — 7 files, 84 tests
passed; the final resolver rerun passed 14 tests after adding one
additional authored-expression regression (85 tests across the same
files).
- `pnpm --filter @webstudio-is/project-build exec vitest run
src/runtime/component-catalog.test.ts
src/runtime/component-templates.test.ts` — 16 tests passed.
- `pnpm --filter @webstudio-is/sdk-components-react-radix test` — 20
tests passed.
- `pnpm --filter @webstudio-is/builder exec tsc --noEmit` — passed.
- `pnpm --filter @webstudio-is/sdk --filter @webstudio-is/react-sdk
--filter @webstudio-is/sdk-components-react --filter
@webstudio-is/sdk-components-animation --filter
@webstudio-is/sdk-components-registry --filter
@webstudio-is/project-build typecheck` — all 6 packages passed.
- `pnpm exec oxlint --deny-warnings` on all changed TypeScript files,
`pnpm exec oxfmt --check` on all changed files, and `git diff --check` —
passed.

Native HTML options use the browser's effective value semantics
(authored value or text); their runtime has no numeric index default.
Radix items continue using their existing runtime index fallback. All
three consumers migrated; no hook mechanism remains.
2026-10-14 20:03:07 +02:00
Oleg Isonen a4c3b37fc2 fix: Unblock reported MCP workflows and improve issue diagnostics (#6438)
## What this fixes

MCP authors can reuse an existing design token in JSX fragments, create
valid border shorthands with CSS variables, and bypass a font that never
settles during responsive screenshots. Common fixed page titles with
spaced separators are also accepted. Issue reports now include the
project ID and safe, bounded IDs from the failed operation, plus clearer
response and browser-startup diagnostics.

## Scope and tradeoffs

- `token("name")` attaches an existing design token; an unknown name
returns a not-found error. Empty token definitions from Builder/UI
remain definitions and follow the normal conflict rules.
- Border shorthands map a single unresolved `var()` to the remaining
width, style, or color field when the other fields are explicit.
- `waitForFonts: false` skips font readiness but still waits for stable
layout frames. The default remains unchanged.
- Fixed titles such as `Pricing - Plans` stay literal. Addition such as
`first + second` remains an expression. Wrap ambiguous subtraction in
parentheses, for example `(first - second)`. Other ambiguous title forms
may still need an exact report.
- Only recognized operation-level IDs are attached to a public GitHub
issue. Authored values, raw arguments, URLs, and credentials are
excluded. IDs from later successful diagnostic reads are not captured.
- This does not fix the underlying response-transform failure; it
improves future reports for that case.

Closes #6424
Closes #6425
Closes #6434
Closes #6439
Ref #6435
Ref #6430
Ref #6417

## Verification

- [x] Reproduced the privacy, page-expression, empty-token, and
content-mode token-reference regressions with failing tests, then
confirmed the fixes pass. A regression test also covers parenthesized
subtraction.
- [x] Full protocol, template, vision, project-build, and CLI test
suites pass; focused Builder issue-report and fragment tests pass. The
protocol, CLI, and project-build suites were rerun after the CI fix.
- [x] Relevant package typechecks, lint, package-boundary check, fixture
pipeline, generated API check, and generated docs check pass. Fixture
output has no diff.
- [x] Reviewed the authoritative MCP documentation and regenerated the
API metadata and MCP docs.
- [x] CSS parser regressions cover border, each side shorthand, and
unresolved variables in width, style, and color positions; css-data
tests, typecheck, and lint pass.
- [x] Fixed the local evaluation runner to pass the task and Webstudio
MCP tools to Luna/low. Each of the six fixtures passed at least once;
focused MCP/evaluator tests, CLI and project-build typechecks, lint, and
formatting pass.
- [x] The complete Luna/low suite passed all six fixtures in two
consecutive sequential runs. Baselines remain incompatible with this
model/effort override and were not updated.

## Follow-up

- If another fixed-title form still fails, use the new project-level
report context and the exact title shape to narrow the parser case.
2026-09-25 21:01:43 +02:00
Oleg Isonen 60c549c46f fix: preserve local styles and improve issue diagnostics (#6408)
Fixes #6386

## Summary

- Keep local `ws:style` declarations after `ws:tokens` so instance
styles override token defaults.
- Capture privacy-safe HTTP timing/status, exceptional session namespace
states, preview freshness/version agreement, and validation paths in
issue reports.
- Derive reportable validation roots from the public operation catalog
instead of maintaining a duplicate allowlist.
- Keep captured failures current by clearing them after a successful
tool call.

## Verification

- Protocol issue-report tests: 3 passed
- Project build MCP: 128 tests passed
- CLI project session: 27 tests passed
- Builder issue reporter: 5 tests passed
- Relevant package typechecks passed
- Generated API metadata is clean
2026-09-17 20:13:27 +02:00
Oleg Isonen 05706b5425 feat: Make Content Block templates the MDX authoring interface (#6307)
Closes #6306

## Outcome

Content Block templates are now the public component model for MDX
authoring.

- Standard Markdown resolves through matching Content Block templates.
- PascalCase JSX resolves to a named template first, then to a
registered component.
- Adding or changing a template immediately affects matching MDX,
including content authored before that template existed.
- Every supported standard MDX element is present in new Content Blocks
by default.
- Template `name` is a stable PascalCase JSX identifier. `label` remains
optional display text only.
- Lowercase JSX represents native HTML and SVG without exposing
`ws.element` in authored syntax.
- Unknown, ambiguous, and unsafe JSX stays in source, produces a
source-ranged Builder diagnostic, and is omitted from published output.
- Frontmatter images preserve exact `$ref` values and use the Asset
description for `alt` text.
- Keyboard and mouse insertion from the slash menu remove the trigger
slash and preserve the selected template.

## Public MDX contract

- Markdown is canonical for standard prose.
- Lowercase JSX creates native elements:
`<section><p>Text</p></section>`.
- PascalCase JSX uses templates and registered components:
`<PromotionCard />`, `<Heading tag="h1">Title</Heading>`, and `<YouTube
src="https://youtu.be/KI5JHpzBK1s" />`.
- Component-name collisions use stable qualified identifiers such as
`Checkbox` and `RadixCheckbox`.
- A self-closing template keeps its designed default subtree. An
explicitly paired empty template clears it.
- Static quoted values and bare booleans are supported.
- Imports, exports, expressions, spreads, functions, executable
JavaScript, unsafe attributes, `<$.*>`, and authored `<ws.element>` are
rejected.
- `ws` remains only for compiler primitives with no React component.

## Implementation

- [x] Add stable, unique template names and keep them separate from
labels.
- [x] Parse and validate Markdown and JSX through their ASTs with
precise source ranges.
- [x] Share component identity between template JSX, Content MDX,
Builder materialization, reload, copy, and publication.
- [x] Resolve templates live after add, edit, rename, reorder, removal,
undo/redo, collaboration updates, and reload.
- [x] Preserve authored props, children, Asset references, template
defaults, and the exact slash-menu template choice.
- [x] Make one typed MDX semantic inventory drive default templates and
slash-menu eligibility.
- [x] Move component-dependent template assembly into the component
registry.
- [x] Replace production `$`, `animation`, and `radix` JSX namespaces
with direct component imports.
- [x] Keep only explicit compiler primitives in `ws` template JSX.
- [x] Migrate repository templates, fixtures, stories, examples, MCP
guidance, CLI guidance, and generated outputs.
- [x] Add Builder unit and Playwright coverage for authoring,
diagnostics, insertion, synchronization, reload, and publication.
- [x] Review and update the authoritative Content Block and generated
MCP documentation.

## Compatibility

- Persisted project data continues to use namespaced component IDs and
`ws:element` for generic HTML elements. No project-data migration is
required.
- Legacy `<ws.element ws:tag="…">`, `<ws.element ws:name="…">`, `<$.*>`,
`<animation.*>`, and `<radix.*>` input remains readable during the
compatibility window.
- Current serializers and repository-owned sources emit only canonical
Markdown, lowercase intrinsic JSX, or PascalCase identifiers.
- Untouched user MDX is not rewritten automatically. Legacy syntax is
canonicalized when its owning source is edited.
- Deferred follow-up: remove legacy namespace readers and proxy support
only after the canonical writer is deployed and existing projects are
backfilled. This is intentionally outside this PR.

## Verification

- [x] CI package tests, typechecks, lint, package boundaries, generated
assets, migrations, and size checks pass.
- [x] All six Builder E2E shards pass.
- [x] Fixture checks pass on Ubuntu and Windows.
- [x] Targeted local Content Engine, Template, SDK, registry, Project
Build, migration, Builder, component, CLI, and Playwright tests pass.
- [x] Runtime contracts, CLI docs, and MCP docs were regenerated from
their owning sources.
- [x] Local fixture generation completed without an unexpected fixture
diff.
- [x] Documentation impact reviewed and included in this PR.
- [x] Review and approve the intentional List visual snapshot change.
- [x] Run the complete agent evaluation suite after explicit approval.
- [x] Run the network fixture link/sync pipeline against the matching PR
deployment after explicit permission.

## Known visual change

The generated List story now targets its actual default `ul` element
instead of `ol`, so the default list styling appears in the visual
snapshot. The visual change was reviewed and the approved visual check
passes.
2026-09-06 15:19:54 +02:00
Oleg Isonen f9681dc30f feat: MDX-backed Content Blocks (#6234)
## Summary

Supersede #6182 with a clean implementation of MDX-backed Content Blocks
on top of the unchanged `main` Content Block editing model. A connected
`.mdx` Asset changes where authored children are loaded and saved; it
must not change how Content mode inserts, edits, selects, moves, copies,
or deletes instances.

The previous implementation remains available in
`agent/mdx-content-block-6124` and
`agent/mdx-content-block-6124-review`. Reuse its independently tested
parsers, converters, schemas, Asset repository, template resolution,
publication, MCP, and UI work where those pieces fit the corrected
architecture. Do not port its per-operation storage routing, projected
editing stores, MDX undo/history, or atomic multi-root machinery.

Ref #6124
Ref #6182
Closes #6261

## Locked behavior

- `.md` keeps its existing Markdown behavior; `.mdx` enables MDX.
- Standard Markdown structures are stored as Markdown. JSX is used only
for structures Markdown cannot represent losslessly.
- Generic elements use `<ws.element ws:tag="…">`; template instances use
`<ws.element ws:name="…">`.
- Invalid, unsafe, or unsupported MDX never prevents editing the file or
publishing the project. Builder and publication use the same recovery
result: recoverable nodes are skipped with source-located warnings,
while an unrecoverable document renders no authored file content in
either environment.
- Connecting a source never serializes existing Content Block children
into the file. **Connect** removes ordinary persisted children,
preserves Templates, sets `src`, and uses file content.
- Disconnecting resets `src`, discards derived instances, leaves the
Content Block empty, and never changes the Asset.
- While connected, normal Content Block mutations update in-memory
instances synchronously. Asset persistence happens afterward and never
blocks rendering.
- Genuine external revision conflicts use the standard reload-required
flow. Rapid local edits are ordered/coalesced and must never conflict
with each other.
- Repeated scopes use Content Block ID plus Collection item identity.
- Multiple Content Blocks may intentionally share one MDX Asset. They
share one revisioned document session and edits propagate to every
connected occurrence.
- Missing templates show warnings/placeholders only in Builder and are
skipped on published sites.

## Reuse strategy

- [x] Audit all 62 original implementation commits and 5 later
review/fix commits
- [x] Reuse the shared Content Engine Markdown/MDX AST, parser,
serializer, diagnostics, reference discovery, and tests
- [x] Reuse SDK source schemas, instance-name utilities, unique template
naming, and source-integrity validation
- [x] Reuse template resolution/materialization and authored-tree
conversion after removing storage-operation coupling
- [x] Reuse revisioned Asset content repository, authorization, limits,
and browser/server transport
- [x] Reuse publication, document-graph, CLI, MCP, source-control UI,
frontmatter UI, and E2E work where contracts remain valid
- [x] Reuse tests as behavioral specifications; rewrite tests that
assert the rejected projection/routing architecture
- [x] Do not port per-operation MDX routing, alternate projected
mutation stores, semantic reconciliation after each command, MDX Builder
history/undo, atomic multi-file rollback, or canvas loading/saving UI


## Old-branch porting map — do not rewrite

Before implementing any checklist batch, inspect the named
implementation and tests on `agent/mdx-content-block-6124-review`. Move
the implementation and its tests first; change only the boundary that
conflicts with the rebuilt architecture. Do not create a second
implementation under a new name. When a test encodes the rejected
projected-store architecture, retain its behavioral assertion and
replace only its setup.

### Already ported

- `parseMarkdownAst`, `createMdxDocumentFromMarkdown`,
`parseMdxDocument`, `serializeMdxDocument`, `preferMarkdownSyntax`,
`discoverMdxBodyAssetReferences`, `discoverMdxAssetReferences`, and
`previewMarkdownToMdxConversion`
- Asset-reference, byte-stream, document-graph, Markdown, and MDX
helpers included in commit `76bc388ba9`
- Tests: the corresponding `packages/content-engine/src/**/*.test.ts`
files already on this branch

### Port with their existing tests

| Area | Functions to move or adapt | Tests to move with them |
| --- | --- | --- |
| Source contract and integrity | `parseContentBlockSourceProp`,
`getContentBlockSource`, `isEqualContentBlockSource`,
`formatContentBlockSourceIntegrityIssue`,
`getContentBlockSourceIntegrityIssues`,
`allocateUniqueContentBlockTemplateName` |
`packages/sdk/src/content-block.test.ts`,
`packages/sdk/src/schema/content-block.test.ts` |
| Existing instance/name utilities | `findChildReferenceIndex`,
`findParentInstanceReference`, `getInstanceName` |
`packages/sdk/src/instances-utils.test.tsx` |
| Template naming in mutations |
`assignUniqueBlockTemplateNamesMutable`,
`findBlockTemplateNameCollision`, `getSourceBackedBlockTemplateContext`,
`getBlockTemplateNameConfirmation`, `getBlockTemplateInsertionIndex` |
`packages/project-build/src/runtime/block.test.ts` |
| Content-mode property permissions | `getContentModePropEligibility`;
preserve its distinction between editable, design-only, incompatible,
and unknown props |
`packages/project-build/src/runtime/content-mode-permissions.test.ts` |
| Template resolution | `resolveMdxTemplates` |
`packages/project-build/src/runtime/mdx-template-resolution.test.ts` |
| Template materialization | `createMdxScopeIdGenerator`,
`materializeMdxTemplates` |
`packages/project-build/src/runtime/mdx-materialization.test.ts` |
| MDX → instances and instances → MDX |
`isMaterializedMdxAuthoredContentRoot`, `preserveMdxAuthoredContentIds`,
`materializeMdxAuthoredContent`, `reconcileMdxAuthoredContent`,
`omitTransientEmptyParagraphs`, `serializeMdxTemplateInsertion` |
`packages/project-build/src/runtime/mdx-authored-content.test.ts`;
retain the pure serialization cases from `mdx-storage-adapter.test.ts` |
| Revisioned Asset I/O | `createAssetContentRevision`,
`readAssetContentBytes`, the package-owned
`createHttpAssetContentRepository`, the thin Builder
`createBuilderHttpAssetContentRepository`, `createAssetContentBridge`,
`initAssetContentBridge`, `getAssetContentBridge`; keep one repository
implementation rather than copying the old Builder duplicate |
`packages/asset-uploader/src/content-repository.test.ts`,
`apps/builder/app/builder/shared/assets/mdx-content-repository.test.ts`,
`apps/builder/app/shared/asset-content-bridge.client.test.ts` |
| Source lifecycle planning | `createContentBlockBodyRemoval`,
`prepareContentBlockConnect`, `prepareContentBlockDisconnect`,
`prepareContentBlockSwitch`, `ContentBlockSourceRevisionConflictError` |
`packages/project-build/src/runtime/content-block-source-lifecycle.test.ts`
|
| Per-Asset revision/session behavior | `getContentBlockSessionSource`,
`isContentBlockSessionSourceCommitted`; reuse the loading,
authorization, revision, queue, conflict, retry, cancellation, and
source-replacement internals of `createMdxAssetEditingSession`, but key
the rebuilt session by Asset rather than Content Block/render root |
Port the applicable cases from
`packages/project-build/src/runtime/mdx-asset-session.test.ts`; replace
root-projection setup with in-memory snapshot setup |
| Template migrations | `validateMdxTemplateMigrationConfirmation`,
`planMdxTemplateMigration` |
`packages/project-build/src/runtime/mdx-template-migration.test.ts` |
| Publication | `createPublishedMdxMaterializationCache`,
`materializePublishedMdx`, `getPublishedMdxContentDatabaseMaxBytes`,
`getDynamicPublishedMdxSourceBlockIds`, `hasDynamicPublishedMdxSources`,
`createPublishedBuildContentCompilationPlan`,
`resolvePublishedMdxDependencyClosure`,
`resolvePublishedMdxAssetCandidates` |
`packages/project-build/src/runtime/published-mdx.test.ts`,
`packages/project-build/src/content-database.test.ts` |
| Build/import integrity | `assertContentBlockSourceIntegrity` and the
Content Block cases in
`getBuildIntegrityIssues`/`formatBuildIntegrityIssue` |
`packages/project-build/src/build-integrity.test.ts` |
| MDX paste/MCP insertion | `insertMdxText` and its input/result
schemas; retain parsing, confirmation, template-resolution, and
insertion-index logic, then invoke the normal mutation path |
`packages/project-build/src/runtime/mdx-paste.test.ts` |
| Builder source control | `createContentBlockSourceController`,
`createBuilderContentBlockSourceController` and the existing
source-control components | `content-block-source-controller.test.ts`,
`content-block-source.test.tsx` |
| Diagnostics | `formatContentBlockDiagnostic`,
`deduplicateContentBlockDiagnostics`, `takeNewContentBlockDiagnostics` |
Port only their focused cases from
`apps/builder/app/shared/content-block-content.test.ts` |

### Explicitly do not port

- `projectContentStorageChanges`, `createContentStorageProjection`,
`createContentStorageSelectorProjection`,
`executeContentStorageMutation`, `executeContentStorageTextReplacement`,
`executeContentStoragePropMutation`, and
`executeContentStorageStructuralMutation`
- `applyMdxContentStorageChanges` and `prepareMdxContentStorageWrites`;
replace them with one post-mutation snapshot serializer built from the
reusable pure `reconcileMdxAuthoredContent` converter
- `createContentBlockApplicationOperations` and the persistence-plan
wrappers that route individual editing commands
- Builder `$runtime*` projected stores, materialized mutation functions,
alternate selector/tree helpers, pending projected-change state, and
MDX-specific mutation sequences from `content-block-content.ts`
- Canvas loading/saving/open-file presentation state, MDX history/undo,
and atomic rollback machinery

## Implementation checklist

### 1. Preserve the `main` editing architecture

- [x] Keep normal synchronous insert, text, Enter, slash menu, property,
delete, move, wrap, copy/paste, selection, and focus mutations unchanged
- [x] Materialize connected content through the same runtime instance
interfaces used by ordinary Content Blocks
- [x] Keep connected authored children in memory and exclude them from
persisted build data
- [x] Route persistence only after a synchronous mutation has updated
the canvas
- [x] Never wait for Asset loading, conversion, or saving before
rendering an interaction
- [x] Add no MDX-specific implementation to individual editing commands

### 2. Shared Markdown and MDX conversion

- [x] Parse Markdown and the Markdown portion of MDX through one shared
AST and plugin configuration
- [x] Implement one shared MDX-to-instances converter and one shared
instances-to-MDX converter
- [x] Serialize headings, paragraphs, links, images, lists, tables, task
lists, blockquotes, thematic breaks, code, emphasis, strong,
strikethrough, and other supported GFM structures as Markdown
- [x] Serialize only non-lossless/custom structures as Webstudio JSX
- [x] Preserve nested Markdown in MDX elements, frontmatter, comments,
tight/loose list semantics, code whitespace, reference links, and Asset
references
- [x] Omit transient empty draft paragraphs instead of emitting
`<ws.element ws:tag="p" />`
- [x] Preserve unsupported source text and authored metadata whenever it
cannot be converted safely
- [x] Add deterministic round-trip and canonical-output coverage for
every supported Markdown structure

### 3. Invalid and unsafe MDX

- [x] Preserve file source exactly; never silently repair or rewrite
invalid source
- [x] Ignore imports, exports, expressions, executable JavaScript,
invalid properties, and other unsupported nodes when they can be
isolated
- [x] Continue rendering valid siblings and report every skipped subtree
once with filename, line, and column
- [x] Use the same recovery pipeline in Builder and publication so both
render the same authored result for the current file revision
- [x] When structural syntax prevents recovery, render no authored file
content in Builder or publication instead of retaining a stale
materialization
- [x] Skip an unrecoverable Content Block on published sites and
continue the build with a warning
- [x] Keep invalid and unsafe files editable and reconnectable
- [x] Enforce UTF-8, byte-size, depth, node-count, property-count, and
generated-output limits with source-located diagnostics where possible

### 4. File formats and Asset creation

- [x] Keep `.md` parsing and raw-HTML behavior unchanged
- [x] Support creating, opening, editing, deleting, and recreating
`.mdx` files through the normal text-file UI
- [x] Open `.mdx` in the same editor experience as `.md`
- [x] Always create an independent empty MDX Asset even when identical
empty files exist
- [x] Allow renaming `.md` to `.mdx` without converting or rewriting its
contents; after rename, handle invalid contents through the normal MDX
diagnostics and recovery pipeline
- [x] Update an already-open MDX editor immediately after canvas
persistence
- [x] Bound read/write buffering and verify the returned Asset identity
and revision

### 5. Source contracts and lifecycle

- [x] Support direct Asset bindings and dynamic expression bindings
- [x] Keep source mutation controls available only in Design mode; show
the connected filename and **Open** action read-only in Content mode
- [x] Reject duplicate source props, unsupported prop types, missing
Assets, non-MDX Assets, and unauthorized Assets
- [x] Analyze selected file diagnostics before confirmation without
modifying the block or file; MDX syntax diagnostics do not block
connection
- [x] For a non-empty block, show **Abort** and **Connect** and explain
that ordinary children will be replaced
- [x] On connect, remove every non-template child, preserve Templates,
set `src`, and materialize file content
- [x] Replace disconnect handoff with normal `src` reset; discard
derived content and leave the block empty
- [x] Remove disconnect confirmation and conversion; resetting a
repeated binding leaves every occurrence empty
- [x] Switching sources validates/loads the target before replacing the
usable binding and leaves both files unchanged
- [x] Revalidate Asset revision and project version when confirmation is
submitted
- [x] Make repeated connect/disconnect operations idempotent
- [x] Prevent disposed, stale, or out-of-order loads from replacing the
newest source
- [x] Keep loading, saving, conflict, empty-file, and open-file actions
off canvas

### 6. Asynchronous persistence and conflicts

- [x] Snapshot the current in-memory authored root after normal
mutations
- [x] Serialize/coalesce rapid edits per resolved Asset and always
prepare against the latest local snapshot
- [x] Allow independent Assets to save concurrently without scope
leakage
- [x] Use one revisioned document session and one ordered save queue per
Asset, even when multiple Content Blocks or render scopes reference it
- [x] After a shared Asset changes, rematerialize every connected
occurrence without persisting their generated instances to project data
- [x] Pin the resolved Asset and exact loaded revision for each write
- [x] Recheck authorization immediately before storage
- [x] Ensure local queued edits never produce stale-revision conflicts
with each other
- [x] On a genuine external revision conflict, preserve local content,
stop writes, and enter the standard reload-required state
- [x] Keep failed non-conflicting writes retryable without discarding
local content
- [x] Finish already accepted persistence writes after a Content Block
unmounts or switches source
- [x] Cancel obsolete loads, materializations, and UI updates after
unmount, reload, or source switch
- [x] Dispose project-scoped roots, queues, parse caches, failure state,
generations, and session subscriptions when the Asset session is
replaced
- [x] Invalidate Asset resources and synchronize open file editors after
successful writes
- [x] Keep multi-root persistence ordered, serial, and non-atomic;
report partial completion without rollback
- [x] Do not add Asset-history undo/redo; native text-editor undo
remains local to the open editor

### 7. Content mode parity

- [x] Empty connected blocks remain selectable and expose the normal
Content-mode `+` affordance
- [x] First insertion renders immediately
- [x] Enter creates and focuses the next paragraph immediately
- [x] Slash commands work in newly created empty paragraphs
- [x] Deleting one selected instance deletes exactly that instance
- [x] Mouse and keyboard moves use the same operation and permissions
- [x] Preserve sibling reorder, nested reparenting, and moves between
editable Content Blocks where `main` permits them
- [x] Preserve content-editable props such as Link `href`
- [x] Preserve copy, paste, cut, multi-cut, wrapping, unwrapping, grid
insertion, selection, Navigator order, and canvas order
- [x] Cover mutation routing centrally, retain the full unconnected
Builder suite, and exercise representative connected interactions in
real Builder E2E

### 8. Template names and permissions

- [x] Resolve only exact user-facing names from the direct flat
Templates list
- [x] Enforce unique names within each Templates list
- [x] Reject duplicate names during rename with inline validation
- [x] Allocate unique names during insert, paste, duplicate, move, wrap,
and unwrap
- [x] Apply valid template properties using the same permissions as
Content mode
- [x] Preserve and warn about ignored design-only, invalid, or stale
properties
- [x] Preserve template shell provenance without serializing expanded
template internals
- [x] Generate deterministic materialized IDs from source identity,
render scope, and authored usage path so focus/selection survives saves
- [x] Warn before renaming or deleting referenced templates; files
remain unchanged
- [x] Provide shared AST-based rename/remove migration operations for
MCP-assisted multi-file cleanup

### 9. Scope, frontmatter, and diagnostics

- [x] Scope materialized content by Content Block ID plus exact
Collection-item/render context
- [x] Prevent repeated instances and dynamic candidates from leaking
generated IDs, template resolution, or diagnostics across scopes while
sharing the underlying Asset source and revision where applicable
- [x] Remove frontmatter editing from Settings; edit the exact YAML
through the MDX Asset file editor
- [x] Preserve the exact frontmatter source byte-for-byte during canvas
body saves
- [x] Route canvas body changes and complete-file editor saves through
one revisioned per-Asset coordinator
- [x] Show a source warning indicator in Settings and precise
source-located diagnostics in the Asset editor
- [x] Deduplicate repeated diagnostics without losing distinct locations
- [x] Keep unresolved-template placeholders selectable and accessible
only in Builder

### 10. Asset references, transfer, and integrity

- [x] Discover and rewrite Asset references in frontmatter, Markdown
links/images, and MDX properties
- [x] Preserve Asset references while editing unrelated content
- [x] Include referenced Asset and template revisions in
dependency/cache identity
- [x] Route referenced Assets through published URL rewriting
- [x] Validate Content Block source integrity during project import,
fragment/page transfer, copy/paste, build integrity, and pre-publish
audit
- [x] Allow multiple Content Blocks and dynamic render scopes to
reference the same MDX Asset without treating the connection as an
integrity error
- [x] Preserve Templates when copying a connected Content Block
- [x] Preserve direct and dynamic Content Block source bindings when
copying or pasting within the same project
- [x] When copying only one rendered Collection item, resolve that
item’s current MDX Asset and replace the copied Content Block’s dynamic
source with a direct binding to that same Asset
- [x] When copying an entire Collection, preserve the Collection, its
instances, and its dynamic source bindings
- [x] Preserve the complete MDX source and frontmatter by reference;
copying a connected block must not rewrite the shared file
- [x] During cross-project transfer, copy every discoverable distinct
bound MDX Asset and rewrite transferred bindings to the destination
Assets
- [x] If some cross-project dynamic candidates cannot be discovered or
copied, continue with successful copies, preserve what can be
represented safely, and warn about skipped candidates
- [x] Revalidate source revisions and the destination project before
applying a cross-project transfer
- [x] If cross-project Asset creation or project insertion fails, leave
the source project unchanged and clean up any newly reserved destination
Asset
- [x] Use the same transfer operation through Builder and MCP

### 11. Dynamic sources and publication

- [x] Resolve dynamic bindings once per render/save scope
- [x] Require finite, discoverable candidate sets for publication and
load no unrelated MDX
- [x] Warn and skip unbounded, missing, unreadable, malformed, or unsafe
sources without failing the complete build
- [x] Materialize only rendered Content Blocks; ignore Templates
definitions and `src` on non-Content-Block instances
- [x] Skip unresolved template subtrees while keeping valid siblings
- [x] Include selected template styles, fonts, resources, unresolved
names, and referenced Asset revisions in generated dependency output
- [x] Keep draft-only MDX Assets out of publication retention

### 12. Builder, MCP, CLI, and shared operations

- [x] Use the same converters, source lifecycle, revision checks,
persistence service, diagnostics, and template migrations in Builder and
MCP
- [x] Keep frontmatter editing file-based; retain whole-frontmatter
runtime operations only where compatibility requires them
- [x] Avoid Builder-only or MCP-only mutation/conversion implementations
- [x] Regenerate runtime/API contracts from their source definitions
- [x] Update CLI Content Engine reference and generated MCP
documentation

### 13. Verification and delivery

- [x] Add regression tests that reproduce failures from the previous
implementation, including double deletion and blocked rendering
- [x] Cover two Content Blocks sharing one Asset: edits from either
block serialize once, update both canvases, preserve scope-specific
Templates, and cannot overwrite a newer local snapshot
- [x] Verify new tests fail against the broken implementation and pass
with the corrected behavior
- [x] Run SDK, Content Engine, Asset Uploader, Project Build, Builder,
React SDK, CLI, Protocol, and HTTP Client affected suites and typechecks
- [x] Run real Builder E2E for source lifecycle, live file refresh,
empty insertion, Enter/slash focus, Link props, delete, keyboard
reorder, switching, disconnect, conflicts, unresolved templates, dynamic
sources, and repeated scopes
- [x] Regenerate shared project fixture bundles and verify a second
generation leaves the tree clean
- [x] Run lint, package boundaries, `pnpm oxfmt --check`, and `git diff
--check`
- [x] Review and update authoritative `docs/` content and navigation
- [ ] Run the complete agent evaluation suite — not run because it
requires separate explicit approval

### 14. Content-mode parity and disconnect handoff

- [x] Allow keyboard reordering in Content mode through the normal move
command while keeping the editable Content Block boundary
- [x] Let already accepted Asset writes drain independently before
resetting the source; disconnect does not load, flush, or convert the
file
- [x] Remove MDX-to-instance copying from disconnect
- [x] Reset `src`, remove the derived tree, and leave the ordinary
Content Block empty
- [x] Preserve automatic text-editor focus after inserting editable
content from the template menu
- [x] Remove Builder frontmatter-save UI and report complete-file save
failures through the Asset editor
- [x] Add regression coverage for keyboard movement, disconnect
teardown, source switching, insertion focus, and paragraph deletion
focus restoration
- [x] Run the complete six-scenario MDX Content Block Builder E2E suite

### 15. Final architecture review fixes

- [x] Keep one canonical ordered pending document per Asset so shared
Content Block occurrences cannot conflict with one another
- [x] Merge ordered local edits from different occurrences while
preserving text changes, insertions, deletions, and sibling reordering;
keep external revisions on the strict reload-required path
- [x] Centralize typed render-scope parsing, repeated-occurrence
detection, scoped variable lookup, and dynamic Asset resolution across
Canvas, Settings, source lifecycle, and copy/paste
- [x] Capture the owning Asset session when a save is accepted so normal
unmount/source-switch teardown cannot redirect or lose it
- [x] Dispose project-owned roots, queues, caches, generations, failure
state, and subscriptions when replacing the project Asset session
- [x] Add regression coverage for same-Asset edits from two occurrences,
same-node last-write ordering, structural reorder, insertion
preservation, scoped source resolution, and project cleanup
- [x] Re-run affected full unit suites, typechecks, production build,
static checks, and all six MDX Content Block Builder E2E scenarios


### 16. Final persistence and transfer hardening

- [x] Drain every accepted per-Asset edit before project-session
teardown while retaining the owning authorization context
- [x] Drain canvas serialization queues and revisioned Asset writes
before pre-publish audit and publication
- [x] Block publication on a queued serialization failure or Asset
revision conflict
- [x] Copy actual Asset bytes during cross-project paste, preserve MDX
reference names, rewrite direct and Collection-backed Asset IDs, and
clean up newly created Assets when insertion fails
- [x] Include every resolved dynamic MDX source when copying an entire
Collection and warn while preserving successful copies when individual
Assets cannot be transferred
- [x] Keep MDX editor save failures visible with retry and route
conflicts through the standard reload-required flow
- [x] Give keyboard nested reparenting the same editable Content Block
boundary used by pointer movement
- [x] Add regression coverage and rerun the complete Content Engine and
Builder unit suites plus type, lint, formatting, package-boundary, and
diff checks

## Current rebuild status

- The connected Content Block now owns one complete MDX document:
frontmatter feeds designed-shell bindings and the explicit Body outlet
owns the authored MDX body.
- Builder and published output resolve the same frontmatter document
graph, including nested document references and Asset references.
- Direct frontmatter text/property edits use the existing ordered
per-Asset session; referenced-document values remain file-owned.
- Repeated Collection occurrences keep isolated runtime shell/Body
identities and occurrence-specific document values.
- The local blog article layout has been upgraded to the document/Body
model and its bindings validate cleanly.
- Full affected package suites and typechecks pass; every new regression
was demonstrated fail/pass.
- `pnpm lint`, package boundaries, `pnpm oxfmt --check`, generated
API/docs checks, fixture builds, and `git diff --check` pass.
- Full fixture synchronization is blocked by the development API
rejecting this local CLI version before fixture generation; existing
fixture bundles build successfully.
- Complete agent evaluations remain pending explicit approval.

### 17. Treat Content Block source as a property

- [x] Declare Content Block `src` in component metadata as a Design-mode
initial property labeled **Source**
- [x] Add the short property description: “Create an .mdx file in
Assets, then select or bind it here.”
- [x] Accept only a direct Asset or an expression resolving to an Asset
ID; reject URL/string sources, missing Assets, and non-MDX Assets
- [x] Extract and reuse the Image-style Asset picker and binding
presentation without duplicating source-control UI
- [x] Render Source through the normal properties section and remove the
separate source-section placement and special `src` filtering
- [x] Route Source changes through Content Block connect, switch, and
disconnect lifecycle operations instead of generic prop updates
- [x] Keep connect confirmation, Open, diagnostics, and persistence
state attached to Source; remove frontmatter and disconnect UI
- [x] Add regression coverage for direct Assets, dynamic Asset bindings,
invalid resolved values, lifecycle operations, and Design/Content mode
visibility
- [x] Re-review authoritative `docs/` content for the Source-property UI
change and update it if required

### 18. Source property UX

- [x] Show one full-width bindable Connect .mdx file button while
disconnected
- [x] Show equal-width filename and Open buttons while connected
- [x] Switch files from the filename button through the source lifecycle
- [x] Override PropertyLabel reset and Alt-click with the disconnect
lifecycle
- [x] Preserve confirmation, pending, failure, repeated-scope, and
dynamic-source behavior
- [x] Add regression coverage for connect, switch, binding, reset, and
disconnect
- [x] Update Content Block documentation
- [x] Run affected tests, typecheck, lint, formatting, package
boundaries, and diff checks


### 19. Reduced file-owned frontmatter follow-up

This checklist supersedes the earlier structured-frontmatter and
disconnect-copy behavior. Frontmatter remains file-owned and is edited
only in the MDX Asset editor.

#### A. Remove structured frontmatter UI

- [x] Remove the JSON frontmatter editor and unused Builder
controller/state
- [x] Keep source mutation Design-mode-only; show connected Source
read-only in Content mode and allow authorized editors to open the
complete file
- [x] Retain runtime frontmatter APIs only where compatibility requires
them

#### B. Preserve source and separate validation

- [x] Preserve the exact frontmatter envelope during every canvas body
save without parsing/stringifying YAML
- [x] Preserve comments, ordering, quoting, whitespace, BOM, newline
style, delimiters, and invalid but closed YAML
- [x] Keep the existing unsupported-node/property MDX recovery; do not
add an ad hoc partial parser
- [x] Parse body and frontmatter independently so either valid half
remains usable
- [x] Keep bounded Asset-query frontmatter extraction unchanged
- [x] Treat structurally malformed MDX as having no safe body tree

#### C. Share Asset state with the raw editor

- [x] Route canvas and complete-file changes through one ordered,
non-blocking per-Asset coordinator
- [x] Update canvas and clean raw editors immediately from shared
in-memory source
- [x] Reject stale dirty-editor saves, preserve unsaved text, and
require reload; never force-overwrite
- [x] Drain accepted writes after source replacement/disconnect and
report background failures globally
- [x] Dispose sessions only after they have no consumers and no queued
work

#### D. Connect, replace, and disconnect

- [x] Load and diagnose before connect; block only
unreadable/unauthorized/non-MDX Assets
- [x] Show **Abort / Connect** for non-empty blocks, including
diagnostics and the recoverable-content warning
- [x] Set `src` and delete persisted children through one runtime
mutation
- [x] Switch to a loaded source without waiting for the previous Asset's
network save
- [x] Remove disconnect confirmation and conversion
- [x] Reset `src` immediately, discard the derived tree, leave the block
empty, and preserve the Asset
- [x] Resetting a bound source leaves every repeated occurrence empty

#### E. Dynamic and shared sources

- [x] Keep one source/revision state per Asset and a separate derived
instances tree per occurrence
- [x] Resolve Templates independently per Content Block and preserve
Builder-only missing-template placeholders
- [x] Resolve bound `src` from the selected Collection occurrence
- [x] Make **Open** use the occurrence's resolved Asset; update the
filename and disable Open when unresolved
- [x] Add direct/bound tests including Collection item A opening `a.mdx`
and item B opening `b.mdx`

#### F. Diagnostics, MCP, and publication

- [x] Show an accessible warning icon beside Source with the first
diagnostic, line/column, and remaining count
- [x] Show precise YAML/MDX diagnostics in the Asset editor without
repetitive canvas/toast UI
- [x] Reuse parser/diagnostic/revision contracts in Builder, build,
publication, and MCP
- [x] Keep MCP frontmatter editing file-based and return structured
diagnostics for complete-source writes
- [x] Emit build warnings; published sites skip invalid/unsupported
content without diagnostic UI

#### G. Verification and documentation

- [x] Add fail/pass regressions for source preservation, independent
validation, stale raw saves, lifecycle, dynamic Open, shared Assets, and
cleanup
- [x] Run affected tests, typechecks, lint, formatting, package
boundaries, fixtures, and diff checks
- [x] Open Markdown and MDX editors at twice the default dialog size
with the right-side preview visible initially
- [x] Document canvas body editing, file-owned frontmatter, Content-mode
Asset access, and cloning an MDX Asset for a new post
- [x] Perform a final correctness, data-loss, DRY, simplification,
accessibility, and performance review
- [x] Keep this checklist synchronized with implementation status

### 20. Content-mode component properties

- [x] Show a connected Content Block Source read-only in Content mode
with an active **Open** action
- [x] Keep source switching, binding, reset, and disconnect available
only in Design mode
- [x] Expose HTML Embed code and preserve Code Text's code-editor
metadata on authored Text Content
- [x] Audit every registered component and expose authored payload while
keeping design, structure, and behavior controls in Design mode
- [x] Keep media dimensions and Code Text theme Design-only while
preserving adapter-owned intrinsic Image metadata during MDX round trips
- [x] Add exact component-permission and regression coverage
- [x] Update authoritative component documentation
- [x] Run affected full suites, typechecks, formatting, lint, package
boundaries, and generated contract/documentation checks


### 21. Designed document shell and Body outlet

- [x] Add one structural **Content Block Body** outlet and keep legacy
Content Blocks without it compatible
- [x] Materialize and persist only `document.body` children through the
Body outlet while preserving the surrounding designed shell and
Templates
- [x] Expose the connected document frontmatter through a Content Block
`document` parameter for shell text and property bindings
- [x] Write direct frontmatter-bound text and properties back through
the same ordered per-Asset session without blocking canvas mutations
- [x] Resolve document `$ref` dependencies and Asset references
consistently in Builder and published output
- [x] Keep referenced-document values read-only on canvas and direct
editors to the referenced file
- [x] Isolate repeated Collection occurrences while retaining
occurrence-specific source, frontmatter, shell, and Body content
- [x] Render the same designed shell, frontmatter values, and Body
substitution in generated/published output
- [x] Upgrade the local blog article layout to bind title, excerpt,
dates, author data, and images from frontmatter and render MDX through
Body
- [x] Add fail/pass regressions for nested Body discovery, lifecycle,
permissions, repeated values, document references, and generated shell
rendering
- [x] Update authoritative Content Block documentation
- [x] Run affected tests and typechecks, formatting, lint, package
boundaries, generated API/docs checks, and fixture builds


### 22. Final nested references and frontmatter editing

- [x] Keep direct frontmatter-bound text and property controls editable
in Content mode without exposing or replacing their bindings
- [x] Keep referenced-document values read-only and preserve legacy
instance-property name normalization
- [x] Render direct frontmatter text bindings through the plain-text
editing path
- [x] Resolve nested legacy and structured Asset references from
referenced Markdown/MDX frontmatter
- [x] Preserve missing Asset references, emit structured
`ASSET_NOT_FOUND` diagnostics, and keep them out of the document graph
- [x] Add fail/pass regressions for the real Settings control and
root/nested missing Asset-reference cases
- [x] Fix exact Content Block selection after the Body node appears in
Navigator
- [x] Run full affected suites/typechecks, all six MDX Content Block E2E
scenarios, deterministic fixture builds, generated checks, lint,
formatting, package boundaries, and diff checks

### Before merge

- [x] Resolve #6261: Nested Asset references in referenced Markdown
frontmatter work consistently in Assets queries, previews, publication,
and static export


### 23. Explicit writable expression bindings

- [x] Add an explicit `readwrite` mode to expression bindings; existing
expressions remain read-only by default
- [x] Accept `readwrite` only for direct static member paths and
centralize their parsing and validation
- [x] Require the expression root to provide a writable data source;
initially support the connected Content Block document frontmatter
- [x] Update Content Block permissions, canvas text editing, and
property controls to require a valid writable binding
- [x] Persist direct frontmatter edits through the existing ordered
per-Asset session
- [x] Create new direct frontmatter bindings with `readwrite` mode and
migrate existing compatible bindings
- [x] Preserve writable mode through schemas, serialization, loading,
copy/paste, transfer, and export
- [x] Add fail/pass regressions for valid paths, derived expressions,
unsupported sources, permissions, persistence, and compatibility
- [x] Review authoritative `docs/` content and update it if the
persisted binding contract affects user-facing documentation
- [x] Reject unsafe frontmatter paths and reject `readwrite` bindings
outside a connected Content Block document
- [x] Preserve expression binding mode through the public HTTP client
and document the MCP authoring contract
- [x] Limit frontmatter value write-back to Content mode so Design mode
retains normal binding replacement/reset behavior
- [x] Keep bindings inside Templates and across nested Content Block
ownership boundaries read-only
- [x] Upgrade compatible legacy bindings even when the Content Block is
disconnected during migration
- [x] Run affected tests, typechecks, formatting, lint, package
boundaries, fixture checks, and `git diff --check`

Verification: full SDK (567), Project Build (2,277), and Builder (2,695)
suites pass, along with affected Expression, Migration, Template, HTTP
Client, and MCP suites; typechecks, lint, formatting, package
boundaries, and diff checks pass. Generated contracts are refreshed.
Fixture synchronization remains blocked before generation because the
development API rejects this local CLI version as incompatible. The
checked-in bundles pass the current Protocol suite and every fixture
build with the current bundle version.
2026-08-29 20:15:20 +02:00
Oleg Isonen 523f555cba fix: Make Content Block template JSX insertable (#6128)
## Summary

Fix MCP component discovery so Content Block template JSX can be passed
directly to `insert-fragment`.

The catalog previously advertised `<$.block-template />`. The hyphen is
invalid in a JSX member name, and the fallback `$` namespace also did
not match the canonical `ws:block-template` component.

## Implementation

- Advertise Webstudio-owned components through the `ws` JSX binding.
- Expose `ws:block-template` as the parser-safe `<ws.blockTemplate />`.
- Resolve that alias back to the unchanged canonical component ID at
runtime.
- Keep catalog formatting and runtime resolution on one shared alias
contract.
- Add regression coverage for inserting a Content Block, its required
template part, nested elements, and authored text.

Existing stored component IDs and Builder behavior remain unchanged.

## Todo

- [x] Advertise parser-compatible Content Block template JSX
- [x] Resolve the JSX alias to `ws:block-template`
- [x] Verify nested authored Content Block insertion
- [x] Verify other `ws:*` discovery entries use the correct namespace
- [x] Review documentation impact — no user-facing documentation changes
are required
- [x] Run project-build tests, typecheck, lint, formatting,
generated-doc checks, and fixtures
- [ ] Run the complete agent evaluation suite after explicit approval

## Verification

- `pnpm --filter @webstudio-is/project-build test` — 2,090 tests passed
- `pnpm --filter @webstudio-is/project-build typecheck`
- `pnpm lint`
- `pnpm exec oxfmt --check
packages/project-build/src/runtime/jsx/bindings.ts
packages/project-build/src/mcp.ts
packages/project-build/src/runtime/components.test.ts
packages/project-build/src/mcp.test.ts`
- `pnpm check:generated-docs`
- `pnpm fixtures` — completed with no fixture diff

Evaluations were not run because repository policy requires separate
explicit approval.

Closes #6127
2026-08-13 19:45:41 +02:00
Oleg Isonen d5b3f3ab80 fix: Improve MCP authoring reliability (#6056)
## Summary

This consolidates the reported MCP reliability fixes into one delivery.

- validate all inserted fragment expressions with the shared runtime
rules and reject text/expression children when component content models
require elements
- return an actionable error for unsupported existing-resource
references in JSX
- preserve siblings rendered before expression children
- accept unresolved CSS-variable shorthand expansion and eliminate
dynamic-alt/`htmlFor` audit false positives
- resolve exposed underscore MCP names during focused discovery
- omit unavailable stopped-preview fields and restart stale iterative
previews for external clients
- bound single and responsive screenshot capture, return
`SCREENSHOT_TIMEOUT`, and discard failed reusable browser sessions
- dispose previews when shared MCP runs fail or stdio clients
disconnect, including complete Windows process-tree cleanup
- resolve generated route templates from the CLI-managed preview
workspace during responsive verification
- retry a browser that exits during rendered-audit startup and report
sanitized exit diagnostics with a concrete browser-path recovery action
- materialize complete current session data before preview generation so
route order and project styles cannot collapse from missing lazy
namespaces
- always include CLI and Node.js version fields in generated issue
reports, with explicit legacy-client fallbacks
- document the updated preview and screenshot contracts and regenerate
the CLI manual

The current branch also retains the protections already present on
`main` for dry-run isolation, CSS rewrite truncation metadata,
stderr-only progress, shortcut argument forwarding, strict MCP schemas,
and refreshed conflict diagnostics.

## Technical choices

- MCP command orchestration stays in `mcp.ts`; preview port/retry
behavior lives with `mcp-preview`, and persisted checkpoint storage
lives in `mcp-checkpoint`. Each module owns its matching tests.
- Expression validation lives on the shared fragment input schema,
reuses `listFragmentExpressions`, and emits the same structured
validation issues as prop bindings, so JSX-derived and structured
fragments follow one contract across fragment, collection, and runtime
UI insertion.
- Element-child requirements come from component registry content models
during fragment insertion; no component names are hardcoded in the JSX
parser. `CollapsibleTrigger` metadata now matches its enforced `asChild`
implementation.
- Stopped preview status returns only `running: false`; unavailable
`url`, `pid`, and `mode` fields are optional and omitted from JSON.
- Explicit `preview.start` restarts a stale compatible iterative
preview; managed screenshots keep their optimized refresh path.
- Screenshot operations share one timeout utility. A single capture or
complete responsive batch is bounded after preview readiness;
lower-level CDP deadlines use the same stable error code.
- Failed managed captures close and forget their browser session. A
closed browser session cannot restart itself while cancellation is in
progress, which prevents orphaned Chromium processes.
- One ownership helper disposes one-shot and multi-project MCP hosts on
success or failure. Shared `mcp run` hosts dispose in one `finally`
boundary, long-running MCP hosts dispose on connection close, and
Windows uses `taskkill /T /F` instead of killing only the npm wrapper.
- Framework template readers receive the absolute template directory
captured from the build root, so later async work cannot redirect them
to the linked project's cwd. The same option drives cleanup for React
Router, Remix, and Vike.
- Browser startup gets one bounded retry before a rendered capture batch
fails. Persistent failures report only the exit code, signal, or spawn
error code plus the supported `WEBSTUDIO_BROWSER_PATH` recovery action;
local paths and raw browser output are not exposed.
- CLI and MCP preview share one session materializer that ensures the
canonical Builder namespace set before writing `.webstudio/data.json`.
Serialization rejects incomplete snapshots instead of converting missing
style/instance namespaces into valid-looking empty arrays, and preview
freshness records the post-materialization project version.
- The CLI injects its package version and `process.versions.node`; issue
formatting now keeps both fields visible even for legacy reports that
omit runtime metadata.

## Checklist

- [x] Add regression coverage for every changed behavior
- [x] Demonstrate new regression tests fail without the corresponding
fixes and pass with them
- [x] Run affected package test suites and typechecks
- [x] Run lint, package-boundary, and generated API checks
- [x] Review fixture impact — no fixture inputs or outputs are affected
- [x] Review documentation impact and update the authoritative MCP CLI
manual source/generated output
- [x] Run the complete local agent evaluation suite

## Verification

- `@webstudio-is/project-build`: 2,090 tests passed
- MCP asset query result modes are preserved across create, read, and
update serialization
- complete local agent evaluation suite: all 5 fixtures passed,
1,308,276 aggregate tokens
- `@webstudio-is/react-sdk`: 140 tests passed
- `@webstudio-is/template`: 43 tests passed
- `webstudio` CLI: 956 tests passed
- issue reporter: 5 targeted tests passed
- MCP host disposal regression was demonstrated failing without the
shared ownership boundary and passing with it
- relevant package typechecks passed
- `pnpm lint` passed
- `pnpm check:package-boundaries` passed
- `pnpm check:generated-api` passed
- generated-doc tests, typecheck, sync, and regeneration passed; the
aggregate clean-tree command reports the intentionally changed generated
CLI manual
- stalled single capture, stalled four-viewport capture, startup retry
across a four-viewport batch, sanitized browser-exit diagnostics,
complete preview materialization, post-refresh version tracking,
closed-session restart, Windows process-tree cleanup, and
preview-workspace regressions were demonstrated failing without their
fixes and passing with them

## Known limitations

- External `baseUrl` screenshots remain caller-owned and intentionally
do not regenerate, restart, or stop that external server.

Closes #6034
Closes #6035
Closes #6038
Closes #6042
Closes #6043
Closes #6044
Closes #6045
Closes #6046
Closes #6047
Closes #6048
Closes #6049
Closes #6050
Closes #6051
Closes #6052
Closes #6059
Closes #6060
2026-08-08 22:22:25 +02:00
Oleg IsonenandClaude Fable 5 de23e2a888 chore: migrate toolchain to oxc (oxfmt, oxlint, rolldown-vite) and typeScript 7 (#5868)
## prettier -> oxfmt (0.58.0)

- .oxfmtrc.json generated with `oxfmt --migrate=prettier`: trailingComma
es5, printWidth 80 (oxfmt defaults to 100), sortPackageJson disabled,
.prettierignore patterns folded in (file deleted).
- ignorePatterns additionally exclude yml/yaml/toml/jsonc and
packages/cli/templates/** so oxfmt covers exactly the file set prettier
used to format (ts,tsx,js,json,css,md) and never touches user-facing
scaffolding artifacts.
- root `format` script, nano-staged pre-commit hook and the CI check
(`pnpm oxfmt --check`, replacing `prettier --cache --check` and its
actions/cache step — oxfmt checks the whole repo in ~0.7s) now run
oxfmt.
- every generator script that ran `prettier --write` on __generated__
output now runs `oxfmt` (css-data, icons, html-data, postgrest, sdk,
sdk-components-*, apps/builder tailwind preflight).
- the four scripts that used prettier's JS `format()` API write raw
source instead and their package.json scripts pipe the generated file
through the oxfmt CLI: project-build generate-docs +
generate-runtime-operation-contracts, cli generate-docs, apps/builder
generate-server-only-api-metadata. oxfmt rejects paths containing "..",
so cross-package generated files are formatted via a subshell cd into
the owning package.
- one-time reformat: oxfmt's canonical output differs from prettier on 8
repo source files (binary-operator continuation indent, import() call
wrapping, sub-pixel spacing of empty for-clauses); oxfmt honors existing
`// prettier-ignore` comments so intentional layouts are kept.
- prettier remains ONLY as a cli devDependency for the
prettier/standalone HTML normalizer inside html-to-jsx.test.ts, and
inside fixtures/ + packages/cli/templates/ (ships to scaffolded user
projects; out of scope here).
- editor config switched from esbenp.prettier-vscode to oxc.oxc-vscode
(devcontainer extension list, .vscode editor.defaultFormatter).

## oxlint 1.56 -> 1.73

- oxlint 1.73 rules-of-hooks special-cases hooks NAMED useEffectEvent:
their results may only be called from effects. Our
app/shared/hook-utils/effect-event.ts is a stable-callback helper
(useEvent pattern), intentionally called from event handlers and passed
as props. The rule is disabled via .oxlintrc.json overrides for the five
files doing this (product code untouched); note those files lose
hook-order linting entirely until the rule becomes configurable.

## TypeScript 5.8.2 -> 7.0.2 (native compiler)

- `tsc` is now the Go-based native binary; all `tsgo` script usages
(workspace packages and fixtures) renamed back to `tsc` and
@typescript/native-preview removed (the package is being phased out).
- fixes for TS7 changed defaults and stricter checks:
- `types` now defaults to [] -> base tsconfig sets "types": ["node"]
(restores the implicit-@types/node behavior of TS5).
- `rootDir` now defaults to ./ -> every tsconfig.dts.json sets
"rootDir": "./src" (TS5011 otherwise).
- import assertions removed -> cli/src/cli.ts uses `with { type: "json"
}` (TS2880).
- stricter isolatedDeclarations inference -> explicit annotations in
css-data bin/property-filter.ts (TS9010) and image image-dev.stories.tsx
(TS9013).
- declaration serialization limit -> project-build builder-runtime.ts
runtimeOperationContracts gets an explicit type (TS7056); the annotation
preserves the narrow RuntimeOperationId union.
- TS7 ships no stable programmatic API:
- react-sdk (component-generator tests drive the compiler API) aliases
its typescript devDependency to @typescript/typescript6@^6.0.2; its bin
is `tsc6`, so the package's typecheck still runs native TS7 tsc from the
workspace root.
- generate-arg-types keeps typescript 5.8.2 for react-docgen-typescript.
- fixtures/ and packages/cli/templates/ keep typescript 5.8.2
(user-facing); fixture scripts still work because plain `tsc` resolves
to their local dependency.

## vite -> rolldown-vite 7.3.1 (oxc bundler)

- global pnpm override `"vite": "npm:rolldown-vite@^7.3.1"`: the builder
app (remix v2 vite plugin works unmodified), every package vite build
and the storybook build now bundle on rolldown/oxc.
- vitest is scoped BACK to vanilla vite via `vitest>vite`,
`vite-node>vite` and `@vitest/mocker>vite` overrides because vitest
3.1's runner does not understand rolldown's SSR transform
(__vite_ssr_exportName__ ReferenceError in every suite).
- those overrides pin vite EXACTLY to 6.3.4: letting ^6.3.4 float to
6.4.3 breaks cli prebuild tests (dynamic import of a generated
app/routes/$.tsx fails to resolve under 6.4.3's loader).

## unrelated cleanup requested during review

- apps/builder commands.ts imports duplicate-tokens-group statically
instead of via dynamic import. The circular dependency the dynamic
import guarded against only exists through the command-panel barrel
(command-panel.tsx > groups/index.ts > commands-group.tsx >
commands.ts); importing the group module directly is cycle-free
(verified with madge), matching the neighboring wrap-group and
convert-group imports.

## verification

- pnpm lint, oxfmt --check: clean.
- pnpm -r typecheck (TS7 native) and pnpm dts: clean.
- pnpm -r test: all suites pass (builder 2093 tests, cli 630, react-sdk
45, project-build 11, ...).
- builder app build, full workspace build, cli build and storybook build
all succeed on rolldown-vite.

## Description

1. What is this PR about (link the issue and add a short description)

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:03:36 +02:00
Oleg Isonen 6e10ae3809 fix: Fix Builder paste transfer data handling and publish sync edge cases (#5861)
- Fix publish sync for empty JSON data variables
- Retry transient asset downloads once with shared sync-client backoff
- Fix Builder paste handling for Webstudio transfer data
- Add transfer/data-envelope terminology and colocated runtime
data-format modules
- Rename page clipboard runtime operation to page transfer semantics
- Report malformed Webstudio/Webflow paste data through paste results
instead of inserting it as text
- Add browser-level E2E coverage for instance, page, folder, template,
generic plugin, malformed data, native input, and content-mode paste
flows

## Details

- Allows empty JSON data variables to sync/publish correctly.
- Adds asset download retry coverage and uses shared backoff logic.
- Introduces runtime `data-formats` modules for data envelopes, instance
transfer data, and page transfer data.
- Removes stale clipboard aliases and `validateTemplateStructure`.
- Keeps clipboard as a Builder UI transport concern while
runtime/protocol use transfer-data terminology.
- Changes paste plugins to return expressive success/error results so
callers can show toast errors and stop fallback insertion.
- Prevents malformed Webstudio transfer payloads from falling through to
text/HTML/markdown insertion.
- Handles malformed Webflow-owned paste data without throwing.
- Adds shared copy-paste fragment insertion helpers to reduce duplicated
breakpoint-warning/fragment-data logic.
2026-07-10 13:51:55 +02:00
Oleg Isonen d291e4b671 feat: Builder api runtime contracts guardrails for MCP (#5857)
This PR expands Webstudio MCP and CLI into a more complete agent
authoring interface. Agents can discover project capabilities, edit real
Webstudio content, use data/resources, verify visual output, and report
useful issues when workflows fail.

## MCP User Capabilities

- Create, update, duplicate, reorder, and delete pages.
- Create pages from templates and manage reusable page templates.
- Inspect project pages, instances, props, styles, assets, variables,
resources, and metadata.
- Insert Webstudio components and authored JSX fragments into pages.
- Edit text, props, styles, breakpoints, design tokens, and CSS
variables.
- Update page metadata, SEO fields, redirects, marketplace metadata, and
project settings.
- Create and use static data variables.
- Create and use dynamic resources fetched over HTTP.
- Use REST, GraphQL, and built-in system resources in authored pages.
- Bind data variables and resources to visible page content.
- Create resource-backed pages such as sitemap XML, structured SEO data,
and API-driven content sections.
- Preview current project state without requiring a publish step.
- Capture screenshots for visual verification.
- Verify responsive designs across desktop, tablet, and mobile
breakpoints.
- Use screenshot diff and OCR-style checks to inspect rendered output.
- Reset and refresh local MCP project state when needed.
- Discover available CLI/MCP commands from generated manuals.
- Use clearer examples for resources, data variables, JSX authoring,
previewing, and visual checks.
- Read marketplace product metadata without requiring a full project
snapshot.
- Get safer guidance when using low-level patch tools.
- Get a ready-to-copy Discord report template when MCP/CLI behavior is
confusing or broken.
2026-07-10 02:13:22 +02:00
d45d1b4274 feat: add plain text page support (#5783)
## Description

Adds support for plain text pages. Related to
https://github.com/webstudio-is/webstudio/issues/4257

This PR introduces `text` as a supported document type, stores plain
text page content in page metadata, and generates `text/plain` route
handlers for Remix and React Router templates.

It also limits HTML-specific Builder actions on text pages, excludes
XML/text pages from the generated static sitemap, and adds SDK coverage
for text document schema and sitemap behavior.

## Steps for reproduction

1. Create or edit a page in Builder.
2. Set the document type to `TEXT`.
3. Enter plain text content in page settings.
4. Build with a Remix or React Router template.
5. Open the generated text page URL, for example `/robots.txt`.
6. Expect the response body to contain the configured text content with
`Content-Type: text/plain; charset=utf-8`.
7. In Builder, expect HTML insert/wrap/convert/tag actions to be
unavailable for text pages.

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [x] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [x] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [x] added tests
- [ ] if any new env variables are added, added them to `.env` file

---------

Co-authored-by: shreyanshkotak <sk7739@nyu.edu>
Co-authored-by: Shrek <122366903+shreyanshkotak@users.noreply.github.com>
2026-05-30 12:10:39 +02:00
Oleg Isonen 8de2fd47f6 fix: CSS shorthand properties not saved when value contains var() (#5688)
Shorthand properties like background: var(--color) were silently dropped
when pasting into the CSS editor because the shorthand expander can't
handle unresolved var() references.

Fix: CSS vars from the current element's computed styles are now
resolved before expanding shorthands. Inline and nested fallbacks
(var(--a, var(--b))) are supported. If a var still can't be resolved, a
toast error is shown instead of silently dropping the property.
2026-04-12 17:14:01 +02:00
Oleg Isonen d339dc55cf feat: Add token conflict detection and resolution for marketplace templates (#5609)
- Add Token class and token() helper in template package for JSX token
support
- Refactor insertFragmentWithConflictResolution to
detectFragmentTokenConflicts
- Refactor insertPageWithConflictResolution to detectPageTokenConflicts
- Move dialog display to callers for better testability
- Add conflictResolution parameter to page-utils
- Add comprehensive tests for token functionality and conflict detection
- Fix bug: remove .values() call on arrays in breakpoint handling
2026-02-05 22:34:17 +01:00
Oleg Isonen fdee339de0 chore: Migrate to tsgo (#5578) 2026-01-20 17:25:12 +01:00
Bogdan Chadkin 02ca084a0c refactor: upgrade to csstree v3 (#5455)
The new version is aware of modern colors and other new css features.
2025-11-23 14:32:12 +03:00
Bogdan Chadkin de444a3501 fix: copy paste search params in resources (#5412)
Reproduced when tried to insert the new baserow template
2025-09-27 16:47:49 +03:00
Bogdan Chadkin 03c7366b04 feat: create projects with 404 page on all plans (#5309)
Ref https://github.com/webstudio-is/webstudio/issues/2782

Here I did a few things

- allow `/*` path on free plan
- allow changing status code on free plan
- create "Not Found" page for all new projects



https://github.com/user-attachments/assets/1f36882f-3c09-4814-a1ee-d6bdfd468467
2025-07-01 18:56:15 +03:00
Oleg Isonen 0219de6d63 feat: Built with webstudio badge (#5243)
## Description

1. What is this PR about (link the issue and add a short description)

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2025-06-06 11:39:07 +02:00
Bogdan Chadkin de61cffde6 experimental: paste tailwind (#5217)
Ref https://github.com/webstudio-is/webstudio/issues/2651

Added "Paste Html With Tailwind Classes" command.

For now only tailwind 3 is supported.
Also fixed a few issues with css parser.


<img width="547" alt="Screenshot 2025-05-18 at 00 38 01"
src="https://github.com/user-attachments/assets/470b63e0-9b17-4769-8a65-679b90d73a89"
/>
2025-05-23 13:19:04 +03:00
Bogdan Chadkin 0c9b138bba feat: support GitHub flavored markdown paste (#5225)
Markdown paste now generate html elements. This unlocks us supporting
more features:

**strikethrough**

`~~text~~` -> <del>text</del>

**tables**

```
| Header 1   | Header 2   |
|------------|------------|
| Cell 1.1   | Cell 1.2   |
| Cell 2.1   | Cell 2.2   |
```

*autolinks*

```
The link is https://github.com
```

is converted to

```html
<p>This link is <a href="https://github.com">https://github.com</a></p>
```
<img width="559" alt="Screenshot 2025-05-20 at 16 44 53"
src="https://github.com/user-attachments/assets/48508140-9791-47f6-b1c1-3b2946dd5583"
/>

<img width="514" alt="image"
src="https://github.com/user-attachments/assets/dc19e67a-0067-458f-bf3c-64d9b73d02e3"
/>
2025-05-21 01:50:00 +03:00
Bogdan Chadkin 452c50a41a refactor: upgrade vite for vitest and storybook (#5165)
Tough cookie. There is a problem with resolving react-remove-scroll,
figured it by reducing allowed amount of server conditions.
2025-04-29 14:17:42 +04:00
Bogdan Chadkin 925ea10391 feat: avoid generating unused preset styles (#5121)
Now we have first-class support for html tags and able to figure out
which tags are rendered so unused preset styles can be removed from
websites.

For example

```
<Box ws:tag="div">
  <Box ws:tag="span">
  </Box>
</Box>
```

will render only div.w-box and span.w-box though there are more presets
for for other tags like main, section etc.

Before all tags were generated
2025-04-15 22:26:58 +07:00
Bogdan ChadkinandOleg Isonen 9032a109ca experimental: support ws:element component in CLI (#5071)
Ref https://github.com/webstudio-is/webstudio/issues/3632

Added `ws:element` component to represent any html and svg elements
within webstudio.

It is rendered as `<div>` by default which can be customized with "tag"
system property (instance.tag in data).

Here added only CLI generator support with converting html attribute
names to react compatible. Will add builder support separately.

---------

Co-authored-by: Oleg Isonen <oleg008@gmail.com>
2025-04-03 14:06:50 +07:00
Bogdan Chadkin 1dc7af7621 refactor: add tag property control (#5080)
https://github.com/webstudio-is/webstudio/issues/3632

Here added dedicated tag property control which will automatically
migrate Box, Text and Heading components to instance.tag internally and
will be used for ws:element component.

Later will extend tag control with tag descriptions and auto inferring
matching tags.

<img width="428" alt="Screenshot 2025-04-02 at 15 56 46"
src="https://github.com/user-attachments/assets/fa8d24ba-b9e8-4221-b47a-a9e923475bb6"
/>
2025-04-03 11:07:43 +07:00
Oleg Isonen 64dde50c70 experimental: Animations section, new component names, new icons (#5062)
## Description

<img width="280" alt="image"
src="https://github.com/user-attachments/assets/b3c29952-8c10-4e8f-88f0-8cea45df87ba"
/>


## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2025-03-27 18:54:55 +01:00
Bogdan Chadkin 935983b543 build: upgrade checks (#4982)
- vitest
- testing-library (got much lighter)
- eslint and plugins (@types/eslint__js no longer necessary)
- storybook
- esbuild
2025-03-11 15:22:57 +07:00
Bogdan Chadkin 78a0fa98c1 refactor: output hyphenated properties from css parser (#4900)
We are going to switch to hyphenated properties in styles. Here
refactored css parser to output hyphenated property instead of camel
case and added camelCaseProperty utility which does the opposite of
hyphenateProperty.
2025-02-22 18:11:57 +07:00
Bogdan Chadkin c3ac4d6ec1 fix: preserve global variables when duplicate page (#4875)
Missed the case. Global variables were recreated and got invalid state.
Now global variables are reused or added without changes.
2025-02-15 02:53:09 +08:00
Ivan StarkovandOleg Isonen 3c42f8ca4b experimental: View Animations support (#4817)
## Description

ref  #2645

- [x] - Add view-timeline-name and scroll-timeline-name support, and
prefixer (use custom property for unsupported browsers)
- [x] - Add initial story

## Steps for reproduction


https://6382151c8b47d4399fb9fc69-uljkhwtbmc.chromatic.com/iframe.html?args=&globals=&id=sdk-components-animation-view-animations--in-out&viewMode=story

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file

---------

Co-authored-by: Oleg Isonen <oleg008@gmail.com>
2025-02-04 06:02:55 +03:00
Bogdan Chadkin eb2a2750b8 refactor: support resources in jsx templates (#4800)
Added full resource configuration in templates.
Will be useful for testing.
2025-01-29 00:26:20 +07:00
Bogdan Chadkin c562e1c6a7 refactor: refresh copy paste tests (#4782)
Here replaced renderJsx with renderData everywhere and refreshed copy
paste tests so they not break CI occasionally. Will help with writing
tests for global variables.
2025-01-24 18:39:24 +07:00
Bogdan Chadkin 6d2c297f9b refactor: add renderData utility to template package (#4780)
renderData is similar to renderTemplate but returns maps of data which
is used by most of builder utilities and component generator.

Here migrated to it css and component generator and used it to generate
data sources where possible.
2025-01-23 20:18:41 +07:00
Bogdan Chadkin 5acb458a68 build: upgrade dev dependencies (#4764)
Nothing critical, all not related to production

storybook 8.5
eslint 9.18
vitest 3
typescript 5.7.3
@types/node 22.10.7
type-fest 4.32
2025-01-21 00:14:56 +07:00
Bogdan Chadkin 94f219c249 refactor: rewrite core components templates with jsx (#4756)
Here refactored all core components with jsx templates and moved them to
sdk package. Fixed all imports as well.
2025-01-17 18:29:40 +07:00
Bogdan Chadkin 178ad00bc3 refactor: rewrite webhook form template with jsx (#4721)
Big one

- added `new Variable(name, initialValue)` to defined unique variable
- added `expression\`${variable}\`` to access variables by reference
- added `new Action(args, expression\`\`)` support
- added `ws:show` prop which is converted to data-ws-show under the hood

All these features are necessary to represent all webhook form features.
They also may be handy in tests.
2025-01-08 18:37:27 +07:00
Bogdan Chadkin 53f03cfaa3 fix: restore missing components in command panel (#4692)
Fixes https://github.com/webstudio-is/webstudio/issues/4689

While refactoring with new component templates
forgot to register them in command panel.

Looks like the prod is affected a little too
2025-01-01 19:45:41 +07:00
Bogdan Chadkin 8a89f8e6b2 refactor: rewrite radix label template with jsx (#4665)
- added radix templates to components panel
- support interpolations in css tagged template
- added theme with static values
2024-12-28 16:58:56 +07:00
Bogdan Chadkin e9117c0a18 refactor: support local styles in templates (#4656)
Here added support for local styles with ws:style prop. It accepts the
result of css`` tagged template which enables formatter support.

Migrated vimeo to new template. Need to check all styles are rewritten
accurately.

```js
<$.Box
  ws:style={css`
    font-size: 10px;
  `}
></$.Box>
```
2024-12-26 00:23:21 +07:00
Bogdan Chadkin a32d6329ed refactor: add templates meta support (#4653)
Here added one more description file for components `*.template.tsx`.

It is decoupled from component meta to fix the issue with some templates
like Sheet being treated as components though we don't have actual
components with this name.

Another reason is have it separately to not include builder specific
code and dependencies into CLI and published websites. Template package
is defined as optional in peer dependencies meta.

In this PR migrated only Button and List as examples. There is still not
support for data sources and styles.

Added support for text placeholders in children
2024-12-25 21:37:15 +07:00
Bogdan Chadkin b1b607e9c4 refactor: simplify template format (#4607)
Here added new renderTemplate function which outputs simplified format
with arrays instead of maps and "children" array to describe roots.

This format is partial of webstudio fragment but does not implement it
fully.

Added support for top level fragment to allow having multiple roots.
2024-12-19 23:45:54 +07:00
Bogdan Chadkin 7f7a8fd9de refactor: split jsx testing utils into template package (#4604)
We can start using testing utilities for components meta. It will
drastically simplify maintenance of components, already works pretty
well for tests.

Here just replaced @webstudio-is/sdk/testing with
@webstudio-is/template.
2024-12-17 16:54:13 +07:00