@moonarc/core 0.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/LICENSE +21 -0
- package/README.md +39 -0
- package/package.json +228 -0
- package/src/components/Accordion.astro +110 -0
- package/src/components/Accordion.meta.ts +33 -0
- package/src/components/AnimatedBeam.astro +217 -0
- package/src/components/AnimatedBeam.meta.ts +39 -0
- package/src/components/AnimatedIcons.astro +193 -0
- package/src/components/AnimatedIcons.meta.ts +34 -0
- package/src/components/AnimatedList.astro +305 -0
- package/src/components/AnimatedList.meta.ts +36 -0
- package/src/components/AnimatedSelect.astro +227 -0
- package/src/components/AnimatedSelect.meta.ts +38 -0
- package/src/components/ArrowButton.astro +133 -0
- package/src/components/ArrowButton.meta.ts +32 -0
- package/src/components/Aurora.astro +188 -0
- package/src/components/Aurora.meta.ts +36 -0
- package/src/components/AvatarStack.astro +205 -0
- package/src/components/AvatarStack.meta.ts +37 -0
- package/src/components/BarGrow.astro +123 -0
- package/src/components/BarGrow.meta.ts +42 -0
- package/src/components/BorderBeam.astro +75 -0
- package/src/components/BorderBeam.meta.ts +33 -0
- package/src/components/BrowserFrame.astro +172 -0
- package/src/components/BrowserFrame.meta.ts +36 -0
- package/src/components/BubbleText.astro +90 -0
- package/src/components/BubbleText.meta.ts +38 -0
- package/src/components/CardStack.astro +79 -0
- package/src/components/CardStack.meta.ts +33 -0
- package/src/components/Carousel.astro +147 -0
- package/src/components/Carousel.meta.ts +35 -0
- package/src/components/CheckDraw.astro +159 -0
- package/src/components/CheckDraw.meta.ts +37 -0
- package/src/components/CircularText.astro +103 -0
- package/src/components/CircularText.meta.ts +38 -0
- package/src/components/CodeTabs.astro +213 -0
- package/src/components/CodeTabs.meta.ts +36 -0
- package/src/components/CommandPalette.astro +363 -0
- package/src/components/CommandPalette.meta.ts +40 -0
- package/src/components/Confetti.astro +143 -0
- package/src/components/Confetti.meta.ts +40 -0
- package/src/components/CopyButton.astro +189 -0
- package/src/components/CopyButton.meta.ts +41 -0
- package/src/components/CountUp.astro +134 -0
- package/src/components/CountUp.meta.ts +40 -0
- package/src/components/CountdownFlip.astro +244 -0
- package/src/components/CountdownFlip.meta.ts +39 -0
- package/src/components/CounterPreloader.astro +122 -0
- package/src/components/CounterPreloader.meta.ts +34 -0
- package/src/components/Cursor.astro +177 -0
- package/src/components/Cursor.meta.ts +37 -0
- package/src/components/CursorGrid.astro +113 -0
- package/src/components/CursorGrid.meta.ts +37 -0
- package/src/components/Curtain.astro +305 -0
- package/src/components/Curtain.meta.ts +38 -0
- package/src/components/Dialog.astro +156 -0
- package/src/components/Dialog.meta.ts +37 -0
- package/src/components/Dock.astro +193 -0
- package/src/components/Dock.meta.ts +35 -0
- package/src/components/DotPattern.astro +58 -0
- package/src/components/DotPattern.meta.ts +32 -0
- package/src/components/DottedMap.astro +153 -0
- package/src/components/DottedMap.meta.ts +38 -0
- package/src/components/Drawer.astro +196 -0
- package/src/components/Drawer.meta.ts +35 -0
- package/src/components/DynamicIsland.astro +122 -0
- package/src/components/DynamicIsland.meta.ts +35 -0
- package/src/components/FileTree.astro +207 -0
- package/src/components/FileTree.meta.ts +34 -0
- package/src/components/FilterGrid.astro +170 -0
- package/src/components/FilterGrid.meta.ts +38 -0
- package/src/components/FooterReveal.astro +54 -0
- package/src/components/FooterReveal.meta.ts +31 -0
- package/src/components/GlitchText.astro +145 -0
- package/src/components/GlitchText.meta.ts +39 -0
- package/src/components/Globe.astro +304 -0
- package/src/components/Globe.meta.ts +41 -0
- package/src/components/GlyphField.astro +363 -0
- package/src/components/GlyphField.meta.ts +53 -0
- package/src/components/GradientText.astro +63 -0
- package/src/components/GradientText.meta.ts +35 -0
- package/src/components/GridPattern.astro +55 -0
- package/src/components/GridPattern.meta.ts +32 -0
- package/src/components/HighlightMarker.astro +68 -0
- package/src/components/HighlightMarker.meta.ts +37 -0
- package/src/components/HorizontalScroll.astro +112 -0
- package/src/components/HorizontalScroll.meta.ts +34 -0
- package/src/components/HoverImageLinks.astro +219 -0
- package/src/components/HoverImageLinks.meta.ts +34 -0
- package/src/components/HoverVideo.astro +179 -0
- package/src/components/HoverVideo.meta.ts +37 -0
- package/src/components/ImageCompare.astro +212 -0
- package/src/components/ImageCompare.meta.ts +39 -0
- package/src/components/ImageTrail.astro +169 -0
- package/src/components/ImageTrail.meta.ts +36 -0
- package/src/components/Lightbox.astro +236 -0
- package/src/components/Lightbox.meta.ts +40 -0
- package/src/components/LinkPreview.astro +234 -0
- package/src/components/LinkPreview.meta.ts +39 -0
- package/src/components/LiquidGlass.astro +148 -0
- package/src/components/LiquidGlass.meta.ts +37 -0
- package/src/components/Magnetic.astro +92 -0
- package/src/components/Magnetic.meta.ts +31 -0
- package/src/components/Marquee.astro +165 -0
- package/src/components/Marquee.meta.ts +38 -0
- package/src/components/Meteors.astro +156 -0
- package/src/components/Meteors.meta.ts +36 -0
- package/src/components/Noise.astro +61 -0
- package/src/components/Noise.meta.ts +33 -0
- package/src/components/NumberFlow.astro +143 -0
- package/src/components/NumberFlow.meta.ts +42 -0
- package/src/components/Orb.astro +130 -0
- package/src/components/Orb.meta.ts +39 -0
- package/src/components/OrbitingCircles.astro +119 -0
- package/src/components/OrbitingCircles.meta.ts +36 -0
- package/src/components/OtpInput.astro +215 -0
- package/src/components/OtpInput.meta.ts +42 -0
- package/src/components/PageTransition.astro +121 -0
- package/src/components/PageTransition.meta.ts +33 -0
- package/src/components/Parallax.astro +69 -0
- package/src/components/Parallax.meta.ts +32 -0
- package/src/components/Particles.astro +210 -0
- package/src/components/Particles.meta.ts +37 -0
- package/src/components/PersistPlayer.astro +126 -0
- package/src/components/PersistPlayer.meta.ts +35 -0
- package/src/components/PhoneFrame.astro +161 -0
- package/src/components/PhoneFrame.meta.ts +35 -0
- package/src/components/PillNav.astro +170 -0
- package/src/components/PillNav.meta.ts +37 -0
- package/src/components/Popover.astro +95 -0
- package/src/components/Popover.meta.ts +34 -0
- package/src/components/PriceSwitch.astro +251 -0
- package/src/components/PriceSwitch.meta.ts +41 -0
- package/src/components/Probe.astro +84 -0
- package/src/components/Progress.astro +165 -0
- package/src/components/Progress.meta.ts +40 -0
- package/src/components/ProgressiveBlur.astro +95 -0
- package/src/components/ProgressiveBlur.meta.ts +36 -0
- package/src/components/PromptInput.astro +273 -0
- package/src/components/PromptInput.meta.ts +46 -0
- package/src/components/ReadingToc.astro +153 -0
- package/src/components/ReadingToc.meta.ts +34 -0
- package/src/components/RetroGrid.astro +84 -0
- package/src/components/RetroGrid.meta.ts +36 -0
- package/src/components/Reveal.astro +292 -0
- package/src/components/Reveal.meta.ts +48 -0
- package/src/components/Ripple.astro +94 -0
- package/src/components/Ripple.meta.ts +34 -0
- package/src/components/RotatingText.astro +170 -0
- package/src/components/RotatingText.meta.ts +35 -0
- package/src/components/ScrollExpand.astro +90 -0
- package/src/components/ScrollExpand.meta.ts +35 -0
- package/src/components/ScrollProgress.astro +73 -0
- package/src/components/ScrollProgress.meta.ts +32 -0
- package/src/components/ScrollScrub.astro +96 -0
- package/src/components/ScrollScrub.meta.ts +37 -0
- package/src/components/ShimmerButton.astro +109 -0
- package/src/components/ShimmerButton.meta.ts +37 -0
- package/src/components/ShinyText.astro +72 -0
- package/src/components/ShinyText.meta.ts +35 -0
- package/src/components/Skeleton.astro +76 -0
- package/src/components/Skeleton.meta.ts +32 -0
- package/src/components/Sparkline.astro +168 -0
- package/src/components/Sparkline.meta.ts +43 -0
- package/src/components/Spinner.astro +79 -0
- package/src/components/Spinner.meta.ts +33 -0
- package/src/components/SplitFlap.astro +157 -0
- package/src/components/SplitFlap.meta.ts +39 -0
- package/src/components/SplitText.astro +208 -0
- package/src/components/SplitText.meta.ts +50 -0
- package/src/components/Spotlight.astro +92 -0
- package/src/components/Spotlight.meta.ts +34 -0
- package/src/components/StaggerMenu.astro +301 -0
- package/src/components/StaggerMenu.meta.ts +39 -0
- package/src/components/StatefulButton.astro +148 -0
- package/src/components/StatefulButton.meta.ts +35 -0
- package/src/components/StickyBanner.astro +168 -0
- package/src/components/StickyBanner.meta.ts +45 -0
- package/src/components/StickyReveal.astro +256 -0
- package/src/components/StickyReveal.meta.ts +33 -0
- package/src/components/StrokeDraw.astro +110 -0
- package/src/components/StrokeDraw.meta.ts +43 -0
- package/src/components/Switch.astro +165 -0
- package/src/components/Switch.meta.ts +37 -0
- package/src/components/Tabs.astro +194 -0
- package/src/components/Tabs.meta.ts +47 -0
- package/src/components/TextRoll.astro +78 -0
- package/src/components/TextRoll.meta.ts +36 -0
- package/src/components/TextScramble.astro +107 -0
- package/src/components/TextScramble.meta.ts +35 -0
- package/src/components/ThemeScript.astro +17 -0
- package/src/components/ThemeToggle.astro +182 -0
- package/src/components/ThemeToggle.meta.ts +34 -0
- package/src/components/Tilt.astro +97 -0
- package/src/components/Tilt.meta.ts +34 -0
- package/src/components/Toast.astro +184 -0
- package/src/components/Toast.meta.ts +33 -0
- package/src/components/TracingBeam.astro +113 -0
- package/src/components/TracingBeam.meta.ts +34 -0
- package/src/components/Typewriter.astro +124 -0
- package/src/components/Typewriter.meta.ts +39 -0
- package/src/components/UnderlineHover.astro +61 -0
- package/src/components/UnderlineHover.meta.ts +34 -0
- package/src/components/VanishInput.astro +240 -0
- package/src/components/VanishInput.meta.ts +41 -0
- package/src/components/VideoDialog.astro +230 -0
- package/src/components/VideoDialog.meta.ts +40 -0
- package/src/components/WeightWave.astro +70 -0
- package/src/components/WeightWave.meta.ts +36 -0
- package/src/components/internal/DialogMorph.astro +97 -0
- package/src/components/internal/PromptInputSubmitOnEnter.astro +32 -0
- package/src/index.d.ts +43 -0
- package/src/index.js +60 -0
- package/src/lib/canvas.ts +124 -0
- package/src/lib/confetti.ts +127 -0
- package/src/lib/contrast.ts +116 -0
- package/src/lib/form.ts +231 -0
- package/src/lib/geo.ts +77 -0
- package/src/lib/gsap.ts +58 -0
- package/src/lib/land.ts +11 -0
- package/src/lib/lenis.ts +112 -0
- package/src/lib/meta.ts +79 -0
- package/src/lib/morph.ts +74 -0
- package/src/lib/reduced-motion-plugin.js +299 -0
- package/src/lib/runtime.ts +215 -0
- package/src/lib/support.ts +102 -0
- package/src/lib/theme.ts +18 -0
- package/src/lib/toast.ts +17 -0
- package/src/styles/base.css +218 -0
- package/src/styles/motion.css +43 -0
- package/src/styles/theme.css +397 -0
- package/src/styles/theme.tailwind.css +75 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AnimatedSelect — a native <select> that blooms, zero JS. In an engine
|
|
4
|
+
* with `appearance: base-select` the button and the `::picker(select)`
|
|
5
|
+
* take the tokens, the options arrive one after another through
|
|
6
|
+
* @starting-style with a delay from `sibling-index()`, the picker leaves
|
|
7
|
+
* through allow-discrete and `::checkmark` is drawn. Everywhere else it is
|
|
8
|
+
* the native select with a token border and a chevron, no animation — the
|
|
9
|
+
* same set, one browser short of the motion. No listbox is imitated: the
|
|
10
|
+
* keyboard, type-to-select, the form value and the accessibility tree are
|
|
11
|
+
* the browser's in every engine.
|
|
12
|
+
*/
|
|
13
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
14
|
+
|
|
15
|
+
interface Option {
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Options with the same group go into one <optgroup>. */
|
|
20
|
+
group?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Props extends Omit<HTMLAttributes<'select'>, 'size'> {
|
|
24
|
+
options: Option[];
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Selected value. */
|
|
27
|
+
value?: string;
|
|
28
|
+
/** Visible label as <label for>. Without it, pass aria-label. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** A disabled first option shown until a choice is made. */
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
size?: 'sm' | 'md';
|
|
33
|
+
id?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { options, name, value, label, placeholder, size = 'md', id, class: className, style, ...rest } = Astro.props;
|
|
37
|
+
// unique per render, not per name: a PromptInput and an AnimatedSelect both named "model" can share a page
|
|
38
|
+
const selectId = id ?? `ma-aselect-${name ?? 'select'}-${Math.random().toString(36).slice(2, 7)}`;
|
|
39
|
+
// group order is first appearance; ungrouped options stay where they are
|
|
40
|
+
const groups: { name: string | undefined; items: Option[] }[] = [];
|
|
41
|
+
for (const o of options) {
|
|
42
|
+
const last = groups[groups.length - 1];
|
|
43
|
+
if (last && last.name === o.group) last.items.push(o);
|
|
44
|
+
else groups.push({ name: o.group, items: [o] });
|
|
45
|
+
}
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
{label && <label class="ma-aselect__label" for={selectId}>{label}</label>}
|
|
49
|
+
<span class:list={['ma-aselect', className]} data-size={size} style={style}>
|
|
50
|
+
<select id={selectId} class="ma-aselect__select" name={name} {...rest}>
|
|
51
|
+
{placeholder && <option value="" disabled selected={value === undefined}>{placeholder}</option>}
|
|
52
|
+
{groups.map((g) =>
|
|
53
|
+
g.name ? (
|
|
54
|
+
<optgroup label={g.name}>{g.items.map((o) => <option value={o.value} disabled={o.disabled} selected={o.value === value}>{o.label}</option>)}</optgroup>
|
|
55
|
+
) : (
|
|
56
|
+
g.items.map((o) => <option value={o.value} disabled={o.disabled} selected={o.value === value}>{o.label}</option>)
|
|
57
|
+
),
|
|
58
|
+
)}
|
|
59
|
+
</select>
|
|
60
|
+
<svg class="ma-aselect__chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
61
|
+
</span>
|
|
62
|
+
|
|
63
|
+
<style is:global>
|
|
64
|
+
@layer components {
|
|
65
|
+
:where(.ma-aselect) {
|
|
66
|
+
--ma-aselect-pad: 0.6em 2.2em 0.6em 0.9em;
|
|
67
|
+
position: relative;
|
|
68
|
+
display: inline-grid;
|
|
69
|
+
vertical-align: middle;
|
|
70
|
+
color: inherit;
|
|
71
|
+
}
|
|
72
|
+
:where(.ma-aselect[data-size='sm']) {
|
|
73
|
+
--ma-aselect-pad: 0.35em 1.9em 0.35em 0.65em;
|
|
74
|
+
font-size: 0.875em;
|
|
75
|
+
}
|
|
76
|
+
:where(.ma-aselect__label) {
|
|
77
|
+
display: block;
|
|
78
|
+
margin-block-end: 0.5em;
|
|
79
|
+
font-size: 0.875em;
|
|
80
|
+
}
|
|
81
|
+
/* every engine: the native select in the tokens; the chevron is ours so the two looks match */
|
|
82
|
+
:where(.ma-aselect__select) {
|
|
83
|
+
appearance: none;
|
|
84
|
+
-webkit-appearance: none;
|
|
85
|
+
grid-area: 1 / 1;
|
|
86
|
+
min-inline-size: 8em;
|
|
87
|
+
margin: 0;
|
|
88
|
+
padding: var(--ma-aselect-pad);
|
|
89
|
+
border: 1px solid var(--ma-edge);
|
|
90
|
+
border-radius: 0.6em;
|
|
91
|
+
background: var(--ma-panel);
|
|
92
|
+
color: inherit;
|
|
93
|
+
font: inherit;
|
|
94
|
+
line-height: 1.2;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
transition: border-color var(--ma-duration-fast) var(--ma-ease-out);
|
|
97
|
+
}
|
|
98
|
+
:where(.ma-aselect__select:hover) {
|
|
99
|
+
border-color: color-mix(in srgb, var(--ma-ink) 35%, transparent);
|
|
100
|
+
}
|
|
101
|
+
:where(.ma-aselect__select:focus-visible) {
|
|
102
|
+
outline: 2px solid var(--ma-ink);
|
|
103
|
+
outline-offset: 2px;
|
|
104
|
+
}
|
|
105
|
+
:where(.ma-aselect__select:disabled) {
|
|
106
|
+
cursor: not-allowed;
|
|
107
|
+
opacity: 0.5;
|
|
108
|
+
transition: none;
|
|
109
|
+
}
|
|
110
|
+
:where(.ma-aselect__select:user-invalid) {
|
|
111
|
+
border-color: #e0563f;
|
|
112
|
+
}
|
|
113
|
+
:where(.ma-aselect__chevron) {
|
|
114
|
+
grid-area: 1 / 1;
|
|
115
|
+
justify-self: end;
|
|
116
|
+
align-self: center;
|
|
117
|
+
inline-size: 1em;
|
|
118
|
+
block-size: 1em;
|
|
119
|
+
margin-inline-end: 0.7em;
|
|
120
|
+
opacity: 0.6;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
transition: rotate var(--ma-duration) var(--ma-ease);
|
|
123
|
+
}
|
|
124
|
+
/* the customizable select: the picker in the tokens, options staggered in, the picker leaving on allow-discrete */
|
|
125
|
+
@supports (appearance: base-select) {
|
|
126
|
+
:where(.ma-aselect__select),
|
|
127
|
+
:where(.ma-aselect__select)::picker(select) {
|
|
128
|
+
appearance: base-select;
|
|
129
|
+
}
|
|
130
|
+
:where(.ma-aselect__select)::picker-icon {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
:where(.ma-aselect:has(.ma-aselect__select:open) .ma-aselect__chevron) {
|
|
134
|
+
rotate: 180deg;
|
|
135
|
+
}
|
|
136
|
+
:where(.ma-aselect__select)::picker(select) {
|
|
137
|
+
margin-block-start: 0.35em;
|
|
138
|
+
padding: 0.35em;
|
|
139
|
+
border: 1px solid var(--ma-edge);
|
|
140
|
+
border-radius: 0.75em;
|
|
141
|
+
background: var(--ma-panel);
|
|
142
|
+
color: inherit;
|
|
143
|
+
box-shadow: 0 12px 32px -16px rgb(0 0 0 / 0.35);
|
|
144
|
+
opacity: 0;
|
|
145
|
+
translate: 0 calc(var(--ma-travel-hover) * -1);
|
|
146
|
+
transition:
|
|
147
|
+
opacity var(--ma-duration-fast) var(--ma-ease-out),
|
|
148
|
+
translate var(--ma-duration-fast) var(--ma-ease-out),
|
|
149
|
+
display var(--ma-duration-fast) allow-discrete,
|
|
150
|
+
overlay var(--ma-duration-fast) allow-discrete;
|
|
151
|
+
}
|
|
152
|
+
:where(.ma-aselect__select:open)::picker(select) {
|
|
153
|
+
opacity: 1;
|
|
154
|
+
translate: 0 0;
|
|
155
|
+
transition-duration: var(--ma-duration);
|
|
156
|
+
transition-timing-function: var(--ma-ease);
|
|
157
|
+
}
|
|
158
|
+
@starting-style {
|
|
159
|
+
:where(.ma-aselect__select:open)::picker(select) {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
translate: 0 calc(var(--ma-travel-hover) * -1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
:where(.ma-aselect__select option) {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 0.5em;
|
|
168
|
+
padding: 0.45em 0.7em;
|
|
169
|
+
border-radius: 0.45em;
|
|
170
|
+
opacity: 1;
|
|
171
|
+
translate: 0 0;
|
|
172
|
+
transition:
|
|
173
|
+
opacity var(--ma-duration) var(--ma-ease-out),
|
|
174
|
+
translate var(--ma-duration) var(--ma-ease),
|
|
175
|
+
background-color var(--ma-duration-fast) var(--ma-ease-out);
|
|
176
|
+
/* one option after another: the delay is its index in the group, on the stagger token */
|
|
177
|
+
transition-delay: calc(sibling-index() * var(--ma-stagger));
|
|
178
|
+
}
|
|
179
|
+
@starting-style {
|
|
180
|
+
:where(.ma-aselect__select:open option) {
|
|
181
|
+
opacity: 0;
|
|
182
|
+
translate: 0 calc(var(--ma-travel-hover) * -1.5);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
:where(.ma-aselect__select option:hover),
|
|
186
|
+
:where(.ma-aselect__select option:focus-visible) {
|
|
187
|
+
background: color-mix(in srgb, var(--ma-ink) 8%, transparent);
|
|
188
|
+
transition-delay: 0s;
|
|
189
|
+
}
|
|
190
|
+
:where(.ma-aselect__select option:checked) {
|
|
191
|
+
font-weight: 500;
|
|
192
|
+
}
|
|
193
|
+
:where(.ma-aselect__select option:disabled) {
|
|
194
|
+
opacity: 0.45;
|
|
195
|
+
}
|
|
196
|
+
:where(.ma-aselect__select option)::checkmark {
|
|
197
|
+
order: 1;
|
|
198
|
+
margin-inline-start: auto;
|
|
199
|
+
font-size: 0.85em;
|
|
200
|
+
opacity: 0.7;
|
|
201
|
+
}
|
|
202
|
+
:where(.ma-aselect__select optgroup) {
|
|
203
|
+
padding-block: 0.25em;
|
|
204
|
+
}
|
|
205
|
+
:where(.ma-aselect__select optgroup)::label {
|
|
206
|
+
padding: 0.35em 0.7em 0.2em;
|
|
207
|
+
font-size: 0.75em;
|
|
208
|
+
letter-spacing: 0.06em;
|
|
209
|
+
text-transform: uppercase;
|
|
210
|
+
opacity: 0.55;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
@media (forced-colors: active) {
|
|
214
|
+
:where(.ma-aselect__select) {
|
|
215
|
+
border-color: ButtonText;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
@media (prefers-reduced-motion: reduce) {
|
|
219
|
+
:where(.ma-aselect__select)::picker(select),
|
|
220
|
+
:where(.ma-aselect__select option),
|
|
221
|
+
:where(.ma-aselect__chevron) {
|
|
222
|
+
transition: none;
|
|
223
|
+
translate: 0 0;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'AnimatedSelect',
|
|
5
|
+
slug: 'animated-select',
|
|
6
|
+
title: 'Animated Select',
|
|
7
|
+
description: 'A native select with an animated picker. Where the engine has appearance: base-select, the picker takes the tokens, the options arrive one after another through @starting-style with a delay from sibling-index(), the list leaves on allow-discrete and the checkmark is drawn. Elsewhere it is the native select with the same border and chevron, no animation. No imitated listbox: the keyboard, type-to-select, the value and the accessibility tree are the browser\'s. Zero JavaScript.',
|
|
8
|
+
summary: 'A native <select> whose picker animates open with staggered options, where the engine has appearance: base-select.',
|
|
9
|
+
category: 'ui',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['click'],
|
|
12
|
+
support: { baseline: 'limited', needs: ['base-select', 'picker', 'sibling-index', 'starting-style', 'allow-discrete'], fallback: 'Firefox, and Safari before 27, show the native select styled with the token border and the chevron; the picker is the platform\'s and nothing animates' },
|
|
13
|
+
readout: '<AnimatedSelect name="model" options={models}>',
|
|
14
|
+
usage: `---\nconst models = [\n { value: 'fable', label: 'Fable 5.1' },\n { value: 'opus', label: 'Opus 5' },\n { value: 'sonnet', label: 'Sonnet 5' },\n { value: 'haiku', label: 'Haiku 4.5', group: 'Small' },\n];\n---\n<AnimatedSelect name="model" label="Model" options={models} value="opus" />\n<AnimatedSelect name="size" aria-label="Size" placeholder="Pick a size" size="sm" options={[{ value: 's', label: 'Small' }, { value: 'l', label: 'Large' }]} required />`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'options', type: '{ value, label, disabled?, group? }[]', description: 'The options; those sharing a group go into one <optgroup>, in order of first appearance.' },
|
|
17
|
+
{ name: 'name', type: 'string', description: 'Form field name.' },
|
|
18
|
+
{ name: 'value', type: 'string', description: 'The selected option\'s value.' },
|
|
19
|
+
{ name: 'label', type: 'string', description: 'Visible label as <label for>. Without it, pass aria-label.' },
|
|
20
|
+
{ name: 'placeholder', type: 'string', description: 'A disabled first option, selected until a choice is made. With required, the form refuses it.' },
|
|
21
|
+
{ name: 'size', type: "'sm' | 'md'", default: "'md'", description: 'Padding and font size.' },
|
|
22
|
+
{ name: 'id', type: 'string', default: 'unique per render', description: 'The select\'s id, and the label\'s for. Unset, ma-aselect- with the name and a random suffix, so a PromptInput and an AnimatedSelect both named model can share a page. Yours wins.' },
|
|
23
|
+
],
|
|
24
|
+
reducedMotion: 'The picker appears in place and the options are all there at once.',
|
|
25
|
+
clientRouter: 'A native select; nothing to bind.',
|
|
26
|
+
craft: [
|
|
27
|
+
'appearance: base-select is the customizable-select feature: the select stays a select (value, name, required, keyboard, type-to-select, the accessibility tree) and its picker becomes a styleable box (::picker(select)) with styleable options. No imitated listbox with its own key handling and its own bugs.',
|
|
28
|
+
'The options stagger with one rule: transition-delay is sibling-index() times the stagger token, so the first option is on screen on the first frame of the picker and the sixth follows a fraction later. On hover the delay is zero, so the highlight never lags the pointer.',
|
|
29
|
+
'The picker enters through @starting-style (a small rise on the preset curve) and leaves through display and overlay on allow-discrete, so the exit is visible and the top layer waits for it.',
|
|
30
|
+
'Engines without the feature get the same border, radius, panel colour and chevron on the native control, so a form with one of each does not look like two libraries. The picker there is the platform\'s own; nothing is emulated.',
|
|
31
|
+
'The chevron is ours in both modes (the native ::picker-icon is hidden) so the two looks share one glyph; under base-select it turns on :open.',
|
|
32
|
+
'With label it renders two nodes: the <label>, then the wrapper span that holds the select and the chevron. class and style go to the wrapper, every other attribute to the select; wrap the component in an element of your own to place both as one.',
|
|
33
|
+
],
|
|
34
|
+
related: ['prompt-input', 'popover', 'switch'],
|
|
35
|
+
intents: ['animated select', 'custom select css', 'styled dropdown native', 'select with animated options', 'customizable select', 'dropdown with stagger'],
|
|
36
|
+
replaces: ['Select (shadcn / Radix)', 'AnimatedSelect (Motion Primitives)', 'Listbox (Headless UI)'],
|
|
37
|
+
fixture: { props: { name: 'ma-fixture', label: 'Fixture content for AnimatedSelect', options: [{ value: 'a', label: 'Alpha' }, { value: 'b', label: 'Beta' }] }, expect: 'Fixture content for AnimatedSelect' },
|
|
38
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ArrowButton — the arrow leaves and a new one arrives on hover, zero JS.
|
|
4
|
+
* Two copies of the icon share one clipped cell; hover translates the first
|
|
5
|
+
* out and the second in on the preset's spring, so the arrow reads as one
|
|
6
|
+
* that kept going. `direction` rotates the cell, so up, down and left travel
|
|
7
|
+
* the way they point; on a right-to-left page right and left are mirrored,
|
|
8
|
+
* so right still points past the end of the label. Hover is read from the
|
|
9
|
+
* button itself or from the link or button around it; :focus-visible draws
|
|
10
|
+
* the same state.
|
|
11
|
+
*/
|
|
12
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
13
|
+
|
|
14
|
+
type Direction = 'right' | 'left' | 'up' | 'down';
|
|
15
|
+
|
|
16
|
+
interface Props extends HTMLAttributes<'button'> {
|
|
17
|
+
/** Element to render. `span` for a label inside your own link or button. */
|
|
18
|
+
as?: 'button' | 'a' | 'span';
|
|
19
|
+
/** For `as="a"`. */
|
|
20
|
+
href?: string;
|
|
21
|
+
/** Where the arrow points and travels. */
|
|
22
|
+
direction?: Direction;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ROT: Record<Direction, number> = { right: 0, down: 90, left: 180, up: -90 };
|
|
26
|
+
const { as: Tag = 'button', href, direction = 'right', class: className, style, ...rest } = Astro.props;
|
|
27
|
+
const vars = [`--ma-arrow-rot:${ROT[direction]}deg`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<Tag class:list={['ma-arrow', className]} data-dir={direction} href={Tag === 'a' ? href : undefined} type={Tag === 'button' ? 'button' : undefined} style={vars} {...rest}>
|
|
31
|
+
<span class="ma-arrow__label"><slot /></span>
|
|
32
|
+
<span class="ma-arrow__box" aria-hidden="true">
|
|
33
|
+
<svg class="ma-arrow__icon ma-arrow__icon--out" viewBox="0 0 16 16"><path d="M2 8h11M9 4l4 4-4 4" /></svg>
|
|
34
|
+
<svg class="ma-arrow__icon ma-arrow__icon--in" viewBox="0 0 16 16"><path d="M2 8h11M9 4l4 4-4 4" /></svg>
|
|
35
|
+
</span>
|
|
36
|
+
</Tag>
|
|
37
|
+
|
|
38
|
+
<style is:global>
|
|
39
|
+
@layer components {
|
|
40
|
+
:where(.ma-arrow) {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.5em;
|
|
44
|
+
padding: 0.65em 1.1em;
|
|
45
|
+
border: 1px solid var(--ma-edge);
|
|
46
|
+
border-radius: 0.6em;
|
|
47
|
+
background: var(--ma-panel);
|
|
48
|
+
color: var(--ma-ink);
|
|
49
|
+
font: inherit;
|
|
50
|
+
font-weight: 500;
|
|
51
|
+
line-height: 1.2;
|
|
52
|
+
text-decoration: none;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
transition:
|
|
55
|
+
border-color var(--ma-duration-fast) var(--ma-ease-out),
|
|
56
|
+
background-color var(--ma-duration-fast) var(--ma-ease-out);
|
|
57
|
+
}
|
|
58
|
+
:where(.ma-arrow[data-dir='left'] .ma-arrow__box) {
|
|
59
|
+
order: -1;
|
|
60
|
+
}
|
|
61
|
+
:where(.ma-arrow__box) {
|
|
62
|
+
display: inline-grid;
|
|
63
|
+
width: 1.1em;
|
|
64
|
+
height: 1.1em;
|
|
65
|
+
overflow: clip;
|
|
66
|
+
rotate: var(--ma-arrow-rot, 0deg);
|
|
67
|
+
flex: none;
|
|
68
|
+
}
|
|
69
|
+
:where(.ma-arrow__icon) {
|
|
70
|
+
grid-area: 1 / 1;
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: 100%;
|
|
73
|
+
fill: none;
|
|
74
|
+
stroke: currentColor;
|
|
75
|
+
stroke-width: 1.6;
|
|
76
|
+
stroke-linecap: round;
|
|
77
|
+
stroke-linejoin: round;
|
|
78
|
+
transition: translate var(--ma-duration) var(--ma-ease);
|
|
79
|
+
}
|
|
80
|
+
:where(.ma-arrow__icon--in) {
|
|
81
|
+
translate: -110% 0;
|
|
82
|
+
}
|
|
83
|
+
/* right to left, a horizontal arrow is mirrored: right points to the inline end (left on the page), away from the
|
|
84
|
+
label as it does left to right, and travels that way; left mirrors back. The mirror is on the cell, so the travel
|
|
85
|
+
inside it follows. [dir] and not :dir(): Chrome has :dir() only from 120, and a build that targets an older
|
|
86
|
+
Chrome rewrites it into a guess from lang, which a page that sets only dir never matches. An ltr island one level
|
|
87
|
+
inside an rtl page is set back */
|
|
88
|
+
:where([dir='rtl'] .ma-arrow:is([data-dir='right'], [data-dir='left']) .ma-arrow__box, .ma-arrow[dir='rtl']:is([data-dir='right'], [data-dir='left']) .ma-arrow__box) {
|
|
89
|
+
scale: -1 1;
|
|
90
|
+
}
|
|
91
|
+
:where([dir='rtl'] [dir='ltr'] .ma-arrow .ma-arrow__box, [dir='rtl'] .ma-arrow[dir='ltr'] .ma-arrow__box) {
|
|
92
|
+
scale: none;
|
|
93
|
+
}
|
|
94
|
+
@media (hover: hover) {
|
|
95
|
+
:where(.ma-arrow:hover),
|
|
96
|
+
:where(:is(a, button, label):hover .ma-arrow) {
|
|
97
|
+
border-color: color-mix(in srgb, var(--ma-ink) 40%, transparent);
|
|
98
|
+
}
|
|
99
|
+
:where(.ma-arrow:hover .ma-arrow__icon--out),
|
|
100
|
+
:where(:is(a, button, label):hover .ma-arrow .ma-arrow__icon--out) {
|
|
101
|
+
translate: 110% 0;
|
|
102
|
+
}
|
|
103
|
+
:where(.ma-arrow:hover .ma-arrow__icon--in),
|
|
104
|
+
:where(:is(a, button, label):hover .ma-arrow .ma-arrow__icon--in) {
|
|
105
|
+
translate: 0 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
:where(.ma-arrow:focus-visible),
|
|
109
|
+
:where(:is(a, button):focus-visible .ma-arrow) {
|
|
110
|
+
outline: 2px solid currentColor;
|
|
111
|
+
outline-offset: 2px;
|
|
112
|
+
}
|
|
113
|
+
:where(.ma-arrow:focus-visible .ma-arrow__icon--out),
|
|
114
|
+
:where(:is(a, button):focus-visible .ma-arrow .ma-arrow__icon--out) {
|
|
115
|
+
translate: 110% 0;
|
|
116
|
+
}
|
|
117
|
+
:where(.ma-arrow:focus-visible .ma-arrow__icon--in),
|
|
118
|
+
:where(:is(a, button):focus-visible .ma-arrow .ma-arrow__icon--in) {
|
|
119
|
+
translate: 0 0;
|
|
120
|
+
}
|
|
121
|
+
@media (prefers-reduced-motion: reduce) {
|
|
122
|
+
:where(.ma-arrow__icon) {
|
|
123
|
+
transition: none;
|
|
124
|
+
}
|
|
125
|
+
:where(.ma-arrow__icon--out) {
|
|
126
|
+
translate: 0 0;
|
|
127
|
+
}
|
|
128
|
+
:where(.ma-arrow__icon--in) {
|
|
129
|
+
translate: -110% 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'ArrowButton',
|
|
5
|
+
slug: 'arrow-button',
|
|
6
|
+
title: 'Arrow Button',
|
|
7
|
+
description: 'A button whose arrow slides out on hover as a new one slides in. Two copies of the icon sit in one clipped cell, one translating out while the other comes in on the preset\'s spring. direction rotates the cell so up, down and left travel the way they point. Hover is read from the containing link or button too; focus draws the same. Zero JavaScript.',
|
|
8
|
+
summary: 'A button whose arrow slides out on hover while a new one slides in, pointing in any of four directions.',
|
|
9
|
+
category: 'ui',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['hover'],
|
|
12
|
+
support: { baseline: 'widely', needs: [] },
|
|
13
|
+
readout: '<ArrowButton direction="right">',
|
|
14
|
+
usage: `<ArrowButton as="a" href="/components/">Browse the catalogue</ArrowButton>\n<ArrowButton direction="down">Read on</ArrowButton>\n<a href="/docs/" class="my-link"><ArrowButton as="span">Docs</ArrowButton></a>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'as', type: "'button' | 'a' | 'span'", default: "'button'", description: 'Element to render; span for a label inside your own link.' },
|
|
17
|
+
{ name: 'href', type: 'string', description: 'For as="a".' },
|
|
18
|
+
{ name: 'direction', type: "'right' | 'left' | 'up' | 'down'", default: "'right'", description: 'Where the arrow points and travels. On a right-to-left page right and left are mirrored: right points to the end of the line (left on the page), past the label, as it does in English.' },
|
|
19
|
+
],
|
|
20
|
+
reducedMotion: 'The arrow stays put; hover changes the border only.',
|
|
21
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
22
|
+
craft: [
|
|
23
|
+
'The travel is 110 % of the cell, not 100 %: the leaving arrow clears the clip before the arriving one lands, so at no frame are two heads visible.',
|
|
24
|
+
'One rotate on the cell handles four directions; the icons and the keyframes are the same, so the four variants cost one rule. Right to left, a scale of -1 on the same cell mirrors right and left under a dir="rtl" ancestor, so the arrow leaves past the label on either side. The selector reads the attribute because Chrome has :dir() only from 120, and a build that targets an older Chrome rewrites it into a guess from lang, which a page that sets only dir never matches.',
|
|
25
|
+
'Hover on the whole button or on the link around a span: the arrow acknowledges the whole target.',
|
|
26
|
+
'On the preset spring (ui: 450 ms): a snap preset makes it a tick, ambient a glide. The preset sets the feel.',
|
|
27
|
+
],
|
|
28
|
+
related: ['text-roll', 'underline-hover', 'shimmer-button'],
|
|
29
|
+
intents: ['arrow button hover', 'button arrow slides out', 'cta arrow animation', 'link arrow animation', 'arrow that moves on hover'],
|
|
30
|
+
replaces: ['ArrowButton (Animata)', 'InteractiveHoverButton (Magic UI)', 'link arrow hover (Tailwind Plus)'],
|
|
31
|
+
fixture: { slot: 'Fixture content for ArrowButton', expect: 'Fixture content for ArrowButton' },
|
|
32
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Aurora — ambient gradient field, zero JS, no WebGL.
|
|
4
|
+
*
|
|
5
|
+
* Four layers, each on the compositor: three soft colour fields drifting at
|
|
6
|
+
* three coprime periods so the pattern never repeats, a slow hue rotation on
|
|
7
|
+
* the whole field so the colours themselves move, and a grain layer stepped
|
|
8
|
+
* at 12 fps so the surface reads as film, not vector. No filter: blur() — the
|
|
9
|
+
* softness comes from the gradient falloff itself, so every frame costs a
|
|
10
|
+
* transform (and one accelerated hue-rotate), never a re-blur. Fills its
|
|
11
|
+
* positioned parent.
|
|
12
|
+
*/
|
|
13
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
14
|
+
|
|
15
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
16
|
+
/** Three colours, any CSS colour. */
|
|
17
|
+
colors?: [string, string, string];
|
|
18
|
+
/** Seconds per drift cycle of the slowest field; the others run at 0.72× and 0.55×. */
|
|
19
|
+
duration?: number;
|
|
20
|
+
/** Overall strength, 0–1. */
|
|
21
|
+
opacity?: number;
|
|
22
|
+
/** Degrees the hues wander either side of the given colours; 0 pins them. */
|
|
23
|
+
hue?: number;
|
|
24
|
+
/** Grain strength, 0–1; 0 removes the layer. */
|
|
25
|
+
grain?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
colors = ['oklch(70% 0.19 300)', 'oklch(78% 0.15 200)', 'oklch(75% 0.17 350)'],
|
|
30
|
+
duration = 22,
|
|
31
|
+
opacity = 0.55,
|
|
32
|
+
hue = 40,
|
|
33
|
+
grain = 0.5,
|
|
34
|
+
class: className,
|
|
35
|
+
style,
|
|
36
|
+
...rest
|
|
37
|
+
} = Astro.props;
|
|
38
|
+
const vars = [
|
|
39
|
+
`--ma-aurora-1:${colors[0]}`,
|
|
40
|
+
`--ma-aurora-2:${colors[1]}`,
|
|
41
|
+
`--ma-aurora-3:${colors[2]}`,
|
|
42
|
+
`--ma-aurora-duration:${duration}s`,
|
|
43
|
+
`--ma-aurora-opacity:${opacity}`,
|
|
44
|
+
`--ma-aurora-hue:${hue}deg`,
|
|
45
|
+
`--ma-aurora-grain:${grain}`,
|
|
46
|
+
typeof style === 'string' ? style : '',
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(';');
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
class:list={['ma-aurora', className]}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
data-grain={grain > 0 ? '' : undefined}
|
|
56
|
+
style={vars}
|
|
57
|
+
{...rest}
|
|
58
|
+
>
|
|
59
|
+
<span class="ma-aurora__field">
|
|
60
|
+
<span class="ma-aurora__blob"></span>
|
|
61
|
+
<span class="ma-aurora__blob"></span>
|
|
62
|
+
<span class="ma-aurora__blob"></span>
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style is:global>
|
|
67
|
+
@layer components {
|
|
68
|
+
:where(.ma-aurora) {
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
opacity: var(--ma-aurora-opacity, 0.55);
|
|
74
|
+
contain: strict;
|
|
75
|
+
}
|
|
76
|
+
:where(.ma-aurora__field) {
|
|
77
|
+
position: absolute;
|
|
78
|
+
inset: -10%;
|
|
79
|
+
will-change: filter;
|
|
80
|
+
animation: ma-aurora-hue calc(var(--ma-aurora-duration, 22s) * 2.6) ease-in-out infinite alternate;
|
|
81
|
+
}
|
|
82
|
+
:where(.ma-aurora__blob) {
|
|
83
|
+
position: absolute;
|
|
84
|
+
width: 70%;
|
|
85
|
+
aspect-ratio: 1;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
background: radial-gradient(circle at center, var(--ma-aurora-c) 0%, color-mix(in oklch, var(--ma-aurora-c), transparent 55%) 28%, transparent 64%);
|
|
88
|
+
will-change: translate, scale;
|
|
89
|
+
animation: ma-aurora-drift var(--ma-aurora-duration, 22s) ease-in-out infinite alternate;
|
|
90
|
+
}
|
|
91
|
+
:where(.ma-aurora__blob:nth-child(1)) {
|
|
92
|
+
--ma-aurora-c: var(--ma-aurora-1);
|
|
93
|
+
top: -25%;
|
|
94
|
+
left: -15%;
|
|
95
|
+
}
|
|
96
|
+
:where(.ma-aurora__blob:nth-child(2)) {
|
|
97
|
+
--ma-aurora-c: var(--ma-aurora-2);
|
|
98
|
+
top: 5%;
|
|
99
|
+
right: -20%;
|
|
100
|
+
animation-name: ma-aurora-drift-2;
|
|
101
|
+
animation-duration: calc(var(--ma-aurora-duration, 22s) * 0.72);
|
|
102
|
+
animation-delay: calc(var(--ma-aurora-duration, 22s) / -3);
|
|
103
|
+
}
|
|
104
|
+
:where(.ma-aurora__blob:nth-child(3)) {
|
|
105
|
+
--ma-aurora-c: var(--ma-aurora-3);
|
|
106
|
+
bottom: -35%;
|
|
107
|
+
left: 15%;
|
|
108
|
+
animation-name: ma-aurora-drift-3;
|
|
109
|
+
animation-duration: calc(var(--ma-aurora-duration, 22s) * 0.55);
|
|
110
|
+
animation-delay: calc(var(--ma-aurora-duration, 22s) / -1.5);
|
|
111
|
+
}
|
|
112
|
+
/* grain: a tiled SVG turbulence, stepped so it flickers like film at 12 fps; translate only, so it stays on the compositor */
|
|
113
|
+
:where(.ma-aurora[data-grain])::after {
|
|
114
|
+
content: '';
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: -20%;
|
|
117
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
118
|
+
background-size: 200px 200px;
|
|
119
|
+
mix-blend-mode: overlay;
|
|
120
|
+
opacity: calc(var(--ma-aurora-grain, 0.5) * 0.5);
|
|
121
|
+
animation: ma-aurora-grain 0.5s steps(6) infinite;
|
|
122
|
+
}
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
:where(.ma-aurora__field),
|
|
125
|
+
:where(.ma-aurora__blob),
|
|
126
|
+
:where(.ma-aurora[data-grain])::after {
|
|
127
|
+
animation: none;
|
|
128
|
+
will-change: auto;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@keyframes ma-aurora-drift {
|
|
134
|
+
0% {
|
|
135
|
+
translate: 0 0;
|
|
136
|
+
scale: 1;
|
|
137
|
+
}
|
|
138
|
+
50% {
|
|
139
|
+
translate: 28% 14%;
|
|
140
|
+
scale: 1.25;
|
|
141
|
+
}
|
|
142
|
+
100% {
|
|
143
|
+
translate: -14% 30%;
|
|
144
|
+
scale: 0.9;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
@keyframes ma-aurora-drift-2 {
|
|
148
|
+
0% {
|
|
149
|
+
translate: 0 0;
|
|
150
|
+
scale: 1.1;
|
|
151
|
+
}
|
|
152
|
+
50% {
|
|
153
|
+
translate: -30% 20%;
|
|
154
|
+
scale: 0.85;
|
|
155
|
+
}
|
|
156
|
+
100% {
|
|
157
|
+
translate: -10% -18%;
|
|
158
|
+
scale: 1.2;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
@keyframes ma-aurora-drift-3 {
|
|
162
|
+
0% {
|
|
163
|
+
translate: 0 0;
|
|
164
|
+
scale: 0.95;
|
|
165
|
+
}
|
|
166
|
+
50% {
|
|
167
|
+
translate: 24% -30%;
|
|
168
|
+
scale: 1.2;
|
|
169
|
+
}
|
|
170
|
+
100% {
|
|
171
|
+
translate: -20% -8%;
|
|
172
|
+
scale: 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
@keyframes ma-aurora-hue {
|
|
176
|
+
from {
|
|
177
|
+
filter: hue-rotate(calc(var(--ma-aurora-hue, 40deg) * -1));
|
|
178
|
+
}
|
|
179
|
+
to {
|
|
180
|
+
filter: hue-rotate(var(--ma-aurora-hue, 40deg));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
@keyframes ma-aurora-grain {
|
|
184
|
+
to {
|
|
185
|
+
translate: 6% 4%;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'Aurora',
|
|
5
|
+
slug: 'aurora',
|
|
6
|
+
title: 'Aurora Background',
|
|
7
|
+
description:
|
|
8
|
+
'Ambient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps. No WebGL, no canvas, no blur filter: every layer is a transform on the compositor.',
|
|
9
|
+
summary: 'An aurora background: three soft colour fields drifting at different speeds under a slow hue shift and film grain.',
|
|
10
|
+
category: 'background',
|
|
11
|
+
tier: 'A',
|
|
12
|
+
trigger: ['always'],
|
|
13
|
+
support: { baseline: 'widely', needs: [] },
|
|
14
|
+
readout: '<Aurora duration={22}>',
|
|
15
|
+
usage: `<section class="relative">\n <Aurora />\n <h1 class="relative">Content above the field</h1>\n</section>`,
|
|
16
|
+
props: [
|
|
17
|
+
{ name: 'colors', type: '[string, string, string]', default: 'violet, cyan, rose (oklch)', description: 'Three colours, any CSS colour. Keep chroma moderate on light backgrounds.' },
|
|
18
|
+
{ name: 'duration', type: 'number', default: '22', description: 'Seconds per drift cycle of the slowest field; the others run at 0.72× and 0.55× so the pattern never repeats. Under 12 stops being ambient.' },
|
|
19
|
+
{ name: 'hue', type: 'number', default: '40', description: 'Degrees the hues wander either side of the given colours over 2.6 cycles; 0 pins them.' },
|
|
20
|
+
{ name: 'grain', type: 'number', default: '0.5', description: 'Grain strength, 0–1; 0 removes the layer.' },
|
|
21
|
+
{ name: 'opacity', type: 'number', default: '0.55', description: 'Overall strength, 0–1.' },
|
|
22
|
+
],
|
|
23
|
+
reducedMotion: 'The fields stop drifting, the hue stops turning and the grain holds still: a static gradient with texture.',
|
|
24
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
25
|
+
craft: [
|
|
26
|
+
'No filter: blur(). A moving blurred layer is re-blurred every frame; a soft radial gradient moving on the compositor is not.',
|
|
27
|
+
'alternate + ease-in-out so the drift never snaps back to its start; three coprime periods (1 : 0.72 : 0.55) so the composition never repeats.',
|
|
28
|
+
'Hue moves with one accelerated hue-rotate on the field, not by repainting gradients; the grain is a static SVG tile whose translate steps at 12 fps.',
|
|
29
|
+
'contain: strict and pointer-events: none, because the layer is decoration and must never affect layout or clicks.',
|
|
30
|
+
'aria-hidden: there is nothing to announce.',
|
|
31
|
+
],
|
|
32
|
+
related: ['reveal'],
|
|
33
|
+
intents: ['aurora background', 'gradient background animation', 'animated gradient blobs', 'ambient hero background astro', 'aurora without webgl'],
|
|
34
|
+
replaces: ['Aurora (React Bits, OGL)', 'AuroraBackground (Aceternity)', 'BackgroundGradientAnimation (Aceternity)'],
|
|
35
|
+
fixture: { expect: 'class="ma-aurora"' },
|
|
36
|
+
} satisfies ComponentMeta;
|