@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.
- package/dist/astro-voice-pill/VoicePill.astro +159 -0
- package/dist/astro-voice-pill/index.ts +27 -0
- package/dist/astro-waveform/Waveform.astro +273 -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 +77 -0
- package/dist/voice-pill/voice-pill.ts +234 -0
- package/dist/waveform/index.ts +39 -0
- package/dist/waveform/waveform-renderer.ts +174 -0
- package/dist/waveform/waveform.styles.ts +19 -0
- package/dist/waveform/waveform.ts +249 -0
- package/package.json +3 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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 =
|
|
51
|
-
const label = labelOverride ??
|
|
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
|
-
|
|
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'
|