Adds indicator for dots, rename and delete dialogs for pages, dark mode support for ui
This commit is contained in:
parent
62a3da0498
commit
2937016ae0
12 changed files with 127 additions and 86 deletions
|
@ -86,22 +86,25 @@ const initialDoc: TLDrawDocument = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Editor(): JSX.Element {
|
export default function Editor(): JSX.Element {
|
||||||
const { value, setValue, status } = usePersistence('doc', initialDoc)
|
// const { value, setValue, status } = usePersistence('doc', initialDoc)
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
// const handleChange = React.useCallback(
|
||||||
(tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
|
// (tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
|
||||||
if (reason.startsWith('session')) {
|
// if (reason.startsWith('session')) {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
setValue(tlstate.document)
|
// setValue(tlstate.document)
|
||||||
},
|
// },
|
||||||
[setValue]
|
// [setValue]
|
||||||
)
|
// )
|
||||||
|
|
||||||
if (status === 'loading' || value === null) {
|
// if (status === 'loading' || value === null) {
|
||||||
return <div />
|
// return <div />
|
||||||
}
|
// }
|
||||||
|
|
||||||
return <TLDraw document={value} onChange={handleChange} />
|
// return <TLDraw document={value} onChange={handleChange} />
|
||||||
|
|
||||||
|
// Will automatically persist data under the provided id, too
|
||||||
|
return <TLDraw id="tldraw" />
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"ismobilejs": "^1.1.1",
|
"ismobilejs": "^1.1.1",
|
||||||
"perfect-freehand": "^0.5.2",
|
"perfect-freehand": "^0.5.2",
|
||||||
"react-hotkeys-hook": "^3.4.0",
|
"react-hotkeys-hook": "^3.4.0",
|
||||||
"rko": "^0.5.18"
|
"rko": "^0.5.19"
|
||||||
},
|
},
|
||||||
"gitHead": "4a7439ddf81b615ee49fddbe00802699975f9375"
|
"gitHead": "4a7439ddf81b615ee49fddbe00802699975f9375"
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import {
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
RowButton,
|
RowButton,
|
||||||
MenuTextInput,
|
|
||||||
DialogInputWrapper,
|
|
||||||
Divider,
|
Divider,
|
||||||
} from '~components/shared'
|
} from '~components/shared'
|
||||||
import type { Data, TLDrawPage } from '~types'
|
import type { Data, TLDrawPage } from '~types'
|
||||||
|
@ -21,9 +19,10 @@ const canDeleteSelector = (s: Data) => {
|
||||||
interface PageOptionsDialogProps {
|
interface PageOptionsDialogProps {
|
||||||
page: TLDrawPage
|
page: TLDrawPage
|
||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX.Element {
|
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||||
const { tlstate, useSelector } = useTLDrawContext()
|
const { tlstate, useSelector } = useTLDrawContext()
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false)
|
const [isOpen, setIsOpen] = React.useState(false)
|
||||||
|
@ -32,18 +31,16 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
||||||
|
|
||||||
const rInput = React.useRef<HTMLInputElement>(null)
|
const rInput = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const [name, setName] = React.useState(page.name || 'Page')
|
|
||||||
|
|
||||||
const handleNameChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setName(e.currentTarget.value)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDuplicate = React.useCallback(() => {
|
const handleDuplicate = React.useCallback(() => {
|
||||||
tlstate.duplicatePage(page.id)
|
tlstate.duplicatePage(page.id)
|
||||||
|
onClose?.()
|
||||||
}, [tlstate])
|
}, [tlstate])
|
||||||
|
|
||||||
const handleDelete = React.useCallback(() => {
|
const handleDelete = React.useCallback(() => {
|
||||||
tlstate.deletePage(page.id)
|
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
||||||
|
tlstate.deletePage(page.id)
|
||||||
|
onClose?.()
|
||||||
|
}
|
||||||
}, [tlstate])
|
}, [tlstate])
|
||||||
|
|
||||||
const handleOpenChange = React.useCallback(
|
const handleOpenChange = React.useCallback(
|
||||||
|
@ -54,27 +51,18 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
||||||
onOpen?.()
|
onOpen?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.length === 0) {
|
|
||||||
tlstate.renamePage(page.id, 'Page')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[tlstate, name]
|
[tlstate, name]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSave = React.useCallback(() => {
|
|
||||||
tlstate.renamePage(page.id, name)
|
|
||||||
}, [tlstate, name])
|
|
||||||
|
|
||||||
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e: React.KeyboardEvent<HTMLDivElement>) {
|
// TODO: Replace with text input
|
||||||
if (e.key === 'Enter') {
|
function handleRename() {
|
||||||
handleSave()
|
const nextName = window.prompt('New name:', page.name)
|
||||||
setIsOpen(false)
|
tlstate.renamePage(page.id, nextName || page.name || 'Page')
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -93,15 +81,9 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Overlay as={DialogOverlay} />
|
<Dialog.Overlay as={DialogOverlay} />
|
||||||
<Dialog.Content as={DialogContent} onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
<Dialog.Content as={DialogContent} onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
||||||
<DialogInputWrapper>
|
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleRename}>
|
||||||
<MenuTextInput
|
Rename
|
||||||
ref={rInput}
|
</Dialog.Action>
|
||||||
value={name}
|
|
||||||
onChange={handleNameChange}
|
|
||||||
onKeyDown={handleKeydown}
|
|
||||||
/>
|
|
||||||
</DialogInputWrapper>
|
|
||||||
<Divider />
|
|
||||||
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleDuplicate}>
|
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleDuplicate}>
|
||||||
Duplicate
|
Duplicate
|
||||||
</Dialog.Action>
|
</Dialog.Action>
|
||||||
|
@ -115,9 +97,6 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
||||||
Delete
|
Delete
|
||||||
</Dialog.Action>
|
</Dialog.Action>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleSave}>
|
|
||||||
Save
|
|
||||||
</Dialog.Action>
|
|
||||||
<Dialog.Cancel as={RowButton} bp={breakpoints}>
|
<Dialog.Cancel as={RowButton} bp={breakpoints}>
|
||||||
Cancel
|
Cancel
|
||||||
</Dialog.Cancel>
|
</Dialog.Cancel>
|
||||||
|
|
|
@ -100,7 +100,7 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</DropdownMenu.ItemIndicator>
|
</DropdownMenu.ItemIndicator>
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
<PageOptionsDialog page={page} />
|
<PageOptionsDialog page={page} onClose={onClose} />
|
||||||
</ButtonWithOptions>
|
</ButtonWithOptions>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
|
|
|
@ -12,25 +12,51 @@ import { ToolsPanel } from '~components/tools-panel'
|
||||||
import { PagePanel } from '~components/page-panel'
|
import { PagePanel } from '~components/page-panel'
|
||||||
import { Menu } from '~components/menu'
|
import { Menu } from '~components/menu'
|
||||||
|
|
||||||
export interface TLDrawProps {
|
// Selectors
|
||||||
document?: TLDrawDocument
|
|
||||||
currentPageId?: string
|
|
||||||
onMount?: (state: TLDrawState) => void
|
|
||||||
onChange?: TLDrawState['_onChange']
|
|
||||||
}
|
|
||||||
|
|
||||||
const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
|
const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
|
||||||
|
|
||||||
const isSelectedShapeWithHandlesSelector = (s: Data) => {
|
const isSelectedShapeWithHandlesSelector = (s: Data) => {
|
||||||
const { shapes } = s.document.pages[s.appState.currentPageId]
|
const { shapes } = s.document.pages[s.appState.currentPageId]
|
||||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||||
return selectedIds.length === 1 && selectedIds.every((id) => shapes[id].handles !== undefined)
|
return selectedIds.length === 1 && selectedIds.every((id) => shapes[id].handles !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
||||||
|
|
||||||
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
||||||
|
|
||||||
const isDarkModeSelector = (s: Data) => s.settings.isDarkMode
|
const isDarkModeSelector = (s: Data) => s.settings.isDarkMode
|
||||||
|
|
||||||
export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }: TLDrawProps) {
|
export interface TLDrawProps {
|
||||||
const [tlstate] = React.useState(() => new TLDrawState())
|
/**
|
||||||
|
* (optional) If provided, the component will load / persist state under this key.
|
||||||
|
*/
|
||||||
|
id?: string
|
||||||
|
/**
|
||||||
|
* (optional) The document to load or update from.
|
||||||
|
*/
|
||||||
|
document?: TLDrawDocument
|
||||||
|
/**
|
||||||
|
* (optional) The current page id.
|
||||||
|
*/
|
||||||
|
currentPageId?: string
|
||||||
|
/**
|
||||||
|
* (optional) A callback to run when the component mounts.
|
||||||
|
*/
|
||||||
|
onMount?: (state: TLDrawState) => void
|
||||||
|
/**
|
||||||
|
* (optional) A callback to run when the component's state changes.
|
||||||
|
*/
|
||||||
|
onChange?: TLDrawState['_onChange']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TLDraw({ id, document, currentPageId, onMount, onChange: _onChange }: TLDrawProps) {
|
||||||
|
const [tlstate, setTlstate] = React.useState(() => new TLDrawState(id))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setTlstate(new TLDrawState(id))
|
||||||
|
}, [id])
|
||||||
|
|
||||||
const [context] = React.useState(() => {
|
const [context] = React.useState(() => {
|
||||||
return { tlstate, useSelector: tlstate.useStore }
|
return { tlstate, useSelector: tlstate.useStore }
|
||||||
})
|
})
|
||||||
|
@ -38,10 +64,15 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }
|
||||||
useKeyboardShortcuts(tlstate)
|
useKeyboardShortcuts(tlstate)
|
||||||
|
|
||||||
const page = context.useSelector(pageSelector)
|
const page = context.useSelector(pageSelector)
|
||||||
|
|
||||||
const pageState = context.useSelector(pageStateSelector)
|
const pageState = context.useSelector(pageStateSelector)
|
||||||
|
|
||||||
const isDarkMode = context.useSelector(isDarkModeSelector)
|
const isDarkMode = context.useSelector(isDarkModeSelector)
|
||||||
|
|
||||||
const isSelecting = context.useSelector(isInSelectSelector)
|
const isSelecting = context.useSelector(isInSelectSelector)
|
||||||
|
|
||||||
const isSelectedHandlesShape = context.useSelector(isSelectedShapeWithHandlesSelector)
|
const isSelectedHandlesShape = context.useSelector(isSelectedShapeWithHandlesSelector)
|
||||||
|
|
||||||
const isInSession = !!tlstate.session
|
const isInSession = !!tlstate.session
|
||||||
|
|
||||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||||
|
|
|
@ -200,7 +200,8 @@ export function useKeyboardShortcuts(tlstate: TLDrawState) {
|
||||||
tlstate.moveToFront()
|
tlstate.moveToFront()
|
||||||
})
|
})
|
||||||
|
|
||||||
useHotkeys('command+shift+backspace', () => {
|
useHotkeys('command+shift+backspace', (e) => {
|
||||||
tlstate.reset()
|
tlstate.reset()
|
||||||
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,25 @@
|
||||||
import type { Theme } from '~types'
|
import React from 'react'
|
||||||
|
import type { Data, Theme } from '~types'
|
||||||
|
import { useTLDrawContext } from './useTLDrawContext'
|
||||||
|
import { dark } from '~styles'
|
||||||
|
|
||||||
|
const themeSelector = (data: Data): Theme => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
|
const { tlstate, useSelector } = useTLDrawContext()
|
||||||
|
|
||||||
|
const theme = useSelector(themeSelector)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.body.classList.add(dark)
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove(dark)
|
||||||
|
}
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: 'light' as Theme,
|
theme,
|
||||||
toggle: () => null,
|
toggle: tlstate.toggleDarkMode,
|
||||||
setTheme: (theme: Theme) => void theme,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,9 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
|
||||||
// For very short lines, draw a point instead of a line
|
// For very short lines, draw a point instead of a line
|
||||||
const bounds = this.getBounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
if (!isEditing && bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2) {
|
const verySmall = bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2
|
||||||
|
|
||||||
|
if (!isEditing && verySmall) {
|
||||||
const sw = strokeWidth * 0.618
|
const sw = strokeWidth * 0.618
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -151,6 +153,14 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
|
||||||
renderIndicator(shape: DrawShape): JSX.Element {
|
renderIndicator(shape: DrawShape): JSX.Element {
|
||||||
const { points } = shape
|
const { points } = shape
|
||||||
|
|
||||||
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
|
const verySmall = bounds.width < 4 && bounds.height < 4
|
||||||
|
|
||||||
|
if (verySmall) {
|
||||||
|
return <circle x={bounds.width / 2} y={bounds.height / 2} r={1} />
|
||||||
|
}
|
||||||
|
|
||||||
const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape))
|
const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape))
|
||||||
|
|
||||||
return <path d={path} />
|
return <path d={path} />
|
||||||
|
|
|
@ -157,17 +157,14 @@ describe('Arrow session', () => {
|
||||||
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
||||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
||||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85])
|
expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85])
|
||||||
// tlstate
|
|
||||||
// .select('target1')
|
|
||||||
// .startTranslateSession([50, 50])
|
|
||||||
// .updateTranslateSession([300, 0])
|
|
||||||
// .completeSession()
|
|
||||||
// expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([66.493, 0])
|
|
||||||
// expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([0, 135])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates the arrow when bound on both sides', () => {
|
it('updates the arrow when bound on both sides', () => {
|
||||||
// TODO
|
// TODO
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('snaps the bend to zero when dragging the bend handle toward the center', () => {
|
||||||
|
// TODO
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -91,8 +91,8 @@ const initialData: Data = {
|
||||||
export class TLDrawState extends StateManager<Data> {
|
export class TLDrawState extends StateManager<Data> {
|
||||||
_onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
|
_onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
|
||||||
|
|
||||||
constructor() {
|
constructor(id = Utils.uniqueId()) {
|
||||||
super(initialData, 'tlstate', 1)
|
super(initialData, id, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectHistory: SelectHistory = {
|
selectHistory: SelectHistory = {
|
||||||
|
@ -258,25 +258,31 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkMode = (): this => {
|
toggleDarkMode = (): this => {
|
||||||
return this.patchState(
|
this.patchState(
|
||||||
{ settings: { isDarkMode: !this.state.settings.isDarkMode } },
|
{ settings: { isDarkMode: !this.state.settings.isDarkMode } },
|
||||||
`settings:toggled_dark_mode`
|
`settings:toggled_dark_mode`
|
||||||
)
|
)
|
||||||
|
this.persist()
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDebugMode = () => {
|
toggleDebugMode = () => {
|
||||||
return this.patchState(
|
this.patchState(
|
||||||
{ settings: { isDebugMode: !this.state.settings.isDebugMode } },
|
{ settings: { isDebugMode: !this.state.settings.isDebugMode } },
|
||||||
`settings:toggled_debug`
|
`settings:toggled_debug`
|
||||||
)
|
)
|
||||||
|
this.persist()
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------- UI ----------------------- */
|
/* ----------------------- UI ----------------------- */
|
||||||
toggleStylePanel = (): this => {
|
toggleStylePanel = (): this => {
|
||||||
return this.patchState(
|
this.patchState(
|
||||||
{ appState: { isStyleOpen: !this.appState.isStyleOpen } },
|
{ appState: { isStyleOpen: !this.appState.isStyleOpen } },
|
||||||
'ui:toggled_style_panel'
|
'ui:toggled_style_panel'
|
||||||
)
|
)
|
||||||
|
this.persist()
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTool = (tool: TLDrawShapeType | 'select'): this => {
|
selectTool = (tool: TLDrawShapeType | 'select'): this => {
|
||||||
|
|
|
@ -99,8 +99,6 @@ const { styled, css, theme, getCssString } = createCss({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const light = theme({})
|
|
||||||
|
|
||||||
const dark = theme({
|
const dark = theme({
|
||||||
colors: {
|
colors: {
|
||||||
brushFill: 'rgba(180, 180, 180, .05)',
|
brushFill: 'rgba(180, 180, 180, .05)',
|
||||||
|
@ -138,4 +136,4 @@ const dark = theme({
|
||||||
|
|
||||||
export default styled
|
export default styled
|
||||||
|
|
||||||
export { css, getCssString, light, dark }
|
export { css, getCssString, dark }
|
||||||
|
|
|
@ -9469,10 +9469,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
hash-base "^3.0.0"
|
hash-base "^3.0.0"
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
|
|
||||||
rko@^0.5.18:
|
rko@^0.5.19:
|
||||||
version "0.5.18"
|
version "0.5.19"
|
||||||
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.18.tgz#cbbc45f073b1db1884112479b18ed04a799065ec"
|
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.19.tgz#33577596167178abc30063b6dd0a8bbde0362c27"
|
||||||
integrity sha512-zh6/NRIZi0gApYMyyJjTLpni/98dmkj/oZhD/KRS2TYZD0V63iIopBWUmD5wSNVVqyOv5o/HHsM1Q4EzdTGeZA==
|
integrity sha512-0KSdDnbhD11GCDvZFARvCJedJuwWIh5F1cCqTGljFF/wQi9PUVu019qH4ME4LdPF3HotMLcdQsxEXmIDeeD0zQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
idb-keyval "^5.1.3"
|
idb-keyval "^5.1.3"
|
||||||
zustand "^3.5.9"
|
zustand "^3.5.9"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue