Skip to content
Agentmart
Submit a tool

adaptive-layout

Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.

Skill by zakariaf

IndexedMIT★ 11
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/zakariaf-adaptive-layout

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids. Use when making screens responsive or for tablets, iPad, foldables, rotation, split screen, orientation lock. Not for tab bars (navigation-and-routing).

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 26, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found
Runs as
Node.js package

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/adaptive-layout

What's inside

Skill name
adaptive-layout
Description
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids. Use when making screens responsive or for tablets, iPad, foldables, rotation, split screen, orientation lock. Not for tab bars (navigation-and-routing).
License
MIT
Instructions
213 lines in SKILL.md · view on GitHub

Before installing

  • Ships executable scripts

Files (45)

  • SKILL.md
  • shared.json · assets/
  • task-board.tsx · examples/
  • tasks-list-detail.md · examples/
  • list-detail-and-navigation.md · references/
  • platforms-orientation-and-resizing.md · references/
  • testing-and-checker.md · references/
  • window-classes-and-panes.md · references/
  • check-adaptive-layout.mjs · scripts/
  • check-lib.mjs · scripts/
  • source-scan.mjs · scripts/lib/
  • selftest.mjs · scripts/
  • adaptive-layout.json · templates/contracts/
  • adaptive-layout.schema.json · templates/contracts/
  • use-window-class.test.ts · templates/src/hooks/
  • use-window-class.ts · templates/src/hooks/
  • list-detail-layout.test.tsx · templates/src/ui/adaptive/
  • list-detail-layout.tsx · templates/src/ui/adaptive/
  • pane-layout.test.ts · templates/src/ui/adaptive/
  • pane-layout.ts · templates/src/ui/adaptive/
  • use-list-detail-layout.test.tsx · templates/src/ui/adaptive/
  • use-list-detail-layout.ts · templates/src/ui/adaptive/
  • use-measured-width.test.ts · templates/src/ui/adaptive/
  • use-measured-width.ts · templates/src/ui/adaptive/
  • app.config.ts · tests/bases/app/
  • adaptive-layout.json · tests/bases/app/contracts/
  • package.json · tests/bases/app/
  • with-task-widgets.js · tests/bases/app/plugins/
  • _layout.tsx · tests/bases/app/src/app/(app)/
  • [id].tsx · tests/bases/app/src/app/(app)/tasks/
  • index.tsx · tests/bases/app/src/app/(app)/tasks/
  • capture-screen.tsx · tests/bases/app/src/features/attachments/
  • settings-screen.tsx · tests/bases/app/src/features/settings/
  • task-board.tsx · tests/bases/app/src/features/tasks/components/
  • task-detail.tsx · tests/bases/app/src/features/tasks/components/
  • task-row.tsx · tests/bases/app/src/features/tasks/components/
  • task-list.ts · tests/bases/app/src/features/tasks/
  • tasks-screen.tsx · tests/bases/app/src/features/tasks/
  • use-tasks-model.ts · tests/bases/app/src/features/tasks/
  • tokens.ts · tests/bases/app/src/theme/
  • screen-frame.tsx · tests/bases/app/src/ui/
  • tsconfig.json · tests/bases/app/
  • CASES.json · tests/fixtures/check-adaptive-layout/
  • SUITE.json · tests/fixtures/check-adaptive-layout/
  • EXPECT.txt · tests/fixtures/check-adaptive-layout/good/

SKILL.md (first lines)

# Adaptive layout

Every screen lays out for the window the app has now, on iOS and Android: one window classifier,
breakpoints in one module, a readable column on wide windows, a list beside its detail when both
fit in the box the panes get, grids sized from their own box, no orientation lock or resizing
opt-out the platforms ignore anyway, and a selection that lives in the route so rotation, resizing
and links keep it. A zero-dependency checker proves what a reviewer would otherwise check by eye.

## Rules that must hold

1. **Layout decides by the window, never the device.** No `Platform.isPad`, `interfaceIdiom`,
   expo-device `DeviceType`, `isTablet()` or `isPhone` helper picks a layout, and no
   `getOrientationAsync` or orientation listener shapes one: read `useWindowClass()` (and its
   `isLandscape`, the window's shape). Why: iPad Split View, Slide Over and Stage Manager, Android
   multi-window and freeform windows, and a foldable opening all resize the app while it runs; an
   iPad in a third of the screen is a compact, portrait-shaped window. A hardware capability in
   `src/services/` (no haptic engine on iPad) is not a layout decision; the rule reads UI code only.
   (`device-branch`, `device-orientation`)
2. **One window classifier, never forked.** `src/hooks/use-window-class.ts` (react-components-and-hooks
   owns it) gives `{ width: 'compact' | 'regular' (600), isLandscape, isLargeText (1.25) }`; no second
   `useBreakpoint`, `useIsTablet` or `REGULAR_WIDTH_MIN` elsewhere. Why: two classifiers disagree at the
   boundary and screens stop agreeing on what "regular" means. (`window-class-fork`)
3. **Breakpoints beyond the class live in `src/ui/adaptive/pane-layout.ts`, as content rules.** A
   width or height compared with a number of 300 or more anywhere else fails; a new rule is a pure
   function ("two panes when the list at 320 and the detail at 400 fit") tested at the window table.
   Why: a number in a screen drifts from every other screen's, and a content rule explains itself.
   (`raw-breakpoint`)
4. **Every screen caps its reading width at `CONTENT_MAX_WIDTH`.** Through `ScreenFrame`,
   `ListDetailLayout`, or a list's `contentContainerStyle` (`width: '100%', maxWidth:
   CONTENT_MAX_WIDTH, alignSelf: 'center'`); a canvas, map, camera or video screen is listed in
   `contracts/adaptive-layout.json` `fullBleedScreens` with its reason; no hand-picked `maxWidth: 720`.
   A screen in a `contracts/architecture.json` `testOnlyFolders` folder (a capture harness, a test
   build's screen) is not judged: it never ships in a store build.
   Why: on a 13-inch iPad in landscape an uncapped line runs 1,300 pt wide. (`readable-width`,
   `readable-width-token`)
5. **No fixed width wider than the narrowest window allows (288).** Regions size by `flex`,
   percentages, the pane layout's `listWidth` or a measured container. Why: a 320 pt Split View pane
   or small phone minus two 16 pt gutters leaves 288; anything wider scrolls sideways or clips.
   (`fixed-width`)
6. **A list and its detail share one screen through `ListDetailLayout`, sized by its own box.** The
   pane count comes from `useListDetailLayout()`: one pane on a compact window; on a regular one, two
   panes only when the list, a hairline and the detail minimum fit in the width `ListDetailLayout`
   measures inside the screen's `SafeAreaView` (721 pt, 801 pt with large text; before the first
   layout, the window minus the side insets). Each screen in `listDetailScreens` renders it with
   `panes={model.panes}`. Why: a regular window is not always wide enough (a half-width iPad Split
   View is 678 pt; an iPad mini beside an 80 pt side rail leaves 664 pt; a form sheet is narrower
   still), and the notch side is not usable space. (`list-detail-screens`)
7. **The selection is the route's, and each route owns its param.** The list route carries
   `?selected=<id>`, the detail route is `[id]`, both render the same screen and each reads only its
   own param; selecting calls `router.push` on one pane, and on two panes `router.setParams` with the
   param the current route owns (`{ selected }` on the list route, `{ id }` on the detail route);
   nothing navigates when the window changes; no `useState` selection. Why: a rotation, a resize, a
   restored route and a link all land on the same item, back never walks through every tapped row,
   and `/tasks/t2?selected=t3` would leave the detail pane on `t2`. (`list-detail-selection`)
8. **Grid columns come from the grid's own box, and a new count remounts the list.**
   `useMeasuredWidth()` + `gridColumnsOf({ width, minCellWidth, gap, maxColumns })`, and
   ``key={`grid-${columns}`}`` on the `FlatList`. Why: inside a pane the window is far wider than the
   grid, and React Native throws "Changing numColumns on the fly is not supported" on the first
   rotation. (`grid-columns-source`, `grid-columns-key`)
9. **Orientation is not locked, except where the contract says why.** The app config sets
   `orientation: 'portrait'` (a phone-only lock in practice) or `'default'`; `'landscape'` needs
   `appOrientation: 'landscape'` in the contract. A per-screen lock (`lockAsync`, a Stack screen's
   `orientation` option) sits only in an `orientationLocks` file with its reason, and every
   `lockAsync` has its `unlockAsync`; expo-screen-orientation's `initialOrientation` lock needs the
   matching `appOrientation`. Why: iPads with multitasking and Android displays 600 dp and wider (apps
   targeting API 36) ignore locks, so the layout must handle both shapes anyway.
   (`orientation-lock`, `orientation-unlock`, `config-orientation`)
10. **The app never opts out of resizing, and supports tablets.** No `requireFullScreen: true` or
    `UIRequiresFullScreen` (unless `UIRequiresFullScreenIgnoredStartingWithVersion` of 27 or lower,
    usually `'26'`, limits it to older iOS), no `resizeableActivity`, aspect-ratio limits or
    `PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY` in the config or a plugin, not even set to the
    default (TN3192 asks apps to drop the key; the checker calls that redundant); `ios.supportsTablet:
    true` unless the contract says `tablets: false` with a reason. Why: Apple deprecated full screen in
    iPadOS 26 and ignores it for apps built with the iOS 27 SDK; Android 16 ignores the opt-outs on
    large displays and the last one stops working at API 37; a phone-only app runs in a scaled frame
    on iPad. (`full-screen-opt-out`, `tablet-support`)
11. **Insets are physical and change with the window.** Rotation moves the notch or cutout to a side;
    take both sides (or `SafeAreaView` edges), never `paddingStart: insets.left`, and never count the
    notch side as pane width (measure inside the edges; subtract the side insets from a window).
    Why: the cutout does not mirror in RTL, and a detail pane under the notch is unusable.
    (react-components-and-hooks' `inset-direction`; the pane tests here)
12. **Every layout decision is a pure function tested at the window table, at `fontScale` 1 and 2.**
    320 × 568, 360 × 800, 402 × 874, 874 × 402 (62 pt side insets), 678 × 1032, 744 × 1133,
    800 × 1280, 1376 × 1032; screenshots on named devices at the same sizes. Why: Jest lays nothing
    out, and adaptive bugs live at the size nobody opened.

## Workflow

1. Read [references/window-classes-and-panes.md](references/window-classes-and-panes.md): window
   versus device, the classifier, the window table, the pane rule, readable widths, grids, fixed
   widths, large text, and the decisions with their reasons.
2. **First time in an app.** Make sure `src/hooks/use-window-class.ts` exists (react-components-and-hooks
   ships it; this skill's synced copy is the same file). Copy `templates/src/ui/adaptive/` to
   `src/ui/adaptive/` with every test, and `templates/contracts/adaptive-layout.json` plus its schema
   to `contracts/`. The kit imports design-system-structure's `@/theme/*`; the tests use
   unit-and-component-tests' `@test/render-with-providers`.
3. **The app config.** Read [references/platforms-orientation-and-resizing.md](references/platforms-orientation-and-resizing.md)
   sections 1-3: keep `orientation: 'portrait'` or `'default'`, set `ios.supportsTablet: true`, remove
   every resizing opt-out, and record a deliberate profile (landscape app, no iPad layout yet) in the
   contract (section 8).
4. **A screen.** Put it in `ScreenFrame` (or cap its list's `contentContainerStyle`); a full-bleed
   screen goes into `fullBleedScreens` with its reason. Decide variations from `useWindowClass()`;
   if a decision needs a width, add a pure function to `pane-layout.ts` with window-table tests.
5. **A list and its detail.** Read [references/list-detail-and-navigation.md](references/list-detail-and-navigation.md)
   and imitate [examples/tasks-list-detail.md](examples/tasks-list-detail.md): two thin routes over one
   screen, the model hook reads the route's own param from `useLocalSearchParams()` and the panes from
   `useListDetailLayout()`, `onSelect` sets the current route's param on two panes, the screen renders
   `<ListDetailLayout panes={model.panes}>` in a `SafeAreaView` with the named edges. Add the screen
   to `listDetailScreens`, and the example's route tests (both routes, a measured box).
6. **A grid.** Imitate [examples/task-board.tsx](examples/task-board.tsx): measure the container,
   `gridColumnsOf` and `gridCellWidthOf`, render nothing until the first layout, key the list by the
   column count.
7. **An orientation lock** (camera, full-screen video): platforms reference section 5. Lock in an
   effect whose cleanup unlocks, list the file in `orientationLocks` with its reason, and still lay the
   screen out for both shapes.
8. **Tests first.** Follow [references/testing-and-checker.md](references/testing-and-checker.md)
   sections 1-2: window-table tables at `fontScale` 1 and 2, the `useWindowDimensions` mock (Jest's
   default `fontScale` is 2), `SafeAreaProvider` with `initialMetrics`, a fired layout event, the
   crossing test. Route tests use the router helper as in the example.

From zakariaf/ReactNative-Skills, MIT.

More from zakariaf/ReactNative-Skills

Signals

Stars
11
Forks
2
Open issues
0
Last commit (this folder)
Oct 17, 2026 (11 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from zakariaf

Cite this page

adaptive-layout skill. Agent Mart. https://agentmart.dev/skills/zakariaf-adaptive-layout (last updated 2026-10-24)