mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
refactor(web): useClipboard hook to reduce duplication (#31308)
Signed-off-by: SherlockShemol <shemol@163.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
This commit is contained in:
@ -3,10 +3,8 @@ import {
|
|||||||
RiClipboardFill,
|
RiClipboardFill,
|
||||||
RiClipboardLine,
|
RiClipboardLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import copy from 'copy-to-clipboard'
|
import { useClipboard } from 'foxact/use-clipboard'
|
||||||
import { debounce } from 'es-toolkit/compat'
|
import { useCallback } from 'react'
|
||||||
import * as React from 'react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
@ -21,32 +19,27 @@ const prefixEmbedded = 'overview.appInfo.embedded'
|
|||||||
|
|
||||||
const CopyFeedback = ({ content }: Props) => {
|
const CopyFeedback = ({ content }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
const { copied, copy, reset } = useClipboard()
|
||||||
|
|
||||||
const onClickCopy = debounce(() => {
|
const handleCopy = useCallback(() => {
|
||||||
copy(content)
|
copy(content)
|
||||||
setIsCopied(true)
|
}, [copy, content])
|
||||||
}, 100)
|
|
||||||
|
|
||||||
const onMouseLeave = debounce(() => {
|
|
||||||
setIsCopied(false)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={
|
popupContent={
|
||||||
(isCopied
|
(copied
|
||||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ActionButton>
|
<ActionButton>
|
||||||
<div
|
<div
|
||||||
onClick={onClickCopy}
|
onClick={handleCopy}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={reset}
|
||||||
>
|
>
|
||||||
{isCopied && <RiClipboardFill className="h-4 w-4" />}
|
{copied && <RiClipboardFill className="h-4 w-4" />}
|
||||||
{!isCopied && <RiClipboardLine className="h-4 w-4" />}
|
{!copied && <RiClipboardLine className="h-4 w-4" />}
|
||||||
</div>
|
</div>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -57,21 +50,16 @@ export default CopyFeedback
|
|||||||
|
|
||||||
export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
|
export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
const { copied, copy, reset } = useClipboard()
|
||||||
|
|
||||||
const onClickCopy = debounce(() => {
|
const handleCopy = useCallback(() => {
|
||||||
copy(content)
|
copy(content)
|
||||||
setIsCopied(true)
|
}, [copy, content])
|
||||||
}, 100)
|
|
||||||
|
|
||||||
const onMouseLeave = debounce(() => {
|
|
||||||
setIsCopied(false)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={
|
popupContent={
|
||||||
(isCopied
|
(copied
|
||||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||||
}
|
}
|
||||||
@ -81,9 +69,9 @@ export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className'
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={onClickCopy}
|
onClick={handleCopy}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={reset}
|
||||||
className={`h-full w-full ${copyStyle.copyIcon} ${isCopied ? copyStyle.copied : ''
|
className={`h-full w-full ${copyStyle.copyIcon} ${copied ? copyStyle.copied : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import copy from 'copy-to-clipboard'
|
import { useClipboard } from 'foxact/use-clipboard'
|
||||||
import { debounce } from 'es-toolkit/compat'
|
import { useCallback } from 'react'
|
||||||
import * as React from 'react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Copy,
|
Copy,
|
||||||
@ -18,29 +16,24 @@ const prefixEmbedded = 'overview.appInfo.embedded'
|
|||||||
|
|
||||||
const CopyIcon = ({ content }: Props) => {
|
const CopyIcon = ({ content }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
const { copied, copy, reset } = useClipboard()
|
||||||
|
|
||||||
const onClickCopy = debounce(() => {
|
const handleCopy = useCallback(() => {
|
||||||
copy(content)
|
copy(content)
|
||||||
setIsCopied(true)
|
}, [copy, content])
|
||||||
}, 100)
|
|
||||||
|
|
||||||
const onMouseLeave = debounce(() => {
|
|
||||||
setIsCopied(false)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={
|
popupContent={
|
||||||
(isCopied
|
(copied
|
||||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div onMouseLeave={onMouseLeave}>
|
<div onMouseLeave={reset}>
|
||||||
{!isCopied
|
{!copied
|
||||||
? (
|
? (
|
||||||
<Copy className="mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={onClickCopy} />
|
<Copy className="mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={handleCopy} />
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<CopyCheck className="mx-1 h-3.5 w-3.5 text-text-tertiary" />
|
<CopyCheck className="mx-1 h-3.5 w-3.5 text-text-tertiary" />
|
||||||
|
|||||||
@ -3,13 +3,8 @@ import * as React from 'react'
|
|||||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||||
import InputWithCopy from './index'
|
import InputWithCopy from './index'
|
||||||
|
|
||||||
// Create a mock function that we can track using vi.hoisted
|
// Mock navigator.clipboard for foxact/use-clipboard
|
||||||
const mockCopyToClipboard = vi.hoisted(() => vi.fn(() => true))
|
const mockWriteText = vi.fn(() => Promise.resolve())
|
||||||
|
|
||||||
// Mock the copy-to-clipboard library
|
|
||||||
vi.mock('copy-to-clipboard', () => ({
|
|
||||||
default: mockCopyToClipboard,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock the i18n hook with custom translations for test assertions
|
// Mock the i18n hook with custom translations for test assertions
|
||||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||||
@ -19,15 +14,16 @@ vi.mock('react-i18next', () => createReactI18nextMock({
|
|||||||
'overview.appInfo.embedded.copied': 'Copied',
|
'overview.appInfo.embedded.copied': 'Copied',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock es-toolkit/compat debounce
|
|
||||||
vi.mock('es-toolkit/compat', () => ({
|
|
||||||
debounce: (fn: any) => fn,
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('InputWithCopy component', () => {
|
describe('InputWithCopy component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockCopyToClipboard.mockClear()
|
mockWriteText.mockClear()
|
||||||
|
// Setup navigator.clipboard mock
|
||||||
|
Object.assign(navigator, {
|
||||||
|
clipboard: {
|
||||||
|
writeText: mockWriteText,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders correctly with default props', () => {
|
it('renders correctly with default props', () => {
|
||||||
@ -55,7 +51,9 @@ describe('InputWithCopy component', () => {
|
|||||||
const copyButton = screen.getByRole('button')
|
const copyButton = screen.getByRole('button')
|
||||||
fireEvent.click(copyButton)
|
fireEvent.click(copyButton)
|
||||||
|
|
||||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('test value')
|
await waitFor(() => {
|
||||||
|
expect(mockWriteText).toHaveBeenCalledWith('test value')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('copies custom value when copyValue prop is provided', async () => {
|
it('copies custom value when copyValue prop is provided', async () => {
|
||||||
@ -65,7 +63,9 @@ describe('InputWithCopy component', () => {
|
|||||||
const copyButton = screen.getByRole('button')
|
const copyButton = screen.getByRole('button')
|
||||||
fireEvent.click(copyButton)
|
fireEvent.click(copyButton)
|
||||||
|
|
||||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('custom copy value')
|
await waitFor(() => {
|
||||||
|
expect(mockWriteText).toHaveBeenCalledWith('custom copy value')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls onCopy callback when copy button is clicked', async () => {
|
it('calls onCopy callback when copy button is clicked', async () => {
|
||||||
@ -76,7 +76,9 @@ describe('InputWithCopy component', () => {
|
|||||||
const copyButton = screen.getByRole('button')
|
const copyButton = screen.getByRole('button')
|
||||||
fireEvent.click(copyButton)
|
fireEvent.click(copyButton)
|
||||||
|
|
||||||
expect(onCopyMock).toHaveBeenCalledWith('test value')
|
await waitFor(() => {
|
||||||
|
expect(onCopyMock).toHaveBeenCalledWith('test value')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows copied state after successful copy', async () => {
|
it('shows copied state after successful copy', async () => {
|
||||||
@ -115,17 +117,19 @@ describe('InputWithCopy component', () => {
|
|||||||
expect(input).toHaveClass('custom-class')
|
expect(input).toHaveClass('custom-class')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles empty value correctly', () => {
|
it('handles empty value correctly', async () => {
|
||||||
const mockOnChange = vi.fn()
|
const mockOnChange = vi.fn()
|
||||||
render(<InputWithCopy value="" onChange={mockOnChange} />)
|
render(<InputWithCopy value="" onChange={mockOnChange} />)
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByDisplayValue('')
|
||||||
const copyButton = screen.getByRole('button')
|
const copyButton = screen.getByRole('button')
|
||||||
|
|
||||||
expect(input).toBeInTheDocument()
|
expect(input).toBeInTheDocument()
|
||||||
expect(copyButton).toBeInTheDocument()
|
expect(copyButton).toBeInTheDocument()
|
||||||
|
|
||||||
fireEvent.click(copyButton)
|
fireEvent.click(copyButton)
|
||||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('')
|
await waitFor(() => {
|
||||||
|
expect(mockWriteText).toHaveBeenCalledWith('')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maintains focus on input after copy', async () => {
|
it('maintains focus on input after copy', async () => {
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { InputProps } from '../input'
|
import type { InputProps } from '../input'
|
||||||
import { RiClipboardFill, RiClipboardLine } from '@remixicon/react'
|
import { RiClipboardFill, RiClipboardLine } from '@remixicon/react'
|
||||||
import copy from 'copy-to-clipboard'
|
import { useClipboard } from 'foxact/use-clipboard'
|
||||||
import { debounce } from 'es-toolkit/compat'
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import ActionButton from '../action-button'
|
import ActionButton from '../action-button'
|
||||||
@ -30,31 +28,16 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
|
||||||
// Determine what value to copy
|
// Determine what value to copy
|
||||||
const valueToString = typeof value === 'string' ? value : String(value || '')
|
const valueToString = typeof value === 'string' ? value : String(value || '')
|
||||||
const finalCopyValue = copyValue || valueToString
|
const finalCopyValue = copyValue || valueToString
|
||||||
|
|
||||||
const onClickCopy = debounce(() => {
|
const { copied, copy, reset } = useClipboard()
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
copy(finalCopyValue)
|
copy(finalCopyValue)
|
||||||
setIsCopied(true)
|
|
||||||
onCopy?.(finalCopyValue)
|
onCopy?.(finalCopyValue)
|
||||||
}, 100)
|
}
|
||||||
|
|
||||||
const onMouseLeave = debounce(() => {
|
|
||||||
setIsCopied(false)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isCopied) {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setIsCopied(false)
|
|
||||||
}, 2000)
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isCopied])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative w-full', wrapperClassName)}>
|
<div className={cn('relative w-full', wrapperClassName)}>
|
||||||
@ -73,21 +56,21 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
|
|||||||
{showCopyButton && (
|
{showCopyButton && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2"
|
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={reset}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={
|
popupContent={
|
||||||
(isCopied
|
(copied
|
||||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onClickCopy}
|
onClick={handleCopy}
|
||||||
className="hover:bg-components-button-ghost-bg-hover"
|
className="hover:bg-components-button-ghost-bg-hover"
|
||||||
>
|
>
|
||||||
{isCopied
|
{copied
|
||||||
? (
|
? (
|
||||||
<RiClipboardFill className="h-3.5 w-3.5 text-text-tertiary" />
|
<RiClipboardFill className="h-3.5 w-3.5 text-text-tertiary" />
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1131,11 +1131,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/input-with-copy/index.spec.tsx": {
|
|
||||||
"ts/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app/components/base/input/index.spec.tsx": {
|
"app/components/base/input/index.spec.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@ -97,6 +97,7 @@
|
|||||||
"emoji-mart": "5.6.0",
|
"emoji-mart": "5.6.0",
|
||||||
"es-toolkit": "1.43.0",
|
"es-toolkit": "1.43.0",
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
|
"foxact": "0.2.52",
|
||||||
"html-entities": "2.6.0",
|
"html-entities": "2.6.0",
|
||||||
"html-to-image": "1.11.13",
|
"html-to-image": "1.11.13",
|
||||||
"i18next": "25.7.3",
|
"i18next": "25.7.3",
|
||||||
|
|||||||
27
web/pnpm-lock.yaml
generated
27
web/pnpm-lock.yaml
generated
@ -183,6 +183,9 @@ importers:
|
|||||||
fast-deep-equal:
|
fast-deep-equal:
|
||||||
specifier: 3.1.3
|
specifier: 3.1.3
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
|
foxact:
|
||||||
|
specifier: 0.2.52
|
||||||
|
version: 0.2.52(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
html-entities:
|
html-entities:
|
||||||
specifier: 2.6.0
|
specifier: 2.6.0
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
@ -5578,6 +5581,17 @@ packages:
|
|||||||
engines: {node: '>=18.3.0'}
|
engines: {node: '>=18.3.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
foxact@0.2.52:
|
||||||
|
resolution: {integrity: sha512-cc3ydJkM/mYkof1/ofI4VlVAiRyfsSDsHRC4UIAXQcnUXCuo0rXM66Zy1ggdxAXL03ikHnh3bPnQ7AYuI/Yzow==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
react-dom: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fraction.js@4.3.7:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
@ -7685,6 +7699,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
|
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
server-only@0.0.1:
|
||||||
|
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
|
||||||
|
|
||||||
serwist@9.5.0:
|
serwist@9.5.0:
|
||||||
resolution: {integrity: sha512-wjrsPWHI5ZM20jIsVKZGN/uAdS2aKOgmIOE4dqUaFhK6SVIzgoJZjTnZ3v29T+NmneuD753jlhGui9eYypsj0A==}
|
resolution: {integrity: sha512-wjrsPWHI5ZM20jIsVKZGN/uAdS2aKOgmIOE4dqUaFhK6SVIzgoJZjTnZ3v29T+NmneuD753jlhGui9eYypsj0A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -14421,6 +14438,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fd-package-json: 2.0.0
|
fd-package-json: 2.0.0
|
||||||
|
|
||||||
|
foxact@0.2.52(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
|
dependencies:
|
||||||
|
client-only: 0.0.1
|
||||||
|
server-only: 0.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
fs-constants@1.0.0:
|
fs-constants@1.0.0:
|
||||||
@ -17035,6 +17060,8 @@ snapshots:
|
|||||||
|
|
||||||
seroval@1.3.2: {}
|
seroval@1.3.2: {}
|
||||||
|
|
||||||
|
server-only@0.0.1: {}
|
||||||
|
|
||||||
serwist@9.5.0(typescript@5.9.3):
|
serwist@9.5.0(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@serwist/utils': 9.5.0
|
'@serwist/utils': 9.5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user