@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.
- package/dist/accordion/accordion.ts +155 -0
- package/dist/accordion/index.ts +9 -1
- package/dist/analytics/analytics-manager.ts +7 -2
- package/dist/analytics/http-sink.ts +3 -1
- package/dist/analytics/types.ts +6 -0
- package/dist/astro-location-selector/LocationSelector.astro +56 -0
- package/dist/astro-location-selector/LocationSelector.stories.ts +15 -0
- package/dist/astro-location-selector/index.ts +1 -0
- package/dist/calendar/calendar.ts +11 -6
- package/dist/call-controls/call-controls.ts +2 -4
- package/dist/carousel/carousel.ts +45 -0
- package/dist/carousel/index.ts +9 -1
- package/dist/command-input/command-input.ts +5 -5
- package/dist/conversation/conversation.ts +9 -7
- package/dist/conversation/types.ts +2 -0
- package/dist/date-picker/date-picker.ts +6 -3
- package/dist/device-frame/device-frame.ts +2 -4
- package/dist/emoji-picker/emoji-picker.ts +2 -3
- package/dist/footer/footer.ts +4 -1
- package/dist/i18n/countries.ts +122 -0
- package/dist/i18n/detect-region.ts +243 -0
- package/dist/i18n/index.ts +47 -0
- package/dist/i18n/languages.ts +181 -0
- package/dist/i18n/locale-utils.ts +123 -0
- package/dist/i18n/voice-registry.ts +92 -0
- package/dist/index.ts +1 -0
- package/dist/location-selector/index.ts +1 -0
- package/dist/location-selector/location-selector.ts +60 -0
- package/dist/logger/telemetry-manager.ts +7 -4
- package/dist/logger/types.ts +4 -0
- package/dist/pagination/index.ts +14 -1
- package/dist/pagination/pagination.styles.ts +33 -0
- package/dist/pagination/pagination.ts +160 -0
- package/dist/presence-indicator/presence-indicator.ts +2 -4
- package/dist/reaction-bar/reaction-bar.ts +2 -3
- package/dist/slider/slider.ts +134 -2
- package/dist/status-indicator/status-indicator.ts +2 -4
- package/dist/thread-view/thread-view.ts +6 -7
- package/dist/toast/toast.ts +16 -5
- package/dist/voice-pill/voice-pill.ts +2 -4
- package/package.json +2 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @refraction-ui/accordion — headless accordion (disclosure) state machine.
|
|
3
|
+
*
|
|
4
|
+
* Owns the *behavior* of an accordion: which item(s) are open, the single vs
|
|
5
|
+
* multiple expansion modes, and the collapsible toggle rule. It has NO UI
|
|
6
|
+
* opinion — React/Astro adapters subscribe to the store and render the parts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Expansion mode: one item open at a time, or any number. */
|
|
10
|
+
export type AccordionType = 'single' | 'multiple'
|
|
11
|
+
|
|
12
|
+
/** Open item id (`single`) or ids (`multiple`). */
|
|
13
|
+
export type AccordionValue = string | string[]
|
|
14
|
+
|
|
15
|
+
/** Options for {@link createAccordion}. */
|
|
16
|
+
export interface AccordionConfig {
|
|
17
|
+
/** Expansion mode (default `'single'`). */
|
|
18
|
+
type?: AccordionType
|
|
19
|
+
/** When `type="single"`, allows the open item to be toggled fully closed. */
|
|
20
|
+
collapsible?: boolean
|
|
21
|
+
/** Controlled open item(s). Pair with `onValueChange`. */
|
|
22
|
+
value?: AccordionValue
|
|
23
|
+
/** Uncontrolled initial open item(s). */
|
|
24
|
+
defaultValue?: AccordionValue
|
|
25
|
+
/** Called whenever the open item(s) change. */
|
|
26
|
+
onValueChange?: (value: AccordionValue) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Snapshot of the store. Returned by `getState()`; treat as immutable. */
|
|
30
|
+
export interface AccordionState {
|
|
31
|
+
type: AccordionType
|
|
32
|
+
collapsible: boolean
|
|
33
|
+
/** Current open item id (`single`) or ids (`multiple`). */
|
|
34
|
+
value: AccordionValue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The framework-agnostic store. React/Astro adapters wrap this. */
|
|
38
|
+
export interface AccordionAPI {
|
|
39
|
+
/** Current immutable snapshot */
|
|
40
|
+
getState(): AccordionState
|
|
41
|
+
/** Subscribe to changes; returns an unsubscribe fn (suits useSyncExternalStore) */
|
|
42
|
+
subscribe(listener: () => void): () => void
|
|
43
|
+
/** Toggle an item open/closed per the type/collapsible rules */
|
|
44
|
+
toggleItem(itemValue: string): void
|
|
45
|
+
/** Whether an item is currently open */
|
|
46
|
+
isOpen(itemValue: string): boolean
|
|
47
|
+
/** Sync a controlled value into the store (adapters call when the prop changes) */
|
|
48
|
+
setValue(value: AccordionValue): void
|
|
49
|
+
/** Update options after creation (adapters keep props in sync) */
|
|
50
|
+
setOptions(options: {
|
|
51
|
+
type?: AccordionType
|
|
52
|
+
collapsible?: boolean
|
|
53
|
+
onValueChange?: (value: AccordionValue) => void
|
|
54
|
+
}): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pure open-state check shared by every adapter. */
|
|
58
|
+
export function isItemOpen(state: AccordionState, itemValue: string): boolean {
|
|
59
|
+
return state.type === 'single'
|
|
60
|
+
? state.value === itemValue
|
|
61
|
+
: Array.isArray(state.value) && state.value.includes(itemValue)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Shallow value equality (arrays compared item-by-item, order-sensitive). */
|
|
65
|
+
function valueEquals(a: AccordionValue, b: AccordionValue): boolean {
|
|
66
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
67
|
+
return a.length === b.length && a.every((v, i) => v === b[i])
|
|
68
|
+
}
|
|
69
|
+
return a === b
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* createAccordion — headless accordion store.
|
|
74
|
+
*
|
|
75
|
+
* Supports controlled (`value`) and uncontrolled (`defaultValue`) usage.
|
|
76
|
+
* `single` mode holds one open item id (`''` when all closed); `multiple`
|
|
77
|
+
* holds an array of open ids. In `single` mode, re-toggling the open item
|
|
78
|
+
* closes it only when `collapsible` is set.
|
|
79
|
+
*/
|
|
80
|
+
export function createAccordion(config: AccordionConfig = {}): AccordionAPI {
|
|
81
|
+
let type: AccordionType = config.type ?? 'single'
|
|
82
|
+
let collapsible: boolean = config.collapsible ?? false
|
|
83
|
+
let onValueChange = config.onValueChange
|
|
84
|
+
|
|
85
|
+
let uncontrolledValue: AccordionValue =
|
|
86
|
+
config.defaultValue ?? (type === 'multiple' ? [] : '')
|
|
87
|
+
let controlledValue: AccordionValue | undefined = config.value
|
|
88
|
+
|
|
89
|
+
const listeners = new Set<() => void>()
|
|
90
|
+
let snapshot: AccordionState = buildSnapshot()
|
|
91
|
+
|
|
92
|
+
function currentValue(): AccordionValue {
|
|
93
|
+
return controlledValue !== undefined ? controlledValue : uncontrolledValue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildSnapshot(): AccordionState {
|
|
97
|
+
return { type, collapsible, value: currentValue() }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function emit(): void {
|
|
101
|
+
snapshot = buildSnapshot()
|
|
102
|
+
for (const l of listeners) l()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const api: AccordionAPI = {
|
|
106
|
+
getState() {
|
|
107
|
+
return snapshot
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
subscribe(listener) {
|
|
111
|
+
listeners.add(listener)
|
|
112
|
+
return () => listeners.delete(listener)
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
toggleItem(itemValue) {
|
|
116
|
+
const value = currentValue()
|
|
117
|
+
let next: AccordionValue
|
|
118
|
+
if (type === 'single') {
|
|
119
|
+
next = value === itemValue && collapsible ? '' : itemValue
|
|
120
|
+
} else {
|
|
121
|
+
const arrValue = Array.isArray(value) ? value : []
|
|
122
|
+
next = arrValue.includes(itemValue)
|
|
123
|
+
? arrValue.filter((v) => v !== itemValue)
|
|
124
|
+
: [...arrValue, itemValue]
|
|
125
|
+
}
|
|
126
|
+
if (controlledValue === undefined) {
|
|
127
|
+
if (valueEquals(uncontrolledValue, next)) return
|
|
128
|
+
uncontrolledValue = next
|
|
129
|
+
}
|
|
130
|
+
onValueChange?.(next)
|
|
131
|
+
emit()
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
isOpen(itemValue) {
|
|
135
|
+
return isItemOpen(snapshot, itemValue)
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
setValue(value) {
|
|
139
|
+
if (controlledValue !== undefined && valueEquals(controlledValue, value)) return
|
|
140
|
+
controlledValue = value
|
|
141
|
+
emit()
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
setOptions(options) {
|
|
145
|
+
const prevType = type
|
|
146
|
+
const prevCollapsible = collapsible
|
|
147
|
+
if (options.type !== undefined) type = options.type
|
|
148
|
+
if (options.collapsible !== undefined) collapsible = options.collapsible
|
|
149
|
+
if (options.onValueChange !== undefined) onValueChange = options.onValueChange
|
|
150
|
+
if (type !== prevType || collapsible !== prevCollapsible) emit()
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return api
|
|
155
|
+
}
|
package/dist/accordion/index.ts
CHANGED
|
@@ -64,6 +64,9 @@ export function createAnalytics(config: AnalyticsConfig): Analytics {
|
|
|
64
64
|
const sampleRate = config.sampleRate ?? 1
|
|
65
65
|
const batchSize = config.batchSize ?? 20
|
|
66
66
|
const flushIntervalMs = config.flushIntervalMs ?? 10_000
|
|
67
|
+
// Injection boundary: ambient time/randomness default here; inject `now`/`random` for determinism.
|
|
68
|
+
const now = config.now ?? (() => new Date())
|
|
69
|
+
const random = config.random ?? (() => Math.random())
|
|
67
70
|
|
|
68
71
|
const session = createSession(config.session)
|
|
69
72
|
const identity = createIdentity(config.identity)
|
|
@@ -86,6 +89,8 @@ export function createAnalytics(config: AnalyticsConfig): Analytics {
|
|
|
86
89
|
createHttpSink({
|
|
87
90
|
endpoint: config.endpoint,
|
|
88
91
|
writeKey: config.writeKey ?? '',
|
|
92
|
+
// One clock for the whole manager: the auto sink stamps sentAt with it too.
|
|
93
|
+
now,
|
|
89
94
|
}),
|
|
90
95
|
)
|
|
91
96
|
}
|
|
@@ -203,7 +208,7 @@ export function createAnalytics(config: AnalyticsConfig): Analytics {
|
|
|
203
208
|
function sampled(): boolean {
|
|
204
209
|
if (sampleRate >= 1) return true
|
|
205
210
|
if (sampleRate <= 0) return false
|
|
206
|
-
return
|
|
211
|
+
return random() < sampleRate
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
function enqueue(ev: AnalyticsEvent): void {
|
|
@@ -238,7 +243,7 @@ export function createAnalytics(config: AnalyticsConfig): Analytics {
|
|
|
238
243
|
userId: identity.userId(),
|
|
239
244
|
sessionId,
|
|
240
245
|
context: buildContext(opts?.context, childCtx),
|
|
241
|
-
timestamp: opts?.timestamp ??
|
|
246
|
+
timestamp: opts?.timestamp ?? now().toISOString(),
|
|
242
247
|
schemaVersion: SCHEMA_VERSION,
|
|
243
248
|
...fields,
|
|
244
249
|
}
|
|
@@ -113,6 +113,8 @@ export function createHttpSink(options: HttpSinkOptions): AnalyticsSink {
|
|
|
113
113
|
const base = endpoint.replace(/\/+$/, '')
|
|
114
114
|
const url = `${base}/v${SCHEMA_VERSION}/batch`
|
|
115
115
|
const authHeader = `Basic ${base64(`${writeKey}:`)}`
|
|
116
|
+
// Injection boundary: `sentAt` defaults to ambient time; inject `now` for determinism.
|
|
117
|
+
const now = options.now ?? (() => new Date())
|
|
116
118
|
|
|
117
119
|
const resolveFetch = (): typeof fetch => {
|
|
118
120
|
if (options.fetchImpl) return options.fetchImpl
|
|
@@ -137,7 +139,7 @@ export function createHttpSink(options: HttpSinkOptions): AnalyticsSink {
|
|
|
137
139
|
function envelope(batch: AnalyticsEvent[]) {
|
|
138
140
|
return {
|
|
139
141
|
batch,
|
|
140
|
-
sentAt:
|
|
142
|
+
sentAt: now().toISOString(),
|
|
141
143
|
batchId: uuidv4(),
|
|
142
144
|
}
|
|
143
145
|
}
|
package/dist/analytics/types.ts
CHANGED
|
@@ -187,6 +187,8 @@ export interface HttpSinkOptions {
|
|
|
187
187
|
maxBatchBytes?: number
|
|
188
188
|
/** Soft per-event byte cap (Segment ≈ 32KB). Default 32_000. */
|
|
189
189
|
maxEventBytes?: number
|
|
190
|
+
/** Clock for the batch `sentAt` stamp. Default: ambient `new Date()` — inject for deterministic tests. */
|
|
191
|
+
now?: () => Date
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
/** `createAnalytics` configuration. */
|
|
@@ -222,6 +224,10 @@ export interface AnalyticsConfig {
|
|
|
222
224
|
batchSize?: number
|
|
223
225
|
/** Auto-flush interval in ms (prod). Default 10_000. */
|
|
224
226
|
flushIntervalMs?: number
|
|
227
|
+
/** Clock for default event timestamps. Default: ambient `new Date()` — inject for deterministic tests/SSR. */
|
|
228
|
+
now?: () => Date
|
|
229
|
+
/** RNG for sample-rate decisions. Default: ambient `Math.random` — inject for deterministic tests. */
|
|
230
|
+
random?: () => number
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
/** Options accepted by every top-level call. */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { createLocationSelector } from '../location-selector/index.ts'
|
|
3
|
+
import { cn } from '../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
defaultCountry?: string
|
|
7
|
+
defaultLanguage?: string
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
defaultCountry = 'US',
|
|
13
|
+
defaultLanguage = 'en',
|
|
14
|
+
className
|
|
15
|
+
} = Astro.props
|
|
16
|
+
|
|
17
|
+
const api = createLocationSelector({
|
|
18
|
+
defaultCountry,
|
|
19
|
+
defaultLanguage
|
|
20
|
+
})
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<div class={cn('flex flex-col gap-4 sm:flex-row', className)}>
|
|
24
|
+
<div class="flex flex-col gap-1.5 flex-1">
|
|
25
|
+
<label for={api.countryProps.id} class="text-sm font-medium">Country</label>
|
|
26
|
+
<select
|
|
27
|
+
id={api.countryProps.id}
|
|
28
|
+
name={api.countryProps.name}
|
|
29
|
+
class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
30
|
+
>
|
|
31
|
+
{api.countries.map((c: any) => {
|
|
32
|
+
const value = c.code || c.id || c;
|
|
33
|
+
const label = c.name || c.label || c;
|
|
34
|
+
return (
|
|
35
|
+
<option value={value} selected={value === defaultCountry}>{label}</option>
|
|
36
|
+
)
|
|
37
|
+
})}
|
|
38
|
+
</select>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="flex flex-col gap-1.5 flex-1">
|
|
41
|
+
<label for={api.languageProps.id} class="text-sm font-medium">Language</label>
|
|
42
|
+
<select
|
|
43
|
+
id={api.languageProps.id}
|
|
44
|
+
name={api.languageProps.name}
|
|
45
|
+
class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
46
|
+
>
|
|
47
|
+
{api.languages.map((l: any) => {
|
|
48
|
+
const value = l.code || l.id || l;
|
|
49
|
+
const label = l.name || l.label || l;
|
|
50
|
+
return (
|
|
51
|
+
<option value={value} selected={value === defaultLanguage}>{label}</option>
|
|
52
|
+
)
|
|
53
|
+
})}
|
|
54
|
+
</select>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Component from './LocationSelector.astro'
|
|
2
|
+
|
|
3
|
+
const meta = {
|
|
4
|
+
title: 'Astro/LocationSelector',
|
|
5
|
+
component: Component,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default meta
|
|
9
|
+
|
|
10
|
+
export const Default = {
|
|
11
|
+
args: {
|
|
12
|
+
defaultCountry: 'Example defaultCountry',
|
|
13
|
+
defaultLanguage: 'Example defaultLanguage'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LocationSelector } from './LocationSelector.astro'
|
|
@@ -18,6 +18,8 @@ export interface CalendarProps {
|
|
|
18
18
|
maxDate?: Date
|
|
19
19
|
/** Specific dates to disable */
|
|
20
20
|
disabledDates?: Date[]
|
|
21
|
+
/** Reference "today" for isToday/aria-current. Default: ambient `new Date()` — inject to avoid SSR/CSR hydration mismatch. */
|
|
22
|
+
today?: Date
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface CalendarDay {
|
|
@@ -52,9 +54,9 @@ export interface CalendarAPI {
|
|
|
52
54
|
/** Select a date */
|
|
53
55
|
select(date: Date): void
|
|
54
56
|
/** ARIA attributes for the calendar grid */
|
|
55
|
-
ariaProps: Partial<AccessibilityProps> & Record<string,
|
|
57
|
+
ariaProps: Partial<AccessibilityProps> & Record<string, string | number | boolean>
|
|
56
58
|
/** Get ARIA attributes for a specific day cell */
|
|
57
|
-
getDayAriaProps(day: CalendarDay): Partial<AccessibilityProps> & Record<string,
|
|
59
|
+
getDayAriaProps(day: CalendarDay): Partial<AccessibilityProps> & Record<string, string | number | boolean>
|
|
58
60
|
/** Generated IDs */
|
|
59
61
|
ids: {
|
|
60
62
|
grid: string
|
|
@@ -89,9 +91,11 @@ export function createCalendar(props: CalendarProps = {}): CalendarAPI {
|
|
|
89
91
|
minDate,
|
|
90
92
|
maxDate,
|
|
91
93
|
disabledDates = [],
|
|
94
|
+
today: injectedToday,
|
|
92
95
|
} = props
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
// Injection boundary: ambient "today" is the default; inject `today` so SSR and client agree.
|
|
98
|
+
const today = injectedToday ?? new Date()
|
|
95
99
|
|
|
96
100
|
// Determine selected date
|
|
97
101
|
const selectedDate = controlledValue ?? defaultValue
|
|
@@ -167,18 +171,19 @@ export function createCalendar(props: CalendarProps = {}): CalendarAPI {
|
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
const ariaProps: Partial<AccessibilityProps> & Record<string,
|
|
174
|
+
const ariaProps: Partial<AccessibilityProps> & Record<string, string | number | boolean> = {
|
|
171
175
|
role: 'grid',
|
|
172
176
|
'aria-labelledby': labelId,
|
|
173
177
|
id: gridId,
|
|
174
178
|
}
|
|
175
179
|
|
|
176
|
-
function getDayAriaProps(day: CalendarDay): Partial<AccessibilityProps> & Record<string,
|
|
180
|
+
function getDayAriaProps(day: CalendarDay): Partial<AccessibilityProps> & Record<string, string | number | boolean> {
|
|
177
181
|
return {
|
|
178
182
|
role: 'gridcell',
|
|
179
183
|
'aria-selected': day.isSelected,
|
|
180
184
|
'aria-disabled': day.isDisabled,
|
|
181
|
-
|
|
185
|
+
// Key absent (not undefined) when not today — the ARIA record type excludes undefined.
|
|
186
|
+
...(day.isToday ? { 'aria-current': 'date' as const } : {}),
|
|
182
187
|
'aria-label': `${DAY_NAMES[day.date.getDay()]}, ${day.date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}`,
|
|
183
188
|
}
|
|
184
189
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
-
|
|
3
1
|
/** Visual/semantic tone of a call control button. */
|
|
4
2
|
export type CallControlTone = 'default' | 'active' | 'destructive'
|
|
5
3
|
|
|
@@ -33,7 +31,7 @@ export interface CallControlsProps {
|
|
|
33
31
|
|
|
34
32
|
export interface CallControlsAPI {
|
|
35
33
|
/** ARIA attributes to spread on the toolbar element (`role="toolbar"`). */
|
|
36
|
-
ariaProps:
|
|
34
|
+
ariaProps: Record<string, string | number | boolean>
|
|
37
35
|
/** Data attributes for styling hooks. */
|
|
38
36
|
dataAttributes: Record<string, string>
|
|
39
37
|
}
|
|
@@ -66,7 +64,7 @@ export function controlTone(
|
|
|
66
64
|
* attributes; adapters spread these onto their container element.
|
|
67
65
|
*/
|
|
68
66
|
export function createCallControls(_props: CallControlsProps = {}): CallControlsAPI {
|
|
69
|
-
const ariaProps:
|
|
67
|
+
const ariaProps: Record<string, string | number | boolean> = {
|
|
70
68
|
role: 'toolbar',
|
|
71
69
|
'aria-label': 'Call controls',
|
|
72
70
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @refraction-ui/carousel — headless "carousel" store.
|
|
3
|
+
*
|
|
4
|
+
* Honest note: despite the name, this component is an expand/collapse panel
|
|
5
|
+
* set (an accordion in disguise) — that is the behavior the React adapter and
|
|
6
|
+
* the docs site demo (single/multiple open panels, collapsible toggles). The
|
|
7
|
+
* state machine is therefore shared with `@refraction-ui/accordion` rather
|
|
8
|
+
* than duplicated. A real slide-navigation carousel (active index, next/prev)
|
|
9
|
+
* would be a different core; this package keeps the shipped behavior.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
createAccordion,
|
|
13
|
+
isItemOpen,
|
|
14
|
+
type AccordionAPI,
|
|
15
|
+
type AccordionConfig,
|
|
16
|
+
type AccordionState,
|
|
17
|
+
type AccordionType,
|
|
18
|
+
type AccordionValue,
|
|
19
|
+
} from '../accordion/index.ts'
|
|
20
|
+
|
|
21
|
+
/** Expansion mode: one item open at a time, or any number. */
|
|
22
|
+
export type CarouselType = AccordionType
|
|
23
|
+
|
|
24
|
+
/** Open item id (`single`) or ids (`multiple`). */
|
|
25
|
+
export type CarouselValue = AccordionValue
|
|
26
|
+
|
|
27
|
+
/** Options for {@link createCarousel}. Mirrors {@link AccordionConfig}. */
|
|
28
|
+
export interface CarouselConfig extends AccordionConfig {}
|
|
29
|
+
|
|
30
|
+
/** Snapshot of the store. Returned by `getState()`; treat as immutable. */
|
|
31
|
+
export interface CarouselState extends AccordionState {}
|
|
32
|
+
|
|
33
|
+
/** The framework-agnostic store. React/Astro adapters wrap this. */
|
|
34
|
+
export type CarouselAPI = AccordionAPI
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* createCarousel — headless expand/collapse panel store. This is the
|
|
38
|
+
* accordion state machine under the carousel name; see the package note.
|
|
39
|
+
*/
|
|
40
|
+
export function createCarousel(config: CarouselConfig = {}): CarouselAPI {
|
|
41
|
+
return createAccordion(config)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Pure open-state check shared by every adapter. */
|
|
45
|
+
export { isItemOpen }
|
package/dist/carousel/index.ts
CHANGED
|
@@ -97,10 +97,10 @@ export class CommandInput {
|
|
|
97
97
|
const matchedTrigger = this.options.triggers.find((t) => t.char === char);
|
|
98
98
|
|
|
99
99
|
if (matchedTrigger) {
|
|
100
|
-
const isStartOfWord = i === 0 ||
|
|
100
|
+
const isStartOfWord = i === 0 || /\s/.test(rawText[i - 1]);
|
|
101
101
|
if (isStartOfWord) {
|
|
102
102
|
const commandText = rawText.slice(i + 1, cursorPosition);
|
|
103
|
-
if (
|
|
103
|
+
if (!/\s/.test(commandText)) {
|
|
104
104
|
let isValid = true;
|
|
105
105
|
if (matchedTrigger.pattern) {
|
|
106
106
|
isValid = matchedTrigger.pattern.test(commandText);
|
|
@@ -118,7 +118,7 @@ export class CommandInput {
|
|
|
118
118
|
break;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
if (
|
|
121
|
+
if (/\s/.test(char)) {
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -130,8 +130,8 @@ export class CommandInput {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const triggerChars = this.options.triggers.map((t) => t.char.replace(/[.*+?^${}()|[\\]
|
|
134
|
-
const patternStr = `(
|
|
133
|
+
const triggerChars = this.options.triggers.map((t) => t.char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
134
|
+
const patternStr = `(^|\\s)(${triggerChars.join('|')})([^\\s]*)`;
|
|
135
135
|
const regex = new RegExp(patternStr, 'g');
|
|
136
136
|
|
|
137
137
|
let currentIndex = 0;
|
|
@@ -33,6 +33,8 @@ export function createConversation(config: ConversationConfig = {}): Conversatio
|
|
|
33
33
|
const assistant = config.assistant ?? DEFAULT_ASSISTANT
|
|
34
34
|
const currentUser = config.currentUser ?? DEFAULT_USER
|
|
35
35
|
const generateTitle = config.generateTitle ?? defaultGenerateTitle
|
|
36
|
+
// Injection boundary: ambient time is the default; inject `now` for deterministic timestamps.
|
|
37
|
+
const now = config.now ?? (() => new Date())
|
|
36
38
|
|
|
37
39
|
let transport = config.transport
|
|
38
40
|
let threadingMode: ThreadingMode = config.threadingMode ?? 'inline'
|
|
@@ -87,7 +89,7 @@ export function createConversation(config: ConversationConfig = {}): Conversatio
|
|
|
87
89
|
|
|
88
90
|
function touch(conversationId: string): void {
|
|
89
91
|
const c = conversations.get(conversationId)
|
|
90
|
-
if (c) conversations.set(conversationId, { ...c, updatedAt:
|
|
92
|
+
if (c) conversations.set(conversationId, { ...c, updatedAt: now() })
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
function ensureActiveConversation(opts?: SendOptions): string {
|
|
@@ -103,12 +105,12 @@ export function createConversation(config: ConversationConfig = {}): Conversatio
|
|
|
103
105
|
opts: { title?: string; metadata?: Record<string, unknown> },
|
|
104
106
|
id?: string,
|
|
105
107
|
): Conversation {
|
|
106
|
-
const
|
|
108
|
+
const timestamp = now()
|
|
107
109
|
const conversation: Conversation = {
|
|
108
110
|
id: id ?? generateId('rfr-conv'),
|
|
109
111
|
title: opts.title ?? DEFAULT_TITLE,
|
|
110
|
-
createdAt:
|
|
111
|
-
updatedAt:
|
|
112
|
+
createdAt: timestamp,
|
|
113
|
+
updatedAt: timestamp,
|
|
112
114
|
metadata: opts.metadata,
|
|
113
115
|
}
|
|
114
116
|
conversations.set(conversation.id, conversation)
|
|
@@ -294,7 +296,7 @@ export function createConversation(config: ConversationConfig = {}): Conversatio
|
|
|
294
296
|
role: 'user',
|
|
295
297
|
author: currentUser,
|
|
296
298
|
content: trimmed,
|
|
297
|
-
timestamp:
|
|
299
|
+
timestamp: now(),
|
|
298
300
|
status: 'sent',
|
|
299
301
|
parentId,
|
|
300
302
|
replyToId,
|
|
@@ -323,7 +325,7 @@ export function createConversation(config: ConversationConfig = {}): Conversatio
|
|
|
323
325
|
role: 'assistant',
|
|
324
326
|
author: assistant,
|
|
325
327
|
content: '',
|
|
326
|
-
timestamp:
|
|
328
|
+
timestamp: now(),
|
|
327
329
|
status: 'streaming',
|
|
328
330
|
parentId,
|
|
329
331
|
}
|
|
@@ -350,7 +352,7 @@ export function createConversation(config: ConversationConfig = {}): Conversatio
|
|
|
350
352
|
role: 'assistant',
|
|
351
353
|
author: assistant,
|
|
352
354
|
content: '',
|
|
353
|
-
timestamp:
|
|
355
|
+
timestamp: now(),
|
|
354
356
|
status: 'streaming',
|
|
355
357
|
parentId,
|
|
356
358
|
}
|
|
@@ -170,6 +170,8 @@ export interface ConversationConfig {
|
|
|
170
170
|
generateTitle?: (firstMessage: string) => string
|
|
171
171
|
/** Threading mode (default 'inline') */
|
|
172
172
|
threadingMode?: ThreadingMode
|
|
173
|
+
/** Clock for conversation/message timestamps. Default: ambient `new Date()` — inject for deterministic SSR/tests. */
|
|
174
|
+
now?: () => Date
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
/** Options for a single `sendMessage`. */
|
|
@@ -26,6 +26,8 @@ export interface DatePickerProps {
|
|
|
26
26
|
onOpenChange?: (open: boolean) => void
|
|
27
27
|
/** The currently displayed month (controlled) */
|
|
28
28
|
currentMonth?: Date
|
|
29
|
+
/** Reference "today" for isToday/aria-current and time defaults. Default: ambient `new Date()` — inject to avoid SSR/CSR hydration mismatch. */
|
|
30
|
+
today?: Date
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface DatePickerState {
|
|
@@ -149,7 +151,8 @@ export function createDatePicker(props: DatePickerProps = {}): DatePickerAPI {
|
|
|
149
151
|
onOpenChange,
|
|
150
152
|
} = props
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
// Injection boundary: ambient "today" is the default; inject `today` so SSR and client agree.
|
|
155
|
+
const today = props.today ?? new Date()
|
|
153
156
|
let internalOpen = controlledOpen ?? defaultOpen
|
|
154
157
|
let currentMonth = props.currentMonth ?? (value ? startOfMonth(value) : startOfMonth(today))
|
|
155
158
|
let view: DatePickerView = 'calendar'
|
|
@@ -230,14 +233,14 @@ export function createDatePicker(props: DatePickerProps = {}): DatePickerAPI {
|
|
|
230
233
|
|
|
231
234
|
function setHours(h: number): void {
|
|
232
235
|
if (h < 0 || h > 23) return
|
|
233
|
-
const newDate = value ? new Date(value) : new Date()
|
|
236
|
+
const newDate = value ? new Date(value) : new Date(today.getTime())
|
|
234
237
|
newDate.setHours(h)
|
|
235
238
|
onChange?.(newDate)
|
|
236
239
|
}
|
|
237
240
|
|
|
238
241
|
function setMinutes(m: number): void {
|
|
239
242
|
if (m < 0 || m > 59) return
|
|
240
|
-
const newDate = value ? new Date(value) : new Date()
|
|
243
|
+
const newDate = value ? new Date(value) : new Date(today.getTime())
|
|
241
244
|
newDate.setMinutes(m)
|
|
242
245
|
onChange?.(newDate)
|
|
243
246
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
-
|
|
3
1
|
export type DeviceType = 'iphone' | 'ipad' | 'android-phone' | 'android-tablet'
|
|
4
2
|
export type DeviceOrientation = 'portrait' | 'landscape'
|
|
5
3
|
|
|
@@ -27,7 +25,7 @@ export interface DeviceFrameAPI {
|
|
|
27
25
|
/** Computed dimensions for the device */
|
|
28
26
|
dimensions: DeviceDimensions
|
|
29
27
|
/** ARIA attributes for the device frame */
|
|
30
|
-
ariaProps:
|
|
28
|
+
ariaProps: Record<string, string | number | boolean>
|
|
31
29
|
/** Data attributes for styling hooks */
|
|
32
30
|
dataAttributes: Record<string, string>
|
|
33
31
|
}
|
|
@@ -92,7 +90,7 @@ export function createDeviceFrame(props: DeviceFrameProps): DeviceFrameAPI {
|
|
|
92
90
|
homeIndicator: spec.homeIndicator,
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
const ariaProps:
|
|
93
|
+
const ariaProps: Record<string, string | number | boolean> = {
|
|
96
94
|
role: 'img',
|
|
97
95
|
'aria-label': `${DEVICE_LABELS[device]} device frame in ${orientation} orientation`,
|
|
98
96
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
1
|
import { generateId } from '../shared/index.ts'
|
|
3
2
|
import {
|
|
4
3
|
EMOJI_DATA,
|
|
@@ -39,7 +38,7 @@ export interface EmojiPickerAPI {
|
|
|
39
38
|
/** Category tabs with labels */
|
|
40
39
|
categoryTabs: { category: EmojiCategory; label: string; emoji: string }[]
|
|
41
40
|
/** ARIA props for the picker container */
|
|
42
|
-
ariaProps:
|
|
41
|
+
ariaProps: Record<string, string | number | boolean>
|
|
43
42
|
/** ARIA props for the search input */
|
|
44
43
|
searchInputProps: Record<string, unknown>
|
|
45
44
|
/** Get ARIA props for an emoji button */
|
|
@@ -118,7 +117,7 @@ export function createEmojiPicker(props: EmojiPickerProps = {}): EmojiPickerAPI
|
|
|
118
117
|
emoji: CATEGORY_ICONS[cat],
|
|
119
118
|
}))
|
|
120
119
|
|
|
121
|
-
const ariaProps:
|
|
120
|
+
const ariaProps: Record<string, string | number | boolean> = {
|
|
122
121
|
role: 'dialog',
|
|
123
122
|
'aria-labelledby': labelId,
|
|
124
123
|
id: containerId,
|
package/dist/footer/footer.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface FooterProps {
|
|
|
13
13
|
copyright?: string
|
|
14
14
|
socialLinks?: SocialLink[]
|
|
15
15
|
columns?: FooterColumn[]
|
|
16
|
+
/** Year for the default copyrightText. Default: ambient current year — inject to avoid year-boundary SSR/CSR mismatch. */
|
|
17
|
+
year?: number
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface FooterAPI {
|
|
@@ -22,7 +24,8 @@ export interface FooterAPI {
|
|
|
22
24
|
|
|
23
25
|
export function createFooter(props: FooterProps = {}): FooterAPI {
|
|
24
26
|
const { copyright } = props
|
|
25
|
-
|
|
27
|
+
// Injection boundary: ambient year is the default; inject `year` so SSR and client agree.
|
|
28
|
+
const year = props.year ?? new Date().getFullYear()
|
|
26
29
|
const copyrightText = copyright ?? `© ${year} All rights reserved.`
|
|
27
30
|
|
|
28
31
|
return {
|