{"_id":"@audemodo/responsive-keepalive","name":"@audemodo/responsive-keepalive","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@audemodo/responsive-keepalive","version":"0.1.0","description":"Render genuinely different component trees per breakpoint without losing state — React 19.2 Activity keep-alive, with anti-thrash and IME-safe switching.","keywords":["react","responsive","breakpoint","keep-alive","activity","state-preservation","mobile","desktop","media-query","ssr"],"license":"MIT","publishConfig":{"access":"public"},"author":{"name":"vi-wolhwa","email":"cerezo00@naver.com","url":"https://github.com/vi-wolhwa"},"repository":{"type":"git","url":"git+https://github.com/AudeModo/audemodo-responsive-keepalive.git"},"homepage":"https://github.com/AudeModo/audemodo-responsive-keepalive#readme","bugs":{"url":"https://github.com/AudeModo/audemodo-responsive-keepalive/issues"},"type":"module","sideEffects":false,"engines":{"node":">=20"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"peerDependencies":{"react":">=19.2.0"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"eslint .","format":"prettier --write .","format:check":"prettier --check .","prepublishOnly":"npm run build","gate":"npm run format:check && npm run typecheck && npm run lint && npm run test && npm run build"},"devDependencies":{"@eslint/js":"^10.0.1","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^6.0.3","eslint":"^10.6.0","eslint-plugin-react-hooks":"^7.1.1","eslint-plugin-simple-import-sort":"^13.0.0","globals":"^17.7.0","jsdom":"^29.1.1","prettier":"^3.9.3","react":"^19.2.7","react-dom":"^19.2.7","tsup":"^8.5.1","typescript":"^6.0.3","typescript-eslint":"^8.62.0","vitest":"^4.1.9"},"_id":"@audemodo/responsive-keepalive@0.1.0","gitHead":"f7ef88d8582cf582ad06183b3c1b6603c43130ad","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-NfzS3VG+omMqImmeZpWZ06jXVog6v406qNHbuBxFDU0omI9abCIcGmzwF51Lqfmvf8tSqMjx5S58ExiioBvDiA==","shasum":"43ac21f43f87f7658112ccd921399632b655e46a","tarball":"https://registry.npmjs.org/@audemodo/responsive-keepalive/-/responsive-keepalive-0.1.0.tgz","fileCount":10,"unpackedSize":156376,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBVPm17vgRlBA/XqBxr+/H4z/HCptEa45lkVuBrxqWIYAiAmxHy6/FyWZqkVyezuYZAWQSbNkkcc+8VwLT9qkn1T6Q=="}]},"_npmUser":{"name":"cerezo00","email":"cerezo00@naver.com"},"directories":{},"maintainers":[{"name":"cerezo00","email":"cerezo00@naver.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/responsive-keepalive_0.1.0_1782874268381_0.06696652111804946"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-01T02:51:08.114Z","0.1.0":"2026-07-01T02:51:08.499Z","modified":"2026-07-01T02:51:08.789Z"},"maintainers":[{"name":"cerezo00","email":"cerezo00@naver.com"}],"description":"Render genuinely different component trees per breakpoint without losing state — React 19.2 Activity keep-alive, with anti-thrash and IME-safe switching.","homepage":"https://github.com/AudeModo/audemodo-responsive-keepalive#readme","keywords":["react","responsive","breakpoint","keep-alive","activity","state-preservation","mobile","desktop","media-query","ssr"],"repository":{"type":"git","url":"git+https://github.com/AudeModo/audemodo-responsive-keepalive.git"},"author":{"name":"vi-wolhwa","email":"cerezo00@naver.com","url":"https://github.com/vi-wolhwa"},"bugs":{"url":"https://github.com/AudeModo/audemodo-responsive-keepalive/issues"},"license":"MIT","readme":"# responsive-keepalive\n\n**Render genuinely different component trees per breakpoint — without losing state.**\nPowered by React 19.2's `<Activity>`, with anti-thrash and IME-safe switching.\n\n[![npm version](https://img.shields.io/npm/v/@audemodo%2Fresponsive-keepalive.svg)](https://www.npmjs.com/package/@audemodo%2Fresponsive-keepalive)\n[![license](https://img.shields.io/npm/l/@audemodo%2Fresponsive-keepalive.svg)](./LICENSE)\n[![types included](https://img.shields.io/npm/types/@audemodo%2Fresponsive-keepalive.svg)](https://www.npmjs.com/package/@audemodo%2Fresponsive-keepalive)\n\nWhen your mobile and desktop views are **different component trees**, switching between\nthem at a breakpoint normally unmounts one and mounts the other — wiping scroll position,\nform input, open menus, and any local state. `responsive-keepalive` keeps the inactive\ntree **mounted but hidden** via React's `<Activity>`, so state survives every switch.\n\n```tsx\n// The same counter's value survives switching mobile ⇄ desktop.\n<Responsive\n  variant={variant}\n  variants={{\n    mobile: () => <MobileLayout />,\n    desktop: () => <DesktopLayout />,\n  }}\n/>\n```\n\n## Features\n\n- 🌳 **Structural** responsiveness — swap whole trees per breakpoint, not just CSS.\n- 💾 **State preserved** across switches (scroll, input, selection) — keep-alive by default.\n- 📐 Viewport, **container** (ResizeObserver), and **value-level** responsiveness.\n- 🧩 A typed **factory** (`createResponsive`) — one config, inferred keys, composable gates.\n- 🔁 **Shared state** across layouts without lifting (`useSharedState`).\n- 🖥️ **SSR-safe** (`useSyncExternalStore`) with an `ssr` fallback variant.\n- ⌨️ **IME-safe** switching (`deferWhileComposing`) and **anti-thrash** (`settleMs`).\n- 🪶 Zero runtime deps, tree-shakeable, dual **ESM/CJS + types**.\n- 🛟 Graceful fallback to `swap` on React &lt; 19.2.\n\n## Install\n\n```bash\nnpm i @audemodo/responsive-keepalive\n```\n\nRequires **React 19.2+** (peer dependency) — `<Activity>` is a React 19.2 feature.\n\n## Quick start\n\nDefine your breakpoints once with the factory:\n\n```tsx\n// responsive.ts\nimport { createResponsive } from '@audemodo/responsive-keepalive';\n\nexport const { Responsive, Provider } = createResponsive(\n  { mobile: 0, desktop: 768 }, // integer px breakpoints\n  { ssr: 'mobile' },\n);\n```\n\nThen render one prop per breakpoint — the active view is resolved from the viewport:\n\n```tsx\n// App.tsx\nimport { Provider, Responsive } from './responsive';\n\nexport function App() {\n  return (\n    <Provider>\n      <Responsive mobile={() => <MobileLayout />} desktop={() => <DesktopLayout />} />\n    </Provider>\n  );\n}\n```\n\nResize across `768px`: the layout switches, but each layout keeps its state.\n\n## Core concepts\n\n| Term         | Meaning                                                                          |\n| ------------ | -------------------------------------------------------------------------------- |\n| **variant**  | The active breakpoint key, e.g. `'mobile' \\| 'desktop'`.                         |\n| **variants** | A map of key → the tree (or `() => tree`) to render.                             |\n| **strategy** | `keepAlive` (default, state preserved via Activity) or `swap` (unmount → reset). |\n| **mount**    | `lazy` (default, mount on first activation) or `eager` (mount all upfront).      |\n| **ssr**      | The variant rendered on the server and before hydration.                         |\n\n## API\n\n### `createResponsive(breakpoints, defaults?)`\n\nBuilds a self-resolving `<Responsive>`, a root `Provider`, composable `Match` gates, and\npre-bound hooks — all typed to your config. Breakpoints are **integer px min-widths**.\n\n```tsx\nexport const { Responsive, Provider, Match, useVariant, useResponsiveValue } = createResponsive(\n  { mobile: 0, desktop: 768 },\n  { ssr: 'mobile' },\n);\n\nexport const { Mobile, Desktop } = Match;\n```\n\n```tsx\n// Composable gates — drop them anywhere under <Provider>.\n<Provider>\n  <Desktop>\n    <DesktopNav />\n  </Desktop>\n  <Mobile>\n    <MobileTabBar />\n  </Mobile>\n</Provider>;\n\n// Pre-bound hooks — no queries to repeat.\nfunction Toolbar() {\n  const variant = useVariant(); // 'mobile' | 'desktop'\n  const gap = useResponsiveValue({ mobile: 8, desktop: 24 });\n  return <div style={{ gap }}>…</div>;\n}\n```\n\nReturns `{ Responsive, Provider, Match, useVariant, useResponsiveValue, breakpoints }`.\n\n### `<Responsive>`\n\nThe controlled primitive — you supply the active `variant` and a `variants` map.\n\n```tsx\nimport { Responsive, useMediaVariant } from '@audemodo/responsive-keepalive';\n\nconst variant = useMediaVariant({ mobile: 0, desktop: 768 }, { ssr: 'mobile' });\n\n<Responsive\n  variant={variant}\n  variants={{\n    mobile: () => <MobileLayout />,\n    desktop: () => <DesktopLayout />,\n  }}\n  strategy=\"keepAlive\" // default\n  mount=\"lazy\" // default\n/>;\n```\n\n| Prop       | Type                                      | Default       | Description                                    |\n| ---------- | ----------------------------------------- | ------------- | ---------------------------------------------- |\n| `variant`  | `K`                                       | —             | The active variant key (you control this).     |\n| `variants` | `Record<K, ReactNode \\| () => ReactNode>` | —             | Map of key → content. `() =>` defers creation. |\n| `strategy` | `'keepAlive' \\| 'swap'`                   | `'keepAlive'` | Preserve inactive state, or unmount it.        |\n| `mount`    | `'lazy' \\| 'eager'`                       | `'lazy'`      | Mount on first activation, or all upfront.     |\n\n### `useMediaVariant(breakpoints, options?)`\n\nResolves a variant key from the **viewport**. Accepts integer px breakpoints (recommended)\nor raw media-query strings (for non-width features like orientation).\n\n```tsx\nconst variant = useMediaVariant(\n  { mobile: 0, desktop: 768 },\n  { ssr: 'mobile', settleMs: 150, deferWhileComposing: true },\n);\n\n// raw queries for non-width features\nconst scheme = useMediaVariant({\n  light: '(prefers-color-scheme: light)',\n  dark: '(prefers-color-scheme: dark)',\n});\n```\n\n### `useResponsiveValue(breakpoints, values, options?)`\n\nPicks a **plain value** per breakpoint — for column counts, gaps, or copy where a whole\ntree is overkill.\n\n```tsx\nconst columns = useResponsiveValue(\n  { mobile: 0, tablet: 600, desktop: 1024 },\n  { mobile: 1, tablet: 2, desktop: 3 },\n  { ssr: 'mobile' },\n);\n```\n\n### `useContainerVariant(ref, breakpoints, options?)`\n\nLike `useMediaVariant`, but resolves from the **element's own width** (via ResizeObserver)\n— so the same card can be a row in a wide column and a stack in a narrow one. Breakpoints\nare integer min content-widths.\n\n```tsx\nconst ref = useRef<HTMLDivElement>(null);\nconst variant = useContainerVariant(ref, { stack: 0, row: 420, wide: 680 }, { ssr: 'stack' });\n\nreturn <div ref={ref}>{/* branch on variant */}</div>;\n```\n\n### `useSharedState(key, initialValue)` & `<SharedStateScope>`\n\nShare state by key across sibling layouts **without lifting it to a parent**. Because the\nstore lives in a scope ancestor, the value also survives `swap` and remounts.\n\n```tsx\nfunction SearchField() {\n  const [query, setQuery] = useSharedState('search', '');\n  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;\n}\n```\n\n> `<Responsive>` and the factory `<Provider>` mount a scope automatically, so inside them\n> you can call `useSharedState` directly. Use `<SharedStateScope>` only for siblings that\n> are under neither.\n\n### Options\n\nAll resolution hooks accept:\n\n| Option                | Type      | Description                                                    |\n| --------------------- | --------- | -------------------------------------------------------------- |\n| `ssr`                 | `K`       | Variant returned on the server and before hydration.           |\n| `settleMs`            | `number`  | Anti-thrash: commit a change only after it is stable for N ms. |\n| `deferWhileComposing` | `boolean` | Hold switches during IME composition (media hooks only).       |\n\n### Exported types\n\n`Strategy`, `Mount`, `VariantMap`, `ResponsiveProps`, `MediaInput`, `MediaVariantOptions`,\n`ResponsiveValueOptions`, `ContainerVariantOptions`, `BreakpointConfig`,\n`CreateResponsiveOptions`, `ConfiguredResponsive`, `ConfiguredResponsiveProps`,\n`ResponsiveProviderProps`, `MatchProps`, `SetSharedState`.\n\n## Recipes\n\n**Next.js / SSR** — feed a request-derived variant so the server renders the right tree:\n\n```tsx\n// app/layout.tsx (server)\n<Provider ssr={isMobileUA(headers()) ? 'mobile' : 'desktop'}>{children}</Provider>\n```\n\n**Shared search across layouts** — the mobile and desktop search boxes stay in sync:\n\n```tsx\n<Provider>\n  <Mobile>\n    <SearchField />\n  </Mobile>\n  <Desktop>\n    <SearchField />\n  </Desktop>\n</Provider>\n```\n\n## Behavior notes\n\n- **SSR** renders the `ssr` variant on the server; after hydration it resolves to the real\n  viewport, which may cause a **one-time** layout shift (not continuous flicker).\n- **React &lt; 19.2** has no `<Activity>`, so the library falls back to `swap` (state is not\n  preserved) and warns once in development.\n- `strategy` and `mount` are independent: `mount=\"eager\"` only matters under `keepAlive`.\n\n## License\n\n[MIT](./LICENSE) © vi-wolhwa\n","readmeFilename":"README.md","_rev":"1-b5fcea7dafefdce792634a98f55e6e0c"}