Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-3522Done

15 3D / Immersive leaves 199 of 280 interactive controls flat — the physical-key rule matches `rounded-(--radius-btn)` only, so Plan with AI, Re-plan, the 3-dots menu and the roadmap search have no depth (motir-core)

Type · implementation defect (CSS selector coverage). Not a planning mistake — the card that shipped the style asked for exactly the right thing; the implementation bound "every button" to ONE utility class. No lesson owed.

Parent · parentless at the ROOT. Found out-of-band (manual dogfooding), so there is no finding story to carry a blocked_by edge into; the discovery subtask MOTIR-1052 and its whole ancestor chain (story MOTIR-804, Epic 7 MOTIR-464) are done, so the epic rung is disqualified and the ladder falls through to a root sibling. relates_to MOTIR-1052.

Discovered in · MOTIR-1052 (7.3.39 Style: 3D / Immersive), shipped in PR #1296. Reported by Yue while running data-style="3d-immersive" in the app.


Root cause / fix

[data-style='3d-immersive'] gives a control its "physical key" thickness through exactly two selectors, at packages/design-system/theme.css:1276-1277 (verified on origin/main @ 052965b3):

[data-style='3d-immersive'] .rounded-\(--radius-btn\),
[data-style='3d-immersive'] button.rounded-\(--radius-input\) { box-shadow: 0 3px 0 0 …; }

The rule keys on the compiled radius utility class, not on the element being interactive. The block's own comment shows the sweep that produced it: the author enumerated the ~35 hand-rolled rounded-(--radius-btn) <button>s and widened the rule to cover that class. Every control the app builds on a different radius utility was never in the sample, so it gets no box-shadow, no base edge, and no press-down — while the panel it sits on floats on a four-layer --shadow-card. That is exactly the "half-3D" failure docs/styles/3d-immersive.md §8 names as its first ❌ ("Leave buttons flat — a flat button beside floating 3D cards is the inconsistency that reads as 'half-3D'"), and it contradicts the shipped registry copy in packages/design-system/src/theme/styles.ts:314"PHYSICAL buttons with real thickness … Nothing is flat."

Measured (not estimated)

A JSX-opening-tag scan (regex over <button|<a|<Link|<input|<textarea|<select and the rounded-* utility in the same tag) across app/, components/ and packages/design-system/src at origin/main @ 052965b3 returns 300 tags carrying a radius utility — 280 of them interactive controls (excluding the 20 text fields):

radius utilitytagsreached by the rule?
rounded-(--radius-btn)59 (button 32 · Link 21 · a 6)✅ raised
button.rounded-(--radius-input)6✅ raised
rounded-(--radius-control)180 (button 143 · Link 23 · a 14)box-shadow: none
rounded-(--radius-badge)12 (button 8 · Link 4)box-shadow: none
rounded-full6box-shadow: none
rounded-sm1box-shadow: none
rounded-(--radius-card)16 (button 8 · Link 8)⚠️ floats on --shadow-card (panel rule) but never presses
text fields (input/select/textarea + the Input primitive's wrapper div)20 + primitive❌ no treatment of any kind

65 raised · 16 float-but-never-press · 199 with no depth at all.

Each control Yue reported, with its actual class

Computed box-shadow measured in headless Chromium against the real compiled token layer (@tailwindcss/postcss over packages/design-system/theme.css, data-style="3d-immersive" data-palette="motir"):

#controlfileradius classcomputed box-shadow
1"Plan with AI" pillcomponents/planning/PlanWithAILauncher.tsx:88rounded-(--radius-badge)its own inline hero glow only — no base edge
2"Building in public" header linkapp/(authed)/_components/build-in-public/BuildingInPublicHeaderLink.tsx:63rounded-(--radius-badge)none
3"Re-plan" entrance (item detail)components/planning/WorkItemPlanEntrance.tsx:123rounded-(--radius-badge)none
43-dots menu trigger (item detail)components/issues/actions/WorkItemActionsMenu.tsx:59 (MENU_TRIGGER_CLASS)rounded-(--radius-control)none
5"Search the roadmap" inputcomponents/planning/ProjectRoadmapCanvas.tsx:889packages/design-system/src/components/ui/Input.tsx:114div.rounded-(--radius-input)none
+"Plan with AI" floating orb (FAB)components/planning/PlanWithAIFab.tsx:136rounded-fullnone

Controls on rounded-(--radius-btn) measured in the same render for contrast: rgb(69,52,179) 0 4px 0 0, rgba(15,15,15,.32) 0 7px 12px -3px (primary) / rgb(200,196,190) 0 3px 0 0, … (neutral). Side-by-side render attached to this card's discussion.

Note on #2: the pre-public BuildInPublicButton (…/BuildInPublicButton.tsx) is on rounded-(--radius-btn) and is raised. The flat one is its post-public successor in the same header slot, the BuildingInPublicHeaderLink badge — and the placement="drawer" variants of both.

Three things a blanket widening would get WRONG — the reason this is not a one-line selector edit

  1. ~54 of the 180 --radius-control controls are menu / option ROWS inside popovers (WorkItemActionsMenu's ITEM_CLASS, combobox options, sidebar rows — w-full / role="menuitem" / role="option"). A 3px base edge on every row of an open menu is worse than flat. ~62 are standalone icon buttons that should be keys; the remaining ~64 need reading. The fix needs a hook that separates the two — a data- attribute on the primitives, or a structural exclusion under [data-surface='popover'] / [data-menu-surface] — not a bare class rule.
  2. The "Plan with AI" pill sets boxShadow in an inline style prop (HERO_STYLE, PlanWithAILauncher.tsx:59-69). An inline declaration beats any style-layer rule, so widening the selector alone leaves this one control unchanged. It needs the base edge folded into HERO_STYLE or a variable the style layer can re-point.
  3. The style block deliberately leaves --radius-pill untouched so avatars / dots / the spinner stay circular — but the FAB is a circular button. A rounded-full rule must raise the button without touching the non-interactive circles.

The text-field half is UNSPECIFIED, not deliberately flat

theme.css:1272-1275 scopes the input arm to button. explicitly "so a real <input>/<textarea> text field (same radius) is NEVER raised" — correct, a text field should not stick up. But nothing was put in its place: docs/styles/3d-immersive.md §4 says only "Input / control — On the face; generous rounded dimensional silhouette", and the shipped result is a 1px-bordered rectangle with box-shadow: none on a page where every panel carries a four-layer float. The skeuomorphic counterpart of a raised key is a recessed field (an inset shadow, pressed INTO the surface); §4 never decides that, so the fix has to amend the spec doc before it can implement it. Settle it in docs/styles/3d-immersive.md §4/§6a as part of this card — it is a two-line amendment to an existing design asset, not a new surface.


Acceptance criteria

  • docs/styles/3d-immersive.md §4's plane ladder states, per control family, whether it is a raised key, a recessed field, or deliberately flat — and §4 gains the text-field decision (recessed vs on-the-face) that it currently leaves unwritten. The §4 table and the [data-style='3d-immersive'] CSS agree row for row.
  • Under data-style="3d-immersive", getComputedStyle(el).boxShadow !== 'none' for each of the six controls named in the table above, measured on the real compiled token layer.
  • Menu and option rows (role="menuitem" / role="option", the w-full rows inside [data-surface='popover'] and [data-menu-surface]) still compute box-shadow: none — the widening must not raise them.
  • [data-variant='ghost'] still computes box-shadow: none; the quiet button stays flat.
  • The :active:not(:disabled) press rule covers the same set the resting rule does — every newly-raised control presses down onto its base, none is left with a resting edge and no press.
  • A guard test in tests/theme/ enumerates the radius utilities the codebase emits on interactive tags (the scan above, run in-test rather than hard-coded) and asserts each one is classified — raised, recessed, or flat-with-a-named-reason. A new radius utility appearing on a control with no classification fails the test. Assert the CLASSIFICATION, not a count: the tag counts in this card's table are a 052965b3 reading and will drift.
  • No --color-* / --el-* colour token is added or changed by the fix — the colour axis stays disjoint (the existing material-rule test still passes).
  • pnpm lint, pnpm typecheck and the theme suite are green on the branch.

Context refs

  • packages/design-system/theme.css:1258-1322 — the physical-key + press rules (the defect) and their comment describing the sweep that produced them.
  • packages/design-system/theme.css:1014-1077 — the [data-style='3d-immersive'] token block: --radius-btn: 14px, --radius-input: 14px, --radius-control: 12px, --radius-badge: 9999px, --radius-card: 20px.
  • packages/design-system/theme.css:1121-1125 — the global panel float on .rounded-\(--radius-card\), the pattern the button rule was modelled on.
  • docs/styles/3d-immersive.md — §4 plane ladder, §6a static depth, §8 ❌ "Leave buttons flat". The design asset this card amends.
  • packages/design-system/src/theme/styles.ts:314-340STYLE_REGISTRY['3d-immersive'], whose dimensions.components promises "PHYSICAL buttons … Nothing is flat."
  • packages/design-system/src/components/ui/Button.tsx:29,79 — the primitive: rounded-(--radius-btn) + the data-variant hook the filled/ghost overrides key on.
  • packages/design-system/src/components/ui/Input.tsx:110-128 — the field shell (data-surface="input", border only, no shadow in any style).
  • The six reported controls: components/planning/PlanWithAILauncher.tsx:59,88 · app/(authed)/_components/build-in-public/BuildingInPublicHeaderLink.tsx:63 · components/planning/WorkItemPlanEntrance.tsx:123 · components/issues/actions/WorkItemActionsMenu.tsx:59 · components/planning/ProjectRoadmapCanvas.tsx:889 · components/planning/PlanWithAIFab.tsx:136.
  • tests/theme/ — the existing theme suite; tests/theme/styleRegistry.test.ts is today the ONLY test that names this style, and it asserts the registry entry's name only. Nothing asserts the style's rendered coverage, which is why the gap shipped.

Resolution

Open.