@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,193 @@
1
+ ---
2
+ /**
3
+ * Dock — a row of icons that swell under the pointer, the neighbours with
4
+ * them, zero JS.
5
+ *
6
+ * Every item's size is one custom property; :has() sets it on the hovered
7
+ * (or keyboard-focused) item, on the two beside it and the two beyond, in
8
+ * three steps from the `magnify` prop, and the width and height transition
9
+ * on the preset curve — so the tiles push each other apart the way a real
10
+ * dock does rather than scaling over one another. The row is a fixed
11
+ * height and the growth goes up. Labels are tooltips above the item, no
12
+ * popover needed. Touch gets a plain row of links.
13
+ */
14
+ import type { HTMLAttributes } from 'astro/types';
15
+
16
+ interface Item {
17
+ href: string;
18
+ label: string;
19
+ /** Inline SVG (or any HTML) for the tile; default: the label's first letter. */
20
+ icon?: string;
21
+ /** Marks the current page (aria-current + a dot). */
22
+ current?: boolean;
23
+ }
24
+
25
+ interface Props extends HTMLAttributes<'nav'> {
26
+ items: Item[];
27
+ /** Tile size in px. */
28
+ size?: number;
29
+ /** Growth of the hovered tile, as a multiple of size; the neighbours get 60 % and 25 % of the growth. */
30
+ magnify?: number;
31
+ /** Pin to the bottom centre of the viewport. */
32
+ fixed?: boolean;
33
+ /** Accessible name of the nav. */
34
+ label?: string;
35
+ }
36
+
37
+ const { items, size = 44, magnify = 1.6, fixed = false, label = 'Dock', class: className, style, ...rest } = Astro.props;
38
+ const inline = [`--ma-dock-size:${size}px`, `--ma-dock-magnify:${magnify}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
39
+ const glyph = (l: string) => `<b>${l.trim().charAt(0).toUpperCase()}</b>`;
40
+ ---
41
+
42
+ <nav class:list={['ma-dock', className]} data-fixed={fixed ? '' : undefined} aria-label={label} style={inline} {...rest}>
43
+ <ul class="ma-dock__list">
44
+ {
45
+ items.map((it) => (
46
+ <li class="ma-dock__item">
47
+ <a href={it.href} class="ma-dock__link" aria-label={it.label} aria-current={it.current ? 'page' : undefined}>
48
+ <span class="ma-dock__icon" aria-hidden="true" set:html={it.icon ?? glyph(it.label)} />
49
+ <span class="ma-dock__label" aria-hidden="true">{it.label}</span>
50
+ </a>
51
+ </li>
52
+ ))
53
+ }
54
+ </ul>
55
+ </nav>
56
+
57
+ <style is:global>
58
+ @layer components {
59
+ :where(.ma-dock) {
60
+ /* the magnification the rules read; reduced motion sets it to 1 */
61
+ --ma-dock-m: var(--ma-dock-magnify, 1.6);
62
+ display: inline-flex;
63
+ padding: 0.375rem;
64
+ border: 1px solid var(--ma-edge);
65
+ border-radius: 1.25rem;
66
+ background: var(--ma-panel);
67
+ backdrop-filter: blur(16px);
68
+ box-shadow: 0 16px 40px -20px rgb(0 0 0 / 0.45);
69
+ }
70
+ :where(.ma-dock[data-fixed]) {
71
+ position: fixed;
72
+ bottom: 1rem;
73
+ left: 50%;
74
+ z-index: 40;
75
+ translate: -50% 0;
76
+ }
77
+ :where(.ma-dock__list) {
78
+ display: flex;
79
+ align-items: flex-end;
80
+ gap: 0.375rem;
81
+ height: var(--ma-dock-size, 44px);
82
+ margin: 0;
83
+ padding: 0;
84
+ list-style: none;
85
+ }
86
+ /* every tile's size is one number; the hover rules below set it */
87
+ :where(.ma-dock__item) {
88
+ --ma-dock-k: 1;
89
+ position: relative;
90
+ flex: none;
91
+ width: calc(var(--ma-dock-size, 44px) * var(--ma-dock-k));
92
+ height: calc(var(--ma-dock-size, 44px) * var(--ma-dock-k));
93
+ transition:
94
+ width var(--ma-duration) var(--ma-ease),
95
+ height var(--ma-duration) var(--ma-ease);
96
+ }
97
+ :where(.ma-dock__link) {
98
+ position: relative;
99
+ display: grid;
100
+ place-items: center;
101
+ width: 100%;
102
+ height: 100%;
103
+ border: 1px solid var(--ma-edge);
104
+ border-radius: 28%;
105
+ background: color-mix(in srgb, var(--ma-ink) 6%, transparent);
106
+ color: inherit;
107
+ text-decoration: none;
108
+ transition: background-color var(--ma-duration-fast) var(--ma-ease-out);
109
+ }
110
+ :where(.ma-dock__link:hover) {
111
+ background: color-mix(in srgb, var(--ma-ink) 11%, transparent);
112
+ }
113
+ :where(.ma-dock__link:focus-visible) {
114
+ outline: 2px solid currentColor;
115
+ outline-offset: 2px;
116
+ }
117
+ :where(.ma-dock__link[aria-current='page'])::after {
118
+ content: '';
119
+ position: absolute;
120
+ bottom: -0.35rem;
121
+ left: 50%;
122
+ width: 4px;
123
+ height: 4px;
124
+ border-radius: 999px;
125
+ background: currentColor;
126
+ translate: -50% 0;
127
+ }
128
+ :where(.ma-dock__icon) {
129
+ display: grid;
130
+ place-items: center;
131
+ width: 56%;
132
+ height: 56%;
133
+ font-weight: 600;
134
+ font-size: calc(var(--ma-dock-size, 44px) * 0.38 * var(--ma-dock-k));
135
+ line-height: 1;
136
+ }
137
+ :where(.ma-dock__icon svg) {
138
+ width: 100%;
139
+ height: 100%;
140
+ }
141
+ /* the label: a tooltip above, arriving from a few px below on the hover travel */
142
+ :where(.ma-dock__label) {
143
+ position: absolute;
144
+ bottom: calc(100% + 0.5rem);
145
+ left: 50%;
146
+ padding: 0.3em 0.65em;
147
+ border-radius: 0.375rem;
148
+ background: var(--ma-ink);
149
+ color: var(--ma-panel);
150
+ font-size: 0.75rem;
151
+ font-weight: 500;
152
+ line-height: 1.2;
153
+ white-space: nowrap;
154
+ opacity: 0;
155
+ translate: -50% var(--ma-travel-hover, 4px);
156
+ pointer-events: none;
157
+ transition:
158
+ opacity var(--ma-duration-fast) var(--ma-ease-out),
159
+ translate var(--ma-duration) var(--ma-ease);
160
+ }
161
+ @media (hover: hover) {
162
+ /* the hovered tile, its two neighbours, and the two beyond — three sizes from one number */
163
+ :where(.ma-dock__item:is(:hover, :has(:focus-visible))) {
164
+ --ma-dock-k: var(--ma-dock-m);
165
+ }
166
+ :where(.ma-dock__item:has(+ .ma-dock__item:is(:hover, :has(:focus-visible)))),
167
+ :where(.ma-dock__item:is(:hover, :has(:focus-visible)) + .ma-dock__item) {
168
+ --ma-dock-k: calc(1 + (var(--ma-dock-m) - 1) * 0.6);
169
+ }
170
+ :where(.ma-dock__item:has(+ .ma-dock__item + .ma-dock__item:is(:hover, :has(:focus-visible)))),
171
+ :where(.ma-dock__item:is(:hover, :has(:focus-visible)) + .ma-dock__item + .ma-dock__item) {
172
+ --ma-dock-k: calc(1 + (var(--ma-dock-m) - 1) * 0.25);
173
+ }
174
+ :where(.ma-dock__item:is(:hover, :has(:focus-visible)) .ma-dock__label) {
175
+ opacity: 1;
176
+ translate: -50% 0;
177
+ }
178
+ }
179
+ @media (prefers-reduced-motion: reduce) {
180
+ :where(.ma-dock) {
181
+ --ma-dock-m: 1;
182
+ }
183
+ :where(.ma-dock__item),
184
+ :where(.ma-dock__label) {
185
+ transition: none;
186
+ translate: -50% 0;
187
+ }
188
+ :where(.ma-dock__item) {
189
+ translate: none;
190
+ }
191
+ }
192
+ }
193
+ </style>
@@ -0,0 +1,35 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'Dock',
5
+ slug: 'dock',
6
+ title: 'Dock',
7
+ description: 'A macOS-style dock: the tile under the pointer swells, its neighbours swell less, and the row pushes apart to make room. Zero JavaScript: :has() sets one size number on the hovered tile and the four around it, and the tiles transition their size on the preset curve. Keyboard focus magnifies the same way; touch gets a plain row of links.',
8
+ summary: 'A macOS-style dock: the hovered icon grows, its neighbours grow less, and the row makes room for them.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['hover'],
12
+ support: { baseline: 'widely', needs: ['has'], fallback: 'without :has() the tiles keep their size and the labels stay hidden' },
13
+ readout: '<Dock magnify={1.6}>',
14
+ usage: `<Dock\n items={[\n { href: '/', label: 'Home', icon: homeSvg, current: true },\n { href: '/work/', label: 'Work', icon: workSvg },\n { href: '/writing/', label: 'Writing', icon: penSvg },\n { href: 'mailto:hi@example.com', label: 'Email', icon: mailSvg },\n ]}\n fixed\n/>`,
15
+ props: [
16
+ { name: 'items', type: '{ href: string; label: string; icon?: string; current?: boolean }[]', description: 'The tiles; icon is inline SVG (or any HTML), default the first letter of the label.' },
17
+ { name: 'size', type: 'number', default: '44', description: 'Tile size in px.' },
18
+ { name: 'magnify', type: 'number', default: '1.6', description: 'Growth of the hovered tile as a multiple of size; the neighbours get 60 % and 25 % of the growth.' },
19
+ { name: 'fixed', type: 'boolean', default: 'false', description: 'Pin to the bottom centre of the viewport.' },
20
+ { name: 'label', type: 'string', default: "'Dock'", description: 'Accessible name of the nav.' },
21
+ ],
22
+ reducedMotion: 'No magnification (the size number stays 1) and the labels appear without travel.',
23
+ clientRouter: 'CSS-only; nothing to rebind. Mark the current page with the item\'s current flag on each page.',
24
+ craft: [
25
+ 'The dock magnifies by neighbourhood, not by pointer distance: the hovered tile, ±1 and ±2, in three steps. A cursor-distance curve needs a script; this reads as a dock at a glance and costs nothing.',
26
+ 'Width and height transition, a deliberate layout change: the point of a dock is that the tiles push each other apart. It is five small elements on a hover, on the preset curve, well inside a frame.',
27
+ 'The row has a fixed height and the tiles align to its bottom, so growth goes up and the dock\'s own box never jumps.',
28
+ 'Labels are aria-hidden tooltips; the link\'s aria-label carries the name, so a screen reader hears "Work, link" without a hover.',
29
+ 'Everything hover-related sits under @media (hover: hover): on a phone the dock is a row of links, which is what a phone wants.',
30
+ ],
31
+ related: ['pill-nav', 'magnetic', 'progressive-blur'],
32
+ intents: ['macos dock', 'dock navigation', 'magnifying icons on hover', 'dock menu astro', 'floating dock bottom of page', 'icon bar that grows under the cursor'],
33
+ replaces: ['Dock (Magic UI)', 'Dock (React Bits)', 'FloatingDock (Aceternity)', 'Dock (Motion Primitives)'],
34
+ fixture: { props: { items: [{ href: '/', label: 'Fixture content for Dock' }, { href: '/two/', label: 'Two' }, { href: '/three/', label: 'Three' }] }, expect: 'Fixture content for Dock' },
35
+ } satisfies ComponentMeta;
@@ -0,0 +1,58 @@
1
+ ---
2
+ /**
3
+ * DotPattern — a field of dots behind content, faded at the edges, with an
4
+ * optional slow pulse. Zero JS: one radial gradient tiled, and a mask.
5
+ * Fills its positioned parent.
6
+ */
7
+ import type { HTMLAttributes } from 'astro/types';
8
+
9
+ interface Props extends HTMLAttributes<'div'> {
10
+ /** Distance between dots in px. */
11
+ size?: number;
12
+ /** Dot radius in px. */
13
+ radius?: number;
14
+ /** Dot colour. */
15
+ color?: string;
16
+ /** Fade toward the edges. */
17
+ fade?: boolean;
18
+ /** Pulse period in seconds; 0 keeps it still. */
19
+ pulse?: number;
20
+ }
21
+
22
+ const { size = 20, radius = 1, color = 'var(--ma-glow)', fade = true, pulse = 0, class: className, style, ...rest } = Astro.props;
23
+ const vars = [`--ma-dp-size:${size}px`, `--ma-dp-r:${radius}px`, `--ma-dp-color:${color}`, `--ma-dp-pulse:${pulse}s`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
24
+ ---
25
+
26
+ <div class:list={['ma-dot-pattern', className]} aria-hidden="true" data-fade={fade ? '' : undefined} data-pulse={pulse > 0 ? '' : undefined} style={vars} {...rest}></div>
27
+
28
+ <style is:global>
29
+ @layer components {
30
+ :where(.ma-dot-pattern) {
31
+ position: absolute;
32
+ inset: 0;
33
+ pointer-events: none;
34
+ background-image: radial-gradient(circle, var(--ma-dp-color, var(--ma-glow)) var(--ma-dp-r, 1px), transparent calc(var(--ma-dp-r, 1px) + 0.5px));
35
+ background-size: var(--ma-dp-size, 20px) var(--ma-dp-size, 20px);
36
+ }
37
+ :where(.ma-dot-pattern[data-fade]) {
38
+ mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
39
+ }
40
+ :where(.ma-dot-pattern[data-pulse]) {
41
+ animation: ma-dot-pattern var(--ma-dp-pulse, 4s) var(--ma-ease-in-out) infinite alternate;
42
+ }
43
+ @media (prefers-reduced-motion: reduce) {
44
+ :where(.ma-dot-pattern) {
45
+ animation: none;
46
+ }
47
+ }
48
+ }
49
+
50
+ @keyframes ma-dot-pattern {
51
+ from {
52
+ opacity: 0.5;
53
+ }
54
+ to {
55
+ opacity: 1;
56
+ }
57
+ }
58
+ </style>
@@ -0,0 +1,32 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'DotPattern',
5
+ slug: 'dot-pattern',
6
+ title: 'Dot Pattern',
7
+ description: 'A field of dots behind content, faded toward the edges, with an optional slow pulse. One tiled radial gradient and a mask; pure CSS. Fills its positioned parent.',
8
+ summary: 'A dot pattern background that fades toward the edges, with an optional slow pulse.',
9
+ category: 'background',
10
+ tier: 'A',
11
+ trigger: ['always'],
12
+ support: { baseline: 'widely', needs: ['mask-image'] },
13
+ readout: '<DotPattern size={20}>',
14
+ usage: `<section class="relative">\n <DotPattern pulse={5} />\n <h2 class="relative">Above the dots</h2>\n</section>`,
15
+ props: [
16
+ { name: 'size', type: 'number', default: '20', description: 'Distance between dots in px.' },
17
+ { name: 'radius', type: 'number', default: '1', description: 'Dot radius in px.' },
18
+ { name: 'color', type: 'string', default: 'var(--ma-glow)', description: 'Dot colour; the token is theme-aware.' },
19
+ { name: 'fade', type: 'boolean', default: 'true', description: 'Radial mask toward the edges.' },
20
+ { name: 'pulse', type: 'number', default: '0', description: 'Seconds per pulse (opacity 0.5 ↔ 1); 0 keeps it still.' },
21
+ ],
22
+ reducedMotion: 'No pulse.',
23
+ clientRouter: 'CSS-only; nothing to rebind.',
24
+ craft: [
25
+ 'The pulse is opacity on the whole layer (compositor only); the gradient itself never changes.',
26
+ 'Dot edge is radius + 0.5 px so it stays crisp at any device pixel ratio.',
27
+ ],
28
+ related: ['grid-pattern', 'noise', 'cursor-grid'],
29
+ intents: ['dot pattern background', 'dotted background', 'dot grid', 'polka dot background css'],
30
+ replaces: ['DotPattern (Magic UI)', 'DotBackground (Aceternity)'],
31
+ fixture: { expect: 'class="ma-dot-pattern"' },
32
+ } satisfies ComponentMeta;
@@ -0,0 +1,153 @@
1
+ ---
2
+ /**
3
+ * DottedMap — the world as a grid of dots, with markers that pulse. The
4
+ * land comes from a 1° bitmask (Natural Earth 110m, built once into
5
+ * lib/land.ts) and is drawn on the server as one <path>: every run of land
6
+ * cells in a row is a horizontal segment, and a dash pattern of zero-length
7
+ * dashes with round caps turns the segment into dots on the grid — so a
8
+ * 2° map is a few hundred segments, not five thousand circles. Markers are
9
+ * a dot and a ring scaling out on a delay from the stagger token; each
10
+ * carries a <title> and the labels are listed for readers. Zero script.
11
+ */
12
+ import type { HTMLAttributes } from 'astro/types';
13
+ import { LAND, LAND_H, LAND_W } from '../lib/land';
14
+ import { decode, maskAt, toMap, type Marker } from '../lib/geo';
15
+
16
+ interface Props extends HTMLAttributes<'div'> {
17
+ /** Cities to mark: latitude, longitude and an optional label. */
18
+ markers?: Marker[];
19
+ /** Grid spacing: low 3°, medium 2°, high 1.5°. */
20
+ density?: 'low' | 'medium' | 'high';
21
+ /** Dot colour. */
22
+ color?: string;
23
+ /** Marker colour. */
24
+ accent?: string;
25
+ /** Pulse the markers. */
26
+ ping?: boolean;
27
+ /** Seconds per pulse. */
28
+ duration?: number;
29
+ /** Include Antarctica. */
30
+ antarctica?: boolean;
31
+ /** Accessible name; the marker labels are appended. */
32
+ label?: string;
33
+ }
34
+
35
+ const { markers = [], density = 'medium', color = 'color-mix(in srgb, var(--ma-ink) 32%, transparent)', accent = 'currentColor', ping = true, duration = 2.4, antarctica = false, label = 'World map', class: className, style, ...rest } = Astro.props;
36
+ const step = density === 'low' ? 3 : density === 'high' ? 1.5 : 2;
37
+ const mask = decode(LAND, LAND_W, LAND_H);
38
+ const south = antarctica ? -90 : -58;
39
+ const north = 84;
40
+ // the drawing is 200 × 100 (2:1, equirectangular); one path, one `M x y h len` per run of land cells in a row
41
+ const W = 200;
42
+ const segs: string[] = [];
43
+ let dots = 0;
44
+ for (let lat = north - step / 2; lat > south; lat -= step) {
45
+ let run = 0;
46
+ let x0 = 0;
47
+ const y = Math.round(((90 - lat) / 180) * 100 * 100) / 100;
48
+ const cols = Math.round(360 / step);
49
+ for (let i = 0; i <= cols; i++) {
50
+ const lng = -180 + (i + 0.5) * step;
51
+ const land = i < cols && maskAt(mask, lat, lng);
52
+ if (land) {
53
+ if (run === 0) x0 = Math.round(((lng + 180) / 360) * W * 100) / 100;
54
+ run++;
55
+ dots++;
56
+ } else if (run) {
57
+ segs.push(`M${x0} ${y}h${Math.round((run - 1) * step * (W / 360) * 100) / 100}`);
58
+ run = 0;
59
+ }
60
+ }
61
+ }
62
+ const d = segs.join('');
63
+ const pins = markers.map((m, i) => {
64
+ const [x, y] = toMap(m.lat, m.lng);
65
+ return { ...m, i, x: Math.round(x * W) / 100, y: Math.round(y * 100) / 100 };
66
+ });
67
+ const viewTop = ((90 - north) / 180) * 100;
68
+ const viewH = ((north - south) / 180) * 100;
69
+ const name = pins.length ? `${label}: ${pins.map((p) => p.label ?? `${p.lat}, ${p.lng}`).join(', ')}` : label;
70
+ const inline = [`--ma-map-step:${(step * W) / 360}`, `--ma-map-color:${color}`, `--ma-map-accent:${accent}`, `--ma-map-dur:${duration}s`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
71
+ ---
72
+
73
+ <div class:list={['ma-map', className]} data-ma-map data-dots={dots} data-path-bytes={d.length} style={inline} {...rest}>
74
+ <svg class="ma-map__svg" viewBox={`0 ${viewTop} ${W} ${viewH}`} preserveAspectRatio="xMidYMid meet" role="img" aria-label={name}>
75
+ <path class="ma-map__land" d={d} />
76
+ {pins.map((p) => (
77
+ <g class="ma-map__pin" transform={`translate(${p.x} ${p.y})`} style={`--ma-map-i:${p.i}`}>
78
+ {p.label && <title>{p.label}</title>}
79
+ {ping && <circle class="ma-map__ring" r="1" />}
80
+ <circle class="ma-map__dot" r="0.9" />
81
+ </g>
82
+ ))}
83
+ </svg>
84
+ {pins.some((p) => p.label) && (
85
+ <ul class="ma-map__list">
86
+ {pins.map((p) => p.label && <li>{p.label}</li>)}
87
+ </ul>
88
+ )}
89
+ </div>
90
+
91
+ <style is:global>
92
+ @layer components {
93
+ :where(.ma-map) {
94
+ position: absolute;
95
+ inset: 0;
96
+ display: grid;
97
+ place-items: center;
98
+ pointer-events: none;
99
+ }
100
+ :where(.ma-map__svg) {
101
+ width: 100%;
102
+ height: 100%;
103
+ overflow: visible;
104
+ }
105
+ /* dots on the grid: zero-length dashes, one per step, round caps — the dot radius is 0.42 of the step, in map units */
106
+ :where(.ma-map__land) {
107
+ fill: none;
108
+ stroke: var(--ma-map-color);
109
+ stroke-width: calc(var(--ma-map-step, 1.11) * 0.84);
110
+ stroke-linecap: round;
111
+ stroke-dasharray: 0 var(--ma-map-step, 1.11);
112
+ }
113
+ :where(.ma-map__dot) {
114
+ fill: var(--ma-map-accent, currentColor);
115
+ }
116
+ :where(.ma-map__ring) {
117
+ fill: none;
118
+ stroke: var(--ma-map-accent, currentColor);
119
+ stroke-width: 0.35;
120
+ transform-box: fill-box;
121
+ transform-origin: center;
122
+ opacity: 0;
123
+ animation: ma-map-ping var(--ma-map-dur) var(--ma-ease-out) infinite;
124
+ animation-delay: calc(var(--ma-map-i, 0) * var(--ma-stagger-relaxed) * 3);
125
+ }
126
+ :where(.ma-map__list) {
127
+ position: absolute;
128
+ width: 1px;
129
+ height: 1px;
130
+ overflow: hidden;
131
+ clip-path: inset(50%);
132
+ white-space: nowrap;
133
+ }
134
+ @media (prefers-reduced-motion: reduce) {
135
+ :where(.ma-map__ring) {
136
+ animation: none;
137
+ opacity: 0.5;
138
+ scale: 2;
139
+ }
140
+ }
141
+ }
142
+
143
+ @keyframes ma-map-ping {
144
+ 0% {
145
+ opacity: 0.9;
146
+ scale: 1;
147
+ }
148
+ 100% {
149
+ opacity: 0;
150
+ scale: 4;
151
+ }
152
+ }
153
+ </style>
@@ -0,0 +1,38 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'DottedMap',
5
+ slug: 'dotted-map',
6
+ title: 'Dotted Map',
7
+ description: 'A world map drawn as a grid of dots, with markers that pulse. The land is a 1° bitmask from Natural Earth, drawn on the server as one <path>: each run of land cells in a row is a segment, and a dash pattern of zero-length dashes with round caps turns it into dots: a few hundred segments, not five thousand circles. Markers ping on a delay from the stagger token and carry a title. Zero JS.',
8
+ summary: 'A world map drawn in dots with pulsing location markers, rendered on the server as one SVG path.',
9
+ category: 'background',
10
+ tier: 'A',
11
+ trigger: ['always'],
12
+ support: { baseline: 'widely', needs: [] },
13
+ readout: '<DottedMap markers={[…]} density="medium">',
14
+ usage: `<div class="relative aspect-[2/1]">\n <DottedMap\n markers={[\n { lat: 40.7, lng: -74, label: 'New York' },\n { lat: 51.5, lng: -0.1, label: 'London' },\n { lat: 1.3, lng: 103.8, label: 'Singapore' },\n ]}\n accent="var(--action)"\n />\n</div>`,
15
+ props: [
16
+ { name: 'markers', type: '{ lat, lng, label? }[]', default: '[]', description: 'Cities to mark; a label becomes a <title> and a line in the hidden list.' },
17
+ { name: 'density', type: "'low' | 'medium' | 'high'", default: "'medium'", description: 'Grid spacing: 3°, 2° or 1.5°. Denser is more HTML; the path size is on the card.' },
18
+ { name: 'color', type: 'string', default: 'ink at 32 %', description: 'Dot colour.' },
19
+ { name: 'accent', type: 'string', default: "'currentColor'", description: 'Marker colour.' },
20
+ { name: 'ping', type: 'boolean', default: 'true', description: 'Pulse the markers.' },
21
+ { name: 'duration', type: 'number', default: '2.4', description: 'Seconds per pulse.' },
22
+ { name: 'antarctica', type: 'boolean', default: 'false', description: 'Include Antarctica (the map is cut at 58° S otherwise).' },
23
+ { name: 'label', type: 'string', default: "'World map'", description: 'Accessible name; marker labels are appended.' },
24
+ ],
25
+ reducedMotion: 'The rings stand still around the markers at half strength.',
26
+ clientRouter: 'CSS-only; nothing to rebind.',
27
+ craft: [
28
+ 'One path, dashed into dots: `M x y h len` per run of land with stroke-dasharray 0 step and round caps puts a dot on every grid point. The path is a few kilobytes where circles would be tens.',
29
+ 'The land data is built once from Natural Earth 110m (public domain) into a 1° bitmask; the map reads it on the server and ships no data to the client.',
30
+ 'The dot radius is 0.42 of the grid step in map units, so density changes both spacing and size together and the map never looks sparse or clogged.',
31
+ 'Markers pulse with scale and opacity on the exit curve, each delayed by its index times the relaxed stagger, so five cities read as a sequence.',
32
+ 'role="img" with the cities in the name, a <title> per marker and a visually hidden list: the map is decorative, the markers are content.',
33
+ ],
34
+ related: ['globe', 'dot-pattern', 'ripple'],
35
+ intents: ['dotted world map', 'world map with markers', 'dot map svg', 'map with pulsing locations', 'regions map astro'],
36
+ replaces: ['DottedMap (Magic UI)', 'WorldMap (Aceternity)', 'dotted-map (npm)'],
37
+ fixture: { props: { markers: [{ lat: 40.7, lng: -74, label: 'New York' }] }, expect: 'class="ma-map"' },
38
+ } satisfies ComponentMeta;