@refraction-ui/astro 0.19.0 → 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 (34) hide show
  1. package/dist/analytics-sink-app-insights/app-insights-sink.ts +1 -1
  2. package/dist/analytics-sink-posthog/client-sdk-sink.ts +1 -1
  3. package/dist/analytics-sink-posthog/replay.ts +1 -1
  4. package/dist/astro-avatar/Avatar.astro +5 -4
  5. package/dist/astro-data-table/DataTable.astro +7 -3
  6. package/dist/astro-native-select/NativeSelect.astro +26 -0
  7. package/dist/astro-native-select/index.ts +1 -0
  8. package/dist/astro-stat-grid/StatGrid.astro +23 -19
  9. package/dist/astro-table/Table.astro +21 -0
  10. package/dist/astro-table/TableCaption.astro +9 -0
  11. package/dist/astro-table/TableCell.astro +13 -0
  12. package/dist/astro-table/TableHead.astro +13 -0
  13. package/dist/astro-table/TableRow.astro +9 -0
  14. package/dist/astro-table/index.ts +1 -0
  15. package/dist/avatar/avatar.styles.ts +7 -2
  16. package/dist/avatar/avatar.ts +8 -3
  17. package/dist/avatar/index.ts +1 -0
  18. package/dist/data-table/data-table.styles.ts +18 -0
  19. package/dist/data-table/data-table.ts +49 -7
  20. package/dist/data-table/index.ts +8 -0
  21. package/dist/index.ts +2 -0
  22. package/dist/line-chart/index.ts +1 -0
  23. package/dist/line-chart/line-chart.ts +36 -9
  24. package/dist/logger/faro-engine.ts +2 -2
  25. package/dist/native-select/index.ts +6 -0
  26. package/dist/native-select/native-select.styles.ts +30 -0
  27. package/dist/native-select/native-select.ts +5 -0
  28. package/dist/stat-grid/index.ts +9 -0
  29. package/dist/stat-grid/stat-grid.styles.ts +73 -6
  30. package/dist/stat-grid/stat-grid.ts +25 -5
  31. package/dist/table/index.ts +16 -0
  32. package/dist/table/table.styles.ts +72 -0
  33. package/dist/table/table.ts +24 -0
  34. package/package.json +3 -1
@@ -131,7 +131,7 @@ function createClientSdkSink(opts: ClientSdkOptions): AnalyticsSink {
131
131
  return instance
132
132
  }
133
133
  // Lazy, dynamic, optional — never a static/hard dependency.
134
- const mod = (await import(appInsightsWebSpecifier)) as {
134
+ const mod = (await import(/* webpackIgnore: true */ /* @vite-ignore */ appInsightsWebSpecifier)) as {
135
135
  ApplicationInsights: new (cfg: {
136
136
  config: {
137
137
  connectionString?: string
@@ -68,7 +68,7 @@ const posthogJsSpecifier = 'posthog-js'
68
68
 
69
69
  async function defaultLoad(): Promise<PostHogJs> {
70
70
  // Dynamic import keeps `posthog-js` out of the graph until first delivery.
71
- const mod = (await import(posthogJsSpecifier)) as unknown as
71
+ const mod = (await import(/* webpackIgnore: true */ /* @vite-ignore */ posthogJsSpecifier)) as unknown as
72
72
  | { default: PostHogJs }
73
73
  | PostHogJs
74
74
  return 'default' in mod ? mod.default : mod
@@ -78,7 +78,7 @@ export interface SessionReplayHandle {
78
78
  const posthogJsSpecifier = 'posthog-js'
79
79
 
80
80
  async function defaultLoad(): Promise<PostHogReplay> {
81
- const mod = (await import(posthogJsSpecifier)) as unknown as
81
+ const mod = (await import(/* webpackIgnore: true */ /* @vite-ignore */ posthogJsSpecifier)) as unknown as
82
82
  | { default: PostHogReplay }
83
83
  | PostHogReplay
84
84
  return 'default' in mod ? mod.default : mod
@@ -1,17 +1,18 @@
1
1
  ---
2
- import { createAvatar, avatarVariants, type AvatarSize } from '../avatar/index.ts'
2
+ import { createAvatar, avatarVariants, type AvatarSize, type AvatarShape } from '../avatar/index.ts'
3
3
  import { cn } from '../shared/index.ts'
4
4
 
5
5
  interface Props extends astroHTML.JSX.HTMLAttributes {
6
6
  size?: AvatarSize
7
+ shape?: AvatarShape
7
8
  }
8
9
 
9
- const { size = 'md', class: className, ...props } = Astro.props
10
- const api = createAvatar({ size })
10
+ const { size = 'md', shape = 'circle', class: className, ...props } = Astro.props
11
+ const api = createAvatar({ size, alt: props['aria-label'] as string | undefined })
11
12
  ---
12
13
 
13
14
  <span
14
- class={cn(avatarVariants({ size }), className)}
15
+ class={cn(avatarVariants({ size, shape }), className)}
15
16
  data-rfr-avatar
16
17
  data-rfr-avatar-size={size}
17
18
  {...api.ariaProps}
@@ -5,6 +5,10 @@ import {
5
5
  headerVariants,
6
6
  cellVariants,
7
7
  rowVariants,
8
+ getColumnValue,
9
+ resolveColumnAlign,
10
+ dataTableAlignClass,
11
+ dataTableNumericClass,
8
12
  type ColumnDef,
9
13
  type SortDirection,
10
14
  } from '../data-table/index.ts'
@@ -41,7 +45,7 @@ const hasFilterable = columns.some((c: ColumnDef) => c.filterable)
41
45
  const hProps = api.getHeaderProps(col)
42
46
  return (
43
47
  <th
44
- class={headerVariants({ sortable: col.sortable ? 'true' : 'false' })}
48
+ class={cn(headerVariants({ sortable: col.sortable ? 'true' : 'false' }), dataTableAlignClass[resolveColumnAlign(col)], col.headerClassName)}
45
49
  role={hProps.role as string}
46
50
  aria-sort={hProps['aria-sort'] as any}
47
51
  data-rfr-data-table-header={col.id}
@@ -94,10 +98,10 @@ const hasFilterable = columns.some((c: ColumnDef) => c.filterable)
94
98
  {columns.map((col: ColumnDef) => (
95
99
  <td
96
100
  role="cell"
97
- class={cellVariants()}
101
+ class={cn(cellVariants(), dataTableAlignClass[resolveColumnAlign(col)], col.numeric && dataTableNumericClass, col.className)}
98
102
  data-column={col.id}
99
103
  >
100
- {String(col.accessor(row) ?? '')}
104
+ {String(getColumnValue(col, row) ?? '')}
101
105
  </td>
102
106
  ))}
103
107
  </tr>
@@ -0,0 +1,26 @@
1
+ ---
2
+ import {
3
+ nativeSelectChevronPath,
4
+ nativeSelectIconClass,
5
+ nativeSelectVariants,
6
+ nativeSelectWrapperClass,
7
+ type NativeSelectSize,
8
+ } from '../native-select/index.ts'
9
+ import { cn } from '../shared/index.ts'
10
+
11
+ interface Props extends Omit<astroHTML.JSX.SelectHTMLAttributes, 'size'> {
12
+ size?: NativeSelectSize
13
+ containerClass?: string
14
+ }
15
+
16
+ const { size = 'default', class: className, containerClass, ...rest } = Astro.props
17
+ ---
18
+
19
+ <span class={cn(nativeSelectWrapperClass, containerClass)} data-slot="native-select">
20
+ <select class={cn(nativeSelectVariants({ size }), className)} {...rest}>
21
+ <slot />
22
+ </select>
23
+ <svg class={nativeSelectIconClass} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
24
+ <path d={nativeSelectChevronPath} />
25
+ </svg>
26
+ </span>
@@ -0,0 +1 @@
1
+ export {}
@@ -2,44 +2,48 @@
2
2
  import {
3
3
  createStatGrid,
4
4
  statColumns,
5
+ statColumnsKey,
5
6
  statGridVariants,
6
- statItemClass,
7
- statValueClass,
8
- statLabelClass,
7
+ statItemVariants,
8
+ statValueVariants,
9
+ statLabelVariants,
10
+ statDescriptionClass,
9
11
  type StatItem,
12
+ type StatGridVariant,
13
+ type StatGridLayout,
14
+ type StatGridColumns,
10
15
  } from '../stat-grid/index.ts'
11
16
  import { cn } from '../shared/index.ts'
12
17
 
13
18
  interface Props extends astroHTML.JSX.HTMLAttributes {
14
- /** The stat items to display in the grid (value and label as strings). */
19
+ /** The stat items to display in the grid. */
15
20
  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
+ /** Maximum columns (reflows on narrow screens) or `auto`. Defaults to statColumns(items.length). */
22
+ columns?: StatGridColumns
23
+ variant?: StatGridVariant
24
+ layout?: StatGridLayout
21
25
  }
22
26
 
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 { items, columns, variant = 'plain', layout = 'value-first', class: className, ...rest } = Astro.props
28
+ const cols = statColumnsKey(columns ?? statColumns(items.length))
27
29
  const { ariaProps, dataAttributes } = createStatGrid()
28
- const gridStyle = `grid-template-columns:repeat(${cols}, 1fr)`
29
30
  ---
30
31
 
31
32
  <div
32
- class={cn(statGridVariants({ columns: colsKey }), className)}
33
- style={gridStyle}
33
+ class={cn(statGridVariants({ columns: cols, variant }), className)}
34
+ data-variant={variant}
35
+ data-layout={layout}
34
36
  {...ariaProps}
35
37
  {...dataAttributes}
36
38
  {...rest}
37
39
  >
38
40
  {
39
41
  items.map((item) => (
40
- <div role="listitem" class={statItemClass}>
41
- <span class={statValueClass}>{item.value}</span>
42
- <span class={statLabelClass}>{item.label}</span>
42
+ <div role="listitem" class={statItemVariants({ variant })} data-tone={item.tone ?? 'default'}>
43
+ {layout === 'label-first' && <span class={statLabelVariants({ layout })}>{item.label}</span>}
44
+ <span class={statValueVariants({ variant, tone: item.tone ?? 'default' })}>{item.value}</span>
45
+ {layout === 'value-first' && <span class={statLabelVariants({ layout })}>{item.label}</span>}
46
+ {item.description && <span class={statDescriptionClass}>{item.description}</span>}
43
47
  </div>
44
48
  ))
45
49
  }
@@ -0,0 +1,21 @@
1
+ ---
2
+ import { createTable, tableContainerClass, tableRootVariants, type TableDensity, type TableHeadTone } from '../table/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.TableHTMLAttributes {
6
+ /** Recorded as data-density; pass the same density to TableHead/TableCell. */
7
+ density?: TableDensity
8
+ headTone?: TableHeadTone
9
+ fixed?: boolean
10
+ containerClass?: string
11
+ }
12
+
13
+ const { density = 'default', headTone = 'default', fixed = false, containerClass, class: className, ...rest } = Astro.props
14
+ const { dataAttributes } = createTable({ density, headTone })
15
+ ---
16
+
17
+ <div class={cn(tableContainerClass, containerClass)} data-slot="table-container">
18
+ <table class={cn(tableRootVariants({ fixed: fixed ? 'true' : 'false' }), className)} {...dataAttributes} {...rest}>
19
+ <slot />
20
+ </table>
21
+ </div>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { tableCaptionClass } 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
+ <caption class={cn(tableCaptionClass, className)} data-slot="table-caption" {...rest}><slot /></caption>
@@ -0,0 +1,13 @@
1
+ ---
2
+ import { tableCellVariants, type TableAlign, type TableDensity } from '../table/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends Omit<astroHTML.JSX.TdHTMLAttributes, 'align'> {
6
+ align?: TableAlign
7
+ density?: TableDensity
8
+ numeric?: boolean
9
+ }
10
+ const { align, density, numeric = false, class: className, ...rest } = Astro.props
11
+ ---
12
+
13
+ <td class={cn(tableCellVariants({ align, density, numeric: numeric ? 'true' : 'false' }), className)} data-slot="table-cell" {...rest}><slot /></td>
@@ -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
 
@@ -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'
package/dist/index.ts CHANGED
@@ -241,3 +241,5 @@ export * from './astro-action-list/index.ts'
241
241
  export * from './astro-funnel-chart/index.ts'
242
242
  export * from './astro-line-chart/index.ts'
243
243
  export * from './astro-page-header/index.ts'
244
+ export * from './astro-table/index.ts'
245
+ export * from './astro-native-select/index.ts'
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  computeLineChart,
3
+ niceScale,
3
4
  tooltipLeftPercent,
4
5
  nextLineChartIndex,
5
6
  lineChartColor,
@@ -27,11 +27,14 @@ export interface LineChartOptions {
27
27
  /** viewBox height. */
28
28
  height: number
29
29
  padding?: Partial<LineChartPadding>
30
- /** Number of gridline intervals on the y-axis. */
30
+ /** Target number of gridline intervals on the y-axis (the nice scale may use fewer). */
31
31
  tickCount?: number
32
32
  /** Headroom multiplier above the largest value. */
33
33
  headroom?: number
34
- /** Show every n-th x label (the last label is always shown). */
34
+ /**
35
+ * Show every n-th x label (the last label is always shown). Defaults to
36
+ * thinning so labels are at least ~48 viewBox units apart.
37
+ */
35
38
  labelEvery?: number
36
39
  }
37
40
 
@@ -91,7 +94,8 @@ export const DEFAULT_LINE_CHART_WIDTH = 760
91
94
  export const DEFAULT_LINE_CHART_HEIGHT = 230
92
95
  const DEFAULT_TICK_COUNT = 4
93
96
  const DEFAULT_HEADROOM = 1.08
94
- const DEFAULT_LABEL_EVERY = 2
97
+ /** Minimum horizontal room per x label when thinning automatically. */
98
+ const MIN_LABEL_SPACING = 48
95
99
  /** Number of `--chart-N` tokens the theme defines. */
96
100
  const CHART_TOKEN_COUNT = 5
97
101
 
@@ -100,6 +104,28 @@ export function lineChartColor(index: number): string {
100
104
  return `hsl(var(--chart-${(index % CHART_TOKEN_COUNT) + 1}))`
101
105
  }
102
106
 
107
+ const NICE_STEPS = [1, 2, 5, 10]
108
+
109
+ /**
110
+ * A "nice" y-scale: a 1-2-5 × 10^k step so there are at most `tickCount`
111
+ * intervals up to `rawMax`, and the max rounded up to a whole step. Tick
112
+ * values are therefore round numbers by construction (0, 2000, 4000, …) —
113
+ * never 5999.9999 — so any tick formatter prints clean labels.
114
+ */
115
+ export function niceScale(rawMax: number, tickCount: number): { max: number; step: number; ticks: number[] } {
116
+ const intervals = Math.max(1, Math.floor(tickCount))
117
+ if (!(rawMax > 0)) return { max: 1, step: 1 / intervals, ticks: Array.from({ length: intervals + 1 }, (_, i) => i / intervals) }
118
+ const target = rawMax / intervals
119
+ const magnitude = 10 ** Math.floor(Math.log10(target))
120
+ const step = NICE_STEPS.map((m) => m * magnitude).find((candidate) => candidate >= target) ?? 10 * magnitude
121
+ const count = Math.ceil(rawMax / step - 1e-9)
122
+ // Strip binary floating-point noise (e.g. 0.1 * 3) to the step's precision.
123
+ const decimals = Math.max(0, -Math.floor(Math.log10(step)))
124
+ const clean = (value: number) => Number(value.toFixed(decimals))
125
+ const ticks = Array.from({ length: count + 1 }, (_, i) => clean(i * step))
126
+ return { max: ticks[ticks.length - 1], step: clean(step), ticks }
127
+ }
128
+
103
129
  function pathFrom(points: Array<[number, number]>): string {
104
130
  return points.map(([px, py], i) => (i ? 'L' : 'M') + px.toFixed(1) + ' ' + py.toFixed(1)).join(' ')
105
131
  }
@@ -117,25 +143,26 @@ export function computeLineChart(options: LineChartOptions): LineChartGeometry {
117
143
  height,
118
144
  tickCount = DEFAULT_TICK_COUNT,
119
145
  headroom = DEFAULT_HEADROOM,
120
- labelEvery = DEFAULT_LABEL_EVERY,
121
146
  } = options
122
147
  const padding = { ...DEFAULT_LINE_CHART_PADDING, ...options.padding }
123
148
  const n = labels.length
124
149
  const values = series.flatMap((s) => s.data)
125
150
  const peak = values.length ? Math.max(...values) : 0
126
- const max = peak > 0 ? peak * headroom : 1
151
+ const scale = niceScale(peak > 0 ? peak * headroom : 0, tickCount)
152
+ const max = scale.max
127
153
  const plotW = width - padding.left - padding.right
128
154
  const plotTop = padding.top
129
155
  const plotBottom = height - padding.bottom
130
156
  const plotH = plotBottom - plotTop
131
157
 
158
+ const labelEvery =
159
+ options.labelEvery ??
160
+ Math.max(1, Math.ceil(n / Math.max(1, Math.floor(plotW / MIN_LABEL_SPACING))))
161
+
132
162
  const x = (index: number) => padding.left + (n > 1 ? (index / (n - 1)) * plotW : plotW / 2)
133
163
  const y = (value: number) => plotTop + (1 - value / max) * plotH
134
164
 
135
- const ticks: LineChartTick[] = Array.from({ length: tickCount + 1 }, (_, t) => {
136
- const value = (max / tickCount) * t
137
- return { value, y: y(value) }
138
- })
165
+ const ticks: LineChartTick[] = scale.ticks.map((value) => ({ value, y: y(value) }))
139
166
 
140
167
  const xLabels: LineChartXLabel[] = labels
141
168
  .map((label, index) => ({ index, label, x: x(index) }))
@@ -74,11 +74,11 @@ async function loadFaroTransport(
74
74
  // Indirected so bundlers keep these as runtime-optional dynamic imports.
75
75
  const sdkName = '@grafana/faro-web-sdk'
76
76
  const tracingName = '@grafana/faro-web-tracing'
77
- const sdk = (await import(/* @vite-ignore */ sdkName)) as {
77
+ const sdk = (await import(/* webpackIgnore: true */ /* @vite-ignore */ sdkName)) as {
78
78
  initializeFaro: (cfg: unknown) => unknown
79
79
  getWebInstrumentations: () => unknown[]
80
80
  }
81
- const tracing = (await import(/* @vite-ignore */ tracingName)) as {
81
+ const tracing = (await import(/* webpackIgnore: true */ /* @vite-ignore */ tracingName)) as {
82
82
  TracingInstrumentation: new () => unknown
83
83
  }
84
84
 
@@ -0,0 +1,6 @@
1
+ export { nativeSelectChevronPath, type NativeSelectSize } from './native-select.js'
2
+ export {
3
+ nativeSelectWrapperClass,
4
+ nativeSelectVariants,
5
+ nativeSelectIconClass,
6
+ } from './native-select.styles.js'
@@ -0,0 +1,30 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** Wrapper that positions the chevron over the select. */
4
+ export const nativeSelectWrapperClass = 'relative inline-flex min-w-0 items-center'
5
+
6
+ /**
7
+ * The `<select>`: the platform's own listbox (mobile pickers, type-to-select,
8
+ * form submission, `change` events) with the input look. Size-owned utilities
9
+ * live only in the size variants so `size` always wins.
10
+ */
11
+ export const nativeSelectVariants = cva({
12
+ base:
13
+ 'w-full cursor-pointer appearance-none rounded-md border border-input bg-background text-foreground shadow-sm ' +
14
+ 'ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ' +
15
+ 'disabled:cursor-not-allowed disabled:opacity-50',
16
+ variants: {
17
+ size: {
18
+ sm: 'h-8 pl-2.5 pr-8 text-xs',
19
+ default: 'h-9 pl-3 pr-9 text-sm',
20
+ lg: 'h-10 pl-3 pr-10 text-base',
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ size: 'default',
25
+ },
26
+ })
27
+
28
+ /** The chevron: decorative, and never eats the click meant for the select. */
29
+ export const nativeSelectIconClass =
30
+ 'pointer-events-none absolute right-2.5 h-4 w-4 shrink-0 text-muted-foreground'
@@ -0,0 +1,5 @@
1
+ /** Visual size of a NativeSelect. */
2
+ export type NativeSelectSize = 'sm' | 'default' | 'lg'
3
+
4
+ /** The chevron's path (a down chevron), so adapters ship no icon dependency. */
5
+ export const nativeSelectChevronPath = 'm6 9 6 6 6-6'
@@ -1,12 +1,21 @@
1
1
  export {
2
2
  createStatGrid,
3
3
  statColumns,
4
+ statColumnsKey,
4
5
  type StatItem,
6
+ type StatTone,
7
+ type StatGridVariant,
8
+ type StatGridLayout,
9
+ type StatGridColumns,
5
10
  type StatGridAPI,
6
11
  } from './stat-grid.js'
7
12
 
8
13
  export {
9
14
  statGridVariants,
15
+ statItemVariants,
16
+ statValueVariants,
17
+ statLabelVariants,
18
+ statDescriptionClass,
10
19
  statItemClass,
11
20
  statValueClass,
12
21
  statLabelClass,
@@ -1,25 +1,92 @@
1
1
  import { cva } from '../shared/index.ts'
2
2
 
3
- /** Grid container that lays out stat items. */
3
+ /**
4
+ * Grid container. Column counts are responsive (one column on a phone, more
5
+ * as the width allows) — never a fixed inline template that cannot reflow.
6
+ * `auto` fits as many ~12rem items per row as the width allows.
7
+ */
4
8
  export const statGridVariants = cva({
5
- base: 'grid gap-8',
9
+ base: 'grid',
6
10
  variants: {
7
11
  columns: {
8
12
  '1': 'grid-cols-1',
9
- '2': 'grid-cols-2',
10
- '3': 'grid-cols-3',
13
+ '2': 'grid-cols-1 sm:grid-cols-2',
14
+ '3': 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
15
+ '4': 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
16
+ auto: 'grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]',
17
+ },
18
+ variant: {
19
+ plain: 'gap-8',
20
+ card: 'gap-3',
11
21
  },
12
22
  },
13
23
  defaultVariants: {
14
24
  columns: '3',
25
+ variant: 'plain',
15
26
  },
16
27
  })
17
28
 
18
29
  /** Individual stat item container (role="listitem"). */
30
+ export const statItemVariants = cva({
31
+ base: 'flex min-w-0 flex-col',
32
+ variants: {
33
+ variant: {
34
+ plain: '',
35
+ card: 'gap-1 rounded-lg border border-border bg-card p-4 text-card-foreground',
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: 'plain',
40
+ },
41
+ })
42
+
43
+ /** @deprecated Use `statItemVariants()`; kept for existing imports. */
19
44
  export const statItemClass = 'flex flex-col'
20
45
 
21
- /** The large bold value of the stat. */
46
+ /** The value of a stat, by variant and tone. */
47
+ export const statValueVariants = cva({
48
+ base: 'font-bold tabular-nums',
49
+ variants: {
50
+ variant: {
51
+ plain: 'text-xl sm:text-2xl',
52
+ card: 'text-2xl leading-tight',
53
+ },
54
+ tone: {
55
+ default: '',
56
+ positive: 'text-success',
57
+ negative: 'text-destructive',
58
+ caution: 'text-warning',
59
+ },
60
+ },
61
+ compoundVariants: [
62
+ { variant: 'plain', tone: 'default', class: 'text-primary' },
63
+ { variant: 'card', tone: 'default', class: 'text-foreground' },
64
+ ],
65
+ defaultVariants: {
66
+ variant: 'plain',
67
+ tone: 'default',
68
+ },
69
+ })
70
+
71
+ /** @deprecated Use `statValueVariants()`; kept for existing imports. */
22
72
  export const statValueClass = 'text-xl sm:text-2xl font-bold text-primary'
23
73
 
24
- /** The descriptive label below the value. */
74
+ /** The descriptive label. Above the value it reads as a small caption. */
75
+ export const statLabelVariants = cva({
76
+ base: 'text-muted-foreground',
77
+ variants: {
78
+ layout: {
79
+ 'value-first': 'mt-1 text-sm leading-snug',
80
+ 'label-first': 'text-xs uppercase tracking-wide',
81
+ },
82
+ },
83
+ defaultVariants: {
84
+ layout: 'value-first',
85
+ },
86
+ })
87
+
88
+ /** @deprecated Use `statLabelVariants()`; kept for existing imports. */
25
89
  export const statLabelClass = 'mt-1 text-sm text-muted-foreground leading-snug'
90
+
91
+ /** Optional line under a stat explaining how it was computed. */
92
+ export const statDescriptionClass = 'text-xs leading-snug text-muted-foreground'
@@ -1,9 +1,24 @@
1
+ /** Semantic colour of a stat's value. */
2
+ export type StatTone = 'default' | 'positive' | 'negative' | 'caution'
3
+ /** `plain` = marketing callouts; `card` = bordered dashboard KPI cards. */
4
+ export type StatGridVariant = 'plain' | 'card'
5
+ /** Whether the label sits under the value (callout) or above it (KPI). */
6
+ export type StatGridLayout = 'value-first' | 'label-first'
7
+ /** Column count: a fixed maximum that reflows down on narrow screens, or `auto`. */
8
+ export type StatGridColumns = 1 | 2 | 3 | 4 | 'auto'
9
+
1
10
  /** A single stat callout displayed in the grid. */
2
11
  export type StatItem = {
3
12
  /** The primary value displayed prominently (e.g. "10k+", "$4.2M"). */
4
13
  value: string
5
- /** The descriptive label shown below the value. */
14
+ /** The descriptive label. */
6
15
  label: string
16
+ /** Stable key; defaults to the index. */
17
+ id?: string
18
+ /** Optional line explaining the figure (how it was computed, a delta). */
19
+ description?: string
20
+ /** Colour of the value. Defaults to `default`. */
21
+ tone?: StatTone
7
22
  }
8
23
 
9
24
  export interface StatGridAPI {
@@ -20,11 +35,16 @@ export interface StatGridAPI {
20
35
  * - 2 items → 2 columns
21
36
  * - 3 or more items → capped at `max` (default 3)
22
37
  */
23
- export function statColumns(count: number, max = 3): number {
24
- if (count <= 0) return 1
25
- if (count === 1) return 1
38
+ export function statColumns(count: number, max = 3): 1 | 2 | 3 | 4 {
39
+ if (count <= 1) return 1
26
40
  if (count === 2) return 2
27
- return Math.min(count, max)
41
+ return Math.min(count, max, 4) as 3 | 4
42
+ }
43
+
44
+ /** The cva key for a column setting (clamped to the supported 1–4). */
45
+ export function statColumnsKey(columns: StatGridColumns): '1' | '2' | '3' | '4' | 'auto' {
46
+ if (columns === 'auto') return 'auto'
47
+ return String(Math.min(Math.max(Math.round(columns), 1), 4)) as '1' | '2' | '3' | '4'
28
48
  }
29
49
 
30
50
  /**
@@ -0,0 +1,16 @@
1
+ export {
2
+ createTable,
3
+ type TableDensity,
4
+ type TableAlign,
5
+ type TableHeadTone,
6
+ type TableAPI,
7
+ } from './table.js'
8
+
9
+ export {
10
+ tableContainerClass,
11
+ tableRootVariants,
12
+ tableCaptionClass,
13
+ tableRowClass,
14
+ tableHeadVariants,
15
+ tableCellVariants,
16
+ } from './table.styles.js'
@@ -0,0 +1,72 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** Scroll container: a wide table scrolls inside it instead of the page. */
4
+ export const tableContainerClass = 'relative w-full overflow-x-auto'
5
+
6
+ /** The `<table>`. `fixed` pins column widths to the header row. */
7
+ export const tableRootVariants = cva({
8
+ base: 'w-full caption-bottom border-collapse text-sm',
9
+ variants: {
10
+ fixed: {
11
+ true: 'table-fixed',
12
+ false: '',
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ fixed: 'false',
17
+ },
18
+ })
19
+
20
+ /** Caption under the table. */
21
+ export const tableCaptionClass = 'mt-2 text-xs text-muted-foreground'
22
+
23
+ /** Body rows: a hairline between rows, none after the last. */
24
+ export const tableRowClass = 'border-b border-border-subtle last:border-b-0'
25
+
26
+ const align = {
27
+ start: 'text-start',
28
+ center: 'text-center',
29
+ end: 'text-end',
30
+ }
31
+
32
+ /** Header cell: subtle surface, rule under it. */
33
+ export const tableHeadVariants = cva({
34
+ base: 'border-b border-border-subtle bg-surface-subtle align-middle',
35
+ variants: {
36
+ align,
37
+ tone: {
38
+ default: 'text-xs font-medium text-muted-foreground',
39
+ eyebrow: 'text-[11px] font-semibold uppercase tracking-wider text-muted-foreground',
40
+ },
41
+ density: {
42
+ compact: 'h-8 px-2',
43
+ default: 'h-9 px-3',
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ align: 'start',
48
+ tone: 'default',
49
+ density: 'default',
50
+ },
51
+ })
52
+
53
+ /** Data cell. */
54
+ export const tableCellVariants = cva({
55
+ base: 'align-middle',
56
+ variants: {
57
+ align,
58
+ density: {
59
+ compact: 'px-2 py-1.5',
60
+ default: 'px-3 py-2.5',
61
+ },
62
+ numeric: {
63
+ true: 'tabular-nums whitespace-nowrap',
64
+ false: '',
65
+ },
66
+ },
67
+ defaultVariants: {
68
+ align: 'start',
69
+ density: 'default',
70
+ numeric: 'false',
71
+ },
72
+ })
@@ -0,0 +1,24 @@
1
+ /** Cell padding for every cell of a table. */
2
+ export type TableDensity = 'compact' | 'default'
3
+ /** Horizontal alignment of a header or data cell (logical, RTL-safe). */
4
+ export type TableAlign = 'start' | 'center' | 'end'
5
+ /** Look of header cells: `eyebrow` is small uppercase tracked text. */
6
+ export type TableHeadTone = 'default' | 'eyebrow'
7
+
8
+ export interface TableAPI {
9
+ /** Data attributes for styling hooks. */
10
+ dataAttributes: Record<string, string>
11
+ }
12
+
13
+ /** Framework-agnostic data attributes for the table root. */
14
+ export function createTable(
15
+ props: { density?: TableDensity; headTone?: TableHeadTone } = {},
16
+ ): TableAPI {
17
+ return {
18
+ dataAttributes: {
19
+ 'data-slot': 'table',
20
+ 'data-density': props.density ?? 'default',
21
+ 'data-head-tone': props.headTone ?? 'default',
22
+ },
23
+ }
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {
@@ -96,6 +96,7 @@
96
96
  "@refraction-ui/astro-mastery-bar": "workspace:*",
97
97
  "@refraction-ui/astro-mini-map": "workspace:*",
98
98
  "@refraction-ui/astro-mobile-nav": "workspace:*",
99
+ "@refraction-ui/astro-native-select": "workspace:*",
99
100
  "@refraction-ui/astro-navbar": "workspace:*",
100
101
  "@refraction-ui/astro-numbered-steps": "workspace:*",
101
102
  "@refraction-ui/astro-otp-input": "workspace:*",
@@ -132,6 +133,7 @@
132
133
  "@refraction-ui/astro-steps": "workspace:*",
133
134
  "@refraction-ui/astro-sticky-note": "workspace:*",
134
135
  "@refraction-ui/astro-switch": "workspace:*",
136
+ "@refraction-ui/astro-table": "workspace:*",
135
137
  "@refraction-ui/astro-table-of-contents": "workspace:*",
136
138
  "@refraction-ui/astro-tabs": "workspace:*",
137
139
  "@refraction-ui/astro-terminal": "workspace:*",