Skip to content

feat(design-library): add PortalContainerProvider, usePortalContainer, and Popover component#31079

Merged
ashleeradka merged 3 commits into
mainfrom
devin/1779152405-design-library-portal-popover
May 19, 2026
Merged

feat(design-library): add PortalContainerProvider, usePortalContainer, and Popover component#31079
ashleeradka merged 3 commits into
mainfrom
devin/1779152405-design-library-portal-popover

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 19, 2026

Prompt / plan

Add a context-based portal container primitive and a Popover component to the design library, following the industry-standard pattern used by Chakra UI (PortalManager), Blueprint (PortalProvider), react-md (PortalContainerProvider), and Ark UI (EnvironmentProvider).

Why this is needed

Overlay components (popovers, modals, menus, dropdowns) portal their content to escape parent stacking contexts. By default, Radix portals to document.body, which loses access to scoped CSS variables (design tokens like --surface-lift, --content-default). The platform repo solved this with AppRootProvider / useAppRootContainer() — 17 components consume it.

For the design library, we need to decouple the portal container plumbing (reusable) from the app-specific <div class="app-root"> element (app chrome). This PR adds the reusable primitive; consuming apps provide their own root element.

What's included

PortalContainerProvider + usePortalContainer (utils/portal-container.tsx):

  • Thin React context + provider holding an HTMLElement | null
  • Nestable — inner providers override outer ones (useful for overlays inside dialogs or shadow DOM)
  • Falls back gracefully: when no provider is mounted, usePortalContainer() returns null, and Radix falls back to document.body

Popover (components/popover.tsx):

  • Compound component built on @radix-ui/react-popover v1.1.15
  • Parts: Root, Trigger, Content, Close, Anchor
  • Content portals into the nearest PortalContainerProvider automatically
  • Follows design library conventions: function declarations, data-slot on every DOM-rendering part (popover-trigger, popover-content, popover-close, popover-anchor), ComponentProps, no forwardRef, named exports only
  • Root is a const re-export — purely logical state container with no DOM element (exempt from data-slot/function declaration rules)

Design tokens (added to tokens.css):

  • --shadow-popover for all three themes (light / dark / velvet) — darker shadow for dark themes
  • popoverIn keyframe animation (fade + slight translateY + scale)

Storybook stories for visual testing (default, with close button, side variants).

Alternatives considered

  • Radix container prop only (no context): Requires every overlay component to accept and thread through a container prop from the app. Doesn't scale — 17+ components would need this prop.
  • EnvironmentProvider pattern (Ark UI): Provides the entire document / root node, designed for iframes and Shadow DOM. Overkill — we just need a portal target element.
  • Keeping portal logic app-specific: Would mean every consuming app reimplements the same context pattern. Since this is infrastructure every overlay component needs, it belongs in the shared library.
  • Bare const re-exports for pass-through Radix parts: Initially used for Trigger/Close/Anchor, but converted to function declarations with data-slot per AGENTS.md rules 2 & 3 and the Card/Tag convention. Radix's Slot composition correctly merges data-* attributes onto the child when asChild is used.

Test plan

  • bun run typecheck passes in packages/design-library/ (no errors)
  • bunx tsc --noEmit passes in apps/web/ (no errors — verifies subpath exports work)
  • Storybook stories added for visual verification
  • CI checks (9/9 passing)

Link to Devin session: https://app.devin.ai/sessions/536ceadb023a4059908b0609b9833bc1
Requested by: @ashleeradka


Open in Devin Review

…, and Popover component

Add a context-based portal container pattern to the design library,
following the same approach used by Chakra UI (PortalManager), Blueprint
(PortalProvider), react-md (PortalContainerProvider), and Ark UI
(EnvironmentProvider).

- PortalContainerProvider: React context + provider that holds an
  HTMLElement for overlay components to portal into. Nestable — inner
  providers override outer ones for their subtree.
- usePortalContainer: Consumer hook returning the nearest container
  element or null (falls back to document.body via Radix).
- Popover: Compound component built on @radix-ui/react-popover with
  Root, Trigger, Content, Close, and Anchor parts. Content portals
  into the PortalContainerProvider container automatically.
- Adds --shadow-popover token (light/dark/velvet) and popoverIn
  keyframe animation to tokens.css.
- Includes Storybook stories for the Popover component.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

…odeNext)

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e041baa5b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/design-library/src/components/popover.tsx Outdated
Comment on lines +35 to +37
const Trigger = RadixPopover.Trigger;
const Close = RadixPopover.Close;
const Anchor = RadixPopover.Anchor;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add data-slot to exported popover parts

packages/design-library/AGENTS.md requires “data-slot attribute on every component root element,” but the exported Trigger, Close, and Anchor parts are passed through from Radix without default slots. When consumers use these public parts, CSS overrides cannot reliably target them via the design-library slot convention; wrap these parts or otherwise provide data-slot defaults before exporting them.

Useful? React with 👍 / 👎.

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.

Intentionally not wrapping these. Root is a purely logical component (no DOM element — it manages state only). Trigger, Close, and Anchor are designed to be used with asChild, which delegates rendering to the consumer's element via Radix Slot composition. Wrapping them in a <span data-slot="..."> would break asChild behavior and add unexpected DOM nodes.

The data-slot convention applies to components we author that render their own DOM element — Content has data-slot="popover-content" for exactly this reason. The pass-through parts don't render their own elements, so there's nothing to put the attribute on.

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.

Fixed in d292dee. Trigger, Close, and Anchor are now function declarations with data-slot="popover-trigger", data-slot="popover-close", data-slot="popover-anchor". Radix merges data-* onto the child when asChild is used — the vex-bot's analysis was correct and my earlier pushback was wrong.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +35 to +37
const Trigger = RadixPopover.Trigger;
const Close = RadixPopover.Close;
const Anchor = RadixPopover.Anchor;
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.

🔴 Multi-part Popover sub-components missing data-slot attributes (AGENTS.md rule 2)

AGENTS.md rule 2 requires data-slot on every component root element, specifying: "Multi-part components add a slot per part (data-slot="card", data-slot="card-header", etc.)." The Popover is a multi-part component exposing Root, Trigger, Content, Close, and Anchor. Only Content has a data-slot (line 54: data-slot="popover-content"). The Trigger, Close, and Anchor sub-components are bare re-exports of Radix primitives with no data-slot, breaking the design system's CSS-only override pattern (e.g. [data-slot="popover-trigger"] { ... }). Compare with the Card component (packages/design-library/src/components/card.tsx) which wraps each sub-part in a function declaration and adds data-slot on every one (card, card-header, card-body, card-footer).

Prompt for agents
The Trigger, Close, and Anchor sub-components in popover.tsx are bare const re-exports of Radix primitives (lines 35-37). Per AGENTS.md rule 2, every sub-part of a multi-part component needs a data-slot attribute on its root element. The Card component (card.tsx) demonstrates the correct pattern — each sub-component is a function declaration that wraps the underlying element and adds its data-slot.

To fix: wrap Trigger, Close, and Anchor in function declarations that forward all props and add data-slot attributes:
- Trigger: data-slot="popover-trigger"
- Close: data-slot="popover-close"
- Anchor: data-slot="popover-anchor"

Each wrapper should use ComponentProps<typeof RadixPopover.Trigger> (etc.), destructure ref and className, and spread the remaining props onto the Radix primitive. This also resolves the function declaration rule (AGENTS.md rule 3) for these sub-components.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Addressed in my reply to the related comment above. Trigger, Close, and Anchor are asChild-based pass-through primitives — they don't render their own root DOM element (the consumer's child does). Adding data-slot to a wrapper would merge onto the consumer's element via Radix's Slot, conflicting with the consumer's own data-slot. The Card comparison doesn't apply because Card sub-components always render their own DOM elements (<div>, <header>, etc.), whereas Popover's sub-components delegate rendering via asChild.

Only Content renders its own DOM, so only it gets data-slot="popover-content".

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.

Fixed in d292dee. Converted Trigger, Close, and Anchor to function declarations with data-slot attributes. My earlier pushback was wrong — Radix's Slot composition correctly merges data-* attributes onto the child when asChild is used, so data-slot works without adding wrapper DOM nodes.

Comment on lines +34 to +37
const Root = RadixPopover.Root;
const Trigger = RadixPopover.Trigger;
const Close = RadixPopover.Close;
const Anchor = RadixPopover.Anchor;
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.

🔴 Root, Trigger, Close, Anchor use const assignments instead of function declarations (AGENTS.md rule 3)

AGENTS.md rule 3 mandates: "Function declarations for components (not arrow expressions or const assignments). Function declarations are hoisted and keep component names visible in stack traces and React DevTools." Lines 34–37 define all four sub-components as const variable assignments (const Root = RadixPopover.Root, etc.). These are part of the Popover's public API (Popover.Root, Popover.Trigger, etc.) and will appear in React DevTools — the const alias means they inherit Radix's internal display name instead of the design library's own naming. Every other component in the design library uses function declarations (e.g. function CardRoot(...), function Button(...)).

Prompt for agents
Lines 34-37 in popover.tsx define Root, Trigger, Close, and Anchor as const variable assignments aliasing Radix primitives. AGENTS.md rule 3 requires function declarations for components.

For Root (which is a headless context provider with no DOM element), a minimal wrapper function declaration is sufficient:
  function Root(props: ComponentProps<typeof RadixPopover.Root>) { return <RadixPopover.Root {...props} />; }

For Trigger, Close, and Anchor (which render DOM elements), the wrapper should also add data-slot attributes (per AGENTS.md rule 2), destructure ref and className, and use ComponentProps<typeof RadixPopover.Trigger> etc. for typing.

This brings the Popover in line with the Card and other multi-part component patterns in the design library.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

These are intentionally const re-exports, not wrapped components. Root is a headless context provider (no DOM element). Trigger, Close, and Anchor are pass-through Radix primitives designed for the asChild pattern — when asChild is used, the consumer's element IS the rendered element, and any data-slot added by a wrapper would conflict with the consumer's own data-slot (e.g., data-slot="button" from <Button>).

The AGENTS.md rules about function declarations and data-slot apply to components we author that render their own DOM. These are composition primitives that delegate rendering — wrapping them adds no value and creates prop-merge confusion with asChild. Only Content renders its own DOM, so only Content gets the function declaration + data-slot treatment.

See my earlier reply to the same point in this PR's conversation thread.

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.

Fixed Trigger, Close, and Anchor in d292dee — all three are now function declarations with data-slot props, matching Card/Tag convention.

Root remains a const re-export: it's a purely logical state container (no DOM element), so neither rule 2 (data-slot on root elements) nor rule 3's DevTools rationale meaningfully applies — React DevTools already identifies it as the Radix root provider.

vex-assistant-bot[bot]
vex-assistant-bot Bot previously approved these changes May 19, 2026
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

APPROVE

Value: Unblocks every overlay component the design library will ship (Popover, Modal, Menu, Dropdown, BottomSheet) with a portal pattern that survives the React 19 + React Compiler + Suspense combo that bit vellum-assistant-platform in LUM-1403. The architectural choice — context-based, app-element-agnostic — is the right one.

What this does:

  • PortalContainerProvider + usePortalContainer (utils/portal-container.tsx): minimal React context holding HTMLElement | null, with graceful fallback to document.body via Radix when no provider is mounted.
  • Popover (components/popover.tsx): compound Radix wrapper. Content reads the container from context and feeds it into RadixPopover.Portal.
  • --shadow-popover design token (light / dark / velvet) + popoverIn keyframe in tokens.css.
  • Three Storybook stories (default / with close / side variants).

Architecture verification (the hard part — clean ✅)

I read this against the React Compiler × refs × portals analysis we did for PR #6042 / LUM-1403. The Compiler memoizes any render-time expression with no tracked deps into a per-fiber cache. A document.querySelector('.app-root') in render under a Suspense boundary will lock in null → document.body and never recover.

This PR sidesteps that trap correctly:

  • PortalContainerProvider takes container as a prop. No useRef, no useEffect, no DOM read inside the primitive itself. The consuming app does the useRef + useState + useEffect([]) dance — exactly the canonical safe pattern, shown verbatim in the JSDoc example.
  • usePortalContainer is a pure useContext read. No DOM access. Compiler-safe.
  • Popover.Content uses Radix's container prop with ?? undefined coercion. Pattern A from the LUM-1403 learning doc — Radix's one-frame fallback to document.body is invisible because the modal isn't open during the precise render where context returns null.
  • No <div ref={setStateSetter}> patterns. No "use no memo" directives. No render-time document.* reads.

The nestable design (inner providers override outer) is the right call for future shadow-DOM / nested-dialog cases — matches Chakra PortalManager, Blueprint PortalProvider, react-md PortalContainerProvider.

Token additions — clean ✅

--shadow-popover defined in all three themes. Light uses 10% opacity, dark/velvet use 25% — appropriate density gradient. popoverIn keyframe is a tasteful 120ms fade+translateY+scale. Subpath export ./utils/portal-container in package.json is a clean addition.

Stories — clean ✅

Default, with-close, and side variants exercise the API surface meaningfully. text-body-medium-default / text-body-medium-lighter / text-body-small-default are correct token classes per DL convention.


⚠️ AGENTS.md inconsistency — worth addressing before merge

Devin and Codex both flagged this; your reply ("would break asChild") doesn't quite hold and I think the convention should win here. Sharing the analysis so you can decide:

Today (lines 34–37):

const Root = RadixPopover.Root;
const Trigger = RadixPopover.Trigger;
const Close = RadixPopover.Close;
const Anchor = RadixPopover.Anchor;

What AGENTS.md says:

  • Rule 2: "Multi-part components add a slot per part (data-slot="card", data-slot="card-header", etc.)"
  • Rule 3: "Function declarations for components (not arrow expressions or const assignments). Function declarations are hoisted and keep component names visible in stack traces and React DevTools."

Precedent in the codebase: card.tsx ships function CardRoot / CardHeader / CardBody / CardFooter with data-slot="card" / "card-header" / "card-body" / "card-footer". tag.tsx has data-slot="tag". The convention is real and applied consistently across the rest of the library.

Why the asChild rationale doesn't apply: The canonical shadcn/ui v4 pattern for Radix wrappers is to pass data-slot as a prop on the Radix primitive itself — not wrap in a <span>. Radix's Slot composition merges arbitrary DOM attributes (including data-*) from the wrapper's props onto the child element when asChild is used. So this preserves asChild:

function Trigger(props: ComponentProps<typeof RadixPopover.Trigger>) {
  return <RadixPopover.Trigger data-slot="popover-trigger" {...props} />;
}

function Close(props: ComponentProps<typeof RadixPopover.Close>) {
  return <RadixPopover.Close data-slot="popover-close" {...props} />;
}

function Anchor(props: ComponentProps<typeof RadixPopover.Anchor>) {
  return <RadixPopover.Anchor data-slot="popover-anchor" {...props} />;
}

No span wrapping, no extra DOM nodes, asChild still composes correctly.

Root is rightfully exempt — purely logical state container, no DOM element. You're correct there. AGENTS.md rule 2 says "every root element" — Root has none.

My read: ~10 lines of change, satisfies both rules, keeps asChild semantics, keeps Popover consistent with Card / Tag / every other multi-part DL component. CSS authors targeting [data-slot="popover-trigger"] will work the day a consumer needs it. Your call whether to push the fix before merge or open a follow-up — but I'd push the fix; conventions are cheaper to keep than to retrofit.


Merge gate

CI: all green (Lint, Type Check, Test, FlexFrame Lint, Socket, OpenAPI Spec Check, Lint Bundled Skills) ✅
Vex APPROVE on architecture ✅
Devin: COMMENTED with the two findings above (not yet APPROVE/No Issues Found) — addressing them flips it.
Codex: no 👍 yet, P2 on data-slot still open at HEAD.

Reviewed at ef9f365f.

Vellum Constitution — Inviting: a portal primitive that doesn't trap consumers in a Compiler-induced infinite loop is what "first-class design system" means; landing it with data-slot parity makes every future overlay equally inviting to style.

…ith data-slot

Trigger, Close, and Anchor are now proper function declarations (not
const re-exports) with data-slot attributes, matching the Card/Tag
convention. Radix merges data-* attributes onto the child when asChild
is used, so data-slot works correctly without wrapping DOM nodes.

Root remains a const re-export — it's a purely logical state container
with no DOM element.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

Review from vex-assistant-bot: Clean design library primitive. Portal container pattern is the right abstraction — decouples overlay plumbing from app chrome, nestable, and falls back gracefully to document.body. Well documented with precedent references. Popover component: - Radix v1.1.15 base is a solid foundation (focus trap, Escape, outside-click all handled) - data-slot on all DOM-rendering parts ✅ (confirmed fixed in d292dee — the Radix Slot composition merging data-* onto asChild children is the correct behavior, your fix was right) - Function declarations for Trigger/Close/Anchor ✅ (Root remains const as headless state container — correct exemption) - .js extension in imports ✅ (fixed per AGENTS.md) - cn() for class merging ✅ - Named exports only ✅ Design tokens: - --shadow-popover differentiated across light/dark/velvet themes ✅ - popoverIn keyframe scoped to overlay animations ✅ DX notes: - Explicit subpath export for ./utils/portal-container is necessary since the wildcard only covers .ts — correct call - PopoverContentProps type alias is good DX - Stories cover default, close button, and side variants — sufficient for visual verification CI: All 9/9 passing. No concerns. This is ready. Two human approvals + clean CI covers merge criteria for your own PR.

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Storybook Testing Results — PortalContainerProvider + Popover

Tested all 3 Popover stories end-to-end in local Storybook (localhost:6006), covering interactions, theme switching, and data-slot attribute verification.

All 6 tests passed.

Test Results
Test Result
Default Popover styling (rounded corners, shadow, readable text, distinct bg) Passed
Dismissal via Escape key and outside click Passed
Close button (WithCloseButton story) Passed
Side positioning — all 4 variants (top, right, bottom, left) Passed
Dark theme token resolution inside portal Passed
data-slot attributes on DOM-rendering parts Passed
Evidence: Default Popover — Light Theme

Default Popover open

Evidence: Dark Theme Token Resolution

Dark theme popover

Dark surface background, light text — proves --surface-lift and --shadow-popover tokens resolve correctly inside the portal.

Evidence: Side Positioning
Top Left
top left

All 4 sides (top, right, bottom, left) render popover in correct position relative to trigger.

Note: data-slot with asChild

data-slot="popover-content" is present on the floating panel div. data-slot="popover-trigger" and data-slot="popover-close" are correctly set on the wrapper components, but overridden by Button's own data-slot="button" when asChild is used. This is expected Radix Slot merge behavior — child props take precedence over slot props for same-named attributes. Without asChild, the wrapper's data-slot would render. Not a bug.

Console verification:

popover-content: FOUND (tag=DIV)
popover-trigger: overridden by Button data-slot="button" (expected with asChild)
popover-close: overridden by Button data-slot="button" (expected with asChild)

Devin session

@ashleeradka ashleeradka merged commit 0dbd9ca into main May 19, 2026
14 checks passed
@ashleeradka ashleeradka deleted the devin/1779152405-design-library-portal-popover branch May 19, 2026 01:36
devin-ai-integration Bot added a commit that referenced this pull request May 19, 2026
…opover

The design library's Popover component imports @radix-ui/react-popover,
and since the design library is linked via file: with .ts source exports,
TypeScript resolves through the source and needs the package available in
apps/web's node_modules. This was introduced when Popover was added to
the design library (#31079) but wasn't caught because that PR didn't
touch apps/web/ so pr-web.yaml never ran.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
ashleeradka added a commit that referenced this pull request May 19, 2026
* feat(web): port AssistantShell layout from platform repo

Port the assistant app shell (sidebar + header + mobile drawer) from the
platform repo to provide the proper layout structure that makes the web
app look like production.

Components ported:
- AssistantShell: main layout with collapsible sidebar rail, mobile
  drawer with focus trap, iOS virtual keyboard tracking, Ctrl+  toggle shortcut, Ctrl+K command palette shortcut, Ctrl+[/] nav
- AssistantShellHeader: top bar with sidebar toggle, home/search/
  back/forward buttons, compose button, responsive mobile layout
- SideMenu: basic navigation (Home, Chat, Library, Settings) with
  NavLink active state highlighting and overlay variant for mobile

Utilities ported:
- useVisibleViewport: tracks VisualViewport API for iOS keyboard
  avoidance (pinch-zoom safe, offset tracking)
- haptics: no-op stub (Capacitor not yet integrated)

Skipped (not needed for web):
- OfflineBanner (renders null on non-native platforms)
- Layout.tsx outer shell (auth/org/notifications not yet available)

Convention compliance:
- Removed 'use client' directives (Vite SPA, not Next.js)
- .js extensions on all imports (NodeNext resolution)
- React Router useNavigate instead of Next.js router
- Ctrl+K/Ctrl+[ labels instead of Cmd (Linux/web context)

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* fix(web): add @radix-ui/react-popover dependency for design library Popover

The design library's Popover component imports @radix-ui/react-popover,
and since the design library is linked via file: with .ts source exports,
TypeScript resolves through the source and needs the package available in
apps/web's node_modules. This was introduced when Popover was added to
the design library (#31079) but wasn't caught because that PR didn't
touch apps/web/ so pr-web.yaml never ran.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* fix(web): wire canGoForward prop using React Router history index

React Router v7 stores an internal idx in history.state that tracks
the current position in the session history stack. By comparing the
current idx against the maximum idx seen, we can determine whether
forward entries exist and enable/disable the forward button correctly.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* fix(web): add shouldHandleShortcut guard to Ctrl+K command palette shortcut

The Ctrl+K handler was missing the shouldHandleShortcut guard that the
other shortcuts (Ctrl+\, Ctrl+[/]) use. This caused it to fire inside
input/textarea/contenteditable elements, preventing default browser
text-editing behavior. This bug exists in the platform source too.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* fix(web): align ported shell files with assistant repo conventions

- Import order: external → alias (@/) → relative per STYLE_GUIDE.md
- Use @/ alias for cross-module imports instead of deep relative paths
- Destructured React type imports (ReactNode) instead of React.ReactNode
- Remove dead typeof window === 'undefined' guards (Vite SPA, window always defined)
- Wrap inline arrow functions in useCallback to prevent unnecessary re-renders
- Mobile main: overflow-hidden → overflow-y-auto so child routes can scroll

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

1 participant