@refraction-ui/astro 0.12.0 → 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.
Files changed (117) hide show
  1. package/dist/astro-audio-room/AudioRoom.astro +81 -0
  2. package/dist/astro-audio-room/index.ts +1 -0
  3. package/dist/astro-call-controls/CallControls.astro +57 -0
  4. package/dist/astro-call-controls/index.ts +1 -0
  5. package/dist/astro-checklist/Checklist.astro +76 -0
  6. package/dist/astro-checklist/index.ts +1 -0
  7. package/dist/astro-editor-status-bar/EditorStatusBar.astro +92 -0
  8. package/dist/astro-editor-status-bar/index.ts +1 -0
  9. package/dist/astro-editor-tabs/EditorTabs.astro +74 -0
  10. package/dist/astro-editor-tabs/index.ts +1 -0
  11. package/dist/astro-floating-reactions/FloatingReactions.astro +56 -0
  12. package/dist/astro-floating-reactions/index.ts +1 -0
  13. package/dist/astro-flow-editor/FlowEditor.astro +99 -0
  14. package/dist/astro-flow-editor/index.ts +3 -0
  15. package/dist/astro-graph-view/GraphView.astro +109 -0
  16. package/dist/astro-graph-view/index.ts +1 -0
  17. package/dist/astro-infinite-canvas/InfiniteCanvas.astro +43 -0
  18. package/dist/astro-infinite-canvas/index.ts +1 -0
  19. package/dist/astro-live-captions/LiveCaptions.astro +54 -0
  20. package/dist/astro-live-captions/index.ts +1 -0
  21. package/dist/astro-live-cursors/LiveCursors.astro +55 -0
  22. package/dist/astro-live-cursors/index.ts +1 -0
  23. package/dist/astro-live-transcript/LiveTranscript.astro +55 -0
  24. package/dist/astro-live-transcript/index.ts +1 -0
  25. package/dist/astro-mini-map/MiniMap.astro +74 -0
  26. package/dist/astro-mini-map/index.ts +1 -0
  27. package/dist/astro-pre-call-lobby/PreCallLobby.astro +202 -0
  28. package/dist/astro-pre-call-lobby/index.ts +1 -0
  29. package/dist/astro-radial-gauge/RadialGauge.astro +144 -0
  30. package/dist/astro-radial-gauge/index.ts +1 -0
  31. package/dist/astro-rating-scale/RatingScale.astro +73 -0
  32. package/dist/astro-rating-scale/index.ts +1 -0
  33. package/dist/astro-sticky-note/StickyNote.astro +55 -0
  34. package/dist/astro-sticky-note/index.ts +1 -0
  35. package/dist/astro-terminal/Terminal.astro +67 -0
  36. package/dist/astro-terminal/index.ts +1 -0
  37. package/dist/astro-test-results/TestResults.astro +76 -0
  38. package/dist/astro-test-results/index.ts +1 -0
  39. package/dist/astro-timeline/Timeline.astro +59 -0
  40. package/dist/astro-timeline/index.ts +1 -0
  41. package/dist/astro-video-grid/VideoGrid.astro +182 -0
  42. package/dist/astro-video-grid/index.ts +1 -0
  43. package/dist/astro-video-tile/VideoTile.astro +104 -0
  44. package/dist/astro-video-tile/index.ts +1 -0
  45. package/dist/astro-wizard/Wizard.astro +137 -0
  46. package/dist/astro-wizard/index.ts +1 -0
  47. package/dist/audio-room/audio-room.styles.ts +88 -0
  48. package/dist/audio-room/audio-room.ts +70 -0
  49. package/dist/audio-room/index.ts +16 -0
  50. package/dist/call-controls/call-controls.styles.ts +63 -0
  51. package/dist/call-controls/call-controls.ts +79 -0
  52. package/dist/call-controls/index.ts +15 -0
  53. package/dist/checklist/checklist.styles.ts +76 -0
  54. package/dist/checklist/checklist.ts +62 -0
  55. package/dist/checklist/index.ts +18 -0
  56. package/dist/editor-status-bar/editor-status-bar.styles.ts +40 -0
  57. package/dist/editor-status-bar/editor-status-bar.ts +78 -0
  58. package/dist/editor-status-bar/index.ts +15 -0
  59. package/dist/editor-tabs/editor-tabs.styles.ts +52 -0
  60. package/dist/editor-tabs/editor-tabs.ts +82 -0
  61. package/dist/editor-tabs/index.ts +15 -0
  62. package/dist/floating-reactions/floating-reactions.styles.ts +54 -0
  63. package/dist/floating-reactions/floating-reactions.ts +85 -0
  64. package/dist/floating-reactions/index.ts +14 -0
  65. package/dist/flow-editor/flow-editor.styles.ts +56 -0
  66. package/dist/flow-editor/flow-editor.ts +126 -0
  67. package/dist/flow-editor/index.ts +18 -0
  68. package/dist/graph-view/graph-view.styles.ts +66 -0
  69. package/dist/graph-view/graph-view.ts +130 -0
  70. package/dist/graph-view/index.ts +21 -0
  71. package/dist/index.ts +23 -0
  72. package/dist/infinite-canvas/index.ts +17 -0
  73. package/dist/infinite-canvas/infinite-canvas.styles.ts +49 -0
  74. package/dist/infinite-canvas/infinite-canvas.ts +97 -0
  75. package/dist/live-captions/index.ts +13 -0
  76. package/dist/live-captions/live-captions.styles.ts +45 -0
  77. package/dist/live-captions/live-captions.ts +57 -0
  78. package/dist/live-cursors/index.ts +14 -0
  79. package/dist/live-cursors/live-cursors.styles.ts +41 -0
  80. package/dist/live-cursors/live-cursors.ts +73 -0
  81. package/dist/live-transcript/index.ts +16 -0
  82. package/dist/live-transcript/live-transcript.styles.ts +48 -0
  83. package/dist/live-transcript/live-transcript.ts +82 -0
  84. package/dist/mini-map/index.ts +16 -0
  85. package/dist/mini-map/mini-map.styles.ts +32 -0
  86. package/dist/mini-map/mini-map.ts +122 -0
  87. package/dist/pre-call-lobby/index.ts +22 -0
  88. package/dist/pre-call-lobby/pre-call-lobby.styles.ts +108 -0
  89. package/dist/pre-call-lobby/pre-call-lobby.ts +61 -0
  90. package/dist/radial-gauge/index.ts +26 -0
  91. package/dist/radial-gauge/radial-gauge.styles.ts +75 -0
  92. package/dist/radial-gauge/radial-gauge.ts +183 -0
  93. package/dist/rating-scale/index.ts +16 -0
  94. package/dist/rating-scale/rating-scale.styles.ts +48 -0
  95. package/dist/rating-scale/rating-scale.ts +101 -0
  96. package/dist/sticky-note/index.ts +14 -0
  97. package/dist/sticky-note/sticky-note.styles.ts +37 -0
  98. package/dist/sticky-note/sticky-note.ts +47 -0
  99. package/dist/terminal/index.ts +13 -0
  100. package/dist/terminal/terminal.styles.ts +56 -0
  101. package/dist/terminal/terminal.ts +60 -0
  102. package/dist/test-results/index.ts +19 -0
  103. package/dist/test-results/test-results.styles.ts +93 -0
  104. package/dist/test-results/test-results.ts +83 -0
  105. package/dist/timeline/index.ts +22 -0
  106. package/dist/timeline/timeline.styles.ts +98 -0
  107. package/dist/timeline/timeline.ts +86 -0
  108. package/dist/video-grid/index.ts +16 -0
  109. package/dist/video-grid/video-grid.styles.ts +38 -0
  110. package/dist/video-grid/video-grid.ts +93 -0
  111. package/dist/video-tile/index.ts +17 -0
  112. package/dist/video-tile/video-tile.styles.ts +59 -0
  113. package/dist/video-tile/video-tile.ts +74 -0
  114. package/dist/wizard/index.ts +28 -0
  115. package/dist/wizard/wizard.styles.ts +147 -0
  116. package/dist/wizard/wizard.ts +128 -0
  117. package/package.json +24 -1
@@ -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,14 @@
1
+ export {
2
+ STICKY_NOTE_COLORS,
3
+ nextStickyColor,
4
+ createStickyNote,
5
+ type StickyNoteColor,
6
+ type CreateStickyNoteOptions,
7
+ type StickyNoteAPI,
8
+ } from './sticky-note.js'
9
+
10
+ export {
11
+ stickyNoteVariants,
12
+ stickyNoteTextClass,
13
+ stickyNoteAuthorClass,
14
+ } from './sticky-note.styles.js'
@@ -0,0 +1,37 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Note container with color variant.
5
+ *
6
+ * Sticky notes are intrinsically colorful — an approved exception to the
7
+ * semantic-token-only rule. Each color maps to a soft tint background with a
8
+ * readable dark text.
9
+ */
10
+ export const stickyNoteVariants = cva({
11
+ base: [
12
+ 'relative inline-flex flex-col gap-2 rounded-md p-4',
13
+ 'shadow-md',
14
+ 'min-w-[160px] min-h-[120px]',
15
+ ].join(' '),
16
+ variants: {
17
+ color: {
18
+ yellow: 'bg-yellow-100 text-yellow-900',
19
+ pink: 'bg-pink-100 text-pink-900',
20
+ blue: 'bg-blue-100 text-blue-900',
21
+ green: 'bg-green-100 text-green-900',
22
+ purple: 'bg-purple-100 text-purple-900',
23
+ orange: 'bg-orange-100 text-orange-900',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ color: 'yellow',
28
+ },
29
+ })
30
+
31
+ /** Class for the editable/static text area inside the note. */
32
+ export const stickyNoteTextClass =
33
+ 'flex-1 w-full resize-none bg-transparent text-sm leading-relaxed placeholder:opacity-60 outline-none'
34
+
35
+ /** Class for the author chip rendered in the note footer. */
36
+ export const stickyNoteAuthorClass =
37
+ 'mt-auto text-xs font-medium opacity-70 truncate'
@@ -0,0 +1,47 @@
1
+ /** The available color variants for a sticky note. */
2
+ export type StickyNoteColor = 'yellow' | 'pink' | 'blue' | 'green' | 'purple' | 'orange'
3
+
4
+ /** Ordered list of all sticky note colors — used for cycling and iteration. */
5
+ export const STICKY_NOTE_COLORS: StickyNoteColor[] = [
6
+ 'yellow',
7
+ 'pink',
8
+ 'blue',
9
+ 'green',
10
+ 'purple',
11
+ 'orange',
12
+ ]
13
+
14
+ /**
15
+ * Returns the next color in the cycle. Wraps from the last color back to the
16
+ * first. Useful for "add note" flows that auto-assign colors in sequence.
17
+ */
18
+ export function nextStickyColor(color: StickyNoteColor): StickyNoteColor {
19
+ const index = STICKY_NOTE_COLORS.indexOf(color)
20
+ const next = (index + 1) % STICKY_NOTE_COLORS.length
21
+ return STICKY_NOTE_COLORS[next]
22
+ }
23
+
24
+ export interface CreateStickyNoteOptions {
25
+ /** Color of the note. */
26
+ color: StickyNoteColor
27
+ }
28
+
29
+ export interface StickyNoteAPI {
30
+ /** Data attributes for styling hooks; spread onto the note container. */
31
+ dataAttributes: { 'data-color': StickyNoteColor }
32
+ /** ARIA props for the note container. */
33
+ ariaProps: { role: 'group' }
34
+ }
35
+
36
+ /**
37
+ * Build the framework-agnostic props for a sticky note container.
38
+ *
39
+ * Returns `role="group"` plus the `data-color` attribute that adapters use
40
+ * as a styling hook. Logic is pure and SSR-safe — no Math.random, no Date.
41
+ */
42
+ export function createStickyNote(options: CreateStickyNoteOptions): StickyNoteAPI {
43
+ return {
44
+ dataAttributes: { 'data-color': options.color },
45
+ ariaProps: { role: 'group' },
46
+ }
47
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ formatPrompt,
3
+ createTerminal,
4
+ type TerminalLineKind,
5
+ type TerminalLine,
6
+ type TerminalAPI,
7
+ } from './terminal.js'
8
+
9
+ export {
10
+ terminalVariants,
11
+ terminalLineVariants,
12
+ terminalPromptClass,
13
+ } from './terminal.styles.js'
@@ -0,0 +1,56 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * The outer scroll container — a dark monospaced surface that houses all
5
+ * terminal lines. Uses semantic card tokens so it inherits the active theme.
6
+ */
7
+ export const terminalVariants = cva({
8
+ base: [
9
+ 'relative rounded-lg border border-border bg-card font-mono text-sm',
10
+ 'overflow-y-auto overflow-x-auto',
11
+ 'p-4 space-y-0.5',
12
+ ].join(' '),
13
+ variants: {
14
+ maxHeight: {
15
+ sm: 'max-h-48',
16
+ md: 'max-h-80',
17
+ lg: 'max-h-[32rem]',
18
+ none: '',
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ maxHeight: 'md',
23
+ },
24
+ })
25
+
26
+ /**
27
+ * A single terminal line. The `kind` variant maps each semantic category to
28
+ * the appropriate semantic token class — no raw color values inline.
29
+ */
30
+ export const terminalLineVariants = cva({
31
+ base: 'block whitespace-pre-wrap break-all leading-relaxed',
32
+ variants: {
33
+ kind: {
34
+ /** The user-typed command: full foreground, bold weight. */
35
+ command: 'text-foreground font-semibold',
36
+ /** Normal program output. */
37
+ stdout: 'text-foreground',
38
+ /** Error / stderr output — uses the destructive semantic token. */
39
+ stderr: 'text-destructive',
40
+ /** Informational summary lines — muted, visually de-emphasised. */
41
+ info: 'text-muted-foreground',
42
+ /**
43
+ * Success / passing lines — green-family.
44
+ * Maps to `text-emerald-500` via the Tailwind semantic alias used
45
+ * across the design system; downstream themes can override this token.
46
+ */
47
+ success: 'text-emerald-500 dark:text-emerald-400',
48
+ },
49
+ },
50
+ defaultVariants: {
51
+ kind: 'stdout',
52
+ },
53
+ })
54
+
55
+ /** The prompt symbol (`$`, `❯`, …) preceding a command line. */
56
+ export const terminalPromptClass = 'select-none mr-2 text-muted-foreground'
@@ -0,0 +1,60 @@
1
+ /** The semantic kind of a terminal output line. */
2
+ export type TerminalLineKind = 'command' | 'stdout' | 'stderr' | 'info' | 'success'
3
+
4
+ /** A single line displayed in the terminal output panel. */
5
+ export interface TerminalLine {
6
+ /** Optional stable key for list rendering. */
7
+ id?: string
8
+ /** Semantic kind — drives styling and screen-reader announcements. */
9
+ kind: TerminalLineKind
10
+ /** Text content of the line. */
11
+ text: string
12
+ }
13
+
14
+ export interface TerminalAPI {
15
+ /**
16
+ * ARIA attributes to spread on the terminal container element.
17
+ * - `role="log"` semantics: assistive technologies announce appended content.
18
+ * - `aria-live="polite"` so new output does not interrupt the user mid-action.
19
+ */
20
+ ariaProps: {
21
+ role: 'log'
22
+ 'aria-live': 'polite'
23
+ 'aria-label'?: string
24
+ }
25
+ /** Data attributes for styling hooks. */
26
+ dataAttributes: Record<string, string>
27
+ }
28
+
29
+ /**
30
+ * Build the prompt string shown before a command line.
31
+ *
32
+ * @param promptSymbol - The shell prompt character(s), e.g. `'$'` or `'❯'`.
33
+ * @param command - The command text that follows the prompt.
34
+ * @returns The formatted prompt string, e.g. `'$ python solution.py'`.
35
+ */
36
+ export function formatPrompt(promptSymbol: string, command: string): string {
37
+ return `${promptSymbol} ${command}`
38
+ }
39
+
40
+ /**
41
+ * Build the framework-agnostic ARIA and data props for a terminal output panel.
42
+ *
43
+ * Returns `role="log"` (live region semantics for streaming output) plus data
44
+ * attributes; adapters spread these onto their scroll container.
45
+ */
46
+ export function createTerminal(options: { label?: string } = {}): TerminalAPI {
47
+ const ariaProps: TerminalAPI['ariaProps'] = {
48
+ role: 'log',
49
+ 'aria-live': 'polite',
50
+ }
51
+ if (options.label) {
52
+ ariaProps['aria-label'] = options.label
53
+ }
54
+
55
+ const dataAttributes: Record<string, string> = {
56
+ 'data-component': 'terminal',
57
+ }
58
+
59
+ return { ariaProps, dataAttributes }
60
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ summarizeTests,
3
+ createTestResults,
4
+ type TestStatus,
5
+ type TestResultData,
6
+ type TestSummary,
7
+ type TestResultsAPI,
8
+ } from './test-results.js'
9
+
10
+ export {
11
+ testResultsContainerVariants,
12
+ testResultsSummaryVariants,
13
+ testRowVariants,
14
+ testStatusBadgeVariants,
15
+ testNameClass,
16
+ testDurationClass,
17
+ testDiffBlockVariants,
18
+ testMessageClass,
19
+ } from './test-results.styles.js'
@@ -0,0 +1,93 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** Outer container that wraps the summary bar and the test row list. */
4
+ export const testResultsContainerVariants = cva({
5
+ base: 'flex flex-col gap-3 w-full',
6
+ variants: {},
7
+ defaultVariants: {},
8
+ })
9
+
10
+ /**
11
+ * Summary bar ("2 / 3 passed").
12
+ *
13
+ * The `outcome` variant maps to a semantic color token so the bar turns green
14
+ * when everything passes, red when there are failures.
15
+ */
16
+ export const testResultsSummaryVariants = cva({
17
+ base: [
18
+ 'flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium',
19
+ 'border',
20
+ ].join(' '),
21
+ variants: {
22
+ outcome: {
23
+ pass: 'border-success/30 bg-success/10 text-success',
24
+ fail: 'border-destructive/30 bg-destructive/10 text-destructive',
25
+ skip: 'border-muted/30 bg-muted/10 text-muted-foreground',
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ outcome: 'pass',
30
+ },
31
+ })
32
+
33
+ /** A single test row in the list. */
34
+ export const testRowVariants = cva({
35
+ base: [
36
+ 'flex flex-col gap-1.5 rounded-md border px-3 py-2.5 text-sm',
37
+ ].join(' '),
38
+ variants: {
39
+ status: {
40
+ pass: 'border-success/20 bg-success/5',
41
+ fail: 'border-destructive/20 bg-destructive/5',
42
+ skip: 'border-muted/20 bg-muted/5',
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ status: 'pass',
47
+ },
48
+ })
49
+
50
+ /** Status icon / badge inside a test row (the PASS / FAIL / SKIP pill). */
51
+ export const testStatusBadgeVariants = cva({
52
+ base: 'inline-flex items-center rounded px-1.5 py-0.5 text-xs font-semibold uppercase tracking-wide',
53
+ variants: {
54
+ status: {
55
+ pass: 'bg-success/20 text-success',
56
+ fail: 'bg-destructive/20 text-destructive',
57
+ skip: 'bg-muted text-muted-foreground',
58
+ },
59
+ },
60
+ defaultVariants: {
61
+ status: 'pass',
62
+ },
63
+ })
64
+
65
+ /** Test name text inside a row. */
66
+ export const testNameClass = 'font-medium text-foreground'
67
+
68
+ /** Duration text ("12 ms") inside a row. */
69
+ export const testDurationClass = 'text-xs text-muted-foreground'
70
+
71
+ /**
72
+ * Expected / actual diff block shown beneath a failing test row.
73
+ *
74
+ * The `side` variant sets the left-border accent color to distinguish the two
75
+ * sides of the diff.
76
+ */
77
+ export const testDiffBlockVariants = cva({
78
+ base: [
79
+ 'rounded border-l-2 bg-muted/30 px-3 py-2 font-mono text-xs text-foreground',
80
+ ].join(' '),
81
+ variants: {
82
+ side: {
83
+ expected: 'border-l-success text-success',
84
+ actual: 'border-l-destructive text-destructive',
85
+ },
86
+ },
87
+ defaultVariants: {
88
+ side: 'actual',
89
+ },
90
+ })
91
+
92
+ /** Error / skip message text beneath a test row. */
93
+ export const testMessageClass = 'text-xs text-muted-foreground italic'