feat: add recite to error book (#587)

This commit is contained in:
Kaiyi 2023-08-19 02:35:36 +08:00 committed by GitHub
parent d5b9f29eb9
commit 1e66000ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 402 additions and 50 deletions

View File

@ -52,5 +52,6 @@ module.exports = {
rules: {
'sort-imports': ['error', { ignoreDeclarationSort: true }],
'@typescript-eslint/consistent-type-imports': 1,
'react/prop-types': 'off',
},
}

View File

@ -13,6 +13,7 @@
"@radix-ui/react-slider": "^1.1.1",
"canvas-confetti": "^1.6.0",
"classnames": "^2.3.2",
"daisyui": "^3.5.1",
"dayjs": "^1.11.8",
"dexie": "^3.2.3",
"dexie-export-import": "^4.0.7",

View File

@ -14,7 +14,7 @@ const Header: React.FC<PropsWithChildren> = ({ children }) => {
<img src={logo} className="mr-3 h-16 w-16" alt="Qwerty Learner Logo" />
<h1>Qwerty Learner</h1>
</NavLink>
<nav className="card on element flex w-auto content-center items-center justify-end space-x-3 rounded-xl bg-white p-4 transition-colors duration-300 dark:bg-gray-800">
<nav className="my-card on element flex w-auto content-center items-center justify-end space-x-3 rounded-xl bg-white p-4 transition-colors duration-300 dark:bg-gray-800">
{children}
</nav>
</div>

View File

@ -27,7 +27,7 @@ body,
height: 100%;
}
.card {
.my-card {
box-shadow: 0px 100px 80px rgba(50, 46, 129, 0.07), 0px 41.7776px 33.4221px rgba(50, 46, 129, 0.0503198),
0px 22.3363px 17.869px rgba(50, 46, 129, 0.0417275), 0px 12.5216px 10.0172px rgba(50, 46, 129, 0.035),
0px 6.6501px 5.32008px rgba(50, 46, 129, 0.0282725), 0px 2.76726px 2.21381px rgba(50, 46, 129, 0.0196802);

View File

@ -1,22 +1,33 @@
import { LoadingWordUI } from './LoadingWordUI'
import useGetWord from './hooks/useGetWord'
import { currentRowDetailAtom } from './store'
import type { groupedWordRecords } from './type'
import { LoadingUI } from '@/components/Loading'
import { idDictionaryMap } from '@/resources/dictionary'
import { useSetAtom } from 'jotai'
import type { FC } from 'react'
import { useCallback } from 'react'
type IErrorRowProps = {
record: groupedWordRecords
}
const ErrorRow: FC<IErrorRowProps> = ({ record }) => {
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
const dictInfo = idDictionaryMap[record.dict]
const { word, isLoading } = useGetWord(record.word, dictInfo)
const { word, isLoading, hasError } = useGetWord(record.word, dictInfo)
const onClick = useCallback(() => {
setCurrentRowDetail(record)
}, [record, setCurrentRowDetail])
return (
<li className="opacity-85 flex w-full items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white">
<li
className="opacity-85 flex w-full cursor-pointer items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white"
onClick={onClick}
>
<span className="basis-2/12 break-normal">{record.word}</span>
<span className="basis-6/12 break-normal">
{!isLoading && word ? word.trans.join(', ') : <LoadingUI className="h-4 w-4 !border-2" />}
{word ? word.trans.join('') : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}
</span>
<span className="basis-1/12 break-normal">{record.wrongCount}</span>
<span className="basis-2/12 break-normal">{dictInfo.name}</span>

View File

@ -0,0 +1,23 @@
import { LoadingUI } from '@/components/Loading'
import type { FC } from 'react'
import ErrorIcon from '~icons/ic/outline-error'
type LoadingWordUIProps = {
className?: string
isLoading: boolean
hasError: boolean
}
export const LoadingWordUI: FC<LoadingWordUIProps> = ({ className, isLoading, hasError }) => {
return (
<div className={`${className}`}>
{hasError ? (
<div className="tooltip !bg-transparent" data-tip="数据加载失败">
<ErrorIcon className="text-red-500" />
</div>
) : (
isLoading && <LoadingUI />
)}
</div>
)
}

View File

@ -7,11 +7,12 @@ type IPaginationProps = {
className?: string
page: number
setPage: (page: number) => void
totalPages: number
}
export const ITEM_PER_PAGE = 20
const Pagination: FC<IPaginationProps> = ({ className, page, setPage }) => {
const Pagination: FC<IPaginationProps> = ({ className, page, setPage, totalPages }) => {
const nextPage = useCallback(() => {
setPage(page + 1)
}, [page, setPage])
@ -28,7 +29,7 @@ const Pagination: FC<IPaginationProps> = ({ className, page, setPage }) => {
>
<PrevIcon />
</button>
<span className="text-black dark:text-white">{page}</span>
<span className="text-black dark:text-white">{`${page} / ${totalPages}`}</span>
<button
className="cursor-pointer rounded-full bg-white p-2 text-indigo-500 shadow-md dark:bg-gray-800 dark:text-indigo-300"
onClick={nextPage}

View File

@ -0,0 +1,25 @@
import type React from 'react'
interface DataTagProps {
icon: React.ElementType
name: string
data: number | string
}
const DataTag: React.FC<DataTagProps> = ({ icon, name, data }) => {
const IconComponent = icon
return (
<div className="g flex h-10 w-40 flex-1 select-none items-center justify-between rounded-md border-gray-400 bg-gray-100 px-3 py-5 shadow dark:border-gray-600 dark:bg-gray-800">
<div className="flex items-center space-x-1 ">
<IconComponent className="h-4 w-4 text-gray-700 dark:text-gray-300" />
<span className="break-keep text-base font-normal text-gray-500 dark:text-gray-300">{name}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-base font-normal text-gray-800 dark:text-gray-200">{data}</span>
</div>
</div>
)
}
export default DataTag

View File

@ -0,0 +1,83 @@
import { currentRowDetailAtom } from '../store'
import type { groupedWordRecords } from '../type'
import { useAtom } from 'jotai'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useCallback } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import NextIcon from '~icons/ooui/next-ltr'
import PrevIcon from '~icons/ooui/next-rtl'
type IRowPaginationProps = {
className?: string
allRecords: groupedWordRecords[]
}
export const ITEM_PER_PAGE = 20
const RowPagination: FC<IRowPaginationProps> = ({ className, allRecords }) => {
const [currentRowDetail, setCurrentRowDetail] = useAtom(currentRowDetailAtom)
const currentIndex = useMemo(() => {
if (!currentRowDetail) return -1
return allRecords.findIndex((record) => record.word === currentRowDetail.word && record.dict === currentRowDetail.dict)
}, [currentRowDetail, allRecords])
const nextRowDetail = useCallback(() => {
if (!currentRowDetail) return
const index = currentIndex
if (index === -1) return
const nextIndex = index + 1
if (nextIndex >= allRecords.length) return
setCurrentRowDetail(allRecords[nextIndex])
}, [currentRowDetail, currentIndex, allRecords, setCurrentRowDetail])
const prevRowDetail = useCallback(() => {
if (!currentRowDetail) return
const index = currentIndex
if (index === -1) return
const prevIndex = index - 1
if (prevIndex < 0) return
setCurrentRowDetail(allRecords[prevIndex])
}, [currentRowDetail, currentIndex, setCurrentRowDetail, allRecords])
useHotkeys(
'left',
(e) => {
prevRowDetail()
e.stopPropagation()
},
{
preventDefault: true,
},
)
useHotkeys(
'right',
(e) => {
nextRowDetail()
e.stopPropagation()
},
{
preventDefault: true,
},
)
return (
<div className={`-gap-1 flex select-none items-center ${className}`}>
<button
className="d cursor-pointer rounded-full p-1 text-indigo-500 focus:outline-none dark:text-indigo-300"
onClick={prevRowDetail}
>
<PrevIcon />
</button>
<span className="text-sm text-black dark:text-white">{`${currentIndex + 1} / ${allRecords.length}`}</span>
<button className="cursor-pointer rounded-full p-1 text-indigo-500 focus:outline-none dark:text-indigo-300" onClick={nextRowDetail}>
<NextIcon />
</button>
</div>
)
}
export default RowPagination

View File

@ -0,0 +1,96 @@
/* eslint-disable react/prop-types */
import { LoadingWordUI } from '../LoadingWordUI'
import useGetWord from '../hooks/useGetWord'
import { currentRowDetailAtom } from '../store'
import type { groupedWordRecords } from '../type'
import DataTag from './DataTag'
import RowPagination from './RowPagination'
import Phonetic from '@/pages/Typing/components/WordPanel/components/Phonetic'
import Letter from '@/pages/Typing/components/WordPanel/components/Word/Letter'
import { idDictionaryMap } from '@/resources/dictionary'
import { useSetAtom } from 'jotai'
import { useCallback, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import HashtagIcon from '~icons/heroicons/chart-pie-20-solid'
import CheckCircle from '~icons/heroicons/check-circle-20-solid'
import ClockIcon from '~icons/heroicons/clock-20-solid'
import XCircle from '~icons/heroicons/x-circle-20-solid'
import IconX from '~icons/tabler/x'
type RowDetailProps = {
currentRowDetail: groupedWordRecords
allRecords: groupedWordRecords[]
}
const RowDetail: React.FC<RowDetailProps> = ({ currentRowDetail, allRecords }) => {
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
const dictInfo = idDictionaryMap[currentRowDetail.dict]
const { word, isLoading, hasError } = useGetWord(currentRowDetail.word, dictInfo)
const rowDetailData: RowDetailData = useMemo(() => {
const time =
currentRowDetail.records.length > 0
? currentRowDetail.records.reduce((acc, cur) => acc + cur.totalTime, 0) / currentRowDetail.records.length
: 0
const timeStr = (time / 1000).toFixed(2)
const correctCount = currentRowDetail.records.length
const wrongCount = currentRowDetail.wrongCount
const sumCount = correctCount + wrongCount
return { time: timeStr, sumCount, correctCount, wrongCount }
}, [currentRowDetail.records, currentRowDetail.wrongCount])
const onClose = useCallback(() => {
setCurrentRowDetail(null)
}, [setCurrentRowDetail])
useHotkeys(
'esc',
(e) => {
onClose()
e.stopPropagation()
},
{ preventDefault: true },
)
return (
<div className="absolute inset-0 flex flex-col items-center justify-center ">
<div className="my-card relative z-10 flex h-[32rem] min-w-[26rem] select-text flex-col items-center justify-around rounded-2xl bg-white px-3 py-10 dark:bg-gray-900">
<IconX className="absolute right-3 top-3 h-6 w-6 cursor-pointer text-gray-400" onClick={onClose} />
<div className="flex flex-col items-center justify-start">
<div>
{currentRowDetail.word.split('').map((t, index) => (
<Letter key={`${index}-${t}`} letter={t} visible state="normal" />
))}
</div>
<div>{word ? <Phonetic word={word} /> : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}</div>
<div className="flex max-w-[24rem] items-center">
<span className={`max-w-4xl text-center font-sans transition-colors duration-300 dark:text-white dark:text-opacity-80`}>
{word ? word.trans.join('') : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}
</span>
</div>
</div>
<div className="item flex flex-col gap-4">
<div className="flex gap-6">
<DataTag icon={ClockIcon} name="平均用时" data={rowDetailData.time} />
<DataTag icon={HashtagIcon} name="练习次数" data={rowDetailData.sumCount} />
</div>
<div className="flex gap-6">
<DataTag icon={CheckCircle} name="正确次数" data={rowDetailData.correctCount} />
<DataTag icon={XCircle} name="错误次数" data={rowDetailData.wrongCount} />
</div>
</div>
<RowPagination className="absolute bottom-6 mt-10" allRecords={allRecords} />
</div>
<div className="absolute inset-0 z-0 cursor-pointer bg-transparent" onClick={onClose}></div>
</div>
)
}
type RowDetailData = {
time: string
sumCount: number
correctCount: number
wrongCount: number
}
export default RowDetail

View File

@ -1,16 +1,27 @@
import type { Dictionary, Word } from '@/typings'
import { wordListFetcher } from '@/utils/wordListFetcher'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
export default function useGetWord(name: string, dict: Dictionary) {
const { data: wordList, error, isLoading } = useSWR(dict.url, wordListFetcher)
const { data: wordList, error, isLoading } = useSWR(dict?.url, wordListFetcher)
const [hasError, setHasError] = useState(false)
const word: Word | undefined = useMemo(() => {
if (!wordList) return undefined
return wordList.find((word) => word.name === name)
const word = wordList.find((word) => word.name === name)
if (word) {
return word
} else {
setHasError(true)
return undefined
}
}, [wordList, name])
return { word, isLoading, error }
useEffect(() => {
if (error) setHasError(true)
}, [error])
return { word, isLoading, hasError }
}

View File

@ -2,9 +2,13 @@ import ErrorRow from './ErrorRow'
import type { ISortType } from './HeadWrongNumber'
import HeadWrongNumber from './HeadWrongNumber'
import Pagination, { ITEM_PER_PAGE } from './Pagination'
import RowDetail from './RowDetail'
import { currentRowDetailAtom } from './store'
import type { groupedWordRecords } from './type'
import { db } from '@/utils/db'
import type { WordRecord } from '@/utils/db/record'
import * as ScrollArea from '@radix-ui/react-scroll-area'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import IconX from '~icons/tabler/x'
@ -15,6 +19,7 @@ export function ErrorBook() {
const totalPages = useMemo(() => Math.ceil(groupedRecords.length / ITEM_PER_PAGE), [groupedRecords.length])
const [sortType, setSortType] = useState<ISortType>('asc')
const navigate = useNavigate()
const currentRowDetail = useAtomValue(currentRowDetailAtom)
const onBack = useCallback(() => {
navigate('/')
@ -47,6 +52,12 @@ export function ErrorBook() {
})
}, [groupedRecords, sortType])
const renderRecords = useMemo(() => {
const start = (currentPage - 1) * ITEM_PER_PAGE
const end = start + ITEM_PER_PAGE
return sortedRecords.slice(start, end)
}, [currentPage, sortedRecords])
useEffect(() => {
db.wordRecords
.where('wrongCount')
@ -61,7 +72,7 @@ export function ErrorBook() {
group = { word: record.word, dict: record.dict, records: [], wrongCount: 0 }
groups.push(group)
}
group.records.push(record)
group.records.push(record as WordRecord)
})
groups.forEach((group) => {
@ -75,36 +86,37 @@ export function ErrorBook() {
})
}, [])
const renderRecords = useMemo(() => {
const start = (currentPage - 1) * ITEM_PER_PAGE
const end = start + ITEM_PER_PAGE
return sortedRecords.slice(start, end)
}, [currentPage, sortedRecords])
return (
<div className="flex h-screen w-full flex-col items-center pb-4">
<IconX className="absolute right-10 top-5 mr-2 h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
<div className="flex w-full flex-1 select-text items-start justify-center overflow-hidden">
<div className="flex h-full w-5/6 flex-col pt-10">
<div className="flex w-full justify-between rounded-lg bg-white px-6 py-5 text-lg text-black shadow-lg dark:bg-gray-800 dark:text-white">
<span className="basis-2/12"></span>
<span className="basis-6/12"></span>
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
<span className="basis-2/12"></span>
</div>
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
<ScrollArea.Viewport className="h-full ">
<div className="flex flex-col gap-3">
{renderRecords.map((record) => (
<ErrorRow key={`${record.dict}-${record.word}`} record={record} />
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent" orientation="vertical"></ScrollArea.Scrollbar>
</ScrollArea.Root>
<>
<div className={`relative flex h-screen w-full flex-col items-center pb-4 ease-in ${currentRowDetail && 'blur-sm'}`}>
<div className="mr-8 mt-4 flex w-auto items-center justify-center self-end">
<h1 className="font-lighter mr-4 w-auto self-end text-gray-500 opacity-70">Tip: 点击错误单词查看详细信息 </h1>
<IconX className="h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
</div>
<div className="flex w-full flex-1 select-text items-start justify-center overflow-hidden">
<div className="flex h-full w-5/6 flex-col pt-10">
<div className="flex w-full justify-between rounded-lg bg-white px-6 py-5 text-lg text-black shadow-lg dark:bg-gray-800 dark:text-white">
<span className="basis-2/12"></span>
<span className="basis-6/12"></span>
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
<span className="basis-2/12"></span>
</div>
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
<ScrollArea.Viewport className="h-full ">
<div className="flex flex-col gap-3">
{renderRecords.map((record) => (
<ErrorRow key={`${record.dict}-${record.word}`} record={record} />
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent" orientation="vertical"></ScrollArea.Scrollbar>
</ScrollArea.Root>
</div>
</div>
<Pagination className="pt-3" page={currentPage} setPage={setPage} totalPages={totalPages} />
</div>
<Pagination className="pt-3" page={currentPage} setPage={setPage} />
</div>
{currentRowDetail && <RowDetail currentRowDetail={currentRowDetail} allRecords={sortedRecords} />}
</>
)
}

View File

@ -0,0 +1,4 @@
import type { groupedWordRecords } from '../type'
import { atom } from 'jotai'
export const currentRowDetailAtom = atom<groupedWordRecords | null>(null)

View File

@ -1,8 +1,8 @@
import type { IWordRecord } from '@/utils/db/record'
import type { WordRecord } from '@/utils/db/record'
export type groupedWordRecords = {
word: string
dict: string
records: IWordRecord[]
records: WordRecord[]
wrongCount: number
}

View File

@ -180,7 +180,7 @@ const ResultScreen = () => {
leaveTo="opacity-0"
>
<div className="flex h-screen items-center justify-center">
<div className="card fixed flex w-[90vw] max-w-6xl flex-col overflow-hidden rounded-3xl bg-white pb-14 pl-10 pr-5 pt-10 shadow-lg dark:bg-gray-800 md:w-4/5 lg:w-3/5">
<div className="my-card fixed flex w-[90vw] max-w-6xl flex-col overflow-hidden rounded-3xl bg-white pb-14 pl-10 pr-5 pt-10 shadow-lg dark:bg-gray-800 md:w-4/5 lg:w-3/5">
<div className="text-center font-sans text-xl font-normal text-gray-900 dark:text-gray-400 md:text-2xl">
{`${currentDictInfo.name}${currentChapter + 1}`}
</div>
@ -268,7 +268,7 @@ const ResultScreen = () => {
{!isLastChapter && (
<Tooltip content="快捷键enter">
<button
className={`btn-primary { isLastChapter ? 'cursor-not-allowed opacity-50' : ''} h-12 text-base font-bold `}
className={`{ isLastChapter ? 'cursor-not-allowed opacity-50' : ''} btn-primary h-12 text-base font-bold `}
type="button"
onClick={nextButtonHandler}
title="下一章节"

View File

@ -12,7 +12,7 @@ export default function Speed() {
const inputNumber = state.chapterData.correctCount + state.chapterData.wrongCount
return (
<div className="card flex w-3/5 rounded-xl bg-white p-4 py-10 opacity-50 transition-colors duration-300 dark:bg-gray-800">
<div className="my-card flex w-3/5 rounded-xl bg-white p-4 py-10 opacity-50 transition-colors duration-300 dark:bg-gray-800">
<InfoBox info={`${minutesString}:${secondsString}`} description="时间" />
<InfoBox info={inputNumber + ''} description="输入数" />
<InfoBox info={state.timerData.wpm + ''} description="WPM" />

View File

@ -1,9 +1,9 @@
import { isTextSelectableAtom, phoneticConfigAtom } from '@/store'
import type { WordWithIndex } from '@/typings'
import type { Word, WordWithIndex } from '@/typings'
import { useAtomValue } from 'jotai'
export type PhoneticProps = {
word: WordWithIndex
word: WordWithIndex | Word
}
function Phonetic({ word }: PhoneticProps) {

View File

@ -55,5 +55,23 @@ module.exports = {
backgroundOpacity: ['dark'],
},
},
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms')],
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms'), require('daisyui')],
daisyui: {
themes: [
{
mytheme: {
primary: '#6366f1',
secondary: '#7dd3fc',
accent: '#cc8316',
neutral: '#272735',
'base-100': '#f0eff1',
info: '#f3f4f6',
success: '#6fe7ab',
warning: '#d6920a',
error: '#f43f5e',
},
},
'dark',
],
},
}

View File

@ -2539,6 +2539,11 @@ color@^4.0.1:
color-convert "^2.0.1"
color-string "^1.9.0"
colord@^2.9:
version "2.9.3"
resolved "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
colorette@^2.0.19:
version "2.0.20"
resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz"
@ -2640,6 +2645,14 @@ css-color-names@^0.0.4:
resolved "https://registry.npmmirror.com/css-color-names/-/css-color-names-0.0.4.tgz"
integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==
css-selector-tokenizer@^0.8:
version "0.8.0"
resolved "https://registry.npmmirror.com/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz#88267ef6238e64f2215ea2764b3e2cf498b845dd"
integrity sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==
dependencies:
cssesc "^3.0.0"
fastparse "^1.1.2"
css-unit-converter@^1.1.1:
version "1.1.2"
resolved "https://registry.npmmirror.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz"
@ -2655,6 +2668,17 @@ csstype@^3.0.2:
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
daisyui@^3.5.1:
version "3.5.1"
resolved "https://registry.npmmirror.com/daisyui/-/daisyui-3.5.1.tgz#7b53668e2b68007b275fec88268f81b4aeaf3781"
integrity sha512-7GG+9QXnr2qQMCqnyFU8TxpaOYJigXiEtmzoivmiiZZHvxqIwYdaMAkgivqTVxEgy3Hot3m1suzZjmt1zUrvmA==
dependencies:
colord "^2.9"
css-selector-tokenizer "^0.8"
postcss "^8"
postcss-js "^4"
tailwindcss "^3"
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
@ -3289,6 +3313,11 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fastparse@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
fastq@^1.6.0:
version "1.15.0"
resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz"
@ -4624,7 +4653,7 @@ postcss-js@^2:
camelcase-css "^2.0.1"
postcss "^7.0.18"
postcss-js@^4.0.1:
postcss-js@^4, postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
@ -4718,6 +4747,15 @@ postcss@^7, postcss@^7.0.18, postcss@^7.0.32:
picocolors "^0.2.1"
source-map "^0.6.1"
postcss@^8:
version "8.4.28"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.0.0, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23:
version "8.4.23"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz"
@ -5397,6 +5435,34 @@ tabbable@^6.0.1:
resolved "https://registry.npmmirror.com/tabbable/-/tabbable-6.1.2.tgz"
integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==
tailwindcss@^3:
version "3.3.3"
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.18.2"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.4.23"
postcss-import "^15.1.0"
postcss-js "^4.0.1"
postcss-load-config "^4.0.1"
postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
resolve "^1.22.2"
sucrase "^3.32.0"
tailwindcss@^3.3.1:
version "3.3.2"
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.2.tgz"