@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.
- package/dist/astro-voice-pill/VoicePill.astro +159 -0
- package/dist/astro-voice-pill/index.ts +27 -0
- package/dist/astro-waveform/Waveform.astro +279 -0
- package/dist/astro-waveform/index.ts +29 -0
- package/dist/index.ts +2 -0
- package/dist/slide-viewer/slide-viewer.styles.ts +3 -1
- package/dist/status-indicator/status-indicator.ts +8 -5
- package/dist/version-selector/version-selector.styles.ts +3 -1
- package/dist/voice-pill/index.ts +31 -0
- package/dist/voice-pill/voice-pill.styles.ts +78 -0
- package/dist/voice-pill/voice-pill.ts +235 -0
- package/dist/waveform/index.ts +39 -0
- package/dist/waveform/waveform-renderer.ts +181 -0
- package/dist/waveform/waveform.styles.ts +19 -0
- package/dist/waveform/waveform.ts +262 -0
- package/package.json +3 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export type VoicePillSpeaker = 'ai' | 'user' | (string & {})
|
|
4
|
+
|
|
5
|
+
export type VoicePillPosition =
|
|
6
|
+
| 'inline'
|
|
7
|
+
| 'top-start'
|
|
8
|
+
| 'top-center'
|
|
9
|
+
| 'top-end'
|
|
10
|
+
| 'bottom-start'
|
|
11
|
+
| 'bottom-center'
|
|
12
|
+
| 'bottom-end'
|
|
13
|
+
| 'left-start'
|
|
14
|
+
| 'left-center'
|
|
15
|
+
| 'left-end'
|
|
16
|
+
| 'right-start'
|
|
17
|
+
| 'right-center'
|
|
18
|
+
| 'right-end'
|
|
19
|
+
|
|
20
|
+
export interface VoicePillProps {
|
|
21
|
+
/** Speaker identity used for labels and data-speaker theming */
|
|
22
|
+
speaker?: VoicePillSpeaker
|
|
23
|
+
/** Primary voice status text */
|
|
24
|
+
label: string
|
|
25
|
+
/** Optional secondary status text */
|
|
26
|
+
sub?: string
|
|
27
|
+
/** Voice activity intensity, clamped from 0 to 1 */
|
|
28
|
+
intensity?: number
|
|
29
|
+
/** Whether voice output/input is muted */
|
|
30
|
+
muted?: boolean
|
|
31
|
+
/** Callback for the optional mute toggle */
|
|
32
|
+
onToggleMute?: () => void
|
|
33
|
+
/** Viewport position, or inline for embedding inside another layout */
|
|
34
|
+
position?: VoicePillPosition
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface VoicePillStyleVars {
|
|
38
|
+
[key: `--${string}`]: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface VoicePillAPI {
|
|
42
|
+
/** Display speaker label */
|
|
43
|
+
speaker: string
|
|
44
|
+
/** Normalized key for data-speaker */
|
|
45
|
+
speakerKey: string
|
|
46
|
+
/** Primary label */
|
|
47
|
+
label: string
|
|
48
|
+
/** Secondary label */
|
|
49
|
+
sub?: string
|
|
50
|
+
/** Clamped raw intensity */
|
|
51
|
+
intensity: number
|
|
52
|
+
/** Intensity used for visual animation after muted state is applied */
|
|
53
|
+
visualIntensity: number
|
|
54
|
+
/** Muted state */
|
|
55
|
+
muted: boolean
|
|
56
|
+
/** Viewport position, or inline for embedding inside another layout */
|
|
57
|
+
position: VoicePillPosition
|
|
58
|
+
/** Fallback initials for the speaker avatar */
|
|
59
|
+
initials: string
|
|
60
|
+
/** Invoke the mute callback when provided */
|
|
61
|
+
toggleMute(): void
|
|
62
|
+
/** ARIA props for the live status pill */
|
|
63
|
+
ariaProps: Partial<AccessibilityProps> & Record<string, unknown>
|
|
64
|
+
/** ARIA props for the mute toggle */
|
|
65
|
+
toggleMuteAriaProps: Record<string, unknown>
|
|
66
|
+
/** Data attributes reflecting speaker/state */
|
|
67
|
+
dataAttributes: Record<string, string>
|
|
68
|
+
/** CSS custom properties that drive pulse visuals */
|
|
69
|
+
style: VoicePillStyleVars
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const DEFAULT_VOICE_PILL_SPEAKER = 'ai'
|
|
73
|
+
export const DEFAULT_VOICE_PILL_POSITION: VoicePillPosition = 'bottom-center'
|
|
74
|
+
|
|
75
|
+
function formatNumber(value: number): string {
|
|
76
|
+
return Number(value.toFixed(3)).toString()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getRawSpeaker(speaker?: VoicePillSpeaker): string {
|
|
80
|
+
const value = String(speaker ?? DEFAULT_VOICE_PILL_SPEAKER).trim()
|
|
81
|
+
return value.length > 0 ? value : DEFAULT_VOICE_PILL_SPEAKER
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clampVoicePillIntensity(intensity = 0): number {
|
|
85
|
+
const value = Number(intensity)
|
|
86
|
+
|
|
87
|
+
if (!Number.isFinite(value)) {
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Math.min(1, Math.max(0, value))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getVoicePillSpeakerKey(speaker?: VoicePillSpeaker): string {
|
|
95
|
+
const normalized = getRawSpeaker(speaker)
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
98
|
+
.replace(/^-+|-+$/g, '')
|
|
99
|
+
|
|
100
|
+
return normalized.length > 0 ? normalized : DEFAULT_VOICE_PILL_SPEAKER
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getVoicePillSpeakerLabel(speaker?: VoicePillSpeaker): string {
|
|
104
|
+
const raw = getRawSpeaker(speaker)
|
|
105
|
+
const key = getVoicePillSpeakerKey(raw)
|
|
106
|
+
|
|
107
|
+
if (key === 'ai') return 'AI'
|
|
108
|
+
if (key === 'user') return 'User'
|
|
109
|
+
|
|
110
|
+
return raw
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getVoicePillInitials(speaker?: VoicePillSpeaker): string {
|
|
114
|
+
const key = getVoicePillSpeakerKey(speaker)
|
|
115
|
+
|
|
116
|
+
if (key === 'ai') return 'AI'
|
|
117
|
+
if (key === 'user') return 'U'
|
|
118
|
+
|
|
119
|
+
const initials = getVoicePillSpeakerLabel(speaker)
|
|
120
|
+
.split(/[\s_-]+/)
|
|
121
|
+
.map((part) => part.charAt(0))
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join('')
|
|
124
|
+
.slice(0, 2)
|
|
125
|
+
.toUpperCase()
|
|
126
|
+
|
|
127
|
+
return initials.length > 0 ? initials : 'V'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getVoicePillPosition(
|
|
131
|
+
position?: VoicePillPosition,
|
|
132
|
+
): VoicePillPosition {
|
|
133
|
+
return position ?? DEFAULT_VOICE_PILL_POSITION
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getVoicePillAriaLabel(props: {
|
|
137
|
+
speaker?: VoicePillSpeaker
|
|
138
|
+
label: string
|
|
139
|
+
sub?: string
|
|
140
|
+
muted?: boolean
|
|
141
|
+
}): string {
|
|
142
|
+
const parts = [`${getVoicePillSpeakerLabel(props.speaker)}: ${props.label}`]
|
|
143
|
+
|
|
144
|
+
if (props.sub) {
|
|
145
|
+
parts.push(props.sub)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (props.muted) {
|
|
149
|
+
parts.push('muted')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return parts.join(', ')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getVoicePillPulseStyle(
|
|
156
|
+
intensity?: number,
|
|
157
|
+
muted = false,
|
|
158
|
+
): VoicePillStyleVars {
|
|
159
|
+
const clamped = clampVoicePillIntensity(intensity)
|
|
160
|
+
const visualIntensity = muted ? 0 : clamped
|
|
161
|
+
const ringOpacity = visualIntensity === 0 ? 0 : 0.12 + visualIntensity * 0.42
|
|
162
|
+
const ringScale = 1 + visualIntensity * 0.35
|
|
163
|
+
const duration = visualIntensity === 0
|
|
164
|
+
? 1800
|
|
165
|
+
: Math.round(1700 - visualIntensity * 700)
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
'--rfr-voice-pill-intensity': formatNumber(clamped),
|
|
169
|
+
'--rfr-voice-pill-visual-intensity': formatNumber(visualIntensity),
|
|
170
|
+
'--rfr-voice-pill-ring-opacity': formatNumber(ringOpacity),
|
|
171
|
+
'--rfr-voice-pill-ring-scale': formatNumber(ringScale),
|
|
172
|
+
'--rfr-voice-pill-pulse-duration': `${duration}ms`,
|
|
173
|
+
'--rfr-voice-pill-pulse-delay': `-${Math.round(duration / 2)}ms`,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function createVoicePill(props: VoicePillProps): VoicePillAPI {
|
|
178
|
+
const {
|
|
179
|
+
speaker: speakerProp,
|
|
180
|
+
label,
|
|
181
|
+
sub,
|
|
182
|
+
intensity: intensityProp,
|
|
183
|
+
muted: mutedProp = false,
|
|
184
|
+
onToggleMute,
|
|
185
|
+
position: positionProp,
|
|
186
|
+
} = props
|
|
187
|
+
|
|
188
|
+
const speaker = getVoicePillSpeakerLabel(speakerProp)
|
|
189
|
+
const speakerKey = getVoicePillSpeakerKey(speakerProp)
|
|
190
|
+
const intensity = clampVoicePillIntensity(intensityProp)
|
|
191
|
+
const muted = Boolean(mutedProp)
|
|
192
|
+
const visualIntensity = muted ? 0 : intensity
|
|
193
|
+
const position = getVoicePillPosition(positionProp)
|
|
194
|
+
|
|
195
|
+
function toggleMute(): void {
|
|
196
|
+
onToggleMute?.()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {
|
|
200
|
+
role: 'status',
|
|
201
|
+
'aria-live': 'polite',
|
|
202
|
+
'aria-atomic': true,
|
|
203
|
+
'aria-label': getVoicePillAriaLabel({ speaker: speakerProp, label, sub, muted }),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const toggleMuteAriaProps: Record<string, unknown> = {
|
|
207
|
+
'aria-label': muted ? 'Unmute voice' : 'Mute voice',
|
|
208
|
+
'aria-pressed': muted,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const dataAttributes: Record<string, string> = {
|
|
212
|
+
'data-speaker': speakerKey,
|
|
213
|
+
'data-muted': String(muted),
|
|
214
|
+
'data-position': position,
|
|
215
|
+
'data-intensity': formatNumber(intensity),
|
|
216
|
+
'data-active': String(visualIntensity > 0),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
speaker,
|
|
221
|
+
speakerKey,
|
|
222
|
+
label,
|
|
223
|
+
sub,
|
|
224
|
+
intensity,
|
|
225
|
+
visualIntensity,
|
|
226
|
+
muted,
|
|
227
|
+
position,
|
|
228
|
+
initials: getVoicePillInitials(speakerProp),
|
|
229
|
+
toggleMute,
|
|
230
|
+
ariaProps,
|
|
231
|
+
toggleMuteAriaProps,
|
|
232
|
+
dataAttributes,
|
|
233
|
+
style: getVoicePillPulseStyle(intensity, muted),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_WAVEFORM_BAR_COUNT,
|
|
3
|
+
DEFAULT_WAVEFORM_COLOR,
|
|
4
|
+
DEFAULT_WAVEFORM_HEIGHT,
|
|
5
|
+
DEFAULT_WAVEFORM_INTENSITY,
|
|
6
|
+
DEFAULT_WAVEFORM_SMOOTHING,
|
|
7
|
+
createSilentSamples,
|
|
8
|
+
createIntensitySamples,
|
|
9
|
+
createWaveform,
|
|
10
|
+
getWaveformPeak,
|
|
11
|
+
isWaveformSampleInput,
|
|
12
|
+
normalizeBarCount,
|
|
13
|
+
normalizeIntensity,
|
|
14
|
+
normalizeSmoothing,
|
|
15
|
+
normalizeWaveformConfig,
|
|
16
|
+
normalizeWaveformSamples,
|
|
17
|
+
resampleWaveformSamples,
|
|
18
|
+
scaleWaveformSamples,
|
|
19
|
+
smoothWaveformSamples,
|
|
20
|
+
toCssDimension,
|
|
21
|
+
type NormalizedWaveformConfig,
|
|
22
|
+
type WaveformAPI,
|
|
23
|
+
type WaveformProps,
|
|
24
|
+
type WaveformSampleInput,
|
|
25
|
+
type WaveformSource,
|
|
26
|
+
type WaveformVariant,
|
|
27
|
+
} from './waveform.js'
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
drawWaveform,
|
|
31
|
+
prepareWaveformCanvas,
|
|
32
|
+
type WaveformCanvasSize,
|
|
33
|
+
type WaveformRenderOptions,
|
|
34
|
+
} from './waveform-renderer.js'
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
waveformCanvasVariants,
|
|
38
|
+
waveformVariants,
|
|
39
|
+
} from './waveform.styles.js'
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_WAVEFORM_COLOR,
|
|
3
|
+
DEFAULT_WAVEFORM_AMPLITUDE,
|
|
4
|
+
DEFAULT_WAVEFORM_INTENSITY,
|
|
5
|
+
getWaveformPeak,
|
|
6
|
+
normalizeAmplitude,
|
|
7
|
+
normalizeBarCount,
|
|
8
|
+
normalizeIntensity,
|
|
9
|
+
resampleWaveformSamples,
|
|
10
|
+
scaleWaveformSamples,
|
|
11
|
+
type WaveformSampleInput,
|
|
12
|
+
type WaveformVariant,
|
|
13
|
+
} from './waveform.js'
|
|
14
|
+
|
|
15
|
+
export interface WaveformCanvasSize {
|
|
16
|
+
width: number
|
|
17
|
+
height: number
|
|
18
|
+
pixelRatio?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WaveformRenderOptions {
|
|
22
|
+
variant?: WaveformVariant
|
|
23
|
+
color?: string
|
|
24
|
+
intensity?: number
|
|
25
|
+
amplitude?: number
|
|
26
|
+
barCount?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function prepareWaveformCanvas(
|
|
30
|
+
canvas: HTMLCanvasElement,
|
|
31
|
+
size: WaveformCanvasSize,
|
|
32
|
+
): CanvasRenderingContext2D | null {
|
|
33
|
+
const width = Math.max(1, Math.floor(size.width))
|
|
34
|
+
const height = Math.max(1, Math.floor(size.height))
|
|
35
|
+
const pixelRatio = Math.max(1, size.pixelRatio ?? 1)
|
|
36
|
+
const pixelWidth = Math.floor(width * pixelRatio)
|
|
37
|
+
const pixelHeight = Math.floor(height * pixelRatio)
|
|
38
|
+
|
|
39
|
+
if (canvas.width !== pixelWidth) {
|
|
40
|
+
canvas.width = pixelWidth
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (canvas.height !== pixelHeight) {
|
|
44
|
+
canvas.height = pixelHeight
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const context = canvas.getContext('2d')
|
|
48
|
+
if (!context) return null
|
|
49
|
+
|
|
50
|
+
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
|
|
51
|
+
|
|
52
|
+
return context
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function drawWaveform(
|
|
56
|
+
context: CanvasRenderingContext2D,
|
|
57
|
+
samples: WaveformSampleInput,
|
|
58
|
+
size: WaveformCanvasSize,
|
|
59
|
+
options: WaveformRenderOptions = {},
|
|
60
|
+
): void {
|
|
61
|
+
const width = Math.max(1, Math.floor(size.width))
|
|
62
|
+
const height = Math.max(1, Math.floor(size.height))
|
|
63
|
+
const variant = options.variant ?? 'bars'
|
|
64
|
+
const color = options.color ?? DEFAULT_WAVEFORM_COLOR
|
|
65
|
+
const intensity = normalizeIntensity(options.intensity ?? DEFAULT_WAVEFORM_INTENSITY)
|
|
66
|
+
const amplitude = normalizeAmplitude(options.amplitude ?? DEFAULT_WAVEFORM_AMPLITUDE)
|
|
67
|
+
const barCount = normalizeBarCount(options.barCount)
|
|
68
|
+
|
|
69
|
+
context.clearRect(0, 0, width, height)
|
|
70
|
+
applyCanvasColor(context, color)
|
|
71
|
+
|
|
72
|
+
if (variant === 'line') {
|
|
73
|
+
drawLine(context, samples, width, height, intensity, amplitude)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (variant === 'rings') {
|
|
78
|
+
drawRings(context, samples, width, height, intensity, amplitude)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
drawBars(context, samples, width, height, intensity, amplitude, barCount)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function drawBars(
|
|
86
|
+
context: CanvasRenderingContext2D,
|
|
87
|
+
samples: WaveformSampleInput,
|
|
88
|
+
width: number,
|
|
89
|
+
height: number,
|
|
90
|
+
intensity: number,
|
|
91
|
+
amplitude: number,
|
|
92
|
+
barCount: number,
|
|
93
|
+
) {
|
|
94
|
+
const bars = scaleWaveformSamples(resampleWaveformSamples(samples, barCount), intensity)
|
|
95
|
+
const slotWidth = width / bars.length
|
|
96
|
+
const barWidth = Math.max(1, slotWidth * 0.64)
|
|
97
|
+
const center = height / 2
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
100
|
+
const value = Math.abs(bars[i])
|
|
101
|
+
const barHeight = Math.max(value * height * amplitude, value > 0 ? 1 : 0)
|
|
102
|
+
const x = i * slotWidth + (slotWidth - barWidth) / 2
|
|
103
|
+
const y = center - barHeight / 2
|
|
104
|
+
|
|
105
|
+
context.fillRect(x, y, barWidth, barHeight)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function drawLine(
|
|
110
|
+
context: CanvasRenderingContext2D,
|
|
111
|
+
samples: WaveformSampleInput,
|
|
112
|
+
width: number,
|
|
113
|
+
height: number,
|
|
114
|
+
intensity: number,
|
|
115
|
+
amplitudeScale: number,
|
|
116
|
+
) {
|
|
117
|
+
const lineSamples = scaleWaveformSamples(samples, intensity)
|
|
118
|
+
const center = height / 2
|
|
119
|
+
const amplitude = height * 0.45 * amplitudeScale
|
|
120
|
+
|
|
121
|
+
context.beginPath()
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < lineSamples.length; i += 1) {
|
|
124
|
+
const x = lineSamples.length <= 1 ? width / 2 : (i / (lineSamples.length - 1)) * width
|
|
125
|
+
const y = center - Number(lineSamples[i]) * amplitude
|
|
126
|
+
|
|
127
|
+
if (i === 0) {
|
|
128
|
+
context.moveTo(x, y)
|
|
129
|
+
} else {
|
|
130
|
+
context.lineTo(x, y)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
context.lineWidth = Math.max(1.5, Math.min(width, height) * 0.025)
|
|
135
|
+
context.lineCap = 'round'
|
|
136
|
+
context.lineJoin = 'round'
|
|
137
|
+
context.stroke()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function drawRings(
|
|
141
|
+
context: CanvasRenderingContext2D,
|
|
142
|
+
samples: WaveformSampleInput,
|
|
143
|
+
width: number,
|
|
144
|
+
height: number,
|
|
145
|
+
intensity: number,
|
|
146
|
+
amplitude: number,
|
|
147
|
+
) {
|
|
148
|
+
const peak = getWaveformPeak(scaleWaveformSamples(samples, intensity)) * amplitude
|
|
149
|
+
const minDimension = Math.min(width, height)
|
|
150
|
+
const centerX = width / 2
|
|
151
|
+
const centerY = height / 2
|
|
152
|
+
const ringCount = 3
|
|
153
|
+
const baseRadius = minDimension * 0.16
|
|
154
|
+
const radiusStep = minDimension * 0.13
|
|
155
|
+
|
|
156
|
+
context.save()
|
|
157
|
+
context.lineWidth = Math.max(1.5, minDimension * 0.018)
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < ringCount; i += 1) {
|
|
160
|
+
const progress = (i + 1) / ringCount
|
|
161
|
+
const pulse = peak * minDimension * 0.1 * progress
|
|
162
|
+
const radius = baseRadius + radiusStep * i + pulse
|
|
163
|
+
|
|
164
|
+
context.globalAlpha = 0.28 + progress * 0.34
|
|
165
|
+
context.beginPath()
|
|
166
|
+
context.arc(centerX, centerY, radius, 0, Math.PI * 2)
|
|
167
|
+
context.stroke()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
context.restore()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function applyCanvasColor(context: CanvasRenderingContext2D, color: string) {
|
|
174
|
+
try {
|
|
175
|
+
context.fillStyle = color
|
|
176
|
+
context.strokeStyle = color
|
|
177
|
+
} catch {
|
|
178
|
+
context.fillStyle = '#000'
|
|
179
|
+
context.strokeStyle = '#000'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const waveformVariants = cva({
|
|
4
|
+
base: 'relative block overflow-hidden',
|
|
5
|
+
variants: {
|
|
6
|
+
variant: {
|
|
7
|
+
bars: '',
|
|
8
|
+
line: '',
|
|
9
|
+
rings: '',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
variant: 'bars',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const waveformCanvasVariants = cva({
|
|
18
|
+
base: 'block h-full w-full',
|
|
19
|
+
})
|