@a4ui/core 0.31.1 → 0.32.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/dist/NumberInput-Bnh2u7pp.js +381 -0
- package/dist/charts/GaugeChart.d.ts +43 -0
- package/dist/charts/LineChart.d.ts +39 -0
- package/dist/charts/RadarChart.d.ts +34 -0
- package/dist/charts/index.d.ts +3 -0
- package/dist/charts.js +563 -100
- package/dist/commerce.js +2 -2
- package/dist/elements.css +111 -0
- package/dist/full.css +111 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3579 -3288
- package/dist/motion-jYMWmZqB.js +119 -0
- package/dist/ui/AnimatedBeam.d.ts +34 -0
- package/dist/ui/BentoGrid.d.ts +39 -0
- package/dist/ui/BorderBeam.d.ts +27 -0
- package/dist/ui/Dock.d.ts +31 -0
- package/dist/ui/Meteors.d.ts +23 -0
- package/package.json +1 -1
- package/src/charts/GaugeChart.tsx +175 -0
- package/src/charts/LineChart.tsx +264 -0
- package/src/charts/RadarChart.tsx +215 -0
- package/src/charts/index.ts +3 -0
- package/src/index.ts +8 -1
- package/src/ui/AnimatedBeam.tsx +187 -0
- package/src/ui/BentoGrid.tsx +89 -0
- package/src/ui/BorderBeam.tsx +96 -0
- package/src/ui/Dock.tsx +124 -0
- package/src/ui/Meteors.tsx +109 -0
- package/dist/NumberInput-BQhVucw-.js +0 -491
- package/dist/cn-B6yFEsav.js +0 -8
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// BentoGrid + BentoCard — responsive grid of glass tiles with variable spans.
|
|
2
|
+
import type { JSX } from 'solid-js'
|
|
3
|
+
import { splitProps } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn'
|
|
6
|
+
|
|
7
|
+
export interface BentoGridProps {
|
|
8
|
+
children: JSX.Element
|
|
9
|
+
class?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BentoCardProps {
|
|
13
|
+
children: JSX.Element
|
|
14
|
+
/** Number of grid columns to span at `sm`+ (2) and `lg`+ (3). Defaults to 1. */
|
|
15
|
+
colSpan?: 1 | 2 | 3
|
|
16
|
+
/** Number of grid rows to span. Defaults to 1. */
|
|
17
|
+
rowSpan?: 1 | 2
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Static lookup so Tailwind's content scanner can see the full class names
|
|
22
|
+
// (dynamic `col-span-${n}` strings are invisible to it and get purged).
|
|
23
|
+
const COL_SPAN_CLASSES: Record<NonNullable<BentoCardProps['colSpan']>, string> = {
|
|
24
|
+
1: '',
|
|
25
|
+
2: 'sm:col-span-2',
|
|
26
|
+
3: 'sm:col-span-2 lg:col-span-3',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ROW_SPAN_CLASSES: Record<NonNullable<BentoCardProps['rowSpan']>, string> = {
|
|
30
|
+
1: '',
|
|
31
|
+
2: 'row-span-2',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Responsive CSS grid for bento-style layouts — 1 column on mobile, 2 at
|
|
36
|
+
* `sm`, 3 at `lg`. Compose with {@link BentoCard} tiles, using `colSpan`/
|
|
37
|
+
* `rowSpan` on each tile to create the varied-size bento look.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <BentoGrid>
|
|
42
|
+
* <BentoCard colSpan={2} rowSpan={2}>
|
|
43
|
+
* <h3 class="text-lg font-semibold">Overview</h3>
|
|
44
|
+
* </BentoCard>
|
|
45
|
+
* <BentoCard>
|
|
46
|
+
* <h3 class="text-lg font-semibold">Uptime</h3>
|
|
47
|
+
* </BentoCard>
|
|
48
|
+
* <BentoCard>
|
|
49
|
+
* <h3 class="text-lg font-semibold">Latency</h3>
|
|
50
|
+
* </BentoCard>
|
|
51
|
+
* <BentoCard colSpan={3}>
|
|
52
|
+
* <h3 class="text-lg font-semibold">Recent activity</h3>
|
|
53
|
+
* </BentoCard>
|
|
54
|
+
* </BentoGrid>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function BentoGrid(props: BentoGridProps): JSX.Element {
|
|
58
|
+
const [local, rest] = splitProps(props, ['class', 'children'])
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
class={cn(
|
|
62
|
+
'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 auto-rows-[minmax(11rem,auto)]',
|
|
63
|
+
local.class,
|
|
64
|
+
)}
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
67
|
+
{local.children}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Glass tile for a {@link BentoGrid}, spanning columns/rows via `colSpan`/`rowSpan`. */
|
|
73
|
+
export function BentoCard(props: BentoCardProps): JSX.Element {
|
|
74
|
+
const [local, rest] = splitProps(props, ['class', 'children', 'colSpan', 'rowSpan'])
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
class={cn(
|
|
78
|
+
'rounded-2xl border border-border bg-card p-5 text-card-foreground overflow-hidden',
|
|
79
|
+
'transition-transform hover:-translate-y-0.5',
|
|
80
|
+
COL_SPAN_CLASSES[local.colSpan ?? 1],
|
|
81
|
+
ROW_SPAN_CLASSES[local.rowSpan ?? 1],
|
|
82
|
+
local.class,
|
|
83
|
+
)}
|
|
84
|
+
{...rest}
|
|
85
|
+
>
|
|
86
|
+
{local.children}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// BorderBeam — a small gradient light segment that travels continuously
|
|
2
|
+
// around the border of its `position: relative` parent, tracing the box's
|
|
3
|
+
// own edge via CSS `offset-path: rect(...)` with `offset-distance` animated
|
|
4
|
+
// 0% → 100%. Pure CSS: no JS animation engine, no ResizeObserver — the
|
|
5
|
+
// browser resolves the rect()'s `auto` edges against the element's own box,
|
|
6
|
+
// so the path always matches the parent's current size however it resizes.
|
|
7
|
+
// The traveling element is a thin bar (not a full square) so it hugs the
|
|
8
|
+
// edge by itself, without needing a border-only mask trick. Reduced motion
|
|
9
|
+
// swaps the moving beam for a static, non-animated edge glow.
|
|
10
|
+
import { type JSX, Show } from 'solid-js'
|
|
11
|
+
|
|
12
|
+
import { cn } from '../lib/cn'
|
|
13
|
+
import { motionReduced } from '../lib/motion'
|
|
14
|
+
|
|
15
|
+
export interface BorderBeamProps {
|
|
16
|
+
/** Length of the traveling gradient segment, in px. @default 60 */
|
|
17
|
+
size?: number
|
|
18
|
+
/** Full trip duration around the border, in seconds. @default 6 */
|
|
19
|
+
duration?: number
|
|
20
|
+
/** Animation start offset, in seconds. @default 0 */
|
|
21
|
+
delay?: number
|
|
22
|
+
class?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const KEYFRAMES_ID = 'a4ui-border-beam-keyframes'
|
|
26
|
+
const KEYFRAMES = '@keyframes border-beam-travel { to { offset-distance: 100%; } }'
|
|
27
|
+
|
|
28
|
+
// Injected once, lazily, the first time a <BorderBeam> renders. The repo's
|
|
29
|
+
// CSS doctrine keeps shared @keyframes in src/styles/tokens.css, but this
|
|
30
|
+
// component ships standalone (tree-shakeable, no required stylesheet import)
|
|
31
|
+
// so it carries its own single small <style> tag instead — de-duped by id,
|
|
32
|
+
// so multiple instances on a page still only inject it once.
|
|
33
|
+
function ensureKeyframes(): void {
|
|
34
|
+
if (typeof document === 'undefined' || document.getElementById(KEYFRAMES_ID)) return
|
|
35
|
+
const style = document.createElement('style')
|
|
36
|
+
style.id = KEYFRAMES_ID
|
|
37
|
+
style.textContent = KEYFRAMES
|
|
38
|
+
document.head.appendChild(style)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decorative light segment that travels continuously around the border of
|
|
43
|
+
* its parent. The parent must be `position: relative` (or similar) — this
|
|
44
|
+
* renders an absolutely-positioned, `pointer-events-none` layer (`inset-0`,
|
|
45
|
+
* `rounded-[inherit]`) tracking the parent's own box. Purely cosmetic
|
|
46
|
+
* (`aria-hidden`); under reduced motion the moving beam is replaced with a
|
|
47
|
+
* static edge glow instead of animating.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <div class="relative overflow-hidden rounded-2xl border border-border p-6">
|
|
52
|
+
* <BorderBeam />
|
|
53
|
+
* <p>Card content</p>
|
|
54
|
+
* </div>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function BorderBeam(props: BorderBeamProps): JSX.Element {
|
|
58
|
+
const size = () => props.size ?? 60
|
|
59
|
+
const duration = () => props.duration ?? 6
|
|
60
|
+
const delay = () => props.delay ?? 0
|
|
61
|
+
|
|
62
|
+
if (!motionReduced()) ensureKeyframes()
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Show
|
|
66
|
+
when={!motionReduced()}
|
|
67
|
+
fallback={
|
|
68
|
+
<span
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
class={cn(
|
|
71
|
+
'pointer-events-none absolute inset-0 rounded-[inherit] shadow-[inset_0_0_0_1px_hsl(var(--primary)/0.35)]',
|
|
72
|
+
props.class,
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<span
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
class={cn('pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]', props.class)}
|
|
80
|
+
>
|
|
81
|
+
<span
|
|
82
|
+
class="absolute top-0 left-0 h-[3px] rounded-full"
|
|
83
|
+
style={{
|
|
84
|
+
width: `${size()}px`,
|
|
85
|
+
'offset-path': `rect(0 auto auto 0 round ${size()}px)`,
|
|
86
|
+
'offset-distance': '0%',
|
|
87
|
+
background:
|
|
88
|
+
'linear-gradient(to right, transparent, hsl(var(--primary)), hsl(var(--accent)), transparent)',
|
|
89
|
+
animation: `border-beam-travel ${duration()}s linear infinite`,
|
|
90
|
+
'animation-delay': `${delay()}s`,
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</span>
|
|
94
|
+
</Show>
|
|
95
|
+
)
|
|
96
|
+
}
|
package/src/ui/Dock.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Dock — macOS-style app dock. A horizontal glass pill of icon buttons that
|
|
2
|
+
// magnify toward the cursor (closest item biggest, tapering over a fixed
|
|
3
|
+
// radius), driven by direct transform + CSS transition (scale only, so it
|
|
4
|
+
// stays on the compositor). No-op magnify under reduced motion — icons stay
|
|
5
|
+
// static but remain clickable.
|
|
6
|
+
import { For, Show, type JSX } from 'solid-js'
|
|
7
|
+
|
|
8
|
+
import { cn } from '../lib/cn'
|
|
9
|
+
import { motionReduced } from '../lib/motion'
|
|
10
|
+
|
|
11
|
+
/** Px radius from the cursor within which items magnify; beyond it, scale is 1×. */
|
|
12
|
+
const FALLOFF_PX = 120
|
|
13
|
+
/** Peak scale applied to the item directly under the cursor. */
|
|
14
|
+
const PEAK_SCALE = 1.6
|
|
15
|
+
|
|
16
|
+
export interface DockItem {
|
|
17
|
+
icon: JSX.Element
|
|
18
|
+
label?: string
|
|
19
|
+
onClick?: (e: MouseEvent) => void
|
|
20
|
+
href?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DockProps {
|
|
24
|
+
items: DockItem[]
|
|
25
|
+
class?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* macOS-style dock: a glass pill of icon buttons that magnify based on
|
|
30
|
+
* horizontal distance to the cursor. Scale-only transform (`transform-origin:
|
|
31
|
+
* bottom`), reset to 1× on pointerleave. Respects `prefers-reduced-motion`
|
|
32
|
+
* (renders static, still fully clickable/keyboard-focusable).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { Home, Search, Settings } from 'lucide-solid'
|
|
37
|
+
*
|
|
38
|
+
* <Dock
|
|
39
|
+
* items={[
|
|
40
|
+
* { icon: <Home size={22} />, label: 'Home', onClick: () => {} },
|
|
41
|
+
* { icon: <Search size={22} />, label: 'Search', onClick: () => {} },
|
|
42
|
+
* { icon: <Settings size={22} />, label: 'Settings', href: '/settings' },
|
|
43
|
+
* ]}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function Dock(props: DockProps): JSX.Element {
|
|
48
|
+
const itemEls: (HTMLElement | undefined)[] = []
|
|
49
|
+
|
|
50
|
+
const handlePointerMove = (event: PointerEvent): void => {
|
|
51
|
+
if (motionReduced()) return
|
|
52
|
+
|
|
53
|
+
for (const el of itemEls) {
|
|
54
|
+
if (!el) continue
|
|
55
|
+
const rect = el.getBoundingClientRect()
|
|
56
|
+
const center = rect.left + rect.width / 2
|
|
57
|
+
const distance = Math.abs(event.clientX - center)
|
|
58
|
+
const scale = 1 + (PEAK_SCALE - 1) * Math.max(0, 1 - distance / FALLOFF_PX)
|
|
59
|
+
el.style.transform = `scale(${scale})`
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handlePointerLeave = (): void => {
|
|
64
|
+
for (const el of itemEls) {
|
|
65
|
+
if (el) el.style.transform = 'scale(1)'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
class={cn(
|
|
72
|
+
'inline-flex items-end gap-2 rounded-2xl border border-border bg-card/70 px-3 py-2 backdrop-blur',
|
|
73
|
+
props.class,
|
|
74
|
+
)}
|
|
75
|
+
onPointerMove={handlePointerMove}
|
|
76
|
+
onPointerLeave={handlePointerLeave}
|
|
77
|
+
>
|
|
78
|
+
<For each={props.items}>
|
|
79
|
+
{(item, i) => {
|
|
80
|
+
const shared = {
|
|
81
|
+
ref: (el: HTMLElement) => {
|
|
82
|
+
itemEls[i()] = el
|
|
83
|
+
},
|
|
84
|
+
class:
|
|
85
|
+
'group relative inline-flex h-11 w-11 shrink-0 origin-bottom items-center justify-center rounded-xl text-foreground transition-transform duration-150 ease-out will-change-transform hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
86
|
+
'aria-label': item.label,
|
|
87
|
+
onClick: item.onClick,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Show
|
|
92
|
+
when={item.href}
|
|
93
|
+
fallback={
|
|
94
|
+
<button type="button" {...shared}>
|
|
95
|
+
{item.icon}
|
|
96
|
+
<Show when={item.label}>
|
|
97
|
+
<span
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md border border-border bg-card px-2 py-1 text-xs whitespace-nowrap text-card-foreground opacity-0 shadow-md transition-opacity duration-150 group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
100
|
+
>
|
|
101
|
+
{item.label}
|
|
102
|
+
</span>
|
|
103
|
+
</Show>
|
|
104
|
+
</button>
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
<a href={item.href} {...shared}>
|
|
108
|
+
{item.icon}
|
|
109
|
+
<Show when={item.label}>
|
|
110
|
+
<span
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md border border-border bg-card px-2 py-1 text-xs whitespace-nowrap text-card-foreground opacity-0 shadow-md transition-opacity duration-150 group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
113
|
+
>
|
|
114
|
+
{item.label}
|
|
115
|
+
</span>
|
|
116
|
+
</Show>
|
|
117
|
+
</a>
|
|
118
|
+
</Show>
|
|
119
|
+
)
|
|
120
|
+
}}
|
|
121
|
+
</For>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Meteors — Aceternity-style decorative meteor shower: thin diagonal streaks
|
|
2
|
+
// that fall down-left and fade, looping with per-meteor stagger. Pure CSS
|
|
3
|
+
// keyframes (injected once into the document head, like LoadingDots) so the
|
|
4
|
+
// motion costs nothing in JS; each meteor's position/delay/duration is derived
|
|
5
|
+
// deterministically from its index (no Math.random, so the layout is
|
|
6
|
+
// reproducible across renders and safe under SSR).
|
|
7
|
+
import { For, type JSX, Show } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { cn } from '../lib/cn'
|
|
10
|
+
import { motionReduced } from '../lib/motion'
|
|
11
|
+
|
|
12
|
+
export interface MeteorsProps {
|
|
13
|
+
/** Number of meteor streaks to render. @default 20 */
|
|
14
|
+
count?: number
|
|
15
|
+
class?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const STYLE_ID = 'a4ui-meteors-style'
|
|
19
|
+
|
|
20
|
+
function ensureStyleInjected(): void {
|
|
21
|
+
if (typeof document === 'undefined' || document.getElementById(STYLE_ID)) return
|
|
22
|
+
const style = document.createElement('style')
|
|
23
|
+
style.id = STYLE_ID
|
|
24
|
+
style.textContent = `
|
|
25
|
+
@keyframes a4-meteor-fall {
|
|
26
|
+
0% { transform: rotate(215deg) translateX(0); opacity: 1; }
|
|
27
|
+
70% { opacity: 1; }
|
|
28
|
+
100% { transform: rotate(215deg) translateX(-500px); opacity: 0; }
|
|
29
|
+
}
|
|
30
|
+
`
|
|
31
|
+
document.head.appendChild(style)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Deterministic pseudo-random value in [0, 1) derived from an index via a
|
|
36
|
+
* sine-based hash. Used to vary each meteor's position/timing without
|
|
37
|
+
* `Math.random`, so the layout is identical across renders (and SSR-safe).
|
|
38
|
+
*/
|
|
39
|
+
function pseudoRandom(seed: number): number {
|
|
40
|
+
const x = Math.sin(seed * 12.9898) * 43758.5453
|
|
41
|
+
return x - Math.floor(x)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decorative full-cover meteor shower: thin diagonal streaks that fall
|
|
46
|
+
* down-left and fade, looping with staggered delays and varied start
|
|
47
|
+
* positions/durations. Purely visual — absolute, `pointer-events-none`,
|
|
48
|
+
* `aria-hidden` — so it layers behind/inside a `relative overflow-hidden`
|
|
49
|
+
* container without affecting layout or a11y. Under {@link motionReduced} it
|
|
50
|
+
* swaps the falling streaks for a handful of static, faint dots.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <div class="relative overflow-hidden rounded-2xl border border-border p-8">
|
|
55
|
+
* <Meteors count={20} />
|
|
56
|
+
* <p class="relative">Content sits above the meteors.</p>
|
|
57
|
+
* </div>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function Meteors(props: MeteorsProps): JSX.Element {
|
|
61
|
+
ensureStyleInjected()
|
|
62
|
+
|
|
63
|
+
const count = () => props.count ?? 20
|
|
64
|
+
const meteors = () => Array.from({ length: count() })
|
|
65
|
+
const staticDots = () => Array.from({ length: Math.min(6, count()) })
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div class={cn('pointer-events-none absolute inset-0 overflow-hidden', props.class)} aria-hidden="true">
|
|
69
|
+
<Show
|
|
70
|
+
when={!motionReduced()}
|
|
71
|
+
fallback={
|
|
72
|
+
<For each={staticDots()}>
|
|
73
|
+
{(_, i) => (
|
|
74
|
+
<span
|
|
75
|
+
class="absolute h-0.5 w-0.5 rounded-full bg-foreground/30"
|
|
76
|
+
style={{
|
|
77
|
+
top: `${pseudoRandom(i() * 2 + 1) * 100}%`,
|
|
78
|
+
left: `${pseudoRandom(i() * 2 + 2) * 100}%`,
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</For>
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
<For each={meteors()}>
|
|
86
|
+
{(_, i) => {
|
|
87
|
+
const left = pseudoRandom(i() * 2 + 1) * 100
|
|
88
|
+
const delay = pseudoRandom(i() * 2 + 2) * 8
|
|
89
|
+
const duration = 2 + pseudoRandom(i() * 3 + 5) * 5
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<span
|
|
93
|
+
class={cn(
|
|
94
|
+
'absolute top-0 h-0.5 w-0.5 rotate-[215deg] rounded-full bg-foreground',
|
|
95
|
+
"before:absolute before:top-1/2 before:h-px before:w-12 before:-translate-y-1/2 before:bg-gradient-to-r before:from-foreground/60 before:to-transparent before:content-['']",
|
|
96
|
+
)}
|
|
97
|
+
style={{
|
|
98
|
+
left: `${left}%`,
|
|
99
|
+
animation: `a4-meteor-fall ${duration}s linear infinite`,
|
|
100
|
+
'animation-delay': `${delay}s`,
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
}}
|
|
105
|
+
</For>
|
|
106
|
+
</Show>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|