@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,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* @internal Phase 0 packaging fixture — not part of the catalogue, and not in
|
|
4
|
+
* the registry or the docs. Exported as `@moonarc/core/internal/Probe` only
|
|
5
|
+
* because tests/consumer imports it from a real install and the site's
|
|
6
|
+
* /playground page shows the runtime's counters on it.
|
|
7
|
+
*
|
|
8
|
+
* Exercises every assumption the library rests on, so tests/consumer can
|
|
9
|
+
* verify them from a real node_modules install:
|
|
10
|
+
* - a prop becomes a CSS custom property set inline
|
|
11
|
+
* - global, layered styles wrapped in :where() (consumer classes must win)
|
|
12
|
+
* - `class` and ...rest are forwarded to the root element
|
|
13
|
+
* - a colour comes from a base.css token (three-state theme)
|
|
14
|
+
* - the shared runtime binds every instance, survives ClientRouter
|
|
15
|
+
* navigation, and never double-binds
|
|
16
|
+
* - a prefers-reduced-motion branch exists
|
|
17
|
+
* Kept after Reveal (Phase 2) arrived: Reveal is catalogue, and the checks
|
|
18
|
+
* above want a fixture that never changes for product reasons.
|
|
19
|
+
*/
|
|
20
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
21
|
+
|
|
22
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
23
|
+
/** Hover transition length in ms. */
|
|
24
|
+
duration?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { duration = 300, class: className, style, ...rest } = Astro.props;
|
|
28
|
+
const inlineStyle = [`--ma-probe-duration:${duration}ms`, typeof style === 'string' ? style : '']
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(';');
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<div class:list={['ma-probe', className]} data-ma-probe style={inlineStyle} {...rest}>
|
|
34
|
+
<slot />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<style is:global>
|
|
38
|
+
/*
|
|
39
|
+
* Global on purpose. Astro's scoping prepends [data-astro-cid] *outside*
|
|
40
|
+
* :where(), which raises specificity to (0,1,0); the .ma- namespace already
|
|
41
|
+
* gives isolation, and the `components` layer + :where() give zero weight so
|
|
42
|
+
* consumer utilities and plain classes always win.
|
|
43
|
+
*/
|
|
44
|
+
@layer components {
|
|
45
|
+
:where(.ma-probe) {
|
|
46
|
+
display: block;
|
|
47
|
+
padding: 1rem 1.25rem;
|
|
48
|
+
border: 1px solid var(--ma-edge);
|
|
49
|
+
border-radius: 0.75rem;
|
|
50
|
+
transform: translateY(0);
|
|
51
|
+
transition: transform var(--ma-probe-duration) var(--ma-ease-out);
|
|
52
|
+
}
|
|
53
|
+
:where(.ma-probe.is-active) {
|
|
54
|
+
transform: translateY(-2px);
|
|
55
|
+
}
|
|
56
|
+
@media (prefers-reduced-motion: reduce) {
|
|
57
|
+
:where(.ma-probe) {
|
|
58
|
+
transition-duration: 0.01ms;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
import { onMount, onPage } from '../lib/runtime';
|
|
66
|
+
|
|
67
|
+
// Test instrumentation: tests/consumer reads window.__maProbe after navigation.
|
|
68
|
+
type Stats = { setups: number; teardowns: number; bound: number };
|
|
69
|
+
const w = window as unknown as { __maProbe?: Stats };
|
|
70
|
+
const stats: Stats = (w.__maProbe ??= { setups: 0, teardowns: 0, bound: 0 });
|
|
71
|
+
|
|
72
|
+
onPage(() => {
|
|
73
|
+
stats.setups++;
|
|
74
|
+
return () => {
|
|
75
|
+
stats.teardowns++;
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
onMount<HTMLElement>('[data-ma-probe]', (el, { signal }) => {
|
|
80
|
+
el.dataset.maBound = String(++stats.bound);
|
|
81
|
+
el.addEventListener('pointerenter', () => el.classList.add('is-active'), { signal });
|
|
82
|
+
el.addEventListener('pointerleave', () => el.classList.remove('is-active'), { signal });
|
|
83
|
+
});
|
|
84
|
+
</script>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Progress — a bar that fills to its value when it scrolls into view, with
|
|
4
|
+
* the number counting alongside; or a ring, with the number in the middle.
|
|
5
|
+
* No script of its own: Reveal is the trigger, CountUp the number; the bar
|
|
6
|
+
* is one translate transition and the ring one dash offset, both on the
|
|
7
|
+
* active preset's curve. The server HTML already shows the final state.
|
|
8
|
+
*/
|
|
9
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
10
|
+
import Reveal from './Reveal.astro';
|
|
11
|
+
import CountUp from './CountUp.astro';
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
14
|
+
/** Value, 0–max. */
|
|
15
|
+
value: number;
|
|
16
|
+
/** Maximum. */
|
|
17
|
+
max?: number;
|
|
18
|
+
/** Visible label before the number. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Show the number. */
|
|
21
|
+
showValue?: boolean;
|
|
22
|
+
/** Suffix after the number. */
|
|
23
|
+
suffix?: string;
|
|
24
|
+
/** Duration in ms. */
|
|
25
|
+
duration?: number;
|
|
26
|
+
/** Bar thickness, any CSS length. */
|
|
27
|
+
height?: string;
|
|
28
|
+
/** Fill colour. */
|
|
29
|
+
color?: string;
|
|
30
|
+
/** A bar, or a ring with the number in the middle. */
|
|
31
|
+
shape?: 'bar' | 'ring';
|
|
32
|
+
/** Ring diameter, any CSS length. */
|
|
33
|
+
size?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { value, max = 100, label, showValue = true, suffix = '%', duration = 1400, height = '0.5rem', color = 'currentColor', shape = 'bar', size = '6rem', class: className, style, ...rest } = Astro.props;
|
|
37
|
+
const pct = Math.max(0, Math.min(100, (value / max) * 100));
|
|
38
|
+
const ring = shape === 'ring';
|
|
39
|
+
// the caller's style joins the variables: passed on through {...rest} it replaced them, and the bar stayed empty
|
|
40
|
+
const vars = [`--ma-prog:${pct / 100};--ma-prog-dur:${duration}ms;--ma-prog-h:${height};--ma-prog-color:${color}${ring ? `;--ma-prog-size:${size}` : ''}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<Reveal
|
|
44
|
+
as="div"
|
|
45
|
+
from="none"
|
|
46
|
+
duration={300}
|
|
47
|
+
class={['ma-progress', ring && 'ma-progress--ring', className].filter(Boolean).join(' ')}
|
|
48
|
+
role="progressbar"
|
|
49
|
+
aria-valuenow={value}
|
|
50
|
+
aria-valuemin={0}
|
|
51
|
+
aria-valuemax={max}
|
|
52
|
+
aria-label={label}
|
|
53
|
+
style={vars}
|
|
54
|
+
{...rest}
|
|
55
|
+
>
|
|
56
|
+
{ring ? (
|
|
57
|
+
<>
|
|
58
|
+
<span class="ma-progress__ring">
|
|
59
|
+
<svg viewBox="0 0 100 100" aria-hidden="true">
|
|
60
|
+
<circle class="ma-progress__ring-track" cx="50" cy="50" r="44" />
|
|
61
|
+
<circle class="ma-progress__ring-fill" cx="50" cy="50" r="44" pathLength="1" />
|
|
62
|
+
</svg>
|
|
63
|
+
{showValue && <span class="ma-progress__center"><CountUp to={value} suffix={suffix} duration={duration} decimals={Number.isInteger(value) ? 0 : 1} class="ma-progress__value" /></span>}
|
|
64
|
+
</span>
|
|
65
|
+
{label && <span class="ma-progress__label">{label}</span>}
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
{(label || showValue) && (
|
|
70
|
+
<div class="ma-progress__head">
|
|
71
|
+
{label && <span class="ma-progress__label">{label}</span>}
|
|
72
|
+
{showValue && <CountUp to={value} suffix={suffix} duration={duration} decimals={Number.isInteger(value) ? 0 : 1} class="ma-progress__value" />}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
<span class="ma-progress__track"><span class="ma-progress__fill"></span></span>
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</Reveal>
|
|
79
|
+
|
|
80
|
+
<style is:global>
|
|
81
|
+
@layer components {
|
|
82
|
+
:where(.ma-progress__head) {
|
|
83
|
+
display: flex;
|
|
84
|
+
justify-content: space-between;
|
|
85
|
+
gap: 1rem;
|
|
86
|
+
margin-bottom: 0.5rem;
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
}
|
|
89
|
+
:where(.ma-progress__value) {
|
|
90
|
+
font-variant-numeric: tabular-nums;
|
|
91
|
+
}
|
|
92
|
+
:where(.ma-progress__track) {
|
|
93
|
+
display: block;
|
|
94
|
+
height: var(--ma-prog-h, 0.5rem);
|
|
95
|
+
border-radius: 999px;
|
|
96
|
+
background: var(--ma-edge);
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
/* the fill is a full-length rounded bar slid in from the left, clipped by the track: its end stays a half circle at
|
|
100
|
+
any value, where a horizontal scale squashed it flat towards zero */
|
|
101
|
+
:where(.ma-progress__fill) {
|
|
102
|
+
display: block;
|
|
103
|
+
height: 100%;
|
|
104
|
+
border-radius: inherit;
|
|
105
|
+
background: var(--ma-prog-color, currentColor);
|
|
106
|
+
translate: calc((var(--ma-prog, 0) - 1) * 100%) 0;
|
|
107
|
+
transition: translate var(--ma-prog-dur, 1400ms) var(--ma-ease-out);
|
|
108
|
+
}
|
|
109
|
+
:where([data-ma-js] .ma-progress:not(.ma-in) .ma-progress__fill) {
|
|
110
|
+
translate: -100% 0;
|
|
111
|
+
transition: none;
|
|
112
|
+
}
|
|
113
|
+
/* ring: one dash offset on a normalised circle, the number in the middle */
|
|
114
|
+
:where(.ma-progress--ring) {
|
|
115
|
+
display: inline-grid;
|
|
116
|
+
justify-items: center;
|
|
117
|
+
gap: 0.5rem;
|
|
118
|
+
}
|
|
119
|
+
:where(.ma-progress__ring) {
|
|
120
|
+
position: relative;
|
|
121
|
+
display: grid;
|
|
122
|
+
place-items: center;
|
|
123
|
+
width: var(--ma-prog-size, 6rem);
|
|
124
|
+
height: var(--ma-prog-size, 6rem);
|
|
125
|
+
}
|
|
126
|
+
:where(.ma-progress__ring > svg) {
|
|
127
|
+
grid-area: 1 / 1;
|
|
128
|
+
width: 100%;
|
|
129
|
+
height: 100%;
|
|
130
|
+
rotate: -90deg;
|
|
131
|
+
}
|
|
132
|
+
:where(.ma-progress__ring-track),
|
|
133
|
+
:where(.ma-progress__ring-fill) {
|
|
134
|
+
fill: none;
|
|
135
|
+
stroke-width: 8;
|
|
136
|
+
}
|
|
137
|
+
:where(.ma-progress__ring-track) {
|
|
138
|
+
stroke: var(--ma-edge);
|
|
139
|
+
}
|
|
140
|
+
:where(.ma-progress__ring-fill) {
|
|
141
|
+
stroke: var(--ma-prog-color, currentColor);
|
|
142
|
+
stroke-linecap: round;
|
|
143
|
+
stroke-dasharray: 1;
|
|
144
|
+
/* × 1px: Firefox rejects a unitless calc() here; with pathLength="1" a px is a path unit */
|
|
145
|
+
stroke-dashoffset: calc((1 - var(--ma-prog, 0)) * 1px);
|
|
146
|
+
transition: stroke-dashoffset var(--ma-prog-dur, 1400ms) var(--ma-ease-out);
|
|
147
|
+
}
|
|
148
|
+
:where([data-ma-js] .ma-progress:not(.ma-in) .ma-progress__ring-fill) {
|
|
149
|
+
stroke-dashoffset: 1;
|
|
150
|
+
transition: none;
|
|
151
|
+
}
|
|
152
|
+
:where(.ma-progress__center) {
|
|
153
|
+
grid-area: 1 / 1;
|
|
154
|
+
font-size: calc(var(--ma-prog-size, 6rem) / 4.5);
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
letter-spacing: -0.02em;
|
|
157
|
+
}
|
|
158
|
+
@media (prefers-reduced-motion: reduce) {
|
|
159
|
+
:where(.ma-progress__fill),
|
|
160
|
+
:where(.ma-progress__ring-fill) {
|
|
161
|
+
transition: none;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'Progress',
|
|
5
|
+
slug: 'progress',
|
|
6
|
+
title: 'Progress',
|
|
7
|
+
description: 'A bar that fills to its value when it scrolls into view, with the number counting up beside it, or a ring with the number in the middle. No script of its own: Reveal is the trigger and CountUp the number; the fill is one translate transition, the ring one dash offset. The server HTML already shows the final state.',
|
|
8
|
+
summary: 'A progress bar or ring that fills to its value when it scrolls into view, with the number counting up beside it.',
|
|
9
|
+
category: 'loading',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['scroll'],
|
|
12
|
+
support: { baseline: 'newly', needs: ['property', 'counter-style'], fallback: 'the bar fills; the number appears without counting' },
|
|
13
|
+
readout: '<Progress value={62}>',
|
|
14
|
+
usage: `<Progress value={62} label="Profile complete" />\n<Progress value={3.2} max={5} suffix=" GB" label="Storage used" />\n<Progress value={78} shape="ring" size="7rem" label="Goal reached" />`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'value', type: 'number', description: 'Value, 0–max.' },
|
|
17
|
+
{ name: 'max', type: 'number', default: '100', description: 'Maximum.' },
|
|
18
|
+
{ name: 'label', type: 'string', description: 'Label before the number; also the accessible name.' },
|
|
19
|
+
{ name: 'showValue', type: 'boolean', default: 'true', description: 'Show the counting number.' },
|
|
20
|
+
{ name: 'suffix', type: 'string', default: "'%'", description: 'Suffix after the number.' },
|
|
21
|
+
{ name: 'duration', type: 'number', default: '1400', description: 'Duration in ms, shared by the fill and the count.' },
|
|
22
|
+
{ name: 'height', type: 'string', default: "'0.5rem'", description: 'Bar thickness.' },
|
|
23
|
+
{ name: 'color', type: 'string', default: "'currentColor'", description: 'Fill colour.' },
|
|
24
|
+
{ name: 'shape', type: "'bar' | 'ring'", default: "'bar'", description: 'A bar, or an SVG ring that fills clockwise from the top with the number in the middle.' },
|
|
25
|
+
{ name: 'size', type: 'string', default: "'6rem'", description: 'Ring diameter.' },
|
|
26
|
+
],
|
|
27
|
+
reducedMotion: 'The bar is full at its value and the number is shown; nothing animates.',
|
|
28
|
+
clientRouter: 'Inherits Reveal: rebinds after every navigation.',
|
|
29
|
+
craft: [
|
|
30
|
+
'A progressbar needs a name: label is the visible one; without it, pass aria-label or aria-labelledby, or aria-hidden="true" when the same number already sits beside the bar as text (a table cell, a stat).',
|
|
31
|
+
'Fill and count share one duration and both ease out, so the number lands the instant the bar stops.',
|
|
32
|
+
'The fill is a full-length rounded bar that translate slides in from the left, clipped by the track: one compositor property, and its end stays a half circle at any value. A horizontal scale would squash that end towards flat as the value nears zero.',
|
|
33
|
+
'role="progressbar" with aria-valuenow; the bar is static information to assistive tech, the motion is for the eye.',
|
|
34
|
+
'The ring is a circle with pathLength="1", so the fill is one dash offset from 1 to 1 − value whatever the diameter; it starts at twelve o\'clock (rotate: −90°) and the number sits in the same grid cell.',
|
|
35
|
+
],
|
|
36
|
+
related: ['count-up', 'skeleton', 'spinner'],
|
|
37
|
+
intents: ['progress bar', 'animated progress bar on scroll', 'skill bar', 'percentage bar animation'],
|
|
38
|
+
replaces: ['Progress (shadcn)', 'animated skill bars'],
|
|
39
|
+
fixture: { props: { value: 62, label: 'Fixture content for Progress' }, expect: 'Fixture content for Progress' },
|
|
40
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProgressiveBlur — an edge that blurs what scrolls under it, more the
|
|
4
|
+
* closer it gets. `layers` absolutely positioned bands, each with a
|
|
5
|
+
* backdrop blur twice the previous one's and a mask that keeps it to its
|
|
6
|
+
* own slice of the edge, so the blur ramps instead of stepping. Nothing
|
|
7
|
+
* animates; the content moving under it is the motion. Without
|
|
8
|
+
* backdrop-filter the edge fades to a colour instead. Sits on the given
|
|
9
|
+
* edge of its positioned parent: put it beside the scroller, in a shared
|
|
10
|
+
* positioned wrapper, since inside the scroller it would scroll away.
|
|
11
|
+
*/
|
|
12
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
13
|
+
|
|
14
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
15
|
+
/** Which edge the blur sits on. */
|
|
16
|
+
direction?: 'top' | 'bottom' | 'left' | 'right';
|
|
17
|
+
/** Bands; more is smoother and dearer to paint. */
|
|
18
|
+
layers?: number;
|
|
19
|
+
/** Blur of the outermost band in px. */
|
|
20
|
+
strength?: number;
|
|
21
|
+
/** Depth of the blurred edge, any CSS length. */
|
|
22
|
+
size?: string;
|
|
23
|
+
/** Colour the edge fades to where backdrop-filter is missing (and, faintly, on top of the blur). */
|
|
24
|
+
color?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { direction = 'bottom', layers = 5, strength = 12, size = '6rem', color = 'var(--ma-panel)', class: className, style, ...rest } = Astro.props;
|
|
28
|
+
const n = Math.max(2, Math.min(8, Math.round(layers)));
|
|
29
|
+
const inline = [`--ma-pblur-n:${n}`, `--ma-pblur-strength:${strength}px`, `--ma-pblur-size:${size}`, `--ma-pblur-color:${color}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
30
|
+
// each band: blur = strength / 2^(n-1-i); mask keeps it between (i-1)/n and (i+1)/n of the depth, solid in the middle
|
|
31
|
+
const bands = Array.from({ length: n }, (_, i) => ({ i, blur: strength / 2 ** (n - 1 - i), a: Math.max(0, ((i - 1) / n) * 100), b: (i / n) * 100, c: Math.min(100, ((i + 1) / n) * 100) }));
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<div class:list={['ma-pblur', className]} aria-hidden="true" data-direction={direction} style={inline} {...rest}>
|
|
35
|
+
{bands.map((b) => <i style={`--ma-pblur-b:${b.blur}px;--ma-pblur-m0:${b.a}%;--ma-pblur-m1:${b.b}%;--ma-pblur-m2:${b.c}%`}></i>)}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<style is:global>
|
|
39
|
+
@layer components {
|
|
40
|
+
:where(.ma-pblur) {
|
|
41
|
+
position: absolute;
|
|
42
|
+
inset: auto 0 0;
|
|
43
|
+
height: var(--ma-pblur-size, 6rem);
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
/* the mask runs from the inside edge (0 %) to the outside edge (100 %) */
|
|
46
|
+
--ma-pblur-to: to bottom;
|
|
47
|
+
}
|
|
48
|
+
:where(.ma-pblur[data-direction='top']) {
|
|
49
|
+
inset: 0 0 auto;
|
|
50
|
+
--ma-pblur-to: to top;
|
|
51
|
+
}
|
|
52
|
+
:where(.ma-pblur[data-direction='left']) {
|
|
53
|
+
inset: 0 auto 0 0;
|
|
54
|
+
width: var(--ma-pblur-size, 6rem);
|
|
55
|
+
height: auto;
|
|
56
|
+
--ma-pblur-to: to left;
|
|
57
|
+
}
|
|
58
|
+
:where(.ma-pblur[data-direction='right']) {
|
|
59
|
+
inset: 0 0 0 auto;
|
|
60
|
+
width: var(--ma-pblur-size, 6rem);
|
|
61
|
+
height: auto;
|
|
62
|
+
--ma-pblur-to: to right;
|
|
63
|
+
}
|
|
64
|
+
:where(.ma-pblur > i) {
|
|
65
|
+
position: absolute;
|
|
66
|
+
inset: 0;
|
|
67
|
+
-webkit-backdrop-filter: blur(var(--ma-pblur-b));
|
|
68
|
+
backdrop-filter: blur(var(--ma-pblur-b));
|
|
69
|
+
-webkit-mask-image: linear-gradient(var(--ma-pblur-to), transparent var(--ma-pblur-m0), #000 var(--ma-pblur-m1), #000 var(--ma-pblur-m2), transparent calc(var(--ma-pblur-m2) + (var(--ma-pblur-m2) - var(--ma-pblur-m1))));
|
|
70
|
+
mask-image: linear-gradient(var(--ma-pblur-to), transparent var(--ma-pblur-m0), #000 var(--ma-pblur-m1), #000 var(--ma-pblur-m2), transparent calc(var(--ma-pblur-m2) + (var(--ma-pblur-m2) - var(--ma-pblur-m1))));
|
|
71
|
+
}
|
|
72
|
+
/* the last band also tints toward the colour so the edge lands on the panel instead of a blurred cut */
|
|
73
|
+
:where(.ma-pblur > i:last-child) {
|
|
74
|
+
background: linear-gradient(var(--ma-pblur-to), transparent, color-mix(in srgb, var(--ma-pblur-color, var(--ma-panel)) 60%, transparent));
|
|
75
|
+
}
|
|
76
|
+
/* no backdrop-filter: one gradient to the colour */
|
|
77
|
+
@supports not (backdrop-filter: blur(1px)) {
|
|
78
|
+
:where(.ma-pblur > i) {
|
|
79
|
+
display: none;
|
|
80
|
+
}
|
|
81
|
+
:where(.ma-pblur > i:last-child) {
|
|
82
|
+
display: block;
|
|
83
|
+
mask-image: none;
|
|
84
|
+
-webkit-mask-image: none;
|
|
85
|
+
background: linear-gradient(var(--ma-pblur-to), transparent, var(--ma-pblur-color, var(--ma-panel)));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/* nothing moves here; the branch is the same edge — stated so the audit sees it */
|
|
89
|
+
@media (prefers-reduced-motion: reduce) {
|
|
90
|
+
:where(.ma-pblur) {
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'ProgressiveBlur',
|
|
5
|
+
slug: 'progressive-blur',
|
|
6
|
+
title: 'Progressive Blur',
|
|
7
|
+
description: 'An edge that blurs what scrolls under it, more the closer it gets: a few bands, each with a backdrop blur twice the last and a mask that keeps it to its own slice, so the blur ramps instead of stepping. Nothing animates: the content moving beneath is the motion. Without backdrop-filter the edge fades to a colour. Zero JS.',
|
|
8
|
+
summary: 'A progressive blur at an edge: content that scrolls under it blurs more the closer it gets.',
|
|
9
|
+
category: 'background',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['always'],
|
|
12
|
+
support: { baseline: 'widely', needs: ['backdrop-filter', 'mask-image'], fallback: 'a gradient to the panel colour instead of a blur' },
|
|
13
|
+
readout: '<ProgressiveBlur direction="bottom" layers={5}>',
|
|
14
|
+
usage: `<!-- the blur is a sibling of the scroller, inside a positioned wrapper: inside the scroller it would scroll away -->\n<div class="relative">\n <div class="h-80 overflow-y-auto">\n <ul>…</ul>\n </div>\n <ProgressiveBlur direction="bottom" size="5rem" />\n</div>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'direction', type: "'top' | 'bottom' | 'left' | 'right'", default: "'bottom'", description: 'Which edge the blur sits on.' },
|
|
17
|
+
{ name: 'layers', type: 'number', default: '5', description: 'Bands, 2–8; more is smoother and costs more paint, because each band is a backdrop pass.' },
|
|
18
|
+
{ name: 'strength', type: 'number', default: '12', description: 'Blur of the outermost band in px; each band inward is half.' },
|
|
19
|
+
{ name: 'size', type: 'string', default: "'6rem'", description: 'Depth of the blurred edge, any CSS length.' },
|
|
20
|
+
{ name: 'color', type: 'string', default: 'var(--ma-panel)', description: 'The colour the edge lands on: a faint tint over the last band, the whole fade where backdrop-filter is missing.' },
|
|
21
|
+
],
|
|
22
|
+
reducedMotion: 'Unchanged. The component has no motion; what moves is the page.',
|
|
23
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
24
|
+
craft: [
|
|
25
|
+
'Blur doubles per band (strength / 2ⁿ inward): a linear ramp of blur radii looks stepped because the eye reads blur logarithmically.',
|
|
26
|
+
'Each band\'s mask overlaps its neighbours by one slice, so the transitions between blur radii are cross-fades rather than seams.',
|
|
27
|
+
'Five bands by default: measured on the catalogue, more bands raised paint cost without a visible gain at 12 px.',
|
|
28
|
+
'The last band also tints toward the panel colour so the edge lands on the surface instead of a blurred cut; the no-backdrop fallback is that same gradient at full strength.',
|
|
29
|
+
'pointer-events: none, so the content under the edge stays scrollable and clickable.',
|
|
30
|
+
'It is absolutely positioned on its parent, so it goes beside the scroller, not in it: an absolute child of a scroll container scrolls with the content and the edge would leave with the first rows.',
|
|
31
|
+
],
|
|
32
|
+
related: ['marquee', 'footer-reveal', 'skeleton'],
|
|
33
|
+
intents: ['progressive blur edge', 'gradient blur bottom of list', 'blurred fade scroll edge', 'apple style progressive blur', 'backdrop blur gradient css'],
|
|
34
|
+
replaces: ['ProgressiveBlur (Magic UI)', 'ProgressiveBlur (Motion Primitives)'],
|
|
35
|
+
fixture: { expect: 'class="ma-pblur"' },
|
|
36
|
+
} satisfies ComponentMeta;
|