mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-12 23:36:00 +00:00
fix(lib): optimized layout elements (#424)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
15fbd3e6ce
commit
590be2b7e5
@ -149,6 +149,7 @@ function App() {
|
||||
/>
|
||||
),
|
||||
name: l.name, // name that appear in Sidebar
|
||||
color: l.menuColor,
|
||||
})),
|
||||
)
|
||||
// eslint-disable-next-line no-catch-all/no-catch-all
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"cypress": "^14.0.3",
|
||||
"daisyui": "^5.0.6",
|
||||
"daisyui": "^5.2.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
|
||||
@ -10,6 +10,7 @@ export interface Route {
|
||||
name: string
|
||||
submenu?: Route[]
|
||||
blank?: boolean
|
||||
color?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,7 +36,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
|
||||
<nav
|
||||
id='sidenav'
|
||||
className={`${appState.sideBarOpen ? 'tw:translate-x-0' : 'tw:-translate-x-full'}
|
||||
${appState.sideBarSlim ? 'tw:w-14' : 'tw:w-48'}
|
||||
${appState.sideBarSlim ? 'tw:w-15' : 'tw:w-48'}
|
||||
${appState.embedded ? 'tw:mt-5.5 tw:h-[calc(100dvh-22px)]' : 'tw:mt-16 tw:h-[calc(100dvh-64px)]'}
|
||||
tw:fixed tw:left-0 tw:transition-all tw:duration-300 tw:top-0 tw:z-10035
|
||||
tw:overflow-hidden tw:shadow-xl tw:dark:bg-zinc-800`}
|
||||
@ -62,7 +63,12 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
|
||||
if (screen.width < 640 && !appState.sideBarSlim) toggleSidebarOpen()
|
||||
}}
|
||||
>
|
||||
{route.icon}
|
||||
<div
|
||||
className='tw:p-1.5 tw:rounded-selector tw:text-white'
|
||||
style={{ backgroundColor: route.color ?? '#777' }}
|
||||
>
|
||||
{route.icon}
|
||||
</div>
|
||||
<span
|
||||
className={`${appState.sideBarSlim ? 'tw:hidden' : ''}`}
|
||||
data-te-sidenav-slim='false'
|
||||
@ -72,7 +78,8 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
|
||||
{(location.pathname.includes(route.path) && route.path.length > 1) ||
|
||||
location.pathname === route.path ? (
|
||||
<span
|
||||
className='tw:absolute tw:inset-y-0 tw:left-0 tw:w-1 tw:rounded-tr-md tw:rounded-br-md tw:bg-primary '
|
||||
className='tw:absolute tw:inset-y-0 tw:left-0 tw:w-1 tw:rounded-tr-md tw:rounded-br-md'
|
||||
style={{ backgroundColor: route.color ?? '#777' }}
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
) : null}
|
||||
@ -104,7 +111,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
|
||||
target={route.blank ? '_blank' : '_self'}
|
||||
to={route.path}
|
||||
className={({ isActive }) =>
|
||||
`${isActive ? 'tw:font-semibold tw:bg-base-200 tw:rounded-none!' : 'tw:font-normal tw:rounded-none!'}`
|
||||
`tw:px-4 ${isActive ? 'tw:font-semibold tw:bg-base-200 tw:rounded-none!' : 'tw:font-normal tw:rounded-none!'}`
|
||||
}
|
||||
onClick={() => {
|
||||
if (screen.width < 640 && !appState.sideBarSlim) toggleSidebarOpen()
|
||||
|
||||
@ -94,7 +94,9 @@ export const UserControl = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='tw:ml-2 tw:mr-2'>{userProfile.name ?? user?.first_name}</div>
|
||||
<div className='tw:ml-2 tw:mr-2 tw:hidden tw:sm:block'>
|
||||
{userProfile.name ?? user?.first_name}
|
||||
</div>
|
||||
</Link>
|
||||
<div className='tw:dropdown tw:dropdown-end'>
|
||||
<label tabIndex={0} className='tw:btn tw:btn-ghost tw:btn-square'>
|
||||
|
||||
@ -1,12 +1,34 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Apply a theme based on the saved preference, a provided default theme, or the user's system preference.
|
||||
* Falls back to the system dark/light preference when no theme is provided.
|
||||
*/
|
||||
export const useTheme = (defaultTheme = 'default') => {
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const initialTheme = savedTheme ? (JSON.parse(savedTheme) as string) : defaultTheme
|
||||
if (initialTheme !== 'default') {
|
||||
document.documentElement.setAttribute('data-theme', defaultTheme)
|
||||
localStorage.setItem('theme', JSON.stringify(initialTheme))
|
||||
const savedThemeRaw = localStorage.getItem('theme')
|
||||
const savedTheme = savedThemeRaw ? (JSON.parse(savedThemeRaw) as string) : undefined
|
||||
|
||||
const prefersDark =
|
||||
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: false
|
||||
|
||||
const fallbackTheme =
|
||||
defaultTheme && defaultTheme !== 'default' ? defaultTheme : prefersDark ? 'dark' : 'light'
|
||||
|
||||
const themeToApply = savedTheme ?? fallbackTheme
|
||||
|
||||
if (themeToApply === 'default') {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
localStorage.removeItem('theme')
|
||||
return
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-theme', themeToApply)
|
||||
|
||||
if (!savedTheme || savedTheme !== themeToApply) {
|
||||
localStorage.setItem('theme', JSON.stringify(themeToApply))
|
||||
}
|
||||
}, [defaultTheme])
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { useState } from 'react'
|
||||
import SVG from 'react-inlinesvg'
|
||||
|
||||
import PlusSVG from '#assets/plus.svg'
|
||||
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
||||
import { useLayers } from '#components/Map/hooks/useLayers'
|
||||
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
|
||||
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
|
||||
|
||||
export default function AddButton({
|
||||
triggerAction,
|
||||
@ -15,6 +17,9 @@ export default function AddButton({
|
||||
const layers = useLayers()
|
||||
const hasUserPermission = useHasUserPermission()
|
||||
const appState = useAppState()
|
||||
const { width } = useWindowDimensions()
|
||||
const isMobile = width < 768
|
||||
const [hideTooltips, setHideTooltips] = useState(false)
|
||||
|
||||
const canAddItems = () => {
|
||||
let canAdd = false
|
||||
@ -30,6 +35,14 @@ export default function AddButton({
|
||||
return canAdd
|
||||
}
|
||||
|
||||
const handleLayerClick = (layer: any) => {
|
||||
triggerAction(layer)
|
||||
// Verstecke Tooltips auf Mobile nach Layer-Auswahl
|
||||
if (isMobile) {
|
||||
setHideTooltips(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{canAddItems() ? (
|
||||
@ -37,10 +50,23 @@ export default function AddButton({
|
||||
<label
|
||||
tabIndex={0}
|
||||
className='tw:z-500 tw:btn tw:btn-circle tw:btn-lg tw:shadow tw:bg-base-100'
|
||||
onClick={() => {
|
||||
if (hideTooltips) {
|
||||
setHideTooltips(false)
|
||||
}
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
if (hideTooltips) {
|
||||
setHideTooltips(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SVG src={PlusSVG} className='tw:h-5 tw:w-5' />
|
||||
</label>
|
||||
<ul tabIndex={0} className='tw:dropdown-content tw:pr-1 tw:list-none'>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='tw:dropdown-content tw:pr-1 tw:list-none tw:space-y-3 tw:pb-3'
|
||||
>
|
||||
{layers.map(
|
||||
(layer) =>
|
||||
layer.api?.createItem &&
|
||||
@ -48,16 +74,23 @@ export default function AddButton({
|
||||
layer.listed && (
|
||||
<li key={layer.name}>
|
||||
<a>
|
||||
<div className='tw:tooltip tw:tooltip-left' data-tip={layer.menuText}>
|
||||
<div
|
||||
className={`tw:tooltip tw:tooltip-left ${isMobile && !hideTooltips ? 'tw:tooltip-open' : ''}`}
|
||||
data-tip={layer.menuText}
|
||||
style={
|
||||
{
|
||||
'--tooltip-color': layer.menuColor || '#777',
|
||||
'--tooltip-text-color': '#ffffff',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='tw:z-500 tw:border-0 tw:p-0 tw:mb-3 tw:w-10 tw:h-10 tw:cursor-pointer tw:rounded-full tw:mouse tw:drop-shadow-md tw:transition tw:ease-in tw:duration-200 tw:focus:outline-hidden tw:flex tw:items-center tw:justify-center'
|
||||
className='tw:z-500 tw:border-0 tw:p-0 tw:w-10 tw:h-10 tw:cursor-pointer tw:rounded-full tw:mouse tw:drop-shadow-md tw:transition tw:ease-in tw:duration-200 tw:focus:outline-hidden tw:flex tw:items-center tw:justify-center'
|
||||
style={{ backgroundColor: layer.menuColor || '#777' }}
|
||||
onClick={() => {
|
||||
triggerAction(layer)
|
||||
}}
|
||||
onClick={() => handleLayerClick(layer)}
|
||||
onTouchEnd={(e) => {
|
||||
triggerAction(layer)
|
||||
handleLayerClick(layer)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
|
||||
@ -38,8 +38,9 @@ export function LayerControl({ expandLayerControl = false }: { expandLayerContro
|
||||
id={layer.name}
|
||||
onChange={() => toggleVisibleLayer(layer)}
|
||||
type='checkbox'
|
||||
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
|
||||
className='tw:checkbox tw:checkbox-xs tw:checkbox-success tw:text-white'
|
||||
checked={isLayerVisible(layer)}
|
||||
style={{ backgroundColor: layer.menuColor, borderColor: layer.menuColor }}
|
||||
/>
|
||||
<span className='tw:text-sm tw:label-text tw:mx-2 tw:cursor-pointer'>
|
||||
{layer.name}
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import type { Item } from '#types/Item'
|
||||
import type { LayerProps } from '#types/LayerProps'
|
||||
|
||||
export const SelectPosition = ({
|
||||
setSelectNewItemPosition,
|
||||
selectNewItemPosition,
|
||||
}: {
|
||||
setSelectNewItemPosition: React.Dispatch<React.SetStateAction<Item | LayerProps | null>>
|
||||
selectNewItemPosition?: Item | LayerProps | null
|
||||
}) => {
|
||||
return (
|
||||
<div className='tw:animate-pulseGrow tw:button tw:z-1000 tw:absolute tw:right-5 tw:top-4 tw:drop-shadow-md'>
|
||||
<label
|
||||
className='tw:btn tw:btn-sm tw:rounded-2xl tw:btn-circle tw:btn-ghost tw:hover:bg-transparent tw:absolute tw:right-0 tw:top-0 tw:text-gray-600'
|
||||
onClick={() => {
|
||||
setSelectNewItemPosition(null)
|
||||
}}
|
||||
>
|
||||
<p className='tw:text-center '>✕</p>
|
||||
</label>
|
||||
<div className='tw:alert tw:bg-base-100 tw:text-base-content'>
|
||||
<div>
|
||||
{selectNewItemPosition && 'layer' in selectNewItemPosition && (
|
||||
<span className='tw:text-lg'>
|
||||
Select new position of <b>{selectNewItemPosition.name}</b> on the map!
|
||||
</span>
|
||||
)}
|
||||
{selectNewItemPosition && 'markerIcon' in selectNewItemPosition && (
|
||||
<span className='tw:text-lg'>Select position on the map!</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
lib/src/Components/Map/Subcomponents/SelectPositionToast.tsx
Normal file
142
lib/src/Components/Map/Subcomponents/SelectPositionToast.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { SVG } from '#components/AppShell'
|
||||
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { LayerProps } from '#types/LayerProps'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
const isItemSelection = (value: Item | LayerProps): value is Item => 'layer' in value
|
||||
|
||||
interface SelectPositionToastProps {
|
||||
selectNewItemPosition: Item | LayerProps | null
|
||||
setSelectNewItemPosition: Dispatch<SetStateAction<Item | LayerProps | null>>
|
||||
}
|
||||
|
||||
export const SelectPositionToast = ({
|
||||
selectNewItemPosition,
|
||||
setSelectNewItemPosition,
|
||||
}: SelectPositionToastProps) => {
|
||||
const toastIdRef = useRef<string | number | null>(null)
|
||||
const toastId = 'select-position-toast'
|
||||
const appState = useAppState()
|
||||
|
||||
// Escape-Key Listener
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && selectNewItemPosition) {
|
||||
toast.dismiss(toastId)
|
||||
toastIdRef.current = null
|
||||
setSelectNewItemPosition(null)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleEscape)
|
||||
return () => window.removeEventListener('keydown', handleEscape)
|
||||
}, [selectNewItemPosition, setSelectNewItemPosition])
|
||||
|
||||
const toastContent = useMemo(() => {
|
||||
if (!selectNewItemPosition) return null
|
||||
|
||||
const itemSelection = isItemSelection(selectNewItemPosition)
|
||||
const layer: LayerProps | null = itemSelection
|
||||
? (selectNewItemPosition.layer ?? null)
|
||||
: selectNewItemPosition
|
||||
const markerIcon = itemSelection
|
||||
? (selectNewItemPosition.layer?.markerIcon ?? selectNewItemPosition.markerIcon)
|
||||
: selectNewItemPosition.markerIcon
|
||||
const message = itemSelection
|
||||
? `Select the new position of ${selectNewItemPosition.name ?? ''} on the map!`
|
||||
: 'Select the position on the map!'
|
||||
|
||||
const dismissToast = () => {
|
||||
toast.dismiss(toastId)
|
||||
toastIdRef.current = null
|
||||
setSelectNewItemPosition(null)
|
||||
}
|
||||
|
||||
const assetsApiUrl = appState.assetsApi.url
|
||||
const assetsBaseUrl: string | undefined = assetsApiUrl.length > 0 ? assetsApiUrl : undefined
|
||||
|
||||
const resolveColor = (...candidates: (string | null | undefined)[]): string => {
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === 'string' && candidate.length > 0) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return '#777'
|
||||
}
|
||||
|
||||
const itemColor =
|
||||
itemSelection && typeof selectNewItemPosition.color === 'string'
|
||||
? selectNewItemPosition.color
|
||||
: undefined
|
||||
const itemLayerColor =
|
||||
itemSelection && typeof selectNewItemPosition.layer?.menuColor === 'string'
|
||||
? selectNewItemPosition.layer.menuColor
|
||||
: undefined
|
||||
const layerMenuColor = layer?.menuColor
|
||||
const baseLayerColor = typeof layerMenuColor === 'string' ? layerMenuColor : undefined
|
||||
|
||||
const backgroundColor = resolveColor(itemColor, itemLayerColor, baseLayerColor)
|
||||
const iconSrc: string | undefined =
|
||||
markerIcon?.image != null
|
||||
? assetsBaseUrl
|
||||
? `${assetsBaseUrl}${markerIcon.image}`
|
||||
: markerIcon.image
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='tw:flex tw:flex-row tw:items-center'>
|
||||
<div
|
||||
className='tw:flex tw:items-center tw:gap-3 tw:p-2 tw:rounded-selector tw:text-white tw:mr-2'
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
{iconSrc && <SVG src={iconSrc} className='tw:h-4 tw:w-4 tw:object-contain' />}
|
||||
</div>
|
||||
<div className='tw:flex tw:flex-col tw:gap-0.5'>
|
||||
<span className='tw:text-sm'>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={dismissToast}
|
||||
className='tw:btn tw:btn-sm tw:btn-ghost tw:btn-circle tw:absolute tw:top-0 tw:right-0'
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}, [appState.assetsApi.url, selectNewItemPosition, setSelectNewItemPosition, toastId])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectNewItemPosition && toastContent) {
|
||||
if (!toastIdRef.current) {
|
||||
toastIdRef.current = toast(toastContent, {
|
||||
toastId,
|
||||
autoClose: false,
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
draggable: false,
|
||||
})
|
||||
} else {
|
||||
toast.update(toastId, {
|
||||
render: toastContent,
|
||||
autoClose: false,
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
draggable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectNewItemPosition && toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current)
|
||||
toastIdRef.current = null
|
||||
}
|
||||
}, [selectNewItemPosition, toastContent, toastId])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -115,6 +115,7 @@ function UtopiaMap({
|
||||
zoom={zoom}
|
||||
zoomControl={showZoomControl}
|
||||
maxZoom={19}
|
||||
minZoom={2}
|
||||
>
|
||||
<UtopiaMapInner
|
||||
geo={geo}
|
||||
|
||||
@ -43,7 +43,7 @@ import { SearchControl } from './Subcomponents/Controls/SearchControl'
|
||||
import { TagsControl } from './Subcomponents/Controls/TagsControl'
|
||||
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
|
||||
import { MapLibreLayer } from './Subcomponents/MapLibreLayer'
|
||||
import { SelectPosition } from './Subcomponents/SelectPosition'
|
||||
import { SelectPositionToast } from './Subcomponents/SelectPositionToast'
|
||||
|
||||
import type { Feature, Geometry as GeoJSONGeometry, GeoJsonObject } from 'geojson'
|
||||
|
||||
@ -332,12 +332,10 @@ export function UtopiaMapInner({
|
||||
)}
|
||||
<MapEventListener />
|
||||
<AddButton triggerAction={setSelectNewItemPosition} />
|
||||
{selectNewItemPosition != null && (
|
||||
<SelectPosition
|
||||
selectNewItemPosition={selectNewItemPosition}
|
||||
setSelectNewItemPosition={setSelectNewItemPosition}
|
||||
/>
|
||||
)}
|
||||
<SelectPositionToast
|
||||
selectNewItemPosition={selectNewItemPosition}
|
||||
setSelectNewItemPosition={setSelectNewItemPosition}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.leaflet-container{
|
||||
background-color: var(--color-base-300);
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a{
|
||||
color: #000 !important;
|
||||
}
|
||||
@ -62,7 +66,6 @@ display: none !important;
|
||||
|
||||
.leaflet-tooltip {
|
||||
border-radius: var(--radius-box);
|
||||
transition: opacity 500ms;
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
|
||||
|
||||
@ -78,3 +78,19 @@
|
||||
.modal-box {
|
||||
max-height: calc(100dvh - 2em);
|
||||
}
|
||||
|
||||
/* Custom tooltip colors for layer-based tooltips */
|
||||
.tw\:tooltip[style*='--tooltip-color']::before,
|
||||
.tw\:tooltip[style*='--tooltip-color']::after {
|
||||
background-color: rgba(0,0,0,0.67);
|
||||
}
|
||||
|
||||
.tw\:tooltip[style*='--tooltip-color']::before {
|
||||
color: var(--tooltip-text-color, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Make sure tooltips sit above Leaflet panes when used inside map popups */
|
||||
.tw\:tooltip::before,
|
||||
.tw\:tooltip::after {
|
||||
z-index: 4000;
|
||||
}
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -121,7 +121,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"cypress": "^14.0.3",
|
||||
"daisyui": "^5.0.6",
|
||||
"daisyui": "^5.2.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
@ -7004,9 +7004,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.1.29",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.1.29.tgz",
|
||||
"integrity": "sha512-4eZhqCXO7CJVNGytTZEIQYJz3fah2gPleuqp4qUD4fD8WoEQIYzKwlOewi8nPaz6NX7vvSLZ+YSjt5Z5zqacGw==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.2.3.tgz",
|
||||
"integrity": "sha512-sldBQUIFCsSPoF4LvoHhIi9GnvBX/3aZD9NoTOvpTSX8sDjO484wQx7yEvRyREMpn4rZMvQSKKskHAHdM8+B4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@ -9221,9 +9221,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom/node_modules/@types/node": {
|
||||
"version": "20.19.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
|
||||
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
|
||||
"version": "20.19.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user