@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,159 @@
1
+ ---
2
+ /**
3
+ * CheckDraw — a checkbox whose tick is drawn, zero JS. A <label> wraps a
4
+ * native `<input type="checkbox">` (`appearance: none`, still focusable,
5
+ * still submitted) and an SVG beside it: the tick is a path with
6
+ * `pathLength="1"` whose dash offset runs 1 → 0 on the preset curve when
7
+ * the input is `:checked`; `:indeterminate` (a DOM property your script
8
+ * sets) draws a short bar instead. The box fills on the fast token first,
9
+ * so the stroke lands on ink. Radios are not this component.
10
+ */
11
+ import type { HTMLAttributes } from 'astro/types';
12
+
13
+ interface Props extends Omit<HTMLAttributes<'input'>, 'type' | 'size'> {
14
+ name?: string;
15
+ value?: string;
16
+ checked?: boolean;
17
+ /** Visible label. Without it, pass aria-label. */
18
+ label?: string;
19
+ /** Box size, any CSS length. */
20
+ size?: string;
21
+ /** Box colour when checked; default the ink token. */
22
+ color?: string;
23
+ }
24
+
25
+ const { name, value = 'on', checked = false, label, size = '1.25rem', color, class: className, style, ...rest } = Astro.props;
26
+ const vars = [`--ma-checkdraw-size:${size}`, color && `--ma-checkdraw-on:${color}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
27
+ ---
28
+
29
+ <label class:list={['ma-checkdraw', className]} style={vars}>
30
+ <span class="ma-checkdraw__box">
31
+ <input type="checkbox" class="ma-checkdraw__input" name={name} value={value} checked={checked} {...rest} />
32
+ <svg class="ma-checkdraw__mark" viewBox="0 0 24 24" aria-hidden="true">
33
+ <path class="ma-checkdraw__tick" d="M5 12.5l4.5 4.5L19 7" pathLength="1" />
34
+ <path class="ma-checkdraw__bar" d="M6 12h12" pathLength="1" />
35
+ </svg>
36
+ </span>
37
+ {label && <span class="ma-checkdraw__label">{label}</span>}
38
+ </label>
39
+
40
+ <style is:global>
41
+ @layer components {
42
+ :where(.ma-checkdraw) {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ gap: 0.6em;
46
+ cursor: pointer;
47
+ -webkit-tap-highlight-color: transparent;
48
+ }
49
+ :where(.ma-checkdraw__box) {
50
+ position: relative;
51
+ flex: none;
52
+ display: grid;
53
+ inline-size: var(--ma-checkdraw-size, 1.25rem);
54
+ block-size: var(--ma-checkdraw-size, 1.25rem);
55
+ }
56
+ :where(.ma-checkdraw__input) {
57
+ /* the box: the input itself, so focus, the value and the keyboard are native */
58
+ appearance: none;
59
+ -webkit-appearance: none;
60
+ position: relative;
61
+ grid-area: 1 / 1;
62
+ inline-size: 100%;
63
+ block-size: 100%;
64
+ margin: 0;
65
+ /* --ma-checkdraw-edge: a section that knows its ground sets a border measured on it (HeroWaitlist, 7G) */
66
+ border: 1.5px solid var(--ma-checkdraw-edge, color-mix(in srgb, var(--ma-ink) 45%, transparent));
67
+ border-radius: 0.3em;
68
+ background: transparent;
69
+ cursor: pointer;
70
+ font: inherit;
71
+ transition:
72
+ background-color var(--ma-duration-fast) var(--ma-ease-out),
73
+ border-color var(--ma-duration-fast) var(--ma-ease-out);
74
+ }
75
+ :where(.ma-checkdraw__input)::after {
76
+ content: '';
77
+ position: absolute;
78
+ inset: min(0px, calc((var(--ma-checkdraw-size, 1.25rem) - 26px) / 2));
79
+ }
80
+ :where(.ma-checkdraw__input:checked),
81
+ :where(.ma-checkdraw__input:indeterminate) {
82
+ background: var(--ma-checkdraw-on, var(--ma-ink));
83
+ border-color: var(--ma-checkdraw-on, var(--ma-ink));
84
+ }
85
+ :where(.ma-checkdraw__mark) {
86
+ grid-area: 1 / 1;
87
+ inline-size: 100%;
88
+ block-size: 100%;
89
+ padding: 12%;
90
+ pointer-events: none;
91
+ fill: none;
92
+ stroke: var(--ma-panel);
93
+ stroke-width: 3;
94
+ stroke-linecap: round;
95
+ stroke-linejoin: round;
96
+ }
97
+ :where(.ma-checkdraw__tick),
98
+ :where(.ma-checkdraw__bar) {
99
+ /* pathLength 1: the dash is the whole stroke, offset 1 hides it, 0 shows it */
100
+ stroke-dasharray: 1;
101
+ stroke-dashoffset: 1;
102
+ transition: stroke-dashoffset var(--ma-duration-fast) var(--ma-ease-in);
103
+ }
104
+ :where(.ma-checkdraw__input:checked ~ .ma-checkdraw__mark .ma-checkdraw__tick),
105
+ :where(.ma-checkdraw__input:indeterminate ~ .ma-checkdraw__mark .ma-checkdraw__bar) {
106
+ stroke-dashoffset: 0;
107
+ transition: stroke-dashoffset var(--ma-duration) var(--ma-ease);
108
+ /* the stroke waits for the box to fill, so it is drawn on ink, not on nothing */
109
+ transition-delay: calc(var(--ma-duration-fast) / 2);
110
+ }
111
+ :where(.ma-checkdraw__input:focus-visible) {
112
+ outline: 2px solid var(--ma-checkdraw-on, var(--ma-ink));
113
+ outline-offset: 3px;
114
+ }
115
+ :where(.ma-checkdraw__input:disabled) {
116
+ cursor: not-allowed;
117
+ opacity: 0.45;
118
+ transition: none;
119
+ }
120
+ :where(.ma-checkdraw__input:disabled ~ .ma-checkdraw__mark *) {
121
+ transition: none;
122
+ }
123
+ :where(.ma-checkdraw:has(.ma-checkdraw__input:disabled)) {
124
+ cursor: not-allowed;
125
+ }
126
+ :where(.ma-checkdraw:has(.ma-checkdraw__input:disabled) .ma-checkdraw__label) {
127
+ opacity: 0.6;
128
+ }
129
+ :where(.ma-checkdraw__label) {
130
+ line-height: 1.2;
131
+ user-select: none;
132
+ }
133
+ @media (forced-colors: active) {
134
+ :where(.ma-checkdraw__input) {
135
+ border-color: ButtonText;
136
+ background: Canvas;
137
+ }
138
+ :where(.ma-checkdraw__input:checked),
139
+ :where(.ma-checkdraw__input:indeterminate) {
140
+ background: Highlight;
141
+ border-color: Highlight;
142
+ }
143
+ :where(.ma-checkdraw__mark) {
144
+ stroke: HighlightText;
145
+ }
146
+ :where(.ma-checkdraw__input:disabled) {
147
+ border-color: GrayText;
148
+ opacity: 1;
149
+ }
150
+ }
151
+ @media (prefers-reduced-motion: reduce) {
152
+ :where(.ma-checkdraw__input),
153
+ :where(.ma-checkdraw__tick),
154
+ :where(.ma-checkdraw__bar) {
155
+ transition: none;
156
+ }
157
+ }
158
+ }
159
+ </style>
@@ -0,0 +1,37 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'CheckDraw',
5
+ slug: 'check-draw',
6
+ title: 'Check Draw',
7
+ description: 'A checkbox whose tick is drawn. A label wraps a native input with appearance: none and an SVG beside it: the box fills on the fast token, then the tick (a path with pathLength 1) runs its dash offset from 1 to 0 on the preset curve. The indeterminate state draws a bar instead. The value submits, required and disabled work, the keyboard is the browser\'s. Zero JavaScript.',
8
+ summary: 'A checkbox whose tick draws itself when checked, on a native input that submits, validates and takes the keyboard.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'widely', needs: [] },
13
+ readout: '<CheckDraw name="terms" label="I agree" required>',
14
+ usage: `<CheckDraw name="terms" label="I agree to the terms" required />\n<CheckDraw name="news" label="Send me the newsletter" checked />\n\n<!-- indeterminate is a DOM property, set from your script -->\n<CheckDraw id="all" name="all" label="Select all" />\n<script>document.getElementById('all').indeterminate = true</script>`,
15
+ props: [
16
+ { name: 'name', type: 'string', description: 'Form field name.' },
17
+ { name: 'value', type: 'string', default: "'on'", description: 'Submitted value.' },
18
+ { name: 'checked', type: 'boolean', default: 'false', description: 'Initial state.' },
19
+ { name: 'label', type: 'string', description: 'Visible label; the whole label toggles the box. Without it, pass aria-label.' },
20
+ { name: 'size', type: 'string', default: "'1.25rem'", description: 'Box size, any CSS length; the stroke scales with it.' },
21
+ { name: 'color', type: 'string', description: 'Box colour when checked; default the ink token.' },
22
+ { name: '--ma-checkdraw-edge', type: 'CSS colour (custom property)', default: 'the ink token at 45 %', description: 'The unchecked box\'s border. Set it in a rule or style on the component or any ancestor that knows its ground. Use a border measured at 3 : 1 against it (WCAG 1.4.11) where 45 % of the ink falls short, for example on a coloured panel.' },
23
+ ],
24
+ reducedMotion: 'The box fills and the tick appears at once.',
25
+ clientRouter: 'A native input; its state travels with the page. Nothing to bind.',
26
+ craft: [
27
+ 'The input stays a checkbox in every sense (FormData, required, disabled, the label click, Space, the accessibility tree), and appearance: none only removes the native drawing. The SVG is a sibling that reads :checked and :indeterminate through the general sibling combinator; no :has() needed.',
28
+ 'pathLength="1" makes the dash arithmetic unit-free: dasharray 1, dashoffset 1 is a hidden stroke, 0 is the whole tick, whatever the box size. The tick is drawn on the preset curve after a delay of half the fast token, so the box has filled and the stroke lands on ink.',
29
+ 'Unchecking runs the offset back on the exit curve in the fast duration: the tick leaves the way it came, quickly, and the box empties with it.',
30
+ 'indeterminate is a DOM property, not an attribute, so the bar has no server state; the stage sets it from its script the way a "select all" control would.',
31
+ 'forced-colors rules keep the box visible under Windows high contrast: a ButtonText border, Highlight when checked, the stroke in HighlightText.',
32
+ ],
33
+ related: ['switch', 'animated-icons', 'stateful-button'],
34
+ intents: ['animated checkbox', 'checkbox with drawn check', 'checkmark animation', 'custom checkbox css', 'indeterminate checkbox', 'checkbox tick draws itself'],
35
+ replaces: ['Checkbox (Motion Primitives)', 'Checkbox (shadcn / Radix)', 'AnimatedCheckbox (Aceternity)'],
36
+ fixture: { props: { name: 'ma-fixture', label: 'Fixture content for CheckDraw' }, expect: 'Fixture content for CheckDraw' },
37
+ } satisfies ComponentMeta;
@@ -0,0 +1,103 @@
1
+ ---
2
+ /**
3
+ * CircularText — a badge of text on a ring, turning slowly around whatever
4
+ * sits in the middle. SVG <textPath> on a circle, justified to the full
5
+ * circumference with textLength, spun by one CSS rotation on the compositor.
6
+ * Zero JS. The centre is the slot.
7
+ */
8
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
9
+
10
+ interface Props extends HTMLAttributes<'span'> {
11
+ /** The text. Ends with a separator (· or —) so the loop has no seam. */
12
+ text: string;
13
+ /** Element to render. */
14
+ as?: HTMLTag;
15
+ /** Ring diameter, any CSS length. */
16
+ size?: string;
17
+ /** Seconds per full turn. */
18
+ duration?: number;
19
+ /** Turn direction. */
20
+ direction?: 'cw' | 'ccw';
21
+ /** Id for the circle path; set it when the same text appears twice on a page. */
22
+ id?: string;
23
+ }
24
+
25
+ const { text, as: Tag = 'span', size = '10em', duration = 18, direction = 'cw', id, class: className, style, ...rest } = Astro.props;
26
+ // the same text always draws the same path id, so the HTML is stable across builds
27
+ const hash = [...text].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);
28
+ const pathId = id ?? `ma-circ-${hash.toString(36)}`;
29
+ const R = 40;
30
+ const circumference = +(2 * Math.PI * R).toFixed(2);
31
+ // font size in viewBox units so the string fits the ring once: 0.62 em per character is Inter's average
32
+ const chars = Array.from(new Intl.Segmenter().segment(text)).length;
33
+ const fontSize = +Math.min(13, circumference / (chars * 0.62)).toFixed(2);
34
+ const vars = [`--ma-circ-size:${size}`, `--ma-circ-dur:${duration}s`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
35
+ ---
36
+
37
+ <Tag class:list={['ma-circ', className]} data-ccw={direction === 'ccw' ? '' : undefined} style={vars} {...rest}>
38
+ <span class="ma-circ__ring" aria-hidden="true">
39
+ <svg viewBox="0 0 100 100">
40
+ <defs><path id={pathId} d={`M50,50 m-${R},0 a${R},${R} 0 1,1 ${2 * R},0 a${R},${R} 0 1,1 -${2 * R},0`} /></defs>
41
+ <text font-size={fontSize}><textPath href={`#${pathId}`} textLength={circumference} lengthAdjust="spacing">{text}</textPath></text>
42
+ </svg>
43
+ </span>
44
+ <span class="ma-circ__center"><slot /></span>
45
+ <span class="ma-sr">{text}</span>
46
+ </Tag>
47
+
48
+ <style is:global>
49
+ @layer components {
50
+ :where(.ma-circ) {
51
+ position: relative;
52
+ display: inline-grid;
53
+ place-items: center;
54
+ width: var(--ma-circ-size, 10em);
55
+ height: var(--ma-circ-size, 10em);
56
+ vertical-align: middle;
57
+ }
58
+ /* the turn is on an HTML wrapper, not the <svg>: a transform on the SVG root is not always composited */
59
+ :where(.ma-circ__ring) {
60
+ grid-area: 1 / 1;
61
+ display: block;
62
+ width: 100%;
63
+ height: 100%;
64
+ /* linear on purpose: continuous rotation, no spring */
65
+ animation: ma-circ-spin var(--ma-circ-dur, var(--ma-dur-ambient)) linear infinite;
66
+ will-change: rotate;
67
+ }
68
+ /* direction: ltr. The path runs clockwise from the left; a right-to-left page laid the text out backwards from the
69
+ path's start, off the path, and the ring was empty. */
70
+ :where(.ma-circ__ring > svg) {
71
+ display: block;
72
+ width: 100%;
73
+ height: 100%;
74
+ overflow: visible;
75
+ direction: ltr;
76
+ fill: currentColor;
77
+ font-family: inherit;
78
+ font-weight: inherit;
79
+ letter-spacing: inherit;
80
+ text-transform: inherit;
81
+ }
82
+ :where(.ma-circ[data-ccw] .ma-circ__ring) {
83
+ animation-direction: reverse;
84
+ }
85
+ :where(.ma-circ__center) {
86
+ grid-area: 1 / 1;
87
+ display: grid;
88
+ place-items: center;
89
+ }
90
+ @media (prefers-reduced-motion: reduce) {
91
+ :where(.ma-circ__ring) {
92
+ animation: none;
93
+ will-change: auto;
94
+ }
95
+ }
96
+ }
97
+
98
+ @keyframes ma-circ-spin {
99
+ to {
100
+ rotate: 1turn;
101
+ }
102
+ }
103
+ </style>
@@ -0,0 +1,38 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'CircularText',
5
+ slug: 'circular-text',
6
+ title: 'Circular Text',
7
+ description:
8
+ 'A ring of text turning slowly around an icon, a logo, or nothing. SVG text on a circle, justified to the full circumference so the loop has no seam, spun by one compositor rotation. Pure CSS; the centre is the slot.',
9
+ summary: 'A ring of text that turns slowly around an icon or a logo, set on a circle so the loop has no gap.',
10
+ category: 'text',
11
+ tier: 'A',
12
+ trigger: ['always'],
13
+ support: { baseline: 'widely', needs: [] },
14
+ readout: '<CircularText duration={18}>',
15
+ usage: `<CircularText text="MEASURED · NOT ESTIMATED · ZERO BYTES · " size="9rem" class="kicker">\n <svg width="28" height="28" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" /></svg>\n</CircularText>`,
16
+ props: [
17
+ { name: 'text', type: 'string', description: 'The text. End it with a separator (· or an em dash) so the loop closes without a seam; 20–40 characters fill the ring at a readable size.' },
18
+ { name: 'as', type: 'HTMLTag', default: "'span'", description: 'Element to render.' },
19
+ { name: 'size', type: 'string', default: "'10em'", description: 'Ring diameter, any CSS length.' },
20
+ { name: 'duration', type: 'number', default: '18', description: 'Seconds per full turn. Under 8 reads as a spinner, not a badge.' },
21
+ { name: 'direction', type: "'cw' | 'ccw'", default: "'cw'", description: 'Turn direction.' },
22
+ { name: 'id', type: 'string', description: 'Id for the circle path, derived from the text by default; set it when the same text appears twice on one page.' },
23
+ ],
24
+ reducedMotion: 'The ring is still.',
25
+ clientRouter: 'CSS-only; nothing to rebind.',
26
+ craft: [
27
+ 'textLength on the textPath with lengthAdjust="spacing" stretches only the letter-spacing: the glyphs keep their shape, the string fills the circumference exactly once, and the seam is a normal word gap.',
28
+ 'The font size is derived from the character count on the server (0.62 em per character, counted in graphemes), so a longer badge gets smaller instead of overlapping itself.',
29
+ 'One rotation on an HTML wrapper around the <svg>, not on the <svg> itself, whose transform is not composited in every engine: the text is rasterised once and turns as a texture.',
30
+ 'The ring is laid out left to right whatever the page\'s direction: the path starts at the left and runs clockwise, and under dir="rtl" the text ran backwards off the path\'s start and the ring was empty.',
31
+ 'Linear timing: a badge that eases would look like it keeps stopping.',
32
+ 'The ring is aria-hidden and a visually hidden span (.ma-sr) carries the sentence, so a screen reader reads the sentence once.',
33
+ ],
34
+ related: ['marquee', 'weight-wave', 'shiny-text'],
35
+ intents: ['circular text', 'text on a circle', 'rotating badge text', 'spinning text ring', 'text around icon'],
36
+ replaces: ['CircularText (React Bits)', 'rotating badges (Magic UI, Animata)'],
37
+ fixture: { props: { text: 'Fixture content for CircularText · ' }, expect: 'ma-sr">Fixture content for CircularText · </span>' },
38
+ } satisfies ComponentMeta;
@@ -0,0 +1,213 @@
1
+ ---
2
+ /**
3
+ * CodeTabs — a code block with tabs (npm / pnpm / bun …) and a copy button
4
+ * per panel. Highlighted at build by Astro's own <Code> (Shiki) with a
5
+ * light and a dark theme as CSS variables, so the block follows the page's
6
+ * theme with no script; every token colour is moved at build until it reads at
7
+ * 4.5 : 1 on the panel (lib/contrast), whichever themes are passed. The tabs are the Tabs pattern — radio inputs and
8
+ * :has() — so arrow keys, focus and one-selected are native and the
9
+ * indicator slides on the preset. The only script is CopyButton's, shared.
10
+ * Long lines scroll inside the panel; the page never widens.
11
+ */
12
+ import type { HTMLAttributes } from 'astro/types';
13
+ import { Code } from 'astro:components';
14
+ import CopyButton from './CopyButton.astro';
15
+ import { readableCode } from '../lib/contrast';
16
+
17
+ interface Tab {
18
+ label: string;
19
+ code: string;
20
+ /** Shiki language id. */
21
+ lang?: string;
22
+ }
23
+
24
+ interface Props extends HTMLAttributes<'div'> {
25
+ tabs: Tab[];
26
+ /** Radio group name and id prefix; unique per page. */
27
+ id: string;
28
+ /** Accessible name of the group, e.g. "Package manager". */
29
+ label?: string;
30
+ active?: number;
31
+ /** Shiki themes for light and dark. */
32
+ themes?: { light: string; dark: string };
33
+ }
34
+
35
+ const { tabs, id, label, active = 0, themes = { light: 'github-light', dark: 'github-dark-default' }, class: className, style, ...rest } = Astro.props;
36
+ const list = tabs.slice(0, 8);
37
+ const vars = [`--ma-codetabs-n:${list.length}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
38
+ ---
39
+
40
+ <div class:list={['ma-codetabs', className]} style={vars} {...rest}>
41
+ <div class="ma-codetabs__bar">
42
+ <div class="ma-codetabs__list" role="radiogroup" aria-label={label}>
43
+ {list.map((t, i) => (
44
+ <>
45
+ <input type="radio" name={id} id={`${id}-${i}`} class="ma-codetabs__input" checked={i === active} />
46
+ <label for={`${id}-${i}`} class="ma-codetabs__tab">{t.label}</label>
47
+ </>
48
+ ))}
49
+ <span class="ma-codetabs__indicator" aria-hidden="true"></span>
50
+ </div>
51
+ <div class="ma-codetabs__copies">
52
+ {list.map((_, i) => <span class="ma-codetabs__copy"><CopyButton target={`#${id}-p${i} pre`} compact /></span>)}
53
+ </div>
54
+ </div>
55
+ <div class="ma-codetabs__panels">
56
+ {list.map((t, i) => (
57
+ <div class="ma-codetabs__panel" id={`${id}-p${i}`}>
58
+ <Code code={t.code} lang={(t.lang ?? 'sh') as never} themes={themes as never} defaultColor={false} transformers={[readableCode()]} />
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+
64
+ <style is:global>
65
+ @layer components {
66
+ :where(.ma-codetabs) {
67
+ display: grid;
68
+ /* the block's own width, not its widest line's: the tab list scrolls sideways and the code scrolls in its <pre> */
69
+ grid-template-columns: minmax(0, 1fr);
70
+ overflow: clip;
71
+ border: 1px solid var(--ma-edge);
72
+ border-radius: 0.75rem;
73
+ background: var(--ma-panel);
74
+ color: var(--ma-ink);
75
+ font-size: 0.875rem;
76
+ }
77
+ :where(.ma-codetabs__bar) {
78
+ display: flex;
79
+ align-items: stretch;
80
+ justify-content: space-between;
81
+ gap: 0.5rem;
82
+ border-bottom: 1px solid var(--ma-edge);
83
+ padding-inline-end: 0.4rem;
84
+ }
85
+ :where(.ma-codetabs__list) {
86
+ position: relative;
87
+ display: grid;
88
+ grid-auto-flow: column;
89
+ grid-auto-columns: 1fr; /* equal in an auto-sized grid: every fr track takes the widest label */
90
+ min-width: 0;
91
+ overflow-x: auto;
92
+ scrollbar-width: none;
93
+ }
94
+ :where(.ma-codetabs__input) {
95
+ position: absolute;
96
+ width: 1px;
97
+ height: 1px;
98
+ overflow: hidden;
99
+ clip-path: inset(50%);
100
+ white-space: nowrap;
101
+ }
102
+ :where(.ma-codetabs__tab) {
103
+ padding: 0.7rem 0.9rem;
104
+ font-family: ui-monospace, monospace;
105
+ font-size: 0.8125em;
106
+ opacity: var(--ma-dim, 0.7);
107
+ cursor: pointer;
108
+ white-space: nowrap;
109
+ transition: opacity var(--ma-duration-fast) var(--ma-ease-out);
110
+ }
111
+ :where(.ma-codetabs__tab:hover),
112
+ :where(.ma-codetabs__input:checked + .ma-codetabs__tab) {
113
+ opacity: 1;
114
+ }
115
+ :where(.ma-codetabs__input:focus-visible + .ma-codetabs__tab) {
116
+ outline: 2px solid currentColor;
117
+ outline-offset: -3px;
118
+ border-radius: 0.375rem;
119
+ }
120
+ /* the indicator is as wide as one tab and translates by whole tabs — tabs share one width so no measuring is needed */
121
+ :where(.ma-codetabs__indicator) {
122
+ position: absolute;
123
+ inset-inline-start: 0;
124
+ bottom: 0;
125
+ width: calc(100% / var(--ma-codetabs-n, 1));
126
+ height: 2px;
127
+ background: currentColor;
128
+ translate: calc(var(--ma-codetab, 0) * 100% * var(--ma-codetabs-dir, 1)) 0;
129
+ transition: translate var(--ma-duration) var(--ma-ease);
130
+ }
131
+ /* right to left the tabs run from the right edge, so the indicator steps left. [dir] and not :dir(): :dir() reached Chrome in 120, and a build that targets an older Chrome rewrites it into a guess from lang, which a page that sets only dir never matches. An
132
+ ltr island one level inside an rtl page is set back */
133
+ :where([dir='rtl'] .ma-codetabs, .ma-codetabs[dir='rtl']) { --ma-codetabs-dir: -1; }
134
+ :where([dir='rtl'] [dir='ltr'] .ma-codetabs, [dir='rtl'] .ma-codetabs[dir='ltr']) { --ma-codetabs-dir: 1; }
135
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(2):checked)) { --ma-codetab: 1; }
136
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(3):checked)) { --ma-codetab: 2; }
137
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(4):checked)) { --ma-codetab: 3; }
138
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(5):checked)) { --ma-codetab: 4; }
139
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(6):checked)) { --ma-codetab: 5; }
140
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(7):checked)) { --ma-codetab: 6; }
141
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(8):checked)) { --ma-codetab: 7; }
142
+ :where(.ma-codetabs__copies) {
143
+ display: grid;
144
+ align-self: center;
145
+ flex: none;
146
+ }
147
+ /* nth-of-type, not nth-child: Astro renders the component's <script> tag next to the first CopyButton. The wrapper is
148
+ this component's own element (7F): a display rule on the button itself competed with CopyButton's own
149
+ `display` at equal specificity, and which one won depended on the order the bundler gave the two stylesheets. */
150
+ :where(.ma-codetabs__copy) {
151
+ grid-area: 1 / 1;
152
+ display: none;
153
+ }
154
+ :where(.ma-codetabs__copy .ma-copy) {
155
+ border-color: transparent;
156
+ background: transparent;
157
+ }
158
+ :where(.ma-codetabs__panel) {
159
+ display: none;
160
+ opacity: 1;
161
+ translate: 0 0;
162
+ transition:
163
+ opacity var(--ma-duration) var(--ma-ease),
164
+ translate var(--ma-duration) var(--ma-ease);
165
+ }
166
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(1):checked) .ma-codetabs__panel:nth-child(1)),
167
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(2):checked) .ma-codetabs__panel:nth-child(2)),
168
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(3):checked) .ma-codetabs__panel:nth-child(3)),
169
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(4):checked) .ma-codetabs__panel:nth-child(4)),
170
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(5):checked) .ma-codetabs__panel:nth-child(5)),
171
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(6):checked) .ma-codetabs__panel:nth-child(6)),
172
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(7):checked) .ma-codetabs__panel:nth-child(7)),
173
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(8):checked) .ma-codetabs__panel:nth-child(8)) {
174
+ display: block;
175
+ }
176
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(1):checked) .ma-codetabs__copy:nth-of-type(1)),
177
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(2):checked) .ma-codetabs__copy:nth-of-type(2)),
178
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(3):checked) .ma-codetabs__copy:nth-of-type(3)),
179
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(4):checked) .ma-codetabs__copy:nth-of-type(4)),
180
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(5):checked) .ma-codetabs__copy:nth-of-type(5)),
181
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(6):checked) .ma-codetabs__copy:nth-of-type(6)),
182
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(7):checked) .ma-codetabs__copy:nth-of-type(7)),
183
+ :where(.ma-codetabs:has(.ma-codetabs__input:nth-of-type(8):checked) .ma-codetabs__copy:nth-of-type(8)) {
184
+ display: inline-flex;
185
+ }
186
+ @starting-style {
187
+ :where(.ma-codetabs__panel) {
188
+ opacity: 0;
189
+ translate: 0 var(--ma-travel-hover, 4px);
190
+ }
191
+ }
192
+ :where(.ma-codetabs__panel > pre) {
193
+ margin: 0;
194
+ padding: 1rem;
195
+ overflow-x: auto;
196
+ background: transparent !important;
197
+ font-size: 0.8125rem;
198
+ line-height: 1.6;
199
+ tab-size: 2;
200
+ }
201
+ /* Shiki with defaultColor: false leaves the colours in two variables per token; --ma-dark (0 or 1 from the base tokens'
202
+ three-state rule) picks one, so the block follows system, data-theme and .dark exactly as every other token does */
203
+ :where(.ma-codetabs__panel pre span) {
204
+ color: color-mix(in srgb, var(--shiki-dark) calc(var(--ma-dark, 0) * 100%), var(--shiki-light));
205
+ }
206
+ @media (prefers-reduced-motion: reduce) {
207
+ :where(.ma-codetabs__indicator),
208
+ :where(.ma-codetabs__panel) {
209
+ transition: none;
210
+ }
211
+ }
212
+ }
213
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'CodeTabs',
5
+ slug: 'code-tabs',
6
+ title: 'Code Tabs',
7
+ description: 'A code block with tabs (for example npm, pnpm, bun) and a copy button per panel. Highlighted at build by Astro\'s own <Code> (Shiki) with light and dark themes as CSS variables, so it follows the page theme with no script. The tabs are radio inputs and :has(), so arrow keys and selection are native and the indicator slides on the preset. The only script is CopyButton\'s, shared; long lines scroll inside the panel.',
8
+ summary: 'Code tabs for npm, pnpm or bun with a copy button on each panel and Shiki highlighting that follows the theme.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'widely', needs: ['has', 'starting-style', 'clipboard'], fallback: 'panels switch without the fade' },
13
+ readout: '<CodeTabs id="install" tabs={[…]}>',
14
+ usage: `<CodeTabs\n id="install"\n tabs={[\n { label: 'npm', code: 'npx astro add moonarc' },\n { label: 'pnpm', code: 'pnpm astro add moonarc' },\n { label: 'bun', code: 'bunx astro add moonarc' },\n ]}\n/>`,
15
+ props: [
16
+ { name: 'tabs', type: '{ label: string; code: string; lang?: string }[]', description: 'Up to eight tabs; lang is a Shiki language id, sh by default.' },
17
+ { name: 'id', type: 'string', description: 'Radio group name and id prefix; unique per page.' },
18
+ { name: 'label', type: 'string', description: 'Accessible name of the radio group ("Package manager").' },
19
+ { name: 'active', type: 'number', default: '0', description: 'Index selected on load.' },
20
+ { name: 'themes', type: '{ light: string; dark: string }', default: 'github-light / github-dark-default', description: 'Shiki themes; both are emitted as variables per token, each colour moved until it reads at 4.5 : 1 on its panel.' },
21
+ ],
22
+ reducedMotion: 'The indicator jumps; panels switch instantly.',
23
+ clientRouter: 'The tabs are CSS; CopyButton rebinds through the shared runtime.',
24
+ craft: [
25
+ 'Highlighting happens at build: the HTML carries the colours as two custom properties per token, and one rule per theme state picks one. Zero bytes of highlighter in the browser.',
26
+ 'Contrast is fixed at build: a transformer (lib/contrast) moves each token colour in OKLCH lightness, hue kept, until it reads at 4.5 : 1 on a panel a shade heavier than the default. As shipped, GitHub light\'s red and orange, and every grey comment, fall short of AA. Colours that already pass are untouched.',
27
+ 'The tabs are a radiogroup, not a tablist: the controls are radios, and a screen reader announces them as radios ("pnpm, radio button, 2 of 3"). label names the group.',
28
+ 'Every panel has its own CopyButton, shown only with its panel (:has() again), and each copies its own <pre>, so no script decides which tab is open.',
29
+ 'Tabs are equal-width columns in an auto-sized grid, so the indicator is one translate of whole widths; no measuring.',
30
+ 'The panel scrolls horizontally inside itself (min-width: 0 on the grid item, overflow-x on the pre), so a long install line never widens a 390 px page.',
31
+ ],
32
+ related: ['tabs', 'copy-button', 'browser-frame'],
33
+ intents: ['code block with tabs', 'npm pnpm yarn tabs', 'install command tabs', 'code tabs with copy button', 'shiki code tabs astro'],
34
+ replaces: ['CodeTabs (Fumadocs)', 'CodeBlock tabs (Aceternity)', 'Tabs + CodeBlock (shadcn)'],
35
+ fixture: { props: { id: 'ma-fixture-codetabs', tabs: [{ label: 'npm', code: 'echo "Fixture content for CodeTabs"' }, { label: 'pnpm', code: 'pnpm x' }] }, expect: 'Fixture content for CodeTabs' },
36
+ } satisfies ComponentMeta;