@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.
Files changed (232) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/package.json +228 -0
  4. package/src/components/Accordion.astro +110 -0
  5. package/src/components/Accordion.meta.ts +33 -0
  6. package/src/components/AnimatedBeam.astro +217 -0
  7. package/src/components/AnimatedBeam.meta.ts +39 -0
  8. package/src/components/AnimatedIcons.astro +193 -0
  9. package/src/components/AnimatedIcons.meta.ts +34 -0
  10. package/src/components/AnimatedList.astro +305 -0
  11. package/src/components/AnimatedList.meta.ts +36 -0
  12. package/src/components/AnimatedSelect.astro +227 -0
  13. package/src/components/AnimatedSelect.meta.ts +38 -0
  14. package/src/components/ArrowButton.astro +133 -0
  15. package/src/components/ArrowButton.meta.ts +32 -0
  16. package/src/components/Aurora.astro +188 -0
  17. package/src/components/Aurora.meta.ts +36 -0
  18. package/src/components/AvatarStack.astro +205 -0
  19. package/src/components/AvatarStack.meta.ts +37 -0
  20. package/src/components/BarGrow.astro +123 -0
  21. package/src/components/BarGrow.meta.ts +42 -0
  22. package/src/components/BorderBeam.astro +75 -0
  23. package/src/components/BorderBeam.meta.ts +33 -0
  24. package/src/components/BrowserFrame.astro +172 -0
  25. package/src/components/BrowserFrame.meta.ts +36 -0
  26. package/src/components/BubbleText.astro +90 -0
  27. package/src/components/BubbleText.meta.ts +38 -0
  28. package/src/components/CardStack.astro +79 -0
  29. package/src/components/CardStack.meta.ts +33 -0
  30. package/src/components/Carousel.astro +147 -0
  31. package/src/components/Carousel.meta.ts +35 -0
  32. package/src/components/CheckDraw.astro +159 -0
  33. package/src/components/CheckDraw.meta.ts +37 -0
  34. package/src/components/CircularText.astro +103 -0
  35. package/src/components/CircularText.meta.ts +38 -0
  36. package/src/components/CodeTabs.astro +213 -0
  37. package/src/components/CodeTabs.meta.ts +36 -0
  38. package/src/components/CommandPalette.astro +363 -0
  39. package/src/components/CommandPalette.meta.ts +40 -0
  40. package/src/components/Confetti.astro +143 -0
  41. package/src/components/Confetti.meta.ts +40 -0
  42. package/src/components/CopyButton.astro +189 -0
  43. package/src/components/CopyButton.meta.ts +41 -0
  44. package/src/components/CountUp.astro +134 -0
  45. package/src/components/CountUp.meta.ts +40 -0
  46. package/src/components/CountdownFlip.astro +244 -0
  47. package/src/components/CountdownFlip.meta.ts +39 -0
  48. package/src/components/CounterPreloader.astro +122 -0
  49. package/src/components/CounterPreloader.meta.ts +34 -0
  50. package/src/components/Cursor.astro +177 -0
  51. package/src/components/Cursor.meta.ts +37 -0
  52. package/src/components/CursorGrid.astro +113 -0
  53. package/src/components/CursorGrid.meta.ts +37 -0
  54. package/src/components/Curtain.astro +305 -0
  55. package/src/components/Curtain.meta.ts +38 -0
  56. package/src/components/Dialog.astro +156 -0
  57. package/src/components/Dialog.meta.ts +37 -0
  58. package/src/components/Dock.astro +193 -0
  59. package/src/components/Dock.meta.ts +35 -0
  60. package/src/components/DotPattern.astro +58 -0
  61. package/src/components/DotPattern.meta.ts +32 -0
  62. package/src/components/DottedMap.astro +153 -0
  63. package/src/components/DottedMap.meta.ts +38 -0
  64. package/src/components/Drawer.astro +196 -0
  65. package/src/components/Drawer.meta.ts +35 -0
  66. package/src/components/DynamicIsland.astro +122 -0
  67. package/src/components/DynamicIsland.meta.ts +35 -0
  68. package/src/components/FileTree.astro +207 -0
  69. package/src/components/FileTree.meta.ts +34 -0
  70. package/src/components/FilterGrid.astro +170 -0
  71. package/src/components/FilterGrid.meta.ts +38 -0
  72. package/src/components/FooterReveal.astro +54 -0
  73. package/src/components/FooterReveal.meta.ts +31 -0
  74. package/src/components/GlitchText.astro +145 -0
  75. package/src/components/GlitchText.meta.ts +39 -0
  76. package/src/components/Globe.astro +304 -0
  77. package/src/components/Globe.meta.ts +41 -0
  78. package/src/components/GlyphField.astro +363 -0
  79. package/src/components/GlyphField.meta.ts +53 -0
  80. package/src/components/GradientText.astro +63 -0
  81. package/src/components/GradientText.meta.ts +35 -0
  82. package/src/components/GridPattern.astro +55 -0
  83. package/src/components/GridPattern.meta.ts +32 -0
  84. package/src/components/HighlightMarker.astro +68 -0
  85. package/src/components/HighlightMarker.meta.ts +37 -0
  86. package/src/components/HorizontalScroll.astro +112 -0
  87. package/src/components/HorizontalScroll.meta.ts +34 -0
  88. package/src/components/HoverImageLinks.astro +219 -0
  89. package/src/components/HoverImageLinks.meta.ts +34 -0
  90. package/src/components/HoverVideo.astro +179 -0
  91. package/src/components/HoverVideo.meta.ts +37 -0
  92. package/src/components/ImageCompare.astro +212 -0
  93. package/src/components/ImageCompare.meta.ts +39 -0
  94. package/src/components/ImageTrail.astro +169 -0
  95. package/src/components/ImageTrail.meta.ts +36 -0
  96. package/src/components/Lightbox.astro +236 -0
  97. package/src/components/Lightbox.meta.ts +40 -0
  98. package/src/components/LinkPreview.astro +234 -0
  99. package/src/components/LinkPreview.meta.ts +39 -0
  100. package/src/components/LiquidGlass.astro +148 -0
  101. package/src/components/LiquidGlass.meta.ts +37 -0
  102. package/src/components/Magnetic.astro +92 -0
  103. package/src/components/Magnetic.meta.ts +31 -0
  104. package/src/components/Marquee.astro +165 -0
  105. package/src/components/Marquee.meta.ts +38 -0
  106. package/src/components/Meteors.astro +156 -0
  107. package/src/components/Meteors.meta.ts +36 -0
  108. package/src/components/Noise.astro +61 -0
  109. package/src/components/Noise.meta.ts +33 -0
  110. package/src/components/NumberFlow.astro +143 -0
  111. package/src/components/NumberFlow.meta.ts +42 -0
  112. package/src/components/Orb.astro +130 -0
  113. package/src/components/Orb.meta.ts +39 -0
  114. package/src/components/OrbitingCircles.astro +119 -0
  115. package/src/components/OrbitingCircles.meta.ts +36 -0
  116. package/src/components/OtpInput.astro +215 -0
  117. package/src/components/OtpInput.meta.ts +42 -0
  118. package/src/components/PageTransition.astro +121 -0
  119. package/src/components/PageTransition.meta.ts +33 -0
  120. package/src/components/Parallax.astro +69 -0
  121. package/src/components/Parallax.meta.ts +32 -0
  122. package/src/components/Particles.astro +210 -0
  123. package/src/components/Particles.meta.ts +37 -0
  124. package/src/components/PersistPlayer.astro +126 -0
  125. package/src/components/PersistPlayer.meta.ts +35 -0
  126. package/src/components/PhoneFrame.astro +161 -0
  127. package/src/components/PhoneFrame.meta.ts +35 -0
  128. package/src/components/PillNav.astro +170 -0
  129. package/src/components/PillNav.meta.ts +37 -0
  130. package/src/components/Popover.astro +95 -0
  131. package/src/components/Popover.meta.ts +34 -0
  132. package/src/components/PriceSwitch.astro +251 -0
  133. package/src/components/PriceSwitch.meta.ts +41 -0
  134. package/src/components/Probe.astro +84 -0
  135. package/src/components/Progress.astro +165 -0
  136. package/src/components/Progress.meta.ts +40 -0
  137. package/src/components/ProgressiveBlur.astro +95 -0
  138. package/src/components/ProgressiveBlur.meta.ts +36 -0
  139. package/src/components/PromptInput.astro +273 -0
  140. package/src/components/PromptInput.meta.ts +46 -0
  141. package/src/components/ReadingToc.astro +153 -0
  142. package/src/components/ReadingToc.meta.ts +34 -0
  143. package/src/components/RetroGrid.astro +84 -0
  144. package/src/components/RetroGrid.meta.ts +36 -0
  145. package/src/components/Reveal.astro +292 -0
  146. package/src/components/Reveal.meta.ts +48 -0
  147. package/src/components/Ripple.astro +94 -0
  148. package/src/components/Ripple.meta.ts +34 -0
  149. package/src/components/RotatingText.astro +170 -0
  150. package/src/components/RotatingText.meta.ts +35 -0
  151. package/src/components/ScrollExpand.astro +90 -0
  152. package/src/components/ScrollExpand.meta.ts +35 -0
  153. package/src/components/ScrollProgress.astro +73 -0
  154. package/src/components/ScrollProgress.meta.ts +32 -0
  155. package/src/components/ScrollScrub.astro +96 -0
  156. package/src/components/ScrollScrub.meta.ts +37 -0
  157. package/src/components/ShimmerButton.astro +109 -0
  158. package/src/components/ShimmerButton.meta.ts +37 -0
  159. package/src/components/ShinyText.astro +72 -0
  160. package/src/components/ShinyText.meta.ts +35 -0
  161. package/src/components/Skeleton.astro +76 -0
  162. package/src/components/Skeleton.meta.ts +32 -0
  163. package/src/components/Sparkline.astro +168 -0
  164. package/src/components/Sparkline.meta.ts +43 -0
  165. package/src/components/Spinner.astro +79 -0
  166. package/src/components/Spinner.meta.ts +33 -0
  167. package/src/components/SplitFlap.astro +157 -0
  168. package/src/components/SplitFlap.meta.ts +39 -0
  169. package/src/components/SplitText.astro +208 -0
  170. package/src/components/SplitText.meta.ts +50 -0
  171. package/src/components/Spotlight.astro +92 -0
  172. package/src/components/Spotlight.meta.ts +34 -0
  173. package/src/components/StaggerMenu.astro +301 -0
  174. package/src/components/StaggerMenu.meta.ts +39 -0
  175. package/src/components/StatefulButton.astro +148 -0
  176. package/src/components/StatefulButton.meta.ts +35 -0
  177. package/src/components/StickyBanner.astro +168 -0
  178. package/src/components/StickyBanner.meta.ts +45 -0
  179. package/src/components/StickyReveal.astro +256 -0
  180. package/src/components/StickyReveal.meta.ts +33 -0
  181. package/src/components/StrokeDraw.astro +110 -0
  182. package/src/components/StrokeDraw.meta.ts +43 -0
  183. package/src/components/Switch.astro +165 -0
  184. package/src/components/Switch.meta.ts +37 -0
  185. package/src/components/Tabs.astro +194 -0
  186. package/src/components/Tabs.meta.ts +47 -0
  187. package/src/components/TextRoll.astro +78 -0
  188. package/src/components/TextRoll.meta.ts +36 -0
  189. package/src/components/TextScramble.astro +107 -0
  190. package/src/components/TextScramble.meta.ts +35 -0
  191. package/src/components/ThemeScript.astro +17 -0
  192. package/src/components/ThemeToggle.astro +182 -0
  193. package/src/components/ThemeToggle.meta.ts +34 -0
  194. package/src/components/Tilt.astro +97 -0
  195. package/src/components/Tilt.meta.ts +34 -0
  196. package/src/components/Toast.astro +184 -0
  197. package/src/components/Toast.meta.ts +33 -0
  198. package/src/components/TracingBeam.astro +113 -0
  199. package/src/components/TracingBeam.meta.ts +34 -0
  200. package/src/components/Typewriter.astro +124 -0
  201. package/src/components/Typewriter.meta.ts +39 -0
  202. package/src/components/UnderlineHover.astro +61 -0
  203. package/src/components/UnderlineHover.meta.ts +34 -0
  204. package/src/components/VanishInput.astro +240 -0
  205. package/src/components/VanishInput.meta.ts +41 -0
  206. package/src/components/VideoDialog.astro +230 -0
  207. package/src/components/VideoDialog.meta.ts +40 -0
  208. package/src/components/WeightWave.astro +70 -0
  209. package/src/components/WeightWave.meta.ts +36 -0
  210. package/src/components/internal/DialogMorph.astro +97 -0
  211. package/src/components/internal/PromptInputSubmitOnEnter.astro +32 -0
  212. package/src/index.d.ts +43 -0
  213. package/src/index.js +60 -0
  214. package/src/lib/canvas.ts +124 -0
  215. package/src/lib/confetti.ts +127 -0
  216. package/src/lib/contrast.ts +116 -0
  217. package/src/lib/form.ts +231 -0
  218. package/src/lib/geo.ts +77 -0
  219. package/src/lib/gsap.ts +58 -0
  220. package/src/lib/land.ts +11 -0
  221. package/src/lib/lenis.ts +112 -0
  222. package/src/lib/meta.ts +79 -0
  223. package/src/lib/morph.ts +74 -0
  224. package/src/lib/reduced-motion-plugin.js +299 -0
  225. package/src/lib/runtime.ts +215 -0
  226. package/src/lib/support.ts +102 -0
  227. package/src/lib/theme.ts +18 -0
  228. package/src/lib/toast.ts +17 -0
  229. package/src/styles/base.css +218 -0
  230. package/src/styles/motion.css +43 -0
  231. package/src/styles/theme.css +397 -0
  232. package/src/styles/theme.tailwind.css +75 -0
@@ -0,0 +1,119 @@
1
+ ---
2
+ /**
3
+ * OrbitingCircles — the children orbit the centre of their parent and stay
4
+ * upright. One animation per item: `rotate(a) translate(r) rotate(−a)` from
5
+ * a to a + 360°, where a is the item's start angle from its index — so the
6
+ * icon turns around the centre without turning itself, on the compositor,
7
+ * with no wrapper. The start angles come from :nth-child, the count from
8
+ * :has(), so nothing is passed twice; `count` overrides when the children
9
+ * are not the whole ring. Two instances at two radii are two rings;
10
+ * `reverse` sends one the other way. Fills its positioned parent.
11
+ */
12
+ import type { HTMLAttributes } from 'astro/types';
13
+
14
+ interface Props extends HTMLAttributes<'div'> {
15
+ /** Orbit radius in px. */
16
+ radius?: number;
17
+ /** Seconds per revolution. */
18
+ duration?: number;
19
+ /** Run counter-clockwise. */
20
+ reverse?: boolean;
21
+ /** Draw the orbit line. */
22
+ path?: boolean;
23
+ /** Number of items on the ring; default counted from the children (up to 12). */
24
+ count?: number;
25
+ /** Item size in px (each child is a square cell of this size). */
26
+ size?: number;
27
+ /** Orbit line colour. */
28
+ color?: string;
29
+ }
30
+
31
+ const { radius = 100, duration = 20, reverse = false, path = true, count, size = 40, color = 'var(--ma-edge)', class: className, style, ...rest } = Astro.props;
32
+ const inline = [`--ma-orbit-r:${radius}px`, `--ma-orbit-dur:${duration}s`, `--ma-orbit-size:${size}px`, `--ma-orbit-color:${color}`, count ? `--ma-orbit-n:${count}` : '', typeof style === 'string' ? style : ''].filter(Boolean).join(';');
33
+ ---
34
+
35
+ <div class:list={['ma-orbit', className]} data-ma-orbit data-path={path ? '' : undefined} data-reverse={reverse ? '' : undefined} style={inline} {...rest}>
36
+ <slot />
37
+ </div>
38
+
39
+ <style is:global>
40
+ @layer components {
41
+ :where(.ma-orbit) {
42
+ position: absolute;
43
+ left: 50%;
44
+ top: 50%;
45
+ width: 0;
46
+ height: 0;
47
+ pointer-events: none;
48
+ }
49
+ :where(.ma-orbit[data-path])::before {
50
+ content: '';
51
+ position: absolute;
52
+ left: 0;
53
+ top: 0;
54
+ width: calc(var(--ma-orbit-r, 100px) * 2);
55
+ height: calc(var(--ma-orbit-r, 100px) * 2);
56
+ translate: -50% -50%;
57
+ border: 1px solid var(--ma-orbit-color, var(--ma-edge));
58
+ border-radius: 50%;
59
+ }
60
+ /* count from the children when not given — the ring with n children matches :has(> :nth-child(n):last-child) */
61
+ :where(.ma-orbit:has(> :nth-child(1):last-child)) { --ma-orbit-n: 1; }
62
+ :where(.ma-orbit:has(> :nth-child(2):last-child)) { --ma-orbit-n: 2; }
63
+ :where(.ma-orbit:has(> :nth-child(3):last-child)) { --ma-orbit-n: 3; }
64
+ :where(.ma-orbit:has(> :nth-child(4):last-child)) { --ma-orbit-n: 4; }
65
+ :where(.ma-orbit:has(> :nth-child(5):last-child)) { --ma-orbit-n: 5; }
66
+ :where(.ma-orbit:has(> :nth-child(6):last-child)) { --ma-orbit-n: 6; }
67
+ :where(.ma-orbit:has(> :nth-child(7):last-child)) { --ma-orbit-n: 7; }
68
+ :where(.ma-orbit:has(> :nth-child(8):last-child)) { --ma-orbit-n: 8; }
69
+ :where(.ma-orbit:has(> :nth-child(9):last-child)) { --ma-orbit-n: 9; }
70
+ :where(.ma-orbit:has(> :nth-child(10):last-child)) { --ma-orbit-n: 10; }
71
+ :where(.ma-orbit:has(> :nth-child(11):last-child)) { --ma-orbit-n: 11; }
72
+ :where(.ma-orbit:has(> :nth-child(12):last-child)) { --ma-orbit-n: 12; }
73
+ :where(.ma-orbit > :nth-child(1)) { --ma-orbit-i: 0; }
74
+ :where(.ma-orbit > :nth-child(2)) { --ma-orbit-i: 1; }
75
+ :where(.ma-orbit > :nth-child(3)) { --ma-orbit-i: 2; }
76
+ :where(.ma-orbit > :nth-child(4)) { --ma-orbit-i: 3; }
77
+ :where(.ma-orbit > :nth-child(5)) { --ma-orbit-i: 4; }
78
+ :where(.ma-orbit > :nth-child(6)) { --ma-orbit-i: 5; }
79
+ :where(.ma-orbit > :nth-child(7)) { --ma-orbit-i: 6; }
80
+ :where(.ma-orbit > :nth-child(8)) { --ma-orbit-i: 7; }
81
+ :where(.ma-orbit > :nth-child(9)) { --ma-orbit-i: 8; }
82
+ :where(.ma-orbit > :nth-child(10)) { --ma-orbit-i: 9; }
83
+ :where(.ma-orbit > :nth-child(11)) { --ma-orbit-i: 10; }
84
+ :where(.ma-orbit > :nth-child(12)) { --ma-orbit-i: 11; }
85
+ :where(.ma-orbit > *) {
86
+ position: absolute;
87
+ left: 0;
88
+ top: 0;
89
+ display: grid;
90
+ place-items: center;
91
+ width: var(--ma-orbit-size, 40px);
92
+ height: var(--ma-orbit-size, 40px);
93
+ margin: calc(var(--ma-orbit-size, 40px) / -2);
94
+ pointer-events: auto;
95
+ /* the start angle, from the index and the count */
96
+ --ma-orbit-a: calc(var(--ma-orbit-i, 0) * 360deg / var(--ma-orbit-n, 1));
97
+ transform: rotate(var(--ma-orbit-a)) translate(var(--ma-orbit-r, 100px)) rotate(calc(-1 * var(--ma-orbit-a)));
98
+ will-change: transform;
99
+ animation: ma-orbit var(--ma-orbit-dur) linear infinite;
100
+ }
101
+ :where(.ma-orbit[data-reverse] > *) {
102
+ animation-direction: reverse;
103
+ }
104
+ @media (prefers-reduced-motion: reduce) {
105
+ :where(.ma-orbit > *) {
106
+ animation: none;
107
+ }
108
+ }
109
+ }
110
+
111
+ @keyframes ma-orbit {
112
+ from {
113
+ transform: rotate(var(--ma-orbit-a)) translate(var(--ma-orbit-r, 100px)) rotate(calc(-1 * var(--ma-orbit-a)));
114
+ }
115
+ to {
116
+ transform: rotate(calc(var(--ma-orbit-a) + 360deg)) translate(var(--ma-orbit-r, 100px)) rotate(calc(-1 * var(--ma-orbit-a) - 360deg));
117
+ }
118
+ }
119
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'OrbitingCircles',
5
+ slug: 'orbiting-circles',
6
+ title: 'Orbiting Circles',
7
+ description: 'The children orbit the centre of their parent and stay upright. One transform animation per item (rotate, translate out, rotate back) from its start angle to a full turn, on the compositor, no wrapper. Start angles come from :nth-child, the count from :has(). Two instances at two radii are two rings; `reverse` sends one the other way. Zero JS.',
8
+ summary: 'Icons or other elements orbiting a centre point while staying upright, in one ring or several.',
9
+ category: 'background',
10
+ tier: 'A',
11
+ trigger: ['always'],
12
+ support: { baseline: 'widely', needs: ['has'], fallback: 'pass `count`; without :has() and without count the items stack at one angle' },
13
+ readout: '<OrbitingCircles radius={100} duration={20}>',
14
+ usage: `<div class="relative h-96">\n <OrbitingCircles radius={110} duration={24}>\n <Icon name="astro" />\n <Icon name="tailwind" />\n <Icon name="vite" />\n <Icon name="typescript" />\n </OrbitingCircles>\n <OrbitingCircles radius={60} duration={16} reverse size={32}>\n <Icon name="npm" />\n <Icon name="pnpm" />\n </OrbitingCircles>\n</div>`,
15
+ props: [
16
+ { name: 'radius', type: 'number', default: '100', description: 'Orbit radius in px.' },
17
+ { name: 'duration', type: 'number', default: '20', description: 'Seconds per revolution.' },
18
+ { name: 'reverse', type: 'boolean', default: 'false', description: 'Run counter-clockwise.' },
19
+ { name: 'path', type: 'boolean', default: 'true', description: 'Draw the orbit line.' },
20
+ { name: 'count', type: 'number', default: 'the number of children', description: 'Items on the ring, when the children are not the whole ring (or for a browser without :has()).' },
21
+ { name: 'size', type: 'number', default: '40', description: 'Item cell in px; each child is centred in a square of this size.' },
22
+ { name: 'color', type: 'string', default: 'var(--ma-edge)', description: 'Orbit line colour.' },
23
+ ],
24
+ reducedMotion: 'The items rest at their start angles on the ring.',
25
+ clientRouter: 'CSS-only; nothing to rebind.',
26
+ craft: [
27
+ 'rotate(a) translate(r) rotate(−a), animated from a to a + 360° on both rotations: the item circles the centre and never turns itself. One animation per item, no counter-rotating wrapper, no JavaScript.',
28
+ 'The start angle is the index over the count; the index is :nth-child, the count is :has(> :nth-child(n):last-child), so a ring of any size up to twelve needs no props at all.',
29
+ 'The whole transform is one property on the compositor; the cell is a fixed square so the icon is centred by layout, not by measurement.',
30
+ 'Linear timing, because it orbits forever.',
31
+ ],
32
+ related: ['animated-beam', 'ripple', 'circular-text'],
33
+ intents: ['orbiting icons', 'icons rotating around logo', 'orbit animation css', 'planets orbit integration logos', 'circular orbit icons astro'],
34
+ replaces: ['OrbitingCircles (Magic UI)', 'Orbit (Cult UI)'],
35
+ fixture: { expect: 'class="ma-orbit"' },
36
+ } satisfies ComponentMeta;
@@ -0,0 +1,215 @@
1
+ ---
2
+ /**
3
+ * OtpInput — a one-time code in cells, zero JS. **One** native input, not
4
+ * one per digit: paste, SMS autofill (`autocomplete="one-time-code"`),
5
+ * screen readers and Backspace all work only in the single-input pattern.
6
+ * The cells are server-rendered boxes under the input; a monospace font with
7
+ * tabular figures, a letter-spacing of one cell minus one character and a
8
+ * matching start padding put every glyph in the middle of its cell, and the
9
+ * caret lands at the front of the next one. The wrapper clips the trailing
10
+ * spacing so no seventh cell appears; the focus ring is drawn on the wrapper
11
+ * through :focus-within so it is never clipped. Full and valid → the check
12
+ * draws itself; `:user-invalid` tints the cells only after the reader has
13
+ * been there. Every attribute you pass goes to the input. The field is
14
+ * left to right on any page, since the glyphs have to meet their cells. A
15
+ * code pasted with spaces or dashes ("123 456") is cut at `length`
16
+ * characters: stripping them needs a script, and this component has none.
17
+ */
18
+ import type { HTMLAttributes } from 'astro/types';
19
+
20
+ interface Props extends Omit<HTMLAttributes<'input'>, 'type' | 'size' | 'maxlength' | 'pattern' | 'inputmode'> {
21
+ /** Number of characters. */
22
+ length?: number;
23
+ /** Digits only (numeric keyboard, \d pattern) or letters and digits. */
24
+ charset?: 'numeric' | 'alphanumeric';
25
+ name?: string;
26
+ /** Visible label, rendered as <label for>. Without it, pass aria-label. */
27
+ label?: string;
28
+ /** Placeholder glyph per cell; the attribute is what :placeholder-shown reads, so it is always set. */
29
+ placeholder?: string;
30
+ /** Cell width, any CSS length; the height and the font follow. */
31
+ cell?: string;
32
+ /** Gap between cells. */
33
+ gap?: string;
34
+ /** The input's id and the label's for. Unset, unique per render. */
35
+ id?: string;
36
+ }
37
+
38
+ const { length = 6, charset = 'numeric', name = 'code', label, placeholder, cell = '2.5rem', gap = '0.5rem', id, class: className, style, ...rest } = Astro.props;
39
+ const n = Math.max(1, Math.min(12, Math.round(length)));
40
+ // unique per render, not per name: a sign-in form and a dialog can both carry a field named "code" (the VanishInput rule)
41
+ const inputId = id ?? `ma-otp-${name}-${Math.random().toString(36).slice(2, 7)}`;
42
+ const pattern = charset === 'numeric' ? `\\d{${n}}` : `[A-Za-z0-9]{${n}}`;
43
+ const vars = [`--ma-otp-n:${n}`, `--ma-otp-cell:${cell}`, `--ma-otp-gap:${gap}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
44
+ const ph = placeholder ?? '•'.repeat(n);
45
+ ---
46
+
47
+ {label && <label class="ma-otp__label" for={inputId}>{label}</label>}
48
+ <span class:list={['ma-otp', className]} style={vars}>
49
+ <span class="ma-otp__field">
50
+ <span class="ma-otp__cells" aria-hidden="true">{Array.from({ length: n }).map(() => <i></i>)}</span>
51
+ <input
52
+ id={inputId}
53
+ class="ma-otp__input"
54
+ type="text"
55
+ name={name}
56
+ inputmode={charset === 'numeric' ? 'numeric' : 'text'}
57
+ autocomplete="one-time-code"
58
+ autocapitalize="off"
59
+ autocorrect="off"
60
+ spellcheck="false"
61
+ maxlength={n}
62
+ pattern={pattern}
63
+ placeholder={ph}
64
+ {...rest}
65
+ />
66
+ </span>
67
+ <svg class="ma-otp__check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7" pathLength="1" /></svg>
68
+ </span>
69
+
70
+ <style is:global>
71
+ @layer components {
72
+ :where(.ma-otp) {
73
+ --ma-otp-font: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
74
+ --ma-otp-h: calc(var(--ma-otp-cell) * 1.2);
75
+ position: relative;
76
+ display: inline-block;
77
+ vertical-align: middle;
78
+ }
79
+ :where(.ma-otp__field) {
80
+ /* the cells and the glyphs meet only left to right: on a right-to-left page the text would start at the far end,
81
+ one letter-spacing off every cell. A code is read in Latin order there too */
82
+ direction: ltr;
83
+ display: inline-grid;
84
+ /* the input's trailing letter-spacing would add a phantom cell: the box is exactly n cells and n − 1 gaps */
85
+ inline-size: calc(var(--ma-otp-n) * var(--ma-otp-cell) + (var(--ma-otp-n) - 1) * var(--ma-otp-gap));
86
+ block-size: var(--ma-otp-h);
87
+ border-radius: 0.5em;
88
+ overflow: clip;
89
+ vertical-align: middle;
90
+ }
91
+ :where(.ma-otp__label) {
92
+ display: block;
93
+ margin-block-end: 0.5em;
94
+ font-size: 0.875em;
95
+ }
96
+ :where(.ma-otp__cells) {
97
+ grid-area: 1 / 1;
98
+ display: flex;
99
+ gap: var(--ma-otp-gap);
100
+ pointer-events: none;
101
+ }
102
+ :where(.ma-otp__cells > i) {
103
+ flex: none;
104
+ inline-size: var(--ma-otp-cell);
105
+ block-size: 100%;
106
+ border: 1px solid var(--ma-edge);
107
+ border-radius: 0.45em;
108
+ background: var(--ma-panel);
109
+ transition:
110
+ border-color var(--ma-duration-fast) var(--ma-ease-out),
111
+ background-color var(--ma-duration-fast) var(--ma-ease-out);
112
+ }
113
+ :where(.ma-otp__input) {
114
+ /* the field: transparent over the cells; every glyph sits in the middle of its cell through spacing alone. One cell
115
+ wider than the box, which clips it: with the last character typed, the caret sits past the box's end, and an input
116
+ exactly as wide scrolled to show it, which moved every digit off its cell and the first out of sight */
117
+ grid-area: 1 / 1;
118
+ inline-size: calc(100% + var(--ma-otp-cell));
119
+ block-size: 100%;
120
+ margin: 0;
121
+ padding: 0 0 0 calc((var(--ma-otp-cell) - 1ch) / 2);
122
+ border: 0;
123
+ background: transparent;
124
+ color: inherit;
125
+ font: 500 calc(var(--ma-otp-cell) * 0.5) / 1 var(--ma-otp-font);
126
+ font-variant-numeric: tabular-nums;
127
+ letter-spacing: calc(var(--ma-otp-cell) + var(--ma-otp-gap) - 1ch);
128
+ text-transform: uppercase;
129
+ caret-color: var(--ma-ink);
130
+ outline: none;
131
+ appearance: none;
132
+ -webkit-appearance: none;
133
+ }
134
+ :where(.ma-otp__input)::placeholder {
135
+ color: color-mix(in srgb, var(--ma-ink) 28%, transparent);
136
+ opacity: 1;
137
+ }
138
+ :where(.ma-otp__input:disabled) {
139
+ cursor: not-allowed;
140
+ opacity: 0.6;
141
+ }
142
+ :where(.ma-otp__input:read-only) {
143
+ caret-color: transparent;
144
+ }
145
+ /* focus: on the field's box, outside the clip; one cell at a time is not known without a script */
146
+ :where(.ma-otp__field:focus-within) {
147
+ outline: 2px solid var(--ma-ink);
148
+ outline-offset: 3px;
149
+ }
150
+ :where(.ma-otp:has(.ma-otp__input:focus) .ma-otp__cells > i) {
151
+ border-color: color-mix(in srgb, var(--ma-ink) 45%, transparent);
152
+ }
153
+ /* full and valid: the cells go ink-edged and the check draws */
154
+ :where(.ma-otp:has(.ma-otp__input:valid:not(:placeholder-shown)) .ma-otp__cells > i) {
155
+ border-color: var(--ma-ink);
156
+ }
157
+ /* the red is a hook: on a coloured ground a caller points --ma-otp-invalid at a red that keeps 3 : 1 there */
158
+ :where(.ma-otp:has(.ma-otp__input:user-invalid) .ma-otp__cells > i) {
159
+ border-color: var(--ma-otp-invalid, #e0563f);
160
+ background: color-mix(in srgb, var(--ma-otp-invalid, #e0563f) 8%, var(--ma-panel));
161
+ }
162
+ :where(.ma-otp:has(.ma-otp__input:disabled) .ma-otp__cells > i),
163
+ :where(.ma-otp:has(.ma-otp__input:disabled) .ma-otp__cells > i) {
164
+ transition: none;
165
+ background: color-mix(in srgb, var(--ma-ink) 6%, var(--ma-panel));
166
+ }
167
+ /* the check: beside the field, never over the last digit */
168
+ :where(.ma-otp__check) {
169
+ position: absolute;
170
+ inset-block-start: 50%;
171
+ inset-inline-start: 100%;
172
+ inline-size: calc(var(--ma-otp-cell) * 0.55);
173
+ block-size: calc(var(--ma-otp-cell) * 0.55);
174
+ margin-inline-start: calc(var(--ma-otp-gap) * 1.2);
175
+ translate: 0 -50%;
176
+ fill: none;
177
+ stroke: var(--ma-ink);
178
+ stroke-width: 2.5;
179
+ stroke-linecap: round;
180
+ stroke-linejoin: round;
181
+ pointer-events: none;
182
+ opacity: 0;
183
+ }
184
+ :where(.ma-otp__check path) {
185
+ stroke-dasharray: 1;
186
+ stroke-dashoffset: 1;
187
+ transition: stroke-dashoffset var(--ma-duration-fast) var(--ma-ease-in);
188
+ }
189
+ :where(.ma-otp:has(.ma-otp__input:valid:not(:placeholder-shown)) .ma-otp__check) {
190
+ opacity: 1;
191
+ }
192
+ :where(.ma-otp:has(.ma-otp__input:valid:not(:placeholder-shown)) .ma-otp__check path) {
193
+ stroke-dashoffset: 0;
194
+ transition: stroke-dashoffset var(--ma-duration) var(--ma-ease);
195
+ }
196
+ @media (forced-colors: active) {
197
+ :where(.ma-otp__cells > i) {
198
+ border-color: ButtonText;
199
+ background: Canvas;
200
+ }
201
+ :where(.ma-otp:has(.ma-otp__input:valid:not(:placeholder-shown)) .ma-otp__cells > i) {
202
+ border-color: Highlight;
203
+ }
204
+ :where(.ma-otp__check) {
205
+ stroke: Highlight;
206
+ }
207
+ }
208
+ @media (prefers-reduced-motion: reduce) {
209
+ :where(.ma-otp__cells > i),
210
+ :where(.ma-otp__check path) {
211
+ transition: none;
212
+ }
213
+ }
214
+ }
215
+ </style>
@@ -0,0 +1,42 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'OtpInput',
5
+ slug: 'otp-input',
6
+ title: 'OTP Input',
7
+ description: 'A one-time code in cells, from one native input. Paste, SMS autofill, screen readers and Backspace only work in the single-input pattern, so the cells are drawn under the field and a monospace font with a computed letter-spacing puts each glyph in the middle of its cell. Full and valid, the check draws itself; :user-invalid tints the cells only after the reader has been there. Zero JavaScript.',
8
+ summary: 'A one-time code input drawn as cells over one native field, so paste, SMS autofill and screen readers work.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'widely', needs: ['has', 'user-invalid'], fallback: 'without :has() the cells keep their resting border and the check never shows; the input itself is unchanged' },
13
+ readout: '<OtpInput length={6} name="code" required>',
14
+ usage: `<form method="post" action="/verify">\n <OtpInput length={6} name="code" label="Enter the code we sent" required />\n <button type="submit">Verify</button>\n</form>\n\n<!-- letters and digits, four cells -->\n<OtpInput length={4} charset="alphanumeric" name="invite" aria-label="Invite code" />`,
15
+ props: [
16
+ { name: 'length', type: 'number', default: '6', description: 'Number of cells (1–12); maxlength and the pattern follow.' },
17
+ { name: 'charset', type: "'numeric' | 'alphanumeric'", default: "'numeric'", description: 'Digits only (numeric keyboard on phones, \\d pattern), or letters and digits.' },
18
+ { name: 'name', type: 'string', default: "'code'", description: 'Form field name; the whole code is one value.' },
19
+ { name: 'label', type: 'string', description: 'Visible label as <label for>. Without it, pass aria-label.' },
20
+ { name: 'placeholder', type: 'string', default: "'•' × length", description: 'One glyph per cell. Always set: :placeholder-shown is what tells an empty field from a full one.' },
21
+ { name: 'cell', type: 'string', default: "'2.5rem'", description: 'Cell width; the height (× 1.2) and the font size (× 0.5) follow.' },
22
+ { name: 'gap', type: 'string', default: "'0.5rem'", description: 'Space between cells.' },
23
+ { name: 'id', type: 'string', default: 'unique per render', description: 'The input\'s id, and the label\'s for. Unset, ma-otp- with the name and a random suffix, so two fields of the same name on one page never share an id. Yours wins.' },
24
+ { name: '--ma-otp-invalid', type: 'CSS colour (custom property)', default: '#e0563f', description: 'The border and tint of the cells once the reader has left the field invalid (:user-invalid). Set it in a rule or style on the component or any ancestor that knows its ground: a red that keeps 3 : 1 against it (WCAG 1.4.11).' },
25
+ ],
26
+ reducedMotion: 'The cells change border at once and the check appears without drawing.',
27
+ clientRouter: 'A native input; nothing to bind. The browser keeps a typed value through a navigation only where it keeps any form value.',
28
+ craft: [
29
+ 'One input, not six. Six inputs need a script to move focus, split a paste, accept an SMS autofill and make Backspace step back, and they still confuse a screen reader. One field with autocomplete="one-time-code" gets all of that from the browser.',
30
+ 'The cells are rendered boxes under a transparent field. In a monospace font every glyph advances 1ch, so a letter-spacing of cell + gap − 1ch and a start padding of (cell − 1ch) / 2 centre each character in its cell, and the caret sits at the front of the next one. Nothing is measured.',
31
+ 'The spacing after the last glyph would draw a seventh cell: the field is exactly n cells wide and clips it, the focus ring is drawn on the field\'s box through :focus-within so the clip never cuts it, and the check sits beside the field, never over the last digit.',
32
+ 'Full and valid is :valid:not(:placeholder-shown). That is why the placeholder attribute is always present: without it an empty, non-required field is :valid and the check would be lit from the start.',
33
+ 'The error tint uses :user-invalid, so the cells are never red before the reader has typed or submitted. required and pattern do the validating; the browser shows the message.',
34
+ 'The active-cell highlight is not built: it needs the value length, which is a script. Listed as a proposal.',
35
+ 'A code pasted with spaces or dashes ("123 456", "123-456") is cut at length characters by maxlength, and the pattern then refuses it: removing separators needs a script. SMS autofill and a copied code without separators fill the field whole.',
36
+ 'The field is direction: ltr on every page, so on a right-to-left page the glyphs still start in the first cell; the label and the check follow the page\'s direction.',
37
+ ],
38
+ related: ['check-draw', 'stateful-button', 'number-flow'],
39
+ intents: ['otp input', 'one time code input', 'verification code boxes', 'pin input', 'sms code input', '6 digit code input css'],
40
+ replaces: ['InputOTP (shadcn)', 'OTPInput (Motion Primitives)', 'react-otp-input'],
41
+ fixture: { props: { name: 'ma-fixture', label: 'Fixture content for OtpInput' }, expect: 'Fixture content for OtpInput' },
42
+ } satisfies ComponentMeta;
@@ -0,0 +1,121 @@
1
+ ---
2
+ /**
3
+ * PageTransition — the page-level motion Astro's <ClientRouter /> makes
4
+ * possible and nothing else on the web can do this cheaply: wrap a region,
5
+ * give it a name, and it animates between pages. Zero JS of its own; the
6
+ * router is Astro's. Presets are keyframes in this file, resolved through
7
+ * the active preset's curve and duration, so page motion matches the rest.
8
+ */
9
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
10
+ import type { TransitionAnimationValue, TransitionDirectionalAnimations } from 'astro';
11
+ import { fade, slide } from 'astro:transitions';
12
+
13
+ type Preset = 'fade' | 'slide' | 'rise' | 'zoom' | 'wipe' | 'scope' | 'morph' | 'none';
14
+
15
+ interface Props extends HTMLAttributes<'div'> {
16
+ /** transition:name — the same name on both pages pairs the elements. */
17
+ name?: string;
18
+ /** How the region enters and leaves. `morph` is the shared-element morph the browser draws itself. */
19
+ preset?: Preset;
20
+ /** Element to render. */
21
+ as?: HTMLTag;
22
+ }
23
+
24
+ const { name, preset = 'rise', as: Tag = 'div', class: className, ...rest } = Astro.props;
25
+
26
+ const own = (kind: 'rise' | 'zoom' | 'wipe' | 'scope'): TransitionDirectionalAnimations => {
27
+ // scope: the old page stays in view, sinking and dimming for the whole entrance, so its exit runs on the entrance duration
28
+ const out = { name: `ma-vt-${kind}-out`, duration: kind === 'scope' ? 'var(--ma-duration)' : 'var(--ma-duration-fast)', easing: 'var(--ma-ease-out)', fillMode: 'both' as const };
29
+ const inn = { name: `ma-vt-${kind}-in`, duration: 'var(--ma-duration)', easing: 'var(--ma-ease)', fillMode: 'both' as const };
30
+ return { forwards: { old: out, new: inn }, backwards: { old: { ...out, name: `ma-vt-${kind}-in`, direction: 'reverse' as const }, new: { ...inn, name: `ma-vt-${kind}-out`, direction: 'reverse' as const } } };
31
+ };
32
+
33
+ const animate: TransitionAnimationValue =
34
+ preset === 'fade' ? fade({ duration: 'var(--ma-duration)' }) : preset === 'slide' ? slide({ duration: 'var(--ma-duration)' }) : preset === 'morph' ? 'initial' : preset === 'none' ? 'none' : own(preset);
35
+ ---
36
+
37
+ {
38
+ name ? (
39
+ <Tag class:list={['ma-page', className]} transition:name={name} transition:animate={animate} {...rest}>
40
+ <slot />
41
+ </Tag>
42
+ ) : (
43
+ <Tag class:list={['ma-page', className]} transition:animate={animate} {...rest}>
44
+ <slot />
45
+ </Tag>
46
+ )
47
+ }
48
+
49
+ <style is:global>
50
+ @layer components {
51
+ :where(.ma-page) {
52
+ display: block;
53
+ }
54
+ @media (prefers-reduced-motion: reduce) {
55
+ /* Astro's router already drops view-transition animations under reduced motion. */
56
+ :where(.ma-page) {
57
+ animation: none;
58
+ }
59
+ }
60
+ }
61
+
62
+ @keyframes ma-vt-rise-in {
63
+ from {
64
+ opacity: 0;
65
+ translate: 0 var(--ma-travel-section, 48px);
66
+ }
67
+ }
68
+ @keyframes ma-vt-rise-out {
69
+ to {
70
+ opacity: 0;
71
+ translate: 0 calc(var(--ma-travel-section, 48px) * -0.5);
72
+ }
73
+ }
74
+ @keyframes ma-vt-zoom-in {
75
+ from {
76
+ opacity: 0;
77
+ scale: 0.96;
78
+ }
79
+ }
80
+ @keyframes ma-vt-zoom-out {
81
+ to {
82
+ opacity: 0;
83
+ scale: 1.03;
84
+ }
85
+ }
86
+ @keyframes ma-vt-wipe-in {
87
+ from {
88
+ clip-path: inset(0 100% 0 0);
89
+ }
90
+ to {
91
+ clip-path: inset(0 0 0 0);
92
+ }
93
+ }
94
+ @keyframes ma-vt-wipe-out {
95
+ from {
96
+ clip-path: inset(0 0 0 0);
97
+ }
98
+ to {
99
+ clip-path: inset(0 0 0 100%);
100
+ }
101
+ }
102
+ /* scope: the old page sinks and dims behind, the new one rises as a sheet in front */
103
+ @keyframes ma-vt-scope-in {
104
+ from {
105
+ translate: 0 100%;
106
+ }
107
+ to {
108
+ translate: 0 0;
109
+ }
110
+ }
111
+ @keyframes ma-vt-scope-out {
112
+ from {
113
+ scale: 1;
114
+ opacity: 1;
115
+ }
116
+ to {
117
+ scale: 0.92;
118
+ opacity: 0.35;
119
+ }
120
+ }
121
+ </style>
@@ -0,0 +1,33 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'PageTransition',
5
+ slug: 'page-transition',
6
+ title: 'Page Transition',
7
+ description: 'Animates a named region between pages: rise, zoom, wipe, scope, fade, slide, or the browser\'s own shared-element morph. Zero JavaScript of its own: Astro\'s <ClientRouter /> drives view transitions, and the presets are keyframes on the active motion preset\'s curve.',
8
+ summary: "Page transition animations for Astro's router: rise, zoom, wipe, fade, slide, or the browser's shared-element morph.",
9
+ category: 'transition',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'newly', needs: ['view-transitions'], fallback: 'an instant swap between pages' },
13
+ readout: '<PageTransition name="hero" preset="rise">',
14
+ usage: `---\n// both pages, same name\n---\n<PageTransition name="hero" preset="rise">\n <h1>Page one</h1>\n</PageTransition>\n\n<PageTransition name="cover" preset="morph">\n <img src="/cover.jpg" alt="" />\n</PageTransition>\n\n<PageTransition name="main" preset="scope">\n <main>…</main>\n</PageTransition>`,
15
+ props: [
16
+ { name: 'name', type: 'string', description: 'transition:name. The same name on two pages pairs the regions; omit it to animate a region on its own.' },
17
+ { name: 'preset', type: "'rise' | 'zoom' | 'wipe' | 'scope' | 'fade' | 'slide' | 'morph' | 'none'", default: "'rise'", description: 'Entrance and exit. scope sinks and dims the old page behind while the new one rises in front, like a sheet; morph lets the browser interpolate size and position between the two pages\' elements.' },
18
+ { name: 'as', type: 'HTMLTag', default: "'div'", description: 'Element to render.' },
19
+ ],
20
+ reducedMotion: 'Astro\'s router skips view-transition animations; the swap is instant.',
21
+ clientRouter: 'This is the ClientRouter. Without it the wrapper is inert and pages load normally.',
22
+ craft: [
23
+ 'Exit at --ma-duration-fast on ease-out, entrance at --ma-duration on the preset spring: the old page gets out of the way before the new one arrives.',
24
+ 'Backwards navigation plays the same keyframes reversed, so back feels like undo.',
25
+ 'rise travels --ma-travel-section (48 px): page-level motion needs more distance than component-level motion to register.',
26
+ 'morph is a shared-element transition: the browser interpolates the paired region\'s size and position between the two pages, so no script measures layout.',
27
+ 'scope keeps the old page in view for the whole entrance (it sinks to 92% and dims to a third behind the arriving sheet), so the exit runs on the entrance duration instead of the fast one; that is the depth cue. Backwards, the sheet drops and the page behind comes back up.',
28
+ ],
29
+ related: ['reveal', 'split-text'],
30
+ intents: ['page transition', 'view transitions astro', 'animate between pages', 'shared element transition', 'transition:name preset', 'route transition'],
31
+ replaces: ['framer-motion AnimatePresence page transitions', 'Barba.js', 'Swup'],
32
+ fixture: { expect: 'Fixture content for PageTransition' },
33
+ } satisfies ComponentMeta;
@@ -0,0 +1,69 @@
1
+ ---
2
+ /**
3
+ * Parallax — an element drifts at its own speed as it crosses the viewport.
4
+ *
5
+ * animation-timeline: view() binds a translate to the element's own passage
6
+ * through the scrollport, so the drift is exact, never lags, and costs no
7
+ * scroll listener. Browsers without scroll timelines show it in place.
8
+ */
9
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
10
+
11
+ interface Props extends HTMLAttributes<'div'> {
12
+ /** Element to render. */
13
+ as?: HTMLTag;
14
+ /** Drift as a fraction of the viewport, −1…1. Negative moves against the scroll. */
15
+ speed?: number;
16
+ /** Axis of travel. */
17
+ axis?: 'y' | 'x';
18
+ }
19
+
20
+ const { as: Tag = 'div', speed = 0.3, axis = 'y', class: className, style, ...rest } = Astro.props;
21
+ const inline = [`--ma-px-speed:${speed}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
22
+ ---
23
+
24
+ <Tag class:list={['ma-parallax', className]} data-axis={axis === 'x' ? 'x' : undefined} style={inline} {...rest}>
25
+ <slot />
26
+ </Tag>
27
+
28
+ <style is:global>
29
+ @layer components {
30
+ @supports (animation-timeline: view()) {
31
+ :where(.ma-parallax) {
32
+ will-change: translate;
33
+ animation: ma-parallax-y linear both;
34
+ }
35
+ :where(.ma-parallax[data-axis='x']) {
36
+ animation-name: ma-parallax-x;
37
+ }
38
+ /* Separate rule on purpose: lightningcss folds animation-timeline into
39
+ the shorthand, which browsers reject. Distinct selector text keeps them apart. */
40
+ :where(html) :where(.ma-parallax) {
41
+ animation-timeline: view();
42
+ animation-range: cover 0% cover 100%;
43
+ }
44
+ }
45
+ @media (prefers-reduced-motion: reduce) {
46
+ :where(.ma-parallax) {
47
+ animation: none;
48
+ will-change: auto;
49
+ }
50
+ }
51
+ }
52
+
53
+ @keyframes ma-parallax-y {
54
+ from {
55
+ translate: 0 calc(var(--ma-px-speed, 0.3) * 50vh);
56
+ }
57
+ to {
58
+ translate: 0 calc(var(--ma-px-speed, 0.3) * -50vh);
59
+ }
60
+ }
61
+ @keyframes ma-parallax-x {
62
+ from {
63
+ translate: calc(var(--ma-px-speed, 0.3) * 50vw) 0;
64
+ }
65
+ to {
66
+ translate: calc(var(--ma-px-speed, 0.3) * -50vw) 0;
67
+ }
68
+ }
69
+ </style>