All checks were successful
Build + deploy releases / build-and-deploy (push) Successful in 8s
Schema-driven form renderer plus zddc-server endpoints that turn any
<name>.form.yaml into a working data-collection form at <path>/<name>.form.html.
Submissions land in <path>/<name>/<YYYY-MM-DD>-<email-sanitized>.yaml,
ACL-gated by the existing .zddc cascade. The form posts back to its own URL;
the server strips ".html" and routes by what's underneath, so create and
update use the same client-side code path.
Form spec dialect: JSON Schema 2020-12 + RJSF-style ui:* hints, written in
YAML. Chosen for LLM authorability — it's the canonical structured-output
target for OpenAI/Anthropic, and the ui:* convention is the most-trained UI
hint vocabulary. Supported subset for v0: type (string/number/integer/boolean/
array/object), enum, min/max, minLength/maxLength, required, additionalProperties:
false, properties, items, format (date, email). Round-trip mode is form-as-truth:
submission YAML is regenerated each save, comments are not preserved (the v1
file-as-truth mode for hand-edited files like .zddc itself is deferred).
New components:
* form/ — sixth single-file HTML tool, vanilla JS renderer (~760 LoC)
* zddc/internal/jsonschema/ — focused JSON Schema validator covering only
the v0 keyword subset. Match-implementation-cost-to-surface-used: a full
library brings 70%+ surface we don't use; revisit when v1 adds $ref +
oneOf + if/then/else.
* zddc/internal/handler/formhandler.go — RecognizeFormRequest / ServeForm,
capability-URL re-edit, atomic submission writes via the new
zddc.WriteAtomic helper extracted from writer.go.
* dispatch() in zddc-server/main.go now intercepts *.form.html and
*.yaml.html before the static-file path; spec existence is the trigger.
Build pipeline: form joins ZDDC_RELEASE_TOOLS in lockstep, gets its own
embedded copy in handler/form.html (separate from the apps cascade —
the form renderer is fixed, not subject to per-folder version overrides).
Tests: 5 new Playwright specs (form-safety) + 14 new Go tests across the
validator and handler. All 172 Playwright tests + 10 Go packages green.
End-to-end manual verification: GET empty → POST 201 + capability URL →
GET re-edit (pre-filled) → POST update → 200, raw YAML browsable, ACL
deny → 403.
Docs: form/ section added to AGENTS.md and ARCHITECTURE.md. AGENTS.md
also documents the implementation-vs-dependency policy. CLAUDE.md repo-shape
list extended.
Deferred (v1+): .zddc editor migration onto this system, file-as-truth
lossless YAML round-trip, ui:show-when conditional visibility, oneOf/anyOf,
apps-cascade preview hook, cascade-fetched form definitions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
3.6 KiB
CSS
200 lines
3.6 KiB
CSS
/* form/ — ZDDC generic form renderer.
|
|
Pulls theme tokens from shared/base.css; only adds form-specific layout. */
|
|
|
|
.form-main {
|
|
max-width: 800px;
|
|
margin: 1.5rem auto;
|
|
padding: 0 1rem 4rem;
|
|
}
|
|
|
|
.form-status {
|
|
padding: 0.75rem 1rem;
|
|
margin-bottom: 1rem;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--color-border);
|
|
}
|
|
|
|
.form-status.is-error {
|
|
background: var(--color-bg-alt);
|
|
border-color: #c43;
|
|
color: #c43;
|
|
}
|
|
|
|
.form-status.is-success {
|
|
background: var(--color-bg-alt);
|
|
border-color: #283;
|
|
color: #283;
|
|
}
|
|
|
|
.form-root {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.form-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.form-field__label {
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.form-field__label .required-mark {
|
|
color: #c43;
|
|
margin-left: 0.15rem;
|
|
}
|
|
|
|
.form-field__description {
|
|
font-size: 0.85rem;
|
|
color: var(--color-text-muted, #666);
|
|
}
|
|
|
|
.form-field__error {
|
|
font-size: 0.85rem;
|
|
color: #c43;
|
|
margin-top: 0.15rem;
|
|
}
|
|
|
|
.form-field__help {
|
|
font-size: 0.8rem;
|
|
color: var(--color-text-muted, #666);
|
|
font-style: italic;
|
|
}
|
|
|
|
.form-field__input,
|
|
.form-field__textarea,
|
|
.form-field__select {
|
|
padding: 0.5rem 0.65rem;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
background: var(--color-bg, #fff);
|
|
color: var(--color-text, #111);
|
|
font: inherit;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-field__textarea {
|
|
min-height: 5em;
|
|
resize: vertical;
|
|
}
|
|
|
|
.form-field__input:focus,
|
|
.form-field__textarea:focus,
|
|
.form-field__select:focus {
|
|
outline: 2px solid var(--color-primary, #1e3a5f);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
.form-field--invalid .form-field__input,
|
|
.form-field--invalid .form-field__textarea,
|
|
.form-field--invalid .form-field__select {
|
|
border-color: #c43;
|
|
}
|
|
|
|
.form-field__radio-group,
|
|
.form-field__checkbox-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.form-field__radio-group label,
|
|
.form-field__checkbox-group label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: 400;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.form-fieldset {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
padding: 0.75rem 1rem 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.form-fieldset__legend {
|
|
font-weight: 600;
|
|
padding: 0 0.4rem;
|
|
}
|
|
|
|
.form-array {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.form-array__row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: flex-start;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
padding: 0.5rem;
|
|
background: var(--color-bg-alt, #f6f6f8);
|
|
}
|
|
|
|
.form-array__row-body {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.form-array__row-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.form-array__add {
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.form-actions {
|
|
margin-top: 1.5rem;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--color-border);
|
|
background: var(--color-bg, #fff);
|
|
color: var(--color-text, #111);
|
|
cursor: pointer;
|
|
font: inherit;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: var(--color-bg-alt, #f6f6f8);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--color-primary, #1e3a5f);
|
|
color: #fff;
|
|
border-color: var(--color-primary, #1e3a5f);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
filter: brightness(1.1);
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 0.2rem 0.5rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.btn[disabled] {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|