@refraction-ui/astro 0.5.0 → 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.
@@ -12,6 +12,7 @@ import { cn } from '../shared/index.ts'
12
12
  interface Props extends Omit<astroHTML.JSX.HTMLAttributes, 'color' | 'height' | 'width'> {
13
13
  samples?: WaveformSampleInput
14
14
  intensity?: number
15
+ amplitude?: number
15
16
  variant?: WaveformVariant
16
17
  height?: number | string
17
18
  width?: number | string
@@ -24,6 +25,7 @@ interface Props extends Omit<astroHTML.JSX.HTMLAttributes, 'color' | 'height' |
24
25
  const {
25
26
  samples,
26
27
  intensity,
28
+ amplitude,
27
29
  variant,
28
30
  height,
29
31
  width,
@@ -39,6 +41,7 @@ const {
39
41
  const api = createWaveform({
40
42
  samples,
41
43
  intensity,
44
+ amplitude,
42
45
  variant,
43
46
  height,
44
47
  width,
@@ -65,6 +68,7 @@ const waveformData = {
65
68
  samples: Array.from(api.samples),
66
69
  variant: api.config.variant,
67
70
  intensity: api.config.intensity,
71
+ amplitude: api.config.amplitude,
68
72
  barCount: api.config.barCount,
69
73
  color: api.config.color,
70
74
  }
@@ -139,10 +143,11 @@ function toKebabCase(value: string): string {
139
143
  })
140
144
  }
141
145
 
142
- const scaleSamples = (samples, intensity) => {
146
+ const scaleSamples = (samples, intensity, amplitude) => {
143
147
  const amount = Math.max(0, Math.min(1, Number(intensity) || 0))
148
+ const visualHeight = Math.max(0, Math.min(1, Number(amplitude) || 0))
144
149
 
145
- return samples.map((sample) => Math.max(-1, Math.min(1, sample * amount)))
150
+ return samples.map((sample) => Math.max(-1, Math.min(1, sample * amount * visualHeight)))
146
151
  }
147
152
 
148
153
  const resolveColor = (element, color) => {
@@ -248,6 +253,7 @@ function toKebabCase(value: string): string {
248
253
  const samples = scaleSamples(
249
254
  normalizeSamples(config.samples, config.barCount),
250
255
  config.intensity,
256
+ config.amplitude,
251
257
  )
252
258
  const color = resolveColor(canvas.parentElement || canvas, config.color || 'currentColor')
253
259
 
@@ -22,33 +22,34 @@ export const voicePillSpeakerStyles =
22
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
23
 
24
24
  export const voicePillPositionVariants = cva({
25
- base: 'fixed z-50',
25
+ base: 'z-50',
26
26
  variants: {
27
27
  position: {
28
+ inline: 'relative',
28
29
  'top-start':
29
- 'top-[calc(env(safe-area-inset-top)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
30
+ 'fixed top-[calc(env(safe-area-inset-top)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
30
31
  'top-center':
31
- 'top-[calc(env(safe-area-inset-top)+1rem)] left-1/2 -translate-x-1/2',
32
+ 'fixed top-[calc(env(safe-area-inset-top)+1rem)] left-1/2 -translate-x-1/2',
32
33
  'top-end':
33
- 'top-[calc(env(safe-area-inset-top)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
34
+ 'fixed top-[calc(env(safe-area-inset-top)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
34
35
  'bottom-start':
35
- 'bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
36
+ 'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
36
37
  'bottom-center':
37
- 'bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-1/2 -translate-x-1/2',
38
+ 'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-1/2 -translate-x-1/2',
38
39
  'bottom-end':
39
- 'bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
40
+ 'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
40
41
  'left-start':
41
- 'left-[calc(env(safe-area-inset-left)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
42
+ 'fixed left-[calc(env(safe-area-inset-left)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
42
43
  'left-center':
43
- 'left-[calc(env(safe-area-inset-left)+1rem)] top-1/2 -translate-y-1/2',
44
+ 'fixed left-[calc(env(safe-area-inset-left)+1rem)] top-1/2 -translate-y-1/2',
44
45
  'left-end':
45
- 'left-[calc(env(safe-area-inset-left)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
46
+ 'fixed left-[calc(env(safe-area-inset-left)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
46
47
  'right-start':
47
- 'right-[calc(env(safe-area-inset-right)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
48
+ 'fixed right-[calc(env(safe-area-inset-right)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
48
49
  'right-center':
49
- 'right-[calc(env(safe-area-inset-right)+1rem)] top-1/2 -translate-y-1/2',
50
+ 'fixed right-[calc(env(safe-area-inset-right)+1rem)] top-1/2 -translate-y-1/2',
50
51
  'right-end':
51
- 'right-[calc(env(safe-area-inset-right)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
52
+ 'fixed right-[calc(env(safe-area-inset-right)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
52
53
  },
53
54
  },
54
55
  defaultVariants: {
@@ -3,6 +3,7 @@ import type { AccessibilityProps } from '../shared/index.ts'
3
3
  export type VoicePillSpeaker = 'ai' | 'user' | (string & {})
4
4
 
5
5
  export type VoicePillPosition =
6
+ | 'inline'
6
7
  | 'top-start'
7
8
  | 'top-center'
8
9
  | 'top-end'
@@ -29,7 +30,7 @@ export interface VoicePillProps {
29
30
  muted?: boolean
30
31
  /** Callback for the optional mute toggle */
31
32
  onToggleMute?: () => void
32
- /** Fixed viewport position */
33
+ /** Viewport position, or inline for embedding inside another layout */
33
34
  position?: VoicePillPosition
34
35
  }
35
36
 
@@ -52,7 +53,7 @@ export interface VoicePillAPI {
52
53
  visualIntensity: number
53
54
  /** Muted state */
54
55
  muted: boolean
55
- /** Fixed viewport position */
56
+ /** Viewport position, or inline for embedding inside another layout */
56
57
  position: VoicePillPosition
57
58
  /** Fallback initials for the speaker avatar */
58
59
  initials: string
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  DEFAULT_WAVEFORM_COLOR,
3
+ DEFAULT_WAVEFORM_AMPLITUDE,
3
4
  DEFAULT_WAVEFORM_INTENSITY,
4
5
  getWaveformPeak,
6
+ normalizeAmplitude,
5
7
  normalizeBarCount,
6
8
  normalizeIntensity,
7
9
  resampleWaveformSamples,
@@ -20,6 +22,7 @@ export interface WaveformRenderOptions {
20
22
  variant?: WaveformVariant
21
23
  color?: string
22
24
  intensity?: number
25
+ amplitude?: number
23
26
  barCount?: number
24
27
  }
25
28
 
@@ -60,22 +63,23 @@ export function drawWaveform(
60
63
  const variant = options.variant ?? 'bars'
61
64
  const color = options.color ?? DEFAULT_WAVEFORM_COLOR
62
65
  const intensity = normalizeIntensity(options.intensity ?? DEFAULT_WAVEFORM_INTENSITY)
66
+ const amplitude = normalizeAmplitude(options.amplitude ?? DEFAULT_WAVEFORM_AMPLITUDE)
63
67
  const barCount = normalizeBarCount(options.barCount)
64
68
 
65
69
  context.clearRect(0, 0, width, height)
66
70
  applyCanvasColor(context, color)
67
71
 
68
72
  if (variant === 'line') {
69
- drawLine(context, samples, width, height, intensity)
73
+ drawLine(context, samples, width, height, intensity, amplitude)
70
74
  return
71
75
  }
72
76
 
73
77
  if (variant === 'rings') {
74
- drawRings(context, samples, width, height, intensity)
78
+ drawRings(context, samples, width, height, intensity, amplitude)
75
79
  return
76
80
  }
77
81
 
78
- drawBars(context, samples, width, height, intensity, barCount)
82
+ drawBars(context, samples, width, height, intensity, amplitude, barCount)
79
83
  }
80
84
 
81
85
  function drawBars(
@@ -84,6 +88,7 @@ function drawBars(
84
88
  width: number,
85
89
  height: number,
86
90
  intensity: number,
91
+ amplitude: number,
87
92
  barCount: number,
88
93
  ) {
89
94
  const bars = scaleWaveformSamples(resampleWaveformSamples(samples, barCount), intensity)
@@ -93,7 +98,7 @@ function drawBars(
93
98
 
94
99
  for (let i = 0; i < bars.length; i += 1) {
95
100
  const value = Math.abs(bars[i])
96
- const barHeight = Math.max(value * height, value > 0 ? 1 : 0)
101
+ const barHeight = Math.max(value * height * amplitude, value > 0 ? 1 : 0)
97
102
  const x = i * slotWidth + (slotWidth - barWidth) / 2
98
103
  const y = center - barHeight / 2
99
104
 
@@ -107,10 +112,11 @@ function drawLine(
107
112
  width: number,
108
113
  height: number,
109
114
  intensity: number,
115
+ amplitudeScale: number,
110
116
  ) {
111
117
  const lineSamples = scaleWaveformSamples(samples, intensity)
112
118
  const center = height / 2
113
- const amplitude = height * 0.45
119
+ const amplitude = height * 0.45 * amplitudeScale
114
120
 
115
121
  context.beginPath()
116
122
 
@@ -137,8 +143,9 @@ function drawRings(
137
143
  width: number,
138
144
  height: number,
139
145
  intensity: number,
146
+ amplitude: number,
140
147
  ) {
141
- const peak = getWaveformPeak(scaleWaveformSamples(samples, intensity))
148
+ const peak = getWaveformPeak(scaleWaveformSamples(samples, intensity)) * amplitude
142
149
  const minDimension = Math.min(width, height)
143
150
  const centerX = width / 2
144
151
  const centerY = height / 2
@@ -8,6 +8,7 @@ export interface WaveformProps {
8
8
  source?: WaveformSource
9
9
  samples?: WaveformSampleInput
10
10
  intensity?: number
11
+ amplitude?: number
11
12
  variant?: WaveformVariant
12
13
  height?: number | string
13
14
  width?: number | string
@@ -20,6 +21,7 @@ export interface WaveformProps {
20
21
  export interface NormalizedWaveformConfig {
21
22
  variant: WaveformVariant
22
23
  intensity: number
24
+ amplitude: number
23
25
  height: number | string
24
26
  width: number | string
25
27
  barCount: number
@@ -40,6 +42,7 @@ export const DEFAULT_WAVEFORM_BAR_COUNT = 48
40
42
  export const DEFAULT_WAVEFORM_COLOR = 'var(--accent-2)'
41
43
  export const DEFAULT_WAVEFORM_HEIGHT = 80
42
44
  export const DEFAULT_WAVEFORM_INTENSITY = 1
45
+ export const DEFAULT_WAVEFORM_AMPLITUDE = 1
43
46
  export const DEFAULT_WAVEFORM_SMOOTHING = 0.8
44
47
 
45
48
  const MAX_BAR_COUNT = 1024
@@ -63,10 +66,17 @@ export function normalizeIntensity(value: number | undefined): number {
63
66
  return Math.min(MAX_INTENSITY, Math.max(0, value))
64
67
  }
65
68
 
69
+ export function normalizeAmplitude(value: number | undefined): number {
70
+ if (value == null || !Number.isFinite(value)) return DEFAULT_WAVEFORM_AMPLITUDE
71
+
72
+ return Math.min(1, Math.max(0, value))
73
+ }
74
+
66
75
  export function normalizeWaveformConfig(props: WaveformProps = {}): NormalizedWaveformConfig {
67
76
  return {
68
77
  variant: props.variant ?? 'bars',
69
78
  intensity: normalizeIntensity(props.intensity),
79
+ amplitude: normalizeAmplitude(props.amplitude),
70
80
  height: props.height ?? DEFAULT_WAVEFORM_HEIGHT,
71
81
  width: props.width ?? '100%',
72
82
  barCount: normalizeBarCount(props.barCount),
@@ -87,6 +97,7 @@ export function createSilentSamples(count = DEFAULT_WAVEFORM_BAR_COUNT): Float32
87
97
  export function createIntensitySamples(
88
98
  intensity = DEFAULT_WAVEFORM_INTENSITY,
89
99
  count = DEFAULT_WAVEFORM_BAR_COUNT,
100
+ phase = 0,
90
101
  ): Float32Array {
91
102
  const normalizedIntensity = normalizeIntensity(intensity)
92
103
  const normalizedCount = normalizeBarCount(count)
@@ -94,11 +105,13 @@ export function createIntensitySamples(
94
105
 
95
106
  if (normalizedIntensity === 0) return samples
96
107
 
108
+ const pulse = 0.72 + Math.sin(phase * 1.8) * 0.18
109
+
97
110
  for (let i = 0; i < normalizedCount; i += 1) {
98
111
  const progress = normalizedCount <= 1 ? 0.5 : i / (normalizedCount - 1)
99
112
  const envelope = 0.35 + Math.sin(progress * Math.PI) * 0.65
100
- const carrier = Math.sin(progress * Math.PI * 6)
101
- samples[i] = carrier * envelope * normalizedIntensity
113
+ const carrier = Math.sin(progress * Math.PI * 6 + phase * 2.8)
114
+ samples[i] = carrier * envelope * normalizedIntensity * pulse
102
115
  }
103
116
 
104
117
  return samples
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {