@salmexio/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 +1 -1
- package/dist/dialogs/ContextMenu/ContextMenu.svelte +6 -6
- package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts +1 -1
- package/dist/dialogs/Modal/Modal.svelte +3 -3
- package/dist/dialogs/Modal/Modal.svelte.d.ts +1 -1
- package/dist/feedback/Alert/Alert.svelte +16 -11
- package/dist/feedback/Alert/Alert.svelte.d.ts +1 -1
- package/dist/feedback/ProgressBar/ProgressBar.svelte +23 -4
- package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +1 -1
- package/dist/feedback/Skeleton/Skeleton.svelte +7 -3
- package/dist/feedback/Skeleton/Skeleton.svelte.d.ts +1 -1
- package/dist/feedback/Spinner/Spinner.svelte +3 -3
- package/dist/feedback/Spinner/Spinner.svelte.d.ts +2 -2
- package/dist/feedback/Toast/Toaster.svelte +10 -10
- package/dist/feedback/Toast/Toaster.svelte.d.ts +1 -1
- package/dist/forms/Checkbox/Checkbox.svelte +13 -8
- package/dist/forms/Checkbox/Checkbox.svelte.d.ts +1 -1
- package/dist/forms/Select/Select.svelte +11 -11
- package/dist/forms/Select/Select.svelte.d.ts +1 -1
- package/dist/forms/Slider/Slider.svelte +27 -27
- package/dist/forms/Slider/Slider.svelte.d.ts +1 -1
- package/dist/forms/TextInput/TextInput.svelte +16 -6
- package/dist/forms/TextInput/TextInput.svelte.d.ts +1 -1
- package/dist/forms/Textarea/Textarea.svelte +5 -5
- package/dist/forms/Textarea/Textarea.svelte.d.ts +1 -1
- package/dist/forms/Toggle/Toggle.svelte +8 -8
- package/dist/forms/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/layout/Card/Card.svelte +6 -4
- package/dist/layout/Card/Card.svelte.d.ts +1 -1
- package/dist/layout/Container/Container.svelte +1 -1
- package/dist/layout/Container/Container.svelte.d.ts +1 -1
- package/dist/layout/ThermalBackground/ThermalBackground.svelte +313 -0
- package/dist/layout/ThermalBackground/ThermalBackground.svelte.d.ts +16 -0
- package/dist/layout/ThermalBackground/ThermalBackground.svelte.d.ts.map +1 -0
- package/dist/layout/ThermalBackground/index.d.ts +2 -0
- package/dist/layout/ThermalBackground/index.d.ts.map +1 -0
- package/dist/layout/ThermalBackground/index.js +1 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +1 -0
- package/dist/navigation/CommandPalette/CommandPalette.svelte +8 -8
- package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts +1 -1
- package/dist/navigation/Tabs/Tabs.svelte +43 -10
- package/dist/navigation/Tabs/Tabs.svelte.d.ts +1 -1
- package/dist/primitives/Badge/Badge.svelte +16 -14
- package/dist/primitives/Badge/Badge.svelte.d.ts +1 -1
- package/dist/primitives/Button/Button.svelte +87 -16
- package/dist/primitives/Button/Button.svelte.d.ts +1 -1
- package/dist/primitives/Tooltip/Tooltip.svelte +1 -1
- package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +1 -1
- package/dist/styles/tokens.css +201 -64
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Slider
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
INFRARED — Range slider with keyboard support, optional value display,
|
|
5
5
|
step marks, min/max labels, and full ARIA compliance.
|
|
6
6
|
|
|
7
7
|
@example
|
|
@@ -199,12 +199,12 @@ function handleChange(e: Event) {
|
|
|
199
199
|
|
|
200
200
|
/* Focused track glow — WebKit */
|
|
201
201
|
.sx-slider-input:focus-visible::-webkit-slider-runnable-track {
|
|
202
|
-
box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-
|
|
202
|
+
box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-primary));
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/* Focused track glow — Firefox */
|
|
206
206
|
.sx-slider-input:focus-visible::-moz-range-track {
|
|
207
|
-
box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-
|
|
207
|
+
box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-primary));
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/* Track — WebKit */
|
|
@@ -213,8 +213,8 @@ function handleChange(e: Event) {
|
|
|
213
213
|
border-radius: var(--sx-radius-full);
|
|
214
214
|
background: linear-gradient(
|
|
215
215
|
to right,
|
|
216
|
-
var(--sx-slider-accent, var(--sx-color-
|
|
217
|
-
var(--sx-slider-accent, var(--sx-color-
|
|
216
|
+
var(--sx-slider-accent, var(--sx-color-primary)) 0%,
|
|
217
|
+
var(--sx-slider-accent, var(--sx-color-primary)) var(--sx-slider-fill),
|
|
218
218
|
var(--sx-color-surface-2) var(--sx-slider-fill),
|
|
219
219
|
var(--sx-color-surface-2) 100%
|
|
220
220
|
);
|
|
@@ -231,7 +231,7 @@ function handleChange(e: Event) {
|
|
|
231
231
|
.sx-slider-input::-moz-range-progress {
|
|
232
232
|
height: var(--sx-slider-track-h, 6px);
|
|
233
233
|
border-radius: var(--sx-radius-full);
|
|
234
|
-
background: var(--sx-slider-accent, var(--sx-color-
|
|
234
|
+
background: var(--sx-slider-accent, var(--sx-color-primary));
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/* Thumb — WebKit */
|
|
@@ -241,7 +241,7 @@ function handleChange(e: Event) {
|
|
|
241
241
|
height: var(--sx-slider-thumb, 18px);
|
|
242
242
|
border-radius: var(--sx-radius-full);
|
|
243
243
|
background: var(--sx-color-text);
|
|
244
|
-
border: 2px solid var(--sx-slider-accent, var(--sx-color-
|
|
244
|
+
border: 2px solid var(--sx-slider-accent, var(--sx-color-primary));
|
|
245
245
|
margin-top: calc((var(--sx-slider-track-h, 6px) - var(--sx-slider-thumb, 18px)) / 2);
|
|
246
246
|
transition:
|
|
247
247
|
box-shadow var(--sx-transition-fast),
|
|
@@ -256,10 +256,10 @@ function handleChange(e: Event) {
|
|
|
256
256
|
|
|
257
257
|
.sx-slider-input:focus-visible::-webkit-slider-thumb {
|
|
258
258
|
box-shadow:
|
|
259
|
-
0 0 0 3px var(--sx-slider-ring, rgba(
|
|
260
|
-
0 0 12px var(--sx-slider-glow, rgba(
|
|
259
|
+
0 0 0 3px var(--sx-slider-ring, rgba(255, 107, 53, 0.35)),
|
|
260
|
+
0 0 12px var(--sx-slider-glow, rgba(255, 107, 53, 0.25));
|
|
261
261
|
transform: scale(1.2);
|
|
262
|
-
background: var(--sx-slider-accent, var(--sx-color-
|
|
262
|
+
background: var(--sx-slider-accent, var(--sx-color-primary));
|
|
263
263
|
border-color: var(--sx-color-text);
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -269,7 +269,7 @@ function handleChange(e: Event) {
|
|
|
269
269
|
height: var(--sx-slider-thumb, 18px);
|
|
270
270
|
border-radius: var(--sx-radius-full);
|
|
271
271
|
background: var(--sx-color-text);
|
|
272
|
-
border: 2px solid var(--sx-slider-accent, var(--sx-color-
|
|
272
|
+
border: 2px solid var(--sx-slider-accent, var(--sx-color-primary));
|
|
273
273
|
transition:
|
|
274
274
|
box-shadow var(--sx-transition-fast),
|
|
275
275
|
transform var(--sx-transition-fast),
|
|
@@ -283,38 +283,38 @@ function handleChange(e: Event) {
|
|
|
283
283
|
|
|
284
284
|
.sx-slider-input:focus-visible::-moz-range-thumb {
|
|
285
285
|
box-shadow:
|
|
286
|
-
0 0 0 3px var(--sx-slider-ring, rgba(
|
|
287
|
-
0 0 12px var(--sx-slider-glow, rgba(
|
|
286
|
+
0 0 0 3px var(--sx-slider-ring, rgba(255, 107, 53, 0.35)),
|
|
287
|
+
0 0 12px var(--sx-slider-glow, rgba(255, 107, 53, 0.25));
|
|
288
288
|
transform: scale(1.2);
|
|
289
|
-
background: var(--sx-slider-accent, var(--sx-color-
|
|
289
|
+
background: var(--sx-slider-accent, var(--sx-color-primary));
|
|
290
290
|
border-color: var(--sx-color-text);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
/* Color variants — accent + focus ring colors */
|
|
294
294
|
.sx-slider-color-cyan {
|
|
295
|
-
--sx-slider-accent: var(--sx-color-
|
|
296
|
-
--sx-slider-ring: rgba(
|
|
297
|
-
--sx-slider-glow: rgba(
|
|
295
|
+
--sx-slider-accent: var(--sx-color-primary);
|
|
296
|
+
--sx-slider-ring: rgba(255, 107, 53, 0.35);
|
|
297
|
+
--sx-slider-glow: rgba(255, 107, 53, 0.25);
|
|
298
298
|
}
|
|
299
299
|
.sx-slider-color-green {
|
|
300
300
|
--sx-slider-accent: var(--sx-color-green);
|
|
301
|
-
--sx-slider-ring: rgba(
|
|
302
|
-
--sx-slider-glow: rgba(
|
|
301
|
+
--sx-slider-ring: rgba(74, 222, 128, 0.35);
|
|
302
|
+
--sx-slider-glow: rgba(74, 222, 128, 0.25);
|
|
303
303
|
}
|
|
304
304
|
.sx-slider-color-gold {
|
|
305
|
-
--sx-slider-accent: var(--sx-color-
|
|
306
|
-
--sx-slider-ring: rgba(
|
|
307
|
-
--sx-slider-glow: rgba(
|
|
305
|
+
--sx-slider-accent: var(--sx-color-secondary);
|
|
306
|
+
--sx-slider-ring: rgba(200, 168, 78, 0.35);
|
|
307
|
+
--sx-slider-glow: rgba(200, 168, 78, 0.25);
|
|
308
308
|
}
|
|
309
309
|
.sx-slider-color-red {
|
|
310
310
|
--sx-slider-accent: var(--sx-color-red);
|
|
311
|
-
--sx-slider-ring: rgba(
|
|
312
|
-
--sx-slider-glow: rgba(
|
|
311
|
+
--sx-slider-ring: rgba(220, 38, 38, 0.35);
|
|
312
|
+
--sx-slider-glow: rgba(220, 38, 38, 0.25);
|
|
313
313
|
}
|
|
314
314
|
.sx-slider-color-purple {
|
|
315
|
-
--sx-slider-accent: var(--sx-color-
|
|
316
|
-
--sx-slider-ring: rgba(
|
|
317
|
-
--sx-slider-glow: rgba(
|
|
315
|
+
--sx-slider-accent: var(--sx-color-teal);
|
|
316
|
+
--sx-slider-ring: rgba(61, 139, 139, 0.35);
|
|
317
|
+
--sx-slider-glow: rgba(61, 139, 139, 0.25);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/* Size variants */
|
|
@@ -36,7 +36,7 @@ interface Props {
|
|
|
36
36
|
/**
|
|
37
37
|
* Slider
|
|
38
38
|
*
|
|
39
|
-
*
|
|
39
|
+
* INFRARED — Range slider with keyboard support, optional value display,
|
|
40
40
|
* step marks, min/max labels, and full ARIA compliance.
|
|
41
41
|
*
|
|
42
42
|
* @example
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component TextInput
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
INFRARED — Clean text field with subtle borders and vermilion accents.
|
|
5
5
|
Label, error, hint, prefix/suffix, clearable, password toggle. Full a11y.
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
@@ -341,13 +341,14 @@ function handleKeyDown(e: KeyboardEvent) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
.sx-input-focused {
|
|
344
|
-
border-color: var(--sx-color-
|
|
345
|
-
|
|
344
|
+
border-color: var(--sx-color-primary);
|
|
345
|
+
animation: sx-focus-breathe 2s ease-in-out infinite;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
.sx-input-error {
|
|
349
349
|
border-color: var(--sx-color-red);
|
|
350
350
|
box-shadow: 0 0 0 3px var(--sx-color-red-ring);
|
|
351
|
+
animation: sx-error-shake 300ms ease-out;
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
.sx-input-valid {
|
|
@@ -476,7 +477,7 @@ function handleKeyDown(e: KeyboardEvent) {
|
|
|
476
477
|
.sx-input-loading {
|
|
477
478
|
display: flex;
|
|
478
479
|
align-items: center;
|
|
479
|
-
color: var(--sx-color-
|
|
480
|
+
color: var(--sx-color-primary);
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
.sx-input-spinner {
|
|
@@ -513,7 +514,7 @@ function handleKeyDown(e: KeyboardEvent) {
|
|
|
513
514
|
|
|
514
515
|
.sx-input-clear:focus-visible,
|
|
515
516
|
.sx-input-password-toggle:focus-visible {
|
|
516
|
-
outline: 2px solid var(--sx-color-
|
|
517
|
+
outline: 2px solid var(--sx-color-primary);
|
|
517
518
|
outline-offset: 2px;
|
|
518
519
|
}
|
|
519
520
|
|
|
@@ -549,12 +550,21 @@ function handleKeyDown(e: KeyboardEvent) {
|
|
|
549
550
|
}
|
|
550
551
|
|
|
551
552
|
.sx-input-charcount-warn {
|
|
552
|
-
color: var(--sx-color-
|
|
553
|
+
color: var(--sx-color-secondary);
|
|
553
554
|
}
|
|
554
555
|
|
|
555
556
|
@media (prefers-reduced-motion: reduce) {
|
|
556
557
|
.sx-input-spinner {
|
|
557
558
|
animation: none;
|
|
558
559
|
}
|
|
560
|
+
|
|
561
|
+
.sx-input-focused {
|
|
562
|
+
animation: none;
|
|
563
|
+
box-shadow: 0 0 0 3px var(--sx-color-primary-ring);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.sx-input-error {
|
|
567
|
+
animation: none;
|
|
568
|
+
}
|
|
559
569
|
}
|
|
560
570
|
</style>
|
|
@@ -41,7 +41,7 @@ interface Props extends Omit<HTMLInputAttributes, 'class' | 'size' | 'inputmode'
|
|
|
41
41
|
/**
|
|
42
42
|
* TextInput
|
|
43
43
|
*
|
|
44
|
-
*
|
|
44
|
+
* INFRARED — Clean text field with subtle borders and vermilion accents.
|
|
45
45
|
* Label, error, hint, prefix/suffix, clearable, password toggle. Full a11y.
|
|
46
46
|
*/
|
|
47
47
|
declare const TextInput: import("svelte").Component<Props, {}, "value">;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Textarea
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
INFRARED — Multi-line text input with custom resize handle,
|
|
5
5
|
auto-resize, character count, validation states, and full accessibility.
|
|
6
6
|
Follows TextInput visual patterns.
|
|
7
7
|
|
|
@@ -391,8 +391,8 @@ onMount(() => {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
.sx-textarea-focused {
|
|
394
|
-
border-color: var(--sx-color-
|
|
395
|
-
box-shadow: 0 0 0 3px var(--sx-color-
|
|
394
|
+
border-color: var(--sx-color-primary);
|
|
395
|
+
box-shadow: 0 0 0 3px var(--sx-color-primary-ring);
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
.sx-textarea-error-state {
|
|
@@ -480,7 +480,7 @@ onMount(() => {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
.sx-textarea-handle-active {
|
|
483
|
-
color: var(--sx-color-
|
|
483
|
+
color: var(--sx-color-primary);
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
/* Vertical: bar along the bottom edge */
|
|
@@ -580,7 +580,7 @@ onMount(() => {
|
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
.sx-textarea-charcount-warning {
|
|
583
|
-
color: var(--sx-color-
|
|
583
|
+
color: var(--sx-color-secondary);
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
.sx-textarea-charcount-exceeded {
|
|
@@ -32,7 +32,7 @@ interface Props {
|
|
|
32
32
|
/**
|
|
33
33
|
* Textarea
|
|
34
34
|
*
|
|
35
|
-
*
|
|
35
|
+
* INFRARED — Multi-line text input with custom resize handle,
|
|
36
36
|
* auto-resize, character count, validation states, and full accessibility.
|
|
37
37
|
* Follows TextInput visual patterns.
|
|
38
38
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Toggle
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
INFRARED — Accessible toggle switch with on/off states,
|
|
5
5
|
label positioning, size variants, and keyboard support.
|
|
6
6
|
Uses role="switch" with aria-checked for screen readers.
|
|
7
7
|
|
|
@@ -139,27 +139,27 @@ function toggle() {
|
|
|
139
139
|
|
|
140
140
|
.sx-toggle-track:focus-visible {
|
|
141
141
|
outline: none;
|
|
142
|
-
box-shadow: 0 0 0 3px var(--sx-color-
|
|
142
|
+
box-shadow: 0 0 0 3px var(--sx-color-primary-ring);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
.sx-toggle-checked {
|
|
146
|
-
background: var(--sx-color-
|
|
147
|
-
border-color: var(--sx-color-
|
|
146
|
+
background: var(--sx-color-primary);
|
|
147
|
+
border-color: var(--sx-color-primary);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
.sx-toggle-checked:hover:not(:disabled) {
|
|
151
|
-
border-color: var(--sx-color-
|
|
152
|
-
background: var(--sx-color-
|
|
151
|
+
border-color: var(--sx-color-primary);
|
|
152
|
+
background: var(--sx-color-primary);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/* Thumb */
|
|
155
|
+
/* Thumb — spring easing for a snappy, physical feel */
|
|
156
156
|
.sx-toggle-thumb {
|
|
157
157
|
display: block;
|
|
158
158
|
width: var(--sx-toggle-thumb, 18px);
|
|
159
159
|
height: var(--sx-toggle-thumb, 18px);
|
|
160
160
|
border-radius: var(--sx-radius-full);
|
|
161
161
|
background: var(--sx-color-text);
|
|
162
|
-
transition: transform var(--sx-
|
|
162
|
+
transition: transform 200ms var(--sx-ease-spring);
|
|
163
163
|
box-shadow: var(--sx-shadow-sm);
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -24,7 +24,7 @@ interface Props {
|
|
|
24
24
|
/**
|
|
25
25
|
* Toggle
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* INFRARED — Accessible toggle switch with on/off states,
|
|
28
28
|
* label positioning, size variants, and keyboard support.
|
|
29
29
|
* Uses role="switch" with aria-checked for screen readers.
|
|
30
30
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Card
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
INFRARED — Glassmorphic panel with frosted background, subtle borders.
|
|
5
5
|
Optional header/footer, semantic element support.
|
|
6
6
|
|
|
7
7
|
@example
|
|
@@ -120,12 +120,14 @@ const currentPadding = $derived(paddingValues[padding]);
|
|
|
120
120
|
|
|
121
121
|
.sx-card-clickable:hover {
|
|
122
122
|
transform: translateY(-2px);
|
|
123
|
-
border-color:
|
|
124
|
-
box-shadow:
|
|
123
|
+
border-color: rgba(255, 107, 53, 0.15);
|
|
124
|
+
box-shadow:
|
|
125
|
+
var(--sx-shadow-md),
|
|
126
|
+
0 0 24px -6px rgba(255, 107, 53, 0.08);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
.sx-card-clickable:focus-visible {
|
|
128
|
-
outline: 2px solid var(--sx-color-
|
|
130
|
+
outline: 2px solid var(--sx-color-primary);
|
|
129
131
|
outline-offset: 2px;
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
/**
|
|
17
17
|
* Card
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* INFRARED — Glassmorphic panel with frosted background, subtle borders.
|
|
20
20
|
* Optional header/footer, semantic element support.
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Container
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
INFRARED — Responsive layout wrapper. Max-width, padding, optional centering.
|
|
5
5
|
Pure layout utility; supports semantic elements.
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
@@ -16,7 +16,7 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'class'> {
|
|
|
16
16
|
/**
|
|
17
17
|
* Container
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* INFRARED — Responsive layout wrapper. Max-width, padding, optional centering.
|
|
20
20
|
* Pure layout utility; supports semantic elements.
|
|
21
21
|
*/
|
|
22
22
|
declare const Container: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ThermalBackground — Dynamic INFRARED background
|
|
3
|
+
|
|
4
|
+
Layered ambient background with thermal blob drift, radar sweep,
|
|
5
|
+
and film grain. Pure CSS — zero JS runtime cost.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
<ThermalBackground /> — default (all layers)
|
|
9
|
+
<ThermalBackground grain={false} /> — no grain overlay
|
|
10
|
+
<ThermalBackground sweep={false} /> — no radar sweep
|
|
11
|
+
<ThermalBackground intensity="low" /> — subtler blobs
|
|
12
|
+
-->
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Show film grain overlay */
|
|
17
|
+
grain?: boolean;
|
|
18
|
+
/** Show radar sweep line */
|
|
19
|
+
sweep?: boolean;
|
|
20
|
+
/** Blob intensity: low = barely visible, default = subtle, high = pronounced */
|
|
21
|
+
intensity?: 'low' | 'default' | 'high';
|
|
22
|
+
/** Additional CSS class */
|
|
23
|
+
class?: string;
|
|
24
|
+
/** Test ID */
|
|
25
|
+
testId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
grain = true,
|
|
30
|
+
sweep = true,
|
|
31
|
+
intensity = 'default',
|
|
32
|
+
class: className = '',
|
|
33
|
+
testId = undefined,
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div
|
|
38
|
+
class="sx-thermal-bg sx-thermal-{intensity} {className}"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
data-testid={testId}
|
|
41
|
+
>
|
|
42
|
+
<!-- Layer 1: Thermal blobs — large diffuse orbs drifting independently -->
|
|
43
|
+
<div class="sx-thermal-blobs">
|
|
44
|
+
<div class="sx-thermal-blob sx-thermal-blob-1"></div>
|
|
45
|
+
<div class="sx-thermal-blob sx-thermal-blob-2"></div>
|
|
46
|
+
<div class="sx-thermal-blob sx-thermal-blob-3"></div>
|
|
47
|
+
<div class="sx-thermal-blob sx-thermal-blob-4"></div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Layer 2: Radar/thermal sweep — horizontal scan line -->
|
|
51
|
+
{#if sweep}
|
|
52
|
+
<div class="sx-thermal-sweep"></div>
|
|
53
|
+
{/if}
|
|
54
|
+
|
|
55
|
+
<!-- Layer 3: Animated film grain -->
|
|
56
|
+
{#if grain}
|
|
57
|
+
<div class="sx-thermal-grain"></div>
|
|
58
|
+
{/if}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
/* ========================================
|
|
63
|
+
ROOT CONTAINER — fixed fullscreen backdrop
|
|
64
|
+
======================================== */
|
|
65
|
+
|
|
66
|
+
.sx-thermal-bg {
|
|
67
|
+
position: fixed;
|
|
68
|
+
inset: 0;
|
|
69
|
+
z-index: 0;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
background: var(--sx-color-base);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ========================================
|
|
76
|
+
THERMAL BLOBS — Drifting infrared orbs
|
|
77
|
+
Large, soft radial gradients that move on
|
|
78
|
+
independent orbits. Each has a unique size,
|
|
79
|
+
color, speed, and path.
|
|
80
|
+
======================================== */
|
|
81
|
+
|
|
82
|
+
.sx-thermal-blobs {
|
|
83
|
+
position: absolute;
|
|
84
|
+
inset: -50%;
|
|
85
|
+
width: 200%;
|
|
86
|
+
height: 200%;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sx-thermal-blob {
|
|
90
|
+
position: absolute;
|
|
91
|
+
border-radius: 50%;
|
|
92
|
+
filter: blur(80px);
|
|
93
|
+
will-change: transform;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Blob 1 — Vermilion/primary — largest, slowest */
|
|
97
|
+
.sx-thermal-blob-1 {
|
|
98
|
+
width: 45vmax;
|
|
99
|
+
height: 45vmax;
|
|
100
|
+
top: 10%;
|
|
101
|
+
left: 15%;
|
|
102
|
+
background: radial-gradient(
|
|
103
|
+
circle,
|
|
104
|
+
rgba(255, 107, 53, 0.08) 0%,
|
|
105
|
+
rgba(255, 107, 53, 0.02) 50%,
|
|
106
|
+
transparent 70%
|
|
107
|
+
);
|
|
108
|
+
animation: sx-thermal-drift-1 35s ease-in-out infinite;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Blob 2 — Brass/secondary — medium, offset orbit */
|
|
112
|
+
.sx-thermal-blob-2 {
|
|
113
|
+
width: 35vmax;
|
|
114
|
+
height: 35vmax;
|
|
115
|
+
top: 55%;
|
|
116
|
+
right: 10%;
|
|
117
|
+
background: radial-gradient(
|
|
118
|
+
circle,
|
|
119
|
+
rgba(200, 168, 78, 0.06) 0%,
|
|
120
|
+
rgba(200, 168, 78, 0.015) 50%,
|
|
121
|
+
transparent 70%
|
|
122
|
+
);
|
|
123
|
+
animation: sx-thermal-drift-2 42s ease-in-out infinite;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Blob 3 — Teal — smallest, fastest, cool counterpoint */
|
|
127
|
+
.sx-thermal-blob-3 {
|
|
128
|
+
width: 30vmax;
|
|
129
|
+
height: 30vmax;
|
|
130
|
+
bottom: 15%;
|
|
131
|
+
left: 40%;
|
|
132
|
+
background: radial-gradient(
|
|
133
|
+
circle,
|
|
134
|
+
rgba(61, 139, 139, 0.06) 0%,
|
|
135
|
+
rgba(61, 139, 139, 0.015) 50%,
|
|
136
|
+
transparent 70%
|
|
137
|
+
);
|
|
138
|
+
animation: sx-thermal-drift-3 28s ease-in-out infinite;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Blob 4 — Deep vermilion — secondary heat signature */
|
|
142
|
+
.sx-thermal-blob-4 {
|
|
143
|
+
width: 38vmax;
|
|
144
|
+
height: 38vmax;
|
|
145
|
+
top: 40%;
|
|
146
|
+
left: 60%;
|
|
147
|
+
background: radial-gradient(
|
|
148
|
+
circle,
|
|
149
|
+
rgba(224, 85, 32, 0.05) 0%,
|
|
150
|
+
rgba(224, 85, 32, 0.01) 50%,
|
|
151
|
+
transparent 70%
|
|
152
|
+
);
|
|
153
|
+
animation: sx-thermal-drift-4 50s ease-in-out infinite;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ========================================
|
|
157
|
+
BLOB DRIFT ORBITS
|
|
158
|
+
Each blob traces a unique Lissajous-like path
|
|
159
|
+
using translate transforms. Different durations
|
|
160
|
+
ensure they never sync up, creating organic motion.
|
|
161
|
+
======================================== */
|
|
162
|
+
|
|
163
|
+
@keyframes sx-thermal-drift-1 {
|
|
164
|
+
0% { transform: translate(0%, 0%) scale(1); }
|
|
165
|
+
25% { transform: translate(12%, -8%) scale(1.05); }
|
|
166
|
+
50% { transform: translate(-5%, 15%) scale(0.95); }
|
|
167
|
+
75% { transform: translate(-15%, -5%) scale(1.08); }
|
|
168
|
+
100% { transform: translate(0%, 0%) scale(1); }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@keyframes sx-thermal-drift-2 {
|
|
172
|
+
0% { transform: translate(0%, 0%) scale(1); }
|
|
173
|
+
20% { transform: translate(-18%, 8%) scale(1.1); }
|
|
174
|
+
40% { transform: translate(8%, -12%) scale(0.9); }
|
|
175
|
+
60% { transform: translate(-8%, -18%) scale(1.05); }
|
|
176
|
+
80% { transform: translate(15%, 5%) scale(0.95); }
|
|
177
|
+
100% { transform: translate(0%, 0%) scale(1); }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@keyframes sx-thermal-drift-3 {
|
|
181
|
+
0% { transform: translate(0%, 0%) rotate(0deg) scale(1); }
|
|
182
|
+
33% { transform: translate(20%, -15%) rotate(5deg) scale(1.1); }
|
|
183
|
+
66% { transform: translate(-15%, 10%) rotate(-3deg) scale(0.92); }
|
|
184
|
+
100% { transform: translate(0%, 0%) rotate(0deg) scale(1); }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes sx-thermal-drift-4 {
|
|
188
|
+
0% { transform: translate(0%, 0%) scale(1); }
|
|
189
|
+
30% { transform: translate(-10%, 12%) scale(1.06); }
|
|
190
|
+
60% { transform: translate(15%, -8%) scale(0.94); }
|
|
191
|
+
100% { transform: translate(0%, 0%) scale(1); }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ========================================
|
|
195
|
+
INTENSITY VARIANTS
|
|
196
|
+
Control blob visibility without changing motion
|
|
197
|
+
======================================== */
|
|
198
|
+
|
|
199
|
+
.sx-thermal-low .sx-thermal-blobs {
|
|
200
|
+
opacity: 0.5;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.sx-thermal-default .sx-thermal-blobs {
|
|
204
|
+
opacity: 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.sx-thermal-high .sx-thermal-blobs {
|
|
208
|
+
opacity: 1.5;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.sx-thermal-high .sx-thermal-blob-1 {
|
|
212
|
+
background: radial-gradient(
|
|
213
|
+
circle,
|
|
214
|
+
rgba(255, 107, 53, 0.14) 0%,
|
|
215
|
+
rgba(255, 107, 53, 0.04) 50%,
|
|
216
|
+
transparent 70%
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.sx-thermal-high .sx-thermal-blob-2 {
|
|
221
|
+
background: radial-gradient(
|
|
222
|
+
circle,
|
|
223
|
+
rgba(200, 168, 78, 0.10) 0%,
|
|
224
|
+
rgba(200, 168, 78, 0.03) 50%,
|
|
225
|
+
transparent 70%
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.sx-thermal-high .sx-thermal-blob-3 {
|
|
230
|
+
background: radial-gradient(
|
|
231
|
+
circle,
|
|
232
|
+
rgba(61, 139, 139, 0.10) 0%,
|
|
233
|
+
rgba(61, 139, 139, 0.03) 50%,
|
|
234
|
+
transparent 70%
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.sx-thermal-high .sx-thermal-blob-4 {
|
|
239
|
+
background: radial-gradient(
|
|
240
|
+
circle,
|
|
241
|
+
rgba(224, 85, 32, 0.09) 0%,
|
|
242
|
+
rgba(224, 85, 32, 0.02) 50%,
|
|
243
|
+
transparent 70%
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ========================================
|
|
248
|
+
RADAR SWEEP — Horizontal scan line
|
|
249
|
+
A faint horizontal gradient that sweeps
|
|
250
|
+
top to bottom like a thermal scanner or
|
|
251
|
+
radar display. Very subtle.
|
|
252
|
+
======================================== */
|
|
253
|
+
|
|
254
|
+
.sx-thermal-sweep {
|
|
255
|
+
position: absolute;
|
|
256
|
+
inset: 0;
|
|
257
|
+
background: linear-gradient(
|
|
258
|
+
180deg,
|
|
259
|
+
transparent 0%,
|
|
260
|
+
rgba(255, 107, 53, 0.015) 48%,
|
|
261
|
+
rgba(255, 107, 53, 0.04) 50%,
|
|
262
|
+
rgba(255, 107, 53, 0.015) 52%,
|
|
263
|
+
transparent 100%
|
|
264
|
+
);
|
|
265
|
+
background-size: 100% 300%;
|
|
266
|
+
animation: sx-thermal-scan 8s linear infinite;
|
|
267
|
+
opacity: 0.7;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@keyframes sx-thermal-scan {
|
|
271
|
+
0% { background-position: 0% 0%; }
|
|
272
|
+
100% { background-position: 0% 100%; }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ========================================
|
|
276
|
+
FILM GRAIN — Animated noise texture
|
|
277
|
+
SVG feTurbulence noise with subtle position
|
|
278
|
+
stepping for a living grain effect.
|
|
279
|
+
======================================== */
|
|
280
|
+
|
|
281
|
+
.sx-thermal-grain {
|
|
282
|
+
position: absolute;
|
|
283
|
+
inset: -50%;
|
|
284
|
+
width: 300%;
|
|
285
|
+
height: 300%;
|
|
286
|
+
opacity: 0.03;
|
|
287
|
+
mix-blend-mode: overlay;
|
|
288
|
+
pointer-events: none;
|
|
289
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
290
|
+
background-size: 256px 256px;
|
|
291
|
+
animation: sx-thermal-grain-drift 0.4s steps(4) infinite;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@keyframes sx-thermal-grain-drift {
|
|
295
|
+
0% { transform: translate(0, 0); }
|
|
296
|
+
25% { transform: translate(-5%, -5%); }
|
|
297
|
+
50% { transform: translate(3%, -8%); }
|
|
298
|
+
75% { transform: translate(-8%, 2%); }
|
|
299
|
+
100% { transform: translate(0, 0); }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ========================================
|
|
303
|
+
REDUCED MOTION — Static fallback
|
|
304
|
+
======================================== */
|
|
305
|
+
|
|
306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
307
|
+
.sx-thermal-blob,
|
|
308
|
+
.sx-thermal-sweep,
|
|
309
|
+
.sx-thermal-grain {
|
|
310
|
+
animation: none !important;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
</style>
|