@refraction-ui/astro 0.18.1 → 0.20.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 (78) hide show
  1. package/dist/action-list/action-list.styles.ts +33 -0
  2. package/dist/action-list/action-list.ts +31 -0
  3. package/dist/action-list/index.ts +16 -0
  4. package/dist/analytics-sink-app-insights/app-insights-sink.ts +1 -1
  5. package/dist/analytics-sink-posthog/client-sdk-sink.ts +1 -1
  6. package/dist/analytics-sink-posthog/replay.ts +1 -1
  7. package/dist/astro-action-list/ActionList.astro +12 -0
  8. package/dist/astro-action-list/ActionListItem.astro +62 -0
  9. package/dist/astro-action-list/index.ts +1 -0
  10. package/dist/astro-avatar/Avatar.astro +5 -4
  11. package/dist/astro-callout/Callout.astro +4 -1
  12. package/dist/astro-card/Card.astro +3 -2
  13. package/dist/astro-card/CardTitle.astro +6 -4
  14. package/dist/astro-data-table/DataTable.astro +7 -3
  15. package/dist/astro-funnel-chart/FunnelChart.astro +64 -0
  16. package/dist/astro-funnel-chart/index.ts +1 -0
  17. package/dist/astro-line-chart/LineChart.astro +82 -0
  18. package/dist/astro-line-chart/index.ts +1 -0
  19. package/dist/astro-native-select/NativeSelect.astro +26 -0
  20. package/dist/astro-native-select/index.ts +1 -0
  21. package/dist/astro-page-header/PageHeader.astro +46 -0
  22. package/dist/astro-page-header/index.ts +1 -0
  23. package/dist/astro-stat-grid/StatGrid.astro +23 -19
  24. package/dist/astro-table/Table.astro +21 -0
  25. package/dist/astro-table/TableCaption.astro +9 -0
  26. package/dist/astro-table/TableCell.astro +13 -0
  27. package/dist/astro-table/TableHead.astro +13 -0
  28. package/dist/astro-table/TableRow.astro +9 -0
  29. package/dist/astro-table/index.ts +1 -0
  30. package/dist/avatar/avatar.styles.ts +7 -2
  31. package/dist/avatar/avatar.ts +8 -3
  32. package/dist/avatar/index.ts +1 -0
  33. package/dist/badge/badge.styles.ts +6 -1
  34. package/dist/badge/badge.ts +24 -2
  35. package/dist/button/button.styles.ts +10 -4
  36. package/dist/button/button.ts +4 -1
  37. package/dist/callout/callout.ts +12 -3
  38. package/dist/card/card.styles.ts +10 -1
  39. package/dist/card/card.ts +14 -0
  40. package/dist/card/index.ts +3 -0
  41. package/dist/data-table/data-table.styles.ts +18 -0
  42. package/dist/data-table/data-table.ts +49 -7
  43. package/dist/data-table/index.ts +8 -0
  44. package/dist/diff-viewer/diff-viewer.styles.ts +3 -0
  45. package/dist/funnel-chart/funnel-chart.styles.ts +16 -0
  46. package/dist/funnel-chart/funnel-chart.ts +68 -0
  47. package/dist/funnel-chart/index.ts +10 -0
  48. package/dist/index.ts +6 -0
  49. package/dist/input/input.styles.ts +2 -2
  50. package/dist/line-chart/index.ts +20 -0
  51. package/dist/line-chart/line-chart.styles.ts +16 -0
  52. package/dist/line-chart/line-chart.ts +215 -0
  53. package/dist/logger/faro-engine.ts +2 -2
  54. package/dist/native-select/index.ts +6 -0
  55. package/dist/native-select/native-select.styles.ts +30 -0
  56. package/dist/native-select/native-select.ts +5 -0
  57. package/dist/page-header/index.ts +15 -0
  58. package/dist/page-header/page-header.styles.ts +21 -0
  59. package/dist/page-header/page-header.ts +27 -0
  60. package/dist/progress-display/progress-display.styles.ts +1 -1
  61. package/dist/select/index.ts +2 -0
  62. package/dist/select/select.styles.ts +9 -3
  63. package/dist/select/select.ts +11 -3
  64. package/dist/separator/index.ts +2 -0
  65. package/dist/separator/separator.styles.ts +17 -2
  66. package/dist/shared/cn.ts +6 -3
  67. package/dist/slide-viewer/slide-viewer.styles.ts +2 -0
  68. package/dist/stat-grid/index.ts +9 -0
  69. package/dist/stat-grid/stat-grid.styles.ts +73 -6
  70. package/dist/stat-grid/stat-grid.ts +25 -5
  71. package/dist/sticky-note/sticky-note.styles.ts +2 -0
  72. package/dist/table/index.ts +16 -0
  73. package/dist/table/table.styles.ts +72 -0
  74. package/dist/table/table.ts +24 -0
  75. package/dist/tabs/index.ts +1 -0
  76. package/dist/tabs/tabs.ts +29 -0
  77. package/dist/textarea/textarea.styles.ts +2 -2
  78. package/package.json +7 -1
@@ -0,0 +1,13 @@
1
+ ---
2
+ import { tableHeadVariants, type TableAlign, type TableDensity, type TableHeadTone } from '../table/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends Omit<astroHTML.JSX.ThHTMLAttributes, 'align'> {
6
+ align?: TableAlign
7
+ density?: TableDensity
8
+ tone?: TableHeadTone
9
+ }
10
+ const { align, density, tone, scope = 'col', class: className, ...rest } = Astro.props
11
+ ---
12
+
13
+ <th scope={scope} class={cn(tableHeadVariants({ align, density, tone }), className)} data-slot="table-head" {...rest}><slot /></th>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { tableRowClass } from '../table/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {}
6
+ const { class: className, ...rest } = Astro.props
7
+ ---
8
+
9
+ <tr class={cn(tableRowClass, className)} data-slot="table-row" {...rest}><slot /></tr>
@@ -0,0 +1 @@
1
+ export {}
@@ -11,8 +11,12 @@ export const avatarTokens: TokenContract = {
11
11
  }
12
12
 
13
13
  export const avatarVariants = cva({
14
- base: 'relative flex shrink-0 overflow-hidden rounded-full',
14
+ base: 'relative flex shrink-0 overflow-hidden',
15
15
  variants: {
16
+ shape: {
17
+ circle: 'rounded-full',
18
+ square: 'rounded-md',
19
+ },
16
20
  size: {
17
21
  xs: 'h-6 w-6',
18
22
  sm: 'h-8 w-8',
@@ -22,6 +26,7 @@ export const avatarVariants = cva({
22
26
  },
23
27
  },
24
28
  defaultVariants: {
29
+ shape: 'circle',
25
30
  size: 'md',
26
31
  },
27
32
  })
@@ -31,7 +36,7 @@ export const avatarImageVariants = cva({
31
36
  })
32
37
 
33
38
  export const avatarFallbackVariants = cva({
34
- base: 'flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground',
39
+ base: 'flex h-full w-full items-center justify-center rounded-[inherit] bg-muted text-muted-foreground',
35
40
  variants: {
36
41
  size: {
37
42
  xs: 'text-xs',
@@ -2,6 +2,9 @@ import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
3
  export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
4
4
 
5
+ /** Outline of the avatar. */
6
+ export type AvatarShape = 'circle' | 'square'
7
+
5
8
  export type AvatarLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error'
6
9
 
7
10
  export interface AvatarProps {
@@ -44,10 +47,12 @@ export function getInitials(name: string): string {
44
47
  export function createAvatar(props: AvatarProps = {}): AvatarAPI {
45
48
  const { src, alt = '', fallback = '', size = 'md' } = props
46
49
 
47
- const ariaProps: Partial<AccessibilityProps> = {
48
- role: 'img',
49
- }
50
+ // role="img" makes its children presentational, so it is only applied when
51
+ // the avatar itself has a name; otherwise the inner <img alt> / fallback
52
+ // text stays exposed.
53
+ const ariaProps: Partial<AccessibilityProps> = {}
50
54
  if (alt) {
55
+ ariaProps.role = 'img'
51
56
  ariaProps['aria-label'] = alt
52
57
  }
53
58
 
@@ -4,6 +4,7 @@ export {
4
4
  type AvatarProps,
5
5
  type AvatarAPI,
6
6
  type AvatarSize,
7
+ type AvatarShape,
7
8
  type AvatarLoadingStatus,
8
9
  } from './avatar.js'
9
10
 
@@ -1,7 +1,7 @@
1
1
  import { cva } from '../shared/index.ts'
2
2
 
3
3
  export const badgeVariants = cva({
4
- base: 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
4
+ base: 'inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
5
5
  variants: {
6
6
  variant: {
7
7
  default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
@@ -11,6 +11,11 @@ export const badgeVariants = cva({
11
11
  outline: 'text-foreground',
12
12
  success: 'border-transparent bg-success text-success-foreground shadow hover:bg-success/80 font-semibold',
13
13
  warning: 'border-transparent bg-warning text-warning-foreground shadow hover:bg-warning/80 font-semibold',
14
+ // Second brand accent + extended status roles (issue #485).
15
+ tertiary: 'border-transparent bg-tertiary text-tertiary-foreground shadow hover:bg-tertiary/80 font-semibold',
16
+ positive: 'border-transparent bg-positive text-positive-foreground shadow hover:bg-positive/80 font-semibold',
17
+ caution: 'border-transparent bg-caution text-caution-foreground shadow hover:bg-caution/80 font-semibold',
18
+ done: 'border-transparent bg-done text-info-foreground shadow hover:bg-done/80 font-semibold',
14
19
  },
15
20
  size: {
16
21
  sm: 'px-2 py-0 text-[10px]',
@@ -1,6 +1,18 @@
1
1
  import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
- export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning'
3
+ export type BadgeVariant =
4
+ | 'default'
5
+ | 'primary'
6
+ | 'secondary'
7
+ | 'destructive'
8
+ | 'outline'
9
+ | 'success'
10
+ | 'warning'
11
+ // Issue #485 — second brand accent + extended status roles.
12
+ | 'tertiary'
13
+ | 'positive'
14
+ | 'caution'
15
+ | 'done'
4
16
  export type BadgeSize = 'sm' | 'md'
5
17
 
6
18
  export interface BadgeProps {
@@ -15,13 +27,23 @@ export interface BadgeAPI {
15
27
  dataAttributes: Record<string, string>
16
28
  }
17
29
 
18
- const statusVariants: BadgeVariant[] = ['success', 'warning', 'destructive']
30
+ const statusVariants: BadgeVariant[] = [
31
+ 'success',
32
+ 'warning',
33
+ 'destructive',
34
+ 'positive',
35
+ 'caution',
36
+ 'done',
37
+ ]
19
38
 
20
39
  /** Icon name for status badge variants — non-color indicator for colorblind safety */
21
40
  const BADGE_ICONS: Partial<Record<BadgeVariant, string>> = {
22
41
  success: 'check',
23
42
  warning: 'alert',
24
43
  destructive: 'x',
44
+ positive: 'check',
45
+ caution: 'alert',
46
+ done: 'check',
25
47
  }
26
48
 
27
49
  /** Get the icon name for a badge variant (undefined for non-status variants) */
@@ -13,7 +13,7 @@ export const buttonTokens: TokenContract = {
13
13
  }
14
14
 
15
15
  export const buttonVariants = cva({
16
- base: 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
16
+ base: 'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
17
17
  variants: {
18
18
  variant: {
19
19
  default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
@@ -24,15 +24,21 @@ export const buttonVariants = cva({
24
24
  destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
25
25
  outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
26
26
  secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
27
+ // Soft tinted brand fill — lower-emphasis primary action (issue #485).
28
+ soft: 'bg-primary-soft text-primary-soft-foreground shadow-sm hover:bg-primary-soft/80',
29
+ // Second brand accent (issue #485).
30
+ tertiary: 'bg-tertiary text-tertiary-foreground shadow-sm hover:bg-tertiary/90',
27
31
  ghost: 'hover:bg-accent hover:text-accent-foreground',
28
32
  link: 'text-primary underline-offset-4 hover:underline',
29
33
  },
30
34
  size: {
31
35
  xs: 'h-7 rounded px-2 text-xs',
32
36
  sm: 'h-8 rounded-md px-3 text-xs',
33
- default: 'h-9 px-4 py-2',
34
- lg: 'h-10 rounded-md px-8',
35
- icon: 'h-9 w-9',
37
+ default: 'h-9 rounded-md px-4 py-2 text-sm',
38
+ lg: 'h-10 rounded-md px-8 text-sm',
39
+ icon: 'h-9 w-9 rounded-md text-sm',
40
+ 'icon-sm': 'h-8 w-8 rounded-md text-xs',
41
+ 'icon-xs': 'h-7 w-7 rounded text-xs',
36
42
  },
37
43
  },
38
44
  defaultVariants: {
@@ -14,6 +14,9 @@ export type ButtonVariant =
14
14
  | 'secondary'
15
15
  | 'ghost'
16
16
  | 'link'
17
+ // Issue #485 — soft tinted brand fill + a real second brand accent.
18
+ | 'soft'
19
+ | 'tertiary'
17
20
 
18
21
  /** Variants that map to the same visual style as `default`. */
19
22
  export const BUTTON_VARIANT_ALIASES: Partial<Record<ButtonVariant, ButtonVariant>> = {
@@ -25,7 +28,7 @@ export function resolveButtonVariant(variant: ButtonVariant | undefined): Button
25
28
  if (!variant) return variant
26
29
  return BUTTON_VARIANT_ALIASES[variant] ?? variant
27
30
  }
28
- export type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon'
31
+ export type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
29
32
 
30
33
  export interface ButtonProps {
31
34
  variant?: ButtonVariant
@@ -1,8 +1,16 @@
1
1
  import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
3
  export interface CalloutProps {
4
- /** Optional accessible role override */
4
+ /** Explicit role; always wins. */
5
5
  role?: string
6
+ /** Destructive callouts are announced (`role="alert"`). */
7
+ destructive?: boolean
8
+ /**
9
+ * Whether the callout has an accessible name (`aria-label` /
10
+ * `aria-labelledby`). Only a named callout becomes a `region` landmark — an
11
+ * unnamed region is an axe violation and landmark noise.
12
+ */
13
+ labelled?: boolean
6
14
  }
7
15
 
8
16
  export interface CalloutAPI {
@@ -13,10 +21,11 @@ export interface CalloutAPI {
13
21
  export function createCallout(props: CalloutProps = {}): CalloutAPI {
14
22
  const ariaProps: Partial<AccessibilityProps> = {}
15
23
 
16
- // Callouts are usually alert or region depending on severity
17
24
  if (props.role) {
18
25
  ariaProps.role = props.role
19
- } else {
26
+ } else if (props.destructive) {
27
+ ariaProps.role = 'alert'
28
+ } else if (props.labelled) {
20
29
  ariaProps.role = 'region'
21
30
  }
22
31
 
@@ -11,8 +11,16 @@ export const cardTokens: TokenContract = {
11
11
  }
12
12
 
13
13
  export const cardVariants = cva({
14
- base: 'rounded-lg border bg-card text-card-foreground shadow',
14
+ base: 'rounded-lg border shadow',
15
15
  variants: {
16
+ variant: {
17
+ // Default — unchanged surface (bg-card + border via the global token).
18
+ default: 'bg-card text-card-foreground',
19
+ // Lower-emphasis container on the subtle surface token (issue #485).
20
+ subtle: 'bg-surface-subtle text-card-foreground',
21
+ // Tinted second-accent card (issue #485).
22
+ tertiary: 'border-transparent bg-tertiary-soft text-tertiary-soft-foreground',
23
+ },
16
24
  padding: {
17
25
  none: '',
18
26
  default: 'p-6',
@@ -20,6 +28,7 @@ export const cardVariants = cva({
20
28
  },
21
29
  },
22
30
  defaultVariants: {
31
+ variant: 'default',
23
32
  padding: 'none',
24
33
  },
25
34
  })
package/dist/card/card.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
+ /**
4
+ * Visual tone of a card (issue #485).
5
+ * - `default` — the standard surface (activates the `--rfr-card-*` contract).
6
+ * - `subtle` — a lower-emphasis container on the subtle surface token.
7
+ * - `tertiary` — a tinted second-brand-accent card.
8
+ */
9
+ export type CardVariant = 'default' | 'subtle' | 'tertiary'
10
+
11
+ /** Built-in inner padding of the Card container. */
12
+ export type CardPadding = 'none' | 'default' | 'compact'
13
+
14
+ /** Heading element for CardTitle. */
15
+ export type CardTitleLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
16
+
3
17
  export interface CardProps {
4
18
  /** Optional accessible role override */
5
19
  role?: string
@@ -7,6 +7,9 @@ export {
7
7
  createCardFooter,
8
8
  type CardProps,
9
9
  type CardAPI,
10
+ type CardVariant,
11
+ type CardPadding,
12
+ type CardTitleLevel,
10
13
  } from './card.js'
11
14
 
12
15
  export {
@@ -53,3 +53,21 @@ export const rowVariants = cva({
53
53
  selected: 'false',
54
54
  },
55
55
  })
56
+
57
+ /** Logical text alignment per column alignment. */
58
+ export const dataTableAlignClass = {
59
+ start: 'text-start',
60
+ center: 'text-center',
61
+ end: 'text-end',
62
+ } as const
63
+
64
+ /** Numeric cells: tabular figures, no wrapping. */
65
+ export const dataTableNumericClass = 'tabular-nums whitespace-nowrap'
66
+
67
+ /** Button inside a sortable header: the keyboard-reachable sort control. */
68
+ export const dataTableSortButtonClass =
69
+ 'inline-flex items-center gap-1 rounded-sm font-inherit text-inherit focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
70
+
71
+ /** Rows with a row action: pointer + keyboard focus ring. */
72
+ export const dataTableActionRowClass =
73
+ 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring'
@@ -5,8 +5,25 @@ export interface ColumnDef<T = Record<string, unknown>> {
5
5
  id: string
6
6
  /** Column header text */
7
7
  header: string
8
- /** Accessor function to get cell value from row */
9
- accessor: (row: T) => unknown
8
+ /**
9
+ * Value of this column for a row. Drives sorting, filtering and — when no
10
+ * `cell` renderer is given — the rendered text. Optional for display-only
11
+ * columns that set `cell`.
12
+ */
13
+ accessor?: (row: T) => unknown
14
+ /**
15
+ * Custom cell renderer. Framework adapters narrow the return type (React:
16
+ * `ReactNode`). When omitted the cell renders `String(accessor(row))`.
17
+ */
18
+ cell?: (row: T, index: number) => unknown
19
+ /** Horizontal alignment of header and cells. Defaults to `start`. */
20
+ align?: DataTableAlign
21
+ /** Numeric column: tabular figures, no wrapping, aligned to `end` unless `align` says otherwise. */
22
+ numeric?: boolean
23
+ /** Extra class for this column's data cells. */
24
+ className?: string
25
+ /** Extra class for this column's header cell. */
26
+ headerClassName?: string
10
27
  /** Whether this column is sortable */
11
28
  sortable?: boolean
12
29
  /** Whether this column is filterable */
@@ -15,6 +32,31 @@ export interface ColumnDef<T = Record<string, unknown>> {
15
32
 
16
33
  export type SortDirection = 'asc' | 'desc'
17
34
 
35
+ /** Horizontal alignment of a column (logical, RTL-safe). */
36
+ export type DataTableAlign = 'start' | 'center' | 'end'
37
+
38
+ /** A column's effective alignment: explicit `align`, else `end` for numeric columns. */
39
+ export function resolveColumnAlign(col: { align?: DataTableAlign; numeric?: boolean }): DataTableAlign {
40
+ return col.align ?? (col.numeric ? 'end' : 'start')
41
+ }
42
+
43
+ /** The raw value of a column for a row (`undefined` without an accessor). */
44
+ export function getColumnValue<T>(col: { accessor?: (row: T) => unknown }, row: T): unknown {
45
+ return col.accessor ? col.accessor(row) : undefined
46
+ }
47
+
48
+ /**
49
+ * Stable React/DOM key for a row: `getRowKey` when given, else the row's
50
+ * index. Prefer a `getRowKey` — index keys break row state on sort/filter.
51
+ */
52
+ export function resolveRowKey<T>(
53
+ row: T,
54
+ index: number,
55
+ getRowKey?: (row: T, index: number) => string | number,
56
+ ): string | number {
57
+ return getRowKey ? getRowKey(row, index) : index
58
+ }
59
+
18
60
  export interface DataTableProps<T = Record<string, unknown>> {
19
61
  /** Column definitions */
20
62
  columns: ColumnDef<T>[]
@@ -74,10 +116,10 @@ export function createDataTable<T = Record<string, unknown>>(
74
116
  for (const [columnId, filterValue] of Object.entries(filters)) {
75
117
  if (!filterValue) continue
76
118
  const col = columns.find((c) => c.id === columnId)
77
- if (!col) continue
119
+ if (!col?.accessor) continue
78
120
  const lowerFilter = filterValue.toLowerCase()
79
121
  result = result.filter((row) => {
80
- const cellValue = col.accessor(row)
122
+ const cellValue = getColumnValue(col, row)
81
123
  return String(cellValue ?? '').toLowerCase().includes(lowerFilter)
82
124
  })
83
125
  }
@@ -91,11 +133,11 @@ export function createDataTable<T = Record<string, unknown>>(
91
133
  if (!sortBy) return filtered
92
134
 
93
135
  const col = columns.find((c) => c.id === sortBy)
94
- if (!col) return filtered
136
+ if (!col?.accessor) return filtered
95
137
 
96
138
  return [...filtered].sort((a, b) => {
97
- const aVal = col.accessor(a)
98
- const bVal = col.accessor(b)
139
+ const aVal = getColumnValue(col, a)
140
+ const bVal = getColumnValue(col, b)
99
141
  const aStr = String(aVal ?? '')
100
142
  const bStr = String(bVal ?? '')
101
143
 
@@ -1,6 +1,10 @@
1
1
  export {
2
2
  createDataTable,
3
+ resolveColumnAlign,
4
+ getColumnValue,
5
+ resolveRowKey,
3
6
  type ColumnDef,
7
+ type DataTableAlign,
4
8
  type SortDirection,
5
9
  type DataTableProps,
6
10
  type DataTableState,
@@ -12,4 +16,8 @@ export {
12
16
  headerVariants,
13
17
  cellVariants,
14
18
  rowVariants,
19
+ dataTableAlignClass,
20
+ dataTableNumericClass,
21
+ dataTableSortButtonClass,
22
+ dataTableActionRowClass,
15
23
  } from './data-table.styles.js'
@@ -9,6 +9,9 @@ export const diffViewerTokens: TokenContract = {
9
9
  sidebarBg: { variable: '--rfr-diff-sidebar-bg', fallback: 'hsl(var(--muted))' },
10
10
  headerBg: { variable: '--rfr-diff-header-bg', fallback: 'hsl(var(--muted))' },
11
11
  border: { variable: '--rfr-diff-border', fallback: 'hsl(var(--border))' },
12
+ // Diff add/del colors are syntax semantics (like code highlighting), not
13
+ // theme status tones — deliberately NOT routed through the success/
14
+ // destructive tokens. The --rfr-diff-* variables are the theming seam.
12
15
  addBg: { variable: '--rfr-diff-add-bg', fallback: 'rgba(46, 160, 67, 0.15)' },
13
16
  delBg: { variable: '--rfr-diff-del-bg', fallback: 'rgba(248, 81, 73, 0.15)' },
14
17
  addFg: { variable: '--rfr-diff-add-fg', fallback: '#3fb950' },
@@ -0,0 +1,16 @@
1
+ /** Token-class styles for FunnelChart; no colour literals. */
2
+ export const funnelChartClasses = {
3
+ root: 'm-0 grid list-none p-0',
4
+ bar: 'relative flex items-center gap-3 overflow-hidden rounded-lg border border-border bg-background px-3.5 py-3',
5
+ fill: 'absolute inset-y-0 left-0 border-r-2 border-primary bg-accent',
6
+ chevron: 'relative shrink-0 text-primary',
7
+ text: 'relative grid min-w-0 flex-1',
8
+ label: 'text-[13px] font-bold text-foreground',
9
+ description: 'text-[11px] text-muted-foreground',
10
+ figure: 'relative grid text-right',
11
+ value: 'text-[15px] font-bold tabular-nums text-foreground',
12
+ share: 'text-[10.5px] text-muted-foreground',
13
+ transition: 'flex items-center gap-2 py-1.5 pl-[18px] text-[11px] text-muted-foreground',
14
+ conversion: 'font-semibold text-success',
15
+ dropped: 'font-semibold text-destructive',
16
+ } as const
@@ -0,0 +1,68 @@
1
+ export interface FunnelChartStep {
2
+ /** Stable key. */
3
+ id: string
4
+ /** Step name. */
5
+ label: string
6
+ /** Secondary line under the label. */
7
+ description?: string
8
+ /** Count at this step. */
9
+ value: number
10
+ }
11
+
12
+ export interface FunnelChartTransition {
13
+ /** Percent (0–100) of this step that reached the next one. */
14
+ conversion: number
15
+ /** Absolute count lost between this step and the next. */
16
+ dropped: number
17
+ }
18
+
19
+ export interface FunnelChartRow {
20
+ step: FunnelChartStep
21
+ /** Percent (0–100) of the first step's value. */
22
+ shareOfTop: number
23
+ /** Width the bar draws at, in percent — never below `minBarPercent`. */
24
+ barPercent: number
25
+ /** Transition to the following step; absent on the last step. */
26
+ next?: FunnelChartTransition
27
+ }
28
+
29
+ /** Smallest bar width in percent, so a tiny step stays visible. */
30
+ export const DEFAULT_FUNNEL_MIN_BAR_PERCENT = 6
31
+
32
+ const percentOf = (part: number, whole: number) => (whole ? (part / whole) * 100 : 0)
33
+
34
+ /**
35
+ * Derive every row of a stepped conversion funnel: each step's share of the top
36
+ * step, the bar width it draws at, and between two steps the continue-rate and
37
+ * how many were lost. Pure and deterministic.
38
+ */
39
+ export function computeFunnel(
40
+ steps: FunnelChartStep[],
41
+ minBarPercent: number = DEFAULT_FUNNEL_MIN_BAR_PERCENT,
42
+ ): FunnelChartRow[] {
43
+ const top = steps.length ? steps[0].value : 0
44
+ return steps.map((step, i) => {
45
+ const shareOfTop = percentOf(step.value, top)
46
+ const following = steps[i + 1]
47
+ return {
48
+ step,
49
+ shareOfTop,
50
+ barPercent: Math.min(100, Math.max(shareOfTop, minBarPercent)),
51
+ next: following
52
+ ? {
53
+ conversion: percentOf(following.value, step.value),
54
+ dropped: step.value - following.value,
55
+ }
56
+ : undefined,
57
+ }
58
+ })
59
+ }
60
+
61
+ /** Default copy + number formatting; every one is overridable by a prop. */
62
+ export const funnelChartDefaults = {
63
+ formatValue: (value: number) => value.toLocaleString('en-US'),
64
+ formatPercent: (percent: number) => `${percent.toFixed(1)}%`,
65
+ shareLabel: (share: string) => `${share} of top`,
66
+ conversionLabel: (percent: string) => `${percent} continue`,
67
+ droppedLabel: (count: string) => `−${count} dropped`,
68
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ computeFunnel,
3
+ funnelChartDefaults,
4
+ DEFAULT_FUNNEL_MIN_BAR_PERCENT,
5
+ type FunnelChartStep,
6
+ type FunnelChartTransition,
7
+ type FunnelChartRow,
8
+ } from './funnel-chart.js'
9
+
10
+ export { funnelChartClasses } from './funnel-chart.styles.js'
package/dist/index.ts CHANGED
@@ -237,3 +237,9 @@ export * from './astro-audience-feature-card/index.ts'
237
237
  export * from './astro-sortable-list/index.ts'
238
238
  export * from './astro-kanban-board/index.ts'
239
239
  export * from './astro-slot-picker/index.ts'
240
+ export * from './astro-action-list/index.ts'
241
+ export * from './astro-funnel-chart/index.ts'
242
+ export * from './astro-line-chart/index.ts'
243
+ export * from './astro-page-header/index.ts'
244
+ export * from './astro-table/index.ts'
245
+ export * from './astro-native-select/index.ts'
@@ -1,11 +1,11 @@
1
1
  import { cva } from '../shared/index.ts'
2
2
 
3
3
  export const inputVariants = cva({
4
- base: 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
4
+ base: 'flex w-full rounded-md border border-input bg-transparent px-3 py-1 shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
5
5
  variants: {
6
6
  size: {
7
7
  sm: 'h-8 text-xs',
8
- default: 'h-9',
8
+ default: 'h-9 text-sm',
9
9
  lg: 'h-10 text-base',
10
10
  },
11
11
  validationState: {
@@ -0,0 +1,20 @@
1
+ export {
2
+ computeLineChart,
3
+ niceScale,
4
+ tooltipLeftPercent,
5
+ nextLineChartIndex,
6
+ lineChartColor,
7
+ DEFAULT_LINE_CHART_PADDING,
8
+ DEFAULT_LINE_CHART_WIDTH,
9
+ DEFAULT_LINE_CHART_HEIGHT,
10
+ type LineChartSeries,
11
+ type LineChartPadding,
12
+ type LineChartOptions,
13
+ type LineChartTick,
14
+ type LineChartXLabel,
15
+ type LineChartPath,
16
+ type LineChartColumn,
17
+ type LineChartGeometry,
18
+ } from './line-chart.js'
19
+
20
+ export { lineChartClasses } from './line-chart.styles.js'
@@ -0,0 +1,16 @@
1
+ /** Token-class styles for LineChart; no colour literals. */
2
+ export const lineChartClasses = {
3
+ root: 'relative',
4
+ svg: 'block w-full rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
5
+ gridline: 'stroke-border',
6
+ axis: 'fill-muted-foreground text-[9px]',
7
+ crosshair: 'stroke-border',
8
+ marker: 'stroke-card',
9
+ tooltip:
10
+ 'pointer-events-none absolute z-10 -translate-x-1/2 whitespace-nowrap rounded-md bg-tertiary px-2.5 py-2 text-[11px] text-tertiary-foreground shadow-lg',
11
+ tooltipLabel: 'mb-1 opacity-60',
12
+ tooltipRow: 'flex justify-between gap-2',
13
+ legend: 'mt-2.5 flex flex-wrap gap-3.5',
14
+ legendItem: 'inline-flex items-center gap-1.5 text-[11px] text-muted-foreground',
15
+ legendSwatch: 'h-[3px] w-2.5 rounded-sm',
16
+ } as const