@refraction-ui/astro 0.13.0 → 0.14.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/dist/astro-audience-feature-card/AudienceFeatureCard.astro +53 -0
- package/dist/astro-audience-feature-card/index.ts +1 -0
- package/dist/astro-brand-network-cell/BrandNetworkCell.astro +77 -0
- package/dist/astro-brand-network-cell/index.ts +1 -0
- package/dist/astro-browser-chrome-mock/BrowserChromeMock.astro +62 -0
- package/dist/astro-browser-chrome-mock/index.ts +1 -0
- package/dist/astro-marquee-strip/MarqueeStrip.astro +63 -0
- package/dist/astro-marquee-strip/index.ts +1 -0
- package/dist/astro-mastery-bar/MasteryBar.astro +56 -0
- package/dist/astro-mastery-bar/index.ts +1 -0
- package/dist/astro-numbered-steps/NumberedSteps.astro +47 -0
- package/dist/astro-numbered-steps/index.ts +1 -0
- package/dist/astro-pricing-card/PricingCard.astro +114 -0
- package/dist/astro-pricing-card/index.ts +1 -0
- package/dist/astro-section-head/SectionHead.astro +44 -0
- package/dist/astro-section-head/index.ts +1 -0
- package/dist/astro-stat-grid/StatGrid.astro +46 -0
- package/dist/astro-stat-grid/index.ts +1 -0
- package/dist/audience-feature-card/audience-feature-card.styles.ts +23 -0
- package/dist/audience-feature-card/audience-feature-card.ts +39 -0
- package/dist/audience-feature-card/index.ts +13 -0
- package/dist/brand-network-cell/brand-network-cell.styles.ts +46 -0
- package/dist/brand-network-cell/brand-network-cell.ts +35 -0
- package/dist/brand-network-cell/index.ts +14 -0
- package/dist/browser-chrome-mock/browser-chrome-mock.styles.ts +43 -0
- package/dist/browser-chrome-mock/browser-chrome-mock.ts +52 -0
- package/dist/browser-chrome-mock/index.ts +17 -0
- package/dist/index.ts +9 -0
- package/dist/marquee-strip/index.ts +14 -0
- package/dist/marquee-strip/marquee-strip.styles.ts +61 -0
- package/dist/marquee-strip/marquee-strip.ts +35 -0
- package/dist/mastery-bar/index.ts +15 -0
- package/dist/mastery-bar/mastery-bar.styles.ts +45 -0
- package/dist/mastery-bar/mastery-bar.ts +55 -0
- package/dist/numbered-steps/index.ts +16 -0
- package/dist/numbered-steps/numbered-steps.styles.ts +36 -0
- package/dist/numbered-steps/numbered-steps.ts +64 -0
- package/dist/pricing-card/index.ts +19 -0
- package/dist/pricing-card/pricing-card.styles.ts +71 -0
- package/dist/pricing-card/pricing-card.ts +37 -0
- package/dist/section-head/index.ts +13 -0
- package/dist/section-head/section-head.styles.ts +30 -0
- package/dist/section-head/section-head.ts +28 -0
- package/dist/stat-grid/index.ts +13 -0
- package/dist/stat-grid/stat-grid.styles.ts +25 -0
- package/dist/stat-grid/stat-grid.ts +41 -0
- package/package.json +10 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createAudienceFeatureCard,
|
|
4
|
+
audienceFeatureCardVariants,
|
|
5
|
+
audienceFeatureCardKickerClass,
|
|
6
|
+
audienceFeatureCardTitleClass,
|
|
7
|
+
audienceFeatureCardBodyClass,
|
|
8
|
+
audienceFeatureCardFooterClass,
|
|
9
|
+
} from '../audience-feature-card/index.ts'
|
|
10
|
+
import { cn } from '../shared/index.ts'
|
|
11
|
+
|
|
12
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
13
|
+
/** Kicker text displayed above the title (e.g. "For teams"). */
|
|
14
|
+
kicker?: string
|
|
15
|
+
/** Main title — required (or pass content via default slot). */
|
|
16
|
+
title?: string
|
|
17
|
+
/** Body copy. */
|
|
18
|
+
body?: string
|
|
19
|
+
/** Footer string (alternative to the named footer slot). */
|
|
20
|
+
footer?: string
|
|
21
|
+
class?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
kicker,
|
|
26
|
+
title,
|
|
27
|
+
body,
|
|
28
|
+
footer,
|
|
29
|
+
class: className,
|
|
30
|
+
...rest
|
|
31
|
+
} = Astro.props
|
|
32
|
+
|
|
33
|
+
const { ariaProps, dataAttributes } = createAudienceFeatureCard()
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
class={cn(audienceFeatureCardVariants(), className)}
|
|
38
|
+
{...ariaProps}
|
|
39
|
+
{...dataAttributes}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{kicker && <p class={audienceFeatureCardKickerClass}>{kicker}</p>}
|
|
43
|
+
{title
|
|
44
|
+
? <p class={audienceFeatureCardTitleClass}>{title}</p>
|
|
45
|
+
: <p class={audienceFeatureCardTitleClass}><slot /></p>
|
|
46
|
+
}
|
|
47
|
+
{body && <p class={audienceFeatureCardBodyClass}>{body}</p>}
|
|
48
|
+
{(footer || Astro.slots.has('footer')) && (
|
|
49
|
+
<div class={audienceFeatureCardFooterClass}>
|
|
50
|
+
{footer ?? <slot name="footer" />}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createBrandNetworkCell,
|
|
4
|
+
brandNetworkCellVariants,
|
|
5
|
+
brandNetworkCellGlyphClass,
|
|
6
|
+
brandNetworkCellDomainClass,
|
|
7
|
+
brandNetworkCellCurrentBadgeClass,
|
|
8
|
+
brandNetworkCellBodyClass,
|
|
9
|
+
brandNetworkCellLinkClass,
|
|
10
|
+
} from '../brand-network-cell/index.ts'
|
|
11
|
+
import { cn } from '../shared/index.ts'
|
|
12
|
+
|
|
13
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
14
|
+
/** Single character or short string used as the brand monogram. */
|
|
15
|
+
glyph: string
|
|
16
|
+
/** CSS colour value for the glyph box background (brand tint). Applied via inline style. */
|
|
17
|
+
glyphBg?: string
|
|
18
|
+
/** CSS colour value for the glyph text (brand tint). Applied via inline style. */
|
|
19
|
+
glyphColor?: string
|
|
20
|
+
/** Primary identifier shown below the glyph (product domain or name). */
|
|
21
|
+
domain: string
|
|
22
|
+
/** Supporting description text. */
|
|
23
|
+
body: string
|
|
24
|
+
/** When provided, renders a link at the bottom of the card. */
|
|
25
|
+
href?: string
|
|
26
|
+
/** Label for the link. Defaults to "Visit →". */
|
|
27
|
+
linkLabel?: string
|
|
28
|
+
/** Marks this cell as the current/active product in the network. */
|
|
29
|
+
current?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
glyph,
|
|
34
|
+
glyphBg,
|
|
35
|
+
glyphColor,
|
|
36
|
+
domain,
|
|
37
|
+
body,
|
|
38
|
+
href,
|
|
39
|
+
linkLabel,
|
|
40
|
+
current = false,
|
|
41
|
+
class: className,
|
|
42
|
+
...rest
|
|
43
|
+
} = Astro.props
|
|
44
|
+
|
|
45
|
+
const { ariaProps, dataAttributes } = createBrandNetworkCell({ current })
|
|
46
|
+
const glyphStyle = `${glyphBg ? `background:${glyphBg};` : ''}${glyphColor ? `color:${glyphColor};` : ''}`
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
class={cn(brandNetworkCellVariants({ current: current ? 'true' : 'false' }), className)}
|
|
51
|
+
{...ariaProps}
|
|
52
|
+
{...dataAttributes}
|
|
53
|
+
{...rest}
|
|
54
|
+
>
|
|
55
|
+
<!-- Glyph box — brand tint via inline style -->
|
|
56
|
+
<div class={brandNetworkCellGlyphClass} style={glyphStyle} aria-hidden="true">
|
|
57
|
+
{glyph}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Domain row -->
|
|
61
|
+
<div class="flex items-center gap-2">
|
|
62
|
+
<span class={brandNetworkCellDomainClass}>{domain}</span>
|
|
63
|
+
{current && (
|
|
64
|
+
<span class={brandNetworkCellCurrentBadgeClass}>You are here</span>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Body -->
|
|
69
|
+
<p class={brandNetworkCellBodyClass}>{body}</p>
|
|
70
|
+
|
|
71
|
+
<!-- Optional link -->
|
|
72
|
+
{href && (
|
|
73
|
+
<a href={href} class={brandNetworkCellLinkClass}>
|
|
74
|
+
{linkLabel ?? 'Visit →'}
|
|
75
|
+
</a>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createBrowserChromeMock,
|
|
4
|
+
splitUrl,
|
|
5
|
+
browserChromeMockVariants,
|
|
6
|
+
chromeBarClass,
|
|
7
|
+
trafficDotClass,
|
|
8
|
+
urlBarClass,
|
|
9
|
+
urlDomainClass,
|
|
10
|
+
statusBadgeVariants,
|
|
11
|
+
contentAreaClass,
|
|
12
|
+
type BrowserChromeStatus,
|
|
13
|
+
} from '../browser-chrome-mock/index.ts'
|
|
14
|
+
import { cn } from '../shared/index.ts'
|
|
15
|
+
|
|
16
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
17
|
+
/** URL to display in the address bar. Bold domain + normal path. */
|
|
18
|
+
url: string
|
|
19
|
+
/** Optional status badge shown in the chrome bar. */
|
|
20
|
+
status?: BrowserChromeStatus
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { url, status, class: className, ...rest } = Astro.props
|
|
24
|
+
|
|
25
|
+
const { ariaProps, dataAttributes } = createBrowserChromeMock({ status })
|
|
26
|
+
const { domain, path } = splitUrl(url)
|
|
27
|
+
|
|
28
|
+
const trafficDots = ['bg-red-400', 'bg-yellow-400', 'bg-green-400'] as const
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<div class={cn(browserChromeMockVariants(), className)} {...ariaProps} {...dataAttributes} {...rest}>
|
|
32
|
+
<!-- Chrome toolbar -->
|
|
33
|
+
<div class={chromeBarClass}>
|
|
34
|
+
<!-- Traffic-light dots -->
|
|
35
|
+
<div class="flex items-center gap-1.5 shrink-0">
|
|
36
|
+
{trafficDots.map((color) => (
|
|
37
|
+
<span class={`${trafficDotClass} ${color}`} />
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- URL bar -->
|
|
42
|
+
<div class={urlBarClass}>
|
|
43
|
+
<span class={urlDomainClass}>{domain}</span>
|
|
44
|
+
{path && <span>{path}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Status badge -->
|
|
48
|
+
{status && (
|
|
49
|
+
<div class={statusBadgeVariants({ status })}>
|
|
50
|
+
{status === 'rec' && (
|
|
51
|
+
<span class="size-1.5 rounded-full bg-destructive animate-pulse" />
|
|
52
|
+
)}
|
|
53
|
+
{status}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Content area -->
|
|
59
|
+
<div class={contentAreaClass}>
|
|
60
|
+
<slot />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createMarqueeStrip,
|
|
4
|
+
marqueeStripVariants,
|
|
5
|
+
marqueeStripInnerClass,
|
|
6
|
+
marqueeStripScrollOuterClass,
|
|
7
|
+
marqueeStripScrollTrackClass,
|
|
8
|
+
marqueeStripLabelClass,
|
|
9
|
+
marqueeStripItemClass,
|
|
10
|
+
} from '../marquee-strip/index.ts'
|
|
11
|
+
import { cn } from '../shared/index.ts'
|
|
12
|
+
|
|
13
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
14
|
+
/** Eyebrow label shown before the items (static mode only). */
|
|
15
|
+
label?: string
|
|
16
|
+
/** List of text items to display. */
|
|
17
|
+
items: string[]
|
|
18
|
+
/**
|
|
19
|
+
* When `true` the items scroll continuously using a CSS `marquee` keyframe.
|
|
20
|
+
* Define the keyframe in your global CSS:
|
|
21
|
+
*
|
|
22
|
+
* ```css
|
|
23
|
+
* @keyframes marquee {
|
|
24
|
+
* from { transform: translateX(0); }
|
|
25
|
+
* to { transform: translateX(-50%); }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
scroll?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
label,
|
|
36
|
+
items,
|
|
37
|
+
scroll = false,
|
|
38
|
+
class: className,
|
|
39
|
+
...rest
|
|
40
|
+
} = Astro.props
|
|
41
|
+
|
|
42
|
+
const { ariaProps, dataAttributes } = createMarqueeStrip({ scroll })
|
|
43
|
+
const scrollVariant = scroll ? 'true' : 'false'
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<div class={cn(marqueeStripVariants({ scroll: scrollVariant }), className)} {...ariaProps} {...dataAttributes} {...rest}>
|
|
47
|
+
{scroll ? (
|
|
48
|
+
<div class={marqueeStripScrollOuterClass}>
|
|
49
|
+
<div class={marqueeStripScrollTrackClass}>
|
|
50
|
+
{[...items, ...items].map((item) => (
|
|
51
|
+
<span class={marqueeStripItemClass}>{item}</span>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<div class={marqueeStripInnerClass}>
|
|
57
|
+
{label && <span class={marqueeStripLabelClass}>{label}</span>}
|
|
58
|
+
{items.map((item) => (
|
|
59
|
+
<span class={marqueeStripItemClass}>{item}</span>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
clampPercent,
|
|
4
|
+
createMasteryBar,
|
|
5
|
+
masteryBarVariants,
|
|
6
|
+
masteryBarFillVariants,
|
|
7
|
+
masteryBarLabelClass,
|
|
8
|
+
masteryBarLeadingLabelClass,
|
|
9
|
+
masteryBarHeaderClass,
|
|
10
|
+
type MasteryBarSize,
|
|
11
|
+
} from '../mastery-bar/index.ts'
|
|
12
|
+
import { cn } from '../shared/index.ts'
|
|
13
|
+
|
|
14
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
15
|
+
/** Progress value (0–100). Values outside this range are clamped. */
|
|
16
|
+
value: number
|
|
17
|
+
/** Label shown on the right side of the header row. */
|
|
18
|
+
label?: string
|
|
19
|
+
/** Label shown on the left side of the header row. */
|
|
20
|
+
leadingLabel?: string
|
|
21
|
+
/** Visual size of the track. */
|
|
22
|
+
size?: MasteryBarSize
|
|
23
|
+
/** Renders the fill at reduced opacity to indicate a muted/secondary state. */
|
|
24
|
+
muted?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
value,
|
|
29
|
+
label,
|
|
30
|
+
leadingLabel,
|
|
31
|
+
size = 'md',
|
|
32
|
+
muted = false,
|
|
33
|
+
class: className,
|
|
34
|
+
...rest
|
|
35
|
+
} = Astro.props
|
|
36
|
+
|
|
37
|
+
const clamped = clampPercent(value)
|
|
38
|
+
const { ariaProps, dataAttributes } = createMasteryBar({ value })
|
|
39
|
+
const hasHeader = leadingLabel != null || label != null
|
|
40
|
+
const fillWidth = `${clamped}%`
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<div class={cn('w-full', className)} {...rest}>
|
|
44
|
+
{hasHeader && (
|
|
45
|
+
<div class={masteryBarHeaderClass}>
|
|
46
|
+
{leadingLabel != null && <span class={masteryBarLeadingLabelClass}>{leadingLabel}</span>}
|
|
47
|
+
{label != null && <span class={masteryBarLabelClass}>{label}</span>}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
<div class={masteryBarVariants({ size })} {...ariaProps} {...dataAttributes}>
|
|
51
|
+
<div
|
|
52
|
+
class={masteryBarFillVariants({ muted: muted ? 'true' : 'false' })}
|
|
53
|
+
style={`width:${fillWidth}`}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createNumberedSteps,
|
|
4
|
+
padOrdinal,
|
|
5
|
+
stepColumns,
|
|
6
|
+
numberedStepsItemClass,
|
|
7
|
+
numberedStepsOrdinalClass,
|
|
8
|
+
numberedStepsTitleClass,
|
|
9
|
+
numberedStepsBodyClass,
|
|
10
|
+
type NumberedStepItem,
|
|
11
|
+
} from '../numbered-steps/index.ts'
|
|
12
|
+
import { cn } from '../shared/index.ts'
|
|
13
|
+
|
|
14
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
15
|
+
/** The step items to display (3–5 recommended). */
|
|
16
|
+
items: NumberedStepItem[]
|
|
17
|
+
/**
|
|
18
|
+
* Override the number of grid columns. Defaults to clamping item count to
|
|
19
|
+
* 2–5 via `stepColumns`.
|
|
20
|
+
*/
|
|
21
|
+
columns?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { items, columns, class: className, ...rest } = Astro.props
|
|
25
|
+
|
|
26
|
+
const cols = columns ?? stepColumns(items.length)
|
|
27
|
+
const gridStyle = `grid-template-columns:repeat(${cols},minmax(0,1fr))`
|
|
28
|
+
const { ariaProps, dataAttributes } = createNumberedSteps()
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
class={cn('grid gap-6', className)}
|
|
33
|
+
style={gridStyle}
|
|
34
|
+
{...ariaProps}
|
|
35
|
+
{...dataAttributes}
|
|
36
|
+
{...rest}
|
|
37
|
+
>
|
|
38
|
+
{
|
|
39
|
+
items.map((item, index) => (
|
|
40
|
+
<div role="listitem" class={numberedStepsItemClass}>
|
|
41
|
+
<span class={numberedStepsOrdinalClass}>{padOrdinal(index + 1)}</span>
|
|
42
|
+
<p class={numberedStepsTitleClass}>{item.title}</p>
|
|
43
|
+
<p class={numberedStepsBodyClass}>{item.body}</p>
|
|
44
|
+
</div>
|
|
45
|
+
))
|
|
46
|
+
}
|
|
47
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createPricingCard,
|
|
4
|
+
pricingCardVariants,
|
|
5
|
+
pricingCardBadgeClass,
|
|
6
|
+
pricingCardNameClass,
|
|
7
|
+
pricingCardPriceClass,
|
|
8
|
+
pricingCardPeriodClass,
|
|
9
|
+
pricingCardDescriptionClass,
|
|
10
|
+
pricingCardFeatureListClass,
|
|
11
|
+
pricingCardFeatureRowClass,
|
|
12
|
+
pricingCardFeatureTickClass,
|
|
13
|
+
pricingCtaVariants,
|
|
14
|
+
type PricingCtaVariant,
|
|
15
|
+
} from '../pricing-card/index.ts'
|
|
16
|
+
import { cn } from '../shared/index.ts'
|
|
17
|
+
|
|
18
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
19
|
+
/** Optional badge label shown above the plan name (e.g. "Most popular"). */
|
|
20
|
+
badge?: string
|
|
21
|
+
/** Plan name (e.g. "Pro", "Starter"). */
|
|
22
|
+
name: string
|
|
23
|
+
/** Price string (e.g. "$29", "Free"). */
|
|
24
|
+
price: string
|
|
25
|
+
/** Billing period or qualifier (e.g. "/ month"). */
|
|
26
|
+
period?: string
|
|
27
|
+
/** Short marketing description. */
|
|
28
|
+
description?: string
|
|
29
|
+
/** List of included features to render as a checklist. */
|
|
30
|
+
features: string[]
|
|
31
|
+
/** CTA button/link label. */
|
|
32
|
+
cta: string
|
|
33
|
+
/** Visual variant for the CTA. */
|
|
34
|
+
ctaVariant?: PricingCtaVariant
|
|
35
|
+
/** When provided, the CTA renders as an `<a>` tag. */
|
|
36
|
+
ctaHref?: string
|
|
37
|
+
/** Highlights the card with a primary ring. */
|
|
38
|
+
featured?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
badge,
|
|
43
|
+
name,
|
|
44
|
+
price,
|
|
45
|
+
period,
|
|
46
|
+
description,
|
|
47
|
+
features,
|
|
48
|
+
cta,
|
|
49
|
+
ctaVariant = 'default',
|
|
50
|
+
ctaHref,
|
|
51
|
+
featured = false,
|
|
52
|
+
class: className,
|
|
53
|
+
...rest
|
|
54
|
+
} = Astro.props
|
|
55
|
+
|
|
56
|
+
const { ariaProps, dataAttributes } = createPricingCard({ featured })
|
|
57
|
+
const ctaClass = pricingCtaVariants({ variant: ctaVariant })
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
class={cn(pricingCardVariants({ featured: featured ? 'true' : 'false' }), className)}
|
|
62
|
+
{...ariaProps}
|
|
63
|
+
{...dataAttributes}
|
|
64
|
+
{...rest}
|
|
65
|
+
>
|
|
66
|
+
{badge && <span class={pricingCardBadgeClass}>{badge}</span>}
|
|
67
|
+
|
|
68
|
+
<p class={pricingCardNameClass}>{name}</p>
|
|
69
|
+
|
|
70
|
+
<div class="mt-4 flex items-baseline gap-1">
|
|
71
|
+
<span class={pricingCardPriceClass}>{price}</span>
|
|
72
|
+
{period && <span class={pricingCardPeriodClass}>{period}</span>}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{description && <p class={`mt-2 ${pricingCardDescriptionClass}`}>{description}</p>}
|
|
76
|
+
|
|
77
|
+
<ul class={`mt-6 ${pricingCardFeatureListClass}`}>
|
|
78
|
+
{
|
|
79
|
+
features.map((feature) => (
|
|
80
|
+
<li class={pricingCardFeatureRowClass}>
|
|
81
|
+
<svg
|
|
82
|
+
class={pricingCardFeatureTickClass}
|
|
83
|
+
viewBox="0 0 16 16"
|
|
84
|
+
fill="none"
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
d="M3 8l3.5 3.5L13 4.5"
|
|
89
|
+
stroke="currentColor"
|
|
90
|
+
stroke-width="1.75"
|
|
91
|
+
stroke-linecap="round"
|
|
92
|
+
stroke-linejoin="round"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
{feature}
|
|
96
|
+
</li>
|
|
97
|
+
))
|
|
98
|
+
}
|
|
99
|
+
</ul>
|
|
100
|
+
|
|
101
|
+
<div class="mt-6">
|
|
102
|
+
{
|
|
103
|
+
ctaHref ? (
|
|
104
|
+
<a href={ctaHref} class={ctaClass}>
|
|
105
|
+
{cta}
|
|
106
|
+
</a>
|
|
107
|
+
) : (
|
|
108
|
+
<button type="button" class={ctaClass}>
|
|
109
|
+
{cta}
|
|
110
|
+
</button>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createSectionHead,
|
|
4
|
+
sectionHeadVariants,
|
|
5
|
+
sectionHeadKickerClass,
|
|
6
|
+
sectionHeadTitleClass,
|
|
7
|
+
sectionHeadLedeClass,
|
|
8
|
+
type SectionHeadAlign,
|
|
9
|
+
} from '../section-head/index.ts'
|
|
10
|
+
import { cn } from '../shared/index.ts'
|
|
11
|
+
|
|
12
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
13
|
+
/** Optional eyebrow / kicker rendered above the title. */
|
|
14
|
+
kicker?: string
|
|
15
|
+
/** Primary heading content. Falls back to the default slot when not set. */
|
|
16
|
+
title?: string
|
|
17
|
+
/** Optional lede paragraph rendered below the title. */
|
|
18
|
+
lede?: string
|
|
19
|
+
/** Horizontal alignment of the cluster. */
|
|
20
|
+
align?: SectionHeadAlign
|
|
21
|
+
/** Override the heading element rendered for the title. Defaults to `h2`. */
|
|
22
|
+
as?: 'h1' | 'h2' | 'h3'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
kicker,
|
|
27
|
+
title,
|
|
28
|
+
lede,
|
|
29
|
+
align = 'center',
|
|
30
|
+
as: Tag = 'h2',
|
|
31
|
+
class: className,
|
|
32
|
+
...rest
|
|
33
|
+
} = Astro.props
|
|
34
|
+
|
|
35
|
+
const { dataAttributes } = createSectionHead({ align })
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<div class={cn(sectionHeadVariants({ align }), className)} {...dataAttributes} {...rest}>
|
|
39
|
+
{kicker && <p class={sectionHeadKickerClass}>{kicker}</p>}
|
|
40
|
+
<Tag class={sectionHeadTitleClass}>
|
|
41
|
+
{title ?? <slot />}
|
|
42
|
+
</Tag>
|
|
43
|
+
{lede && <p class={sectionHeadLedeClass}>{lede}</p>}
|
|
44
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createStatGrid,
|
|
4
|
+
statColumns,
|
|
5
|
+
statGridVariants,
|
|
6
|
+
statItemClass,
|
|
7
|
+
statValueClass,
|
|
8
|
+
statLabelClass,
|
|
9
|
+
type StatItem,
|
|
10
|
+
} from '../stat-grid/index.ts'
|
|
11
|
+
import { cn } from '../shared/index.ts'
|
|
12
|
+
|
|
13
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
14
|
+
/** The stat items to display in the grid (value and label as strings). */
|
|
15
|
+
items: StatItem[]
|
|
16
|
+
/**
|
|
17
|
+
* Number of columns. Defaults to statColumns(items.length):
|
|
18
|
+
* 1 item → 1, 2 items → 2, 3+ items → 3.
|
|
19
|
+
*/
|
|
20
|
+
columns?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { items, columns, class: className, ...rest } = Astro.props
|
|
24
|
+
|
|
25
|
+
const cols = columns ?? statColumns(items.length)
|
|
26
|
+
const colsKey = String(cols) as '1' | '2' | '3'
|
|
27
|
+
const { ariaProps, dataAttributes } = createStatGrid()
|
|
28
|
+
const gridStyle = `grid-template-columns:repeat(${cols}, 1fr)`
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
class={cn(statGridVariants({ columns: colsKey }), className)}
|
|
33
|
+
style={gridStyle}
|
|
34
|
+
{...ariaProps}
|
|
35
|
+
{...dataAttributes}
|
|
36
|
+
{...rest}
|
|
37
|
+
>
|
|
38
|
+
{
|
|
39
|
+
items.map((item) => (
|
|
40
|
+
<div role="listitem" class={statItemClass}>
|
|
41
|
+
<span class={statValueClass}>{item.value}</span>
|
|
42
|
+
<span class={statLabelClass}>{item.label}</span>
|
|
43
|
+
</div>
|
|
44
|
+
))
|
|
45
|
+
}
|
|
46
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Container — full-height flex column so footers align across a row. */
|
|
4
|
+
export const audienceFeatureCardVariants = cva({
|
|
5
|
+
base: 'flex h-full flex-col rounded-xl border border-border bg-card p-5',
|
|
6
|
+
variants: {},
|
|
7
|
+
defaultVariants: {},
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/** Kicker text above the title. */
|
|
11
|
+
export const audienceFeatureCardKickerClass =
|
|
12
|
+
'text-[10px] uppercase tracking-widest text-primary font-semibold'
|
|
13
|
+
|
|
14
|
+
/** Main title. */
|
|
15
|
+
export const audienceFeatureCardTitleClass =
|
|
16
|
+
'mt-2 text-lg font-bold text-foreground'
|
|
17
|
+
|
|
18
|
+
/** Body copy. */
|
|
19
|
+
export const audienceFeatureCardBodyClass =
|
|
20
|
+
'mt-1 text-sm text-muted-foreground leading-relaxed'
|
|
21
|
+
|
|
22
|
+
/** Footer area — pushed to bottom via mt-auto. */
|
|
23
|
+
export const audienceFeatureCardFooterClass = 'mt-auto pt-4 text-xs'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export interface AudienceFeatureCardProps {
|
|
4
|
+
/** Kicker text displayed above the title (e.g. "For teams"). */
|
|
5
|
+
kicker?: string
|
|
6
|
+
/** Main title — required. */
|
|
7
|
+
title: string
|
|
8
|
+
/** Body copy. */
|
|
9
|
+
body: string
|
|
10
|
+
/** Footer content (optional). */
|
|
11
|
+
footer?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AudienceFeatureCardAPI {
|
|
15
|
+
/** ARIA attributes to spread on the group element (`role="group"`). */
|
|
16
|
+
ariaProps: Partial<AccessibilityProps>
|
|
17
|
+
/** Data attributes for styling hooks. */
|
|
18
|
+
dataAttributes: Record<string, string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the framework-agnostic group props for an audience feature card.
|
|
23
|
+
*
|
|
24
|
+
* Returns `role="group"` plus data attributes; adapters spread these onto
|
|
25
|
+
* their container and render the kicker, title, body, and footer slots.
|
|
26
|
+
*/
|
|
27
|
+
export function createAudienceFeatureCard(
|
|
28
|
+
_props: Partial<AudienceFeatureCardProps> = {},
|
|
29
|
+
): AudienceFeatureCardAPI {
|
|
30
|
+
const ariaProps: Partial<AccessibilityProps> = {
|
|
31
|
+
role: 'group',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dataAttributes: Record<string, string> = {
|
|
35
|
+
'data-component': 'audience-feature-card',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { ariaProps, dataAttributes }
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createAudienceFeatureCard,
|
|
3
|
+
type AudienceFeatureCardProps,
|
|
4
|
+
type AudienceFeatureCardAPI,
|
|
5
|
+
} from './audience-feature-card.js'
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
audienceFeatureCardVariants,
|
|
9
|
+
audienceFeatureCardKickerClass,
|
|
10
|
+
audienceFeatureCardTitleClass,
|
|
11
|
+
audienceFeatureCardBodyClass,
|
|
12
|
+
audienceFeatureCardFooterClass,
|
|
13
|
+
} from './audience-feature-card.styles.js'
|