diff --git a/apps/web/index.html b/apps/web/index.html index d738c2c..6ad2888 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -12,7 +12,7 @@ diff --git a/apps/web/package.json b/apps/web/package.json index f8531cf..09153ed 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,12 +23,14 @@ "clsx": "^2.1.1", "idb-keyval": "^6.2.1", "jotai": "^2.12.0", + "jotai-effect": "^2.0.1", "jotai-optics": "^0.4.0", "lucide-react": "^0.475.0", "optics-ts": "^2.4.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2" + "react-hook-form": "^7.54.2", + "react-hotkeys-hook": "^4.6.1" }, "devDependencies": { "@lingui/cli": "^5.2.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index abe743c..1a11a7b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,34 +1,58 @@ -import { Trans } from '@lingui/react/macro'; -import { TooltipProvider } from '@workspace/ui/components/tooltip'; -import { siteConfig } from '@/config/site'; -import { I18nProvider } from '@/components/I18nProvider'; +import { Suspense } from 'react'; +import { LoaderCircle } from 'lucide-react'; +import { Separator } from '@workspace/ui/components/separator'; +import { AppProvider } from '@/components/AppProvider'; import { ChangeTheme } from '@/components/ChangeTheme'; -import { Editor } from '@/components/Editor'; +import { ChangeWaveFunction } from '@/components/ChangeWaveFunction'; +import { CopyImage } from '@/components/CopyImage'; +import { ExportImage } from '@/components/ExportImage'; +import { Graphics } from '@/components/Graphics'; +import { ImportImages } from '@/components/ImportImages'; +import { NoImages } from '@/components/NoImages'; +import { OptimizeWaveform } from '@/components/OptimizeWaveform'; +import { RemoveImages } from '@/components/RemoveImages'; +import { ReverseImages } from '@/components/ReverseImages'; +import { Waveform } from '@/components/Waveform'; export function App() { return ( - - -
- + +
+
+
+
+
+ + + + + + + + + +
+
+ +
+ +
+ } + > + } + /> +
-
+
); } diff --git a/apps/web/src/atoms/importAtoms.ts b/apps/web/src/atoms/importAtoms.ts index 08d1240..aa5f9be 100644 --- a/apps/web/src/atoms/importAtoms.ts +++ b/apps/web/src/atoms/importAtoms.ts @@ -10,7 +10,7 @@ const importAbortControllerAtom = atom(null); export const importSignalAtom = atom( get => get(importAbortControllerAtom)?.signal, - (get, set, refresh: boolean) => { + (get, set, refresh?: boolean) => { get(importAbortControllerAtom)?.abort(); set(importAbortControllerAtom, refresh ? new AbortController() : null); } diff --git a/apps/web/src/atoms/largestImageAtom.ts b/apps/web/src/atoms/largestImageAtom.ts index b982cb9..46196df 100644 --- a/apps/web/src/atoms/largestImageAtom.ts +++ b/apps/web/src/atoms/largestImageAtom.ts @@ -4,12 +4,10 @@ import { unwrap } from 'jotai/utils'; export const largestImageAtom = atom(async get => { const images = await get(imagesAtom); - const imagesByArea = [...images].sort( - (a, b) => a.width * a.height - b.width * b.height - ); - return imagesByArea.length - ? imagesByArea[imagesByArea.length - 1] - : undefined; + const largestImage = [...images] + .sort((a, b) => a.width * a.height - b.width * b.height) + .pop(); + return largestImage; }); export const unwrappedLargestImageAtom = unwrap(largestImageAtom); diff --git a/apps/web/src/atoms/localeAtom.ts b/apps/web/src/atoms/localeAtom.ts deleted file mode 100644 index bf1433e..0000000 --- a/apps/web/src/atoms/localeAtom.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from 'jotai'; - -export const localeAtom = atom('en'); diff --git a/apps/web/src/atoms/localeAtoms.ts b/apps/web/src/atoms/localeAtoms.ts new file mode 100644 index 0000000..54f4199 --- /dev/null +++ b/apps/web/src/atoms/localeAtoms.ts @@ -0,0 +1,22 @@ +import { atom } from 'jotai'; +import { observe } from 'jotai-effect'; +import { atomWithStorage } from 'jotai/utils'; +import { setupI18n } from '@lingui/core'; +import { messages as enMessages } from '@/locales/en'; + +export const i18nAtom = atom( + setupI18n({ + messages: { + en: enMessages + } + }) +); + +export const localeAtom = atomWithStorage('locale', 'en', undefined, { + getOnInit: true +}); + +observe(get => { + const i18n = get.peek(i18nAtom); + i18n.activate(get(localeAtom)); +}); diff --git a/apps/web/src/atoms/waveAtom.ts b/apps/web/src/atoms/waveAtom.ts index 19d06a0..4a2b591 100644 --- a/apps/web/src/atoms/waveAtom.ts +++ b/apps/web/src/atoms/waveAtom.ts @@ -25,14 +25,14 @@ export const waveAtom = atom(get => { const { width, height } = largestImage; const halfHeight = height / 2; - const ys = [halfHeight]; - let py = halfHeight; + let prevY = halfHeight; + const ys = [prevY]; for (let x = 0; x <= width; x++) { const y = amplitude * waveFunction((2 * Math.PI * x) / wavelength) + halfHeight; - ys.push(round(y - py)); - py = y; + ys.push(round(y - prevY)); + prevY = y; } return `m0 ${ys.join(' 1 ')} V0H0Z`; diff --git a/apps/web/src/atoms/waveformAtoms.ts b/apps/web/src/atoms/waveformAtoms.ts index 4d1686b..e3d3993 100644 --- a/apps/web/src/atoms/waveformAtoms.ts +++ b/apps/web/src/atoms/waveformAtoms.ts @@ -1,45 +1,92 @@ import { atom } from 'jotai'; import { focusAtom } from 'jotai-optics'; import { atomWithStorage } from 'jotai/utils'; +import { createCountReducer } from '@/utils/createCountReducer'; +import { reducerAtom } from '@/utils/reducerAtom'; +const STEP_PERCENTAGE = 0.01; + +export const MIN_ROTATION = 0; +export const MAX_ROTATION = 360; export const MIN_WAVELENGTH = 9; export const MIN_AMPLITUDE = 0; export interface Waveform { rotation: number; wavelength: number; - wavelengthMax?: number; + wavelengthMax: number; amplitude: number; - amplitudeMax?: number; + amplitudeMax: number; } export const waveformAtom = atomWithStorage( 'waveform', - { rotation: 0, wavelength: MIN_WAVELENGTH, amplitude: MIN_AMPLITUDE }, + { + rotation: 0, + wavelength: MIN_WAVELENGTH, + amplitude: MIN_AMPLITUDE, + wavelengthMax: MIN_WAVELENGTH + 1, + amplitudeMax: MIN_AMPLITUDE + 1 + }, undefined, { getOnInit: true } ); -export const rotationAtom = focusAtom(waveformAtom, optic => +// Rotation + +export const baseRotationAtom = focusAtom(waveformAtom, optic => optic.prop('rotation') ); -export const wavelengthAtom = focusAtom(waveformAtom, optic => - optic.prop('wavelength') +export const rotationAtom = reducerAtom( + baseRotationAtom, + createCountReducer({ + min: MIN_ROTATION, + max: MAX_ROTATION, + step: Math.abs(MAX_ROTATION - MIN_ROTATION) * STEP_PERCENTAGE + }) ); -export const amplitudeAtom = focusAtom(waveformAtom, optic => - optic.prop('amplitude') +// Wavelength + +export const baseWavelengthAtom = focusAtom(waveformAtom, optic => + optic.prop('wavelength') ); export const wavelengthMaxAtom = focusAtom(waveformAtom, optic => optic.prop('wavelengthMax') ); +export const wavelengthAtom = reducerAtom( + baseWavelengthAtom, + createCountReducer({ + min: MIN_WAVELENGTH, + max: get => get(wavelengthMaxAtom), + step: get => + Math.abs(MIN_WAVELENGTH - get(wavelengthMaxAtom)) * STEP_PERCENTAGE + }) +); + +// Amplitude + +export const baseAmplitudeAtom = focusAtom(waveformAtom, optic => + optic.prop('amplitude') +); + export const amplitudeMaxAtom = focusAtom(waveformAtom, optic => optic.prop('amplitudeMax') ); +export const amplitudeAtom = reducerAtom( + baseAmplitudeAtom, + createCountReducer({ + min: MIN_AMPLITUDE, + max: get => get(amplitudeMaxAtom), + step: get => + Math.abs(MIN_AMPLITUDE - get(amplitudeMaxAtom)) * STEP_PERCENTAGE + }) +); + export const optimizeWaveformAtom = atom( null, (_get, set, image?: HTMLImageElement) => { diff --git a/apps/web/src/components/AppProvider.tsx b/apps/web/src/components/AppProvider.tsx new file mode 100644 index 0000000..6f9ff4d --- /dev/null +++ b/apps/web/src/components/AppProvider.tsx @@ -0,0 +1,14 @@ +import { useAtomValue } from 'jotai'; +import { I18nProvider } from '@lingui/react'; +import { TooltipProvider } from '@workspace/ui/components/tooltip'; +import { i18nAtom } from '@/atoms/localeAtoms'; + +export function AppProvider(props: React.PropsWithChildren) { + const i18n = useAtomValue(i18nAtom); + + return ( + + {props.children} + + ); +} diff --git a/apps/web/src/components/AtomSlider.tsx b/apps/web/src/components/AtomSlider.tsx index f8fef51..be1967a 100644 --- a/apps/web/src/components/AtomSlider.tsx +++ b/apps/web/src/components/AtomSlider.tsx @@ -1,32 +1,63 @@ -import { PrimitiveAtom, useAtom } from 'jotai'; +import { useAtom, WritableAtom } from 'jotai'; +import { useHotkeys } from 'react-hotkeys-hook'; import { LabelProps } from '@radix-ui/react-label'; import { cn } from '@workspace/ui/lib/utils'; import { Label } from '@workspace/ui/components/label'; import { Slider } from '@workspace/ui/components/slider'; +import { CountAction } from '@/utils/createCountReducer'; +import { Shortcut } from '@/components/Shortcut'; export interface AtomSliderProps - extends Omit, 'value' | 'onValueCHange'> { - atom: PrimitiveAtom; + extends Omit, 'value' | 'onValueChange'> { + atom: WritableAtom; label: React.ReactNode; - LabelProps?: Partial; - renderValue?: (value: number) => React.ReactNode; + labelProps?: Partial; + hotkeys?: [incrementHotkey: string, decrementHotkey: string]; } export function AtomSlider(props: AtomSliderProps) { - const { atom, className, label, LabelProps, renderValue, ...other } = props; - const [value, setValue] = useAtom(atom); + const { atom, className, label, labelProps, hotkeys = [], ...other } = props; + const [value, dispatch] = useAtom(atom); + const [incrementHotkey] = hotkeys; const handleValueChange = ([value]: number[]) => { - setValue(value); + dispatch({ type: 'CHANGE', value }); }; + useHotkeys( + hotkeys, + (_, { hotkey }) => { + dispatch({ + type: hotkey === incrementHotkey ? 'INCREMENT' : 'DECREMENT' + }); + }, + { enabled: !!hotkeys.length && !other.disabled } + ); + return (
- - - {renderValue?.(value)} - + + {hotkeys.length > 0 && ( +
+ {hotkeys.map(hotkey => ( + + ))} +
+ )}
diff --git a/apps/web/src/components/ChangeTheme.tsx b/apps/web/src/components/ChangeTheme.tsx index cdd4f7a..f45eebb 100644 --- a/apps/web/src/components/ChangeTheme.tsx +++ b/apps/web/src/components/ChangeTheme.tsx @@ -1,27 +1,31 @@ import { Moon, Sun } from 'lucide-react'; -import { Trans } from '@lingui/react/macro'; +import { useLingui } from '@lingui/react/macro'; import { Button } from '@workspace/ui/components/button'; import { useTheme } from '@/hooks/useTheme'; -export function ChangeTheme() { - const [appliedTheme, setTheme] = useTheme(); +export type ChangeThemeProps = Omit< + React.ComponentProps, + 'onClick' +>; - const toggleTheme = () => { - setTheme(appliedTheme === 'dark' ? 'light' : 'dark'); +export function ChangeTheme(props: ChangeThemeProps) { + const { t } = useLingui(); + const [theme, setTheme] = useTheme(); + + const handleThemeClick = () => { + setTheme(theme === 'dark' ? 'light' : 'dark'); }; return ( ); } diff --git a/apps/web/src/components/ChangeWaveFunction.tsx b/apps/web/src/components/ChangeWaveFunction.tsx index c32891c..9874c84 100644 --- a/apps/web/src/components/ChangeWaveFunction.tsx +++ b/apps/web/src/components/ChangeWaveFunction.tsx @@ -1,5 +1,4 @@ import { useAtom, useAtomValue } from 'jotai'; -import { Trans } from '@lingui/react/macro'; import { waveFunctionAtom } from '@/atoms/waveFunctionAtom'; import { ToggleGroup, @@ -11,31 +10,39 @@ import { WAVE_FUNCTION } from '@/constants'; import { WaveFunction } from '@/types'; import { unwrappedImagesAtom } from '@/atoms/imagesAtom'; -export function ChangeWaveFunction() { +export type ChanveWaveFunctionProps = Omit< + React.ComponentProps, + | 'type' + | 'value' + | 'defaultValue' + | 'waveFunction' + | 'onValueChange' + | 'disabled' +>; + +export function ChangeWaveFunction(props: ChanveWaveFunctionProps) { const images = useAtomValue(unwrappedImagesAtom); const [waveFunction, setWaveFunction] = useAtom(waveFunctionAtom); - const handleWaveFunctionChange = (waveFunction: WaveFunction) => { - if (waveFunction) { - setWaveFunction(waveFunction); + const handleWaveFunctionChange = (nextWaveFunction: WaveFunction) => { + if (nextWaveFunction) { + setWaveFunction(nextWaveFunction); } }; return ( - Sine - Cosine ); diff --git a/apps/web/src/components/ChooseFiles.tsx b/apps/web/src/components/ChooseFiles.tsx index 74e3c82..2253497 100644 --- a/apps/web/src/components/ChooseFiles.tsx +++ b/apps/web/src/components/ChooseFiles.tsx @@ -8,10 +8,7 @@ export interface ChooseFilesProps extends React.ComponentProps { options?: CoreFileOptions; } -export const ChooseFiles = React.forwardRef(function ChooseFiles( - props: ChooseFilesProps, - ref: React.ForwardedRef -) { +export function ChooseFiles(props: ChooseFilesProps) { const { onFilesChange, options, ...other } = props; const handleClick = async () => { try { @@ -32,8 +29,7 @@ export const ChooseFiles = React.forwardRef(function ChooseFiles( return ( ) : null; diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx deleted file mode 100644 index d862dbc..0000000 --- a/apps/web/src/components/Editor.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Suspense } from 'react'; -import { Eclipse, Loader2 } from 'lucide-react'; -import { Trans } from '@lingui/react/macro'; -import { cn } from '@workspace/ui/lib/utils'; -import { AspectRatio } from '@workspace/ui/components/aspect-ratio'; -import { Waveform } from '@/components/Waveform'; -import { Graphics } from '@/components/Graphics'; -import { ExportImage } from '@/components/ExportImage'; -import { SeeExample } from '@/components/SeeExample'; -import { CopyImage } from '@/components/CopyImage'; -import { ReverseImages } from '@/components/ReverseImages'; -import { ImportImages } from '@/components/ImportImages'; -import { RemoveImages } from '@/components/RemoveImages'; -import { ChangeWaveFunction } from '@/components/ChangeWaveFunction'; -import { OptimizeWaveform } from '@/components/OptimizeWaveform'; -import { useSetAtom } from 'jotai'; -import { graphicsAtom } from '@/atoms/graphicsAtom'; - -export type EditorProps = React.ComponentProps<'div'>; - -export function Editor(props: EditorProps) { - const { className, ...other } = props; - const setGraphics = useSetAtom(graphicsAtom); - - return ( -
- - - } - > - - -

Shadowave

-

- - Import images featuring both light and dark themes to start. - -

- -
- } - /> - - - -
-
-
- - - -
-
- - -
-
- -
- - -
-
- - ); -} diff --git a/apps/web/src/components/ExportImage.tsx b/apps/web/src/components/ExportImage.tsx index 468a275..ccdbfc9 100644 --- a/apps/web/src/components/ExportImage.tsx +++ b/apps/web/src/components/ExportImage.tsx @@ -1,7 +1,15 @@ import { useAtom, useAtomValue } from 'jotai'; -import { ImageDown, Save } from 'lucide-react'; -import { Trans } from '@lingui/react/macro'; +import { Download, ImageDown, Save } from 'lucide-react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { Trans, useLingui } from '@lingui/react/macro'; import { cn } from '@workspace/ui/lib/utils'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuShortcut, + DropdownMenuItem +} from '@workspace/ui/components/dropdown-menu'; import { graphicsAtom } from '@/atoms/graphicsAtom'; import { ExportFileHandle, @@ -9,14 +17,12 @@ import { exportFileHandleAtom } from '@/atoms/exportAtoms'; import { Button } from '@workspace/ui/components/button'; -import { Tooltip } from '@workspace/ui/components/tooltip'; import { LoadableIcon } from '@/components/LoadableIcon'; -import { LOADABLE_STATE } from '@/constants'; - -export type ExportImageProps = React.ComponentProps<'div'>; +import { HOTKEYS, LOADABLE_STATE } from '@/constants'; +import { Shortcut } from './Shortcut'; -export function ExportImage(props: ExportImageProps) { - const { className, ...other } = props; +export function ExportImage() { + const { t } = useLingui(); const graphics = useAtomValue(graphicsAtom); const exportFileHandle = useAtomValue(exportFileHandleAtom); const [exportState, exportAsImage] = useAtom(exportAtom); @@ -29,47 +35,57 @@ export function ExportImage(props: ExportImageProps) { exportAsImage(exportFileHandle); }; + useHotkeys( + [HOTKEYS.SAVE, HOTKEYS.SAVE_AS], + (_, { hotkey }) => { + exportAsImage(hotkey === HOTKEYS.SAVE ? exportFileHandle : null); + }, + { enabled: () => !disabled, preventDefault: true } + ); + return ( -
- Save to}> + + - - {exportFileHandle && ( - <> - Choose file}> - - - - )} -
+ + + {exportFileHandle && ( + + + + + Save to {exportFileHandle.name} + + + + + + + )} + + + + Save as + + + + + + + ); } diff --git a/apps/web/src/components/Graphics.tsx b/apps/web/src/components/Graphics.tsx index bb02c3e..62f070e 100644 --- a/apps/web/src/components/Graphics.tsx +++ b/apps/web/src/components/Graphics.tsx @@ -1,10 +1,12 @@ import React, { useId } from 'react'; -import { useAtomValue } from 'jotai'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { cn } from '@workspace/ui/lib/utils'; import { waveAtom } from '@/atoms/waveAtom'; import { orderedImagesAtom } from '@/atoms/orderedImagesAtom'; import { rotationAtom } from '@/atoms/waveformAtoms'; import { largestImageAtom } from '@/atoms/largestImageAtom'; import { scaleAtom } from '@/atoms/scaleAtom'; +import { graphicsAtom } from '@/atoms/graphicsAtom'; declare module 'react' { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -13,13 +15,15 @@ declare module 'react' { } } -export interface GraphicsProps extends React.ComponentProps<'svg'> { +export interface GraphicsProps + extends Omit, 'viewBox'> { fallback?: React.ReactNode; } export function Graphics(props: GraphicsProps) { - const { fallback, ref, ...other } = props; + const { fallback, style, className, ...other } = props; + const setGraphics = useSetAtom(graphicsAtom); const orderedImages = useAtomValue(orderedImagesAtom); const wave = useAtomValue(waveAtom); const largestImage = useAtomValue(largestImageAtom); @@ -33,45 +37,52 @@ export function Graphics(props: GraphicsProps) { const { width, height } = largestImage; const sectionHeight = height / orderedImages.length; - const sectionRotationScale = - 1 + Math.abs(Math.sin((rotation * Math.PI) / 180)); + const rotationScale = 1 + Math.abs(Math.sin((rotation * Math.PI) / 180)); return ( - {orderedImages.map( - (image, i) => - i !== orderedImages.length - 1 && ( - - - - ) - )} + {orderedImages.slice(0, -1).map((image, i) => ( + + + + ))} {orderedImages.map((image, i) => ( = 1 && { - clipPath: `url(#${waveId}${orderedImages.length - i})` - })} + style={{ zIndex: i + 1 }} + clipPath={ + i >= 1 ? `url(#${waveId}${orderedImages.length - i})` : undefined + } /> ))} diff --git a/apps/web/src/components/I18nProvider.tsx b/apps/web/src/components/I18nProvider.tsx deleted file mode 100644 index 18714ea..0000000 --- a/apps/web/src/components/I18nProvider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useAtomValue } from 'jotai'; -import { setupI18n } from '@lingui/core'; -import { I18nProvider as LinguiI18nProvider } from '@lingui/react'; -import { localeAtom } from '@/atoms/localeAtom'; -import { messages as enMessages } from '@/locales/en'; - -export function I18nProvider({ children }: React.PropsWithChildren) { - const locale = useAtomValue(localeAtom); - const [i18n] = useState(() => - setupI18n({ - messages: { - en: enMessages - }, - locale - }) - ); - - useEffect(() => { - i18n.activate(locale); - }, [i18n, locale]); - - return {children}; -} diff --git a/apps/web/src/components/ImportImages.tsx b/apps/web/src/components/ImportImages.tsx index 246defe..6faa7ec 100644 --- a/apps/web/src/components/ImportImages.tsx +++ b/apps/web/src/components/ImportImages.tsx @@ -1,6 +1,7 @@ import { useAtom, useAtomValue } from 'jotai'; -import { Camera, Monitor, Smartphone, Tablet } from 'lucide-react'; +import { Camera, Folder, Monitor, Smartphone, Tablet } from 'lucide-react'; import { useForm } from 'react-hook-form'; +import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, Plural, useLingui } from '@lingui/react/macro'; import { cn } from '@workspace/ui/lib/utils'; import { Button } from '@workspace/ui/components/button'; @@ -32,7 +33,7 @@ import { LoadableIcon } from '@/components/LoadableIcon'; import { importAtom, importSignalAtom } from '@/atoms/importAtoms'; import { unwrappedImagesAtom } from '@/atoms/imagesAtom'; import { isValidationError } from '@/utils/client'; -import { LOADABLE_STATE } from '@/constants'; +import { HOTKEYS, LOADABLE_STATE } from '@/constants'; import { Site } from '@/types'; export type ImportImagesProps = React.ComponentProps; @@ -77,15 +78,23 @@ export function ImportImages(props: ImportImagesProps) { } }; + useHotkeys(HOTKEYS.IMPORT, () => toggleImportSignal(true), { + preventDefault: true + }); + return ( @@ -95,7 +104,7 @@ export function ImportImages(props: ImportImagesProps) { - The side should respond to the light and dark themes using the{' '} + The site should respond to the light and dark themes using the{' '} prefers-color-scheme media query. If it does not, choose files instead. @@ -116,11 +125,7 @@ export function ImportImages(props: ImportImagesProps) { * - + {fieldState.invalid ? ( @@ -151,7 +156,7 @@ export function ImportImages(props: ImportImagesProps) { aria-label={t`Desktop`} className="flex-grow" > - + Desktop - + Tablet - + Mobile @@ -195,7 +200,7 @@ export function ImportImages(props: ImportImagesProps) { diff --git a/apps/web/src/components/NoImages.tsx b/apps/web/src/components/NoImages.tsx new file mode 100644 index 0000000..e07991b --- /dev/null +++ b/apps/web/src/components/NoImages.tsx @@ -0,0 +1,46 @@ +import { useSetAtom } from 'jotai'; +import { ImageOff, Folder } from 'lucide-react'; +import { Trans } from '@lingui/react/macro'; +import { Button } from '@workspace/ui/components/button'; +import { Shortcut } from '@/components/Shortcut'; +import { importAtom, importSignalAtom } from '@/atoms/importAtoms'; +import { siteConfig } from '@/config/site'; +import { HOTKEYS } from '@/constants'; + +export function NoImages() { + const importImage = useSetAtom(importAtom); + const toggleImportSignal = useSetAtom(importSignalAtom); + + const handleClick = () => { + importImage(siteConfig.example); + }; + + const handleImportClick = () => { + toggleImportSignal(true); + }; + + return ( +
+ +

+ No images +

+

+ + Get started by importing your images or check out the example to see + how it works. + +

+
+ + +
+
+ ); +} diff --git a/apps/web/src/components/OptimizeWaveform.tsx b/apps/web/src/components/OptimizeWaveform.tsx index d04cc3e..8421238 100644 --- a/apps/web/src/components/OptimizeWaveform.tsx +++ b/apps/web/src/components/OptimizeWaveform.tsx @@ -1,10 +1,10 @@ import { useAtomValue, useSetAtom } from 'jotai'; import { WandSparkles } from 'lucide-react'; -import { optimizeWaveformAtom } from '@/atoms/waveformAtoms'; -import { Button } from '@workspace/ui/components/button'; -import { unwrappedLargestImageAtom } from '@/atoms/largestImageAtom'; import { useLingui } from '@lingui/react/macro'; +import { Button } from '@workspace/ui/components/button'; import { Tooltip } from '@workspace/ui/components/tooltip'; +import { optimizeWaveformAtom } from '@/atoms/waveformAtoms'; +import { unwrappedLargestImageAtom } from '@/atoms/largestImageAtom'; export function OptimizeWaveform() { const { t } = useLingui(); diff --git a/apps/web/src/components/SeeExample.tsx b/apps/web/src/components/SeeExample.tsx deleted file mode 100644 index dc16be0..0000000 --- a/apps/web/src/components/SeeExample.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useSetAtom } from 'jotai'; -import { Trans } from '@lingui/react/macro'; -import { Button } from '@workspace/ui/components/button'; -import { siteConfig } from '@/config/site'; -import { importAtom } from '@/atoms/importAtoms'; - -export function SeeExample() { - const importImage = useSetAtom(importAtom); - - const handleClick = () => { - importImage(siteConfig.example); - }; - - return ( - - ); -} diff --git a/apps/web/src/components/Shortcut.tsx b/apps/web/src/components/Shortcut.tsx new file mode 100644 index 0000000..673abd7 --- /dev/null +++ b/apps/web/src/components/Shortcut.tsx @@ -0,0 +1,90 @@ +import { cn } from '@workspace/ui/lib/utils'; +import { cva, VariantProps } from 'class-variance-authority'; + +type Keys = string; +type KeyDisplay = string | { other: string; macos: string }; +type KeyDisplayMapping = Record; + +const defaultKeyDisplayMapping: KeyDisplayMapping = { + meta: { other: 'Meta', macos: '⌘' }, + shift: '⇧', + alt: { other: 'Alt', macos: '⌥' }, + ctrl: { other: 'Ctrl', macos: '⌃' }, + mod: { other: 'Ctrl', macos: '⌘' }, + arrowup: '↑', + arrowdown: '↓' +}; + +const shortcutVariants = cva( + 'ml-auto hidden tracking-widest select-none sm:flex px-1 pointer-events-none gap-1 text-muted-foreground', + { + variants: { + variant: { + default: '', + outline: 'rounded border bg-muted' + }, + size: { + default: '', + sm: 'text-xs' + } + }, + + defaultVariants: { + variant: 'default', + size: 'default' + } + } +); + +export interface ShortcutOptions + extends React.ComponentProps<'kbd'>, + VariantProps { + keys: Keys; + keyComponent?: React.ElementType; + keyDisplayMapping?: KeyDisplayMapping; + combinationKey?: string; + platform?: string; +} + +export function Shortcut(props: ShortcutOptions) { + const { + keys, + keyComponent: KeyComponent = 'span', + keyDisplayMapping: keyDisplayMappingProp, + combinationKey = '+', + platform = navigator.platform, + variant, + size, + className, + ...other + } = props; + + const isMac = platform.startsWith('Mac'); + const keyDisplayMapping = { + ...defaultKeyDisplayMapping, + ...keyDisplayMappingProp + }; + + return ( + + {keys + .trim() + .split(combinationKey) + .map(part => { + const display = keyDisplayMapping[part.trim().toLowerCase()] || part; + return ( + + {typeof display === 'string' + ? display + : isMac + ? display.macos + : display.other} + + ); + })} + + ); +} diff --git a/apps/web/src/components/Waveform.tsx b/apps/web/src/components/Waveform.tsx index fde3591..7a1205b 100644 --- a/apps/web/src/components/Waveform.tsx +++ b/apps/web/src/components/Waveform.tsx @@ -7,29 +7,31 @@ import { amplitudeAtom, amplitudeMaxAtom, MIN_WAVELENGTH, - MIN_AMPLITUDE + MIN_AMPLITUDE, + MIN_ROTATION, + MAX_ROTATION } from '@/atoms/waveformAtoms'; import { AtomSlider } from '@/components/AtomSlider'; import { unwrappedImagesAtom } from '@/atoms/imagesAtom'; +import { HOTKEYS } from '@/constants'; -export type WaveformProps = React.ComponentProps<'div'>; - -export function Waveform(props: WaveformProps) { +export function Waveform() { const { t } = useLingui(); const images = useAtomValue(unwrappedImagesAtom); const wavelengthMax = useAtomValue(wavelengthMaxAtom); const amplitudeMax = useAtomValue(amplitudeMaxAtom); + const disabled = !images.length; return ( -
+ <> `${rotation}°`} - min={0} - max={360} - disabled={!images.length} - ThumbProps={{ + min={MIN_ROTATION} + max={MAX_ROTATION} + disabled={disabled} + hotkeys={[HOTKEYS.ROTATION_UP, HOTKEYS.ROTATION_DOWN]} + thumbProps={{ 'aria-label': t`Rotation` }} /> @@ -38,8 +40,9 @@ export function Waveform(props: WaveformProps) { atom={wavelengthAtom} min={MIN_WAVELENGTH} max={wavelengthMax} - disabled={!images.length} - ThumbProps={{ + disabled={disabled} + hotkeys={[HOTKEYS.WAVELENGTH_UP, HOTKEYS.WAVELENGTH_DOWN]} + thumbProps={{ 'aria-label': t`Wavelength` }} /> @@ -48,11 +51,12 @@ export function Waveform(props: WaveformProps) { atom={amplitudeAtom} min={MIN_AMPLITUDE} max={amplitudeMax} - disabled={!images.length} - ThumbProps={{ + disabled={disabled} + hotkeys={[HOTKEYS.AMPLITUDE_UP, HOTKEYS.AMPLITUDE_DOWN]} + thumbProps={{ 'aria-label': t`Amplitude` }} /> -
+ ); } diff --git a/apps/web/src/constants.ts b/apps/web/src/constants.ts index 23c1d70..b80f6fa 100644 --- a/apps/web/src/constants.ts +++ b/apps/web/src/constants.ts @@ -14,6 +14,19 @@ export const WAVE_FUNCTION = { COS: 'cos' } as const; +export const HOTKEYS = { + IMPORT: 'mod+O', + SAVE: 'mod+S', + SAVE_AS: 'mod+shift+S', + COPY: 'mod+C', + ROTATION_UP: '1+ArrowUp', + ROTATION_DOWN: '1+ArrowDown', + WAVELENGTH_UP: '2+ArrowUp', + WAVELENGTH_DOWN: '2+ArrowDown', + AMPLITUDE_UP: '3+ArrowUp', + AMPLITUDE_DOWN: '3+ArrowDown' +} as const; + export const DEFAULT_LOADABLE_STATE_TIMEOUT = 1000; export const DEFAULT_FILENAME = 'shadowave'; diff --git a/apps/web/src/utils/createCountReducer.ts b/apps/web/src/utils/createCountReducer.ts new file mode 100644 index 0000000..8120a38 --- /dev/null +++ b/apps/web/src/utils/createCountReducer.ts @@ -0,0 +1,47 @@ +import { Getter } from 'jotai'; + +type NumberOrGetter = + | number + | undefined + | ((get: Getter) => number | undefined); + +function read(get: Getter, value?: NumberOrGetter) { + return typeof value === 'number' ? value : value?.(get); +} + +export type CountAction = + | { type: 'CHANGE'; value: number } + | { type: 'INCREMENT' } + | { type: 'DECREMENT' }; + +export interface CreateCountReducerOptions { + min?: NumberOrGetter; + max?: NumberOrGetter; + step?: NumberOrGetter; +} + +export function createCountReducer({ + min, + max, + step +}: CreateCountReducerOptions = {}) { + return (get: Getter, prev: number, action: CountAction) => { + switch (action.type) { + case 'CHANGE': { + return action.value; + } + case 'INCREMENT': { + return Math.min( + read(get, max) ?? Infinity, + prev + (read(get, step) ?? 1) + ); + } + case 'DECREMENT': { + return Math.max( + read(get, min) ?? -Infinity, + prev - (read(get, step) ?? 1) + ); + } + } + }; +} diff --git a/apps/web/src/utils/createGraphIcon.ts b/apps/web/src/utils/createGraphIcon.ts index bd4d66b..6faa502 100644 --- a/apps/web/src/utils/createGraphIcon.ts +++ b/apps/web/src/utils/createGraphIcon.ts @@ -5,11 +5,9 @@ const height = 24; export function createGraphIcon(iconName: string, getY: (x: number) => number) { const points: number[] = []; - for (let x = 0; x <= width; x++) { points.push(x, height / 2 + getY(x)); } - return createLucideIcon(iconName, [ ['polyline', { key: 'points', points: points.join(' ') }] ]); diff --git a/apps/web/src/utils/reducerAtom.ts b/apps/web/src/utils/reducerAtom.ts new file mode 100644 index 0000000..7f7f429 --- /dev/null +++ b/apps/web/src/utils/reducerAtom.ts @@ -0,0 +1,14 @@ +import { atom, Getter, WritableAtom } from 'jotai'; +import { SetStateAction } from 'react'; + +export function reducerAtom( + baseAtom: WritableAtom], void>, + reducer: (get: Getter, value: Value, action: Action) => Value +) { + return atom( + get => get(baseAtom), + (get, set, action: Action) => { + set(baseAtom, value => reducer(get, value, action)); + } + ); +} diff --git a/bun.lock b/bun.lock index faa095f..83ece98 100644 --- a/bun.lock +++ b/bun.lock @@ -7,12 +7,12 @@ "eslint": "^9.20.0", "husky": "^9.1.7", "lint-staged": "^15.4.3", - "prettier": "3.5.0", + "prettier": "3.5.2", "prettier-plugin-tailwindcss": "^0.6.11", - "turbo": "^2.4.1", + "turbo": "^2.4.2", "typescript": "^5.7.3", - "vite": "6.1.0", - "vitest": "^3.0.5", + "vite": "6.1.1", + "vitest": "^3.0.6", }, }, "apps/api": { @@ -43,12 +43,14 @@ "clsx": "^2.1.1", "idb-keyval": "^6.2.1", "jotai": "^2.12.0", + "jotai-effect": "^2.0.1", "jotai-optics": "^0.4.0", "lucide-react": "^0.475.0", "optics-ts": "^2.4.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", + "react-hotkeys-hook": "^4.6.1", }, "devDependencies": { "@lingui/cli": "^5.2.0", @@ -75,15 +77,15 @@ "name": "@workspace/eslint-config", "version": "0.0.0", "devDependencies": { - "@eslint/js": "^9.20.0", + "@eslint/js": "^9.21.0", "@types/eslint-config-prettier": "^6.11.3", - "@types/eslint__js": "^8.42.3", + "@types/eslint__js": "^9.14.0", "eslint-config-prettier": "^10.0.1", - "eslint-config-turbo": "^2.4.1", + "eslint-config-turbo": "^2.4.2", "eslint-plugin-react-hooks": "5.2.0-canary-de1eaa26-20250124", "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.14.0", - "typescript-eslint": "8.24.0", + "globals": "^16.0.0", + "typescript-eslint": "8.24.1", }, "peerDependencies": { "eslint": "^9.0.0", @@ -92,7 +94,7 @@ "packages/schema": { "name": "@workspace/schema", "devDependencies": { - "@sinclair/typebox": "^0.34.16", + "@sinclair/typebox": "^0.34.28", }, "peerDependencies": { "@sinclair/typebox": "*", @@ -103,9 +105,9 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-aspect-ratio": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-toggle": "^1.1.2", @@ -117,12 +119,12 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", - "tailwind-merge": "^3.0.1", + "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", }, "devDependencies": { - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "postcss": "^8.5.2", "react": "^19.0.0", "tailwindcss": "^4.0.6", @@ -235,7 +237,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="], - "@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="], + "@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -311,8 +313,6 @@ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], - "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -347,6 +347,8 @@ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="], + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.2.3", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], @@ -485,11 +487,9 @@ "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], - "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], - "@types/eslint-config-prettier": ["@types/eslint-config-prettier@6.11.3", "", {}, "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ=="], - "@types/eslint__js": ["@types/eslint__js@8.42.3", "", { "dependencies": { "@types/eslint": "*" } }, "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw=="], + "@types/eslint__js": ["@types/eslint__js@9.14.0", "", { "dependencies": { "@eslint/js": "*" } }, "sha512-s0jepCjOJWB/GKcuba4jISaVpBudw3ClXJ3fUK4tugChUMQsp6kSwuA8Dcx6wFd/JsJqcY8n4rEpa5RTHs5ypA=="], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], @@ -507,9 +507,9 @@ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/react": ["@types/react@19.0.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="], + "@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], - "@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="], + "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], @@ -519,37 +519,37 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/type-utils": "8.24.0", "@typescript-eslint/utils": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/utils": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0" } }, "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.24.0", "", {}, "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg=="], "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="], - "@vitest/expect": ["@vitest/expect@3.0.5", "", { "dependencies": { "@vitest/spy": "3.0.5", "@vitest/utils": "3.0.5", "chai": "^5.1.2", "tinyrainbow": "^2.0.0" } }, "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA=="], + "@vitest/expect": ["@vitest/expect@3.0.7", "", { "dependencies": { "@vitest/spy": "3.0.7", "@vitest/utils": "3.0.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw=="], - "@vitest/mocker": ["@vitest/mocker@3.0.5", "", { "dependencies": { "@vitest/spy": "3.0.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw=="], + "@vitest/mocker": ["@vitest/mocker@3.0.7", "", { "dependencies": { "@vitest/spy": "3.0.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w=="], - "@vitest/pretty-format": ["@vitest/pretty-format@3.0.5", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA=="], + "@vitest/pretty-format": ["@vitest/pretty-format@3.0.7", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg=="], - "@vitest/runner": ["@vitest/runner@3.0.5", "", { "dependencies": { "@vitest/utils": "3.0.5", "pathe": "^2.0.2" } }, "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A=="], + "@vitest/runner": ["@vitest/runner@3.0.7", "", { "dependencies": { "@vitest/utils": "3.0.7", "pathe": "^2.0.3" } }, "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g=="], - "@vitest/snapshot": ["@vitest/snapshot@3.0.5", "", { "dependencies": { "@vitest/pretty-format": "3.0.5", "magic-string": "^0.30.17", "pathe": "^2.0.2" } }, "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA=="], + "@vitest/snapshot": ["@vitest/snapshot@3.0.7", "", { "dependencies": { "@vitest/pretty-format": "3.0.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA=="], - "@vitest/spy": ["@vitest/spy@3.0.5", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg=="], + "@vitest/spy": ["@vitest/spy@3.0.7", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w=="], - "@vitest/utils": ["@vitest/utils@3.0.5", "", { "dependencies": { "@vitest/pretty-format": "3.0.5", "loupe": "^3.1.2", "tinyrainbow": "^2.0.0" } }, "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg=="], + "@vitest/utils": ["@vitest/utils@3.0.7", "", { "dependencies": { "@vitest/pretty-format": "3.0.7", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg=="], "@workspace/api": ["@workspace/api@workspace:apps/api"], @@ -623,7 +623,7 @@ "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], + "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -633,7 +633,7 @@ "caniuse-lite": ["caniuse-lite@1.0.30001699", "", {}, "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w=="], - "chai": ["chai@5.1.2", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw=="], + "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -741,13 +741,13 @@ "eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="], - "eslint-config-turbo": ["eslint-config-turbo@2.4.1", "", { "dependencies": { "eslint-plugin-turbo": "2.4.1" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-IFlqpBrWgVG1VJO34H2X2IgxU363CB67Bzsd/MekcD7gPAcRtNIyn8cQXibZL/+jZxdbacp1JLm26iHPg8XqgQ=="], + "eslint-config-turbo": ["eslint-config-turbo@2.4.2", "", { "dependencies": { "eslint-plugin-turbo": "2.4.2" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-yPiW5grffSWETp/3bVPUXWQkHfiLoOBb3wuBbM90HlKkLOhggySn9C6/yUccprqRguMgR5OzXIuzDuUnX6bulw=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0-canary-de1eaa26-20250124", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-BJdM6GV56L/RvLZ6SefQNB2hf1cDT9Jj6s31czVuNp3MzOCpE4owM5f0pj01+MPtu5yF3h1HL8e0BqSwOLl4Ww=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="], - "eslint-plugin-turbo": ["eslint-plugin-turbo@2.4.1", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-sTJZrgn7G1rF7RHZcj367g2cT0+E5v0lt9LD+nBpTnCyDA/yB0PwSm/QM+aXQ39vxuK9sqlUL2LyPXyGFvUXsg=="], + "eslint-plugin-turbo": ["eslint-plugin-turbo@2.4.2", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-67IZtvOFaWDnUmYMV3luRIE1kqL+ok5MxPEsIPUqH2vQggML7jmZFZx/P9jhXAoFH+pViEz5QEzDa2DBLHqzQg=="], "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], @@ -825,7 +825,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="], + "globals": ["globals@16.0.0", "", {}, "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A=="], "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], @@ -897,6 +897,8 @@ "jotai": ["jotai@2.12.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-j5B4NmUw8gbuN7AG4NufWw00rfpm6hexL2CVhKD7juoP2YyD9FEUV5ar921JMvadyrxQhU1NpuKUL3QfsAlVpA=="], + "jotai-effect": ["jotai-effect@2.0.1", "", { "peerDependencies": { "jotai": ">=2.12.1" } }, "sha512-jZw18vUzlerIMQCaB75Ndxr9+6BR/fCt7Ah0CO2RpR5UhtBbXurgIprAZoVSzADaWoTN/qj0pyT6IQQWWP6dpA=="], + "jotai-optics": ["jotai-optics@0.4.0", "", { "peerDependencies": { "jotai": ">=2.0.0", "optics-ts": ">=2.0.0" } }, "sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g=="], "js-sha256": ["js-sha256@0.10.1", "", {}, "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw=="], @@ -1055,7 +1057,7 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@2.0.2", "", {}, "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], @@ -1075,7 +1077,7 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="], + "prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="], "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="], @@ -1103,6 +1105,8 @@ "react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], + "react-hotkeys-hook": ["react-hotkeys-hook@4.6.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-XlZpbKUj9tkfgPgT9gA+1p7Ey6vFIZHttUjPqpTdyT5nqQ8mHL7elxvSbaC+dpSiHUSmr21Ya1mDxBZG3aje4Q=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], @@ -1193,7 +1197,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="], + "tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="], "tailwindcss": ["tailwindcss@4.0.6", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="], @@ -1231,19 +1235,19 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.4.1", "", { "optionalDependencies": { "turbo-darwin-64": "2.4.1", "turbo-darwin-arm64": "2.4.1", "turbo-linux-64": "2.4.1", "turbo-linux-arm64": "2.4.1", "turbo-windows-64": "2.4.1", "turbo-windows-arm64": "2.4.1" }, "bin": { "turbo": "bin/turbo" } }, "sha512-XIIHXAhvD3sv34WLaN/969WTHCHYmm3zf0XQ+CrEP1A7ffIQG50cwNcp7Gh96CaGyjEXMh9odoHyggoZQ3Prvw=="], + "turbo": ["turbo@2.4.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.4.3", "turbo-darwin-arm64": "2.4.3", "turbo-linux-64": "2.4.3", "turbo-linux-arm64": "2.4.3", "turbo-windows-64": "2.4.3", "turbo-windows-arm64": "2.4.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-CpUHqrLTnV9R41RiWMSj9ZNHl++I7eTUIU+pH5O5a/2fRMhiKI6JdgBszuQ2YyaZYXkZZh/oHwbgzS1pug0zMg=="], - "turbo-darwin-64": ["turbo-darwin-64@2.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-oos3Gz5N6ol2/7+ys0wPENhl7ZzeVKIumn2BR7X2oE5dEPxNPDMOpKBwreU9ToCxM94e+uFTzKgjcUJpBqpTHA=="], + "turbo-darwin-64": ["turbo-darwin-64@2.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-TFKiCSJAAN8E7g8isVzocm5OaUGjXn5DVBCeIFmJ2dKe/wY64Gvw37Y5HnyKZLM2JWdEI2SkXrDLNfGMGwfXyg=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoIQsSSvCJDTShgX+v+doSP/g0kAhHhq5p2fpsEAlougs2wcQvwv/LndeqojzkHbxB39lOQmqBYHJcki46Q3oQ=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pRCRHe5LPyF9L0qxCMGycl2NM9OV5obrG+J4/foO/e7a0yfwMRnQ7lJdHrrT5zfEYrj0/WN0ZxsaRAqyM3qZZw=="], - "turbo-linux-64": ["turbo-linux-64@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iXIeG8YhluaJF/5KQEudRf8ECBWND8X0yxXDrFIq2wmLLCg4A7gSSzVcBq30rYYeyyU4xMj/sm3HbsAaao3jjg=="], + "turbo-linux-64": ["turbo-linux-64@2.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4SZj0K1OKXFBwGe7bNVYKcNJct6mdESWfq0zEYUp2yiWPxcihMWc60aGTccI5OC54UxhS52PcrBoiKQzIB6vkA=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jd5apBV7lBGn3CnkQN/hEMbwazNgZcrwLt6DIkWy/TSi5xfSQEqcR3k9HxviQ7hKMcr1Q1hN6FHWm8Vw90Ej4A=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-/wp0csB7GW6VUYsKAuMjVtvGy+Bo2g8RyE+5yx3/rD9+e8wHy0TZUiAiRtj0aeVJXiVPd/MA6u6IdxjV95/qxA=="], - "turbo-windows-64": ["turbo-windows-64@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-4RYRAijohyQ7uetZY4SSikSgGccq+7tmnljdm/XezpK9t0+3gldKA2vHF0++yLZeZr+CFgqmBeGSFi7B+vhc2g=="], + "turbo-windows-64": ["turbo-windows-64@2.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-GpZl4YiIqf1eNFHys+tT9LgCfTi67TykOdI6xD7PB+oFsZisjQzhAVu/BW3oTe6lHRe0zndEQMuodP+fGAXHbA=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4lZB0+AxWB01Adx5xHZhO746FgaHR0T3qzEDF2nf/nx8LAUtN3iwaZQgAsTsblaAKjiM7lxWDI0s/Q3fektsPg=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-AjyuOdQc2wRn2C+RksQ8rhpzvvpD3FOG3hUz9S9uhE5FpgUFUynGnYgFlKVnHkL/k7aslBL4dzNE7S4UaJeywA=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -1253,7 +1257,7 @@ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - "typescript-eslint": ["typescript-eslint@8.24.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/parser": "8.24.0", "@typescript-eslint/utils": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ=="], + "typescript-eslint": ["typescript-eslint@8.24.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript-eslint/utils": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -1269,13 +1273,13 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@6.1.0", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.1", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ=="], + "vite": ["vite@6.1.1", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="], - "vite-node": ["vite-node@3.0.5", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.2", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A=="], + "vite-node": ["vite-node@3.0.7", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A=="], "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "vitest": ["vitest@3.0.5", "", { "dependencies": { "@vitest/expect": "3.0.5", "@vitest/mocker": "3.0.5", "@vitest/pretty-format": "^3.0.5", "@vitest/runner": "3.0.5", "@vitest/snapshot": "3.0.5", "@vitest/spy": "3.0.5", "@vitest/utils": "3.0.5", "chai": "^5.1.2", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.5", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.5", "@vitest/ui": "3.0.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q=="], + "vitest": ["vitest@3.0.7", "", { "dependencies": { "@vitest/expect": "3.0.7", "@vitest/mocker": "3.0.7", "@vitest/pretty-format": "^3.0.7", "@vitest/runner": "3.0.7", "@vitest/snapshot": "3.0.7", "@vitest/spy": "3.0.7", "@vitest/utils": "3.0.7", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.7", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.7", "@vitest/ui": "3.0.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], @@ -1349,6 +1353,8 @@ "@testing-library/dom/pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "@types/bun/bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], @@ -1361,6 +1367,8 @@ "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "eslint/@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="], + "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1399,8 +1407,6 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "vite/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], - "whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], diff --git a/package.json b/package.json index 5b1c9a5..28656bb 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,14 @@ "eslint": "^9.20.0", "husky": "^9.1.7", "lint-staged": "^15.4.3", - "prettier": "3.5.0", + "prettier": "3.5.2", "prettier-plugin-tailwindcss": "^0.6.11", - "turbo": "^2.4.1", + "turbo": "^2.4.2", "typescript": "^5.7.3", - "vite": "6.1.0", - "vitest": "^3.0.5" + "vite": "6.1.1", + "vitest": "^3.0.6" }, - "packageManager": "bun@1.2.2", + "packageManager": "bun@1.2.3", "overrides": { "@swc/core": "1.7.40", "@sinclair/typebox": "^0.34.15" diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 72cd9fe..d238c73 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -14,14 +14,14 @@ "eslint": "^9.0.0" }, "devDependencies": { - "@eslint/js": "^9.20.0", + "@eslint/js": "^9.21.0", "@types/eslint-config-prettier": "^6.11.3", - "@types/eslint__js": "^8.42.3", + "@types/eslint__js": "^9.14.0", "eslint-config-prettier": "^10.0.1", - "eslint-config-turbo": "^2.4.1", + "eslint-config-turbo": "^2.4.2", "eslint-plugin-react-hooks": "5.2.0-canary-de1eaa26-20250124", "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.14.0", - "typescript-eslint": "8.24.0" + "globals": "^16.0.0", + "typescript-eslint": "8.24.1" } } diff --git a/packages/schema/package.json b/packages/schema/package.json index 83b3ed3..b8db2dd 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -10,7 +10,7 @@ "lint": "eslint . --max-warnings 0" }, "devDependencies": { - "@sinclair/typebox": "^0.34.16" + "@sinclair/typebox": "^0.34.28" }, "peerDependencies": { "@sinclair/typebox": "*" diff --git a/packages/ui/package.json b/packages/ui/package.json index 1b0fd17..dff228c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,9 +16,9 @@ }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-aspect-ratio": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-toggle": "^1.1.2", @@ -30,12 +30,12 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", - "tailwind-merge": "^3.0.1", + "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "postcss": "^8.5.2", "react": "^19.0.0", "tailwindcss": "^4.0.6", diff --git a/packages/ui/src/components/aspect-ratio.tsx b/packages/ui/src/components/aspect-ratio.tsx deleted file mode 100644 index afb7845..0000000 --- a/packages/ui/src/components/aspect-ratio.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; - -function AspectRatio({ - ...props -}: React.ComponentProps) { - return ; -} - -export { AspectRatio }; diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx new file mode 100644 index 0000000..df1da7d --- /dev/null +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@workspace/ui/lib/utils'; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent +}; diff --git a/packages/ui/src/components/separator.tsx b/packages/ui/src/components/separator.tsx new file mode 100644 index 0000000..9f8a42a --- /dev/null +++ b/packages/ui/src/components/separator.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; + +import { cn } from '@workspace/ui/lib/utils'; + +function Separator({ + className, + orientation = 'horizontal', + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/packages/ui/src/components/slider.tsx b/packages/ui/src/components/slider.tsx index 559df02..aabb99f 100644 --- a/packages/ui/src/components/slider.tsx +++ b/packages/ui/src/components/slider.tsx @@ -5,14 +5,14 @@ import { cn } from '@workspace/ui/lib/utils'; interface SliderProps extends React.ComponentProps { - ThumbProps?: Omit, 'className'>; + thumbProps?: Omit, 'className'>; } function Slider({ className, defaultValue, value, - ThumbProps, + thumbProps, min = 0, max = 100, ...props @@ -58,7 +58,7 @@ function Slider({ data-slot="slider-thumb" key={index} className="border-primary bg-background ring-ring/20 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" - {...ThumbProps} + {...thumbProps} /> ))} diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 7ae7bd2..2f878bb 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -1,5 +1,4 @@ @import 'tailwindcss'; - @plugin 'tailwindcss-animate'; @custom-variant dark (&:is(.dark *)); @@ -68,7 +67,7 @@ @theme inline { --font-*: initial; - --font-sans: Noto Sans; + --font-sans: Geist; --color-border: hsl(var(--border)); --color-input: hsl(var(--input)); @@ -108,14 +107,17 @@ from { height: 0; } + to { height: var(--radix-accordion-content-height); } } + @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } + to { height: 0; } @@ -125,42 +127,40 @@ @utility container { margin-inline: auto; padding-inline: 2rem; - @media (width >= --theme(--breakpoint-sm)) { + + @media (width >=--theme(--breakpoint-sm)) { max-width: none; } - @media (width >= 1400px) { + + @media (width >=1400px) { max-width: 1400px; } } -/* - The default border color has changed to `currentColor` in Tailwind CSS v4, - so we've added these compatibility styles to make sure everything still - looks the same as it did with Tailwind CSS v3. +@utility bg-grid { + --grid-size: 24px; + --grid-alpha: 0.03; + --grid-color: rgba(0, 0, 0, var(--grid-alpha)); - If we ever want to remove these styles, we need to add an explicit border - color utility to any element that depends on these defaults. -*/ -@layer base { - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentColor); + @variant dark { + --grid-color: rgba(255, 255, 255, var(--grid-alpha)) } + + background-size: var(--grid-size) var(--grid-size); + background-image: linear-gradient(to right, var(--grid-color) 1px, transparent 1px), + linear-gradient(to bottom, var(--grid-color) 1px, transparent 1px); } +/* bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px] bg-slate-100 dark:bg-[radial-gradient(#374151_1px,transparent_1px)] dark:bg-slate-900 */ + @layer base { * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground flex min-h-dvh flex-col antialiased sm:justify-center; } - a { - @apply font-medium underline underline-offset-4; - } button:not(:disabled), [role="button"]:not(:disabled) {