@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,196 @@
1
+ ---
2
+ /**
3
+ * Drawer — a bottom sheet you can drag closed.
4
+ *
5
+ * A native <dialog> for the top layer, focus and Escape; the invoker
6
+ * command opens it and @starting-style / allow-discrete animate it. The
7
+ * script adds one thing CSS cannot: the sheet follows the finger, and on
8
+ * release either springs back (preset curve) or leaves — by distance or
9
+ * by velocity — with the transition interrupted mid-flight if you grab it.
10
+ */
11
+ import type { HTMLAttributes } from 'astro/types';
12
+
13
+ interface Props extends HTMLAttributes<'dialog'> {
14
+ /** Dialog id; the trigger points at it. */
15
+ id: string;
16
+ /** Trigger label; or use the `trigger` slot. */
17
+ label?: string;
18
+ /** Sheet height, any CSS length. */
19
+ height?: string;
20
+ /** Fraction of the height that must be dragged to dismiss. */
21
+ threshold?: number;
22
+ /** Add the inline click handler for browsers without invoker commands. */
23
+ polyfill?: boolean;
24
+ }
25
+
26
+ const { id, label = 'Open', height = 'auto', threshold = 0.35, polyfill = false, class: className, style, ...rest } = Astro.props;
27
+ const inline = [`--ma-drawer-h:${height}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
28
+ const triggerAttrs = { command: 'show-modal', commandfor: id };
29
+ const dialogAttrs = { closedby: 'any' as const };
30
+ ---
31
+
32
+ {Astro.slots.has('trigger') ? <slot name="trigger" /> : <button type="button" class="ma-drawer__trigger" {...triggerAttrs}>{label}</button>}
33
+ <dialog id={id} class:list={['ma-drawer', className]} data-ma-drawer data-threshold={threshold} style={inline} {...dialogAttrs} {...rest}>
34
+ <div class="ma-drawer__sheet">
35
+ <div class="ma-drawer__handle" aria-hidden="true"></div>
36
+ <div class="ma-drawer__body">
37
+ <slot />
38
+ </div>
39
+ </div>
40
+ </dialog>
41
+ {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>}
42
+
43
+ <style is:global>
44
+ @layer components {
45
+ :where(.ma-drawer) {
46
+ inset: auto 0 0;
47
+ margin: 0 auto;
48
+ width: 100%;
49
+ max-width: 40rem;
50
+ max-height: 90dvh;
51
+ height: var(--ma-drawer-h, auto);
52
+ border: 0;
53
+ padding: 0;
54
+ background: transparent;
55
+ color: inherit;
56
+ overflow: visible;
57
+ /* --ma-drag: live pointer offset written by the script */
58
+ translate: 0 var(--ma-drag, 0px);
59
+ transition:
60
+ translate var(--ma-duration-fast) var(--ma-ease-in),
61
+ display var(--ma-duration-fast) allow-discrete,
62
+ overlay var(--ma-duration-fast) allow-discrete;
63
+ }
64
+ :where(.ma-drawer:not([open])) {
65
+ translate: 0 100%;
66
+ }
67
+ :where(.ma-drawer[open]) {
68
+ transition-duration: var(--ma-duration);
69
+ transition-timing-function: var(--ma-ease-drawer);
70
+ }
71
+ :where(.ma-drawer[data-dragging]) {
72
+ transition: none;
73
+ }
74
+ @starting-style {
75
+ :where(.ma-drawer[open]) {
76
+ translate: 0 100%;
77
+ }
78
+ }
79
+ :where(.ma-drawer)::backdrop {
80
+ background: var(--ma-scrim);
81
+ opacity: 0;
82
+ transition:
83
+ opacity var(--ma-duration-fast) var(--ma-ease-out),
84
+ display var(--ma-duration-fast) allow-discrete,
85
+ overlay var(--ma-duration-fast) allow-discrete;
86
+ }
87
+ :where(.ma-drawer[open])::backdrop {
88
+ opacity: 1;
89
+ transition-duration: var(--ma-duration);
90
+ }
91
+ @starting-style {
92
+ :where(.ma-drawer[open])::backdrop {
93
+ opacity: 0;
94
+ }
95
+ }
96
+ :where(.ma-drawer__sheet) {
97
+ display: grid;
98
+ grid-template-rows: auto 1fr;
99
+ max-height: 90dvh;
100
+ border-radius: 1.25rem 1.25rem 0 0;
101
+ background: var(--ma-panel);
102
+ border: 1px solid var(--ma-edge);
103
+ border-bottom: 0;
104
+ box-shadow: 0 -24px 64px -32px rgb(0 0 0 / 0.5);
105
+ touch-action: none;
106
+ }
107
+ :where(.ma-drawer__handle) {
108
+ width: 2.5rem;
109
+ height: 0.3rem;
110
+ margin: 0.75rem auto 0.25rem;
111
+ border-radius: 999px;
112
+ background: var(--ma-edge);
113
+ cursor: grab;
114
+ }
115
+ :where(.ma-drawer__body) {
116
+ padding: 1rem 1.5rem 1.5rem;
117
+ overflow: auto;
118
+ touch-action: pan-y;
119
+ }
120
+ :where(.ma-drawer__trigger) {
121
+ cursor: pointer;
122
+ }
123
+ @media (prefers-reduced-motion: reduce) {
124
+ :where(.ma-drawer),
125
+ :where(.ma-drawer[open]),
126
+ :where(.ma-drawer)::backdrop {
127
+ transition-duration: var(--ma-duration-fast);
128
+ transition-timing-function: linear;
129
+ }
130
+ }
131
+ }
132
+ </style>
133
+
134
+ <script>
135
+ import { onMount } from '../lib/runtime';
136
+
137
+ onMount<HTMLDialogElement>('[data-ma-drawer]', (dialog, { signal }) => {
138
+ const sheet = dialog.querySelector<HTMLElement>('.ma-drawer__sheet');
139
+ const body = dialog.querySelector<HTMLElement>('.ma-drawer__body');
140
+ if (!sheet || !body) return;
141
+ const threshold = Number(dialog.dataset.threshold ?? 0.35);
142
+ let startY = 0;
143
+ let lastY = 0;
144
+ let lastT = 0;
145
+ let v = 0;
146
+ // 0 idle, 1 pressed, 2 dragging
147
+ let state = 0;
148
+
149
+ sheet.addEventListener(
150
+ 'pointerdown',
151
+ (e) => {
152
+ // a scrolled body scrolls; only a body at the top hands the gesture to the sheet
153
+ if (body.contains(e.target as Node) && body.scrollTop > 0) return;
154
+ state = 1;
155
+ startY = lastY = e.clientY;
156
+ lastT = e.timeStamp;
157
+ v = 0;
158
+ },
159
+ { signal },
160
+ );
161
+ sheet.addEventListener(
162
+ 'pointermove',
163
+ (e) => {
164
+ if (!state) return;
165
+ const dy = Math.max(0, e.clientY - startY);
166
+ // a press is a click until it travels 6 px: only then does the sheet take the pointer, so a button, a link or a
167
+ // checkbox inside keeps its click (captured on pointerdown, the click went to the sheet)
168
+ if (state < 2) {
169
+ if (dy < 6) return;
170
+ state = 2;
171
+ dialog.dataset.dragging = '';
172
+ sheet.setPointerCapture(e.pointerId);
173
+ }
174
+ const dt = e.timeStamp - lastT || 1;
175
+ v = (e.clientY - lastY) / dt; // px per ms, positive = downward
176
+ lastY = e.clientY;
177
+ lastT = e.timeStamp;
178
+ dialog.style.setProperty('--ma-drag', `${dy}px`);
179
+ },
180
+ { signal },
181
+ );
182
+ const release = (e: PointerEvent) => {
183
+ const was = state;
184
+ state = 0;
185
+ if (was < 2) return;
186
+ delete dialog.dataset.dragging;
187
+ const dy = Math.max(0, e.clientY - startY);
188
+ const h = dialog.getBoundingClientRect().height || 1;
189
+ dialog.style.removeProperty('--ma-drag');
190
+ if (dy / h > threshold || v > 0.6) dialog.close();
191
+ };
192
+ // on the dialog: a press let go over the backdrop before it became a drag ends there, uncaptured
193
+ dialog.addEventListener('pointerup', release, { signal });
194
+ dialog.addEventListener('pointercancel', release, { signal });
195
+ });
196
+ </script>
@@ -0,0 +1,35 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'Drawer',
5
+ slug: 'drawer',
6
+ title: 'Drawer',
7
+ description: 'A bottom sheet that slides up over a scrim and can be dragged closed: it follows the finger, and on release springs back or leaves depending on distance and velocity, interruptible mid-flight. A native <dialog> does the top layer, focus and Escape; the script is only the gesture.',
8
+ summary: 'A bottom sheet that slides up and can be dragged closed. On release it springs back or leaves, by distance and speed.',
9
+ category: 'ui',
10
+ tier: 'C',
11
+ trigger: ['click', 'pointer'],
12
+ support: { baseline: 'newly', needs: ['command', 'starting-style', 'allow-discrete', 'dialog'], fallback: 'without invoker commands the trigger needs polyfill; without closedby (Safari) a tap on the scrim does nothing, and the drag and Escape still close the sheet' },
13
+ readout: '<Drawer threshold={0.35}>',
14
+ usage: `<Drawer id="filters" label="Filters" height="60dvh">\n <h2>Filters</h2>\n <p>Drag down to dismiss.</p>\n</Drawer>`,
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: 'height', type: 'string', default: "'auto'", description: 'Sheet height, up to 90 dvh.' },
19
+ { name: 'threshold', type: 'number', default: '0.35', description: 'Fraction of the height that must be dragged to dismiss; a fast flick dismisses regardless.' },
20
+ { name: 'polyfill', type: 'boolean', default: 'false', description: 'Inline a click handler (403 B raw) for browsers without invoker commands. Document-level: one per page is enough.' },
21
+ ],
22
+ reducedMotion: 'Opens and closes with a 150 ms linear slide; dragging still works because it is direct manipulation.',
23
+ clientRouter: 'Bound through the shared runtime; pointer listeners are aborted before the swap and a sheet open during navigation closes with the page.',
24
+ craft: [
25
+ 'While dragging, transition: none, so the sheet stays under the finger. On release the transition returns, so the spring-back is the drawer curve.',
26
+ 'Two ways out: 35% of the height, or 0.6 px/ms. A short fast flick should close; a long slow drag that stops short should not.',
27
+ 'The body scrolls; the sheet only takes the gesture when the body is at its top, so scrolling content never accidentally dismisses.',
28
+ 'A press becomes a drag after 6 px of travel, and only then does the sheet capture the pointer: until then it is a click, so a button, a link or a checkbox inside the sheet works with a mouse, a pen or a finger.',
29
+ 'Exit is ease-in at the fast duration: things leaving accelerate away.',
30
+ ],
31
+ related: ['dialog', 'popover', 'toast'],
32
+ intents: ['bottom sheet', 'drawer', 'draggable sheet', 'swipe to dismiss', 'mobile drawer astro', 'vaul alternative'],
33
+ replaces: ['Vaul', 'Drawer (shadcn)', 'react-spring-bottom-sheet'],
34
+ fixture: { props: { id: 'ma-fixture-drawer', label: 'Fixture content for Drawer' }, expect: 'Fixture content for Drawer' },
35
+ } satisfies ComponentMeta;
@@ -0,0 +1,122 @@
1
+ ---
2
+ /**
3
+ * DynamicIsland — a black pill that changes shape with its state, zero
4
+ * JS. `auto → auto` never animates, so every state declares its size:
5
+ * `states: [{ name, width, height, radius }]` in rem. The active state is
6
+ * `data-state` on the element — set by the prop or by your script — and
7
+ * because the names come from props, one small inline style block is
8
+ * rendered per instance (an id from a hash of the states) that maps each name to its
9
+ * three lengths and shows the matching child. Width, height and radius
10
+ * transition on the preset spring; the old content blurs and fades on the
11
+ * fast token, the new one arrives after it. Children carry `data-state`;
12
+ * the first state is the default.
13
+ */
14
+ import type { HTMLAttributes } from 'astro/types';
15
+
16
+ interface State {
17
+ name: string;
18
+ /** rem */
19
+ width: number;
20
+ /** rem */
21
+ height: number;
22
+ /** rem; default half the height (a pill). */
23
+ radius?: number;
24
+ }
25
+
26
+ interface Props extends HTMLAttributes<'div'> {
27
+ states: State[];
28
+ /** Active state; default the first. */
29
+ state?: string;
30
+ /** Element id — deterministic from the states when omitted; pass one for two identical islands on a page. */
31
+ id?: string;
32
+ /** Announce content changes (aria-live="polite" on the body). */
33
+ live?: boolean;
34
+ /** Island colour; the island is black by nature, not the ink token. */
35
+ color?: string;
36
+ textColor?: string;
37
+ }
38
+
39
+ const { states, state, id, live = false, color, textColor, class: className, style, ...rest } = Astro.props;
40
+ const first = states[0] ?? { name: 'idle', width: 7.5, height: 2.25 };
41
+ // djb2 over the states: the same states give the same id on every build, so a visual test is stable
42
+ const hash = (s: string) => {
43
+ let h = 5381;
44
+ for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
45
+ return h.toString(36);
46
+ };
47
+ const islandId = id ?? `ma-island-${hash(JSON.stringify(states))}`;
48
+ const active = states.some((s) => s.name === state) ? state! : first.name;
49
+ const vars = [color && `--ma-di-bg:${color}`, textColor && `--ma-di-fg:${textColor}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
50
+ // both come from props and are printed into CSS: the id as an identifier (CSS.escape's rules: an id may start with a
51
+ // digit or hold punctuation, and `#2fa` alone is not a selector), each name as a quoted string. `<` is escaped in both,
52
+ // so no value can close the style element it is printed in.
53
+ const hex = (c: string) => `\\${c.codePointAt(0)!.toString(16)} `;
54
+ const ident = (v: string) => v.replace(/^-?\d|[\0-\x1f\x7f]|[^\w\u0080-\uffff-]/g, (c) => (c.length > 1 ? c[0] + hex(c[1]!) : /[\d\0-\x1f\x7f]/.test(c) ? hex(c) : `\\${c}`));
55
+ const str = (v: string) => `"${v.replace(/["\\]/g, '\\$&').replace(/[\n\r\f<]/g, hex)}"`;
56
+ const sel = `#${ident(islandId)}`;
57
+ const rule = (s: State) => {
58
+ const r = s.radius ?? s.height / 2;
59
+ const n = str(s.name);
60
+ return `${sel}[data-state=${n}]{--ma-di-w:${s.width}rem;--ma-di-h:${s.height}rem;--ma-di-r:${r}rem}${sel}[data-state=${n}]>.ma-island__body>[data-state=${n}]{opacity:1;filter:none;scale:1;pointer-events:auto;transition-delay:var(--ma-duration-fast)}`;
61
+ };
62
+ const css = states.map(rule).join('');
63
+ ---
64
+
65
+ <style is:global>
66
+ @layer components {
67
+ :where(.ma-island) {
68
+ --ma-di-w: 7.5rem;
69
+ --ma-di-h: 2.25rem;
70
+ --ma-di-r: 1.125rem;
71
+ display: grid;
72
+ place-items: stretch;
73
+ inline-size: var(--ma-di-w);
74
+ block-size: var(--ma-di-h);
75
+ border-radius: var(--ma-di-r);
76
+ background: var(--ma-di-bg, #0a0a0b);
77
+ color: var(--ma-di-fg, #fff);
78
+ overflow: clip;
79
+ box-shadow: 0 8px 30px -12px rgb(0 0 0 / 0.6);
80
+ transition:
81
+ inline-size var(--ma-duration) var(--ma-ease),
82
+ block-size var(--ma-duration) var(--ma-ease),
83
+ border-radius var(--ma-duration) var(--ma-ease);
84
+ }
85
+ :where(.ma-island__body) {
86
+ display: grid;
87
+ inline-size: 100%;
88
+ block-size: 100%;
89
+ min-inline-size: 0;
90
+ min-block-size: 0;
91
+ }
92
+ /* every state's content sits in the same cell; the active one is picked by the instance's own rules */
93
+ :where(.ma-island__body > [data-state]) {
94
+ grid-area: 1 / 1;
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 0.6em;
98
+ min-inline-size: 0;
99
+ opacity: 0;
100
+ filter: blur(4px);
101
+ scale: 0.92;
102
+ pointer-events: none;
103
+ transition:
104
+ opacity var(--ma-duration-fast) var(--ma-ease-out),
105
+ filter var(--ma-duration-fast) var(--ma-ease-out),
106
+ scale var(--ma-duration-fast) var(--ma-ease-out);
107
+ }
108
+ @media (prefers-reduced-motion: reduce) {
109
+ :where(.ma-island),
110
+ :where(.ma-island__body > [data-state]) {
111
+ transition: none;
112
+ filter: none;
113
+ scale: 1;
114
+ }
115
+ }
116
+ }
117
+ </style>
118
+
119
+ <div class:list={['ma-island', className]} id={islandId} data-state={active} style={vars || undefined} {...rest}>
120
+ <div class="ma-island__body" aria-live={live ? 'polite' : undefined}><slot /></div>
121
+ </div>
122
+ <style is:inline set:html={css}></style>
@@ -0,0 +1,35 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'DynamicIsland',
5
+ slug: 'dynamic-island',
6
+ title: 'Dynamic Island',
7
+ description: 'A black pill that changes shape with its state. auto → auto never animates, so every state declares its width, height and radius in rem; the active one is data-state on the element, set by your script, and a small style block rendered per instance maps each name to its lengths and shows the matching child. The shape transitions on the preset spring; the old content blurs out on the fast token and the new one arrives after it. Zero JavaScript.',
8
+ summary: "A black pill that changes shape with its state, like the iPhone's Dynamic Island, blurring its content out and in.",
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'widely', needs: [] },
13
+ readout: '<DynamicIsland states={states} state="music">',
14
+ usage: `---\nconst states = [\n { name: 'idle', width: 7.5, height: 2.25 },\n { name: 'music', width: 15, height: 3.5 },\n { name: 'call', width: 14, height: 3.25 },\n { name: 'timer', width: 11, height: 5, radius: 1.5 },\n];\n---\n<DynamicIsland id="island" states={states} state="idle" live>\n <span data-state="idle"><i class="dot" /></span>\n <span data-state="music"><img src="/art.jpg" alt="" /> Now playing · Orbit</span>\n <span data-state="call">Ari · 00:42</span>\n <span data-state="timer">12:00</span>\n</DynamicIsland>\n\n<script>\n // your code drives it: one attribute\n document.getElementById('island').dataset.state = 'music';\n</script>`,
15
+ props: [
16
+ { name: 'states', type: '{ name, width, height, radius? }[]', description: 'Every state and its size in rem; radius defaults to half the height. The first is the default.' },
17
+ { name: 'state', type: 'string', default: 'states[0].name', description: 'The active state, as data-state on the element.' },
18
+ { name: 'id', type: 'string', default: 'a hash of the states', description: 'Element id and the selector of the per-instance rules, escaped for CSS (an id may start with a digit). Deterministic when omitted; pass one when two identical islands share a page.' },
19
+ { name: 'live', type: 'boolean', default: 'false', description: 'aria-live="polite" on the body, so a state change is announced.' },
20
+ { name: 'color', type: 'string', default: "'#0a0a0b'", description: 'Island colour; black by default, independent of the ink token.' },
21
+ { name: 'textColor', type: 'string', default: "'#fff'", description: 'Content colour.' },
22
+ ],
23
+ reducedMotion: 'The shape and the content change at once, no spring, no blur.',
24
+ clientRouter: 'Static; the state attribute is on the element and goes with the page. The per-instance style block is part of the page HTML and swaps with it.',
25
+ craft: [
26
+ 'Sizes are declared because they must be: a box that animates from one auto size to another needs a script to measure both, and this one has none. Each state is three lengths, and the transition between them is on the preset spring: lively overshoots, ui settles.',
27
+ 'The names come from props, so the stylesheet cannot know them: the component renders one small style block per instance, keyed by an id hashed from the states, with one rule per state for the lengths and one for the child that shows. It is CSS, printed in the readout.',
28
+ 'Content per state is one default slot with data-state children, because a dynamic slot name inside a map() fails at prerender (7B). Children stack in one grid cell; the inactive ones are blurred, scaled down and transparent, and the active one waits one fast token so the old is gone before it arrives.',
29
+ 'The island stays black in both themes: it is an object on the screen, not a panel of the page. color and textColor override it.',
30
+ ],
31
+ related: ['stateful-button', 'phone-frame', 'toast'],
32
+ intents: ['dynamic island', 'morphing pill notification', 'island state animation', 'pill that changes shape', 'live activity pill', 'expanding notification pill'],
33
+ replaces: ['DynamicIsland (Cult UI)', 'Dynamic Island (Kokonut UI)', 'Toolbar Dynamic (Motion Primitives)'],
34
+ fixture: { props: { id: 'ma-fixture-island', states: [{ name: 'idle', width: 7.5, height: 2.25 }, { name: 'wide', width: 14, height: 3.5 }] }, slot: '<span data-state="idle">Fixture content for DynamicIsland</span><span data-state="wide">Wide</span>', expect: 'Fixture content for DynamicIsland' },
35
+ } satisfies ComponentMeta;
@@ -0,0 +1,207 @@
1
+ ---
2
+ /**
3
+ * FileTree — a project tree of folders that open with an animated height,
4
+ * zero JS.
5
+ *
6
+ * Folders are native <details>: the click, the keyboard and the open state
7
+ * are the browser's; the height animates through ::details-content laid
8
+ * out as a grid whose only row goes 0fr → 1fr (the Accordion technique),
9
+ * the chevron turns and the folder glyph opens. `open` lists the paths to
10
+ * start open; `highlight` the files to light up ("this is the one you
11
+ * change"). Semantics are nested lists — role="tree" is not claimed because
12
+ * arrow-key navigation is not provided. Renders itself for each level.
13
+ */
14
+ import type { HTMLAttributes } from 'astro/types';
15
+
16
+ export interface Node {
17
+ name: string;
18
+ children?: Node[];
19
+ /** Light this row up. */
20
+ highlight?: boolean;
21
+ /** Start open (folders); also settable through the `open` prop by path. */
22
+ open?: boolean;
23
+ }
24
+
25
+ interface Props extends HTMLAttributes<'div'> {
26
+ tree: Node[];
27
+ /** Paths to start open, e.g. ['src', 'src/pages']. */
28
+ open?: string[];
29
+ /** Paths to light up, e.g. ['src/pages/index.astro']. */
30
+ highlight?: string[];
31
+ /** Accessible name of the tree. */
32
+ label?: string;
33
+ /** Internal: the path of this level. */
34
+ path?: string;
35
+ /** Internal: nested level. */
36
+ nested?: boolean;
37
+ }
38
+
39
+ const { tree, open = [], highlight = [], label = 'Files', path = '', nested = false, class: className, ...rest } = Astro.props;
40
+ const join = (a: string, b: string) => (a ? `${a}/${b}` : b);
41
+ const Level = Astro.self;
42
+ ---
43
+
44
+ {
45
+ nested ? (
46
+ <ul class="ma-ftree__list">
47
+ {tree.map((n) => {
48
+ const p = join(path, n.name);
49
+ return n.children ? (
50
+ <li class="ma-ftree__folder">
51
+ <details class="ma-ftree__details" open={n.open || open.includes(p)}>
52
+ <summary class="ma-ftree__row" data-highlight={n.highlight || highlight.includes(p) ? '' : undefined}>
53
+ <svg class="ma-ftree__chevron" aria-hidden="true" viewBox="0 0 16 16" width="12" height="12"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
54
+ <svg class="ma-ftree__glyph" aria-hidden="true" viewBox="0 0 20 20" width="16" height="16">
55
+ <path class="ma-ftree__folder-back" d="M2 5.5A1.5 1.5 0 0 1 3.5 4h4l2 2h7A1.5 1.5 0 0 1 18 7.5v7A1.5 1.5 0 0 1 16.5 16h-13A1.5 1.5 0 0 1 2 14.5z" fill="currentColor" opacity="0.35" />
56
+ <path class="ma-ftree__folder-flap" d="M2 8.5h16v6A1.5 1.5 0 0 1 16.5 16h-13A1.5 1.5 0 0 1 2 14.5z" fill="currentColor" />
57
+ </svg>
58
+ <span class="ma-ftree__name">{n.name}</span>
59
+ </summary>
60
+ <div class="ma-ftree__body">
61
+ <Level tree={n.children} open={open} highlight={highlight} path={p} nested />
62
+ </div>
63
+ </details>
64
+ </li>
65
+ ) : (
66
+ <li class="ma-ftree__file">
67
+ <span class="ma-ftree__row" data-highlight={n.highlight || highlight.includes(p) ? '' : undefined}>
68
+ <svg class="ma-ftree__glyph" aria-hidden="true" viewBox="0 0 20 20" width="16" height="16"><path d="M5 2h7l4 4v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" /><path d="M12 2v4h4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" /></svg>
69
+ <span class="ma-ftree__name">{n.name}</span>
70
+ </span>
71
+ </li>
72
+ );
73
+ })}
74
+ </ul>
75
+ ) : (
76
+ <div class:list={['ma-ftree', className]} role="group" aria-label={label} {...rest}>
77
+ <Level tree={tree} open={open} highlight={highlight} path={path} nested />
78
+ </div>
79
+ )
80
+ }
81
+
82
+ <style is:global>
83
+ @layer components {
84
+ :where(.ma-ftree) {
85
+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
86
+ font-size: 0.8125rem;
87
+ line-height: 1.4;
88
+ }
89
+ :where(.ma-ftree__list) {
90
+ margin: 0;
91
+ padding: 0;
92
+ list-style: none;
93
+ }
94
+ /* nested levels indent behind a guide line */
95
+ :where(.ma-ftree__list .ma-ftree__list) {
96
+ margin-inline-start: 0.8rem;
97
+ padding-inline-start: 0.7rem;
98
+ border-inline-start: 1px solid var(--ma-edge);
99
+ }
100
+ :where(.ma-ftree__row) {
101
+ position: relative;
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.4rem;
105
+ padding: 0.25rem 0.5rem;
106
+ border-radius: 0.375rem;
107
+ color: inherit;
108
+ transition: background-color var(--ma-duration-fast) var(--ma-ease-out);
109
+ }
110
+ :where(.ma-ftree__file .ma-ftree__row) {
111
+ padding-inline-start: 1.55rem;
112
+ }
113
+ :where(summary.ma-ftree__row) {
114
+ cursor: pointer;
115
+ list-style: none;
116
+ font-weight: 500;
117
+ }
118
+ :where(summary.ma-ftree__row)::-webkit-details-marker {
119
+ display: none;
120
+ }
121
+ :where(summary.ma-ftree__row:hover) {
122
+ background: color-mix(in srgb, var(--ma-ink) 6%, transparent);
123
+ }
124
+ :where(summary.ma-ftree__row:focus-visible) {
125
+ outline: 2px solid currentColor;
126
+ outline-offset: -2px;
127
+ }
128
+ :where(.ma-ftree__glyph) {
129
+ flex: none;
130
+ opacity: 0.75;
131
+ }
132
+ :where(.ma-ftree__chevron) {
133
+ flex: none;
134
+ opacity: 0.55;
135
+ transition: rotate var(--ma-duration) var(--ma-ease);
136
+ }
137
+ /* right to left the closed chevron points left, into the line it starts (a half turn of a symmetric glyph), and the
138
+ open rule below turns it the other way to point down. [dir] and not :dir(): Chrome has :dir() only from 120, and a
139
+ build that targets an older Chrome rewrites it into a guess from lang, which a page that sets only dir never
140
+ matches. An ltr island one level inside an rtl page is set back */
141
+ :where([dir='rtl'] .ma-ftree__chevron, .ma-ftree[dir='rtl'] .ma-ftree__chevron) {
142
+ rotate: 180deg;
143
+ }
144
+ :where([dir='rtl'] [dir='ltr'] .ma-ftree__chevron, [dir='rtl'] .ma-ftree[dir='ltr'] .ma-ftree__chevron) {
145
+ rotate: none;
146
+ }
147
+ :where(.ma-ftree__details[open] > .ma-ftree__row .ma-ftree__chevron) {
148
+ rotate: 90deg;
149
+ }
150
+ /* the folder opens: the flap tips down from its bottom edge */
151
+ :where(.ma-ftree__folder-flap) {
152
+ transform-box: fill-box;
153
+ transform-origin: 50% 100%;
154
+ transition: transform var(--ma-duration) var(--ma-ease);
155
+ }
156
+ :where(.ma-ftree__details[open] > .ma-ftree__row .ma-ftree__folder-flap) {
157
+ transform: skewX(-16deg) scaleY(0.82);
158
+ }
159
+ /* the animated part: the content box is a grid whose only row interpolates 0fr → 1fr */
160
+ :where(.ma-ftree__details)::details-content {
161
+ display: grid;
162
+ grid-template-rows: 0fr;
163
+ transition:
164
+ grid-template-rows var(--ma-duration) var(--ma-ease),
165
+ content-visibility var(--ma-duration) allow-discrete;
166
+ }
167
+ :where(.ma-ftree__details[open])::details-content {
168
+ grid-template-rows: 1fr;
169
+ }
170
+ :where(.ma-ftree__body) {
171
+ min-height: 0;
172
+ overflow: clip;
173
+ }
174
+ /* the highlighted row: a tint and a dot in the accent, default the ink */
175
+ :where(.ma-ftree__row[data-highlight]) {
176
+ background: color-mix(in srgb, var(--ma-ftree-accent, var(--ma-ink)) 10%, transparent);
177
+ color: var(--ma-ftree-accent, inherit);
178
+ font-weight: 600;
179
+ }
180
+ :where(.ma-ftree__row[data-highlight])::after {
181
+ content: '';
182
+ position: absolute;
183
+ top: 50%;
184
+ inset-inline-end: 0.6rem;
185
+ width: 6px;
186
+ height: 6px;
187
+ border-radius: 999px;
188
+ background: var(--ma-ftree-accent, currentColor);
189
+ translate: 0 -50%;
190
+ }
191
+ /* ::details-content in a rule of its own: in an engine that does not know it, a selector list that names it is
192
+ dropped whole, and the chevron, the flap and the rows kept their transitions under reduced motion. The minifier merges
193
+ neighbouring rules with the same declarations back into one list, so this one sits behind a feature test */
194
+ @media (prefers-reduced-motion: reduce) {
195
+ @supports selector(::details-content) {
196
+ :where(.ma-ftree__details)::details-content {
197
+ transition: none;
198
+ }
199
+ }
200
+ :where(.ma-ftree__chevron),
201
+ :where(.ma-ftree__folder-flap),
202
+ :where(.ma-ftree__row) {
203
+ transition: none;
204
+ }
205
+ }
206
+ }
207
+ </style>
@@ -0,0 +1,34 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'FileTree',
5
+ slug: 'file-tree',
6
+ title: 'File Tree',
7
+ description: 'A project tree whose folders open with an animated height, a turning chevron and a folder glyph that tips open; rows can be lit up to say "this is the file you change". Zero JavaScript: folders are native <details> and the height is ::details-content laid out as a grid row from 0fr to 1fr, the Accordion technique nested as deep as the tree goes.',
8
+ summary: 'A project file tree whose folders open with an animated height, with rows you can highlight.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['click'],
12
+ support: { baseline: 'newly', needs: ['details-content', 'grid-rows-animation'], fallback: 'folders open at once' },
13
+ readout: '<FileTree open={["src"]}>',
14
+ usage: `<FileTree\n open={['src', 'src/components']}\n highlight={['src/components/Reveal.astro']}\n tree={[\n { name: 'src', children: [\n { name: 'components', children: [{ name: 'Reveal.astro' }, { name: 'Marquee.astro' }] },\n { name: 'pages', children: [{ name: 'index.astro' }] },\n ] },\n { name: 'astro.config.mjs' },\n { name: 'package.json' },\n ]}\n/>`,
15
+ props: [
16
+ { name: 'tree', type: '{ name: string; children?: Node[]; highlight?: boolean; open?: boolean }[]', description: 'The tree; a node with children is a folder.' },
17
+ { name: 'open', type: 'string[]', default: '[]', description: 'Paths that start open, joined with "/" (src/pages).' },
18
+ { name: 'highlight', type: 'string[]', default: '[]', description: 'Paths to light up.' },
19
+ { name: 'label', type: 'string', default: "'Files'", description: 'Accessible name of the tree.' },
20
+ ],
21
+ reducedMotion: 'Folders open at once; the chevron and the flap jump.',
22
+ clientRouter: 'Native; nothing to rebind. The open state lives in the details elements and resets with the page.',
23
+ craft: [
24
+ 'Native <details> per folder: click, Enter, Space, and the open attribute are the browser\'s, at every depth.',
25
+ 'The height comes from ::details-content as a grid row interpolating 0fr → 1fr. Nothing is measured, and nested folders animate independently because each has its own row.',
26
+ 'The folder glyph is two paths; the flap skews and shortens from its bottom edge when open, so the icon tells the state without a second icon set.',
27
+ 'Nested lists sit behind a hairline guide, indented by one row, so depth reads at a glance in a docs sidebar. Indents, the guide and the highlight dot are logical properties, and under a dir="rtl" ancestor the closed chevron points left and turns the other way to open, so a right-to-left page gets the tree the other way round. The selector reads the attribute because Chrome has :dir() only from 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.',
28
+ 'The semantics are nested lists and a group label. role="tree" is not claimed because the arrow-key navigation the role promises is not provided.',
29
+ ],
30
+ related: ['accordion', 'code-tabs', 'docs-shell'],
31
+ intents: ['file tree', 'project structure tree', 'animated folder tree', 'directory tree astro', 'docs file explorer', 'nested details accordion'],
32
+ replaces: ['FileTree (Magic UI)', 'Tree (shadcn / Radix)', 'FileTree (Fumadocs)'],
33
+ fixture: { props: { open: ['src'], tree: [{ name: 'src', children: [{ name: 'Fixture content for FileTree' }] }, { name: 'package.json' }] }, expect: 'Fixture content for FileTree' },
34
+ } satisfies ComponentMeta;