Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .wordpress-org/screenshot-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 7 additions & 4 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ build step. Themes are token sets; the renderer only ever reads tokens.

## 1. Non-negotiables

- **OpenStation is optional.** Every shell call sits behind a `function_exists()`
gate resolved through `includes/shell-api.php` (same pattern as AllTerrain
Work). No shell → the builder renders on a plain admin page and the front-end
is untouched. No `Requires Plugins:` header.
- **OpenStation is required.** `Requires Plugins: desktop-mode` in the header;
WordPress 6.5+ enforces it at activation and blocks deactivating the shell
underneath us. The builder is a native shell window — the desktop *is* the
product. `includes/shell-api.php`'s `function_exists()` gates stay as defense
in depth (older WP, force-removed shell): admin shows a notice, the front end
keeps rendering published forms. *(Changed 2026-08-19; was "optional" through
0.1.0.)*
- **Everything is a post.** A form is a post, an entry is a post, an entry note
is a *comment*, a theme is a post. No bespoke tables. That buys REST,
`current_user_can()`, revisions, search, trash, the privacy exporter/eraser
Expand Down
43 changes: 38 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,15 @@ is a post. Nothing lives in a bespoke table, so the REST API,
`current_user_can()`, revisions, search, the trash, and WordPress's own privacy
exporter and eraser already work on this data without a line of integration code.

**OpenStation is optional.** Every shell call sits behind a `function_exists()`
gate, so a site without the shell loses the desktop affordances and keeps a
complete forms plugin under **Forms** in wp-admin. There is deliberately no
`Requires Plugins:` header.
**OpenStation is required.** The plugin declares `Requires Plugins:
desktop-mode`, so WordPress 6.5+ installs and activates the shell alongside it
and refuses to pull the shell out from underneath it. The builder is a native
shell window — the desktop is the product, not a skin on it — so the plugin
does not ship a lesser version of itself for sites without one. The
`function_exists()` gates in `includes/shell-api.php` remain as defense in
depth for half-upgraded sites: without the shell an admin notice says what is
missing, and published forms keep rendering so no visitor ever pays for an
admin dependency.

---

Expand All @@ -82,7 +87,6 @@ renderer, same stylesheet, same theme resolution, so what you see is what a
visitor gets. Save, and the preview window refreshes itself. Drag a field, watch
it change, drag another; the builder never closes.

Without OpenStation the same button opens the same URL in a tab.

---

Expand Down Expand Up @@ -236,6 +240,33 @@ separate place.

---

## Nine abilities, so AI agents can use the forms

The plugin registers its whole working surface with WordPress's Abilities API,
which is how a site describes what it can do to agents and MCP clients. Each
ability is a thin adapter over the same functions the windows and the REST API
call — an agent and a human clicking the same button get the same behaviour,
the same validation and the same capability checks.

| Ability | What it does |
|---|---|
| `list-forms` | Every form with id, title, theme, shortcode, entry count and questions — the ids feed everything else |
| `get-form` | One form's questions: field ids, types, labels, required flags, choices |
| `list-field-types` | The building vocabulary: all 37 types and what each stores |
| `create-form` | Builds a form from a title and a loose field list; returns the shortcode |
| `set-form-theme` | Dresses a form in any installed theme |
| `submit-form` | Submits through the visitor pipeline — validation, anti-spam, storage, notifications; refusals return per-field errors |
| `list-entries` | Queries submissions — search, date range, status, pagination — raw and human-readable |
| `get-entry` | One submission, every answer labelled and formatted |
| `form-report` | The analytics as structured data: conversion, timeline, distributions, NPS, group-by breakdowns |

Ask an assistant to *"make a booking form and show me last week's responses"*,
and these are what it calls. The full contract — permissions, schemas, and the
one honest liberty `submit-form` takes with the time trap — is in
[`docs/abilities.md`](docs/abilities.md).

---

## Accessibility

Not a section that says "we care about accessibility". The things that were
Expand Down Expand Up @@ -409,6 +440,8 @@ npm run test:php # 465 PHP tests, inside wp-env
## Documentation

- [`docs/hooks-reference.md`](docs/hooks-reference.md) — every action and filter
- [`docs/abilities.md`](docs/abilities.md) — the nine WordPress Abilities AI
agents use the forms through: build, theme, submit, query, report
- [`docs/field-types.md`](docs/field-types.md) — adding a field type
- [`docs/themes.md`](docs/themes.md) — the token surface, and adding a token
- [`docs/javascript.md`](docs/javascript.md) — events, drag payloads, the bundles
Expand Down
16 changes: 11 additions & 5 deletions allterrain-forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* Plugin URI: https://github.com/AllTerrainDeveloper/forms
* Description: Forms for WordPress with every premium feature free — conditional logic, calculations, multi-page, file uploads, signatures, repeaters, entry management, ten themes — built as an OpenStation desktop app with a drag-and-drop builder.
* Version: 0.1.0
* Requires at least: 6.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Requires Plugins: desktop-mode
* Author: Daniel Lopez
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
Expand Down Expand Up @@ -40,10 +41,14 @@
* privacy exporter and eraser already work on this data without a line of
* integration code.
*
* OpenStation is **optional**. Every shell call sits behind a `function_exists()`
* gate resolved through `includes/shell-api.php`, so a site without the shell
* loses the desktop affordances and keeps a complete forms plugin. That is why
* there is deliberately no `Requires Plugins:` header.
* OpenStation is **required** — the `Requires Plugins: desktop-mode` header
* says so, and WordPress 6.5+ enforces it at activation. The builder is a
* native shell window; the desktop is the product, not a skin on it. Every
* shell call still resolves through `includes/shell-api.php`'s
* `function_exists()` gates, but as defense in depth for half-upgraded sites
* rather than as a supported mode: without the shell, an admin notice says
* what is missing, visitors' published forms keep rendering so nobody's
* front end breaks, and nothing else pretends to work.
*
* @package AllTerrain_Forms
*/
Expand Down Expand Up @@ -189,6 +194,7 @@
require_once ATF_DIR . 'includes/importer-gravityforms.php';
require_once ATF_DIR . 'includes/import-notice.php';
require_once ATF_DIR . 'includes/rest.php';
require_once ATF_DIR . 'includes/abilities.php';
require_once ATF_DIR . 'includes/preview.php';
require_once ATF_DIR . 'includes/shortcode.php';
require_once ATF_DIR . 'includes/block.php';
Expand Down
3 changes: 2 additions & 1 deletion assets/css/builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,8 @@
them stacked on themselves rather than on a page. */
.atfs-preview__frame:has( .atf-is-dark ),
.atfb-preview:has( .atf-is-dark ),
.atf-block-preview:has( .atf-is-dark ) {
.atf-block-preview:has( .atf-is-dark ),
.atfb--dark-form .atfb-preview {
background: #101418;
color: #f0f0f1;
}
Expand Down
91 changes: 80 additions & 11 deletions assets/css/form.css
Original file line number Diff line number Diff line change
Expand Up @@ -191,26 +191,95 @@
apart without JavaScript — which is why a floating theme needs a placeholder,
and why the space is reserved with a non-breaking placeholder when there
isn't one. */
.atf-labels-floating .atf-field {
/* Floating is only offered where a label *can* float: a single control the
label sits inside of and lifts out of. A radio group's legend, a scale's
heading or a signature pad's label has nothing to float into, and floating
them anyway painted the question straight across its own answers — so every
other type simply keeps its label on top, unstyled by this block.

Two sub-cases inside the floatable set:
- Text-shaped inputs lift on focus or once they hold a value, told apart by
`:placeholder-shown` — the reserved single-space placeholder in
`atf_control_attributes()` is what makes that selector answerable.
- Selects and date/time inputs always display something, so
`:placeholder-shown` never matches and their label is simply always
lifted — which is the only honest float for a control that is never
visually empty.

The lift raises the label into headroom reserved above the input rather
than parking it astride the input's top border: a border-straddling label
needs a background mask, and no one mask survives ten themes of gradients
and glass. */
.atf-labels-floating
.atf-field:is(
.atf-field--text,
.atf-field--email,
.atf-field--tel,
.atf-field--url,
.atf-field--number,
.atf-field--password,
.atf-field--textarea,
.atf-field--select,
.atf-field--country,
.atf-field--date,
.atf-field--time,
.atf-field--datetime
) {
position: relative;
padding-block-start: 0;
}

.atf-labels-floating .atf-label {
padding-block-start: calc( var( --atf-size-label ) * 1.3 );
}

.atf-labels-floating
.atf-field:is(
.atf-field--text,
.atf-field--email,
.atf-field--tel,
.atf-field--url,
.atf-field--number,
.atf-field--password,
.atf-field--textarea,
.atf-field--select,
.atf-field--country,
.atf-field--date,
.atf-field--time,
.atf-field--datetime
)
> .atf-label {
position: absolute;
inset-block-start: var( --atf-pad-field-y );
inset-block-start: calc( var( --atf-size-label ) * 1.3 + var( --atf-pad-field-y ) );
inset-inline-start: calc( var( --atf-pad-field-x ) + var( --atf-border-width ) );
max-inline-size: calc( 100% - 2 * var( --atf-pad-field-x ) );
overflow: hidden;
margin: 0;
pointer-events: none;
transform-origin: left top;
transition: transform var( --atf-transition-duration ) var( --atf-transition-easing ),
white-space: nowrap;
text-overflow: ellipsis;
transform-origin: 0 0;
transition: inset-block-start var( --atf-transition-duration ) var( --atf-transition-easing ),
transform var( --atf-transition-duration ) var( --atf-transition-easing ),
color var( --atf-transition-duration ) var( --atf-transition-easing );
color: var( --atf-text-muted );
}

.atf-labels-floating .atf-input:not( :placeholder-shown ) ~ .atf-label,
.atf-labels-floating .atf-input:focus ~ .atf-label {
transform: translateY( -0.85em ) scale( 0.78 );
.atf-labels-floating
.atf-field:is(
.atf-field--text,
.atf-field--email,
.atf-field--tel,
.atf-field--url,
.atf-field--number,
.atf-field--password,
.atf-field--textarea,
.atf-field--select,
.atf-field--country,
.atf-field--date,
.atf-field--time,
.atf-field--datetime
):is( :has( > .atf-input:focus ), :has( > .atf-input:not( :placeholder-shown ) ) )
> .atf-label {
inset-block-start: 0;
inset-inline-start: 0;
transform: scale( 0.82 );
color: var( --atf-accent );
}

Expand Down
15 changes: 11 additions & 4 deletions assets/js/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,20 @@ var allTerrainFormsAnalytics = function(exports) {
const string = search.toString();
return string ? `?${string}` : "";
}
function withObjectOverrides(form) {
const settings = form?.schema?.settings;
if (settings && (!settings.themeOverrides || Array.isArray(settings.themeOverrides))) {
settings.themeOverrides = { ...settings.themeOverrides };
}
return form;
}
const api = {
config: () => get("/config"),
listForms: () => get("/forms"),
getForm: (id) => get(`/forms/${id}`),
createForm: (body) => post("/forms", body),
updateForm: (id, body) => post(`/forms/${id}`, body),
duplicateForm: (id) => post(`/forms/${id}/duplicate`, {}),
getForm: (id) => get(`/forms/${id}`).then(withObjectOverrides),
createForm: (body) => post("/forms", body).then(withObjectOverrides),
updateForm: (id, body) => post(`/forms/${id}`, body).then(withObjectOverrides),
duplicateForm: (id) => post(`/forms/${id}/duplicate`, {}).then(withObjectOverrides),
deleteForm: (id) => del(`/forms/${id}`),
preview: (id, body) => post(`/forms/${id}/preview`, body),
/**
Expand Down
Loading
Loading