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 ? ( ) : ( } /> ) ); ``` ## 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 ================================================ Vite + React + TS
================================================ 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 ( <>
Vite logo React logo

Vite + React

Edit src/App.tsx and save to test HMR

Click on the Vite and React logos to learn more

) } 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( {import.meta.env.DEV ? ( } /> ) : ( )} ); ================================================ FILE: examples/vite-hello-world/src/vite-env.d.ts ================================================ /// ================================================ 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) { return (

Something went wrong.

{error.message}
); } ================================================ 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) { const { position, setPosition, openByDefault, setOpenByDefault, closeViaOutsideClick, setCloseViaOutsideClick, closeViaEscapeKey, setCloseViaEscapeKey, } = settings; return (
General setOpenByDefault(!openByDefault)} checked={openByDefault} /> setCloseViaEscapeKey(!closeViaEscapeKey)} checked={closeViaEscapeKey} /> { setCloseViaOutsideClick(!closeViaOutsideClick); }} checked={closeViaOutsideClick} />
); } ================================================ 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) { const { delay, setDelay, delayChanged, customResponses, setCustomResponses } = httpSettings; return (
HTTP setDelay(parseInt(e.target.value))} /> {customResponses.map((setting) => ( ))}
); } ================================================ 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; /** 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; } /** Display custom devtools settings for your project */ export function Switchboard({ appSlot, children, mswSettings, openKeyboardShortcut, ErrorFallback, className, defaults, }: Readonly) { 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 */} {mswIsReady ? appSlot :

Initializing msw...

}
{isOpen ? ( <> setIsOpen(!isOpen)} /> {children} {requestHandlers && requestHandlers.length > 0 && ( )} ) : ( setIsOpen(!isOpen)} /> )}
); } ================================================ 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 ( ); } ================================================ 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) { setLabel("Copied ✅"); if (onClick) onClick(e); setTimeout(() => { setLabel(labelDefault); }, hideCopiedConfirmationAfterXMilliSeconds); } return ( ); } ================================================ FILE: src/components/DeleteButton.tsx ================================================ import Button, { ButtonProps } from "./Button"; export default function DeleteButton(props: ButtonProps) { return ( ); } ================================================ FILE: src/components/Field.tsx ================================================ type FieldProps = { /** Child elements */ children: React.ReactNode; }; export default function Field({ children }: FieldProps) { return
{children}
; } ================================================ 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>; }; export default function HttpCustomResponseForm({ customResponse, setCustomResponses, }: Readonly) { const { handler, delay, status, response } = customResponse; // TODO: Support all response properties: https://mswjs.io/docs/api/response#properties return (
{handler}{" "} setCustomResponses((r) => r.filter((e) => e.handler !== handler)) } />
setCustomResponses((r) => r.map((s) => s.handler === handler ? { ...s, delay: parseInt(e.target.value), } : s ) ) } /> setCustomResponses((r) => r.map((s) => s.handler === handler ? { ...s, status: parseInt(e.target.value), } : s ) ) } /> setCustomResponses((r) => r.map((s) => s.handler === handler ? { ...s, response: e.target.value, } : s ) ) } />
); } ================================================ 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 ( ); } ================================================ FILE: src/components/Label.tsx ================================================ interface LabelProps extends React.ComponentPropsWithoutRef<"label"> { /** Label */ children: React.ReactNode; } export default function Label({ children, htmlFor }: LabelProps) { return ( ); } ================================================ FILE: src/components/OpenButton.tsx ================================================ import Button, { ButtonProps } from "./Button"; export default function OpenButton(props: ButtonProps) { return ( ); } ================================================ 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 ( <>