Restyle the Mobile Command Universe gesture diagrams#4462
Conversation
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.
This comment was marked as resolved.
This comment was marked as resolved.
|
@BayuAri I think this I've seen this error before. Could you try a fresh reinstall (i.e. deleting |
|
@fbmcipher |
BayuAri
left a comment
There was a problem hiding this comment.
It is great gradient color and smooth clean design.
No issue from me.
@BayuAri This means the local I would recommend not getting in the habit of deleting |
raineorshine
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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={(() => { |
There was a problem hiding this comment.
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)' }} |
There was a problem hiding this comment.
Want to use a theme color?
Closes #4186.
This PR reworks how command gesture diagrams are rendered on the Command Universe ("Commands") surface. This involved giving
GestureDiagramadditional 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
GestureDiagrama 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
GestureDiagramThe bulk of the work. All additions to
GestureDiagramare opt-in props with defaults that preserve the existing rendering everywhere elseGestureDiagramis used.Two-color gradient (
gradient) – fades the entire stroke between two colors along the path. Critically, when agradientis 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 ofGestureDiagram.**Wide chevron arrowhead (
arrowhead='outlined-wide'), matching the design. Its geometry is exposed viachevronApexAngle(default 80°) andchevronSize(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 whengradientis 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, theoutlined-widearrowhead 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 totrueto preserve current behavior). Our new design does not have a glow around gestures.iOS Safari centering fix
Straight gestures now compute their
viewBoxduring render (so React owns the attribute) instead of setting it imperatively in anonRefcallback. TheonRefmethod 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
GestureDiagramintoCommandUniverseGridItemwith the production gradient / chevron / fit-to-container settings.<table>/<tbody>/<tr>/<td>fromCommandTable, but it renders a 2-column grid of cards, not a table, so we use divs instead.To-Do