@refraction-ui/astro 0.4.12 → 0.5.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.
@@ -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,273 @@
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
+ variant?: WaveformVariant
16
+ height?: number | string
17
+ width?: number | string
18
+ barCount?: number
19
+ smoothing?: number
20
+ color?: string
21
+ paused?: boolean
22
+ }
23
+
24
+ const {
25
+ samples,
26
+ intensity,
27
+ variant,
28
+ height,
29
+ width,
30
+ barCount,
31
+ smoothing,
32
+ color,
33
+ paused,
34
+ class: className,
35
+ style,
36
+ ...props
37
+ } = Astro.props
38
+
39
+ const api = createWaveform({
40
+ samples,
41
+ intensity,
42
+ variant,
43
+ height,
44
+ width,
45
+ barCount,
46
+ smoothing,
47
+ color,
48
+ paused,
49
+ })
50
+
51
+ const styleObject = style && typeof style === 'object'
52
+ ? (style as Record<string, string | number | undefined>)
53
+ : {}
54
+
55
+ const rootStyle = mergeStyles(style, {
56
+ width: toCssDimension(width) ?? styleObject.width ?? '100%',
57
+ height:
58
+ toCssDimension(height) ??
59
+ styleObject.height ??
60
+ `${api.config.height}px`,
61
+ '--waveform-color': api.config.color,
62
+ })
63
+
64
+ const waveformData = {
65
+ samples: Array.from(api.samples),
66
+ variant: api.config.variant,
67
+ intensity: api.config.intensity,
68
+ barCount: api.config.barCount,
69
+ color: api.config.color,
70
+ }
71
+
72
+ function mergeStyles(
73
+ base: unknown,
74
+ extra: Record<string, string | number | undefined>,
75
+ ): string {
76
+ const baseStyle = styleToString(base)
77
+ const extraStyle = Object.entries(extra)
78
+ .filter(([, value]) => value !== undefined)
79
+ .map(([key, value]) => `${toKebabCase(key)}:${value}`)
80
+ .join(';')
81
+
82
+ return [baseStyle, extraStyle].filter(Boolean).join(';')
83
+ }
84
+
85
+ function styleToString(styleValue: unknown): string {
86
+ if (typeof styleValue === 'string') return styleValue
87
+ if (!styleValue || typeof styleValue !== 'object') return ''
88
+
89
+ return Object.entries(styleValue as Record<string, string | number | undefined>)
90
+ .filter(([, value]) => value !== undefined)
91
+ .map(([key, value]) => `${toKebabCase(key)}:${value}`)
92
+ .join(';')
93
+ }
94
+
95
+ function toKebabCase(value: string): string {
96
+ return value.startsWith('--')
97
+ ? value
98
+ : value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
99
+ }
100
+ ---
101
+
102
+ <div
103
+ class={cn(waveformVariants({ variant: api.config.variant }), className)}
104
+ style={rootStyle}
105
+ {...api.ariaProps}
106
+ {...api.dataAttributes}
107
+ {...props}
108
+ >
109
+ <canvas
110
+ class={waveformCanvasVariants()}
111
+ aria-hidden="true"
112
+ data-waveform-canvas=""
113
+ data-waveform={JSON.stringify(waveformData)}
114
+ ></canvas>
115
+ </div>
116
+
117
+ <script>
118
+ const normalizeSamples = (samples, count) => {
119
+ const values = Array.isArray(samples) ? samples : []
120
+ const targetCount = Math.max(1, Math.min(1024, Math.round(Number(count) || 48)))
121
+
122
+ if (values.length === 0) return Array.from({ length: targetCount }, () => 0)
123
+ if (values.length === targetCount) return values.map((value) => Number(value) || 0)
124
+
125
+ return Array.from({ length: targetCount }, (_, index) => {
126
+ const start = (index * values.length) / targetCount
127
+ const end = ((index + 1) * values.length) / targetCount
128
+ const firstIndex = Math.floor(start)
129
+ const lastIndex = Math.max(firstIndex + 1, Math.ceil(end))
130
+ let total = 0
131
+ let count = 0
132
+
133
+ for (let sampleIndex = firstIndex; sampleIndex < lastIndex && sampleIndex < values.length; sampleIndex += 1) {
134
+ total += Number(values[sampleIndex]) || 0
135
+ count += 1
136
+ }
137
+
138
+ return count > 0 ? total / count : Number(values[Math.min(firstIndex, values.length - 1)]) || 0
139
+ })
140
+ }
141
+
142
+ const scaleSamples = (samples, intensity) => {
143
+ const amount = Math.max(0, Math.min(1, Number(intensity) || 0))
144
+
145
+ return samples.map((sample) => Math.max(-1, Math.min(1, sample * amount)))
146
+ }
147
+
148
+ const resolveColor = (element, color) => {
149
+ const match = String(color).match(/^var\((--[^),\s]+)(?:,[^)]+)?\)$/)
150
+
151
+ if (!match || typeof window.getComputedStyle !== 'function') return color
152
+
153
+ return window.getComputedStyle(element).getPropertyValue(match[1]).trim() || color
154
+ }
155
+
156
+ const prepareCanvas = (canvas) => {
157
+ const root = canvas.parentElement
158
+ const rect = root?.getBoundingClientRect()
159
+ const width = Math.max(1, Math.round(rect?.width || root?.clientWidth || 300))
160
+ const height = Math.max(1, Math.round(rect?.height || root?.clientHeight || 80))
161
+ const ratio = window.devicePixelRatio || 1
162
+
163
+ canvas.width = Math.round(width * ratio)
164
+ canvas.height = Math.round(height * ratio)
165
+ const context = canvas.getContext('2d')
166
+
167
+ if (!context) return null
168
+
169
+ canvas.style.width = '100%'
170
+ canvas.style.height = '100%'
171
+ context.setTransform(ratio, 0, 0, ratio, 0, 0)
172
+ context.clearRect(0, 0, width, height)
173
+
174
+ return { context, width, height }
175
+ }
176
+
177
+ const drawBars = (context, samples, width, height, color) => {
178
+ const gap = samples.length > 96 ? 1 : 2
179
+ const barWidth = Math.max(1, (width - gap * Math.max(0, samples.length - 1)) / samples.length)
180
+ const centerY = height / 2
181
+
182
+ context.fillStyle = color
183
+
184
+ samples.forEach((sample, index) => {
185
+ const magnitude = Math.max(0.06, Math.abs(sample))
186
+ const barHeight = Math.max(1, magnitude * height)
187
+ const x = index * (barWidth + gap)
188
+ const y = centerY - barHeight / 2
189
+
190
+ context.fillRect(x, y, barWidth, barHeight)
191
+ })
192
+ }
193
+
194
+ const drawLine = (context, samples, width, height, color) => {
195
+ const centerY = height / 2
196
+
197
+ context.strokeStyle = color
198
+ context.lineWidth = 2
199
+ context.lineCap = 'round'
200
+ context.lineJoin = 'round'
201
+ context.beginPath()
202
+
203
+ samples.forEach((sample, index) => {
204
+ const x = samples.length <= 1 ? width / 2 : (index / (samples.length - 1)) * width
205
+ const y = centerY - sample * centerY * 0.9
206
+
207
+ if (index === 0) context.moveTo(x, y)
208
+ else context.lineTo(x, y)
209
+ })
210
+
211
+ context.stroke()
212
+ }
213
+
214
+ const drawRings = (context, samples, width, height, color) => {
215
+ const peak = samples.reduce((current, sample) => Math.max(current, Math.abs(sample)), 0)
216
+ const centerX = width / 2
217
+ const centerY = height / 2
218
+ const maxRadius = Math.max(1, Math.min(width, height) / 2 - 2)
219
+
220
+ context.strokeStyle = color
221
+ context.lineWidth = Math.max(2, maxRadius * 0.08)
222
+
223
+ ;[0.38, 0.64, 0.9].forEach((scale, index) => {
224
+ context.globalAlpha = 0.35 + peak * (0.2 + index * 0.12)
225
+ context.beginPath()
226
+ context.arc(centerX, centerY, Math.max(1, maxRadius * scale * (0.85 + peak * 0.15)), 0, Math.PI * 2)
227
+ context.stroke()
228
+ })
229
+
230
+ context.globalAlpha = 1
231
+ }
232
+
233
+ const drawWaveform = (canvas) => {
234
+ if (!(canvas instanceof HTMLCanvasElement)) return
235
+
236
+ let config
237
+
238
+ try {
239
+ config = JSON.parse(canvas.dataset.waveform || '{}')
240
+ } catch {
241
+ config = {}
242
+ }
243
+
244
+ const prepared = prepareCanvas(canvas)
245
+ if (!prepared) return
246
+
247
+ const { context, width, height } = prepared
248
+ const samples = scaleSamples(
249
+ normalizeSamples(config.samples, config.barCount),
250
+ config.intensity,
251
+ )
252
+ const color = resolveColor(canvas.parentElement || canvas, config.color || 'currentColor')
253
+
254
+ if (config.variant === 'line') {
255
+ drawLine(context, samples, width, height, color)
256
+ } else if (config.variant === 'rings') {
257
+ drawRings(context, samples, width, height, color)
258
+ } else {
259
+ drawBars(context, samples, width, height, color)
260
+ }
261
+ }
262
+
263
+ const canvases = document.querySelectorAll('canvas[data-waveform-canvas][data-waveform]')
264
+
265
+ canvases.forEach((canvas) => {
266
+ drawWaveform(canvas)
267
+
268
+ if (typeof ResizeObserver === 'function' && canvas.parentElement) {
269
+ const observer = new ResizeObserver(() => drawWaveform(canvas))
270
+ observer.observe(canvas.parentElement)
271
+ }
272
+ })
273
+ </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,77 @@
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: 'fixed z-50',
26
+ variants: {
27
+ position: {
28
+ 'top-start':
29
+ 'top-[calc(env(safe-area-inset-top)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
30
+ 'top-center':
31
+ 'top-[calc(env(safe-area-inset-top)+1rem)] left-1/2 -translate-x-1/2',
32
+ 'top-end':
33
+ 'top-[calc(env(safe-area-inset-top)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
34
+ 'bottom-start':
35
+ 'bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
36
+ 'bottom-center':
37
+ 'bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-1/2 -translate-x-1/2',
38
+ 'bottom-end':
39
+ 'bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
40
+ 'left-start':
41
+ 'left-[calc(env(safe-area-inset-left)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
42
+ 'left-center':
43
+ 'left-[calc(env(safe-area-inset-left)+1rem)] top-1/2 -translate-y-1/2',
44
+ 'left-end':
45
+ 'left-[calc(env(safe-area-inset-left)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
46
+ 'right-start':
47
+ 'right-[calc(env(safe-area-inset-right)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
48
+ 'right-center':
49
+ 'right-[calc(env(safe-area-inset-right)+1rem)] top-1/2 -translate-y-1/2',
50
+ 'right-end':
51
+ 'right-[calc(env(safe-area-inset-right)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
52
+ },
53
+ },
54
+ defaultVariants: {
55
+ position: 'bottom-center',
56
+ },
57
+ })
58
+
59
+ export const voicePillAvatarWrapStyles =
60
+ 'relative flex h-10 w-10 shrink-0 items-center justify-center'
61
+
62
+ export const voicePillPulseRingStyles =
63
+ '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'
64
+
65
+ export const voicePillAvatarStyles =
66
+ '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'
67
+
68
+ export const voicePillTextStyles = 'min-w-0 flex-1'
69
+
70
+ export const voicePillLabelStyles =
71
+ 'block truncate text-sm font-medium leading-tight'
72
+
73
+ export const voicePillSubStyles =
74
+ 'block truncate text-xs leading-tight text-muted-foreground'
75
+
76
+ export const voicePillMuteButtonStyles =
77
+ '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'