@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/package.json +228 -0
- package/src/components/Accordion.astro +110 -0
- package/src/components/Accordion.meta.ts +33 -0
- package/src/components/AnimatedBeam.astro +217 -0
- package/src/components/AnimatedBeam.meta.ts +39 -0
- package/src/components/AnimatedIcons.astro +193 -0
- package/src/components/AnimatedIcons.meta.ts +34 -0
- package/src/components/AnimatedList.astro +305 -0
- package/src/components/AnimatedList.meta.ts +36 -0
- package/src/components/AnimatedSelect.astro +227 -0
- package/src/components/AnimatedSelect.meta.ts +38 -0
- package/src/components/ArrowButton.astro +133 -0
- package/src/components/ArrowButton.meta.ts +32 -0
- package/src/components/Aurora.astro +188 -0
- package/src/components/Aurora.meta.ts +36 -0
- package/src/components/AvatarStack.astro +205 -0
- package/src/components/AvatarStack.meta.ts +37 -0
- package/src/components/BarGrow.astro +123 -0
- package/src/components/BarGrow.meta.ts +42 -0
- package/src/components/BorderBeam.astro +75 -0
- package/src/components/BorderBeam.meta.ts +33 -0
- package/src/components/BrowserFrame.astro +172 -0
- package/src/components/BrowserFrame.meta.ts +36 -0
- package/src/components/BubbleText.astro +90 -0
- package/src/components/BubbleText.meta.ts +38 -0
- package/src/components/CardStack.astro +79 -0
- package/src/components/CardStack.meta.ts +33 -0
- package/src/components/Carousel.astro +147 -0
- package/src/components/Carousel.meta.ts +35 -0
- package/src/components/CheckDraw.astro +159 -0
- package/src/components/CheckDraw.meta.ts +37 -0
- package/src/components/CircularText.astro +103 -0
- package/src/components/CircularText.meta.ts +38 -0
- package/src/components/CodeTabs.astro +213 -0
- package/src/components/CodeTabs.meta.ts +36 -0
- package/src/components/CommandPalette.astro +363 -0
- package/src/components/CommandPalette.meta.ts +40 -0
- package/src/components/Confetti.astro +143 -0
- package/src/components/Confetti.meta.ts +40 -0
- package/src/components/CopyButton.astro +189 -0
- package/src/components/CopyButton.meta.ts +41 -0
- package/src/components/CountUp.astro +134 -0
- package/src/components/CountUp.meta.ts +40 -0
- package/src/components/CountdownFlip.astro +244 -0
- package/src/components/CountdownFlip.meta.ts +39 -0
- package/src/components/CounterPreloader.astro +122 -0
- package/src/components/CounterPreloader.meta.ts +34 -0
- package/src/components/Cursor.astro +177 -0
- package/src/components/Cursor.meta.ts +37 -0
- package/src/components/CursorGrid.astro +113 -0
- package/src/components/CursorGrid.meta.ts +37 -0
- package/src/components/Curtain.astro +305 -0
- package/src/components/Curtain.meta.ts +38 -0
- package/src/components/Dialog.astro +156 -0
- package/src/components/Dialog.meta.ts +37 -0
- package/src/components/Dock.astro +193 -0
- package/src/components/Dock.meta.ts +35 -0
- package/src/components/DotPattern.astro +58 -0
- package/src/components/DotPattern.meta.ts +32 -0
- package/src/components/DottedMap.astro +153 -0
- package/src/components/DottedMap.meta.ts +38 -0
- package/src/components/Drawer.astro +196 -0
- package/src/components/Drawer.meta.ts +35 -0
- package/src/components/DynamicIsland.astro +122 -0
- package/src/components/DynamicIsland.meta.ts +35 -0
- package/src/components/FileTree.astro +207 -0
- package/src/components/FileTree.meta.ts +34 -0
- package/src/components/FilterGrid.astro +170 -0
- package/src/components/FilterGrid.meta.ts +38 -0
- package/src/components/FooterReveal.astro +54 -0
- package/src/components/FooterReveal.meta.ts +31 -0
- package/src/components/GlitchText.astro +145 -0
- package/src/components/GlitchText.meta.ts +39 -0
- package/src/components/Globe.astro +304 -0
- package/src/components/Globe.meta.ts +41 -0
- package/src/components/GlyphField.astro +363 -0
- package/src/components/GlyphField.meta.ts +53 -0
- package/src/components/GradientText.astro +63 -0
- package/src/components/GradientText.meta.ts +35 -0
- package/src/components/GridPattern.astro +55 -0
- package/src/components/GridPattern.meta.ts +32 -0
- package/src/components/HighlightMarker.astro +68 -0
- package/src/components/HighlightMarker.meta.ts +37 -0
- package/src/components/HorizontalScroll.astro +112 -0
- package/src/components/HorizontalScroll.meta.ts +34 -0
- package/src/components/HoverImageLinks.astro +219 -0
- package/src/components/HoverImageLinks.meta.ts +34 -0
- package/src/components/HoverVideo.astro +179 -0
- package/src/components/HoverVideo.meta.ts +37 -0
- package/src/components/ImageCompare.astro +212 -0
- package/src/components/ImageCompare.meta.ts +39 -0
- package/src/components/ImageTrail.astro +169 -0
- package/src/components/ImageTrail.meta.ts +36 -0
- package/src/components/Lightbox.astro +236 -0
- package/src/components/Lightbox.meta.ts +40 -0
- package/src/components/LinkPreview.astro +234 -0
- package/src/components/LinkPreview.meta.ts +39 -0
- package/src/components/LiquidGlass.astro +148 -0
- package/src/components/LiquidGlass.meta.ts +37 -0
- package/src/components/Magnetic.astro +92 -0
- package/src/components/Magnetic.meta.ts +31 -0
- package/src/components/Marquee.astro +165 -0
- package/src/components/Marquee.meta.ts +38 -0
- package/src/components/Meteors.astro +156 -0
- package/src/components/Meteors.meta.ts +36 -0
- package/src/components/Noise.astro +61 -0
- package/src/components/Noise.meta.ts +33 -0
- package/src/components/NumberFlow.astro +143 -0
- package/src/components/NumberFlow.meta.ts +42 -0
- package/src/components/Orb.astro +130 -0
- package/src/components/Orb.meta.ts +39 -0
- package/src/components/OrbitingCircles.astro +119 -0
- package/src/components/OrbitingCircles.meta.ts +36 -0
- package/src/components/OtpInput.astro +215 -0
- package/src/components/OtpInput.meta.ts +42 -0
- package/src/components/PageTransition.astro +121 -0
- package/src/components/PageTransition.meta.ts +33 -0
- package/src/components/Parallax.astro +69 -0
- package/src/components/Parallax.meta.ts +32 -0
- package/src/components/Particles.astro +210 -0
- package/src/components/Particles.meta.ts +37 -0
- package/src/components/PersistPlayer.astro +126 -0
- package/src/components/PersistPlayer.meta.ts +35 -0
- package/src/components/PhoneFrame.astro +161 -0
- package/src/components/PhoneFrame.meta.ts +35 -0
- package/src/components/PillNav.astro +170 -0
- package/src/components/PillNav.meta.ts +37 -0
- package/src/components/Popover.astro +95 -0
- package/src/components/Popover.meta.ts +34 -0
- package/src/components/PriceSwitch.astro +251 -0
- package/src/components/PriceSwitch.meta.ts +41 -0
- package/src/components/Probe.astro +84 -0
- package/src/components/Progress.astro +165 -0
- package/src/components/Progress.meta.ts +40 -0
- package/src/components/ProgressiveBlur.astro +95 -0
- package/src/components/ProgressiveBlur.meta.ts +36 -0
- package/src/components/PromptInput.astro +273 -0
- package/src/components/PromptInput.meta.ts +46 -0
- package/src/components/ReadingToc.astro +153 -0
- package/src/components/ReadingToc.meta.ts +34 -0
- package/src/components/RetroGrid.astro +84 -0
- package/src/components/RetroGrid.meta.ts +36 -0
- package/src/components/Reveal.astro +292 -0
- package/src/components/Reveal.meta.ts +48 -0
- package/src/components/Ripple.astro +94 -0
- package/src/components/Ripple.meta.ts +34 -0
- package/src/components/RotatingText.astro +170 -0
- package/src/components/RotatingText.meta.ts +35 -0
- package/src/components/ScrollExpand.astro +90 -0
- package/src/components/ScrollExpand.meta.ts +35 -0
- package/src/components/ScrollProgress.astro +73 -0
- package/src/components/ScrollProgress.meta.ts +32 -0
- package/src/components/ScrollScrub.astro +96 -0
- package/src/components/ScrollScrub.meta.ts +37 -0
- package/src/components/ShimmerButton.astro +109 -0
- package/src/components/ShimmerButton.meta.ts +37 -0
- package/src/components/ShinyText.astro +72 -0
- package/src/components/ShinyText.meta.ts +35 -0
- package/src/components/Skeleton.astro +76 -0
- package/src/components/Skeleton.meta.ts +32 -0
- package/src/components/Sparkline.astro +168 -0
- package/src/components/Sparkline.meta.ts +43 -0
- package/src/components/Spinner.astro +79 -0
- package/src/components/Spinner.meta.ts +33 -0
- package/src/components/SplitFlap.astro +157 -0
- package/src/components/SplitFlap.meta.ts +39 -0
- package/src/components/SplitText.astro +208 -0
- package/src/components/SplitText.meta.ts +50 -0
- package/src/components/Spotlight.astro +92 -0
- package/src/components/Spotlight.meta.ts +34 -0
- package/src/components/StaggerMenu.astro +301 -0
- package/src/components/StaggerMenu.meta.ts +39 -0
- package/src/components/StatefulButton.astro +148 -0
- package/src/components/StatefulButton.meta.ts +35 -0
- package/src/components/StickyBanner.astro +168 -0
- package/src/components/StickyBanner.meta.ts +45 -0
- package/src/components/StickyReveal.astro +256 -0
- package/src/components/StickyReveal.meta.ts +33 -0
- package/src/components/StrokeDraw.astro +110 -0
- package/src/components/StrokeDraw.meta.ts +43 -0
- package/src/components/Switch.astro +165 -0
- package/src/components/Switch.meta.ts +37 -0
- package/src/components/Tabs.astro +194 -0
- package/src/components/Tabs.meta.ts +47 -0
- package/src/components/TextRoll.astro +78 -0
- package/src/components/TextRoll.meta.ts +36 -0
- package/src/components/TextScramble.astro +107 -0
- package/src/components/TextScramble.meta.ts +35 -0
- package/src/components/ThemeScript.astro +17 -0
- package/src/components/ThemeToggle.astro +182 -0
- package/src/components/ThemeToggle.meta.ts +34 -0
- package/src/components/Tilt.astro +97 -0
- package/src/components/Tilt.meta.ts +34 -0
- package/src/components/Toast.astro +184 -0
- package/src/components/Toast.meta.ts +33 -0
- package/src/components/TracingBeam.astro +113 -0
- package/src/components/TracingBeam.meta.ts +34 -0
- package/src/components/Typewriter.astro +124 -0
- package/src/components/Typewriter.meta.ts +39 -0
- package/src/components/UnderlineHover.astro +61 -0
- package/src/components/UnderlineHover.meta.ts +34 -0
- package/src/components/VanishInput.astro +240 -0
- package/src/components/VanishInput.meta.ts +41 -0
- package/src/components/VideoDialog.astro +230 -0
- package/src/components/VideoDialog.meta.ts +40 -0
- package/src/components/WeightWave.astro +70 -0
- package/src/components/WeightWave.meta.ts +36 -0
- package/src/components/internal/DialogMorph.astro +97 -0
- package/src/components/internal/PromptInputSubmitOnEnter.astro +32 -0
- package/src/index.d.ts +43 -0
- package/src/index.js +60 -0
- package/src/lib/canvas.ts +124 -0
- package/src/lib/confetti.ts +127 -0
- package/src/lib/contrast.ts +116 -0
- package/src/lib/form.ts +231 -0
- package/src/lib/geo.ts +77 -0
- package/src/lib/gsap.ts +58 -0
- package/src/lib/land.ts +11 -0
- package/src/lib/lenis.ts +112 -0
- package/src/lib/meta.ts +79 -0
- package/src/lib/morph.ts +74 -0
- package/src/lib/reduced-motion-plugin.js +299 -0
- package/src/lib/runtime.ts +215 -0
- package/src/lib/support.ts +102 -0
- package/src/lib/theme.ts +18 -0
- package/src/lib/toast.ts +17 -0
- package/src/styles/base.css +218 -0
- package/src/styles/motion.css +43 -0
- package/src/styles/theme.css +397 -0
- package/src/styles/theme.tailwind.css +75 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AvatarStack — overlapping avatars and a "+2 398", zero JS. A list of
|
|
4
|
+
* lazy images (or initials where there is no photo) overlapped through a
|
|
5
|
+
* negative margin, each with a ring in the surface colour so the overlap
|
|
6
|
+
* reads. Hover the list and the row spreads; hover one and it rises and
|
|
7
|
+
* its name appears above it — a CSS tooltip, no popover. Avatars are not
|
|
8
|
+
* focusable: six tab stops for decoration is a keyboard trap, and the
|
|
9
|
+
* names are in the alt text. With `href` an avatar becomes a link, is
|
|
10
|
+
* focusable, and shows its name on :focus-visible as well. Touch gets the
|
|
11
|
+
* still row. The count is formatted at build with Intl (`locale`).
|
|
12
|
+
*/
|
|
13
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
14
|
+
|
|
15
|
+
interface Person {
|
|
16
|
+
name: string;
|
|
17
|
+
/** Photo URL; without one, the initials are drawn. */
|
|
18
|
+
src?: string;
|
|
19
|
+
href?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Props extends HTMLAttributes<'ul'> {
|
|
23
|
+
people: Person[];
|
|
24
|
+
/** Avatars shown; the rest go into the chip. */
|
|
25
|
+
max?: number;
|
|
26
|
+
/** Total count; the chip shows count − max. Defaults to people.length. */
|
|
27
|
+
count?: number;
|
|
28
|
+
/** Avatar size, any CSS length. */
|
|
29
|
+
size?: string;
|
|
30
|
+
/** Intl locale for the chip's number. */
|
|
31
|
+
locale?: string;
|
|
32
|
+
/** Screen-reader text of the chip, with `{n}` for the formatted number. */
|
|
33
|
+
moreLabel?: string;
|
|
34
|
+
/** Ring colour around each avatar; default the panel token. */
|
|
35
|
+
ring?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { people, max = 5, count, size = '2.5rem', locale = 'en', moreLabel = 'and {n} others', ring, class: className, style, ...rest } = Astro.props;
|
|
39
|
+
const shown = people.slice(0, Math.max(1, max));
|
|
40
|
+
const total = count ?? people.length;
|
|
41
|
+
const more = Math.max(0, total - shown.length);
|
|
42
|
+
const moreText = new Intl.NumberFormat(locale).format(more);
|
|
43
|
+
const initials = (name: string) =>
|
|
44
|
+
name
|
|
45
|
+
.split(/\s+/)
|
|
46
|
+
.map((w) => w[0] ?? '')
|
|
47
|
+
.join('')
|
|
48
|
+
.slice(0, 2)
|
|
49
|
+
.toUpperCase();
|
|
50
|
+
const vars = [`--ma-avatars-size:${size}`, ring && `--ma-avatars-ring:${ring}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<ul class:list={['ma-avatars', className]} style={vars} {...rest}>
|
|
54
|
+
{
|
|
55
|
+
shown.map((p) => (
|
|
56
|
+
<li class="ma-avatars__item">
|
|
57
|
+
{p.href ? (
|
|
58
|
+
<a href={p.href} class="ma-avatars__face">
|
|
59
|
+
{p.src ? <img src={p.src} alt={p.name} loading="lazy" decoding="async" /> : <span class="ma-avatars__initials" aria-hidden="true">{initials(p.name)}</span>}
|
|
60
|
+
{!p.src && <span class="ma-avatars__sr">{p.name}</span>}
|
|
61
|
+
</a>
|
|
62
|
+
) : (
|
|
63
|
+
<span class="ma-avatars__face">
|
|
64
|
+
{p.src ? <img src={p.src} alt={p.name} loading="lazy" decoding="async" /> : <span class="ma-avatars__initials" aria-hidden="true">{initials(p.name)}</span>}
|
|
65
|
+
{!p.src && <span class="ma-avatars__sr">{p.name}</span>}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
<span class="ma-avatars__tip" aria-hidden="true">{p.name}</span>
|
|
69
|
+
</li>
|
|
70
|
+
))
|
|
71
|
+
}
|
|
72
|
+
{more > 0 && (
|
|
73
|
+
<li class="ma-avatars__item ma-avatars__more">
|
|
74
|
+
<span class="ma-avatars__face" aria-hidden="true">+{moreText}</span>
|
|
75
|
+
<span class="ma-avatars__sr">{moreLabel.replace('{n}', moreText)}</span>
|
|
76
|
+
</li>
|
|
77
|
+
)}
|
|
78
|
+
</ul>
|
|
79
|
+
|
|
80
|
+
<style is:global>
|
|
81
|
+
@layer components {
|
|
82
|
+
:where(.ma-avatars) {
|
|
83
|
+
--ma-avatars-overlap: calc(var(--ma-avatars-size) * -0.26);
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
margin: 0;
|
|
87
|
+
padding: 0;
|
|
88
|
+
padding-inline-start: calc(var(--ma-avatars-overlap) * -1);
|
|
89
|
+
list-style: none;
|
|
90
|
+
isolation: isolate;
|
|
91
|
+
}
|
|
92
|
+
:where(.ma-avatars__item) {
|
|
93
|
+
position: relative;
|
|
94
|
+
margin-inline-start: var(--ma-avatars-overlap);
|
|
95
|
+
transition:
|
|
96
|
+
margin var(--ma-duration) var(--ma-ease),
|
|
97
|
+
translate var(--ma-duration) var(--ma-ease);
|
|
98
|
+
}
|
|
99
|
+
:where(.ma-avatars__face) {
|
|
100
|
+
display: grid;
|
|
101
|
+
place-items: center;
|
|
102
|
+
inline-size: var(--ma-avatars-size);
|
|
103
|
+
block-size: var(--ma-avatars-size);
|
|
104
|
+
border-radius: 50%;
|
|
105
|
+
box-shadow: 0 0 0 2px var(--ma-avatars-ring, var(--ma-panel));
|
|
106
|
+
background: color-mix(in srgb, var(--ma-ink) 10%, var(--ma-panel));
|
|
107
|
+
color: inherit;
|
|
108
|
+
overflow: clip;
|
|
109
|
+
text-decoration: none;
|
|
110
|
+
font-size: calc(var(--ma-avatars-size) * 0.3);
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
letter-spacing: -0.02em;
|
|
113
|
+
}
|
|
114
|
+
:where(.ma-avatars__face > img) {
|
|
115
|
+
inline-size: 100%;
|
|
116
|
+
block-size: 100%;
|
|
117
|
+
object-fit: cover;
|
|
118
|
+
}
|
|
119
|
+
:where(.ma-avatars__more .ma-avatars__face) {
|
|
120
|
+
background: var(--ma-ink);
|
|
121
|
+
color: var(--ma-panel);
|
|
122
|
+
font-size: calc(var(--ma-avatars-size) * 0.3);
|
|
123
|
+
font-variant-numeric: tabular-nums;
|
|
124
|
+
/* a count wider than the circle grows into a pill instead of spilling out of it: +2,398. border-box, so a project
|
|
125
|
+
without a reset (content-box) still gets a circle for "+3" */
|
|
126
|
+
box-sizing: border-box;
|
|
127
|
+
inline-size: auto;
|
|
128
|
+
min-inline-size: var(--ma-avatars-size);
|
|
129
|
+
padding-inline: calc(var(--ma-avatars-size) * 0.2);
|
|
130
|
+
border-radius: calc(var(--ma-avatars-size) / 2);
|
|
131
|
+
}
|
|
132
|
+
/* centred on its avatar: left and -50% are the same in both directions, where inset-inline-start would be right: 50%
|
|
133
|
+
on a right-to-left page and push the name a whole width off */
|
|
134
|
+
:where(.ma-avatars__tip) {
|
|
135
|
+
position: absolute;
|
|
136
|
+
inset-block-end: calc(100% + 0.5em);
|
|
137
|
+
left: 50%;
|
|
138
|
+
padding: 0.3em 0.6em;
|
|
139
|
+
border-radius: 0.4em;
|
|
140
|
+
background: var(--ma-ink);
|
|
141
|
+
color: var(--ma-panel);
|
|
142
|
+
font-size: 0.75rem;
|
|
143
|
+
line-height: 1.2;
|
|
144
|
+
white-space: nowrap;
|
|
145
|
+
translate: -50% var(--ma-travel-hover);
|
|
146
|
+
opacity: 0;
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
transition:
|
|
149
|
+
opacity var(--ma-duration-fast) var(--ma-ease-out),
|
|
150
|
+
translate var(--ma-duration-fast) var(--ma-ease-out);
|
|
151
|
+
}
|
|
152
|
+
/* a pointer that hovers: the row spreads; the one under it rises and names itself. Touch never sees this. */
|
|
153
|
+
@media (hover: hover) and (pointer: fine) {
|
|
154
|
+
:where(.ma-avatars:hover .ma-avatars__item) {
|
|
155
|
+
margin-inline-start: calc(var(--ma-avatars-size) * 0.08);
|
|
156
|
+
}
|
|
157
|
+
:where(.ma-avatars:hover .ma-avatars__item:first-child) {
|
|
158
|
+
margin-inline-start: var(--ma-avatars-overlap);
|
|
159
|
+
}
|
|
160
|
+
:where(.ma-avatars__item:hover) {
|
|
161
|
+
z-index: 1;
|
|
162
|
+
translate: 0 calc(var(--ma-travel-hover) * -1);
|
|
163
|
+
}
|
|
164
|
+
:where(.ma-avatars__item:hover .ma-avatars__tip) {
|
|
165
|
+
opacity: 1;
|
|
166
|
+
translate: -50% 0;
|
|
167
|
+
transition-duration: var(--ma-duration);
|
|
168
|
+
transition-timing-function: var(--ma-ease);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/* a linked avatar reached from the keyboard names itself the same way */
|
|
172
|
+
:where(.ma-avatars__item:has(.ma-avatars__face:focus-visible)) {
|
|
173
|
+
z-index: 1;
|
|
174
|
+
translate: 0 calc(var(--ma-travel-hover) * -1);
|
|
175
|
+
}
|
|
176
|
+
:where(.ma-avatars__item:has(.ma-avatars__face:focus-visible) .ma-avatars__tip) {
|
|
177
|
+
opacity: 1;
|
|
178
|
+
translate: -50% 0;
|
|
179
|
+
}
|
|
180
|
+
:where(.ma-avatars__face:focus-visible) {
|
|
181
|
+
outline: 2px solid var(--ma-ink);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
}
|
|
184
|
+
:where(.ma-avatars__sr) {
|
|
185
|
+
position: absolute;
|
|
186
|
+
inline-size: 1px;
|
|
187
|
+
block-size: 1px;
|
|
188
|
+
margin: -1px;
|
|
189
|
+
padding: 0;
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
clip-path: inset(50%);
|
|
192
|
+
white-space: nowrap;
|
|
193
|
+
}
|
|
194
|
+
@media (prefers-reduced-motion: reduce) {
|
|
195
|
+
:where(.ma-avatars__item),
|
|
196
|
+
:where(.ma-avatars__tip) {
|
|
197
|
+
transition: none;
|
|
198
|
+
}
|
|
199
|
+
:where(.ma-avatars__item:hover),
|
|
200
|
+
:where(.ma-avatars__item:has(.ma-avatars__face:focus-visible)) {
|
|
201
|
+
translate: 0 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'AvatarStack',
|
|
5
|
+
slug: 'avatar-stack',
|
|
6
|
+
title: 'Avatar Stack',
|
|
7
|
+
description: 'A row of overlapping avatars with a count chip such as "+2 398". Lazy images or initials, overlapped by a negative margin with a ring in the surface colour. Hover the row and it spreads; hover one and it rises and its name appears above it in a CSS tooltip. Avatars are not tab stops unless they carry an href; the names live in the alt text and the chip has its own screen-reader line. The count is formatted with Intl at build. Zero JavaScript.',
|
|
8
|
+
summary: 'Overlapping avatars with a count chip. The row spreads on hover, and each face shows its name in a tooltip.',
|
|
9
|
+
category: 'ui',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['hover'],
|
|
12
|
+
support: { baseline: 'widely', needs: [] },
|
|
13
|
+
readout: '<AvatarStack people={people} max={5} count={2403}>',
|
|
14
|
+
usage: `---\nconst people = [\n { name: 'Ari Santoso', src: '/avatars/ari.jpg' },\n { name: 'Mei Lin', src: '/avatars/mei.jpg' },\n { name: 'Noor Haddad' },\n { name: 'Tomás Reyes', href: '/team/tomas/' },\n];\n---\n<AvatarStack people={people} max={4} count={2403} locale="en" />\n<p>Joined by <AvatarStack people={people} max={3} size="1.5rem" /> and the rest of the list.</p>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'people', type: '{ name, src?, href? }[]', description: 'Who to show. src is a photo (lazy, async); without one the initials are drawn. href makes the avatar a link and a tab stop.' },
|
|
17
|
+
{ name: 'max', type: 'number', default: '5', description: 'Avatars shown; the rest are the chip.' },
|
|
18
|
+
{ name: 'count', type: 'number', default: 'people.length', description: 'The total; the chip prints count − max.' },
|
|
19
|
+
{ name: 'size', type: 'string', default: "'2.5rem'", description: 'Avatar size; the overlap, the ring and the initials scale with it.' },
|
|
20
|
+
{ name: 'locale', type: 'string', default: "'en'", description: 'Intl.NumberFormat locale for the chip.' },
|
|
21
|
+
{ name: 'moreLabel', type: 'string', default: "'and {n} others'", description: 'What a screen reader gets for the chip; {n} is the formatted number.' },
|
|
22
|
+
{ name: 'ring', type: 'string', description: 'Ring colour; default the panel token. Pass the surface the stack sits on.' },
|
|
23
|
+
],
|
|
24
|
+
reducedMotion: 'The spread happens without a transition and nothing rises; the tooltip still appears.',
|
|
25
|
+
clientRouter: 'Static; nothing to bind.',
|
|
26
|
+
craft: [
|
|
27
|
+
'The overlap is a negative inline margin and the ring is a box-shadow in the surface colour, so the stack reads as a stack on any background. Pass ring to match a card. On hover the margin goes positive and the row opens on the preset curve; the first item keeps its margin so the row does not jump sideways.',
|
|
28
|
+
'The name is a tooltip inside the item (absolute, above, centred with translate), rising from the hover travel token to zero. It is aria-hidden: the accessible name of each avatar is its alt, and a decorative row is not six tab stops.',
|
|
29
|
+
'With href an avatar is a real link and reaches the keyboard; :focus-visible then does what hover does, through :has() on the item, so the name shows for a keyboard reader as well.',
|
|
30
|
+
'The whole hover block is behind (hover: hover) and (pointer: fine): on touch the row is still and no tooltip appears, because a tooltip that opens on tap has no way to close.',
|
|
31
|
+
'The chip is formatted with Intl.NumberFormat at build time (locale), giving "2,398" or "2 398", and carries its own screen-reader sentence from moreLabel.',
|
|
32
|
+
],
|
|
33
|
+
related: ['team-grid', 'tilt', 'link-preview'],
|
|
34
|
+
intents: ['avatar stack', 'avatar group', 'overlapping avatars', 'avatar circles hover', 'joined by avatars', 'social proof avatars'],
|
|
35
|
+
replaces: ['AnimatedTooltip (Aceternity)', 'AvatarCircles (Magic UI)', 'AvatarGroup (shadcn examples)'],
|
|
36
|
+
fixture: { props: { people: [{ name: 'Fixture content for AvatarStack' }, { name: 'Second Person' }, { name: 'Third Person' }], max: 2, count: 2403 }, expect: 'Fixture content for AvatarStack' },
|
|
37
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* BarGrow — bars grow from the baseline one after another when the chart
|
|
4
|
+
* scrolls into view. Server-rendered from the data: each bar is a flex
|
|
5
|
+
* child whose height is its value and whose scale transitions from 0 to 1
|
|
6
|
+
* on a per-index delay; hovering a bar shows its value. Composes Reveal;
|
|
7
|
+
* zero script of its own. The accessible name summarises the series.
|
|
8
|
+
*/
|
|
9
|
+
import type { HTMLAttributes, HTMLTag } from 'astro/types';
|
|
10
|
+
import Reveal from './Reveal.astro';
|
|
11
|
+
|
|
12
|
+
interface Bar {
|
|
13
|
+
label?: string;
|
|
14
|
+
value: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
18
|
+
/** The series: numbers, or { label, value }. */
|
|
19
|
+
data: (number | Bar)[];
|
|
20
|
+
/** Element to render. */
|
|
21
|
+
as?: HTMLTag;
|
|
22
|
+
/** Value that fills the full height. Default: the largest value. */
|
|
23
|
+
max?: number;
|
|
24
|
+
/** Chart height, any CSS length. */
|
|
25
|
+
height?: string;
|
|
26
|
+
/** Bar colour. */
|
|
27
|
+
color?: string;
|
|
28
|
+
/** Grow duration in ms. Unset: the preset's --ma-duration-slow. */
|
|
29
|
+
duration?: number;
|
|
30
|
+
/** Delay between bars in ms. Unset: the preset's --ma-stagger. */
|
|
31
|
+
stagger?: number;
|
|
32
|
+
/** Accessible name; the summary (min, max, last) is appended. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Delay in ms before the first bar. */
|
|
35
|
+
delay?: number;
|
|
36
|
+
/** Play once and stay. */
|
|
37
|
+
once?: boolean;
|
|
38
|
+
/** Fraction visible to trigger. */
|
|
39
|
+
threshold?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { data, as = 'div', max, height = '8rem', color = 'var(--chart-1, currentColor)', duration, stagger, label, delay, once, threshold, class: className, style, ...rest } = Astro.props;
|
|
43
|
+
const bars: Bar[] = data.map((d) => (typeof d === 'number' ? { value: d } : d));
|
|
44
|
+
const values = bars.map((b) => b.value);
|
|
45
|
+
const top = max ?? (Math.max(...values, 0) || 1);
|
|
46
|
+
// an empty series is named as one: min and max of nothing are Infinity and -Infinity, and its last value is undefined
|
|
47
|
+
const name = `${label ? `${label}: ` : ''}${bars.length ? `${bars.length} bars, min ${Math.min(...values)}, max ${Math.max(...values)}, last ${values[values.length - 1]}` : 'no data'}`;
|
|
48
|
+
const hasLabels = bars.some((b) => b.label);
|
|
49
|
+
// the bars' delay and index are the chart's own (--ma-bars-delay, --ma-bars-i): read as --ma-delay and --ma-i, the delay
|
|
50
|
+
// was whatever a Reveal or any other element around the chart had set, and it is no longer the name Reveal writes
|
|
51
|
+
const vars = [`--ma-bars-h:${height}`, `--ma-bars-color:${color}`, duration !== undefined && `--ma-bars-dur:${duration}ms`, stagger !== undefined && `--ma-bars-stagger:${stagger}ms`, delay !== undefined && `--ma-bars-delay:${delay}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<Reveal as={as} from="none" delay={delay} once={once} threshold={threshold} class={['ma-bars', className].filter(Boolean).join(' ')} role="img" aria-label={name} style={vars} {...rest}>
|
|
55
|
+
<span class="ma-bars__track" aria-hidden="true">
|
|
56
|
+
{bars.map((b, i) => <span class="ma-bars__bar" data-value={b.value} style={`--ma-bars-i:${i};--ma-bars-v:${Math.max(0, Math.min(1, b.value / top))}`}></span>)}
|
|
57
|
+
</span>
|
|
58
|
+
{hasLabels && <span class="ma-bars__labels" aria-hidden="true">{bars.map((b) => <span class="ma-bars__label">{b.label}</span>)}</span>}
|
|
59
|
+
</Reveal>
|
|
60
|
+
|
|
61
|
+
<style is:global>
|
|
62
|
+
@layer components {
|
|
63
|
+
:where(.ma-bars) {
|
|
64
|
+
display: block;
|
|
65
|
+
}
|
|
66
|
+
:where(.ma-bars__track) {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: flex-end;
|
|
69
|
+
gap: 0.3em;
|
|
70
|
+
height: var(--ma-bars-h, 8rem);
|
|
71
|
+
}
|
|
72
|
+
:where(.ma-bars__bar) {
|
|
73
|
+
position: relative;
|
|
74
|
+
flex: 1;
|
|
75
|
+
min-height: 2px;
|
|
76
|
+
height: calc(var(--ma-bars-v, 0) * 100%);
|
|
77
|
+
border-radius: 0.2em 0.2em 0 0;
|
|
78
|
+
background: var(--ma-bars-color, currentColor);
|
|
79
|
+
transform-origin: 50% 100%;
|
|
80
|
+
scale: 1 1;
|
|
81
|
+
transition: scale var(--ma-bars-dur, var(--ma-duration-slow)) var(--ma-ease-out) calc(var(--ma-bars-delay, 0ms) + var(--ma-bars-i, 0) * var(--ma-bars-stagger, var(--ma-stagger)));
|
|
82
|
+
}
|
|
83
|
+
/* the value, on hover */
|
|
84
|
+
:where(.ma-bars__bar)::after {
|
|
85
|
+
content: attr(data-value);
|
|
86
|
+
position: absolute;
|
|
87
|
+
inset: auto 50% 100% auto;
|
|
88
|
+
translate: 50% 0;
|
|
89
|
+
margin-bottom: 0.35em;
|
|
90
|
+
font-size: 0.75em;
|
|
91
|
+
font-variant-numeric: tabular-nums;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
opacity: 0;
|
|
94
|
+
transition: opacity var(--ma-duration-fast) var(--ma-ease-out);
|
|
95
|
+
}
|
|
96
|
+
:where(.ma-bars__bar:hover)::after {
|
|
97
|
+
opacity: 1;
|
|
98
|
+
}
|
|
99
|
+
:where(.ma-bars__labels) {
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: 0.3em;
|
|
102
|
+
margin-top: 0.5em;
|
|
103
|
+
font-size: 0.75em;
|
|
104
|
+
opacity: var(--ma-dim, 0.7);
|
|
105
|
+
}
|
|
106
|
+
:where(.ma-bars__label) {
|
|
107
|
+
flex: 1;
|
|
108
|
+
text-align: center;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
text-overflow: ellipsis;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
}
|
|
113
|
+
:where([data-ma-js] .ma-bars:not(.ma-in) .ma-bars__bar) {
|
|
114
|
+
scale: 1 0;
|
|
115
|
+
transition: none;
|
|
116
|
+
}
|
|
117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
118
|
+
:where(.ma-bars__bar) {
|
|
119
|
+
transition: none;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'BarGrow',
|
|
5
|
+
slug: 'bar-grow',
|
|
6
|
+
title: 'Bar Grow',
|
|
7
|
+
description:
|
|
8
|
+
'A bar chart whose bars grow from the baseline one after another when it scrolls into view; hovering a bar shows its value. Rendered on the server from the data: each bar is a flex child scaled from 0 to 1 on a per-index delay. Composes Reveal; no script of its own. The accessible name summarises the series.',
|
|
9
|
+
summary: 'A bar chart whose bars grow from the baseline one after another as it scrolls into view, with values on hover.',
|
|
10
|
+
category: 'data',
|
|
11
|
+
tier: 'A',
|
|
12
|
+
trigger: ['scroll'],
|
|
13
|
+
support: { baseline: 'widely', needs: [] },
|
|
14
|
+
readout: '<BarGrow data={[…]}>',
|
|
15
|
+
usage: `<BarGrow data={[12, 18, 15, 22, 30, 28, 41]} label="Commits per day" class="text-action" />\n<BarGrow data={days.map((d) => ({ label: d.name, value: d.n }))} height="6rem" stagger={40} />`,
|
|
16
|
+
props: [
|
|
17
|
+
{ name: 'data', type: '(number | { label?, value })[]', description: 'The series; objects add a label under the bar.' },
|
|
18
|
+
{ name: 'as', type: 'HTMLTag', default: "'div'", description: 'Element to render.' },
|
|
19
|
+
{ name: 'max', type: 'number', default: 'largest value', description: 'Value that fills the full height.' },
|
|
20
|
+
{ name: 'height', type: 'string', default: "'8rem'", description: 'Chart height.' },
|
|
21
|
+
{ name: 'color', type: 'string', default: "'var(--chart-1, currentColor)'", description: 'Bar colour: the first chart colour of the theme contract when a theme defines one, else the text colour.' },
|
|
22
|
+
{ name: 'duration', type: 'number', default: 'preset (700)', description: 'Grow duration per bar in ms. Unset, the preset\'s --ma-duration-slow.' },
|
|
23
|
+
{ name: 'stagger', type: 'number', default: 'preset (ui: 60)', description: 'Delay between bars in ms.' },
|
|
24
|
+
{ name: 'label', type: 'string', description: 'Accessible name; "n bars, min, max, last" is appended.' },
|
|
25
|
+
{ name: 'delay', type: 'number', default: '0', description: 'Delay in ms before the chart fades in and the first bar grows. A delay on a Reveal around the chart does not reach the bars: inside one, give the chart the same delay.' },
|
|
26
|
+
{ name: 'once', type: 'boolean', default: 'true', description: 'Play once and stay.' },
|
|
27
|
+
{ name: 'threshold', type: 'number', default: '0.2', description: 'Fraction visible to trigger.' },
|
|
28
|
+
],
|
|
29
|
+
reducedMotion: 'The bars stand at their values at once.',
|
|
30
|
+
clientRouter: 'Inherits Reveal: rebinds after every navigation.',
|
|
31
|
+
craft: [
|
|
32
|
+
'scale on a bottom origin, not height: one compositor property, no layout per frame, and the rounded top stays rounded.',
|
|
33
|
+
'Left to right on the preset stagger: a chart is read in order, and a stagger that runs with the reading direction is the chart being drawn.',
|
|
34
|
+
'Ease-out at the slow duration: bars that shoot up and settle read as data arriving; bars that ease in read as loading.',
|
|
35
|
+
'The value is a data attribute painted by ::after on hover, with no tooltip element per bar and no script.',
|
|
36
|
+
'role="img" with a summary of the series in the name; the bars are aria-hidden decoration on top of that sentence.',
|
|
37
|
+
],
|
|
38
|
+
related: ['sparkline', 'progress', 'count-up'],
|
|
39
|
+
intents: ['animated bar chart', 'bars grow on scroll', 'css bar chart animation', 'stagger bars chart', 'tiny bar chart astro'],
|
|
40
|
+
replaces: ['bar charts (Animata, Preline)', 'Chart.js for a seven-bar chart'],
|
|
41
|
+
fixture: { props: { data: [3, 5, 4, 8], label: 'Fixture content for BarGrow' }, expect: 'Fixture content for BarGrow' },
|
|
42
|
+
} satisfies ComponentMeta;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* BorderBeam — a light travels around the edge of a card, zero JS.
|
|
4
|
+
*
|
|
5
|
+
* The element carries a conic gradient whose start angle is a registered
|
|
6
|
+
* custom property; two masks composited with `exclude` keep only the border
|
|
7
|
+
* ring. The angle grows, so the beam runs clockwise and the head colour sits
|
|
8
|
+
* at the leading end. Animating the angle repaints the ring, nothing else.
|
|
9
|
+
* Absolutely positioned: give the parent `position: relative` and a radius.
|
|
10
|
+
*/
|
|
11
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
14
|
+
/** Seconds per lap. */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** Length of the beam as a fraction of the perimeter, 0.05–0.5. */
|
|
17
|
+
length?: number;
|
|
18
|
+
/** Ring thickness in px. */
|
|
19
|
+
width?: number;
|
|
20
|
+
/** Beam colours: head and tail. */
|
|
21
|
+
colors?: [string, string];
|
|
22
|
+
/** Start angle in degrees. */
|
|
23
|
+
from?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { duration = 6, length = 0.2, width = 1.5, colors = ['oklch(78% 0.16 60)', 'oklch(70% 0.19 300)'], from = 0, class: className, style, ...rest } = Astro.props;
|
|
27
|
+
const span = Math.max(0.05, Math.min(length, 0.5)) * 360;
|
|
28
|
+
const vars = [`--ma-beam-duration:${duration}s`, `--ma-beam-span:${span}deg`, `--ma-beam-width:${width}px`, `--ma-beam-1:${colors[0]}`, `--ma-beam-2:${colors[1]}`, `--ma-beam-from:${from}deg`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
class:list={['ma-beam', className]}
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
style={vars}
|
|
35
|
+
{...rest}
|
|
36
|
+
>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<style is:global>
|
|
40
|
+
@property --ma-beam-angle {
|
|
41
|
+
syntax: '<angle>';
|
|
42
|
+
inherits: false;
|
|
43
|
+
initial-value: 0deg;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@layer components {
|
|
47
|
+
:where(.ma-beam) {
|
|
48
|
+
position: absolute;
|
|
49
|
+
inset: 0;
|
|
50
|
+
border-radius: inherit;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
padding: var(--ma-beam-width, 1.5px);
|
|
53
|
+
/* clockwise, so the head is the far end of the span: transparent at the start, the tail colour 60% of the length
|
|
54
|
+
behind the head, and a hard edge at the head. Without @property the angle falls back to 0 and the beam holds
|
|
55
|
+
still at `from` (the keyframe then flips it from 0 to a full turn, the same place) */
|
|
56
|
+
background: conic-gradient(from calc(var(--ma-beam-angle, 0deg) + var(--ma-beam-from, 0deg)), transparent 0deg, var(--ma-beam-2) calc(var(--ma-beam-span, 72deg) * 0.4), var(--ma-beam-1) var(--ma-beam-span, 72deg), transparent var(--ma-beam-span, 72deg));
|
|
57
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
58
|
+
-webkit-mask-composite: xor;
|
|
59
|
+
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
60
|
+
mask-composite: exclude;
|
|
61
|
+
animation: ma-beam var(--ma-beam-duration, 6s) linear infinite;
|
|
62
|
+
}
|
|
63
|
+
@media (prefers-reduced-motion: reduce) {
|
|
64
|
+
:where(.ma-beam) {
|
|
65
|
+
animation: none;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@keyframes ma-beam {
|
|
71
|
+
to {
|
|
72
|
+
--ma-beam-angle: 360deg;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ComponentMeta } from '../lib/meta';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'BorderBeam',
|
|
5
|
+
slug: 'border-beam',
|
|
6
|
+
title: 'Border Beam',
|
|
7
|
+
description: 'A beam of light travels around the edge of a card. A conic gradient behind two composited masks leaves only the ring; the start angle animates as a registered custom property. Pure CSS, inherits the parent radius.',
|
|
8
|
+
summary: "A beam of light that travels around the edge of a card, following the card's border radius.",
|
|
9
|
+
category: 'background',
|
|
10
|
+
tier: 'A',
|
|
11
|
+
trigger: ['always'],
|
|
12
|
+
support: { baseline: 'newly', needs: ['property', 'mask-image'], fallback: 'the beam holds still at its start angle' },
|
|
13
|
+
readout: '<BorderBeam duration={6}>',
|
|
14
|
+
usage: `<div class="relative rounded-2xl border p-6">\n <BorderBeam />\n <h3>Pro</h3>\n</div>`,
|
|
15
|
+
props: [
|
|
16
|
+
{ name: 'duration', type: 'number', default: '6', description: 'Seconds per lap. Under 3 reads as a spinner.' },
|
|
17
|
+
{ name: 'length', type: 'number', default: '0.2', description: 'Beam length as a fraction of the perimeter, 0.05–0.5.' },
|
|
18
|
+
{ name: 'width', type: 'number', default: '1.5', description: 'Ring thickness in px.' },
|
|
19
|
+
{ name: 'colors', type: '[string, string]', default: 'amber, violet', description: 'Head and tail colours.' },
|
|
20
|
+
{ name: 'from', type: 'number', default: '0', description: 'Start angle in degrees; offset several cards so they do not sync.' },
|
|
21
|
+
],
|
|
22
|
+
reducedMotion: 'The beam holds still at its start angle (from).',
|
|
23
|
+
clientRouter: 'CSS-only; nothing to rebind.',
|
|
24
|
+
craft: [
|
|
25
|
+
'The ring is a mask, not a border: the beam can fade along its length and the card underneath keeps its own border.',
|
|
26
|
+
'Linear timing, because it laps forever.',
|
|
27
|
+
'The beam runs clockwise and the head colour is its leading end, with a hard edge; the tail colour sits 60% of the length behind it and fades to nothing, like a comet.',
|
|
28
|
+
],
|
|
29
|
+
related: ['spotlight', 'aurora', 'tilt'],
|
|
30
|
+
intents: ['border beam', 'animated border', 'glowing card border', 'light around card edge', 'moving border gradient'],
|
|
31
|
+
replaces: ['BorderBeam (Magic UI)', 'MovingBorder (Aceternity)', 'StarBorder (React Bits)'],
|
|
32
|
+
fixture: { expect: 'class="ma-beam"' },
|
|
33
|
+
} satisfies ComponentMeta;
|