@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,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* FilterGrid — filter buttons over a grid; the cards that stay slide to
|
|
4
|
+
* their new places, the ones that go fade out, the ones that return fade
|
|
5
|
+
* in. The move is a same-document view transition through lib/morph: each
|
|
6
|
+
* card is a group (view-transition-name: match-element) only for the
|
|
7
|
+
* duration, so a router swap never sees them; an engine without
|
|
8
|
+
* match-element cross-fades the grid. The script toggles aria-pressed and
|
|
9
|
+
* hidden — the geometry is the browser's. Without JavaScript every card
|
|
10
|
+
* shows and the bar is hidden; under reduced motion the cards change
|
|
11
|
+
* places at once.
|
|
12
|
+
*/
|
|
13
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
14
|
+
|
|
15
|
+
interface Filter {
|
|
16
|
+
label: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
21
|
+
filters: Filter[];
|
|
22
|
+
/** Label of the "everything" button; false for none. */
|
|
23
|
+
all?: string | false;
|
|
24
|
+
/** Several filters at once (any match). */
|
|
25
|
+
multi?: boolean;
|
|
26
|
+
/** Mirror the selection in the URL as ?<name>=a,b and read it on load. */
|
|
27
|
+
sync?: boolean;
|
|
28
|
+
/** The query parameter for sync. */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** Grid columns. */
|
|
31
|
+
columns?: number;
|
|
32
|
+
/** Accessible name of the filter group. */
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { filters, all = 'All', multi = false, sync = false, name = 'filter', columns = 3, label = 'Filter', class: className, style, ...rest } = Astro.props;
|
|
37
|
+
const inline = [`--ma-fgrid-cols:${columns}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<div class:list={['ma-fgrid', className]} data-ma-fgrid data-multi={multi ? '' : undefined} data-sync={sync ? name : undefined} style={inline} {...rest}>
|
|
41
|
+
<div class="ma-fgrid__bar" role="group" aria-label={label}>
|
|
42
|
+
{all !== false && <button type="button" class="ma-fgrid__filter" aria-pressed="true" data-value="">{all}</button>}
|
|
43
|
+
{filters.map((f) => <button type="button" class="ma-fgrid__filter" aria-pressed="false" data-value={f.value}>{f.label}</button>)}
|
|
44
|
+
</div>
|
|
45
|
+
<ul class="ma-fgrid__grid">
|
|
46
|
+
<slot />
|
|
47
|
+
</ul>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<style is:global>
|
|
51
|
+
@layer components {
|
|
52
|
+
:where(.ma-fgrid__bar) {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
gap: 0.375rem;
|
|
56
|
+
margin-bottom: 1rem;
|
|
57
|
+
}
|
|
58
|
+
/* no script yet (or ever): every card shows, the bar is not offered */
|
|
59
|
+
:where(.ma-fgrid:not([data-live]) .ma-fgrid__bar) {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
:where(.ma-fgrid__filter) {
|
|
63
|
+
padding: 0.35em 0.85em;
|
|
64
|
+
border: 1px solid var(--ma-edge);
|
|
65
|
+
border-radius: 999px;
|
|
66
|
+
background: var(--ma-panel);
|
|
67
|
+
color: inherit;
|
|
68
|
+
font: inherit;
|
|
69
|
+
font-size: 0.8125rem;
|
|
70
|
+
font-weight: 500;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
opacity: 0.8;
|
|
73
|
+
transition:
|
|
74
|
+
background-color var(--ma-duration-fast) var(--ma-ease-out),
|
|
75
|
+
color var(--ma-duration-fast) var(--ma-ease-out),
|
|
76
|
+
opacity var(--ma-duration-fast) var(--ma-ease-out),
|
|
77
|
+
scale var(--ma-duration) var(--ma-ease);
|
|
78
|
+
}
|
|
79
|
+
:where(.ma-fgrid__filter:hover) {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
}
|
|
82
|
+
:where(.ma-fgrid__filter:active) {
|
|
83
|
+
scale: 0.96;
|
|
84
|
+
}
|
|
85
|
+
:where(.ma-fgrid__filter[aria-pressed='true']) {
|
|
86
|
+
background: var(--ma-ink);
|
|
87
|
+
border-color: var(--ma-ink);
|
|
88
|
+
color: var(--ma-panel);
|
|
89
|
+
opacity: 1;
|
|
90
|
+
}
|
|
91
|
+
:where(.ma-fgrid__filter:focus-visible) {
|
|
92
|
+
outline: 2px solid currentColor;
|
|
93
|
+
outline-offset: 2px;
|
|
94
|
+
}
|
|
95
|
+
:where(.ma-fgrid__grid) {
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: repeat(var(--ma-fgrid-cols, 3), minmax(0, 1fr));
|
|
98
|
+
gap: 0.75rem;
|
|
99
|
+
margin: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
list-style: none;
|
|
102
|
+
}
|
|
103
|
+
/* hidden is the filter: a card's own display (a flex card, a utility class) must not bring it back. The UA's [hidden]
|
|
104
|
+
rule loses to any author display; an important declaration in a layer beats every normal one */
|
|
105
|
+
:where(.ma-fgrid__grid > [hidden]) {
|
|
106
|
+
display: none !important;
|
|
107
|
+
}
|
|
108
|
+
/* each card is a group only while the filter transition runs — match-element names them, nothing is written by the script */
|
|
109
|
+
:where(:root[data-ma-vt='filter']) .ma-fgrid__grid > * {
|
|
110
|
+
view-transition-name: match-element;
|
|
111
|
+
}
|
|
112
|
+
:where(:root)[data-ma-vt='filter']::view-transition-group(*) {
|
|
113
|
+
animation-duration: var(--ma-duration);
|
|
114
|
+
animation-timing-function: var(--ma-ease);
|
|
115
|
+
}
|
|
116
|
+
:where(:root)[data-ma-vt='filter']::view-transition-old(*):only-child,
|
|
117
|
+
:where(:root)[data-ma-vt='filter']::view-transition-new(*):only-child {
|
|
118
|
+
animation-duration: var(--ma-duration-fast);
|
|
119
|
+
animation-timing-function: var(--ma-ease-out);
|
|
120
|
+
}
|
|
121
|
+
@media (prefers-reduced-motion: reduce) {
|
|
122
|
+
:where(.ma-fgrid__filter) {
|
|
123
|
+
transition: none;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
|
|
129
|
+
<script>
|
|
130
|
+
import { onMount } from '../lib/runtime';
|
|
131
|
+
import { vt } from '../lib/morph';
|
|
132
|
+
|
|
133
|
+
onMount<HTMLElement>('[data-ma-fgrid]', (root, { signal }) => {
|
|
134
|
+
const btns = [...root.querySelectorAll<HTMLButtonElement>('.ma-fgrid__filter')];
|
|
135
|
+
const items = [...root.querySelectorAll<HTMLElement>('.ma-fgrid__grid > *')];
|
|
136
|
+
const key = root.dataset.sync;
|
|
137
|
+
const press = (b: HTMLElement, on: boolean) => (b.ariaPressed = `${on}`);
|
|
138
|
+
const on = () => btns.filter((b) => b.ariaPressed == 'true').map((b) => b.dataset.value!).filter(Boolean);
|
|
139
|
+
const update = () => {
|
|
140
|
+
const a = on();
|
|
141
|
+
items.forEach((li) => (li.hidden = !!a.length && !a.some((v) => ` ${li.dataset.tags} `.includes(` ${v} `))));
|
|
142
|
+
if (key) {
|
|
143
|
+
const u = new URL(location.href);
|
|
144
|
+
a.length ? u.searchParams.set(key, a.join(',')) : u.searchParams.delete(key);
|
|
145
|
+
history.replaceState(history.state, '', u);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
root.addEventListener(
|
|
149
|
+
'click',
|
|
150
|
+
(e) => {
|
|
151
|
+
const b = (e.target as Element).closest<HTMLButtonElement>('.ma-fgrid__filter');
|
|
152
|
+
if (!b) return;
|
|
153
|
+
// one at a time, or (multi) toggle and keep "all" pressed exactly when nothing else is
|
|
154
|
+
if (!b.dataset.value || !('multi' in root.dataset)) btns.forEach((x) => press(x, x === b));
|
|
155
|
+
else {
|
|
156
|
+
press(b, b.ariaPressed != 'true');
|
|
157
|
+
btns.forEach((x) => x.dataset.value || press(x, !on().length));
|
|
158
|
+
}
|
|
159
|
+
vt(update, 'filter');
|
|
160
|
+
},
|
|
161
|
+
{ signal },
|
|
162
|
+
);
|
|
163
|
+
const init = key && new URL(location.href).searchParams.get(key);
|
|
164
|
+
if (init) {
|
|
165
|
+
btns.forEach((b) => press(b, init.split(',').includes(b.dataset.value!)));
|
|
166
|
+
update();
|
|
167
|
+
}
|
|
168
|
+
root.dataset.live = '';
|
|
169
|
+
});
|
|
170
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'FilterGrid',
|
|
5
|
+
slug: 'filter-grid',
|
|
6
|
+
title: 'Filter Grid',
|
|
7
|
+
description: 'Filter buttons over a grid of cards: the cards that stay slide to their new places, the ones that go fade out, the ones that return fade in. The motion is a same-document view transition, so the browser measures and moves them. The script toggles aria-pressed and hidden; each card is a transition group only for the duration through view-transition-name: match-element. Without JavaScript every card shows; under reduced motion they change places at once.',
|
|
8
|
+
summary: 'Filter buttons over a card grid: the cards that stay slide to their new places while the others fade out.',
|
|
9
|
+
category: 'ui',
|
|
10
|
+
tier: 'B',
|
|
11
|
+
trigger: ['click'],
|
|
12
|
+
support: { baseline: 'newly', needs: ['view-transitions', 'match-element'], fallback: 'without view transitions the cards change places at once; without match-element the grid cross-fades and no card slides' },
|
|
13
|
+
readout: '<FilterGrid multi>',
|
|
14
|
+
usage: `<FilterGrid filters={[{ label: 'Web', value: 'web' }, { label: 'Brand', value: 'brand' }, { label: 'Motion', value: 'motion' }]} sync>\n <li data-tags="web motion"><a href="/work/one/">…</a></li>\n <li data-tags="brand"><a href="/work/two/">…</a></li>\n <li data-tags="web"><a href="/work/three/">…</a></li>\n</FilterGrid>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'filters', type: '{ label: string; value: string }[]', description: 'The buttons; a card matches when its data-tags contains the value.' },
|
|
17
|
+
{ name: 'all', type: 'string | false', default: "'All'", description: 'Label of the everything button; false for none.' },
|
|
18
|
+
{ name: 'multi', type: 'boolean', default: 'false', description: 'Several filters at once, any match.' },
|
|
19
|
+
{ name: 'sync', type: 'boolean', default: 'false', description: 'Mirror the selection in the URL (?filter=web,brand) and read it on load.' },
|
|
20
|
+
{ name: 'name', type: 'string', default: "'filter'", description: 'The query parameter for sync.' },
|
|
21
|
+
{ name: 'columns', type: 'number', default: '3', description: 'Grid columns.' },
|
|
22
|
+
{ name: 'label', type: 'string', default: "'Filter'", description: 'Accessible name of the button group.' },
|
|
23
|
+
],
|
|
24
|
+
reducedMotion: 'The cards change places at once; the buttons keep their pressed state.',
|
|
25
|
+
clientRouter: 'Bound through the shared runtime. The cards carry a view-transition-name only during a filter transition, so a navigation snapshots nothing of them; sync uses replaceState, which the router respects.',
|
|
26
|
+
craft: [
|
|
27
|
+
'A view transition does what FLIP libraries do (measure, move, fade) in the compositor and with nothing measured by hand: the script changes hidden, the browser draws the rearrangement.',
|
|
28
|
+
'Groups exist only during the transition: view-transition-name: match-element under html[data-ma-vt="filter"], so a ClientRouter swap never captures a hundred cards and the script writes no names. The per-card naming fallback for engines without match-element (Chrome 137, Firefox 144, Safari 18.4) was built, cost 170 B raw that the tier did not have, and went: those engines get the cross-fade.',
|
|
29
|
+
'Cards that leave fade on the fast token (old snapshot only), cards that arrive fade in the same, cards that stay glide on the preset curve. That is three rules and one shared root cross-fade from base.css.',
|
|
30
|
+
'aria-pressed on real buttons, in a group with a name: the state is the ARIA, and the CSS styles the attribute.',
|
|
31
|
+
'The filter is the hidden attribute, held by one important declaration in the components layer: the browser\'s own [hidden] rule loses to any author display, so a card styled as a flex box would otherwise stay on screen.',
|
|
32
|
+
'Without the script the bar is not shown, because its buttons would do nothing.',
|
|
33
|
+
],
|
|
34
|
+
related: ['lightbox', 'hover-video', 'blog-grid'],
|
|
35
|
+
intents: ['filterable grid', 'filter cards with animation', 'isotope style filter', 'portfolio filter astro', 'animated grid reflow', 'tag filter gallery'],
|
|
36
|
+
replaces: ['Isotope', 'LayoutGroup grid (Framer Motion)', 'FilterableGrid (Motion Primitives)', 'MixItUp'],
|
|
37
|
+
fixture: { props: { filters: [{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }] }, slot: '<li data-tags="one">Fixture content for FilterGrid</li><li data-tags="two">Two</li>', expect: 'Fixture content for FilterGrid' },
|
|
38
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* FooterReveal — the page content slides up and the footer is already
|
|
4
|
+
* there underneath. No timeline, no script: the footer is position: sticky
|
|
5
|
+
* at the bottom of the scrollport with a fixed height, and the content in
|
|
6
|
+
* front carries a higher z-index and its own background, so the only motion
|
|
7
|
+
* is the scroll itself. On a page shorter than the viewport the footer is
|
|
8
|
+
* simply below the content. Reduced motion changes nothing — there is no
|
|
9
|
+
* animation to remove.
|
|
10
|
+
*/
|
|
11
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
14
|
+
/** Footer height, any CSS length. */
|
|
15
|
+
height?: string;
|
|
16
|
+
/** Background of the content in front — must be opaque. */
|
|
17
|
+
background?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { height = '20rem', background = 'Canvas', class: className, style, ...rest } = Astro.props;
|
|
21
|
+
const vars = [`--ma-fr-h:${height}`, `--ma-fr-bg:${background}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div class:list={['ma-fr', className]} style={vars} {...rest}>
|
|
25
|
+
<div class="ma-fr__content"><slot /></div>
|
|
26
|
+
<footer class="ma-fr__footer"><slot name="footer" /></footer>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<style is:global>
|
|
30
|
+
@layer components {
|
|
31
|
+
:where(.ma-fr) {
|
|
32
|
+
position: relative;
|
|
33
|
+
}
|
|
34
|
+
:where(.ma-fr__content) {
|
|
35
|
+
position: relative;
|
|
36
|
+
z-index: 1;
|
|
37
|
+
background: var(--ma-fr-bg, Canvas);
|
|
38
|
+
}
|
|
39
|
+
:where(.ma-fr__footer) {
|
|
40
|
+
position: sticky;
|
|
41
|
+
bottom: 0;
|
|
42
|
+
z-index: 0;
|
|
43
|
+
height: var(--ma-fr-h, 20rem);
|
|
44
|
+
display: grid;
|
|
45
|
+
align-content: end;
|
|
46
|
+
}
|
|
47
|
+
/* there is no motion to reduce; the rule exists so the audit sees the branch was considered */
|
|
48
|
+
@media (prefers-reduced-motion: reduce) {
|
|
49
|
+
:where(.ma-fr__footer) {
|
|
50
|
+
position: sticky;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'FooterReveal',
|
|
5
|
+
slug: 'footer-reveal',
|
|
6
|
+
title: 'Footer Reveal',
|
|
7
|
+
description: 'The page content slides up to reveal a footer that is already in place underneath. No timeline and no script: the footer is position: sticky at the bottom of the scrollport with a fixed height, and the content in front carries a higher z-index and its own background, so the only motion is the scroll itself. On a page shorter than the viewport the footer simply sits below the content.',
|
|
8
|
+
summary: 'A footer reveal: at the bottom of the page the content slides up to uncover a footer already in place underneath.',
|
|
9
|
+
category: 'scroll',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['scroll'],
|
|
12
|
+
support: { baseline: 'widely', needs: [] },
|
|
13
|
+
readout: '<FooterReveal height="20rem">',
|
|
14
|
+
usage: `<FooterReveal height="22rem" background="var(--bg)">\n <main>…</main>\n <div slot="footer" class="p-10">\n <p class="text-6xl font-bold">Moonarc</p>\n </div>\n</FooterReveal>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'height', type: 'string', default: "'20rem'", description: 'Footer height, any CSS length.' },
|
|
17
|
+
{ name: 'background', type: 'string', default: "'Canvas'", description: 'Background of the content in front. It must be opaque, or the footer shows through.' },
|
|
18
|
+
],
|
|
19
|
+
reducedMotion: 'Nothing changes: there is no animation, only the scroll, which the reader controls.',
|
|
20
|
+
clientRouter: 'Static layout; nothing to rebind. Verified across swaps and on pages shorter than the viewport.',
|
|
21
|
+
craft: [
|
|
22
|
+
'Sticky, not fixed: a fixed footer needs a spacer and breaks in any scroll container; a sticky one needs a height and works inside anything that scrolls.',
|
|
23
|
+
'The content is z-index 1 with a background; the footer is z-index 0 beneath. The reveal comes from the content ending while the footer stays put, so there is nothing to animate and nothing to reduce.',
|
|
24
|
+
'The footer\'s content is aligned to its bottom, so the top edge of the design is what is uncovered first.',
|
|
25
|
+
'On a short page sticky never engages: the footer is below the content and the layout is the ordinary one.',
|
|
26
|
+
],
|
|
27
|
+
related: ['card-stack', 'parallax', 'scroll-progress'],
|
|
28
|
+
intents: ['footer reveal on scroll', 'sticky footer reveal', 'footer behind content', 'parallax footer', 'content slides over footer'],
|
|
29
|
+
replaces: ['FooterReveal (Motion Primitives)', 'sticky footer reveal (Codrops)', 'reveal footer (Osmo)'],
|
|
30
|
+
fixture: { slot: 'Fixture content for FooterReveal', expect: 'Fixture content for FooterReveal' },
|
|
31
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* GlitchText — an RGB split and a few jumping slices, every few seconds.
|
|
4
|
+
* The text is painted once; two pseudo-element copies (content: attr) in
|
|
5
|
+
* two colours are clipped to horizontal slices and offset by the hover
|
|
6
|
+
* travel token, on step-end keyframes so nothing tweens — a glitch that
|
|
7
|
+
* eases is a wobble. The burst is the last tenth of the period; the rest
|
|
8
|
+
* of the time the text is clean. Zero JS. Reduced motion: still.
|
|
9
|
+
*/
|
|
10
|
+
import type { HTMLAttributes, HTMLTag } from 'astro/types';
|
|
11
|
+
|
|
12
|
+
interface Props extends HTMLAttributes<'span'> {
|
|
13
|
+
/** The text. */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Element to render. */
|
|
16
|
+
as?: HTMLTag;
|
|
17
|
+
/** Seconds between bursts. */
|
|
18
|
+
period?: number;
|
|
19
|
+
/** Horizontal offset of the split in px. Unset: the preset's --ma-travel-hover. */
|
|
20
|
+
shift?: number;
|
|
21
|
+
/** Skew of the base text during the burst, degrees. */
|
|
22
|
+
skew?: number;
|
|
23
|
+
/** Colours of the two copies. */
|
|
24
|
+
colorA?: string;
|
|
25
|
+
colorB?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { text, as: Tag = 'span', period = 4, shift, skew = 3, colorA = 'oklch(0.65 0.22 25)', colorB = 'oklch(0.72 0.14 210)', class: className, style, ...rest } = Astro.props;
|
|
29
|
+
const vars = [`--ma-glitch-period:${period}s`, shift !== undefined && `--ma-glitch-shift:${shift}px`, `--ma-glitch-skew:${skew}deg`, `--ma-glitch-a:${colorA}`, `--ma-glitch-b:${colorB}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<Tag class:list={['ma-glitch', className]} data-text={text} style={vars} {...rest}>{text}</Tag>
|
|
33
|
+
|
|
34
|
+
<style is:global>
|
|
35
|
+
@layer components {
|
|
36
|
+
:where(.ma-glitch) {
|
|
37
|
+
position: relative;
|
|
38
|
+
display: inline-block;
|
|
39
|
+
animation: ma-glitch-root var(--ma-glitch-period, var(--ma-dur-ambient)) step-end infinite;
|
|
40
|
+
}
|
|
41
|
+
:where(.ma-glitch)::before,
|
|
42
|
+
:where(.ma-glitch)::after {
|
|
43
|
+
/* paint only: an empty alternative after the slash keeps the two copies out of the element's accessible name, which
|
|
44
|
+
read the text three times. The first line is for engines without that syntax. */
|
|
45
|
+
content: attr(data-text);
|
|
46
|
+
content: attr(data-text) / '';
|
|
47
|
+
position: absolute;
|
|
48
|
+
inset: 0;
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
clip-path: inset(100% 0 0 0);
|
|
51
|
+
animation: ma-glitch-a var(--ma-glitch-period, var(--ma-dur-ambient)) step-end infinite;
|
|
52
|
+
}
|
|
53
|
+
:where(.ma-glitch)::before {
|
|
54
|
+
color: var(--ma-glitch-a);
|
|
55
|
+
}
|
|
56
|
+
:where(.ma-glitch)::after {
|
|
57
|
+
color: var(--ma-glitch-b);
|
|
58
|
+
animation-name: ma-glitch-b;
|
|
59
|
+
}
|
|
60
|
+
@media (prefers-reduced-motion: reduce) {
|
|
61
|
+
:where(.ma-glitch) {
|
|
62
|
+
animation: none;
|
|
63
|
+
}
|
|
64
|
+
:where(.ma-glitch)::before,
|
|
65
|
+
:where(.ma-glitch)::after {
|
|
66
|
+
content: none;
|
|
67
|
+
animation: none;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* the burst is the last tenth of the period; step-end so every frame is a jump */
|
|
73
|
+
@keyframes ma-glitch-a {
|
|
74
|
+
0%,
|
|
75
|
+
89% {
|
|
76
|
+
clip-path: inset(100% 0 0 0);
|
|
77
|
+
translate: 0 0;
|
|
78
|
+
}
|
|
79
|
+
90% {
|
|
80
|
+
clip-path: inset(8% 0 62% 0);
|
|
81
|
+
translate: var(--ma-glitch-shift, var(--ma-travel-hover)) 0;
|
|
82
|
+
}
|
|
83
|
+
92% {
|
|
84
|
+
clip-path: inset(52% 0 18% 0);
|
|
85
|
+
translate: calc(-1 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
|
|
86
|
+
}
|
|
87
|
+
94% {
|
|
88
|
+
clip-path: inset(24% 0 44% 0);
|
|
89
|
+
translate: var(--ma-glitch-shift, var(--ma-travel-hover)) 0;
|
|
90
|
+
}
|
|
91
|
+
96% {
|
|
92
|
+
clip-path: inset(70% 0 6% 0);
|
|
93
|
+
translate: calc(-0.5 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
|
|
94
|
+
}
|
|
95
|
+
98%,
|
|
96
|
+
100% {
|
|
97
|
+
clip-path: inset(100% 0 0 0);
|
|
98
|
+
translate: 0 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
@keyframes ma-glitch-b {
|
|
102
|
+
0%,
|
|
103
|
+
89% {
|
|
104
|
+
clip-path: inset(100% 0 0 0);
|
|
105
|
+
translate: 0 0;
|
|
106
|
+
}
|
|
107
|
+
90% {
|
|
108
|
+
clip-path: inset(60% 0 10% 0);
|
|
109
|
+
translate: calc(-1 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
|
|
110
|
+
}
|
|
111
|
+
92% {
|
|
112
|
+
clip-path: inset(14% 0 66% 0);
|
|
113
|
+
translate: var(--ma-glitch-shift, var(--ma-travel-hover)) 0;
|
|
114
|
+
}
|
|
115
|
+
94% {
|
|
116
|
+
clip-path: inset(40% 0 30% 0);
|
|
117
|
+
translate: calc(-1 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
|
|
118
|
+
}
|
|
119
|
+
96% {
|
|
120
|
+
clip-path: inset(2% 0 82% 0);
|
|
121
|
+
translate: calc(0.5 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
|
|
122
|
+
}
|
|
123
|
+
98%,
|
|
124
|
+
100% {
|
|
125
|
+
clip-path: inset(100% 0 0 0);
|
|
126
|
+
translate: 0 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
@keyframes ma-glitch-root {
|
|
130
|
+
0%,
|
|
131
|
+
89% {
|
|
132
|
+
transform: none;
|
|
133
|
+
}
|
|
134
|
+
91% {
|
|
135
|
+
transform: skewX(calc(-1 * var(--ma-glitch-skew, 3deg)));
|
|
136
|
+
}
|
|
137
|
+
93% {
|
|
138
|
+
transform: skewX(var(--ma-glitch-skew, 3deg));
|
|
139
|
+
}
|
|
140
|
+
95%,
|
|
141
|
+
100% {
|
|
142
|
+
transform: none;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'GlitchText',
|
|
5
|
+
slug: 'glitch-text',
|
|
6
|
+
title: 'Glitch Text',
|
|
7
|
+
description:
|
|
8
|
+
'Splits text into two coloured copies and jumps a few slices sideways for a fraction of a second, every few seconds; the rest of the time the text is clean. Two pseudo-element copies clipped to slices on step-end keyframes, so nothing tweens. Pure CSS, and completely still under reduced motion.',
|
|
9
|
+
summary: 'A glitch text effect: two coloured copies and a few slices jump sideways for a moment every few seconds.',
|
|
10
|
+
category: 'text',
|
|
11
|
+
tier: 'A',
|
|
12
|
+
trigger: ['always'],
|
|
13
|
+
support: { baseline: 'widely', needs: ['clip-path'] },
|
|
14
|
+
readout: '<GlitchText period={4}>',
|
|
15
|
+
usage: `<h1><GlitchText text="Signal lost." /></h1>\n<GlitchText text="404" period={2.5} shift={6} colorA="var(--ma-ink)" colorB="oklch(0.8 0.15 152)" />`,
|
|
16
|
+
props: [
|
|
17
|
+
{ name: 'text', type: 'string', description: 'The text; repeated by two pseudo-elements, so it must be a plain string.' },
|
|
18
|
+
{ name: 'as', type: 'HTMLTag', default: "'span'", description: 'Element to render.' },
|
|
19
|
+
{ name: 'period', type: 'number', default: '4', description: 'Seconds between bursts. The burst itself is the last tenth of the period.' },
|
|
20
|
+
{ name: 'shift', type: 'number', default: 'preset (ui: 4)', description: 'Horizontal offset of the slices in px. Unset, the preset\'s --ma-travel-hover.' },
|
|
21
|
+
{ name: 'skew', type: 'number', default: '3', description: 'Skew of the base text during the burst, in degrees. 0 keeps the base text still.' },
|
|
22
|
+
{ name: 'colorA', type: 'string', default: "'oklch(0.65 0.22 25)'", description: 'Colour of the first copy.' },
|
|
23
|
+
{ name: 'colorB', type: 'string', default: "'oklch(0.72 0.14 210)'", description: 'Colour of the second copy.' },
|
|
24
|
+
],
|
|
25
|
+
reducedMotion: 'No burst at all: the copies are removed and the text stands still. A slower glitch would still be a glitch.',
|
|
26
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
27
|
+
craft: [
|
|
28
|
+
'step-end everywhere: a glitch is a sequence of wrong frames. The moment any of it interpolates it reads as a wobble.',
|
|
29
|
+
'The burst is a tenth of the period: 400 ms of noise every 4 s. Continuous glitching reads as a loading state; a rare one reads as texture.',
|
|
30
|
+
'The base text is real text in its own colour and never moves in layout; the skew is a transform, so the line around it is untouched.',
|
|
31
|
+
'The slice offset defaults to the preset\'s hover travel: 2 px on snap, 6 px on lively, so the glitch keeps the page\'s scale.',
|
|
32
|
+
'Two copies clipped to different slice sequences: the eye reads a split, not a flicker.',
|
|
33
|
+
'Generated content is part of an element\'s accessible name, so each copy declares an empty alternative (content: attr(data-text) / \'\') and a screen reader reads the text once.',
|
|
34
|
+
],
|
|
35
|
+
related: ['text-scramble', 'shiny-text', 'noise'],
|
|
36
|
+
intents: ['glitch text', 'rgb split text', 'glitch effect css', 'cyberpunk text animation', 'distorted text effect'],
|
|
37
|
+
replaces: ['GlitchText (React Bits)', 'glitch text (Kokonut, Animata)'],
|
|
38
|
+
fixture: { props: { text: 'Fixture content for GlitchText' }, expect: 'Fixture content for GlitchText' },
|
|
39
|
+
} satisfies ComponentMeta;
|