@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.
- package/dist/action-list/action-list.styles.ts +33 -0
- package/dist/action-list/action-list.ts +31 -0
- package/dist/action-list/index.ts +16 -0
- package/dist/astro-action-list/ActionList.astro +12 -0
- package/dist/astro-action-list/ActionListItem.astro +62 -0
- package/dist/astro-action-list/index.ts +1 -0
- package/dist/astro-callout/Callout.astro +4 -1
- package/dist/astro-card/Card.astro +3 -2
- package/dist/astro-card/CardTitle.astro +6 -4
- package/dist/astro-funnel-chart/FunnelChart.astro +64 -0
- package/dist/astro-funnel-chart/index.ts +1 -0
- package/dist/astro-line-chart/LineChart.astro +82 -0
- package/dist/astro-line-chart/index.ts +1 -0
- package/dist/astro-page-header/PageHeader.astro +46 -0
- package/dist/astro-page-header/index.ts +1 -0
- package/dist/badge/badge.styles.ts +6 -1
- package/dist/badge/badge.ts +24 -2
- package/dist/button/button.styles.ts +10 -4
- package/dist/button/button.ts +4 -1
- package/dist/callout/callout.ts +12 -3
- package/dist/card/card.styles.ts +10 -1
- package/dist/card/card.ts +14 -0
- package/dist/card/index.ts +3 -0
- package/dist/diff-viewer/diff-viewer.styles.ts +3 -0
- package/dist/funnel-chart/funnel-chart.styles.ts +16 -0
- package/dist/funnel-chart/funnel-chart.ts +68 -0
- package/dist/funnel-chart/index.ts +10 -0
- package/dist/index.ts +4 -0
- package/dist/input/input.styles.ts +2 -2
- package/dist/line-chart/index.ts +19 -0
- package/dist/line-chart/line-chart.styles.ts +16 -0
- package/dist/line-chart/line-chart.ts +188 -0
- package/dist/page-header/index.ts +15 -0
- package/dist/page-header/page-header.styles.ts +21 -0
- package/dist/page-header/page-header.ts +27 -0
- package/dist/progress-display/progress-display.styles.ts +1 -1
- package/dist/select/index.ts +2 -0
- package/dist/select/select.styles.ts +9 -3
- package/dist/select/select.ts +11 -3
- package/dist/separator/index.ts +2 -0
- package/dist/separator/separator.styles.ts +17 -2
- package/dist/shared/cn.ts +6 -3
- package/dist/slide-viewer/slide-viewer.styles.ts +2 -0
- package/dist/sticky-note/sticky-note.styles.ts +2 -0
- package/dist/tabs/index.ts +1 -0
- package/dist/tabs/tabs.ts +29 -0
- package/dist/textarea/textarea.styles.ts +2 -2
- 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
|
+
}
|
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
|
|
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
|
+
}
|
package/dist/select/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const selectTokens: TokenContract = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const selectTriggerVariants = cva({
|
|
15
|
-
base: 'flex
|
|
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: '
|
|
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: '
|
|
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'
|
package/dist/select/select.ts
CHANGED
|
@@ -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
|
package/dist/separator/index.ts
CHANGED
|
@@ -12,8 +12,16 @@ export const separatorVariants = cva({
|
|
|
12
12
|
base: '',
|
|
13
13
|
variants: {
|
|
14
14
|
orientation: {
|
|
15
|
-
horizontal: 'h-px w-full
|
|
16
|
-
vertical: 'w-px h-full
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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',
|
package/dist/tabs/index.ts
CHANGED
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
|
|
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.
|
|
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:*",
|