[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# Generated CSS\nsrc/index.css\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Cory House\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# React Switchboard 🎛\n\nQuickly create custom DevTools for your React app.\n\n- [Live Demo](https://switchboard-beta.vercel.app/) 🚀\n- [Demo repo using Vite](https://github.com/coryhouse/switchboard-with-vite-demo)\n\n## Quick Start\n\n```\nnpm install react-switchboard -D\n```\n\n### Vite Example\n\nCall `Switchboard` in your project root. Pass your app's main component to `Switchboard's` `appSlot` prop. Import `Switchboard` CSS. Lazy load `Switchboard` via an environment variable so it's excluded from your production bundle.\n\n```tsx\nimport { lazy } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport \"react-switchboard/dist/index.css\";\n\nconst Switchboard = lazy(() => import(\"react-switchboard\"));\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  import.meta.env.PROD ? (\n    <App />\n  ) : (\n    <Suspense fallback=\"Loading Switchboard...\">\n      <Switchboard appSlot={<App />} />\n    </Suspense>\n  )\n);\n```\n\n## Headless\n\nThe `Switchboard` component accepts children so you can specify what it renders. If you want complete control over the UI, use the `useSwitchboard` and `useSwitchboardState` hooks instead of the `Switchboard` component.\n\n```tsx\nfunction CustomSwitchboard() {\n  const { generalSettings, switchboardWindowRef, copySettingsUrlToClipboard } =\n    useSwitchboard();\n\n  // Use useSwitchboardState hook for custom settings.\n  const [user, setUser] = useSwitchboardState(\"sb-user\", null);\n\n  return {\n    /* Your custom JSX to render your desired UI */\n  };\n}\n```\n\n## Why Switchboard?\n\nCode faster.\nReproduce edge cases.\nDo real-time demos.\nUse Switchboard to configure automated tests.\n\n### Common Uses\n\n- Login / switch users instantly\n- Change feature toggles\n- Configure mock APIs\n- Force errors\n- Simulate network slowness for specific requests\n- Configure automated test scenarios\n- Simulate incoming traffic and write conflicts\n\nMore info in this 25 minute conference talk: [Creating Custom Dev Tools for Your React App at React Rally](https://www.youtube.com/live/DGG6xpllTiE?si=vq7z35p3V_2ce68H&t=24527)\n\n## API\n\n### Components\n\n- `Switchboard` - The main component that renders your app and the Switchboard UI.\n\n### Hooks\n\n- `useSwitchboard` - Logic for running Switchboard. Useful to create a custom Switchboard UI.\n- `useSwitchboardState` - Declare Switchboard state. This state is automatically initialized from the URL, and written to localStorage so that it persists between sessions. Useful to extend Switchboard's features with custom settings for your app, or if you want to create a custom Switchboard UI.\n\n## FAQ\n\n- **How does mocking work?** Switchboard intercepts fetch requests via [Mock Service Worker](https://mswjs.io/), and displays a UI for configuring the mock responses.\n- **Why does `Switchboard` render my app?** If you configure Switchboard to force the app to throw an error, Switchboard continues to render so you can change Switchboard's settings.\n\n- **Why lazy loading?** Lazy load `Switchboard` via `React.lazy` and `Suspense` so that it's excluded your app's prod bundle.\n\n- **How can I toggle Switchboard?** Use an environment variable to enable `Switchboard`. For example, tweak Vite's dev npm script to set an environment variable using [cross-env](https://www.npmjs.com/package/cross-env):\n\n```bash\n\"dev\": \"cross-env VITE_ENABLE_SWITCHBOARD=Y vite\",\n```\n\nThen, read this environment variable in your app's entry point.\n\n## Acknowledgements\n\n- [Mock Service Worker](https://mswjs.io/) - Switchboard mocks HTTP requests via msw.\n\n## Inspiration\n\n- [React Query Devtools](http://react-query.tanstack.com/devtools)\n- [https://github.com/dataarts/dat.gui](https://github.com/dataarts/dat.gui?tab=readme-ov-file)\n"
  },
  {
    "path": "examples/vite-hello-world/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "examples/vite-hello-world/README.md",
    "content": "# React Switchboard with Vite - Hello World\n\nSee [main.tsx](https://github.com/coryhouse/react-switchboard/blob/main/examples/vite-hello-world/src/main.tsx). The rest of the project is a standard Vite project. Run via `npm run dev`.\n"
  },
  {
    "path": "examples/vite-hello-world/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config({\n  extends: [js.configs.recommended, ...tseslint.configs.recommended],\n  files: ['**/*.{ts,tsx}'],\n  ignores: ['dist'],\n  languageOptions: {\n    ecmaVersion: 2020,\n    globals: globals.browser,\n  },\n  plugins: {\n    'react-hooks': reactHooks,\n    'react-refresh': reactRefresh,\n  },\n  rules: {\n    ...reactHooks.configs.recommended.rules,\n    'react-refresh/only-export-components': [\n      'warn',\n      { allowConstantExport: true },\n    ],\n  },\n})\n"
  },
  {
    "path": "examples/vite-hello-world/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/vite-hello-world/package.json",
    "content": "{\n  \"name\": \"vite-hello-world\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.8.0\",\n    \"@types/react\": \"^18.3.3\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@vitejs/plugin-react\": \"^4.3.1\",\n    \"eslint\": \"^9.8.0\",\n    \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.9\",\n    \"globals\": \"^15.9.0\",\n    \"react-switchboard\": \"latest\",\n    \"typescript\": \"^5.5.3\",\n    \"typescript-eslint\": \"^8.0.0\",\n    \"vite\": \"^5.4.0\"\n  }\n}\n"
  },
  {
    "path": "examples/vite-hello-world/src/App.css",
    "content": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  a:nth-of-type(2) .logo {\n    animation: logo-spin infinite 20s linear;\n  }\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "examples/vite-hello-world/src/App.tsx",
    "content": "import { useState } from 'react'\nimport reactLogo from './assets/react.svg'\nimport viteLogo from '/vite.svg'\nimport './App.css'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n\n  return (\n    <>\n      <div>\n        <a href=\"https://vitejs.dev\" target=\"_blank\">\n          <img src={viteLogo} className=\"logo\" alt=\"Vite logo\" />\n        </a>\n        <a href=\"https://react.dev\" target=\"_blank\">\n          <img src={reactLogo} className=\"logo react\" alt=\"React logo\" />\n        </a>\n      </div>\n      <h1>Vite + React</h1>\n      <div className=\"card\">\n        <button onClick={() => setCount((count) => count + 1)}>\n          count is {count}\n        </button>\n        <p>\n          Edit <code>src/App.tsx</code> and save to test HMR\n        </p>\n      </div>\n      <p className=\"read-the-docs\">\n        Click on the Vite and React logos to learn more\n      </p>\n    </>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "examples/vite-hello-world/src/index.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "examples/vite-hello-world/src/main.tsx",
    "content": "import { lazy, StrictMode, Suspense } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"react-switchboard/dist/index.css\";\nimport \"./index.css\";\n\nconst Switchboard = lazy(() => import(\"react-switchboard\"));\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    {import.meta.env.DEV ? (\n      <Suspense fallback=\"Loading Switchboard...\">\n        <Switchboard appSlot={<App />} />\n      </Suspense>\n    ) : (\n      <App />\n    )}\n  </StrictMode>\n);\n"
  },
  {
    "path": "examples/vite-hello-world/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/vite-hello-world/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "examples/vite-hello-world/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n"
  },
  {
    "path": "examples/vite-hello-world/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "examples/vite-hello-world/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-switchboard\",\n  \"version\": \"0.0.27\",\n  \"description\": \"Quickly create custom DevTools for your React app\",\n  \"scripts\": {\n    \"prebuild\": \"tailwindcss -i ./src/input.css -o src/index.css\",\n    \"build\": \"tsup\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"prepublish\": \"npm run build\",\n    \"knip\": \"knip\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\",\n      \"types\": \"./dist/index.d.ts\"\n    },\n    \"./dist/index.css\": {\n      \"import\": \"./dist/index.css\",\n      \"require\": \"./dist/index.css\"\n    }\n  },\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/coryhouse/react-switchboard.git\"\n  },\n  \"keywords\": [\n    \"react\",\n    \"devtools\",\n    \"reusable\",\n    \"component\",\n    \"toolkit\"\n  ],\n  \"author\": \"Cory House\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/coryhouse/react-switchboard/issues\"\n  },\n  \"homepage\": \"https://github.com/coryhouse/react-switchboard#readme\",\n  \"devDependencies\": {\n    \"@types/react\": \"^18.3.3\",\n    \"knip\": \"^5.27.1\",\n    \"tailwindcss\": \"^3.4.7\",\n    \"tsup\": \"^8.2.3\",\n    \"typescript\": \"^5.5.4\"\n  },\n  \"dependencies\": {\n    \"clsx\": \"^2.1.1\",\n    \"msw\": \"^2.3.4\",\n    \"react\": \"^18.3.1\",\n    \"react-error-boundary\": \"^4.0.13\",\n    \"react-use-keypress\": \"^1.3.1\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^18.0.0\"\n  }\n}\n"
  },
  {
    "path": "src/ErrorFallback.tsx",
    "content": "import { FallbackProps } from \"react-error-boundary\";\nimport Button from \"./components/Button\";\n\nexport default function ErrorFallback({\n  error,\n  resetErrorBoundary,\n}: Readonly<FallbackProps>) {\n  return (\n    <div role=\"alert\" className=\"grid h-screen place-content-center\">\n      <h1 className=\"font-bold text-xl\">Something went wrong.</h1>\n      <pre>{error.message}</pre>\n      <Button\n        className=\"bg-blue-600 text-white mt-4\"\n        onClick={resetErrorBoundary}\n      >\n        Try again\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/GeneralSettings.tsx",
    "content": "import * as React from \"react\";\nimport Field from \"./components/Field\";\nimport Select from \"./components/Select\";\nimport CopySettingsButton from \"./components/CopySettingsButton\";\nimport Button from \"./components/Button\";\nimport { Position } from \"./switchboard.types\";\nimport Checkbox from \"./components/Checkbox\";\nimport { getLocalStorageSwitchboardKeys } from \"./localStorage.utils\";\nimport { GeneralSettings } from \"./useSwitchboard\";\n\ninterface GeneralSettingsProps {\n  settings: GeneralSettings;\n  copySettingsUrlToClipboard: () => void;\n}\n\nexport default function GeneralSettings({\n  settings,\n  copySettingsUrlToClipboard,\n}: Readonly<GeneralSettingsProps>) {\n  const {\n    position,\n    setPosition,\n    openByDefault,\n    setOpenByDefault,\n    closeViaOutsideClick,\n    setCloseViaOutsideClick,\n    closeViaEscapeKey,\n    setCloseViaEscapeKey,\n  } = settings;\n\n  return (\n    <details className=\"sb-mt-4\" open>\n      <summary className=\"sb-mt-4 sb-font-bold\">General</summary>\n\n      <Field>\n        <Select\n          width=\"full\"\n          label=\"Position\"\n          value={position}\n          onChange={(e) => setPosition(e.target.value as Position)}\n        >\n          <option value=\"sb-top-left\">Top left</option>\n          <option value=\"sb-top-right\">Top Right</option>\n          <option value=\"sb-bottom-left\">Bottom left</option>\n          <option value=\"sb-bottom-right\">Bottom right</option>\n        </Select>\n      </Field>\n\n      <Field>\n        <Checkbox\n          id=\"openByDefault\"\n          label=\"Open by default\"\n          onChange={() => setOpenByDefault(!openByDefault)}\n          checked={openByDefault}\n        />\n      </Field>\n\n      <Field>\n        <Checkbox\n          id=\"closeViaEscapeKey\"\n          label=\"Close via escape key\"\n          onChange={() => setCloseViaEscapeKey(!closeViaEscapeKey)}\n          checked={closeViaEscapeKey}\n        />\n      </Field>\n\n      <Field>\n        <Checkbox\n          id=\"closeViaOutsideClick\"\n          label=\"Close via outside click\"\n          onChange={() => {\n            setCloseViaOutsideClick(!closeViaOutsideClick);\n          }}\n          checked={closeViaOutsideClick}\n        />\n      </Field>\n\n      <div className=\"sb-flex sb-flex-row\">\n        <Field>\n          <CopySettingsButton\n            className=\"sb-mr-2 sb-w-32\"\n            onClick={copySettingsUrlToClipboard}\n          />\n        </Field>\n\n        <Field>\n          <Button\n            className=\"sb-mr-2\"\n            onClick={() => {\n              const switchboardKeys = getLocalStorageSwitchboardKeys();\n              // Remove Switchboard settings from localStorage and reload\n              switchboardKeys.forEach((key) => localStorage.removeItem(key));\n              window.location.reload();\n            }}\n          >\n            Clear Settings\n          </Button>\n        </Field>\n      </div>\n    </details>\n  );\n}\n"
  },
  {
    "path": "src/Http.tsx",
    "content": "import HttpCustomResponseForm from \"./components/HttpCustomResponseForm\";\nimport Field from \"./components/Field\";\nimport Input from \"./components/Input\";\nimport Select from \"./components/Select\";\nimport { httpDefaults, HttpSettings } from \"./useSwitchboard\";\nimport { RequestHandler } from \"msw\";\n\ntype HttpProps = {\n  httpSettings: HttpSettings;\n  requestHandlers: RequestHandler[];\n};\n\nexport function Http({ httpSettings, requestHandlers }: Readonly<HttpProps>) {\n  const { delay, setDelay, delayChanged, customResponses, setCustomResponses } =\n    httpSettings;\n\n  return (\n    <details open>\n      <summary className=\"sb-mt-4 sb-font-bold\">HTTP</summary>\n      <Field>\n        <Input\n          id=\"globalDelay\"\n          width=\"full\"\n          changed={delayChanged}\n          type=\"number\"\n          label=\"Global Delay\"\n          value={delay}\n          onChange={(e) => setDelay(parseInt(e.target.value))}\n        />\n      </Field>\n\n      <Field>\n        <Select\n          width=\"full\"\n          label=\"Customize Request Handler\"\n          // Value need not change since the selected value disappears once selected.\n          value=\"\"\n          onChange={(e) => {\n            setCustomResponses([\n              ...customResponses,\n              {\n                handler: e.target.value,\n                delay: httpDefaults.delay,\n                status: httpDefaults.status,\n                response: httpDefaults.response,\n              },\n            ]);\n          }}\n        >\n          <option>Select Handler</option>\n          {requestHandlers\n            // Filter out handlers that are already customized\n            .filter(\n              (rh) => !customResponses.some((r) => r.handler === rh.info.header)\n            )\n            .sort((a, b) => a.info.header.localeCompare(b.info.header))\n            .map((rh) => (\n              <option key={rh.info.header}>{rh.info.header}</option>\n            ))}\n        </Select>\n      </Field>\n\n      {customResponses.map((setting) => (\n        <HttpCustomResponseForm\n          key={setting.handler}\n          customResponse={setting}\n          setCustomResponses={setCustomResponses}\n        />\n      ))}\n    </details>\n  );\n}\n"
  },
  {
    "path": "src/Switchboard.tsx",
    "content": "import React, { ComponentType, useState } from \"react\";\nimport cx from \"clsx\";\nimport CloseButton from \"./components/CloseButton\";\nimport OpenButton from \"./components/OpenButton\";\nimport { SwitchboardDefaults } from \"./switchboard.types\";\nimport { ErrorBoundary, FallbackProps } from \"react-error-boundary\";\nimport GeneralSettings from \"./GeneralSettings\";\nimport { useSwitchboard } from \"./useSwitchboard\";\nimport { Http } from \"./Http\";\nimport { RequestHandler } from \"msw\";\nimport { StartOptions } from \"msw/browser\";\nimport \"./index.css\";\nimport DefaultErrorFallback from \"./ErrorFallback\";\nimport { useHttp } from \"./useHttp\";\n\ninterface KeyboardShortcut {\n  key: string | string[];\n  alt?: boolean;\n  ctrl?: boolean;\n}\n\nexport interface SwitchboardMswSettings {\n  /** Function that returns an array of [Mock Service Worker](https://mswjs.io/) request handlers. */\n  requestHandlers: () => RequestHandler[];\n\n  /** [Mock Service worker start options](https://mswjs.io/docs/api/setup-worker/start/#options) */\n  startOptions?: StartOptions;\n}\n\ninterface SwitchboardProps {\n  /** The app to render */\n  appSlot: React.ReactNode;\n\n  /** CSS to apply to the root element. */\n  className?: string;\n\n  /** Specify optional default values for various settings */\n  defaults?: Partial<SwitchboardDefaults>;\n\n  /** Configure Mock Service Worker request handlers. */\n  mswSettings?: SwitchboardMswSettings;\n\n  /** Specify a keyboard shortcut that toggles the window open/closed */\n  openKeyboardShortcut?: KeyboardShortcut;\n\n  /** Custom content and settings to render inside Switchboard */\n  children?: React.ReactNode;\n\n  /** Error react-error-boundary fallback component to render if the app's top-level error boundary is hit. If omitted, Switchboard's default error fallback is used. */\n  ErrorFallback?: ComponentType<FallbackProps>;\n}\n\n/** Display custom devtools settings for your project */\nexport function Switchboard({\n  appSlot,\n  children,\n  mswSettings,\n  openKeyboardShortcut,\n  ErrorFallback,\n  className,\n  defaults,\n}: Readonly<SwitchboardProps>) {\n  const [mswIsReady, setMswIsReady] = useState(!mswSettings);\n  const {\n    generalSettings,\n    httpSettings,\n    switchboardWindowRef,\n    copySettingsUrlToClipboard,\n  } = useSwitchboard({\n    openKeyboardShortcut,\n    overriddenDefaults: defaults,\n  });\n\n  const { requestHandlers } = useHttp(() => setMswIsReady(true), mswSettings);\n\n  const { isOpen, setIsOpen, position } = generalSettings;\n\n  // TODO: Implement\n  const hasAppBehaviorChanges = false;\n\n  return (\n    <>\n      {/* Wrap app in ErrorBoundary so Switchboard continues to display even if the app errors */}\n      <ErrorBoundary FallbackComponent={ErrorFallback ?? DefaultErrorFallback}>\n        {mswIsReady ? appSlot : <p>Initializing msw...</p>}\n      </ErrorBoundary>\n\n      <section\n        ref={switchboardWindowRef}\n        className={cx(\n          \"sb-fixed sb-p-4 sb-border sb-shadow-xl sb-max-h-screen sb-overflow-auto sb-bg-white sb-opacity-90 sb-text-left\",\n          {\n            \"sb-w-16 sb-h-16\": !isOpen,\n            \"sb-bg-yellow-100\": !isOpen && hasAppBehaviorChanges,\n            \"sb-bottom-0\": position.includes(\"bottom\"),\n            \"sb-top-0\": position.includes(\"top\"),\n            \"sb-right-0\": position.includes(\"right\"),\n            \"sb-left-0\": position.includes(\"left\"),\n          },\n          className\n        )}\n      >\n        {isOpen ? (\n          <>\n            <CloseButton\n              aria-label=\"Close DevTools\"\n              onClick={() => setIsOpen(!isOpen)}\n            />\n            {children}\n\n            {requestHandlers && requestHandlers.length > 0 && (\n              <Http\n                httpSettings={httpSettings}\n                requestHandlers={requestHandlers}\n              />\n            )}\n            <GeneralSettings\n              settings={generalSettings}\n              copySettingsUrlToClipboard={copySettingsUrlToClipboard}\n            />\n          </>\n        ) : (\n          <OpenButton\n            aria-label=\"Open DevTools\"\n            onClick={() => setIsOpen(!isOpen)}\n          />\n        )}\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/clipboardUtils.ts",
    "content": "// Write the provided string to the clipboard\nexport async function writeToClipboard(content: string) {\n  const type = \"text/plain\";\n  const blob = new Blob([content], {\n    type,\n  });\n  const data = [new ClipboardItem({ [type]: blob })];\n  return navigator.clipboard.write(data);\n}\n"
  },
  {
    "path": "src/components/Button.tsx",
    "content": "import cx from \"clsx\";\nexport interface ButtonProps extends React.ComponentPropsWithoutRef<\"button\"> {\n  variant?: \"primary\" | \"secondary\" | \"icon\" | \"expander\";\n}\n\nexport default function Button({\n  className,\n  variant = \"primary\",\n  ...rest\n}: ButtonProps) {\n  return (\n    <button\n      className={cx(\n        className,\n        \"sb-border sb-border-slate-400 sb-p-1 sb-rounded\",\n        {\n          \"sb-bg-blue-600 sb-text-white\": variant === \"primary\",\n          \"sb-bg-white sb-border-none sb-p-1 sb-inline-flex sb-items-center sb-justify-center sb-text-gray-400 sb-hover:text-gray-500 sb-hover:bg-gray-100\":\n            variant === \"icon\",\n          \"sb-absolute sb-inset-0 sb-border-none sb-inline-flex sb-items-center sb-justify-center sb-text-gray-400 sb-hover:text-gray-500 sb-hover:backdrop-brightness-90\":\n            variant === \"expander\",\n        }\n      )}\n      {...rest}\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/Checkbox.tsx",
    "content": "import { ReactNode } from \"react\";\nimport cx from \"clsx\";\n\ninterface CheckboxProps extends React.ComponentPropsWithoutRef<\"input\"> {\n  /** Input label */\n  label: ReactNode;\n\n  /** Required for a11y */\n  id: string;\n}\n\nexport default function Checkbox(props: CheckboxProps) {\n  const { id, onChange, checked, className, ...rest } = props;\n  return (\n    <span>\n      <input\n        className={cx(\n          \"sb-border-slate-400 sb-border-solid sb-border p-1 sb-rounded\",\n          className\n        )}\n        type=\"checkbox\"\n        id={id}\n        checked={checked}\n        onChange={onChange}\n        {...rest}\n      />\n      <label className=\"sb-ml-4\" htmlFor={id}>\n        {props.label}\n      </label>\n    </span>\n  );\n}\n"
  },
  {
    "path": "src/components/CloseButton.tsx",
    "content": "import Button, { ButtonProps } from \"./Button\";\n\nexport default function CloseButton(props: ButtonProps) {\n  const { variant = \"icon\", ...rest } = props;\n  return (\n    <div className=\"sb-flex sb-flex-row-reverse\">\n      <Button variant={variant} {...rest}>\n        <span className=\"sb-sr-only\">Close menu</span>\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          className=\"sb-h-6 sb-w-6\"\n          fill=\"none\"\n          viewBox=\"0 0 24 24\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n        >\n          <path\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            d=\"M6 18L18 6M6 6l12 12\"\n          />\n        </svg>\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/CopySettingsButton.tsx",
    "content": "import { useState } from \"react\";\nimport Button from \"./Button\";\n\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\ninterface ButtonProps extends React.ComponentPropsWithoutRef<\"button\"> {}\n\nconst labelDefault = \"Copy Settings\";\n\nconst hideCopiedConfirmationAfterXMilliSeconds = 2000;\n\nexport default function CopySettingsButton({ onClick, ...rest }: ButtonProps) {\n  const [label, setLabel] = useState(labelDefault);\n\n  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {\n    setLabel(\"Copied ✅\");\n    if (onClick) onClick(e);\n    setTimeout(() => {\n      setLabel(labelDefault);\n    }, hideCopiedConfirmationAfterXMilliSeconds);\n  }\n\n  return (\n    <Button variant=\"primary\" onClick={handleClick} {...rest}>\n      {label}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "src/components/DeleteButton.tsx",
    "content": "import Button, { ButtonProps } from \"./Button\";\n\nexport default function DeleteButton(props: ButtonProps) {\n  return (\n    <Button variant=\"icon\" {...props}>\n      <span className=\"sb-sr-only\">Delete</span>\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        className=\"sb-h-6 w-6\"\n        fill=\"none\"\n        viewBox=\"0 0 24 24\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n      >\n        <path\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"\n        />\n      </svg>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "src/components/Field.tsx",
    "content": "type FieldProps = {\n  /** Child elements */\n  children: React.ReactNode;\n};\n\nexport default function Field({ children }: FieldProps) {\n  return <div className=\"sb-mt-4\">{children}</div>;\n}\n"
  },
  {
    "path": "src/components/HttpCustomResponseForm.tsx",
    "content": "import { CustomResponse } from \"../http.types\";\nimport DeleteButton from \"./DeleteButton\";\nimport Input from \"./Input\";\n\nexport const customResponseDefaults = {\n  delay: 0,\n  status: 200,\n  response: undefined,\n};\n\ntype CustomResponseFormProps = {\n  customResponse: CustomResponse;\n  setCustomResponses: React.Dispatch<React.SetStateAction<CustomResponse[]>>;\n};\n\nexport default function HttpCustomResponseForm({\n  customResponse,\n  setCustomResponses,\n}: Readonly<CustomResponseFormProps>) {\n  const { handler, delay, status, response } = customResponse;\n\n  // TODO: Support all response properties: https://mswjs.io/docs/api/response#properties\n  return (\n    <fieldset className=\"sb-mt-4 sb-border sb-p-2\">\n      <legend>\n        {handler}{\" \"}\n        <DeleteButton\n          onClick={() =>\n            setCustomResponses((r) => r.filter((e) => e.handler !== handler))\n          }\n        />\n      </legend>\n      <div className=\"sb-flex sb-flex-row\">\n        <Input\n          id={`${handler}-delay`}\n          type=\"number\"\n          changed={delay !== customResponseDefaults.delay}\n          label=\"Delay\"\n          className=\"sb-w-20 sb-mr-4\"\n          value={delay}\n          onChange={(e) =>\n            setCustomResponses((r) =>\n              r.map((s) =>\n                s.handler === handler\n                  ? {\n                      ...s,\n                      delay: parseInt(e.target.value),\n                    }\n                  : s\n              )\n            )\n          }\n        />\n\n        <Input\n          id={`${handler}-status`}\n          type=\"number\"\n          changed={status !== customResponseDefaults.status}\n          label=\"Status\"\n          className=\"sb-w-20 sb-mr-4\"\n          value={status}\n          onChange={(e) =>\n            setCustomResponses((r) =>\n              r.map((s) =>\n                s.handler === handler\n                  ? {\n                      ...s,\n                      status: parseInt(e.target.value),\n                    }\n                  : s\n              )\n            )\n          }\n        />\n\n        <Input\n          id={`${handler}-custom-response`}\n          type=\"text\"\n          changed={response !== customResponseDefaults.response}\n          label=\"Response\"\n          className=\"sb-w-20\"\n          value={response}\n          placeholder=\"Default\"\n          onChange={(e) =>\n            setCustomResponses((r) =>\n              r.map((s) =>\n                s.handler === handler\n                  ? {\n                      ...s,\n                      response: e.target.value,\n                    }\n                  : s\n              )\n            )\n          }\n        />\n      </div>\n    </fieldset>\n  );\n}\n"
  },
  {
    "path": "src/components/Input.tsx",
    "content": "import cx from \"clsx\";\nimport Label from \"./Label\";\n\ninterface InputProps extends React.ComponentPropsWithoutRef<\"input\"> {\n  /** Input ID - Specifying here so it's required by TypeScript */\n  id: string;\n\n  /** Input label */\n  label: string;\n\n  /** Set to true to highlight the label so that it is visually marked as changed from the default. */\n  changed?: boolean;\n\n  /** Specify input's width */\n  width?: \"full\" | \"default\";\n}\n\nexport default function Input(props: InputProps) {\n  const {\n    id,\n    onChange,\n    label,\n    value,\n    changed = false,\n    className,\n    width = \"default\",\n    ...rest\n  } = props;\n  return (\n    <span>\n      <Label className=\"block\" htmlFor={id}>\n        {label}\n      </Label>\n      <input\n        className={cx(\n          \"sb-border-slate-400 sb-border-solid sb-border sb-rounded p-1\",\n          { \"sb-bg-yellow-100\": changed },\n          { \"sb-w-full\": width === \"full\" },\n          className\n        )}\n        type=\"text\"\n        id={id}\n        value={value}\n        onChange={onChange}\n        {...rest}\n      />\n    </span>\n  );\n}\n"
  },
  {
    "path": "src/components/Label.tsx",
    "content": "interface LabelProps extends React.ComponentPropsWithoutRef<\"label\"> {\n  /** Label */\n  children: React.ReactNode;\n}\n\nexport default function Label({ children, htmlFor }: LabelProps) {\n  return (\n    <label className=\"sb-block\" htmlFor={htmlFor}>\n      {children}\n    </label>\n  );\n}\n"
  },
  {
    "path": "src/components/OpenButton.tsx",
    "content": "import Button, { ButtonProps } from \"./Button\";\n\nexport default function OpenButton(props: ButtonProps) {\n  return (\n    <Button variant=\"expander\" {...props}>\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        className=\"sb-h-6 sb-w-6\"\n        fill=\"none\"\n        viewBox=\"0 0 24 24\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n      >\n        <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M12 4v16m8-8H4\" />\n      </svg>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "src/components/Select.tsx",
    "content": "import clsx from \"clsx\";\nimport Label from \"./Label\";\n\ninterface SelectProps extends React.ComponentPropsWithoutRef<\"select\"> {\n  /** Set to true to highlight the label so that it is visually marked as changed from the default. */\n  changed?: boolean;\n\n  /** Input label */\n  label: string;\n\n  /** Specify select's width */\n  width?: \"full\" | \"default\";\n}\n\nexport default function Select(props: SelectProps) {\n  const {\n    id,\n    onChange,\n    width = \"default\",\n    changed = false,\n    label,\n    value,\n    ...rest\n  } = props;\n  return (\n    <>\n      <Label className=\"sb-block\" htmlFor={id}>\n        {label}\n      </Label>\n      <select\n        className={clsx(\n          \"sb-border-slate-400 sb-border-solid sb-border sb-p-1 sb-rounded\",\n          {\n            \"sb-bg-yellow-100\": changed,\n            \"sb-w-full\": width === \"full\",\n          }\n        )}\n        id={id}\n        value={value}\n        onChange={onChange}\n        {...rest}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/http.types.ts",
    "content": "import { RequestHandler } from \"msw\";\nimport { StartOptions } from \"msw/browser\";\n\nexport interface CustomResponse {\n  /** Response handler name */\n  handler: string;\n\n  /** Delay the response by a specified number of milliseconds. */\n  delay?: number;\n\n  /** HTTP status code to return for this call */\n  status?: number;\n\n  /** Optional response. */\n  response?: string;\n}\n\nexport interface MswSettings {\n  /** A function that accepts custom settings and returns an array of Mock Service Worker request handlers */\n  requestHandlers: () => RequestHandler[];\n\n  /** Optional Mock Service worker start options */\n  startOptions?: StartOptions;\n\n  /** Global delay in milliseconds */\n  delay?: number;\n\n  /** Array of custom responses */\n  customResponses: CustomResponse[];\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { useSwitchboardState } from \"./useSwitchboardState\";\nimport { useSwitchboard } from \"./useSwitchboard\";\nimport { Switchboard } from \"./Switchboard\";\nimport { Http } from \"./Http\";\nimport { useHttp } from \"./useHttp\";\nimport { CustomResponse, MswSettings } from \"./http.types\";\nimport {\n  Position,\n  switchboardPositions,\n  SwitchboardDefaults,\n  SwitchboardConfig,\n} from \"./switchboard.types\";\nimport { customResponseDefaults } from \"./components/HttpCustomResponseForm\";\n\nexport {\n  useSwitchboard,\n  useSwitchboardState,\n  Http,\n  useHttp,\n  CustomResponse,\n  Position,\n  switchboardPositions,\n  SwitchboardDefaults,\n  SwitchboardConfig,\n  MswSettings,\n  customResponseDefaults,\n};\n\nexport default Switchboard;\n"
  },
  {
    "path": "src/input.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "src/localStorage.utils.ts",
    "content": "// Get list of localStorage items that start with \"sb-\"\nexport function getLocalStorageSwitchboardKeys() {\n  return Object.keys(localStorage).filter((key) => key.startsWith(\"sb-\"));\n}\n"
  },
  {
    "path": "src/switchboard.types.ts",
    "content": "import { CustomResponse } from \"./http.types\";\n\nexport const switchboardPositions = [\n  \"top-left\",\n  \"top-right\",\n  \"bottom-left\",\n  \"bottom-right\",\n] as const;\n\n/** Union of Switchboard window positions */\nexport type Position = (typeof switchboardPositions)[number];\n\n/** Setting defaults */\nexport interface SwitchboardDefaults {\n  /** Set to true to enable closing Switchboard by clicking outside the window by default */\n  closeViaOutsideClick: boolean;\n\n  /** When true, close Switchboard when the escape key is pressed */\n  closeViaEscapeKey?: boolean;\n\n  /** The default delay for mock HTTP requests */\n  delay: number;\n\n  /** The default window position */\n  position: Position;\n\n  /** Set to true to open Switchboard by default */\n  openByDefault: boolean;\n}\n\nexport interface SwitchboardConfig {\n  /** Set to true to open the DevTools window by default */\n  openByDefault: boolean;\n\n  /** Switchboard window position */\n  position: Position;\n\n  /** Global HTTP delay */\n  delay: number;\n\n  /** Array of custom responses */\n  customResponses: CustomResponse[];\n}\n"
  },
  {
    "path": "src/types/react-use-keypress.d.ts",
    "content": "// TODO: Remove this when types are provided. Pull from https://github.com/jacobbuck/react-use-keypress/issues/6#issue-1319821201\ndeclare module \"react-use-keypress\" {\n  export default function useKeyPress(\n    key: KeyboardEvent[\"key\"] | KeyboardEvent[\"key\"][],\n    callback?: (e: KeyboardEvent) => void\n  );\n}\n"
  },
  {
    "path": "src/useHttp.ts",
    "content": "import { useEffect } from \"react\";\nimport { setupWorker } from \"msw/browser\";\nimport { SwitchboardMswSettings } from \"./Switchboard\";\n\n/** Configure msw */\nexport function useHttp(\n  setIsReady: () => void,\n  mswSettings?: SwitchboardMswSettings\n) {\n  useEffect(() => {\n    if (!mswSettings) {\n      setIsReady();\n      return;\n    }\n    const setup = async () => {\n      const worker = setupWorker(...mswSettings.requestHandlers());\n      await worker.start(mswSettings.startOptions);\n      setIsReady();\n    };\n    setup();\n  }, []);\n\n  return {\n    requestHandlers: mswSettings?.requestHandlers(),\n  };\n}\n"
  },
  {
    "path": "src/useOutsideClick.ts",
    "content": "import React, { useEffect } from \"react\";\n\n/**\n * Call a function when the user clicks outside the ref passed.\n * @param ref Clicks outside this element will trigger the function provided to onOutsideClick\n * @param onOutsideClick Function called when the user clicks outside the element specified in the ref argument\n * @returns void\n */\nexport default function useOutsideClick(\n  ref: React.RefObject<HTMLElement>,\n  onOutsideClick: (event: globalThis.MouseEvent) => void\n) {\n  useEffect(() => {\n    function handleClickOutside(event: globalThis.MouseEvent) {\n      if (\n        ref.current &&\n        event.target instanceof Node &&\n        !ref.current.contains(event.target)\n      ) {\n        onOutsideClick(event);\n      }\n    }\n\n    // Bind the event listener\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      // Unbind the event listener on clean up\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  });\n}\n"
  },
  {
    "path": "src/useSwitchboard.ts",
    "content": "import React, { useState, useRef } from \"react\";\nimport useKeypress from \"react-use-keypress\";\nimport useOutsideClick from \"./useOutsideClick\";\nimport { Position, SwitchboardDefaults } from \"./switchboard.types\";\nimport { writeToClipboard } from \"./clipboardUtils\";\nimport { useSwitchboardState } from \"./useSwitchboardState\";\nimport { getLocalStorageSwitchboardKeys } from \"./localStorage.utils\";\nimport { CustomResponse } from \"./http.types\";\n\nconst maxUrlLength = 2000;\n\nexport const httpDefaults = {\n  delay: 0,\n  status: 200,\n  response: undefined,\n};\n\ninterface KeyboardShortcut {\n  key: string | string[];\n  alt?: boolean;\n  ctrl?: boolean;\n}\n\nexport interface GeneralSettings {\n  isOpen: boolean;\n  setIsOpen: (isOpen: boolean) => void;\n  position: Position;\n  setPosition: (position: Position) => void;\n  openByDefault: boolean;\n  setOpenByDefault: (openByDefault: boolean) => void;\n  closeViaOutsideClick: boolean;\n  setCloseViaOutsideClick: (closeViaOutsideClick: boolean) => void;\n  closeViaEscapeKey: boolean;\n  setCloseViaEscapeKey: (closeViaEscapeKey: boolean) => void;\n}\n\nexport interface HttpSettings {\n  delay: number;\n  setDelay: (delay: number) => void;\n  delayChanged: boolean;\n  customResponses: CustomResponse[];\n  setCustomResponses: React.Dispatch<React.SetStateAction<CustomResponse[]>>;\n}\n\ninterface UseSwitchboardArgs {\n  /** Override the built in setting defaults */\n  overriddenDefaults?: Partial<SwitchboardDefaults>;\n\n  /** Specify a keyboard shortcut that toggles the window open/closed */\n  openKeyboardShortcut?: KeyboardShortcut;\n}\n\n/** This component is useful to display custom devtools settings for your project */\nexport function useSwitchboard({\n  openKeyboardShortcut,\n  overriddenDefaults,\n}: UseSwitchboardArgs | undefined = {}) {\n  // These settings use the useSwitchboardState hook so that the settings persist in localStorage and are optionally initialized via the URL\n  const [openByDefault, setOpenByDefault] = useSwitchboardState(\n    \"sb-openByDefault\",\n    overriddenDefaults?.openByDefault ?? true\n  );\n\n  const [isOpen, setIsOpen] = useState(openByDefault);\n\n  const [closeViaOutsideClick, setCloseViaOutsideClick] = useSwitchboardState(\n    \"sb-closeViaOutsideClick\",\n    overriddenDefaults?.closeViaOutsideClick ?? false\n  );\n\n  const [closeViaEscapeKey, setCloseViaEscapeKey] = useSwitchboardState(\n    \"sb-closeViaEscapeKey\",\n    overriddenDefaults?.closeViaEscapeKey ?? true\n  );\n\n  const [position, setPosition] = useSwitchboardState(\n    \"sb-position\",\n    overriddenDefaults?.position ?? \"top-left\"\n  );\n\n  const [delay, setDelay, delayChanged] = useSwitchboardState(\n    \"sb-delay\",\n    httpDefaults.delay\n  );\n\n  const [customResponses, setCustomResponses] = useSwitchboardState<\n    CustomResponse[]\n  >(\"sb-customResponses\", []);\n\n  const switchboardWindowRef = useRef<HTMLDivElement>(null);\n\n  useKeypress(\"Escape\", () => {\n    if (closeViaEscapeKey) setIsOpen(false);\n  });\n\n  useKeypress(openKeyboardShortcut ? openKeyboardShortcut.key : [], (e) => {\n    if (openKeyboardShortcut?.alt && !e.altKey) return;\n    if (openKeyboardShortcut?.ctrl && !e.ctrlKey) return;\n    setIsOpen((current) => !current);\n  });\n\n  useOutsideClick(switchboardWindowRef, () => {\n    if (closeViaOutsideClick) setIsOpen(false);\n  });\n\n  // Convert the settings to URL search params\n  function getSettingsAsQueryParams() {\n    const switchboardKeys = getLocalStorageSwitchboardKeys();\n\n    // Encode the settings into search params\n    const params = new URLSearchParams();\n    switchboardKeys.forEach((key) => {\n      params.set(key, localStorage.getItem(key)!);\n    });\n\n    return \"?\" + params.toString();\n  }\n\n  async function copySettingsUrlToClipboard() {\n    const url = window.location.href + getSettingsAsQueryParams();\n    try {\n      await writeToClipboard(url);\n      if (url.length > maxUrlLength) {\n        alert(\n          `Warning: The URL copied to your clipboard may not work in all browsers because it's over ${maxUrlLength} characters. To reduce the length, consider redesigning your settings state to store identifiers (such as recordId=1) instead of specifying raw data.`\n        );\n      }\n    } catch (err) {\n      () => alert(\"Failed to copy settings URL to clipboard\");\n    }\n  }\n\n  const generalSettings: GeneralSettings = {\n    isOpen,\n    setIsOpen,\n    position,\n    setPosition,\n    openByDefault,\n    setOpenByDefault,\n    closeViaOutsideClick,\n    setCloseViaOutsideClick,\n    closeViaEscapeKey,\n    setCloseViaEscapeKey,\n  };\n\n  const httpSettings: HttpSettings = {\n    delay,\n    setDelay,\n    delayChanged,\n    customResponses,\n    setCustomResponses,\n  };\n\n  return {\n    generalSettings,\n    httpSettings,\n    copySettingsUrlToClipboard,\n    switchboardWindowRef,\n  };\n}\n"
  },
  {
    "path": "src/useSwitchboardState.ts",
    "content": "import { useCallback, useState } from \"react\";\n\n/** Returns a string that contains the current URL with the specified key and value in the querystring */\nfunction getUrlWithUpdatedQuery(url: URL, key: string, value: unknown = null) {\n  const urlWithoutQuerystring = url.href.split(\"?\")[0];\n  const params = new URLSearchParams(url.search);\n  // Remove existing querystring if it exists. Here's why:\n  // 1. This assures the newly generated URL doesn't contain the param twice.\n  // 2. We only add the param if a value is provided, so removing it cleans up the URL if no value has been provided for the key.\n  params.delete(key);\n  if (value) params.append(key, JSON.stringify(value));\n  return urlWithoutQuerystring + \"?\" + params.toString();\n}\n\ninterface SwitchboardStateOptions {\n  /** Set to true to show values that match the default value in the URL.\n   * By default, if the selected value matches the default value, it's omitted from the URL.\n   * This keeps the URL as short as possible.  */\n  // TODO: Finish refactor to union\n  urlBehavior?: \"initialization-only\" | \"initialize-and-display-always\";\n\n  /** Set to true to store values that match the default value in localStorage.\n   * By default, if the selected value matches the default value, it's omitted from localStorage.\n   * This keeps localStorage as minimal as possible.\n   */\n  storeDefaultValuesInLocalStorage?: boolean;\n}\n\ntype SwitchboardKey<TKey, TPrefix extends string> = TKey extends string\n  ? `${TPrefix}${TKey}`\n  : never;\n\n/**\n * This hook makes it easy to declare state for devtools.\n * It's a fork of https://usehooks.com/useLocalStorage/,\n * but enhanced to read the URL as a way to override the specified default.\n * Since DevTools often benefit from being initialized via the URL,\n * it reads optional default values from the URL. And since it's handy\n * for the DevTools to \"remember\" settings between hard refreshes,\n * it writes settings to localStorage onChange.\n *\n * Finally, if neither the URL or localStorage is set, it falls back\n * to the provided default.\n * In summary, it sets the default value in the following order:\n * 1. URL\n * 2. localStorage\n * 3. Specified default\n *\n * So, in other words, if the URL isn't provided, it falls back to localStorage.\n * If localStorage isn't set, it falls back to the specified default.\n *\n * This hook writes each state change to 2 spots:\n * 1. localStorage (so settings persist after the tab is closed)\n * 2. local state variable (so React renders when the state changes)\n *\n *\n * @param key The URL param to check for the default, as well as the key used to write the value to localStorage\n * @param defaultValue The default value to use if the URL and localStorage don't have a matching value for the provided key.\n * */\nexport function useSwitchboardState<T>(\n  /** Prefix each key with \"sb-\" to \"namespace\" all Switchboard settings. This avoids naming collisions and supports easily removing only the Switchboard settings from localStorage when necessary. */\n  key: SwitchboardKey<string, \"sb-\">,\n  defaultValue: T,\n  options?: SwitchboardStateOptions\n) {\n  // State to store our value\n  // Pass initial state function to useState so logic is only executed once\n  const [storedValue, setStoredValue] = useState<T>(() => {\n    if (typeof window === \"undefined\") {\n      return defaultValue;\n    }\n\n    // First, check the URL for a value and use it for the default if found.\n    const params = new URLSearchParams(window.location.search);\n    const urlValue = params.get(key);\n    if (urlValue) {\n      // TODO: Validate the object\n      const parsedObject = JSON.parse(urlValue);\n      // Update localStorage with URL value too\n      // TODO: Use localforage instead.\n      window.localStorage.setItem(key, JSON.stringify(parsedObject));\n\n      // Clear out the URL now that we read the value and stored it in localStorage. This keeps the URL clean.\n      // TODO: Make this an option\n      const newUrl = getUrlWithUpdatedQuery(new URL(window.location.href), key);\n      window.history.pushState(\"\", \"DevTools state update\", newUrl);\n\n      return parsedObject;\n    }\n\n    // If URL doesn't contain the key, then fall back to checking localStorage for a default value\n    try {\n      // Get from local storage by key\n      const item = window.localStorage.getItem(key);\n      // Parse stored json or if none return initialValue\n\n      // TODO: Use Zod to validate the querystring\n      return item ? JSON.parse(item) : defaultValue;\n    } catch (error) {\n      // If error also return initialValue\n      console.error(error);\n      return defaultValue;\n    }\n  });\n\n  // Return a wrapped version of useState's setter function that persists the new value to localStorage.\n  const setValue = useCallback(\n    (value: T | ((val: T) => T)) => {\n      try {\n        // Allow value to be a function so we have same API as useState\n        const valueToStore =\n          value instanceof Function ? value(storedValue) : value;\n\n        // Step 1: Save state, so React re-renders\n        setStoredValue(valueToStore);\n\n        // Step 2: Save to localStorage, so the settings persist after the window is closed\n        if (typeof window !== \"undefined\") {\n          // If the value is the initial value, then we can omit it from localStorage.\n          // But, go ahead and put it in localStorage anyway if storeDefaultValuesInLocalStorage is true.\n          if (\n            valueToStore == defaultValue &&\n            !options?.storeDefaultValuesInLocalStorage\n          ) {\n            window.localStorage.removeItem(key);\n          } else {\n            window.localStorage.setItem(key, JSON.stringify(valueToStore));\n          }\n        }\n      } catch (error) {\n        // TODO: Improve error handling\n        console.error(error);\n      }\n    },\n    [defaultValue, key, options?.storeDefaultValuesInLocalStorage, storedValue]\n  );\n\n  const isChanged = storedValue !== defaultValue;\n\n  return [storedValue, setValue, isChanged] as const;\n}\n"
  },
  {
    "path": "tailwind.config.cjs",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  prefix: \"sb-\",\n  content: [\"./src/**/*.{js,ts,jsx,tsx}\"],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n  corePlugins: {\n    preflight: false,\n  },\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Default\",\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"inlineSources\": false,\n    \"isolatedModules\": false,\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"exclude\": [\"dist\", \"node_modules\"]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig((options) => ({\n  entry: [\"src/index.ts\"],\n  clean: true,\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  external: [\"react\", \"msw\"],\n  ...options,\n}));\n"
  }
]