@refraction-ui/astro 0.17.0 → 0.17.2

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.
Files changed (41) hide show
  1. package/dist/accordion/accordion.ts +155 -0
  2. package/dist/accordion/index.ts +9 -1
  3. package/dist/analytics/analytics-manager.ts +7 -2
  4. package/dist/analytics/http-sink.ts +3 -1
  5. package/dist/analytics/types.ts +6 -0
  6. package/dist/astro-location-selector/LocationSelector.astro +56 -0
  7. package/dist/astro-location-selector/LocationSelector.stories.ts +15 -0
  8. package/dist/astro-location-selector/index.ts +1 -0
  9. package/dist/calendar/calendar.ts +11 -6
  10. package/dist/call-controls/call-controls.ts +2 -4
  11. package/dist/carousel/carousel.ts +45 -0
  12. package/dist/carousel/index.ts +9 -1
  13. package/dist/command-input/command-input.ts +5 -5
  14. package/dist/conversation/conversation.ts +9 -7
  15. package/dist/conversation/types.ts +2 -0
  16. package/dist/date-picker/date-picker.ts +6 -3
  17. package/dist/device-frame/device-frame.ts +2 -4
  18. package/dist/emoji-picker/emoji-picker.ts +2 -3
  19. package/dist/footer/footer.ts +4 -1
  20. package/dist/i18n/countries.ts +122 -0
  21. package/dist/i18n/detect-region.ts +243 -0
  22. package/dist/i18n/index.ts +47 -0
  23. package/dist/i18n/languages.ts +181 -0
  24. package/dist/i18n/locale-utils.ts +123 -0
  25. package/dist/i18n/voice-registry.ts +92 -0
  26. package/dist/index.ts +1 -0
  27. package/dist/location-selector/index.ts +1 -0
  28. package/dist/location-selector/location-selector.ts +60 -0
  29. package/dist/logger/telemetry-manager.ts +7 -4
  30. package/dist/logger/types.ts +4 -0
  31. package/dist/pagination/index.ts +14 -1
  32. package/dist/pagination/pagination.styles.ts +33 -0
  33. package/dist/pagination/pagination.ts +160 -0
  34. package/dist/presence-indicator/presence-indicator.ts +2 -4
  35. package/dist/reaction-bar/reaction-bar.ts +2 -3
  36. package/dist/slider/slider.ts +134 -2
  37. package/dist/status-indicator/status-indicator.ts +2 -4
  38. package/dist/thread-view/thread-view.ts +6 -7
  39. package/dist/toast/toast.ts +16 -5
  40. package/dist/voice-pill/voice-pill.ts +2 -4
  41. package/package.json +2 -1
@@ -1,2 +1,134 @@
1
- // Core slider logic
2
- export interface SliderProps {}
1
+ /**
2
+ * @refraction-ui/slider — headless slider.
3
+ *
4
+ * Owns the *behavior* of a single-value range slider: min/max clamping,
5
+ * step rounding, and keyboard increment/decrement semantics. It has NO UI
6
+ * opinion — adapters render the control (a native `<input type="range">` in
7
+ * React, which already applies these same keyboard rules natively) and spread
8
+ * the ARIA object.
9
+ */
10
+
11
+ /** Options for {@link createSlider}. */
12
+ export interface SliderProps {
13
+ /** Current value. Clamped into `[min, max]` and rounded to `step`. */
14
+ value?: number
15
+ /** Minimum selectable value (default 0). */
16
+ min?: number
17
+ /** Maximum selectable value (default 100). */
18
+ max?: number
19
+ /** Granularity between selectable values (default 1). */
20
+ step?: number
21
+ /** Disables the control. */
22
+ disabled?: boolean
23
+ /** Accessible label attached to the slider, when provided. */
24
+ 'aria-label'?: string
25
+ }
26
+
27
+ /** The computed view of a slider control. */
28
+ export interface SliderAPI {
29
+ /** Normalized value (clamped + step-rounded). */
30
+ value: number
31
+ min: number
32
+ max: number
33
+ step: number
34
+ /** Value position within the range, 0–100. */
35
+ percentage: number
36
+ /** ARIA attributes for the slider element (`role="slider"` + value attrs). */
37
+ ariaProps: Record<string, string | number | boolean>
38
+ /** Data attributes for styling hooks. */
39
+ dataAttributes: Record<string, string>
40
+ }
41
+
42
+ /** Clamp a value into `[min, max]`. */
43
+ export function clampSliderValue(value: number, min = 0, max = 100): number {
44
+ if (Number.isNaN(value)) return min
45
+ return Math.min(Math.max(value, min), max)
46
+ }
47
+
48
+ /**
49
+ * Round a value to the nearest multiple of `step`, offset from `min`.
50
+ * Non-positive steps leave the value unchanged. The result is re-rounded to
51
+ * the step's decimal precision to avoid float noise (e.g. 0.30000000000000004).
52
+ */
53
+ export function roundToStep(value: number, min = 0, step = 1): number {
54
+ if (!step || step <= 0 || Number.isNaN(value)) return value
55
+ const snapped = min + Math.round((value - min) / step) * step
56
+ const decimals = (String(step).split('.')[1] ?? '').length
57
+ return Number(snapped.toFixed(decimals))
58
+ }
59
+
60
+ /**
61
+ * Pure keyboard rule for a slider (WAI-ARIA slider pattern), shared by every
62
+ * adapter that does NOT render a native range input (native inputs already
63
+ * implement these keys):
64
+ *
65
+ * - ArrowRight / ArrowUp → +step
66
+ * - ArrowLeft / ArrowDown → -step
67
+ * - PageUp / PageDown → ±10×step
68
+ * - Home / End → min / max
69
+ *
70
+ * The result is clamped; unhandled keys return `current` unchanged.
71
+ */
72
+ export function getSliderValueFromKey(
73
+ current: number,
74
+ key: string,
75
+ options: { min?: number; max?: number; step?: number } = {},
76
+ ): number {
77
+ const { min = 0, max = 100, step = 1 } = options
78
+ switch (key) {
79
+ case 'ArrowRight':
80
+ case 'ArrowUp':
81
+ return clampSliderValue(current + step, min, max)
82
+ case 'ArrowLeft':
83
+ case 'ArrowDown':
84
+ return clampSliderValue(current - step, min, max)
85
+ case 'PageUp':
86
+ return clampSliderValue(current + step * 10, min, max)
87
+ case 'PageDown':
88
+ return clampSliderValue(current - step * 10, min, max)
89
+ case 'Home':
90
+ return min
91
+ case 'End':
92
+ return max
93
+ default:
94
+ return current
95
+ }
96
+ }
97
+
98
+ /**
99
+ * createSlider — the framework-agnostic view of a slider: normalized value,
100
+ * percentage position, and the ARIA object (`role="slider"`,
101
+ * `aria-valuemin/max/now`). Adapters own the value state
102
+ * (controlled/uncontrolled) and call this per render.
103
+ */
104
+ export function createSlider(props: SliderProps = {}): SliderAPI {
105
+ const min = props.min ?? 0
106
+ const max = props.max ?? 100
107
+ const step = props.step ?? 1
108
+ const value = clampSliderValue(roundToStep(props.value ?? min, min, step), min, max)
109
+ const percentage = max === min ? 0 : ((value - min) / (max - min)) * 100
110
+
111
+ const ariaProps: Record<string, string | number | boolean> = {
112
+ role: 'slider',
113
+ 'aria-valuemin': min,
114
+ 'aria-valuemax': max,
115
+ 'aria-valuenow': value,
116
+ 'aria-orientation': 'horizontal',
117
+ }
118
+ if (props.disabled) ariaProps['aria-disabled'] = true
119
+ if (props['aria-label']) ariaProps['aria-label'] = props['aria-label']
120
+
121
+ return {
122
+ value,
123
+ min,
124
+ max,
125
+ step,
126
+ percentage,
127
+ ariaProps,
128
+ dataAttributes: {
129
+ 'data-value': String(value),
130
+ 'data-min': String(min),
131
+ 'data-max': String(max),
132
+ },
133
+ }
134
+ }
@@ -1,5 +1,3 @@
1
- import type { AccessibilityProps } from '../shared/index.ts'
2
-
3
1
  export type StatusType = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'neutral'
4
2
 
5
3
  export interface StatusProps {
@@ -13,7 +11,7 @@ export interface StatusProps {
13
11
 
14
12
  export interface StatusAPI {
15
13
  /** ARIA attributes for the status indicator */
16
- ariaProps: Partial<AccessibilityProps> & Record<string, unknown>
14
+ ariaProps: Record<string, string | number | boolean>
17
15
  /** The status type */
18
16
  type: StatusType
19
17
  /** The display label */
@@ -54,7 +52,7 @@ export function createStatusIndicator(props: StatusProps): StatusAPI {
54
52
  const label = labelOverride ?? STATUS_INDICATOR_LABELS[type]
55
53
  const pulse = pulseOverride ?? (type === 'pending')
56
54
 
57
- const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {
55
+ const ariaProps: Record<string, string | number | boolean> = {
58
56
  role: 'status',
59
57
  'aria-label': label,
60
58
  }
@@ -1,4 +1,3 @@
1
- import type { AccessibilityProps } from '../shared/index.ts'
2
1
  import { generateId } from '../shared/index.ts'
3
2
 
4
3
  export interface MessageReaction {
@@ -70,11 +69,11 @@ export interface ThreadViewAPI {
70
69
  /** Format relative time (e.g., "2 minutes ago") */
71
70
  formatRelativeTime(date: Date): string
72
71
  /** ARIA props for the thread container */
73
- ariaProps: Partial<AccessibilityProps> & Record<string, unknown>
72
+ ariaProps: Record<string, string | number | boolean>
74
73
  /** Get ARIA props for a message */
75
- getMessageAriaProps(message: MessageData): Record<string, unknown>
74
+ getMessageAriaProps(message: MessageData): Record<string, string | number | boolean>
76
75
  /** Get ARIA props for a reply button */
77
- getReplyButtonAriaProps(messageId: string): Record<string, unknown>
76
+ getReplyButtonAriaProps(messageId: string): Record<string, string | number | boolean>
78
77
  /** Generated IDs */
79
78
  ids: {
80
79
  thread: string
@@ -132,14 +131,14 @@ export function createThreadView(props: ThreadViewProps): ThreadViewAPI {
132
131
  onReact?.(messageId, emoji)
133
132
  }
134
133
 
135
- const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {
134
+ const ariaProps: Record<string, string | number | boolean> = {
136
135
  role: 'log',
137
136
  'aria-label': 'Message thread',
138
137
  'aria-live': 'polite',
139
138
  id: threadId,
140
139
  }
141
140
 
142
- function getMessageAriaProps(message: MessageData): Record<string, unknown> {
141
+ function getMessageAriaProps(message: MessageData): Record<string, string | number | boolean> {
143
142
  const isOwn = currentUserId && message.author.id === currentUserId
144
143
  return {
145
144
  role: 'article',
@@ -147,7 +146,7 @@ export function createThreadView(props: ThreadViewProps): ThreadViewAPI {
147
146
  }
148
147
  }
149
148
 
150
- function getReplyButtonAriaProps(_messageId: string): Record<string, unknown> {
149
+ function getReplyButtonAriaProps(_messageId: string): Record<string, string | number | boolean> {
151
150
  return {
152
151
  role: 'button',
153
152
  'aria-label': `Reply to message`,
@@ -25,6 +25,8 @@ export interface ToastProps {
25
25
  open?: boolean
26
26
  /** Callback when open state changes */
27
27
  onOpenChange?: (open: boolean) => void
28
+ /** Elapsed-time clock for timer pause/resume math (ms). Default: ambient `Date.now()` — inject for deterministic tests. */
29
+ now?: () => number
28
30
  }
29
31
 
30
32
  export interface ToastState {
@@ -55,6 +57,8 @@ export function createToast(props: ToastProps = {}): ToastAPI {
55
57
  onOpenChange,
56
58
  } = props
57
59
 
60
+ // Injection boundary: ambient time is the default; inject `now` for deterministic timer math.
61
+ const now = props.now ?? (() => Date.now())
58
62
  let isOpen = controlledOpen ?? true
59
63
  let timerId: ReturnType<typeof setTimeout> | null = null
60
64
  let remaining = duration
@@ -77,21 +81,21 @@ export function createToast(props: ToastProps = {}): ToastAPI {
77
81
  if (duration <= 0) return
78
82
  clearTimer()
79
83
  remaining = duration
80
- startedAt = Date.now()
84
+ startedAt = now()
81
85
  timerId = setTimeout(dismiss, remaining)
82
86
  }
83
87
 
84
88
  function pauseTimer(): void {
85
89
  if (timerId === null) return
86
90
  clearTimer()
87
- remaining = remaining - (Date.now() - startedAt)
91
+ remaining = remaining - (now() - startedAt)
88
92
  if (remaining < 0) remaining = 0
89
93
  }
90
94
 
91
95
  function resumeTimer(): void {
92
96
  if (duration <= 0 || remaining <= 0) return
93
97
  clearTimer()
94
- startedAt = Date.now()
98
+ startedAt = now()
95
99
  timerId = setTimeout(dismiss, remaining)
96
100
  }
97
101
 
@@ -136,7 +140,14 @@ export interface ToastManagerAPI {
136
140
  subscribe(fn: (toasts: ReadonlyArray<ToastEntry>) => void): () => void
137
141
  }
138
142
 
139
- export function createToastManager(): ToastManagerAPI {
143
+ export interface ToastManagerConfig {
144
+ /** Clock for entry `createdAt` stamps (ms). Default: ambient `Date.now()` — inject for deterministic tests. */
145
+ now?: () => number
146
+ }
147
+
148
+ export function createToastManager(config: ToastManagerConfig = {}): ToastManagerAPI {
149
+ // Injection boundary: ambient time is the default; inject `now` for deterministic createdAt.
150
+ const now = config.now ?? (() => Date.now())
140
151
  let toasts: ToastEntry[] = []
141
152
  const listeners = new Set<(toasts: ReadonlyArray<ToastEntry>) => void>()
142
153
  const timers = new Map<string, ReturnType<typeof setTimeout>>()
@@ -171,7 +182,7 @@ export function createToastManager(): ToastManagerAPI {
171
182
  message,
172
183
  variant,
173
184
  duration,
174
- createdAt: Date.now(),
185
+ createdAt: now(),
175
186
  }
176
187
 
177
188
  toasts = [...toasts, entry]
@@ -1,5 +1,3 @@
1
- import type { AccessibilityProps } from '../shared/index.ts'
2
-
3
1
  export type VoicePillSpeaker = 'ai' | 'user' | (string & {})
4
2
 
5
3
  export type VoicePillPosition =
@@ -60,7 +58,7 @@ export interface VoicePillAPI {
60
58
  /** Invoke the mute callback when provided */
61
59
  toggleMute(): void
62
60
  /** ARIA props for the live status pill */
63
- ariaProps: Partial<AccessibilityProps> & Record<string, unknown>
61
+ ariaProps: Record<string, string | number | boolean>
64
62
  /** ARIA props for the mute toggle */
65
63
  toggleMuteAriaProps: Record<string, unknown>
66
64
  /** Data attributes reflecting speaker/state */
@@ -196,7 +194,7 @@ export function createVoicePill(props: VoicePillProps): VoicePillAPI {
196
194
  onToggleMute?.()
197
195
  }
198
196
 
199
- const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {
197
+ const ariaProps: Record<string, string | number | boolean> = {
200
198
  role: 'status',
201
199
  'aria-live': 'polite',
202
200
  'aria-atomic': true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {
@@ -84,6 +84,7 @@
84
84
  "@refraction-ui/astro-live-captions": "workspace:*",
85
85
  "@refraction-ui/astro-live-cursors": "workspace:*",
86
86
  "@refraction-ui/astro-live-transcript": "workspace:*",
87
+ "@refraction-ui/astro-location-selector": "workspace:*",
87
88
  "@refraction-ui/astro-logger": "workspace:*",
88
89
  "@refraction-ui/astro-markdown-renderer": "workspace:*",
89
90
  "@refraction-ui/astro-marquee-strip": "workspace:*",