Skip to content

feat(certificates): required form fields as their own list - #1742

Merged
rpgmem merged 1 commit into
developfrom
claude/required-form-fields
Oct 13, 2026
Merged

rpgmem merged 1 commit into
developfrom
claude/required-form-fields

Conversation

@rpgmem

@rpgmem rpgmem commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Closes #1741

Problem

One list, Settings → Certificates → Required Certificate Tags, did two jobs. It named the tags a certificate layout must print, and it named the form fields a form must have. A field derived from a tag had no label and no type of its own: the builder locked its name but left its type free, so a required email could be saved as Text. The defaults also could not express "{{schedule}} in the layout, but no field for it".

Change

  • Settings → Certificates now has two cards.
    • Required Certificate Tags (the layout). Default {{auth_code}}, {{name}}, {{cpf_rf}}, {{schedule}}. {{auth_code}} stays forced. {{schedule}} is asked only of a form with an Event Schedule (FormEditorSaveValidator::layout_required_tags()).
    • Required Form Fields, a new table of field name, label and type. Types are limited to text, textarea, email, number and date. Rows can be added and removed, except the Email row, which is locked and also forced by the reader. Default rows are name (Text), cpf_rf (Text) and email (Email). An administrator can remove name or cpf_rf.
  • Form editor
    • A new form is seeded from the table.
    • A required field keeps its name, type and required flag; only the label is editable. The type is posted through a hidden input next to a disabled select.
    • The save handler forces the configured type.
    • A form missing required fields shows a notice with an "Add the required fields" button, which adds them as locked rows (ffc-admin-field-builder.js).
  • Compatibility
    • An install that never saved the table derives it from its old tag list: each tag the plugin does not generate itself becomes a Text field.
    • A tag list saved before this change never named {{schedule}}, so the reader keeps the schedule check until the tab is saved again.
  • required_form_fields is added to the undeclared-settings baseline, like required_certificate_tags. A declared default would be wrong here: when the key is absent, the reader takes the legacy path above.
  • New script ffc-certificate-settings.js for adding and removing table rows. A small CSS rule keeps the table inside the card.

Tests

  • PHPUnit:
    • SettingsReaderTest: default table, saved table with an invalid type or name, emptied table still requires Email, legacy derivation, legacy {{schedule}}.
    • RequiredCertificateFieldsTest: an administrator can drop name/cpf_rf but not Email, required_field(), {{schedule}} only with an Event Schedule.
    • SettingsSaveHandlerTest: table sanitising covers a duplicate name, a generated tag, a hyphen, an unknown type and an empty name; a save without the table leaves it alone.
    • FormEditorSaveHandlerTest: the type is forced, and missing fields are reported by name.
    • FormEditorBuilderMetaboxTest: type locked, missing notice and button, a complete form shows no notice, a new form is seeded.
  • Vitest: adding fields, which lock the row and drop the notice, and a payload that cannot be read; adding and removing table rows, where Email cannot be removed. All 2046 tests pass.
  • e2e on a local WordPress:
    • removed cpf_rf and added a course (Date) field, then saved and reloaded: both changes persisted;
    • a new form was seeded with the configured types;
    • an existing form offered the missing fields and added them locked, with no JS errors.
  • PHPStan, row shapes (level 9), PHPCS, ESLint, Stylelint, and the i18n, CSS and docs guards all pass locally.

Risk

There is no migration. The legacy path keeps existing installs behaving as before until the tab is saved. A required field saved with the wrong type is corrected on its form's next save.

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA


Generated by Claude Code

Settings → Certificates keeps the layout tags and, apart, a table of the
fields every certificate form must have (name, label, type; default
name, cpf_rf, email). Email cannot be removed. {{auth_code}} stays
forced; {{schedule}} is asked only of a form with an Event Schedule.

A new form starts with the table's fields; in the builder their name,
type and required flag are locked, and a form missing one offers an
"Add the required fields" button. The save handler forces the table's
type. An install that never saved the table derives it from the old
tag list, and keeps the schedule check until the tab is saved again.

Refs #1741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA
@rpgmem rpgmem added enhancement New feature or request area:certificates labels Oct 13, 2026 — with Claude
@rpgmem
rpgmem marked this pull request as ready for review October 13, 2026 16:58
@rpgmem
rpgmem merged commit b681be1 into develop Oct 13, 2026
21 checks passed
@rpgmem
rpgmem deleted the claude/required-form-fields branch October 13, 2026 16:58
rpgmem added a commit that referenced this pull request Oct 13, 2026
…tab, plugin icon and "View details" (#1753)

* docs(changelog): backfill the 6.38.0 release commit reference (#1711)

Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* feat(updates): give the plugin an icon on Dashboard → Updates (#1713)

* feat(updates): give the plugin an icon on Dashboard → Updates

GitHubUpdater now sends an icons map (1x, 2x, svg) in the update object
and the View details payload, pointing at assets/img/brand/ in the
installed copy, so WordPress stops drawing its grey placeholder. The
directory also ships black, white, dark-screen and outline variants.
PluginIconAssetsTest ties each advertised URL to a file of the right
size that the release zip keeps.

Closes #1712

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(updates): ship every icon variant as PNG too

Each SVG variant now has a 128 px and a 256 px PNG beside it in
assets/img/brand/, rendered from the SVG, and PluginIconAssetsTest
checks every one exists at its size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* docs(changelog): name the PNG variants

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(updates): fill the View details modal with a banner and the readme's tabs (#1715)

Closes #1714


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* feat(updates): translate View details and open screenshots in an overlay (#1717)

* feat(updates): fill the View details modal with a banner and the readme's tabs

Closes #1714

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(updates): translate View details and open screenshots in an overlay

Closes #1716

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(updates): read the asset suffix instead of assuming .min

SCRIPT_DEBUG may already be defined by an earlier test in the process,
which made the enqueued URL drop the .min suffix in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* test: read the clock on both sides of calls that snap to a boundary (#1720)

The rate-limit window tests, the certificates dashboard midnight test and
the export filename test read the expected value from the clock once,
before or after the call, against code that rounds the current time to a
minute, hour, day, week, month or site-timezone midnight. A run that
crossed the boundary between the two reads failed with nothing changed.
They now read it before and after and accept either.

CLAUDE.md records the clock rule, and that a readme.txt change carries
languages/readme-pt_BR.txt in the same PR, with the release cross-check.

Closes #1718
Closes #1719


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* chore(i18n): compile the .l10n.php in wp i18n make-php's shape (#1722)

The compiler wrote every .po header and no domain. It now writes WP-CLI's
shape: domain from X-Domain, six headers in WP-CLI's order, then
messages, with an integer-like key bare as PHP stores it. The recompiled
catalogue is byte-identical to `wp i18n make-php`'s output; only the
header keys changed. CLAUDE.md records the shape, and how to run the
fresh-install checks against the container's MariaDB without the
uninstall step that would delete a symlinked checkout.

Closes #1721


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* fix(admin): dark mode for what core paints, docs navigation overflow (#1724)

* fix(admin): extend dark mode to native controls and fix the docs navigation overflow

- Declare color-scheme: dark on admin screens, so scrollbars and the
  time picker's clock follow the theme.
- Style the colour picker's label and Iris popup, .button-link,
  checkboxes and radios with the palette.
- Make the Quick Navigation search box fill its card; it was 25em in a
  280px column and scrolled the card sideways.
- Restore the padding core's .widefat tfoot rule removed from the
  Geolocation "Add new location" fields.

Closes #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): cover links, title actions, notices and core boxes in dark mode

A sweep of every FFC admin screen in Chromium flagged what core paints by
itself on the dark canvas: links (1.87-2.65:1), "Add New" / "Export CSV",
disabled primary buttons (core uses !important), colour inputs, the file
button, notice links, the Publish box footer, row actions and Screen
Options. Each now reads the palette.

Also: the form builder's row title reads the text colour instead of the
grip's grey, the submissions ID column is sized for six digits, and the
QR generator's required mark uses ffc-required.

Refs #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(audience): give the booking details title the theme's text colour

The admin modal is appended to <body>, outside .wrap, so its heading took
core's near-black h3 and vanished on the dark card.

Refs #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the dark disabled primary button a forced-colors contour

ForcedColorsContourTest caught it: the rule paints a ground, so under
forced colours the button would lose its boundary.

Refs #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(admin): dark mode on the Personal Calendars and Certificate templates screens (#1726)

* fix(admin): extend dark mode to native controls and fix the docs navigation overflow

- Declare color-scheme: dark on admin screens, so scrollbars and the
  time picker's clock follow the theme.
- Style the colour picker's label and Iris popup, .button-link,
  checkboxes and radios with the palette.
- Make the Quick Navigation search box fill its card; it was 25em in a
  280px column and scrolled the card sideways.
- Restore the padding core's .widefat tfoot rule removed from the
  Geolocation "Add new location" fields.

Closes #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): cover links, title actions, notices and core boxes in dark mode

A sweep of every FFC admin screen in Chromium flagged what core paints by
itself on the dark canvas: links (1.87-2.65:1), "Add New" / "Export CSV",
disabled primary buttons (core uses !important), colour inputs, the file
button, notice links, the Publish box footer, row actions and Screen
Options. Each now reads the palette.

Also: the form builder's row title reads the text colour instead of the
grip's grey, the submissions ID column is sized for six digits, and the
QR generator's required mark uses ffc-required.

Refs #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(audience): give the booking details title the theme's text colour

The admin modal is appended to <body>, outside .wrap, so its heading took
core's near-black h3 and vanished on the dark card.

Refs #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): dark mode on the Personal Calendars and Certificate templates screens

Their lists and editors are WordPress post screens that
AdminAssetsManager::is_ffc_page() never matches, so the toggle that puts
ffc-dark-mode on <html> never loaded and they rendered light.
SelfSchedulingAdmin and CertTemplateAdminScreen now enqueue the palette
and the toggle themselves. The false TOGGLE_NOT_NEEDED reason for the
calendar editor is rewritten, and the stale SelfSchedulingAdmin entry
leaves the register.

The rich-text e-mail editor's tabs, toolbar and status bar, and the
"Add title" placeholder, now read the palette.

Closes #1725

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the dark disabled primary button a forced-colors contour

ForcedColorsContourTest caught it: the rule paints a ground, so under
forced colours the button would lose its boundary.

Refs #1723

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the dark active TinyMCE button a forced-colors contour

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(public-design): form fields follow the form's font, and Arial is offered (#1728)

Fields, selects, textareas and buttons inherit the font the labels use
instead of the browser's own. Public Design offers the same three fonts
as Email Signature Generator and Institutional Email: the theme's (the
default), the plugin's system font, or Arial.

Closes #1727


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* feat(forms): checkbox fields render their options, and every field carries limits (#1733)

A "Checkbox" field fell through to a generic input styled as a 45px text
box, ignored its options and posted `on`. Options were one
comma-separated string, so none could contain a comma. Required, length,
range and "is one of the options" were enforced by the browser only.

- Core\FormField reads a field's options (the new `choices` list or the
  legacy string) and its limits, and sanitises an editor row.
- The field builder edits options one per row, shuffles, caps checkbox
  selections, and takes min/max length (ceilings 255 / 2,000), number
  ranges with decimal places and date ranges. A new row is a copy of
  the server template instead of a second markup built in JS.
- Frontend\FormFieldRenderer renders radio/checkbox groups as a
  fieldset, the limits as attributes and a hint, and a counter;
  ffc-form-fields.js shuffles, counts and enforces the cap.
- Submission\FieldRulesGuard enforces every rule on the server.

Closes #1729


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* feat(forms): standard fields validate as they claim, and required tags protect their fields (#1735)

* feat(forms): checkbox fields render their options, and every field carries limits

A "Checkbox" field fell through to a generic input styled as a 45px text
box, ignored its options and posted `on`. Options were one
comma-separated string, so none could contain a comma. Required, length,
range and "is one of the options" were enforced by the browser only.

- Core\FormField reads a field's options (the new `choices` list or the
  legacy string) and its limits, and sanitises an editor row.
- The field builder edits options one per row, shuffles, caps checkbox
  selections, and takes min/max length (ceilings 255 / 2,000), number
  ranges with decimal places and date ranges. A new row is a copy of
  the server template instead of a second markup built in JS.
- Frontend\FormFieldRenderer renders radio/checkbox groups as a
  fieldset, the limits as attributes and a hint, and a counter;
  ffc-form-fields.js shuffles, counts and enforces the cap.
- Submission\FieldRulesGuard enforces every rule on the server.

Closes #1729

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* wip: #1734

* feat(forms): standard fields validate as they claim, and required tags protect their fields

- cpf_rf: an invalid CPF/RF blocks the submit through custom validity and
  its message stays until corrected; maxlength 14, numeric keyboard.
- email: an invalid address is reported as such, checked with is_email(),
  instead of "Email address is required."
- Name fields need at least two words and 4 characters, in the browser
  and in FieldRulesGuard.
- "Required Certificate Tags" moves from Advanced to a new Certificates
  tab; the layout editor and the template screen list the configured
  tags instead of a fixed three.
- A field a required tag calls for is badged and amber in the builder,
  always required, with a read-only name and no Remove; saving a form
  without one warns which tag has no field.

Closes #1734

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(forms): forced-colors contour for the required-tag badge, canonical email in the sanitizer

The badge and the locked field name paint only a ground, which forced-colors
discards, so both take a contour. The sanitizer canonicalises the address
before judging it, as IdentifierIdiomTest requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(settings): the Certificates tab saves the required tags one per line (#1737)

The section read the ffc_settings array after get_post_array() had run every
element through sanitize_text_field(), which turns line breaks into spaces,
so the Save button stored the list on one line and it read as one tag. The
textarea is now read raw and sanitised as a textarea, and the reader also
splits on whitespace, so a list already stored on one line reads again.

Closes #1736


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* fix(frontend): the core installs its API when the FFC namespace already exists (#1740)

ffc-form-fields.js prints before ffc-core.js and creates window.FFC, so the
core's `window.FFC = window.FFC || {…}` skipped its own object and every
certificate form submit failed on FFC.request is not a function. The core now
installs its API over an existing namespace, keeping what was already there,
and keeps modules registered before it loaded.

Closes #1739


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* feat(quiz): per-field scoring with correct answers, exact-match checkbox and range answers (#1738)

* feat(quiz): per-field scoring with correct answers, exact-match checkbox and range answers (in progress)

QuizQuestion reads a field's quiz configuration with the pre-#1730 rule as the
fallback; QuizScorer scores choice fields by correct option(s) or by points per
option, text when answered and number/date by a correct range; the editor gets
a per-field quiz block with per-option correct marks and points; show-correct
marks each question right or wrong; {{correct_count}} and {{question_count}}.

Tests, documentation and catalogues follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(quiz): tests, documentation and catalogues for per-field scoring

QuizScorer and QuizQuestion tests (each mode, legacy forms, the exact checkbox
set, the floored sum, ranges, the ampersand an option can carry); the save
handler stores the option rows and fits the question; the builder renders the
aligned marks and the quiz block; the per-question verdict on the form. The
Quiz topic describes the modes and the two new tokens; the verdict and the
option mark take a context so "Correct" does not read as the verb. A level-9
row-shape cast is fixed in the persister.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(quiz): the admin quiz toggle test no longer expects the old points input

The per-field quiz settings moved to the field builder, which has its own
tests; this one keeps checking the form-level settings the toggle shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(quiz): stub _x where the form editor renders the field builder

The option mark now takes a translation context, and the metabox renderer
test renders the builder rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(certificates): required form fields as their own list (#1742)

Settings → Certificates keeps the layout tags and, apart, a table of the
fields every certificate form must have (name, label, type; default
name, cpf_rf, email). Email cannot be removed. {{auth_code}} stays
forced; {{schedule}} is asked only of a form with an Event Schedule.

A new form starts with the table's fields; in the builder their name,
type and required flag are locked, and a form missing one offers an
"Add the required fields" button. The save handler forces the table's
type. An install that never saved the table derives it from the old
tag list, and keeps the schedule check until the tab is saved again.

Refs #1741


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

* feat(quiz): window-leave reset, unpassed attempts are not certificates, manual grading (#1743)

* feat(quiz): optional reset when the participant leaves the window

A quiz setting, off by default, clears the scored answers when the
participant switches tabs or minimises the browser. It listens to
visibilitychange, not blur, so a notification, a screen reader or a
click into an embedded video does not trigger it. The form warns before
the participant starts, and on return an alertdialog (the shared modal)
says why. Name, CPF/RF and unscored fields stay.

Leaving is not an attempt. The browser counts the exits in a hidden
field kept in sessionStorage; a reset quiz stores the count, capped at
999, as _quiz_focus_losses, shown read-only on the submission screen.
A deterrent, not a lock, and the documentation says so.

Refs #1731

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(quiz): an attempt that did not pass is not a certificate

process_submission() inserts every quiz attempt as an issued certificate
and schedules the email pipeline; the persister lowers the status to the
attempt's result right after. Nothing downstream read the status, so a
first attempt that failed was emailed a working certificate link and its
auth code verified.

Core\SubmissionStatus names the statuses and which of them is a
certificate. The async pipeline, verification by auth code and by magic
link, the REST lookup and the user dashboard (list and count) now skip a
quiz attempt that did not pass.

Closes #1744

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(quiz): manual grading of open answers

A text or textarea question can be set to "Manual grading". An attempt
with such an answer is stored as "Quiz: Awaiting Grading": no result,
no certificate, and the same CPF/RF cannot start another attempt while
it waits. The submission edit screen grades it through its own form
(admin_post_ffc_grade_submission, capability ffc_grade_certificates,
seeded from ffc_edit_certificates); saving publishes the attempt or
records Retry/Failed, and emails the participant the result through
two new editable templates.

Closes #1732

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(quiz): seed the grade-cap flag in the integration harness

The bootstrap smoke tests stub every one-shot capability flag so the
Loader does not run migrations against the harness's fake $wpdb; the
flag #1732 added was missing, so the grant ran and failed on
wpdb::get_blog_prefix().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(quiz): strict window-leave level on a reusable steps control (#1746)

* feat(quiz): strict window-leave level on a reusable three-position switch

The quiz reset on leaving now has three levels stored in
quiz_reset_on_leave (0 Off, 1 Moderate, 2 Strict; existing forms keep
their behaviour). Strict also counts the window losing the focus, after
a global grace period (Settings -> Certificates), ignoring focus moving
into an iframe of the page. "Start again" reloads the page so shuffled
options are shuffled again.

Adds AdminUI::render_tri_toggle (severity and traffic palettes, pure
CSS over native radios) and an enum type to both autosave endpoints.

Closes #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(css): budget the three-position switch's pill radius

The track is 30px high and its ends are half that; the `pill` role
(12px) would leave them visibly squared, so the literal stays and the
budget says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): steps control replaces the three-position switch

AdminUI::render_steps() draws an ordered scale of 3 to 11 values as a
rail with one native radio per stop, filled up to the chosen one. It
takes palette (primary, severity, traffic), per-step tones, labels
(all or selected), size, shape and fluid; outside 3-11 options it
prints a select and calls _doing_it_wrong() naming the control that
fits. Styles live in the new ffc-control-* family in ffc-common.css,
through tokens only, with no radius or z-index literal (the radius
budget goes back to 5). The quiz's Leaving the Window level uses it
with the severity palette. CLAUDE.md gains a Controls section saying
which control fits which kind of value.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the steps fieldset's phpcs annotation a real reason

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(admin): ordered three-level settings use the steps control (#1747)

* feat(quiz): strict window-leave level on a reusable three-position switch

The quiz reset on leaving now has three levels stored in
quiz_reset_on_leave (0 Off, 1 Moderate, 2 Strict; existing forms keep
their behaviour). Strict also counts the window losing the focus, after
a global grace period (Settings -> Certificates), ignoring focus moving
into an iframe of the page. "Start again" reloads the page so shuffled
options are shuffled again.

Adds AdminUI::render_tri_toggle (severity and traffic palettes, pure
CSS over native radios) and an enum type to both autosave endpoints.

Closes #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(css): budget the three-position switch's pill radius

The track is 30px high and its ends are half that; the `pill` role
(12px) would leave them visibly squared, so the literal stays and the
budget says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): steps control replaces the three-position switch

AdminUI::render_steps() draws an ordered scale of 3 to 11 values as a
rail with one native radio per stop, filled up to the chosen one. It
takes palette (primary, severity, traffic), per-step tones, labels
(all or selected), size, shape and fluid; outside 3-11 options it
prints a select and calls _doing_it_wrong() naming the control that
fits. Styles live in the new ffc-control-* family in ffc-common.css,
through tokens only, with no radius or z-index literal (the radius
budget goes back to 5). The quiz's Leaving the Window level uses it
with the severity palette. CLAUDE.md gains a Controls section saying
which control fits which kind of value.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): ordered three-level settings use the steps control

The four geofence display modes, the two private-calendar displays and
the geolocation IP API fallback move from selects to
AdminUI::render_steps(), ordered from the most shown (or most
permissive) to the least. Stored values are unchanged, and a value the
scale does not know shows the select's old default. The API fallback
wears the severity palette; the display modes keep the neutral one.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the steps fieldset's phpcs annotation a real reason

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(admin): the on/off toggle joins the ffc-control family (#1749)

* feat(quiz): strict window-leave level on a reusable three-position switch

The quiz reset on leaving now has three levels stored in
quiz_reset_on_leave (0 Off, 1 Moderate, 2 Strict; existing forms keep
their behaviour). Strict also counts the window losing the focus, after
a global grace period (Settings -> Certificates), ignoring focus moving
into an iframe of the page. "Start again" reloads the page so shuffled
options are shuffled again.

Adds AdminUI::render_tri_toggle (severity and traffic palettes, pure
CSS over native radios) and an enum type to both autosave endpoints.

Closes #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(css): budget the three-position switch's pill radius

The track is 30px high and its ends are half that; the `pill` role
(12px) would leave them visibly squared, so the literal stays and the
budget says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): steps control replaces the three-position switch

AdminUI::render_steps() draws an ordered scale of 3 to 11 values as a
rail with one native radio per stop, filled up to the chosen one. It
takes palette (primary, severity, traffic), per-step tones, labels
(all or selected), size, shape and fluid; outside 3-11 options it
prints a select and calls _doing_it_wrong() naming the control that
fits. Styles live in the new ffc-control-* family in ffc-common.css,
through tokens only, with no radius or z-index literal (the radius
budget goes back to 5). The quiz's Leaving the Window level uses it
with the severity palette. CLAUDE.md gains a Controls section saying
which control fits which kind of value.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): ordered three-level settings use the steps control

The four geofence display modes, the two private-calendar displays and
the geolocation IP API fallback move from selects to
AdminUI::render_steps(), ordered from the most shown (or most
permissive) to the least. Stored values are unchanged, and a value the
scale does not know shows the select's old default. The API fallback
wears the severity palette; the display modes keep the neutral one.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the steps fieldset's phpcs annotation a real reason

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): the on/off toggle joins the ffc-control family

`.ffc-toggle*` becomes `.ffc-control-toggle*` everywhere it is printed
(PHP, templates, JS-built markup, CSS and tests). The toggle gains the
family's hover and pressed halo, a greyed disabled state instead of an
opacity fade, and an optional `mark` that draws a check or a cross on
the knob from the Core\Icons registry (new Icons::MASKS custom
properties). The user dashboard's notification toggles drop their
second implementation, which hid the checkbox with display: none and
so took it out of keyboard reach.

Closes #1748

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): toggle mark layers on the token scale; dashboard radius budget locked in

The mark's z-index reads --ffc-z-raised and the knob keeps the default
stacking, so ffc-common.css adds no layer literal. Removing the
dashboard's private toggle switch left one radius literal fewer, so the
budget ratchets down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(admin): segmented control; steps and segmented keep their layout in a .form-table (#1751)

* feat(quiz): strict window-leave level on a reusable three-position switch

The quiz reset on leaving now has three levels stored in
quiz_reset_on_leave (0 Off, 1 Moderate, 2 Strict; existing forms keep
their behaviour). Strict also counts the window losing the focus, after
a global grace period (Settings -> Certificates), ignoring focus moving
into an iframe of the page. "Start again" reloads the page so shuffled
options are shuffled again.

Adds AdminUI::render_tri_toggle (severity and traffic palettes, pure
CSS over native radios) and an enum type to both autosave endpoints.

Closes #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* test(css): budget the three-position switch's pill radius

The track is 30px high and its ends are half that; the `pill` role
(12px) would leave them visibly squared, so the literal stays and the
budget says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): steps control replaces the three-position switch

AdminUI::render_steps() draws an ordered scale of 3 to 11 values as a
rail with one native radio per stop, filled up to the chosen one. It
takes palette (primary, severity, traffic), per-step tones, labels
(all or selected), size, shape and fluid; outside 3-11 options it
prints a select and calls _doing_it_wrong() naming the control that
fits. Styles live in the new ffc-control-* family in ffc-common.css,
through tokens only, with no radius or z-index literal (the radius
budget goes back to 5). The quiz's Leaving the Window level uses it
with the severity palette. CLAUDE.md gains a Controls section saying
which control fits which kind of value.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): ordered three-level settings use the steps control

The four geofence display modes, the two private-calendar displays and
the geolocation IP API fallback move from selects to
AdminUI::render_steps(), ordered from the most shown (or most
permissive) to the least. Stored values are unchanged, and a value the
scale does not know shows the select's old default. The API fallback
wears the severity palette; the display modes keep the neutral one.

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): give the steps fieldset's phpcs annotation a real reason

Refs #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): the on/off toggle joins the ffc-control family

`.ffc-toggle*` becomes `.ffc-control-toggle*` everywhere it is printed
(PHP, templates, JS-built markup, CSS and tests). The toggle gains the
family's hover and pressed halo, a greyed disabled state instead of an
opacity fade, and an optional `mark` that draws a check or a cross on
the knob from the Core\Icons registry (new Icons::MASKS custom
properties). The user dashboard's notification toggles drop their
second implementation, which hid the checkbox with display: none and
so took it out of keyboard reach.

Closes #1748

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): toggle mark layers on the token scale; dashboard radius budget locked in

The mark's z-index reads --ffc-z-raised and the knob keeps the default
stacking, so ffc-common.css adds no layer literal. Removing the
dashboard's private toggle switch left one radius literal fewer, so the
budget ratchets down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* feat(admin): segmented control for small unordered choices

AdminUI::render_segmented() draws 2 to 6 options with no order between
them as one row of native radios, the chosen one filled. Out of range it
prints a select and tells the developer. Dark Mode, Code Editor Theme and
the geofence Time Behavior use it, with the same stored values; the
autosave badge anchors after the control so it never breaks the
checked-label rule.

Closes #1750

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

* fix(admin): steps and segmented controls keep their layout in a .form-table

WordPress's forms.css makes every label in a .form-table fieldset
inline-block with a forced margin, which outranked the stop's flex column
and left each mark 28px left of its rail on every settings table that
adopted the steps control. Both controls' option labels now outrank it;
measured in a local WordPress, every mark sits on its stop's centre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

---------

Co-authored-by: Claude <noreply@anthropic.com>

* release: 6.39.0 — bump, finalize CHANGELOG, review docs (#1752)

Bumps the three version sites, renames [Unreleased] to [6.39.0]
(2026-10-13) and condenses it by story (25 bullets to 15, every cited
issue kept), rewrites the Upgrade Notice, and brings the readme's Quiz
Mode line, in both languages, up to per-field scoring, manual grading
and the leave-the-window reset.


Claude-Session: https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:certificates enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants