From 3df82c08a3301b1d241722d3cfab8464ff17faa9 Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Mon, 11 May 2026 13:36:38 +0200 Subject: [PATCH 01/11] add Ettic Admin UI scaffold (assets + Admin/Footer + reference Settings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the shared Ettic admin design system into the plugin: - assets/css/opentrust-admin.css, assets/js/opentrust-admin.js — the design tokens, components, and dirty-tracking/toast/modal JS used by upcoming admin migrations. New asset paths and handles; existing assets/css/admin.css and assets/js/admin.js are untouched. - includes/Admin/Footer.php — branded footer used across every OpenTrust admin screen. URL_DOCS points at plugins.ettic.nl/opentrust. - includes/Admin/Settings.php — reference implementation of the design system's page shell (topbar + sections + every control type). Loaded by PHPStan path scan but NOT booted; OpenTrust's existing admin classes migrate to the same markup vocabulary in the commits that follow. - opentrust.php — defines OPENTRUST_FILE / OPENTRUST_URL as aliases for the existing OPENTRUST_PLUGIN_FILE / OPENTRUST_PLUGIN_URL, so the shared-template files run unmodified. Requires Footer.php on load. Behavior unchanged in this commit: no menu page registered by the new files, no enqueue, no markup. Just installed and ready. Template source: /Users/nolderoos/Claude Code/Ettic Admin UI (post v1). --- assets/css/opentrust-admin.css | 1365 ++++++++++++++++++++++++++++++++ assets/js/opentrust-admin.js | 655 +++++++++++++++ includes/Admin/Footer.php | 56 ++ includes/Admin/Settings.php | 431 ++++++++++ opentrust.php | 14 + 5 files changed, 2521 insertions(+) create mode 100644 assets/css/opentrust-admin.css create mode 100644 assets/js/opentrust-admin.js create mode 100644 includes/Admin/Footer.php create mode 100644 includes/Admin/Settings.php diff --git a/assets/css/opentrust-admin.css b/assets/css/opentrust-admin.css new file mode 100644 index 0000000..715e1e9 --- /dev/null +++ b/assets/css/opentrust-admin.css @@ -0,0 +1,1365 @@ +/* Ettic admin design system template. + * + * BEFORE USING: replace `opentrust` everywhere with your plugin's slug + * (CSS class scope, JS data-attrs, PHP namespace, filenames). The CSS + * custom properties (`--ettic-*`) are the shared Ettic brand vocabulary + * and should stay — override `--ettic-blue` per plugin to re-skin. + * + * Scope: every selector lives under `.opentrust-admin` so tokens are + * inherited and styles cannot leak to other wp-admin screens. */ + +.opentrust-admin { + /* Brand */ + --ettic-blue: #0F5CFA; + --ettic-blue-hover: #0A4ED4; + --ettic-blue-fg: #FFFFFF; + --ettic-blue-soft: #DFEFFF; + + /* Surfaces */ + --ettic-ink: #031018; + --ettic-ink-soft: #051016; + --ettic-page: #F9FCFF; + --ettic-surface: #FFFFFF; + --ettic-surface-2: #F5F7FA; + --ettic-border: #E2E5E9; + --ettic-border-hi: #D4D8DD; + + /* Topbar (dark) */ + --tb-bg: #031018; + --tb-text: rgba(255,255,255,0.66); + --tb-text-hover: rgba(255,255,255,0.92); + --tb-text-strong: #FFFFFF; + --tb-divider: rgba(255,255,255,0.08); + + /* Text */ + --tx-strong: #051016; + --tx-body: #2A323B; + --tx-muted: #5A6573; + --tx-quiet: #7E8895; + + /* Status */ + --warn: #B95000; + --warn-bg: #FFF7EC; + --warn-border: #F4D7AC; + + /* Shape */ + --r-sm: 5px; + --r-md: 7px; + --r-lg: 10px; + + /* Type stack — Inter / JetBrains Mono if installed, else system */ + --ff-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + --ff-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + + font-family: var(--ff-sans); + font-size: 14px; + line-height: 1.5; + color: var(--tx-body); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.opentrust-admin *, +.opentrust-admin *::before, +.opentrust-admin *::after { box-sizing: border-box; } + +.opentrust-admin a { color: var(--ettic-blue); text-decoration: none; } +.opentrust-admin a:hover { text-decoration: underline; } + +.opentrust-admin code { + font-family: var(--ff-mono); + font-size: 0.92em; +} + +/* !important defends against themes/plugins that constrain .wrap max-width. */ +.wrap.opentrust-admin { + margin: 10px 20px 0 0 !important; + max-width: none !important; + width: auto !important; + min-width: 0 !important; + box-sizing: border-box; +} + +.opentrust-admin .opentrust-form, +.opentrust-admin .opentrust-topbar { width: 100%; max-width: none; } + +/* Hide default WP h1 */ +.wrap.opentrust-admin > h1.wp-heading-inline, +.wrap.opentrust-admin > h1:first-of-type { display: none; } + +/* Topbar — sticky dark strip + sibling hero block. Siblings under .opentrust-form + * so the bar's sticky containing block is the full form, not the short hero. */ +.opentrust-admin .opentrust-topbar__bar { + min-height: 56px; + display: flex; + align-items: stretch; + /* Bleeds past .wrap gutters + #wpcontent padding for true edge-to-edge. */ + margin: -10px -20px 0; + padding: 0 28px 0 38px; + gap: 4px; + border-bottom: 1px solid var(--tb-divider); + flex-wrap: wrap; + /* Sits below admin bar (32/46px); z-20 keeps it above content, below modals/toasts. */ + position: sticky; + top: 0; + z-index: 20; + background: var(--tb-bg); +} + +body.admin-bar .opentrust-admin .opentrust-topbar__bar { top: 32px; } + +@media screen and (max-width: 782px) { + body.admin-bar .opentrust-admin .opentrust-topbar__bar { top: 46px; } + /* Mobile: #wpcontent loses its padding — neutralize bleed so bar stays on-screen. */ + .opentrust-admin .opentrust-topbar__bar, + .opentrust-admin .opentrust-topbar__head { + margin-left: 0; + margin-right: 0; + } + .opentrust-admin .opentrust-topbar__bar { padding-left: 18px; padding-right: 8px; } + .opentrust-admin .opentrust-topbar__head { padding-left: 26px; padding-right: 26px; } +} + +.opentrust-admin .opentrust-topbar__brand { + display: flex; + align-items: center; + gap: 11px; +} + +.opentrust-admin .opentrust-topbar__mark { width: 26px; height: 26px; flex-shrink: 0; } + +.opentrust-admin .opentrust-topbar__name { + color: var(--tb-text-strong); + font-weight: 600; + font-size: 14.5px; + letter-spacing: -0.005em; +} + +.opentrust-admin .opentrust-topbar__version { + margin-left: 10px; + padding: 2px 8px; + font-size: 11px; + font-weight: 600; + font-family: var(--ff-mono); + color: rgba(255,255,255,0.55); + background: rgba(255,255,255,0.06); + border-radius: 4px; + letter-spacing: 0.02em; +} + +.opentrust-admin .opentrust-topbar__actions { + display: flex; + align-items: center; + gap: 6px; + padding: 0 0 0 8px; + flex-shrink: 0; +} + +/* Right cluster: dirty indicator + Save/Discard, glued to right edge. */ +.opentrust-admin .opentrust-topbar__right { + display: flex; + align-items: stretch; + margin-left: auto; + min-width: 0; +} + +/* Unsaved-changes indicator */ +.opentrust-admin .opentrust-topbar__dirty { + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px; + font-size: 12.5px; + color: rgba(255,255,255,0.80); + font-weight: 500; + flex-shrink: 0; + white-space: nowrap; +} + +.opentrust-admin .opentrust-topbar__dirty-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #F4A547; + position: relative; + flex-shrink: 0; + box-shadow: 0 0 0 0.5px rgba(244, 165, 71, 0.5); +} + +.opentrust-admin .opentrust-topbar__dirty-dot::after { + content: ''; + position: absolute; + inset: -3px; + border-radius: 50%; + background: rgba(244, 165, 71, 0.34); + animation: opentrust-dirty-pulse 2.2s ease-out infinite; + pointer-events: none; +} + +@keyframes opentrust-dirty-pulse { + 0% { transform: scale(0.5); opacity: 0.7; } + 80% { transform: scale(2.6); opacity: 0; } + 100% { transform: scale(2.6); opacity: 0; } +} + +.opentrust-admin .opentrust-topbar__dirty-num { + color: #fff; + font-weight: 600; + font-variant-numeric: tabular-nums; + margin-right: 2px; +} + +/* .is-clean hides the indicator; saved/discarded feedback goes through toasts. */ +.opentrust-admin .opentrust-topbar__dirty.is-clean { display: none; } + +@media (prefers-reduced-motion: reduce) { + .opentrust-admin .opentrust-topbar__dirty-dot::after { animation: none; } +} + +/* Page head: edge-to-edge hero that fuses with the sticky bar above. */ +.opentrust-admin .opentrust-topbar__head { + background: var(--tb-bg); + margin: 0 -20px 28px; + padding: 26px 46px 30px; +} + +.opentrust-admin .opentrust-topbar__head h1 { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.022em; + margin: 0 0 6px; + color: #fff; + line-height: 1.2; + padding: 0; +} + +.opentrust-admin .opentrust-topbar__head p { + margin: 0; + font-size: 14.5px; + color: rgba(255,255,255,0.62); + max-width: 70ch; + line-height: 1.55; +} + +/* Buttons */ +.opentrust-admin .opentrust-btn { + appearance: none; + border: 1px solid transparent; + background: transparent; + font-family: inherit; + font-size: 13px; + font-weight: 500; + padding: 7px 14px; + border-radius: var(--r-md); + cursor: pointer; + transition: background-color 130ms ease, border-color 130ms ease, color 130ms ease, box-shadow 130ms ease; + line-height: 1.4; + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; + text-decoration: none; + height: auto; +} + +.opentrust-admin .opentrust-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(15,92,250,0.30); } + +.opentrust-admin .opentrust-btn--primary { + background: var(--ettic-blue); + color: var(--ettic-blue-fg); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 1px 0 rgba(15,92,250,0.20); +} + +.opentrust-admin .opentrust-btn--primary:hover { background: var(--ettic-blue-hover); color: #fff; text-decoration: none; } + +.opentrust-admin .opentrust-btn--ghost { + border-color: var(--ettic-border); + color: var(--tx-strong); + background: #fff; +} + +.opentrust-admin .opentrust-btn--ghost:hover { + background: var(--ettic-surface-2); + border-color: var(--ettic-border-hi); + color: var(--tx-strong); + text-decoration: none; +} + +.opentrust-admin .opentrust-btn--ghost-dark { + border-color: rgba(255,255,255,0.16); + color: rgba(255,255,255,0.86); + background: transparent; + font-size: 13px; + padding: 6px 12px; +} + +.opentrust-admin .opentrust-btn--ghost-dark:hover { + background: rgba(255,255,255,0.06); + color: #fff; + border-color: rgba(255,255,255,0.22); + text-decoration: none; +} + +.opentrust-admin .opentrust-btn--text { color: var(--tx-muted); padding: 8px 8px; } +.opentrust-admin .opentrust-btn--text:hover { color: var(--tx-strong); background: var(--ettic-surface-2); text-decoration: none; } + +.opentrust-admin .opentrust-btn--danger { + background: #B32D2E; + color: #fff; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.16), 0 1px 0 rgba(179,45,46,0.20); +} +.opentrust-admin .opentrust-btn--danger:hover { background: #931E1F; color: #fff; } + +.opentrust-admin .opentrust-btn--sm { padding: 6px 11px; font-size: 13px; } + +.opentrust-admin .opentrust-btn[disabled], +.opentrust-admin .opentrust-btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; } + +.opentrust-admin .opentrust-btn--primary[disabled] { + background: rgba(255,255,255,0.08); + color: rgba(255,255,255,0.42); + box-shadow: none; + cursor: not-allowed; + pointer-events: none; +} + +.opentrust-admin .opentrust-btn--ghost-dark[disabled] { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; +} + +.opentrust-admin .opentrust-btn--loading { pointer-events: none; cursor: wait; } +.opentrust-admin .opentrust-btn--loading .opentrust-btn__label { opacity: 0.85; } + +.opentrust-admin .opentrust-btn__spinner { + width: 12px; + height: 12px; + border: 1.5px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: opentrust-spin 600ms linear infinite; + display: inline-block; + flex-shrink: 0; + margin-right: 2px; +} + +@keyframes opentrust-spin { to { transform: rotate(360deg); } } + +/* Stack / blocks (h2 outside cards) */ +/* Width contract: 92% / max 1600px / 0 auto — mirrored on .opentrust-footer + * so stack and footer line up at the same width. */ +.opentrust-admin .opentrust-stack { + display: flex; + flex-direction: column; + gap: 28px; + padding: 0 0 32px; + width: 92%; + max-width: 1600px; + margin: 0 auto; + box-sizing: border-box; +} + +.opentrust-admin .opentrust-block { padding: 0; } + +.opentrust-admin .opentrust-block__head { margin-bottom: 14px; } + +.opentrust-admin .opentrust-block__head h2 { + font-size: 18px; + font-weight: 700; + letter-spacing: -0.012em; + margin: 0; + color: var(--tx-strong); + line-height: 1.3; + padding: 0; +} + +.opentrust-admin .opentrust-block__head p { + font-size: 13.5px; + color: var(--tx-muted); + margin: 4px 0 0; + max-width: 68ch; + line-height: 1.55; +} + +/* Cards. Namespaced to avoid colliding with WP core `.card` (caps at 520px). */ +.opentrust-admin .opentrust-card { + background: var(--ettic-surface); + border: 1px solid var(--ettic-border); + border-radius: var(--r-lg); + overflow: hidden; +} + +/* Field rows */ +.opentrust-admin .opentrust-row { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 24px; + padding: 18px 14px; +} + +.opentrust-admin .opentrust-row + .opentrust-row { border-top: 1px solid var(--ettic-border); } + +.opentrust-admin .opentrust-row--stacked { + grid-template-columns: 1fr; + align-items: flex-start; +} + +.opentrust-admin .opentrust-row__main { min-width: 0; } + +.opentrust-admin .opentrust-row__label { + display: block; + font-size: 14px; + font-weight: 600; + color: var(--tx-strong); + margin-bottom: 3px; + letter-spacing: -0.002em; +} + +.opentrust-admin .opentrust-row__help { + margin: 0; + font-size: 13px; + color: var(--tx-muted); + line-height: 1.55; + max-width: 64ch; +} + +.opentrust-admin .opentrust-row__help code { + background: var(--ettic-surface-2); + padding: 1px 5px; + border-radius: 4px; + font-size: 11.5px; + color: var(--tx-strong); + border: 1px solid var(--ettic-border); +} + +.opentrust-admin .opentrust-row__control { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + position: relative; +} + +.opentrust-admin .opentrust-row__control--stack { + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +/* Per-row dirty mark (orange dot left of the control) */ +.opentrust-admin .opentrust-row__dirty-mark { + position: absolute; + top: 50%; + left: -14px; + transform: translateY(-50%); + width: 5px; + height: 5px; + border-radius: 50%; + background: #F4A547; + pointer-events: none; + opacity: 0; + transition: opacity 160ms ease; +} + +/* Stacked controls: pin dot to input row, not column center. */ +.opentrust-admin .opentrust-row__control--stack .opentrust-row__dirty-mark { top: 19px; } + +.opentrust-admin .opentrust-row.is-dirty .opentrust-row__dirty-mark { opacity: 1; } + +/* Toggle (CSS-only state) */ +.opentrust-admin .opentrust-toggle { + position: relative; + display: inline-block; + width: 38px; + height: 22px; + border-radius: 999px; + background: var(--ettic-border-hi); + cursor: pointer; + transition: background-color 200ms ease; + flex-shrink: 0; + vertical-align: middle; +} + +.opentrust-admin .opentrust-toggle__input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: pointer; + z-index: 1; +} + +.opentrust-admin .opentrust-toggle__thumb { + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.18), 0 0 0 0.5px rgba(0,0,0,0.04); + transition: transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1); + pointer-events: none; +} + +.opentrust-admin .opentrust-toggle:has(.opentrust-toggle__input:checked) { background: var(--ettic-blue); } +.opentrust-admin .opentrust-toggle:has(.opentrust-toggle__input:checked) .opentrust-toggle__thumb { transform: translateX(16px); } + +.opentrust-admin .opentrust-toggle:focus-within { + outline: none; + box-shadow: 0 0 0 3px rgba(15,92,250,0.25); +} + +.opentrust-admin .opentrust-toggle:has(.opentrust-toggle__input:disabled) { + opacity: 0.5; + cursor: not-allowed; +} +.opentrust-admin .opentrust-toggle:has(.opentrust-toggle__input:disabled) .opentrust-toggle__input { cursor: not-allowed; } + +/* Inputs */ +.opentrust-admin .opentrust-input { + appearance: none; + border: 1px solid var(--ettic-border); + background: #fff; + font-family: inherit; + font-size: 13.5px; + color: var(--tx-strong); + padding: 8px 11px; + border-radius: var(--r-md); + width: 100%; + transition: border-color 130ms ease, box-shadow 130ms ease; + box-shadow: none; + margin: 0; + height: auto; + min-height: 0; + line-height: 1.4; +} + +.opentrust-admin .opentrust-input:focus { + outline: none; + border-color: var(--ettic-blue); + box-shadow: 0 0 0 3px rgba(15,92,250,0.18); +} + +.opentrust-admin .opentrust-input--mono { font-family: var(--ff-mono); font-size: 13px; } +.opentrust-admin .opentrust-input--num { width: 88px; } +.opentrust-admin .opentrust-input--md { width: 280px; } + +.opentrust-admin .opentrust-input--invalid { + border-color: #B32D2E; + box-shadow: 0 0 0 3px rgba(179, 45, 46, 0.10); +} +.opentrust-admin .opentrust-input--invalid:focus { + border-color: #B32D2E; + box-shadow: 0 0 0 3px rgba(179, 45, 46, 0.20); +} + +/* Select */ +.opentrust-admin .opentrust-select { + position: relative; + display: inline-flex; + width: 280px; +} + +.opentrust-admin .opentrust-select select { + appearance: none; + width: 100%; + border: 1px solid var(--ettic-border); + background: #fff; + font-family: inherit; + font-size: 13.5px; + color: var(--tx-strong); + padding: 8px 32px 8px 11px; + border-radius: var(--r-md); + cursor: pointer; + box-shadow: none; + margin: 0; + height: auto; + min-height: 0; + line-height: 1.4; +} + +.opentrust-admin .opentrust-select select:focus { + outline: none; + border-color: var(--ettic-blue); + box-shadow: 0 0 0 3px rgba(15,92,250,0.18); +} + +.opentrust-admin .opentrust-select::after { + content: ''; + position: absolute; + right: 12px; + top: 50%; + width: 7px; + height: 7px; + border-right: 1.5px solid var(--tx-muted); + border-bottom: 1.5px solid var(--tx-muted); + transform: translateY(-70%) rotate(45deg); + pointer-events: none; +} + +/* Color picker (fused swatch + hex input) */ +.opentrust-admin .opentrust-color { + display: inline-flex; + align-items: stretch; + border: 1px solid var(--ettic-border); + border-radius: var(--r-md); + overflow: hidden; + background: #fff; + width: 200px; + transition: border-color 130ms ease, box-shadow 130ms ease; +} + +.opentrust-admin .opentrust-color input[type="color"] { + -webkit-appearance: none; + appearance: none; + width: 36px; + height: auto; + flex-shrink: 0; + border: none; + border-right: 1px solid var(--ettic-border); + background: transparent; + padding: 4px; + cursor: pointer; + margin: 0; + box-shadow: none; + min-height: 0; +} +.opentrust-admin .opentrust-color input[type="color"]::-webkit-color-swatch { border: none; border-radius: 3px; } +.opentrust-admin .opentrust-color input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; } +.opentrust-admin .opentrust-color input[type="color"]::-moz-color-swatch { border: none; border-radius: 3px; } + +.opentrust-admin .opentrust-color .opentrust-input { + border: none; + border-radius: 0; + flex: 1; + min-width: 0; +} + +.opentrust-admin .opentrust-color .opentrust-input:focus { box-shadow: none; } + +.opentrust-admin .opentrust-color:focus-within { + border-color: var(--ettic-blue); + box-shadow: 0 0 0 3px rgba(15,92,250,0.18); +} + +.opentrust-admin .opentrust-color.is-invalid { + border-color: #B32D2E; + box-shadow: 0 0 0 3px rgba(179, 45, 46, 0.12); +} +.opentrust-admin .opentrust-color.is-invalid input[type="color"] { border-right-color: rgba(179, 45, 46, 0.30); } + +/* Logo / media (preview + replace/remove) */ +.opentrust-admin .opentrust-media { + display: flex; + align-items: center; + gap: 12px; +} + +.opentrust-admin .opentrust-media__preview { + width: 84px; + height: 52px; + border: 1px dashed var(--ettic-border-hi); + border-radius: var(--r-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; + color: var(--tx-quiet); + background: var(--ettic-surface-2); + overflow: hidden; +} + +.opentrust-admin .opentrust-media__preview img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + display: block; +} + +.opentrust-admin .opentrust-media__preview--filled { + border-style: solid; + border-color: var(--ettic-border); + background: #fff; + color: var(--tx-strong); +} + +.opentrust-admin .opentrust-media__controls { display: flex; gap: 4px; align-items: center; } + +/* Segmented control (radios styled as buttons) */ +.opentrust-admin .opentrust-seg { + display: inline-flex; + background: var(--ettic-surface-2); + border: 1px solid var(--ettic-border); + border-radius: var(--r-md); + padding: 2px; + gap: 1px; +} + +.opentrust-admin .opentrust-seg__btn { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + font-family: inherit; + font-size: 13px; + font-weight: 500; + color: var(--tx-muted); + padding: 5px 12px; + border-radius: 5px; + cursor: pointer; + transition: background-color 130ms ease, color 130ms ease; + min-width: 32px; + user-select: none; +} + +.opentrust-admin .opentrust-seg__input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: pointer; +} + +.opentrust-admin .opentrust-seg__btn:hover { color: var(--tx-strong); } + +.opentrust-admin .opentrust-seg__btn:has(.opentrust-seg__input:checked) { + background: #fff; + color: var(--tx-strong); + box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 0 0 0.5px rgba(0,0,0,0.05); + cursor: default; +} + +.opentrust-admin .opentrust-seg__btn:focus-within { + outline: none; + box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 0 0 2px rgba(15,92,250,0.32); +} + +/* Chips (multi-select, role list etc.) */ +.opentrust-admin .opentrust-chips { display: flex; flex-wrap: wrap; gap: 6px; } + +.opentrust-admin .opentrust-chip { + position: relative; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 11px; + border-radius: 999px; + border: 1px solid var(--ettic-border); + background: #fff; + font-size: 12.5px; + font-weight: 500; + color: var(--tx-muted); + cursor: pointer; + transition: all 130ms ease; + user-select: none; + font-family: inherit; +} + +.opentrust-admin .opentrust-chip__input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: pointer; +} + +.opentrust-admin .opentrust-chip:hover { border-color: var(--ettic-border-hi); color: var(--tx-strong); } + +.opentrust-admin .opentrust-chip:has(.opentrust-chip__input:checked) { + background: var(--ettic-blue-soft); + border-color: rgba(15,92,250,0.30); + color: var(--ettic-blue); +} + +.opentrust-admin .opentrust-chip__check { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--ettic-blue); + color: #fff; + display: none; + align-items: center; + justify-content: center; +} + +.opentrust-admin .opentrust-chip__check svg { width: 8px; height: 8px; } +.opentrust-admin .opentrust-chip:has(.opentrust-chip__input:checked) .opentrust-chip__check { display: inline-flex; } + +.opentrust-admin .opentrust-chip:focus-within { + outline: none; + box-shadow: 0 0 0 3px rgba(15,92,250,0.22); +} + +/* Code block (mono readonly + Copy button) */ +.opentrust-admin .opentrust-code-block { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--ff-mono); + font-size: 12.5px; + background: var(--ettic-surface-2); + border: 1px solid var(--ettic-border); + border-radius: var(--r-sm); + padding: 8px 11px; + color: var(--tx-strong); + overflow-x: auto; + white-space: nowrap; + width: 100%; + max-width: 460px; +} + +.opentrust-admin .opentrust-code-block__copy { + margin-left: auto; + font-family: var(--ff-sans); + font-size: 11.5px; + font-weight: 500; + color: var(--tx-muted); + background: transparent; + border: none; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; +} + +.opentrust-admin .opentrust-code-block__copy:hover { color: var(--ettic-blue); background: rgba(15,92,250,0.08); } + +/* Action row (lighter than .opentrust-row, for utility actions) */ +.opentrust-admin .opentrust-action-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 8px; + position: relative; +} + +.opentrust-admin .opentrust-action-row + .opentrust-action-row, +.opentrust-admin .opentrust-row + .opentrust-action-row, +.opentrust-admin .opentrust-action-row + .opentrust-row { border-top: 1px solid var(--ettic-border); } + +.opentrust-admin .opentrust-action-row__main h3 { + margin: 0; + font-size: 13.5px; + font-weight: 600; + color: var(--tx-strong); + padding: 0; +} + +.opentrust-admin .opentrust-action-row__main p { + margin: 2px 0 0; + font-size: 12.5px; + color: var(--tx-muted); + max-width: 56ch; +} + +/* Foot meta */ +.opentrust-admin .opentrust-foot-meta { + margin-top: 8px; + font-size: 12.5px; + color: var(--tx-quiet); + display: flex; + align-items: center; + gap: 16px; + padding: 0 4px; +} + +.opentrust-admin .opentrust-foot-meta code { + font-family: var(--ff-mono); + background: var(--ettic-surface-2); + border: 1px solid var(--ettic-border); + border-radius: 4px; + padding: 1px 6px; + font-size: 11.5px; + color: var(--tx-muted); +} + +/* Notices (info/success/warn/error) */ +.opentrust-admin .opentrust-notice { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 13px 16px; + border-radius: var(--r-md); + border: 1px solid; + font-size: 13.5px; + line-height: 1.55; +} + +.opentrust-admin .opentrust-notice__icon { + flex-shrink: 0; + width: 18px; + height: 18px; + margin-top: 1px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.opentrust-admin .opentrust-notice__icon svg { width: 16px; height: 16px; } +.opentrust-admin .opentrust-notice__body { flex: 1; min-width: 0; } +.opentrust-admin .opentrust-notice__title { font-weight: 600; margin: 0 0 2px; font-size: 13.5px; } +.opentrust-admin .opentrust-notice__msg { margin: 0; } +.opentrust-admin .opentrust-notice__msg a { font-weight: 500; } + +.opentrust-admin .opentrust-notice__close { + flex-shrink: 0; + background: transparent; + border: none; + color: inherit; + opacity: 0.5; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + font-size: 16px; + line-height: 1; + font-family: inherit; +} +.opentrust-admin .opentrust-notice__close:hover { opacity: 1; background: rgba(0,0,0,0.05); } + +.opentrust-admin .opentrust-notice--info { background: #F1F7FF; border-color: #C9DEFF; color: #1A3F73; } +.opentrust-admin .opentrust-notice--info .opentrust-notice__title { color: #0A3A75; } +.opentrust-admin .opentrust-notice--info .opentrust-notice__icon { color: #0F5CFA; } + +.opentrust-admin .opentrust-notice--success { background: #ECF9F1; border-color: #B6E5C9; color: #1B5E36; } +.opentrust-admin .opentrust-notice--success .opentrust-notice__title { color: #0F4528; } +.opentrust-admin .opentrust-notice--success .opentrust-notice__icon { color: #1B8E45; } + +.opentrust-admin .opentrust-notice--warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn); } +.opentrust-admin .opentrust-notice--warn .opentrust-notice__title { color: #7A3300; } +.opentrust-admin .opentrust-notice--warn .opentrust-notice__icon { color: var(--warn); } + +.opentrust-admin .opentrust-notice--error { background: #FCEEED; border-color: #F4C9C7; color: #B32D2E; } +.opentrust-admin .opentrust-notice--error .opentrust-notice__title { color: #7A1B1C; } +.opentrust-admin .opentrust-notice--error .opentrust-notice__icon { color: #B32D2E; } + +/* Toasts (bottom-right) */ +body > .opentrust-toast-stack { + position: fixed; + bottom: 22px; + right: 22px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 99999; + pointer-events: none; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; +} + +.opentrust-toast-stack .opentrust-toast { + background: #0E1A24; + color: #fff; + padding: 11px 12px; + border-radius: 9px; + box-shadow: 0 12px 28px rgba(0,0,0,0.22), 0 0 0 1px rgba(255,255,255,0.06); + display: flex; + align-items: center; + gap: 10px; + font-size: 13.5px; + font-weight: 500; + min-width: 280px; + max-width: 380px; + pointer-events: auto; + animation: opentrust-toast-in 220ms cubic-bezier(0.34, 1.4, 0.64, 1); + line-height: 1.4; +} + +.opentrust-toast-stack .opentrust-toast.is-leaving { animation: opentrust-toast-out 200ms ease forwards; } + +.opentrust-toast-stack .opentrust-toast__icon { + width: 20px; + height: 20px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.opentrust-toast-stack .opentrust-toast__icon svg { width: 12px; height: 12px; } + +.opentrust-toast-stack .opentrust-toast--success .opentrust-toast__icon { background: rgba(77, 213, 131, 0.18); color: #6EE599; } +.opentrust-toast-stack .opentrust-toast--error .opentrust-toast__icon { background: rgba(244, 99, 99, 0.20); color: #FF8A8A; } +.opentrust-toast-stack .opentrust-toast--info .opentrust-toast__icon { background: rgba(15, 92, 250, 0.22); color: #6FA8FF; } + +.opentrust-toast-stack .opentrust-toast__msg { flex: 1; } + +.opentrust-toast-stack .opentrust-toast__close { + background: transparent; + border: none; + color: rgba(255,255,255,0.50); + padding: 2px 6px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + line-height: 1; + font-family: inherit; +} +.opentrust-toast-stack .opentrust-toast__close:hover { color: #fff; background: rgba(255,255,255,0.08); } + +@keyframes opentrust-toast-in { from { opacity: 0; transform: translateY(12px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } } +@keyframes opentrust-toast-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(8px); } } + +/* Modal / confirm dialog (renders to body) */ +body > .opentrust-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(3, 16, 24, 0.42); + -webkit-backdrop-filter: blur(2px); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + z-index: 100000; + padding: 20px; + animation: opentrust-backdrop-in 160ms ease; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; +} + +body > .opentrust-modal-backdrop.is-leaving { animation: opentrust-backdrop-out 140ms ease forwards; } + +.opentrust-modal-backdrop .opentrust-modal { + background: #fff; + border-radius: 12px; + width: 460px; + max-width: 100%; + box-shadow: 0 24px 48px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.04); + overflow: hidden; + animation: opentrust-modal-in 220ms cubic-bezier(0.34, 1.4, 0.64, 1); + color: #2A323B; + font-size: 14px; +} + +.opentrust-modal-backdrop.is-leaving .opentrust-modal { animation: opentrust-modal-out 140ms ease forwards; } + +.opentrust-modal-backdrop .opentrust-modal__head { + padding: 22px 24px 0; + display: flex; + align-items: flex-start; + gap: 14px; +} + +.opentrust-modal-backdrop .opentrust-modal__icon { + width: 36px; + height: 36px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.opentrust-modal-backdrop .opentrust-modal__icon svg { width: 18px; height: 18px; } +.opentrust-modal-backdrop .opentrust-modal__icon--danger { background: #FCEEED; color: #B32D2E; } +.opentrust-modal-backdrop .opentrust-modal__icon--warn { background: #FFF7EC; color: #B95000; } + +.opentrust-modal-backdrop .opentrust-modal__head-text { padding-top: 4px; } + +.opentrust-modal-backdrop .opentrust-modal__head-text h3 { + font-size: 16.5px; + font-weight: 700; + letter-spacing: -0.01em; + margin: 0 0 4px; + color: #051016; + padding: 0; +} + +.opentrust-modal-backdrop .opentrust-modal__lede { margin: 0; font-size: 13.5px; color: #5A6573; line-height: 1.5; } + +.opentrust-modal-backdrop .opentrust-modal__body { + padding: 12px 24px 22px 74px; + font-size: 13.5px; + color: #5A6573; + line-height: 1.6; +} + +.opentrust-modal-backdrop .opentrust-modal__body p { margin: 0; } +.opentrust-modal-backdrop .opentrust-modal__body p + p { margin-top: 10px; } +.opentrust-modal-backdrop .opentrust-modal__body code { + background: #F5F7FA; + border: 1px solid #E2E5E9; + padding: 1px 5px; + border-radius: 4px; + font-size: 12px; + font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace; +} + +.opentrust-modal-backdrop .opentrust-modal__foot { + padding: 14px 18px; + background: #F5F7FA; + border-top: 1px solid #E2E5E9; + display: flex; + justify-content: flex-end; + gap: 8px; +} + +/* Modal buttons — duplicated for body-level rendering (no .opentrust-admin scope) */ +.opentrust-modal-backdrop .opentrust-btn { + appearance: none; + border: 1px solid transparent; + background: transparent; + font-family: inherit; + font-size: 13px; + font-weight: 500; + padding: 7px 14px; + border-radius: 7px; + cursor: pointer; + transition: background-color 130ms ease, border-color 130ms ease, color 130ms ease; + line-height: 1.4; + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; + height: auto; +} + +.opentrust-modal-backdrop .opentrust-btn--ghost { + border-color: #E2E5E9; + color: #051016; + background: #fff; +} +.opentrust-modal-backdrop .opentrust-btn--ghost:hover { background: #F5F7FA; } + +.opentrust-modal-backdrop .opentrust-btn--primary { + background: #0F5CFA; + color: #fff; +} +.opentrust-modal-backdrop .opentrust-btn--primary:hover { background: #0A4ED4; } + +.opentrust-modal-backdrop .opentrust-btn--danger { + background: #B32D2E; + color: #fff; +} +.opentrust-modal-backdrop .opentrust-btn--danger:hover { background: #931E1F; } + +.opentrust-modal-backdrop .opentrust-btn--loading { pointer-events: none; cursor: wait; } +.opentrust-modal-backdrop .opentrust-btn__spinner { + width: 12px; + height: 12px; + border: 1.5px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: opentrust-spin 600ms linear infinite; + display: inline-block; + flex-shrink: 0; + margin-right: 2px; +} + +@keyframes opentrust-backdrop-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes opentrust-backdrop-out { from { opacity: 1; } to { opacity: 0; } } +@keyframes opentrust-modal-in { from { opacity: 0; transform: scale(0.95) translateY(6px); } to { opacity: 1; transform: scale(1) translateY(0); } } +@keyframes opentrust-modal-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.97); } } + +/* Field validation feedback */ +.opentrust-admin .opentrust-field-msg { + font-size: 12.5px; + display: inline-flex; + align-items: center; + gap: 5px; + font-weight: 500; + line-height: 1.4; + margin: 0; +} +.opentrust-admin .opentrust-field-msg--error { color: #B32D2E; } +.opentrust-admin .opentrust-field-msg svg { width: 12px; height: 12px; flex-shrink: 0; } +.opentrust-admin .opentrust-field-msg code { + font-family: var(--ff-mono); + background: #FCEEED; + padding: 1px 5px; + border-radius: 3px; + font-size: 11.5px; + border: 1px solid rgba(179, 45, 46, 0.20); + color: #B32D2E; + font-weight: 500; +} + +.opentrust-admin .opentrust-field-counter { + font-size: 11.5px; + color: var(--tx-quiet); + font-family: var(--ff-mono); + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; +} +.opentrust-admin .opentrust-field-counter--warn { color: var(--warn); } +.opentrust-admin .opentrust-field-counter--error { color: #B32D2E; font-weight: 600; } + +/* Tooltip / disabled-with-reason info pill */ +.opentrust-admin .opentrust-tooltip-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + border: 1px solid var(--ettic-border-hi); + background: #fff; + color: var(--tx-muted); + font-size: 10px; + font-weight: 700; + cursor: help; + margin-left: 8px; + position: relative; + flex-shrink: 0; + font-family: inherit; + padding: 0; + vertical-align: middle; + line-height: 1; +} + +.opentrust-admin .opentrust-tooltip-trigger:hover, +.opentrust-admin .opentrust-tooltip-trigger:focus { + border-color: var(--warn); + color: var(--warn); + outline: none; +} + +.opentrust-admin .opentrust-tooltip-trigger:focus-visible { + box-shadow: 0 0 0 3px rgba(244, 165, 71, 0.22); +} + +.opentrust-admin .opentrust-tooltip { + position: absolute; + bottom: calc(100% + 10px); + right: -10px; + background: #0E1A24; + color: rgba(255, 255, 255, 0.92); + padding: 11px 13px; + border-radius: 8px; + width: 252px; + font-size: 12.5px; + font-weight: 400; + line-height: 1.5; + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(255, 255, 255, 0.06); + opacity: 0; + visibility: hidden; + transform: translateY(4px); + transition: opacity 160ms ease, transform 160ms ease, visibility 160ms; + z-index: 50; + text-align: left; + letter-spacing: 0; + pointer-events: none; +} + +.opentrust-admin .opentrust-tooltip::after { + content: ''; + position: absolute; + top: 100%; + right: 14px; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #0E1A24; +} + +.opentrust-admin .opentrust-tooltip strong { + color: #fff; + font-weight: 600; + display: block; + margin-bottom: 3px; + font-size: 13px; +} + +.opentrust-admin .opentrust-tooltip a { color: #6FA8FF; font-weight: 500; pointer-events: auto; } + +.opentrust-admin .opentrust-tooltip-trigger:hover .opentrust-tooltip, +.opentrust-admin .opentrust-tooltip-trigger:focus .opentrust-tooltip, +.opentrust-admin .opentrust-tooltip-trigger:focus-within .opentrust-tooltip { + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; +} + +/* Footer card — rendered as last child of .opentrust-admin so tokens resolve. + * Width matches .opentrust-stack so it sits in the same column as the cards. */ +.opentrust-admin .opentrust-footer { + width: 92%; + max-width: 1600px; + margin: 0 auto 32px; + background: transparent; + border: 0; + padding: 0; + box-sizing: border-box; + display: block; +} + +/* __inner gets its own padding for content breathing room. */ +.opentrust-admin .opentrust-footer__inner { + background: var(--ettic-surface); + border: 1px solid var(--ettic-border); + border-radius: var(--r-lg); + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.opentrust-admin .opentrust-footer__top { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.opentrust-admin .opentrust-footer__lead { + color: var(--tx-muted); + font-size: 13px; + line-height: 1.5; +} + +.opentrust-admin .opentrust-footer__brand { + color: var(--tx-strong); + font-weight: 600; + margin-right: 6px; +} + +.opentrust-admin .opentrust-footer__version { + font-size: 11.5px; + color: var(--tx-quiet); + font-family: var(--ff-mono); + white-space: nowrap; + letter-spacing: 0.02em; +} + +.opentrust-admin .opentrust-footer__version-num { + color: var(--tx-strong); + font-weight: 500; +} + +.opentrust-admin .opentrust-footer__nav { + display: flex; + flex-wrap: wrap; + gap: 0 2px; + align-items: center; + font-size: 13px; +} + +.opentrust-admin .opentrust-footer__nav a { + color: var(--tx-muted); + text-decoration: none; + padding: 4px 8px; + border-radius: 5px; + transition: background-color 130ms ease, color 130ms ease; +} + +.opentrust-admin .opentrust-footer__nav a:hover, +.opentrust-admin .opentrust-footer__nav a:focus { + color: var(--ettic-blue); + background: var(--ettic-blue-soft); + outline: none; +} + +.opentrust-admin .opentrust-footer__sep { + color: var(--ettic-border-hi); + user-select: none; +} diff --git a/assets/js/opentrust-admin.js b/assets/js/opentrust-admin.js new file mode 100644 index 0000000..4c80055 --- /dev/null +++ b/assets/js/opentrust-admin.js @@ -0,0 +1,655 @@ +/** + * Ettic admin design system — JS template. + * + * BEFORE USING: replace `opentrust` everywhere with your plugin's slug. + * Vanilla JS, no jQuery. Provides: media pickers, color sync, dirty + * tracking, char counters, hex validation, form submission, notices, + * toasts, confirm modals, loading state. Public surfaces (`showToast`, + * `showConfirm`, `setLoading`) are local to this IIFE — expose them on + * a namespace if you need cross-script access. + */ + +(function () { + 'use strict'; + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); + } else { + init(); + } + + function init() { + initMediaPickers(); + initColorPickers(); + initRowDirtyMarks(); + initDirtyTracking(); + initCharCounters(); + initHexValidation(); + initFormSubmission(); + initNoticeDismissal(); + initWpNoticeScoop(); + initToastFromUrl(); + initDiscardFlash(); + } + + // Media pickers (logo + agency favicon, via wp.media) + function initMediaPickers() { + var pickers = document.querySelectorAll( '[data-opentrust-media-picker]' ); + Array.prototype.forEach.call( pickers, bindMediaPicker ); + } + + function bindMediaPicker( root ) { + var idInput = root.querySelector( '[data-opentrust-media-id]' ); + var preview = root.querySelector( '[data-opentrust-media-preview]' ); + var pickBtn = root.querySelector( '[data-opentrust-media-pick]' ); + var clearBtn = root.querySelector( '[data-opentrust-media-clear]' ); + if ( ! idInput || ! pickBtn ) { + return; + } + + var frame; + pickBtn.addEventListener( 'click', function ( ev ) { + ev.preventDefault(); + if ( ! frame ) { + frame = wp.media( { + title: pickBtn.textContent || 'Choose image', + button: { text: 'Use this image' }, + library: { type: [ 'image/png', 'image/jpeg', 'image/webp', 'image/svg+xml' ] }, + multiple: false + } ); + frame.on( 'select', function () { + var attachment = frame.state().get( 'selection' ).first().toJSON(); + idInput.value = attachment.id; + if ( preview ) { + preview.innerHTML = ''; + var img = document.createElement( 'img' ); + img.src = attachment.url; + img.alt = ''; + preview.appendChild( img ); + preview.classList.add( 'opentrust-media__preview--filled' ); + } + if ( clearBtn ) { + clearBtn.style.display = ''; + } + idInput.dispatchEvent( new Event( 'change', { bubbles: true } ) ); + } ); + } + frame.open(); + } ); + + if ( clearBtn ) { + clearBtn.addEventListener( 'click', function ( ev ) { + ev.preventDefault(); + idInput.value = '0'; + if ( preview ) { + preview.innerHTML = ''; + preview.classList.remove( 'opentrust-media__preview--filled' ); + } + clearBtn.style.display = 'none'; + idInput.dispatchEvent( new Event( 'change', { bubbles: true } ) ); + } ); + } + } + + // Color picker — native swatch synced with hex text input + function initColorPickers() { + document.querySelectorAll( '.opentrust-admin .opentrust-color' ).forEach( function ( color ) { + var swatch = color.querySelector( 'input[type="color"]' ); + var text = color.querySelector( 'input[type="text"]' ); + if ( ! swatch || ! text ) { + return; + } + + swatch.addEventListener( 'input', function () { + text.value = swatch.value.toUpperCase(); + text.dispatchEvent( new Event( 'input', { bubbles: true } ) ); + } ); + + text.addEventListener( 'input', function () { + var v = text.value.trim(); + if ( /^#?[0-9a-fA-F]{6}$/.test( v ) ) { + swatch.value = v.charAt( 0 ) === '#' ? v : '#' + v; + } + } ); + } ); + } + + // Per-row dirty mark — auto-injected, opacity toggled via .is-dirty + function initRowDirtyMarks() { + document.querySelectorAll( '.opentrust-admin .opentrust-row' ).forEach( function ( row ) { + var control = row.querySelector( '.opentrust-row__control' ); + if ( ! control ) { + return; + } + var mark = document.createElement( 'span' ); + mark.className = 'opentrust-row__dirty-mark'; + mark.setAttribute( 'aria-hidden', 'true' ); + control.appendChild( mark ); + } ); + } + + // Dirty tracking — per-field signature compared against page-load snapshot. + // A "field" = inputs sharing a `name` (text, radio group, checkbox group, hidden+checkbox toggle). + var dirtySet, initialSig; + var dirtyEl, numEl, labelEl, saveBtn, discardBtn, form; + var IGNORE_NAMES = { option_page: 1, action: 1, _wpnonce: 1, _wp_http_referer: 1, submit: 1 }; + + function initDirtyTracking() { + dirtyEl = document.querySelector( '[data-dirty]' ); + numEl = document.querySelector( '[data-dirty-num]' ); + labelEl = document.querySelector( '[data-dirty-label]' ); + saveBtn = document.querySelector( '[data-save]' ); + discardBtn = document.querySelector( '[data-discard]' ); + form = document.querySelector( '.opentrust-admin form' ); + if ( ! dirtyEl || ! saveBtn || ! form ) { + return; + } + + dirtySet = {}; + initialSig = {}; + captureInitialState(); + renderDirty(); + + form.addEventListener( 'input', onInteract ); + form.addEventListener( 'change', onInteract ); + } + + function isTrackable( name ) { + return !! name && ! IGNORE_NAMES[ name ]; + } + + function cssEscape( s ) { + if ( window.CSS && CSS.escape ) { + return CSS.escape( s ); + } + return String( s ).replace( /(["\\])/g, '\\$1' ); + } + + function fieldSignature( name ) { + var els = form.querySelectorAll( '[name="' + cssEscape( name ) + '"]' ); + if ( ! els.length ) { + return ''; + } + var hasCheckable = false; + for ( var i = 0; i < els.length; i++ ) { + if ( els[ i ].type === 'checkbox' || els[ i ].type === 'radio' ) { + hasCheckable = true; + break; + } + } + if ( hasCheckable ) { + var vals = []; + for ( var j = 0; j < els.length; j++ ) { + var el = els[ j ]; + if ( ( el.type === 'checkbox' || el.type === 'radio' ) && el.checked ) { + vals.push( el.value ); + } + } + vals.sort(); + return vals.join( '|' ); + } + // Last wins, matching PHP $_POST behavior for duplicate names. + return els[ els.length - 1 ].value; + } + + function captureInitialState() { + var seen = {}; + var els = form.querySelectorAll( '[name]' ); + for ( var i = 0; i < els.length; i++ ) { + var name = els[ i ].name; + if ( seen[ name ] || ! isTrackable( name ) ) { + continue; + } + seen[ name ] = 1; + initialSig[ name ] = fieldSignature( name ); + } + } + + function updateFieldDirty( name ) { + if ( ! isTrackable( name ) || ! ( name in initialSig ) ) { + return; + } + var current = fieldSignature( name ); + if ( current === initialSig[ name ] ) { + delete dirtySet[ name ]; + } else { + dirtySet[ name ] = 1; + } + var el = form.querySelector( '[name="' + cssEscape( name ) + '"]' ); + if ( el ) { + var row = el.closest( '.opentrust-row' ); + if ( row ) { + row.classList.toggle( 'is-dirty', !! dirtySet[ name ] ); + } + } + renderDirty(); + } + + function onInteract( ev ) { + var t = ev.target; + if ( ! t.matches || ! t.matches( 'input, select, textarea' ) ) { + return; + } + if ( ! t.name ) { + return; + } + updateFieldDirty( t.name ); + } + + function dirtyCount() { + return Object.keys( dirtySet ).length; + } + + function renderDirty() { + if ( ! dirtyEl ) { + return; + } + var count = dirtyCount(); + if ( count === 0 ) { + dirtyEl.classList.add( 'is-clean' ); + if ( labelEl ) { + labelEl.textContent = ''; + } + if ( saveBtn ) { + saveBtn.setAttribute( 'disabled', '' ); + } + if ( discardBtn ) { + discardBtn.setAttribute( 'disabled', '' ); + } + } else { + dirtyEl.classList.remove( 'is-clean' ); + if ( numEl ) { + numEl.textContent = count; + } + if ( labelEl ) { + labelEl.textContent = count === 1 ? ' unsaved change' : ' unsaved changes'; + } + if ( saveBtn ) { + saveBtn.removeAttribute( 'disabled' ); + } + if ( discardBtn ) { + discardBtn.removeAttribute( 'disabled' ); + } + } + } + + // Char counters under capped text inputs + function initCharCounters() { + document.querySelectorAll( '[data-counter]' ).forEach( function ( el ) { + var max = parseInt( el.getAttribute( 'maxlength' ), 10 ); + if ( ! max ) { + return; + } + var counter = document.createElement( 'div' ); + counter.className = 'opentrust-field-counter'; + el.parentNode.appendChild( counter ); + + function update() { + var len = el.value.length; + counter.textContent = len + ' / ' + max; + counter.classList.toggle( 'opentrust-field-counter--warn', len >= max - 5 && len < max ); + counter.classList.toggle( 'opentrust-field-counter--error', len >= max ); + } + + el.addEventListener( 'input', update ); + update(); + } ); + } + + // Live hex validation + function initHexValidation() { + document.querySelectorAll( '[data-validate-hex]' ).forEach( function ( el ) { + var colorEl = el.closest( '.opentrust-color' ); + if ( ! colorEl ) { + return; + } + var msgEl = null; + + function validate() { + var v = el.value.trim(); + var valid = /^#?[0-9a-fA-F]{6}$/.test( v ); + if ( v && ! valid ) { + colorEl.classList.add( 'is-invalid' ); + el.classList.add( 'opentrust-input--invalid' ); + if ( ! msgEl ) { + msgEl = document.createElement( 'div' ); + msgEl.className = 'opentrust-field-msg opentrust-field-msg--error'; + msgEl.innerHTML = 'Use a 6-character hex like #0F5CFA'; + colorEl.parentNode.appendChild( msgEl ); + } + } else { + colorEl.classList.remove( 'is-invalid' ); + el.classList.remove( 'opentrust-input--invalid' ); + if ( msgEl ) { + msgEl.remove(); + msgEl = null; + } + } + } + + el.addEventListener( 'input', validate ); + } ); + } + + // Form submission — Save = native submit, Discard = reload with sessionStorage flag + function initFormSubmission() { + if ( ! form ) { + return; + } + + form.addEventListener( 'submit', function () { + // Loading state before browser navigates. + setLoading( saveBtn, true, 'Saving…' ); + setLoading( discardBtn, true, 'Saving…' ); + } ); + + if ( discardBtn ) { + discardBtn.addEventListener( 'click', function ( e ) { + if ( discardBtn.hasAttribute( 'disabled' ) ) { + return; + } + e.preventDefault(); + try { + sessionStorage.setItem( 'opentrust_discarded', '1' ); + } catch ( err ) { /* private mode / quota */ } + window.location.reload(); + } ); + } + } + + // Notice dismissal + function initNoticeDismissal() { + document.querySelectorAll( '.opentrust-admin .opentrust-notice__close' ).forEach( function ( btn ) { + btn.addEventListener( 'click', function () { + var notice = btn.closest( '.opentrust-notice' ); + if ( ! notice ) { + return; + } + notice.style.transition = 'opacity 160ms ease, transform 160ms ease'; + notice.style.opacity = '0'; + notice.style.transform = 'translateY(-4px)'; + setTimeout( function () { notice.remove(); }, 180 ); + } ); + } ); + } + + // Toast system — floating stack on body + var toastStack; + + function getToastStack() { + if ( ! toastStack ) { + toastStack = document.querySelector( '.opentrust-toast-stack' ); + if ( ! toastStack ) { + toastStack = document.createElement( 'div' ); + toastStack.className = 'opentrust-toast-stack'; + toastStack.setAttribute( 'aria-live', 'polite' ); + document.body.appendChild( toastStack ); + } + } + return toastStack; + } + + function toastIcon( type ) { + if ( type === 'success' ) { + return ''; + } + if ( type === 'error' ) { + return ''; + } + return ''; + } + + function showToast( opts ) { + var type = opts.type || 'success'; + var message = opts.message || ''; + var duration = opts.duration || 3500; + var stack = getToastStack(); + + var toast = document.createElement( 'div' ); + toast.className = 'opentrust-toast opentrust-toast--' + type; + toast.innerHTML = + '' + toastIcon( type ) + '' + + '' + + ''; + toast.querySelector( '.opentrust-toast__msg' ).textContent = message; + stack.appendChild( toast ); + + var timer; + var dismiss = function () { + clearTimeout( timer ); + if ( toast.classList.contains( 'is-leaving' ) ) { + return; + } + toast.classList.add( 'is-leaving' ); + setTimeout( function () { toast.remove(); }, 200 ); + }; + toast.querySelector( '.opentrust-toast__close' ).addEventListener( 'click', dismiss ); + timer = setTimeout( dismiss, duration ); + } + + function initToastFromUrl() { + var params = new URLSearchParams( window.location.search ); + var changed = false; + + if ( params.get( 'settings-updated' ) === 'true' ) { + showToast( { type: 'success', message: 'Settings saved.', duration: 7000 } ); + params.delete( 'settings-updated' ); + changed = true; + } + + // Extend here: add `params.get( 'yourflag' )` branches that map admin + // redirects (e.g. ?yourflag=sent after a wp_safe_redirect from a form + // handler) to toasts, then `params.delete()` to keep the URL clean. + + if ( changed ) { + var newSearch = params.toString(); + var newUrl = window.location.pathname + ( newSearch ? '?' + newSearch : '' ); + window.history.replaceState( {}, '', newUrl ); + } + } + + // Discard flash — sessionStorage flag set pre-reload, consumed here. + function initDiscardFlash() { + var flag; + try { + flag = sessionStorage.getItem( 'opentrust_discarded' ); + if ( flag ) { + sessionStorage.removeItem( 'opentrust_discarded' ); + } + } catch ( err ) { return; } + if ( flag === '1' ) { + showToast( { type: 'info', message: 'Changes discarded.', duration: 6000 } ); + } + } + + // WP notice scooper — options-head.php auto-calls settings_errors(), rendering a duplicate .notice + // above our wrap. Reroute into toasts. Tight scope (only id^="setting-error-") so unrelated admin + // notices stay where users expect them. + function initWpNoticeScoop() { + var wrap = document.querySelector( '.wrap.opentrust-admin' ); + if ( ! wrap || ! wrap.parentNode ) { + return; + } + var notices = wrap.parentNode.querySelectorAll( 'div[id^="setting-error-"]' ); + Array.prototype.forEach.call( notices, function ( notice ) { + var msgEl = notice.querySelector( 'p' ) || notice; + var msg = ( msgEl.textContent || '' ).trim(); + if ( ! msg ) { + notice.remove(); + return; + } + var type = 'info'; + if ( notice.classList.contains( 'notice-success' ) || notice.classList.contains( 'updated' ) ) { + type = 'success'; + } else if ( notice.classList.contains( 'notice-error' ) || notice.classList.contains( 'error' ) ) { + type = 'error'; + } else if ( notice.classList.contains( 'notice-warning' ) ) { + type = 'info'; + } + showToast( { type: type, message: msg, duration: type === 'error' ? 12000 : 8000 } ); + notice.remove(); + } ); + } + + // Confirm modal + function showConfirm( opts ) { + var title = opts.title; + var lede = opts.lede || ''; + var body = opts.body || ''; + var confirmText = opts.confirmText || 'Confirm'; + var cancelText = opts.cancelText || 'Cancel'; + var danger = ! ! opts.danger; + var onConfirm = opts.onConfirm; + + var backdrop = document.createElement( 'div' ); + backdrop.className = 'opentrust-modal-backdrop'; + backdrop.innerHTML = + ''; + backdrop.querySelector( '[data-modal-title]' ).textContent = title; + if ( lede ) { + backdrop.querySelector( '[data-modal-lede]' ).textContent = lede; + } + backdrop.querySelector( '[data-modal-body]' ).innerHTML = body; + backdrop.querySelector( '[data-cancel]' ).textContent = cancelText; + backdrop.querySelector( '[data-confirm]' ).textContent = confirmText; + + document.body.appendChild( backdrop ); + + var confirmBtn = backdrop.querySelector( '[data-confirm]' ); + var cancelBtn = backdrop.querySelector( '[data-cancel]' ); + + function close() { + if ( backdrop.classList.contains( 'is-leaving' ) ) { + return; + } + backdrop.classList.add( 'is-leaving' ); + setTimeout( function () { backdrop.remove(); }, 140 ); + } + + cancelBtn.addEventListener( 'click', function () { + if ( confirmBtn.classList.contains( 'opentrust-btn--loading' ) ) { + return; + } + close(); + } ); + + confirmBtn.addEventListener( 'click', function () { + if ( confirmBtn.classList.contains( 'opentrust-btn--loading' ) ) { + return; + } + setLoading( confirmBtn, true, 'Working…' ); + cancelBtn.setAttribute( 'disabled', '' ); + Promise.resolve( onConfirm && onConfirm() ).then( function () { + setLoading( confirmBtn, false ); + cancelBtn.removeAttribute( 'disabled' ); + close(); + } ).catch( function () { + setLoading( confirmBtn, false ); + cancelBtn.removeAttribute( 'disabled' ); + close(); + } ); + } ); + + backdrop.addEventListener( 'click', function ( e ) { + if ( e.target === backdrop && ! confirmBtn.classList.contains( 'opentrust-btn--loading' ) ) { + close(); + } + } ); + + function escHandler( e ) { + if ( e.key === 'Escape' && ! confirmBtn.classList.contains( 'opentrust-btn--loading' ) ) { + close(); + document.removeEventListener( 'keydown', escHandler ); + } + } + document.addEventListener( 'keydown', escHandler ); + + setTimeout( function () { confirmBtn.focus(); }, 50 ); + } + + // Loading state helper + function setLoading( btn, on, label ) { + if ( ! btn ) { + return; + } + if ( on ) { + if ( ! btn.dataset.origHtml ) { + btn.dataset.origHtml = btn.innerHTML; + } + btn.classList.add( 'opentrust-btn--loading' ); + btn.setAttribute( 'disabled', '' ); + var txt = label || btn.dataset.origHtml; + btn.innerHTML = '' + txt + ''; + } else { + btn.classList.remove( 'opentrust-btn--loading' ); + btn.removeAttribute( 'disabled' ); + if ( btn.dataset.origHtml ) { + btn.innerHTML = btn.dataset.origHtml; + delete btn.dataset.origHtml; + } + } + } + + // --------------------------------------------------------------- + // Pattern: AJAX action with confirm + toast feedback. + // Copy and adapt this when wiring an action-row button to a wp_ajax + // endpoint. The confirm modal is optional — omit it for non-destructive + // actions. Activate by calling it from init() once you've added markup + // like: + // + // function initMyAction() { + // var root = document.querySelector( '[data-opentrust-action-root]' ); + // if ( ! root ) return; + // var ajaxurl = root.dataset.ajaxurl || window.ajaxurl; + // var nonce = root.dataset.nonce; + // root.addEventListener( 'click', function ( ev ) { + // var btn = ev.target.closest( '[data-opentrust-action]' ); + // if ( ! btn ) return; + // ev.preventDefault(); + // showConfirm( { + // title: 'Are you sure?', + // confirmText: 'Yes, do it', + // danger: true, + // onConfirm: function () { + // return fetch( ajaxurl, { + // method: 'POST', credentials: 'same-origin', + // headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + // body: new URLSearchParams( { + // action: 'opentrust_' + btn.dataset.opentrustAction, + // _ajax_nonce: nonce + // } ) + // } ) + // .then( function ( r ) { return r.json(); } ) + // .then( function ( p ) { + // if ( ! p || ! p.success ) { + // showToast( { type: 'error', message: ( p && p.data && p.data.message ) || 'Request failed.' } ); + // return; + // } + // showToast( { type: 'success', message: p.data.message || 'Done.' } ); + // } ) + // .catch( function () { + // showToast( { type: 'error', message: 'Network error.' } ); + // } ); + // } + // } ); + // } ); + // } + // --------------------------------------------------------------- +})(); diff --git a/includes/Admin/Footer.php b/includes/Admin/Footer.php new file mode 100644 index 0000000..27c7dc0 --- /dev/null +++ b/includes/Admin/Footer.php @@ -0,0 +1,56 @@ + + + id ) { + return; + } + remove_all_actions( 'admin_notices' ); + remove_all_actions( 'all_admin_notices' ); + remove_all_actions( 'user_admin_notices' ); + remove_all_actions( 'network_admin_notices' ); + } + + /** + * Prepend "Settings" link on the plugin row. + * + * @param array $links + * @return array + */ + public static function plugin_action_links( array $links ): array { + $settings_link = sprintf( + '%s', + esc_url( admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ), + esc_html__( 'Settings', 'opentrust' ) + ); + array_unshift( $links, $settings_link ); + return $links; + } + + public static function register(): void { + register_setting( + self::OPTION_GROUP, + self::OPTION_NAME, + [ + 'type' => 'array', + 'show_in_rest' => false, + 'sanitize_callback' => [ self::class, 'sanitize' ], + 'default' => self::defaults(), + ] + ); + } + + public static function menu(): void { + add_options_page( + __( 'OpenTrust', 'opentrust' ), + __( 'OpenTrust', 'opentrust' ), + 'manage_options', + self::PAGE_SLUG, + [ self::class, 'render_page' ] + ); + } + + public static function enqueue( string $hook ): void { + if ( 'settings_page_' . self::PAGE_SLUG !== $hook ) { + return; + } + + wp_enqueue_media(); // required for the media picker control + wp_enqueue_style( 'opentrust-admin', OPENTRUST_URL . 'assets/css/opentrust-admin.css', [], OPENTRUST_VERSION ); + wp_enqueue_script( 'opentrust-admin', OPENTRUST_URL . 'assets/js/opentrust-admin.js', [], OPENTRUST_VERSION, true ); + } + + /** + * Default settings shape. Mirror this in your sanitize callback — anything + * you don't merge from $input falls through to the previous stored value. + * + * @return array + */ + public static function defaults(): array { + return [ + 'enabled' => true, + 'mode' => 'auto', + 'frequency' => 5, + 'display_name' => '', + 'brand_color' => '#0F5CFA', + 'logo_id' => 0, + ]; + } + + /** + * Read a single setting with a fallback. Avoids leaking the option key all + * over the field renderers. Replace with your project's existing helper if + * you already have one. + * + * @param mixed $default + * @return mixed + */ + private static function get( string $key, $default = null ) { + $settings = get_option( self::OPTION_NAME, self::defaults() ); + if ( ! is_array( $settings ) ) { + return $default; + } + return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default; + } + + public static function render_page(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + ?> +
+
+ + + + +
+

+

+
+ +
+ +
+
+ +
+ +
+
+

+

+
+
+ + + + + + + + +
+
+ +
+
+ +

+
+
+ + + +
+
+ +
+
+ +

+
+
+
+ + + +
+
+
+ +
+
+ +

+
+
+ + +
+
+ +
+
+ +

+
+
+ +
+
+ __( 'Auto', 'opentrust' ), + 'manual' => __( 'Manual', 'opentrust' ), + 'off' => __( 'Off', 'opentrust' ), + ]; + ?> +
+
+ +

wrapped in .opentrust-select for the chevron and focus ring.', 'opentrust' ); ?>

+
+
+
+ +
+
+
+ +
+
+ +

+
+
+
+ + +
+
+
+ +
+
+ +

+
+
+
+ +
+ + + +
+
+ + +
+
+
+
+ +
+
+

+

+
+ +
+ + */ + public static function sanitize( $input ): array { + $current = get_option( self::OPTION_NAME, self::defaults() ); + if ( ! is_array( $current ) ) { + $current = self::defaults(); + } + if ( ! is_array( $input ) ) { + return $current; + } + + $out = $current; + + // Toggle: hidden 0 + checkbox 1 means $input['enabled'] is always set. + $out['enabled'] = ! empty( $input['enabled'] ); + + if ( isset( $input['mode'] ) ) { + $allowed = [ 'auto', 'manual', 'off' ]; + $candidate = (string) $input['mode']; + $out['mode'] = in_array( $candidate, $allowed, true ) ? $candidate : 'auto'; + } + + if ( isset( $input['frequency'] ) ) { + $out['frequency'] = max( 1, min( 30, absint( $input['frequency'] ) ) ); + } + + if ( isset( $input['display_name'] ) ) { + $clean = sanitize_text_field( (string) $input['display_name'] ); + $out['display_name'] = function_exists( 'mb_substr' ) ? mb_substr( $clean, 0, 60 ) : substr( $clean, 0, 60 ); + } + + if ( isset( $input['brand_color'] ) ) { + $sanitized = sanitize_hex_color( (string) $input['brand_color'] ); + $out['brand_color'] = $sanitized ? $sanitized : '#0F5CFA'; + } + + if ( isset( $input['logo_id'] ) ) { + $id = absint( $input['logo_id'] ); + $out['logo_id'] = ( $id > 0 && wp_attachment_is_image( $id ) ) ? $id : 0; + } + + return $out; + } +} diff --git a/opentrust.php b/opentrust.php index 363b693..c7ce623 100644 --- a/opentrust.php +++ b/opentrust.php @@ -24,6 +24,14 @@ define('OPENTRUST_PLUGIN_FILE', __FILE__); define('OPENTRUST_DB_VERSION', 2); +// Ettic shared-template compatibility shims. The shared admin design system +// at /Users/.../Ettic Admin UI uses PLUGINSLUG_FILE / PLUGINSLUG_URL as its +// constant placeholder, which renames to OPENTRUST_FILE / OPENTRUST_URL. +// Aliased to the existing OPENTRUST_PLUGIN_* constants so the ported files +// run unmodified. +define('OPENTRUST_FILE', OPENTRUST_PLUGIN_FILE); +define('OPENTRUST_URL', OPENTRUST_PLUGIN_URL); + require_once OPENTRUST_PLUGIN_DIR . 'includes/class-opentrust.php'; require_once OPENTRUST_PLUGIN_DIR . 'includes/class-opentrust-admin.php'; require_once OPENTRUST_PLUGIN_DIR . 'includes/class-opentrust-admin-settings.php'; @@ -38,6 +46,12 @@ require_once OPENTRUST_PLUGIN_DIR . 'includes/class-opentrust-io.php'; require_once OPENTRUST_PLUGIN_DIR . 'includes/class-opentrust-admin-tools.php'; +// Ettic shared admin design system. Footer.php renders the branded footer +// used across every OpenTrust admin screen. Settings.php is the reference +// implementation of the design system's page shell — not booted by default; +// the migrated admin classes consume its markup vocabulary directly. +require_once OPENTRUST_PLUGIN_DIR . 'includes/Admin/Footer.php'; + // Chat (OTC) — policy chat feature. require_once OPENTRUST_PLUGIN_DIR . 'includes/class-opentrust-chat-secrets.php'; require_once OPENTRUST_PLUGIN_DIR . 'includes/providers/class-opentrust-chat-provider.php'; From 7f588b3f64a4f6ae2af90b70c9045604caee11a0 Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Mon, 11 May 2026 13:39:40 +0200 Subject: [PATCH 02/11] apply design system chrome to settings page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps OpenTrust_Admin_Settings::render_settings_page() in .opentrust-admin and emits the shared template's dark topbar + footer: - Topbar bar: brand mark (OpenTrust shield, 26x26 white-on-blue), version pill, dirty-changes counter (data-dirty), Discard + Save buttons. Save carries form="opentrust-settings-form" so it submits the active tab's Settings API form from outside it. Discard reloads. - Topbar head: page title + one-paragraph description; "View Trust Center" link moves into the topbar as a ghost-dark button. - Tabbar (NEW component, OpenTrust-only): the 4 tabs render as a light strip BELOW the topbar instead of inside it. The shared template's "no tabs in topbar" invariant assumes plugins use WP submenus for section nav; OpenTrust's ?tab=... URLs are user-bookmarkable so we keep them and style them as a separate component. Marked clearly in the CSS as not-for-upstream-extraction. - Footer: \OpenTrust\Admin\Footer::render() at the bottom of every tab. - AI tab "Live" pill: re-themed as .opentrust-tabbar__badge (replaces the inline-styled .ot-pill--live). General + Contact forms now have id="opentrust-settings-form" so the topbar Save submits them. The per-tab submit_button() is dropped. On the AI + IO tabs the Save/Discard buttons aren't rendered (they have bespoke admin-post forms that wire up in their respective migration commits). class-opentrust-admin.php enqueues opentrust-admin.css/js as a separate handle (opentrust-admin-ds) on plugin pages only, never on CPT meta-box screens. Existing admin.css/admin.js stay loaded for the CPT screens. Field markup inside the forms is untouched — that migrates per-tab in commits 3-7. Mid-migration the WP-native form-table rows will look a bit out of place inside the design-system wrap; that's expected. --- assets/css/opentrust-admin.css | 75 ++++++++++ includes/class-opentrust-admin-settings.php | 144 ++++++++++++-------- includes/class-opentrust-admin.php | 23 ++++ 3 files changed, 187 insertions(+), 55 deletions(-) diff --git a/assets/css/opentrust-admin.css b/assets/css/opentrust-admin.css index 715e1e9..154bd1d 100644 --- a/assets/css/opentrust-admin.css +++ b/assets/css/opentrust-admin.css @@ -1363,3 +1363,78 @@ body > .opentrust-modal-backdrop.is-leaving { animation: opentrust-backdrop-out color: var(--ettic-border-hi); user-select: none; } + +/* ----------------------------------------------------------------------- * + * OpenTrust-only extension: tabbar strip below the topbar. * + * * + * The shared Ettic template forbids horizontal tabs INSIDE the dark * + * topbar — OpenTrust's 4-tab settings page predates that invariant and * + * users have bookmarked the ?tab=... URLs. This component renders the * + * tabs as a separate, lighter strip BELOW the topbar so the design * + * system's chrome stays clean while the existing URLs keep working. * + * * + * Not part of the shared design system. Do not extract upstream without * + * first resolving whether multi-page plugins should always use WP * + * submenus instead of tabs. * + * ----------------------------------------------------------------------- */ + +.opentrust-admin .opentrust-tabbar { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: stretch; + margin: -8px 0 18px; + padding: 0 4px; + border-bottom: 1px solid var(--ettic-border); +} + +.opentrust-admin .opentrust-tabbar__tab { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 14px 11px; + margin-bottom: -1px; + color: var(--tx-muted); + font-size: 13.5px; + font-weight: 500; + line-height: 1.2; + text-decoration: none; + border: 1px solid transparent; + border-bottom: 2px solid transparent; + border-radius: var(--r-sm) var(--r-sm) 0 0; + transition: color 120ms ease, border-color 120ms ease, background 120ms ease; +} + +.opentrust-admin .opentrust-tabbar__tab:hover, +.opentrust-admin .opentrust-tabbar__tab:focus-visible { + color: var(--tx-strong); + background: var(--ettic-surface-2); + text-decoration: none; + outline: none; +} + +.opentrust-admin .opentrust-tabbar__tab.is-active { + color: var(--ettic-blue); + border-bottom-color: var(--ettic-blue); + background: transparent; +} + +.opentrust-admin .opentrust-tabbar__tab.is-active:hover { + background: transparent; +} + +.opentrust-admin .opentrust-tabbar__badge { + display: inline-flex; + align-items: center; + height: 18px; + padding: 0 7px; + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: #166534; + background: #dcfce7; + border-radius: 9px; + line-height: 1; +} diff --git a/includes/class-opentrust-admin-settings.php b/includes/class-opentrust-admin-settings.php index bcefda6..35a6753 100644 --- a/includes/class-opentrust-admin-settings.php +++ b/includes/class-opentrust-admin-settings.php @@ -568,63 +568,97 @@ public function render_settings_page(): void { $tab = 'general'; } $base_url = admin_url('admin.php?page=opentrust'); + + // General + Contact are Settings-API-saveable. AI + IO have their own + // bespoke forms (admin-post handlers / sub-forms) and don't wire into + // the topbar Save until their respective migration commits. + $has_settings_form = in_array($tab, ['general', 'contact'], true); + $tabs = [ + 'general' => ['label' => __('General', 'opentrust'), 'url' => $base_url], + 'contact' => ['label' => __('Contact', 'opentrust'), 'url' => add_query_arg('tab', 'contact', $base_url)], + 'ai' => ['label' => __('AI Chat', 'opentrust'), 'url' => add_query_arg('tab', 'ai', $base_url)], + 'io' => ['label' => __('Import & Export', 'opentrust'), 'url' => add_query_arg('tab', 'io', $base_url)], + ]; ?> -
-

- -

- - → - -

- - - - - render_tab(); ?> - - render_ai_tab($settings); ?> - -
- '; - do_settings_sections('opentrust-settings-contact'); - submit_button(); - ?> -
- -
- '; - do_settings_sections('opentrust-settings-general'); - submit_button(); - ?> -
- +
+ + +
+

+

+
+ + + +
+ + render_tab(); ?> + + render_ai_tab($settings); ?> + +
+ '; + do_settings_sections('opentrust-settings-contact'); + ?> +
+ +
+ '; + do_settings_sections('opentrust-settings-general'); + ?> +
+ +
+ + id, 'toplevel_page_opentrust') + || str_starts_with($screen->id, 'opentrust_page_'); + + if ($is_plugin_page) { + wp_enqueue_style( + 'opentrust-admin-ds', + OPENTRUST_PLUGIN_URL . 'assets/css/opentrust-admin.css', + [], + OPENTRUST_VERSION + ); + wp_enqueue_script( + 'opentrust-admin-ds', + OPENTRUST_PLUGIN_URL . 'assets/js/opentrust-admin.js', + [], + OPENTRUST_VERSION, + true + ); + } + // Localize the handful of admin strings that admin.js renders directly // (e.g. wp.media modal titles). Catalog-screen strings are shipped // separately below via window.OpenTrustCatalog. From 82745baa52fbda500d6352d1f031fc726c3e8d12 Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Mon, 11 May 2026 13:44:03 +0200 Subject: [PATCH 03/11] migrate General tab fields to design system rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces do_settings_sections('opentrust-settings-general') with a manual render that emits .opentrust-block + .opentrust-card + .opentrust-row markup. The register_setting + sanitize cascade are untouched — only the render side moves to design-system markup. Sections: - General → endpoint slug, page title, company name, tagline - Branding → logo + avatar via .opentrust-media (template's wp.media wiring via [data-opentrust-media-picker], replacing the legacy data-ot-media-* bindings for these two fields), accent color via .opentrust-color (native swatch + hex pair, the existing contrast-warning widget hangs beneath as a .opentrust-row__control--stack child), credit toggle - Visible Sections → six chip toggles (.opentrust-chip with the template's :has(:checked) selector, no JS needed) Color picker: - Drops wp-color-picker for the accent field (existing $('.ot-color-picker') init now finds nothing — kept in admin.js for commit 9 cleanup since removing the wp-color-picker dep also requires unhooking the style/script enqueue in class-opentrust-admin.php, out of scope here). - admin.js accent-warning code now binds to the design system's hex text input via `input` events. opentrust-admin.js already syncs swatch→text on input, so swatch picks and direct hex typing both reach updateAccentWarning. The legacy add_settings_section('opentrust_general'...) + opentrust_branding + opentrust_sections registrations stay in register_settings(). They no longer render because do_settings_sections is gone, but they're harmless and removing them is bundled into the commit 9 cleanup once Contact + AI tabs have also migrated and the old render_*_field methods can be deleted in one sweep. PHPStan level 5 clean. --- assets/js/admin.js | 37 ++-- includes/class-opentrust-admin-settings.php | 226 +++++++++++++++++++- 2 files changed, 240 insertions(+), 23 deletions(-) diff --git a/assets/js/admin.js b/assets/js/admin.js index 93f1d65..d0e5529 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -145,25 +145,23 @@ } $(function () { - // ── Colour picker ────────────────────────── - var $accentInput = $('#opentrust_accent_color'); - var $forceExact = $('#opentrust_accent_force_exact'); + // ── Accent contrast warning ──────────────── + // Bound to native input events on the design system's hex text input + // (#opentrust_accent_color). The template's initColorPickers in + // opentrust-admin.js already syncs the swatch to + // the hex text input via `input` events, so listening here catches + // both swatch picks and direct hex typing. + var $accentInput = $('#opentrust_accent_color'); + var $forceExact = $('#opentrust_accent_force_exact'); var $accentWarning = $('#opentrust-accent-warning'); - $('.ot-color-picker').wpColorPicker({ - change: function (event, ui) { - // wpColorPicker fires `change` before the input is updated, - // so defer a tick before reading the value. - setTimeout(function () { - updateAccentWarning(ui.color.toString()); - }, 0); - }, - clear: function () { - setTimeout(function () { - updateAccentWarning($accentInput.val()); - }, 0); - } - }); + if ($accentInput.length) { + $accentInput.on('input', function () { + updateAccentWarning($accentInput.val()); + }); + // Initial check on page load. + updateAccentWarning($accentInput.val()); + } // Live-toggle the override class so the warning tone updates without // a page reload. The actual clamping still happens server-side — the @@ -172,11 +170,6 @@ $accentWarning.toggleClass('ot-accent-warning--override', this.checked); }); - // Initial check on page load. - if ($accentInput.length) { - updateAccentWarning($accentInput.val()); - } - // ── Media uploader (logo + avatar) ───────── $('[data-ot-media-field]').each(function () { var $field = $(this); diff --git a/includes/class-opentrust-admin-settings.php b/includes/class-opentrust-admin-settings.php index 35a6753..e0793e7 100644 --- a/includes/class-opentrust-admin-settings.php +++ b/includes/class-opentrust-admin-settings.php @@ -338,6 +338,230 @@ public function render_sections_field(array $args): void { } } + // ────────────────────────────────────────────── + // Design-system field renderers + // ────────────────────────────────────────────── + // + // These emit the shared Ettic admin design system row markup directly, + // bypassing do_settings_sections(). The Settings API registration in + // register_settings() (above) is what `settings_fields()` reads for the + // nonce + option_page hidden inputs — that wiring stays unchanged. + // add_settings_section/field calls for migrated tabs remain registered + // but un-rendered; commit cleanup removes them once all tabs migrate. + + private function ds_render_section_general(): void { + $settings = OpenTrust::get_settings(); + ?> +
+
+

+

+
+
+ ds_row_text('endpoint_slug', __('Endpoint Slug', 'opentrust'), (string) ($settings['endpoint_slug'] ?? ''), __('The URL path for your trust center (e.g. "trust-center" = yoursite.com/trust-center/).', 'opentrust')); ?> + ds_row_text('page_title', __('Page Title', 'opentrust'), (string) ($settings['page_title'] ?? '')); ?> + ds_row_text('company_name', __('Company Name', 'opentrust'), (string) ($settings['company_name'] ?? '')); ?> + ds_row_textarea('tagline', __('Tagline', 'opentrust'), (string) ($settings['tagline'] ?? ''), __('A short description displayed below the company name in the hero.', 'opentrust')); ?> +
+
+ +
+
+

+

+
+
+ ds_row_media('logo_id', __('Logo', 'opentrust'), (int) ($settings['logo_id'] ?? 0), __('Used in the hero and sticky nav. A white version is recommended — it sits on a dark background.', 'opentrust')); ?> + ds_row_media('avatar_id', __('AI Avatar', 'opentrust'), (int) ($settings['avatar_id'] ?? 0), __('Square image used as the avatar on AI chat responses. A colored background with a light/dark mark on top works well.', 'opentrust')); ?> + ds_row_accent_color($settings); ?> + ds_row_toggle('show_powered_by', __('Credit Link', 'opentrust'), !empty($settings['show_powered_by']), __('Show a "Powered by OpenTrust" credit in the trust center footer. Off by default — public credits are opt-in.', 'opentrust')); ?> +
+
+ +
+
+

+

+
+
+ ds_row_sections((array) ($settings['sections_visible'] ?? [])); ?> +
+
+ +
+
+ + +

+ +
+
+ +
+
+ +
+
+ + +

+ +
+
+ +
+
+ +
+
+ + +

+ +
+
+ + +
+
+ +
+
+ + +

+ +
+
+
+ +
+ + + +
+
+ + +
+
+
+
+ +
+
+ +

+
+
+
+ + +
+ +
+
+ __('Certifications & Compliance', 'opentrust'), + 'policies' => __('Policies', 'opentrust'), + 'subprocessors' => __('Subprocessors', 'opentrust'), + 'data_practices' => __('Data Practices', 'opentrust'), + 'faqs' => __('FAQs', 'opentrust'), + 'contact' => __('Contact & DPO', 'opentrust'), + ]; + ?> +
+
+ +

+
+
+
+ $label): ?> + + + +
+
+
+ '; - do_settings_sections('opentrust-settings-general'); + $this->ds_render_section_general(); ?> From d5ecde1685e4e9f679e0afa6e3db9b6f8e24af6f Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Mon, 11 May 2026 13:44:55 +0200 Subject: [PATCH 04/11] migrate Contact tab fields to design system rows Replaces do_settings_sections('opentrust-settings-contact') with a manual ds_render_section_contact() emitting .opentrust-block + .opentrust-card + .opentrust-row markup. Same pattern as the General tab migration in the previous commit. Adds ds_row_text_typed() for email/url inputs so the native HTML5 type flows through (mobile keyboards, inline validation, autofill hints) while the row shell stays consistent with ds_row_text(). The legacy register_settings() entries for the Contact tab stay registered but un-rendered; they get pruned together with the other un-used Settings API registrations once AI + IO tabs have also migrated. PHPStan level 5 clean. --- includes/class-opentrust-admin-settings.php | 51 ++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/includes/class-opentrust-admin-settings.php b/includes/class-opentrust-admin-settings.php index e0793e7..2ac3963 100644 --- a/includes/class-opentrust-admin-settings.php +++ b/includes/class-opentrust-admin-settings.php @@ -390,6 +390,55 @@ private function ds_render_section_general(): void { +
+
+

+

+
+
+ ds_row_textarea('company_description', __('Company Description', 'opentrust'), (string) ($settings['company_description'] ?? ''), __('Two or three sentences describing what the company does. Rendered under the "Get in touch" section title.', 'opentrust')); ?> + ds_row_text('dpo_name', __('DPO Name', 'opentrust'), (string) ($settings['dpo_name'] ?? ''), __('Data Protection Officer name. Required under GDPR for many organisations.', 'opentrust')); ?> + ds_row_text_typed('dpo_email', __('DPO Email', 'opentrust'), (string) ($settings['dpo_email'] ?? ''), 'email', __('Dedicated DPO mailbox. Rendered as a mailto link.', 'opentrust')); ?> + ds_row_text_typed('security_email', __('Security Contact Email', 'opentrust'), (string) ($settings['security_email'] ?? ''), 'email', __('For vulnerability reports and security questions. Often separate from the DPO.', 'opentrust')); ?> + ds_row_text_typed('contact_form_url', __('Contact Form URL', 'opentrust'), (string) ($settings['contact_form_url'] ?? ''), 'url', __('Optional link to a gated contact form.', 'opentrust')); ?> + ds_row_textarea('contact_address', __('Mailing Address', 'opentrust'), (string) ($settings['contact_address'] ?? ''), __('Postal address for formal GDPR / legal notices.', 'opentrust')); ?> + ds_row_text_typed('pgp_key_url', __('PGP Public Key URL', 'opentrust'), (string) ($settings['pgp_key_url'] ?? ''), 'url', __("Optional link to your security team's PGP public key.", 'opentrust')); ?> + ds_row_text('company_registration', __('Company Registration Number', 'opentrust'), (string) ($settings['company_registration'] ?? ''), __('KvK (NL), Companies House (UK), Handelsregister (DE), EIN (US), or equivalent business registration.', 'opentrust')); ?> + ds_row_text('vat_number', __('VAT / Tax ID', 'opentrust'), (string) ($settings['vat_number'] ?? ''), __('VAT number, sales-tax ID, or equivalent international tax identifier.', 'opentrust')); ?> +
+
+ ' placeholder="https://" inputmode="url" autocomplete="off"', + 'email' => ' autocomplete="off"', + default => '', + }; + ?> +
+
+ + +

+ +
+
+ > +
+
+ @@ -868,7 +917,7 @@ class="opentrust-tabbar__tab'; - do_settings_sections('opentrust-settings-contact'); + $this->ds_render_section_contact(); ?> From 3e4ae27688427817eaba4dbc6574fd41078ba83b Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Mon, 11 May 2026 13:50:09 +0200 Subject: [PATCH 05/11] migrate AI Chat tab to design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites OpenTrust_Admin_AI::render_ai_tab(), the provider picker, the provider cards, and the main AI settings form to emit design-system markup. All admin-post handlers (key save/forget/refresh, summary sweep) stay untouched. - render_ai_tab() now wraps the intro + rationale in an .opentrust-block with a card containing an .opentrust-disclosure (new component — see the CSS extension block). - Transient notices switch from WP-native .notice to .opentrust-notice variants (success/error/warn/info) via a shared ds_notice() helper. - Summary backfill banner: .opentrust-notice--warn with an embedded admin-post form in .opentrust-notice__actions, primary ghost button. - Non-anthropic active warning: .opentrust-notice--warn. - Provider picker: .opentrust-block headed "Step 1 — Connect Anthropic", primary card uses .opentrust-card + .opentrust-ai-card--primary variant. Advanced disclosure wraps the existing flat grid of .opentrust-ai-card--advanced cards. - Provider card: same content (title/badge/keylink/saved-state/form) but emits design-system input + button classes, and the saved-state ✓ icon becomes a structured .opentrust-ai-card__check pill matching the design language. - AI settings form: id="opentrust-settings-form" so the topbar Save in the wrap submits it. submit_button() dropped. The table.form-table becomes two .opentrust-block sections: "Step 2 — Model & defaults" (model + budgets + rate limits + max-msg + contact URL + three toggles) and "Anti-abuse — Cloudflare Turnstile" (toggle + site key + secret key with masked-bullet placeholder for the encrypted blob). - AI tab is now treated as has_settings_form so the topbar Save + Discard render. The opentrust-admin.js dirty tracker prefers the form the Save button is wired to (via HTML5 form="..." attribute) instead of just .opentrust-admin form, so the AI tab's many sub-forms (key save, refresh, forget) don't confuse the dirty tracker. - Oversized-policies warning becomes an .opentrust-notice--error with a .opentrust-notice__list bullet list. CSS additions appended to opentrust-admin.css under an OpenTrust-only extensions block: .opentrust-disclosure, .opentrust-ai-card variants, .opentrust-ai-advanced__grid, .opentrust-ai-model-row, .opentrust-row__unit, .opentrust-field-msg--success, .opentrust-notice__list, .opentrust-notice__actions. Local ds_row_number() and ds_row_toggle() helpers live in OpenTrust_Admin_AI because OpenTrust_Admin_Settings::ds_row_* are private. Lifting them into a shared helper class can happen alongside the Settings API registrations cleanup in commit 9. PHPStan level 5 clean. --- assets/css/opentrust-admin.css | 209 +++++++ assets/js/opentrust-admin.js | 9 +- includes/class-opentrust-admin-ai.php | 628 ++++++++++---------- includes/class-opentrust-admin-settings.php | 8 +- 4 files changed, 547 insertions(+), 307 deletions(-) diff --git a/assets/css/opentrust-admin.css b/assets/css/opentrust-admin.css index 154bd1d..84f0f65 100644 --- a/assets/css/opentrust-admin.css +++ b/assets/css/opentrust-admin.css @@ -1438,3 +1438,212 @@ body > .opentrust-modal-backdrop.is-leaving { animation: opentrust-backdrop-out border-radius: 9px; line-height: 1; } + +/* ----------------------------------------------------------------------- * + * OpenTrust-only extensions: AI tab atoms. * + * * + * Provider cards (full-width primary card, side-by-side advanced grid), * + * disclosure (
) styling, the model row that pairs the select * + * with the Refresh button, and a couple of small utilities used by AI/ * + * Contact rows (unit suffix, success field-msg, notice list/actions). * + * * + * Not part of the shared design system. Extracted with the AI tab if/ * + * when the template gains a provider-card primitive. * + * ----------------------------------------------------------------------- */ + +/* Disclosure (
) inside a card or block */ +.opentrust-admin .opentrust-disclosure { + border: 1px solid var(--ettic-border); + border-radius: var(--r-md); + background: #fff; +} + +.opentrust-admin .opentrust-card > .opentrust-disclosure { + border: 0; + border-radius: 0; + background: transparent; +} + +.opentrust-admin .opentrust-disclosure > summary { + cursor: pointer; + padding: 12px 14px; + font-weight: 600; + color: var(--tx-strong); + font-size: 13.5px; + list-style: none; + user-select: none; + transition: background 120ms ease; +} + +.opentrust-admin .opentrust-disclosure > summary::-webkit-details-marker { display: none; } + +.opentrust-admin .opentrust-disclosure > summary::before { + content: "▸"; + display: inline-block; + width: 14px; + color: var(--tx-muted); + transition: transform 140ms ease; +} + +.opentrust-admin .opentrust-disclosure[open] > summary::before { transform: rotate(90deg); } + +.opentrust-admin .opentrust-disclosure > summary:hover { background: var(--ettic-surface-2); } + +.opentrust-admin .opentrust-disclosure__body { + padding: 4px 14px 14px; + color: var(--tx-body); +} + +.opentrust-admin .opentrust-disclosure__body > p { + margin: 0 0 10px; + line-height: 1.55; +} + +.opentrust-admin .opentrust-disclosure__body > p:last-child { margin-bottom: 0; } + +/* AI provider card */ +.opentrust-admin .opentrust-ai-card { padding: 16px 18px; } + +.opentrust-admin .opentrust-ai-card--primary { + margin-top: 14px; + border-color: var(--ettic-blue); + box-shadow: 0 1px 2px rgba(15,92,250,0.06); +} + +.opentrust-admin .opentrust-ai-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 6px; +} + +.opentrust-admin .opentrust-ai-card__title { + margin: 0; + font-size: 15px; + font-weight: 600; + color: var(--tx-strong); +} + +.opentrust-admin .opentrust-ai-card__badge { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 10px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--ettic-blue); + background: var(--ettic-blue-soft); + border-radius: 11px; + line-height: 1; +} + +.opentrust-admin .opentrust-ai-card__description { + margin: 4px 0 10px; + color: var(--tx-body); + line-height: 1.55; +} + +.opentrust-admin .opentrust-ai-card__keylink { + margin: 0 0 14px; + font-size: 13px; +} + +.opentrust-admin .opentrust-ai-card__saved { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 12px; + margin: 0 0 10px; + background: #ECF9F1; + border: 1px solid #B6E5C9; + color: #1B5E36; + border-radius: var(--r-sm); + font-size: 13px; +} + +.opentrust-admin .opentrust-ai-card__check { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 50%; + background: #1B8E45; + color: #fff; +} + +.opentrust-admin .opentrust-ai-card__check svg { width: 9px; height: 9px; } + +.opentrust-admin .opentrust-ai-card__saved code { + background: transparent; + color: inherit; + font-size: 12.5px; +} + +.opentrust-admin .opentrust-ai-card__form { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.opentrust-admin .opentrust-ai-card__form .opentrust-input { flex: 1 1 260px; min-width: 0; } + +/* Advanced provider grid */ +.opentrust-admin .opentrust-ai-advanced__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 14px; + margin-top: 8px; +} + +.opentrust-admin .opentrust-ai-card--advanced { padding: 14px 16px; } +.opentrust-admin .opentrust-ai-card--advanced .opentrust-ai-card__title { font-size: 14px; } + +/* Model select + refresh button */ +.opentrust-admin .opentrust-ai-model-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.opentrust-admin .opentrust-ai-model-row .opentrust-select { min-width: 320px; } + +/* Generic row helpers */ +.opentrust-admin .opentrust-row__unit { + font-size: 13px; + color: var(--tx-muted); + margin-left: 8px; +} + +/* Field message — success variant (template ships error only) */ +.opentrust-admin .opentrust-field-msg--success { color: #1B5E36; } + +/* Notice — list + action-row variants used by AI tab banners */ +.opentrust-admin .opentrust-notice__list { + margin: 6px 0 0 18px; + padding: 0; + list-style: disc; +} + +.opentrust-admin .opentrust-notice__list li { + margin: 2px 0; +} + +.opentrust-admin .opentrust-notice__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +/* Default inside notice body styled like a title */ +.opentrust-admin .opentrust-notice__body > strong { + display: block; + font-weight: 600; + margin: 0 0 2px; + font-size: 13.5px; +} diff --git a/assets/js/opentrust-admin.js b/assets/js/opentrust-admin.js index 4c80055..7ed96e7 100644 --- a/assets/js/opentrust-admin.js +++ b/assets/js/opentrust-admin.js @@ -140,7 +140,14 @@ labelEl = document.querySelector( '[data-dirty-label]' ); saveBtn = document.querySelector( '[data-save]' ); discardBtn = document.querySelector( '[data-discard]' ); - form = document.querySelector( '.opentrust-admin form' ); + // Prefer the form the topbar Save is wired to (HTML5 `form="..."` attr, + // which points at the active tab's Settings API form on multi-form + // screens like AI Chat). Fall back to the first form inside the wrap. + var saveTargetId = saveBtn ? saveBtn.getAttribute( 'form' ) : null; + form = saveTargetId ? document.getElementById( saveTargetId ) : null; + if ( ! form ) { + form = document.querySelector( '.opentrust-admin form' ); + } if ( ! dirtyEl || ! saveBtn || ! form ) { return; } diff --git a/includes/class-opentrust-admin-ai.php b/includes/class-opentrust-admin-ai.php index c7add98..0439695 100644 --- a/includes/class-opentrust-admin-ai.php +++ b/includes/class-opentrust-admin-ai.php @@ -50,69 +50,70 @@ public function render_ai_tab(array $settings): void { $has_active_key = $active_provider !== '' && isset($stored_keys[$active_provider]); $is_non_anthropic_active = $has_active_key && $active_provider !== 'anthropic'; - // Surface any transient notice from the admin-post handlers. + // Transient notice from the admin-post handlers. $notice = get_transient('opentrust_ai_notice_' . get_current_user_id()); if (is_array($notice)) { delete_transient('opentrust_ai_notice_' . get_current_user_id()); - $class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success'; - printf( - '

%s

', - esc_attr($class), - esc_html((string) $notice['message']) - ); + $variant = $notice['type'] === 'error' ? 'error' : 'success'; + $this->ds_notice($variant, (string) ($notice['message'] ?? '')); } $this->render_summary_backfill_banner($settings, $has_active_key); - ?> - -
- -

- ' . esc_html(ucfirst($active_provider)) . '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - ); - ?> -

-
- - -

- Anthropic Claude with the native Citations API to answer visitor questions about your trust center. Every claim the assistant makes is tied to an exact quote from one of your published documents — so no policy text is invented and nothing is paraphrased into something you did not actually publish.', 'opentrust'), - ['strong' => []] + if ($is_non_anthropic_active): + $intro = sprintf( + /* translators: %s: provider label, e.g. OpenAI */ + __('You are currently using %s. Only Anthropic uses a structural Citations API — every other provider relies on prompted citation tags the model can ignore or fabricate. For a published trust center, switch to Anthropic below.', 'opentrust'), + ucfirst($active_provider) ); ?> -

+ + -
- -
+
+
+

compliance surface. If the assistant invents a security commitment you never made, that is not a UX papercut — it is a misrepresentation of your security posture, and your customers and auditors will hold you to it.', 'opentrust'), + __('OpenTrust uses Anthropic Claude with the native Citations API to answer visitor questions about your trust center. Every claim the assistant makes is tied to an exact quote from one of your published documents — no policy text is invented, nothing is paraphrased into something you did not publish.', 'opentrust'), ['strong' => []] ); ?>

-

- only major provider that exposes a structural Citations API. Documents are sent as typed blocks and the model emits citations as first-class events containing the exact source document and the exact quoted text. The model literally cannot return a citation for text that is not in your source documents.', 'opentrust'), - ['strong' => []] - ); - ?> -

-

- -

+
+
+
+ +
+

+ compliance surface. If the assistant invents a security commitment you never made, that is not a UX papercut — it is a misrepresentation of your security posture, and your customers and auditors will hold you to it.', 'opentrust'), + ['strong' => []] + ); + ?> +

+

+ only major provider that exposes a structural Citations API. Documents are sent as typed blocks and the model emits citations as first-class events containing the exact source document and the exact quoted text. The model literally cannot return a citation for text that is not in your source documents.', 'opentrust'), + ['strong' => []] + ); + ?> +

+

+ +

+
+
-
+ render_ai_provider_picker($settings, $stored_keys); ?> @@ -122,6 +123,24 @@ public function render_ai_tab(array $settings): void { +
+
+ + + +

+
+
+ @@ -140,32 +159,27 @@ private function render_summary_backfill_banner(array $settings, bool $has_activ if ($missing < 1) { return; } + $heading = sprintf( + /* translators: %d is the number of policies missing AI summaries. */ + _n( + '%d policy is missing an AI summary.', + '%d policies are missing AI summaries.', + $missing, + 'opentrust' + ), + (int) $missing + ); ?> -
-

- - - - -

-
- - - -
+
+
+ +

+
+ + + +
+
' . esc_html__('Choose a provider and add your key', 'opentrust') . ''; - echo '
'; - foreach ($providers as $provider) { - $this->render_provider_card($provider, $stored_keys, $active_provider, 'advanced'); - } - echo '
'; + ?> +
+
+

+

+
+
+ + render_provider_card($provider, $stored_keys, $active_provider, 'advanced'); ?> + +
+
+ -

- - render_provider_card($primary, $stored_keys, $active_provider, 'primary'); ?> - - -
> - - -
- -

- -

-

- - -

-
- -
- - render_provider_card($provider, $stored_keys, $active_provider, 'advanced'); ?> - -
-
- +
+
+

+

+
+ render_provider_card($primary, $stored_keys, $active_provider, 'primary'); ?> + + +
> + +
+ +
+ + render_provider_card($provider, $stored_keys, $active_provider, 'advanced'); ?> + +
+
+
+ +
-
-

+
+

- +
-

+

- -
- ✓ +
+ +
-
+ -
-
+ - -
@@ -305,205 +332,202 @@ private function render_provider_card(array $provider, array $stored_keys, strin } private function render_ai_settings_form(array $settings): void { - $active_provider = $settings['ai_provider']; + $active_provider = (string) $settings['ai_provider']; $models = $this->get_cached_model_list($active_provider); - $current_model = $settings['ai_model'] ?? ''; + $current_model = (string) ($settings['ai_model'] ?? ''); $refresh_url = wp_nonce_url( admin_url('admin-post.php?action=opentrust_ai_refresh_models&provider=' . rawurlencode($active_provider)), 'opentrust_ai_refresh_models' ); + $cached_at = (int) ($settings['ai_model_list_cached_at'] ?? 0); + $oversized = class_exists('OpenTrust_Chat_Corpus') ? OpenTrust_Chat_Corpus::oversized_policies() : []; + $secret_saved = !empty($settings['turnstile_secret_key']); ?> -

- -
+ - + // sanitize callback carries every non-AI key forward from $old, so we + // do NOT need to re-POST values from other tabs as hidden inputs. ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+ +
+ + + + +
+
+
+ + ds_row_number('ai_daily_token_budget', __('Daily token budget', 'opentrust'), (int) ($settings['ai_daily_token_budget'] ?? OpenTrust_Chat_Budget::DEFAULT_DAILY_TOKEN_BUDGET), 0, 100000000, 10000, __('tokens', 'opentrust'), __('Hard cap per site per day. Default 500,000 (~$12/day at Sonnet 4.5 rates).', 'opentrust')); ?> + + ds_row_number('ai_monthly_token_budget', __('Monthly token budget', 'opentrust'), (int) ($settings['ai_monthly_token_budget'] ?? OpenTrust_Chat_Budget::DEFAULT_MONTHLY_TOKEN_BUDGET), 0, 1000000000, 100000, __('tokens', 'opentrust'), __('Hard cap per site per month. Default 10,000,000.', 'opentrust')); ?> + + ds_row_number('ai_rate_limit_per_ip', __('Rate limit — per IP', 'opentrust'), (int) ($settings['ai_rate_limit_per_ip'] ?? 10), 0, 1000, 1, __('messages per minute', 'opentrust')); ?> + + ds_row_number('ai_rate_limit_per_session', __('Rate limit — per session', 'opentrust'), (int) ($settings['ai_rate_limit_per_session'] ?? 50), 0, 10000, 1, __('messages per hour', 'opentrust')); ?> + + ds_row_number('ai_max_message_length', __('Max message length', 'opentrust'), (int) ($settings['ai_max_message_length'] ?? OpenTrust_Chat::DEFAULT_MAX_MESSAGE_LENGTH), 100, 4000, 100, __('characters', 'opentrust')); ?> + +
+
+ +

+
+
+ +
+
+ + ds_row_toggle('ai_show_model_attribution', __('Visitor display', 'opentrust'), !empty($settings['ai_show_model_attribution']), __('Show the active model name under the chat input.', 'opentrust')); ?> + + ds_row_toggle('ai_logging_enabled', __('Analytics logging', 'opentrust'), !empty($settings['ai_logging_enabled']), __('Log anonymised visitor questions for admin review (90-day auto-purge, no PII).', 'opentrust')); ?> + + ds_row_toggle('ai_auto_summarize', __('Improve answer quality', 'opentrust'), !empty($settings['ai_auto_summarize']), __('Auto-generate a 2–3 sentence AI summary of each policy for routing. Cost ~$0.05–$0.10 per 50 policies lifetime; pennies per edit afterward. Uses your configured AI key.', 'opentrust')); ?> +
+ + + + + -

-

- -

- - - - - - - - - - - - - - - - +
+
+

+

+
+
+ ds_row_toggle('ai_turnstile_enabled', __('Enable Turnstile for chat', 'opentrust'), !empty($settings['ai_turnstile_enabled']), __('Require Turnstile verification on first chat message.', 'opentrust')); ?> + +
+
+ +

+
+
+ +
+
+ +
+
+ +

+
+
+ + + + + + + +
+
+
+
+
+
+ + +

+ +
+
+ + +
+
+ +
+
+ + +

+ +
+
+ + +
+
+ ['label' => __('General', 'opentrust'), 'url' => $base_url], 'contact' => ['label' => __('Contact', 'opentrust'), 'url' => add_query_arg('tab', 'contact', $base_url)], From 006cb2ba70a67e7ca5a66e35e256f00581de401b Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Mon, 11 May 2026 13:52:38 +0200 Subject: [PATCH 06/11] migrate Questions log screen to design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenTrust_Admin_Questions::render_page() now wraps in .opentrust-admin with the dark topbar (brand mark + version + back-to-AI-settings link + View Trust Center link), no Save (read-only screen), the topbar__head title block, an .opentrust-stack of section blocks, and the shared \OpenTrust\Admin\Footer at the bottom. Each surface on the page is rebuilt with design-system markup: - Logging on/off banner → .opentrust-notice--success or --warn with an inline toggle action in .opentrust-notice__actions. - Filter form → .opentrust-block + .opentrust-card with a new .opentrust-filterbar layout (label-above-input columns + grouped actions on the right). Inputs and select use design system classes. - Log table → .opentrust-card--flush (new variant — drops card padding so the table can span edges) containing a token-styled .opentrust-log-table. Refused rows highlight in warn tones; meta columns use mono, tabular-nums, and muted text. - Pagination → paginate_links() output restyled inside .opentrust-log-table__pagination (rounded chip per page, blue active, gray dots). - Danger zone → .opentrust-action-row inside a card with a .opentrust-btn--danger Clear button (kept the same confirm() flow). CSS additions (Questions-only extensions block in opentrust-admin.css): .opentrust-card--flush, .opentrust-filterbar*, .opentrust-log-table*, plus the pagination styling that overrides paginate_links()'s defaults. Logic is unchanged — same filter handling, same query, same export / clear / toggle-logging admin-post handlers. PHPStan level 5 clean. --- assets/css/opentrust-admin.css | 125 +++++++ includes/class-opentrust-admin-questions.php | 326 +++++++++++-------- 2 files changed, 323 insertions(+), 128 deletions(-) diff --git a/assets/css/opentrust-admin.css b/assets/css/opentrust-admin.css index 84f0f65..e0298d9 100644 --- a/assets/css/opentrust-admin.css +++ b/assets/css/opentrust-admin.css @@ -1647,3 +1647,128 @@ body > .opentrust-modal-backdrop.is-leaving { animation: opentrust-backdrop-out margin: 0 0 2px; font-size: 13.5px; } + +/* ----------------------------------------------------------------------- * + * OpenTrust-only extensions: Questions log screen. * + * * + * Filter toolbar (label-above-input columns + an actions group), a * + * lightweight log table styled with design tokens, and a flush card * + * variant that lets the table span the card edge-to-edge. * + * ----------------------------------------------------------------------- */ + +/* Flush card: drops the padding so a table or media block can span edges */ +.opentrust-admin .opentrust-card--flush { padding: 0; overflow: hidden; } + +/* Filter toolbar */ +.opentrust-admin .opentrust-filterbar { + display: flex; + flex-wrap: wrap; + gap: 10px 14px; + align-items: flex-end; +} + +.opentrust-admin .opentrust-filterbar__field { display: flex; flex-direction: column; gap: 4px; min-width: 0; } +.opentrust-admin .opentrust-filterbar__field label { + font-size: 11px; + font-weight: 600; + color: var(--tx-muted); + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.opentrust-admin .opentrust-filterbar__field input, +.opentrust-admin .opentrust-filterbar__field .opentrust-select { min-width: 0; } + +.opentrust-admin .opentrust-filterbar__actions { + display: inline-flex; + flex-wrap: wrap; + gap: 8px; + margin-left: auto; +} + +.opentrust-admin .opentrust-filterbar__export { margin-left: auto; } + +/* Log table */ +.opentrust-admin .opentrust-log-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 13px; + color: var(--tx-body); +} + +.opentrust-admin .opentrust-log-table thead th { + padding: 9px 14px; + font-size: 11px; + font-weight: 600; + color: var(--tx-muted); + letter-spacing: 0.04em; + text-transform: uppercase; + text-align: left; + background: var(--ettic-surface-2); + border-bottom: 1px solid var(--ettic-border); + white-space: nowrap; +} + +.opentrust-admin .opentrust-log-table tbody td { + padding: 10px 14px; + border-bottom: 1px solid var(--ettic-border); + vertical-align: top; +} + +.opentrust-admin .opentrust-log-table tbody tr:last-child td { border-bottom: 0; } +.opentrust-admin .opentrust-log-table tbody tr:nth-child(even) { background: #FBFCFD; } +.opentrust-admin .opentrust-log-table tbody tr.is-refused { background: #FFF7EC; } +.opentrust-admin .opentrust-log-table tbody tr.is-refused:nth-child(even) { background: #FFF1DA; } + +.opentrust-admin .opentrust-log-table__empty td { text-align: center; color: var(--tx-muted); padding: 24px 14px; } + +.opentrust-admin .opentrust-log-table__date { white-space: nowrap; color: var(--tx-muted); font-size: 12.5px; } +.opentrust-admin .opentrust-log-table__num { text-align: right; width: 64px; font-variant-numeric: tabular-nums; } +.opentrust-admin .opentrust-log-table__meta { font-size: 12px; color: var(--tx-muted); font-variant-numeric: tabular-nums; white-space: nowrap; } +.opentrust-admin .opentrust-log-table__model { font-family: var(--ff-mono); font-size: 11.5px; background: transparent; color: var(--tx-muted); padding: 0; } + +.opentrust-admin .opentrust-log-table__refused { + display: inline-block; + padding: 1px 7px; + margin-right: 6px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + background: var(--warn-bg); + color: var(--warn); + border-radius: 9px; +} + +/* Pagination — restyle paginate_links() output to fit the design system */ +.opentrust-admin .opentrust-log-table__pagination { + display: flex; + justify-content: center; + gap: 4px; + padding: 12px 14px; + background: var(--ettic-surface-2); + border-top: 1px solid var(--ettic-border); + font-size: 13px; +} + +.opentrust-admin .opentrust-log-table__pagination .page-numbers { + display: inline-flex; + align-items: center; + min-width: 28px; + height: 28px; + padding: 0 9px; + border-radius: var(--r-sm); + border: 1px solid var(--ettic-border); + background: #fff; + color: var(--tx-strong); + text-decoration: none; +} + +.opentrust-admin .opentrust-log-table__pagination .page-numbers:hover { border-color: var(--ettic-border-hi); color: var(--ettic-blue); } +.opentrust-admin .opentrust-log-table__pagination .page-numbers.current { + background: var(--ettic-blue); + border-color: var(--ettic-blue); + color: #fff; +} +.opentrust-admin .opentrust-log-table__pagination .page-numbers.dots { border: 0; background: transparent; color: var(--tx-quiet); } diff --git a/includes/class-opentrust-admin-questions.php b/includes/class-opentrust-admin-questions.php index a8bd6a5..d7feb65 100644 --- a/includes/class-opentrust-admin-questions.php +++ b/includes/class-opentrust-admin-questions.php @@ -55,12 +55,14 @@ public function render_page(): void { ]; // phpcs:enable WordPress.Security.NonceVerification.Recommended - $result = OpenTrust_Chat_Log::query($filters); - $total = $result['total']; - $rows = $result['rows']; - $pages = max(1, (int) ceil($total / $filters['per_page'])); - $models = OpenTrust_Chat_Log::distinct_models(); - $counts = OpenTrust_Chat_Log::total_count(); + $result = OpenTrust_Chat_Log::query($filters); + $total = $result['total']; + $rows = $result['rows']; + $pages = max(1, (int) ceil($total / $filters['per_page'])); + $models = OpenTrust_Chat_Log::distinct_models(); + $counts = OpenTrust_Chat_Log::total_count(); + $tc_url = home_url('/' . ($settings['endpoint_slug'] ?? OpenTrust::DEFAULT_ENDPOINT_SLUG) . '/'); + $back_url = admin_url('admin.php?page=opentrust&tab=ai'); $export_url = wp_nonce_url( admin_url('admin-post.php?action=opentrust_ai_questions_export&' . http_build_query(array_filter($filters + ['paged' => 0]))), @@ -75,137 +77,205 @@ public function render_page(): void { 'opentrust_ai_toggle_logging' ); - $notice = get_transient('opentrust_ai_notice_' . get_current_user_id()); - if (is_array($notice)) { - delete_transient('opentrust_ai_notice_' . get_current_user_id()); - $class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success'; - printf('

%s

', esc_attr($class), esc_html((string) $notice['message'])); - } + $logging_on = !empty($settings['ai_logging_enabled']); ?> -
-

- -

- -

- -
- - - ✓ - - ✗ - - - - - - - - +
+ + -
- -
-
- - -
-
- - +
+

+

+
+ +
+ +

%s

', + esc_attr($variant), + esc_html((string) ($notice['message'] ?? '')) + ); + } + ?> + +
+
+ + + +

+ +

+
+ + + +
-
- - +
+ +
+
+

+
+
+ + +
+ + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + + +
+
-
- - +
+ +
+
+

+

+ + 0): ?> + · + +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
created_at . ' UTC'))); ?> + refused): ?> + + + question); ?> + model); ?>citation_count; ?>tokens_in; ?> / ↑tokens_out; ?>response_ms; ?>ms
+ + 1): + $base = add_query_arg($filters + ['page' => 'opentrust-questions'], admin_url('admin.php')); + $base = remove_query_arg('paged', $base); + ?> +
+ add_query_arg('paged', '%#%', $base), + 'format' => '', + 'current' => $filters['page'], + 'total' => $pages, + 'prev_text' => '‹', + 'next_text' => '›', + ]); + ?> +
+
- - - -
- - - - - - - - - - - - - - - - - - refused ? 'background:#fef9c3' : ''; - ?> - - - - - - - - - - - -
created_at . ' UTC'))); ?> - refused): ?> - - - question); ?> - model); ?>citation_count; ?> - ↓tokens_in; ?> / ↑tokens_out; ?> - - response_ms; ?>ms -
- - 1): - $base = add_query_arg($filters + ['page' => 'opentrust-questions'], admin_url('admin.php')); - $base = remove_query_arg('paged', $base); - ?> -
-
- add_query_arg('paged', '%#%', $base), - 'format' => '', - 'current' => $filters['page'], - 'total' => $pages, - 'prev_text' => '‹', - 'next_text' => '›', - ]); - ?> + + +
+
+

+
+
+
+
+

+

+
+ + + +
-
- + +
-
-

-

- -

+
Date: Mon, 11 May 2026 13:55:11 +0200 Subject: [PATCH 07/11] migrate Import & Export tab to design system Rewrites OpenTrust_Admin_Tools::render_tab(), render_export_panel(), render_import_panel(), and render_preview_screen() to use design-system markup. All admin-post handlers (export, import_preview, import_apply) are unchanged. - Transient notices switch to .opentrust-notice variants. - Tab intro becomes an .opentrust-block header. - Export panel: own .opentrust-block + .opentrust-card. The kind picker (content vs settings) becomes an .opentrust-seg segmented control. Content selection uses an .opentrust-io-cpt-list of
groups (one per CPT) with nested checkboxes. "Bundle media" becomes a .opentrust-toggle row. Submit moves into an .opentrust-action-row. - Import panel: warn banner becomes .opentrust-notice--warn. File input gets an .opentrust-input--file shim (dashed border, native chrome retained). Conflict strategy becomes an .opentrust-seg with three options. Submit moves into an .opentrust-action-row. - Preview screen: errors/warnings as .opentrust-notice--error/--warn with .opentrust-notice__list bullet lists. Per-CPT preview tables reuse .opentrust-log-table styling inside .opentrust-card--flush with a new .opentrust-card__header bar above each. Action column becomes .opentrust-io-preview-table__pill with create/update/skip/ new color variants. Confirm + Cancel buttons move into an .opentrust-io-confirm row at the bottom. CSS additions appended to opentrust-admin.css under the I&E extensions block: .opentrust-io-cpt-list / .opentrust-io-cpt*, .opentrust-input--file, .opentrust-card__header / .opentrust-card__title, the preview-table pills, and .opentrust-io-confirm. PHPStan level 5 clean. --- assets/css/opentrust-admin.css | 123 +++++++ includes/class-opentrust-admin-tools.php | 412 +++++++++++++---------- 2 files changed, 364 insertions(+), 171 deletions(-) diff --git a/assets/css/opentrust-admin.css b/assets/css/opentrust-admin.css index e0298d9..13d092d 100644 --- a/assets/css/opentrust-admin.css +++ b/assets/css/opentrust-admin.css @@ -1772,3 +1772,126 @@ body > .opentrust-modal-backdrop.is-leaving { animation: opentrust-backdrop-out color: #fff; } .opentrust-admin .opentrust-log-table__pagination .page-numbers.dots { border: 0; background: transparent; color: var(--tx-quiet); } + +/* ----------------------------------------------------------------------- * + * OpenTrust-only extensions: Import & Export tab. * + * * + * CPT collapsible groups inside the export form, a preview table that * + * reuses .opentrust-log-table styling, and small action pills (create / * + * update / skip / new) for the preview screen. * + * ----------------------------------------------------------------------- */ + +/* CPT picker — collapsible list inside the Export form */ +.opentrust-admin .opentrust-io-cpt-list { + display: flex; + flex-direction: column; + gap: 6px; + border: 1px solid var(--ettic-border); + border-radius: var(--r-md); + background: #fff; + padding: 8px 10px; +} + +.opentrust-admin .opentrust-io-cpt > summary { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 6px; + cursor: pointer; + list-style: none; + font-size: 13px; + user-select: none; +} + +.opentrust-admin .opentrust-io-cpt > summary::-webkit-details-marker { display: none; } +.opentrust-admin .opentrust-io-cpt > summary::before { + content: "▸"; + display: inline-block; + width: 12px; + color: var(--tx-muted); + transition: transform 140ms ease; +} +.opentrust-admin .opentrust-io-cpt[open] > summary::before { transform: rotate(90deg); } + +.opentrust-admin .opentrust-io-cpt__head { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; } +.opentrust-admin .opentrust-io-cpt__name { font-weight: 500; color: var(--tx-strong); } +.opentrust-admin .opentrust-io-cpt__count { color: var(--tx-muted); font-size: 12px; } + +.opentrust-admin .opentrust-io-cpt__items { + margin: 4px 0 8px 28px; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; +} + +.opentrust-admin .opentrust-io-cpt__items label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--tx-body); +} + +.opentrust-admin .opentrust-io-cpt__status { font-style: italic; color: var(--tx-muted); } + +/* File input shim — native, but with token spacing */ +.opentrust-admin .opentrust-input--file { + padding: 6px 8px; + background: #fff; + border: 1px dashed var(--ettic-border-hi); +} + +/* Card header for preview tables (a small bar above a flush table) */ +.opentrust-admin .opentrust-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 14px; + background: var(--ettic-surface-2); + border-bottom: 1px solid var(--ettic-border); +} + +.opentrust-admin .opentrust-card__title { + margin: 0; + font-size: 13px; + font-weight: 600; + color: var(--tx-strong); +} + +/* Preview table action pills */ +.opentrust-admin .opentrust-io-preview-table__action { width: 110px; } +.opentrust-admin .opentrust-io-preview-table__uuid { + font-family: var(--ff-mono); + font-size: 11.5px; + color: var(--tx-muted); + width: 200px; + word-break: break-all; +} + +.opentrust-admin .opentrust-io-preview-table__uuid code { background: transparent; padding: 0; color: inherit; } + +.opentrust-admin .opentrust-io-preview-table__pill { + display: inline-block; + padding: 2px 9px; + border-radius: 9px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.opentrust-admin .opentrust-io-preview-table__pill--create { background: #ECF9F1; color: #1B5E36; } +.opentrust-admin .opentrust-io-preview-table__pill--update { background: var(--ettic-blue-soft); color: var(--ettic-blue); } +.opentrust-admin .opentrust-io-preview-table__pill--skip { background: var(--ettic-surface-2); color: var(--tx-muted); } +.opentrust-admin .opentrust-io-preview-table__pill--new { background: #FBF1FF; color: #6B2DAA; } +.opentrust-admin .opentrust-io-preview-table__pill--rename { background: #FBF1FF; color: #6B2DAA; } + +/* Confirm + Cancel button row */ +.opentrust-admin .opentrust-io-confirm { + display: flex; + gap: 10px; + justify-content: flex-end; +} diff --git a/includes/class-opentrust-admin-tools.php b/includes/class-opentrust-admin-tools.php index 016e22b..e1fb2ff 100644 --- a/includes/class-opentrust-admin-tools.php +++ b/includes/class-opentrust-admin-tools.php @@ -41,29 +41,28 @@ public function render_tab(): void { $notice = get_transient('opentrust_io_notice_' . get_current_user_id()); if (is_array($notice)) { delete_transient('opentrust_io_notice_' . get_current_user_id()); - $class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success'; - printf('

%s

', esc_attr($class), wp_kses_post((string) $notice['message'])); + $variant = $notice['type'] === 'error' ? 'error' : 'success'; + printf( + '

%s

', + esc_attr($variant), + wp_kses_post((string) ($notice['message'] ?? '')) + ); } $exportable = OpenTrust_IO::exportable_summary(); ?> -

- -

+
+
+

+

+
+
render_preview_screen($preview); ?> -
-
-

- render_export_panel($exportable); ?> -
-
-

- render_import_panel(); ?> -
-
+ render_export_panel($exportable); ?> + render_import_panel(); ?> -
- - - -
- - - -
- -
- - $items): - $label = $this->cpt_label($cpt); - $count = count($items); - ?> -
- - - - () - -
    - -
  • - -
  • - -
-
- -
- - - -

- -

-
+
+
+

+

+
+
+
+ + + +
+
+ +

+
+
+
+ + +
+
+
+ +
+
+ +

+
+
+
+ $items): + $label = $this->cpt_label($cpt); + $count = count($items); + ?> +
+ + + +
    + +
  • + +
  • + +
+
+ +
+
+
+ +
+
+ +

+
+
+ + +
+
+ +
+
+

+

+
+ +
+
+
+
-
- - - -
- - +
+
+

+

+
+ +
+
+ +

+
-

-
- - - - -

- -
- - - - -
- -

- -

- +
+
+ + + +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ +

+
+
+
+ + + +
+
+
+ +
+
+

+

+
+ +
+
+
+
-

- -
-

-
    - -
  • - -
+ -
-
    - -
  • - -
-
- - - -

- -

- -

- -

- - $rows): ?> - -

cpt_label($cpt)); ?>

- - - - - - - - - - - - - - - +
+
+ +
    + +
  • -
-
- + +
+
-
- - - - - +
+
+

+

+
+ + + $rows): ?> + +
+
+

cpt_label($cpt)); ?>

+
+ + + + + + + + + + + + + + + + + +
+
+ - - + +
+
+ + + + + + +
+
+
Date: Mon, 11 May 2026 13:59:07 +0200 Subject: [PATCH 08/11] clean up legacy field renderers + drop wp-color-picker dep, regen POT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that every settings tab renders through ds_render_section_*, the old Settings API field plumbing is dead weight. Removes: - The `add_field()` helper and every `add_settings_section` / `add_settings_field` call from OpenTrust_Admin_Settings::register_settings() (~95 lines of registration). - The eight render_*_field methods + the shared render_input_field() and render_media_field() (~190 lines). One stays on the page in spirit — ds_row_* — but no consumer needs the legacy variants. - The wp-color-picker style enqueue and the 'wp-color-picker' dep on the admin.js script. Settings now uses the design system's native .opentrust-color widget; the only place that registered the jQuery picker had already migrated away in commit 3. - The dead `$('[data-ot-media-field]')` block in admin.js (logo/avatar uploads moved to the template's [data-opentrust-media-picker]). - The .ot-logo-upload / .ot-logo-preview CSS rules. CPT meta-box uploads use different classes (.ot-policy-attachment-*, .ot-upload-badge, .ot-upload-artifact) and remain untouched. Top-of-file docblock for class-opentrust-admin-settings.php updated to match the new model: no add_settings_section / add_settings_field; page is rendered manually with ds_render_section_*. `wp i18n make-pot` regenerated — captures every new translatable string from the design-system migration (mostly section labels, help copy, button labels, and notice text). Plain-permalinks notice (OpenTrust_Admin::render_plain_permalinks_notice) and the WP.org review prompt are intentionally left as WP-native `.notice` markup: they also appear on CPT edit screens which are out of design-system scope, so swapping to .opentrust-notice classes would leave them unstyled on those screens. PHPStan level 5 clean. Grep -rni 'pluginslug' returns 0 hits. --- assets/css/admin.css | 25 - assets/js/admin.js | 46 +- includes/class-opentrust-admin-settings.php | 309 +------ includes/class-opentrust-admin.php | 7 +- languages/opentrust.pot | 927 +++++++++++++------- 5 files changed, 612 insertions(+), 702 deletions(-) diff --git a/assets/css/admin.css b/assets/css/admin.css index 3826337..644cb3c 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -141,31 +141,6 @@ margin: 0; } -/* Logo upload */ -.ot-logo-upload { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; -} - -/* The logo is rendered on the dark hero/nav surface, so the admin preview - mirrors that background — otherwise a white-on-white logo vanishes. */ -.ot-logo-preview img { - border: 1px solid #d1d5db; - border-radius: 4px; - padding: 10px 14px; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), - #111827; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); -} - -.ot-logo-upload .description { - flex-basis: 100%; - margin: 4px 0 0; -} - /* Certification meta box */ .ot-meta-field { margin-bottom: 16px; diff --git a/assets/js/admin.js b/assets/js/admin.js index d0e5529..11eb5ab 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -170,48 +170,10 @@ $accentWarning.toggleClass('ot-accent-warning--override', this.checked); }); - // ── Media uploader (logo + avatar) ───────── - $('[data-ot-media-field]').each(function () { - var $field = $(this); - var $input = $field.find('[data-ot-media-input]'); - var $preview = $field.find('.ot-logo-preview'); - var $uploadBtn = $field.find('[data-ot-media-upload]'); - var $removeBtn = $field.find('[data-ot-media-remove]'); - var frame; - - $uploadBtn.on('click', function (e) { - e.preventDefault(); - - if (!frame) { - frame = wp.media({ - title: $uploadBtn.text(), - multiple: false, - library: { type: 'image' }, - button: { text: $uploadBtn.text() } - }); - - frame.on('select', function () { - var attachment = frame.state().get('selection').first().toJSON(); - var src = (attachment.sizes && attachment.sizes.medium) - ? attachment.sizes.medium.url - : attachment.url; - $input.val(attachment.id); - $preview.find('img').attr('src', src); - $preview.show(); - $removeBtn.show(); - }); - } - - frame.open(); - }); - - $removeBtn.on('click', function (e) { - e.preventDefault(); - $input.val('0'); - $preview.hide(); - $removeBtn.hide(); - }); - }); + // Settings page logo + avatar uploads moved to the design system's + // .opentrust-media component (template JS handles them via + // [data-opentrust-media-picker]). The legacy [data-ot-media-field] + // wiring lives on only inside the CPT meta boxes below. // ── Certification badge uploader ─────────── $('.ot-upload-badge').on('click', function (e) { diff --git a/includes/class-opentrust-admin-settings.php b/includes/class-opentrust-admin-settings.php index 37c76c8..9b7e546 100644 --- a/includes/class-opentrust-admin-settings.php +++ b/includes/class-opentrust-admin-settings.php @@ -1,13 +1,14 @@ 'array', 'sanitize_callback' => [$this, 'sanitize_settings'], 'default' => OpenTrust::defaults(), ]); - - // ── General tab ────────────────────────────────────────────── - add_settings_section( - 'opentrust_general', - __('General Settings', 'opentrust'), - fn() => null, - 'opentrust-settings-general' - ); - - $this->add_field('endpoint_slug', __('Endpoint Slug', 'opentrust'), 'render_text_field', 'opentrust_general', 'opentrust-settings-general', [ - 'description' => __('The URL path for your trust center (e.g., "trust-center" = yoursite.com/trust-center/).', 'opentrust'), - ]); - - $this->add_field('page_title', __('Page Title', 'opentrust'), 'render_text_field', 'opentrust_general', 'opentrust-settings-general'); - - $this->add_field('company_name', __('Company Name', 'opentrust'), 'render_text_field', 'opentrust_general', 'opentrust-settings-general'); - - $this->add_field('tagline', __('Tagline', 'opentrust'), 'render_textarea_field', 'opentrust_general', 'opentrust-settings-general', [ - 'description' => __('A short description displayed below the company name in the hero section.', 'opentrust'), - ]); - - // Branding section (General tab). - add_settings_section( - 'opentrust_branding', - __('Branding', 'opentrust'), - fn() => null, - 'opentrust-settings-general' - ); - - $this->add_field('logo_id', __('Logo', 'opentrust'), 'render_logo_field', 'opentrust_branding', 'opentrust-settings-general'); - $this->add_field('avatar_id', __('AI Avatar', 'opentrust'), 'render_avatar_field', 'opentrust_branding', 'opentrust-settings-general'); - - $this->add_field('accent_color', __('Accent Color', 'opentrust'), 'render_color_field', 'opentrust_branding', 'opentrust-settings-general', [ - 'description' => __('Used for buttons, links, and highlights. Choose a color that matches your brand.', 'opentrust'), - ]); - - $this->add_field('show_powered_by', __('Credit Link', 'opentrust'), 'render_show_powered_by_field', 'opentrust_branding', 'opentrust-settings-general'); - - // Sections visibility (General tab). - add_settings_section( - 'opentrust_sections', - __('Visible Sections', 'opentrust'), - fn() => print('

' . esc_html__('Choose which sections to display on the trust center.', 'opentrust') . '

'), - 'opentrust-settings-general' - ); - - $this->add_field('sections_visible', __('Sections', 'opentrust'), 'render_sections_field', 'opentrust_sections', 'opentrust-settings-general'); - - // ── Contact tab ────────────────────────────────────────────── - // Fields are optional — the frontend block renders only when at least one field below is populated. - add_settings_section( - 'opentrust_contact', - __('Get in touch', 'opentrust'), - fn() => print('

' . esc_html__('Publish a dark-accent "Get in touch" block on the trust center. Every field is optional — the block only appears if at least one is filled in.', 'opentrust') . '

'), - 'opentrust-settings-contact' - ); - - $this->add_field('company_description', __('Company Description', 'opentrust'), 'render_textarea_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('Two or three sentences describing what the company does. Rendered under the "Get in touch" section title.', 'opentrust'), - ]); - - $this->add_field('dpo_name', __('DPO Name', 'opentrust'), 'render_text_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('Data Protection Officer name. Required under GDPR for many organisations.', 'opentrust'), - ]); - - $this->add_field('dpo_email', __('DPO Email', 'opentrust'), 'render_email_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('Dedicated DPO mailbox. Rendered as a mailto link.', 'opentrust'), - ]); - - $this->add_field('security_email', __('Security Contact Email', 'opentrust'), 'render_email_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('For vulnerability reports and security questions. Often separate from the DPO.', 'opentrust'), - ]); - - $this->add_field('contact_form_url', __('Contact Form URL', 'opentrust'), 'render_url_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('Optional link to a gated contact form.', 'opentrust'), - ]); - - $this->add_field('contact_address', __('Mailing Address', 'opentrust'), 'render_textarea_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('Postal address for formal GDPR / legal notices.', 'opentrust'), - ]); - - $this->add_field('pgp_key_url', __('PGP Public Key URL', 'opentrust'), 'render_url_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('Optional link to your security team\'s PGP public key.', 'opentrust'), - ]); - - $this->add_field('company_registration', __('Company Registration Number', 'opentrust'), 'render_text_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('KvK (NL), Companies House (UK), Handelsregister (DE), EIN (US), or equivalent business registration.', 'opentrust'), - ]); - - $this->add_field('vat_number', __('VAT / Tax ID', 'opentrust'), 'render_text_field', 'opentrust_contact', 'opentrust-settings-contact', [ - 'description' => __('VAT number, sales-tax ID, or equivalent international tax identifier.', 'opentrust'), - ]); - - } - - private function add_field(string $key, string $title, string $callback, string $section, string $page = 'opentrust-settings-general', array $extra = []): void { - add_settings_field( - 'opentrust_' . $key, - $title, - [$this, $callback], - $page, - $section, - array_merge(['key' => $key], $extra) - ); - } - - // ────────────────────────────────────────────── - // Field renderers - // ────────────────────────────────────────────── - - public function render_text_field(array $args): void { - $this->render_input_field('text', $args); - } - - public function render_email_field(array $args): void { - $this->render_input_field('email', $args, ['autocomplete' => 'off']); - } - - public function render_url_field(array $args): void { - $this->render_input_field('url', $args, ['placeholder' => 'https://', 'autocomplete' => 'off']); - } - - public function render_textarea_field(array $args): void { - $this->render_input_field('textarea', $args); - } - - /** - * Shared renderer for the Settings API string-input field family. - * One unified path so escaping rules and id/name conventions can't drift - * between text/email/url/textarea variants. - * - * @param 'text'|'email'|'url'|'textarea' $type - * @param array{key:string, description?:string} $args - * @param array $extra_attrs - */ - private function render_input_field(string $type, array $args, array $extra_attrs = []): void { - $settings = OpenTrust::get_settings(); - $key = $args['key']; - $value = $settings[$key] ?? ''; - - $attr_html = ''; - foreach ($extra_attrs as $name => $val) { - $attr_html .= ' ' . $name . '="' . esc_attr($val) . '"'; - } - - if ($type === 'textarea') { - printf( - '', - esc_attr($key), - esc_textarea($value), - $attr_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute values escaped above; names are hardcoded - ); - } else { - printf( - '', - esc_attr($key), - esc_attr($value), - $attr_html, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute values escaped above; names are hardcoded - esc_attr($type) - ); - } - - if (!empty($args['description'])) { - printf('

%s

', esc_html($args['description'])); - } - } - - public function render_color_field(array $args): void { - $settings = OpenTrust::get_settings(); - $value = $settings['accent_color'] ?? '#2563EB'; - $force_exact = !empty($settings['accent_force_exact']); - printf( - '', - esc_attr($value) - ); - ?> - - %s

', esc_html($args['description'])); - } - } - - public function render_logo_field(array $args): void { - $this->render_media_field( - 'logo_id', - __('Select Logo', 'opentrust'), - __('Used in the hero and sticky nav. A white version is recommended — it sits on a dark background.', 'opentrust') - ); - } - - public function render_avatar_field(array $args): void { - $this->render_media_field( - 'avatar_id', - __('Select Avatar', 'opentrust'), - __('Square image used as the avatar on AI chat responses. Use a colored background with a light or dark favicon or logo on top.', 'opentrust') - ); - } - - private function render_media_field(string $key, string $button_label, string $description): void { - $settings = OpenTrust::get_settings(); - $media_id = (int) ($settings[$key] ?? 0); - $media_url = $media_id ? wp_get_attachment_image_url($media_id, 'medium') : ''; - ?> -
-
> - -
- - - -

-
- %s', - checked($checked, true, false), - esc_html__('Show a "Powered by OpenTrust" credit in the trust center footer.', 'opentrust') - ); - printf( - '

%s

', - esc_html__('Off by default. Public credits are opt-in.', 'opentrust') - ); - } - - public function render_sections_field(array $args): void { - $settings = OpenTrust::get_settings(); - $visible = $settings['sections_visible'] ?? []; - - $sections = [ - 'certifications' => __('Certifications & Compliance', 'opentrust'), - 'policies' => __('Policies', 'opentrust'), - 'subprocessors' => __('Subprocessors', 'opentrust'), - 'data_practices' => __('Data Practices', 'opentrust'), - 'faqs' => __('FAQs', 'opentrust'), - 'contact' => __('Contact & DPO', 'opentrust'), - ]; - - foreach ($sections as $key => $label) { - $checked = !empty($visible[$key]); - printf( - '', - esc_attr($key), - checked($checked, true, false), - esc_html($label) - ); - } } // ────────────────────────────────────────────── @@ -346,8 +63,6 @@ public function render_sections_field(array $args): void { // bypassing do_settings_sections(). The Settings API registration in // register_settings() (above) is what `settings_fields()` reads for the // nonce + option_page hidden inputs — that wiring stays unchanged. - // add_settings_section/field calls for migrated tabs remain registered - // but un-rendered; commit cleanup removes them once all tabs migrate. private function ds_render_section_general(): void { $settings = OpenTrust::get_settings(); diff --git a/includes/class-opentrust-admin.php b/includes/class-opentrust-admin.php index c75d860..9198d84 100644 --- a/includes/class-opentrust-admin.php +++ b/includes/class-opentrust-admin.php @@ -130,7 +130,6 @@ public function enqueue_assets(string $hook): void { return; } - wp_enqueue_style('wp-color-picker'); wp_enqueue_media(); wp_enqueue_style( @@ -140,10 +139,14 @@ public function enqueue_assets(string $hook): void { OPENTRUST_VERSION ); + // admin.js still drives CPT-meta-box uploads (badge, artifact, policy + // PDF) — keep jQuery for those handlers. wp-color-picker dropped: the + // settings page now uses the design system's native .opentrust-color + // widget; no other consumer remains. wp_enqueue_script( 'opentrust-admin', OPENTRUST_PLUGIN_URL . 'assets/js/admin.js', - ['wp-color-picker', 'jquery'], + ['jquery'], OPENTRUST_VERSION, true ); diff --git a/languages/opentrust.pot b/languages/opentrust.pot index fdda5af..a49aa37 100644 --- a/languages/opentrust.pot +++ b/languages/opentrust.pot @@ -9,13 +9,19 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-05-07T12:53:39+00:00\n" +"POT-Creation-Date: 2026-05-11T11:58:43+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: opentrust\n" #. Plugin Name of the plugin #: opentrust.php +#: includes/Admin/Settings.php:86 +#: includes/Admin/Settings.php:87 +#: includes/Admin/Settings.php:152 +#: includes/Admin/Settings.php:170 +#: includes/class-opentrust-admin-questions.php:91 +#: includes/class-opentrust-admin-settings.php:579 #: includes/class-opentrust-admin.php:56 #: includes/class-opentrust-admin.php:57 msgid "OpenTrust" @@ -41,468 +47,690 @@ msgstr "" msgid "https://plugins.ettic.nl" msgstr "" -#: includes/class-opentrust-admin-ai.php:70 -msgid "Heads up: citation fidelity is not guaranteed on your active provider." +#: includes/Admin/Footer.php:36 +msgid "OpenTrust by Ettic." +msgstr "" + +#: includes/Admin/Footer.php:37 +msgid "Focused, open-source WordPress plugins." +msgstr "" + +#: includes/Admin/Footer.php:41 +msgid "OpenTrust resources" +msgstr "" + +#: includes/Admin/Footer.php:42 +msgid "Docs" +msgstr "" + +#: includes/Admin/Footer.php:44 +msgid "GitHub" +msgstr "" + +#: includes/Admin/Footer.php:46 +msgid "Support" +msgstr "" + +#: includes/Admin/Footer.php:48 +msgid "Report a vulnerability" +msgstr "" + +#: includes/Admin/Footer.php:50 +msgid "Like OpenTrust? Leave a review and help us keep building open source." +msgstr "" + +#: includes/Admin/Footer.php:50 +#: includes/class-opentrust-admin-review.php:120 +msgid "Leave a review" +msgstr "" + +#: includes/Admin/Settings.php:65 +#: includes/class-opentrust-admin.php:67 +#: includes/class-opentrust-admin.php:68 +msgid "Settings" +msgstr "" + +#: includes/Admin/Settings.php:163 +#: includes/class-opentrust-admin-settings.php:593 +msgid "Discard" +msgstr "" + +#: includes/Admin/Settings.php:164 +#: includes/class-opentrust-admin-settings.php:594 +msgid "Save changes" +msgstr "" + +#: includes/Admin/Settings.php:171 +#: includes/class-opentrust-admin-settings.php:608 +msgid "Self-hosted, open-source trust center for security policies, subprocessors, certifications, and data practices." +msgstr "" + +#: includes/Admin/Settings.php:191 +msgid "Example controls" +msgstr "" + +#: includes/Admin/Settings.php:192 +msgid "One row per control type. Each demonstrates the markup, naming, and JS hooks expected by the design system." +msgstr "" + +#: includes/Admin/Settings.php:214 +msgid "Enabled" +msgstr "" + +#: includes/Admin/Settings.php:215 +msgid "Master on/off switch. Toggles render brand-blue when on." +msgstr "" + +#: includes/Admin/Settings.php:236 +#: includes/Admin/Settings.php:240 +msgid "Frequency" +msgstr "" + +#: includes/Admin/Settings.php:237 +msgid "Use a segmented control for small enumerable choices. Radio inputs under the hood." +msgstr "" + +#: includes/Admin/Settings.php:259 +msgid "Number input" +msgstr "" + +#: includes/Admin/Settings.php:260 +msgid "Use --num modifier to clamp width. Range constraints in HTML, validated again in sanitize." +msgstr "" + +#: includes/Admin/Settings.php:264 +msgid "minutes" +msgstr "" + +#: includes/Admin/Settings.php:276 +msgid "Display name" +msgstr "" + +#: includes/Admin/Settings.php:277 +msgid "data-counter + maxlength auto-renders a \"n / max\" counter below the input." +msgstr "" + +#: includes/Admin/Settings.php:290 +msgid "Auto" +msgstr "" + +#: includes/Admin/Settings.php:291 +msgid "Manual" +msgstr "" + +#: includes/Admin/Settings.php:292 +msgid "Off" +msgstr "" + +#: includes/Admin/Settings.php:297 +msgid "Mode" +msgstr "" + +#: includes/Admin/Settings.php:298 +msgid "Native
- +
@@ -236,8 +247,10 @@ public function render_page(): void { 1): - $base = add_query_arg($filters + ['page' => 'opentrust-questions'], admin_url('admin.php')); - $base = remove_query_arg('paged', $base); + $base = add_query_arg( + ['page' => 'opentrust-questions'] + $log_filter_params, + admin_url('admin.php') + ); ?>

-
+
@@ -310,7 +294,7 @@ private function ds_row_sections(array $visible): void {

-
+
$label): ?> @@ -552,7 +536,7 @@ public function render_settings_page(): void { $tc_url = home_url('/' . ($settings['endpoint_slug'] ?? OpenTrust::DEFAULT_ENDPOINT_SLUG) . '/'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab switch on admin settings page. $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'general'; - if (!in_array($tab, ['general', 'contact', 'ai', 'io'], true)) { + if (!in_array($tab, self::TABS, true)) { $tab = 'general'; } $base_url = admin_url('admin.php?page=opentrust'); @@ -580,32 +564,28 @@ public function render_settings_page(): void { v
-
- + +
0
- - - -
+
+
-

-

+
+

+

+
+ + → +
-
+
$items): $label = $this->cpt_label($cpt); @@ -173,7 +173,7 @@ private function render_import_panel(): void {

-
+

@@ -304,10 +304,18 @@ private function render_preview_screen(array $preview): void { - + __('Create', 'opentrust'), + 'update' => __('Update', 'opentrust'), + 'skip' => __('Skip', 'opentrust'), + ]; + foreach ($rows as $r): + $action = (string) $r['action']; + ?> - + @@ -317,16 +325,14 @@ private function render_preview_screen(array $preview): void { -
-
- - - - - - -
-
+
+ + + + + + +
' . esc_html__('Errors:', 'opentrust') . '
' . esc_html(implode('
', (array) $result['errors'])); + $msg .= '
' . esc_html__('Errors:', 'opentrust') . '
' . implode('
', array_map('esc_html', (array) $result['errors'])); } $this->bounce_notice($msg, !empty($result['errors']) ? 'error' : 'success'); diff --git a/includes/class-opentrust-admin.php b/includes/class-opentrust-admin.php index 91c646d..8b198e0 100644 --- a/includes/class-opentrust-admin.php +++ b/includes/class-opentrust-admin.php @@ -139,7 +139,7 @@ public function enqueue_assets(string $hook): void { if (!$needs_media && $screen->id === 'toplevel_page_opentrust') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab gate. $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'general'; - $needs_media = !in_array($tab, ['contact', 'ai', 'io'], true); + $needs_media = $tab === 'general'; } if ($needs_media) { wp_enqueue_media(); @@ -187,19 +187,27 @@ public function enqueue_assets(string $hook): void { ); } - // Localize the handful of admin strings that admin.js renders directly - // (e.g. wp.media modal titles). Catalog-screen strings are shipped - // separately below via window.OpenTrustCatalog. + // Localize strings rendered directly from JS (wp.media modal titles in + // admin.js, and the design system's tab-switch confirm modal in + // opentrust-admin.js). Catalog-screen strings ship below via + // window.OpenTrustCatalog. wp_add_inline_script( 'opentrust-admin', 'window.OpenTrustAdmin = ' . wp_json_encode([ 'i18n' => [ - 'selectBadgeImage' => __('Select Badge Image', 'opentrust'), - 'useAsBadge' => __('Use as Badge', 'opentrust'), - 'selectArtifact' => __('Select Proof Artifact', 'opentrust'), - 'useAsArtifact' => __('Use This File', 'opentrust'), - 'uploadArtifact' => __('Upload File', 'opentrust'), - 'replaceArtifact' => __('Replace File', 'opentrust'), + 'selectBadgeImage' => __('Select Badge Image', 'opentrust'), + 'useAsBadge' => __('Use as Badge', 'opentrust'), + 'selectArtifact' => __('Select Proof Artifact', 'opentrust'), + 'useAsArtifact' => __('Use This File', 'opentrust'), + 'uploadArtifact' => __('Upload File', 'opentrust'), + 'replaceArtifact' => __('Replace File', 'opentrust'), + 'tabSwitchTitle' => __('Discard unsaved changes?', 'opentrust'), + 'tabSwitchOneUnsaved' => __('You have 1 unsaved change on this tab. Switching tabs will discard it.', 'opentrust'), + /* translators: %d: number of unsaved changes on the current tab. */ + 'tabSwitchManyUnsaved' => __('You have %d unsaved changes on this tab. Switching tabs will discard them.', 'opentrust'), + 'tabSwitchBody' => __('Each tab saves independently. Save first to keep your changes, or discard them and switch.', 'opentrust'), + 'tabSwitchConfirm' => __('Discard and switch', 'opentrust'), + 'tabSwitchCancel' => __('Stay on this tab', 'opentrust'), ], ]) . ';', 'before' @@ -208,8 +216,7 @@ public function enqueue_assets(string $hook): void { // Catalog autofill: ship the bundled vendor / practice catalog only on // the new-post screen for the two CPTs that support it. Edit screens // are deliberately excluded so we never stomp existing values. - $screen = get_current_screen(); - if ($hook === 'post-new.php' && $screen && in_array($screen->post_type, ['ot_subprocessor', 'ot_data_practice', 'ot_certification'], true)) { + if ($hook === 'post-new.php' && in_array($screen->post_type, ['ot_subprocessor', 'ot_data_practice', 'ot_certification'], true)) { $payload = [ 'postType' => $screen->post_type, 'catalog' => OpenTrust_Catalog::for_js($screen->post_type), diff --git a/languages/opentrust.pot b/languages/opentrust.pot index a49aa37..069bd15 100644 --- a/languages/opentrust.pot +++ b/languages/opentrust.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-05-11T11:58:43+00:00\n" +"POT-Creation-Date: 2026-05-11T12:50:55+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: opentrust\n" @@ -20,8 +20,8 @@ msgstr "" #: includes/Admin/Settings.php:87 #: includes/Admin/Settings.php:152 #: includes/Admin/Settings.php:170 -#: includes/class-opentrust-admin-questions.php:91 -#: includes/class-opentrust-admin-settings.php:579 +#: includes/class-opentrust-admin-questions.php:100 +#: includes/class-opentrust-admin-settings.php:563 #: includes/class-opentrust-admin.php:56 #: includes/class-opentrust-admin.php:57 msgid "OpenTrust" @@ -91,17 +91,17 @@ msgid "Settings" msgstr "" #: includes/Admin/Settings.php:163 -#: includes/class-opentrust-admin-settings.php:593 +#: includes/class-opentrust-admin-settings.php:577 msgid "Discard" msgstr "" #: includes/Admin/Settings.php:164 -#: includes/class-opentrust-admin-settings.php:594 +#: includes/class-opentrust-admin-settings.php:578 msgid "Save changes" msgstr "" #: includes/Admin/Settings.php:171 -#: includes/class-opentrust-admin-settings.php:608 +#: includes/class-opentrust-admin-settings.php:592 msgid "Self-hosted, open-source trust center for security policies, subprocessors, certifications, and data practices." msgstr "" @@ -179,12 +179,12 @@ msgid "Native color swatch fused with a hex input. JS keeps them in sync and liv msgstr "" #: includes/Admin/Settings.php:326 -#: includes/class-opentrust-admin-settings.php:259 +#: includes/class-opentrust-admin-settings.php:243 msgid "Color picker" msgstr "" #: includes/Admin/Settings.php:342 -#: includes/class-opentrust-admin-settings.php:89 +#: includes/class-opentrust-admin-settings.php:73 msgid "Logo" msgstr "" @@ -193,12 +193,12 @@ msgid "Stores the attachment ID. JS binds wp.media to any [data-opentrust-media- msgstr "" #: includes/Admin/Settings.php:354 -#: includes/class-opentrust-admin-settings.php:233 +#: includes/class-opentrust-admin-settings.php:217 msgid "Replace" msgstr "" #: includes/Admin/Settings.php:355 -#: includes/class-opentrust-admin-settings.php:234 +#: includes/class-opentrust-admin-settings.php:218 #: includes/class-opentrust-cpt.php:391 #: includes/class-opentrust-cpt.php:403 #: includes/class-opentrust-cpt.php:534 @@ -221,516 +221,516 @@ msgid "Run now" msgstr "" #. translators: %s: provider label, e.g. OpenAI -#: includes/class-opentrust-admin-ai.php:66 +#: includes/class-opentrust-admin-ai.php:57 #, php-format msgid "You are currently using %s. Only Anthropic uses a structural Citations API — every other provider relies on prompted citation tags the model can ignore or fabricate. For a published trust center, switch to Anthropic below." msgstr "" -#: includes/class-opentrust-admin-ai.php:72 +#: includes/class-opentrust-admin-ai.php:63 msgid "Heads up: citation fidelity is not guaranteed on your active provider." msgstr "" -#: includes/class-opentrust-admin-ai.php:80 +#: includes/class-opentrust-admin-ai.php:71 msgid "Citation-backed AI assistant" msgstr "" -#: includes/class-opentrust-admin-ai.php:84 +#: includes/class-opentrust-admin-ai.php:75 msgid "OpenTrust uses Anthropic Claude with the native Citations API to answer visitor questions about your trust center. Every claim the assistant makes is tied to an exact quote from one of your published documents — no policy text is invented, nothing is paraphrased into something you did not publish." msgstr "" -#: includes/class-opentrust-admin-ai.php:92 +#: includes/class-opentrust-admin-ai.php:83 msgid "Why Anthropic, and not OpenAI or another provider?" msgstr "" -#: includes/class-opentrust-admin-ai.php:97 +#: includes/class-opentrust-admin-ai.php:88 msgid "A trust center is a compliance surface. If the assistant invents a security commitment you never made, that is not a UX papercut — it is a misrepresentation of your security posture, and your customers and auditors will hold you to it." msgstr "" -#: includes/class-opentrust-admin-ai.php:105 +#: includes/class-opentrust-admin-ai.php:96 msgid "Anthropic is the only major provider that exposes a structural Citations API. Documents are sent as typed blocks and the model emits citations as first-class events containing the exact source document and the exact quoted text. The model literally cannot return a citation for text that is not in your source documents." msgstr "" -#: includes/class-opentrust-admin-ai.php:111 +#: includes/class-opentrust-admin-ai.php:102 msgid "Every other provider (including OpenAI and any model accessed via OpenRouter) relies on prompted citation tags that we parse out of the answer after the fact. That works most of the time, but the model can ignore the instructions, make up document IDs, or attach a citation to a sentence it actually hallucinated. We support these providers as an escape hatch for organisations that cannot use Anthropic for procurement or data-residency reasons — but we very strongly recommend you do not run a public trust center on them." msgstr "" #. translators: %d is the number of policies missing AI summaries. -#: includes/class-opentrust-admin-ai.php:164 +#: includes/class-opentrust-admin-ai.php:155 #, php-format msgid "%d policy is missing an AI summary." msgid_plural "%d policies are missing AI summaries." msgstr[0] "" msgstr[1] "" -#: includes/class-opentrust-admin-ai.php:176 +#: includes/class-opentrust-admin-ai.php:167 msgid "Generate them now so the assistant can route questions accurately." msgstr "" -#: includes/class-opentrust-admin-ai.php:180 +#: includes/class-opentrust-admin-ai.php:171 msgid "Generate now" msgstr "" -#: includes/class-opentrust-admin-ai.php:208 +#: includes/class-opentrust-admin-ai.php:199 msgid "Connect a provider" msgstr "" -#: includes/class-opentrust-admin-ai.php:209 +#: includes/class-opentrust-admin-ai.php:200 msgid "Anthropic is not registered. Pick any available provider to continue." msgstr "" -#: includes/class-opentrust-admin-ai.php:225 +#: includes/class-opentrust-admin-ai.php:216 msgid "Step 1 — Connect Anthropic" msgstr "" -#: includes/class-opentrust-admin-ai.php:226 +#: includes/class-opentrust-admin-ai.php:217 msgid "Paste your Anthropic API key. We validate it on save and cache the model list for routing." msgstr "" -#: includes/class-opentrust-admin-ai.php:232 +#: includes/class-opentrust-admin-ai.php:223 msgid "Advanced: use a different provider (not recommended)" msgstr "" -#: includes/class-opentrust-admin-ai.php:236 +#: includes/class-opentrust-admin-ai.php:227 msgid "These providers cannot guarantee citation fidelity." msgstr "" -#: includes/class-opentrust-admin-ai.php:237 +#: includes/class-opentrust-admin-ai.php:228 msgid "OpenAI and OpenRouter rely on prompted [[cite:document-id]] tags that we parse out of the answer after generation. The model can ignore the instruction, invent document IDs, or attach a citation to a sentence it actually hallucinated. We cannot detect when this happens." msgstr "" -#: includes/class-opentrust-admin-ai.php:239 +#: includes/class-opentrust-admin-ai.php:230 msgid "Do not use these providers for a published trust center" msgstr "" -#: includes/class-opentrust-admin-ai.php:240 +#: includes/class-opentrust-admin-ai.php:231 msgid "unless your organisation genuinely cannot use Anthropic for procurement, contractual, or data-residency reasons. Inaccurate claims about your security posture are a real compliance risk." msgstr "" -#: includes/class-opentrust-admin-ai.php:281 +#: includes/class-opentrust-admin-ai.php:272 msgid "Required for citation fidelity" msgstr "" -#: includes/class-opentrust-admin-ai.php:287 +#: includes/class-opentrust-admin-ai.php:278 msgid "Uses Claude with the native Citations API. Every quote the assistant attributes to one of your documents is structurally guaranteed to come from that document." msgstr "" #. translators: %s: provider name (e.g. Anthropic) -#: includes/class-opentrust-admin-ai.php:295 +#: includes/class-opentrust-admin-ai.php:286 #, php-format msgid "Get a %s API key" msgstr "" -#: includes/class-opentrust-admin-ai.php:311 +#: includes/class-opentrust-admin-ai.php:302 msgid "Remove the saved key for this provider?" msgstr "" -#: includes/class-opentrust-admin-ai.php:312 +#: includes/class-opentrust-admin-ai.php:303 msgid "Replace key" msgstr "" #. translators: %s: provider name (e.g. Anthropic) -#: includes/class-opentrust-admin-ai.php:322 +#: includes/class-opentrust-admin-ai.php:313 #, php-format msgid "Paste your %s API key…" msgstr "" -#: includes/class-opentrust-admin-ai.php:326 +#: includes/class-opentrust-admin-ai.php:317 msgid "Validate & save" msgstr "" -#: includes/class-opentrust-admin-ai.php:355 +#: includes/class-opentrust-admin-ai.php:346 msgid "Step 2 — Model & defaults" msgstr "" -#: includes/class-opentrust-admin-ai.php:356 +#: includes/class-opentrust-admin-ai.php:347 msgid "Pick a model and tune budgets, rate limits, and visitor-facing behavior." msgstr "" -#: includes/class-opentrust-admin-ai.php:361 +#: includes/class-opentrust-admin-ai.php:352 msgid "Active model" msgstr "" #. translators: %s: human-readable time difference (e.g. "5 minutes") -#: includes/class-opentrust-admin-ai.php:366 +#: includes/class-opentrust-admin-ai.php:357 #, php-format msgid "Model list cached %s ago." msgstr "" -#: includes/class-opentrust-admin-ai.php:375 +#: includes/class-opentrust-admin-ai.php:366 msgid "No cached models found. Use Refresh to re-fetch the model list." msgstr "" -#: includes/class-opentrust-admin-ai.php:384 +#: includes/class-opentrust-admin-ai.php:375 msgid "Recommended" msgstr "" -#: includes/class-opentrust-admin-ai.php:392 +#: includes/class-opentrust-admin-ai.php:383 msgid "Refresh models" msgstr "" -#: includes/class-opentrust-admin-ai.php:398 +#: includes/class-opentrust-admin-ai.php:389 msgid "Daily token budget" msgstr "" -#: includes/class-opentrust-admin-ai.php:398 -#: includes/class-opentrust-admin-ai.php:400 +#: includes/class-opentrust-admin-ai.php:389 +#: includes/class-opentrust-admin-ai.php:391 msgid "tokens" msgstr "" -#: includes/class-opentrust-admin-ai.php:398 +#: includes/class-opentrust-admin-ai.php:389 msgid "Hard cap per site per day. Default 500,000 (~$12/day at Sonnet 4.5 rates)." msgstr "" -#: includes/class-opentrust-admin-ai.php:400 +#: includes/class-opentrust-admin-ai.php:391 msgid "Monthly token budget" msgstr "" -#: includes/class-opentrust-admin-ai.php:400 +#: includes/class-opentrust-admin-ai.php:391 msgid "Hard cap per site per month. Default 10,000,000." msgstr "" -#: includes/class-opentrust-admin-ai.php:402 +#: includes/class-opentrust-admin-ai.php:393 msgid "Rate limit — per IP" msgstr "" -#: includes/class-opentrust-admin-ai.php:402 +#: includes/class-opentrust-admin-ai.php:393 msgid "messages per minute" msgstr "" -#: includes/class-opentrust-admin-ai.php:404 +#: includes/class-opentrust-admin-ai.php:395 msgid "Rate limit — per session" msgstr "" -#: includes/class-opentrust-admin-ai.php:404 +#: includes/class-opentrust-admin-ai.php:395 msgid "messages per hour" msgstr "" -#: includes/class-opentrust-admin-ai.php:406 +#: includes/class-opentrust-admin-ai.php:397 msgid "Max message length" msgstr "" -#: includes/class-opentrust-admin-ai.php:406 +#: includes/class-opentrust-admin-ai.php:397 msgid "characters" msgstr "" -#: includes/class-opentrust-admin-ai.php:410 +#: includes/class-opentrust-admin-ai.php:401 msgid "Refuse-to-answer contact URL" msgstr "" -#: includes/class-opentrust-admin-ai.php:411 +#: includes/class-opentrust-admin-ai.php:402 msgid "When the AI cannot confidently answer, it links here. Leave blank to use the trust center home." msgstr "" -#: includes/class-opentrust-admin-ai.php:418 +#: includes/class-opentrust-admin-ai.php:409 msgid "Visitor display" msgstr "" -#: includes/class-opentrust-admin-ai.php:418 +#: includes/class-opentrust-admin-ai.php:409 msgid "Show the active model name under the chat input." msgstr "" -#: includes/class-opentrust-admin-ai.php:420 +#: includes/class-opentrust-admin-ai.php:411 msgid "Analytics logging" msgstr "" -#: includes/class-opentrust-admin-ai.php:420 +#: includes/class-opentrust-admin-ai.php:411 msgid "Log anonymised visitor questions for admin review (90-day auto-purge, no PII)." msgstr "" -#: includes/class-opentrust-admin-ai.php:422 +#: includes/class-opentrust-admin-ai.php:413 msgid "Improve answer quality" msgstr "" -#: includes/class-opentrust-admin-ai.php:422 +#: includes/class-opentrust-admin-ai.php:413 msgid "Auto-generate a 2–3 sentence AI summary of each policy for routing. Cost ~$0.05–$0.10 per 50 policies lifetime; pennies per edit afterward. Uses your configured AI key." msgstr "" -#: includes/class-opentrust-admin-ai.php:429 +#: includes/class-opentrust-admin-ai.php:420 msgid "Oversized policies" msgstr "" -#: includes/class-opentrust-admin-ai.php:430 +#: includes/class-opentrust-admin-ai.php:421 msgid "The following policies will be truncated when retrieved by the AI. Consider splitting them into shorter documents:" msgstr "" #. translators: 1: policy title, 2: token count. -#: includes/class-opentrust-admin-ai.php:436 +#: includes/class-opentrust-admin-ai.php:427 #, php-format msgid "%1$s (~%2$s tokens)" msgstr "" -#: includes/class-opentrust-admin-ai.php:449 +#: includes/class-opentrust-admin-ai.php:440 msgid "Anti-abuse — Cloudflare Turnstile" msgstr "" -#: includes/class-opentrust-admin-ai.php:450 +#: includes/class-opentrust-admin-ai.php:441 msgid "Optional. Turnstile challenges suspicious visitors on their first chat message of a session. Requires a free Cloudflare account." msgstr "" -#: includes/class-opentrust-admin-ai.php:453 +#: includes/class-opentrust-admin-ai.php:444 msgid "Enable Turnstile for chat" msgstr "" -#: includes/class-opentrust-admin-ai.php:453 +#: includes/class-opentrust-admin-ai.php:444 msgid "Require Turnstile verification on first chat message." msgstr "" -#: includes/class-opentrust-admin-ai.php:457 +#: includes/class-opentrust-admin-ai.php:448 msgid "Turnstile Site Key" msgstr "" -#: includes/class-opentrust-admin-ai.php:458 +#: includes/class-opentrust-admin-ai.php:449 msgid "Public site key from your Cloudflare Turnstile widget." msgstr "" -#: includes/class-opentrust-admin-ai.php:467 +#: includes/class-opentrust-admin-ai.php:458 msgid "Turnstile Secret Key" msgstr "" -#: includes/class-opentrust-admin-ai.php:468 +#: includes/class-opentrust-admin-ai.php:459 msgid "Stored server-side, encrypted via libsodium. Never exposed to the frontend." msgstr "" -#: includes/class-opentrust-admin-ai.php:471 +#: includes/class-opentrust-admin-ai.php:462 msgid "Enter secret key…" msgstr "" -#: includes/class-opentrust-admin-ai.php:475 +#: includes/class-opentrust-admin-ai.php:466 msgid "Key saved" msgstr "" -#: includes/class-opentrust-admin-ai.php:555 -#: includes/class-opentrust-admin-ai.php:620 -#: includes/class-opentrust-admin-ai.php:657 -#: includes/class-opentrust-admin-ai.php:708 -#: includes/class-opentrust-admin-questions.php:289 -#: includes/class-opentrust-admin-questions.php:330 -#: includes/class-opentrust-admin-questions.php:347 -#: includes/class-opentrust-admin-tools.php:502 +#: includes/class-opentrust-admin-ai.php:546 +#: includes/class-opentrust-admin-ai.php:611 +#: includes/class-opentrust-admin-ai.php:648 +#: includes/class-opentrust-admin-ai.php:699 +#: includes/class-opentrust-admin-questions.php:300 +#: includes/class-opentrust-admin-questions.php:341 +#: includes/class-opentrust-admin-questions.php:358 +#: includes/class-opentrust-admin-tools.php:510 msgid "You do not have permission to perform this action." msgstr "" -#: includes/class-opentrust-admin-ai.php:564 -#: includes/class-opentrust-admin-ai.php:628 -#: includes/class-opentrust-admin-ai.php:664 +#: includes/class-opentrust-admin-ai.php:555 +#: includes/class-opentrust-admin-ai.php:619 +#: includes/class-opentrust-admin-ai.php:655 msgid "Unknown provider." msgstr "" -#: includes/class-opentrust-admin-ai.php:568 +#: includes/class-opentrust-admin-ai.php:559 msgid "API key cannot be empty." msgstr "" -#: includes/class-opentrust-admin-ai.php:575 +#: includes/class-opentrust-admin-ai.php:566 msgid "Validation failed." msgstr "" #. translators: 1: provider label, 2: provider error message -#: includes/class-opentrust-admin-ai.php:577 +#: includes/class-opentrust-admin-ai.php:568 #, php-format msgid "%1$s rejected the key: %2$s" msgstr "" #. translators: 1: provider label, 2: number of models -#: includes/class-opentrust-admin-ai.php:613 +#: includes/class-opentrust-admin-ai.php:604 #, php-format msgid "%1$s key validated. Found %2$d model(s)." msgstr "" -#: includes/class-opentrust-admin-ai.php:651 +#: includes/class-opentrust-admin-ai.php:642 msgid "Key removed." msgstr "" -#: includes/class-opentrust-admin-ai.php:670 +#: includes/class-opentrust-admin-ai.php:661 msgid "No key on file for this provider." msgstr "" -#: includes/class-opentrust-admin-ai.php:676 +#: includes/class-opentrust-admin-ai.php:667 msgid "Refresh failed." msgstr "" #. translators: %s: error message from the provider -#: includes/class-opentrust-admin-ai.php:678 +#: includes/class-opentrust-admin-ai.php:669 #, php-format msgid "Refresh failed: %s" msgstr "" #. translators: %d: number of models -#: includes/class-opentrust-admin-ai.php:694 +#: includes/class-opentrust-admin-ai.php:685 #, php-format msgid "Model list refreshed. Found %d model(s)." msgstr "" #. translators: %d is the number of policies enqueued for summary generation. -#: includes/class-opentrust-admin-ai.php:724 +#: includes/class-opentrust-admin-ai.php:715 #, php-format msgid "Queued %d policy for AI summary generation. Summaries will appear over the next minute." msgid_plural "Queued %d policies for AI summary generation. Summaries will appear over the next few minutes." msgstr[0] "" msgstr[1] "" -#: includes/class-opentrust-admin-ai.php:732 +#: includes/class-opentrust-admin-ai.php:723 msgid "All policies already have up-to-date AI summaries." msgstr "" -#: includes/class-opentrust-admin-questions.php:97 +#: includes/class-opentrust-admin-questions.php:106 msgid "AI settings" msgstr "" -#: includes/class-opentrust-admin-questions.php:100 -#: includes/class-opentrust-admin-settings.php:591 -#: includes/class-opentrust-admin-settings.php:599 +#: includes/class-opentrust-admin-questions.php:109 +#: includes/class-opentrust-admin-settings.php:575 +#: includes/class-opentrust-admin-settings.php:583 msgid "View Trust Center" msgstr "" -#: includes/class-opentrust-admin-questions.php:107 +#: includes/class-opentrust-admin-questions.php:116 msgid "AI Questions" msgstr "" -#: includes/class-opentrust-admin-questions.php:108 +#: includes/class-opentrust-admin-questions.php:117 msgid "Questions visitors have asked your trust center chat. Identifiers are hashed; rows auto-purge after 90 days." msgstr "" -#: includes/class-opentrust-admin-questions.php:130 +#: includes/class-opentrust-admin-questions.php:139 msgid "Logging is ON" msgstr "" -#: includes/class-opentrust-admin-questions.php:131 +#: includes/class-opentrust-admin-questions.php:140 msgid "Logging is OFF" msgstr "" #. translators: %d: number of questions -#: includes/class-opentrust-admin-questions.php:137 +#: includes/class-opentrust-admin-questions.php:146 #, php-format msgid "%d question logged in the last 90 days." msgid_plural "%d questions logged in the last 90 days." msgstr[0] "" msgstr[1] "" -#: includes/class-opentrust-admin-questions.php:142 +#: includes/class-opentrust-admin-questions.php:151 msgid "Toggle visitor question logging?" msgstr "" -#: includes/class-opentrust-admin-questions.php:143 +#: includes/class-opentrust-admin-questions.php:152 msgid "Disable logging" msgstr "" -#: includes/class-opentrust-admin-questions.php:143 +#: includes/class-opentrust-admin-questions.php:152 msgid "Enable logging" msgstr "" -#: includes/class-opentrust-admin-questions.php:151 +#: includes/class-opentrust-admin-questions.php:160 msgid "Filter & export" msgstr "" -#: includes/class-opentrust-admin-questions.php:157 +#: includes/class-opentrust-admin-questions.php:166 msgid "Search" msgstr "" -#: includes/class-opentrust-admin-questions.php:158 +#: includes/class-opentrust-admin-questions.php:167 msgid "Search questions…" msgstr "" -#: includes/class-opentrust-admin-questions.php:161 -#: includes/class-opentrust-admin-questions.php:207 +#: includes/class-opentrust-admin-questions.php:170 +#: includes/class-opentrust-admin-questions.php:216 msgid "Model" msgstr "" -#: includes/class-opentrust-admin-questions.php:164 +#: includes/class-opentrust-admin-questions.php:173 msgid "Any" msgstr "" -#: includes/class-opentrust-admin-questions.php:172 +#: includes/class-opentrust-admin-questions.php:181 msgid "From" msgstr "" -#: includes/class-opentrust-admin-questions.php:176 +#: includes/class-opentrust-admin-questions.php:185 msgid "To" msgstr "" -#: includes/class-opentrust-admin-questions.php:180 +#: includes/class-opentrust-admin-questions.php:189 msgid "Apply" msgstr "" -#: includes/class-opentrust-admin-questions.php:181 +#: includes/class-opentrust-admin-questions.php:190 msgid "Reset" msgstr "" -#: includes/class-opentrust-admin-questions.php:182 +#: includes/class-opentrust-admin-questions.php:191 msgid "Download CSV" msgstr "" -#: includes/class-opentrust-admin-questions.php:190 +#: includes/class-opentrust-admin-questions.php:199 msgid "Log" msgstr "" #. translators: %d: total rows -#: includes/class-opentrust-admin-questions.php:194 +#: includes/class-opentrust-admin-questions.php:203 #, php-format msgid "%d total" msgstr "" -#: includes/class-opentrust-admin-questions.php:197 +#: includes/class-opentrust-admin-questions.php:206 msgid "most recent first; refused answers are highlighted" msgstr "" -#: includes/class-opentrust-admin-questions.php:205 +#: includes/class-opentrust-admin-questions.php:214 #: includes/class-opentrust-version.php:161 msgid "Date" msgstr "" -#: includes/class-opentrust-admin-questions.php:206 +#: includes/class-opentrust-admin-questions.php:215 msgid "Question" msgstr "" -#: includes/class-opentrust-admin-questions.php:208 +#: includes/class-opentrust-admin-questions.php:217 msgid "Cites" msgstr "" -#: includes/class-opentrust-admin-questions.php:209 +#: includes/class-opentrust-admin-questions.php:218 msgid "Tokens" msgstr "" -#: includes/class-opentrust-admin-questions.php:210 +#: includes/class-opentrust-admin-questions.php:219 msgid "Latency" msgstr "" -#: includes/class-opentrust-admin-questions.php:216 +#: includes/class-opentrust-admin-questions.php:225 msgid "No questions logged yet." msgstr "" -#: includes/class-opentrust-admin-questions.php:224 +#: includes/class-opentrust-admin-questions.php:233 msgid "Refused" msgstr "" -#: includes/class-opentrust-admin-questions.php:261 +#: includes/class-opentrust-admin-questions.php:272 msgid "Danger zone" msgstr "" -#: includes/class-opentrust-admin-questions.php:266 +#: includes/class-opentrust-admin-questions.php:277 msgid "Clear entire question log" msgstr "" -#: includes/class-opentrust-admin-questions.php:267 +#: includes/class-opentrust-admin-questions.php:278 msgid "Permanently deletes every logged question. Cannot be undone." msgstr "" -#: includes/class-opentrust-admin-questions.php:270 +#: includes/class-opentrust-admin-questions.php:281 msgid "Permanently delete all logged questions? This cannot be undone." msgstr "" -#: includes/class-opentrust-admin-questions.php:271 +#: includes/class-opentrust-admin-questions.php:282 msgid "Clear log" msgstr "" -#: includes/class-opentrust-admin-questions.php:338 +#: includes/class-opentrust-admin-questions.php:349 msgid "Question log cleared." msgstr "" -#: includes/class-opentrust-admin-questions.php:357 +#: includes/class-opentrust-admin-questions.php:368 msgid "Logging enabled." msgstr "" -#: includes/class-opentrust-admin-questions.php:357 +#: includes/class-opentrust-admin-questions.php:368 msgid "Logging disabled." msgstr "" @@ -764,198 +764,198 @@ msgstr "" msgid "You do not have permission to dismiss this notice." msgstr "" -#: includes/class-opentrust-admin-settings.php:72 -#: includes/class-opentrust-admin-settings.php:565 +#: includes/class-opentrust-admin-settings.php:56 +#: includes/class-opentrust-admin-settings.php:549 #: includes/class-opentrust-render.php:516 msgid "General" msgstr "" -#: includes/class-opentrust-admin-settings.php:73 +#: includes/class-opentrust-admin-settings.php:57 msgid "Endpoint, page title, and company identity." msgstr "" -#: includes/class-opentrust-admin-settings.php:76 +#: includes/class-opentrust-admin-settings.php:60 msgid "Endpoint Slug" msgstr "" -#: includes/class-opentrust-admin-settings.php:76 +#: includes/class-opentrust-admin-settings.php:60 msgid "The URL path for your trust center (e.g. \"trust-center\" = yoursite.com/trust-center/)." msgstr "" -#: includes/class-opentrust-admin-settings.php:77 +#: includes/class-opentrust-admin-settings.php:61 msgid "Page Title" msgstr "" -#: includes/class-opentrust-admin-settings.php:78 +#: includes/class-opentrust-admin-settings.php:62 msgid "Company Name" msgstr "" -#: includes/class-opentrust-admin-settings.php:79 +#: includes/class-opentrust-admin-settings.php:63 msgid "Tagline" msgstr "" -#: includes/class-opentrust-admin-settings.php:79 +#: includes/class-opentrust-admin-settings.php:63 msgid "A short description displayed below the company name in the hero." msgstr "" -#: includes/class-opentrust-admin-settings.php:85 +#: includes/class-opentrust-admin-settings.php:69 msgid "Branding" msgstr "" -#: includes/class-opentrust-admin-settings.php:86 +#: includes/class-opentrust-admin-settings.php:70 msgid "Logo, AI avatar, accent color, and credit link." msgstr "" -#: includes/class-opentrust-admin-settings.php:89 +#: includes/class-opentrust-admin-settings.php:73 msgid "Used in the hero and sticky nav. A white version is recommended — it sits on a dark background." msgstr "" -#: includes/class-opentrust-admin-settings.php:90 +#: includes/class-opentrust-admin-settings.php:74 msgid "AI Avatar" msgstr "" -#: includes/class-opentrust-admin-settings.php:90 +#: includes/class-opentrust-admin-settings.php:74 msgid "Square image used as the avatar on AI chat responses. A colored background with a light/dark mark on top works well." msgstr "" -#: includes/class-opentrust-admin-settings.php:92 +#: includes/class-opentrust-admin-settings.php:76 msgid "Credit Link" msgstr "" -#: includes/class-opentrust-admin-settings.php:92 +#: includes/class-opentrust-admin-settings.php:76 msgid "Show a \"Powered by OpenTrust\" credit in the trust center footer. Off by default — public credits are opt-in." msgstr "" -#: includes/class-opentrust-admin-settings.php:98 +#: includes/class-opentrust-admin-settings.php:82 msgid "Visible Sections" msgstr "" -#: includes/class-opentrust-admin-settings.php:99 +#: includes/class-opentrust-admin-settings.php:83 msgid "Choose which sections appear on the trust center." msgstr "" -#: includes/class-opentrust-admin-settings.php:113 +#: includes/class-opentrust-admin-settings.php:97 #: templates/partials/contact.php:111 msgid "Get in touch" msgstr "" -#: includes/class-opentrust-admin-settings.php:114 +#: includes/class-opentrust-admin-settings.php:98 msgid "Publish a dark-accent \"Get in touch\" block on the trust center. Every field is optional — the block only appears if at least one is filled in." msgstr "" -#: includes/class-opentrust-admin-settings.php:117 +#: includes/class-opentrust-admin-settings.php:101 msgid "Company Description" msgstr "" -#: includes/class-opentrust-admin-settings.php:117 +#: includes/class-opentrust-admin-settings.php:101 msgid "Two or three sentences describing what the company does. Rendered under the \"Get in touch\" section title." msgstr "" -#: includes/class-opentrust-admin-settings.php:118 +#: includes/class-opentrust-admin-settings.php:102 msgid "DPO Name" msgstr "" -#: includes/class-opentrust-admin-settings.php:118 +#: includes/class-opentrust-admin-settings.php:102 msgid "Data Protection Officer name. Required under GDPR for many organisations." msgstr "" -#: includes/class-opentrust-admin-settings.php:119 +#: includes/class-opentrust-admin-settings.php:103 msgid "DPO Email" msgstr "" -#: includes/class-opentrust-admin-settings.php:119 +#: includes/class-opentrust-admin-settings.php:103 msgid "Dedicated DPO mailbox. Rendered as a mailto link." msgstr "" -#: includes/class-opentrust-admin-settings.php:120 +#: includes/class-opentrust-admin-settings.php:104 msgid "Security Contact Email" msgstr "" -#: includes/class-opentrust-admin-settings.php:120 +#: includes/class-opentrust-admin-settings.php:104 msgid "For vulnerability reports and security questions. Often separate from the DPO." msgstr "" -#: includes/class-opentrust-admin-settings.php:121 +#: includes/class-opentrust-admin-settings.php:105 msgid "Contact Form URL" msgstr "" -#: includes/class-opentrust-admin-settings.php:121 +#: includes/class-opentrust-admin-settings.php:105 msgid "Optional link to a gated contact form." msgstr "" -#: includes/class-opentrust-admin-settings.php:122 +#: includes/class-opentrust-admin-settings.php:106 #: templates/partials/contact.php:84 msgid "Mailing Address" msgstr "" -#: includes/class-opentrust-admin-settings.php:122 +#: includes/class-opentrust-admin-settings.php:106 msgid "Postal address for formal GDPR / legal notices." msgstr "" -#: includes/class-opentrust-admin-settings.php:123 +#: includes/class-opentrust-admin-settings.php:107 msgid "PGP Public Key URL" msgstr "" -#: includes/class-opentrust-admin-settings.php:123 +#: includes/class-opentrust-admin-settings.php:107 msgid "Optional link to your security team's PGP public key." msgstr "" -#: includes/class-opentrust-admin-settings.php:124 +#: includes/class-opentrust-admin-settings.php:108 msgid "Company Registration Number" msgstr "" -#: includes/class-opentrust-admin-settings.php:124 +#: includes/class-opentrust-admin-settings.php:108 msgid "KvK (NL), Companies House (UK), Handelsregister (DE), EIN (US), or equivalent business registration." msgstr "" -#: includes/class-opentrust-admin-settings.php:125 +#: includes/class-opentrust-admin-settings.php:109 #: templates/partials/contact.php:100 msgid "VAT / Tax ID" msgstr "" -#: includes/class-opentrust-admin-settings.php:125 +#: includes/class-opentrust-admin-settings.php:109 msgid "VAT number, sales-tax ID, or equivalent international tax identifier." msgstr "" -#: includes/class-opentrust-admin-settings.php:254 +#: includes/class-opentrust-admin-settings.php:238 msgid "Accent Color" msgstr "" -#: includes/class-opentrust-admin-settings.php:255 +#: includes/class-opentrust-admin-settings.php:239 msgid "Used for buttons, links, and highlights. Choose a color that matches your brand." msgstr "" -#: includes/class-opentrust-admin-settings.php:265 +#: includes/class-opentrust-admin-settings.php:249 msgid "Low contrast on white backgrounds" msgstr "" -#: includes/class-opentrust-admin-settings.php:266 +#: includes/class-opentrust-admin-settings.php:250 msgid "Using your exact color on white backgrounds" msgstr "" -#: includes/class-opentrust-admin-settings.php:269 +#: includes/class-opentrust-admin-settings.php:253 msgid "Your chosen color is too light for buttons, links, and borders on white sections. On those surfaces OpenTrust will use a darker, on-brand variant:" msgstr "" -#: includes/class-opentrust-admin-settings.php:272 +#: includes/class-opentrust-admin-settings.php:256 msgid "You've chosen to keep your exact color on white backgrounds. Buttons, links, and borders in those sections may be hard to read." msgstr "" -#: includes/class-opentrust-admin-settings.php:284 +#: includes/class-opentrust-admin-settings.php:268 msgid "The hero and navigation still use your exact color." msgstr "" -#: includes/class-opentrust-admin-settings.php:289 +#: includes/class-opentrust-admin-settings.php:273 msgid "Use my exact color anyway — skip the contrast adjustment." msgstr "" -#: includes/class-opentrust-admin-settings.php:300 +#: includes/class-opentrust-admin-settings.php:284 #: templates/partials/certifications.php:22 msgid "Certifications & Compliance" msgstr "" -#: includes/class-opentrust-admin-settings.php:301 -#: includes/class-opentrust-admin-tools.php:529 +#: includes/class-opentrust-admin-settings.php:285 +#: includes/class-opentrust-admin-tools.php:537 #: includes/class-opentrust-cpt.php:151 #: includes/class-opentrust-cpt.php:161 #: templates/chat.php:45 @@ -964,8 +964,8 @@ msgstr "" msgid "Policies" msgstr "" -#: includes/class-opentrust-admin-settings.php:302 -#: includes/class-opentrust-admin-tools.php:531 +#: includes/class-opentrust-admin-settings.php:286 +#: includes/class-opentrust-admin-tools.php:539 #: includes/class-opentrust-cpt.php:218 #: includes/class-opentrust-cpt.php:227 #: templates/chat.php:47 @@ -975,8 +975,8 @@ msgstr "" msgid "Subprocessors" msgstr "" -#: includes/class-opentrust-admin-settings.php:303 -#: includes/class-opentrust-admin-tools.php:532 +#: includes/class-opentrust-admin-settings.php:287 +#: includes/class-opentrust-admin-tools.php:540 #: includes/class-opentrust-cpt.php:251 #: includes/class-opentrust-cpt.php:260 #: templates/chat.php:48 @@ -985,44 +985,44 @@ msgstr "" msgid "Data Practices" msgstr "" -#: includes/class-opentrust-admin-settings.php:304 -#: includes/class-opentrust-admin-tools.php:533 +#: includes/class-opentrust-admin-settings.php:288 +#: includes/class-opentrust-admin-tools.php:541 #: includes/class-opentrust-cpt.php:284 #: includes/class-opentrust-cpt.php:294 msgid "FAQs" msgstr "" -#: includes/class-opentrust-admin-settings.php:305 +#: includes/class-opentrust-admin-settings.php:289 msgid "Contact & DPO" msgstr "" -#: includes/class-opentrust-admin-settings.php:310 +#: includes/class-opentrust-admin-settings.php:294 msgid "Sections" msgstr "" -#: includes/class-opentrust-admin-settings.php:311 +#: includes/class-opentrust-admin-settings.php:295 msgid "Click a section to toggle its visibility. Hidden sections still preserve their content; only the public page changes." msgstr "" -#: includes/class-opentrust-admin-settings.php:566 +#: includes/class-opentrust-admin-settings.php:550 #: templates/chat.php:60 #: templates/trust-center.php:121 msgid "Contact" msgstr "" -#: includes/class-opentrust-admin-settings.php:567 +#: includes/class-opentrust-admin-settings.php:551 msgid "AI Chat" msgstr "" -#: includes/class-opentrust-admin-settings.php:568 +#: includes/class-opentrust-admin-settings.php:552 msgid "Import & Export" msgstr "" -#: includes/class-opentrust-admin-settings.php:611 +#: includes/class-opentrust-admin-settings.php:595 msgid "OpenTrust settings sections" msgstr "" -#: includes/class-opentrust-admin-settings.php:619 +#: includes/class-opentrust-admin-settings.php:603 msgid "Live" msgstr "" @@ -1126,6 +1126,7 @@ msgid "Conflict strategy" msgstr "" #: includes/class-opentrust-admin-tools.php:212 +#: includes/class-opentrust-admin-tools.php:311 msgid "Skip" msgstr "" @@ -1180,49 +1181,57 @@ msgstr "" msgid "UUID" msgstr "" -#: includes/class-opentrust-admin-tools.php:324 +#: includes/class-opentrust-admin-tools.php:309 +msgid "Create" +msgstr "" + +#: includes/class-opentrust-admin-tools.php:310 +msgid "Update" +msgstr "" + +#: includes/class-opentrust-admin-tools.php:332 msgid "Cancel" msgstr "" -#: includes/class-opentrust-admin-tools.php:326 +#: includes/class-opentrust-admin-tools.php:334 msgid "Confirm and import" msgstr "" -#: includes/class-opentrust-admin-tools.php:351 +#: includes/class-opentrust-admin-tools.php:359 msgid "Pick at least one record to export." msgstr "" -#: includes/class-opentrust-admin-tools.php:381 +#: includes/class-opentrust-admin-tools.php:389 msgid "No file uploaded." msgstr "" -#: includes/class-opentrust-admin-tools.php:387 +#: includes/class-opentrust-admin-tools.php:395 msgid "Upload exceeds size limit." msgstr "" -#: includes/class-opentrust-admin-tools.php:402 +#: includes/class-opentrust-admin-tools.php:410 msgid "Could not store uploaded file." msgstr "" -#: includes/class-opentrust-admin-tools.php:451 +#: includes/class-opentrust-admin-tools.php:459 msgid "Import cancelled." msgstr "" -#: includes/class-opentrust-admin-tools.php:456 +#: includes/class-opentrust-admin-tools.php:464 msgid "Import has unresolved errors." msgstr "" #. translators: %1$d: created, %2$d: updated, %3$d: skipped -#: includes/class-opentrust-admin-tools.php:476 +#: includes/class-opentrust-admin-tools.php:484 #, php-format msgid "Imported: %1$d created, %2$d updated, %3$d skipped." msgstr "" -#: includes/class-opentrust-admin-tools.php:484 +#: includes/class-opentrust-admin-tools.php:492 msgid "Errors:" msgstr "" -#: includes/class-opentrust-admin-tools.php:530 +#: includes/class-opentrust-admin-tools.php:538 #: includes/class-opentrust-cpt.php:185 #: includes/class-opentrust-cpt.php:194 #: templates/chat.php:46 @@ -1236,83 +1245,109 @@ msgstr "" msgid "Questions" msgstr "" -#: includes/class-opentrust-admin.php:184 +#: includes/class-opentrust-admin.php:198 msgid "Select Badge Image" msgstr "" -#: includes/class-opentrust-admin.php:185 +#: includes/class-opentrust-admin.php:199 msgid "Use as Badge" msgstr "" -#: includes/class-opentrust-admin.php:186 +#: includes/class-opentrust-admin.php:200 msgid "Select Proof Artifact" msgstr "" -#: includes/class-opentrust-admin.php:187 +#: includes/class-opentrust-admin.php:201 msgid "Use This File" msgstr "" -#: includes/class-opentrust-admin.php:188 +#: includes/class-opentrust-admin.php:202 #: includes/class-opentrust-cpt.php:402 msgid "Upload File" msgstr "" -#: includes/class-opentrust-admin.php:189 +#: includes/class-opentrust-admin.php:203 #: includes/class-opentrust-cpt.php:402 msgid "Replace File" msgstr "" #: includes/class-opentrust-admin.php:204 -msgid "No match in catalog, just keep typing to add manually." +msgid "Discard unsaved changes?" msgstr "" #: includes/class-opentrust-admin.php:205 +msgid "You have 1 unsaved change on this tab. Switching tabs will discard it." +msgstr "" + +#. translators: %d: number of unsaved changes on the current tab. +#: includes/class-opentrust-admin.php:207 +#, php-format +msgid "You have %d unsaved changes on this tab. Switching tabs will discard them." +msgstr "" + +#: includes/class-opentrust-admin.php:208 +msgid "Each tab saves independently. Save first to keep your changes, or discard them and switch." +msgstr "" + +#: includes/class-opentrust-admin.php:209 +msgid "Discard and switch" +msgstr "" + +#: includes/class-opentrust-admin.php:210 +msgid "Stay on this tab" +msgstr "" + +#: includes/class-opentrust-admin.php:224 +msgid "No match in catalog, just keep typing to add manually." +msgstr "" + +#: includes/class-opentrust-admin.php:225 msgid "Auto-filled from catalog, you may want to verify this." msgstr "" -#: includes/class-opentrust-admin.php:206 +#: includes/class-opentrust-admin.php:226 msgid "Auto-filled template, please verify this matches how you use this service." msgstr "" -#: includes/class-opentrust-admin.php:207 +#: includes/class-opentrust-admin.php:227 msgid "click to autofill" msgstr "" -#: includes/class-opentrust-admin.php:208 +#: includes/class-opentrust-admin.php:228 msgid "Catalog suggestions" msgstr "" -#: includes/class-opentrust-admin.php:254 +#: includes/class-opentrust-admin.php:274 msgid "OpenTrust requires pretty permalinks." msgstr "" #. translators: %s: link to Settings → Permalinks -#: includes/class-opentrust-admin.php:258 +#: includes/class-opentrust-admin.php:278 #, php-format msgid "Your site is using \"Plain\" permalinks. Please go to %s and choose any other option (Post name is the WordPress default)." msgstr "" -#: includes/class-opentrust-admin.php:259 +#: includes/class-opentrust-admin.php:279 msgid "Settings → Permalinks" msgstr "" -#: includes/class-opentrust-admin.php:264 +#: includes/class-opentrust-admin.php:284 msgid "Without pretty permalinks, every link OpenTrust generates returns 404 — including the trust center page itself. Visitors will not be able to reach your policies, certifications, or chat." msgstr "" -#: includes/class-opentrust-admin.php:268 +#: includes/class-opentrust-admin.php:288 msgid "Read-only fallback if you cannot change permalinks" msgstr "" -#: includes/class-opentrust-admin.php:272 +#: includes/class-opentrust-admin.php:292 msgid "You can preview the trust center via raw query-string URLs:" msgstr "" -#: includes/class-opentrust-admin.php:280 +#: includes/class-opentrust-admin.php:300 msgid "This is for testing only." msgstr "" -#: includes/class-opentrust-admin.php:281 +#: includes/class-opentrust-admin.php:301 msgid "Switching to pretty permalinks is the only supported configuration." msgstr ""