@traky-app/ui 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -19
- package/package.json +48 -3
- package/registry/traky/json/command.json +1 -1
- package/registry/traky/ui/command.tsx +1 -0
- package/src/hooks/use-document-theme.ts +25 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/disable-input-autofill.ts +23 -0
- package/src/lib/sidebar-persistence.ts +76 -0
- package/src/lib/utils.ts +30 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/alert-dialog.tsx +163 -0
- package/src/ui/alert.tsx +66 -0
- package/src/ui/animated-beam.tsx +185 -0
- package/src/ui/animated-list.tsx +71 -0
- package/src/ui/avatar.tsx +53 -0
- package/src/ui/badge.tsx +46 -0
- package/src/ui/bento-grid.tsx +109 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/bubble.tsx +125 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +64 -0
- package/src/ui/calendar.tsx +213 -0
- package/src/ui/card.tsx +92 -0
- package/src/ui/carousel.tsx +240 -0
- package/src/ui/chart.tsx +380 -0
- package/src/ui/checkbox.tsx +31 -0
- package/src/ui/checkpoint.tsx +94 -0
- package/src/ui/code-block.tsx +232 -0
- package/src/ui/collapsible.tsx +33 -0
- package/src/ui/command.tsx +191 -0
- package/src/ui/context-menu.tsx +251 -0
- package/src/ui/dialog.tsx +142 -0
- package/src/ui/drawer.tsx +135 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +104 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/globe.tsx +234 -0
- package/src/ui/hover-card.tsx +44 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +77 -0
- package/src/ui/input.tsx +24 -0
- package/src/ui/item.tsx +193 -0
- package/src/ui/kbd.tsx +28 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/map.tsx +2226 -0
- package/src/ui/marker.tsx +69 -0
- package/src/ui/marquee.tsx +74 -0
- package/src/ui/message.tsx +92 -0
- package/src/ui/navigation-menu.tsx +169 -0
- package/src/ui/number-ticker.tsx +90 -0
- package/src/ui/pagination.tsx +122 -0
- package/src/ui/popover.tsx +48 -0
- package/src/ui/progress.tsx +31 -0
- package/src/ui/select.tsx +193 -0
- package/src/ui/separator.tsx +28 -0
- package/src/ui/sheet.tsx +143 -0
- package/src/ui/sidebar.tsx +764 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/sonner.tsx +71 -0
- package/src/ui/spinner.tsx +15 -0
- package/src/ui/switch.tsx +41 -0
- package/src/ui/table.tsx +116 -0
- package/src/ui/tabs.tsx +66 -0
- package/src/ui/textarea.tsx +20 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +47 -0
- package/src/ui/tooltip.tsx +61 -0
- package/src/ui/trace.tsx +165 -0
package/src/ui/field.tsx
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
import { Label } from "./label"
|
|
8
|
+
import { Separator } from "./separator"
|
|
9
|
+
|
|
10
|
+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
11
|
+
return (
|
|
12
|
+
<fieldset
|
|
13
|
+
data-slot="field-set"
|
|
14
|
+
className={cn(
|
|
15
|
+
"flex flex-col gap-6",
|
|
16
|
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function FieldLegend({
|
|
25
|
+
className,
|
|
26
|
+
variant = "legend",
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
29
|
+
return (
|
|
30
|
+
<legend
|
|
31
|
+
data-slot="field-legend"
|
|
32
|
+
data-variant={variant}
|
|
33
|
+
className={cn(
|
|
34
|
+
"mb-3 font-medium",
|
|
35
|
+
"data-[variant=legend]:text-base",
|
|
36
|
+
"data-[variant=label]:text-sm",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
data-slot="field-group"
|
|
48
|
+
className={cn(
|
|
49
|
+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fieldVariants = cva(
|
|
58
|
+
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
|
59
|
+
{
|
|
60
|
+
variants: {
|
|
61
|
+
orientation: {
|
|
62
|
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
63
|
+
horizontal: [
|
|
64
|
+
"flex-row items-center",
|
|
65
|
+
"[&>[data-slot=field-label]]:flex-auto",
|
|
66
|
+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
67
|
+
],
|
|
68
|
+
responsive: [
|
|
69
|
+
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
|
70
|
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
71
|
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultVariants: {
|
|
76
|
+
orientation: "vertical",
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
function Field({
|
|
82
|
+
className,
|
|
83
|
+
orientation = "vertical",
|
|
84
|
+
...props
|
|
85
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
role="group"
|
|
89
|
+
data-slot="field"
|
|
90
|
+
data-orientation={orientation}
|
|
91
|
+
className={cn(fieldVariants({ orientation }), className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
data-slot="field-content"
|
|
101
|
+
className={cn(
|
|
102
|
+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
103
|
+
className
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function FieldLabel({
|
|
111
|
+
className,
|
|
112
|
+
...props
|
|
113
|
+
}: React.ComponentProps<typeof Label>) {
|
|
114
|
+
return (
|
|
115
|
+
<Label
|
|
116
|
+
data-slot="field-label"
|
|
117
|
+
className={cn(
|
|
118
|
+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
119
|
+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
120
|
+
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
data-slot="field-label"
|
|
132
|
+
className={cn(
|
|
133
|
+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
134
|
+
className
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
142
|
+
return (
|
|
143
|
+
<p
|
|
144
|
+
data-slot="field-description"
|
|
145
|
+
className={cn(
|
|
146
|
+
"text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
147
|
+
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
148
|
+
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
149
|
+
className
|
|
150
|
+
)}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function FieldSeparator({
|
|
157
|
+
children,
|
|
158
|
+
className,
|
|
159
|
+
...props
|
|
160
|
+
}: React.ComponentProps<"div"> & {
|
|
161
|
+
children?: React.ReactNode
|
|
162
|
+
}) {
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
data-slot="field-separator"
|
|
166
|
+
data-content={!!children}
|
|
167
|
+
className={cn(
|
|
168
|
+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<Separator className="absolute inset-0 top-1/2" />
|
|
174
|
+
{children && (
|
|
175
|
+
<span
|
|
176
|
+
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
|
177
|
+
data-slot="field-separator-content"
|
|
178
|
+
>
|
|
179
|
+
{children}
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function FieldError({
|
|
187
|
+
className,
|
|
188
|
+
children,
|
|
189
|
+
errors,
|
|
190
|
+
...props
|
|
191
|
+
}: React.ComponentProps<"div"> & {
|
|
192
|
+
errors?: Array<{ message?: string } | undefined>
|
|
193
|
+
}) {
|
|
194
|
+
const content = useMemo(() => {
|
|
195
|
+
if (children) {
|
|
196
|
+
return children
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!errors?.length) {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const uniqueErrors = [
|
|
204
|
+
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
if (uniqueErrors.length === 1) {
|
|
208
|
+
return uniqueErrors[0]?.message
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
213
|
+
{uniqueErrors.map(
|
|
214
|
+
(error, index) =>
|
|
215
|
+
error?.message && <li key={index}>{error.message}</li>
|
|
216
|
+
)}
|
|
217
|
+
</ul>
|
|
218
|
+
)
|
|
219
|
+
}, [children, errors])
|
|
220
|
+
|
|
221
|
+
if (!content) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
role="alert"
|
|
228
|
+
data-slot="field-error"
|
|
229
|
+
className={cn("text-sm font-normal text-destructive", className)}
|
|
230
|
+
{...props}
|
|
231
|
+
>
|
|
232
|
+
{content}
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export {
|
|
238
|
+
Field,
|
|
239
|
+
FieldLabel,
|
|
240
|
+
FieldDescription,
|
|
241
|
+
FieldError,
|
|
242
|
+
FieldGroup,
|
|
243
|
+
FieldLegend,
|
|
244
|
+
FieldSeparator,
|
|
245
|
+
FieldSet,
|
|
246
|
+
FieldContent,
|
|
247
|
+
FieldTitle,
|
|
248
|
+
}
|
package/src/ui/globe.tsx
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react"
|
|
4
|
+
import createGlobe, { type COBEOptions } from "cobe"
|
|
5
|
+
import { useMotionValue, useSpring } from "motion/react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
|
|
9
|
+
const MOVEMENT_DAMPING = 1400
|
|
10
|
+
const MARKER_HOLD_MS = 2200
|
|
11
|
+
const MARKER_TRANSITION_MS = 1800
|
|
12
|
+
|
|
13
|
+
function easeInOutCubic(t: number): number {
|
|
14
|
+
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function lerp(a: number, b: number, t: number): number {
|
|
18
|
+
return a + (b - a) * t
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function lerpAngle(a: number, b: number, t: number): number {
|
|
22
|
+
let diff = b - a
|
|
23
|
+
while (diff > Math.PI) diff -= 2 * Math.PI
|
|
24
|
+
while (diff < -Math.PI) diff += 2 * Math.PI
|
|
25
|
+
return a + diff * t
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const GLOBE_CONFIG: COBEOptions = {
|
|
29
|
+
width: 800,
|
|
30
|
+
height: 800,
|
|
31
|
+
onRender: () => {},
|
|
32
|
+
devicePixelRatio: 2,
|
|
33
|
+
phi: 0,
|
|
34
|
+
theta: 0.3,
|
|
35
|
+
dark: 0,
|
|
36
|
+
diffuse: 0.4,
|
|
37
|
+
mapSamples: 16000,
|
|
38
|
+
mapBrightness: 1.2,
|
|
39
|
+
baseColor: [1, 1, 1],
|
|
40
|
+
markerColor: [251 / 255, 100 / 255, 21 / 255],
|
|
41
|
+
glowColor: [1, 1, 1],
|
|
42
|
+
markers: [
|
|
43
|
+
{ location: [14.5995, 120.9842], size: 0.03 },
|
|
44
|
+
{ location: [19.076, 72.8777], size: 0.1 },
|
|
45
|
+
{ location: [23.8103, 90.4125], size: 0.05 },
|
|
46
|
+
{ location: [30.0444, 31.2357], size: 0.07 },
|
|
47
|
+
{ location: [39.9042, 116.4074], size: 0.08 },
|
|
48
|
+
{ location: [-23.5505, -46.6333], size: 0.1 },
|
|
49
|
+
{ location: [19.4326, -99.1332], size: 0.1 },
|
|
50
|
+
{ location: [40.7128, -74.006], size: 0.1 },
|
|
51
|
+
{ location: [34.6937, 135.5022], size: 0.05 },
|
|
52
|
+
{ location: [41.0082, 28.9784], size: 0.06 },
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Globe({
|
|
57
|
+
className,
|
|
58
|
+
config = GLOBE_CONFIG,
|
|
59
|
+
autoRotate = true,
|
|
60
|
+
interactive = true,
|
|
61
|
+
panBetweenMarkers = false,
|
|
62
|
+
cycleMarkers = false,
|
|
63
|
+
markerHoldMs = MARKER_HOLD_MS,
|
|
64
|
+
markerTransitionMs = MARKER_TRANSITION_MS,
|
|
65
|
+
}: {
|
|
66
|
+
className?: string
|
|
67
|
+
config?: COBEOptions
|
|
68
|
+
autoRotate?: boolean
|
|
69
|
+
interactive?: boolean
|
|
70
|
+
/** @deprecated Usa cycleMarkers */
|
|
71
|
+
panBetweenMarkers?: boolean
|
|
72
|
+
/** Centra cada marker uno por uno con pausa y transición suave. */
|
|
73
|
+
cycleMarkers?: boolean
|
|
74
|
+
markerHoldMs?: number
|
|
75
|
+
markerTransitionMs?: number
|
|
76
|
+
}) {
|
|
77
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
78
|
+
const phiRef = useRef(config.phi ?? 0)
|
|
79
|
+
const widthRef = useRef(0)
|
|
80
|
+
const pointerInteracting = useRef<number | null>(null)
|
|
81
|
+
const pointerInteractionMovement = useRef(0)
|
|
82
|
+
|
|
83
|
+
const shouldCycleMarkers = cycleMarkers || panBetweenMarkers
|
|
84
|
+
|
|
85
|
+
const fixedTheta = config.theta ?? 0.22
|
|
86
|
+
|
|
87
|
+
const markerViews = useMemo((): Array<{ phi: number }> | null => {
|
|
88
|
+
if (!shouldCycleMarkers || !config.markers?.length) return null
|
|
89
|
+
return config.markers.map((marker) => ({
|
|
90
|
+
phi: Math.PI - ((marker.location[1] * Math.PI) / 180 - Math.PI / 2),
|
|
91
|
+
}))
|
|
92
|
+
}, [shouldCycleMarkers, config.markers])
|
|
93
|
+
|
|
94
|
+
const markerPhiRange = useMemo((): [number, number] | null => {
|
|
95
|
+
if (!panBetweenMarkers || cycleMarkers || !config.markers?.length) return null
|
|
96
|
+
const phis = config.markers.map(
|
|
97
|
+
(marker) => Math.PI - ((marker.location[1] * Math.PI) / 180 - Math.PI / 2),
|
|
98
|
+
)
|
|
99
|
+
return [Math.min(...phis), Math.max(...phis)]
|
|
100
|
+
}, [panBetweenMarkers, cycleMarkers, config.markers])
|
|
101
|
+
|
|
102
|
+
const r = useMotionValue(0)
|
|
103
|
+
const rs = useSpring(r, {
|
|
104
|
+
mass: 1,
|
|
105
|
+
damping: 30,
|
|
106
|
+
stiffness: 100,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const updatePointerInteraction = (value: number | null) => {
|
|
110
|
+
if (!interactive) return
|
|
111
|
+
pointerInteracting.current = value
|
|
112
|
+
if (canvasRef.current) {
|
|
113
|
+
canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const updateMovement = (clientX: number) => {
|
|
118
|
+
if (!interactive) return
|
|
119
|
+
if (pointerInteracting.current !== null) {
|
|
120
|
+
const delta = clientX - pointerInteracting.current
|
|
121
|
+
pointerInteractionMovement.current = delta
|
|
122
|
+
r.set(r.get() + delta / MOVEMENT_DAMPING)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const canvas = canvasRef.current
|
|
128
|
+
if (!canvas) return
|
|
129
|
+
|
|
130
|
+
const onResize = () => {
|
|
131
|
+
widthRef.current = canvas.offsetWidth
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onResize()
|
|
135
|
+
window.addEventListener("resize", onResize)
|
|
136
|
+
|
|
137
|
+
const resizeObserver = new ResizeObserver(onResize)
|
|
138
|
+
resizeObserver.observe(canvas)
|
|
139
|
+
|
|
140
|
+
const globe = createGlobe(canvas, {
|
|
141
|
+
...config,
|
|
142
|
+
width: widthRef.current * 2,
|
|
143
|
+
height: widthRef.current * 2,
|
|
144
|
+
onRender: (state) => {
|
|
145
|
+
if (shouldCycleMarkers && markerViews && markerViews.length > 0) {
|
|
146
|
+
const segment = markerHoldMs + markerTransitionMs
|
|
147
|
+
const total = markerViews.length * segment
|
|
148
|
+
const elapsed = Date.now() % total
|
|
149
|
+
const index = Math.floor(elapsed / segment)
|
|
150
|
+
const local = elapsed % segment
|
|
151
|
+
const from = markerViews[index % markerViews.length]
|
|
152
|
+
const to = markerViews[(index + 1) % markerViews.length]
|
|
153
|
+
|
|
154
|
+
if (local < markerHoldMs) {
|
|
155
|
+
state.phi = from.phi
|
|
156
|
+
} else {
|
|
157
|
+
const t = easeInOutCubic((local - markerHoldMs) / markerTransitionMs)
|
|
158
|
+
state.phi = lerpAngle(from.phi, to.phi, t)
|
|
159
|
+
}
|
|
160
|
+
state.theta = fixedTheta
|
|
161
|
+
} else if (panBetweenMarkers && markerPhiRange) {
|
|
162
|
+
const [phiMin, phiMax] = markerPhiRange
|
|
163
|
+
const t = Date.now() / 5200
|
|
164
|
+
state.phi = phiMin + ((Math.sin(t) + 1) / 2) * (phiMax - phiMin)
|
|
165
|
+
} else if (autoRotate && !pointerInteracting.current) {
|
|
166
|
+
phiRef.current += 0.005
|
|
167
|
+
state.phi = interactive ? phiRef.current + rs.get() : (config.phi ?? 0)
|
|
168
|
+
} else {
|
|
169
|
+
state.phi = interactive ? phiRef.current + rs.get() : (config.phi ?? 0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
state.theta = shouldCycleMarkers ? fixedTheta : (config.theta ?? state.theta)
|
|
173
|
+
state.scale = config.scale ?? 1
|
|
174
|
+
state.width = widthRef.current * 2
|
|
175
|
+
state.height = widthRef.current * 2
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
canvas.style.opacity = "1"
|
|
181
|
+
}, 0)
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
globe.destroy()
|
|
185
|
+
window.removeEventListener("resize", onResize)
|
|
186
|
+
resizeObserver.disconnect()
|
|
187
|
+
}
|
|
188
|
+
}, [
|
|
189
|
+
rs,
|
|
190
|
+
config,
|
|
191
|
+
autoRotate,
|
|
192
|
+
interactive,
|
|
193
|
+
panBetweenMarkers,
|
|
194
|
+
cycleMarkers,
|
|
195
|
+
shouldCycleMarkers,
|
|
196
|
+
markerViews,
|
|
197
|
+
markerPhiRange,
|
|
198
|
+
markerHoldMs,
|
|
199
|
+
markerTransitionMs,
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
className={cn(
|
|
205
|
+
"absolute inset-0 mx-auto aspect-square w-full max-w-150",
|
|
206
|
+
className
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
<canvas
|
|
210
|
+
className={cn(
|
|
211
|
+
"size-full opacity-0 transition-opacity duration-500 contain-[layout_paint_size]",
|
|
212
|
+
interactive ? "cursor-grab" : "cursor-default",
|
|
213
|
+
)}
|
|
214
|
+
ref={canvasRef}
|
|
215
|
+
onPointerDown={
|
|
216
|
+
interactive
|
|
217
|
+
? (e) => {
|
|
218
|
+
pointerInteracting.current = e.clientX
|
|
219
|
+
updatePointerInteraction(e.clientX)
|
|
220
|
+
}
|
|
221
|
+
: undefined
|
|
222
|
+
}
|
|
223
|
+
onPointerUp={interactive ? () => updatePointerInteraction(null) : undefined}
|
|
224
|
+
onPointerOut={interactive ? () => updatePointerInteraction(null) : undefined}
|
|
225
|
+
onMouseMove={interactive ? (e) => updateMovement(e.clientX) : undefined}
|
|
226
|
+
onTouchMove={
|
|
227
|
+
interactive
|
|
228
|
+
? (e) => e.touches[0] && updateMovement(e.touches[0].clientX)
|
|
229
|
+
: undefined
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
11
|
+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function HoverCardTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
17
|
+
return (
|
|
18
|
+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function HoverCardContent({
|
|
23
|
+
className,
|
|
24
|
+
align = "center",
|
|
25
|
+
sideOffset = 4,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
28
|
+
return (
|
|
29
|
+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
|
30
|
+
<HoverCardPrimitive.Content
|
|
31
|
+
data-slot="hover-card-content"
|
|
32
|
+
align={align}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
className={cn(
|
|
35
|
+
"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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</HoverCardPrimitive.Portal>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
import { Button } from "./button"
|
|
8
|
+
import { Input } from "./input"
|
|
9
|
+
import { Textarea } from "./textarea"
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
|
|
18
|
+
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
19
|
+
|
|
20
|
+
// Variants based on alignment.
|
|
21
|
+
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
22
|
+
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
23
|
+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
24
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
25
|
+
|
|
26
|
+
// Focus state.
|
|
27
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
|
|
28
|
+
|
|
29
|
+
// Error state.
|
|
30
|
+
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
31
|
+
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputGroupAddonVariants = cva(
|
|
40
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
41
|
+
{
|
|
42
|
+
variants: {
|
|
43
|
+
align: {
|
|
44
|
+
"inline-start":
|
|
45
|
+
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
46
|
+
"inline-end":
|
|
47
|
+
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
48
|
+
"block-start":
|
|
49
|
+
"order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
|
|
50
|
+
"block-end":
|
|
51
|
+
"order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
align: "inline-start",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
function InputGroupAddon({
|
|
61
|
+
className,
|
|
62
|
+
align = "inline-start",
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
role="group"
|
|
68
|
+
data-slot="input-group-addon"
|
|
69
|
+
data-align={align}
|
|
70
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
71
|
+
onClick={(e) => {
|
|
72
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
76
|
+
}}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inputGroupButtonVariants = cva(
|
|
83
|
+
"flex items-center gap-2 text-sm shadow-none",
|
|
84
|
+
{
|
|
85
|
+
variants: {
|
|
86
|
+
size: {
|
|
87
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
88
|
+
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
|
89
|
+
"icon-xs":
|
|
90
|
+
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
91
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
defaultVariants: {
|
|
95
|
+
size: "xs",
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
function InputGroupButton({
|
|
101
|
+
className,
|
|
102
|
+
type = "button",
|
|
103
|
+
variant = "ghost",
|
|
104
|
+
size = "xs",
|
|
105
|
+
...props
|
|
106
|
+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
107
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
108
|
+
return (
|
|
109
|
+
<Button
|
|
110
|
+
type={type}
|
|
111
|
+
data-size={size}
|
|
112
|
+
variant={variant}
|
|
113
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
120
|
+
return (
|
|
121
|
+
<span
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function InputGroupInput({
|
|
132
|
+
className,
|
|
133
|
+
...props
|
|
134
|
+
}: React.ComponentProps<"input">) {
|
|
135
|
+
return (
|
|
136
|
+
<Input
|
|
137
|
+
data-slot="input-group-control"
|
|
138
|
+
className={cn(
|
|
139
|
+
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
140
|
+
className
|
|
141
|
+
)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function InputGroupTextarea({
|
|
148
|
+
className,
|
|
149
|
+
...props
|
|
150
|
+
}: React.ComponentProps<"textarea">) {
|
|
151
|
+
return (
|
|
152
|
+
<Textarea
|
|
153
|
+
data-slot="input-group-control"
|
|
154
|
+
className={cn(
|
|
155
|
+
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
156
|
+
className
|
|
157
|
+
)}
|
|
158
|
+
{...props}
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
InputGroup,
|
|
165
|
+
InputGroupAddon,
|
|
166
|
+
InputGroupButton,
|
|
167
|
+
InputGroupText,
|
|
168
|
+
InputGroupInput,
|
|
169
|
+
InputGroupTextarea,
|
|
170
|
+
}
|