├── .nvmrc ├── __tests__ ├── .gitkeep ├── custom-render.tsx ├── setup.ts └── devtools │ └── basic.test.tsx ├── .husky ├── pre-commit └── commit-msg ├── __mocks__ └── styleMock.js ├── src ├── DevTools │ ├── index.ts │ ├── Extension │ │ ├── index.ts │ │ ├── components │ │ │ └── Shell │ │ │ │ ├── index.ts │ │ │ │ ├── components │ │ │ │ ├── Header │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ └── ThemeToggle.tsx │ │ │ │ │ └── Header.tsx │ │ │ │ ├── JSONTree │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── JSONTree.css │ │ │ │ │ ├── JSONTree.tsx │ │ │ │ │ └── utils │ │ │ │ │ │ ├── get-json-tree-theme.ts │ │ │ │ │ │ ├── get-item-string.ts │ │ │ │ │ │ └── use-JSON-tree-styling.ts │ │ │ │ ├── AtomViewer │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AtomList │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── AtomList.tsx │ │ │ │ │ │ └── AtomDetail │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── MemoizedValueRenderer.tsx │ │ │ │ │ │ │ ├── AtomMetaDetails.tsx │ │ │ │ │ │ │ ├── DisplayAtomDetails.tsx │ │ │ │ │ │ │ ├── AtomDependentsList.tsx │ │ │ │ │ │ │ └── AtomValue.tsx │ │ │ │ │ │ │ ├── atoms.ts │ │ │ │ │ │ │ └── AtomDetail.tsx │ │ │ │ │ ├── AtomViewer.css │ │ │ │ │ ├── utils │ │ │ │ │ │ └── filter-atoms-by-string.ts │ │ │ │ │ ├── atoms.ts │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── use.ts │ │ │ │ │ │ └── useInternalAtomValue.ts │ │ │ │ │ └── AtomViewer.tsx │ │ │ │ ├── TimeTravel │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── SnapshotList │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ ├── Header.css │ │ │ │ │ │ │ │ ├── SnapshotSearchInput.tsx │ │ │ │ │ │ │ │ ├── Header.tsx │ │ │ │ │ │ │ │ ├── RecordHistory.tsx │ │ │ │ │ │ │ │ ├── SnapshotListNavigation.tsx │ │ │ │ │ │ │ │ └── ClearHistory.tsx │ │ │ │ │ │ │ ├── atoms.ts │ │ │ │ │ │ │ └── SnapshotList.tsx │ │ │ │ │ │ ├── SnapshotDetail │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ └── DisplaySnapshotDetails │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── SnapshotMetaDetails.tsx │ │ │ │ │ │ │ │ │ ├── SnapshotValue.tsx │ │ │ │ │ │ │ │ │ ├── SnapshotActions.tsx │ │ │ │ │ │ │ │ │ └── TreeView.tsx │ │ │ │ │ │ │ │ │ ├── atoms.ts │ │ │ │ │ │ │ │ │ └── DisplaySnapshotDetails.tsx │ │ │ │ │ │ │ ├── SnapshotDetail.tsx │ │ │ │ │ │ │ └── atoms.ts │ │ │ │ │ │ ├── PlayBar.css │ │ │ │ │ │ ├── PlaybackSpeedDropdown.tsx │ │ │ │ │ │ └── PlayBar.tsx │ │ │ │ │ ├── TimeTravel.css │ │ │ │ │ ├── utils │ │ │ │ │ │ ├── find-snapshot-by-id.ts │ │ │ │ │ │ ├── filter-snapshot-history-by-string.ts │ │ │ │ │ │ └── create-diff-patcher.ts │ │ │ │ │ ├── TimeTravel.tsx │ │ │ │ │ └── useSyncSnapshotHistory.ts │ │ │ │ ├── CodeSyntaxHighlighter.css │ │ │ │ ├── ActionListItem.css │ │ │ │ ├── PanelResizeHandle.css │ │ │ │ ├── CodeSyntaxHighlighter.tsx │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ ├── MetaInfo.tsx │ │ │ │ ├── PanelResizeHandle.tsx │ │ │ │ ├── ActionListItem.tsx │ │ │ │ ├── TabsHeader.tsx │ │ │ │ ├── ShellResizeBar.tsx │ │ │ │ └── ErrorBoundary.tsx │ │ │ │ ├── Shell.css │ │ │ │ ├── atoms.ts │ │ │ │ └── Shell.tsx │ │ ├── ShellTriggerButton.css │ │ └── Extension.tsx │ ├── fonts │ │ ├── files │ │ │ ├── Inter-Bold.woff2 │ │ │ ├── Inter-Medium.woff2 │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── Inter-SemiBold.woff2 │ │ │ ├── JetBrainsMono-Bold.woff2 │ │ │ ├── JetBrainsMono-Regular.woff2 │ │ │ └── JetBrainsMono-SemiBold.woff2 │ │ └── fonts.css │ ├── utils │ │ ├── index.ts │ │ ├── create-timestamp.ts │ │ ├── atom-to-printable.ts │ │ ├── generate-local-storage-key.ts │ │ ├── stringify-atom-value.ts │ │ └── get-type-of-atom-value.ts │ ├── constants.ts │ ├── atoms │ │ ├── is-shell-open-atom.ts │ │ ├── user-custom-store.ts │ │ ├── shell-styles.ts │ │ ├── values-atom.ts │ │ └── devtools-options.ts │ ├── hooks │ │ ├── useThemeMode.tsx │ │ ├── useUserStore.ts │ │ └── useAtomsSnapshots.ts │ ├── internal-jotai-store.ts │ └── DevTools.tsx ├── utils │ ├── types.ts │ ├── index.ts │ ├── hooks │ │ └── useDevToolsStore.ts │ ├── useGotoAtomsSnapshot.ts │ ├── redux-extension │ │ ├── getReduxExtension.ts │ │ └── createReduxConnection.ts │ ├── useAtomsDebugValue.ts │ ├── useAtomDevtools.ts │ ├── useAtomsDevtools.ts │ └── useAtomsSnapshot.ts ├── stories │ ├── Default │ │ ├── Demos │ │ │ ├── demo-store.ts │ │ │ ├── ThemeToggle.tsx │ │ │ ├── Counter.tsx │ │ │ ├── Async.tsx │ │ │ ├── DemoApp.tsx │ │ │ ├── Random.tsx │ │ │ └── Todos.tsx │ │ ├── DevTools.stories.tsx │ │ └── Playground │ │ │ ├── SomeComponent.tsx │ │ │ ├── Counter.tsx │ │ │ ├── Playground.stories.tsx │ │ │ ├── Async.tsx │ │ │ └── Playground.tsx │ └── ProviderLess │ │ ├── Counter.tsx │ │ └── DevToolsProviderLess.stories.tsx ├── index.ts └── types.ts ├── .commitlintrc ├── postcss.config.js ├── docs └── internal │ ├── demo-screenshot.png │ └── release-guide.md ├── react-shim.js ├── .codesandbox └── ci.json ├── .eslintignore ├── .lintstagedrc ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .storybook ├── preview.ts └── main.ts ├── .babelrc.json ├── tsconfig.json ├── types └── global.d.ts ├── .github └── workflows │ └── ci.yml ├── tsconfig.build.json ├── .release-it.json ├── LICENSE ├── jest.config.ts ├── tsup.config.ts ├── .eslintrc.js ├── package.json └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /__tests__/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run lint-staged 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint --edit $1 2 | 3 | -------------------------------------------------------------------------------- /src/DevTools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DevTools'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Extension'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Shell'; 2 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/JSONTree/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JSONTree'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AtomViewer'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TimeTravel'; 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-mantine': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/components/AtomList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AtomList'; 2 | -------------------------------------------------------------------------------- /docs/internal/demo-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/docs/internal/demo-screenshot.png -------------------------------------------------------------------------------- /react-shim.js: -------------------------------------------------------------------------------- 1 | // Why? See - https://github.com/egoist/tsup/issues/792 2 | import React from 'react'; 3 | export { React }; 4 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/components/AtomDetail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AtomDetail'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SnapshotList'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotDetail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SnapshotDetail'; 2 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "build", 3 | "sandboxes": ["new", "react-typescript-react-ts"], 4 | "node": "20" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | build 4 | .eslintrc.js 5 | coverage 6 | react-shim.js 7 | __mocks__/styleMock.js 8 | postcss.config.js -------------------------------------------------------------------------------- /src/DevTools/Extension/ShellTriggerButton.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-trigger-button { 2 | img { 3 | height: 2rem; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/DevTools/fonts/files/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/Inter-Bold.woff2 -------------------------------------------------------------------------------- /src/DevTools/fonts/files/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/Inter-Medium.woff2 -------------------------------------------------------------------------------- /src/DevTools/fonts/files/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/Inter-Regular.woff2 -------------------------------------------------------------------------------- /src/DevTools/fonts/files/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /src/DevTools/fonts/files/JetBrainsMono-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/JetBrainsMono-Bold.woff2 -------------------------------------------------------------------------------- /src/DevTools/fonts/files/JetBrainsMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/JetBrainsMono-Regular.woff2 -------------------------------------------------------------------------------- /src/DevTools/fonts/files/JetBrainsMono-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-devtools/HEAD/src/DevTools/fonts/files/JetBrainsMono-SemiBold.woff2 -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotDetail/components/DisplaySnapshotDetails/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DisplaySnapshotDetails'; 2 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/CodeSyntaxHighlighter.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-code-syntax-highlighter { 2 | border-radius: var(--mantine-radius-md); 3 | } 4 | -------------------------------------------------------------------------------- /src/DevTools/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-type-of-atom-value'; 2 | export * from './atom-to-printable'; 3 | export * from './stringify-atom-value'; 4 | export * from './generate-local-storage-key'; 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.{ts,tsx,js,jsx}": [ 3 | "eslint --quiet --fix", 4 | "prettier --write" 5 | ], 6 | "**/*.{md,yml,json,babelrc,eslintrc,prettierrc}": [ 7 | "prettier --write" 8 | ] 9 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "proseWrap": "always", 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "vunguyentuan.vscode-postcss", 6 | "vunguyentuan.vscode-css-variables" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | controls: { 3 | matchers: { 4 | color: /(background|color)$/i, 5 | date: /Date$/, 6 | }, 7 | }, 8 | }; 9 | export const tags = ['autodocs']; 10 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/AtomViewer.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-atom-viewer-wrapper { 2 | background: light-dark( 3 | var(--mantine-color-gray-2), 4 | var(--mantine-color-dark-8) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/TimeTravel.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-time-travel-wrapper { 2 | background: light-dark( 3 | var(--mantine-color-gray-2), 4 | var(--mantine-color-dark-8) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type {} from '@redux-devtools/extension'; 2 | 3 | // FIXME https://github.com/reduxjs/redux-devtools/issues/1097 4 | // This is an INTERNAL type alias. 5 | export type Message = { 6 | type: string; 7 | payload?: any; 8 | state?: any; 9 | }; 10 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/JSONTree/JSONTree.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-json-tree-wrapper { 2 | font-family: var(--mantine-font-family-monospace); 3 | font-size: 13px; 4 | ul:first-of-type { 5 | border-radius: var(--mantine-radius-md); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/DevTools/constants.ts: -------------------------------------------------------------------------------- 1 | export const shellStyleDefaults = { 2 | minHeight: 200, // in px 3 | maxHeight: '90%', 4 | defaultHeight: 370, // in px 5 | }; 6 | 7 | export enum TabKeys { 8 | AtomViewer = 'atom-viewer', 9 | TimeTravel = 'time-travel', 10 | AtomGraph = 'atom-graph', 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { useAtomsSnapshot } from './useAtomsSnapshot'; 2 | export { useGotoAtomsSnapshot } from './useGotoAtomsSnapshot'; 3 | export { useAtomsDebugValue } from './useAtomsDebugValue'; 4 | export { useAtomDevtools } from './useAtomDevtools'; 5 | export { useAtomsDevtools } from './useAtomsDevtools'; 6 | -------------------------------------------------------------------------------- /src/DevTools/atoms/is-shell-open-atom.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from 'jotai/vanilla/utils'; 2 | import { generateLocalStorageKey } from '../utils/generate-local-storage-key'; 3 | 4 | const key = generateLocalStorageKey('is-shell-open', 0); 5 | export const isShellOpenAtom = atomWithStorage(key, null); 6 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/ActionListItem.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-monospace-font { 2 | font-family: var(--mantine-font-family-monospace); 3 | font-size: var(--mantine-font-size-sm) !important; 4 | } 5 | 6 | .internal-jotai-devtools-navlink { 7 | border-radius: var(--mantine-radius-md); 8 | } 9 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/utils/find-snapshot-by-id.ts: -------------------------------------------------------------------------------- 1 | import { SnapshotHistory } from '../atoms'; 2 | 3 | export const findSnapshotById = ( 4 | snapshots: SnapshotHistory[], 5 | id: SnapshotHistory['id'], 6 | ) => { 7 | return snapshots.find((snapshot) => snapshot.id === id); 8 | }; 9 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /src/DevTools/hooks/useThemeMode.tsx: -------------------------------------------------------------------------------- 1 | import { useComputedColorScheme } from '@mantine/core'; 2 | 3 | export const useThemeMode = (light: L, dark: T) => { 4 | const computedColorScheme = useComputedColorScheme('light', { 5 | getInitialValueInEffect: true, 6 | }); 7 | 8 | return computedColorScheme === 'light' ? light : dark; 9 | }; 10 | -------------------------------------------------------------------------------- /src/stories/Default/Demos/demo-store.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { createStore, getDefaultStore } from 'jotai/vanilla'; 3 | 4 | export const demoStore = getDefaultStore(); 5 | export const DemoJotaiStoreContext = 6 | createContext>(demoStore); 7 | 8 | export const demoStoreOptions = { store: demoStore }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "./jest.config.ts", 9 | "__tests__/**/*", 10 | "types", 11 | "tsup.config.ts", 12 | "jest", 13 | "@testing-library/jest-dom" 14 | ], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/custom-render.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren, StrictMode } from 'react'; 2 | import { RenderOptions, render } from '@testing-library/react'; 3 | 4 | const AllTheProviders = ({ children }: PropsWithChildren) => { 5 | return {children}; 6 | }; 7 | 8 | export const customRender = (ui: React.ReactElement, options?: RenderOptions) => 9 | render(ui, { wrapper: AllTheProviders, ...options }); 10 | -------------------------------------------------------------------------------- /src/DevTools/utils/create-timestamp.ts: -------------------------------------------------------------------------------- 1 | const formatTimeToTimeStamp = (time: number) => 2 | new Date(time).toLocaleString('en-US', { 3 | hour: 'numeric', 4 | minute: 'numeric', 5 | second: 'numeric', 6 | fractionalSecondDigits: 3, 7 | }); 8 | 9 | export const createTimestamp = () => { 10 | // Monotonic clock 11 | const time = performance.timeOrigin + performance.now(); 12 | return formatTimeToTimeStamp(time); 13 | }; 14 | -------------------------------------------------------------------------------- /src/stories/ProviderLess/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { atom, useAtom } from 'jotai'; 3 | 4 | const countAtom = atom(0); 5 | countAtom.debugLabel = 'countAtom'; 6 | 7 | export const Counter = () => { 8 | const [count, setCount] = useAtom(countAtom); 9 | 10 | return ( 11 |
12 | {count}  13 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/components/Header.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-header-wrapper { 2 | position: 'sticky'; 3 | top: 0; 4 | margin-top: var(--mantine-spacing-xs); 5 | } 6 | 7 | .internal-jotai-devtools-header-content { 8 | border-radius: var(--mantine-radius-md); 9 | background-color: light-dark( 10 | var(--mantine-color-white), 11 | var(--mantine-color-dark-7) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Declare global variables for TypeScript and VSCode. 2 | // Do not rename this file or move these types into index.d.ts 3 | // @see https://code.visualstudio.com/docs/nodejs/working-with-javascript#_global-variables-and-type-checking 4 | declare let __DEV__: boolean; 5 | 6 | declare module '*.png'; 7 | declare module '*.svg'; 8 | declare module '*.jpeg'; 9 | declare module '*.jpg'; 10 | declare module '*.woff2'; 11 | declare module '*.module.css'; 12 | -------------------------------------------------------------------------------- /docs/internal/release-guide.md: -------------------------------------------------------------------------------- 1 | ### Manual release Guide 2 | 3 | ### Pre-release 4 | 5 | #### Setup 6 | 7 | - Login to npm using `npm login` 8 | - Setup `GITHUB_TOKEN` on your local environment. Verify it using 9 | `echo $GITHUB_TOKEN` 10 | 11 | Run the following to conduct a release 12 | 13 | - `pnpm run release` (installs and bundles all the required files) 14 | - Select a version to release 15 | - Answer all the prompts, and select publish to npm as well as GitHub 16 | -------------------------------------------------------------------------------- /src/utils/hooks/useDevToolsStore.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from 'jotai'; 2 | import { Options } from '../../types'; 3 | import { 4 | composeWithDevTools, 5 | isDevToolsStore, 6 | } from '../internals/compose-with-devtools'; 7 | 8 | export const useDevToolsStore = ( 9 | options: Options, 10 | ): ReturnType => { 11 | const store = useStore(options); 12 | 13 | return composeWithDevTools(store); 14 | }; 15 | 16 | export { isDevToolsStore }; 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { DevToolsProps } from './DevTools'; 2 | import { InternalDevTools } from './DevTools'; 3 | 4 | // This is a workaround to make DevTools tree-shakable in production builds 5 | // This is due to a limitation in tsup where it does not support preserving signatures 6 | // of exports or generating separate chunks for exports 7 | export const DevTools: typeof InternalDevTools = __DEV__ 8 | ? InternalDevTools 9 | : () => null; 10 | 11 | export * from './utils'; 12 | -------------------------------------------------------------------------------- /src/DevTools/atoms/user-custom-store.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue, useSetAtom } from 'jotai/react'; 2 | import { atom } from 'jotai/vanilla'; 3 | import { Store } from 'src/types'; 4 | import { useDevtoolsJotaiStoreOptions } from '../internal-jotai-store'; 5 | 6 | const userStore = atom(undefined); 7 | 8 | export const useUserStoreValue = () => 9 | useAtomValue(userStore, useDevtoolsJotaiStoreOptions()); 10 | 11 | export const useSetUserStore = () => 12 | useSetAtom(userStore, useDevtoolsJotaiStoreOptions()); 13 | -------------------------------------------------------------------------------- /src/DevTools/utils/atom-to-printable.ts: -------------------------------------------------------------------------------- 1 | import { AnyAtom } from '../../types'; 2 | 3 | /** 4 | * We label atoms with debugLabel to make it easier to identify them in the DevTools + avoid the naming collisions 5 | * when showing multiple atoms that are not labeled 6 | * 7 | * @param atom AnyAtom 8 | * @returns printable string based on the atom's debugLabel or a default string 9 | */ 10 | export const atomToPrintable = (atom: AnyAtom): string => { 11 | return atom.debugLabel ? atom.debugLabel : ``; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/useGotoAtomsSnapshot.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import type { AtomsSnapshot, Options } from '../types'; 3 | import { isDevToolsStore, useDevToolsStore } from './hooks/useDevToolsStore'; 4 | 5 | export function useGotoAtomsSnapshot(options?: Options) { 6 | const store = useDevToolsStore(options); 7 | return useCallback( 8 | (snapshot: AtomsSnapshot) => { 9 | if (isDevToolsStore(store)) { 10 | store.restoreAtoms(snapshot.values); 11 | } 12 | }, 13 | [store], 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/DevTools/atoms/shell-styles.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from 'jotai/vanilla/utils'; 2 | import { shellStyleDefaults } from '../constants'; 3 | import { generateLocalStorageKey } from '../utils/generate-local-storage-key'; 4 | 5 | type ShellStyleAtomData = { 6 | height: number; 7 | isDragging: boolean; 8 | }; 9 | const key = generateLocalStorageKey('shell-height', 0); 10 | 11 | export const shellStylesAtom = atomWithStorage(key, { 12 | height: shellStyleDefaults.defaultHeight, 13 | isDragging: false, 14 | }); 15 | -------------------------------------------------------------------------------- /src/DevTools/utils/generate-local-storage-key.ts: -------------------------------------------------------------------------------- 1 | const PREFIX = 'jotai-devtools'; 2 | /** 3 | * Generates a key for local storage based on the key and version 4 | * @param key - the key to use 5 | * @param version - the version to apply 6 | * @returns the generated key 7 | */ 8 | export const generateLocalStorageKey = < 9 | const Key extends string, 10 | const Version extends string | number, 11 | >( 12 | key: Key, 13 | version: Version, 14 | ): `${typeof PREFIX}-${Key}-V${Version}` => { 15 | return `${PREFIX}-${key}-V${version}`; 16 | }; 17 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/utils/filter-snapshot-history-by-string.ts: -------------------------------------------------------------------------------- 1 | import { SnapshotHistory } from '../atoms'; 2 | 3 | export const filterSnapshotHistoryByString = ( 4 | searchString: string, 5 | defaultSnapshots: SnapshotHistory[], 6 | ) => { 7 | const normalizedStr = searchString.trim().toLocaleLowerCase(); 8 | if (!normalizedStr) { 9 | return defaultSnapshots; 10 | } 11 | 12 | return defaultSnapshots.filter(({ label }) => { 13 | const snapshotLabel = String(label); 14 | return snapshotLabel.includes(normalizedStr); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/DevTools/atoms/values-atom.ts: -------------------------------------------------------------------------------- 1 | import { useAtom, useAtomValue } from 'jotai/react'; 2 | import { atom } from 'jotai/vanilla'; 3 | import { ValuesAtomTuple } from 'src/types'; 4 | import { useDevtoolsJotaiStoreOptions } from '../internal-jotai-store'; 5 | 6 | export const valuesAtom = atom([]); 7 | 8 | /** 9 | * @internal 10 | * 11 | * @returns [ValuesAtomTuple, Setter] 12 | */ 13 | export const useSnapshotValues = () => 14 | useAtom(valuesAtom, useDevtoolsJotaiStoreOptions()); 15 | 16 | export const useSnapshotValuesValue = () => 17 | useAtomValue(valuesAtom, useDevtoolsJotaiStoreOptions()); 18 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/Shell.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-shell { 2 | position: fixed; 3 | width: calc(100% - 1.25rem); 4 | left: 50%; 5 | bottom: 0.625rem; 6 | transform: translate(-50%, 0%); 7 | border-color: light-dark( 8 | var(--mantine-color-gray-3), 9 | var(--mantine-color-dark-4) 10 | ); 11 | border-width: 1px; 12 | border-style: solid; 13 | border-radius: 8px; 14 | background: light-dark( 15 | var(--mantine-color-white), 16 | var(--mantine-color-dark-7) 17 | ); 18 | display: flex !important; 19 | flex-direction: column !important; 20 | z-index: 99999; 21 | } 22 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotDetail/components/DisplaySnapshotDetails/components/SnapshotMetaDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Title } from '@mantine/core'; 3 | import { MetaInfo } from '../../../../../../MetaInfo'; 4 | 5 | type SnapshotMetaDetailsProps = { 6 | timestamp: string; 7 | }; 8 | 9 | export const SnapshotMetaDetails = (props: SnapshotMetaDetailsProps) => { 10 | return ( 11 | 12 | 13 | Meta 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/DevTools/internal-jotai-store.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import type { Store } from 'src/types'; 3 | 4 | export const InternalDevToolsContext = createContext( 5 | undefined, 6 | ); 7 | 8 | export const useInternalStore = (): Store => { 9 | const store = useContext(InternalDevToolsContext); 10 | if (!store) { 11 | throw new Error( 12 | `Unable to find internal Jotai store, Did you wrap the component within DevToolsProvider?`, 13 | ); 14 | } 15 | return store; 16 | }; 17 | 18 | export const useDevtoolsJotaiStoreOptions = () => ({ 19 | store: useInternalStore(), 20 | }); 21 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/atoms.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai/react'; 2 | import { atomWithStorage } from 'jotai/vanilla/utils'; 3 | import { TabKeys } from '../../../constants'; 4 | import { useDevtoolsJotaiStoreOptions } from '../../../internal-jotai-store'; 5 | import { generateLocalStorageKey } from '../../../utils/'; 6 | 7 | const selectedShellTabKey = generateLocalStorageKey('selected-shell-tab', 0); 8 | 9 | const selectedShellTab = atomWithStorage( 10 | selectedShellTabKey, 11 | TabKeys.AtomViewer, 12 | ); 13 | 14 | export const useSelectedShellTab = () => 15 | useAtom(selectedShellTab, useDevtoolsJotaiStoreOptions()); 16 | -------------------------------------------------------------------------------- /src/stories/Default/Demos/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionIcon, useMantineColorScheme } from '@mantine/core'; 3 | import { IconMoonStars, IconSun } from '@tabler/icons-react'; 4 | 5 | export const ThemeToggle = () => { 6 | const { colorScheme, toggleColorScheme } = useMantineColorScheme(); 7 | const dark = colorScheme === 'dark'; 8 | 9 | return ( 10 | toggleColorScheme()} 14 | title="Toggle color scheme" 15 | > 16 | {dark ? : } 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/components/AtomDetail/components/MemoizedValueRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CodeSyntaxHighlighter } from '../../../../CodeSyntaxHighlighter'; 3 | 4 | type MemoizedValueRendererProps = { 5 | value: string; 6 | }; 7 | 8 | export const MemoizedValueRenderer = React.memo( 9 | ({ value }: MemoizedValueRendererProps): JSX.Element => { 10 | return ( 11 | 17 | {value} 18 | 19 | ); 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/utils/filter-atoms-by-string.ts: -------------------------------------------------------------------------------- 1 | import { ValuesAtomTuple } from 'src/types'; 2 | import { atomToPrintable } from '../../../../../../utils'; 3 | 4 | export const filterAtomsByString = ( 5 | searchString: string, 6 | defaultAtoms: ValuesAtomTuple[], 7 | ) => { 8 | const normalizedStr = searchString.trim().toLocaleLowerCase(); 9 | if (!normalizedStr) { 10 | return defaultAtoms; 11 | } 12 | 13 | return defaultAtoms.filter(([atomTuple]) => { 14 | const parsedDebugLabel = atomToPrintable(atomTuple); 15 | const normalizedLabel = parsedDebugLabel.toLocaleLowerCase(); 16 | return normalizedLabel.includes(normalizedStr); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/Header/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionIcon, useMantineColorScheme } from '@mantine/core'; 3 | import { IconMoonStars, IconSun } from '@tabler/icons-react'; 4 | 5 | export const ThemeToggle = () => { 6 | const { colorScheme, toggleColorScheme } = useMantineColorScheme(); 7 | const dark = colorScheme === 'dark'; 8 | 9 | return ( 10 | toggleColorScheme()} 14 | title="Toggle color scheme" 15 | > 16 | {dark ? : } 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/PanelResizeHandle.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-panel-resize-handle-wrapper { 2 | display: flex; 3 | align-items: center; 4 | height: 100%; 5 | .internal-jotai-devtools-panel-resize-handle-content { 6 | transition: 7 | max-height, 8 | min-height, 9 | height, 10 | 0.2s ease-out; 11 | } 12 | } 13 | 14 | [data-resize-handle-active] 15 | .internal-jotai-devtools-panel-resize-handle-wrapper, 16 | .internal-jotai-devtools-panel-resize-handle-wrapper:hover { 17 | .internal-jotai-devtools-panel-resize-handle-content { 18 | height: 90% !important; 19 | min-height: 90% !important; 20 | max-height: 90% !important; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/components/AtomDetail/atoms.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai/react'; 2 | import { atomWithStorage } from 'jotai/vanilla/utils'; 3 | import { useDevtoolsJotaiStoreOptions } from '../../../../../../../internal-jotai-store'; 4 | import { generateLocalStorageKey } from './../../../../../../../utils/generate-local-storage-key'; 5 | 6 | export type AtomValueViewer = 'raw-value' | 'json-tree'; 7 | 8 | const key = generateLocalStorageKey('atom-value-viewer', 0); 9 | export const atomValueViewer = atomWithStorage( 10 | key, 11 | 'raw-value', 12 | ); 13 | 14 | export const useAtomValueViewer = () => 15 | useAtom(atomValueViewer, useDevtoolsJotaiStoreOptions()); 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v5 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v5 17 | with: 18 | node-version: lts/* 19 | cache: 'pnpm' 20 | cache-dependency-path: '**/pnpm-lock.yaml' 21 | - run: pnpm install --frozen-lockfile 22 | - run: pnpm run lint 23 | - run: pnpm run test:ci 24 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/CodeSyntaxHighlighter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CodeHighlight, CodeHighlightProps } from '@mantine/code-highlight'; 3 | import clsx from 'clsx'; 4 | import './CodeSyntaxHighlighter.css'; 5 | 6 | export type CodeSyntaxHighlighterProps = Omit & { 7 | children: CodeHighlightProps['code']; 8 | }; 9 | 10 | export const CodeSyntaxHighlighter = ({ 11 | children, 12 | ...rest 13 | }: React.PropsWithChildren) => { 14 | return ( 15 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotDetail/components/DisplaySnapshotDetails/atoms.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { atomWithStorage } from 'jotai/vanilla/utils'; 3 | import { useDevtoolsJotaiStoreOptions } from './../../../../../../../../../internal-jotai-store'; 4 | import { generateLocalStorageKey } from './../../../../../../../../../utils/generate-local-storage-key'; 5 | 6 | export type SnapshotValueViewer = 'state' | 'diff'; 7 | 8 | const key = generateLocalStorageKey('snapshot-value-viewer', 0); 9 | 10 | export const snapshotValueViewer = atomWithStorage( 11 | key, 12 | 'diff', 13 | ); 14 | 15 | export const useSnapshotValueViewer = () => 16 | useAtom(snapshotValueViewer, useDevtoolsJotaiStoreOptions()); 17 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/components/SnapshotSearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { TextInput } from '@mantine/core'; 3 | import { useSnapshotSearchInput } from '../../../atoms'; 4 | 5 | export const SnapshotSearchInput = memo(() => { 6 | const [userInput, setUserInput] = useSnapshotSearchInput(); 7 | 8 | const handleOnChange: React.ChangeEventHandler = ( 9 | event, 10 | ) => { 11 | const { 12 | target: { value }, 13 | } = event; 14 | setUserInput(value); 15 | }; 16 | 17 | return ( 18 | 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /src/stories/ProviderLess/DevToolsProviderLess.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, StoryFn } from '@storybook/react-webpack5'; 3 | import { DevTools, DevToolsProps } from '../../'; 4 | import { Counter } from './Counter'; 5 | 6 | export default { 7 | component: DevTools, 8 | title: 'DevtoolsProviderLess', 9 | argTypes: { 10 | store: { 11 | control: { 12 | type: false, 13 | }, 14 | }, 15 | options: { 16 | control: { 17 | type: false, 18 | }, 19 | }, 20 | }, 21 | } as Meta; 22 | 23 | const Template: StoryFn = (args) => ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | 30 | export const Default = Template.bind({}); 31 | 32 | Default.args = { 33 | isInitialOpen: true, 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "jsx": "react", 6 | "module": "es2015", 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "noUncheckedIndexedAccess": true, 11 | "resolveJsonModule": true, 12 | "typeRoots": ["./node_modules/@types", "./types/*"], 13 | "sourceMap": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "exactOptionalPropertyTypes": true, 16 | "isolatedModules": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "jotai-devtools": ["./src"], 20 | "jotai-devtools/utils": ["./src/utils"] 21 | } 22 | }, 23 | "include": ["src/**/*", "types"], 24 | "exclude": ["node_modules", "dist", "tsup.config.ts", "postcss.config.js"] 25 | } 26 | -------------------------------------------------------------------------------- /src/DevTools/hooks/useUserStore.ts: -------------------------------------------------------------------------------- 1 | import { Options } from 'src/types'; 2 | import { useDevToolsStore } from '../../utils/hooks/useDevToolsStore'; 3 | import { useUserStoreValue } from '../atoms/user-custom-store'; 4 | import { isDevToolsStore } from './../../utils/internals/compose-with-devtools'; 5 | 6 | export const useUserStore = (): ReturnType => { 7 | const possibleUserStore = useUserStoreValue(); 8 | 9 | const userStore = useDevToolsStore( 10 | // This defaults to user's default store in a `provider-less` mode 11 | possibleUserStore ? { store: possibleUserStore } : undefined, 12 | ); 13 | 14 | return userStore; 15 | }; 16 | 17 | export { isDevToolsStore }; 18 | 19 | export const useUserStoreOptions = (): Options => { 20 | const store = useUserStore(); 21 | return { store: store }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { MantineStyleProp, Text } from '@mantine/core'; 3 | import { IconAlertCircle } from '@tabler/icons-react'; 4 | import { useThemeMode } from '../../../../hooks/useThemeMode'; 5 | type ErrorMessageProps = { 6 | message: string; 7 | }; 8 | 9 | const textStyles: MantineStyleProp = { 10 | display: 'flex', 11 | alignItems: 'center', 12 | }; 13 | 14 | export const ErrorMessage = ({ message }: ErrorMessageProps) => { 15 | const themedRedColor = useThemeMode('red.8', 'red.5'); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | {message} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/MetaInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Code, CodeProps, DefaultMantineColor, Text } from '@mantine/core'; 3 | 4 | type MetaInfoProps = Pick & { 5 | label: string; 6 | value: string; 7 | }; 8 | 9 | export const MetaInfo = ({ label, value, color }: MetaInfoProps) => { 10 | return ( 11 | 12 | 19 | {label} 20 | 21 | 26 | {value} 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Group, Stack } from '@mantine/core'; 3 | import { ClearHistory } from './ClearHistory'; 4 | import './Header.css'; 5 | import { RecordHistory } from './RecordHistory'; 6 | import { SnapshotListNavigation } from './SnapshotListNavigation'; 7 | import { SnapshotSearchInput } from './SnapshotSearchInput'; 8 | 9 | export const Header = () => { 10 | return ( 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 4 | }, 5 | "[javascriptreact]": { 6 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 7 | }, 8 | "[typescript]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 13 | }, 14 | "files.autoSave": "onFocusChange", 15 | "editor.smoothScrolling": true, 16 | "eslint.validate": [ 17 | "javascript", 18 | "javascriptreact", 19 | "typescript", 20 | "typescriptreact" 21 | ], 22 | "editor.codeActionsOnSave": { 23 | "source.fixAll.eslint": "explicit" 24 | }, 25 | "editor.multiCursorModifier": "alt", 26 | "eslint.useESLintClass": true, 27 | "cssVariables.lookupFiles": [ 28 | "**/*.css", 29 | "**/*.scss", 30 | "**/*.sass", 31 | "**/*.less", 32 | "node_modules/@mantine/core/styles.css" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}" 4 | }, 5 | "git": { 6 | "commitMessage": "chore: release v${version}", 7 | "requireUpstream": true, 8 | "requireCleanWorkingDir": true 9 | }, 10 | "github": { 11 | "release": true 12 | }, 13 | "plugins": { 14 | "@release-it/conventional-changelog": { 15 | "infile": "CHANGELOG.md", 16 | "preset": { 17 | "name": "conventionalcommits", 18 | "types": [ 19 | { "type": "feat", "section": "Features" }, 20 | { "type": "fix", "section": "Bug Fixes" }, 21 | { "type": "refactor", "section": "Refactors" }, 22 | { "type": "perf", "section": "Performance improvements" }, 23 | { "type": "chore", "hidden": true }, 24 | { "type": "style", "hidden": true }, 25 | { "type": "docs", "hidden": true }, 26 | { "type": "test", "hidden": true } 27 | ] 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import * as ResizeObserverModule from 'resize-observer-polyfill'; 3 | 4 | (global as any).ResizeObserver = ResizeObserverModule.default; 5 | 6 | const originalWarn = console.warn; 7 | beforeAll(() => { 8 | console.warn = (...args: any[]) => { 9 | if ( 10 | typeof args[0] === 'string' && 11 | args[0].includes( 12 | '[jotai-devtools]: automatic tree-shaking in development mode is being deprecated', 13 | ) 14 | ) { 15 | return; 16 | } 17 | originalWarn(...args); 18 | }; 19 | }); 20 | 21 | afterAll(() => { 22 | console.warn = originalWarn; 23 | }); 24 | 25 | Object.defineProperty(window, 'matchMedia', { 26 | writable: true, 27 | value: (query: string) => ({ 28 | matches: false, 29 | media: query, 30 | onchange: null, 31 | addListener: jest.fn(), 32 | removeListener: jest.fn(), 33 | addEventListener: jest.fn(), 34 | removeEventListener: jest.fn(), 35 | dispatchEvent: jest.fn(), 36 | }), 37 | }); 38 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotDetail/components/DisplaySnapshotDetails/DisplaySnapshotDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Title } from '@mantine/core'; 3 | import { SelectedSnapshotDetail } from '../../atoms'; 4 | import { SnapshotActions } from './components/SnapshotActions'; 5 | import { SnapshotMetaDetails } from './components/SnapshotMetaDetails'; 6 | import { SnapshotValue } from './components/SnapshotValue'; 7 | 8 | type DisplaySnapshotDetailsProps = { 9 | details: SelectedSnapshotDetail; 10 | }; 11 | 12 | export const DisplaySnapshotDetails = (props: DisplaySnapshotDetailsProps) => { 13 | return ( 14 | 15 | Snapshot {props.details.label} 16 | 17 | 18 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/PanelResizeHandle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, MantineStyleProp } from '@mantine/core'; 3 | import { PanelResizeHandle as ReactPanelResizeHandle } from 'react-resizable-panels'; 4 | import { useThemeMode } from '../../../../hooks/useThemeMode'; 5 | import './PanelResizeHandle.css'; 6 | 7 | const innerContainerStyles: MantineStyleProp = { 8 | borderRadius: '2rem', 9 | verticalAlign: 'middle', 10 | }; 11 | 12 | export const PanelResizeHandle = () => { 13 | return ( 14 | 15 | 19 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai/vanilla'; 2 | import { atomWithDefault } from 'jotai/vanilla/utils'; 3 | import { AnyAtom, ValuesAtomTuple } from 'src/types'; 4 | import { valuesAtom } from '../../../../../atoms/values-atom'; 5 | import { filterAtomsByString } from './utils/filter-atoms-by-string'; 6 | 7 | type SelectedAtomAtomData = { atomKey: string; atom: AnyAtom }; 8 | 9 | export const selectedAtomAtom = atom( 10 | undefined, 11 | ); 12 | 13 | // used to preserve search input across tab switch 14 | const searchInputInternalValueAtom = atom(''); 15 | 16 | export const filteredValuesAtom = atomWithDefault((get) => { 17 | const filteredByString = filterAtomsByString( 18 | get(searchInputInternalValueAtom), 19 | get(valuesAtom), 20 | ); 21 | 22 | return filteredByString; 23 | }); 24 | 25 | export const searchInputAtom = atom( 26 | (get) => get(searchInputInternalValueAtom), 27 | (_, set, searchInput: string) => { 28 | set(searchInputInternalValueAtom, searchInput); 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/hooks/use.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import ReactExports from 'react'; 3 | 4 | export const isPromise = (x: unknown): x is Promise => 5 | x instanceof Promise; 6 | 7 | // Copied as is, from Jotai core 8 | export const use = 9 | ReactExports.use || 10 | (( 11 | promise: Promise & { 12 | status?: 'pending' | 'fulfilled' | 'rejected'; 13 | value?: T; 14 | reason?: unknown; 15 | }, 16 | ): T => { 17 | if (promise.status === 'pending') { 18 | throw promise; 19 | } else if (promise.status === 'fulfilled') { 20 | return promise.value as T; 21 | } else if (promise.status === 'rejected') { 22 | throw promise.reason; 23 | } else { 24 | promise.status = 'pending'; 25 | promise.then( 26 | (v) => { 27 | promise.status = 'fulfilled'; 28 | promise.value = v; 29 | }, 30 | (e) => { 31 | promise.status = 'rejected'; 32 | promise.reason = e; 33 | }, 34 | ); 35 | throw promise; 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jotai Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stories/Default/DevTools.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, StoryObj } from '@storybook/react-webpack5'; 3 | import { DevTools, DevToolsProps } from '../../'; 4 | import { DemoApp } from './Demos/DemoApp'; 5 | 6 | export default { 7 | component: DevTools, 8 | title: 'Devtools', 9 | } as Meta; 10 | 11 | type CustomStorybookProps = DevToolsProps & { 12 | 'options.shouldShowPrivateAtoms': boolean; 13 | }; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | render: ({ ...args }) => { 19 | const nextOptions = { 20 | ...args.options, 21 | shouldShowPrivateAtoms: args['options.shouldShowPrivateAtoms'], 22 | }; 23 | const props = { 24 | ...args, 25 | options: nextOptions, 26 | }; 27 | return ; 28 | }, 29 | args: { 30 | isInitialOpen: true, 31 | 'options.shouldShowPrivateAtoms': false, 32 | }, 33 | argTypes: { 34 | store: { 35 | control: { 36 | type: false, 37 | }, 38 | }, 39 | options: { 40 | control: { 41 | type: false, 42 | }, 43 | }, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/AtomViewer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Flex } from '@mantine/core'; 3 | import { Panel, PanelGroup } from 'react-resizable-panels'; 4 | import { PanelResizeHandle } from '../PanelResizeHandle'; 5 | import './AtomViewer.css'; 6 | import { AtomDetail } from './components/AtomDetail'; 7 | import { AtomList } from './components/AtomList'; 8 | 9 | const panelStyles = { overflow: 'auto' }; 10 | 11 | export const AtomViewer = React.memo(() => { 12 | return ( 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/components/RecordHistory.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionIcon, Tooltip } from '@mantine/core'; 3 | import { IconPlayerRecordFilled } from '@tabler/icons-react'; 4 | import { useThemeMode } from '../../../../../../../../hooks/useThemeMode'; 5 | import { useShouldRecordSnapshotHistory } from '../../../atoms'; 6 | 7 | export const RecordHistory = () => { 8 | const [shouldRecord, setShouldRecord] = useShouldRecordSnapshotHistory(); 9 | const handleOnClick = () => { 10 | setShouldRecord((prev) => !prev); 11 | }; 12 | const label = shouldRecord 13 | ? 'Stop recording snapshot history' 14 | : 'Record snapshot history'; 15 | return ( 16 | 17 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/DevTools/utils/stringify-atom-value.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from 'javascript-stringify'; 2 | import { AnyAtomValue } from 'src/types'; 3 | import { getTypeOfAtomValue } from './get-type-of-atom-value'; 4 | 5 | const stringifyWithDoubleQuotes: Parameters[1] = ( 6 | value, 7 | _, 8 | stringify, 9 | ) => { 10 | if (typeof value === 'string') { 11 | return '"' + value.replace(/"/g, '\\"') + '"'; 12 | } 13 | 14 | return stringify(value); 15 | }; 16 | const literalStringValues = ['bigint', 'symbol', 'undefined', 'function']; 17 | 18 | export const ErrorSymbol = Symbol('parsing-error'); 19 | 20 | export const stringifyAtomValue = ( 21 | atomValue: AnyAtomValue, 22 | ): string | typeof ErrorSymbol => { 23 | const type = getTypeOfAtomValue(atomValue); 24 | 25 | if (literalStringValues.includes(type)) { 26 | return String(atomValue); 27 | } 28 | 29 | try { 30 | const result = stringify(atomValue, stringifyWithDoubleQuotes, 2); 31 | 32 | // Perhaps a value that we couldn't serialize? 33 | if (typeof result === 'undefined') { 34 | return String(atomValue); 35 | } 36 | 37 | return result; 38 | } catch (e) { 39 | return ErrorSymbol; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/stories/Default/Demos/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Button, Code, Title } from '@mantine/core'; 3 | import { useTimeout } from '@mantine/hooks'; 4 | import { useAtom } from 'jotai/react'; 5 | import { atom } from 'jotai/vanilla'; 6 | import { demoStoreOptions } from './demo-store'; 7 | 8 | const countAtom = atom(0); 9 | countAtom.debugLabel = 'countAtom'; 10 | 11 | export const Counter = () => { 12 | const [count, setCount] = useAtom(countAtom, demoStoreOptions); 13 | const add = React.useCallback(() => setCount((c) => c + 1), [setCount]); 14 | 15 | useTimeout( 16 | () => { 17 | // automatically trigger updates when testing time travel feature 18 | const emptyArray = Array.from({ length: 0 }); 19 | emptyArray.forEach(add); 20 | }, 21 | 200, 22 | { autoInvoke: true }, 23 | ); 24 | return ( 25 | 26 | Counter 27 | {count} 28 | 29 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/DevTools/utils/get-type-of-atom-value.ts: -------------------------------------------------------------------------------- 1 | import { WritableAtom } from 'jotai/vanilla'; 2 | import { AnyAtom, AnyAtomValue, WithInitialValue } from 'src/types'; 3 | 4 | const isValueAtom = (value: AnyAtomValue): value is AnyAtom => { 5 | return ( 6 | typeof (value as Partial)?.read === 'function' || 7 | typeof (value as WritableAtom)?.write === 'function' || 8 | !!(value as WithInitialValue)?.init || 9 | !!(value as Partial)?.debugLabel 10 | ); 11 | }; 12 | 13 | type TypeOfReturn = 14 | | 'string' 15 | | 'number' 16 | | 'bigint' 17 | | 'boolean' 18 | | 'symbol' 19 | | 'undefined' 20 | | 'object' 21 | | 'function'; 22 | 23 | export type AtomValueType = 24 | | 'promise' 25 | | 'array' 26 | | 'null' 27 | | 'atom' 28 | | TypeOfReturn; 29 | 30 | export const getTypeOfAtomValue = (value: AnyAtomValue): AtomValueType => { 31 | if (value instanceof Promise) { 32 | return 'promise'; 33 | } 34 | 35 | if (Array.isArray(value)) { 36 | return 'array'; 37 | } 38 | 39 | if (value === null) { 40 | return 'null'; 41 | } 42 | 43 | if (isValueAtom(value)) { 44 | return 'atom'; 45 | } 46 | 47 | const result = typeof value; 48 | return result; 49 | }; 50 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/components/AtomDetail/components/AtomMetaDetails.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from '@mantine/core'; 3 | import { useThemeMode } from '../../../../../../../../hooks/useThemeMode'; 4 | import { AtomValueType } from '../../../../../../../../utils/get-type-of-atom-value'; 5 | import { MetaInfo } from '../../../../MetaInfo'; 6 | 7 | type AtomMetaDetailsProps = { 8 | debugLabel: string; 9 | atomValueType: AtomValueType; 10 | isAtomPrivate?: boolean | undefined; 11 | }; 12 | 13 | export const AtomMetaDetails = React.memo( 14 | ({ 15 | debugLabel, 16 | atomValueType, 17 | isAtomPrivate, 18 | }: AtomMetaDetailsProps): JSX.Element => { 19 | const privateColor = useThemeMode('red.1', 'red.7'); 20 | 21 | return ( 22 | 23 | 24 | Meta 25 | 26 | 27 | 28 | {isAtomPrivate && ( 29 | 30 | )} 31 | 32 | ); 33 | }, 34 | ); 35 | 36 | AtomMetaDetails.displayName = 'AtomMetaDetails'; 37 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/components/SnapshotListNavigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionIcon, Group } from '@mantine/core'; 3 | import { IconChevronDown, IconChevronUp } from '@tabler/icons-react'; 4 | import { useThemeMode } from '../../../../../../../../hooks/useThemeMode'; 5 | import { useSnapshotHistoryNavigation } from '../atoms'; 6 | 7 | export const SnapshotListNavigation = () => { 8 | const { prev, next } = useSnapshotHistoryNavigation(); 9 | return ( 10 | 11 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/AtomViewer/components/AtomDetail/components/DisplayAtomDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Title } from '@mantine/core'; 3 | import { AnyAtom } from 'src/types'; 4 | import { 5 | atomToPrintable, 6 | getTypeOfAtomValue, 7 | } from '../../../../../../../../utils'; 8 | import { useInternalAtomValue } from '../../../hooks/useInternalAtomValue'; 9 | import { AtomDependentsList } from './AtomDependentsList'; 10 | import { AtomMetaDetails } from './AtomMetaDetails'; 11 | import { AtomValue } from './AtomValue'; 12 | 13 | type DisplayAtomDetailsProps = { 14 | atom: AnyAtom; 15 | }; 16 | 17 | export const DisplayAtomDetails = ({ atom }: DisplayAtomDetailsProps) => { 18 | const atomValue = useInternalAtomValue(atom); 19 | const atomValueType = getTypeOfAtomValue(atomValue); 20 | 21 | return ( 22 | 23 | Atom Details 24 | 29 | 30 | 31 | 32 | {/* TODO add dependencies list */} 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/PlayBar.css: -------------------------------------------------------------------------------- 1 | .internal-jotai-devtools-playbar-wrapper { 2 | height: 56px; 3 | border-top: 0.09rem solid 4 | light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); 5 | display: flex; 6 | align-items: center; 7 | padding: var(--mantine-spacing-sm); 8 | gap: 12px; 9 | } 10 | 11 | .internal-jotai-devtools-playbar-root { 12 | flex-grow: 1; 13 | } 14 | 15 | .internal-jotai-devtools-playbar-markLabel { 16 | display: none; 17 | } 18 | 19 | .internal-jotai-devtools-playbar-bar { 20 | background-color: light-dark( 21 | var(--mantine-color-dark-4), 22 | var(--mantine-color-gray-6) 23 | ); 24 | } 25 | 26 | .internal-jotai-devtools-playbar-track:before { 27 | background-color: light-dark( 28 | var(--mantine-color-gray-3), 29 | var(--mantine-color-dark-4) 30 | ); 31 | } 32 | 33 | .internal-jotai-devtools-playbar-mark { 34 | background-color: light-dark( 35 | var(--mantine-color-gray-7), 36 | var(--mantine-color-gray-5) 37 | ); 38 | border-width: 0; 39 | } 40 | 41 | .internal-jotai-devtools-playbar-thumb { 42 | height: 14px !important; 43 | width: 14px !important; 44 | border-width: 3px !important; 45 | border-color: light-dark( 46 | var(--mantine-color-dark-4), 47 | var(--mantine-color-gray-6) 48 | ) !important; 49 | } 50 | -------------------------------------------------------------------------------- /src/DevTools/hooks/useAtomsSnapshots.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | import { useAtomsSnapshot as useJotaiAtomsSnapshot } from '../../utils'; 3 | import { useDevToolsOptionsValue } from '../atoms/devtools-options'; 4 | import { useSnapshotValues } from '../atoms/values-atom'; 5 | import { useUserStore } from './useUserStore'; 6 | 7 | type SnapshotOptions = Parameters[0]; 8 | 9 | export const useAtomsSnapshots = () => { 10 | const { shouldShowPrivateAtoms } = useDevToolsOptionsValue(); 11 | const store = useUserStore(); 12 | const opts: SnapshotOptions = { store, shouldShowPrivateAtoms }; 13 | 14 | const currentSnapshots = useJotaiAtomsSnapshot(opts); 15 | return currentSnapshots; 16 | }; 17 | 18 | // We're doing this to to prevent creating multiple 19 | // copies for values array and share it via DevtoolsJotaiStore 20 | // The idea is for the entire Shell to share values atom 21 | export const useSyncSnapshotValuesToAtom = () => { 22 | const currentSnapshots = useAtomsSnapshots(); 23 | const [values, setValues] = useSnapshotValues(); 24 | 25 | const valuesArr = useMemo(() => { 26 | const nextValues = Array.from(currentSnapshots.values); 27 | 28 | return nextValues; 29 | }, [currentSnapshots.values]); 30 | 31 | useEffect(() => { 32 | setValues(valuesArr); 33 | }, [setValues, valuesArr]); 34 | 35 | return values; 36 | }; 37 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from 'jotai/react'; 2 | import type { Atom, WritableAtom, createStore } from 'jotai/vanilla'; 3 | import type { 4 | INTERNAL_AtomState, 5 | INTERNAL_Store, 6 | } from 'jotai/vanilla/internals'; 7 | 8 | export type DevStore = { 9 | get_internal_weak_map: () => { 10 | get: (atom: Atom) => INTERNAL_AtomState | undefined; 11 | }; 12 | get_mounted_atoms: () => Set>; 13 | restore_atoms: (values: Iterable, unknown]>) => void; 14 | }; 15 | 16 | export type StoreWithoutDevMethods = ReturnType; 17 | export type StoreWithDevMethods = INTERNAL_Store & DevStore; 18 | 19 | export type Store = StoreWithoutDevMethods | StoreWithDevMethods; 20 | 21 | export type Options = Parameters[0]; 22 | 23 | export type AnyAtomValue = unknown; 24 | export type AnyAtomError = unknown; 25 | export type AnyAtom = Atom; 26 | export type AnyWritableAtom = WritableAtom; 27 | 28 | export type AtomsValues = Map; // immutable 29 | export type AtomsDependents = Map>; // immutable 30 | export type AtomsSnapshot = Readonly<{ 31 | values: AtomsValues; 32 | dependents: AtomsDependents; 33 | }>; 34 | 35 | export type WithInitialValue = { 36 | init: Value; 37 | }; 38 | 39 | export type ValuesAtomTuple = [AnyAtom, AnyAtomValue]; 40 | -------------------------------------------------------------------------------- /src/stories/Default/Playground/SomeComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Title } from '@mantine/core'; 3 | import { atom, useAtom, useAtomValue } from 'jotai'; 4 | 5 | const baseCountAtom = atom(1); 6 | baseCountAtom.debugLabel = 'baseCountAtom'; 7 | 8 | const doubleAtom = atom( 9 | (get) => get(baseCountAtom) * 2, 10 | (get, set, update: number) => { 11 | set(baseCountAtom, update / 2); 12 | }, 13 | ); 14 | doubleAtom.debugLabel = 'doubleAtom'; 15 | 16 | const initialAtom = atom((get) => ({ 17 | a: get(baseCountAtom), 18 | })); 19 | initialAtom.debugLabel = 'initialAtom'; 20 | 21 | const arrayAtoms = atom([initialAtom]); 22 | arrayAtoms.debugLabel = 'arrayAtoms'; 23 | 24 | export const SomeComponent = () => { 25 | const [count, setCount] = useAtom(doubleAtom); 26 | useAtomValue(arrayAtoms); 27 | const handleOnClick = React.useCallback(() => { 28 | setCount(count + 1); 29 | }, [setCount, count]); 30 | 31 | return ( 32 |
33 | hey there! {count} 34 | 35 |
36 | ); 37 | }; 38 | 39 | export const SomeComponentWithToggle = () => { 40 | const [shouldShow, setShouldShow] = React.useState(true); 41 | 42 | const handleOntoggle = React.useCallback(() => { 43 | setShouldShow((s) => !s); 44 | }, [setShouldShow]); 45 | 46 | return ( 47 | <> 48 | Toggle 49 | {shouldShow ? : null} 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/SnapshotList/components/ClearHistory.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionIcon } from '@mantine/core'; 3 | import { IconTrashX } from '@tabler/icons-react'; 4 | import { useThemeMode } from '../../../../../../../../hooks/useThemeMode'; 5 | import { 6 | useFilteredSnapshotHistoryAtomValue, 7 | useSetSelectedSnapshotId, 8 | useSetSnapshotHistory, 9 | } from '../../../atoms'; 10 | 11 | export const ClearHistory = () => { 12 | const filteredSnapshotHistory = useFilteredSnapshotHistoryAtomValue(); 13 | const setSnapshotHistory = useSetSnapshotHistory(); 14 | const setSelectedSnapshotIdx = useSetSelectedSnapshotId(); 15 | 16 | const handleOnClick = () => { 17 | setSnapshotHistory((prev) => { 18 | const lastItem = prev.at(-1); 19 | if (lastItem) { 20 | // Make last item hidden so that it doesn't show up in the list 21 | // but we could still use it to show the last snapshot 22 | lastItem.isHidden = true; 23 | lastItem.label = 0; 24 | return [lastItem]; 25 | } 26 | return []; 27 | }); 28 | 29 | setSelectedSnapshotIdx(undefined); 30 | }; 31 | 32 | return ( 33 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/redux-extension/getReduxExtension.ts: -------------------------------------------------------------------------------- 1 | // Original but incomplete type of the redux extension package 2 | type Extension = NonNullable; 3 | 4 | export type ReduxExtension = { 5 | /** Create a connection to the extension. 6 | * This will connect a store (like an atom) to the extension and 7 | * display it within the extension tab. 8 | * 9 | * @param options https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md 10 | * @returns https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Methods.md#connectoptions 11 | */ 12 | connect: Extension['connect']; 13 | 14 | /** Disconnects all existing connections to the redux extension. 15 | * Only use this when you are sure that no other connection exists 16 | * or you want to remove all existing connections. 17 | */ 18 | disconnect?: () => void; 19 | 20 | /** Have a look at the documentation for more methods: 21 | * https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Methods.md 22 | */ 23 | }; 24 | 25 | /** Returns the global redux extension object if available */ 26 | export const getReduxExtension = ( 27 | enabled = __DEV__, 28 | ): ReduxExtension | undefined => { 29 | if (!enabled) { 30 | return undefined; 31 | } 32 | 33 | const reduxExtension = window.__REDUX_DEVTOOLS_EXTENSION__; 34 | if (!reduxExtension && __DEV__) { 35 | console.warn('Please install/enable Redux devtools extension'); 36 | return undefined; 37 | } 38 | 39 | return reduxExtension; 40 | }; 41 | -------------------------------------------------------------------------------- /src/stories/Default/Playground/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Button, Code, Title } from '@mantine/core'; 3 | import { useTimeout } from '@mantine/hooks'; 4 | import { useAtom, useAtomValue } from 'jotai/react'; 5 | import { atom } from 'jotai/vanilla'; 6 | 7 | const countAtom = atom(0); 8 | countAtom.debugLabel = 'countAtom'; 9 | 10 | const doubleCountAtom = atom((get) => get(countAtom) * 2); 11 | doubleCountAtom.debugLabel = 'doubleCountAtom'; 12 | 13 | const doubleCountInNestedObjectAtom = atom((get) => { 14 | return { 15 | doubleCount: { 16 | nested: { 17 | value: get(doubleCountAtom), 18 | }, 19 | }, 20 | }; 21 | }); 22 | doubleCountInNestedObjectAtom.debugLabel = 'doubleCountInNestedObjectAtom'; 23 | 24 | export const Counter = () => { 25 | const [count, setCount] = useAtom(countAtom); 26 | 27 | useAtomValue(doubleCountAtom); 28 | useAtomValue(doubleCountInNestedObjectAtom); 29 | 30 | const add = React.useCallback(() => setCount((c) => c + 1), [setCount]); 31 | useTimeout( 32 | () => { 33 | const emptyArray = Array.from({ length: 0 }); 34 | emptyArray.forEach(add); 35 | }, 36 | 200, 37 | { autoInvoke: true }, 38 | ); 39 | return ( 40 | 41 | Counter 42 | {count} 43 | 44 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/DevTools/Extension/components/Shell/components/TimeTravel/components/PlaybackSpeedDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Select } from '@mantine/core'; 3 | import { defaultPlaybackOptions, usePlaybackSpeedOption } from '../atoms'; 4 | 5 | const options = Object.keys( 6 | defaultPlaybackOptions, 7 | ) as (keyof typeof defaultPlaybackOptions)[]; 8 | 9 | const isValidPlaybackOption = ( 10 | value: string | null, 11 | ): value is keyof typeof defaultPlaybackOptions => { 12 | return options.includes(value as keyof typeof defaultPlaybackOptions); 13 | }; 14 | 15 | export const PlaybackSpeedDropdown = () => { 16 | const [value, setOption] = usePlaybackSpeedOption(); 17 | 18 | const handleOnChange = (value: string | null) => { 19 | // User select the option that was already selected 20 | if (value === null) { 21 | return; 22 | } 23 | 24 | if (isValidPlaybackOption(value)) { 25 | return setOption(value); 26 | } 27 | throw new Error(`[jotai-devtools]: invalid playback option: ${value}`); 28 | }; 29 | 30 | return ( 31 |