migrate-container-queries

bởi sentry

Hướng dẫn di chuyển media query viewport (@media, useMedia) sang container query trong frontend của Sentry. Sử dụng khi di chuyển bố cục responsive sang container…

npx skills add https://github.com/getsentry/sentry --skill migrate-container-queries

Container Query Migration Guide

Migrate viewport-based responsive logic (@media, useMedia, and screen:-prefixed responsive props) to container queries so components respond to their own available space instead of the raw viewport.

Always do a visual check. After every migration, resize the element (not just the window) and confirm the layout is identical and flips at the intended width. A good way to narrow an element without touching the window is to open a resizable panel next to it — e.g. drag out the Seer explorer sidebar, which squeezes the middle content. The token scales differ, so a mechanical swap that compiles can still render wrong.

Approach: refactor first, swap second

Stop at the first rung that fits. Prefer replacing hand-rolled CSS with primitives over a mechanical token swap.

RungWhenDo
1. Primitive propsThe @media only flips layout (flex-direction, display, grid-template, gap, visibility, width)Delete the styled component; use Container/Flex/Grid/Stack responsive props (direction={{xs: 'column', md: 'row'}})
2. @container swapCSS can't be a prop (descendant selectors, pseudo-elements, font-size, complex grid-template-areas)Keep the styled component; swap @media → @container, theme.breakpoints.* → theme.container.*
3. Container-scoped JSWidth is read in JS to branch renderingReplace useMedia(...) with useResponsivePropValue({...}) for a threshold boolean, or useContainerBreakpoint() to branch on the active key
4. Leave as useMediaGenuine media feature, not widthDo nothing — these do not migrate

⚠️ Convert to the nearest container scale

Breakpoint and container scales have different keys and different pixel values — this is not a rename. MAP BY PIXEL VALUE, NOT BY KEY: breakpoints.sm does NOT become container.sm. Reusing the same key is the #1 migration bug.

theme.breakpoints (viewport / @media), base 2xs:

2xsxssmmdlgxl2xl
0px500px800px992px1200px1440px2560px

theme.container (container / @container), base zero:

zero3xs2xsxssmmdlgxl2xl3xl4xl5xl
0px320px384px448px512px576px640px768px896px1024px1152px1280px

Rule: take the old breakpoint's pixel value and pick the container token whose pixel value is nearest to it — not the token with the same name. breakpoints.sm is 800px, so it maps to container.xl (768px), not container.sm (512px). Then confirm with a visual check: the container is often narrower than the viewport, so the nearest-px token is a starting point, not a guarantee.

Genuine viewport width → screen: keys, not useMedia

When layout truly must follow the window (not the component's room), don't keep useMedia — use a screen:-prefixed responsive prop, which resolves against the viewport on the theme.breakpoints scale: direction={{zero: 'column', 'screen:lg': 'row'}}. Bare keys and screen: keys can mix on one prop. Prefer bare (container) keys; reach for screen: only when the viewport genuinely drives the layout.

Existing screen: props are migration candidates

screen:-prefixed responsive props compile to viewport media queries. Audit them in the same pass as @media and useMedia. If the layout responds to the component's available width, remove the screen: prefix and use bare container keys. Map the old viewport breakpoint by pixel value, and add an explicit container base when the narrow layout differs:

// Old — viewport width drives a component layout
<Grid columns={{'screen:2xs': '1fr', 'screen:sm': 'auto 1fr auto auto'}} />

// New — component width drives the layout; screen sm (800px) → container xl (768px)
<Grid columns={{zero: '1fr', xl: 'auto 1fr auto auto'}} />

Keep the screen: key only when the window itself is the intended source of truth. screen: is not a container-query migration target just because it is already a responsive prop.

Keep useMedia only for non-width media features

Width — container or viewport — has a prop/hook path above. Leave useMedia in place only for: prefers-color-scheme, prefers-reduced-motion, hover, pointer, max-height / height-based, resolution, print.

container-type: only when no query container is in scope

Default: don't add one. Bare keys and @container already resolve against the nearest ancestor container, and product views have one: ContentStack (#main, views/organizationLayout/index.tsx) wraps the routed <Outlet /> with containerType="inline-size"; topBar and #modal-portal cover their own subtrees. Add container-type only when a subtree must respond to its own width rather than the page's — then:

  • Use inline-size (width only). size also queries height, which collapses content unless height is set elsewhere.
  • In a reusable component that may already sit inside a container, make it conditional to avoid a redundant one — containerType={hasParentQueryContainer ? 'normal' : 'inline-size'} via useHasContainerQuery() (see components/core/breadcrumbList/breadcrumbList.tsx).

Examples

Rung 1 — styled @media → primitive props (preferred)

// Old — delete the styled component
const Row = styled('div')`
  display: flex;
  flex-direction: row;
  gap: ${p => p.theme.space.md};
  @media (max-width: ${p => p.theme.breakpoints.sm}) {
    flex-direction: column;
  }
`;

// New
import {Flex} from '@sentry/scraps/layout';
<Flex direction={{xs: 'column', sm: 'row'}} gap="md">

Rung 2 — @media → @container (when it can't be a prop)

// Old
@media (max-width: ${p => p.theme.breakpoints.md}) { ... }

// New — swap at-rule AND scale; md breakpoint (992px) → nearest container token by px
// is 3xl (1024px), NOT theme.container.md by matching key
@container (max-width: ${p => p.theme.container['3xl']}) { ... }

Rung 3 — useMedia (width) → container-scoped JS

Both helpers below read the nearest query container (call from a descendant of one) and re-render as it crosses a breakpoint. A single max-width boolean is cleanest as a responsive value; reach for the active key only when you branch on the key itself.

// Old
const isNarrow = useMedia(`(max-width: ${theme.breakpoints.sm})`);

// New — resolve a responsive boolean against the container, same mobile-first
// cascade as CSS. A max-width query is "on by default, off past the threshold",
// so name only the threshold key. Map by pixel value: breakpoints.sm (800px) →
// nearest container token is xl (768px).
import {useResponsivePropValue} from '@sentry/scraps/layout';

const isNarrow = useResponsivePropValue({zero: true, xl: false});
// below xl → true, at/above xl → false — one key on each side, nothing to enumerate.

Reach for useContainerBreakpoint() instead only when you branch on the key itself (e.g. picking one of several layouts), not a single threshold. It returns the container's active key ('zero' … '5xl') — don't compare it with === 'zero' for a max-width case: that fires only below 320px and drops the 320–768px range the original query treated as narrow.

Migration Checklist

Took the lowest rung that fits (above). Then verify the gotchas:

  • Mapped to the container token with the nearest pixel value, not the same name — e.g. breakpoints.sm → container.xl, not container.sm
  • For width read in JS, used useResponsivePropValue({...}) for a threshold boolean; reserved useContainerBreakpoint() for branching on the key — never === 'zero' to mean "narrow" (that's only <320px)
  • Routed genuine viewport-width cases to screen: keys; kept useMedia only for non-width media features
  • Audited existing screen:-prefixed layout props — migrated component-width cases to bare container keys and kept screen: only for genuine viewport-width behavior
  • Added container-type only when a subtree needs its own; used inline-size
  • Confirmed a query-container ancestor exists (@container silently no-ops without one)
  • Visual check: resized the element and confirmed identical output flipping at the intended width

Thêm skills từ sentry

architecture-review
sentry
Đánh giá sức khỏe mã nguồn ở cấp độ nhân viên. Phát hiện các module nguyên khối, lỗi âm thầm, khoảng trống an toàn kiểu dữ liệu, lỗ hổng độ phủ kiểm thử và các vấn đề về thân thiện với LLM.
pi-agent-integration
sentry
Integrate `@mariozechner/pi-agent-core` as the agent abstraction inside another library or runtime. Use when implementing or refactoring Pi Agent wrappers,…
sentry-debug-issue
sentry
Gỡ lỗi và sửa một sự cố Sentry — tìm nó (bằng liên kết, ID hoặc tìm kiếm), lấy ngữ cảnh đầy đủ (stack trace, breadcrumbs, trace, logs), tùy chọn chạy Seer root-cause /…
sentry-svelte-sdk
sentry
Thiết lập đầy đủ Sentry SDK cho Svelte và SvelteKit. Sử dụng khi được yêu cầu "thêm Sentry vào Svelte", "thêm Sentry vào SvelteKit", "cài đặt @sentry/sveltekit", hoặc cấu hình…
sentry-svelte-sdk
sentry
Thiết lập đầy đủ Sentry SDK cho Svelte và SvelteKit. Sử dụng khi được yêu cầu "thêm Sentry vào Svelte", "thêm Sentry vào SvelteKit", "cài đặt @sentry/sveltekit", hoặc cấu hình…
skill-scanner
sentry
Quét kỹ năng tác nhân để tìm vấn đề bảo mật. Sử dụng khi được yêu cầu "quét một kỹ năng",
prompt-optimizer
sentry
Tạo, tối ưu hóa và cải tiến lặp lại các prompt cho agent, system prompt, developer prompt và các mẫu prompt có thể tái sử dụng. Sử dụng khi được yêu cầu cải thiện một…
commit
sentry
commit — một kỹ năng có thể cài đặt cho các tác nhân AI, được xuất bản bởi getsentry/sentry-docs.