feat(mobile): purpose-built phone view with a radial launcher#62
Draft
AllTerrainDeveloper wants to merge 1 commit intotrunkfrom
Draft
feat(mobile): purpose-built phone view with a radial launcher#62AllTerrainDeveloper wants to merge 1 commit intotrunkfrom
AllTerrainDeveloper wants to merge 1 commit intotrunkfrom
Conversation
- Implement tests for resolveMode() to classify viewports as desktop, tablet, or mobile based on width. - Test override functionality to ensure mode can be pinned regardless of viewport size. - Include tests for plugin hook integration via desktop_mode_responsive_resolve. - Verify subscription mechanism returns an unsubscribe function. - Ensure getMode() returns the cached current mode correctly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ship a real mobile experience for Desktop Mode. Auto-detects narrow
viewports, swaps the chrome for a touch-shaped policy, and replaces
the dock + title bar with a single floating radial launcher that
covers navigation, app-switching, and per-window actions.
Mobile.View.mov
Closes #55
Why
The desktop metaphor breaks at phone widths: dragging windows is
useless on touch, the dock crowds the viewport, and the 56 px title
bar plus tabs strip eats every spare pixel of admin content. Mobile
mode is a separate UX rather than a degraded desktop — opinionated,
gesture-first, and reachable with one thumb.
What ships
Detection
ResizeObserverprobe insrc/mobile/index.tsresolves'desktop' | 'tablet' | 'mobile'from viewport width. Stamps theresult on
<html data-wp-desktop-mode="…">so CSS rules cascadeoff the attribute and JS reads it live (no module-level lag).
wp_is_mobile()so phones don'tflash desktop chrome before the first JS tick.
wp.desktop.mode()— synchronous accessor.wp.desktop.responsive.subscribe(fn)— fires on every flip.wp.desktop.responsive.override('mobile' | null)— in-memory pinfor testing / power-user "always desktop" preference.
RESPONSIVE_MODE_CHANGEDaction (and matchingwp-desktop-mode-changedCustomEvent) for plugins on the bus.Window policy
In mobile mode every window is full-bleed and uneditable:
radial owns submenu navigation.
WINDOW_OPENEDsubscriberand a CSS pin (belt-and-suspenders against plugin geometry writes).
wp-desktop.window.drag-allowedand…resize-allowed. Pluginscan layer additional vetoes (e.g. while a modal is showing).
IFRAME_READYso a fresh pagealways lands at the top — not wherever Core's last save banner /
scroll-restoration left it.
Radial launcher (the centrepiece)
A single floating
+button at the bottom centre of the viewportexpands into an arc of icons above it. Replaces the dock, the
bottom thumbnail switcher, and the in-window tab strip in one
component.
State machine
edge, with a
dashicons-arrow-up-alt2chevron at the very topand a gentle accent-coloured pulse so it's discoverable without
being noisy. Tap → reveals + opens in one motion.
the apex, action chips fan out at the upper corners.
back to peeking. Every interaction resets the timer:
pointerdown / move / up on the gesture pad, taps on tiles,
taps on action chips, taps on the preview thumb, FAB taps,
drill in / drill back.
Tile presentation
CSS vars
--wpd-btn-size/--wpd-btn-icon-sizeso the samestyle scales the embedded
<wpd-window-button>).<defs><path>+<text><textPath>. 11 px font with a 2.5 pxdark stroke (
paint-order: stroke) so labels stay legible ifthey graze the icon. Labels truncate at 13 chars with an
ellipsis — no
textLength/lengthAdjustdistortion.Layout & gestures
fading opacity — natural "more behind here" affordance, no
separate UI element.
apex animates into the exact 0° slot.
px gesture pad anchored on the FAB centre. Pointer events bubble
to the radial root which feeds the unified gesture handler.
Movement threshold of 8 px discriminates tap from drag, so a
quick tap on a tile activates it cleanly.
Animations
existing tile DOM, so a continuous drag doesn't churn nodes or
flicker transitions.
the parent set out (
scale(0.4)+ opacity 0) and the childrenin (
scale(0.4) → 1, opacity fade) via a CSS keyframe.Visibility is driven by a
--wpdm-radial-visibilityCSS var sothe open/leaving classes can override cleanly (an inline
opacitywould have beaten any class rule).the keyframe so they don't restart on every pointermove.
Content
The root level merges, in priority order:
Windowinstance,tinted accent-colour to read as "switch to" rather than "open".
Tapping uses
manager.focus()directly so identity ispreserved even when multiple windows share a URL.
Bin, plugin-registered native windows from
desktop_mode_register_window(). OS Settings has a sentinelroute through
wp.desktop.openOsSettings()because it isn'tin the public registry.
wp_register_desktop_icon(). De-duplicated against ci: makenpm run test:phpactually work #2 whenthey target the same native id.
Plugins, Users, Settings, every CPT and plugin-contributed
page. Drilling into one fans out its submenu items, with the
parent itself re-prepended as a leaf so the user can still
reach the parent landing page.
Submenu tiles inherit their parent's icon — drilled into Posts,
every fan-out tile carries the Posts dashicon to reinforce "still
in Posts" rather than displaying a generic chevron that conveys
nothing.
The home grid on the wallpaper does the same merge: a new
desktop_mode_desktop_iconsJS filter at the top ofrenderDesktopIcons()injects every dock item as a syntheticicon when
[data-wp-desktop-mode="mobile"]is set on<html>.A
repaintDesktopIcons()helper synchronously replays the lastrenderDesktopIconscall (with the fingerprint cache busted) somode flips take effect immediately — no REST round-trip.
Smart preview thumbnail
When the snapped apex item matches an open window, a translucent
glass card floats in the middle of the screen:
"Tap to switch". One tap focuses the existing window.
pills, one per window, with the focused window's pill tinted
accent. Pick exactly which instance to bring forward.
multiflag: list-screen URLs (Posts, Pages, Media, Users) stack a new
instance via
openNew(), singleton URLs (Settings, Dashboard)focus the existing one. The "Tap to switch" thumb is the
dedicated affordance for focusing regardless of
multi.Window action chips
Reload + Close fan out at the upper-left and upper-right
corners of the radial when it's open AND a window is focused.
Title bars are gone in mobile mode — these are the user's only
path to those actions, deliberately reachable in the same thumb
zone as the FAB. Close tints red on press as the danger telegraph.
Hidden when no window is focused (nothing to act on) via a
--has-focusroot class kept in sync with WindowManager hooks.CSS and chrome adjustments
stylesheet inflates it to 46 px+ otherwise) with row items
pinned to match. The "Howdy, admin" / username badges hide so
the bar stays single-row.
#wpbody/#wpbody-content/#wpcontentpadding +margin zeroed on the parent shell, and
#wpbody { padding-top: 0 }added to chromeless mode (Core's
@media (max-width:600px)rulewas leaving 46 px of dead space at the top of every iframed page).
grid replaces them.
Hooks added
PHP filters:
desktop_mode_mode_typewp_is_mobile() ? 'mobile' : 'desktop'desktop_mode_responsive_breakpoints{ mobile: 640, tablet: 1024 }JS filters / actions (via
wp.desktop.hooks):RESPONSIVE_MODE_CHANGED{ from, to, viewport }on every flip.WINDOW_DRAG_ALLOWEDtrue; mobile returnsfalse.WINDOW_RESIZE_ALLOWEDdesktop_mode_responsive_resolvedesktop_mode_desktop_iconsdesktop_mode_mobile_app_switcherTests + verification
tests/vitest/mobile.test.ts— coversresolveMode, breakpointclassification, override behavior, plugin filter override,
subscribe API.
npm run lint,tsc --noEmit,npm run test:js— all green(746 / 746).
npm run build— desktop bundle clean (~205 KB gzipped).Files
Added
src/mobile/index.ts— detection, force-maximize, drag/resizefilter subscribers, scroll-to-top, mode flips.
src/mobile/radial.ts— RadialLauncher class (FAB, gesture pad,arc, tiles, snap, preview, action chips, state machine).
assets/css/mobile.css— every[data-wp-desktop-mode="mobile"]rule.
tests/vitest/mobile.test.tsdocs/examples/mobile-mode.mdModified
src/hooks.ts— three new HOOKS constants.src/types.ts—DesktopMode,responsiveBreakpoints,initialModeonDesktopConfig.src/desktop.ts— bootbootMobile; exposemode()/responsiveon the public API.src/window/pointer.ts— drag/resize filter gates with attribute-driven mode lookup (avoids import cycle).
src/desktop-icons.ts—desktop_mode_desktop_iconsfilter,repaintDesktopIcons()/resetDesktopIconsFingerprint()helpers.
src/ui/components/wpd-window-button/wpd-window-button.styles.ts—shadow-DOM CSS vars
--wpd-btn-size,--wpd-btn-icon-size.src/public-api.ts/src/global.d.ts— surfaceDesktopMode.assets/css/chromeless.css—#wpbodypadding zero.includes/render.php— payload extends withresponsiveBreakpoints+initialMode; enqueue mobile CSS.includes/assets.php— registerwp-desktop-mobilestyle.docs/architecture.md,docs/javascript-reference.md,docs/hooks-reference.md,docs/examples/README.md— everysurface documented.
Out of scope
'tablet'is detected but currentlybehaves like desktop. Reserved for a future hybrid pass
(split view, slide-over, horizontal bottom dock).
override is the testing affordance only).