@refraction-ui/astro 0.11.1 → 0.13.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-audio-room/AudioRoom.astro +81 -0
- package/dist/astro-audio-room/index.ts +1 -0
- package/dist/astro-call-controls/CallControls.astro +57 -0
- package/dist/astro-call-controls/index.ts +1 -0
- package/dist/astro-checklist/Checklist.astro +76 -0
- package/dist/astro-checklist/index.ts +1 -0
- package/dist/astro-editor-status-bar/EditorStatusBar.astro +92 -0
- package/dist/astro-editor-status-bar/index.ts +1 -0
- package/dist/astro-editor-tabs/EditorTabs.astro +74 -0
- package/dist/astro-editor-tabs/index.ts +1 -0
- package/dist/astro-empty-state/EmptyState.astro +57 -0
- package/dist/astro-empty-state/index.ts +1 -0
- package/dist/astro-floating-reactions/FloatingReactions.astro +56 -0
- package/dist/astro-floating-reactions/index.ts +1 -0
- package/dist/astro-flow-editor/FlowEditor.astro +99 -0
- package/dist/astro-flow-editor/index.ts +3 -0
- package/dist/astro-graph-view/GraphView.astro +109 -0
- package/dist/astro-graph-view/index.ts +1 -0
- package/dist/astro-infinite-canvas/InfiniteCanvas.astro +43 -0
- package/dist/astro-infinite-canvas/index.ts +1 -0
- package/dist/astro-input/Input.astro +5 -1
- package/dist/astro-live-captions/LiveCaptions.astro +54 -0
- package/dist/astro-live-captions/index.ts +1 -0
- package/dist/astro-live-cursors/LiveCursors.astro +55 -0
- package/dist/astro-live-cursors/index.ts +1 -0
- package/dist/astro-live-transcript/LiveTranscript.astro +55 -0
- package/dist/astro-live-transcript/index.ts +1 -0
- package/dist/astro-mini-map/MiniMap.astro +74 -0
- package/dist/astro-mini-map/index.ts +1 -0
- package/dist/astro-password-input/PasswordInput.astro +102 -0
- package/dist/astro-password-input/index.ts +1 -0
- package/dist/astro-pre-call-lobby/PreCallLobby.astro +202 -0
- package/dist/astro-pre-call-lobby/index.ts +1 -0
- package/dist/astro-radial-gauge/RadialGauge.astro +144 -0
- package/dist/astro-radial-gauge/index.ts +1 -0
- package/dist/astro-rating-scale/RatingScale.astro +73 -0
- package/dist/astro-rating-scale/index.ts +1 -0
- package/dist/astro-segmented-control/SegmentedControl.astro +106 -0
- package/dist/astro-segmented-control/SegmentedControlItem.astro +32 -0
- package/dist/astro-segmented-control/index.ts +1 -0
- package/dist/astro-separator/Separator.astro +49 -0
- package/dist/astro-separator/index.ts +1 -0
- package/dist/astro-social-auth-button/SocialAuthButton.astro +110 -0
- package/dist/astro-social-auth-button/SocialAuthRow.astro +13 -0
- package/dist/astro-social-auth-button/index.ts +1 -0
- package/dist/astro-sticky-note/StickyNote.astro +55 -0
- package/dist/astro-sticky-note/index.ts +1 -0
- package/dist/astro-terminal/Terminal.astro +67 -0
- package/dist/astro-terminal/index.ts +1 -0
- package/dist/astro-test-results/TestResults.astro +76 -0
- package/dist/astro-test-results/index.ts +1 -0
- package/dist/astro-timeline/Timeline.astro +59 -0
- package/dist/astro-timeline/index.ts +1 -0
- package/dist/astro-video-grid/VideoGrid.astro +182 -0
- package/dist/astro-video-grid/index.ts +1 -0
- package/dist/astro-video-tile/VideoTile.astro +104 -0
- package/dist/astro-video-tile/index.ts +1 -0
- package/dist/astro-wizard/Wizard.astro +137 -0
- package/dist/astro-wizard/index.ts +1 -0
- package/dist/audio-room/audio-room.styles.ts +88 -0
- package/dist/audio-room/audio-room.ts +70 -0
- package/dist/audio-room/index.ts +16 -0
- package/dist/call-controls/call-controls.styles.ts +63 -0
- package/dist/call-controls/call-controls.ts +79 -0
- package/dist/call-controls/index.ts +15 -0
- package/dist/checklist/checklist.styles.ts +76 -0
- package/dist/checklist/checklist.ts +62 -0
- package/dist/checklist/index.ts +18 -0
- package/dist/editor-status-bar/editor-status-bar.styles.ts +40 -0
- package/dist/editor-status-bar/editor-status-bar.ts +78 -0
- package/dist/editor-status-bar/index.ts +15 -0
- package/dist/editor-tabs/editor-tabs.styles.ts +52 -0
- package/dist/editor-tabs/editor-tabs.ts +82 -0
- package/dist/editor-tabs/index.ts +15 -0
- package/dist/empty-state/empty-state.styles.ts +42 -0
- package/dist/empty-state/empty-state.ts +27 -0
- package/dist/empty-state/index.ts +14 -0
- package/dist/floating-reactions/floating-reactions.styles.ts +54 -0
- package/dist/floating-reactions/floating-reactions.ts +85 -0
- package/dist/floating-reactions/index.ts +14 -0
- package/dist/flow-editor/flow-editor.styles.ts +56 -0
- package/dist/flow-editor/flow-editor.ts +126 -0
- package/dist/flow-editor/index.ts +18 -0
- package/dist/graph-view/graph-view.styles.ts +66 -0
- package/dist/graph-view/graph-view.ts +130 -0
- package/dist/graph-view/index.ts +21 -0
- package/dist/index.ts +45 -17
- package/dist/infinite-canvas/index.ts +17 -0
- package/dist/infinite-canvas/infinite-canvas.styles.ts +49 -0
- package/dist/infinite-canvas/infinite-canvas.ts +97 -0
- package/dist/input/index.ts +2 -0
- package/dist/input/input.styles.ts +4 -0
- package/dist/input/input.ts +39 -3
- package/dist/live-captions/index.ts +13 -0
- package/dist/live-captions/live-captions.styles.ts +45 -0
- package/dist/live-captions/live-captions.ts +57 -0
- package/dist/live-cursors/index.ts +14 -0
- package/dist/live-cursors/live-cursors.styles.ts +41 -0
- package/dist/live-cursors/live-cursors.ts +73 -0
- package/dist/live-transcript/index.ts +16 -0
- package/dist/live-transcript/live-transcript.styles.ts +48 -0
- package/dist/live-transcript/live-transcript.ts +82 -0
- package/dist/mini-map/index.ts +16 -0
- package/dist/mini-map/mini-map.styles.ts +32 -0
- package/dist/mini-map/mini-map.ts +122 -0
- package/dist/password-input/index.ts +9 -0
- package/dist/password-input/password-input.styles.ts +10 -0
- package/dist/password-input/password-input.ts +45 -0
- package/dist/pre-call-lobby/index.ts +22 -0
- package/dist/pre-call-lobby/pre-call-lobby.styles.ts +108 -0
- package/dist/pre-call-lobby/pre-call-lobby.ts +61 -0
- package/dist/radial-gauge/index.ts +26 -0
- package/dist/radial-gauge/radial-gauge.styles.ts +75 -0
- package/dist/radial-gauge/radial-gauge.ts +183 -0
- package/dist/rating-scale/index.ts +16 -0
- package/dist/rating-scale/rating-scale.styles.ts +48 -0
- package/dist/rating-scale/rating-scale.ts +101 -0
- package/dist/segmented-control/index.ts +13 -0
- package/dist/segmented-control/segmented-control.styles.ts +42 -0
- package/dist/segmented-control/segmented-control.ts +80 -0
- package/dist/separator/index.ts +12 -0
- package/dist/separator/separator.styles.ts +34 -0
- package/dist/separator/separator.ts +53 -0
- package/dist/social-auth-button/index.ts +14 -0
- package/dist/social-auth-button/social-auth-button.styles.ts +16 -0
- package/dist/social-auth-button/social-auth-button.ts +51 -0
- package/dist/sticky-note/index.ts +14 -0
- package/dist/sticky-note/sticky-note.styles.ts +37 -0
- package/dist/sticky-note/sticky-note.ts +47 -0
- package/dist/terminal/index.ts +13 -0
- package/dist/terminal/terminal.styles.ts +56 -0
- package/dist/terminal/terminal.ts +60 -0
- package/dist/test-results/index.ts +19 -0
- package/dist/test-results/test-results.styles.ts +93 -0
- package/dist/test-results/test-results.ts +83 -0
- package/dist/timeline/index.ts +22 -0
- package/dist/timeline/timeline.styles.ts +98 -0
- package/dist/timeline/timeline.ts +86 -0
- package/dist/video-grid/index.ts +16 -0
- package/dist/video-grid/video-grid.styles.ts +38 -0
- package/dist/video-grid/video-grid.ts +93 -0
- package/dist/video-tile/index.ts +17 -0
- package/dist/video-tile/video-tile.styles.ts +59 -0
- package/dist/video-tile/video-tile.ts +74 -0
- package/dist/wizard/index.ts +28 -0
- package/dist/wizard/wizard.styles.ts +147 -0
- package/dist/wizard/wizard.ts +128 -0
- package/package.json +47 -19
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Outer lobby card — a centered panel that holds all preflight controls.
|
|
5
|
+
*/
|
|
6
|
+
export const preCallLobbyVariants = cva({
|
|
7
|
+
base: [
|
|
8
|
+
'flex flex-col gap-6 rounded-2xl border border-border bg-card p-6 shadow-sm',
|
|
9
|
+
'w-full max-w-lg mx-auto',
|
|
10
|
+
].join(' '),
|
|
11
|
+
variants: {},
|
|
12
|
+
defaultVariants: {},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Camera preview area. The `camera` variant switches between the live preview
|
|
17
|
+
* slot and an avatar / placeholder shown when the camera is off.
|
|
18
|
+
*/
|
|
19
|
+
export const preCallCameraVariants = cva({
|
|
20
|
+
base: [
|
|
21
|
+
'relative flex items-center justify-center overflow-hidden rounded-xl',
|
|
22
|
+
'aspect-video w-full bg-muted',
|
|
23
|
+
].join(' '),
|
|
24
|
+
variants: {
|
|
25
|
+
camera: {
|
|
26
|
+
on: '',
|
|
27
|
+
off: 'bg-muted',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
camera: 'on',
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** Placeholder shown inside the camera area when the camera is off. */
|
|
36
|
+
export const preCallCameraPlaceholderClass = [
|
|
37
|
+
'flex flex-col items-center justify-center gap-2',
|
|
38
|
+
'text-muted-foreground select-none',
|
|
39
|
+
].join(' ')
|
|
40
|
+
|
|
41
|
+
/** Row that holds the label and the <select> for a device picker. */
|
|
42
|
+
export const preCallDeviceRowClass = 'flex flex-col gap-1'
|
|
43
|
+
|
|
44
|
+
/** Label text above a device picker. */
|
|
45
|
+
export const preCallDeviceLabelClass = 'text-xs font-medium text-muted-foreground'
|
|
46
|
+
|
|
47
|
+
/** The <select> element used for device selection. */
|
|
48
|
+
export const preCallDeviceSelectClass = [
|
|
49
|
+
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1',
|
|
50
|
+
'text-sm text-foreground shadow-sm',
|
|
51
|
+
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
52
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
53
|
+
].join(' ')
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mic level meter container. Renders a row of bars.
|
|
57
|
+
*/
|
|
58
|
+
export const preCallMicMeterClass = 'flex items-end gap-0.5 h-5'
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An individual bar in the mic level meter.
|
|
62
|
+
* The `lit` variant distinguishes active (lit) from inactive bars.
|
|
63
|
+
*/
|
|
64
|
+
export const preCallMicBarVariants = cva({
|
|
65
|
+
base: 'w-1.5 rounded-sm transition-colors',
|
|
66
|
+
variants: {
|
|
67
|
+
lit: {
|
|
68
|
+
true: 'bg-primary',
|
|
69
|
+
false: 'bg-muted-foreground/30',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
defaultVariants: {
|
|
73
|
+
lit: 'false',
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/** Row holding the camera-toggle and mic-toggle buttons. */
|
|
78
|
+
export const preCallControlsRowClass = 'flex items-center gap-3'
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Toggle button for camera or microphone.
|
|
82
|
+
* The `active` variant styles the enabled vs disabled states.
|
|
83
|
+
*/
|
|
84
|
+
export const preCallToggleVariants = cva({
|
|
85
|
+
base: [
|
|
86
|
+
'inline-flex items-center justify-center rounded-full p-2.5',
|
|
87
|
+
'transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
88
|
+
'focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
89
|
+
].join(' '),
|
|
90
|
+
variants: {
|
|
91
|
+
active: {
|
|
92
|
+
true: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
93
|
+
false: 'bg-destructive/10 text-destructive hover:bg-destructive/20',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
defaultVariants: {
|
|
97
|
+
active: 'false',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
/** Primary "Join" button. */
|
|
102
|
+
export const preCallJoinButtonClass = [
|
|
103
|
+
'inline-flex w-full items-center justify-center rounded-lg px-4 py-2.5',
|
|
104
|
+
'bg-primary text-primary-foreground text-sm font-semibold',
|
|
105
|
+
'shadow transition-colors hover:bg-primary/90',
|
|
106
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
107
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
108
|
+
].join(' ')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** A single selectable media device (camera, microphone, or speaker). */
|
|
2
|
+
export interface MediaDeviceOption {
|
|
3
|
+
id: string
|
|
4
|
+
label: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The kind of media device. */
|
|
8
|
+
export type DeviceKind = 'camera' | 'microphone' | 'speaker'
|
|
9
|
+
|
|
10
|
+
export interface PreCallLobbyProps {
|
|
11
|
+
/** Whether the camera is currently enabled. */
|
|
12
|
+
cameraOn: boolean
|
|
13
|
+
/** Whether the microphone is currently enabled. */
|
|
14
|
+
micOn: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PreCallLobbyAPI {
|
|
18
|
+
/** ARIA attributes to spread on the group element. */
|
|
19
|
+
ariaProps: { role: 'group'; 'aria-label': string }
|
|
20
|
+
/** Data attributes for styling hooks. */
|
|
21
|
+
dataAttributes: { 'data-camera': string; 'data-mic': string }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the framework-agnostic group props for a pre-call lobby / device-check panel.
|
|
26
|
+
*
|
|
27
|
+
* Returns `role="group"` plus data attributes reflecting camera and mic state.
|
|
28
|
+
* Adapters spread these onto their container and render the device pickers,
|
|
29
|
+
* preview, and join button themselves.
|
|
30
|
+
*/
|
|
31
|
+
export function createPreCallLobby(props: PreCallLobbyProps): PreCallLobbyAPI {
|
|
32
|
+
const { cameraOn, micOn } = props
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
ariaProps: {
|
|
36
|
+
role: 'group',
|
|
37
|
+
'aria-label': 'Device setup',
|
|
38
|
+
},
|
|
39
|
+
dataAttributes: {
|
|
40
|
+
'data-camera': cameraOn ? 'on' : 'off',
|
|
41
|
+
'data-mic': micOn ? 'on' : 'off',
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert a microphone level (0..1) to a count of lit bars.
|
|
48
|
+
*
|
|
49
|
+
* Pure function — no side effects, no JSX. Adapters use this to render
|
|
50
|
+
* the mic level meter by iterating `0..barCount` and lighting bars up to
|
|
51
|
+
* the returned value.
|
|
52
|
+
*
|
|
53
|
+
* @param level Current mic level in the range [0, 1].
|
|
54
|
+
* @param barCount Total number of bars in the meter.
|
|
55
|
+
* @returns Number of bars that should appear "lit" (0..barCount).
|
|
56
|
+
*/
|
|
57
|
+
export function micLevelToBars(level: number, barCount: number): number {
|
|
58
|
+
if (barCount <= 0) return 0
|
|
59
|
+
const clamped = Math.min(1, Math.max(0, level))
|
|
60
|
+
return Math.round(clamped * barCount)
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
clampValue,
|
|
3
|
+
valueToFraction,
|
|
4
|
+
polarToCartesian,
|
|
5
|
+
describeArc,
|
|
6
|
+
gaugeStrokeDashoffset,
|
|
7
|
+
resolveZoneTone,
|
|
8
|
+
createRadialGauge,
|
|
9
|
+
GAUGE_SIZE_PX,
|
|
10
|
+
type GaugeSize,
|
|
11
|
+
type GaugeTone,
|
|
12
|
+
type GaugeZone,
|
|
13
|
+
type RadialGaugeProps,
|
|
14
|
+
type RadialGaugeAPI,
|
|
15
|
+
} from './radial-gauge.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
radialGaugeRootVariants,
|
|
19
|
+
gaugeTrackClass,
|
|
20
|
+
gaugeArcClass,
|
|
21
|
+
gaugeArcToneVariants,
|
|
22
|
+
gaugeLabelClass,
|
|
23
|
+
gaugeSublabelClass,
|
|
24
|
+
GAUGE_LABEL_FONT_SIZE,
|
|
25
|
+
GAUGE_SUBLABEL_FONT_SIZE,
|
|
26
|
+
} from './radial-gauge.styles.js'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Root SVG element variants — controls the rendered size via an inline
|
|
5
|
+
* width/height (adapters use GAUGE_SIZE_PX) and sets up the color context.
|
|
6
|
+
*/
|
|
7
|
+
export const radialGaugeRootVariants = cva({
|
|
8
|
+
base: 'inline-block shrink-0',
|
|
9
|
+
variants: {
|
|
10
|
+
size: {
|
|
11
|
+
sm: '',
|
|
12
|
+
md: '',
|
|
13
|
+
lg: '',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
size: 'md',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Track (background) circle stroke class.
|
|
23
|
+
* Uses a muted foreground so it sits behind the value arc without competing.
|
|
24
|
+
*/
|
|
25
|
+
export const gaugeTrackClass = 'fill-none stroke-muted-foreground/20'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Value arc stroke class — resolved per tone via {@link gaugeArcToneVariants}.
|
|
29
|
+
* `currentColor` stroke picks up the Tailwind text-* class applied by the
|
|
30
|
+
* tone variant so we only need one class-generating helper.
|
|
31
|
+
*/
|
|
32
|
+
export const gaugeArcClass = 'fill-none stroke-current transition-[stroke-dashoffset] duration-300 ease-out'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tone variants for the value arc. Adapters combine this with `gaugeArcClass`.
|
|
36
|
+
*/
|
|
37
|
+
export const gaugeArcToneVariants = cva({
|
|
38
|
+
base: '',
|
|
39
|
+
variants: {
|
|
40
|
+
tone: {
|
|
41
|
+
default: 'text-foreground',
|
|
42
|
+
success: 'text-emerald-500',
|
|
43
|
+
warning: 'text-amber-500',
|
|
44
|
+
danger: 'text-destructive',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
defaultVariants: {
|
|
48
|
+
tone: 'default',
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/** Center label (primary text) classes. */
|
|
53
|
+
export const gaugeLabelClass = 'fill-foreground text-center font-semibold'
|
|
54
|
+
|
|
55
|
+
/** Center sub-label (secondary text) classes. */
|
|
56
|
+
export const gaugeSublabelClass = 'fill-muted-foreground'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Font size for the primary label by size variant.
|
|
60
|
+
* Expressed as SVG `font-size` values (px) for use in the `<text>` element.
|
|
61
|
+
*/
|
|
62
|
+
export const GAUGE_LABEL_FONT_SIZE: Record<'sm' | 'md' | 'lg', number> = {
|
|
63
|
+
sm: 14,
|
|
64
|
+
md: 20,
|
|
65
|
+
lg: 26,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Font size for the sub-label by size variant.
|
|
70
|
+
*/
|
|
71
|
+
export const GAUGE_SUBLABEL_FONT_SIZE: Record<'sm' | 'md' | 'lg', number> = {
|
|
72
|
+
sm: 9,
|
|
73
|
+
md: 11,
|
|
74
|
+
lg: 13,
|
|
75
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Types
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
/** The visual size of the gauge. */
|
|
7
|
+
export type GaugeSize = 'sm' | 'md' | 'lg'
|
|
8
|
+
|
|
9
|
+
/** Tone mapped to a zone range. */
|
|
10
|
+
export type GaugeTone = 'default' | 'success' | 'warning' | 'danger'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A threshold zone that applies a semantic tone when the gauge value falls
|
|
14
|
+
* at or below `upTo`. Zones should be listed in ascending `upTo` order; the
|
|
15
|
+
* first zone whose `upTo >= value` wins.
|
|
16
|
+
*/
|
|
17
|
+
export interface GaugeZone {
|
|
18
|
+
/** Value up to (and including) which this zone applies. */
|
|
19
|
+
upTo: number
|
|
20
|
+
/** Semantic tone for the arc while the value is in this zone. */
|
|
21
|
+
tone: GaugeTone
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RadialGaugeProps {
|
|
25
|
+
/** Current gauge value. */
|
|
26
|
+
value: number
|
|
27
|
+
/** Minimum possible value. */
|
|
28
|
+
min?: number
|
|
29
|
+
/** Maximum possible value. */
|
|
30
|
+
max?: number
|
|
31
|
+
/** Visual size. */
|
|
32
|
+
size?: GaugeSize
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RadialGaugeAPI {
|
|
36
|
+
/** ARIA attributes to spread on the SVG element (`role="meter"`). */
|
|
37
|
+
ariaProps: Record<string, string | number>
|
|
38
|
+
/** Data attributes for styling hooks. */
|
|
39
|
+
dataAttributes: Record<string, string>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Pixel diameters for each size variant (exported for use in adapters)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
export const GAUGE_SIZE_PX: Record<GaugeSize, number> = {
|
|
46
|
+
sm: 80,
|
|
47
|
+
md: 120,
|
|
48
|
+
lg: 160,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Pure geometry helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clamp `value` to the inclusive range `[min, max]`.
|
|
57
|
+
*/
|
|
58
|
+
export function clampValue(value: number, min: number, max: number): number {
|
|
59
|
+
return Math.min(Math.max(value, min), max)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Map a `value` within `[min, max]` to a fraction in `[0, 1]`.
|
|
64
|
+
* Returns 0 when `min === max` to avoid division by zero.
|
|
65
|
+
*/
|
|
66
|
+
export function valueToFraction(value: number, min: number, max: number): number {
|
|
67
|
+
if (max === min) return 0
|
|
68
|
+
return clampValue((value - min) / (max - min), 0, 1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert polar coordinates to Cartesian.
|
|
73
|
+
* `angleDeg` is measured clockwise from the positive x-axis (SVG convention).
|
|
74
|
+
*/
|
|
75
|
+
export function polarToCartesian(
|
|
76
|
+
cx: number,
|
|
77
|
+
cy: number,
|
|
78
|
+
r: number,
|
|
79
|
+
angleDeg: number,
|
|
80
|
+
): { x: number; y: number } {
|
|
81
|
+
const rad = ((angleDeg - 90) * Math.PI) / 180
|
|
82
|
+
return {
|
|
83
|
+
x: cx + r * Math.cos(rad),
|
|
84
|
+
y: cy + r * Math.sin(rad),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build an SVG arc path `d` string from `startAngleDeg` to `endAngleDeg`
|
|
90
|
+
* (both measured clockwise from the top / 12-o'clock position).
|
|
91
|
+
*
|
|
92
|
+
* Returns an empty string when the arc span is zero.
|
|
93
|
+
*/
|
|
94
|
+
export function describeArc(
|
|
95
|
+
cx: number,
|
|
96
|
+
cy: number,
|
|
97
|
+
r: number,
|
|
98
|
+
startAngleDeg: number,
|
|
99
|
+
endAngleDeg: number,
|
|
100
|
+
): string {
|
|
101
|
+
const span = endAngleDeg - startAngleDeg
|
|
102
|
+
if (span === 0) return ''
|
|
103
|
+
|
|
104
|
+
// For a full circle we need two arcs to avoid SVG treating start === end as
|
|
105
|
+
// a no-op. Shift the end by 0.001° so the path stays valid.
|
|
106
|
+
const clampedEnd = span >= 360 ? startAngleDeg + 359.999 : endAngleDeg
|
|
107
|
+
|
|
108
|
+
const start = polarToCartesian(cx, cy, r, startAngleDeg)
|
|
109
|
+
const end = polarToCartesian(cx, cy, r, clampedEnd)
|
|
110
|
+
const largeArc = clampedEnd - startAngleDeg > 180 ? 1 : 0
|
|
111
|
+
|
|
112
|
+
return [
|
|
113
|
+
`M ${start.x} ${start.y}`,
|
|
114
|
+
`A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y}`,
|
|
115
|
+
].join(' ')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compute the `stroke-dashoffset` for a full-circle ring gauge.
|
|
120
|
+
*
|
|
121
|
+
* With `stroke-dasharray="circumference circumference"` and this offset, the
|
|
122
|
+
* visible portion of the stroke equals `fraction × circumference`.
|
|
123
|
+
*/
|
|
124
|
+
export function gaugeStrokeDashoffset(fraction: number, circumference: number): number {
|
|
125
|
+
return circumference * (1 - fraction)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Zone resolution
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the active zone tone for the current `value`.
|
|
134
|
+
* Zones are tested in ascending `upTo` order; the first zone whose
|
|
135
|
+
* `upTo >= value` wins. Falls back to `'default'` when no zone matches.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveZoneTone(
|
|
138
|
+
value: number,
|
|
139
|
+
zones: GaugeZone[],
|
|
140
|
+
_max: number,
|
|
141
|
+
): GaugeTone {
|
|
142
|
+
if (!zones || zones.length === 0) return 'default'
|
|
143
|
+
|
|
144
|
+
// Sort a copy ascending by upTo so callers don't have to pre-sort.
|
|
145
|
+
const sorted = [...zones].sort((a, b) => a.upTo - b.upTo)
|
|
146
|
+
|
|
147
|
+
for (const zone of sorted) {
|
|
148
|
+
if (value <= zone.upTo) return zone.tone
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Value is above all zone thresholds — use the last zone's tone.
|
|
152
|
+
return sorted[sorted.length - 1].tone
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Factory
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build the framework-agnostic props for a radial gauge element.
|
|
161
|
+
*
|
|
162
|
+
* Returns `role="meter"` plus ARIA value attributes and data attributes;
|
|
163
|
+
* adapters spread these onto their `<svg>` element.
|
|
164
|
+
*/
|
|
165
|
+
export function createRadialGauge(props: RadialGaugeProps): RadialGaugeAPI {
|
|
166
|
+
const { value, min = 0, max = 100, size = 'md' } = props
|
|
167
|
+
|
|
168
|
+
const clamped = clampValue(value, min, max)
|
|
169
|
+
|
|
170
|
+
const ariaProps: Record<string, string | number> = {
|
|
171
|
+
role: 'meter',
|
|
172
|
+
'aria-valuenow': clamped,
|
|
173
|
+
'aria-valuemin': min,
|
|
174
|
+
'aria-valuemax': max,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const dataAttributes: Record<string, string> = {
|
|
178
|
+
'data-size': size,
|
|
179
|
+
'data-value': String(clamped),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { ariaProps, dataAttributes }
|
|
183
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createRatingScale,
|
|
3
|
+
resolveRatingPoints,
|
|
4
|
+
getNextRatingIndex,
|
|
5
|
+
type RatingScaleSize,
|
|
6
|
+
type RatingScalePoint,
|
|
7
|
+
type RatingScaleProps,
|
|
8
|
+
type RatingScaleAPI,
|
|
9
|
+
} from './rating-scale.js'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
ratingScaleVariants,
|
|
13
|
+
ratingScaleTrackClass,
|
|
14
|
+
ratingScaleItemVariants,
|
|
15
|
+
ratingScaleLabelClass,
|
|
16
|
+
} from './rating-scale.styles.js'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Container row that holds the points plus optional end labels. */
|
|
4
|
+
export const ratingScaleVariants = cva({
|
|
5
|
+
base: 'inline-flex items-center gap-3',
|
|
6
|
+
variants: {
|
|
7
|
+
size: {
|
|
8
|
+
sm: '',
|
|
9
|
+
md: '',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
size: 'md',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/** Row of point buttons (the scale itself, between the end labels). */
|
|
18
|
+
export const ratingScaleTrackClass = 'inline-flex items-center gap-1.5'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A single point button. The `state` variant carries the selected vs unselected
|
|
22
|
+
* treatment so adapters never inline these classes.
|
|
23
|
+
*/
|
|
24
|
+
export const ratingScaleItemVariants = cva({
|
|
25
|
+
base: [
|
|
26
|
+
'inline-flex items-center justify-center rounded-md border font-medium tabular-nums',
|
|
27
|
+
'transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
28
|
+
'focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
29
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
30
|
+
].join(' '),
|
|
31
|
+
variants: {
|
|
32
|
+
size: {
|
|
33
|
+
sm: 'size-8 text-xs',
|
|
34
|
+
md: 'size-10 text-sm',
|
|
35
|
+
},
|
|
36
|
+
state: {
|
|
37
|
+
checked: 'border-primary bg-primary text-primary-foreground',
|
|
38
|
+
unchecked: 'border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: {
|
|
42
|
+
size: 'md',
|
|
43
|
+
state: 'unchecked',
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** End-label text flanking the scale (e.g. "Never seen this" / "Could write it"). */
|
|
48
|
+
export const ratingScaleLabelClass = 'text-xs text-muted-foreground'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Visual size of a rating scale. */
|
|
4
|
+
export type RatingScaleSize = 'sm' | 'md'
|
|
5
|
+
|
|
6
|
+
/** A single selectable point on the scale (1-indexed value + accessible label). */
|
|
7
|
+
export interface RatingScalePoint {
|
|
8
|
+
/** The value this point represents. */
|
|
9
|
+
value: number
|
|
10
|
+
/** Accessible label for the point (falls back to the value). */
|
|
11
|
+
label?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RatingScaleProps {
|
|
15
|
+
/** Currently selected value (controlled). */
|
|
16
|
+
value?: number
|
|
17
|
+
/** Number of points on the scale. Ignored when {@link points} is given. */
|
|
18
|
+
count?: number
|
|
19
|
+
/** Visual size. */
|
|
20
|
+
size?: RatingScaleSize
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RatingScaleAPI {
|
|
24
|
+
/** ARIA attributes to spread on the group element (`role="radiogroup"`). */
|
|
25
|
+
ariaProps: Partial<AccessibilityProps>
|
|
26
|
+
/** Data attributes for styling hooks. */
|
|
27
|
+
dataAttributes: Record<string, string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the framework-agnostic group props for a rating / Likert scale.
|
|
32
|
+
*
|
|
33
|
+
* Returns `role="radiogroup"` plus data attributes; adapters spread these onto
|
|
34
|
+
* their container and render the points themselves. A rating scale is a
|
|
35
|
+
* single-select control with radio semantics, like a segmented control, but
|
|
36
|
+
* tuned for ordinal self-rating (e.g. a 1–5 Likert prompt with end labels).
|
|
37
|
+
*/
|
|
38
|
+
export function createRatingScale(props: RatingScaleProps = {}): RatingScaleAPI {
|
|
39
|
+
const { value, size = 'md' } = props
|
|
40
|
+
|
|
41
|
+
const ariaProps: Partial<AccessibilityProps> = {
|
|
42
|
+
role: 'radiogroup',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dataAttributes: Record<string, string> = {
|
|
46
|
+
'data-size': size,
|
|
47
|
+
}
|
|
48
|
+
if (value !== undefined) {
|
|
49
|
+
dataAttributes['data-value'] = String(value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { ariaProps, dataAttributes }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the list of points for a scale from either an explicit list or a
|
|
57
|
+
* `count` (producing `1..count`). The single source of truth shared by every
|
|
58
|
+
* adapter so a `count={5}` scale renders identical points everywhere.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveRatingPoints(
|
|
61
|
+
points: RatingScalePoint[] | undefined,
|
|
62
|
+
count = 5,
|
|
63
|
+
): RatingScalePoint[] {
|
|
64
|
+
if (points && points.length > 0) return points
|
|
65
|
+
return Array.from({ length: Math.max(0, count) }, (_, i) => ({ value: i + 1 }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pure keyboard-navigation rule shared by every adapter.
|
|
70
|
+
*
|
|
71
|
+
* Unlike a wrapping segmented control, an ordinal rating scale **clamps** at
|
|
72
|
+
* the ends (you can't wrap from "5 — strongly agree" back to "1"). Home/End
|
|
73
|
+
* jump to the extremes; unhandled keys return `current` unchanged.
|
|
74
|
+
*
|
|
75
|
+
* - ArrowRight / ArrowUp → next (clamped to last)
|
|
76
|
+
* - ArrowLeft / ArrowDown → previous (clamped to first)
|
|
77
|
+
* - Home → first
|
|
78
|
+
* - End → last
|
|
79
|
+
*/
|
|
80
|
+
export function getNextRatingIndex(
|
|
81
|
+
current: number,
|
|
82
|
+
key: string,
|
|
83
|
+
count: number,
|
|
84
|
+
): number {
|
|
85
|
+
if (count <= 0) return current
|
|
86
|
+
|
|
87
|
+
switch (key) {
|
|
88
|
+
case 'ArrowRight':
|
|
89
|
+
case 'ArrowUp':
|
|
90
|
+
return Math.min(current + 1, count - 1)
|
|
91
|
+
case 'ArrowLeft':
|
|
92
|
+
case 'ArrowDown':
|
|
93
|
+
return Math.max(current - 1, 0)
|
|
94
|
+
case 'Home':
|
|
95
|
+
return 0
|
|
96
|
+
case 'End':
|
|
97
|
+
return count - 1
|
|
98
|
+
default:
|
|
99
|
+
return current
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createSegmentedControl,
|
|
3
|
+
getNextSegmentIndex,
|
|
4
|
+
type SegmentedControlSize,
|
|
5
|
+
type SegmentedControlValue,
|
|
6
|
+
type SegmentedControlProps,
|
|
7
|
+
type SegmentedControlAPI,
|
|
8
|
+
} from './segmented-control.js'
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
segmentedControlVariants,
|
|
12
|
+
segmentedControlItemVariants,
|
|
13
|
+
} from './segmented-control.styles.js'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Pill-shaped container that holds the segments (`role="radiogroup"`). */
|
|
4
|
+
export const segmentedControlVariants = cva({
|
|
5
|
+
base: 'inline-flex items-center gap-1 rounded-lg bg-muted p-1',
|
|
6
|
+
variants: {
|
|
7
|
+
size: {
|
|
8
|
+
sm: '',
|
|
9
|
+
md: '',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
size: 'md',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A single segment (`role="radio"`). The `state` variant carries the
|
|
19
|
+
* active vs inactive visual treatment so adapters never inline these classes.
|
|
20
|
+
*/
|
|
21
|
+
export const segmentedControlItemVariants = cva({
|
|
22
|
+
base: [
|
|
23
|
+
'inline-flex items-center justify-center gap-1.5 rounded-md font-medium',
|
|
24
|
+
'whitespace-nowrap transition-colors',
|
|
25
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
26
|
+
'focus-visible:ring-offset-2 focus-visible:ring-offset-muted',
|
|
27
|
+
].join(' '),
|
|
28
|
+
variants: {
|
|
29
|
+
size: {
|
|
30
|
+
sm: 'px-2.5 py-1 text-xs',
|
|
31
|
+
md: 'px-3 py-1.5 text-sm',
|
|
32
|
+
},
|
|
33
|
+
state: {
|
|
34
|
+
checked: 'bg-background text-foreground shadow-sm',
|
|
35
|
+
unchecked: 'text-muted-foreground hover:text-foreground',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
size: 'md',
|
|
40
|
+
state: 'unchecked',
|
|
41
|
+
},
|
|
42
|
+
})
|