@a4ui/core 0.13.0 → 0.14.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 +48 -38
- package/dist/elements.css +3 -0
- package/dist/full.css +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2451 -2305
- package/dist/ui/NotificationStack.d.ts +7 -7
- package/dist/ui/Ripple.d.ts +25 -0
- package/dist/ui/Typewriter.d.ts +28 -0
- package/package.json +14 -6
- package/src/index.ts +3 -1
- package/src/ui/NotificationStack.tsx +101 -66
- package/src/ui/Ripple.tsx +119 -0
- package/src/ui/Typewriter.tsx +149 -0
- package/src/elements.tsx +0 -167
- package/src/styles/space.css +0 -510
- package/src/styles/tokens.css +0 -213
package/src/elements.tsx
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
// Web Components entry (`@a4ui/core/elements`) — a curated subset of A4ui's
|
|
2
|
-
// presentational components registered as framework-agnostic custom elements so
|
|
3
|
-
// they can be used from React/Next.js, Vue, Svelte, or plain HTML.
|
|
4
|
-
//
|
|
5
|
-
// Design notes:
|
|
6
|
-
// - Each element renders in LIGHT DOM (noShadowDOM) so the global stylesheet
|
|
7
|
-
// (`@a4ui/core/elements.css`) and CSS theme variables apply normally — Shadow
|
|
8
|
-
// DOM would isolate the Tailwind utility classes the components rely on.
|
|
9
|
-
// - Props map to attributes/properties (solid-element coerces by the declared
|
|
10
|
-
// default's type: number/boolean/string). Text content is taken from a `label`
|
|
11
|
-
// or `text` attribute, falling back to the element's initial text.
|
|
12
|
-
// - Only leaf, primitive-prop components are wrapped. Rich/interactive/portalled
|
|
13
|
-
// components (Modal, Combobox, DataGrid, …) remain SolidJS-only — see the docs.
|
|
14
|
-
//
|
|
15
|
-
// This bundle is self-contained (solid-js is bundled in), so non-Solid apps need
|
|
16
|
-
// no Solid toolchain: `import '@a4ui/core/elements'` once, then use the tags.
|
|
17
|
-
import { customElement, noShadowDOM } from 'solid-element'
|
|
18
|
-
import type { JSX } from 'solid-js'
|
|
19
|
-
|
|
20
|
-
import { Alert, type AlertTone } from './ui/Alert'
|
|
21
|
-
import { Avatar } from './ui/Avatar'
|
|
22
|
-
import { Badge, type BadgeTone } from './ui/Badge'
|
|
23
|
-
import { Button, type ButtonVariant } from './ui/Button'
|
|
24
|
-
import { Clock } from './ui/Clock'
|
|
25
|
-
import { Countdown } from './ui/Countdown'
|
|
26
|
-
import { Kbd } from './ui/Kbd'
|
|
27
|
-
import { Meter } from './ui/Meter'
|
|
28
|
-
import { Progress } from './ui/Progress'
|
|
29
|
-
import { Rating } from './ui/Rating'
|
|
30
|
-
import { RingProgress } from './ui/RingProgress'
|
|
31
|
-
import { Separator } from './ui/Separator'
|
|
32
|
-
import { Spinner } from './ui/Spinner'
|
|
33
|
-
import { Stat, type StatTone } from './ui/Stat'
|
|
34
|
-
|
|
35
|
-
/** The initial text an author put between the tags, before Solid renders over it. */
|
|
36
|
-
function initialText(element: unknown): string {
|
|
37
|
-
return ((element as HTMLElement).textContent ?? '').trim()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Register every A4ui custom element. Import for its side effect. */
|
|
41
|
-
export function defineA4uiElements(): void {
|
|
42
|
-
customElement(
|
|
43
|
-
'a4-button',
|
|
44
|
-
{ variant: 'primary', disabled: false, type: 'button', label: '' },
|
|
45
|
-
(props, { element }): JSX.Element => {
|
|
46
|
-
noShadowDOM()
|
|
47
|
-
const text = props.label || initialText(element)
|
|
48
|
-
return (
|
|
49
|
-
<Button
|
|
50
|
-
variant={props.variant as ButtonVariant}
|
|
51
|
-
disabled={props.disabled}
|
|
52
|
-
type={props.type as 'button' | 'submit' | 'reset'}
|
|
53
|
-
>
|
|
54
|
-
{text}
|
|
55
|
-
</Button>
|
|
56
|
-
)
|
|
57
|
-
},
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
customElement('a4-badge', { tone: 'neutral', label: '' }, (props, { element }): JSX.Element => {
|
|
61
|
-
noShadowDOM()
|
|
62
|
-
return <Badge tone={props.tone as BadgeTone}>{props.label || initialText(element)}</Badge>
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
customElement(
|
|
66
|
-
// `heading`, not `title` — the latter is a global HTML attribute and would
|
|
67
|
-
// clash with the element's native `title` (tooltip) property.
|
|
68
|
-
'a4-alert',
|
|
69
|
-
{ tone: 'info', heading: '', text: '' },
|
|
70
|
-
(props, { element }): JSX.Element => {
|
|
71
|
-
noShadowDOM()
|
|
72
|
-
return (
|
|
73
|
-
<Alert tone={props.tone as AlertTone} title={props.heading || undefined}>
|
|
74
|
-
{props.text || initialText(element)}
|
|
75
|
-
</Alert>
|
|
76
|
-
)
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
customElement('a4-spinner', { label: 'Loading' }, (props): JSX.Element => {
|
|
81
|
-
noShadowDOM()
|
|
82
|
-
return <Spinner label={props.label} />
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
customElement('a4-avatar', { src: '', alt: '', fallback: '' }, (props): JSX.Element => {
|
|
86
|
-
noShadowDOM()
|
|
87
|
-
return <Avatar src={props.src || undefined} alt={props.alt} fallback={props.fallback || ''} />
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
customElement('a4-progress', { value: 0, max: 100, label: '' }, (props): JSX.Element => {
|
|
91
|
-
noShadowDOM()
|
|
92
|
-
return <Progress value={props.value} max={props.max} label={props.label || undefined} />
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
customElement('a4-meter', { value: 0, max: 100, label: '' }, (props): JSX.Element => {
|
|
96
|
-
noShadowDOM()
|
|
97
|
-
return <Meter value={props.value} max={props.max} label={props.label || undefined} />
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
customElement('a4-ring-progress', { value: 0, size: 96, thickness: 8, label: '' }, (props): JSX.Element => {
|
|
101
|
-
noShadowDOM()
|
|
102
|
-
return (
|
|
103
|
-
<RingProgress
|
|
104
|
-
value={props.value}
|
|
105
|
-
size={props.size}
|
|
106
|
-
thickness={props.thickness}
|
|
107
|
-
label={props.label || undefined}
|
|
108
|
-
/>
|
|
109
|
-
)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
customElement('a4-stat', { label: '', value: 0, tone: 'neutral' }, (props): JSX.Element => {
|
|
113
|
-
noShadowDOM()
|
|
114
|
-
return <Stat label={props.label} value={props.value} tone={props.tone as StatTone} />
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
customElement('a4-kbd', { label: '' }, (props, { element }): JSX.Element => {
|
|
118
|
-
noShadowDOM()
|
|
119
|
-
return <Kbd>{props.label || initialText(element)}</Kbd>
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
customElement('a4-separator', { orientation: 'horizontal' }, (props): JSX.Element => {
|
|
123
|
-
noShadowDOM()
|
|
124
|
-
return <Separator orientation={props.orientation as 'horizontal' | 'vertical'} />
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
customElement('a4-rating', { value: 0, max: 5, readonly: false }, (props, { element }): JSX.Element => {
|
|
128
|
-
noShadowDOM()
|
|
129
|
-
return (
|
|
130
|
-
<Rating
|
|
131
|
-
value={props.value}
|
|
132
|
-
max={props.max}
|
|
133
|
-
readonly={props.readonly}
|
|
134
|
-
onChange={(v) =>
|
|
135
|
-
(element as unknown as HTMLElement).dispatchEvent(new CustomEvent('change', { detail: v }))
|
|
136
|
-
}
|
|
137
|
-
/>
|
|
138
|
-
)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
customElement('a4-countdown', { to: '' }, (props): JSX.Element => {
|
|
142
|
-
noShadowDOM()
|
|
143
|
-
// eslint-disable-next-line solid/reactivity -- read the `to` attribute once to fix the target
|
|
144
|
-
const target = props.to ? new Date(props.to) : new Date(Date.now() + 86_400_000)
|
|
145
|
-
return <Countdown to={target} />
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
customElement(
|
|
149
|
-
'a4-clock',
|
|
150
|
-
{ variant: 'digital', seconds: true, hour12: false, timezone: '', size: 160 },
|
|
151
|
-
(props): JSX.Element => {
|
|
152
|
-
noShadowDOM()
|
|
153
|
-
return (
|
|
154
|
-
<Clock
|
|
155
|
-
variant={props.variant as 'digital' | 'analog'}
|
|
156
|
-
seconds={props.seconds}
|
|
157
|
-
hour12={props.hour12}
|
|
158
|
-
timeZone={props.timezone || undefined}
|
|
159
|
-
size={props.size}
|
|
160
|
-
/>
|
|
161
|
-
)
|
|
162
|
-
},
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Registering on import is the ergonomic default for `import '@a4ui/core/elements'`.
|
|
167
|
-
defineA4uiElements()
|
package/src/styles/space.css
DELETED
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
/* A4ui — starfield backdrop styles for <SpaceBackground/>.
|
|
2
|
-
*
|
|
3
|
-
* Extracted from the source project's space-theme.css. Mount SpaceBackground as
|
|
4
|
-
* a single fixed full-viewport layer BEHIND all content (z-index 0), with app
|
|
5
|
-
* content at z-index >= 10 (AppShell already does this). Bundled into
|
|
6
|
-
* `a4ui/styles.css` after the component surfaces. */
|
|
7
|
-
|
|
8
|
-
#space {
|
|
9
|
-
position: fixed;
|
|
10
|
-
inset: 0;
|
|
11
|
-
z-index: 0;
|
|
12
|
-
pointer-events: none;
|
|
13
|
-
overflow: hidden;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/* Nebula tinted from the active theme (--primary / --accent / --destructive), so
|
|
17
|
-
switching palettes recolors the whole backdrop. Space's tokens keep it blue. */
|
|
18
|
-
#space .sky {
|
|
19
|
-
position: absolute;
|
|
20
|
-
inset: 0;
|
|
21
|
-
background:
|
|
22
|
-
radial-gradient(1200px 700px at 82% -8%, hsl(var(--primary) / 0.34), transparent 60%),
|
|
23
|
-
radial-gradient(900px 600px at 8% 12%, hsl(var(--accent) / 0.28), transparent 60%),
|
|
24
|
-
radial-gradient(1000px 800px at 50% 120%, hsl(var(--accent) / 0.22), transparent 55%),
|
|
25
|
-
radial-gradient(700px 500px at 30% 55%, hsl(var(--destructive) / 0.12), transparent 60%),
|
|
26
|
-
hsl(var(--background));
|
|
27
|
-
}
|
|
28
|
-
[data-theme='light'] #space .sky {
|
|
29
|
-
background:
|
|
30
|
-
radial-gradient(1100px 680px at 84% -10%, hsl(var(--primary) / 0.22), transparent 60%),
|
|
31
|
-
radial-gradient(900px 600px at 6% 8%, hsl(var(--accent) / 0.2), transparent 60%),
|
|
32
|
-
radial-gradient(1000px 820px at 50% 120%, hsl(var(--accent) / 0.16), transparent 55%),
|
|
33
|
-
radial-gradient(700px 500px at 30% 55%, hsl(var(--destructive) / 0.1), transparent 60%),
|
|
34
|
-
hsl(var(--background));
|
|
35
|
-
}
|
|
36
|
-
@keyframes nebulaShift {
|
|
37
|
-
0%,
|
|
38
|
-
100% {
|
|
39
|
-
filter: hue-rotate(0deg);
|
|
40
|
-
}
|
|
41
|
-
50% {
|
|
42
|
-
filter: hue-rotate(18deg);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/* Aurora — dark theme only */
|
|
47
|
-
#space .aurora {
|
|
48
|
-
position: absolute;
|
|
49
|
-
top: -10%;
|
|
50
|
-
left: -10%;
|
|
51
|
-
right: -10%;
|
|
52
|
-
height: 46%;
|
|
53
|
-
background: linear-gradient(
|
|
54
|
-
180deg,
|
|
55
|
-
hsl(var(--accent) / 0.16),
|
|
56
|
-
hsl(var(--primary) / 0.1) 45%,
|
|
57
|
-
transparent 80%
|
|
58
|
-
);
|
|
59
|
-
filter: blur(40px);
|
|
60
|
-
opacity: 0.55;
|
|
61
|
-
mix-blend-mode: screen;
|
|
62
|
-
}
|
|
63
|
-
[data-theme='light'] #space .aurora {
|
|
64
|
-
display: none;
|
|
65
|
-
}
|
|
66
|
-
@keyframes auroraSway {
|
|
67
|
-
0%,
|
|
68
|
-
100% {
|
|
69
|
-
transform: translateX(-2%) scaleY(1);
|
|
70
|
-
opacity: 0.5;
|
|
71
|
-
}
|
|
72
|
-
50% {
|
|
73
|
-
transform: translateX(3%) scaleY(1.08);
|
|
74
|
-
opacity: 0.68;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/* Randomized starfield — each star is generated in SpaceBackground.tsx with a
|
|
79
|
-
random position/size/brightness/bloom, so there's no visible tiling pattern.
|
|
80
|
-
Static (no drift): moving the field behind the frosted cards re-blurs every
|
|
81
|
-
frame and felt laggy; a subset twinkles for life instead. */
|
|
82
|
-
#space #starfield {
|
|
83
|
-
position: absolute;
|
|
84
|
-
inset: 0;
|
|
85
|
-
}
|
|
86
|
-
[data-theme='light'] #space #starfield {
|
|
87
|
-
opacity: 0.85;
|
|
88
|
-
}
|
|
89
|
-
/* A few stars drift smoothly like the satellite: each is a tiny element moving
|
|
90
|
-
via its own transform (GPU-composited, cheap), NOT the whole field. Uses
|
|
91
|
-
`alternate` so it eases out and back with no loop jump. Per-star --dx/--dy. */
|
|
92
|
-
@keyframes starDrift {
|
|
93
|
-
from {
|
|
94
|
-
transform: translate(0, 0);
|
|
95
|
-
}
|
|
96
|
-
to {
|
|
97
|
-
transform: translate(var(--dx, 0), var(--dy, 0));
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/* Rocket exhaust flame (custom SVG rocket in SpaceBackground.tsx) — flickers. */
|
|
102
|
-
.rk-flame {
|
|
103
|
-
transform-box: fill-box;
|
|
104
|
-
transform-origin: 50% 0%;
|
|
105
|
-
animation: rocketFlame 0.12s ease-in-out infinite alternate;
|
|
106
|
-
}
|
|
107
|
-
@keyframes rocketFlame {
|
|
108
|
-
from {
|
|
109
|
-
transform: scaleY(0.8);
|
|
110
|
-
opacity: 0.85;
|
|
111
|
-
}
|
|
112
|
-
to {
|
|
113
|
-
transform: scaleY(1.06);
|
|
114
|
-
opacity: 1;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/* Legacy tiled star layers (kept for parity; the current SpaceBackground scatters
|
|
119
|
-
individual stars into #starfield instead). Sized generously: small dots nearly
|
|
120
|
-
vanish under backdrop-filter blur, so keep them >= 2.5px. */
|
|
121
|
-
#space .stars {
|
|
122
|
-
position: absolute;
|
|
123
|
-
inset: -50%;
|
|
124
|
-
background-repeat: repeat;
|
|
125
|
-
background-image:
|
|
126
|
-
radial-gradient(4px 4px at 20px 30px, hsl(var(--foreground)), transparent),
|
|
127
|
-
radial-gradient(3.6px 3.6px at 120px 90px, hsl(var(--foreground)), transparent),
|
|
128
|
-
radial-gradient(3.2px 3.2px at 210px 160px, hsl(217 91% 78%), transparent),
|
|
129
|
-
radial-gradient(4.8px 4.8px at 300px 60px, hsl(199 89% 82%), transparent),
|
|
130
|
-
radial-gradient(3.4px 3.4px at 380px 220px, hsl(330 80% 84%), transparent),
|
|
131
|
-
radial-gradient(3px 3px at 90px 190px, hsl(var(--foreground)), transparent),
|
|
132
|
-
radial-gradient(2.8px 2.8px at 260px 250px, hsl(48 90% 82%), transparent);
|
|
133
|
-
background-size: 420px 300px;
|
|
134
|
-
opacity: 1;
|
|
135
|
-
animation: drift 140s linear infinite;
|
|
136
|
-
}
|
|
137
|
-
[data-theme='light'] #space .stars {
|
|
138
|
-
background-image:
|
|
139
|
-
radial-gradient(4px 4px at 20px 30px, hsl(38 92% 50%), transparent),
|
|
140
|
-
radial-gradient(3.6px 3.6px at 120px 90px, hsl(42 90% 52%), transparent),
|
|
141
|
-
radial-gradient(3.2px 3.2px at 210px 160px, hsl(32 88% 48%), transparent),
|
|
142
|
-
radial-gradient(4.8px 4.8px at 300px 60px, hsl(45 95% 55%), transparent),
|
|
143
|
-
radial-gradient(3.4px 3.4px at 380px 220px, hsl(28 85% 50%), transparent),
|
|
144
|
-
radial-gradient(3px 3px at 90px 190px, hsl(40 90% 50%), transparent),
|
|
145
|
-
radial-gradient(2.8px 2.8px at 260px 250px, hsl(48 90% 52%), transparent);
|
|
146
|
-
}
|
|
147
|
-
#space .stars.s2 {
|
|
148
|
-
background-size: 640px 480px;
|
|
149
|
-
opacity: 0.85;
|
|
150
|
-
animation-duration: 200s;
|
|
151
|
-
}
|
|
152
|
-
#space .stars.s3 {
|
|
153
|
-
background-size: 260px 190px;
|
|
154
|
-
opacity: 1;
|
|
155
|
-
animation-duration: 95s;
|
|
156
|
-
background-image:
|
|
157
|
-
radial-gradient(6px 6px at 40px 50px, hsl(var(--foreground)), transparent),
|
|
158
|
-
radial-gradient(5.6px 5.6px at 160px 130px, hsl(199 89% 85%), transparent),
|
|
159
|
-
radial-gradient(5.8px 5.8px at 250px 40px, hsl(275 80% 86%), transparent),
|
|
160
|
-
radial-gradient(5px 5px at 90px 20px, hsl(330 85% 85%), transparent);
|
|
161
|
-
}
|
|
162
|
-
[data-theme='light'] #space .stars.s3 {
|
|
163
|
-
opacity: 0.8;
|
|
164
|
-
background-image:
|
|
165
|
-
radial-gradient(6px 6px at 40px 50px, hsl(38 92% 50%), transparent),
|
|
166
|
-
radial-gradient(5.6px 5.6px at 160px 130px, hsl(45 95% 54%), transparent),
|
|
167
|
-
radial-gradient(5.8px 5.8px at 250px 40px, hsl(30 88% 48%), transparent),
|
|
168
|
-
radial-gradient(5px 5px at 90px 20px, hsl(50 92% 55%), transparent);
|
|
169
|
-
}
|
|
170
|
-
@keyframes drift {
|
|
171
|
-
from {
|
|
172
|
-
transform: translate3d(0, 0, 0);
|
|
173
|
-
}
|
|
174
|
-
to {
|
|
175
|
-
transform: translate3d(-420px, -300px, 0);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/* Individually-placed twinkling stars */
|
|
180
|
-
.twinkle {
|
|
181
|
-
position: absolute;
|
|
182
|
-
width: 5px;
|
|
183
|
-
height: 5px;
|
|
184
|
-
border-radius: 50%;
|
|
185
|
-
background: hsl(var(--foreground));
|
|
186
|
-
box-shadow: 0 0 14px 3px hsl(var(--foreground) / 0.9);
|
|
187
|
-
animation: twinkle 3.2s ease-in-out infinite;
|
|
188
|
-
}
|
|
189
|
-
[data-theme='light'] .twinkle {
|
|
190
|
-
background: hsl(40 92% 50%);
|
|
191
|
-
box-shadow: 0 0 14px 3px hsl(40 92% 50% / 0.85);
|
|
192
|
-
}
|
|
193
|
-
@keyframes twinkle {
|
|
194
|
-
0%,
|
|
195
|
-
100% {
|
|
196
|
-
opacity: 0.25;
|
|
197
|
-
transform: scale(0.6);
|
|
198
|
-
}
|
|
199
|
-
50% {
|
|
200
|
-
opacity: 1;
|
|
201
|
-
transform: scale(1.15);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/* Constellations — thin line art connecting a handful of stars */
|
|
206
|
-
#space .constellation {
|
|
207
|
-
position: absolute;
|
|
208
|
-
opacity: 1;
|
|
209
|
-
}
|
|
210
|
-
[data-theme='light'] #space .constellation {
|
|
211
|
-
opacity: 0.6;
|
|
212
|
-
}
|
|
213
|
-
[data-theme='light'] #space .constellation .cline {
|
|
214
|
-
stroke: hsl(38 92% 50%) !important;
|
|
215
|
-
}
|
|
216
|
-
[data-theme='light'] #space .constellation .cdot {
|
|
217
|
-
fill: hsl(222 47% 20%) !important;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/* Planets / moons — decorative, corner-anchored, gentle float */
|
|
221
|
-
.planet {
|
|
222
|
-
border-radius: 50%;
|
|
223
|
-
}
|
|
224
|
-
.planet::after {
|
|
225
|
-
content: '';
|
|
226
|
-
position: absolute;
|
|
227
|
-
inset: -34% -55%;
|
|
228
|
-
border-radius: 50%;
|
|
229
|
-
border: 3px solid hsl(var(--foreground) / 0.18);
|
|
230
|
-
transform: rotate(-24deg);
|
|
231
|
-
}
|
|
232
|
-
.planet-glow {
|
|
233
|
-
border-radius: 50%;
|
|
234
|
-
filter: blur(2px);
|
|
235
|
-
}
|
|
236
|
-
@keyframes floaty {
|
|
237
|
-
0%,
|
|
238
|
-
100% {
|
|
239
|
-
transform: translateY(0) rotate(0deg);
|
|
240
|
-
}
|
|
241
|
-
50% {
|
|
242
|
-
transform: translateY(-14px) rotate(4deg);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
.floaty {
|
|
246
|
-
animation: floaty 12s ease-in-out infinite;
|
|
247
|
-
}
|
|
248
|
-
.floaty.slow {
|
|
249
|
-
animation-duration: 18s;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/* Asteroid belt (small drifting rocks) */
|
|
253
|
-
.asteroid {
|
|
254
|
-
border-radius: 40% 55% 50% 45%;
|
|
255
|
-
background: hsl(var(--foreground) / 0.35);
|
|
256
|
-
}
|
|
257
|
-
@keyframes asteroidDrift {
|
|
258
|
-
0%,
|
|
259
|
-
100% {
|
|
260
|
-
transform: translate(0, 0) rotate(0deg);
|
|
261
|
-
}
|
|
262
|
-
50% {
|
|
263
|
-
transform: translate(-8px, 6px) rotate(25deg);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/* Shooting star — bright core + tapered glowing trail */
|
|
268
|
-
.shooter {
|
|
269
|
-
position: absolute;
|
|
270
|
-
height: 2.5px;
|
|
271
|
-
border-radius: 999px;
|
|
272
|
-
transform-origin: left center;
|
|
273
|
-
background: linear-gradient(
|
|
274
|
-
90deg,
|
|
275
|
-
hsla(0, 0%, 100%, 0) 0%,
|
|
276
|
-
hsla(199, 90%, 85%, 0.55) 55%,
|
|
277
|
-
hsla(0, 0%, 100%, 0.98) 100%
|
|
278
|
-
);
|
|
279
|
-
filter: blur(0.2px) drop-shadow(0 0 3px hsla(199, 90%, 80%, 0.5));
|
|
280
|
-
}
|
|
281
|
-
.shooter::before {
|
|
282
|
-
content: '';
|
|
283
|
-
position: absolute;
|
|
284
|
-
right: -2px;
|
|
285
|
-
top: 50%;
|
|
286
|
-
width: 6px;
|
|
287
|
-
height: 6px;
|
|
288
|
-
border-radius: 50%;
|
|
289
|
-
transform: translateY(-50%);
|
|
290
|
-
background: radial-gradient(circle, #fff 0%, hsla(199, 95%, 88%, 0.95) 50%, transparent 75%);
|
|
291
|
-
box-shadow:
|
|
292
|
-
0 0 8px 2px #fff,
|
|
293
|
-
0 0 20px 7px hsla(199, 90%, 75%, 0.85),
|
|
294
|
-
0 0 38px 14px hsla(199, 90%, 60%, 0.4);
|
|
295
|
-
}
|
|
296
|
-
.shooter::after {
|
|
297
|
-
content: '';
|
|
298
|
-
position: absolute;
|
|
299
|
-
left: 0;
|
|
300
|
-
top: 50%;
|
|
301
|
-
width: 55%;
|
|
302
|
-
height: 1px;
|
|
303
|
-
transform: translateY(-50%);
|
|
304
|
-
background: hsla(199, 85%, 92%, 0.65);
|
|
305
|
-
filter: blur(2px);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/* Satellite (drifting glyph, e.g. a lucide "satellite" icon) */
|
|
309
|
-
@keyframes satelliteDrift {
|
|
310
|
-
from {
|
|
311
|
-
transform: translate(0, 0) rotate(-18deg);
|
|
312
|
-
}
|
|
313
|
-
to {
|
|
314
|
-
transform: translate(126vw, 34vh) rotate(-18deg);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
#satellite {
|
|
318
|
-
animation: satelliteDrift 46s linear infinite;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/* Cursor-following ambient glow (nebula that trails the pointer) */
|
|
322
|
-
#cursorGlow {
|
|
323
|
-
border-radius: 50%;
|
|
324
|
-
transform: translate(-50%, -50%);
|
|
325
|
-
background: radial-gradient(circle, hsl(217 91% 60% / 0.1), transparent 70%);
|
|
326
|
-
opacity: 0;
|
|
327
|
-
transition: opacity 0.4s ease;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
@media (prefers-reduced-motion: reduce) {
|
|
331
|
-
html:not(.force-motion) #space .stars,
|
|
332
|
-
html:not(.force-motion) .twinkle,
|
|
333
|
-
html:not(.force-motion) .floaty,
|
|
334
|
-
html:not(.force-motion) .shooter,
|
|
335
|
-
html:not(.force-motion) #satellite,
|
|
336
|
-
html:not(.force-motion) .asteroid {
|
|
337
|
-
animation: none !important;
|
|
338
|
-
}
|
|
339
|
-
html:not(.force-motion) #cursorGlow {
|
|
340
|
-
display: none !important;
|
|
341
|
-
}
|
|
342
|
-
html:not(.force-motion) #space .sky,
|
|
343
|
-
html:not(.force-motion) #space .aurora {
|
|
344
|
-
animation: none !important;
|
|
345
|
-
}
|
|
346
|
-
.card.glow-edge::before {
|
|
347
|
-
display: none;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/* ---- Generic themed scenery (non-space themes) --------------------------
|
|
352
|
-
A themed nebula (same token-tinted gradients as the starfield) plus a field
|
|
353
|
-
of slowly floating motif glyphs — <ThemedScenery motifs={[...]} />. Lighter
|
|
354
|
-
than the bespoke SpaceBackground so every extra theme costs almost nothing. */
|
|
355
|
-
#scenery {
|
|
356
|
-
position: fixed;
|
|
357
|
-
inset: 0;
|
|
358
|
-
z-index: 0;
|
|
359
|
-
pointer-events: none;
|
|
360
|
-
overflow: hidden;
|
|
361
|
-
}
|
|
362
|
-
#scenery .themed-sky {
|
|
363
|
-
position: absolute;
|
|
364
|
-
inset: 0;
|
|
365
|
-
animation: sceneryPulse 16s ease-in-out infinite;
|
|
366
|
-
background:
|
|
367
|
-
radial-gradient(1200px 700px at 82% -8%, hsl(var(--primary) / 0.34), transparent 60%),
|
|
368
|
-
radial-gradient(900px 600px at 8% 12%, hsl(var(--accent) / 0.28), transparent 60%),
|
|
369
|
-
radial-gradient(1000px 800px at 50% 120%, hsl(var(--accent) / 0.22), transparent 55%),
|
|
370
|
-
radial-gradient(700px 500px at 30% 55%, hsl(var(--destructive) / 0.12), transparent 60%),
|
|
371
|
-
hsl(var(--background));
|
|
372
|
-
}
|
|
373
|
-
[data-theme='light'] #scenery .themed-sky {
|
|
374
|
-
background:
|
|
375
|
-
radial-gradient(1100px 680px at 84% -10%, hsl(var(--primary) / 0.22), transparent 60%),
|
|
376
|
-
radial-gradient(900px 600px at 6% 8%, hsl(var(--accent) / 0.2), transparent 60%),
|
|
377
|
-
radial-gradient(1000px 820px at 50% 120%, hsl(var(--accent) / 0.16), transparent 55%),
|
|
378
|
-
radial-gradient(700px 500px at 30% 55%, hsl(var(--destructive) / 0.1), transparent 60%),
|
|
379
|
-
hsl(var(--background));
|
|
380
|
-
}
|
|
381
|
-
#scenery .motif {
|
|
382
|
-
position: absolute;
|
|
383
|
-
user-select: none;
|
|
384
|
-
will-change: transform;
|
|
385
|
-
animation: motifFloat var(--dur, 9s) ease-in-out var(--delay, 0s) infinite;
|
|
386
|
-
}
|
|
387
|
-
@keyframes motifFloat {
|
|
388
|
-
0%,
|
|
389
|
-
100% {
|
|
390
|
-
transform: translateY(0) rotate(var(--r, 0deg));
|
|
391
|
-
}
|
|
392
|
-
50% {
|
|
393
|
-
transform: translateY(-16px) rotate(calc(var(--r, 0deg) + 5deg));
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
/* Subtle nebula "breathing" so the themed backdrop isn't static. */
|
|
397
|
-
@keyframes sceneryPulse {
|
|
398
|
-
0%,
|
|
399
|
-
100% {
|
|
400
|
-
filter: brightness(1);
|
|
401
|
-
}
|
|
402
|
-
50% {
|
|
403
|
-
filter: brightness(1.06);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
@media (prefers-reduced-motion: reduce) {
|
|
407
|
-
html:not(.force-motion) #scenery .motif,
|
|
408
|
-
html:not(.force-motion) #scenery .themed-sky {
|
|
409
|
-
animation: none !important;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/* ---- Snow scenery (the "snow" theme) ------------------------------------ */
|
|
414
|
-
#snow,
|
|
415
|
-
#xmas {
|
|
416
|
-
position: fixed;
|
|
417
|
-
inset: 0;
|
|
418
|
-
z-index: 0;
|
|
419
|
-
pointer-events: none;
|
|
420
|
-
overflow: hidden;
|
|
421
|
-
}
|
|
422
|
-
#snow .snow-sky {
|
|
423
|
-
position: absolute;
|
|
424
|
-
inset: 0;
|
|
425
|
-
background:
|
|
426
|
-
radial-gradient(1200px 800px at 50% -12%, hsl(var(--primary) / 0.18), transparent 60%),
|
|
427
|
-
radial-gradient(900px 700px at 82% 8%, hsl(var(--accent) / 0.14), transparent 60%), hsl(var(--background));
|
|
428
|
-
}
|
|
429
|
-
/* Snowflakes (shared by snow + christmas). Each drifts down with a sideways sway. */
|
|
430
|
-
#snow .flake,
|
|
431
|
-
#xmas .flake {
|
|
432
|
-
position: absolute;
|
|
433
|
-
top: -12px;
|
|
434
|
-
border-radius: 50%;
|
|
435
|
-
background: #fff;
|
|
436
|
-
box-shadow: 0 0 4px rgba(255, 255, 255, 0.6);
|
|
437
|
-
will-change: transform;
|
|
438
|
-
}
|
|
439
|
-
[data-theme='light'] #snow .flake,
|
|
440
|
-
[data-theme='light'] #xmas .flake {
|
|
441
|
-
background: hsl(205 45% 82%);
|
|
442
|
-
box-shadow: 0 0 3px hsl(205 45% 72% / 0.6);
|
|
443
|
-
}
|
|
444
|
-
@keyframes snowFall {
|
|
445
|
-
0% {
|
|
446
|
-
transform: translate(0, -12px);
|
|
447
|
-
}
|
|
448
|
-
100% {
|
|
449
|
-
transform: translate(var(--sway, 12px), 112vh);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
/* Snow settling at the bottom + a frosted cap on cards (snow theme only). */
|
|
453
|
-
#snow .snow-bank {
|
|
454
|
-
position: absolute;
|
|
455
|
-
inset: auto 0 0 0;
|
|
456
|
-
height: 9vh;
|
|
457
|
-
background: linear-gradient(to top, hsl(0 0% 100% / 0.5), transparent);
|
|
458
|
-
filter: blur(2px);
|
|
459
|
-
}
|
|
460
|
-
[data-theme='light'] #snow .snow-bank {
|
|
461
|
-
background: linear-gradient(to top, hsl(0 0% 100% / 0.95), transparent);
|
|
462
|
-
}
|
|
463
|
-
[data-scene='snow'] .card::after {
|
|
464
|
-
content: '';
|
|
465
|
-
position: absolute;
|
|
466
|
-
top: -3px;
|
|
467
|
-
left: 10px;
|
|
468
|
-
right: 10px;
|
|
469
|
-
height: 5px;
|
|
470
|
-
border-radius: 9999px;
|
|
471
|
-
background: hsl(0 0% 100% / 0.8);
|
|
472
|
-
filter: blur(1.5px);
|
|
473
|
-
pointer-events: none;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/* ---- Christmas scenery (the "christmas" theme) -------------------------- */
|
|
477
|
-
#xmas .xmas-sky {
|
|
478
|
-
position: absolute;
|
|
479
|
-
inset: 0;
|
|
480
|
-
background:
|
|
481
|
-
radial-gradient(1000px 700px at 50% -12%, hsl(var(--primary) / 0.14), transparent 55%),
|
|
482
|
-
radial-gradient(900px 600px at 15% 16%, hsl(var(--accent) / 0.16), transparent 60%),
|
|
483
|
-
hsl(var(--background));
|
|
484
|
-
}
|
|
485
|
-
#xmas .light {
|
|
486
|
-
position: absolute;
|
|
487
|
-
top: 10px;
|
|
488
|
-
width: 9px;
|
|
489
|
-
height: 9px;
|
|
490
|
-
border-radius: 50%;
|
|
491
|
-
animation: twinkleLight 2.2s ease-in-out infinite;
|
|
492
|
-
}
|
|
493
|
-
@keyframes twinkleLight {
|
|
494
|
-
0%,
|
|
495
|
-
100% {
|
|
496
|
-
opacity: 1;
|
|
497
|
-
box-shadow: 0 0 9px currentColor;
|
|
498
|
-
}
|
|
499
|
-
50% {
|
|
500
|
-
opacity: 0.35;
|
|
501
|
-
box-shadow: 0 0 3px currentColor;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
@media (prefers-reduced-motion: reduce) {
|
|
505
|
-
html:not(.force-motion) #snow .flake,
|
|
506
|
-
html:not(.force-motion) #xmas .flake,
|
|
507
|
-
html:not(.force-motion) #xmas .light {
|
|
508
|
-
animation: none !important;
|
|
509
|
-
}
|
|
510
|
-
}
|