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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The format follows [Keep a Changelog] (https://keepachangelog.com/en/1.1.0/).
- **Required certificate tags have their own Settings tab and protect their fields** (#1734). The list moved from Advanced to a new Certificates tab; the layout and template screens list the configured tags instead of a fixed three. A field a required tag calls for is amber, badged, always required, and cannot be renamed or removed; a form missing one is warned on save.
- **Required form fields are a list of their own** (#1741). Settings → Certificates keeps the layout tags and, apart, the fields every form needs with label and type (default `name`, `cpf_rf`, `email`; Email stays). New forms start with them, their type is locked, and the editor adds missing ones.
- **Ordered three-level settings use the steps control** (#1745). The four geofence display modes, the two private-calendar displays and the IP API fallback store the same values as before; an unknown stored value shows the old default.
- **The on/off toggle joins the control family** (#1748). `.ffc-toggle` is now `.ffc-control-toggle` everywhere, with the shared hover and pressed halo, a greyed disabled state instead of a faded one, and an optional check / cross mark on the knob.

### Fixed

Expand All @@ -36,6 +37,7 @@ The format follows [Keep a Changelog] (https://keepachangelog.com/en/1.1.0/).
- **Settings → Certificates saves the required tags one per line** (#1736). The Save button stored the list on a single line, which then read as one tag; a list already stored that way reads as separate tags again.
- **A certificate form submits again** (#1739). A script printed before `ffc-core.js` that created the `FFC` namespace made the core skip its own API, so every submit failed on `FFC.request is not a function`; the core now installs its API and keeps what was already there.
- **A quiz attempt that did not pass is no longer a certificate** (#1744). The first attempt was saved as issued and its certificate link emailed before its result was known; its code also verified. The email, verification by code or link, the REST lookup and the user dashboard now skip it.
- **The dashboard's notification toggles can be reached by keyboard** (#1748). They used a second toggle that hid its checkbox with `display: none`; they now use the plugin's one toggle.

## [6.38.0] (2026-10-12) — `529e80a1`

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ A setting that picks among a few values is drawn by a control from the `ffc-cont
| 2 to 6 with no order | segmented | `AdminUI::render_segmented()` (planned) |
| anything else | a `<select>` | — |

- **The toggle still wears its pre-family class**, `.ffc-toggle`; moving it and every screen that prints it to `ffc-control-toggle` is planned, in the PR that gives it the shared states.
- **One toggle, everywhere** (#1748). `.ffc-control-toggle` is the only on/off control, the user dashboard's included — it had a second implementation that hid its checkbox with `display: none`, which took it out of keyboard reach. A toggle built in JS prints the same markup `render_toggle()` does. `mark` adds a check or a cross on the knob; the knob is white in both themes, so the mark's ink is fixed too, and the drawings come from `Core\Icons` through `--ffc-icon-mask-*` custom properties (`Icons::MASKS`).
- **Order is the test for steps.** A rail and a fill say "more of the same thing"; three unrelated choices on a rail lie about their relation. Off → Moderate → Strict is a scale; Dark / Light / Auto is not.
- **Out of range is handled, not refused.** `render_steps()` given fewer or more options than it draws prints a select, so the field still works, and calls `_doing_it_wrong()` naming the control that fits — which surfaces under `WP_DEBUG`, where the developer is.
- **Every control is native inputs plus CSS.** Radios or a checkbox carry the value, so the form posts without a script, the arrow keys and screen readers work, and autosave (`data-ffc-autosave-key` / `-form-key` on the inputs) saves the group as one field with no change to the widget.
Expand Down Expand Up @@ -943,7 +943,7 @@ Classes that already encapsulate `get_option('ffc_settings')` in their own priva

### Settings writes — autosave toggles (the no-clobber invariant)

On/off **toggle switches** (`.ffc-toggle`, rendered via `AdminUI::render_toggle()`) auto-save on flip: mark the input `data-ffc-autosave-key="<key>"`, add the key to `SettingsAjaxEndpoint::allowlist()` (`{option, path?, type, cap}`), and have the tab call `enqueue_autosave_infra()`. This is the standard for atomic, side-effect-free boolean settings. **Two autosave endpoints exist and stay separate on purpose** (do NOT unify — bad-façade trap): `SettingsAjaxEndpoint` (`ffc_update_setting`) writes WP **options** under a global cap; `FormMetaAjaxEndpoint` (`ffc_update_form_meta`, attribute `data-ffc-autosave-form-key`) writes per-post **meta** gated on `edit_post` of that exact form. **The JS is one widget serving both** (#1116) — `FFC.Admin.autoSaveField` in `assets/js/ffc-admin-autosave.js`, wired by `bootAutoSaveFields()`, which scans **both** attributes and takes `action` / `payload` / `request` / `strings` per endpoint. The endpoints stay separate; only their client did not need to be. That statement was written here before it was true: the form-meta half was a second implementation inside `assets/js/ffc-admin.js` (delegated, `change` only, no debounce, its own badge whose CSS hardcoded hex and therefore ignored dark mode), and the cost was not theoretical — the #1114 validity guard had to be written twice to reach both halves. So a change to autosave behaviour is now one edit; **a screen that renders either attribute must enqueue `ffc-admin-autosave`** (settings tabs get it from `enqueue_autosave_infra()`, the form editor enqueues it directly), or its fields silently have no listener.
On/off **toggle switches** (`.ffc-control-toggle`, rendered via `AdminUI::render_toggle()`) auto-save on flip: mark the input `data-ffc-autosave-key="<key>"`, add the key to `SettingsAjaxEndpoint::allowlist()` (`{option, path?, type, cap}`), and have the tab call `enqueue_autosave_infra()`. This is the standard for atomic, side-effect-free boolean settings. **Two autosave endpoints exist and stay separate on purpose** (do NOT unify — bad-façade trap): `SettingsAjaxEndpoint` (`ffc_update_setting`) writes WP **options** under a global cap; `FormMetaAjaxEndpoint` (`ffc_update_form_meta`, attribute `data-ffc-autosave-form-key`) writes per-post **meta** gated on `edit_post` of that exact form. **The JS is one widget serving both** (#1116) — `FFC.Admin.autoSaveField` in `assets/js/ffc-admin-autosave.js`, wired by `bootAutoSaveFields()`, which scans **both** attributes and takes `action` / `payload` / `request` / `strings` per endpoint. The endpoints stay separate; only their client did not need to be. That statement was written here before it was true: the form-meta half was a second implementation inside `assets/js/ffc-admin.js` (delegated, `change` only, no debounce, its own badge whose CSS hardcoded hex and therefore ignored dark mode), and the cost was not theoretical — the #1114 validity guard had to be written twice to reach both halves. So a change to autosave behaviour is now one edit; **a screen that renders either attribute must enqueue `ffc-admin-autosave`** (settings tabs get it from `enqueue_autosave_infra()`, the form editor enqueues it directly), or its fields silently have no listener.

Two things the convergence measured, worth not re-deriving. Every one of the 16 form-meta fields is a checkbox from `AdminUI::render_toggle()`, so adopting the settings half's `change`+`input` binding changed nothing visible — there is no text field on that path to "save while typing", and a checkbox's two events coalesce in the debounce. And the **delegated** binding was the half *not* kept, against the original proposal: nothing exercises it (all fields of both kinds are server-rendered at page load), and adopting it would have meant rewriting the half with 115 fields, per-field debounce, multi-checkbox groups and `confirmOff` to gain that. `bootAutoSaveFields()` is idempotent and exported, so DOM inserted later has a supported answer — call it again.

Expand Down
4 changes: 2 additions & 2 deletions assets/css/ffc-calendar-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@

/* Email notification toggles — vertical stack with breathing room.
The container drives the stacking (flex column): the base
`.form-table td .ffc-toggle { display: inline-flex }` rule in ffc-common.css
is more specific than a `.ffc-email-toggles .ffc-toggle` override, so making
`.form-table td .ffc-control-toggle { display: inline-flex }` rule in ffc-common.css
is more specific than a `.ffc-email-toggles .ffc-control-toggle` override, so making
each toggle a column flex-item is what reliably puts one per line without an
!important or a specificity arms race. `flex-start` keeps each toggle at its
natural width instead of stretching across the cell. */
Expand Down
133 changes: 108 additions & 25 deletions assets/css/ffc-common.css
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,11 @@
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13 2L4 14h7l-1 8 9-12h-7z'/%3E%3C/svg%3E");
}

:root {
--ffc-icon-mask-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E");
--ffc-icon-mask-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E");
}

.ffc-svg-icon {
color: var(--ffc-icon-tone, currentColor);
}
Expand Down Expand Up @@ -2008,19 +2013,32 @@
.ffc-color-dot { display: inline-block; width: 12px; height: 12px; border-radius: var(--ffc-radius-full); vertical-align: middle; background-color: var(--ffc-color, currentColor); }

/* ==========================================================================
TOGGLE SWITCH — mobile-style ON/OFF for boolean settings.
Replaces native checkbox visuals while preserving the underlying input
for accessibility (screen readers, keyboard nav, form-POST semantics).
Markup contract:
<label class="ffc-toggle">
TOGGLE (#1748) — AdminUI::render_toggle(), the two-state member of the
ffc-control-* family (see CONTROLS below). Markup:
<label class="ffc-control-toggle">
<input type="checkbox" name="..." value="1" checked>
<span class="ffc-toggle-track" aria-hidden="true"></span>
<span class="ffc-toggle-label">Field label text</span>
<span class="ffc-control-toggle-track" aria-hidden="true"></span>
<span class="ffc-control-toggle-label">Field label text</span>
</label>
It shares the family's state language: a halo for hover and pressed, the
2px primary ring for keyboard focus, and a disabled state that greys
instead of fading. .ffc-control-toggle-mark adds a check or a cross on the
knob, so on and off do not rest on colour alone.
========================================================================== */
.ffc-toggle,
.form-table td .ffc-toggle,
.form-table td fieldset .ffc-toggle {
/* The family's shared hover / pressed halo. */
.ffc-control-toggle,
.ffc-control-steps {
--ffc-control-halo: color-mix(in srgb, var(--ffc-primary) 18%, transparent);
--ffc-control-knob-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
/* The ink of a mark drawn on the toggle's knob. The knob is white in both
themes (see below), so its ink is fixed too: --ffc-primary lightens in
the dark theme and would fall under 3:1 on white (#1748). */
--ffc-control-knob-ink: #50575e;
}

.ffc-control-toggle,
.form-table td .ffc-control-toggle,
.form-table td fieldset .ffc-control-toggle {
/* Multiple selectors win the specificity battle against WP admin's
`.form-table td fieldset label { display: inline-block }` which
would otherwise collapse the toggle and overlap the label text. */
Expand All @@ -2033,7 +2051,7 @@
position: relative;
}

.ffc-toggle input[type="checkbox"] {
.ffc-control-toggle input[type="checkbox"] {
/* Visually hidden but reachable by keyboard + assistive tech. */
position: absolute;
opacity: 0;
Expand All @@ -2042,8 +2060,9 @@
pointer-events: none;
}

.ffc-toggle-track {
.ffc-control-toggle-track {
position: relative;
isolation: isolate;
flex: 0 0 auto;
width: 38px;
height: 22px;
Expand All @@ -2052,7 +2071,7 @@
transition: background-color 0.15s ease;
}

.ffc-toggle-track::after {
.ffc-control-toggle-track::after {
content: "";
position: absolute;
top: 2px;
Expand All @@ -2064,39 +2083,104 @@
theme the track is dark, and a knob in var(--ffc-bg-card) would vanish
into it. Literal by decision, not by oversight (#1126 B). */
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transition: transform 0.15s ease;
box-shadow: var(--ffc-control-knob-shadow);
transition: transform 0.15s ease, width 0.15s ease, box-shadow 0.15s ease;
}

.ffc-toggle input[type="checkbox"]:checked + .ffc-toggle-track {
.ffc-control-toggle input[type="checkbox"]:checked + .ffc-control-toggle-track {
background: var(--ffc-primary);
}

.ffc-toggle input[type="checkbox"]:checked + .ffc-toggle-track::after {
.ffc-control-toggle input[type="checkbox"]:checked + .ffc-control-toggle-track::after {
transform: translateX(16px);
}

.ffc-toggle input[type="checkbox"]:focus-visible + .ffc-toggle-track {
.ffc-control-toggle:hover input[type="checkbox"]:not(:disabled) + .ffc-control-toggle-track::after {
box-shadow: var(--ffc-control-knob-shadow), 0 0 0 7px var(--ffc-control-halo);
}

/* Pressed: the knob stretches towards where it is going. */
.ffc-control-toggle:active input[type="checkbox"]:not(:disabled) + .ffc-control-toggle-track::after {
width: 22px;
box-shadow: var(--ffc-control-knob-shadow), 0 0 0 9px var(--ffc-control-halo);
}

.ffc-control-toggle:active input[type="checkbox"]:checked:not(:disabled) + .ffc-control-toggle-track::after {
transform: translateX(12px);
}

.ffc-control-toggle input[type="checkbox"]:focus-visible + .ffc-control-toggle-track {
outline: 2px solid var(--ffc-primary);
outline-offset: 2px;
}

.ffc-toggle input[type="checkbox"]:disabled + .ffc-toggle-track {
opacity: 0.5;
/* Disabled: the colour leaves, nothing fades. */
.ffc-control-toggle input[type="checkbox"]:disabled + .ffc-control-toggle-track {
background: var(--ffc-gray-300);
cursor: not-allowed;
}

.ffc-toggle-label {
.ffc-control-toggle input[type="checkbox"]:disabled:checked + .ffc-control-toggle-track {
background: var(--ffc-gray-400);
}

.ffc-control-toggle input[type="checkbox"]:disabled + .ffc-control-toggle-track::after {
box-shadow: none;
}

.ffc-control-toggle:has(input[type="checkbox"]:disabled) {
cursor: not-allowed;
}

.ffc-control-toggle:has(input[type="checkbox"]:disabled) .ffc-control-toggle-label {
color: var(--ffc-text-muted);
}

.ffc-control-toggle-label {
color: inherit;
}

/* Mark: the glyph rides above the knob; its shape, not its colour, says on or off. */
.ffc-control-toggle-mark .ffc-control-toggle-track::before {
content: "";
position: absolute;
/* Above the knob (::after, which paints later at z-index auto). */
z-index: var(--ffc-z-raised);
top: 6px;
left: 6px;
width: 10px;
height: 10px;
background-color: var(--ffc-control-knob-ink);
-webkit-mask: var(--ffc-icon-mask-x) center / contain no-repeat;
mask: var(--ffc-icon-mask-x) center / contain no-repeat;
transition: transform 0.15s ease;
}

.ffc-control-toggle-mark input[type="checkbox"]:checked + .ffc-control-toggle-track::before {
-webkit-mask-image: var(--ffc-icon-mask-check);
mask-image: var(--ffc-icon-mask-check);
transform: translateX(16px);
}

.ffc-control-toggle-mark input[type="checkbox"]:disabled + .ffc-control-toggle-track::before {
background-color: var(--ffc-gray-400);
}

@media (prefers-reduced-motion: reduce) {
.ffc-toggle-track,
.ffc-toggle-track::after {
.ffc-control-toggle-track,
.ffc-control-toggle-track::before,
.ffc-control-toggle-track::after {
transition: none;
}
}

@media (forced-colors: active) {
.ffc-control-toggle-mark .ffc-control-toggle-track::before {
forced-color-adjust: none;
background-color: CanvasText;
}
}

/* ==========================================================================
CONTROLS (#1745) — the .ffc-control-* family, drawn by AdminUI.
One state language for all of them: a halo (--ffc-control-halo) for hover
Expand All @@ -2115,7 +2199,6 @@
A stop's data-tone overrides the palette for that step.
========================================================================== */
.ffc-control-steps {
--ffc-control-halo: color-mix(in srgb, var(--ffc-primary) 18%, transparent);
--ffc-control-steps-rail: 6px;
--ffc-control-steps-dot: 8px;
--ffc-control-steps-thumb: 20px;
Expand Down Expand Up @@ -2451,7 +2534,7 @@
gap: var(--ffc-spacing-2xs);
}

.ffc-features-cell .ffc-toggle {
.ffc-features-cell .ffc-control-toggle {
margin: 0;
}

Expand Down
2 changes: 1 addition & 1 deletion assets/css/ffc-common.min.css

Large diffs are not rendered by default.

48 changes: 0 additions & 48 deletions assets/css/ffc-user-dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -1199,54 +1199,6 @@
gap: var(--ffc-spacing-md);
}

.ffc-toggle-label {
display: flex;
align-items: center;
gap: var(--ffc-spacing-lg);
cursor: pointer;
font-size: var(--ffc-font-size-base);
color: var(--ffc-text);
padding: var(--ffc-spacing-xs) 0;
}

.ffc-toggle-label input[type="checkbox"] {
display: none;
}

.ffc-toggle-switch {
position: relative;
width: 40px;
height: 22px;
background: var(--ffc-border);
border-radius: 11px;
transition: background 0.2s;
flex-shrink: 0;
}

.ffc-toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
/* Branco nos dois temas de propósito: no escuro a pista é escura, e um
botão em var(--ffc-bg-card) sumiria dentro dela. Mesma decisão do
interruptor de ffc-common.css (#1126). */
background: #fff;
border-radius: var(--ffc-radius-full);
transition: transform 0.2s;
box-shadow: var(--ffc-shadow-sm);
}

.ffc-toggle-label input:checked + .ffc-toggle-switch {
background: var(--ffc-primary);
}

.ffc-toggle-label input:checked + .ffc-toggle-switch::after {
transform: translateX(18px);
}

/* ==========================================================================
LGPD SECTION
========================================================================== */
Expand Down
2 changes: 1 addition & 1 deletion assets/css/ffc-user-dashboard.min.css

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions assets/css/ffc-user-permissions.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Grouped-card layout for the per-user capability manager on the WordPress
* user-edit / profile screen. The toggle switch itself comes from
* ffc-common.css (.ffc-toggle); this file adds the panel, context summary,
* ffc-common.css (.ffc-control-toggle); this file adds the panel, context summary,
* search/preset toolbar, collapsible group cards, slug chips and origin
* badges.
*
Expand Down Expand Up @@ -429,12 +429,12 @@
}

/* role-granted capability: amber, "locked" toggle */
.ffc-toggle.ffc-cap-toggle--byrole input[type="checkbox"]:checked + .ffc-toggle-track {
.ffc-control-toggle.ffc-cap-toggle--byrole input[type="checkbox"]:checked + .ffc-control-toggle-track {
background: var(--ffc-warning);
cursor: not-allowed;
}

.ffc-toggle.ffc-cap-toggle--byrole .ffc-toggle-track::after {
.ffc-control-toggle.ffc-cap-toggle--byrole .ffc-control-toggle-track::after {
box-shadow: 0 0 0 2px var(--ffc-warning-bg);
}

Expand Down
Loading
Loading