@refraction-ui/astro 0.4.13 → 0.5.1

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.
@@ -0,0 +1,159 @@
1
+ ---
2
+ import {
3
+ createVoicePill,
4
+ voicePillAvatarStyles,
5
+ voicePillAvatarWrapStyles,
6
+ voicePillLabelStyles,
7
+ voicePillMuteButtonStyles,
8
+ voicePillPositionVariants,
9
+ voicePillPulseRingStyles,
10
+ voicePillRootStyles,
11
+ voicePillSpeakerStyles,
12
+ voicePillSubStyles,
13
+ voicePillTextStyles,
14
+ type VoicePillPosition,
15
+ type VoicePillSpeaker,
16
+ } from '../voice-pill/index.ts'
17
+ import { cn } from '../shared/index.ts'
18
+
19
+ interface Props extends astroHTML.JSX.HTMLAttributes {
20
+ speaker?: VoicePillSpeaker
21
+ label: string
22
+ sub?: string
23
+ subtitle?: string
24
+ intensity?: number
25
+ muted?: boolean
26
+ position?: VoicePillPosition
27
+ showMuteButton?: boolean
28
+ }
29
+
30
+ const {
31
+ speaker,
32
+ label,
33
+ sub,
34
+ subtitle,
35
+ intensity,
36
+ muted,
37
+ position,
38
+ showMuteButton = false,
39
+ class: className,
40
+ style,
41
+ ...props
42
+ } = Astro.props
43
+
44
+ const api = createVoicePill({
45
+ speaker,
46
+ label,
47
+ sub: sub ?? subtitle,
48
+ intensity,
49
+ muted,
50
+ position,
51
+ })
52
+
53
+ const mergedStyle = mergeStyles(style, api.style)
54
+
55
+ const pulseStyle = {
56
+ animationDuration: 'var(--rfr-voice-pill-pulse-duration)',
57
+ transform: 'scale(var(--rfr-voice-pill-ring-scale))',
58
+ }
59
+
60
+ const delayedPulseStyle = {
61
+ ...pulseStyle,
62
+ animationDelay: 'var(--rfr-voice-pill-pulse-delay)',
63
+ }
64
+
65
+ function mergeStyles(
66
+ base: unknown,
67
+ extra: Record<string, string | number | undefined>,
68
+ ): string {
69
+ const baseStyle = styleToString(base)
70
+ const extraStyle = Object.entries(extra)
71
+ .filter(([, value]) => value !== undefined)
72
+ .map(([key, value]) => `${toKebabCase(key)}:${value}`)
73
+ .join(';')
74
+
75
+ return [baseStyle, extraStyle].filter(Boolean).join(';')
76
+ }
77
+
78
+ function styleToString(styleValue: unknown): string {
79
+ if (typeof styleValue === 'string') return styleValue
80
+ if (!styleValue || typeof styleValue !== 'object') return ''
81
+
82
+ return Object.entries(styleValue as Record<string, string | number | undefined>)
83
+ .filter(([, value]) => value !== undefined)
84
+ .map(([key, value]) => `${toKebabCase(key)}:${value}`)
85
+ .join(';')
86
+ }
87
+
88
+ function toKebabCase(value: string): string {
89
+ return value.startsWith('--')
90
+ ? value
91
+ : value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
92
+ }
93
+ ---
94
+
95
+ <div
96
+ class={cn(
97
+ voicePillPositionVariants({ position: api.position }),
98
+ voicePillRootStyles,
99
+ voicePillSpeakerStyles,
100
+ className,
101
+ )}
102
+ style={mergedStyle}
103
+ {...api.ariaProps}
104
+ {...api.dataAttributes}
105
+ {...props}
106
+ >
107
+ <span class={voicePillAvatarWrapStyles} aria-hidden="true">
108
+ {api.visualIntensity > 0 && <span class={voicePillPulseRingStyles} style={pulseStyle}></span>}
109
+ {
110
+ api.visualIntensity > 0 && (
111
+ <span class={voicePillPulseRingStyles} style={delayedPulseStyle}></span>
112
+ )
113
+ }
114
+ <span class={voicePillAvatarStyles}>
115
+ <slot name="avatar">{api.initials}</slot>
116
+ </span>
117
+ </span>
118
+
119
+ <span class={voicePillTextStyles}>
120
+ <span class={voicePillLabelStyles}>{api.label}</span>
121
+ {api.sub && <span class={voicePillSubStyles}>{api.sub}</span>}
122
+ </span>
123
+
124
+ {
125
+ showMuteButton && (
126
+ <button
127
+ type="button"
128
+ class={voicePillMuteButtonStyles}
129
+ {...api.toggleMuteAriaProps}
130
+ >
131
+ <svg
132
+ aria-hidden="true"
133
+ class="h-4 w-4"
134
+ fill="none"
135
+ stroke="currentColor"
136
+ stroke-linecap="round"
137
+ stroke-linejoin="round"
138
+ stroke-width="2"
139
+ viewBox="0 0 24 24"
140
+ >
141
+ <path d="M4 9v6h4l5 4V5L8 9H4Z"></path>
142
+ {
143
+ api.muted ? (
144
+ <>
145
+ <path d="m19 9-4 4"></path>
146
+ <path d="m15 9 4 4"></path>
147
+ </>
148
+ ) : (
149
+ <>
150
+ <path d="M16 8.5a4 4 0 0 1 0 7"></path>
151
+ <path d="M18.5 6a7 7 0 0 1 0 12"></path>
152
+ </>
153
+ )
154
+ }
155
+ </svg>
156
+ </button>
157
+ )
158
+ }
159
+ </div>
@@ -0,0 +1,27 @@
1
+ export { default as VoicePill } from './VoicePill.astro'
2
+ export {
3
+ createVoicePill,
4
+ clampVoicePillIntensity,
5
+ getVoicePillAriaLabel,
6
+ getVoicePillInitials,
7
+ getVoicePillPosition,
8
+ getVoicePillPulseStyle,
9
+ getVoicePillSpeakerKey,
10
+ getVoicePillSpeakerLabel,
11
+ type VoicePillAPI,
12
+ type VoicePillPosition,
13
+ type VoicePillProps as CoreVoicePillProps,
14
+ type VoicePillSpeaker,
15
+ type VoicePillStyleVars,
16
+ voicePillAvatarStyles,
17
+ voicePillAvatarWrapStyles,
18
+ voicePillLabelStyles,
19
+ voicePillMuteButtonStyles,
20
+ voicePillPositionVariants,
21
+ voicePillPulseRingStyles,
22
+ voicePillRootStyles,
23
+ voicePillSpeakerStyles,
24
+ voicePillSubStyles,
25
+ voicePillTextStyles,
26
+ voicePillTokens,
27
+ } from '../voice-pill/index.ts'
@@ -0,0 +1,279 @@
1
+ ---
2
+ import {
3
+ createWaveform,
4
+ toCssDimension,
5
+ waveformCanvasVariants,
6
+ waveformVariants,
7
+ type WaveformSampleInput,
8
+ type WaveformVariant,
9
+ } from '../waveform/index.ts'
10
+ import { cn } from '../shared/index.ts'
11
+
12
+ interface Props extends Omit<astroHTML.JSX.HTMLAttributes, 'color' | 'height' | 'width'> {
13
+ samples?: WaveformSampleInput
14
+ intensity?: number
15
+ amplitude?: number
16
+ variant?: WaveformVariant
17
+ height?: number | string
18
+ width?: number | string
19
+ barCount?: number
20
+ smoothing?: number
21
+ color?: string
22
+ paused?: boolean
23
+ }
24
+
25
+ const {
26
+ samples,
27
+ intensity,
28
+ amplitude,
29
+ variant,
30
+ height,
31
+ width,
32
+ barCount,
33
+ smoothing,
34
+ color,
35
+ paused,
36
+ class: className,
37
+ style,
38
+ ...props
39
+ } = Astro.props
40
+
41
+ const api = createWaveform({
42
+ samples,
43
+ intensity,
44
+ amplitude,
45
+ variant,
46
+ height,
47
+ width,
48
+ barCount,
49
+ smoothing,
50
+ color,
51
+ paused,
52
+ })
53
+
54
+ const styleObject = style && typeof style === 'object'
55
+ ? (style as Record<string, string | number | undefined>)
56
+ : {}
57
+
58
+ const rootStyle = mergeStyles(style, {
59
+ width: toCssDimension(width) ?? styleObject.width ?? '100%',
60
+ height:
61
+ toCssDimension(height) ??
62
+ styleObject.height ??
63
+ `${api.config.height}px`,
64
+ '--waveform-color': api.config.color,
65
+ })
66
+
67
+ const waveformData = {
68
+ samples: Array.from(api.samples),
69
+ variant: api.config.variant,
70
+ intensity: api.config.intensity,
71
+ amplitude: api.config.amplitude,
72
+ barCount: api.config.barCount,
73
+ color: api.config.color,
74
+ }
75
+
76
+ function mergeStyles(
77
+ base: unknown,
78
+ extra: Record<string, string | number | undefined>,
79
+ ): string {
80
+ const baseStyle = styleToString(base)
81
+ const extraStyle = Object.entries(extra)
82
+ .filter(([, value]) => value !== undefined)
83
+ .map(([key, value]) => `${toKebabCase(key)}:${value}`)
84
+ .join(';')
85
+
86
+ return [baseStyle, extraStyle].filter(Boolean).join(';')
87
+ }
88
+
89
+ function styleToString(styleValue: unknown): string {
90
+ if (typeof styleValue === 'string') return styleValue
91
+ if (!styleValue || typeof styleValue !== 'object') return ''
92
+
93
+ return Object.entries(styleValue as Record<string, string | number | undefined>)
94
+ .filter(([, value]) => value !== undefined)
95
+ .map(([key, value]) => `${toKebabCase(key)}:${value}`)
96
+ .join(';')
97
+ }
98
+
99
+ function toKebabCase(value: string): string {
100
+ return value.startsWith('--')
101
+ ? value
102
+ : value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
103
+ }
104
+ ---
105
+
106
+ <div
107
+ class={cn(waveformVariants({ variant: api.config.variant }), className)}
108
+ style={rootStyle}
109
+ {...api.ariaProps}
110
+ {...api.dataAttributes}
111
+ {...props}
112
+ >
113
+ <canvas
114
+ class={waveformCanvasVariants()}
115
+ aria-hidden="true"
116
+ data-waveform-canvas=""
117
+ data-waveform={JSON.stringify(waveformData)}
118
+ ></canvas>
119
+ </div>
120
+
121
+ <script>
122
+ const normalizeSamples = (samples, count) => {
123
+ const values = Array.isArray(samples) ? samples : []
124
+ const targetCount = Math.max(1, Math.min(1024, Math.round(Number(count) || 48)))
125
+
126
+ if (values.length === 0) return Array.from({ length: targetCount }, () => 0)
127
+ if (values.length === targetCount) return values.map((value) => Number(value) || 0)
128
+
129
+ return Array.from({ length: targetCount }, (_, index) => {
130
+ const start = (index * values.length) / targetCount
131
+ const end = ((index + 1) * values.length) / targetCount
132
+ const firstIndex = Math.floor(start)
133
+ const lastIndex = Math.max(firstIndex + 1, Math.ceil(end))
134
+ let total = 0
135
+ let count = 0
136
+
137
+ for (let sampleIndex = firstIndex; sampleIndex < lastIndex && sampleIndex < values.length; sampleIndex += 1) {
138
+ total += Number(values[sampleIndex]) || 0
139
+ count += 1
140
+ }
141
+
142
+ return count > 0 ? total / count : Number(values[Math.min(firstIndex, values.length - 1)]) || 0
143
+ })
144
+ }
145
+
146
+ const scaleSamples = (samples, intensity, amplitude) => {
147
+ const amount = Math.max(0, Math.min(1, Number(intensity) || 0))
148
+ const visualHeight = Math.max(0, Math.min(1, Number(amplitude) || 0))
149
+
150
+ return samples.map((sample) => Math.max(-1, Math.min(1, sample * amount * visualHeight)))
151
+ }
152
+
153
+ const resolveColor = (element, color) => {
154
+ const match = String(color).match(/^var\((--[^),\s]+)(?:,[^)]+)?\)$/)
155
+
156
+ if (!match || typeof window.getComputedStyle !== 'function') return color
157
+
158
+ return window.getComputedStyle(element).getPropertyValue(match[1]).trim() || color
159
+ }
160
+
161
+ const prepareCanvas = (canvas) => {
162
+ const root = canvas.parentElement
163
+ const rect = root?.getBoundingClientRect()
164
+ const width = Math.max(1, Math.round(rect?.width || root?.clientWidth || 300))
165
+ const height = Math.max(1, Math.round(rect?.height || root?.clientHeight || 80))
166
+ const ratio = window.devicePixelRatio || 1
167
+
168
+ canvas.width = Math.round(width * ratio)
169
+ canvas.height = Math.round(height * ratio)
170
+ const context = canvas.getContext('2d')
171
+
172
+ if (!context) return null
173
+
174
+ canvas.style.width = '100%'
175
+ canvas.style.height = '100%'
176
+ context.setTransform(ratio, 0, 0, ratio, 0, 0)
177
+ context.clearRect(0, 0, width, height)
178
+
179
+ return { context, width, height }
180
+ }
181
+
182
+ const drawBars = (context, samples, width, height, color) => {
183
+ const gap = samples.length > 96 ? 1 : 2
184
+ const barWidth = Math.max(1, (width - gap * Math.max(0, samples.length - 1)) / samples.length)
185
+ const centerY = height / 2
186
+
187
+ context.fillStyle = color
188
+
189
+ samples.forEach((sample, index) => {
190
+ const magnitude = Math.max(0.06, Math.abs(sample))
191
+ const barHeight = Math.max(1, magnitude * height)
192
+ const x = index * (barWidth + gap)
193
+ const y = centerY - barHeight / 2
194
+
195
+ context.fillRect(x, y, barWidth, barHeight)
196
+ })
197
+ }
198
+
199
+ const drawLine = (context, samples, width, height, color) => {
200
+ const centerY = height / 2
201
+
202
+ context.strokeStyle = color
203
+ context.lineWidth = 2
204
+ context.lineCap = 'round'
205
+ context.lineJoin = 'round'
206
+ context.beginPath()
207
+
208
+ samples.forEach((sample, index) => {
209
+ const x = samples.length <= 1 ? width / 2 : (index / (samples.length - 1)) * width
210
+ const y = centerY - sample * centerY * 0.9
211
+
212
+ if (index === 0) context.moveTo(x, y)
213
+ else context.lineTo(x, y)
214
+ })
215
+
216
+ context.stroke()
217
+ }
218
+
219
+ const drawRings = (context, samples, width, height, color) => {
220
+ const peak = samples.reduce((current, sample) => Math.max(current, Math.abs(sample)), 0)
221
+ const centerX = width / 2
222
+ const centerY = height / 2
223
+ const maxRadius = Math.max(1, Math.min(width, height) / 2 - 2)
224
+
225
+ context.strokeStyle = color
226
+ context.lineWidth = Math.max(2, maxRadius * 0.08)
227
+
228
+ ;[0.38, 0.64, 0.9].forEach((scale, index) => {
229
+ context.globalAlpha = 0.35 + peak * (0.2 + index * 0.12)
230
+ context.beginPath()
231
+ context.arc(centerX, centerY, Math.max(1, maxRadius * scale * (0.85 + peak * 0.15)), 0, Math.PI * 2)
232
+ context.stroke()
233
+ })
234
+
235
+ context.globalAlpha = 1
236
+ }
237
+
238
+ const drawWaveform = (canvas) => {
239
+ if (!(canvas instanceof HTMLCanvasElement)) return
240
+
241
+ let config
242
+
243
+ try {
244
+ config = JSON.parse(canvas.dataset.waveform || '{}')
245
+ } catch {
246
+ config = {}
247
+ }
248
+
249
+ const prepared = prepareCanvas(canvas)
250
+ if (!prepared) return
251
+
252
+ const { context, width, height } = prepared
253
+ const samples = scaleSamples(
254
+ normalizeSamples(config.samples, config.barCount),
255
+ config.intensity,
256
+ config.amplitude,
257
+ )
258
+ const color = resolveColor(canvas.parentElement || canvas, config.color || 'currentColor')
259
+
260
+ if (config.variant === 'line') {
261
+ drawLine(context, samples, width, height, color)
262
+ } else if (config.variant === 'rings') {
263
+ drawRings(context, samples, width, height, color)
264
+ } else {
265
+ drawBars(context, samples, width, height, color)
266
+ }
267
+ }
268
+
269
+ const canvases = document.querySelectorAll('canvas[data-waveform-canvas][data-waveform]')
270
+
271
+ canvases.forEach((canvas) => {
272
+ drawWaveform(canvas)
273
+
274
+ if (typeof ResizeObserver === 'function' && canvas.parentElement) {
275
+ const observer = new ResizeObserver(() => drawWaveform(canvas))
276
+ observer.observe(canvas.parentElement)
277
+ }
278
+ })
279
+ </script>
@@ -0,0 +1,29 @@
1
+ export { default as Waveform } from './Waveform.astro'
2
+ export {
3
+ createIntensitySamples,
4
+ createSilentSamples,
5
+ createWaveform,
6
+ drawWaveform,
7
+ getWaveformPeak,
8
+ isWaveformSampleInput,
9
+ normalizeBarCount,
10
+ normalizeIntensity,
11
+ normalizeSmoothing,
12
+ normalizeWaveformConfig,
13
+ normalizeWaveformSamples,
14
+ prepareWaveformCanvas,
15
+ resampleWaveformSamples,
16
+ scaleWaveformSamples,
17
+ smoothWaveformSamples,
18
+ toCssDimension,
19
+ type NormalizedWaveformConfig,
20
+ type WaveformAPI,
21
+ type WaveformCanvasSize,
22
+ type WaveformProps as CoreWaveformProps,
23
+ type WaveformRenderOptions,
24
+ type WaveformSampleInput,
25
+ type WaveformSource,
26
+ type WaveformVariant,
27
+ waveformCanvasVariants,
28
+ waveformVariants,
29
+ } from '../waveform/index.ts'
package/dist/index.ts CHANGED
@@ -61,6 +61,8 @@ export * from './astro-toast/index.ts';
61
61
  export * from './astro-tooltip/index.ts';
62
62
  export * from './astro-version-selector/index.ts';
63
63
  export * from './astro-video-player/index.ts';
64
+ export * from './astro-voice-pill/index.ts';
65
+ export * from './astro-waveform/index.ts';
64
66
  export * from './astro-table-of-contents/index.ts';
65
67
  export * from './astro-carousel/index.ts';
66
68
  export * from './astro-slider/index.ts';
@@ -26,7 +26,7 @@ export const slideViewerVariants = cva({
26
26
  },
27
27
  })
28
28
 
29
- export const progressBarVariants = cva({
29
+ const slideViewerProgressBarVariants = cva({
30
30
  base: 'h-1 w-full bg-muted rounded-full overflow-hidden',
31
31
  variants: {
32
32
  position: {
@@ -40,6 +40,8 @@ export const progressBarVariants = cva({
40
40
  },
41
41
  })
42
42
 
43
+ export { slideViewerProgressBarVariants as progressBarVariants }
44
+
43
45
  export const slideTypeBadgeVariants = cva({
44
46
  base: 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
45
47
  variants: {
@@ -24,7 +24,7 @@ export interface StatusAPI {
24
24
  color: string
25
25
  }
26
26
 
27
- const STATUS_COLORS: Record<StatusType, string> = {
27
+ const STATUS_INDICATOR_COLORS: Record<StatusType, string> = {
28
28
  success: 'green',
29
29
  error: 'red',
30
30
  warning: 'yellow',
@@ -33,7 +33,7 @@ const STATUS_COLORS: Record<StatusType, string> = {
33
33
  neutral: 'gray',
34
34
  }
35
35
 
36
- const STATUS_LABELS: Record<StatusType, string> = {
36
+ const STATUS_INDICATOR_LABELS: Record<StatusType, string> = {
37
37
  success: 'Success',
38
38
  error: 'Error',
39
39
  warning: 'Warning',
@@ -42,13 +42,16 @@ const STATUS_LABELS: Record<StatusType, string> = {
42
42
  neutral: 'Neutral',
43
43
  }
44
44
 
45
- export { STATUS_COLORS, STATUS_LABELS }
45
+ export {
46
+ STATUS_INDICATOR_COLORS as STATUS_COLORS,
47
+ STATUS_INDICATOR_LABELS as STATUS_LABELS,
48
+ }
46
49
 
47
50
  export function createStatusIndicator(props: StatusProps): StatusAPI {
48
51
  const { type, label: labelOverride, pulse: pulseOverride } = props
49
52
 
50
- const color = STATUS_COLORS[type]
51
- const label = labelOverride ?? STATUS_LABELS[type]
53
+ const color = STATUS_INDICATOR_COLORS[type]
54
+ const label = labelOverride ?? STATUS_INDICATOR_LABELS[type]
52
55
  const pulse = pulseOverride ?? (type === 'pending')
53
56
 
54
57
  const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {
@@ -14,7 +14,7 @@ export const versionSelectorVariants = cva({
14
14
  },
15
15
  })
16
16
 
17
- export const optionVariants = cva({
17
+ const versionSelectorOptionVariants = cva({
18
18
  base: 'flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-accent',
19
19
  variants: {
20
20
  selected: {
@@ -27,6 +27,8 @@ export const optionVariants = cva({
27
27
  },
28
28
  })
29
29
 
30
+ export { versionSelectorOptionVariants as optionVariants }
31
+
30
32
  export const latestBadgeVariants = cva({
31
33
  base: 'inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5',
32
34
  variants: {
@@ -0,0 +1,31 @@
1
+ export {
2
+ createVoicePill,
3
+ clampVoicePillIntensity,
4
+ getVoicePillAriaLabel,
5
+ getVoicePillInitials,
6
+ getVoicePillPosition,
7
+ getVoicePillPulseStyle,
8
+ getVoicePillSpeakerKey,
9
+ getVoicePillSpeakerLabel,
10
+ DEFAULT_VOICE_PILL_POSITION,
11
+ DEFAULT_VOICE_PILL_SPEAKER,
12
+ type VoicePillAPI,
13
+ type VoicePillPosition,
14
+ type VoicePillProps,
15
+ type VoicePillSpeaker,
16
+ type VoicePillStyleVars,
17
+ } from './voice-pill.js'
18
+
19
+ export {
20
+ voicePillAvatarStyles,
21
+ voicePillAvatarWrapStyles,
22
+ voicePillLabelStyles,
23
+ voicePillMuteButtonStyles,
24
+ voicePillPositionVariants,
25
+ voicePillPulseRingStyles,
26
+ voicePillRootStyles,
27
+ voicePillSpeakerStyles,
28
+ voicePillSubStyles,
29
+ voicePillTextStyles,
30
+ voicePillTokens,
31
+ } from './voice-pill.styles.js'
@@ -0,0 +1,78 @@
1
+ import { cva } from '../shared/index.ts'
2
+ import type { TokenContract } from '../shared/index.ts'
3
+
4
+ export const voicePillTokens: TokenContract = {
5
+ name: 'voice-pill',
6
+ tokens: {
7
+ bg: { variable: '--rfr-voice-pill-bg', fallback: 'hsl(var(--background))' },
8
+ fg: { variable: '--rfr-voice-pill-fg', fallback: 'hsl(var(--foreground))' },
9
+ border: { variable: '--rfr-voice-pill-border', fallback: 'hsl(var(--border))' },
10
+ accent: { variable: '--rfr-voice-pill-accent', fallback: 'hsl(var(--primary))' },
11
+ accentFg: {
12
+ variable: '--rfr-voice-pill-accent-foreground',
13
+ fallback: 'hsl(var(--primary-foreground))',
14
+ },
15
+ },
16
+ }
17
+
18
+ export const voicePillRootStyles =
19
+ 'inline-flex min-w-0 max-w-[min(calc(100vw-2rem),22rem)] items-center gap-3 rounded-full border border-border bg-background px-3 py-2 text-foreground shadow-lg ring-1 ring-border/50 transition-opacity data-[muted=true]:opacity-80'
20
+
21
+ export const voicePillSpeakerStyles =
22
+ '[--rfr-voice-pill-accent:hsl(var(--primary))] [--rfr-voice-pill-accent-foreground:hsl(var(--primary-foreground))] data-[speaker=user]:[--rfr-voice-pill-accent:hsl(var(--accent-foreground))] data-[speaker=user]:[--rfr-voice-pill-accent-foreground:hsl(var(--accent))] data-[muted=true]:[--rfr-voice-pill-accent:hsl(var(--muted-foreground))]'
23
+
24
+ export const voicePillPositionVariants = cva({
25
+ base: 'z-50',
26
+ variants: {
27
+ position: {
28
+ inline: 'relative',
29
+ 'top-start':
30
+ 'fixed top-[calc(env(safe-area-inset-top)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
31
+ 'top-center':
32
+ 'fixed top-[calc(env(safe-area-inset-top)+1rem)] left-1/2 -translate-x-1/2',
33
+ 'top-end':
34
+ 'fixed top-[calc(env(safe-area-inset-top)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
35
+ 'bottom-start':
36
+ 'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
37
+ 'bottom-center':
38
+ 'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-1/2 -translate-x-1/2',
39
+ 'bottom-end':
40
+ 'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
41
+ 'left-start':
42
+ 'fixed left-[calc(env(safe-area-inset-left)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
43
+ 'left-center':
44
+ 'fixed left-[calc(env(safe-area-inset-left)+1rem)] top-1/2 -translate-y-1/2',
45
+ 'left-end':
46
+ 'fixed left-[calc(env(safe-area-inset-left)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
47
+ 'right-start':
48
+ 'fixed right-[calc(env(safe-area-inset-right)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
49
+ 'right-center':
50
+ 'fixed right-[calc(env(safe-area-inset-right)+1rem)] top-1/2 -translate-y-1/2',
51
+ 'right-end':
52
+ 'fixed right-[calc(env(safe-area-inset-right)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
53
+ },
54
+ },
55
+ defaultVariants: {
56
+ position: 'bottom-center',
57
+ },
58
+ })
59
+
60
+ export const voicePillAvatarWrapStyles =
61
+ 'relative flex h-10 w-10 shrink-0 items-center justify-center'
62
+
63
+ export const voicePillPulseRingStyles =
64
+ 'pointer-events-none absolute inset-0 rounded-full border border-[var(--rfr-voice-pill-accent)] opacity-[var(--rfr-voice-pill-ring-opacity)] motion-safe:animate-ping motion-reduce:animate-none'
65
+
66
+ export const voicePillAvatarStyles =
67
+ 'relative z-10 flex h-10 w-10 items-center justify-center overflow-hidden rounded-full bg-[var(--rfr-voice-pill-accent)] text-[var(--rfr-voice-pill-accent-foreground)] text-xs font-semibold uppercase'
68
+
69
+ export const voicePillTextStyles = 'min-w-0 flex-1'
70
+
71
+ export const voicePillLabelStyles =
72
+ 'block truncate text-sm font-medium leading-tight'
73
+
74
+ export const voicePillSubStyles =
75
+ 'block truncate text-xs leading-tight text-muted-foreground'
76
+
77
+ export const voicePillMuteButtonStyles =
78
+ 'inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'