Full Code of coryhouse/react-switchboard for AI

main 07a28f1adbf8 cached
47 files
49.9 KB
13.8k tokens
47 symbols
1 requests
Download .txt
Repository: coryhouse/react-switchboard
Branch: main
Commit: 07a28f1adbf8
Files: 47
Total size: 49.9 KB

Directory structure:
gitextract_27h3g6k8/

├── .gitignore
├── LICENSE
├── README.md
├── examples/
│   └── vite-hello-world/
│       ├── .gitignore
│       ├── README.md
│       ├── eslint.config.js
│       ├── index.html
│       ├── package.json
│       ├── src/
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── index.css
│       │   ├── main.tsx
│       │   └── vite-env.d.ts
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── package.json
├── src/
│   ├── ErrorFallback.tsx
│   ├── GeneralSettings.tsx
│   ├── Http.tsx
│   ├── Switchboard.tsx
│   ├── clipboardUtils.ts
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Checkbox.tsx
│   │   ├── CloseButton.tsx
│   │   ├── CopySettingsButton.tsx
│   │   ├── DeleteButton.tsx
│   │   ├── Field.tsx
│   │   ├── HttpCustomResponseForm.tsx
│   │   ├── Input.tsx
│   │   ├── Label.tsx
│   │   ├── OpenButton.tsx
│   │   └── Select.tsx
│   ├── http.types.ts
│   ├── index.ts
│   ├── input.css
│   ├── localStorage.utils.ts
│   ├── switchboard.types.ts
│   ├── types/
│   │   └── react-use-keypress.d.ts
│   ├── useHttp.ts
│   ├── useOutsideClick.ts
│   ├── useSwitchboard.ts
│   └── useSwitchboardState.ts
├── tailwind.config.cjs
├── tsconfig.json
└── tsup.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# Generated CSS
src/index.css


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Cory House

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# React Switchboard 🎛

Quickly create custom DevTools for your React app.

- [Live Demo](https://switchboard-beta.vercel.app/) 🚀
- [Demo repo using Vite](https://github.com/coryhouse/switchboard-with-vite-demo)

## Quick Start

```
npm install react-switchboard -D
```

### Vite Example

Call `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.

```tsx
import { lazy } from "react";
import { createRoot } from "react-dom/client";
import "react-switchboard/dist/index.css";

const Switchboard = lazy(() => import("react-switchboard"));

createRoot(document.getElementById("root")!).render(
  import.meta.env.PROD ? (
    <App />
  ) : (
    <Suspense fallback="Loading Switchboard...">
      <Switchboard appSlot={<App />} />
    </Suspense>
  )
);
```

## Headless

The `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.

```tsx
function CustomSwitchboard() {
  const { generalSettings, switchboardWindowRef, copySettingsUrlToClipboard } =
    useSwitchboard();

  // Use useSwitchboardState hook for custom settings.
  const [user, setUser] = useSwitchboardState("sb-user", null);

  return {
    /* Your custom JSX to render your desired UI */
  };
}
```

## Why Switchboard?

Code faster.
Reproduce edge cases.
Do real-time demos.
Use Switchboard to configure automated tests.

### Common Uses

- Login / switch users instantly
- Change feature toggles
- Configure mock APIs
- Force errors
- Simulate network slowness for specific requests
- Configure automated test scenarios
- Simulate incoming traffic and write conflicts

More 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)

## API

### Components

- `Switchboard` - The main component that renders your app and the Switchboard UI.

### Hooks

- `useSwitchboard` - Logic for running Switchboard. Useful to create a custom Switchboard UI.
- `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.

## FAQ

- **How does mocking work?** Switchboard intercepts fetch requests via [Mock Service Worker](https://mswjs.io/), and displays a UI for configuring the mock responses.
- **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.

- **Why lazy loading?** Lazy load `Switchboard` via `React.lazy` and `Suspense` so that it's excluded your app's prod bundle.

- **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):

```bash
"dev": "cross-env VITE_ENABLE_SWITCHBOARD=Y vite",
```

Then, read this environment variable in your app's entry point.

## Acknowledgements

- [Mock Service Worker](https://mswjs.io/) - Switchboard mocks HTTP requests via msw.

## Inspiration

- [React Query Devtools](http://react-query.tanstack.com/devtools)
- [https://github.com/dataarts/dat.gui](https://github.com/dataarts/dat.gui?tab=readme-ov-file)


================================================
FILE: examples/vite-hello-world/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: examples/vite-hello-world/README.md
================================================
# React Switchboard with Vite - Hello World

See [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`.


================================================
FILE: examples/vite-hello-world/eslint.config.js
================================================
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config({
  extends: [js.configs.recommended, ...tseslint.configs.recommended],
  files: ['**/*.{ts,tsx}'],
  ignores: ['dist'],
  languageOptions: {
    ecmaVersion: 2020,
    globals: globals.browser,
  },
  plugins: {
    'react-hooks': reactHooks,
    'react-refresh': reactRefresh,
  },
  rules: {
    ...reactHooks.configs.recommended.rules,
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
  },
})


================================================
FILE: examples/vite-hello-world/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


================================================
FILE: examples/vite-hello-world/package.json
================================================
{
  "name": "vite-hello-world",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.8.0",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^9.8.0",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.9",
    "globals": "^15.9.0",
    "react-switchboard": "latest",
    "typescript": "^5.5.3",
    "typescript-eslint": "^8.0.0",
    "vite": "^5.4.0"
  }
}


================================================
FILE: examples/vite-hello-world/src/App.css
================================================
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}


================================================
FILE: examples/vite-hello-world/src/App.tsx
================================================
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App


================================================
FILE: examples/vite-hello-world/src/index.css
================================================
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}


================================================
FILE: examples/vite-hello-world/src/main.tsx
================================================
import { lazy, StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "react-switchboard/dist/index.css";
import "./index.css";

const Switchboard = lazy(() => import("react-switchboard"));

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    {import.meta.env.DEV ? (
      <Suspense fallback="Loading Switchboard...">
        <Switchboard appSlot={<App />} />
      </Suspense>
    ) : (
      <App />
    )}
  </StrictMode>
);


================================================
FILE: examples/vite-hello-world/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: examples/vite-hello-world/tsconfig.app.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}


================================================
FILE: examples/vite-hello-world/tsconfig.json
================================================
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}


================================================
FILE: examples/vite-hello-world/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/vite-hello-world/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})


================================================
FILE: package.json
================================================
{
  "name": "react-switchboard",
  "version": "0.0.27",
  "description": "Quickly create custom DevTools for your React app",
  "scripts": {
    "prebuild": "tailwindcss -i ./src/input.css -o src/index.css",
    "build": "tsup",
    "test": "echo \"Error: no test specified\" && exit 1",
    "prepublish": "npm run build",
    "knip": "knip"
  },
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./dist/index.css": {
      "import": "./dist/index.css",
      "require": "./dist/index.css"
    }
  },
  "types": "./dist/index.d.ts",
  "files": [
    "dist"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/coryhouse/react-switchboard.git"
  },
  "keywords": [
    "react",
    "devtools",
    "reusable",
    "component",
    "toolkit"
  ],
  "author": "Cory House",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/coryhouse/react-switchboard/issues"
  },
  "homepage": "https://github.com/coryhouse/react-switchboard#readme",
  "devDependencies": {
    "@types/react": "^18.3.3",
    "knip": "^5.27.1",
    "tailwindcss": "^3.4.7",
    "tsup": "^8.2.3",
    "typescript": "^5.5.4"
  },
  "dependencies": {
    "clsx": "^2.1.1",
    "msw": "^2.3.4",
    "react": "^18.3.1",
    "react-error-boundary": "^4.0.13",
    "react-use-keypress": "^1.3.1"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  }
}


================================================
FILE: src/ErrorFallback.tsx
================================================
import { FallbackProps } from "react-error-boundary";
import Button from "./components/Button";

export default function ErrorFallback({
  error,
  resetErrorBoundary,
}: Readonly<FallbackProps>) {
  return (
    <div role="alert" className="grid h-screen place-content-center">
      <h1 className="font-bold text-xl">Something went wrong.</h1>
      <pre>{error.message}</pre>
      <Button
        className="bg-blue-600 text-white mt-4"
        onClick={resetErrorBoundary}
      >
        Try again
      </Button>
    </div>
  );
}


================================================
FILE: src/GeneralSettings.tsx
================================================
import * as React from "react";
import Field from "./components/Field";
import Select from "./components/Select";
import CopySettingsButton from "./components/CopySettingsButton";
import Button from "./components/Button";
import { Position } from "./switchboard.types";
import Checkbox from "./components/Checkbox";
import { getLocalStorageSwitchboardKeys } from "./localStorage.utils";
import { GeneralSettings } from "./useSwitchboard";

interface GeneralSettingsProps {
  settings: GeneralSettings;
  copySettingsUrlToClipboard: () => void;
}

export default function GeneralSettings({
  settings,
  copySettingsUrlToClipboard,
}: Readonly<GeneralSettingsProps>) {
  const {
    position,
    setPosition,
    openByDefault,
    setOpenByDefault,
    closeViaOutsideClick,
    setCloseViaOutsideClick,
    closeViaEscapeKey,
    setCloseViaEscapeKey,
  } = settings;

  return (
    <details className="sb-mt-4" open>
      <summary className="sb-mt-4 sb-font-bold">General</summary>

      <Field>
        <Select
          width="full"
          label="Position"
          value={position}
          onChange={(e) => setPosition(e.target.value as Position)}
        >
          <option value="sb-top-left">Top left</option>
          <option value="sb-top-right">Top Right</option>
          <option value="sb-bottom-left">Bottom left</option>
          <option value="sb-bottom-right">Bottom right</option>
        </Select>
      </Field>

      <Field>
        <Checkbox
          id="openByDefault"
          label="Open by default"
          onChange={() => setOpenByDefault(!openByDefault)}
          checked={openByDefault}
        />
      </Field>

      <Field>
        <Checkbox
          id="closeViaEscapeKey"
          label="Close via escape key"
          onChange={() => setCloseViaEscapeKey(!closeViaEscapeKey)}
          checked={closeViaEscapeKey}
        />
      </Field>

      <Field>
        <Checkbox
          id="closeViaOutsideClick"
          label="Close via outside click"
          onChange={() => {
            setCloseViaOutsideClick(!closeViaOutsideClick);
          }}
          checked={closeViaOutsideClick}
        />
      </Field>

      <div className="sb-flex sb-flex-row">
        <Field>
          <CopySettingsButton
            className="sb-mr-2 sb-w-32"
            onClick={copySettingsUrlToClipboard}
          />
        </Field>

        <Field>
          <Button
            className="sb-mr-2"
            onClick={() => {
              const switchboardKeys = getLocalStorageSwitchboardKeys();
              // Remove Switchboard settings from localStorage and reload
              switchboardKeys.forEach((key) => localStorage.removeItem(key));
              window.location.reload();
            }}
          >
            Clear Settings
          </Button>
        </Field>
      </div>
    </details>
  );
}


================================================
FILE: src/Http.tsx
================================================
import HttpCustomResponseForm from "./components/HttpCustomResponseForm";
import Field from "./components/Field";
import Input from "./components/Input";
import Select from "./components/Select";
import { httpDefaults, HttpSettings } from "./useSwitchboard";
import { RequestHandler } from "msw";

type HttpProps = {
  httpSettings: HttpSettings;
  requestHandlers: RequestHandler[];
};

export function Http({ httpSettings, requestHandlers }: Readonly<HttpProps>) {
  const { delay, setDelay, delayChanged, customResponses, setCustomResponses } =
    httpSettings;

  return (
    <details open>
      <summary className="sb-mt-4 sb-font-bold">HTTP</summary>
      <Field>
        <Input
          id="globalDelay"
          width="full"
          changed={delayChanged}
          type="number"
          label="Global Delay"
          value={delay}
          onChange={(e) => setDelay(parseInt(e.target.value))}
        />
      </Field>

      <Field>
        <Select
          width="full"
          label="Customize Request Handler"
          // Value need not change since the selected value disappears once selected.
          value=""
          onChange={(e) => {
            setCustomResponses([
              ...customResponses,
              {
                handler: e.target.value,
                delay: httpDefaults.delay,
                status: httpDefaults.status,
                response: httpDefaults.response,
              },
            ]);
          }}
        >
          <option>Select Handler</option>
          {requestHandlers
            // Filter out handlers that are already customized
            .filter(
              (rh) => !customResponses.some((r) => r.handler === rh.info.header)
            )
            .sort((a, b) => a.info.header.localeCompare(b.info.header))
            .map((rh) => (
              <option key={rh.info.header}>{rh.info.header}</option>
            ))}
        </Select>
      </Field>

      {customResponses.map((setting) => (
        <HttpCustomResponseForm
          key={setting.handler}
          customResponse={setting}
          setCustomResponses={setCustomResponses}
        />
      ))}
    </details>
  );
}


================================================
FILE: src/Switchboard.tsx
================================================
import React, { ComponentType, useState } from "react";
import cx from "clsx";
import CloseButton from "./components/CloseButton";
import OpenButton from "./components/OpenButton";
import { SwitchboardDefaults } from "./switchboard.types";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import GeneralSettings from "./GeneralSettings";
import { useSwitchboard } from "./useSwitchboard";
import { Http } from "./Http";
import { RequestHandler } from "msw";
import { StartOptions } from "msw/browser";
import "./index.css";
import DefaultErrorFallback from "./ErrorFallback";
import { useHttp } from "./useHttp";

interface KeyboardShortcut {
  key: string | string[];
  alt?: boolean;
  ctrl?: boolean;
}

export interface SwitchboardMswSettings {
  /** Function that returns an array of [Mock Service Worker](https://mswjs.io/) request handlers. */
  requestHandlers: () => RequestHandler[];

  /** [Mock Service worker start options](https://mswjs.io/docs/api/setup-worker/start/#options) */
  startOptions?: StartOptions;
}

interface SwitchboardProps {
  /** The app to render */
  appSlot: React.ReactNode;

  /** CSS to apply to the root element. */
  className?: string;

  /** Specify optional default values for various settings */
  defaults?: Partial<SwitchboardDefaults>;

  /** Configure Mock Service Worker request handlers. */
  mswSettings?: SwitchboardMswSettings;

  /** Specify a keyboard shortcut that toggles the window open/closed */
  openKeyboardShortcut?: KeyboardShortcut;

  /** Custom content and settings to render inside Switchboard */
  children?: React.ReactNode;

  /** 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. */
  ErrorFallback?: ComponentType<FallbackProps>;
}

/** Display custom devtools settings for your project */
export function Switchboard({
  appSlot,
  children,
  mswSettings,
  openKeyboardShortcut,
  ErrorFallback,
  className,
  defaults,
}: Readonly<SwitchboardProps>) {
  const [mswIsReady, setMswIsReady] = useState(!mswSettings);
  const {
    generalSettings,
    httpSettings,
    switchboardWindowRef,
    copySettingsUrlToClipboard,
  } = useSwitchboard({
    openKeyboardShortcut,
    overriddenDefaults: defaults,
  });

  const { requestHandlers } = useHttp(() => setMswIsReady(true), mswSettings);

  const { isOpen, setIsOpen, position } = generalSettings;

  // TODO: Implement
  const hasAppBehaviorChanges = false;

  return (
    <>
      {/* Wrap app in ErrorBoundary so Switchboard continues to display even if the app errors */}
      <ErrorBoundary FallbackComponent={ErrorFallback ?? DefaultErrorFallback}>
        {mswIsReady ? appSlot : <p>Initializing msw...</p>}
      </ErrorBoundary>

      <section
        ref={switchboardWindowRef}
        className={cx(
          "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",
          {
            "sb-w-16 sb-h-16": !isOpen,
            "sb-bg-yellow-100": !isOpen && hasAppBehaviorChanges,
            "sb-bottom-0": position.includes("bottom"),
            "sb-top-0": position.includes("top"),
            "sb-right-0": position.includes("right"),
            "sb-left-0": position.includes("left"),
          },
          className
        )}
      >
        {isOpen ? (
          <>
            <CloseButton
              aria-label="Close DevTools"
              onClick={() => setIsOpen(!isOpen)}
            />
            {children}

            {requestHandlers && requestHandlers.length > 0 && (
              <Http
                httpSettings={httpSettings}
                requestHandlers={requestHandlers}
              />
            )}
            <GeneralSettings
              settings={generalSettings}
              copySettingsUrlToClipboard={copySettingsUrlToClipboard}
            />
          </>
        ) : (
          <OpenButton
            aria-label="Open DevTools"
            onClick={() => setIsOpen(!isOpen)}
          />
        )}
      </section>
    </>
  );
}


================================================
FILE: src/clipboardUtils.ts
================================================
// Write the provided string to the clipboard
export async function writeToClipboard(content: string) {
  const type = "text/plain";
  const blob = new Blob([content], {
    type,
  });
  const data = [new ClipboardItem({ [type]: blob })];
  return navigator.clipboard.write(data);
}


================================================
FILE: src/components/Button.tsx
================================================
import cx from "clsx";
export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
  variant?: "primary" | "secondary" | "icon" | "expander";
}

export default function Button({
  className,
  variant = "primary",
  ...rest
}: ButtonProps) {
  return (
    <button
      className={cx(
        className,
        "sb-border sb-border-slate-400 sb-p-1 sb-rounded",
        {
          "sb-bg-blue-600 sb-text-white": variant === "primary",
          "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":
            variant === "icon",
          "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":
            variant === "expander",
        }
      )}
      {...rest}
    />
  );
}


================================================
FILE: src/components/Checkbox.tsx
================================================
import { ReactNode } from "react";
import cx from "clsx";

interface CheckboxProps extends React.ComponentPropsWithoutRef<"input"> {
  /** Input label */
  label: ReactNode;

  /** Required for a11y */
  id: string;
}

export default function Checkbox(props: CheckboxProps) {
  const { id, onChange, checked, className, ...rest } = props;
  return (
    <span>
      <input
        className={cx(
          "sb-border-slate-400 sb-border-solid sb-border p-1 sb-rounded",
          className
        )}
        type="checkbox"
        id={id}
        checked={checked}
        onChange={onChange}
        {...rest}
      />
      <label className="sb-ml-4" htmlFor={id}>
        {props.label}
      </label>
    </span>
  );
}


================================================
FILE: src/components/CloseButton.tsx
================================================
import Button, { ButtonProps } from "./Button";

export default function CloseButton(props: ButtonProps) {
  const { variant = "icon", ...rest } = props;
  return (
    <div className="sb-flex sb-flex-row-reverse">
      <Button variant={variant} {...rest}>
        <span className="sb-sr-only">Close menu</span>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="sb-h-6 sb-w-6"
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
          strokeWidth="2"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M6 18L18 6M6 6l12 12"
          />
        </svg>
      </Button>
    </div>
  );
}


================================================
FILE: src/components/CopySettingsButton.tsx
================================================
import { useState } from "react";
import Button from "./Button";

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {}

const labelDefault = "Copy Settings";

const hideCopiedConfirmationAfterXMilliSeconds = 2000;

export default function CopySettingsButton({ onClick, ...rest }: ButtonProps) {
  const [label, setLabel] = useState(labelDefault);

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    setLabel("Copied ✅");
    if (onClick) onClick(e);
    setTimeout(() => {
      setLabel(labelDefault);
    }, hideCopiedConfirmationAfterXMilliSeconds);
  }

  return (
    <Button variant="primary" onClick={handleClick} {...rest}>
      {label}
    </Button>
  );
}


================================================
FILE: src/components/DeleteButton.tsx
================================================
import Button, { ButtonProps } from "./Button";

export default function DeleteButton(props: ButtonProps) {
  return (
    <Button variant="icon" {...props}>
      <span className="sb-sr-only">Delete</span>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        className="sb-h-6 w-6"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        strokeWidth="2"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          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"
        />
      </svg>
    </Button>
  );
}


================================================
FILE: src/components/Field.tsx
================================================
type FieldProps = {
  /** Child elements */
  children: React.ReactNode;
};

export default function Field({ children }: FieldProps) {
  return <div className="sb-mt-4">{children}</div>;
}


================================================
FILE: src/components/HttpCustomResponseForm.tsx
================================================
import { CustomResponse } from "../http.types";
import DeleteButton from "./DeleteButton";
import Input from "./Input";

export const customResponseDefaults = {
  delay: 0,
  status: 200,
  response: undefined,
};

type CustomResponseFormProps = {
  customResponse: CustomResponse;
  setCustomResponses: React.Dispatch<React.SetStateAction<CustomResponse[]>>;
};

export default function HttpCustomResponseForm({
  customResponse,
  setCustomResponses,
}: Readonly<CustomResponseFormProps>) {
  const { handler, delay, status, response } = customResponse;

  // TODO: Support all response properties: https://mswjs.io/docs/api/response#properties
  return (
    <fieldset className="sb-mt-4 sb-border sb-p-2">
      <legend>
        {handler}{" "}
        <DeleteButton
          onClick={() =>
            setCustomResponses((r) => r.filter((e) => e.handler !== handler))
          }
        />
      </legend>
      <div className="sb-flex sb-flex-row">
        <Input
          id={`${handler}-delay`}
          type="number"
          changed={delay !== customResponseDefaults.delay}
          label="Delay"
          className="sb-w-20 sb-mr-4"
          value={delay}
          onChange={(e) =>
            setCustomResponses((r) =>
              r.map((s) =>
                s.handler === handler
                  ? {
                      ...s,
                      delay: parseInt(e.target.value),
                    }
                  : s
              )
            )
          }
        />

        <Input
          id={`${handler}-status`}
          type="number"
          changed={status !== customResponseDefaults.status}
          label="Status"
          className="sb-w-20 sb-mr-4"
          value={status}
          onChange={(e) =>
            setCustomResponses((r) =>
              r.map((s) =>
                s.handler === handler
                  ? {
                      ...s,
                      status: parseInt(e.target.value),
                    }
                  : s
              )
            )
          }
        />

        <Input
          id={`${handler}-custom-response`}
          type="text"
          changed={response !== customResponseDefaults.response}
          label="Response"
          className="sb-w-20"
          value={response}
          placeholder="Default"
          onChange={(e) =>
            setCustomResponses((r) =>
              r.map((s) =>
                s.handler === handler
                  ? {
                      ...s,
                      response: e.target.value,
                    }
                  : s
              )
            )
          }
        />
      </div>
    </fieldset>
  );
}


================================================
FILE: src/components/Input.tsx
================================================
import cx from "clsx";
import Label from "./Label";

interface InputProps extends React.ComponentPropsWithoutRef<"input"> {
  /** Input ID - Specifying here so it's required by TypeScript */
  id: string;

  /** Input label */
  label: string;

  /** Set to true to highlight the label so that it is visually marked as changed from the default. */
  changed?: boolean;

  /** Specify input's width */
  width?: "full" | "default";
}

export default function Input(props: InputProps) {
  const {
    id,
    onChange,
    label,
    value,
    changed = false,
    className,
    width = "default",
    ...rest
  } = props;
  return (
    <span>
      <Label className="block" htmlFor={id}>
        {label}
      </Label>
      <input
        className={cx(
          "sb-border-slate-400 sb-border-solid sb-border sb-rounded p-1",
          { "sb-bg-yellow-100": changed },
          { "sb-w-full": width === "full" },
          className
        )}
        type="text"
        id={id}
        value={value}
        onChange={onChange}
        {...rest}
      />
    </span>
  );
}


================================================
FILE: src/components/Label.tsx
================================================
interface LabelProps extends React.ComponentPropsWithoutRef<"label"> {
  /** Label */
  children: React.ReactNode;
}

export default function Label({ children, htmlFor }: LabelProps) {
  return (
    <label className="sb-block" htmlFor={htmlFor}>
      {children}
    </label>
  );
}


================================================
FILE: src/components/OpenButton.tsx
================================================
import Button, { ButtonProps } from "./Button";

export default function OpenButton(props: ButtonProps) {
  return (
    <Button variant="expander" {...props}>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        className="sb-h-6 sb-w-6"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        strokeWidth="2"
      >
        <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
      </svg>
    </Button>
  );
}


================================================
FILE: src/components/Select.tsx
================================================
import clsx from "clsx";
import Label from "./Label";

interface SelectProps extends React.ComponentPropsWithoutRef<"select"> {
  /** Set to true to highlight the label so that it is visually marked as changed from the default. */
  changed?: boolean;

  /** Input label */
  label: string;

  /** Specify select's width */
  width?: "full" | "default";
}

export default function Select(props: SelectProps) {
  const {
    id,
    onChange,
    width = "default",
    changed = false,
    label,
    value,
    ...rest
  } = props;
  return (
    <>
      <Label className="sb-block" htmlFor={id}>
        {label}
      </Label>
      <select
        className={clsx(
          "sb-border-slate-400 sb-border-solid sb-border sb-p-1 sb-rounded",
          {
            "sb-bg-yellow-100": changed,
            "sb-w-full": width === "full",
          }
        )}
        id={id}
        value={value}
        onChange={onChange}
        {...rest}
      />
    </>
  );
}


================================================
FILE: src/http.types.ts
================================================
import { RequestHandler } from "msw";
import { StartOptions } from "msw/browser";

export interface CustomResponse {
  /** Response handler name */
  handler: string;

  /** Delay the response by a specified number of milliseconds. */
  delay?: number;

  /** HTTP status code to return for this call */
  status?: number;

  /** Optional response. */
  response?: string;
}

export interface MswSettings {
  /** A function that accepts custom settings and returns an array of Mock Service Worker request handlers */
  requestHandlers: () => RequestHandler[];

  /** Optional Mock Service worker start options */
  startOptions?: StartOptions;

  /** Global delay in milliseconds */
  delay?: number;

  /** Array of custom responses */
  customResponses: CustomResponse[];
}


================================================
FILE: src/index.ts
================================================
import { useSwitchboardState } from "./useSwitchboardState";
import { useSwitchboard } from "./useSwitchboard";
import { Switchboard } from "./Switchboard";
import { Http } from "./Http";
import { useHttp } from "./useHttp";
import { CustomResponse, MswSettings } from "./http.types";
import {
  Position,
  switchboardPositions,
  SwitchboardDefaults,
  SwitchboardConfig,
} from "./switchboard.types";
import { customResponseDefaults } from "./components/HttpCustomResponseForm";

export {
  useSwitchboard,
  useSwitchboardState,
  Http,
  useHttp,
  CustomResponse,
  Position,
  switchboardPositions,
  SwitchboardDefaults,
  SwitchboardConfig,
  MswSettings,
  customResponseDefaults,
};

export default Switchboard;


================================================
FILE: src/input.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;


================================================
FILE: src/localStorage.utils.ts
================================================
// Get list of localStorage items that start with "sb-"
export function getLocalStorageSwitchboardKeys() {
  return Object.keys(localStorage).filter((key) => key.startsWith("sb-"));
}


================================================
FILE: src/switchboard.types.ts
================================================
import { CustomResponse } from "./http.types";

export const switchboardPositions = [
  "top-left",
  "top-right",
  "bottom-left",
  "bottom-right",
] as const;

/** Union of Switchboard window positions */
export type Position = (typeof switchboardPositions)[number];

/** Setting defaults */
export interface SwitchboardDefaults {
  /** Set to true to enable closing Switchboard by clicking outside the window by default */
  closeViaOutsideClick: boolean;

  /** When true, close Switchboard when the escape key is pressed */
  closeViaEscapeKey?: boolean;

  /** The default delay for mock HTTP requests */
  delay: number;

  /** The default window position */
  position: Position;

  /** Set to true to open Switchboard by default */
  openByDefault: boolean;
}

export interface SwitchboardConfig {
  /** Set to true to open the DevTools window by default */
  openByDefault: boolean;

  /** Switchboard window position */
  position: Position;

  /** Global HTTP delay */
  delay: number;

  /** Array of custom responses */
  customResponses: CustomResponse[];
}


================================================
FILE: src/types/react-use-keypress.d.ts
================================================
// TODO: Remove this when types are provided. Pull from https://github.com/jacobbuck/react-use-keypress/issues/6#issue-1319821201
declare module "react-use-keypress" {
  export default function useKeyPress(
    key: KeyboardEvent["key"] | KeyboardEvent["key"][],
    callback?: (e: KeyboardEvent) => void
  );
}


================================================
FILE: src/useHttp.ts
================================================
import { useEffect } from "react";
import { setupWorker } from "msw/browser";
import { SwitchboardMswSettings } from "./Switchboard";

/** Configure msw */
export function useHttp(
  setIsReady: () => void,
  mswSettings?: SwitchboardMswSettings
) {
  useEffect(() => {
    if (!mswSettings) {
      setIsReady();
      return;
    }
    const setup = async () => {
      const worker = setupWorker(...mswSettings.requestHandlers());
      await worker.start(mswSettings.startOptions);
      setIsReady();
    };
    setup();
  }, []);

  return {
    requestHandlers: mswSettings?.requestHandlers(),
  };
}


================================================
FILE: src/useOutsideClick.ts
================================================
import React, { useEffect } from "react";

/**
 * Call a function when the user clicks outside the ref passed.
 * @param ref Clicks outside this element will trigger the function provided to onOutsideClick
 * @param onOutsideClick Function called when the user clicks outside the element specified in the ref argument
 * @returns void
 */
export default function useOutsideClick(
  ref: React.RefObject<HTMLElement>,
  onOutsideClick: (event: globalThis.MouseEvent) => void
) {
  useEffect(() => {
    function handleClickOutside(event: globalThis.MouseEvent) {
      if (
        ref.current &&
        event.target instanceof Node &&
        !ref.current.contains(event.target)
      ) {
        onOutsideClick(event);
      }
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  });
}


================================================
FILE: src/useSwitchboard.ts
================================================
import React, { useState, useRef } from "react";
import useKeypress from "react-use-keypress";
import useOutsideClick from "./useOutsideClick";
import { Position, SwitchboardDefaults } from "./switchboard.types";
import { writeToClipboard } from "./clipboardUtils";
import { useSwitchboardState } from "./useSwitchboardState";
import { getLocalStorageSwitchboardKeys } from "./localStorage.utils";
import { CustomResponse } from "./http.types";

const maxUrlLength = 2000;

export const httpDefaults = {
  delay: 0,
  status: 200,
  response: undefined,
};

interface KeyboardShortcut {
  key: string | string[];
  alt?: boolean;
  ctrl?: boolean;
}

export interface GeneralSettings {
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
  position: Position;
  setPosition: (position: Position) => void;
  openByDefault: boolean;
  setOpenByDefault: (openByDefault: boolean) => void;
  closeViaOutsideClick: boolean;
  setCloseViaOutsideClick: (closeViaOutsideClick: boolean) => void;
  closeViaEscapeKey: boolean;
  setCloseViaEscapeKey: (closeViaEscapeKey: boolean) => void;
}

export interface HttpSettings {
  delay: number;
  setDelay: (delay: number) => void;
  delayChanged: boolean;
  customResponses: CustomResponse[];
  setCustomResponses: React.Dispatch<React.SetStateAction<CustomResponse[]>>;
}

interface UseSwitchboardArgs {
  /** Override the built in setting defaults */
  overriddenDefaults?: Partial<SwitchboardDefaults>;

  /** Specify a keyboard shortcut that toggles the window open/closed */
  openKeyboardShortcut?: KeyboardShortcut;
}

/** This component is useful to display custom devtools settings for your project */
export function useSwitchboard({
  openKeyboardShortcut,
  overriddenDefaults,
}: UseSwitchboardArgs | undefined = {}) {
  // These settings use the useSwitchboardState hook so that the settings persist in localStorage and are optionally initialized via the URL
  const [openByDefault, setOpenByDefault] = useSwitchboardState(
    "sb-openByDefault",
    overriddenDefaults?.openByDefault ?? true
  );

  const [isOpen, setIsOpen] = useState(openByDefault);

  const [closeViaOutsideClick, setCloseViaOutsideClick] = useSwitchboardState(
    "sb-closeViaOutsideClick",
    overriddenDefaults?.closeViaOutsideClick ?? false
  );

  const [closeViaEscapeKey, setCloseViaEscapeKey] = useSwitchboardState(
    "sb-closeViaEscapeKey",
    overriddenDefaults?.closeViaEscapeKey ?? true
  );

  const [position, setPosition] = useSwitchboardState(
    "sb-position",
    overriddenDefaults?.position ?? "top-left"
  );

  const [delay, setDelay, delayChanged] = useSwitchboardState(
    "sb-delay",
    httpDefaults.delay
  );

  const [customResponses, setCustomResponses] = useSwitchboardState<
    CustomResponse[]
  >("sb-customResponses", []);

  const switchboardWindowRef = useRef<HTMLDivElement>(null);

  useKeypress("Escape", () => {
    if (closeViaEscapeKey) setIsOpen(false);
  });

  useKeypress(openKeyboardShortcut ? openKeyboardShortcut.key : [], (e) => {
    if (openKeyboardShortcut?.alt && !e.altKey) return;
    if (openKeyboardShortcut?.ctrl && !e.ctrlKey) return;
    setIsOpen((current) => !current);
  });

  useOutsideClick(switchboardWindowRef, () => {
    if (closeViaOutsideClick) setIsOpen(false);
  });

  // Convert the settings to URL search params
  function getSettingsAsQueryParams() {
    const switchboardKeys = getLocalStorageSwitchboardKeys();

    // Encode the settings into search params
    const params = new URLSearchParams();
    switchboardKeys.forEach((key) => {
      params.set(key, localStorage.getItem(key)!);
    });

    return "?" + params.toString();
  }

  async function copySettingsUrlToClipboard() {
    const url = window.location.href + getSettingsAsQueryParams();
    try {
      await writeToClipboard(url);
      if (url.length > maxUrlLength) {
        alert(
          `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.`
        );
      }
    } catch (err) {
      () => alert("Failed to copy settings URL to clipboard");
    }
  }

  const generalSettings: GeneralSettings = {
    isOpen,
    setIsOpen,
    position,
    setPosition,
    openByDefault,
    setOpenByDefault,
    closeViaOutsideClick,
    setCloseViaOutsideClick,
    closeViaEscapeKey,
    setCloseViaEscapeKey,
  };

  const httpSettings: HttpSettings = {
    delay,
    setDelay,
    delayChanged,
    customResponses,
    setCustomResponses,
  };

  return {
    generalSettings,
    httpSettings,
    copySettingsUrlToClipboard,
    switchboardWindowRef,
  };
}


================================================
FILE: src/useSwitchboardState.ts
================================================
import { useCallback, useState } from "react";

/** Returns a string that contains the current URL with the specified key and value in the querystring */
function getUrlWithUpdatedQuery(url: URL, key: string, value: unknown = null) {
  const urlWithoutQuerystring = url.href.split("?")[0];
  const params = new URLSearchParams(url.search);
  // Remove existing querystring if it exists. Here's why:
  // 1. This assures the newly generated URL doesn't contain the param twice.
  // 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.
  params.delete(key);
  if (value) params.append(key, JSON.stringify(value));
  return urlWithoutQuerystring + "?" + params.toString();
}

interface SwitchboardStateOptions {
  /** Set to true to show values that match the default value in the URL.
   * By default, if the selected value matches the default value, it's omitted from the URL.
   * This keeps the URL as short as possible.  */
  // TODO: Finish refactor to union
  urlBehavior?: "initialization-only" | "initialize-and-display-always";

  /** Set to true to store values that match the default value in localStorage.
   * By default, if the selected value matches the default value, it's omitted from localStorage.
   * This keeps localStorage as minimal as possible.
   */
  storeDefaultValuesInLocalStorage?: boolean;
}

type SwitchboardKey<TKey, TPrefix extends string> = TKey extends string
  ? `${TPrefix}${TKey}`
  : never;

/**
 * This hook makes it easy to declare state for devtools.
 * It's a fork of https://usehooks.com/useLocalStorage/,
 * but enhanced to read the URL as a way to override the specified default.
 * Since DevTools often benefit from being initialized via the URL,
 * it reads optional default values from the URL. And since it's handy
 * for the DevTools to "remember" settings between hard refreshes,
 * it writes settings to localStorage onChange.
 *
 * Finally, if neither the URL or localStorage is set, it falls back
 * to the provided default.
 * In summary, it sets the default value in the following order:
 * 1. URL
 * 2. localStorage
 * 3. Specified default
 *
 * So, in other words, if the URL isn't provided, it falls back to localStorage.
 * If localStorage isn't set, it falls back to the specified default.
 *
 * This hook writes each state change to 2 spots:
 * 1. localStorage (so settings persist after the tab is closed)
 * 2. local state variable (so React renders when the state changes)
 *
 *
 * @param key The URL param to check for the default, as well as the key used to write the value to localStorage
 * @param defaultValue The default value to use if the URL and localStorage don't have a matching value for the provided key.
 * */
export function useSwitchboardState<T>(
  /** 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. */
  key: SwitchboardKey<string, "sb-">,
  defaultValue: T,
  options?: SwitchboardStateOptions
) {
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === "undefined") {
      return defaultValue;
    }

    // First, check the URL for a value and use it for the default if found.
    const params = new URLSearchParams(window.location.search);
    const urlValue = params.get(key);
    if (urlValue) {
      // TODO: Validate the object
      const parsedObject = JSON.parse(urlValue);
      // Update localStorage with URL value too
      // TODO: Use localforage instead.
      window.localStorage.setItem(key, JSON.stringify(parsedObject));

      // Clear out the URL now that we read the value and stored it in localStorage. This keeps the URL clean.
      // TODO: Make this an option
      const newUrl = getUrlWithUpdatedQuery(new URL(window.location.href), key);
      window.history.pushState("", "DevTools state update", newUrl);

      return parsedObject;
    }

    // If URL doesn't contain the key, then fall back to checking localStorage for a default value
    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored json or if none return initialValue

      // TODO: Use Zod to validate the querystring
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      // If error also return initialValue
      console.error(error);
      return defaultValue;
    }
  });

  // Return a wrapped version of useState's setter function that persists the new value to localStorage.
  const setValue = useCallback(
    (value: T | ((val: T) => T)) => {
      try {
        // Allow value to be a function so we have same API as useState
        const valueToStore =
          value instanceof Function ? value(storedValue) : value;

        // Step 1: Save state, so React re-renders
        setStoredValue(valueToStore);

        // Step 2: Save to localStorage, so the settings persist after the window is closed
        if (typeof window !== "undefined") {
          // If the value is the initial value, then we can omit it from localStorage.
          // But, go ahead and put it in localStorage anyway if storeDefaultValuesInLocalStorage is true.
          if (
            valueToStore == defaultValue &&
            !options?.storeDefaultValuesInLocalStorage
          ) {
            window.localStorage.removeItem(key);
          } else {
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
          }
        }
      } catch (error) {
        // TODO: Improve error handling
        console.error(error);
      }
    },
    [defaultValue, key, options?.storeDefaultValuesInLocalStorage, storedValue]
  );

  const isChanged = storedValue !== defaultValue;

  return [storedValue, setValue, isChanged] as const;
}


================================================
FILE: tailwind.config.cjs
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
  prefix: "sb-",
  content: ["./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
  corePlugins: {
    preflight: false,
  },
};


================================================
FILE: tsconfig.json
================================================
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": false,
    "isolatedModules": false,
    "lib": ["ESNext", "DOM"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "jsx": "react-jsx"
  },
  "exclude": ["dist", "node_modules"]
}


================================================
FILE: tsup.config.ts
================================================
import { defineConfig } from "tsup";

export default defineConfig((options) => ({
  entry: ["src/index.ts"],
  clean: true,
  format: ["cjs", "esm"],
  dts: true,
  sourcemap: true,
  external: ["react", "msw"],
  ...options,
}));
Download .txt
gitextract_27h3g6k8/

├── .gitignore
├── LICENSE
├── README.md
├── examples/
│   └── vite-hello-world/
│       ├── .gitignore
│       ├── README.md
│       ├── eslint.config.js
│       ├── index.html
│       ├── package.json
│       ├── src/
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── index.css
│       │   ├── main.tsx
│       │   └── vite-env.d.ts
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── package.json
├── src/
│   ├── ErrorFallback.tsx
│   ├── GeneralSettings.tsx
│   ├── Http.tsx
│   ├── Switchboard.tsx
│   ├── clipboardUtils.ts
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Checkbox.tsx
│   │   ├── CloseButton.tsx
│   │   ├── CopySettingsButton.tsx
│   │   ├── DeleteButton.tsx
│   │   ├── Field.tsx
│   │   ├── HttpCustomResponseForm.tsx
│   │   ├── Input.tsx
│   │   ├── Label.tsx
│   │   ├── OpenButton.tsx
│   │   └── Select.tsx
│   ├── http.types.ts
│   ├── index.ts
│   ├── input.css
│   ├── localStorage.utils.ts
│   ├── switchboard.types.ts
│   ├── types/
│   │   └── react-use-keypress.d.ts
│   ├── useHttp.ts
│   ├── useOutsideClick.ts
│   ├── useSwitchboard.ts
│   └── useSwitchboardState.ts
├── tailwind.config.cjs
├── tsconfig.json
└── tsup.config.ts
Download .txt
SYMBOL INDEX (47 symbols across 24 files)

FILE: examples/vite-hello-world/src/App.tsx
  function App (line 6) | function App() {

FILE: src/ErrorFallback.tsx
  function ErrorFallback (line 4) | function ErrorFallback({

FILE: src/GeneralSettings.tsx
  type GeneralSettingsProps (line 11) | interface GeneralSettingsProps {
  function GeneralSettings (line 16) | function GeneralSettings({

FILE: src/Http.tsx
  type HttpProps (line 8) | type HttpProps = {
  function Http (line 13) | function Http({ httpSettings, requestHandlers }: Readonly<HttpProps>) {

FILE: src/Switchboard.tsx
  type KeyboardShortcut (line 16) | interface KeyboardShortcut {
  type SwitchboardMswSettings (line 22) | interface SwitchboardMswSettings {
  type SwitchboardProps (line 30) | interface SwitchboardProps {
  function Switchboard (line 54) | function Switchboard({

FILE: src/clipboardUtils.ts
  function writeToClipboard (line 2) | async function writeToClipboard(content: string) {

FILE: src/components/Button.tsx
  type ButtonProps (line 2) | interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
  function Button (line 6) | function Button({

FILE: src/components/Checkbox.tsx
  type CheckboxProps (line 4) | interface CheckboxProps extends React.ComponentPropsWithoutRef<"input"> {
  function Checkbox (line 12) | function Checkbox(props: CheckboxProps) {

FILE: src/components/CloseButton.tsx
  function CloseButton (line 3) | function CloseButton(props: ButtonProps) {

FILE: src/components/CopySettingsButton.tsx
  type ButtonProps (line 5) | interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {}
  function CopySettingsButton (line 11) | function CopySettingsButton({ onClick, ...rest }: ButtonProps) {

FILE: src/components/DeleteButton.tsx
  function DeleteButton (line 3) | function DeleteButton(props: ButtonProps) {

FILE: src/components/Field.tsx
  type FieldProps (line 1) | type FieldProps = {
  function Field (line 6) | function Field({ children }: FieldProps) {

FILE: src/components/HttpCustomResponseForm.tsx
  type CustomResponseFormProps (line 11) | type CustomResponseFormProps = {
  function HttpCustomResponseForm (line 16) | function HttpCustomResponseForm({

FILE: src/components/Input.tsx
  type InputProps (line 4) | interface InputProps extends React.ComponentPropsWithoutRef<"input"> {
  function Input (line 18) | function Input(props: InputProps) {

FILE: src/components/Label.tsx
  type LabelProps (line 1) | interface LabelProps extends React.ComponentPropsWithoutRef<"label"> {
  function Label (line 6) | function Label({ children, htmlFor }: LabelProps) {

FILE: src/components/OpenButton.tsx
  function OpenButton (line 3) | function OpenButton(props: ButtonProps) {

FILE: src/components/Select.tsx
  type SelectProps (line 4) | interface SelectProps extends React.ComponentPropsWithoutRef<"select"> {
  function Select (line 15) | function Select(props: SelectProps) {

FILE: src/http.types.ts
  type CustomResponse (line 4) | interface CustomResponse {
  type MswSettings (line 18) | interface MswSettings {

FILE: src/localStorage.utils.ts
  function getLocalStorageSwitchboardKeys (line 2) | function getLocalStorageSwitchboardKeys() {

FILE: src/switchboard.types.ts
  type Position (line 11) | type Position = (typeof switchboardPositions)[number];
  type SwitchboardDefaults (line 14) | interface SwitchboardDefaults {
  type SwitchboardConfig (line 31) | interface SwitchboardConfig {

FILE: src/useHttp.ts
  function useHttp (line 6) | function useHttp(

FILE: src/useOutsideClick.ts
  function useOutsideClick (line 9) | function useOutsideClick(

FILE: src/useSwitchboard.ts
  type KeyboardShortcut (line 18) | interface KeyboardShortcut {
  type GeneralSettings (line 24) | interface GeneralSettings {
  type HttpSettings (line 37) | interface HttpSettings {
  type UseSwitchboardArgs (line 45) | interface UseSwitchboardArgs {
  function useSwitchboard (line 54) | function useSwitchboard({

FILE: src/useSwitchboardState.ts
  function getUrlWithUpdatedQuery (line 4) | function getUrlWithUpdatedQuery(url: URL, key: string, value: unknown = ...
  type SwitchboardStateOptions (line 15) | interface SwitchboardStateOptions {
  type SwitchboardKey (line 29) | type SwitchboardKey<TKey, TPrefix extends string> = TKey extends string
  function useSwitchboardState (line 60) | function useSwitchboardState<T>(
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (57K chars).
[
  {
    "path": ".gitignore",
    "chars": 2078,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2024 Cory House\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 3682,
    "preview": "# React Switchboard 🎛\n\nQuickly create custom DevTools for your React app.\n\n- [Live Demo](https://switchboard-beta.vercel"
  },
  {
    "path": "examples/vite-hello-world/.gitignore",
    "chars": 253,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "examples/vite-hello-world/README.md",
    "chars": 233,
    "preview": "# React Switchboard with Vite - Hello World\n\nSee [main.tsx](https://github.com/coryhouse/react-switchboard/blob/main/exa"
  },
  {
    "path": "examples/vite-hello-world/eslint.config.js",
    "chars": 689,
    "preview": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reac"
  },
  {
    "path": "examples/vite-hello-world/index.html",
    "chars": 366,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
  },
  {
    "path": "examples/vite-hello-world/package.json",
    "chars": 722,
    "preview": "{\n  \"name\": \"vite-hello-world\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \""
  },
  {
    "path": "examples/vite-hello-world/src/App.css",
    "chars": 606,
    "preview": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  paddin"
  },
  {
    "path": "examples/vite-hello-world/src/App.tsx",
    "chars": 905,
    "preview": "import { useState } from 'react'\nimport reactLogo from './assets/react.svg'\nimport viteLogo from '/vite.svg'\nimport './A"
  },
  {
    "path": "examples/vite-hello-world/src/index.css",
    "chars": 1161,
    "preview": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n"
  },
  {
    "path": "examples/vite-hello-world/src/main.tsx",
    "chars": 514,
    "preview": "import { lazy, StrictMode, Suspense } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport App from \"./Ap"
  },
  {
    "path": "examples/vite-hello-world/src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/vite-hello-world/tsconfig.app.json",
    "chars": 552,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/vite-hello-world/tsconfig.json",
    "chars": 119,
    "preview": "{\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",
    "chars": 479,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true"
  },
  {
    "path": "examples/vite-hello-world/vite.config.ts",
    "chars": 163,
    "preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
  },
  {
    "path": "package.json",
    "chars": 1444,
    "preview": "{\n  \"name\": \"react-switchboard\",\n  \"version\": \"0.0.27\",\n  \"description\": \"Quickly create custom DevTools for your React "
  },
  {
    "path": "src/ErrorFallback.tsx",
    "chars": 538,
    "preview": "import { FallbackProps } from \"react-error-boundary\";\nimport Button from \"./components/Button\";\n\nexport default function"
  },
  {
    "path": "src/GeneralSettings.tsx",
    "chars": 2874,
    "preview": "import * as React from \"react\";\nimport Field from \"./components/Field\";\nimport Select from \"./components/Select\";\nimport"
  },
  {
    "path": "src/Http.tsx",
    "chars": 2189,
    "preview": "import HttpCustomResponseForm from \"./components/HttpCustomResponseForm\";\nimport Field from \"./components/Field\";\nimport"
  },
  {
    "path": "src/Switchboard.tsx",
    "chars": 4127,
    "preview": "import React, { ComponentType, useState } from \"react\";\nimport cx from \"clsx\";\nimport CloseButton from \"./components/Clo"
  },
  {
    "path": "src/clipboardUtils.ts",
    "chars": 284,
    "preview": "// Write the provided string to the clipboard\nexport async function writeToClipboard(content: string) {\n  const type = \""
  },
  {
    "path": "src/components/Button.tsx",
    "chars": 905,
    "preview": "import cx from \"clsx\";\nexport interface ButtonProps extends React.ComponentPropsWithoutRef<\"button\"> {\n  variant?: \"prim"
  },
  {
    "path": "src/components/Checkbox.tsx",
    "chars": 726,
    "preview": "import { ReactNode } from \"react\";\nimport cx from \"clsx\";\n\ninterface CheckboxProps extends React.ComponentPropsWithoutRe"
  },
  {
    "path": "src/components/CloseButton.tsx",
    "chars": 711,
    "preview": "import Button, { ButtonProps } from \"./Button\";\n\nexport default function CloseButton(props: ButtonProps) {\n  const { var"
  },
  {
    "path": "src/components/CopySettingsButton.tsx",
    "chars": 772,
    "preview": "import { useState } from \"react\";\nimport Button from \"./Button\";\n\n// eslint-disable-next-line @typescript-eslint/no-empt"
  },
  {
    "path": "src/components/DeleteButton.tsx",
    "chars": 665,
    "preview": "import Button, { ButtonProps } from \"./Button\";\n\nexport default function DeleteButton(props: ButtonProps) {\n  return (\n "
  },
  {
    "path": "src/components/Field.tsx",
    "chars": 189,
    "preview": "type FieldProps = {\n  /** Child elements */\n  children: React.ReactNode;\n};\n\nexport default function Field({ children }:"
  },
  {
    "path": "src/components/HttpCustomResponseForm.tsx",
    "chars": 2687,
    "preview": "import { CustomResponse } from \"../http.types\";\nimport DeleteButton from \"./DeleteButton\";\nimport Input from \"./Input\";\n"
  },
  {
    "path": "src/components/Input.tsx",
    "chars": 1082,
    "preview": "import cx from \"clsx\";\nimport Label from \"./Label\";\n\ninterface InputProps extends React.ComponentPropsWithoutRef<\"input\""
  },
  {
    "path": "src/components/Label.tsx",
    "chars": 284,
    "preview": "interface LabelProps extends React.ComponentPropsWithoutRef<\"label\"> {\n  /** Label */\n  children: React.ReactNode;\n}\n\nex"
  },
  {
    "path": "src/components/OpenButton.tsx",
    "chars": 473,
    "preview": "import Button, { ButtonProps } from \"./Button\";\n\nexport default function OpenButton(props: ButtonProps) {\n  return (\n   "
  },
  {
    "path": "src/components/Select.tsx",
    "chars": 973,
    "preview": "import clsx from \"clsx\";\nimport Label from \"./Label\";\n\ninterface SelectProps extends React.ComponentPropsWithoutRef<\"sel"
  },
  {
    "path": "src/http.types.ts",
    "chars": 776,
    "preview": "import { RequestHandler } from \"msw\";\nimport { StartOptions } from \"msw/browser\";\n\nexport interface CustomResponse {\n  /"
  },
  {
    "path": "src/index.ts",
    "chars": 723,
    "preview": "import { useSwitchboardState } from \"./useSwitchboardState\";\nimport { useSwitchboard } from \"./useSwitchboard\";\nimport {"
  },
  {
    "path": "src/input.css",
    "chars": 59,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "src/localStorage.utils.ts",
    "chars": 184,
    "preview": "// Get list of localStorage items that start with \"sb-\"\nexport function getLocalStorageSwitchboardKeys() {\n  return Obje"
  },
  {
    "path": "src/switchboard.types.ts",
    "chars": 1074,
    "preview": "import { CustomResponse } from \"./http.types\";\n\nexport const switchboardPositions = [\n  \"top-left\",\n  \"top-right\",\n  \"bo"
  },
  {
    "path": "src/types/react-use-keypress.d.ts",
    "chars": 312,
    "preview": "// TODO: Remove this when types are provided. Pull from https://github.com/jacobbuck/react-use-keypress/issues/6#issue-1"
  },
  {
    "path": "src/useHttp.ts",
    "chars": 608,
    "preview": "import { useEffect } from \"react\";\nimport { setupWorker } from \"msw/browser\";\nimport { SwitchboardMswSettings } from \"./"
  },
  {
    "path": "src/useOutsideClick.ts",
    "chars": 981,
    "preview": "import React, { useEffect } from \"react\";\n\n/**\n * Call a function when the user clicks outside the ref passed.\n * @param"
  },
  {
    "path": "src/useSwitchboard.ts",
    "chars": 4755,
    "preview": "import React, { useState, useRef } from \"react\";\nimport useKeypress from \"react-use-keypress\";\nimport useOutsideClick fr"
  },
  {
    "path": "src/useSwitchboardState.ts",
    "chars": 5981,
    "preview": "import { useCallback, useState } from \"react\";\n\n/** Returns a string that contains the current URL with the specified ke"
  },
  {
    "path": "tailwind.config.cjs",
    "chars": 217,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  prefix: \"sb-\",\n  content: [\"./src/**/*.{js,ts,jsx,tsx}\""
  },
  {
    "path": "tsconfig.json",
    "chars": 611,
    "preview": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Default\",\n  \"compilerOptions\": {\n    \"composite\": "
  },
  {
    "path": "tsup.config.ts",
    "chars": 231,
    "preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig((options) => ({\n  entry: [\"src/index.ts\"],\n  clean: tr"
  }
]

About this extraction

This page contains the full source code of the coryhouse/react-switchboard GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (49.9 KB), approximately 13.8k tokens, and a symbol index with 47 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!