Skip to content

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

Merged
rpgmem merged 11 commits into
developfrom
claude/control-toggle
Oct 13, 2026
Merged

rpgmem merged 11 commits into
developfrom
claude/control-toggle

Conversation

@rpgmem

@rpgmem rpgmem commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Closes #1748

Stacked on #1747, which is stacked on #1746. Until those merge, this diff also shows their commits; develop comes in by a merge commit after each merge.

Problem

The two-state toggle predates the ffc-control-* family (#1745):

  • its classes belong to no family;
  • it has no hover or pressed state;
  • its disabled state fades through opacity: 0.5.

A second toggle also existed. The user dashboard's notification preferences built their own markup and CSS and hid the checkbox with display: none, so a keyboard user could not reach them.

Change

  • Rename everywhere it is printed: .ffc-toggle → .ffc-control-toggle, .ffc-toggle-track → .ffc-control-toggle-track, .ffc-toggle-label → .ffc-control-toggle-label. This covers PHP, templates, JS-built markup, CSS and tests. The CHANGELOG history is left as written.
  • User dashboard: the notification toggles print the same markup render_toggle() does. Their CSS (.ffc-toggle-switch) is deleted. The checkbox is focusable again.
  • States:
    • hover and pressed draw the family halo (--ffc-control-halo, now declared once for the family), and pressed also stretches the knob;
    • disabled is greyed through tokens with no opacity, and its OpacityContrastTest entry is removed.
  • mark (opt-in): draws a check or a cross on the knob, so on and off do not rest on colour alone.
    • The drawings come from Core\Icons through new --ffc-icon-mask-check / --ffc-icon-mask-x custom properties (Icons::MASKS, generated with the rest of the icon block).
    • The knob is white in both themes by an earlier decision, so the mark's ink is a fixed dark grey (--ffc-control-knob-ink). --ffc-primary lightens in the dark theme and would fall under 3:1 on white. That is one more colour literal in ffc-common.css, and the budget records why.
  • Knob shadow: now one token (--ffc-control-knob-shadow) instead of a repeated literal.
  • Docs: CLAUDE.md "Controls" and "Settings writes" updated; CHANGELOG has a Changed and a Fixed bullet.

Tests

  • PHP:
    • AdminUITest: mark is opt-in, and no ffc-toggle class is left;
    • IconsTest: every mask variable names a drawing and is emitted;
    • updated assertions in the tests that printed the old classes;
    • literal and opacity registers updated.
  • Vitest: the dashboard's notification toggle uses the family markup and its checkbox takes focus. All 135 files and 2063 tests pass.
  • Checks: PHPCS, PHPStan and ESLint pass on the touched files.

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_018srx5hc431EYmG4MHUr2KA


Generated by Claude Code

…itch

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
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
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
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
`.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
@rpgmem rpgmem added enhancement New feature or request area:infra labels Oct 13, 2026 — with Claude
…udget 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
# Conflicts:
#	languages/ffcertificate-pt_BR.l10n.php
#	languages/ffcertificate-pt_BR.mo
#	languages/ffcertificate-pt_BR.po
#	languages/ffcertificate.pot
@rpgmem
rpgmem marked this pull request as ready for review October 13, 2026 21:57
@rpgmem
rpgmem enabled auto-merge (squash) October 13, 2026 21:57
@rpgmem
rpgmem merged commit 1d4e7bd into develop Oct 13, 2026
21 checks passed
@rpgmem
rpgmem deleted the claude/control-toggle branch October 13, 2026 22:11
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:infra enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants