@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,305 @@
1
+ ---
2
+ /**
3
+ * Curtain — a full-viewport wipe between two pages. Where PageTransition
4
+ * animates a region, Curtain owns the root: it animates the browser's own
5
+ * ::view-transition-old(root) / -new(root) with a clip-path (iris, doors,
6
+ * shutter, diagonal) or a mask (blinds, stagger, pixels), so the new page
7
+ * comes in through a shape while the old one stays put beneath it. Back
8
+ * navigation plays the shape in reverse on the leaving page. `title` is a
9
+ * real panel with a view-transition-name that carries the next page's title
10
+ * across the swap, and only across a swap: a same-document transition the
11
+ * library runs (html[data-ma-vt]) leaves it out. One element in the layout;
12
+ * the mode is a prop and lives in the DOM, so :has() on the root selects the
13
+ * rules — nothing global.
14
+ * The only script is the iris origin: the last pointer-down, written on
15
+ * <html> and rewritten after every swap.
16
+ */
17
+ import type { HTMLAttributes } from 'astro/types';
18
+
19
+ type Mode = 'iris' | 'blinds' | 'shutter' | 'doors' | 'pixels' | 'stagger' | 'diagonal' | 'title';
20
+
21
+ interface Props extends HTMLAttributes<'div'> {
22
+ /** The shape. */
23
+ mode?: Mode;
24
+ /** The page's title, for `title` mode: shown on the panel while it covers the swap. */
25
+ title?: string;
26
+ /** Duration in ms. Unset: the preset's --ma-dur-gentle (title: --ma-dur-ambient). */
27
+ duration?: number;
28
+ /** Panel colours for `title` mode. */
29
+ color?: string;
30
+ textColor?: string;
31
+ }
32
+
33
+ const { mode = 'iris', title, duration, color, textColor, class: className, style, ...rest } = Astro.props;
34
+ const vars = [color && `--ma-curtain-color:${color}`, textColor && `--ma-curtain-text:${textColor}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
35
+ ---
36
+
37
+ <div class:list={['ma-curtain', className]} data-mode={mode} data-duration={duration} aria-hidden="true" style={vars || undefined} {...rest}>
38
+ {title && <div class="ma-curtain__panel">{title}</div>}
39
+ </div>
40
+
41
+ <style is:global>
42
+ @property --ma-curtain-f {
43
+ syntax: '<percentage>';
44
+ inherits: false;
45
+ initial-value: 0%;
46
+ }
47
+ @property --ma-curtain-px {
48
+ syntax: '<length>';
49
+ inherits: false;
50
+ initial-value: 0px;
51
+ }
52
+
53
+ @layer components {
54
+ :where(.ma-curtain) {
55
+ display: none;
56
+ }
57
+ /* title: the panel lives on the page, fully clipped, so its snapshot exists for the transition */
58
+ :where(.ma-curtain[data-mode='title']) {
59
+ display: block;
60
+ position: fixed;
61
+ inset: 0;
62
+ z-index: 2147483000;
63
+ pointer-events: none;
64
+ clip-path: inset(0 0 100% 0);
65
+ }
66
+ /* named for page swaps only: inline, the name also put the full-screen panel into every same-document transition the
67
+ library runs (a morphing dialog, FilterGrid), and the title swept across the page */
68
+ :where(:root:not([data-ma-vt]) .ma-curtain__panel) {
69
+ view-transition-name: ma-curtain-title;
70
+ }
71
+ :where(.ma-curtain__panel) {
72
+ position: absolute;
73
+ inset: 0;
74
+ display: grid;
75
+ place-items: center;
76
+ padding: 6vmin;
77
+ /* the panel is the page's own surface, laid over Canvas so a translucent token never lets the old page through */
78
+ background: linear-gradient(var(--ma-curtain-color, var(--ma-panel)), var(--ma-curtain-color, var(--ma-panel))), Canvas;
79
+ color: var(--ma-curtain-text, var(--ma-ink));
80
+ font-size: clamp(2rem, 6vw, 5rem);
81
+ font-weight: 700;
82
+ letter-spacing: -0.03em;
83
+ line-height: 1.05;
84
+ text-align: center;
85
+ text-wrap: balance;
86
+ }
87
+
88
+ /* The shape layer: the new page on the way forward, the old page on the way back. The other one holds still. */
89
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain:not([data-mode='title'])))::view-transition-old(root),
90
+ :where(:root[data-astro-transition='back']:has(.ma-curtain:not([data-mode='title'])))::view-transition-new(root) {
91
+ animation: none;
92
+ mix-blend-mode: normal;
93
+ }
94
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain:not([data-mode='title'])))::view-transition-new(root),
95
+ :where(:root[data-astro-transition='back']:has(.ma-curtain:not([data-mode='title'])))::view-transition-old(root) {
96
+ animation-duration: var(--ma-curtain-dur, var(--ma-dur-gentle));
97
+ animation-timing-function: var(--ma-ease-out);
98
+ animation-fill-mode: both;
99
+ mix-blend-mode: normal;
100
+ }
101
+ :where(:root[data-astro-transition='back']:has(.ma-curtain:not([data-mode='title'])))::view-transition-old(root) {
102
+ z-index: 1;
103
+ animation-direction: reverse;
104
+ animation-timing-function: var(--ma-ease-in);
105
+ }
106
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='iris']))::view-transition-new(root),
107
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='iris']))::view-transition-old(root) {
108
+ animation-name: ma-curtain-iris;
109
+ }
110
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='doors']))::view-transition-new(root),
111
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='doors']))::view-transition-old(root) {
112
+ animation-name: ma-curtain-doors;
113
+ }
114
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='shutter']))::view-transition-new(root),
115
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='shutter']))::view-transition-old(root) {
116
+ animation-name: ma-curtain-shutter;
117
+ }
118
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='diagonal']))::view-transition-new(root),
119
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='diagonal']))::view-transition-old(root) {
120
+ animation-name: ma-curtain-diagonal;
121
+ }
122
+ /* blinds: eight slats whose filled part grows; the gradient reads the animated registered property */
123
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='blinds']))::view-transition-new(root),
124
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='blinds']))::view-transition-old(root) {
125
+ animation-name: ma-curtain-blinds;
126
+ mask-image: repeating-linear-gradient(#000 0 var(--ma-curtain-f), transparent 0 12.5%);
127
+ }
128
+ /* stagger: eight columns, one after another */
129
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='stagger']))::view-transition-new(root),
130
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='stagger']))::view-transition-old(root) {
131
+ animation-name: ma-curtain-stagger;
132
+ animation-timing-function: steps(8, end);
133
+ mask-image: linear-gradient(#000 0 0);
134
+ mask-repeat: no-repeat;
135
+ }
136
+ /* pixels: a grid of squares growing in six steps — two gradients intersected, one per axis */
137
+ :where(:root:not([data-astro-transition='back']):has(.ma-curtain[data-mode='pixels']))::view-transition-new(root),
138
+ :where(:root[data-astro-transition='back']:has(.ma-curtain[data-mode='pixels']))::view-transition-old(root) {
139
+ animation-name: ma-curtain-pixels;
140
+ animation-timing-function: steps(6, end);
141
+ mask-image:
142
+ linear-gradient(90deg, transparent calc(50% - var(--ma-curtain-px) / 2), #000 0 calc(50% + var(--ma-curtain-px) / 2), transparent 0),
143
+ linear-gradient(transparent calc(50% - var(--ma-curtain-px) / 2), #000 0 calc(50% + var(--ma-curtain-px) / 2), transparent 0);
144
+ mask-size: 6vmin 6vmin;
145
+ mask-composite: intersect;
146
+ }
147
+
148
+ /* title: the old page holds, the panel rises with the next title, the new page is there when it leaves */
149
+ :where(:root:has(.ma-curtain[data-mode='title']))::view-transition-old(root) {
150
+ animation: ma-curtain-cut-old var(--ma-curtain-dur, var(--ma-dur-ambient)) linear both;
151
+ mix-blend-mode: normal;
152
+ }
153
+ :where(:root:has(.ma-curtain[data-mode='title']))::view-transition-new(root) {
154
+ animation: ma-curtain-cut-new var(--ma-curtain-dur, var(--ma-dur-ambient)) linear both;
155
+ mix-blend-mode: normal;
156
+ }
157
+ :where(:root:has(.ma-curtain[data-mode='title']))::view-transition-group(ma-curtain-title) {
158
+ z-index: 2;
159
+ animation: ma-curtain-title var(--ma-curtain-dur, var(--ma-dur-ambient)) var(--ma-ease-in-out) both;
160
+ }
161
+ :where(:root:has(.ma-curtain[data-mode='title']))::view-transition-old(ma-curtain-title) {
162
+ display: none;
163
+ }
164
+ :where(:root:has(.ma-curtain[data-mode='title']))::view-transition-new(ma-curtain-title) {
165
+ animation: none;
166
+ mix-blend-mode: normal;
167
+ }
168
+
169
+ @media (prefers-reduced-motion: reduce) {
170
+ :where(:root:has(.ma-curtain))::view-transition-old(root),
171
+ :where(:root:has(.ma-curtain))::view-transition-new(root),
172
+ :where(:root:has(.ma-curtain))::view-transition-group(ma-curtain-title),
173
+ :where(:root:has(.ma-curtain))::view-transition-new(ma-curtain-title) {
174
+ animation: none;
175
+ mask-image: none;
176
+ clip-path: none;
177
+ }
178
+ :where(:root:has(.ma-curtain))::view-transition-old(root),
179
+ :where(:root:has(.ma-curtain))::view-transition-group(ma-curtain-title) {
180
+ display: none;
181
+ }
182
+ }
183
+ }
184
+
185
+ @keyframes ma-curtain-iris {
186
+ from {
187
+ clip-path: circle(0 at var(--ma-curtain-x, 50%) var(--ma-curtain-y, 50%));
188
+ }
189
+ to {
190
+ clip-path: circle(150% at var(--ma-curtain-x, 50%) var(--ma-curtain-y, 50%));
191
+ }
192
+ }
193
+ @keyframes ma-curtain-doors {
194
+ from {
195
+ clip-path: inset(0 50%);
196
+ }
197
+ to {
198
+ clip-path: inset(0 0);
199
+ }
200
+ }
201
+ @keyframes ma-curtain-shutter {
202
+ from {
203
+ clip-path: inset(0 0 100% 0);
204
+ }
205
+ to {
206
+ clip-path: inset(0 0 0 0);
207
+ }
208
+ }
209
+ @keyframes ma-curtain-diagonal {
210
+ from {
211
+ clip-path: polygon(0 0, 0 0, 0 0);
212
+ }
213
+ to {
214
+ clip-path: polygon(0 0, 200% 0, 0 200%);
215
+ }
216
+ }
217
+ @keyframes ma-curtain-blinds {
218
+ from {
219
+ --ma-curtain-f: 0%;
220
+ }
221
+ to {
222
+ --ma-curtain-f: 12.5%;
223
+ }
224
+ }
225
+ @keyframes ma-curtain-stagger {
226
+ from {
227
+ mask-size: 0% 100%;
228
+ }
229
+ to {
230
+ mask-size: 100% 100%;
231
+ }
232
+ }
233
+ @keyframes ma-curtain-pixels {
234
+ from {
235
+ --ma-curtain-px: 0px;
236
+ }
237
+ to {
238
+ --ma-curtain-px: 6vmin;
239
+ }
240
+ }
241
+ @keyframes ma-curtain-cut-old {
242
+ 0%,
243
+ 49.9% {
244
+ opacity: 1;
245
+ }
246
+ 50%,
247
+ 100% {
248
+ opacity: 0;
249
+ }
250
+ }
251
+ @keyframes ma-curtain-cut-new {
252
+ 0%,
253
+ 49.9% {
254
+ opacity: 0;
255
+ }
256
+ 50%,
257
+ 100% {
258
+ opacity: 1;
259
+ }
260
+ }
261
+ @keyframes ma-curtain-title {
262
+ from {
263
+ transform: translateY(100%);
264
+ }
265
+ 38%,
266
+ 62% {
267
+ transform: translateY(0);
268
+ }
269
+ to {
270
+ transform: translateY(-100%);
271
+ }
272
+ }
273
+ </style>
274
+
275
+ <script>
276
+ // The iris opens from the last pointer-down. The point is written on <html>, because the
277
+ // view-transition pseudo-elements inherit from it, and written again after every swap,
278
+ // because the router replaces <html>'s attributes. Back/forward opens from the centre.
279
+ // The curtain's duration prop rides along the same way.
280
+ let x = '50%';
281
+ let y = '50%';
282
+ const write = () => {
283
+ const s = document.documentElement.style;
284
+ s.setProperty('--ma-curtain-x', x);
285
+ s.setProperty('--ma-curtain-y', y);
286
+ const d = document.querySelector<HTMLElement>('.ma-curtain')?.dataset.duration;
287
+ if (d) s.setProperty('--ma-curtain-dur', `${d}ms`);
288
+ else s.removeProperty('--ma-curtain-dur');
289
+ };
290
+ document.addEventListener(
291
+ 'pointerdown',
292
+ (e) => {
293
+ x = `${e.clientX}px`;
294
+ y = `${e.clientY}px`;
295
+ write();
296
+ },
297
+ { passive: true },
298
+ );
299
+ addEventListener('popstate', () => {
300
+ x = y = '50%';
301
+ write();
302
+ });
303
+ document.addEventListener('astro:after-swap', write);
304
+ write();
305
+ </script>
@@ -0,0 +1,38 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'Curtain',
5
+ slug: 'curtain',
6
+ title: 'Curtain',
7
+ description:
8
+ 'A full-viewport wipe between pages: the next page opens through an iris from where you clicked, or through blinds, a shutter, doors, pixels, staggered columns, a diagonal, or a title panel that carries the next page\'s name. It animates the browser\'s own root view-transition snapshots with clip-path and mask, on Astro\'s router; back plays the shape in reverse. The only script is the click point.',
9
+ summary: 'A full-screen page transition: the next page opens through an iris from the click, or through blinds, doors or pixels.',
10
+ category: 'transition',
11
+ tier: 'B',
12
+ trigger: ['click'],
13
+ support: { baseline: 'newly', needs: ['view-transitions', 'has', 'clip-path', 'mask-image', 'mask-composite', 'property'], fallback: 'an instant swap between pages' },
14
+ readout: '<Curtain mode="iris">',
15
+ usage: `---\n// in the layout, once, on every page\n---\n<Curtain mode="iris" />\n\n<Curtain mode="title" title={Astro.props.title} />`,
16
+ props: [
17
+ { name: 'mode', type: "'iris' | 'blinds' | 'shutter' | 'doors' | 'pixels' | 'stagger' | 'diagonal' | 'title'", default: "'iris'", description: 'The shape. The destination page\'s mode plays, because the new document\'s CSS styles the transition.' },
18
+ { name: 'title', type: 'string', description: 'For title mode: the page\'s title, shown on the panel while it covers the swap. Give every page its own.' },
19
+ { name: 'duration', type: 'number', default: 'preset (760; title 1390)', description: 'Duration in ms. Unset, the preset\'s --ma-dur-gentle; title mode uses --ma-dur-ambient for its three beats.' },
20
+ { name: 'color', type: 'string', default: 'var(--ma-panel)', description: 'Panel colour in title mode: the page surface by default, over an opaque Canvas layer.' },
21
+ { name: 'textColor', type: 'string', default: 'var(--ma-ink)', description: 'Panel text colour in title mode.' },
22
+ ],
23
+ reducedMotion: 'Astro\'s router skips view transitions under the OS setting; under the site\'s switch the snapshots are cut without a shape.',
24
+ clientRouter: 'This is the ClientRouter: Curtain has no effect without it. The click-point script listens once on the document and rewrites the origin after every swap.',
25
+ craft: [
26
+ 'Curtain owns the root and PageTransition owns regions: a named region on the same page is captured separately and keeps its own preset, and the curtain wipes everything else. Use both; never two curtains.',
27
+ 'The old page holds still and the new one comes in through the shape. The default cross-fade is replaced (animation: none on the old snapshot, mix-blend-mode: normal on both), because a fade under a wipe looks muddy.',
28
+ 'The mode is read with :root:has(.ma-curtain[data-mode]) on the destination page, so a page decides how it is entered. An editorial page can open with a title while the shop uses an iris.',
29
+ 'The iris starts at the last pointer-down: one document listener, two custom properties on <html>, rewritten after the swap because the router replaces the root\'s attributes. Back and forward open from the centre.',
30
+ 'Blinds and pixels animate a registered custom property that a mask gradient reads, so the mask is re-rasterised on each frame. Eight stripes or a grid of squares are cheap to rasterise, and pixels use steps(6) because pixels do not tween.',
31
+ 'Back plays the same keyframes reversed on the leaving page, above the arriving one, so going back looks like the forward move undone.',
32
+ 'The title panel is a real element on every page, clipped away in normal life; its snapshot is what the browser slides across, so the title is the next page\'s own, not a copy the script knows about.',
33
+ ],
34
+ related: ['page-transition', 'persist-player', 'reveal'],
35
+ intents: ['page curtain transition', 'iris page transition', 'full screen wipe between pages', 'view transition root clip-path', 'blinds page transition', 'pixel page transition astro'],
36
+ replaces: ['page-transition examples (motion.dev)', 'curtain / pixel transitions (Codrops)', 'Barba.js and Swup transitions'],
37
+ fixture: { props: { mode: 'iris' }, expect: 'class="ma-curtain"' },
38
+ } satisfies ComponentMeta;
@@ -0,0 +1,156 @@
1
+ ---
2
+ /**
3
+ * Dialog — a native modal that opens and closes with motion, zero JS.
4
+ *
5
+ * The trigger is a button with the invoker `command="show-modal"`; closing
6
+ * is a form with method="dialog", which every browser has. Motion is
7
+ * @starting-style in, transition-behavior: allow-discrete out, so display
8
+ * and the top layer wait for the exit to finish. `polyfill` adds an inline
9
+ * click handler (403 B raw) for browsers that predate invoker commands. `morph`
10
+ * renders one internal component with a measured script (lib/morph): the
11
+ * box grows out of its trigger on a same-document view transition and
12
+ * returns to it on close; the default stays at 0 B.
13
+ */
14
+ import type { HTMLAttributes } from 'astro/types';
15
+ import DialogMorph from './internal/DialogMorph.astro';
16
+
17
+ interface Props extends HTMLAttributes<'dialog'> {
18
+ /** Element id; the trigger points at it. */
19
+ id: string;
20
+ /** Trigger label; or use the `trigger` slot. */
21
+ label?: string;
22
+ /** Close on backdrop click or Escape (closedby="any"). */
23
+ dismiss?: boolean;
24
+ /** Show the close button. */
25
+ close?: boolean;
26
+ /** Accessible name of the close button. */
27
+ closeLabel?: string;
28
+ /** Add the inline click handler for browsers without invoker commands. */
29
+ polyfill?: boolean;
30
+ /** Grow out of the trigger and back into it on a same-document view transition (adds the measured morph script; without view transitions or under reduced motion it opens and closes as usual). */
31
+ morph?: boolean;
32
+ }
33
+
34
+ const { id, label = 'Open', dismiss = true, close = true, closeLabel = 'Close', polyfill = false, morph = false, class: className, ...rest } = Astro.props;
35
+ const triggerAttrs = { command: 'show-modal', commandfor: id };
36
+ const dialogAttrs = dismiss ? { closedby: 'any' as const } : {};
37
+ ---
38
+
39
+ {Astro.slots.has('trigger') ? <slot name="trigger" /> : <button type="button" class="ma-dialog__trigger" {...triggerAttrs}>{label}</button>}
40
+ <dialog id={id} class:list={['ma-dialog', className]} data-morph={morph ? '' : undefined} {...dialogAttrs} {...rest}>
41
+ <div class="ma-dialog__panel">
42
+ <slot />
43
+ {close && (
44
+ <form method="dialog" class="ma-dialog__close">
45
+ <button type="submit" aria-label={closeLabel}>
46
+ <svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /></svg>
47
+ </button>
48
+ </form>
49
+ )}
50
+ </div>
51
+ </dialog>
52
+ {morph && <DialogMorph for={id} />}
53
+ {polyfill && <script is:inline>window.__maCmd||(window.__maCmd=1,document.addEventListener('click',function(e){if('command'in HTMLButtonElement.prototype||e.defaultPrevented)return;var b=e.target.closest('[commandfor]');if(!b)return;var d=document.getElementById(b.getAttribute('commandfor'));if(!d)return;var c=b.getAttribute('command');c==='show-modal'?d.showModal():c==='close'?d.close():c==='toggle-popover'?d.togglePopover():0}))</script>}
54
+
55
+ <style is:global>
56
+ @layer components {
57
+ :where(.ma-dialog) {
58
+ inset: 0;
59
+ margin: auto;
60
+ max-width: min(32rem, calc(100vw - 2rem));
61
+ border: 1px solid var(--ma-edge);
62
+ border-radius: 1rem;
63
+ padding: 0;
64
+ background: var(--ma-panel);
65
+ color: inherit;
66
+ box-shadow: 0 24px 64px -24px rgb(0 0 0 / 0.4);
67
+ opacity: 0;
68
+ scale: 0.96;
69
+ translate: 0 var(--ma-travel-hover, 4px);
70
+ transition:
71
+ opacity var(--ma-duration-fast) var(--ma-ease-out),
72
+ scale var(--ma-duration-fast) var(--ma-ease-out),
73
+ translate var(--ma-duration-fast) var(--ma-ease-out),
74
+ display var(--ma-duration-fast) allow-discrete,
75
+ overlay var(--ma-duration-fast) allow-discrete;
76
+ }
77
+ :where(.ma-dialog[open]) {
78
+ opacity: 1;
79
+ scale: 1;
80
+ translate: 0 0;
81
+ transition-duration: var(--ma-duration);
82
+ transition-timing-function: var(--ma-ease);
83
+ }
84
+ @starting-style {
85
+ :where(.ma-dialog[open]) {
86
+ opacity: 0;
87
+ scale: 0.96;
88
+ translate: 0 var(--ma-travel-enter, 24px);
89
+ }
90
+ }
91
+ :where(.ma-dialog)::backdrop {
92
+ background: var(--ma-scrim);
93
+ backdrop-filter: blur(6px);
94
+ opacity: 0;
95
+ transition:
96
+ opacity var(--ma-duration-fast) var(--ma-ease-out),
97
+ display var(--ma-duration-fast) allow-discrete,
98
+ overlay var(--ma-duration-fast) allow-discrete;
99
+ }
100
+ :where(.ma-dialog[open])::backdrop {
101
+ opacity: 1;
102
+ transition-duration: var(--ma-duration);
103
+ }
104
+ @starting-style {
105
+ :where(.ma-dialog[open])::backdrop {
106
+ opacity: 0;
107
+ }
108
+ }
109
+ :where(.ma-dialog__panel) {
110
+ position: relative;
111
+ padding: 1.5rem;
112
+ }
113
+ /* the close button's corner stays clear: a first line that runs the width of the panel ends before it */
114
+ :where(.ma-dialog__panel:has(> .ma-dialog__close)) {
115
+ padding-inline-end: 3.25rem;
116
+ }
117
+ :where(.ma-dialog__close) {
118
+ position: absolute;
119
+ inset-block-start: 0.75rem;
120
+ inset-inline-end: 0.75rem;
121
+ }
122
+ /* a plain button with no page reset under it: no UA border, background, padding or font */
123
+ :where(.ma-dialog__close button) {
124
+ display: grid;
125
+ place-items: center;
126
+ width: 2rem;
127
+ height: 2rem;
128
+ padding: 0;
129
+ border: 0;
130
+ border-radius: 999px;
131
+ background: none;
132
+ color: inherit;
133
+ font: inherit;
134
+ cursor: pointer;
135
+ opacity: 0.6;
136
+ transition: opacity var(--ma-duration-fast) var(--ma-ease-out), background-color var(--ma-duration-fast) var(--ma-ease-out);
137
+ }
138
+ :where(.ma-dialog__close button:hover) {
139
+ opacity: 1;
140
+ background: var(--ma-edge);
141
+ }
142
+ :where(.ma-dialog__trigger) {
143
+ cursor: pointer;
144
+ }
145
+ @media (prefers-reduced-motion: reduce) {
146
+ :where(.ma-dialog),
147
+ :where(.ma-dialog[open]),
148
+ :where(.ma-dialog)::backdrop {
149
+ scale: 1;
150
+ translate: 0 0;
151
+ transition-duration: var(--ma-duration-fast);
152
+ transition-timing-function: linear;
153
+ }
154
+ }
155
+ }
156
+ </style>
@@ -0,0 +1,37 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'Dialog',
5
+ slug: 'dialog',
6
+ title: 'Dialog',
7
+ description: 'A native modal that scales in over a blurred backdrop and scales out again, closes on Escape, the close button or a backdrop click, and traps focus, all handled by the browser. Zero JavaScript: the trigger uses the invoker command attribute; the exit is transition-behavior: allow-discrete.',
8
+ summary: 'A native modal dialog that scales in over a blurred backdrop, closes on Escape or a backdrop click, and traps focus.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'newly', needs: ['command', 'starting-style', 'allow-discrete', 'dialog'], fallback: 'without invoker commands the trigger needs polyfill (an inline handler of 403 B raw); without @starting-style and allow-discrete the modal opens and closes without motion; without closedby (Safari) a backdrop click does nothing, and Escape and the close button still close it' },
13
+ readout: '<Dialog command="show-modal">',
14
+ usage: `<Dialog id="terms" label="Read the terms">\n <h2>Terms</h2>\n <p>Short.</p>\n</Dialog>\n\n<Dialog id="custom" polyfill>\n <button slot="trigger" type="button" command="show-modal" commandfor="custom">Any trigger</button>\n <p>Contents.</p>\n</Dialog>\n\n<!-- grows out of its trigger on a view transition -->\n<Dialog id="card" morph label="Open the card">\n <p>The box you clicked is the box that opened.</p>\n</Dialog>`,
15
+ props: [
16
+ { name: 'id', type: 'string', description: 'Dialog id; the trigger points at it.' },
17
+ { name: 'label', type: 'string', default: "'Open'", description: 'Trigger label. Use the trigger slot for custom markup (give it command="show-modal" commandfor={id}).' },
18
+ { name: 'dismiss', type: 'boolean', default: 'true', description: 'Close on backdrop click, via closedby="any" (Chrome 134, Firefox 141; not in Safari yet).' },
19
+ { name: 'close', type: 'boolean', default: 'true', description: 'Render the close button (a form with method="dialog"). The panel keeps its inline-end corner clear for it.' },
20
+ { name: 'closeLabel', type: 'string', default: "'Close'", description: 'Accessible name of the close button.' },
21
+ { name: 'polyfill', type: 'boolean', default: 'false', description: 'Inline a click handler (403 B raw) for browsers without invoker commands (Safari < 26.2, Firefox < 144). It is document-level: one per page covers every Dialog, Drawer and Popover trigger.' },
22
+ { name: 'morph', type: 'boolean', default: 'false', description: 'Grow out of the trigger and return to it on a same-document view transition. Adds the measured morph script (see the Dialog.morph budget); without view transitions or under reduced motion the dialog opens and closes as usual. Lightbox and VideoDialog build on it.' },
23
+ ],
24
+ reducedMotion: 'Opens and closes with a 150 ms crossfade; no scale, no travel. With morph, the view transition is skipped and the dialog opens as usual.',
25
+ clientRouter: 'Native; nothing to rebind. A dialog open during navigation is closed with the page. The morph script binds through the shared runtime and sets its view-transition-name only for the duration of a transition, so the router never sees it.',
26
+ craft: [
27
+ 'Enter uses the preset spring at the preset duration; exit is 150 ms ease-out, because exits are always faster than entrances.',
28
+ 'display and overlay transition with allow-discrete so the element leaves the top layer only after the fade completes.',
29
+ 'The close button is a form with method="dialog": that closes a dialog in every browser that has one, no command attribute needed. It sits at the inline end (the left in a right-to-left page), and the panel pads that side so text never runs under it.',
30
+ 'The panel has a theme-aware background and a 24 px shadow; the backdrop blurs 6 px so the page recedes without going black.',
31
+ 'morph is an optional prop that carries a script: it is rendered as an internal component only when set, so the default measures 0 B and the variant is measured separately. The trigger and the dialog share one view-transition-name for one transition each way; the root gets a plain cross-fade on the tokens through html[data-ma-vt], which also keeps a Curtain on the same page from opening its iris.',
32
+ ],
33
+ related: ['popover', 'drawer', 'accordion'],
34
+ intents: ['modal dialog', 'animated modal', 'dialog open close animation', 'native dialog astro', 'modal without javascript'],
35
+ replaces: ['Dialog (Radix / shadcn)', 'Modal (Aceternity)', 'AnimatedModal (Aceternity)'],
36
+ fixture: { props: { id: 'ma-fixture-dialog', label: 'Fixture content for Dialog' }, expect: 'Fixture content for Dialog', variants: { morph: { props: { morph: true }, expect: 'data-ma-dialog-morph' } } },
37
+ } satisfies ComponentMeta;