Skip to content

Restyle the Mobile Command Universe gesture diagrams#4462

Open
fbmcipher wants to merge 12 commits into
cybersemics:mainfrom
fbmcipher:fbmcipher/issue-4186-cmduniverse
Open

Restyle the Mobile Command Universe gesture diagrams#4462
fbmcipher wants to merge 12 commits into
cybersemics:mainfrom
fbmcipher:fbmcipher/issue-4186-cmduniverse

Conversation

@fbmcipher

@fbmcipher fbmcipher commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Closes #4186.

Screenshot 2026-06-26 at 01 15 36

This PR reworks how command gesture diagrams are rendered on the Command Universe ("Commands") surface. This involved giving GestureDiagram additional the props and capabilities it needed to match the spec mockup (like gradient strokes and a wider chevron arrowhead sizing), wiring those into the command grid, and fixing an iOS-only centering bug along the way.

Testing

@BayuAri The changes in this PR should be purely aesthetic – no functionality changes in the Command Universe are expected. Because we've modified GestureDiagram a lot in this PR, it is probably worth checking the other interfaces in which that component is used, to make sure they still look and work the same as before. Thank you :)


Changes

GestureDiagram

The bulk of the work. All additions to GestureDiagram are opt-in props with defaults that preserve the existing rendering everywhere else GestureDiagram is used.

  • Two-color gradient (gradient) – fades the entire stroke between two colors along the path. Critically, when a gradient is supplied the whole gesture is drawn as a single chord-aligned <path> rather than per-segment paths. Without doing this, the gesture diagram showed visible seams where per-segment strokes connected. Of course, we rely on the per-segment paths in other uses of GestureDiagram.

  • **Wide chevron arrowhead (arrowhead='outlined-wide'), matching the design. Its geometry is exposed via chevronApexAngle (default 80°) and chevronSize (default 2.2× stroke) so the shape, size and angle of the arrowhead is fully tunable.

  • cornerRadius – rounds the bends/corners in the gesture. This only works when gradient is passed, because it only works with the continuous <path> rendering mode.

  • tipExtension – adds length to just the final segment, so that there's breathing room between the last bend and the arrowhead. Without this prop, the outlined-wide arrowhead overlaps the gesture and looks messy.

  • fillContainer – the diagram sizes itself to its parent and uses a centered square viewBox clamped to a common extent, so every gesture fills its cell at a uniform visual scale regardless of shape.

  • glow – toggles the soft drop-shadow glow (defaults to true to preserve current behavior). Our new design does not have a glow around gestures.

iOS Safari centering fix

Straight gestures now compute their viewBox during render (so React owns the attribute) instead of setting it imperatively in an onRef callback. The onRef method was causing an inconsistency where iOS Safari wasn't always repainting, leaving some gestures off-center. Computing during render solved the issue.

Command grid integration

  • Wired the upgraded GestureDiagram into CommandUniverseGridItem with the production gradient / chevron / fit-to-container settings.
  • Replaced the table markup with divs. The grid had inherited <table>/<tbody>/<tr>/<td> from CommandTable, but it renders a 2-column grid of cards, not a table, so we use divs instead.

To-Do

  • Resolve the failing Puppeteer test.

fbmcipher added 12 commits June 25, 2026 19:15
Exposes a `gradient={ from, to }` prop. When supplied, the diagram is drawn
as a single <path> with one chord-aligned linear gradient instead of
per-segment fades, so the color transition is smooth and seam-free across
joins. Existing call sites (no `gradient`) keep their per-segment behavior.
Adds new optional props (all opt-in, default behavior unchanged):

- arrowhead='outlined-wide' renders a continuous chevron at the path's
  natural tip, with legs splaying backward at a configurable apex angle
  (chevronApexAngle, chevronSize) — matches the Mobile Command Universe
  spec mockup.
- fillContainer mode sizes the wrapping span to its parent and uses a
  square viewBox derived from the gesture's bbox (clamped to size +
  tipExtension), so straight, rounded, and rdld gestures render at a
  uniform on-screen scale across cells.
- Single chord-aligned gradient now applies to rounded and the rdld
  help glyph, not just straight gestures, eliminating per-segment seams.
- cornerRadius softens interior vertices on straight single-gradient
  gestures with quadratic curves.
- tipExtension lengthens the last segment so the final bend has
  breathing room before the arrowhead.
- glow=false disables the drop-shadow filter.
- debugBorder draws a 1px blue outline around the diagram bbox for
  layout debugging.
Two iOS Safari quirks made the gesture render outside the cell's blue
debug border on iPhone while Chrome looked fine:

- Imperative `setAttribute('viewBox', …)` in `onRef` didn't always
  trigger a repaint on iOS — the SVG would briefly render with user
  space = pixel space and the post-mount attribute update wouldn't
  flush. Compute the viewBox during render for straight gestures and
  pass it as a JSX prop so React owns the attribute. Also set
  `preserveAspectRatio` explicitly. The rounded/rdld branch still uses
  `getBBox` via the ref since it needs measured geometry.

- `height: 100%` on the GestureDiagram span and SVG depended on the
  parent div's `aspect-ratio: 1/1` establishing a definite height for
  percentage-height descendants. iOS Safari doesn't always treat that
  as definite, so the span collapsed while the SVG fell back to its
  intrinsic ratio and rendered taller than the span. Drop the
  percentage heights in `fillContainer` mode and let the SVG's intrinsic
  ratio (from its square viewBox) drive both heights.

Also force `display: block` on the wrapping `<td>`/`<div>` so they
don't sit in a table-cell layout when their `<tbody>`/`<tr>` parents are
overridden to grid/flex.
…ction defaults

Renders the Mobile Command Universe gesture diagrams with the dialed-in
fillContainer + outlined-wide chevron + two-color gradient treatment, using
the values previously tuned through the (now-removed) dev debug overlay as
inlined production defaults. Keeps the existing isTouch gate; the overlay had
dropped it only so the controls had something to drive on desktop.
Now that the Command Universe values are baked in, drop the debug-only and
redundant knobs:
- Remove the `debugBorder` prop entirely (no callers; was just the dev blue box).
- Drop `chevronApexAngle`/`chevronSize` from the grid call site since they
  equalled the component defaults (no behavior change). Kept as props.
- Mark `chevronApexAngle`/`chevronSize` as only affecting `arrowhead='outlined-wide'`,
  and clarify `cornerRadius` only applies when `gradient` is supplied.

Keeps the genuinely reusable knobs (fillContainer, gradient, glow, cornerRadius,
tipExtension) as real props.
The chevron apex angle (80°) and size (2.2× stroke) were only ever rendered with
their dialed-in values, so replace the chevronApexAngle/chevronSize props with
CHEVRON_APEX_ANGLE/CHEVRON_SIZE module constants. Also finish documenting why
cornerRadius requires a gradient (single continuous path vs per-segment paths).
The CommandUniverse grid inherited <table>/<tbody>/<tr>/<td> markup from
CommandTable, but it renders a 2-column grid of cards, not tabular data.
Every element overrode its native display (grid/flex/block), and the
orphaned <td> needed an explicit display:block to stop iOS Safari from
falling back to table-cell layout. Swap all of them for <div>s, which
drops that workaround entirely while leaving the intended flex/grid
layout untouched.
The render path between tipExtension and computedViewBox has several
prop-gated blocks. Lead each with what triggers it (tipExtension,
gradient, fillContainer, arrowhead) so the conditional flow is scannable.
Comments only; no logic change.
Revert the bake-in from 0262c8c: the chevron apex angle and size are
real geometry knobs, so expose them as props (defaults 80 / 2.2) rather
than hiding them as module constants that imply the values are fixed.
The grid call site passes them explicitly again so it honestly declares
the chevron geometry it depends on instead of relying on matching
defaults.
@fbmcipher fbmcipher requested a review from BayuAri June 26, 2026 00:24
@BayuAri

This comment was marked as resolved.

@fbmcipher

Copy link
Copy Markdown
Collaborator Author

@BayuAri I think this I've seen this error before. Could you try a fresh reinstall (i.e. deleting node_modules with rm -r node_modules and then reinstalling with yarn install)? I don't think it is related to this PR.

@BayuAri

BayuAri commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

@fbmcipher
You are right.
It is gone after i restart the server and vs code.
Sorry for the false alarm

@BayuAri BayuAri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is great gradient color and smooth clean design.
No issue from me.

@raineorshine

raineorshine commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

[TypeScript] Cannot find module 'webview-background' or its corresponding type declarations.

@BayuAri This means the local webview-background package was not built correctly. It is usually built automatically after running yarn, but you can also trigger it manually with yarn build:packages.

I would recommend not getting in the habit of deleting node_modules. It's very rare that node_modules would become corrupted.

@raineorshine raineorshine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the submission!

chevronPoints and computedViewBox should probably be in a useMemo and/or custom hook. GestureDiagram gets a bit too big if we stuff all the new functionality into the existing component.

Please add visual test coverage of all the new GestureDiagram props to https://github.com/cybersemics/em/blob/afc7fa022b4a25710216f40c7efa915dc756a811/src/components/modals/TestGestureDiagram.tsx and update the snapshot. Since chevronApexAngle, chevronSize, cornerRadius, tipExtension, etc seem to all be set and used together, I'm fine with a minimal number of <GestureDiagram> instances that combine all the new props. No need to test them orthogonally and all a lot of test cases that are not actually used.

// per-segment paths produce at their overlapping rounded joins, and applies to straight,
// `rounded`, and the special-case 'rdld' help glyph. When unset, the per-segment <style> path runs
// instead (see GradientStyleBlock).
const useSingleGradient = !!gradient

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reserve useX naming for hooks.

This variable probably shouldn't exist anyway. Just use gradient/!gradient as needed.

// Renders the chevron as a separate <path> whose apex sits at the gesture's natural tip, with two
// legs splaying BACKWARD (≈65° apex by default). Independent of `gradient`: the chevron can use
// the gesture's gradient or fall back to a solid color.
const useChevronArrowhead = arrowhead === 'outlined-wide' && path !== 'rdld' && !rounded && positions.length >= 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reserve useX naming for hooks.

// concatenate the per-segment arcs into one continuous path so segment joins
// disappear and the gradient flows as one stroke.
<path
d={(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested IIFEs are usually a sign that you need to break this down into smaller components/hooks.

chevronSize={2.2}
cornerRadius={12}
tipExtension={28}
gradient={{ from: 'rgba(88, 181, 212, 0.45)', to: 'rgba(255, 255, 255, 1)' }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want to use a theme color?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restyle the gesture diagram in the Command Universe

3 participants