@adres/mgda-ui 0.0.0-stage → 0.0.1
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/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- package/README.md +0 -3
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Progress } from "./progress"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
export type QuotaBarShape = "bar" | "ring"
|
|
8
|
+
|
|
9
|
+
export interface QuotaBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
/** Current usage. */
|
|
11
|
+
used: number
|
|
12
|
+
/** Maximum allowed. When `used >= total`, the bar renders in the error
|
|
13
|
+
* variant and the "full" state is shown. */
|
|
14
|
+
total: number
|
|
15
|
+
/** Compact mode - uses a smaller bar / ring. */
|
|
16
|
+
compact?: boolean
|
|
17
|
+
/** Visual shape. `bar` (default) is a horizontal Progress with the caption
|
|
18
|
+
* underneath; `ring` puts a circular Progress on the inline-start side
|
|
19
|
+
* with the caption stacked beside it and the pct% inside the ring. */
|
|
20
|
+
shape?: QuotaBarShape
|
|
21
|
+
/** Ring-only: pixel diameter override (passes through to Progress). */
|
|
22
|
+
ringSize?: number
|
|
23
|
+
/** Ring-only: stroke width in pixels. */
|
|
24
|
+
strokeWidth?: number
|
|
25
|
+
/** Unit label appended after the count - e.g. "seats", "licences", "GB".
|
|
26
|
+
* Used in the "X / Y {unit} used" sentence. */
|
|
27
|
+
unit?: string
|
|
28
|
+
/** Label shown next to the count when the quota is *not* full.
|
|
29
|
+
* Defaults to `"{remaining} remaining"`. */
|
|
30
|
+
remainingLabel?: (remaining: number) => string
|
|
31
|
+
/** Replaces the default "No {unit} remaining" string in the full state. */
|
|
32
|
+
fullLabel?: string
|
|
33
|
+
/** When the quota is full, the full-state CTA renders as a link to this
|
|
34
|
+
* href. Use this OR `ctaOnClick`, not both. */
|
|
35
|
+
ctaHref?: string
|
|
36
|
+
/** When the quota is full, the full-state CTA renders as a button that
|
|
37
|
+
* fires this callback (use for non-routing actions like opening a Drawer). */
|
|
38
|
+
ctaOnClick?: () => void
|
|
39
|
+
/** Visible text for the full-state CTA. Defaults to "Request More". */
|
|
40
|
+
ctaLabel?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Visual quota indicator - a Progress bar (or ring) plus a small
|
|
44
|
+
* "{used} / {total}" caption and an optional CTA when full.
|
|
45
|
+
* Domain-agnostic: pass `unit` to control the wording for your use case
|
|
46
|
+
* (seats, licences, storage GB, emails sent this month, …).
|
|
47
|
+
*
|
|
48
|
+
* Threshold ramp:
|
|
49
|
+
* • < 90 % - `default` variant
|
|
50
|
+
* • 90–99 % - `warning` variant, "remaining" text warning-tinted
|
|
51
|
+
* • ≥ 100 % - `error` variant, "No {unit} remaining" + optional CTA */
|
|
52
|
+
export const QuotaBar = React.forwardRef<HTMLDivElement, QuotaBarProps>(
|
|
53
|
+
(
|
|
54
|
+
{
|
|
55
|
+
used,
|
|
56
|
+
total,
|
|
57
|
+
compact,
|
|
58
|
+
shape = "bar",
|
|
59
|
+
ringSize,
|
|
60
|
+
strokeWidth,
|
|
61
|
+
unit = "items",
|
|
62
|
+
remainingLabel,
|
|
63
|
+
fullLabel,
|
|
64
|
+
ctaHref,
|
|
65
|
+
ctaOnClick,
|
|
66
|
+
ctaLabel = "Request More",
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
},
|
|
70
|
+
ref,
|
|
71
|
+
) => {
|
|
72
|
+
const isFull = used >= total
|
|
73
|
+
const remaining = Math.max(0, total - used)
|
|
74
|
+
const pct = total > 0 ? (used / total) * 100 : 0
|
|
75
|
+
const isWarn = !isFull && pct >= 90
|
|
76
|
+
const remainingText = remainingLabel
|
|
77
|
+
? remainingLabel(remaining)
|
|
78
|
+
: `${remaining} remaining`
|
|
79
|
+
|
|
80
|
+
const variant = isFull ? "error" : isWarn ? "warning" : "default"
|
|
81
|
+
const size: "sm" | "md" = compact ? "sm" : "md"
|
|
82
|
+
|
|
83
|
+
const ctaNode = !isFull ? null : ctaOnClick ? (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={ctaOnClick}
|
|
87
|
+
className="ax:text-primary ax:hover:underline ax:focus-visible:outline-none ax:focus-visible:underline ax:cursor-pointer"
|
|
88
|
+
>
|
|
89
|
+
{ctaLabel}
|
|
90
|
+
</button>
|
|
91
|
+
) : ctaHref ? (
|
|
92
|
+
<a
|
|
93
|
+
href={ctaHref}
|
|
94
|
+
className="ax:text-primary ax:hover:underline ax:focus-visible:outline-none ax:focus-visible:underline"
|
|
95
|
+
>
|
|
96
|
+
{ctaLabel}
|
|
97
|
+
</a>
|
|
98
|
+
) : null
|
|
99
|
+
|
|
100
|
+
// ── Ring shape ────────────────────────────────────────────────────────────
|
|
101
|
+
if (shape === "ring") {
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={ref}
|
|
105
|
+
className={cn("ax:flex ax:items-center ax:gap-md", className)}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<div className="ax:relative ax:inline-flex ax:shrink-0">
|
|
109
|
+
<Progress
|
|
110
|
+
shape="ring"
|
|
111
|
+
value={used}
|
|
112
|
+
max={total}
|
|
113
|
+
size={size}
|
|
114
|
+
variant={variant}
|
|
115
|
+
ringSize={ringSize}
|
|
116
|
+
strokeWidth={strokeWidth}
|
|
117
|
+
/>
|
|
118
|
+
{/* Center pct% - the ring conveys the proportion visually, this
|
|
119
|
+
gives a precise reading at a glance. Hidden when the ring is
|
|
120
|
+
tiny (compact, no override) since the text wouldn't fit. */}
|
|
121
|
+
{!(compact && !ringSize) && (
|
|
122
|
+
<span
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
className={cn(
|
|
125
|
+
"ax:absolute ax:inset-0 ax:flex ax:items-center ax:justify-center",
|
|
126
|
+
"ax:t-caption ax:font-medium",
|
|
127
|
+
isFull ? "ax:text-destructive-strong" : isWarn ? "ax:text-warning-strong" : "ax:text-foreground",
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
{Math.round(pct)}%
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="ax:flex-1 ax:min-w-0 ax:flex ax:flex-col ax:gap-2xs ax:t-caption ax:text-muted-foreground">
|
|
136
|
+
{isFull ? (
|
|
137
|
+
<>
|
|
138
|
+
<span className="ax:text-destructive-strong ax:font-medium">
|
|
139
|
+
{fullLabel ?? `No ${unit} remaining`}
|
|
140
|
+
</span>
|
|
141
|
+
{ctaNode}
|
|
142
|
+
</>
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<span>
|
|
146
|
+
{used} / {total} {unit} used
|
|
147
|
+
</span>
|
|
148
|
+
<span className={cn(isWarn && "ax:text-warning-strong ax:font-medium")}>
|
|
149
|
+
{remainingText}
|
|
150
|
+
</span>
|
|
151
|
+
</>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Bar shape (default) ───────────────────────────────────────────────────
|
|
159
|
+
return (
|
|
160
|
+
<div ref={ref} className={cn("ax:flex ax:flex-col ax:gap-xs", className)} {...props}>
|
|
161
|
+
<Progress
|
|
162
|
+
value={used}
|
|
163
|
+
max={total}
|
|
164
|
+
size={size}
|
|
165
|
+
variant={variant}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
<div className="ax:flex ax:items-center ax:justify-between ax:gap-xs ax:t-caption ax:text-muted-foreground">
|
|
169
|
+
{isFull ? (
|
|
170
|
+
<>
|
|
171
|
+
<span className="ax:text-destructive-strong ax:font-medium">
|
|
172
|
+
{fullLabel ?? `No ${unit} remaining`}
|
|
173
|
+
</span>
|
|
174
|
+
{ctaNode}
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
<span>
|
|
179
|
+
{used} / {total} {unit} used
|
|
180
|
+
</span>
|
|
181
|
+
<span className={cn(isWarn && "ax:text-warning-strong ax:font-medium")}>
|
|
182
|
+
{remainingText}
|
|
183
|
+
</span>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
QuotaBar.displayName = "QuotaBar"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Quote } from "./quote"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Quote> = {
|
|
6
|
+
title: "Components/Quote",
|
|
7
|
+
component: Quote,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
children: "Great products are built on great design systems.",
|
|
11
|
+
size: "md",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof Quote>
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
export const quoteVariants = cva(
|
|
8
|
+
"ax:italic",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
sm: "ax:t-body-sm",
|
|
13
|
+
md: "ax:t-body-md",
|
|
14
|
+
lg: "ax:t-body-lg",
|
|
15
|
+
xl: "ax:text-xl",
|
|
16
|
+
},
|
|
17
|
+
color: {
|
|
18
|
+
default: "ax:text-foreground",
|
|
19
|
+
muted: "ax:text-muted-foreground",
|
|
20
|
+
accent: "ax:text-accent-foreground",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
size: "md",
|
|
25
|
+
color: "default",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export interface QuoteProps
|
|
31
|
+
extends Omit<React.HTMLAttributes<HTMLQuoteElement>, "color">,
|
|
32
|
+
VariantProps<typeof quoteVariants> {}
|
|
33
|
+
|
|
34
|
+
export const Quote = React.forwardRef<HTMLQuoteElement, QuoteProps>(
|
|
35
|
+
({ className, size, color, ...props }, ref) => (
|
|
36
|
+
<q
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(quoteVariants({ size, color }), className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
Quote.displayName = "Quote"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { RadioCard, RadioCardDescription, RadioCardGroup, RadioCardTitle } from "./radio-card"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof RadioCardGroup> = {
|
|
6
|
+
title: "Components/RadioCard",
|
|
7
|
+
component: RadioCardGroup,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof RadioCardGroup>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<RadioCardGroup defaultValue="monthly" className="ax:max-w-md">
|
|
17
|
+
<RadioCard value="monthly">
|
|
18
|
+
<RadioCardTitle>Monthly</RadioCardTitle>
|
|
19
|
+
<RadioCardDescription>Pay as you go each month.</RadioCardDescription>
|
|
20
|
+
</RadioCard>
|
|
21
|
+
<RadioCard value="yearly">
|
|
22
|
+
<RadioCardTitle>Yearly</RadioCardTitle>
|
|
23
|
+
<RadioCardDescription>Save 20% with annual billing.</RadioCardDescription>
|
|
24
|
+
</RadioCard>
|
|
25
|
+
</RadioCardGroup>
|
|
26
|
+
),
|
|
27
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
5
|
+
import { Circle, CircleCheck } from "lucide-react"
|
|
6
|
+
import { cva } from "class-variance-authority"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
|
|
9
|
+
export type RadioCardSize = "sm" | "base" | "lg" | "xl"
|
|
10
|
+
export type RadioCardOrientation = "horizontal" | "vertical"
|
|
11
|
+
|
|
12
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
interface RadioCardGroupContext {
|
|
15
|
+
size: RadioCardSize
|
|
16
|
+
compact: boolean
|
|
17
|
+
orientation: RadioCardOrientation
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const RadioCardGroupContext = React.createContext<RadioCardGroupContext>({
|
|
21
|
+
size: "base",
|
|
22
|
+
compact: false,
|
|
23
|
+
orientation: "vertical",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// ─── Group ────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface RadioCardGroupProps
|
|
29
|
+
extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> {
|
|
30
|
+
size?: RadioCardSize
|
|
31
|
+
compact?: boolean
|
|
32
|
+
orientation?: RadioCardOrientation
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const RadioCardGroup = React.forwardRef<
|
|
36
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
37
|
+
RadioCardGroupProps
|
|
38
|
+
>(({ className, size = "base", compact = false, orientation = "vertical", ...props }, ref) => (
|
|
39
|
+
<RadioCardGroupContext.Provider value={{ size, compact, orientation }}>
|
|
40
|
+
<RadioGroupPrimitive.Root
|
|
41
|
+
ref={ref}
|
|
42
|
+
orientation={orientation}
|
|
43
|
+
className={cn(
|
|
44
|
+
compact
|
|
45
|
+
? orientation === "horizontal" ? "ax:flex ax:flex-row" : "ax:flex ax:flex-col"
|
|
46
|
+
: "ax:grid ax:gap-3",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
</RadioCardGroupContext.Provider>
|
|
52
|
+
))
|
|
53
|
+
RadioCardGroup.displayName = "RadioCardGroup"
|
|
54
|
+
|
|
55
|
+
// ─── Card ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
// Checked background is intentionally omitted here: it's applied conditionally
|
|
58
|
+
// below so compact and default modes can use different tint strengths.
|
|
59
|
+
const radioCardVariants = cva(
|
|
60
|
+
[
|
|
61
|
+
"ax:relative ax:flex ax:items-start ax:cursor-pointer ax:border ax:border-border ax:bg-card",
|
|
62
|
+
"ax:transition-colors ax:transition-snappy",
|
|
63
|
+
// Selected border matches the selected title/icon colour (primary-strong /
|
|
64
|
+
// dark:primary) rather than the base --primary.
|
|
65
|
+
"ax:has-[[data-state=checked]]:border-primary",
|
|
66
|
+
"ax:dark:has-[[data-state=checked]]:border-primary",
|
|
67
|
+
// Hover only on unchecked cards - darker brand-aware border (≈30% foreground)
|
|
68
|
+
// for a clearly visible interactivity cue. `hover:z-10` lifts the hovered
|
|
69
|
+
// card above its compact-mode neighbours so the new border isn't clipped
|
|
70
|
+
// by the adjacent card's idle border.
|
|
71
|
+
"ax:[&:not(:has([data-state=checked]))]:hover:border-foreground/50",
|
|
72
|
+
"ax:[&:not(:has([data-state=checked]))]:hover:z-10",
|
|
73
|
+
"ax:has-[[data-disabled]]:cursor-not-allowed ax:has-[[data-disabled]]:opacity-50",
|
|
74
|
+
// Trailing radio indicator (top-right): a hollow ring shown BY DEFAULT that
|
|
75
|
+
// swaps to a filled check when the card is selected. Both glyphs share one
|
|
76
|
+
// stacked slot so the swap is layout-shift-free. State is driven from the
|
|
77
|
+
// card (ancestor) selectors (mirroring CheckboxCard) so source order
|
|
78
|
+
// resolves the overrides.
|
|
79
|
+
// off ring: --input at rest, --foreground/40 on hover, hidden when checked
|
|
80
|
+
// on check: hidden by default; primary-strong / dark:primary when checked,
|
|
81
|
+
// mirrors the selected title colour
|
|
82
|
+
"ax:[&_[data-slot=radio-card-off]]:text-input",
|
|
83
|
+
"ax:[&:not(:has([data-state=checked])):hover_[data-slot=radio-card-off]]:text-foreground/40",
|
|
84
|
+
"ax:[&:has([data-state=checked])_[data-slot=radio-card-off]]:opacity-0",
|
|
85
|
+
"ax:[&_[data-slot=radio-card-on]]:text-primary-foreground ax:[&_[data-slot=radio-card-on]]:opacity-0",
|
|
86
|
+
"ax:dark:[&_[data-slot=radio-card-on]]:text-primary-foreground",
|
|
87
|
+
"ax:[&:has([data-state=checked])_[data-slot=radio-card-on]]:opacity-100",
|
|
88
|
+
],
|
|
89
|
+
{
|
|
90
|
+
variants: {
|
|
91
|
+
size: {
|
|
92
|
+
sm: "ax:p-3 ax:gap-3 ax:text-xs",
|
|
93
|
+
base: "ax:p-4 ax:gap-3 ax:text-sm",
|
|
94
|
+
lg: "ax:p-5 ax:gap-4 ax:text-base",
|
|
95
|
+
xl: "ax:px-7 ax:py-5 ax:gap-4 ax:text-base",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
defaultVariants: { size: "base" },
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// Default mode: subtle background tint on selection PLUS primary-coloured
|
|
103
|
+
// title and icon (mirrors compact mode so both variants read the same way
|
|
104
|
+
// at-a-glance: selected card → brand-tinted title + icon).
|
|
105
|
+
const DEFAULT_CHECKED =
|
|
106
|
+
"ax:has-[[data-state=checked]]:bg-primary-muted " +
|
|
107
|
+
"ax:[&:has([data-state=checked])_[data-slot=radio-card-title]]:text-primary-foreground " +
|
|
108
|
+
"ax:[&:has([data-state=checked])_[data-slot=radio-card-icon]]:text-primary-foreground " +
|
|
109
|
+
// Dark mode: primary-strong is a mid sage that reads muddy on a dark card, so
|
|
110
|
+
// brighten the selected title + icon (matches the bright checked indicator).
|
|
111
|
+
"ax:dark:[&:has([data-state=checked])_[data-slot=radio-card-title]]:text-primary-foreground " +
|
|
112
|
+
"ax:dark:[&:has([data-state=checked])_[data-slot=radio-card-icon]]:text-primary-foreground"
|
|
113
|
+
|
|
114
|
+
// Compact mode: primary-weakest background tint plus primary-coloured title and
|
|
115
|
+
// icon on selection. The [&:has(...)_[data-slot]] form styles a DESCENDANT
|
|
116
|
+
// matching data-slot whenever the label itself contains a checked radio.
|
|
117
|
+
const COMPACT_CHECKED =
|
|
118
|
+
"ax:has-[[data-state=checked]]:bg-primary-muted " +
|
|
119
|
+
"ax:[&:has([data-state=checked])_[data-slot=radio-card-title]]:text-primary-foreground " +
|
|
120
|
+
"ax:[&:has([data-state=checked])_[data-slot=radio-card-icon]]:text-primary-foreground " +
|
|
121
|
+
// Dark mode: brighten the selected title + icon (see DEFAULT_CHECKED).
|
|
122
|
+
"ax:dark:[&:has([data-state=checked])_[data-slot=radio-card-title]]:text-primary-foreground " +
|
|
123
|
+
"ax:dark:[&:has([data-state=checked])_[data-slot=radio-card-icon]]:text-primary-foreground"
|
|
124
|
+
|
|
125
|
+
const COMPACT_LAYOUT: Record<RadioCardOrientation, string> = {
|
|
126
|
+
horizontal: [
|
|
127
|
+
"ax:rounded-none",
|
|
128
|
+
"ax:first:rounded-l-(--input-radius) ax:last:rounded-r-(--input-radius)",
|
|
129
|
+
"ax:[&:not(:first-child)]:-ml-px",
|
|
130
|
+
"ax:has-[[data-state=checked]]:z-10",
|
|
131
|
+
].join(" "),
|
|
132
|
+
vertical: [
|
|
133
|
+
"ax:rounded-none",
|
|
134
|
+
"ax:first:rounded-t-(--input-radius) ax:last:rounded-b-(--input-radius)",
|
|
135
|
+
"ax:[&:not(:first-child)]:-mt-px",
|
|
136
|
+
"ax:has-[[data-state=checked]]:z-10",
|
|
137
|
+
].join(" "),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const INDICATOR_SIZE: Record<RadioCardSize, string> = {
|
|
141
|
+
sm: "ax:size-3.5",
|
|
142
|
+
base: "ax:size-4",
|
|
143
|
+
lg: "ax:size-4",
|
|
144
|
+
xl: "ax:size-5",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface RadioCardProps
|
|
148
|
+
extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
|
|
149
|
+
size?: RadioCardSize
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const RadioCard = React.forwardRef<
|
|
153
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
154
|
+
RadioCardProps
|
|
155
|
+
>(({ className, size: sizeProp, children, ...props }, ref) => {
|
|
156
|
+
const { size: ctxSize, compact, orientation } = React.useContext(RadioCardGroupContext)
|
|
157
|
+
const size = sizeProp ?? ctxSize
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
// No htmlFor needed: label activates its first labelable descendant (the button) on click.
|
|
161
|
+
<label
|
|
162
|
+
className={cn(
|
|
163
|
+
radioCardVariants({ size }),
|
|
164
|
+
compact ? [COMPACT_LAYOUT[orientation], COMPACT_CHECKED] : ["ax:rounded-(--input-radius)", DEFAULT_CHECKED],
|
|
165
|
+
className
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
<div className="ax:flex-1 ax:min-w-0">{children}</div>
|
|
169
|
+
<span
|
|
170
|
+
aria-hidden="true"
|
|
171
|
+
className={cn("ax:relative ax:shrink-0 ax:mt-0", INDICATOR_SIZE[size])}
|
|
172
|
+
>
|
|
173
|
+
{/* Unchecked - hollow ring, shown by default. */}
|
|
174
|
+
<Circle
|
|
175
|
+
data-slot="radio-card-off"
|
|
176
|
+
className="ax:absolute ax:inset-0 ax:size-full ax:transition ax:transition-snappy"
|
|
177
|
+
/>
|
|
178
|
+
{/* Checked - filled check, revealed on selection. */}
|
|
179
|
+
<CircleCheck
|
|
180
|
+
data-slot="radio-card-on"
|
|
181
|
+
className="ax:absolute ax:inset-0 ax:size-full ax:transition ax:transition-snappy"
|
|
182
|
+
/>
|
|
183
|
+
</span>
|
|
184
|
+
<RadioGroupPrimitive.Item ref={ref} className="ax:sr-only" {...props} />
|
|
185
|
+
</label>
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
RadioCard.displayName = "RadioCard"
|
|
189
|
+
|
|
190
|
+
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
// data-slot is used by the compact checked selectors to colour title and icon primary.
|
|
193
|
+
export const RadioCardIcon = React.forwardRef<
|
|
194
|
+
HTMLDivElement,
|
|
195
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
196
|
+
>(({ className, ...props }, ref) => (
|
|
197
|
+
<div
|
|
198
|
+
ref={ref}
|
|
199
|
+
data-slot="radio-card-icon"
|
|
200
|
+
className={cn("ax:shrink-0 ax:text-muted-foreground", className)}
|
|
201
|
+
{...props}
|
|
202
|
+
/>
|
|
203
|
+
))
|
|
204
|
+
RadioCardIcon.displayName = "RadioCardIcon"
|
|
205
|
+
|
|
206
|
+
export const RadioCardTitle = React.forwardRef<
|
|
207
|
+
HTMLParagraphElement,
|
|
208
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
209
|
+
>(({ className, ...props }, ref) => (
|
|
210
|
+
<p
|
|
211
|
+
ref={ref}
|
|
212
|
+
data-slot="radio-card-title"
|
|
213
|
+
className={cn("ax:t-label-sm ax:text-foreground", className)}
|
|
214
|
+
{...props}
|
|
215
|
+
/>
|
|
216
|
+
))
|
|
217
|
+
RadioCardTitle.displayName = "RadioCardTitle"
|
|
218
|
+
|
|
219
|
+
export const RadioCardDescription = React.forwardRef<
|
|
220
|
+
HTMLParagraphElement,
|
|
221
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
222
|
+
>(({ className, ...props }, ref) => (
|
|
223
|
+
<p ref={ref} className={cn("ax:text-[0.875em] ax:text-muted-foreground ax:mt-1", className)} {...props} />
|
|
224
|
+
))
|
|
225
|
+
RadioCardDescription.displayName = "RadioCardDescription"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Label } from "./input"
|
|
4
|
+
import { RadioGroup, RadioGroupItem } from "./radio-group"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
7
|
+
title: "Components/RadioGroup",
|
|
8
|
+
component: RadioGroup,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof RadioGroup>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<RadioGroup defaultValue="comfortable">
|
|
18
|
+
<div className="ax:flex ax:items-center ax:gap-sm">
|
|
19
|
+
<RadioGroupItem value="default" id="r1" />
|
|
20
|
+
<Label htmlFor="r1">Default</Label>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="ax:flex ax:items-center ax:gap-sm">
|
|
23
|
+
<RadioGroupItem value="comfortable" id="r2" />
|
|
24
|
+
<Label htmlFor="r2">Comfortable</Label>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="ax:flex ax:items-center ax:gap-sm">
|
|
27
|
+
<RadioGroupItem value="compact" id="r3" />
|
|
28
|
+
<Label htmlFor="r3">Compact</Label>
|
|
29
|
+
</div>
|
|
30
|
+
</RadioGroup>
|
|
31
|
+
),
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
5
|
+
import { Circle } from "lucide-react"
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
|
|
9
|
+
export const RadioGroup = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<RadioGroupPrimitive.Root
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn("ax:grid ax:gap-2", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
))
|
|
19
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
20
|
+
|
|
21
|
+
const radioItemVariants = cva(
|
|
22
|
+
[
|
|
23
|
+
"ax:aspect-square ax:rounded-full ax:border ax:border-input",
|
|
24
|
+
"ax:transition-colors ax:transition-snappy",
|
|
25
|
+
"ax:focus:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2 ax:focus-visible:ring-offset-background",
|
|
26
|
+
"ax:cursor-pointer ax:disabled:cursor-not-allowed ax:disabled:opacity-50",
|
|
27
|
+
// primary-weak fill + primary-strong dot. The fill auto-inverts to a light-ish
|
|
28
|
+
// tint in dark, so the dot flips to a dark `primary-muted` there (dark: override)
|
|
29
|
+
// to keep contrast on that lighter fill. Mirrors Checkbox.
|
|
30
|
+
"ax:data-[state=checked]:bg-primary-weak ax:data-[state=checked]:border-primary-weak ax:data-[state=checked]:text-primary-strong ax:dark:data-[state=checked]:text-primary-muted",
|
|
31
|
+
],
|
|
32
|
+
{
|
|
33
|
+
variants: {
|
|
34
|
+
size: {
|
|
35
|
+
sm: "ax:size-3",
|
|
36
|
+
base: "ax:size-4",
|
|
37
|
+
lg: "ax:size-5",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: { size: "base" },
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const INDICATOR_SIZE: Record<NonNullable<VariantProps<typeof radioItemVariants>["size"]>, string> = {
|
|
45
|
+
sm: "ax:size-1.5",
|
|
46
|
+
base: "ax:size-2",
|
|
47
|
+
lg: "ax:size-2.5",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RadioGroupItemProps
|
|
51
|
+
extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
|
|
52
|
+
VariantProps<typeof radioItemVariants> {}
|
|
53
|
+
|
|
54
|
+
export const RadioGroupItem = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
56
|
+
RadioGroupItemProps
|
|
57
|
+
>(({ className, size, ...props }, ref) => (
|
|
58
|
+
<RadioGroupPrimitive.Item
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn(radioItemVariants({ size }), className)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<RadioGroupPrimitive.Indicator className="ax:flex ax:items-center ax:justify-center">
|
|
64
|
+
<Circle className={cn(INDICATOR_SIZE[size ?? "base"], "ax:fill-current ax:text-current")} />
|
|
65
|
+
</RadioGroupPrimitive.Indicator>
|
|
66
|
+
</RadioGroupPrimitive.Item>
|
|
67
|
+
))
|
|
68
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Reasoning, ReasoningContent, ReasoningTrigger } from "./reasoning"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { Text } from "./text"
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Reasoning> = {
|
|
8
|
+
title: "Components/Reasoning",
|
|
9
|
+
component: Reasoning,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof Reasoning>
|
|
16
|
+
|
|
17
|
+
const THOUGHT =
|
|
18
|
+
"The buyer wants a Yas Island townhouse under AED 5M with a garden. 11 listings match. The cheapest asks AED 4.2M, " +
|
|
19
|
+
"and the median registered sale in Yas Acres is AED 4.6M, so it is priced below recent sales."
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<Reasoning>
|
|
24
|
+
<ReasoningTrigger>Show reasoning</ReasoningTrigger>
|
|
25
|
+
<ReasoningContent>{THOUGHT}</ReasoningContent>
|
|
26
|
+
</Reasoning>
|
|
27
|
+
),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function StreamingDemo() {
|
|
31
|
+
const [text, setText] = React.useState("")
|
|
32
|
+
const streaming = text.length < THOUGHT.length && text.length > 0
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
if (!streaming) return
|
|
35
|
+
const timer = window.setTimeout(() => setText(THOUGHT.slice(0, text.length + 4)), 40)
|
|
36
|
+
return () => window.clearTimeout(timer)
|
|
37
|
+
}, [text, streaming])
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12, alignItems: "flex-start" }}>
|
|
40
|
+
<Reasoning streaming={streaming}>
|
|
41
|
+
<ReasoningTrigger>{streaming ? "Thinking" : "Thought for 4 seconds"}</ReasoningTrigger>
|
|
42
|
+
<ReasoningContent>{text}</ReasoningContent>
|
|
43
|
+
</Reasoning>
|
|
44
|
+
{!streaming && text && <Text as="p" size="2">The AED 4.2M Yas Acres townhouse is the best value.</Text>}
|
|
45
|
+
<Button variant="outline" size="sm" disabled={streaming} onClick={() => setText(THOUGHT.slice(0, 4))}>Stream</Button>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const Streaming: Story = { render: () => <StreamingDemo /> }
|