Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
HFT-informatics-weekend
SI - Time Series Map Viewer
Commits
b6e87b59
Commit
b6e87b59
authored
Oct 11, 2025
by
Heisenberg5124
Browse files
Initial commit
parent
b2e6db41
Changes
9
Expand all
Hide whitespace changes
Inline
Side-by-side
frontend/package-lock.json
View file @
b6e87b59
This diff is collapsed.
Click to expand it.
frontend/package.json
View file @
b6e87b59
...
@@ -13,8 +13,10 @@
...
@@ -13,8 +13,10 @@
"@napi-rs/canvas"
:
"^0.1.80"
,
"@napi-rs/canvas"
:
"^0.1.80"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-radio-group"
:
"^1.3.8"
,
"@radix-ui/react-radio-group"
:
"^1.3.8"
,
"@radix-ui/react-select"
:
"^2.2.6"
,
"@radix-ui/react-slider"
:
"^1.3.6"
,
"@radix-ui/react-slider"
:
"^1.3.6"
,
"@radix-ui/react-slot"
:
"^1.2.3"
,
"@radix-ui/react-slot"
:
"^1.2.3"
,
"@radix-ui/react-switch"
:
"^1.2.6"
,
"@tailwindcss/vite"
:
"^4.1.14"
,
"@tailwindcss/vite"
:
"^4.1.14"
,
"@tanstack/react-router"
:
"^1.132.47"
,
"@tanstack/react-router"
:
"^1.132.47"
,
"canvas"
:
"^3.2.0"
,
"canvas"
:
"^3.2.0"
,
...
...
frontend/src/App.tsx
View file @
b6e87b59
import
'
./App.css
'
import
'
./App.css
'
import
{
MapViewer
}
from
"
@/components/map/map-viewer.tsx
"
;
import
{
MapLayerCard
}
from
"
@/components/map/map-layer-card.tsx
"
;
import
{
MapLayerCard
}
from
"
@/components/map/map-layer-card.tsx
"
;
import
{
useState
}
from
"
react
"
;
import
{
useState
}
from
"
react
"
;
import
TimelineSlider
from
"
@/components/timeline/timeline-slider.tsx
"
;
import
{
SingleMap
}
from
"
@/components/map/single-map.tsx
"
;
import
{
SplitMap
}
from
"
@/components/map/split-map.tsx
"
;
import
{
Card
,
CardContent
}
from
"
@/components/ui/card.tsx
"
;
import
{
Switch
}
from
"
@/components/ui/switch.tsx
"
;
import
{
Label
}
from
"
@/components/ui/label.tsx
"
;
type
Mode
=
'
single
'
|
'
split
'
;
function
App
()
{
function
App
()
{
const
MIN_YEAR
=
2020
;
const
MIN_YEAR
=
2020
;
const
MAX_YEAR
=
2025
;
const
MAX_YEAR
=
2025
;
const
INITIAL_YEAR
=
2022
;
const
INITIAL_YEAR
=
2022
;
const
[
year
,
setYear
]
=
useState
<
number
>
(
INITIAL_YEAR
);
const
[
mode
,
setMode
]
=
useState
<
Mode
>
(
'
single
'
);
return
(
return
(
<
div
className
=
"h-screen w-screen overflow-hidden flex flex-col"
>
<
div
className
=
"h-screen w-screen overflow-hidden relative"
>
<
div
className
=
"relative flex-1 min-h-0 min-w-0 overflow-hidden"
>
<
div
className
=
"absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3"
>
<
div
className
=
"absolute inset-0"
>
<
MapLayerCard
/>
<
MapViewer
year
=
{
year
}
/>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
</
div
>
<
CardContent
className
=
"p-4"
>
<
div
className
=
"absolute right-4 top-4 z-10 w-1/6"
>
<
div
className
=
"flex items-center justify-between gap-3"
>
<
MapLayerCard
/>
<
Label
htmlFor
=
"mode-switch"
className
=
"text-xs text-muted-foreground"
>
</
div
>
Split mode
</
Label
>
<
Switch
id
=
"mode-switch"
checked
=
{
mode
===
'
split
'
}
onCheckedChange
=
{
(
checked
)
=>
setMode
(
checked
?
'
split
'
:
'
single
'
)
}
/>
</
div
>
</
CardContent
>
</
Card
>
</
div
>
</
div
>
<
div
<
div
className
=
"h-full w-full"
>
className
=
"
{
mode
===
'
single
'
?
(
absolute
left-1
/
2
bottom-5
-
translate-x-1
/
2
z-30
<
SingleMap
pointer-events-none
minYear
=
{
MIN_YEAR
}
pb-
[
env
(
safe-area-inset-bottom
)]
maxYear
=
{
MAX_YEAR
}
w-full
initialYear
=
{
INITIAL_YEAR
}
"
className
=
"h-full w-full"
>
/>
<
div
)
:
(
className
=
"
<
SplitMap
pointer-events-auto
mx-auto
minYear
=
{
MIN_YEAR
}
max-w-
[
min
(
1000px
,
92vw
)]
maxYear
=
{
MAX_YEAR
}
"
initialLeftYear
=
{
INITIAL_YEAR
-
1
}
>
initialRightYear
=
{
INITIAL_YEAR
}
<
TimelineSlider
className
=
"h-full w-full"
min
=
{
MIN_YEAR
}
max
=
{
MAX_YEAR
}
value
=
{
year
}
onChange
=
{
setYear
}
step
=
{
1
}
loop
label
=
{
(
v
)
=>
`
${
v
}
`
}
/>
/>
</
div
>
)
}
</
div
>
</
div
>
</
div
>
</
div
>
)
)
;
}
}
export
default
App
export
default
App
frontend/src/components/map/map-viewer.tsx
View file @
b6e87b59
import
{
Map
,
Source
,
Layer
}
from
'
react-map-gl/maplibre
'
;
import
{
Layer
,
Map
,
Source
,
type
ViewStateChangeEvent
}
from
'
react-map-gl/maplibre
'
;
import
*
as
React
from
"
react
"
;
interface
CameraState
{
longitude
:
number
;
latitude
:
number
;
zoom
:
number
;
bearing
?:
number
;
pitch
?:
number
;
}
interface
MapViewerProps
{
interface
MapViewerProps
{
year
:
number
year
:
number
;
viewState
?:
CameraState
;
onMove
?:
(
e
:
ViewStateChangeEvent
)
=>
void
;
style
?:
React
.
CSSProperties
;
mapStyleUrl
?:
string
;
}
}
function
MapViewer
({
year
}:
MapViewerProps
)
{
const
DEFAULT_VIEW
=
{
latitude
:
48.78035
,
longitude
:
9.17289
,
zoom
:
13
,
bearing
:
0
,
pitch
:
0
};
function
MapViewer
({
year
,
viewState
,
onMove
,
style
,
mapStyleUrl
}:
MapViewerProps
)
{
console
.
log
(
'
year
'
,
year
);
const
tilesUrl
=
`/tiles/ndvi_
${
year
}
/{z}/{x}/{y}.png`
;
const
tilesUrl
=
`/tiles/ndvi_
${
year
}
/{z}/{x}/{y}.png`
;
return
(
return
(
<
Map
initialViewState
=
{
{
latitude
:
48.78035
,
longitude
:
9.17289
,
zoom
:
13
}
}
<
Map
mapStyle
=
"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
>
{
...(
viewState
?
viewState
:
{})
}
{
...(
!
viewState
?
{
initialViewState
:
DEFAULT_VIEW
}
:
{})
}
onMove
=
{
onMove
}
style
=
{
style
}
mapStyle
=
{
mapStyleUrl
??
'
https://basemaps.cartocdn.com/gl/positron-gl-style/style.json
'
}
>
<
Source
key
=
{
year
}
id
=
"ndvi"
type
=
"raster"
tiles
=
{
[
tilesUrl
]
}
tileSize
=
{
256
}
/>
<
Source
key
=
{
year
}
id
=
"ndvi"
type
=
"raster"
tiles
=
{
[
tilesUrl
]
}
tileSize
=
{
256
}
/>
<
Layer
id
=
"ndvi-layer"
type
=
"raster"
source
=
"ndvi"
paint
=
{
{
'
raster-opacity
'
:
0.85
}
}
/>
<
Layer
id
=
"ndvi-layer"
type
=
"raster"
source
=
"ndvi"
paint
=
{
{
'
raster-opacity
'
:
0.85
}
}
/>
</
Map
>
</
Map
>
...
...
frontend/src/components/map/single-map.tsx
0 → 100644
View file @
b6e87b59
import
{
useState
}
from
"
react
"
;
import
{
MapViewer
}
from
"
@/components/map/map-viewer.tsx
"
;
import
TimelineSlider
from
"
@/components/timeline/timeline-slider.tsx
"
;
interface
SingleMapProps
{
minYear
:
number
;
maxYear
:
number
;
initialYear
?:
number
;
className
?:
string
;
}
function
SingleMap
({
minYear
,
maxYear
,
initialYear
,
className
}:
SingleMapProps
)
{
const
start
=
initialYear
??
maxYear
;
const
[
year
,
setYear
]
=
useState
<
number
>
(
start
);
return
(
<
div
className
=
{
`relative
${
className
??
''
}
`
}
>
<
div
className
=
"absolute inset-0"
>
<
MapViewer
year
=
{
year
}
/>
</
div
>
{
/* bottom timeline slider */
}
<
div
className
=
"absolute left-1/2 bottom-5 -translate-x-1/2 z-30 pointer-events-none pb-[env(safe-area-inset-bottom)] w-full"
>
<
div
className
=
"pointer-events-auto mx-auto max-w-[min(1000px,92vw)]"
>
<
TimelineSlider
min
=
{
minYear
}
max
=
{
maxYear
}
value
=
{
year
}
onChange
=
{
setYear
}
step
=
{
1
}
loop
label
=
{
(
v
)
=>
`
${
v
}
`
}
/>
</
div
>
</
div
>
</
div
>
);
}
export
{
SingleMap
};
\ No newline at end of file
frontend/src/components/map/split-map-viewer.tsx
0 → 100644
View file @
b6e87b59
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
'
react
'
;
import
type
{
ViewState
,
ViewStateChangeEvent
}
from
'
react-map-gl/maplibre
'
;
import
*
as
React
from
"
react
"
;
import
{
MapViewer
}
from
"
@/components/map/map-viewer.tsx
"
;
interface
SplitMapViewerProps
{
leftYear
:
number
;
rightYear
:
number
;
initialDivider
?:
number
;
initialViewState
?:
ViewState
;
style
?:
React
.
CSSProperties
;
}
interface
CameraState
{
longitude
:
number
;
latitude
:
number
;
zoom
:
number
;
bearing
?:
number
;
pitch
?:
number
;
}
const
FALLBACK_VIEW
=
{
latitude
:
48.78035
,
longitude
:
9.17289
,
zoom
:
13
,
bearing
:
0
,
pitch
:
0
};
export
function
SplitMapViewer
({
leftYear
,
rightYear
,
initialDivider
=
0.5
,
initialViewState
,
style
}:
SplitMapViewerProps
)
{
const
containerRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
);
const
[
divider
,
setDivider
]
=
useState
(
Math
.
min
(
0.95
,
Math
.
max
(
0.05
,
initialDivider
)));
const
[
viewState
,
setViewState
]
=
useState
<
CameraState
>
(
initialViewState
??
FALLBACK_VIEW
);
const
[
dragging
,
setDragging
]
=
useState
(
false
);
const
onMove
=
useCallback
((
e
:
ViewStateChangeEvent
)
=>
{
setViewState
(
e
.
viewState
);
},
[]);
useEffect
(()
=>
{
if
(
!
dragging
)
return
;
const
onMoveDoc
=
(
ev
:
MouseEvent
)
=>
{
const
el
=
containerRef
.
current
;
if
(
!
el
)
return
;
const
rect
=
el
.
getBoundingClientRect
();
const
pct
=
(
ev
.
clientX
-
rect
.
left
)
/
rect
.
width
;
setDivider
(
Math
.
min
(
0.98
,
Math
.
max
(
0.02
,
pct
)));
};
const
onUpDoc
=
()
=>
setDragging
(
false
);
window
.
addEventListener
(
'
mousemove
'
,
onMoveDoc
);
window
.
addEventListener
(
'
mouseup
'
,
onUpDoc
);
return
()
=>
{
window
.
removeEventListener
(
'
mousemove
'
,
onMoveDoc
);
window
.
removeEventListener
(
'
mouseup
'
,
onUpDoc
);
};
},
[
dragging
]);
const
leftClip
=
`inset(0
${
100
-
divider
*
100
}
% 0 0)`
;
const
rightClip
=
`inset(0 0 0
${
divider
*
100
}
%)`
;
return
(
<
div
ref
=
{
containerRef
}
style
=
{
{
position
:
'
relative
'
,
width
:
'
100%
'
,
height
:
'
100%
'
,
overflow
:
'
hidden
'
,
...
style
}
}
>
{
/* Right map (bottom layer) */
}
<
div
style
=
{
{
position
:
'
absolute
'
,
inset
:
0
,
clipPath
:
rightClip
}
}
><
MapViewer
year
=
{
rightYear
}
viewState
=
{
viewState
}
onMove
=
{
onMove
}
style
=
{
{
height
:
'
100%
'
}
}
mapStyleUrl
=
"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
/>
</
div
>
{
/* Left map (top layer) */
}
<
div
style
=
{
{
position
:
'
absolute
'
,
inset
:
0
,
clipPath
:
leftClip
,
pointerEvents
:
'
auto
'
}
}
>
<
MapViewer
year
=
{
leftYear
}
viewState
=
{
viewState
}
onMove
=
{
onMove
}
style
=
{
{
height
:
'
100%
'
}
}
mapStyleUrl
=
"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
/>
</
div
>
{
/* Divider */
}
<
div
role
=
"separator"
aria-orientation
=
"vertical"
onMouseDown
=
{
()
=>
setDragging
(
true
)
}
style
=
{
{
position
:
'
absolute
'
,
top
:
0
,
bottom
:
0
,
left
:
`
${
divider
*
100
}
%`
,
width
:
2
,
transform
:
'
translateX(-1px)
'
,
background
:
'
#fff
'
,
boxShadow
:
'
0 0 0 1px rgba(0,0,0,0.25)
'
,
cursor
:
'
col-resize
'
,
zIndex
:
10
}
}
>
<
div
style
=
{
{
position
:
'
absolute
'
,
top
:
'
50%
'
,
left
:
'
50%
'
,
transform
:
'
translate(-50%, -50%)
'
,
width
:
14
,
height
:
14
,
borderRadius
:
9999
,
background
:
'
#fff
'
,
boxShadow
:
'
0 1px 3px rgba(0,0,0,0.35)
'
}
}
/>
</
div
>
</
div
>
);
}
frontend/src/components/map/split-map.tsx
0 → 100644
View file @
b6e87b59
import
{
useCallback
,
useMemo
,
useState
}
from
'
react
'
;
import
{
MapViewer
}
from
'
@/components/map/map-viewer
'
;
import
type
{
ViewStateChangeEvent
}
from
'
react-map-gl/maplibre
'
;
import
{
Card
,
CardContent
}
from
'
@/components/ui/card
'
;
import
{
Label
}
from
'
@/components/ui/label
'
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
'
@/components/ui/select
'
;
type
CameraState
=
{
longitude
:
number
;
latitude
:
number
;
zoom
:
number
;
bearing
?:
number
;
pitch
?:
number
;
};
interface
SplitMapProps
{
minYear
:
number
;
maxYear
:
number
;
initialLeftYear
?:
number
;
initialRightYear
?:
number
;
className
?:
string
;
// e.g., "h-full w-full"
}
const
FALLBACK_VIEW
:
CameraState
=
{
latitude
:
48.78035
,
longitude
:
9.17289
,
zoom
:
13
,
bearing
:
0
,
pitch
:
0
};
function
SplitMap
({
minYear
,
maxYear
,
initialLeftYear
,
initialRightYear
,
className
}:
SplitMapProps
)
{
const
years
=
useMemo
(
()
=>
Array
.
from
({
length
:
maxYear
-
minYear
+
1
},
(
_
,
i
)
=>
minYear
+
i
),
[
minYear
,
maxYear
]
);
const
[
leftYear
,
setLeftYear
]
=
useState
<
number
>
(
initialLeftYear
??
years
[
0
]);
const
[
rightYear
,
setRightYear
]
=
useState
<
number
>
(
initialRightYear
??
years
.
at
(
-
1
)
!
);
// Shared camera so the two maps stay in sync
const
[
viewState
,
setViewState
]
=
useState
<
CameraState
>
(
FALLBACK_VIEW
);
const
onMove
=
useCallback
((
e
:
ViewStateChangeEvent
)
=>
{
const
{
longitude
,
latitude
,
zoom
,
bearing
,
pitch
}
=
e
.
viewState
;
setViewState
({
longitude
,
latitude
,
zoom
,
bearing
,
pitch
});
},
[]);
return
(
<
div
className
=
{
`relative
${
className
??
''
}
`
}
>
{
/* Side-by-side panes */
}
<
div
className
=
"grid grid-cols-2 h-full w-full"
>
{
/* LEFT MAP PANE */
}
<
div
className
=
"relative min-w-0 min-h-0"
>
<
div
className
=
"absolute inset-0"
>
<
MapViewer
year
=
{
leftYear
}
viewState
=
{
viewState
}
onMove
=
{
onMove
}
style
=
{
{
height
:
'
100%
'
,
width
:
'
100%
'
}
}
/>
</
div
>
{
/* Bottom-centered selector (robust centering, like slider) */
}
<
div
className
=
"absolute inset-x-0 bottom-5 z-50 flex justify-center pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[90%] max-w-sm"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
CardContent
>
<
Label
htmlFor
=
"leftYear"
className
=
"text-xs text-muted-foreground mb-1 block"
>
Left year
</
Label
>
<
Select
value
=
{
String
(
leftYear
)
}
onValueChange
=
{
(
v
)
=>
setLeftYear
(
Number
(
v
))
}
>
<
SelectTrigger
id
=
"leftYear"
className
=
"h-9"
>
<
SelectValue
placeholder
=
"Select year"
/>
</
SelectTrigger
>
<
SelectContent
>
{
years
.
map
((
y
)
=>
(
<
SelectItem
key
=
{
`left-
${
y
}
`
}
value
=
{
String
(
y
)
}
>
{
y
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
CardContent
>
</
Card
>
</
div
>
</
div
>
</
div
>
{
/* RIGHT MAP PANE */
}
<
div
className
=
"relative min-w-0 min-h-0"
>
<
div
className
=
"absolute inset-0"
>
<
MapViewer
year
=
{
rightYear
}
viewState
=
{
viewState
}
onMove
=
{
onMove
}
style
=
{
{
height
:
'
100%
'
,
width
:
'
100%
'
}
}
/>
</
div
>
{
/* Bottom-centered selector */
}
<
div
className
=
"absolute inset-x-0 bottom-5 z-50 flex justify-center pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[90%] max-w-sm"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
CardContent
>
<
Label
htmlFor
=
"rightYear"
className
=
"text-xs text-muted-foreground mb-1 block"
>
Right year
</
Label
>
<
Select
value
=
{
String
(
rightYear
)
}
onValueChange
=
{
(
v
)
=>
setRightYear
(
Number
(
v
))
}
>
<
SelectTrigger
id
=
"rightYear"
className
=
"h-9"
>
<
SelectValue
placeholder
=
"Select year"
/>
</
SelectTrigger
>
<
SelectContent
>
{
years
.
map
((
y
)
=>
(
<
SelectItem
key
=
{
`right-
${
y
}
`
}
value
=
{
String
(
y
)
}
>
{
y
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
CardContent
>
</
Card
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
);
}
export
{
SplitMap
};
frontend/src/components/ui/select.tsx
0 → 100644
View file @
b6e87b59
import
*
as
React
from
"
react
"
import
*
as
SelectPrimitive
from
"
@radix-ui/react-select
"
import
{
CheckIcon
,
ChevronDownIcon
,
ChevronUpIcon
}
from
"
lucide-react
"
import
{
cn
}
from
"
@/lib/utils
"
function
Select
({
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Root
>
)
{
return
<
SelectPrimitive
.
Root
data-slot
=
"select"
{
...
props
}
/>
}
function
SelectGroup
({
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Group
>
)
{
return
<
SelectPrimitive
.
Group
data-slot
=
"select-group"
{
...
props
}
/>
}
function
SelectValue
({
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Value
>
)
{
return
<
SelectPrimitive
.
Value
data-slot
=
"select-value"
{
...
props
}
/>
}
function
SelectTrigger
({
className
,
size
=
"
default
"
,
children
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Trigger
>
&
{
size
?:
"
sm
"
|
"
default
"
})
{
return
(
<
SelectPrimitive
.
Trigger
data-slot
=
"select-trigger"
data-size
=
{
size
}
className
=
{
cn
(
"
border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4
"
,
className
)
}
{
...
props
}
>
{
children
}
<
SelectPrimitive
.
Icon
asChild
>
<
ChevronDownIcon
className
=
"size-4 opacity-50"
/>
</
SelectPrimitive
.
Icon
>
</
SelectPrimitive
.
Trigger
>
)
}
function
SelectContent
({
className
,
children
,
position
=
"
popper
"
,
align
=
"
center
"
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Content
>
)
{
return
(
<
SelectPrimitive
.
Portal
>
<
SelectPrimitive
.
Content
data-slot
=
"select-content"
className
=
{
cn
(
"
bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md
"
,
position
===
"
popper
"
&&
"
data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1
"
,
className
)
}
position
=
{
position
}
align
=
{
align
}
{
...
props
}
>
<
SelectScrollUpButton
/>
<
SelectPrimitive
.
Viewport
className
=
{
cn
(
"
p-1
"
,
position
===
"
popper
"
&&
"
h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1
"
)
}
>
{
children
}
</
SelectPrimitive
.
Viewport
>
<
SelectScrollDownButton
/>
</
SelectPrimitive
.
Content
>
</
SelectPrimitive
.
Portal
>
)
}
function
SelectLabel
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Label
>
)
{
return
(
<
SelectPrimitive
.
Label
data-slot
=
"select-label"
className
=
{
cn
(
"
text-muted-foreground px-2 py-1.5 text-xs
"
,
className
)
}
{
...
props
}
/>
)
}
function
SelectItem
({
className
,
children
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Item
>
)
{
return
(
<
SelectPrimitive
.
Item
data-slot
=
"select-item"
className
=
{
cn
(
"
focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2
"
,
className
)
}
{
...
props
}
>
<
span
className
=
"absolute right-2 flex size-3.5 items-center justify-center"
>
<
SelectPrimitive
.
ItemIndicator
>
<
CheckIcon
className
=
"size-4"
/>
</
SelectPrimitive
.
ItemIndicator
>
</
span
>
<
SelectPrimitive
.
ItemText
>
{
children
}
</
SelectPrimitive
.
ItemText
>
</
SelectPrimitive
.
Item
>
)
}
function
SelectSeparator
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Separator
>
)
{
return
(
<
SelectPrimitive
.
Separator
data-slot
=
"select-separator"
className
=
{
cn
(
"
bg-border pointer-events-none -mx-1 my-1 h-px
"
,
className
)
}
{
...
props
}
/>
)
}
function
SelectScrollUpButton
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
ScrollUpButton
>
)
{
return
(
<
SelectPrimitive
.
ScrollUpButton
data-slot
=
"select-scroll-up-button"
className
=
{
cn
(
"
flex cursor-default items-center justify-center py-1
"
,
className
)
}
{
...
props
}
>
<
ChevronUpIcon
className
=
"size-4"
/>
</
SelectPrimitive
.
ScrollUpButton
>
)
}
function
SelectScrollDownButton
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
ScrollDownButton
>
)
{
return
(
<
SelectPrimitive
.
ScrollDownButton
data-slot
=
"select-scroll-down-button"
className
=
{
cn
(
"
flex cursor-default items-center justify-center py-1
"
,
className
)
}
{
...
props
}
>
<
ChevronDownIcon
className
=
"size-4"
/>
</
SelectPrimitive
.
ScrollDownButton
>
)
}
export
{
Select
,
SelectContent
,
SelectGroup
,
SelectItem
,
SelectLabel
,
SelectScrollDownButton
,
SelectScrollUpButton
,
SelectSeparator
,
SelectTrigger
,
SelectValue
,
}
frontend/src/components/ui/switch.tsx
0 → 100644
View file @
b6e87b59
import
*
as
React
from
"
react
"
import
*
as
SwitchPrimitive
from
"
@radix-ui/react-switch
"
import
{
cn
}
from
"
@/lib/utils
"
function
Switch
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SwitchPrimitive
.
Root
>
)
{
return
(
<
SwitchPrimitive
.
Root
data-slot
=
"switch"
className
=
{
cn
(
"
peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50
"
,
className
)
}
{
...
props
}
>
<
SwitchPrimitive
.
Thumb
data-slot
=
"switch-thumb"
className
=
{
cn
(
"
bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0
"
)
}
/>
</
SwitchPrimitive
.
Root
>
)
}
export
{
Switch
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment