feat(gallery): add tag filtering options and UI components

- Introduced a new tag filtering mode with options for 'union' and 'intersection'.
- Added Checkbox and Switch components for user interface interactions.
- Updated the FilterPanel to include tag filter mode selection.
- Enhanced photo filtering logic to accommodate the new tag filter mode.
- Updated localization files to include new tag filter labels.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-08-31 23:38:43 +08:00
parent 473fc90add
commit 1cc5c76ee7
18 changed files with 511 additions and 95 deletions

View File

@@ -99,7 +99,11 @@ This is a pnpm workspace with multiple applications and packages:
- Use flat keys with `.` separation (e.g., `exif.camera.model`)
- Support pluralization with `_one` and `_other` suffixes
- Modify English first, then other languages (ESLint auto-removes unused keys)
- Avoid nested key conflicts in flat structure
- **CRITICAL: Avoid nested key conflicts in flat structure**
- ❌ WRONG: `"action.tag.mode.and": "AND"` + `"action.tag.mode.and.tooltip": "..."`
- ✅ CORRECT: `"action.tag.mode.and": "AND"` + `"action.tag.tooltip.and": "..."`
- Rule: A key cannot be both a string value AND a parent object
- Each key must be completely independent in the flat structure
### Testing Strategy
- Check README.md and package.json scripts for test commands

View File

@@ -26,6 +26,7 @@
"@lobehub/fluent-emoji": "2.0.0",
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
"@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-context-menu": "2.2.15",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "2.1.15",
@@ -33,6 +34,7 @@
"@radix-ui/react-popover": "1.1.14",
"@radix-ui/react-scroll-area": "1.2.9",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tooltip": "1.2.7",
"@react-hook/window-size": "3.1.1",
"@remixicon/react": "4.6.0",

View File

@@ -10,6 +10,7 @@ export const gallerySettingAtom = atom({
selectedCameras: [] as string[], // Selected camera display names
selectedLenses: [] as string[], // Selected lens display names
selectedRatings: null as number | null, // Selected minimum rating threshold
tagFilterMode: 'union' as 'union' | 'intersection', // Tag filtering logic mode
tagSearchQuery: '' as string,
cameraSearchQuery: '' as string, // Camera search query
lensSearchQuery: '' as string, // Lens search query

View File

@@ -7,6 +7,8 @@ import { gallerySettingAtom } from '~/atoms/app'
import { Button } from '~/components/ui/button'
import { clsxm } from '~/lib/cn'
import { Checkbox } from '../ui/checkbox'
const allTags = photoLoader.getAllTags()
const allCameras = photoLoader.getAllCameras()
const allLenses = photoLoader.getAllLenses()
@@ -134,6 +136,16 @@ export const FilterPanel = () => {
setRatingSearchQuery('')
}, [setGallerySetting])
const setTagFilterMode = useCallback(
(mode: 'union' | 'intersection') => {
setGallerySetting((prev) => ({
...prev,
tagFilterMode: mode,
}))
},
[setGallerySetting],
)
// Search handlers with useCallback
const onTagSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -266,13 +278,13 @@ export const FilterPanel = () => {
data: [] as string[],
filteredData: [] as string[],
selectedItems: [] as string[],
searchQuery: '',
searchPlaceholder: '',
searchQuery: ratingSearchQuery,
searchPlaceholder: t('action.rating.search'),
emptyMessage: '',
notFoundMessage: '',
onToggle: () => {},
onClear: clearRatings,
onSearchChange: () => {},
onSearchChange: onRatingSearchChange,
},
],
[
@@ -291,7 +303,6 @@ export const FilterPanel = () => {
toggleTag,
toggleCamera,
toggleLens,
setRating,
clearTags,
clearCameras,
clearLenses,
@@ -328,6 +339,7 @@ export const FilterPanel = () => {
selectedCameras: [],
selectedLenses: [],
selectedRatings: null,
tagFilterMode: 'union',
tagSearchQuery: '',
cameraSearchQuery: '',
lensSearchQuery: '',
@@ -385,7 +397,7 @@ export const FilterPanel = () => {
<i className="i-mingcute-search-line absolute top-1/2 right-3 -translate-y-1/2 text-gray-400" />
</div>
)}
{currentTab.count > 0 && activeTab != 'ratings' && (
{currentTab.count > 0 && activeTab !== 'ratings' && (
<Button
variant="ghost"
size="xs"
@@ -416,23 +428,46 @@ export const FilterPanel = () => {
{currentTab.notFoundMessage}
</div>
) : (
<div className="pb-safe-offset-4 lg:pb-safe -mx-4 -mb-4 max-h-64 overflow-y-auto px-4 lg:mx-0 lg:mb-0 lg:px-0">
{(currentTab.filteredData).map((item) => (
<div
key={item}
onClick={() => currentTab.onToggle(item)}
className={clsxm(
'hover:bg-accent/50 flex cursor-pointer items-center rounded-md bg-transparent px-2 py-2.5 transition-colors lg:py-2',
currentTab.selectedItems.includes(item) && 'bg-accent/20',
)}
>
<span className="mr-2 flex-1 truncate">{item}</span>
{currentTab.selectedItems.includes(item) && (
<i className="i-mingcute-check-line ml-auto text-green-600 dark:text-green-400" />
)}
<>
{/* Tag Filter Mode Toggle - Only show for tags tab when tags are selected */}
{activeTab === 'tags' && (
<div className="mb-3 flex items-center gap-3 px-2 text-xs text-gray-600 dark:text-gray-400">
<span>{t('action.tag.match.label')}</span>
<label className="flex cursor-pointer items-center gap-1.5">
<Checkbox
checked={gallerySetting.tagFilterMode === 'union'}
onCheckedChange={() => setTagFilterMode('union')}
/>
<span>{t('action.tag.match.any')}</span>
</label>
<label className="flex cursor-pointer items-center gap-1.5">
<Checkbox
checked={gallerySetting.tagFilterMode === 'intersection'}
onCheckedChange={() => setTagFilterMode('intersection')}
/>
<span>{t('action.tag.match.all')}</span>
</label>
</div>
))}
</div>
)}
<div className="pb-safe-offset-4 lg:pb-safe -mx-4 -mb-4 max-h-64 overflow-y-auto px-4 lg:mx-0 lg:mb-0 lg:px-0">
{currentTab.filteredData.map((item) => (
<div
key={item}
onClick={() => currentTab.onToggle(item)}
className={clsxm(
'hover:bg-accent/50 flex cursor-pointer items-center rounded-md bg-transparent px-2 py-2.5 transition-colors lg:py-2',
currentTab.selectedItems.includes(item) && 'bg-accent/20',
)}
>
<span className="mr-2 flex-1 truncate">{item}</span>
{currentTab.selectedItems.includes(item) && (
<i className="i-mingcute-check-line ml-auto text-green-600 dark:text-green-400" />
)}
</div>
))}
</div>
</>
)}
</div>
</div>
@@ -471,7 +506,7 @@ const StarRating = ({
className={clsxm(
'text-2xl',
rating <= (hoveredRating ?? value ?? 0)
? 'i-mingcute-star-fill text-yellow-400'
? 'i-mingcute-star-fill text-yellow-400 dark:text-yellow-500'
: 'i-mingcute-star-line text-gray-300 dark:text-gray-600',
)}
/>

View File

@@ -0,0 +1,140 @@
'use client'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import type { HTMLMotionProps } from 'motion/react'
import { m as motion } from 'motion/react'
import * as React from 'react'
import { clsxm } from '~/lib/cn'
type CheckboxProps = React.ComponentProps<typeof CheckboxPrimitive.Root> &
HTMLMotionProps<'button'> & {
indeterminate?: boolean
}
function Checkbox({
className,
onCheckedChange,
indeterminate,
...props
}: CheckboxProps) {
const [isChecked, setIsChecked] = React.useState(
props?.checked ?? props?.defaultChecked ?? false,
)
React.useEffect(() => {
if (props?.checked !== undefined) setIsChecked(props.checked)
}, [props?.checked])
// Determine the actual state including indeterminate
const checkboxState = indeterminate
? 'indeterminate'
: isChecked
? 'checked'
: 'unchecked'
const handleCheckedChange = React.useCallback(
(checked: boolean) => {
setIsChecked(checked)
onCheckedChange?.(checked)
},
[onCheckedChange],
)
return (
<CheckboxPrimitive.Root
{...props}
onCheckedChange={handleCheckedChange}
asChild
>
<motion.button
data-slot="checkbox"
className={clsxm(
'peer size-5 flex items-center justify-center shrink-0 rounded-sm bg-fill transition-colors duration-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent data-[state=checked]:text-white',
indeterminate && 'bg-accent text-white',
className,
)}
whileTap={{ scale: 0.95 }}
whileHover={{ scale: 1.05 }}
{...props}
>
<CheckboxPrimitive.Indicator forceMount asChild>
<motion.svg
data-slot="checkbox-indicator"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="3.5"
stroke="currentColor"
className="size-3.5"
initial="unchecked"
animate={checkboxState}
>
{/* Checkmark path */}
<motion.path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
variants={{
checked: {
pathLength: 1,
opacity: 1,
transition: {
duration: 0.2,
delay: 0.2,
},
},
unchecked: {
pathLength: 0,
opacity: 0,
transition: {
duration: 0.2,
},
},
indeterminate: {
pathLength: 0,
opacity: 0,
transition: {
duration: 0.1,
},
},
}}
/>
{/* Indeterminate line */}
<motion.path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12h12"
variants={{
checked: {
pathLength: 0,
opacity: 0,
transition: {
duration: 0.1,
},
},
unchecked: {
pathLength: 0,
opacity: 0,
transition: {
duration: 0.1,
},
},
indeterminate: {
pathLength: 1,
opacity: 1,
transition: {
duration: 0.2,
delay: 0.1,
},
},
}}
/>
</motion.svg>
</CheckboxPrimitive.Indicator>
</motion.button>
</CheckboxPrimitive.Root>
)
}
export { Checkbox, type CheckboxProps }

View File

@@ -0,0 +1 @@
export * from './Checkbox'

View File

@@ -0,0 +1,114 @@
'use client'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import type { HTMLMotionProps } from 'motion/react'
import { m as motion } from 'motion/react'
import * as React from 'react'
import { clsxm as cn } from '~/lib/cn'
type SwitchProps = React.ComponentProps<typeof SwitchPrimitives.Root> &
HTMLMotionProps<'button'> & {
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
thumbIcon?: React.ReactNode
}
function Switch({
className,
leftIcon,
rightIcon,
thumbIcon,
onCheckedChange,
...props
}: SwitchProps) {
const [isChecked, setIsChecked] = React.useState(
props?.checked ?? props?.defaultChecked ?? false,
)
const [isTapped, setIsTapped] = React.useState(false)
React.useEffect(() => {
if (props?.checked !== undefined) setIsChecked(props.checked)
}, [props?.checked])
const handleCheckedChange = React.useCallback(
(checked: boolean) => {
setIsChecked(checked)
onCheckedChange?.(checked)
},
[onCheckedChange],
)
return (
<SwitchPrimitives.Root
{...props}
onCheckedChange={handleCheckedChange}
asChild
>
<motion.button
data-slot="switch"
className={cn(
'focus-visible:ring-accent focus-visible:ring-offset-background data-[state=checked]:bg-accent data-[state=unchecked]:bg-fill-secondary relative flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full p-[3px] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:justify-end data-[state=unchecked]:justify-start',
className,
)}
whileTap="tap"
initial={false}
onTapStart={() => setIsTapped(true)}
onTapCancel={() => setIsTapped(false)}
onTap={() => setIsTapped(false)}
{...props}
>
{leftIcon && (
<motion.div
data-slot="switch-left-icon"
animate={
isChecked ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 }
}
transition={{ type: 'spring', bounce: 0 }}
className="text-text-secondary absolute top-1/2 left-1 -translate-y-1/2 [&_svg]:size-3"
>
{typeof leftIcon !== 'string' ? leftIcon : null}
</motion.div>
)}
{rightIcon && (
<motion.div
data-slot="switch-right-icon"
animate={
isChecked ? { scale: 0, opacity: 0 } : { scale: 1, opacity: 1 }
}
transition={{ type: 'spring', bounce: 0 }}
className="text-text-secondary absolute top-1/2 right-1 -translate-y-1/2 [&_svg]:size-3"
>
{typeof rightIcon !== 'string' ? rightIcon : null}
</motion.div>
)}
<SwitchPrimitives.Thumb asChild>
<motion.div
data-slot="switch-thumb"
whileTap="tab"
className={cn(
'bg-background text-text-secondary relative z-[1] flex items-center justify-center rounded-full shadow-lg ring-0 [&_svg]:size-3',
)}
layout
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
style={{
width: 18,
height: 18,
}}
animate={
isTapped
? { width: 21, transition: { duration: 0.1 } }
: { width: 18, transition: { duration: 0.1 } }
}
>
{thumbIcon && typeof thumbIcon !== 'string' ? thumbIcon : null}
</motion.div>
</SwitchPrimitives.Thumb>
</motion.button>
</SwitchPrimitives.Root>
)
}
export { Switch, type SwitchProps }

View File

@@ -19,15 +19,22 @@ const filterAndSortPhotos = (
selectedLenses: string[],
selectedRatings: number | null,
sortOrder: 'asc' | 'desc',
tagFilterMode: 'union' | 'intersection' = 'union',
) => {
// 根据 tags、cameras、lenses 和 ratings 筛选
let filteredPhotos = data
// Tags 筛选:照片必须包含至少一个选中的标签
// Tags 筛选:根据模式进行并集或交集筛选
if (selectedTags.length > 0) {
filteredPhotos = filteredPhotos.filter((photo) =>
selectedTags.some((tag) => photo.tags.includes(tag)),
)
filteredPhotos = filteredPhotos.filter((photo) => {
if (tagFilterMode === 'intersection') {
// 交集模式:照片必须包含所有选中的标签
return selectedTags.every((tag) => photo.tags.includes(tag))
} else {
// 并集模式:照片必须包含至少一个选中的标签
return selectedTags.some((tag) => photo.tags.includes(tag))
}
})
}
// Cameras 筛选:照片的相机必须匹配选中的相机之一
@@ -93,6 +100,7 @@ export const getFilteredPhotos = () => {
currentGallerySetting.selectedLenses,
currentGallerySetting.selectedRatings,
currentGallerySetting.sortOrder,
currentGallerySetting.tagFilterMode,
)
}
@@ -103,6 +111,7 @@ export const usePhotos = () => {
selectedCameras,
selectedLenses,
selectedRatings,
tagFilterMode,
} = useAtomValue(gallerySettingAtom)
const masonryItems = useMemo(() => {
@@ -112,6 +121,7 @@ export const usePhotos = () => {
selectedLenses,
selectedRatings,
sortOrder,
tagFilterMode,
)
}, [
sortOrder,
@@ -119,6 +129,7 @@ export const usePhotos = () => {
selectedCameras,
selectedLenses,
selectedRatings,
tagFilterMode,
])
return masonryItems

View File

@@ -95,12 +95,17 @@ const useStateRestoreFromUrl = () => {
const ratingsFromSearchParams = searchParams.get('rating')
? Number(searchParams.get('rating'))
: null
const tagModeFromSearchParams = searchParams.get('tag_mode') as
| 'union'
| 'intersection'
| null
if (
tagsFromSearchParams ||
camerasFromSearchParams ||
lensesFromSearchParams ||
ratingsFromSearchParams !== null
ratingsFromSearchParams !== null ||
tagModeFromSearchParams
) {
setGallerySetting((prev) => ({
...prev,
@@ -108,14 +113,20 @@ const useStateRestoreFromUrl = () => {
selectedCameras: camerasFromSearchParams || prev.selectedCameras,
selectedLenses: lensesFromSearchParams || prev.selectedLenses,
selectedRatings: ratingsFromSearchParams ?? prev.selectedRatings,
tagFilterMode: tagModeFromSearchParams || prev.tagFilterMode,
}))
}
}, [openViewer, photoId, searchParams, setGallerySetting])
}
const useSyncStateToUrl = () => {
const { selectedTags, selectedCameras, selectedLenses, selectedRatings } =
useAtomValue(gallerySettingAtom)
const {
selectedTags,
selectedCameras,
selectedLenses,
selectedRatings,
tagFilterMode,
} = useAtomValue(gallerySettingAtom)
const [_, setSearchParams] = useSearchParams()
const navigate = useNavigate()
@@ -149,19 +160,22 @@ const useSyncStateToUrl = () => {
const cameras = selectedCameras.join(',')
const lenses = selectedLenses.join(',')
const rating = selectedRatings?.toString() ?? ''
const tagMode = tagFilterMode === 'union' ? '' : tagFilterMode
setSearchParams((search) => {
const currentTags = search.get('tags')
const currentCameras = search.get('cameras')
const currentLenses = search.get('lenses')
const currentRating = search.get('rating')
const currentTagMode = search.get('tag_mode')
// Check if anything has changed
if (
currentTags === tags &&
currentCameras === cameras &&
currentLenses === lenses &&
currentRating === rating
currentRating === rating &&
currentTagMode === tagMode
) {
return search
}
@@ -196,7 +210,21 @@ const useSyncStateToUrl = () => {
newer.delete('rating')
}
// Update tag filter mode
if (tagMode) {
newer.set('tag_mode', tagMode)
} else {
newer.delete('tag_mode')
}
return newer
})
}, [selectedTags, selectedCameras, selectedLenses, selectedRatings, setSearchParams])
}, [
selectedTags,
selectedCameras,
selectedLenses,
selectedRatings,
tagFilterMode,
setSearchParams,
])
}

View File

@@ -9,7 +9,6 @@ import { Spring } from '~/lib/spring'
import { ContextMenuProvider } from './context-menu-provider'
import { EventProvider } from './event-provider'
import { I18nProvider } from './i18n-provider'
import { SettingSync } from './setting-sync'
import { StableRouterProvider } from './stable-router-provider'
export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
@@ -18,7 +17,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
<Provider store={jotaiStore}>
<EventProvider />
<StableRouterProvider />
<SettingSync />
<ContextMenuProvider />
<I18nProvider>{children}</I18nProvider>
</Provider>

View File

@@ -1,7 +0,0 @@
const useUISettingSync = () => {}
export const SettingSync = () => {
useUISettingSync()
return null
}

View File

@@ -23,6 +23,11 @@
"action.tag.clear": "Clear",
"action.tag.empty": "No tags available",
"action.tag.filter": "Tag Filter",
"action.tag.match.all": "All tags",
"action.tag.match.any": "Any tag",
"action.tag.match.label": "Match:",
"action.tag.mode.and": "AND",
"action.tag.mode.or": "OR",
"action.tag.not-found": "No tags match your search",
"action.tag.search": "Search Tags",
"action.view.github": "View GitHub Repository",

View File

@@ -23,6 +23,8 @@
"action.tag.clear": "クリア",
"action.tag.empty": "タグがありません",
"action.tag.filter": "タグフィルター",
"action.tag.mode.and": "AND",
"action.tag.mode.or": "OR",
"action.tag.not-found": "検索に一致するタグがありません",
"action.tag.search": "タグ検索",
"action.view.github": "GitHub リポジトリを表示",

View File

@@ -23,6 +23,8 @@
"action.tag.clear": "지우기",
"action.tag.empty": "태그가 없습니다",
"action.tag.filter": "태그 필터",
"action.tag.mode.and": "AND",
"action.tag.mode.or": "OR",
"action.tag.not-found": "검색과 일치하는 태그가 없습니다",
"action.tag.search": "태그 검색",
"action.view.github": "GitHub 리포지토리 보기",

View File

@@ -23,6 +23,8 @@
"action.tag.clear": "清除",
"action.tag.empty": "暂无标签",
"action.tag.filter": "标签筛选",
"action.tag.mode.and": "AND",
"action.tag.mode.or": "OR",
"action.tag.not-found": "没有标签匹配您的搜索",
"action.tag.search": "搜索标签",
"action.view.github": "查看 GitHub 仓库",

View File

@@ -23,6 +23,8 @@
"action.tag.clear": "清除",
"action.tag.empty": "暫無標籤",
"action.tag.filter": "標籤篩選",
"action.tag.mode.and": "AND",
"action.tag.mode.or": "OR",
"action.tag.not-found": "沒有標籤符合您的搜尋",
"action.tag.search": "搜尋標籤",
"action.view.github": "查看 GitHub 倉庫",

View File

@@ -23,6 +23,8 @@
"action.tag.clear": "清除",
"action.tag.empty": "暫無標籤",
"action.tag.filter": "標籤篩選",
"action.tag.mode.and": "AND",
"action.tag.mode.or": "OR",
"action.tag.not-found": "沒有標籤符合您的搜尋",
"action.tag.search": "搜尋標籤",
"action.view.github": "檢視 GitHub 存放庫",

181
pnpm-lock.yaml generated
View File

@@ -281,6 +281,9 @@ importers:
'@radix-ui/react-avatar':
specifier: 1.1.10
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-checkbox':
specifier: 1.3.3
version: 1.3.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-context-menu':
specifier: 2.2.15
version: 2.2.15(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -302,6 +305,9 @@ importers:
'@radix-ui/react-slot':
specifier: 1.2.3
version: 1.2.3(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-switch':
specifier: 1.2.6
version: 1.2.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-tooltip':
specifier: 1.2.7
version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2267,9 +2273,6 @@ packages:
'@mapbox/point-geometry@1.1.0':
resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==}
'@mapbox/tiny-sdf@2.0.6':
resolution: {integrity: sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==}
'@mapbox/tiny-sdf@2.0.7':
resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==}
@@ -2445,6 +2448,9 @@ packages:
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-arrow@1.0.2':
resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==}
peerDependencies:
@@ -2477,6 +2483,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-checkbox@1.3.3':
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
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
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -2717,6 +2736,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
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
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@1.0.2':
resolution: {integrity: sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==}
peerDependencies:
@@ -2776,6 +2808,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-switch@1.2.6':
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
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
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.0.5':
resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
peerDependencies:
@@ -2869,6 +2914,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.1':
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.0.0':
resolution: {integrity: sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==}
peerDependencies:
@@ -5283,9 +5337,6 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
earcut@3.0.1:
resolution: {integrity: sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==}
earcut@3.0.2:
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
@@ -5952,9 +6003,6 @@ packages:
engines: {node: '>=18'}
hasBin: true
gl-matrix@3.4.3:
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
gl-matrix@3.4.4:
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
@@ -7443,9 +7491,6 @@ packages:
resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==}
engines: {node: '>=12'}
potpack@2.0.0:
resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==}
potpack@2.1.0:
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
@@ -10174,7 +10219,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/helper-compilation-targets@7.27.2':
dependencies:
@@ -10273,15 +10318,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.27.3(@babel/core@7.28.3)':
dependencies:
'@babel/core': 7.28.3
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.28.0
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)':
dependencies:
'@babel/core': 7.28.3
@@ -10302,7 +10338,7 @@ snapshots:
'@babel/core': 7.28.3
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-wrap-function': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10340,8 +10376,8 @@ snapshots:
'@babel/helper-wrap-function@7.27.1':
dependencies:
'@babel/template': 7.27.2
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/traverse': 7.28.3
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -10380,7 +10416,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.3
'@babel/helper-plugin-utils': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10407,7 +10443,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.3
'@babel/helper-plugin-utils': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10441,7 +10477,7 @@ snapshots:
'@babel/core': 7.28.3
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.3)
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10488,7 +10524,7 @@ snapshots:
'@babel/helper-globals': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3)
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10502,7 +10538,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.3
'@babel/helper-plugin-utils': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10559,7 +10595,7 @@ snapshots:
'@babel/core': 7.28.3
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-plugin-utils': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10586,7 +10622,7 @@ snapshots:
'@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.3)':
dependencies:
'@babel/core': 7.28.3
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.3)
'@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10594,7 +10630,7 @@ snapshots:
'@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.3)':
dependencies:
'@babel/core': 7.28.3
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.3)
'@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10602,17 +10638,17 @@ snapshots:
'@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.3)':
dependencies:
'@babel/core': 7.28.3
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.3)
'@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3)
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.3)':
dependencies:
'@babel/core': 7.28.3
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.3)
'@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10645,7 +10681,7 @@ snapshots:
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3)
'@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.3)
'@babel/traverse': 7.28.0
'@babel/traverse': 7.28.3
transitivePeerDependencies:
- supports-color
@@ -10874,7 +10910,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.3
'@babel/helper-plugin-utils': 7.27.1
'@babel/types': 7.28.0
'@babel/types': 7.28.2
esutils: 2.0.3
'@babel/runtime@7.27.6': {}
@@ -11838,9 +11874,6 @@ snapshots:
'@mapbox/point-geometry@1.1.0': {}
'@mapbox/tiny-sdf@2.0.6':
optional: true
'@mapbox/tiny-sdf@2.0.7': {}
'@mapbox/unitbezier@0.0.1': {}
@@ -12059,6 +12092,8 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-arrow@1.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
@@ -12088,6 +12123,22 @@ snapshots:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
@@ -12370,6 +12421,16 @@ snapshots:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-primitive@1.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
@@ -12433,6 +12494,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-tooltip@1.0.5(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
@@ -12536,6 +12612,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-previous@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-rect@1.0.0(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
@@ -15136,9 +15218,6 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
earcut@3.0.1:
optional: true
earcut@3.0.2: {}
ejs@3.1.10:
@@ -16067,9 +16146,6 @@ snapshots:
- conventional-commits-filter
- conventional-commits-parser
gl-matrix@3.4.3:
optional: true
gl-matrix@3.4.4: {}
glob-parent@5.1.2:
@@ -16895,7 +16971,7 @@ snapshots:
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/mapbox-gl-supported': 3.0.0
'@mapbox/point-geometry': 0.1.0
'@mapbox/tiny-sdf': 2.0.6
'@mapbox/tiny-sdf': 2.0.7
'@mapbox/unitbezier': 0.0.1
'@mapbox/vector-tile': 1.3.1
'@mapbox/whoots-js': 3.1.0
@@ -16907,15 +16983,15 @@ snapshots:
'@types/supercluster': 7.1.3
cheap-ruler: 4.0.0
csscolorparser: 1.0.3
earcut: 3.0.1
earcut: 3.0.2
geojson-vt: 4.0.2
gl-matrix: 3.4.3
gl-matrix: 3.4.4
grid-index: 1.1.0
kdbush: 4.0.2
martinez-polygon-clipping: 0.7.4
murmurhash-js: 1.0.0
pbf: 3.3.0
potpack: 2.0.0
potpack: 2.1.0
quickselect: 3.0.0
serialize-to-js: 3.1.2
supercluster: 8.0.1
@@ -17967,9 +18043,6 @@ snapshots:
postgres@3.4.7: {}
potpack@2.0.0:
optional: true
potpack@2.1.0: {}
preact@10.26.8: {}