From b306c0bf29444d7b30eb716540bed823c7776b6c Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Fri, 19 Jun 2026 14:24:44 -0600 Subject: [PATCH 01/38] docs(09): capture phase context --- .../phases/09-editor-ux-polish/09-CONTEXT.md | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 .planning/phases/09-editor-ux-polish/09-CONTEXT.md diff --git a/.planning/phases/09-editor-ux-polish/09-CONTEXT.md b/.planning/phases/09-editor-ux-polish/09-CONTEXT.md new file mode 100644 index 0000000..b5c7b83 --- /dev/null +++ b/.planning/phases/09-editor-ux-polish/09-CONTEXT.md @@ -0,0 +1,190 @@ +# Phase 9: Editor UX Polish - Context + +**Gathered:** 2026-06-19 +**Status:** Ready for planning +**Source:** `/gsd:discuss-phase 9` — four gray areas selected and deep-dived with the user + + +## Phase Boundary + +Refine the **existing** edit-mode toolbar surface for clarity and small/mobile +use. No new architecture, no storage/REST change — every behavioral change carries +its accessibility guardrail. Three roadmap requirements, all on `assets/maestro.js` ++ `assets/maestro.css` (+ the i18n strings in `includes/class-assets.php`): + +- **UX-03** — Replace the verbose idle status with a short, glanceable edit-mode + indicator (signalled by more than colour), and on first run draw the eye to the + menu itself. +- **UX-04** — Move the rename field's visible label into the input as a placeholder, + while keeping a programmatic accessible name. +- **UX-07** — Denser control/input sizing at narrow widths so the toolbar fits and + reads well on mobile, keeping a ≥44px real touch-target floor. + +Out of scope: reparenting, separators, drag-drop rework, icon-set changes, any +change to the cosmetic-delta storage model or the `maestro/v1/config` REST contract, +and any new capability beyond polishing these three surfaces. + + + + +## Implementation Decisions (LOCKED — from user) + +### UX-03 — Status indicator copy + mode signal +- **Idle copy: "Edit Mode"** (replaces `'Editor active — click an item to edit.'`, + `includes/class-assets.php:97`). + - **Reconciliation note (for plan-checker):** the roadmap success criterion #1 + literally reads `"Menu Edit Mode"`. The user chose the shorter **"Edit Mode"**. + This is a deliberate wording refinement, not a missed criterion — treat + "Edit Mode" as the locked target and record that it satisfies the *intent* of + criterion #1 (short, glanceable, non-colour-signalled). Same reconciliation + pattern Phase 8 used for REL-06. +- **Non-colour signal: a leading dashicon** beside the green status (WCAG 1.4.1 — + not colour alone). Recommended glyph **`dashicons-edit`** (pencil); final glyph is + planner's discretion. + - **Supersedes** the `assets/maestro.css:285` note ("idle deliberately has NO + icon"). UX-03 explicitly wants an icon/label signal, so the leading idle icon is + now intended. The net idle indicator = green + dashicon + "Edit Mode" text. +- **Save states:** the "Edit Mode" mode indicator **persists**; transient + `Saving…` / `Saved` / `Save failed` states render as a **separate** element beside + it (mode is always legible). Keep the existing `wp.a11y.speak()` save-announcement + plumbing. + +### UX-03 — First-run attention cue +- A first-run cue already exists: `buildFirstRunCue()` (`assets/maestro.js:194`) + + `.maestro-firstrun` (`assets/maestro.css:373+`) — localStorage-gated, + keyboard-dismissible text banner ("Click a menu item to start editing." / "Got it"). +- **Keep the existing text banner AND add a subtle one-shot pulse/outline** that + draws the eye to the menu. + - **Target:** the **first editable top-level menu item** (teaches the core + "click an item to edit" gesture directly). + - **Duration:** **one short animation (~1–2s), then stops** — no persistent/looping + motion. Under `prefers-reduced-motion: reduce` it degrades to a static outline or + nothing (reuse the existing `@media (prefers-reduced-motion)` block at + `assets/maestro.css:307`). + - **Gate:** same localStorage first-run gate as the banner (cue shows once). + +### UX-04 — Rename placeholder + accessible name +- **Field stays pre-filled** with the selected item's current title (current + `populatePanel` behavior). The placeholder only appears when the field is empty — + **rename / commit-on-Enter / revert-on-Escape logic is unchanged** (no empty-commit + behavior change to design or test). +- **Remove the visible "Rename " text label** that currently wraps the input + (`assets/maestro.js:379–381`) and set it as the input's **placeholder: "Menu label"** + (noun describing the value — better placeholder semantics than the action verb). +- **Keep a programmatic accessible name via a visually-hidden ` + + +## Specific Ideas — codebase anchors + +- **Idle status string:** `includes/class-assets.php:97` (`'idle' => …`) plus the + sibling `saving` / `saved` / `saveError` strings (98–100). New strings for the mode + label / dashicon affordance go in this same `i18n` array. +- **First-run cue:** `buildFirstRunCue()` call site `assets/maestro.js:194`; + `.maestro-firstrun*` styles `assets/maestro.css:373–420`; reduced-motion block + `assets/maestro.css:307`. First-run strings live at `class-assets.php` `firstRun` / + `firstRunDismiss` (~124–127). +- **Rename field:** `assets/maestro.js:377–392` — the `screen-reader-text` panel + label, the ` + + +## Existing Code Insights + +### Reusable Assets +- `buildFirstRunCue()` + `.maestro-firstrun` + the localStorage gate: extend, don't + rebuild — the cue mechanism and dismiss/gate already exist. +- `screen-reader-text` visually-hidden convention (panel label, modified badge): + reuse for the UX-04 programmatic rename label. +- `@media (prefers-reduced-motion: reduce)` block (`maestro.css:307`): the place to + neutralize the new first-run pulse for reduced-motion users. +- `wp.a11y.speak()` announcement plumbing: reuse for any new status announcements. + +### Established Patterns +- Single vanilla-JS editor file (`assets/maestro.js`), no build step; `el()` helper + builds DOM with `textContent` (XSS-safe — keep it that way). +- All edit-mode styling in `assets/maestro.css`; i18n strings localized from + `includes/class-assets.php`. +- WP admin mobile breaks at 782px — align UX-07 there. + +### Integration Points +- i18n strings: `includes/class-assets.php` `i18n` array → consumed as `I.*` in + `maestro.js`. +- The toolbar/status DOM is built in `buildToolbar()`; the selected-item panel + (rename/icon/visibility) in the panel builder around `maestro.js:370+`. + + + + +## Deferred Ideas + +None — discussion stayed within the UX-03 / UX-04 / UX-07 scope. (Larger backlog +items — reparenting, separators, custom icon upload, import/export, multisite +defaults, configurable menu width — remain in the PROJECT.md post-1.0 backlog, not +this phase.) + + + +--- + +*Phase: 09-editor-ux-polish* +*Context gathered: 2026-06-19* From e65e392ee2f71c5683275138b1ab1966b2ff7a13 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Fri, 19 Jun 2026 14:24:50 -0600 Subject: [PATCH 02/38] docs(state): record phase 9 context session --- .planning/STATE.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 1aaf1a8..8c155e7 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,17 +1,17 @@ --- gsd_state_version: 1.0 milestone: v1.2 -milestone_name: Editor UX polish -status: ready_to_plan -stopped_at: v1.2 roadmap created — Phase 9 ready to plan -last_updated: "2026-06-17T15:30:00.000Z" -last_activity: 2026-06-17 — v1.2 roadmap created (Phase 9: Editor UX Polish, UX-03/UX-04/UX-07) +milestone_name: Editor UX Polish +status: planning +stopped_at: Phase 9 context gathered +last_updated: "2026-06-19T20:24:50.616Z" +last_activity: 2026-06-17 — v1.2 roadmap written; v1.1 milestone archived; Phase 9 is the single phase for UX-03, UX-04, UX-07 progress: - total_phases: 9 - completed_phases: 8 - total_plans: 0 - completed_plans: 0 - percent: 0 + total_phases: 4 + completed_phases: 2 + total_plans: 10 + completed_plans: 9 + percent: 90 --- # Project State @@ -84,6 +84,6 @@ None. ## Session Continuity -Last session: 2026-06-17T15:30:00.000Z -Stopped at: v1.2 roadmap created — Phase 9 defined, files written, ready to run /gsd:plan-phase 9 -Resume file: None +Last session: 2026-06-19T20:24:50.613Z +Stopped at: Phase 9 context gathered +Resume file: .planning/phases/09-editor-ux-polish/09-CONTEXT.md From ad4c0b8ee5140d371b50c338589548d871666d0a Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Fri, 19 Jun 2026 14:35:33 -0600 Subject: [PATCH 03/38] docs(planning): schedule V2-16 as Phase 10 research spike; add V2-17 privileged-tier backlog item --- .planning/PROJECT.md | 2 +- .planning/REQUIREMENTS.md | 12 ++++++++---- .planning/ROADMAP.md | 18 ++++++++++++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index 0d62a94..e091c8b 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -72,7 +72,7 @@ security boundary. - **Release assets:** Phase 4 is complete. WordPress.org icon, banner, and screenshot graphics exist under `.wordpress-org/` and are referenced from the GitHub/wp.org readmes. User-facing documentation is published in the GitHub README, WordPress.org readme, and `docs/user-guide.md`. - **Localization:** The plugin is translation-ready with the `maestro` text domain and `Domain Path: /languages`. PHP strings use WordPress translation helpers, and JavaScript editor labels are passed through `maestroData.i18n` from PHP. The repo ships a POT template plus starter catalogs for `es_ES`, `de_DE`, `ja`, `fr_FR`, `pt_BR`, and `it_IT`; WordPress.org language packs can still override and extend them, and native-speaker/Polyglots review is welcome. - **Submit:** Phase 5 is complete. The runtime zip builds cleanly, WPCS passes, Plugin Check 2.0.0 reports no errors on the extracted build zip, npm audit reports 0 vulnerabilities after removing unused `@wordpress/scripts`, and local unit/integration/E2E tests pass. **The plugin has been submitted to WordPress.org** and is in the review queue; approval and SVN access are pending (external, out of our hands). On approval: commit to SVN `trunk`, tag `1.0.0`, and upload `.wordpress-org/` to the SVN `assets/` dir. -- **Future roadmap (post-1.0 backlog):** reparenting (top↔sub, highlighting minefield); separator management; keyboard-accessible reordering; per-item-reset UI affordance with a "modified" indicator; custom icon upload (SVG sanitization); import/export config as JSON; optional enforcement bridge (opt-in, clearly-labelled defense-in-depth); multisite/network defaults with per-site override; configurable admin-menu width (V2-09); admin-toolbar editing feasibility research (V2-10); UI/UX design polish for edit-mode hierarchy, responsive behavior, modified-state affordances, status clarity, and icon-picker scanability (V2-12); documentation link hygiene for prose references to project files (V2-13); deterministic banner source/regeneration with the "ADMIN MENU" leader line removed (V2-14). +- **Future roadmap (post-1.0 backlog):** reparenting (top↔sub, highlighting minefield); separator management; keyboard-accessible reordering; per-item-reset UI affordance with a "modified" indicator; custom icon upload (SVG sanitization); import/export config as JSON; optional enforcement bridge (opt-in, clearly-labelled defense-in-depth); multisite/network defaults with per-site override; configurable admin-menu width (V2-09); admin-toolbar editing feasibility research (V2-10); UI/UX design polish for edit-mode hierarchy, responsive behavior, modified-state affordances, status clarity, and icon-picker scanability (V2-12); documentation link hygiene for prose references to project files (V2-13); deterministic banner source/regeneration with the "ADMIN MENU" leader line removed (V2-14); role cloning / per-user cosmetic hiding (V2-15); third-party menu compatibility research, WooCommerce-first (V2-16 — pulled forward to v1.2 Phase 10, 2026-06-19); and a single-site "super-admin equivalent" / privileged editor tier research item (V2-17, 2026-06-19) — note it edges toward the Out-of-Scope "page locking" line, so an *enforced* tier likely belongs in the sibling **wp-sudo** project or a documented bridge, not Maestro core. ## Constraints diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index a0f17ba..b12e11b 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -1,7 +1,7 @@ # Requirements: Maestro **Defined:** 2026-06-13 -**Last updated:** 2026-06-17 — v1.2 traceability added (UX-03, UX-04, UX-07 → Phase 9) +**Last updated:** 2026-06-19 — V2-16 pulled forward to Phase 10 (v1.2 research spike); V2-17 (single-site privileged editor tier) added to v2 backlog **Core Value:** Editing the admin menu happens directly on the menu, with zero ceremony and zero risk to access. ## v1.0 Requirements — ✅ shipped & archived @@ -94,7 +94,8 @@ Post-1.0 backlog (from SPEC.md → Roadmap). Tracked, not in this roadmap. - **(b) Dynamic inheritance** — register the clone with no stored caps and a `user_has_cap` / `map_meta_cap` filter that resolves the clone to its source role at request time. Always in sync, negligible per-check cost, and keeps the autoloaded `wp_user_roles` option lean (favour few, slim roles over many fat ones). - **Alternative that may obviate roles entirely: per-user visibility** — store hidden items keyed by user ID instead of cloning a role. More direct for "hide from one admin," but adds a new dimension to the delta model (today: global + per-role) and new storage/merge logic. *Constraint:* must stay inside the "visibility is cosmetic" principle (see Out of Scope) — privileges are untouched; this only widens *who* a cosmetic rule can target. *Deliverable first:* a short feasibility note (snapshot vs dynamic vs per-user), not a build commitment. Relates to V2-07 (enforcement bridge), V2-08 (multisite defaults). -- **V2-16**: *Research* — compatibility with popular plugins that build their admin menu in non-standard ways. Many high-install plugins manipulate the admin menu outside the normal `add_menu_page`/`add_submenu_page` flow: custom positions, dynamically/conditionally injected items, late or re-registered menus, count/notification badges baked into the title string, custom separators, or direct surgery on the `$menu`/`$submenu` globals. Maestro's sparse-delta replay keys on stable slugs and applies on a late `admin_menu` pass, so these patterns may not rename/reorder/hide/re-icon cleanly (cf. the existing "submenu sort relies on the late `admin_menu` pass" known limit). Survey the most-installed likely offenders — **WooCommerce first** (it reorders/injects heavily and adds its own top-level + submenus), plus e.g. Jetpack, Yoast SEO / Rank Math, Elementor and other page builders, WPForms, and LMS/membership plugins. For each: document how it registers its menu, what breaks under Maestro's rename/reorder/hide/icon, and whether the fix is a slug-resolution tweak, a later/again hook, special-casing, or a documented limitation. *Deliverable:* a compatibility research note + a prioritized fix/limitation list, **not** a build commitment. Relates to V2-01 (reparenting), V2-02 (separators). +- **V2-16**: *Research* — compatibility with popular plugins that build their admin menu in non-standard ways. Many high-install plugins manipulate the admin menu outside the normal `add_menu_page`/`add_submenu_page` flow: custom positions, dynamically/conditionally injected items, late or re-registered menus, count/notification badges baked into the title string, custom separators, or direct surgery on the `$menu`/`$submenu` globals. Maestro's sparse-delta replay keys on stable slugs and applies on a late `admin_menu` pass, so these patterns may not rename/reorder/hide/re-icon cleanly (cf. the existing "submenu sort relies on the late `admin_menu` pass" known limit). Survey the most-installed likely offenders — **WooCommerce first** (it reorders/injects heavily and adds its own top-level + submenus), plus e.g. Jetpack, Yoast SEO / Rank Math, Elementor and other page builders, WPForms, and LMS/membership plugins. For each: document how it registers its menu, what breaks under Maestro's rename/reorder/hide/icon, and whether the fix is a slug-resolution tweak, a later/again hook, special-casing, or a documented limitation. *Deliverable:* a compatibility research note + a prioritized fix/limitation list, **not** a build commitment. Relates to V2-01 (reparenting), V2-02 (separators). **→ Promoted to a concrete phase 2026-06-19: Phase 10 (v1.2) research spike.** +- **V2-17**: *Research* — single-site **"super-admin equivalent" / privileged editor tier**. On a non-multisite install every administrator shares `manage_options`, so any admin can open the editor and *undo* another admin's cosmetic hide/rename/reorder — there is no notion of a higher tier. This item asks whether a **designated** admin (a single-site analogue of the network super admin) could configure the menu for *other* admins in a way regular admins cannot change — i.e. gating **who may edit/lock the configuration**, a different axis from the existing per-role visibility (which only governs *who a cosmetic rule targets*, via [`maestro_capability`](../maestro-menu-editor.php#L42), default `manage_options`). **Principle tension (resolve up front):** "lock what other admins see so they can't undo it" crosses from *cosmetic visibility* into *enforcement / page locking*, which is explicitly **Out of Scope** below ("Real access control / page locking"). Any design must either **(a)** stay strictly cosmetic — a privileged *editor/config-owner* tier (e.g. a custom `maestro_manage` cap gating who may *enter edit mode*), while each hidden page's own capability remains the real gate and a regular admin can still reach a page they hold the cap for — or **(b)** be an explicit, clearly-labelled *enforcement* opt-in, in which case it likely does **not** belong in core Maestro. **wp-sudo angle:** a "sudo for admins" / privilege-tier concept is precisely the sibling **wp-sudo** project's domain (cf. the wp-sudo thread already referenced in this roadmap); the natural home for an *enforced* privileged tier is wp-sudo, or a documented Maestro↔wp-sudo bridge, not Maestro core. *Deliverable:* a feasibility note — capability-model options (custom cap via the `maestro_capability` filter vs. a wp-sudo bridge), the cosmetic-vs-enforcement boundary, and a recommendation on whether/where to build — **not** a build commitment. Relates to V2-07 (enforcement bridge), V2-15 (role cloning / per-user hiding), V2-08 (multisite defaults), and the Out-of-Scope "Real access control / page locking" row. ## Out of Scope @@ -132,12 +133,15 @@ Post-1.0 backlog (from SPEC.md → Roadmap). Tracked, not in this roadmap. | UX-03 | Phase 9: Editor UX Polish | Pending | | UX-04 | Phase 9: Editor UX Polish | Pending | | UX-07 | Phase 9: Editor UX Polish | Pending | +| V2-16 | Phase 10: Third-Party Menu Compatibility Research | Pending (research spike) | **Coverage (v1.2):** -- v1.2 active requirements: 3 (UX-03, UX-04, UX-07) — all mapped to Phase 9 +- v1.2 build requirements: 3 (UX-03, UX-04, UX-07) — all mapped to Phase 9 +- v1.2 research spike: V2-16 (WooCommerce-first third-party menu compatibility) — pulled forward 2026-06-19, mapped to Phase 10 - Unmapped: 0 ✓ - UX-05 and UX-06: shipped in v1.1.1 — not included in v1.2 scope +- V2-17 (single-site privileged editor tier) added to the v2 backlog 2026-06-19 — research, not yet scheduled --- *Requirements defined: 2026-06-13* -*Last updated: 2026-06-17 — v1.2 traceability added; UX-03/UX-04/UX-07 → Phase 9* +*Last updated: 2026-06-19 — V2-16 → Phase 10 (research spike); V2-17 added to v2 backlog* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 78f6b30..022c8c7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -4,7 +4,7 @@ - ✅ **v1.0 WordPress.org Release Readiness** — Phases 1–5 (shipped 2026-06-14; submitted to .org, awaiting review) → [archive](milestones/v1.0-ROADMAP.md) - ✅ **v1.1 Polish & Accessibility** — Phases 6–8 (shipped 2026-06-17) -- 🚧 **v1.2 Editor UX Polish** — Phase 9 (in progress) +- 🚧 **v1.2 Editor UX Polish** — Phases 9–10 (Phase 9 editor polish in progress; Phase 10 a WooCommerce-first third-party menu compatibility **research spike**, pulled forward from V2-16 on 2026-06-19) ## Phases @@ -97,10 +97,23 @@ Full phase details, success criteria, and outcomes are archived in 5. The full zero-regression bar holds: PHP unit, integration, and e2e suites green; phpcs clean **Plans**: TBD +### Phase 10: Third-Party Menu Compatibility Research +**Goal**: A documented, evidence-based picture of how Maestro's sparse-delta replay behaves against the highest-install plugins that build their admin menu in non-standard ways — with a prioritized fix/limitation list, not a build commitment +**Depends on**: Phase 9 +**Requirements**: V2-16 +**Type**: **Research spike** — pulled forward from the v2 backlog 2026-06-19. Deliverable is a compatibility note + prioritized fix/limitation list; no production menu-handling code is committed in this phase (optional test-harness scaffolding only). +**Success Criteria** (what must be TRUE): + 1. **WooCommerce (priority #1)** plus a surveyed set (e.g. Jetpack, Yoast SEO / Rank Math, Elementor or another page builder, WPForms, and an LMS/membership plugin) are each documented: how they register or manipulate the admin menu (custom positions, conditional/late injection, re-registered menus, count/notification badges baked into title strings, custom separators, direct `$menu`/`$submenu` surgery) + 2. For each surveyed plugin, what breaks under Maestro's rename / reorder / hide / re-icon is recorded with concrete reproduction notes + 3. Each breakage is classified by fix type — slug-resolution tweak, later/again `admin_menu` hook, special-casing, or documented limitation — and prioritized + 4. A reproducible test environment is specified — e.g. a `.wp-env.json` (or equivalent) variant that loads WooCommerce and the other offenders, since the current env loads `"plugins": []` and exercises Maestro alone — delivered as a committed harness and/or a clear recommendation + 5. The research note lands in the repo (e.g. `docs/` or `.planning/`) and feeds the prioritized backlog (relates to V2-01 reparenting, V2-02 separators); no change to the zero-regression bar +**Plans**: TBD + ## Progress **Execution Order:** -v1.0 complete (Phases 1–5, archived). v1.1 complete (Phases 6–8, archived). v1.2 executes: 9 +v1.0 complete (Phases 1–5, archived). v1.1 complete (Phases 6–8, archived). v1.2 executes: 9 → 10 (10 is a research spike; may run independently of 9) | Phase | Milestone | Plans Complete | Status | Completed | |-------|-----------|----------------|--------|-----------| @@ -113,3 +126,4 @@ v1.0 complete (Phases 1–5, archived). v1.1 complete (Phases 6–8, archived). | 7. Visual Polish & Icons | v1.1 | 4/4 | Complete | 2026-06-17 | | 8. Docs & Brand Assets | v1.1 | 4/4 (executable scope; REL-07/08 deferred) | Complete | 2026-06-17 | | 9. Editor UX Polish | v1.2 | 0/TBD | Not started | - | +| 10. Third-Party Menu Compatibility Research | v1.2 | 0/TBD | Not started (research spike) | - | From b26a97a44b1759d532c30183e6766206906adce2 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Fri, 19 Jun 2026 14:47:37 -0600 Subject: [PATCH 04/38] =?UTF-8?q?docs(09):=20research=20phase=20=E2=80=94?= =?UTF-8?q?=20editor=20UX=20polish=20(UX-03,=20UX-04,=20UX-07)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../phases/09-editor-ux-polish/09-RESEARCH.md | 881 ++++++++++++++++++ 1 file changed, 881 insertions(+) create mode 100644 .planning/phases/09-editor-ux-polish/09-RESEARCH.md diff --git a/.planning/phases/09-editor-ux-polish/09-RESEARCH.md b/.planning/phases/09-editor-ux-polish/09-RESEARCH.md new file mode 100644 index 0000000..7d56ba7 --- /dev/null +++ b/.planning/phases/09-editor-ux-polish/09-RESEARCH.md @@ -0,0 +1,881 @@ +# Phase 9: Editor UX Polish — Research + +**Researched:** 2026-06-19 +**Domain:** Vanilla JS DOM, CSS animations, WCAG accessibility, WordPress dashicons +**Confidence:** HIGH — all findings grounded in the actual codebase; no speculative claims + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**UX-03 — Status indicator copy + mode signal** +- Idle copy: "Edit Mode" (replaces `'idle' => 'Editor active — click an item to edit.'` at `includes/class-assets.php:97`). Reconciliation: roadmap reads "Menu Edit Mode" but user chose the shorter "Edit Mode"; treat this as satisfying the intent. +- Non-colour signal: a leading dashicon beside the green status (WCAG 1.4.1). Recommended glyph `dashicons-edit`; final glyph is planner's discretion. +- Supersedes the `assets/maestro.css:285` comment ("idle deliberately has NO icon"). The idle indicator is now: green + dashicon + "Edit Mode" text. +- Save states: "Edit Mode" indicator persists; transient `Saving…` / `Saved` / `Save failed` states render as a SEPARATE element beside it (mode is always legible). Keep existing `wp.a11y.speak()` plumbing. + +**UX-03 — First-run attention cue** +- Keep the existing text banner (`buildFirstRunCue()` / `.maestro-firstrun`) AND add a subtle one-shot pulse/outline on the FIRST EDITABLE top-level menu item. +- Duration: one short animation (~1–2s), then stops. Under `prefers-reduced-motion: reduce` degrades to a static outline or nothing. Reuse the existing `@media (prefers-reduced-motion)` block at `assets/maestro.css:307`. +- Same localStorage first-run gate as the banner (cue shows once, key `maestroFirstRunDone`). + +**UX-04 — Rename placeholder + accessible name** +- Field stays pre-filled with the item's current title. Placeholder ("Menu label") shows only when the field is empty. Rename / commit-on-Enter / revert-on-Escape logic UNCHANGED. +- Remove the visible "Rename " text label (`assets/maestro.js:380` — `document.createTextNode( I.rename + ' ' )`). +- Keep a programmatic accessible name: visually-hidden ` + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| UX-03 | Replace verbose idle status with short "Edit Mode" indicator (dashicon + text + green, WCAG 1.4.1), plus a one-shot first-run pulse on the first editable menu item, localStorage-gated | Status DOM in `buildToolbar()` lines 362–367; i18n `idle` key at `class-assets.php:97`; first-run gate in `buildFirstRunCue()` lines 1060–1098; first editable item selector documented below | +| UX-04 | Move visible "Rename " label into a placeholder; keep a visually-hidden ` + +--- + +## Summary + +Phase 9 is a contained polish phase — three independent CSS/JS/i18n changes to the existing edit-mode toolbar surface. No new architecture, no REST contract changes, no storage changes. The codebase is well-mapped and the prior phases (6–8) have established every pattern this phase needs. + +The most technically nuanced change is UX-03's two-part status redesign: splitting the status element into a persistent mode indicator plus a transient save-state indicator. This is the only structural DOM change in the phase. The first-run pulse requires a new `@keyframes` animation and a reliable "first editable top-level item" selector (documented below). UX-04 is largely a string + markup swap. UX-07 is CSS-only for the density pass, with potential structural adjustments reserved for after a screenshot review. + +The TDD seam is an extension of the `assets/maestro-logic.js` + `tests/js/` infrastructure from Phase 6. Three pure functions need to be extracted and test-driven: `modeStatusLabel(state)` (state→{label,icon} mapping), `firstRunSeen()` (localStorage gate boolean), and `placeholderForValue(value)` (empty-vs-filled decision). All three are writable as `expect(fn(input)).toBe(output)` before implementation. + +**Primary recommendation:** Follow the locked decisions exactly. Implement in plan order: (1) UX-04 first (simplest, no animation, ground truth for the rename field), (2) UX-03 status split (DOM structural change), (3) UX-03 first-run pulse (new animation), (4) UX-07 mobile density pass (CSS-only, ends with a screenshot-gate plan step). + +--- + +## Exact Code Locations (per requirement) + +### UX-03 — Status indicator + +**i18n string to change (`includes/class-assets.php:97`):** +```php +// CURRENT (line 97): +'idle' => __( 'Editor active — click an item to edit.', 'maestro-menu-editor' ), + +// TARGET: +'idle' => __( 'Edit Mode', 'maestro-menu-editor' ), +``` + +**New i18n strings to add (after line 100, in the same array):** +```php +// Add after 'saveError' (line 100): +'modeLabel' => __( 'Edit Mode', 'maestro-menu-editor' ), // persistent mode indicator text +``` + +**DOM structure to change (`assets/maestro.js:362–367` inside `buildToolbar()`):** + +Current (lines 362–367): +```javascript +statusEl = el( 'span', 'maestro-status maestro-status-idle' ); +statusEl.setAttribute( 'role', 'status' ); +statusEl.setAttribute( 'aria-live', 'polite' ); +statusEl.setAttribute( 'aria-atomic', 'true' ); +statusEl.textContent = I.idle; +bar.appendChild( statusEl ); +``` + +Target structure — two sibling elements: a static mode label + a transient save-state span: +```javascript +// Mode indicator — always visible, never changes text +var modeEl = el( 'span', 'maestro-mode-label' ); +var modeIcon = el( 'span', 'maestro-mode-icon dashicons dashicons-edit' ); +modeIcon.setAttribute( 'aria-hidden', 'true' ); +modeEl.appendChild( modeIcon ); +modeEl.appendChild( document.createTextNode( I.modeLabel ) ); +bar.appendChild( modeEl ); + +// Save-state indicator — transient, aria-live +statusEl = el( 'span', 'maestro-status maestro-status-idle' ); +statusEl.setAttribute( 'role', 'status' ); +statusEl.setAttribute( 'aria-live', 'polite' ); +statusEl.setAttribute( 'aria-atomic', 'true' ); +bar.appendChild( statusEl ); +``` + +**`setStatus()` function change (`assets/maestro.js:955–966`):** + +Current: sets `statusEl.textContent` with `I.idle` for idle state, and transient copy for saving/saved/error. + +Target: the save-state element shows nothing at idle (empty / `display:none` / aria-hidden), and shows the transient message only during save states. Mode label is never touched by `setStatus()`. + +**CSS change (`assets/maestro.css:285–298` — idle-no-icon comment block):** + +Current comment at line 285: +``` +/* Idle has NO icon: the visible toolbar and "Editor active" text already signal + * edit mode, and a leading idle dot read as a fake control. The icon appears + * only while saving / saved / error. */ +``` + +The idle icon block must be added (with the new `.maestro-mode-label` + dashicon structure handled in CSS rather than the `::before` pseudo-element approach). The existing `::before` pseudo-element mechanism on `.maestro-status` stays for saving/saved/error states. The idle dashicon is supplied via a real `` child element in the DOM (not CSS `content:`), which avoids the BUG-04 fake-control appearance while giving a real, aria-hidden glyph. + +**LocalizationTest impact:** `LocalizationTest::expected_i18n_keys()` (`tests/integration/LocalizationTest.php:57–75`) asserts specific keys. The `idle` key still exists (its value changes); add `modeLabel` as a new key. The test must be updated to include `modeLabel` in its list. The payload-size budget in `PerformanceTest` (256 KiB ceiling) will not be affected — the new key adds fewer than 20 bytes. + +--- + +### UX-03 — First-run pulse on first editable item + +**Current first-run gate (`assets/maestro.js:1060–1098`):** + +`buildFirstRunCue()` reads `localStorage.getItem('maestroFirstRunDone') === '1'`, returns early if seen, otherwise builds and appends `.maestro-firstrun`. + +**Addition: after building the text banner, add the pulse:** +```javascript +// Inside buildFirstRunCue(), after the banner is built and appended: +var firstItem = document.querySelector( '#adminmenu > li.menu-top.maestro-item' ); +if ( firstItem ) { + firstItem.classList.add( 'maestro-firstrun-pulse' ); + // One-shot: remove after animation completes + firstItem.addEventListener( 'animationend', function handler() { + firstItem.classList.remove( 'maestro-firstrun-pulse' ); + firstItem.removeEventListener( 'animationend', handler ); + } ); +} +``` + +**Selector reliability:** `#adminmenu > li.menu-top.maestro-item` is the established project selector (used in e2e tests at `editor.spec.ts:33`, and in `initSortables()` at `maestro.js:884`). It finds exactly the editable top-level items. The `querySelector` (first match) gives the topmost item, which is the correct target. This selector is only called after `init()` has run `D.menu.forEach(...)` which stamps `dataset.maestroSlug` and adds `maestro-item` to all editable items, so the class is guaranteed present. + +**CSS additions for the pulse (add to first-run block, `maestro.css:373+`):** +```css +@keyframes maestro-pulse-item { + 0% { outline: 2px solid transparent; outline-offset: 2px; } + 30% { outline: 2px solid #2271b1; outline-offset: 2px; } + 70% { outline: 2px solid #2271b1; outline-offset: 2px; } + 100% { outline: 2px solid transparent; outline-offset: 2px; } +} +#adminmenu li.maestro-firstrun-pulse { + animation: maestro-pulse-item 1.5s ease-in-out 1 forwards; +} +@media ( prefers-reduced-motion: reduce ) { + #adminmenu li.maestro-firstrun-pulse { + animation: none; + outline: 2px solid #2271b1; + outline-offset: 2px; + } +} +``` + +The `prefers-reduced-motion` rule degrades the animation to a static outline (visible but not moving), consistent with the existing `@media (prefers-reduced-motion: reduce)` block at `maestro.css:307` which already neutralises the saving-spinner animation. + +**Dismiss integration:** The existing `dismiss()` function in `buildFirstRunCue()` removes `.maestro-firstrun` from the DOM and sets the localStorage flag. The pulse class should also be removed on dismiss (in case the user dismisses before the animation completes): +```javascript +function dismiss() { + try { window.localStorage.setItem( 'maestroFirstRunDone', '1' ); } catch (e) {} + cue.remove(); + // Also clear the pulse if it's still animating + if ( firstItem ) { firstItem.classList.remove( 'maestro-firstrun-pulse' ); } +} +``` + +--- + +### UX-04 — Rename placeholder + accessible name + +**Current rename field structure (`assets/maestro.js:379–392`):** +```javascript +var renameField = el( 'label', 'maestro-panel-field' ); +renameField.appendChild( document.createTextNode( I.rename + ' ' ) ); // line 380 — REMOVE THIS +var rename = el( 'input', 'maestro-rename-input' ); +rename.type = 'text'; +// ... event handlers ... +renameField.appendChild( rename ); +``` + +The `