@handled-ai/design-system 0.16.1 → 0.17.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/components/contextual-quick-action-launcher.d.ts +32 -0
- package/dist/components/contextual-quick-action-launcher.js +202 -0
- package/dist/components/contextual-quick-action-launcher.js.map +1 -0
- package/dist/components/data-table-condition-filter.js +26 -9
- package/dist/components/data-table-condition-filter.js.map +1 -1
- package/dist/components/data-table-filter.js +3 -14
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +46 -0
- package/dist/components/score-why-chips.js +281 -0
- package/dist/components/score-why-chips.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +37 -1
- package/dist/prototype/prototype-inbox-view.d.ts +9 -3
- package/dist/prototype/prototype-inbox-view.js +28 -96
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +193 -0
- package/src/components/__tests__/data-table-condition-filter.test.tsx +26 -0
- package/src/components/__tests__/data-table-filter.test.tsx +21 -0
- package/src/components/contextual-quick-action-launcher.tsx +231 -0
- package/src/components/data-table-condition-filter.tsx +39 -11
- package/src/components/data-table-filter.tsx +3 -19
- package/src/components/score-why-chips.tsx +358 -0
- package/src/index.ts +2 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +326 -0
- package/src/prototype/prototype-config.ts +35 -0
- package/src/prototype/prototype-inbox-view.tsx +31 -104
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Info } from "lucide-react"
|
|
5
|
+
import { ScoreBreakdown, type ScoreFactor } from "./score-breakdown"
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
import type {
|
|
8
|
+
QueueItem,
|
|
9
|
+
SignalScoreData,
|
|
10
|
+
SignalScoreExplanationBucket,
|
|
11
|
+
SignalScoreExplanationSignal,
|
|
12
|
+
SignalScoreUrgencyLabel,
|
|
13
|
+
} from "../prototype/prototype-config"
|
|
14
|
+
|
|
15
|
+
export function getSignalScoreUrgencyLabel(
|
|
16
|
+
score: number,
|
|
17
|
+
providedLabel?: SignalScoreUrgencyLabel,
|
|
18
|
+
): SignalScoreUrgencyLabel {
|
|
19
|
+
if (providedLabel) return providedLabel
|
|
20
|
+
if (score >= 80) return "Urgent"
|
|
21
|
+
if (score >= 60) return "High"
|
|
22
|
+
if (score >= 35) return "Medium"
|
|
23
|
+
return "Low"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getUrgencyChipClass(label: SignalScoreUrgencyLabel) {
|
|
27
|
+
switch (label) {
|
|
28
|
+
case "Urgent":
|
|
29
|
+
return "border-red-200 bg-red-50 text-red-700 hover:bg-red-100 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300"
|
|
30
|
+
case "High":
|
|
31
|
+
return "border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100 dark:border-orange-900/50 dark:bg-orange-950/30 dark:text-orange-300"
|
|
32
|
+
case "Medium":
|
|
33
|
+
return "border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-300"
|
|
34
|
+
case "Low":
|
|
35
|
+
return "border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-300"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function classificationForScore(score?: number): string | undefined {
|
|
40
|
+
if (score == null) return undefined
|
|
41
|
+
if (score >= 80) return "Urgent"
|
|
42
|
+
if (score >= 60) return "High"
|
|
43
|
+
if (score >= 35) return "Medium"
|
|
44
|
+
return "Low"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scoreRangeForUrgency(label: SignalScoreUrgencyLabel): string {
|
|
48
|
+
switch (label) {
|
|
49
|
+
case "Urgent":
|
|
50
|
+
return "80-100"
|
|
51
|
+
case "High":
|
|
52
|
+
return "60-79"
|
|
53
|
+
case "Medium":
|
|
54
|
+
return "35-59"
|
|
55
|
+
case "Low":
|
|
56
|
+
return "0-34"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeDomId(...parts: Array<string | undefined>): string {
|
|
61
|
+
return parts
|
|
62
|
+
.filter((part): part is string => Boolean(part))
|
|
63
|
+
.join("-")
|
|
64
|
+
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scoreFactorToPriorityBucket(factor: ScoreFactor): SignalScoreExplanationBucket {
|
|
68
|
+
return {
|
|
69
|
+
key: factor.key,
|
|
70
|
+
label: factor.label,
|
|
71
|
+
kind: "factor",
|
|
72
|
+
score: factor.score ?? undefined,
|
|
73
|
+
classification: factor.risk ?? classificationForScore(factor.score ?? undefined),
|
|
74
|
+
rationale: factor.why,
|
|
75
|
+
factorKeys: [factor.key],
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bucketHasSignalRows(bucket: SignalScoreExplanationBucket): boolean {
|
|
80
|
+
return (
|
|
81
|
+
(bucket.signals?.length ?? 0) > 0 ||
|
|
82
|
+
(bucket.signalIds?.length ?? 0) > 0 ||
|
|
83
|
+
Boolean(bucket.primarySignalId)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getSignalScoreBuckets(signalData: SignalScoreData): SignalScoreExplanationBucket[] {
|
|
88
|
+
return (signalData.explanationBuckets ?? []).filter(
|
|
89
|
+
(bucket) => bucket.kind !== "factor" && bucketHasSignalRows(bucket),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getBucketSignals(bucket: SignalScoreExplanationBucket): SignalScoreExplanationSignal[] {
|
|
94
|
+
if (bucket.signals && bucket.signals.length > 0) return bucket.signals
|
|
95
|
+
|
|
96
|
+
const signalIds = bucket.signalIds && bucket.signalIds.length > 0 ? bucket.signalIds : bucket.primarySignalId ? [bucket.primarySignalId] : []
|
|
97
|
+
const uniqueSignalIds = Array.from(new Set(signalIds))
|
|
98
|
+
|
|
99
|
+
return uniqueSignalIds.map((signalId) => ({
|
|
100
|
+
id: signalId,
|
|
101
|
+
label: `${bucket.label} signal`,
|
|
102
|
+
}))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getPriorityBuckets(signalData: SignalScoreData): SignalScoreExplanationBucket[] {
|
|
106
|
+
if (signalData.explanationBuckets !== undefined) return signalData.explanationBuckets
|
|
107
|
+
// Legacy fallback for consumers that still provide score factors but have not
|
|
108
|
+
// migrated to explanation buckets. WHY chips intentionally do not use this.
|
|
109
|
+
return signalData.factors.map(scoreFactorToPriorityBucket)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isSameExplanation(a?: string, b?: string): boolean {
|
|
113
|
+
return Boolean(a && b && a.trim() === b.trim())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SignalPriorityChipProps {
|
|
117
|
+
score: number
|
|
118
|
+
urgencyLabel?: SignalScoreUrgencyLabel
|
|
119
|
+
isOpen?: boolean
|
|
120
|
+
controlsId?: string
|
|
121
|
+
onClick?: () => void
|
|
122
|
+
className?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function SignalPriorityChip({
|
|
126
|
+
score,
|
|
127
|
+
urgencyLabel: providedLabel,
|
|
128
|
+
isOpen,
|
|
129
|
+
controlsId,
|
|
130
|
+
onClick,
|
|
131
|
+
className,
|
|
132
|
+
}: SignalPriorityChipProps) {
|
|
133
|
+
const urgencyLabel = getSignalScoreUrgencyLabel(score, providedLabel)
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onClick}
|
|
139
|
+
aria-expanded={isOpen}
|
|
140
|
+
aria-controls={controlsId}
|
|
141
|
+
className={cn(
|
|
142
|
+
"inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
143
|
+
getUrgencyChipClass(urgencyLabel),
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
{urgencyLabel} Priority
|
|
148
|
+
<Info className="h-3 w-3" />
|
|
149
|
+
</button>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface SignalPriorityPanelProps {
|
|
154
|
+
signalData: SignalScoreData
|
|
155
|
+
className?: string
|
|
156
|
+
id?: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function SignalPriorityPanel({ signalData, className, id }: SignalPriorityPanelProps) {
|
|
160
|
+
const urgencyLabel = getSignalScoreUrgencyLabel(signalData.score, signalData.urgencyLabel)
|
|
161
|
+
const buckets = React.useMemo(() => getPriorityBuckets(signalData), [signalData])
|
|
162
|
+
const topBucketRationale = buckets.find((bucket) => bucket.rationale)?.rationale
|
|
163
|
+
const primaryUrgencyExplanation = signalData.urgencyExplanation ?? signalData.whyNow ?? topBucketRationale
|
|
164
|
+
const whyNowSection = isSameExplanation(signalData.whyNow, primaryUrgencyExplanation) ? undefined : signalData.whyNow
|
|
165
|
+
const topFactorSection =
|
|
166
|
+
isSameExplanation(topBucketRationale, primaryUrgencyExplanation) || isSameExplanation(topBucketRationale, whyNowSection)
|
|
167
|
+
? undefined
|
|
168
|
+
: topBucketRationale
|
|
169
|
+
const scoreRange = scoreRangeForUrgency(urgencyLabel)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div id={id} className={cn("rounded-lg border border-border bg-muted/20 p-3 text-xs", className)} role="region" aria-label="Priority explanation">
|
|
173
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
174
|
+
<div>
|
|
175
|
+
<p className="font-semibold text-foreground">Why this is {urgencyLabel.toLowerCase()} priority</p>
|
|
176
|
+
{primaryUrgencyExplanation ? <p className="mt-1 leading-relaxed text-muted-foreground">{primaryUrgencyExplanation}</p> : null}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="flex shrink-0 flex-wrap gap-1.5 text-[11px] text-muted-foreground">
|
|
179
|
+
<span className="rounded-full bg-background px-2 py-0.5">Score {signalData.score}/100</span>
|
|
180
|
+
<span className="rounded-full bg-background px-2 py-0.5">{urgencyLabel} range: {scoreRange}</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{(whyNowSection || topFactorSection) && (
|
|
185
|
+
<div className="mt-3 grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
|
|
186
|
+
{whyNowSection ? (
|
|
187
|
+
<div className="rounded-md bg-background/80 p-2">
|
|
188
|
+
<p className="font-medium text-foreground">Why now</p>
|
|
189
|
+
<p className="mt-0.5 leading-relaxed">{whyNowSection}</p>
|
|
190
|
+
</div>
|
|
191
|
+
) : null}
|
|
192
|
+
{topFactorSection ? (
|
|
193
|
+
<div className="rounded-md bg-background/80 p-2">
|
|
194
|
+
<p className="font-medium text-foreground">Top factor</p>
|
|
195
|
+
<p className="mt-0.5 leading-relaxed">{topFactorSection}</p>
|
|
196
|
+
</div>
|
|
197
|
+
) : null}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{signalData.factors.length > 0 ? (
|
|
202
|
+
<ScoreBreakdown
|
|
203
|
+
className="mt-3"
|
|
204
|
+
factors={signalData.factors}
|
|
205
|
+
onFactorFeedback={signalData.onFactorFeedback}
|
|
206
|
+
initialFeedback={signalData.initialFactorFeedback}
|
|
207
|
+
/>
|
|
208
|
+
) : null}
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface ScoreWhyChipsProps {
|
|
214
|
+
item: QueueItem
|
|
215
|
+
signalData: SignalScoreData
|
|
216
|
+
onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void
|
|
217
|
+
className?: string
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface SignalRowProps {
|
|
221
|
+
item: QueueItem
|
|
222
|
+
bucketKey: string
|
|
223
|
+
signal: SignalScoreExplanationSignal
|
|
224
|
+
onOpenSignalBucket?: ScoreWhyChipsProps["onOpenSignalBucket"]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function SignalRow({ item, bucketKey, signal, onOpenSignalBucket }: SignalRowProps) {
|
|
228
|
+
const rowContent = (
|
|
229
|
+
<>
|
|
230
|
+
<div className="flex items-start justify-between gap-2">
|
|
231
|
+
<p className="font-medium text-foreground">{signal.label}</p>
|
|
232
|
+
{signal.time ? <span className="shrink-0 text-[11px] text-muted-foreground/70">{signal.time}</span> : null}
|
|
233
|
+
</div>
|
|
234
|
+
{signal.description ? <p className="mt-1 leading-relaxed text-muted-foreground">{signal.description}</p> : null}
|
|
235
|
+
{(signal.source || signal.metric) && (
|
|
236
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5 text-[11px] text-muted-foreground/80">
|
|
237
|
+
{signal.source ? <span className="rounded-full bg-muted px-2 py-0.5">{signal.source}</span> : null}
|
|
238
|
+
{signal.metric ? <span className="rounded-full bg-muted px-2 py-0.5">{signal.metric}</span> : null}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if (signal.id && onOpenSignalBucket) {
|
|
245
|
+
return (
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
className="w-full rounded-md bg-background/80 p-2 text-left text-xs transition-colors hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
249
|
+
onClick={() => onOpenSignalBucket({ item, bucketKey, signalId: signal.id! })}
|
|
250
|
+
>
|
|
251
|
+
{rowContent}
|
|
252
|
+
</button>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return <div className="rounded-md bg-background/80 p-2 text-xs">{rowContent}</div>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function ScoreWhyChips({
|
|
260
|
+
item,
|
|
261
|
+
signalData,
|
|
262
|
+
onOpenSignalBucket,
|
|
263
|
+
className,
|
|
264
|
+
}: ScoreWhyChipsProps) {
|
|
265
|
+
const [selectedBucketKey, setSelectedBucketKey] = React.useState<string | null>(null)
|
|
266
|
+
|
|
267
|
+
React.useEffect(() => {
|
|
268
|
+
setSelectedBucketKey(null)
|
|
269
|
+
}, [item.id])
|
|
270
|
+
|
|
271
|
+
const reactId = React.useId()
|
|
272
|
+
const idPrefix = makeDomId("score-why", reactId, item.id)
|
|
273
|
+
const buckets = React.useMemo(() => getSignalScoreBuckets(signalData), [signalData])
|
|
274
|
+
const selectedBucket = buckets.find((bucket) => bucket.key === selectedBucketKey) ?? null
|
|
275
|
+
const selectedBucketSignals = selectedBucket ? getBucketSignals(selectedBucket) : []
|
|
276
|
+
const selectedPanelId = selectedBucket ? `${idPrefix}-panel-${makeDomId(selectedBucket.key)}` : undefined
|
|
277
|
+
|
|
278
|
+
if (buckets.length === 0) return null
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className={cn("mt-4", className)}>
|
|
282
|
+
<div className="mb-2 flex items-center gap-2">
|
|
283
|
+
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Why</span>
|
|
284
|
+
</div>
|
|
285
|
+
<div className="flex flex-wrap gap-1.5">
|
|
286
|
+
{buckets.map((bucket) => {
|
|
287
|
+
const isSelected = selectedBucketKey === bucket.key
|
|
288
|
+
const panelId = `${idPrefix}-panel-${makeDomId(bucket.key)}`
|
|
289
|
+
return (
|
|
290
|
+
<button
|
|
291
|
+
key={bucket.key}
|
|
292
|
+
type="button"
|
|
293
|
+
onClick={() => setSelectedBucketKey((prev) => (prev === bucket.key ? null : bucket.key))}
|
|
294
|
+
aria-expanded={isSelected}
|
|
295
|
+
aria-controls={panelId}
|
|
296
|
+
className={cn(
|
|
297
|
+
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
298
|
+
isSelected
|
|
299
|
+
? "border-foreground/30 bg-foreground text-background"
|
|
300
|
+
: "border-border bg-background text-muted-foreground hover:bg-muted/60 hover:text-foreground",
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{bucket.label}
|
|
304
|
+
{bucket.signalCount && bucket.signalCount > 1 ? (
|
|
305
|
+
<span className={cn("rounded-full px-1.5 py-0 text-[10px]", isSelected ? "bg-background/20" : "bg-muted")}>×{bucket.signalCount}</span>
|
|
306
|
+
) : null}
|
|
307
|
+
</button>
|
|
308
|
+
)
|
|
309
|
+
})}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{selectedBucket && (
|
|
313
|
+
<div
|
|
314
|
+
id={selectedPanelId}
|
|
315
|
+
className="mt-3 rounded-lg border border-border bg-muted/20 p-3"
|
|
316
|
+
role="region"
|
|
317
|
+
aria-label={`${selectedBucket.label} details`}
|
|
318
|
+
>
|
|
319
|
+
<div className="flex items-start justify-between gap-3">
|
|
320
|
+
<div>
|
|
321
|
+
<p className="text-sm font-semibold text-foreground">{selectedBucket.label}</p>
|
|
322
|
+
<div className="mt-1 flex flex-wrap gap-1.5 text-[11px] text-muted-foreground">
|
|
323
|
+
<span className="rounded-full bg-background px-2 py-0.5">
|
|
324
|
+
{selectedBucket.signalCount ?? selectedBucketSignals.length} signals
|
|
325
|
+
</span>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{selectedBucketSignals.length > 0 ? (
|
|
331
|
+
<ul className="mt-3 space-y-2" aria-label="Matching signals">
|
|
332
|
+
{selectedBucketSignals.map((signal, index) => (
|
|
333
|
+
<li key={signal.id ?? `${selectedBucket.key}-signal-${index}`}>
|
|
334
|
+
<SignalRow
|
|
335
|
+
item={item}
|
|
336
|
+
bucketKey={selectedBucket.key}
|
|
337
|
+
signal={signal}
|
|
338
|
+
onOpenSignalBucket={onOpenSignalBucket}
|
|
339
|
+
/>
|
|
340
|
+
</li>
|
|
341
|
+
))}
|
|
342
|
+
</ul>
|
|
343
|
+
) : selectedBucket.evidence && selectedBucket.evidence.length > 0 ? (
|
|
344
|
+
<ul className="mt-3 space-y-1.5" aria-label="Matching signals">
|
|
345
|
+
{selectedBucket.evidence.map((evidence, index) => (
|
|
346
|
+
<li key={`${selectedBucket.key}-evidence-${index}`} className="flex gap-2 text-xs text-muted-foreground">
|
|
347
|
+
<span className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-primary" />
|
|
348
|
+
<span className="leading-relaxed">{evidence}</span>
|
|
349
|
+
</li>
|
|
350
|
+
))}
|
|
351
|
+
</ul>
|
|
352
|
+
) : null}
|
|
353
|
+
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { CollapsibleSection, type CollapsibleSectionProps } from "./components/c
|
|
|
23
23
|
export * from "./components/compliance-badge"
|
|
24
24
|
export * from "./components/contact-chip"
|
|
25
25
|
export * from "./components/contact-list"
|
|
26
|
+
export * from "./components/contextual-quick-action-launcher"
|
|
26
27
|
export * from "./components/dashboard-cards"
|
|
27
28
|
export * from "./components/data-table"
|
|
28
29
|
export * from "./components/data-table-condition-filter"
|
|
@@ -66,6 +67,7 @@ export * from "./components/rich-text-toolbar"
|
|
|
66
67
|
export * from "./components/score-analysis-modal"
|
|
67
68
|
export * from "./components/score-breakdown"
|
|
68
69
|
export * from "./components/score-feedback"
|
|
70
|
+
export * from "./components/score-why-chips"
|
|
69
71
|
export * from "./components/score-ring"
|
|
70
72
|
export * from "./components/scroll-area"
|
|
71
73
|
export * from "./components/select"
|