@refraction-ui/astro 0.13.0 → 0.15.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 (62) 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-kanban-board/KanbanBoard.astro +92 -0
  8. package/dist/astro-kanban-board/index.ts +1 -0
  9. package/dist/astro-marquee-strip/MarqueeStrip.astro +63 -0
  10. package/dist/astro-marquee-strip/index.ts +1 -0
  11. package/dist/astro-mastery-bar/MasteryBar.astro +56 -0
  12. package/dist/astro-mastery-bar/index.ts +1 -0
  13. package/dist/astro-numbered-steps/NumberedSteps.astro +47 -0
  14. package/dist/astro-numbered-steps/index.ts +1 -0
  15. package/dist/astro-pricing-card/PricingCard.astro +114 -0
  16. package/dist/astro-pricing-card/index.ts +1 -0
  17. package/dist/astro-section-head/SectionHead.astro +44 -0
  18. package/dist/astro-section-head/index.ts +1 -0
  19. package/dist/astro-slot-picker/SlotPicker.astro +105 -0
  20. package/dist/astro-slot-picker/index.ts +1 -0
  21. package/dist/astro-sortable-list/SortableList.astro +68 -0
  22. package/dist/astro-sortable-list/index.ts +1 -0
  23. package/dist/astro-stat-grid/StatGrid.astro +46 -0
  24. package/dist/astro-stat-grid/index.ts +1 -0
  25. package/dist/audience-feature-card/audience-feature-card.styles.ts +23 -0
  26. package/dist/audience-feature-card/audience-feature-card.ts +39 -0
  27. package/dist/audience-feature-card/index.ts +13 -0
  28. package/dist/brand-network-cell/brand-network-cell.styles.ts +46 -0
  29. package/dist/brand-network-cell/brand-network-cell.ts +35 -0
  30. package/dist/brand-network-cell/index.ts +14 -0
  31. package/dist/browser-chrome-mock/browser-chrome-mock.styles.ts +43 -0
  32. package/dist/browser-chrome-mock/browser-chrome-mock.ts +52 -0
  33. package/dist/browser-chrome-mock/index.ts +17 -0
  34. package/dist/index.ts +12 -0
  35. package/dist/kanban-board/index.ts +20 -0
  36. package/dist/kanban-board/kanban-board.styles.ts +78 -0
  37. package/dist/kanban-board/kanban-board.ts +82 -0
  38. package/dist/marquee-strip/index.ts +14 -0
  39. package/dist/marquee-strip/marquee-strip.styles.ts +61 -0
  40. package/dist/marquee-strip/marquee-strip.ts +35 -0
  41. package/dist/mastery-bar/index.ts +15 -0
  42. package/dist/mastery-bar/mastery-bar.styles.ts +45 -0
  43. package/dist/mastery-bar/mastery-bar.ts +55 -0
  44. package/dist/numbered-steps/index.ts +16 -0
  45. package/dist/numbered-steps/numbered-steps.styles.ts +36 -0
  46. package/dist/numbered-steps/numbered-steps.ts +64 -0
  47. package/dist/pricing-card/index.ts +19 -0
  48. package/dist/pricing-card/pricing-card.styles.ts +71 -0
  49. package/dist/pricing-card/pricing-card.ts +37 -0
  50. package/dist/section-head/index.ts +13 -0
  51. package/dist/section-head/section-head.styles.ts +30 -0
  52. package/dist/section-head/section-head.ts +28 -0
  53. package/dist/slot-picker/index.ts +18 -0
  54. package/dist/slot-picker/slot-picker.styles.ts +70 -0
  55. package/dist/slot-picker/slot-picker.ts +64 -0
  56. package/dist/sortable-list/index.ts +13 -0
  57. package/dist/sortable-list/sortable-list.styles.ts +42 -0
  58. package/dist/sortable-list/sortable-list.ts +90 -0
  59. package/dist/stat-grid/index.ts +13 -0
  60. package/dist/stat-grid/stat-grid.styles.ts +25 -0
  61. package/dist/stat-grid/stat-grid.ts +41 -0
  62. package/package.json +13 -1
@@ -0,0 +1,78 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** The outer board: horizontal scroll, flex row, gap between columns. */
4
+ export const kanbanBoardVariants = cva({
5
+ base: 'flex flex-row gap-4 overflow-x-auto pb-4',
6
+ variants: {},
7
+ defaultVariants: {},
8
+ })
9
+
10
+ /** A single column: muted background, rounded, fixed minimum width. */
11
+ export const kanbanColumnVariants = cva({
12
+ base: 'flex flex-col min-w-[260px] max-w-[300px] flex-shrink-0 rounded-xl bg-muted/40',
13
+ variants: {},
14
+ defaultVariants: {},
15
+ })
16
+
17
+ /** Column header row: icon slot + title + count badge. */
18
+ export const kanbanColumnHeaderClass =
19
+ 'flex items-center gap-2 px-4 pt-4 pb-2'
20
+
21
+ /** Column title text. */
22
+ export const kanbanColumnTitleClass = 'flex-1 text-sm font-semibold text-foreground truncate'
23
+
24
+ /**
25
+ * Column count badge: a compact pill to the right of the title.
26
+ * Uses muted tokens so it sits quietly against any column accent.
27
+ */
28
+ export const kanbanColumnCountClass =
29
+ 'ml-auto inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground'
30
+
31
+ /**
32
+ * Thin accent bar directly below the header.
33
+ * Callers apply `style={{ '--kanban-accent': def.accent }}` on the column
34
+ * element; this bar reads `var(--kanban-accent)` via an inline style.
35
+ * When no accent is supplied the bar is invisible (transparent).
36
+ */
37
+ export const kanbanAccentBarClass = 'h-0.5 w-full'
38
+
39
+ /**
40
+ * Note/gate bar shown beneath the accent bar.
41
+ * Displays brief stage notes (e.g. "Technical screen required").
42
+ */
43
+ export const kanbanNoteBarClass =
44
+ 'mx-4 mb-2 rounded-md bg-muted px-3 py-1.5 text-xs text-muted-foreground'
45
+
46
+ /** Column body: card stack with gap, padded sides and bottom. */
47
+ export const kanbanColumnBodyClass = 'flex flex-col gap-2 px-3 pb-3'
48
+
49
+ /**
50
+ * "+N more" overflow button.
51
+ * Styled as a subtle full-width ghost button inside the column.
52
+ */
53
+ export const kanbanOverflowButtonClass = [
54
+ 'mt-1 w-full rounded-lg border border-dashed border-border',
55
+ 'py-1.5 text-center text-xs text-muted-foreground',
56
+ 'transition-colors hover:bg-muted hover:text-foreground',
57
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
58
+ ].join(' ')
59
+
60
+ /**
61
+ * Default card surface.
62
+ *
63
+ * Variants:
64
+ * - `clickable: 'true'` — adds hover treatment for interactive cards.
65
+ * - `clickable: 'false'` — static (default).
66
+ */
67
+ export const kanbanCardVariants = cva({
68
+ base: 'rounded-lg border border-border bg-card p-3 text-sm text-foreground',
69
+ variants: {
70
+ clickable: {
71
+ true: 'cursor-pointer transition-colors hover:bg-muted/60',
72
+ false: '',
73
+ },
74
+ },
75
+ defaultVariants: {
76
+ clickable: 'false',
77
+ },
78
+ })
@@ -0,0 +1,82 @@
1
+ /** A single column definition for the Kanban board. */
2
+ export type KanbanColumnDef = {
3
+ /** Unique identifier for this column (matches cards' column id). */
4
+ id: string
5
+ /** Display title of the column (e.g. stage name). */
6
+ title: string
7
+ /** Optional accent color applied via CSS var `--kanban-accent`. */
8
+ accent?: string
9
+ /** Optional note or gate description shown below the column header. */
10
+ note?: string
11
+ }
12
+
13
+ export interface KanbanBoardAPI {
14
+ /** ARIA attributes to spread on the board container. */
15
+ ariaProps: { role: 'group'; 'aria-label': string }
16
+ /** Data attributes for styling hooks. */
17
+ dataAttributes: Record<string, string>
18
+ }
19
+
20
+ /**
21
+ * Build the framework-agnostic board props.
22
+ *
23
+ * Returns `role="group"` with a fixed accessible label and data attributes
24
+ * so adapters can style based on state without inlining logic.
25
+ */
26
+ export function createKanbanBoard(): KanbanBoardAPI {
27
+ return {
28
+ ariaProps: {
29
+ role: 'group',
30
+ 'aria-label': 'Board',
31
+ },
32
+ dataAttributes: {
33
+ 'data-component': 'kanban-board',
34
+ },
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Bucket cards into a Map keyed by column id, preserving column order.
40
+ * Columns that have no cards get an empty array entry so callers can always
41
+ * iterate `columns` and look up their bucket safely.
42
+ *
43
+ * @param cards - All cards to distribute.
44
+ * @param getColumnId - Selector that reads the column id from a card.
45
+ * @param columns - Column definitions (determines output order).
46
+ */
47
+ export function cardsByColumn<T>(
48
+ cards: T[],
49
+ getColumnId: (c: T) => string,
50
+ columns: KanbanColumnDef[],
51
+ ): Map<string, T[]> {
52
+ const map = new Map<string, T[]>()
53
+ // Pre-populate every column so empty columns are present.
54
+ for (const col of columns) {
55
+ map.set(col.id, [])
56
+ }
57
+ for (const card of cards) {
58
+ const colId = getColumnId(card)
59
+ const bucket = map.get(colId)
60
+ if (bucket !== undefined) {
61
+ bucket.push(card)
62
+ }
63
+ // Cards referencing unknown columns are silently dropped.
64
+ }
65
+ return map
66
+ }
67
+
68
+ /**
69
+ * Split a card list into a visible slice and an overflow count.
70
+ *
71
+ * @param cards - Full list of cards in the column.
72
+ * @param cap - Maximum visible cards. Defaults to 5.
73
+ */
74
+ export function visibleAndOverflow<T>(
75
+ cards: T[],
76
+ cap = 5,
77
+ ): { visible: T[]; overflow: number } {
78
+ if (cards.length <= cap) {
79
+ return { visible: cards, overflow: 0 }
80
+ }
81
+ return { visible: cards.slice(0, cap), overflow: cards.length - cap }
82
+ }
@@ -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'
@@ -0,0 +1,71 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Root pricing card container. The `featured` variant adds a primary-coloured
5
+ * ring to draw the eye to the recommended plan.
6
+ */
7
+ export const pricingCardVariants = cva({
8
+ base: [
9
+ 'relative flex flex-col rounded-2xl border border-border bg-card p-6',
10
+ 'transition-shadow',
11
+ ].join(' '),
12
+ variants: {
13
+ featured: {
14
+ true: 'ring-1 ring-primary',
15
+ false: '',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ featured: 'false',
20
+ },
21
+ })
22
+
23
+ /** "Best value" / "Most popular" badge above the plan name. */
24
+ export const pricingCardBadgeClass =
25
+ 'mb-3 inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary'
26
+
27
+ /** Plan name (e.g. "Pro", "Starter"). */
28
+ export const pricingCardNameClass = 'text-lg font-bold text-foreground'
29
+
30
+ /** Main price figure (e.g. "$29", "Free"). */
31
+ export const pricingCardPriceClass = 'text-3xl font-bold text-foreground'
32
+
33
+ /** Billing period or qualifier (e.g. "/ month", "forever"). */
34
+ export const pricingCardPeriodClass = 'text-sm text-muted-foreground'
35
+
36
+ /** Short marketing description beneath the price. */
37
+ export const pricingCardDescriptionClass = 'text-sm text-muted-foreground'
38
+
39
+ /** Unordered list of included features. */
40
+ export const pricingCardFeatureListClass = 'flex flex-col gap-2'
41
+
42
+ /** A single feature row — tick icon + text. */
43
+ export const pricingCardFeatureRowClass = 'flex items-start gap-2 text-sm text-foreground'
44
+
45
+ /** Inline check SVG rendered before each feature string. */
46
+ export const pricingCardFeatureTickClass = 'mt-0.5 size-4 shrink-0 text-primary'
47
+
48
+ /**
49
+ * CTA button/link that appears at the bottom of the card.
50
+ *
51
+ * `default` — filled primary style (call to action on featured plans).
52
+ * `outline` — bordered secondary style (for non-featured plans).
53
+ */
54
+ export const pricingCtaVariants = cva({
55
+ base: [
56
+ 'mt-auto inline-flex w-full items-center justify-center rounded-lg px-4 py-2',
57
+ 'text-sm font-medium transition-colors',
58
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
59
+ 'disabled:pointer-events-none disabled:opacity-50',
60
+ ].join(' '),
61
+ variants: {
62
+ variant: {
63
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
64
+ outline:
65
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
66
+ },
67
+ },
68
+ defaultVariants: {
69
+ variant: 'default',
70
+ },
71
+ })
@@ -0,0 +1,37 @@
1
+ import type { AccessibilityProps } from '../shared/index.ts'
2
+
3
+ /** CTA button/link visual variant. */
4
+ export type PricingCtaVariant = 'default' | 'outline'
5
+
6
+ export interface PricingCardOptions {
7
+ /** Whether this card is highlighted as the featured / recommended plan. */
8
+ featured?: boolean
9
+ }
10
+
11
+ export interface PricingCardAPI {
12
+ /** ARIA props to spread on the card container. */
13
+ ariaProps: Partial<AccessibilityProps>
14
+ /** Data attributes for styling hooks and CSS selectors. */
15
+ dataAttributes: Record<string, string>
16
+ }
17
+
18
+ /**
19
+ * Build the framework-agnostic props for a pricing plan card.
20
+ *
21
+ * Returns `role="group"` (semantically groups the plan's content without
22
+ * imposing listitem semantics on a standalone card) and a `data-featured`
23
+ * attribute that adapters can use for conditional ring/highlight styling.
24
+ */
25
+ export function createPricingCard(options: PricingCardOptions = {}): PricingCardAPI {
26
+ const { featured = false } = options
27
+
28
+ const ariaProps: Partial<AccessibilityProps> = {
29
+ role: 'group',
30
+ }
31
+
32
+ const dataAttributes: Record<string, string> = {
33
+ 'data-featured': String(featured),
34
+ }
35
+
36
+ return { ariaProps, dataAttributes }
37
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ createSectionHead,
3
+ type SectionHeadAlign,
4
+ type SectionHeadProps,
5
+ type SectionHeadAPI,
6
+ } from './section-head.js'
7
+
8
+ export {
9
+ sectionHeadVariants,
10
+ sectionHeadKickerClass,
11
+ sectionHeadTitleClass,
12
+ sectionHeadLedeClass,
13
+ } from './section-head.styles.js'
@@ -0,0 +1,30 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Container variant that controls the alignment of the whole cluster.
5
+ * Center: auto-margins + text-center + constrained max-width for comfortable
6
+ * line length. Left: no margin constraint, flush left.
7
+ */
8
+ export const sectionHeadVariants = cva({
9
+ base: 'flex flex-col',
10
+ variants: {
11
+ align: {
12
+ center: 'mx-auto text-center max-w-2xl',
13
+ left: 'text-left',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ align: 'center',
18
+ },
19
+ })
20
+
21
+ /** Small-caps eyebrow label above the title. */
22
+ export const sectionHeadKickerClass =
23
+ 'text-[11px] uppercase tracking-widest text-primary font-semibold'
24
+
25
+ /** Primary h2 heading. */
26
+ export const sectionHeadTitleClass =
27
+ 'text-3xl sm:text-4xl font-bold tracking-tight text-foreground'
28
+
29
+ /** Optional lede paragraph below the title. */
30
+ export const sectionHeadLedeClass = 'mt-3 text-muted-foreground leading-relaxed'