@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,35 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'ShinyText',
|
|
5
|
+
slug: 'shiny-text',
|
|
6
|
+
title: 'Shiny Text',
|
|
7
|
+
description:
|
|
8
|
+
'A highlight sweeps across the text every few seconds. The text keeps its own colour; the shine is a gradient clipped to a repeated copy on top, and only its position animates. Pure CSS.',
|
|
9
|
+
summary: 'A shiny text effect: a highlight sweeps across the text every few seconds while the text keeps its colour.',
|
|
10
|
+
category: 'text',
|
|
11
|
+
tier: 'A',
|
|
12
|
+
trigger: ['always'],
|
|
13
|
+
support: { baseline: 'widely', needs: ['bg-clip-text'] },
|
|
14
|
+
readout: '<ShinyText duration={3}>',
|
|
15
|
+
usage: `<h2><ShinyText text="Measured, not estimated." /></h2>\n<ShinyText text="Pro" color="oklch(85% 0.17 70)" duration={2} />`,
|
|
16
|
+
props: [
|
|
17
|
+
{ name: 'text', type: 'string', description: 'The text; repeated in a pseudo-element, so it must be a plain string.' },
|
|
18
|
+
{ name: 'as', type: 'HTMLTag', default: "'span'", description: 'Element to render.' },
|
|
19
|
+
{ name: 'duration', type: 'number', default: '3', description: 'Seconds per cycle; the sweep takes 60% of it and the rest is a pause.' },
|
|
20
|
+
{ name: 'color', type: 'string', default: 'var(--ma-shine)', description: 'Highlight colour; the token is theme-aware.' },
|
|
21
|
+
{ name: 'width', type: 'number', default: '0.25', description: 'Highlight width as a fraction of the text, 0.05–0.6.' },
|
|
22
|
+
],
|
|
23
|
+
reducedMotion: 'The sweep is removed; the text shows in its own colour.',
|
|
24
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
25
|
+
craft: [
|
|
26
|
+
'The base text is real text in a real colour; the pseudo-element only adds the highlight, so there is no transparent-text fallback problem and the colour is inherited as usual.',
|
|
27
|
+
'A 60/40 sweep-to-pause ratio: a highlight that never rests reads as a loading state.',
|
|
28
|
+
'Generated content is part of an element\'s accessible name, so the copy declares an empty alternative (content: attr(data-text) / \'\') and a screen reader reads the sentence once.',
|
|
29
|
+
'Only background-position moves, on a 250% texture: no layout and no filter. background-position is not composited, so every frame repaints the text\'s own box (measured in Chromium); keep it to a headline or a label.',
|
|
30
|
+
],
|
|
31
|
+
related: ['gradient-text', 'split-text'],
|
|
32
|
+
intents: ['shiny text', 'shimmer text', 'text highlight sweep', 'glossy text animation', 'shine effect on text'],
|
|
33
|
+
replaces: ['ShinyText (React Bits)', 'AnimatedShinyText (Magic UI)'],
|
|
34
|
+
fixture: { props: { text: 'Fixture content for ShinyText' }, expect: 'Fixture content for ShinyText' },
|
|
35
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Skeleton — placeholder blocks with a shimmer, zero JS. The shimmer is a
|
|
4
|
+
* gradient texture whose position moves; nothing repaints.
|
|
5
|
+
*/
|
|
6
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
9
|
+
/** Text lines to draw. */
|
|
10
|
+
lines?: number;
|
|
11
|
+
/** Draw a round avatar beside the lines. */
|
|
12
|
+
avatar?: boolean;
|
|
13
|
+
/** Seconds per shimmer pass. */
|
|
14
|
+
duration?: number;
|
|
15
|
+
/** Corner radius of the blocks, any CSS length. */
|
|
16
|
+
radius?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { lines = 3, avatar = false, duration = 1.6, radius = '0.375rem', class: className, ...rest } = Astro.props;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class:list={['ma-skeleton', className]} aria-busy="true" aria-live="polite" data-avatar={avatar ? '' : undefined} style={`--ma-sk-dur:${duration}s;--ma-sk-r:${radius}`} {...rest}>
|
|
23
|
+
{avatar && <i class="ma-skeleton__avatar"></i>}
|
|
24
|
+
<div class="ma-skeleton__lines">
|
|
25
|
+
{Array.from({ length: Math.max(1, lines) }, (_, i) => <i class="ma-skeleton__line" style={`--ma-w:${i === lines - 1 && lines > 1 ? 60 : 100 - (i % 3) * 8}%`}></i>)}
|
|
26
|
+
</div>
|
|
27
|
+
<slot />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<style is:global>
|
|
31
|
+
@layer components {
|
|
32
|
+
:where(.ma-skeleton) {
|
|
33
|
+
display: flex;
|
|
34
|
+
gap: 0.75rem;
|
|
35
|
+
align-items: flex-start;
|
|
36
|
+
}
|
|
37
|
+
:where(.ma-skeleton__lines) {
|
|
38
|
+
flex: 1;
|
|
39
|
+
display: grid;
|
|
40
|
+
gap: 0.5rem;
|
|
41
|
+
}
|
|
42
|
+
:where(.ma-skeleton__line),
|
|
43
|
+
:where(.ma-skeleton__avatar) {
|
|
44
|
+
display: block;
|
|
45
|
+
background-color: var(--ma-edge);
|
|
46
|
+
background-image: linear-gradient(100deg, transparent 30%, var(--ma-shine) 50%, transparent 70%);
|
|
47
|
+
background-size: 200% 100%;
|
|
48
|
+
background-position: 150% 0;
|
|
49
|
+
animation: ma-skeleton var(--ma-sk-dur, 1.6s) var(--ma-ease-in-out) infinite;
|
|
50
|
+
}
|
|
51
|
+
:where(.ma-skeleton__line) {
|
|
52
|
+
height: 0.8em;
|
|
53
|
+
width: var(--ma-w, 100%);
|
|
54
|
+
border-radius: var(--ma-sk-r, 0.375rem);
|
|
55
|
+
}
|
|
56
|
+
:where(.ma-skeleton__avatar) {
|
|
57
|
+
flex: none;
|
|
58
|
+
width: 2.5rem;
|
|
59
|
+
height: 2.5rem;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
}
|
|
62
|
+
@media (prefers-reduced-motion: reduce) {
|
|
63
|
+
:where(.ma-skeleton__line),
|
|
64
|
+
:where(.ma-skeleton__avatar) {
|
|
65
|
+
animation: none;
|
|
66
|
+
background-image: none;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@keyframes ma-skeleton {
|
|
72
|
+
to {
|
|
73
|
+
background-position: -50% 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'Skeleton',
|
|
5
|
+
slug: 'skeleton',
|
|
6
|
+
title: 'Skeleton',
|
|
7
|
+
description: 'Placeholder lines, optionally with an avatar, shimmering while content loads. The shimmer is a gradient texture whose position moves; pure CSS. Marks itself busy for assistive tech.',
|
|
8
|
+
summary: 'Skeleton loading placeholders: shimmering lines and an optional avatar, marked busy for assistive tech.',
|
|
9
|
+
category: 'loading',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['always'],
|
|
12
|
+
support: { baseline: 'widely', needs: [] },
|
|
13
|
+
readout: '<Skeleton lines={3}>',
|
|
14
|
+
usage: `<Skeleton lines={3} avatar />`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'lines', type: 'number', default: '3', description: 'Text lines; widths vary and the last is short, like real text.' },
|
|
17
|
+
{ name: 'avatar', type: 'boolean', default: 'false', description: 'A round block beside the lines.' },
|
|
18
|
+
{ name: 'duration', type: 'number', default: '1.6', description: 'Seconds per shimmer pass.' },
|
|
19
|
+
{ name: 'radius', type: 'string', default: "'0.375rem'", description: 'Corner radius of the lines.' },
|
|
20
|
+
],
|
|
21
|
+
reducedMotion: 'Static blocks, no shimmer.',
|
|
22
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
23
|
+
craft: [
|
|
24
|
+
'Line widths follow a pattern (100 / 92 / 84, last one 60%) so the block reads as text, not as bars.',
|
|
25
|
+
'The shimmer moves on background-position; ease-in-out gives it a pass-and-rest rhythm rather than a conveyor.',
|
|
26
|
+
'aria-busy and aria-live: assistive tech is told the region is loading and hears the content when it lands.',
|
|
27
|
+
],
|
|
28
|
+
related: ['spinner', 'progress'],
|
|
29
|
+
intents: ['skeleton loader', 'loading placeholder', 'shimmer loading', 'content placeholder animation'],
|
|
30
|
+
replaces: ['Skeleton (shadcn)', 'react-loading-skeleton'],
|
|
31
|
+
fixture: { expect: 'class="ma-skeleton"' },
|
|
32
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Sparkline — a small line chart that draws itself left to right when it
|
|
4
|
+
* scrolls into view, the last point pulsing once drawn. The SVG is rendered
|
|
5
|
+
* on the server from the data; the line is uncovered by a clip-path inset
|
|
6
|
+
* whose right edge travels from the left of the chart to past its right, so
|
|
7
|
+
* it draws at any size. Composes Reveal for the trigger; zero script of its
|
|
8
|
+
* own. The label summarises the series.
|
|
9
|
+
*/
|
|
10
|
+
import type { HTMLAttributes, HTMLTag } from 'astro/types';
|
|
11
|
+
import Reveal from './Reveal.astro';
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<'span'> {
|
|
14
|
+
/** The series. */
|
|
15
|
+
data: number[];
|
|
16
|
+
/** Element to render. */
|
|
17
|
+
as?: HTMLTag;
|
|
18
|
+
/** Size in px — also the viewBox, so the drawing scales with it; it shrinks to fit a narrower container. */
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
/** Line colour. */
|
|
22
|
+
color?: string;
|
|
23
|
+
/** Fill the area under the line. */
|
|
24
|
+
fill?: boolean;
|
|
25
|
+
/** Mark the last point. */
|
|
26
|
+
dot?: boolean;
|
|
27
|
+
/** Draw duration in ms. Unset: the preset's --ma-duration-slow. */
|
|
28
|
+
duration?: number;
|
|
29
|
+
/** Accessible name; the summary (min, max, last) is appended. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Delay in ms before drawing. */
|
|
32
|
+
delay?: number;
|
|
33
|
+
/** Play once and stay. */
|
|
34
|
+
once?: boolean;
|
|
35
|
+
/** Fraction visible to trigger. */
|
|
36
|
+
threshold?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { data, as = 'span', width = 120, height = 32, color = 'var(--chart-1, currentColor)', fill = false, dot = true, duration, label, delay, once, threshold, class: className, style, ...rest } = Astro.props;
|
|
40
|
+
const series = data.length ? data : [0];
|
|
41
|
+
const n = series.length;
|
|
42
|
+
const min = Math.min(...series);
|
|
43
|
+
const max = Math.max(...series);
|
|
44
|
+
const span = max - min || 1;
|
|
45
|
+
const pad = 3;
|
|
46
|
+
const x = (i: number) => pad + (n > 1 ? (i / (n - 1)) * (width - 2 * pad) : (width - 2 * pad) / 2);
|
|
47
|
+
const y = (v: number) => height - pad - ((v - min) / span) * (height - 2 * pad);
|
|
48
|
+
const d = series.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
|
49
|
+
const area = `${d} L${x(n - 1).toFixed(1)},${height} L${x(0).toFixed(1)},${height} Z`;
|
|
50
|
+
const last = series[n - 1]!;
|
|
51
|
+
const name = `${label ? `${label}: ` : ''}${n} points, min ${min}, max ${max}, last ${last}`;
|
|
52
|
+
const vars = [`--ma-spark-color:${color}`, duration !== undefined && `--ma-spark-dur:${duration}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<Reveal as={as} from="none" delay={delay} once={once} threshold={threshold} class={['ma-spark', className].filter(Boolean).join(' ')} role="img" aria-label={name} style={vars} {...rest}>
|
|
56
|
+
<svg class="ma-spark__svg" viewBox={`0 0 ${width} ${height}`} width={width} height={height} aria-hidden="true">
|
|
57
|
+
{fill && <path class="ma-spark__area" d={area} />}
|
|
58
|
+
<path class="ma-spark__line" d={d} vector-effect="non-scaling-stroke" />
|
|
59
|
+
{dot && (
|
|
60
|
+
<>
|
|
61
|
+
<circle class="ma-spark__ring" cx={x(n - 1).toFixed(1)} cy={y(last).toFixed(1)} r="3" />
|
|
62
|
+
<circle class="ma-spark__dot" cx={x(n - 1).toFixed(1)} cy={y(last).toFixed(1)} r="2.5" />
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
</svg>
|
|
66
|
+
</Reveal>
|
|
67
|
+
|
|
68
|
+
<style is:global>
|
|
69
|
+
@layer components {
|
|
70
|
+
:where(.ma-spark) {
|
|
71
|
+
display: inline-block;
|
|
72
|
+
max-width: 100%;
|
|
73
|
+
line-height: 0;
|
|
74
|
+
vertical-align: middle;
|
|
75
|
+
}
|
|
76
|
+
:where(.ma-spark__svg) {
|
|
77
|
+
display: block;
|
|
78
|
+
max-width: 100%;
|
|
79
|
+
height: auto;
|
|
80
|
+
overflow: visible;
|
|
81
|
+
color: var(--ma-spark-color, currentColor);
|
|
82
|
+
}
|
|
83
|
+
:where(.ma-spark__line) {
|
|
84
|
+
fill: none;
|
|
85
|
+
stroke: currentColor;
|
|
86
|
+
stroke-width: 2;
|
|
87
|
+
stroke-linecap: round;
|
|
88
|
+
stroke-linejoin: round;
|
|
89
|
+
/* A wipe, where this used to be a pathLength="1" dash: the stroke is non-scaling, so its dashes are measured on
|
|
90
|
+
screen while pathLength is measured in the viewBox, and a chart drawn four times its viewBox came out as a
|
|
91
|
+
quarter dash, a gap, a dash. The inset reaches 1em past every edge so the round caps and joins are not cut. */
|
|
92
|
+
clip-path: inset(-1em);
|
|
93
|
+
transition: clip-path var(--ma-spark-dur, var(--ma-duration-slow)) var(--ma-ease-out) var(--ma-r-delay, 0ms);
|
|
94
|
+
}
|
|
95
|
+
:where(.ma-spark__area) {
|
|
96
|
+
fill: currentColor;
|
|
97
|
+
opacity: 0.12;
|
|
98
|
+
transition: opacity var(--ma-duration) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-spark-dur, var(--ma-duration-slow)));
|
|
99
|
+
}
|
|
100
|
+
:where(.ma-spark__dot) {
|
|
101
|
+
fill: currentColor;
|
|
102
|
+
transition: opacity var(--ma-duration-fast) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-spark-dur, var(--ma-duration-slow)));
|
|
103
|
+
}
|
|
104
|
+
/* hidden until its animation starts, so nothing shows while the line is still drawing */
|
|
105
|
+
:where(.ma-spark__ring) {
|
|
106
|
+
fill: currentColor;
|
|
107
|
+
opacity: 0;
|
|
108
|
+
transform-box: fill-box;
|
|
109
|
+
transform-origin: 50% 50%;
|
|
110
|
+
animation: ma-spark-pulse var(--ma-dur-ambient) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-spark-dur, var(--ma-duration-slow))) infinite;
|
|
111
|
+
}
|
|
112
|
+
/* Under the JS gate the line starts undrawn; Reveal's .ma-in lets it draw, then the dot lands and pulses. */
|
|
113
|
+
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__line) {
|
|
114
|
+
clip-path: inset(-1em calc(100% + 1em) -1em -1em);
|
|
115
|
+
transition: none;
|
|
116
|
+
}
|
|
117
|
+
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__area),
|
|
118
|
+
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__dot) {
|
|
119
|
+
opacity: 0;
|
|
120
|
+
transition: none;
|
|
121
|
+
}
|
|
122
|
+
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__ring) {
|
|
123
|
+
animation: none;
|
|
124
|
+
opacity: 0;
|
|
125
|
+
}
|
|
126
|
+
@media (prefers-reduced-motion: reduce) {
|
|
127
|
+
:where(.ma-spark__line),
|
|
128
|
+
:where(.ma-spark__area),
|
|
129
|
+
:where(.ma-spark__dot) {
|
|
130
|
+
transition: none;
|
|
131
|
+
}
|
|
132
|
+
:where(.ma-spark__ring) {
|
|
133
|
+
animation: none;
|
|
134
|
+
opacity: 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/* Print: drawn, with its area and its last point. A chart printed before it scrolled into view was empty. */
|
|
138
|
+
@media print {
|
|
139
|
+
:where(.ma-spark .ma-spark__line) {
|
|
140
|
+
clip-path: none;
|
|
141
|
+
transition: none;
|
|
142
|
+
}
|
|
143
|
+
:where(.ma-spark .ma-spark__area) {
|
|
144
|
+
opacity: 0.12;
|
|
145
|
+
transition: none;
|
|
146
|
+
}
|
|
147
|
+
:where(.ma-spark .ma-spark__dot) {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
transition: none;
|
|
150
|
+
}
|
|
151
|
+
:where(.ma-spark .ma-spark__ring) {
|
|
152
|
+
animation: none;
|
|
153
|
+
opacity: 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@keyframes ma-spark-pulse {
|
|
159
|
+
from {
|
|
160
|
+
scale: 1;
|
|
161
|
+
opacity: 0.5;
|
|
162
|
+
}
|
|
163
|
+
to {
|
|
164
|
+
scale: 3;
|
|
165
|
+
opacity: 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'Sparkline',
|
|
5
|
+
slug: 'sparkline',
|
|
6
|
+
title: 'Sparkline',
|
|
7
|
+
description:
|
|
8
|
+
'A small line chart that draws itself from left to right when it scrolls into view; the last point lands and pulses. The SVG is rendered on the server from the data, the line is uncovered by a clip that sweeps from its left edge to its right, and the accessible name summarises the series. Composes Reveal; no script of its own.',
|
|
9
|
+
summary: 'A small line chart that draws itself from left to right as it scrolls into view, its last point pulsing.',
|
|
10
|
+
category: 'data',
|
|
11
|
+
tier: 'A',
|
|
12
|
+
trigger: ['scroll'],
|
|
13
|
+
support: { baseline: 'widely', needs: ['clip-path'] },
|
|
14
|
+
readout: '<Sparkline data={[…]}>',
|
|
15
|
+
usage: `<Sparkline data={[12, 18, 15, 22, 30, 28, 41]} label="Stars per week" class="text-signal" />\n<Sparkline data={downloads} width={240} height={48} fill dot={false} duration={1200} />`,
|
|
16
|
+
props: [
|
|
17
|
+
{ name: 'data', type: 'number[]', description: 'The series, in order.' },
|
|
18
|
+
{ name: 'as', type: 'HTMLTag', default: "'span'", description: 'Element to render.' },
|
|
19
|
+
{ name: 'width', type: 'number', default: '120', description: 'Width in px, also the viewBox; the drawing scales with it and shrinks to fit a narrower container.' },
|
|
20
|
+
{ name: 'height', type: 'number', default: '32', description: 'See width.' },
|
|
21
|
+
{ name: 'color', type: 'string', default: "'var(--chart-1, currentColor)'", description: 'Line colour; the area and the dot follow it. The first chart colour of the theme contract when a theme defines one, else the text colour.' },
|
|
22
|
+
{ name: 'fill', type: 'boolean', default: 'false', description: 'Tint the area under the line once it is drawn.' },
|
|
23
|
+
{ name: 'dot', type: 'boolean', default: 'true', description: 'Mark the last point and pulse it.' },
|
|
24
|
+
{ name: 'duration', type: 'number', default: 'preset (700)', description: 'Draw duration in ms. Unset, the preset\'s --ma-duration-slow.' },
|
|
25
|
+
{ name: 'label', type: 'string', description: 'Accessible name; "n points, min, max, last" is appended.' },
|
|
26
|
+
{ name: 'delay', type: 'number', default: '0', description: 'Delay before drawing.' },
|
|
27
|
+
{ name: 'once', type: 'boolean', default: 'true', description: 'Play once and stay.' },
|
|
28
|
+
{ name: 'threshold', type: 'number', default: '0.2', description: 'Fraction visible to trigger.' },
|
|
29
|
+
],
|
|
30
|
+
reducedMotion: 'The line is drawn at once and the last point does not pulse.',
|
|
31
|
+
clientRouter: 'Inherits Reveal: rebinds after every navigation.',
|
|
32
|
+
craft: [
|
|
33
|
+
'The draw is a clip-path inset whose right edge moves from the left of the chart to past its right, so it is one transition whatever the data and whatever size the chart is drawn at. A pathLength="1" dash does the same only at the viewBox\'s own size: with a non-scaling stroke the dash is measured on screen, and a chart drawn four times its viewBox showed a quarter dash, a gap, a dash.',
|
|
34
|
+
'Drawn on ease-out at the slow duration: a chart line should arrive like a pen stroke, fast at first and settling at the last point.',
|
|
35
|
+
'The dot lands when the line finishes (its transition is delayed by the draw duration) and then pulses on the ambient duration, marking the current value.',
|
|
36
|
+
'vector-effect: non-scaling-stroke keeps the line 2 px at any size, so the same component works at 80 px in a table cell and 400 px in a card.',
|
|
37
|
+
'role="img" and a label that says min, max and last: the summary a sighted reader gets from the shape.',
|
|
38
|
+
],
|
|
39
|
+
related: ['bar-grow', 'count-up', 'progress'],
|
|
40
|
+
intents: ['sparkline', 'animated line chart', 'svg line draw on scroll', 'tiny chart astro', 'trend line animation'],
|
|
41
|
+
replaces: ['Sparkline (Preline charts)', 'react-sparklines', 'ApexCharts sparklines'],
|
|
42
|
+
fixture: { props: { data: [3, 5, 4, 8, 7, 11], label: 'Fixture content for Sparkline' }, expect: 'Fixture content for Sparkline' },
|
|
43
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Spinner — an arc that chases its tail, zero JS. One SVG circle: the dash
|
|
4
|
+
* length breathes while the whole thing rotates, so the arc grows and
|
|
5
|
+
* shrinks like a real indeterminate indicator, not a fixed wedge.
|
|
6
|
+
*/
|
|
7
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
8
|
+
|
|
9
|
+
interface Props extends HTMLAttributes<'span'> {
|
|
10
|
+
/** Diameter in px. */
|
|
11
|
+
size?: number;
|
|
12
|
+
/** Stroke width in px. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Colour. */
|
|
15
|
+
color?: string;
|
|
16
|
+
/** Accessible label. */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Seconds per rotation. */
|
|
19
|
+
duration?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { size = 20, width = 2, color = 'currentColor', label = 'Loading', duration = 1.2, class: className, ...rest } = Astro.props;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<span class:list={['ma-spinner', className]} role="status" aria-label={label} style={`--ma-spin-size:${size}px;--ma-spin-w:${width}px;--ma-spin-color:${color};--ma-spin-dur:${duration}s`} {...rest}>
|
|
26
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
27
|
+
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-linecap="round" />
|
|
28
|
+
</svg>
|
|
29
|
+
</span>
|
|
30
|
+
|
|
31
|
+
<style is:global>
|
|
32
|
+
@layer components {
|
|
33
|
+
:where(.ma-spinner) {
|
|
34
|
+
display: inline-block;
|
|
35
|
+
width: var(--ma-spin-size, 20px);
|
|
36
|
+
height: var(--ma-spin-size, 20px);
|
|
37
|
+
color: var(--ma-spin-color, currentColor);
|
|
38
|
+
vertical-align: middle;
|
|
39
|
+
}
|
|
40
|
+
:where(.ma-spinner svg) {
|
|
41
|
+
display: block;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
animation: ma-spinner-rotate var(--ma-spin-dur, 1.2s) linear infinite;
|
|
45
|
+
}
|
|
46
|
+
:where(.ma-spinner circle) {
|
|
47
|
+
stroke-width: var(--ma-spin-w, 2px);
|
|
48
|
+
stroke-dasharray: 63;
|
|
49
|
+
stroke-dashoffset: 47;
|
|
50
|
+
animation: ma-spinner-dash calc(var(--ma-spin-dur, 1.2s) * 1.5) var(--ma-ease-in-out) infinite;
|
|
51
|
+
}
|
|
52
|
+
@media (prefers-reduced-motion: reduce) {
|
|
53
|
+
:where(.ma-spinner circle) {
|
|
54
|
+
animation: none;
|
|
55
|
+
stroke-dashoffset: 47;
|
|
56
|
+
}
|
|
57
|
+
:where(.ma-spinner svg) {
|
|
58
|
+
animation-duration: calc(var(--ma-spin-dur, 1.2s) * 2.5);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes ma-spinner-rotate {
|
|
64
|
+
to {
|
|
65
|
+
rotate: 360deg;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
@keyframes ma-spinner-dash {
|
|
69
|
+
0% {
|
|
70
|
+
stroke-dashoffset: 56;
|
|
71
|
+
}
|
|
72
|
+
50% {
|
|
73
|
+
stroke-dashoffset: 16;
|
|
74
|
+
}
|
|
75
|
+
100% {
|
|
76
|
+
stroke-dashoffset: 56;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'Spinner',
|
|
5
|
+
slug: 'spinner',
|
|
6
|
+
title: 'Spinner',
|
|
7
|
+
description: 'An indeterminate arc whose length breathes while it rotates, so it looks like it is working rather than spinning a fixed wedge. One SVG circle, pure CSS; announces itself as a status.',
|
|
8
|
+
summary: 'A loading spinner: an arc whose length breathes while it rotates, announced as a status.',
|
|
9
|
+
category: 'loading',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['always'],
|
|
12
|
+
support: { baseline: 'widely', needs: [] },
|
|
13
|
+
readout: '<Spinner size={20}>',
|
|
14
|
+
usage: `<button disabled><Spinner size={16} /> Saving…</button>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'size', type: 'number', default: '20', description: 'Diameter in px.' },
|
|
17
|
+
{ name: 'width', type: 'number', default: '2', description: 'Stroke width in px.' },
|
|
18
|
+
{ name: 'color', type: 'string', default: "'currentColor'", description: 'Colour.' },
|
|
19
|
+
{ name: 'label', type: 'string', default: "'Loading'", description: 'Accessible label.' },
|
|
20
|
+
{ name: 'duration', type: 'number', default: '1.2', description: 'Seconds per rotation.' },
|
|
21
|
+
],
|
|
22
|
+
reducedMotion: 'A fixed arc rotating at less than half speed, still readable as loading but no longer busy.',
|
|
23
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
24
|
+
craft: [
|
|
25
|
+
'Rotation is linear; the dash length is ease-in-out on a different period, so the arc never repeats the same shape at the same angle.',
|
|
26
|
+
'stroke-linecap: round, because a square-ended arc reads as broken.',
|
|
27
|
+
'role="status" with a label: the spinner is announced once, not on every frame.',
|
|
28
|
+
],
|
|
29
|
+
related: ['skeleton', 'progress', 'ripple'],
|
|
30
|
+
intents: ['loading spinner', 'css spinner', 'indeterminate loader', 'spinner astro'],
|
|
31
|
+
replaces: ['Spinner (shadcn)', 'react-spinners'],
|
|
32
|
+
fixture: { expect: 'class="ma-spinner"' },
|
|
33
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* SplitFlap — a departures board. Every character is a cell whose letter is
|
|
4
|
+
* a CSS counter on a registered integer: the integer transitions from blank
|
|
5
|
+
* to the target letter's index in the charset, so the glyph is rewritten one
|
|
6
|
+
* step at a time until it locks, and a flap on the cell turns once per step.
|
|
7
|
+
* Composes Reveal for the scroll trigger; zero script of its own. The server
|
|
8
|
+
* HTML carries the final text (.ma-sr + counters at their targets).
|
|
9
|
+
*/
|
|
10
|
+
import type { HTMLAttributes, HTMLTag } from 'astro/types';
|
|
11
|
+
import Reveal from './Reveal.astro';
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<'span'> {
|
|
14
|
+
/** The text. Boards are uppercase: letters are uppercased, characters outside the set render as ?. */
|
|
15
|
+
text: string;
|
|
16
|
+
/** Element to render. */
|
|
17
|
+
as?: HTMLTag;
|
|
18
|
+
/** Milliseconds per character step (one flap). Unset: the preset's --ma-stagger. */
|
|
19
|
+
step?: number;
|
|
20
|
+
/** Milliseconds between neighbouring cells starting. Unset: the preset's --ma-stagger-tight. */
|
|
21
|
+
stagger?: number;
|
|
22
|
+
/** Delay in ms before the first cell. */
|
|
23
|
+
delay?: number;
|
|
24
|
+
/** Play once and stay. */
|
|
25
|
+
once?: boolean;
|
|
26
|
+
/** Fraction visible to trigger. */
|
|
27
|
+
threshold?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { text, as = 'span', step, stagger, delay, once, threshold, class: className, style, ...rest } = Astro.props;
|
|
31
|
+
|
|
32
|
+
// Index 0 is blank; the @counter-style below lists the same symbols in the same order.
|
|
33
|
+
const SET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:.-/+&?!';
|
|
34
|
+
// one cell per grapheme, not per code point: an emoji with a skin tone is one ? on the board, not two
|
|
35
|
+
const cells = Array.from(new Intl.Segmenter().segment(text), (g) => g.segment).map((c) => {
|
|
36
|
+
const i = SET.indexOf(c.toUpperCase());
|
|
37
|
+
return { c, to: c === ' ' ? -1 : i >= 0 ? i : SET.indexOf('?') };
|
|
38
|
+
});
|
|
39
|
+
const vars = [step !== undefined && `--ma-flap-step:${step}ms`, stagger !== undefined && `--ma-flap-stagger:${stagger}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
40
|
+
// dir="auto": the board is a flex row, which follows the page's direction, so on a right-to-left page it spelled its
|
|
41
|
+
// text back to front. The board takes its direction from its own text instead.
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<Reveal as={as} from="none" delay={delay} once={once} threshold={threshold} class={['ma-flap', className].filter(Boolean).join(' ')} dir="auto" style={vars || undefined} {...rest}>
|
|
45
|
+
{cells.map(({ to }, i) => (to < 0 ? <span class="ma-flap__gap" aria-hidden="true"></span> : <span class="ma-flap__cell" aria-hidden="true" style={`--ma-flap-to:${to};--ma-i:${i}`}><b class="ma-flap__top"></b><b class="ma-flap__bot"></b><b class="ma-flap__flap"></b></span>))}
|
|
46
|
+
<span class="ma-sr">{text}</span>
|
|
47
|
+
</Reveal>
|
|
48
|
+
|
|
49
|
+
<style is:global>
|
|
50
|
+
@property --ma-flap-n {
|
|
51
|
+
syntax: '<integer>';
|
|
52
|
+
inherits: false;
|
|
53
|
+
initial-value: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@layer components {
|
|
57
|
+
/* the charset, in the same order as the server's SET — value 0 is the blank */
|
|
58
|
+
@counter-style ma-flap-set {
|
|
59
|
+
system: fixed 0;
|
|
60
|
+
symbols: ' ' 'A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' 'X' 'Y' 'Z' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' ':' '.' '-' '/' '+' '&' '?' '!';
|
|
61
|
+
}
|
|
62
|
+
:where(.ma-flap) {
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
flex-wrap: wrap;
|
|
65
|
+
gap: 0.08em;
|
|
66
|
+
line-height: 1;
|
|
67
|
+
font-variant-numeric: tabular-nums;
|
|
68
|
+
}
|
|
69
|
+
:where(.ma-flap__gap) {
|
|
70
|
+
width: 0.5em;
|
|
71
|
+
}
|
|
72
|
+
:where(.ma-flap__cell) {
|
|
73
|
+
position: relative;
|
|
74
|
+
display: inline-grid;
|
|
75
|
+
min-width: 1em;
|
|
76
|
+
height: 1.3em;
|
|
77
|
+
padding-inline: 0.1em;
|
|
78
|
+
perspective: 6em;
|
|
79
|
+
--ma-flap-n: var(--ma-flap-to, 0);
|
|
80
|
+
/* counter-set, not counter-reset: WebKit paints a stale glyph for siblings when a counter-reset value changes through a transition (found in 7B) */
|
|
81
|
+
counter-set: ma-flap var(--ma-flap-n);
|
|
82
|
+
/* linear on purpose: an integer ramp, so every character step takes the same time */
|
|
83
|
+
transition: --ma-flap-n calc(var(--ma-flap-to, 0) * var(--ma-flap-step, var(--ma-stagger))) linear calc(var(--ma-r-delay, 0ms) + var(--ma-i, 0) * var(--ma-flap-stagger, var(--ma-stagger-tight)));
|
|
84
|
+
}
|
|
85
|
+
/* the hinge */
|
|
86
|
+
:where(.ma-flap__cell)::after {
|
|
87
|
+
content: '';
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 50% 0 auto 0;
|
|
90
|
+
height: 1px;
|
|
91
|
+
background: var(--ma-glow);
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
}
|
|
94
|
+
:where(.ma-flap__top),
|
|
95
|
+
:where(.ma-flap__bot),
|
|
96
|
+
:where(.ma-flap__flap) {
|
|
97
|
+
grid-area: 1 / 1;
|
|
98
|
+
display: grid;
|
|
99
|
+
place-items: center;
|
|
100
|
+
font-weight: inherit;
|
|
101
|
+
border-radius: 0.12em;
|
|
102
|
+
background: var(--ma-edge);
|
|
103
|
+
}
|
|
104
|
+
:where(.ma-flap__top)::before,
|
|
105
|
+
:where(.ma-flap__bot)::before,
|
|
106
|
+
:where(.ma-flap__flap)::before {
|
|
107
|
+
content: counter(ma-flap, ma-flap-set);
|
|
108
|
+
white-space: pre;
|
|
109
|
+
}
|
|
110
|
+
:where(.ma-flap__top),
|
|
111
|
+
:where(.ma-flap__flap) {
|
|
112
|
+
clip-path: inset(0 0 50% 0);
|
|
113
|
+
}
|
|
114
|
+
:where(.ma-flap__bot) {
|
|
115
|
+
clip-path: inset(50% 0 0 0);
|
|
116
|
+
}
|
|
117
|
+
:where(.ma-flap__flap) {
|
|
118
|
+
transform-origin: 50% 50%;
|
|
119
|
+
backface-visibility: hidden;
|
|
120
|
+
}
|
|
121
|
+
/* Under the JS gate the cells start blank and step to their letter once Reveal marks the board in view; the flap turns once per step. */
|
|
122
|
+
:where([data-ma-js] .ma-flap:not(.ma-in) .ma-flap__cell) {
|
|
123
|
+
--ma-flap-n: 0;
|
|
124
|
+
transition: none;
|
|
125
|
+
}
|
|
126
|
+
:where(.ma-flap.ma-in .ma-flap__flap) {
|
|
127
|
+
animation: ma-flap-turn var(--ma-flap-step, var(--ma-stagger)) var(--ma-ease-in) calc(var(--ma-r-delay, 0ms) + var(--ma-i, 0) * var(--ma-flap-stagger, var(--ma-stagger-tight))) var(--ma-flap-to, 0);
|
|
128
|
+
}
|
|
129
|
+
@media (prefers-reduced-motion: reduce) {
|
|
130
|
+
:where(.ma-flap__cell) {
|
|
131
|
+
transition: none;
|
|
132
|
+
}
|
|
133
|
+
:where(.ma-flap.ma-in .ma-flap__flap) {
|
|
134
|
+
animation: none;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/* Print: every cell at its letter. A board printed before it scrolled into view was a row of blank cells. */
|
|
138
|
+
@media print {
|
|
139
|
+
:where(.ma-flap .ma-flap__cell) {
|
|
140
|
+
--ma-flap-n: var(--ma-flap-to, 0);
|
|
141
|
+
transition: none;
|
|
142
|
+
}
|
|
143
|
+
:where(.ma-flap .ma-flap__flap) {
|
|
144
|
+
animation: none;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@keyframes ma-flap-turn {
|
|
150
|
+
from {
|
|
151
|
+
transform: rotateX(0);
|
|
152
|
+
}
|
|
153
|
+
to {
|
|
154
|
+
transform: rotateX(-90deg);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
</style>
|