@refraction-ui/astro 0.5.0 → 0.6.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/analytics/analytics-manager.ts +361 -0
- package/dist/analytics/consent.ts +40 -0
- package/dist/analytics/console-sink.ts +37 -0
- package/dist/analytics/http-sink.ts +213 -0
- package/dist/analytics/identity.ts +64 -0
- package/dist/analytics/index.ts +60 -0
- package/dist/analytics/mock-sink.ts +62 -0
- package/dist/analytics/noop.ts +48 -0
- package/dist/analytics/redaction.ts +93 -0
- package/dist/analytics/session.ts +162 -0
- package/dist/analytics/storage.ts +119 -0
- package/dist/analytics/types.ts +273 -0
- package/dist/analytics/uuid.ts +63 -0
- package/dist/astro-analytics/AnalyticsScript.astro +130 -0
- package/dist/astro-analytics/index.ts +49 -0
- package/dist/astro-logger/TelemetryScript.astro +108 -0
- package/dist/astro-logger/index.ts +48 -0
- package/dist/astro-logger/library-error-capture.ts +121 -0
- package/dist/astro-logger/telemetry-middleware.ts +92 -0
- package/dist/astro-waveform/Waveform.astro +8 -2
- package/dist/index.ts +2 -0
- package/dist/logger/console-sink.ts +64 -0
- package/dist/logger/faro-engine.ts +130 -0
- package/dist/logger/index.ts +39 -0
- package/dist/logger/mock-sink.ts +38 -0
- package/dist/logger/noop.ts +52 -0
- package/dist/logger/presets.ts +51 -0
- package/dist/logger/redact.ts +33 -0
- package/dist/logger/telemetry-manager.ts +229 -0
- package/dist/logger/types.ts +121 -0
- package/dist/shared/dev-feedback.ts +154 -0
- package/dist/shared/index.ts +25 -0
- package/dist/shared/library-origin-error.ts +240 -0
- package/dist/voice-pill/voice-pill.styles.ts +14 -13
- package/dist/voice-pill/voice-pill.ts +3 -2
- package/dist/waveform/waveform-renderer.ts +13 -6
- package/dist/waveform/waveform.ts +15 -2
- package/package.json +4 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* library-origin-error — builds the **library-origin error envelope** for
|
|
3
|
+
* errors that originate inside a `@refraction-ui/*` package.
|
|
4
|
+
*
|
|
5
|
+
* The envelope is expressed THROUGH the existing telemetry record contract
|
|
6
|
+
* (`@refraction-ui/logger`'s `LogRecord`) — the contract is reused, NOT
|
|
7
|
+
* redefined. To keep `@refraction-ui/shared` zero-dependency, the record is
|
|
8
|
+
* mirrored structurally (see {@link DevFeedbackRecord}) rather than imported;
|
|
9
|
+
* a consumer wiring the real logger sink satisfies it structurally.
|
|
10
|
+
*
|
|
11
|
+
* Aggressive redaction (epic #247 guardrails): the payload is package,
|
|
12
|
+
* componentName, version, a normalized stack **fingerprint hash**, and
|
|
13
|
+
* framework only. Never app state, props values, user data, PII, or full app
|
|
14
|
+
* stack frames.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { DevFeedbackRecord, DevFeedbackSink } from './dev-feedback.js'
|
|
18
|
+
|
|
19
|
+
/** Matches any stack frame that references a `@refraction-ui/*` package. */
|
|
20
|
+
const REFRACTION_FRAME = /@refraction-ui\//
|
|
21
|
+
|
|
22
|
+
/** Frameworks a refraction-ui adapter can run under. */
|
|
23
|
+
export type LibraryFramework =
|
|
24
|
+
| 'react'
|
|
25
|
+
| 'angular'
|
|
26
|
+
| 'astro'
|
|
27
|
+
| 'vue'
|
|
28
|
+
| 'svelte'
|
|
29
|
+
| 'vanilla'
|
|
30
|
+
| 'unknown'
|
|
31
|
+
|
|
32
|
+
/** Inputs describing where a library-origin error came from. */
|
|
33
|
+
export interface LibraryOriginErrorInput {
|
|
34
|
+
/** Originating package, e.g. `@refraction-ui/react`. */
|
|
35
|
+
package: string
|
|
36
|
+
/** refraction-ui component the error originated in, e.g. `Dialog`. */
|
|
37
|
+
componentName: string
|
|
38
|
+
/** Originating package version, e.g. `0.1.5`. */
|
|
39
|
+
version: string
|
|
40
|
+
/** Host framework the component was running under. */
|
|
41
|
+
framework: LibraryFramework
|
|
42
|
+
/** The thrown error (or its stack string). Used ONLY to derive a hash. */
|
|
43
|
+
error?: unknown
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The redacted, library-origin payload carried in `LogRecord.context`. Contains
|
|
48
|
+
* NO app data — only the five identifying fields plus a stack fingerprint hash.
|
|
49
|
+
*/
|
|
50
|
+
export interface LibraryOriginEnvelope {
|
|
51
|
+
/** Marks this record as a library-origin error (for downstream routing). */
|
|
52
|
+
origin: 'refraction-ui'
|
|
53
|
+
package: string
|
|
54
|
+
componentName: string
|
|
55
|
+
version: string
|
|
56
|
+
framework: LibraryFramework
|
|
57
|
+
/** Stable, app-data-free hash of the normalized stack. */
|
|
58
|
+
fingerprint: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize a stack trace into a deterministic, app-data-free string before
|
|
63
|
+
* hashing:
|
|
64
|
+
* - keep only frames that reference a `@refraction-ui/*` package (drop the
|
|
65
|
+
* app's own frames — library-origin only),
|
|
66
|
+
* - strip absolute paths, line/column numbers, query strings, and hashes,
|
|
67
|
+
* - sort-free (preserve call order) but whitespace-collapsed.
|
|
68
|
+
*
|
|
69
|
+
* If no refraction-ui frames are present the function returns an empty string,
|
|
70
|
+
* which yields a stable "no-frames" fingerprint rather than leaking app frames.
|
|
71
|
+
*/
|
|
72
|
+
function normalizeStack(stack: string): string {
|
|
73
|
+
const frames = stack
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((line) => line.trim())
|
|
76
|
+
.filter((line) => line.startsWith('at ') || line.includes('@'))
|
|
77
|
+
.filter((line) => REFRACTION_FRAME.test(line))
|
|
78
|
+
.map((line) =>
|
|
79
|
+
line
|
|
80
|
+
// drop everything from the path/url onward, keep the symbol
|
|
81
|
+
.replace(/\((?:.*?)(@refraction-ui\/[^):]+)[^)]*\)/, '($1)')
|
|
82
|
+
.replace(/(@refraction-ui\/[^\s:?#]+)[^\s)]*/g, '$1')
|
|
83
|
+
// strip :line:col
|
|
84
|
+
.replace(/:\d+:\d+/g, '')
|
|
85
|
+
// strip line:col without colon prefix
|
|
86
|
+
.replace(/:\d+\b/g, '')
|
|
87
|
+
// strip query/hash
|
|
88
|
+
.replace(/[?#][^\s)]*/g, '')
|
|
89
|
+
.replace(/\s+/g, ' ')
|
|
90
|
+
.trim(),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return frames.join('\n')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Deterministic, dependency-free 53-bit string hash (cyrb53). Stable across
|
|
98
|
+
* runs and processes for the same input — required so the intake can dedupe by
|
|
99
|
+
* fingerprint. NOT cryptographic; it only needs to be collision-resistant
|
|
100
|
+
* enough to group identical normalized stacks.
|
|
101
|
+
*/
|
|
102
|
+
function cyrb53(str: string, seed = 0): string {
|
|
103
|
+
let h1 = 0xdeadbeef ^ seed
|
|
104
|
+
let h2 = 0x41c6ce57 ^ seed
|
|
105
|
+
for (let i = 0; i < str.length; i++) {
|
|
106
|
+
const ch = str.charCodeAt(i)
|
|
107
|
+
h1 = Math.imul(h1 ^ ch, 2654435761)
|
|
108
|
+
h2 = Math.imul(h2 ^ ch, 1597334677)
|
|
109
|
+
}
|
|
110
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
|
|
111
|
+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
|
112
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
|
|
113
|
+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
|
114
|
+
const out = 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
|
115
|
+
return out.toString(16).padStart(14, '0')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stackOf(error: unknown): string {
|
|
119
|
+
if (typeof error === 'string') return error
|
|
120
|
+
if (
|
|
121
|
+
error &&
|
|
122
|
+
typeof error === 'object' &&
|
|
123
|
+
'stack' in error &&
|
|
124
|
+
typeof (error as { stack?: unknown }).stack === 'string'
|
|
125
|
+
) {
|
|
126
|
+
return (error as { stack: string }).stack
|
|
127
|
+
}
|
|
128
|
+
return ''
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Compute the stable stack fingerprint for a library-origin error. Exposed
|
|
133
|
+
* separately so capture seams / tests can assert fingerprint stability without
|
|
134
|
+
* building the whole record.
|
|
135
|
+
*/
|
|
136
|
+
export function stackFingerprint(error: unknown): string {
|
|
137
|
+
const normalized = normalizeStack(stackOf(error))
|
|
138
|
+
return cyrb53(normalized)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the redacted library-origin envelope for an error.
|
|
143
|
+
*/
|
|
144
|
+
export function libraryOriginEnvelope(
|
|
145
|
+
input: LibraryOriginErrorInput,
|
|
146
|
+
): LibraryOriginEnvelope {
|
|
147
|
+
return {
|
|
148
|
+
origin: 'refraction-ui',
|
|
149
|
+
package: input.package,
|
|
150
|
+
componentName: input.componentName,
|
|
151
|
+
version: input.version,
|
|
152
|
+
framework: input.framework,
|
|
153
|
+
fingerprint: stackFingerprint(input.error),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the full library-origin error record, expressed THROUGH the existing
|
|
159
|
+
* telemetry record contract (`LogRecord`, mirrored as {@link DevFeedbackRecord}
|
|
160
|
+
* to avoid a hard logger dependency). The redacted envelope rides in
|
|
161
|
+
* `context`; no app data, props, PII, or app stack frames are included.
|
|
162
|
+
*
|
|
163
|
+
* This record is exactly what a consumer-wired telemetry sink (or the
|
|
164
|
+
* `devWarn`/`devError` injected sink) consumes.
|
|
165
|
+
*/
|
|
166
|
+
export function libraryOriginError(
|
|
167
|
+
input: LibraryOriginErrorInput,
|
|
168
|
+
): DevFeedbackRecord {
|
|
169
|
+
const envelope = libraryOriginEnvelope(input)
|
|
170
|
+
return {
|
|
171
|
+
level: 'error',
|
|
172
|
+
message: `${input.package}/${input.componentName}: library-origin error`,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
context: { ...envelope },
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Library-origin predicate — `true` iff the error's stack contains at least
|
|
180
|
+
* one frame that references a `@refraction-ui/*` package. This is the gate
|
|
181
|
+
* every per-framework capture seam uses: only library-origin errors are
|
|
182
|
+
* eligible; the app's own errors return `false` and pass through untouched.
|
|
183
|
+
*
|
|
184
|
+
* Mirrors exactly the frame-detection rule {@link stackFingerprint} normalizes
|
|
185
|
+
* by — no new contract, just the companion predicate the seams need. Never
|
|
186
|
+
* throws for a missing/odd error (returns `false`).
|
|
187
|
+
*/
|
|
188
|
+
export function isLibraryOriginError(error: unknown): boolean {
|
|
189
|
+
const stack = stackOf(error)
|
|
190
|
+
if (!stack) return false
|
|
191
|
+
return REFRACTION_FRAME.test(stack)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The fixed identity of the refraction-ui package/component a seam tags an
|
|
196
|
+
* error with. The seam supplies this; the stack itself supplies the
|
|
197
|
+
* fingerprint. Never carries app data.
|
|
198
|
+
*/
|
|
199
|
+
export type LibraryOriginIdentity = Pick<
|
|
200
|
+
LibraryOriginErrorInput,
|
|
201
|
+
'package' | 'componentName' | 'version' | 'framework'
|
|
202
|
+
>
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The single capture primitive shared by every per-framework seam (React error
|
|
206
|
+
* boundary, Angular `ErrorHandler`, Astro middleware). It performs the entire
|
|
207
|
+
* guarded flow in one place so the seams stay thin and identical in behavior:
|
|
208
|
+
*
|
|
209
|
+
* 1. **Library-origin filter** — if the error's stack has no
|
|
210
|
+
* `@refraction-ui/*` frame it is the app's own error: return `null` and do
|
|
211
|
+
* NOT touch, capture, or forward it.
|
|
212
|
+
* 2. **Tag** — build the redacted {@link libraryOriginError} record
|
|
213
|
+
* (package/componentName/version/framework + app-data-free fingerprint).
|
|
214
|
+
* 3. **Route to the optional sink** — forward the record to the
|
|
215
|
+
* consumer-injected sink ONLY if one was wired; a broken sink can never
|
|
216
|
+
* break the consumer app. With no sink this is a no-op beyond returning
|
|
217
|
+
* the record (so a seam can still surface it locally).
|
|
218
|
+
*
|
|
219
|
+
* @returns the tagged record for a library-origin error, or `null` when the
|
|
220
|
+
* error is app-origin (the "pass through untouched" signal).
|
|
221
|
+
*/
|
|
222
|
+
export function captureLibraryOriginError(
|
|
223
|
+
error: unknown,
|
|
224
|
+
identity: LibraryOriginIdentity,
|
|
225
|
+
sink: DevFeedbackSink | null | undefined,
|
|
226
|
+
): DevFeedbackRecord | null {
|
|
227
|
+
if (!isLibraryOriginError(error)) return null
|
|
228
|
+
|
|
229
|
+
const record = libraryOriginError({ ...identity, error })
|
|
230
|
+
|
|
231
|
+
if (sink) {
|
|
232
|
+
try {
|
|
233
|
+
sink.log(record)
|
|
234
|
+
} catch {
|
|
235
|
+
// A broken sink must never break the consumer app.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return record
|
|
240
|
+
}
|
|
@@ -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: '
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "All Refraction UI Astro components in one package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -93,7 +93,9 @@
|
|
|
93
93
|
"@refraction-ui/astro-link-card": "workspace:*",
|
|
94
94
|
"@refraction-ui/astro-card-grid": "workspace:*",
|
|
95
95
|
"@refraction-ui/astro-payment": "workspace:*",
|
|
96
|
-
"@refraction-ui/astro-command-input": "workspace:*"
|
|
96
|
+
"@refraction-ui/astro-command-input": "workspace:*",
|
|
97
|
+
"@refraction-ui/astro-logger": "workspace:*",
|
|
98
|
+
"@refraction-ui/astro-analytics": "workspace:*"
|
|
97
99
|
},
|
|
98
100
|
"publishConfig": {
|
|
99
101
|
"access": "public"
|