@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.
Files changed (47) hide show
  1. package/dist/astro-audience-feature-card/AudienceFeatureCard.astro +53 -0
  2. package/dist/astro-audience-feature-card/index.ts +1 -0
  3. package/dist/astro-brand-network-cell/BrandNetworkCell.astro +77 -0
  4. package/dist/astro-brand-network-cell/index.ts +1 -0
  5. package/dist/astro-browser-chrome-mock/BrowserChromeMock.astro +62 -0
  6. package/dist/astro-browser-chrome-mock/index.ts +1 -0
  7. package/dist/astro-marquee-strip/MarqueeStrip.astro +63 -0
  8. package/dist/astro-marquee-strip/index.ts +1 -0
  9. package/dist/astro-mastery-bar/MasteryBar.astro +56 -0
  10. package/dist/astro-mastery-bar/index.ts +1 -0
  11. package/dist/astro-numbered-steps/NumberedSteps.astro +47 -0
  12. package/dist/astro-numbered-steps/index.ts +1 -0
  13. package/dist/astro-pricing-card/PricingCard.astro +114 -0
  14. package/dist/astro-pricing-card/index.ts +1 -0
  15. package/dist/astro-section-head/SectionHead.astro +44 -0
  16. package/dist/astro-section-head/index.ts +1 -0
  17. package/dist/astro-stat-grid/StatGrid.astro +46 -0
  18. package/dist/astro-stat-grid/index.ts +1 -0
  19. package/dist/audience-feature-card/audience-feature-card.styles.ts +23 -0
  20. package/dist/audience-feature-card/audience-feature-card.ts +39 -0
  21. package/dist/audience-feature-card/index.ts +13 -0
  22. package/dist/brand-network-cell/brand-network-cell.styles.ts +46 -0
  23. package/dist/brand-network-cell/brand-network-cell.ts +35 -0
  24. package/dist/brand-network-cell/index.ts +14 -0
  25. package/dist/browser-chrome-mock/browser-chrome-mock.styles.ts +43 -0
  26. package/dist/browser-chrome-mock/browser-chrome-mock.ts +52 -0
  27. package/dist/browser-chrome-mock/index.ts +17 -0
  28. package/dist/index.ts +9 -0
  29. package/dist/marquee-strip/index.ts +14 -0
  30. package/dist/marquee-strip/marquee-strip.styles.ts +61 -0
  31. package/dist/marquee-strip/marquee-strip.ts +35 -0
  32. package/dist/mastery-bar/index.ts +15 -0
  33. package/dist/mastery-bar/mastery-bar.styles.ts +45 -0
  34. package/dist/mastery-bar/mastery-bar.ts +55 -0
  35. package/dist/numbered-steps/index.ts +16 -0
  36. package/dist/numbered-steps/numbered-steps.styles.ts +36 -0
  37. package/dist/numbered-steps/numbered-steps.ts +64 -0
  38. package/dist/pricing-card/index.ts +19 -0
  39. package/dist/pricing-card/pricing-card.styles.ts +71 -0
  40. package/dist/pricing-card/pricing-card.ts +37 -0
  41. package/dist/section-head/index.ts +13 -0
  42. package/dist/section-head/section-head.styles.ts +30 -0
  43. package/dist/section-head/section-head.ts +28 -0
  44. package/dist/stat-grid/index.ts +13 -0
  45. package/dist/stat-grid/stat-grid.styles.ts +25 -0
  46. package/dist/stat-grid/stat-grid.ts +41 -0
  47. package/package.json +10 -1
@@ -0,0 +1,46 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Root card container. The `current` variant adds the primary ring that signals
5
+ * "You are here". Background and border come from semantic tokens so the card
6
+ * works in both light and dark themes without callers touching class names.
7
+ */
8
+ export const brandNetworkCellVariants = cva({
9
+ base: [
10
+ 'relative rounded-xl border border-border bg-card p-4',
11
+ 'flex flex-col gap-3 transition-shadow',
12
+ ].join(' '),
13
+ variants: {
14
+ current: {
15
+ true: 'ring-1 ring-primary',
16
+ false: '',
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ current: 'false',
21
+ },
22
+ })
23
+
24
+ /**
25
+ * Square glyph box — the brand monogram or icon.
26
+ * Background and text colour are caller-provided via inline `style` (brand
27
+ * tints vary per product); this class only controls geometry and typography.
28
+ */
29
+ export const brandNetworkCellGlyphClass =
30
+ 'flex size-10 items-center justify-center rounded-lg text-sm font-bold'
31
+
32
+ /** Domain / product name row. */
33
+ export const brandNetworkCellDomainClass = 'font-semibold text-foreground'
34
+
35
+ /**
36
+ * "You are here" badge shown beside the domain when `current` is true.
37
+ * Uses semantic token colours so it inherits the active theme automatically.
38
+ */
39
+ export const brandNetworkCellCurrentBadgeClass =
40
+ 'inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary'
41
+
42
+ /** Supporting body text below the domain. */
43
+ export const brandNetworkCellBodyClass = 'text-sm text-muted-foreground'
44
+
45
+ /** Inline link row rendered at the bottom of the card. */
46
+ export const brandNetworkCellLinkClass = 'text-sm text-primary'
@@ -0,0 +1,35 @@
1
+ import type { AccessibilityProps } from '../shared/index.ts'
2
+
3
+ export interface BrandNetworkCellProps {
4
+ /** Whether this cell represents the current/active product in the network. */
5
+ current?: boolean
6
+ }
7
+
8
+ export interface BrandNetworkCellAPI {
9
+ /** ARIA attributes to spread on the group element (`role="group"`). */
10
+ ariaProps: Partial<AccessibilityProps>
11
+ /** Data attributes for styling hooks. */
12
+ dataAttributes: Record<string, string>
13
+ }
14
+
15
+ /**
16
+ * Build the framework-agnostic group props for a brand network card.
17
+ *
18
+ * Returns `role="group"` plus a `data-current` attribute so adapters and
19
+ * consumers can style the "You are here" state without coupling to class names.
20
+ */
21
+ export function createBrandNetworkCell(
22
+ props: BrandNetworkCellProps = {},
23
+ ): BrandNetworkCellAPI {
24
+ const { current = false } = props
25
+
26
+ const ariaProps: Partial<AccessibilityProps> = {
27
+ role: 'group',
28
+ }
29
+
30
+ const dataAttributes: Record<string, string> = {
31
+ 'data-current': String(current),
32
+ }
33
+
34
+ return { ariaProps, dataAttributes }
35
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ createBrandNetworkCell,
3
+ type BrandNetworkCellProps,
4
+ type BrandNetworkCellAPI,
5
+ } from './brand-network-cell.js'
6
+
7
+ export {
8
+ brandNetworkCellVariants,
9
+ brandNetworkCellGlyphClass,
10
+ brandNetworkCellDomainClass,
11
+ brandNetworkCellCurrentBadgeClass,
12
+ brandNetworkCellBodyClass,
13
+ brandNetworkCellLinkClass,
14
+ } from './brand-network-cell.styles.js'
@@ -0,0 +1,43 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** Outer frame wrapping the chrome bar and content. */
4
+ export const browserChromeMockVariants = cva({
5
+ base: 'rounded-xl border border-border overflow-hidden bg-card',
6
+ variants: {},
7
+ defaultVariants: {},
8
+ })
9
+
10
+ /** The chrome toolbar row containing dots, URL bar, and optional badge. */
11
+ export const chromeBarClass =
12
+ 'flex items-center gap-2 bg-muted/40 border-b border-border px-3 py-2'
13
+
14
+ /** A single traffic-light dot. */
15
+ export const trafficDotClass = 'size-2.5 rounded-full border border-border'
16
+
17
+ /** Centered URL bar. */
18
+ export const urlBarClass = 'flex-1 text-center font-mono text-[11px] text-muted-foreground'
19
+
20
+ /** Bold domain segment inside the URL bar. */
21
+ export const urlDomainClass = 'font-semibold text-foreground'
22
+
23
+ /**
24
+ * Status badge shown in the chrome bar.
25
+ *
26
+ * live → emerald tint (approved exception to semantic tokens)
27
+ * rec → destructive tint with a pulsing dot
28
+ */
29
+ export const statusBadgeVariants = cva({
30
+ base: 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
31
+ variants: {
32
+ status: {
33
+ live: 'text-emerald-500 bg-emerald-500/10',
34
+ rec: 'text-destructive bg-destructive/10',
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ status: 'live',
39
+ },
40
+ })
41
+
42
+ /** Inner content area that receives slotted/children content. */
43
+ export const contentAreaClass = 'p-0'
@@ -0,0 +1,52 @@
1
+ /** Recording or live stream status for the browser chrome mock. */
2
+ export type BrowserChromeStatus = 'live' | 'rec'
3
+
4
+ export interface BrowserChromeMockProps {
5
+ /** URL to display in the address bar. */
6
+ url: string
7
+ /** Optional status badge shown in the chrome bar. */
8
+ status?: BrowserChromeStatus
9
+ }
10
+
11
+ export interface BrowserChromeMockAPI {
12
+ /** ARIA attributes to spread on the group element. */
13
+ ariaProps: { role: 'group'; 'aria-label': string }
14
+ /** Data attributes for styling hooks. */
15
+ dataAttributes: Record<string, string>
16
+ }
17
+
18
+ /** Split a URL into bold domain and normal path segments. */
19
+ export function splitUrl(url: string): { domain: string; path: string } {
20
+ const slashIndex = url.indexOf('/')
21
+ if (slashIndex === -1) {
22
+ return { domain: url, path: '' }
23
+ }
24
+ return {
25
+ domain: url.slice(0, slashIndex),
26
+ path: url.slice(slashIndex),
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Build the framework-agnostic props for a BrowserChromeMock.
32
+ *
33
+ * Returns a `role="group"` aria label plus optional `data-status` so adapters
34
+ * can hook CSS without duplicating logic.
35
+ */
36
+ export function createBrowserChromeMock(
37
+ props: Pick<BrowserChromeMockProps, 'status'> = {},
38
+ ): BrowserChromeMockAPI {
39
+ const { status } = props
40
+
41
+ const ariaProps = {
42
+ role: 'group' as const,
43
+ 'aria-label': 'Browser preview',
44
+ }
45
+
46
+ const dataAttributes: Record<string, string> = {}
47
+ if (status !== undefined) {
48
+ dataAttributes['data-status'] = status
49
+ }
50
+
51
+ return { ariaProps, dataAttributes }
52
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ createBrowserChromeMock,
3
+ splitUrl,
4
+ type BrowserChromeStatus,
5
+ type BrowserChromeMockProps,
6
+ type BrowserChromeMockAPI,
7
+ } from './browser-chrome-mock.js'
8
+
9
+ export {
10
+ browserChromeMockVariants,
11
+ chromeBarClass,
12
+ trafficDotClass,
13
+ urlBarClass,
14
+ urlDomainClass,
15
+ statusBadgeVariants,
16
+ contentAreaClass,
17
+ } from './browser-chrome-mock.styles.js'
package/dist/index.ts CHANGED
@@ -5,13 +5,16 @@ export * from './astro-ai/index.ts';
5
5
  export * from './astro-analytics/index.ts';
6
6
  export * from './astro-animated-text/index.ts';
7
7
  export * from './astro-app-shell/index.ts';
8
+ export * from './astro-audience-feature-card/index.ts';
8
9
  export * from './astro-audio-room/index.ts';
9
10
  export * from './astro-auth/index.ts';
10
11
  export * from './astro-avatar/index.ts';
11
12
  export * from './astro-avatar-group/index.ts';
12
13
  export * from './astro-badge/index.ts';
13
14
  export * from './astro-bottom-nav/index.ts';
15
+ export * from './astro-brand-network-cell/index.ts';
14
16
  export * from './astro-breadcrumbs/index.ts';
17
+ export * from './astro-browser-chrome-mock/index.ts';
15
18
  export * from './astro-button/index.ts';
16
19
  export * from './astro-calendar/index.ts';
17
20
  export * from './astro-call-controls/index.ts';
@@ -61,9 +64,12 @@ export * from './astro-live-cursors/index.ts';
61
64
  export * from './astro-live-transcript/index.ts';
62
65
  export * from './astro-logger/index.ts';
63
66
  export * from './astro-markdown-renderer/index.ts';
67
+ export * from './astro-marquee-strip/index.ts';
68
+ export * from './astro-mastery-bar/index.ts';
64
69
  export * from './astro-mini-map/index.ts';
65
70
  export * from './astro-mobile-nav/index.ts';
66
71
  export * from './astro-navbar/index.ts';
72
+ export * from './astro-numbered-steps/index.ts';
67
73
  export * from './astro-otp-input/index.ts';
68
74
  export * from './astro-pagination/index.ts';
69
75
  export * from './astro-password-input/index.ts';
@@ -71,6 +77,7 @@ export * from './astro-payment/index.ts';
71
77
  export * from './astro-popover/index.ts';
72
78
  export * from './astro-pre-call-lobby/index.ts';
73
79
  export * from './astro-presence-indicator/index.ts';
80
+ export * from './astro-pricing-card/index.ts';
74
81
  export * from './astro-progress-display/index.ts';
75
82
  export * from './astro-radial-gauge/index.ts';
76
83
  export * from './astro-radio/index.ts';
@@ -79,6 +86,7 @@ export * from './astro-reaction-bar/index.ts';
79
86
  export * from './astro-resizable-layout/index.ts';
80
87
  export * from './astro-rich-editor/index.ts';
81
88
  export * from './astro-search-bar/index.ts';
89
+ export * from './astro-section-head/index.ts';
82
90
  export * from './astro-segmented-control/index.ts';
83
91
  export * from './astro-select/index.ts';
84
92
  export * from './astro-separator/index.ts';
@@ -88,6 +96,7 @@ export * from './astro-skip-to-content/index.ts';
88
96
  export * from './astro-slide-viewer/index.ts';
89
97
  export * from './astro-slider/index.ts';
90
98
  export * from './astro-social-auth-button/index.ts';
99
+ export * from './astro-stat-grid/index.ts';
91
100
  export * from './astro-status-indicator/index.ts';
92
101
  export * from './astro-steps/index.ts';
93
102
  export * from './astro-sticky-note/index.ts';
@@ -0,0 +1,14 @@
1
+ export {
2
+ createMarqueeStrip,
3
+ type MarqueeStripProps,
4
+ type MarqueeStripAPI,
5
+ } from './marquee-strip.js'
6
+
7
+ export {
8
+ marqueeStripVariants,
9
+ marqueeStripInnerClass,
10
+ marqueeStripScrollOuterClass,
11
+ marqueeStripScrollTrackClass,
12
+ marqueeStripLabelClass,
13
+ marqueeStripItemClass,
14
+ } from './marquee-strip.styles.js'
@@ -0,0 +1,61 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Container — a full-width band with a subtle muted background and border.
5
+ *
6
+ * The `scroll` variant switches the inner track from wrapping flex to an
7
+ * overflow-hidden marquee track. See {@link marqueeStripInnerClass} and
8
+ * {@link marqueeStripScrollTrackClass} for the corresponding track classes.
9
+ */
10
+ export const marqueeStripVariants = cva({
11
+ base: 'w-full bg-muted/30 border-y border-border py-3',
12
+ variants: {
13
+ scroll: {
14
+ true: '',
15
+ false: '',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ scroll: 'false',
20
+ },
21
+ })
22
+
23
+ /**
24
+ * Inner wrapper for static (non-scroll) mode.
25
+ *
26
+ * Items wrap naturally so they never overflow on narrow viewports.
27
+ */
28
+ export const marqueeStripInnerClass =
29
+ 'flex flex-wrap items-center gap-3 px-4'
30
+
31
+ /**
32
+ * Outer clip track for scroll mode — hides overflow so the looping animation
33
+ * is invisible at the edges.
34
+ */
35
+ export const marqueeStripScrollOuterClass = 'overflow-hidden px-4'
36
+
37
+ /**
38
+ * Moving track for scroll mode.
39
+ *
40
+ * The `marquee` keyframe must be defined in the consuming app's global CSS
41
+ * (or via a Tailwind plugin). The canonical definition is:
42
+ *
43
+ * ```css
44
+ * @keyframes marquee {
45
+ * from { transform: translateX(0); }
46
+ * to { transform: translateX(-50%); }
47
+ * }
48
+ * ```
49
+ *
50
+ * Items are duplicated once by the adapter so the strip appears seamless
51
+ * (the track is effectively 200 % wide and translates to –50 %).
52
+ */
53
+ export const marqueeStripScrollTrackClass =
54
+ 'flex items-center gap-3 w-max animate-[marquee_30s_linear_infinite]'
55
+
56
+ /** Strip label — shown before the items (uppercase eyebrow style). */
57
+ export const marqueeStripLabelClass =
58
+ 'text-xs font-semibold uppercase tracking-widest text-muted-foreground shrink-0'
59
+
60
+ /** Individual item text. */
61
+ export const marqueeStripItemClass = 'text-sm text-foreground'
@@ -0,0 +1,35 @@
1
+ import type { AccessibilityProps } from '../shared/index.ts'
2
+
3
+ export interface MarqueeStripProps {
4
+ /** Whether to continuously scroll the items. */
5
+ scroll?: boolean
6
+ }
7
+
8
+ export interface MarqueeStripAPI {
9
+ /** ARIA attributes to spread on the container element (`role="group"`). */
10
+ ariaProps: Partial<AccessibilityProps>
11
+ /** Data attributes for styling hooks. */
12
+ dataAttributes: Record<string, string>
13
+ }
14
+
15
+ /**
16
+ * Build the framework-agnostic container props for a marquee strip.
17
+ *
18
+ * Returns `role="group"` plus a `data-scroll` attribute. Adapters spread these
19
+ * onto their container and render the label and items themselves. In scroll mode
20
+ * the adapter is responsible for duplicating items once to achieve a seamless
21
+ * loop (the CSS `marquee` keyframe translates from 0 to –50%).
22
+ */
23
+ export function createMarqueeStrip(props: MarqueeStripProps = {}): MarqueeStripAPI {
24
+ const { scroll = false } = props
25
+
26
+ const ariaProps: Partial<AccessibilityProps> = {
27
+ role: 'group',
28
+ }
29
+
30
+ const dataAttributes: Record<string, string> = {
31
+ 'data-scroll': String(scroll),
32
+ }
33
+
34
+ return { ariaProps, dataAttributes }
35
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ clampPercent,
3
+ createMasteryBar,
4
+ type MasteryBarSize,
5
+ type MasteryBarProps,
6
+ type MasteryBarAPI,
7
+ } from './mastery-bar.js'
8
+
9
+ export {
10
+ masteryBarVariants,
11
+ masteryBarFillVariants,
12
+ masteryBarLabelClass,
13
+ masteryBarLeadingLabelClass,
14
+ masteryBarHeaderClass,
15
+ } from './mastery-bar.styles.js'
@@ -0,0 +1,45 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Track (outer container): rounded, muted background.
5
+ * `size` variant controls the track height.
6
+ */
7
+ export const masteryBarVariants = cva({
8
+ base: 'w-full overflow-hidden rounded-full bg-muted',
9
+ variants: {
10
+ size: {
11
+ sm: 'h-1.5',
12
+ md: 'h-2',
13
+ lg: 'h-3',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ size: 'md',
18
+ },
19
+ })
20
+
21
+ /**
22
+ * Fill (inner bar): rounded, primary background.
23
+ * `muted` variant renders at reduced opacity for secondary/inactive states.
24
+ */
25
+ export const masteryBarFillVariants = cva({
26
+ base: 'h-full rounded-full bg-primary',
27
+ variants: {
28
+ muted: {
29
+ true: 'bg-primary/70',
30
+ false: 'bg-primary',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ muted: 'false',
35
+ },
36
+ })
37
+
38
+ /** Class applied to each text label (leading or trailing). */
39
+ export const masteryBarLabelClass = 'text-xs text-muted-foreground'
40
+
41
+ /** Class for the leading (left-side) label. */
42
+ export const masteryBarLeadingLabelClass = 'text-xs text-muted-foreground'
43
+
44
+ /** Header row above the track: leading label on the left, label on the right. */
45
+ export const masteryBarHeaderClass = 'flex justify-between items-baseline mb-1'
@@ -0,0 +1,55 @@
1
+ /** Visual size of a mastery bar. */
2
+ export type MasteryBarSize = 'sm' | 'md' | 'lg'
3
+
4
+ export interface MasteryBarProps {
5
+ /** Progress value (0–100). Values outside this range are clamped. */
6
+ value: number
7
+ /** Label shown on the right side of the header row. */
8
+ label?: string
9
+ /** Label shown on the left side of the header row. */
10
+ leadingLabel?: string
11
+ /** Visual size of the track. */
12
+ size?: MasteryBarSize
13
+ /** Renders the fill at reduced opacity to indicate a muted/secondary state. */
14
+ muted?: boolean
15
+ }
16
+
17
+ export interface MasteryBarAPI {
18
+ /** ARIA attributes to spread on the progressbar element. */
19
+ ariaProps: Record<string, string | number | boolean>
20
+ /** Data attributes for styling hooks. */
21
+ dataAttributes: Record<string, string>
22
+ }
23
+
24
+ /**
25
+ * Clamp a raw numeric value to the [0, 100] range.
26
+ *
27
+ * Values below 0 become 0; values above 100 become 100; NaN is treated as 0.
28
+ */
29
+ export function clampPercent(value: number): number {
30
+ if (Number.isNaN(value)) return 0
31
+ return Math.min(100, Math.max(0, value))
32
+ }
33
+
34
+ /**
35
+ * Build the framework-agnostic progressbar props for a mastery bar.
36
+ *
37
+ * Returns `role="progressbar"` ARIA attributes plus data attributes; adapters
38
+ * spread these onto their track element.
39
+ */
40
+ export function createMasteryBar({ value }: Pick<MasteryBarProps, 'value'>): MasteryBarAPI {
41
+ const clamped = clampPercent(value)
42
+
43
+ const ariaProps: Record<string, string | number | boolean> = {
44
+ role: 'progressbar',
45
+ 'aria-valuenow': clamped,
46
+ 'aria-valuemin': 0,
47
+ 'aria-valuemax': 100,
48
+ }
49
+
50
+ const dataAttributes: Record<string, string> = {
51
+ 'data-value': String(clamped),
52
+ }
53
+
54
+ return { ariaProps, dataAttributes }
55
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ createNumberedSteps,
3
+ padOrdinal,
4
+ stepColumns,
5
+ type NumberedStepItem,
6
+ type NumberedStepsProps,
7
+ type NumberedStepsAPI,
8
+ } from './numbered-steps.js'
9
+
10
+ export {
11
+ numberedStepsVariants,
12
+ numberedStepsItemClass,
13
+ numberedStepsOrdinalClass,
14
+ numberedStepsTitleClass,
15
+ numberedStepsBodyClass,
16
+ } from './numbered-steps.styles.js'
@@ -0,0 +1,36 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Container grid for the numbered steps. The `columns` variant maps to a
5
+ * fixed Tailwind grid-cols class; adapters that need a dynamic column count
6
+ * use `gridTemplateColumns` inline style via {@link stepColumns} instead and
7
+ * omit this variant.
8
+ */
9
+ export const numberedStepsVariants = cva({
10
+ base: 'grid gap-6',
11
+ variants: {
12
+ columns: {
13
+ '2': 'grid-cols-2',
14
+ '3': 'grid-cols-3',
15
+ '4': 'grid-cols-4',
16
+ '5': 'grid-cols-5',
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ columns: '3',
21
+ },
22
+ })
23
+
24
+ /** Individual step card. */
25
+ export const numberedStepsItemClass =
26
+ 'rounded-xl border border-border bg-card p-5'
27
+
28
+ /** Zero-padded ordinal badge above each step title. */
29
+ export const numberedStepsOrdinalClass = 'font-mono text-sm text-primary'
30
+
31
+ /** Step title. */
32
+ export const numberedStepsTitleClass = 'mt-2 text-sm font-bold text-foreground'
33
+
34
+ /** Step body copy. */
35
+ export const numberedStepsBodyClass =
36
+ 'mt-1 text-xs text-muted-foreground leading-relaxed'
@@ -0,0 +1,64 @@
1
+ /** A single step item in the numbered steps grid. */
2
+ export type NumberedStepItem = {
3
+ /** Step title displayed prominently below the ordinal badge. */
4
+ title: string
5
+ /** Step body copy displayed below the title. */
6
+ body: string
7
+ }
8
+
9
+ export interface NumberedStepsProps {
10
+ /** The step items to display. */
11
+ items: NumberedStepItem[]
12
+ /**
13
+ * Number of grid columns. When omitted, defaults to clamping the item count
14
+ * to the 2–5 range via {@link stepColumns}.
15
+ */
16
+ columns?: number
17
+ }
18
+
19
+ export interface NumberedStepsAPI {
20
+ /** ARIA attributes to spread on the container element (`role="list"`). */
21
+ ariaProps: { role: 'list' }
22
+ /** Data attributes for styling hooks. */
23
+ dataAttributes: Record<string, string>
24
+ }
25
+
26
+ /**
27
+ * Zero-pad a 1-indexed ordinal to at least two digits.
28
+ *
29
+ * @example
30
+ * padOrdinal(1) // '01'
31
+ * padOrdinal(10) // '10'
32
+ */
33
+ export function padOrdinal(n: number): string {
34
+ return String(n).padStart(2, '0')
35
+ }
36
+
37
+ /**
38
+ * Derive the number of grid columns from the item count.
39
+ * Clamps the count to the 2–5 range.
40
+ *
41
+ * @example
42
+ * stepColumns(3) // 3
43
+ * stepColumns(1) // 2
44
+ * stepColumns(6) // 5
45
+ */
46
+ export function stepColumns(count: number): number {
47
+ return Math.min(5, Math.max(2, count))
48
+ }
49
+
50
+ /**
51
+ * Build the framework-agnostic container props for a numbered-steps grid.
52
+ *
53
+ * Returns `role="list"` plus data attributes; adapters spread these onto their
54
+ * container and render each step as a `role="listitem"`.
55
+ */
56
+ export function createNumberedSteps(): NumberedStepsAPI {
57
+ const ariaProps = { role: 'list' } as const
58
+
59
+ const dataAttributes: Record<string, string> = {
60
+ 'data-component': 'numbered-steps',
61
+ }
62
+
63
+ return { ariaProps, dataAttributes }
64
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ createPricingCard,
3
+ type PricingCtaVariant,
4
+ type PricingCardOptions,
5
+ type PricingCardAPI,
6
+ } from './pricing-card.js'
7
+
8
+ export {
9
+ pricingCardVariants,
10
+ pricingCardBadgeClass,
11
+ pricingCardNameClass,
12
+ pricingCardPriceClass,
13
+ pricingCardPeriodClass,
14
+ pricingCardDescriptionClass,
15
+ pricingCardFeatureListClass,
16
+ pricingCardFeatureRowClass,
17
+ pricingCardFeatureTickClass,
18
+ pricingCtaVariants,
19
+ } from './pricing-card.styles.js'