@refraction-ui/astro 0.18.1 → 0.19.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 (48) 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/astro-action-list/ActionList.astro +12 -0
  5. package/dist/astro-action-list/ActionListItem.astro +62 -0
  6. package/dist/astro-action-list/index.ts +1 -0
  7. package/dist/astro-callout/Callout.astro +4 -1
  8. package/dist/astro-card/Card.astro +3 -2
  9. package/dist/astro-card/CardTitle.astro +6 -4
  10. package/dist/astro-funnel-chart/FunnelChart.astro +64 -0
  11. package/dist/astro-funnel-chart/index.ts +1 -0
  12. package/dist/astro-line-chart/LineChart.astro +82 -0
  13. package/dist/astro-line-chart/index.ts +1 -0
  14. package/dist/astro-page-header/PageHeader.astro +46 -0
  15. package/dist/astro-page-header/index.ts +1 -0
  16. package/dist/badge/badge.styles.ts +6 -1
  17. package/dist/badge/badge.ts +24 -2
  18. package/dist/button/button.styles.ts +10 -4
  19. package/dist/button/button.ts +4 -1
  20. package/dist/callout/callout.ts +12 -3
  21. package/dist/card/card.styles.ts +10 -1
  22. package/dist/card/card.ts +14 -0
  23. package/dist/card/index.ts +3 -0
  24. package/dist/diff-viewer/diff-viewer.styles.ts +3 -0
  25. package/dist/funnel-chart/funnel-chart.styles.ts +16 -0
  26. package/dist/funnel-chart/funnel-chart.ts +68 -0
  27. package/dist/funnel-chart/index.ts +10 -0
  28. package/dist/index.ts +4 -0
  29. package/dist/input/input.styles.ts +2 -2
  30. package/dist/line-chart/index.ts +19 -0
  31. package/dist/line-chart/line-chart.styles.ts +16 -0
  32. package/dist/line-chart/line-chart.ts +188 -0
  33. package/dist/page-header/index.ts +15 -0
  34. package/dist/page-header/page-header.styles.ts +21 -0
  35. package/dist/page-header/page-header.ts +27 -0
  36. package/dist/progress-display/progress-display.styles.ts +1 -1
  37. package/dist/select/index.ts +2 -0
  38. package/dist/select/select.styles.ts +9 -3
  39. package/dist/select/select.ts +11 -3
  40. package/dist/separator/index.ts +2 -0
  41. package/dist/separator/separator.styles.ts +17 -2
  42. package/dist/shared/cn.ts +6 -3
  43. package/dist/slide-viewer/slide-viewer.styles.ts +2 -0
  44. package/dist/sticky-note/sticky-note.styles.ts +2 -0
  45. package/dist/tabs/index.ts +1 -0
  46. package/dist/tabs/tabs.ts +29 -0
  47. package/dist/textarea/textarea.styles.ts +2 -2
  48. package/package.json +5 -1
@@ -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,7 @@ 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'
@@ -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,19 @@
1
+ export {
2
+ computeLineChart,
3
+ tooltipLeftPercent,
4
+ nextLineChartIndex,
5
+ lineChartColor,
6
+ DEFAULT_LINE_CHART_PADDING,
7
+ DEFAULT_LINE_CHART_WIDTH,
8
+ DEFAULT_LINE_CHART_HEIGHT,
9
+ type LineChartSeries,
10
+ type LineChartPadding,
11
+ type LineChartOptions,
12
+ type LineChartTick,
13
+ type LineChartXLabel,
14
+ type LineChartPath,
15
+ type LineChartColumn,
16
+ type LineChartGeometry,
17
+ } from './line-chart.js'
18
+
19
+ 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
@@ -0,0 +1,188 @@
1
+ export interface LineChartSeries {
2
+ /** Stable key for the series. */
3
+ id: string
4
+ /** Legend / tooltip name. */
5
+ name: string
6
+ /**
7
+ * Any CSS colour (a token `hsl(var(--…))` or a data colour). Defaults to the
8
+ * theme's chart palette (`--chart-1` … `--chart-5`) by series position.
9
+ */
10
+ color?: string
11
+ /** One value per label, in label order. */
12
+ data: number[]
13
+ }
14
+
15
+ export interface LineChartPadding {
16
+ top: number
17
+ right: number
18
+ bottom: number
19
+ left: number
20
+ }
21
+
22
+ export interface LineChartOptions {
23
+ series: LineChartSeries[]
24
+ labels: string[]
25
+ /** viewBox width — the SVG scales to its container's width. */
26
+ width: number
27
+ /** viewBox height. */
28
+ height: number
29
+ padding?: Partial<LineChartPadding>
30
+ /** Number of gridline intervals on the y-axis. */
31
+ tickCount?: number
32
+ /** Headroom multiplier above the largest value. */
33
+ headroom?: number
34
+ /** Show every n-th x label (the last label is always shown). */
35
+ labelEvery?: number
36
+ }
37
+
38
+ export interface LineChartTick {
39
+ value: number
40
+ y: number
41
+ }
42
+
43
+ export interface LineChartXLabel {
44
+ index: number
45
+ label: string
46
+ x: number
47
+ }
48
+
49
+ export interface LineChartPath {
50
+ id: string
51
+ /** Resolved stroke colour. */
52
+ color: string
53
+ /** The stroke path. */
54
+ line: string
55
+ /** The line closed down to the zero baseline, for an area fill. */
56
+ area: string
57
+ }
58
+
59
+ export interface LineChartColumn {
60
+ index: number
61
+ x: number
62
+ width: number
63
+ }
64
+
65
+ export interface LineChartGeometry {
66
+ width: number
67
+ height: number
68
+ padding: LineChartPadding
69
+ /** Top of the shared y-scale. */
70
+ max: number
71
+ ticks: LineChartTick[]
72
+ xLabels: LineChartXLabel[]
73
+ paths: LineChartPath[]
74
+ /** One hover/focus column per category. */
75
+ columns: LineChartColumn[]
76
+ x: (index: number) => number
77
+ y: (value: number) => number
78
+ /** Top and bottom of the plot area, for a crosshair. */
79
+ plotTop: number
80
+ plotBottom: number
81
+ }
82
+
83
+ export const DEFAULT_LINE_CHART_PADDING: LineChartPadding = {
84
+ top: 14,
85
+ right: 14,
86
+ bottom: 24,
87
+ left: 40,
88
+ }
89
+
90
+ export const DEFAULT_LINE_CHART_WIDTH = 760
91
+ export const DEFAULT_LINE_CHART_HEIGHT = 230
92
+ const DEFAULT_TICK_COUNT = 4
93
+ const DEFAULT_HEADROOM = 1.08
94
+ const DEFAULT_LABEL_EVERY = 2
95
+ /** Number of `--chart-N` tokens the theme defines. */
96
+ const CHART_TOKEN_COUNT = 5
97
+
98
+ /** The theme chart colour for the series at `index` (wraps around). */
99
+ export function lineChartColor(index: number): string {
100
+ return `hsl(var(--chart-${(index % CHART_TOKEN_COUNT) + 1}))`
101
+ }
102
+
103
+ function pathFrom(points: Array<[number, number]>): string {
104
+ return points.map(([px, py], i) => (i ? 'L' : 'M') + px.toFixed(1) + ' ' + py.toFixed(1)).join(' ')
105
+ }
106
+
107
+ /**
108
+ * Pure geometry for a responsive multi-series line chart. Every series shares
109
+ * ONE y-scale (zero baseline to the largest value plus headroom), so series are
110
+ * comparable. Coordinates are in viewBox units; the SVG scales to its container.
111
+ */
112
+ export function computeLineChart(options: LineChartOptions): LineChartGeometry {
113
+ const {
114
+ series,
115
+ labels,
116
+ width,
117
+ height,
118
+ tickCount = DEFAULT_TICK_COUNT,
119
+ headroom = DEFAULT_HEADROOM,
120
+ labelEvery = DEFAULT_LABEL_EVERY,
121
+ } = options
122
+ const padding = { ...DEFAULT_LINE_CHART_PADDING, ...options.padding }
123
+ const n = labels.length
124
+ const values = series.flatMap((s) => s.data)
125
+ const peak = values.length ? Math.max(...values) : 0
126
+ const max = peak > 0 ? peak * headroom : 1
127
+ const plotW = width - padding.left - padding.right
128
+ const plotTop = padding.top
129
+ const plotBottom = height - padding.bottom
130
+ const plotH = plotBottom - plotTop
131
+
132
+ const x = (index: number) => padding.left + (n > 1 ? (index / (n - 1)) * plotW : plotW / 2)
133
+ const y = (value: number) => plotTop + (1 - value / max) * plotH
134
+
135
+ const ticks: LineChartTick[] = Array.from({ length: tickCount + 1 }, (_, t) => {
136
+ const value = (max / tickCount) * t
137
+ return { value, y: y(value) }
138
+ })
139
+
140
+ const xLabels: LineChartXLabel[] = labels
141
+ .map((label, index) => ({ index, label, x: x(index) }))
142
+ .filter(({ index }) => index % labelEvery === 0 || index === n - 1)
143
+
144
+ const paths: LineChartPath[] = series.map((s, si) => {
145
+ const points = s.data.map((v, i): [number, number] => [x(i), y(v)])
146
+ const line = pathFrom(points)
147
+ const first = points.length ? points[0][0] : x(0)
148
+ const last = points.length ? points[points.length - 1][0] : x(0)
149
+ const base = y(0).toFixed(1)
150
+ const area = `${line} L ${last.toFixed(1)} ${base} L ${first.toFixed(1)} ${base} Z`
151
+ return { id: s.id, color: s.color ?? lineChartColor(si), line, area }
152
+ })
153
+
154
+ const colW = n > 1 ? plotW / (n - 1) : plotW
155
+ const columns: LineChartColumn[] = labels.map((_, index) => ({
156
+ index,
157
+ x: x(index) - colW / 2,
158
+ width: colW,
159
+ }))
160
+
161
+ return { width, height, padding, max, ticks, xLabels, paths, columns, x, y, plotTop, plotBottom }
162
+ }
163
+
164
+ /** Where a tooltip for `index` sits horizontally, as a percent of the chart width. */
165
+ export function tooltipLeftPercent(geometry: LineChartGeometry, index: number): number {
166
+ return (geometry.x(index) / geometry.width) * 100
167
+ }
168
+
169
+ /**
170
+ * The category index a keyboard key moves to from `current` (null = nothing
171
+ * active yet). Returns `current` unchanged for keys the chart doesn't handle.
172
+ */
173
+ export function nextLineChartIndex(current: number | null, key: string, count: number): number | null {
174
+ if (count === 0) return null
175
+ const last = count - 1
176
+ switch (key) {
177
+ case 'ArrowRight':
178
+ return current == null ? 0 : Math.min(current + 1, last)
179
+ case 'ArrowLeft':
180
+ return current == null ? last : Math.max(current - 1, 0)
181
+ case 'Home':
182
+ return 0
183
+ case 'End':
184
+ return last
185
+ default:
186
+ return current
187
+ }
188
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ createPageHeader,
3
+ type PageHeaderTitleLevel,
4
+ type PageHeaderProps,
5
+ type PageHeaderAPI,
6
+ } from './page-header.js'
7
+
8
+ export {
9
+ pageHeaderClass,
10
+ pageHeaderTextClass,
11
+ pageHeaderKickerClass,
12
+ pageHeaderTitleClass,
13
+ pageHeaderDescriptionClass,
14
+ pageHeaderActionsClass,
15
+ } from './page-header.styles.js'
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Root: text cluster on the leading edge, actions on the trailing edge; wraps
3
+ * on narrow screens. Carries no outer margin — the page layout spaces it.
4
+ */
5
+ export const pageHeaderClass = 'flex flex-wrap items-start justify-between gap-4'
6
+
7
+ /** Text cluster (kicker, title, description). */
8
+ export const pageHeaderTextClass = 'flex min-w-0 flex-1 flex-col gap-1'
9
+
10
+ /** Small uppercase eyebrow above the title. */
11
+ export const pageHeaderKickerClass =
12
+ 'break-words text-[11px] uppercase tracking-wider text-muted-foreground'
13
+
14
+ /** Title — app-sized, not a marketing display size. */
15
+ export const pageHeaderTitleClass = 'break-words text-xl font-semibold text-foreground'
16
+
17
+ /** Supporting copy under the title, capped at a readable measure. */
18
+ export const pageHeaderDescriptionClass = 'max-w-prose text-sm text-muted-foreground'
19
+
20
+ /** Trailing actions row. */
21
+ export const pageHeaderActionsClass = 'flex flex-none flex-wrap items-center gap-2'
@@ -0,0 +1,27 @@
1
+ /** Heading element used for the page title. */
2
+ export type PageHeaderTitleLevel = 'h1' | 'h2' | 'h3'
3
+
4
+ export interface PageHeaderProps {
5
+ /** Whether an actions slot will be rendered. */
6
+ hasActions?: boolean
7
+ }
8
+
9
+ export interface PageHeaderAPI {
10
+ /** Data attributes for styling hooks. */
11
+ dataAttributes: Record<string, string>
12
+ }
13
+
14
+ /**
15
+ * Build the framework-agnostic data attributes for a PageHeader — the heading
16
+ * cluster at the top of an application page (kicker, title, description and a
17
+ * trailing actions row). Unlike SectionHead (a centred marketing heading) it is
18
+ * sized for dense app chrome and never centres.
19
+ */
20
+ export function createPageHeader(props: PageHeaderProps = {}): PageHeaderAPI {
21
+ return {
22
+ dataAttributes: {
23
+ 'data-slot': 'page-header',
24
+ 'data-has-actions': props.hasActions ? 'true' : 'false',
25
+ },
26
+ }
27
+ }
@@ -58,7 +58,7 @@ export const badgeItemVariants = cva({
58
58
  })
59
59
 
60
60
  export const progressBarVariants = cva({
61
- base: 'h-2 w-full overflow-hidden rounded-full bg-secondary',
61
+ base: 'w-full overflow-hidden rounded-full bg-secondary',
62
62
  variants: {
63
63
  size: {
64
64
  sm: 'h-1',
@@ -8,6 +8,8 @@ export {
8
8
  export {
9
9
  selectTriggerVariants,
10
10
  selectContentVariants,
11
+ selectRootClass,
12
+ selectValueClass,
11
13
  selectItemVariants,
12
14
  selectTokens,
13
15
  } from './select.styles.js'
@@ -12,7 +12,7 @@ export const selectTokens: TokenContract = {
12
12
  }
13
13
 
14
14
  export const selectTriggerVariants = cva({
15
- base: 'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
15
+ base: 'flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
16
16
  variants: {
17
17
  size: {
18
18
  sm: 'h-8 text-xs',
@@ -26,10 +26,10 @@ export const selectTriggerVariants = cva({
26
26
  })
27
27
 
28
28
  export const selectContentVariants = cva({
29
- base: 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
29
+ base: 'absolute left-0 top-full z-50 mt-1 max-h-96 w-full min-w-[8rem] overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
30
30
  variants: {
31
31
  position: {
32
- popper: 'translate-y-1',
32
+ popper: '',
33
33
  'item-aligned': '',
34
34
  },
35
35
  },
@@ -50,3 +50,9 @@ export const selectItemVariants = cva({
50
50
  selected: 'false',
51
51
  },
52
52
  })
53
+
54
+ /** Positioning context: the listbox floats under the trigger. */
55
+ export const selectRootClass = 'relative'
56
+
57
+ /** The selected label inside the trigger; truncates, dims the placeholder. */
58
+ export const selectValueClass = 'truncate text-left data-[placeholder]:text-muted-foreground'
@@ -14,6 +14,12 @@ export interface SelectProps {
14
14
  disabled?: boolean
15
15
  open?: boolean
16
16
  name?: string
17
+ /**
18
+ * Element ids to link trigger and listbox. Adapters should pass ids that
19
+ * are stable across renders (e.g. React `useId`); when omitted, fresh ids
20
+ * are generated on every call.
21
+ */
22
+ ids?: { trigger: string; content: string }
17
23
  }
18
24
 
19
25
  export interface SelectAPI {
@@ -63,14 +69,16 @@ export function createSelect(props: SelectProps = {}): SelectAPI {
63
69
  const isInteractive = !disabled
64
70
  const selectedOption = options.find((o) => o.value === value)
65
71
 
66
- const triggerId = generateId('select-trigger')
67
- const contentId = generateId('select-content')
72
+ const triggerId = props.ids?.trigger ?? generateId('select-trigger')
73
+ const contentId = props.ids?.content ?? generateId('select-content')
68
74
 
75
+ // No aria-labelledby on the trigger: pointing a combobox at itself would
76
+ // override a caller's aria-label / <label for> and leave the accessible
77
+ // name as the displayed value.
69
78
  const triggerAriaProps: Partial<AccessibilityProps> = {
70
79
  role: 'combobox',
71
80
  'aria-expanded': open,
72
81
  'aria-controls': contentId,
73
- 'aria-labelledby': triggerId,
74
82
  }
75
83
  if (disabled) {
76
84
  triggerAriaProps['aria-disabled'] = true
@@ -8,5 +8,7 @@ export {
8
8
  export {
9
9
  separatorVariants,
10
10
  separatorLineClass,
11
+ separatorSubtleLineClass,
11
12
  separatorLabelClass,
13
+ type SeparatorTone,
12
14
  } from './separator.styles.js'
@@ -12,8 +12,16 @@ export const separatorVariants = cva({
12
12
  base: '',
13
13
  variants: {
14
14
  orientation: {
15
- horizontal: 'h-px w-full bg-border',
16
- vertical: 'w-px h-full bg-border',
15
+ horizontal: 'h-px w-full',
16
+ vertical: 'w-px h-full',
17
+ },
18
+ // Line color (issue #485). `default` is the standard border token;
19
+ // `subtle` uses the hairline `border-subtle` token for a lighter rule.
20
+ // Keeping the color here (not in `orientation`) keeps every existing call
21
+ // pixel-identical while adding the subtle option.
22
+ tone: {
23
+ default: 'bg-border',
24
+ subtle: 'bg-border-subtle',
17
25
  },
18
26
  labeled: {
19
27
  true: 'flex items-center',
@@ -22,13 +30,20 @@ export const separatorVariants = cva({
22
30
  },
23
31
  defaultVariants: {
24
32
  orientation: 'horizontal',
33
+ tone: 'default',
25
34
  labeled: 'false',
26
35
  },
27
36
  })
28
37
 
38
+ /** Line tone for the separator rule and labeled-divider flanks. */
39
+ export type SeparatorTone = 'default' | 'subtle'
40
+
29
41
  /** Class for each flanking line in the labeled-divider variant. */
30
42
  export const separatorLineClass = 'h-px flex-1 bg-border'
31
43
 
44
+ /** Hairline variant of {@link separatorLineClass} (issue #485). */
45
+ export const separatorSubtleLineClass = 'h-px flex-1 bg-border-subtle'
46
+
32
47
  /** Class for the centered label in the labeled-divider variant. */
33
48
  export const separatorLabelClass =
34
49
  'px-3 text-xs uppercase tracking-wide text-muted-foreground'
package/dist/shared/cn.ts CHANGED
@@ -3,9 +3,12 @@
3
3
  * Handles conditional classes, arrays, and falsy values.
4
4
  * No external dependencies (no clsx, no tailwind-merge).
5
5
  *
6
- * For Tailwind class conflict resolution (e.g., 'p-2 p-4' → 'p-4'),
7
- * consumers can use @refraction-ui/tailwind-config which provides
8
- * a tw-merge-aware variant of this function.
6
+ * It does NOT resolve Tailwind conflicts: `cn('p-2', 'p-4')` keeps both and
7
+ * the stylesheet order decides. Components therefore keep size-owned
8
+ * utilities (height, text size, padding) out of their cva `base` and only in
9
+ * the matching variant, so a variant prop always wins; a consumer `className`
10
+ * that must beat a component utility can use Tailwind's `!` important
11
+ * modifier (e.g. `!h-7`).
9
12
  */
10
13
 
11
14
  type ClassValue = string | number | boolean | undefined | null | ClassValue[]
@@ -45,6 +45,8 @@ export { slideViewerProgressBarVariants as progressBarVariants }
45
45
  export const slideTypeBadgeVariants = cva({
46
46
  base: 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
47
47
  variants: {
48
+ // Slide-type accents are a content-category palette (lesson/quiz/exercise…),
49
+ // not status tones — deliberately not routed through the theme tokens.
48
50
  type: {
49
51
  lesson: 'bg-blue-100 text-blue-800',
50
52
  quiz: 'bg-purple-100 text-purple-800',
@@ -14,6 +14,8 @@ export const stickyNoteVariants = cva({
14
14
  'min-w-[160px] min-h-[120px]',
15
15
  ].join(' '),
16
16
  variants: {
17
+ // Named paper colors are product semantics (physical sticky notes),
18
+ // not status tones — deliberately not routed through the theme tokens.
17
19
  color: {
18
20
  yellow: 'bg-yellow-100 text-yellow-900',
19
21
  pink: 'bg-pink-100 text-pink-900',
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  createTabs,
3
+ getNextTabIndex,
3
4
  type TabsProps,
4
5
  type TabsAPI,
5
6
  type TabsState,
package/dist/tabs/tabs.ts CHANGED
@@ -131,3 +131,32 @@ export function createTabs(props: TabsProps = {}): TabsAPI {
131
131
  idPrefix,
132
132
  }
133
133
  }
134
+
135
+ /**
136
+ * Roving-focus target for a key press inside a tab list (WAI-ARIA Tabs
137
+ * pattern). Given the index of the focused tab among the ENABLED tabs, returns
138
+ * the index to move to, or `null` when the key is not a navigation key for this
139
+ * orientation. Arrow keys wrap; Home/End jump to the ends.
140
+ */
141
+ export function getNextTabIndex(
142
+ current: number,
143
+ key: string,
144
+ count: number,
145
+ orientation: 'horizontal' | 'vertical' = 'horizontal',
146
+ ): number | null {
147
+ if (count === 0) return null
148
+ const prev = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
149
+ const next = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
150
+ switch (key) {
151
+ case next:
152
+ return current < 0 ? 0 : (current + 1) % count
153
+ case prev:
154
+ return current < 0 ? count - 1 : (current - 1 + count) % count
155
+ case 'Home':
156
+ return 0
157
+ case 'End':
158
+ return count - 1
159
+ default:
160
+ return null
161
+ }
162
+ }
@@ -1,11 +1,11 @@
1
1
  import { cva } from '../shared/index.ts'
2
2
 
3
3
  export const textareaVariants = cva({
4
- base: 'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm 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-2 shadow-sm 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: 'min-h-[40px] text-xs',
8
- default: 'min-h-[60px]',
8
+ default: 'min-h-[60px] text-sm',
9
9
  lg: 'min-h-[80px] text-base',
10
10
  },
11
11
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@refraction-ui/astro-accordion": "workspace:*",
25
+ "@refraction-ui/astro-action-list": "workspace:*",
25
26
  "@refraction-ui/astro-ai": "workspace:*",
26
27
  "@refraction-ui/astro-analytics": "workspace:*",
27
28
  "@refraction-ui/astro-animated-text": "workspace:*",
@@ -71,6 +72,7 @@
71
72
  "@refraction-ui/astro-floating-reactions": "workspace:*",
72
73
  "@refraction-ui/astro-flow-editor": "workspace:*",
73
74
  "@refraction-ui/astro-footer": "workspace:*",
75
+ "@refraction-ui/astro-funnel-chart": "workspace:*",
74
76
  "@refraction-ui/astro-graph-view": "workspace:*",
75
77
  "@refraction-ui/astro-icon-system": "workspace:*",
76
78
  "@refraction-ui/astro-infinite-canvas": "workspace:*",
@@ -81,6 +83,7 @@
81
83
  "@refraction-ui/astro-kanban-board": "workspace:*",
82
84
  "@refraction-ui/astro-keyboard-shortcut": "workspace:*",
83
85
  "@refraction-ui/astro-language-selector": "workspace:*",
86
+ "@refraction-ui/astro-line-chart": "workspace:*",
84
87
  "@refraction-ui/astro-link-card": "workspace:*",
85
88
  "@refraction-ui/astro-live-captions": "workspace:*",
86
89
  "@refraction-ui/astro-live-cursors": "workspace:*",
@@ -96,6 +99,7 @@
96
99
  "@refraction-ui/astro-navbar": "workspace:*",
97
100
  "@refraction-ui/astro-numbered-steps": "workspace:*",
98
101
  "@refraction-ui/astro-otp-input": "workspace:*",
102
+ "@refraction-ui/astro-page-header": "workspace:*",
99
103
  "@refraction-ui/astro-pagination": "workspace:*",
100
104
  "@refraction-ui/astro-password-input": "workspace:*",
101
105
  "@refraction-ui/astro-payment": "workspace:*",