@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,105 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "../lib/cn"
|
|
4
|
+
|
|
5
|
+
// Numeric `size` is a convenience API over the `t-*` scale defined in typography.css.
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type TextAs = "span" | "p" | "div" | "label" | "strong" | "em"
|
|
10
|
+
export type TextSize = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
|
|
11
|
+
export type TextWeight = "light" | "regular" | "medium" | "bold"
|
|
12
|
+
export type TextAlign = "left" | "center" | "right"
|
|
13
|
+
export type TextColor = "default" | "muted" | "strong" | "primary" | "accent" | "success" | "warning" | "destructive"
|
|
14
|
+
export type TextWrap = "wrap" | "nowrap" | "balance" | "pretty"
|
|
15
|
+
|
|
16
|
+
// ─── Variants ─────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const textVariants = cva("", {
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
"1": "ax:t-body-xs",
|
|
22
|
+
"2": "ax:t-body-sm",
|
|
23
|
+
"3": "ax:t-body-md",
|
|
24
|
+
"4": "ax:t-body-lg",
|
|
25
|
+
"5": "ax:t-body-xl",
|
|
26
|
+
"6": "ax:t-h1",
|
|
27
|
+
"7": "ax:t-display-sm",
|
|
28
|
+
"8": "ax:t-display-md",
|
|
29
|
+
"9": "ax:t-display-xl",
|
|
30
|
+
},
|
|
31
|
+
weight: {
|
|
32
|
+
light: "ax:font-light",
|
|
33
|
+
regular: "ax:font-normal",
|
|
34
|
+
medium: "ax:font-medium",
|
|
35
|
+
bold: "ax:font-bold",
|
|
36
|
+
},
|
|
37
|
+
align: {
|
|
38
|
+
left: "ax:text-left",
|
|
39
|
+
center: "ax:text-center",
|
|
40
|
+
right: "ax:text-right",
|
|
41
|
+
},
|
|
42
|
+
color: {
|
|
43
|
+
default: "ax:text-foreground",
|
|
44
|
+
muted: "ax:text-muted-foreground",
|
|
45
|
+
strong: "ax:text-foreground-strong",
|
|
46
|
+
primary: "ax:text-primary-foreground",
|
|
47
|
+
accent: "ax:text-accent-foreground",
|
|
48
|
+
success: "ax:text-success-foreground",
|
|
49
|
+
warning: "ax:text-warning-foreground",
|
|
50
|
+
destructive: "ax:text-destructive-foreground",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: {
|
|
54
|
+
size: "3",
|
|
55
|
+
weight: "regular",
|
|
56
|
+
color: "default",
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const WRAP_CLASS: Record<TextWrap, string> = {
|
|
61
|
+
wrap: "ax:text-wrap",
|
|
62
|
+
nowrap: "ax:text-nowrap",
|
|
63
|
+
balance: "ax:text-balance",
|
|
64
|
+
pretty: "ax:text-pretty",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface TextProps
|
|
70
|
+
extends Omit<React.HTMLAttributes<HTMLElement>, "color">,
|
|
71
|
+
VariantProps<typeof textVariants> {
|
|
72
|
+
as?: TextAs
|
|
73
|
+
truncate?: boolean
|
|
74
|
+
wrap?: TextWrap
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const Text = React.forwardRef<HTMLElement, TextProps>(
|
|
78
|
+
(
|
|
79
|
+
{
|
|
80
|
+
as: Tag = "span",
|
|
81
|
+
className,
|
|
82
|
+
size,
|
|
83
|
+
weight,
|
|
84
|
+
align,
|
|
85
|
+
color,
|
|
86
|
+
truncate,
|
|
87
|
+
wrap,
|
|
88
|
+
...props
|
|
89
|
+
},
|
|
90
|
+
ref
|
|
91
|
+
) => (
|
|
92
|
+
<Tag
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
ref={ref as any}
|
|
95
|
+
className={cn(
|
|
96
|
+
textVariants({ size, weight, align, color }),
|
|
97
|
+
truncate && "ax:truncate",
|
|
98
|
+
wrap && WRAP_CLASS[wrap],
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
Text.displayName = "Text"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { ThinkingBar } from "./thinking-bar"
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ThinkingBar> = {
|
|
5
|
+
title: "Components/ThinkingBar",
|
|
6
|
+
component: ThinkingBar,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
|
|
9
|
+
args: { text: "Thinking" },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof ThinkingBar>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {}
|
|
16
|
+
|
|
17
|
+
export const WithActions: Story = {
|
|
18
|
+
args: { text: "Comparing registered sales", onClick: () => undefined, onStop: () => undefined },
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { ThinkingBar } from "./thinking-bar"
|
|
5
|
+
|
|
6
|
+
const buttons = (html: string) => html.match(/<button/g)?.length ?? 0
|
|
7
|
+
|
|
8
|
+
describe("ThinkingBar", () => {
|
|
9
|
+
it("shows a shimmering Thinking status with no buttons by default", () => {
|
|
10
|
+
const html = renderToStaticMarkup(<ThinkingBar />)
|
|
11
|
+
expect(html).toContain('data-slot="shimmer-text"')
|
|
12
|
+
expect(html).toContain(">Thinking<")
|
|
13
|
+
expect(buttons(html)).toBe(0)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("turns the label into a button with a chevron when onClick is passed", () => {
|
|
17
|
+
const html = renderToStaticMarkup(<ThinkingBar text="Searching listings" onClick={() => undefined} />)
|
|
18
|
+
expect(buttons(html)).toBe(1)
|
|
19
|
+
expect(html).toContain("lucide-chevron-right")
|
|
20
|
+
expect(html).toContain(">Searching listings<")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("adds the stop action with its label when onStop is passed", () => {
|
|
24
|
+
expect(renderToStaticMarkup(<ThinkingBar onStop={() => undefined} />)).toContain(">Answer now</button>")
|
|
25
|
+
expect(renderToStaticMarkup(<ThinkingBar onStop={() => undefined} stopLabel="Skip" />)).toContain(">Skip</button>")
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronRight } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { ShimmerText } from "./shimmer-text"
|
|
7
|
+
|
|
8
|
+
export interface ThinkingBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> {
|
|
9
|
+
/** The status, present tense. */
|
|
10
|
+
text?: string
|
|
11
|
+
/** Makes the label a button with a chevron, e.g. to open the reasoning. */
|
|
12
|
+
onClick?: () => void
|
|
13
|
+
/** Adds a stop action on the right, e.g. to answer without more thinking. */
|
|
14
|
+
onStop?: () => void
|
|
15
|
+
stopLabel?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ThinkingBar - one line that says the assistant is thinking: a shimmering
|
|
20
|
+
* status, optionally a button to see more, and an optional action to stop
|
|
21
|
+
* and answer now.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <ThinkingBar text="Thinking" onClick={openReasoning} onStop={answerNow} />
|
|
25
|
+
*/
|
|
26
|
+
export const ThinkingBar = React.forwardRef<HTMLDivElement, ThinkingBarProps>(
|
|
27
|
+
({ text = "Thinking", onClick, onStop, stopLabel = "Answer now", className, ...props }, ref) => (
|
|
28
|
+
<div
|
|
29
|
+
ref={ref}
|
|
30
|
+
data-slot="thinking-bar"
|
|
31
|
+
className={cn("ax:flex ax:w-full ax:items-center ax:justify-between ax:gap-md ax:t-body-sm", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{onClick ? (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={onClick}
|
|
38
|
+
className="ax:group/thinking ax:flex ax:items-center ax:gap-1 ax:rounded-xs ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring"
|
|
39
|
+
>
|
|
40
|
+
<ShimmerText className="ax:font-medium">{text}</ShimmerText>
|
|
41
|
+
<ChevronRight
|
|
42
|
+
className="ax:size-4 ax:text-muted-foreground ax:transition-colors ax:group-hover/thinking:text-foreground"
|
|
43
|
+
aria-hidden
|
|
44
|
+
/>
|
|
45
|
+
</button>
|
|
46
|
+
) : (
|
|
47
|
+
<ShimmerText className="ax:font-medium">{text}</ShimmerText>
|
|
48
|
+
)}
|
|
49
|
+
{onStop && (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={onStop}
|
|
53
|
+
className={cn(
|
|
54
|
+
"ax:shrink-0 ax:rounded-xs ax:text-muted-foreground ax:underline ax:decoration-dotted ax:underline-offset-4 ax:transition-colors",
|
|
55
|
+
"ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring",
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{stopLabel}
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
ThinkingBar.displayName = "ThinkingBar"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { StreamingCursor, ThinkingSteps } from "./thinking-steps"
|
|
4
|
+
import { Text } from "./text"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ThinkingSteps> = {
|
|
7
|
+
title: "Components/ThinkingSteps",
|
|
8
|
+
component: ThinkingSteps,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
|
|
11
|
+
args: {
|
|
12
|
+
running: true,
|
|
13
|
+
total: 3,
|
|
14
|
+
steps: [
|
|
15
|
+
{ label: "Searched 128 listings on Yas Island", status: "done" },
|
|
16
|
+
{ label: "Comparing registered sales", status: "running" },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof ThinkingSteps>
|
|
23
|
+
|
|
24
|
+
export const Running: Story = {}
|
|
25
|
+
|
|
26
|
+
export const Done: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
running: false,
|
|
29
|
+
steps: [
|
|
30
|
+
{ label: "Searched 128 listings on Yas Island", status: "done" },
|
|
31
|
+
{ label: "Compared 42 registered sales", status: "done" },
|
|
32
|
+
{ label: "Checked developer handover dates", status: "done" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Cursor: Story = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<Text as="p" size="2">
|
|
40
|
+
Yas Acres townhouses with a private garden start at
|
|
41
|
+
<StreamingCursor />
|
|
42
|
+
</Text>
|
|
43
|
+
),
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { StreamingCursor, ThinkingSteps, thinkingStepsSummary } from "./thinking-steps"
|
|
5
|
+
|
|
6
|
+
describe("thinkingStepsSummary", () => {
|
|
7
|
+
it("names the step in progress out of the planned total while running", () => {
|
|
8
|
+
expect(thinkingStepsSummary({ running: true, done: 1, total: 3 })).toBe("Working, step 2 of 3")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("never counts past the total", () => {
|
|
12
|
+
expect(thinkingStepsSummary({ running: true, done: 3, total: 3 })).toBe("Working, step 3 of 3")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("counts steps used once finished, singular for one", () => {
|
|
16
|
+
expect(thinkingStepsSummary({ running: false, done: 3, total: 3 })).toBe("Used 3 steps")
|
|
17
|
+
expect(thinkingStepsSummary({ running: false, done: 1, total: 1 })).toBe("Used 1 step")
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe("ThinkingSteps", () => {
|
|
22
|
+
it("opens the step list while running", () => {
|
|
23
|
+
const html = renderToStaticMarkup(
|
|
24
|
+
<ThinkingSteps running total={2} steps={[{ label: "Searched listings", status: "done" }, { label: "Comparing sales", status: "running" }]} />,
|
|
25
|
+
)
|
|
26
|
+
expect(html).toContain("Working, step 2 of 2")
|
|
27
|
+
expect(html).toContain('aria-expanded="true"')
|
|
28
|
+
expect(html).toContain("Comparing sales")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("folds the list away once done", () => {
|
|
32
|
+
const html = renderToStaticMarkup(<ThinkingSteps running={false} steps={[{ label: "Searched listings", status: "done" }]} />)
|
|
33
|
+
expect(html).toContain("Used 1 step")
|
|
34
|
+
expect(html).toContain('aria-expanded="false"')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("StreamingCursor", () => {
|
|
39
|
+
it("is hidden from assistive tech", () => {
|
|
40
|
+
expect(renderToStaticMarkup(<StreamingCursor />)).toContain('aria-hidden="true"')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronDown } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { ShimmerText } from "./shimmer-text"
|
|
7
|
+
|
|
8
|
+
export type ThinkingStepStatus = "done" | "running"
|
|
9
|
+
|
|
10
|
+
export interface ThinkingStep {
|
|
11
|
+
/** Past tense once done ("Searched 12 listings"), present while running ("Searching listings"). */
|
|
12
|
+
label: string
|
|
13
|
+
status: ThinkingStepStatus
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** "Working, step 2 of 4" while running; "Used 4 steps" once finished. */
|
|
17
|
+
export function thinkingStepsSummary({ running, done, total }: { running: boolean; done: number; total: number }) {
|
|
18
|
+
if (running) return `Working, step ${Math.min(done + 1, total)} of ${total}`
|
|
19
|
+
return `Used ${done} ${done === 1 ? "step" : "steps"}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ThinkingStepsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> {
|
|
23
|
+
/** Steps to show so far, in order. Leave out steps that have not started. */
|
|
24
|
+
steps: ThinkingStep[]
|
|
25
|
+
/** True while the assistant is still working; the list opens and the summary shimmers. */
|
|
26
|
+
running: boolean
|
|
27
|
+
/** Steps planned in all, for "step 2 of 4". Defaults to `steps.length`. */
|
|
28
|
+
total?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ThinkingSteps - what the assistant is doing before and while it answers: a
|
|
33
|
+
* muted one-line summary that toggles the step list, which hangs off a thin
|
|
34
|
+
* rule. Open while running, folded away once done.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <ThinkingSteps running={working} total={3} steps={[{ label: "Searched listings", status: "done" }]} />
|
|
38
|
+
*/
|
|
39
|
+
export const ThinkingSteps = React.forwardRef<HTMLDivElement, ThinkingStepsProps>(
|
|
40
|
+
({ steps, running, total = steps.length, className, ...props }, ref) => {
|
|
41
|
+
const id = React.useId()
|
|
42
|
+
const [open, setOpen] = React.useState(running)
|
|
43
|
+
React.useEffect(() => setOpen(running), [running])
|
|
44
|
+
const done = steps.filter((step) => step.status === "done").length
|
|
45
|
+
const summary = thinkingStepsSummary({ running, done, total })
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div ref={ref} data-slot="thinking-steps" className={cn("ax:text-sm", className)} {...props}>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
aria-expanded={open}
|
|
52
|
+
aria-controls={`${id}-steps`}
|
|
53
|
+
onClick={() => setOpen((value) => !value)}
|
|
54
|
+
className="ax:flex ax:items-center ax:gap-1 ax:rounded-xs ax:text-muted-foreground ax:transition-colors ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring"
|
|
55
|
+
>
|
|
56
|
+
{running ? <ShimmerText className="ax:font-medium">{summary}</ShimmerText> : <span>{summary}</span>}
|
|
57
|
+
<ChevronDown className={cn("ax:size-4 ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")} aria-hidden />
|
|
58
|
+
</button>
|
|
59
|
+
{open && (
|
|
60
|
+
<div id={`${id}-steps`} className="ax:mt-sm ax:grid ax:grid-cols-[min-content_minmax(0,1fr)] ax:gap-x-sm">
|
|
61
|
+
<div className="ax:w-0.5 ax:self-stretch ax:rounded-full ax:bg-muted" aria-hidden />
|
|
62
|
+
<ol className="ax:flex ax:min-w-0 ax:flex-col ax:gap-xs">
|
|
63
|
+
{steps.map((step, index) => (
|
|
64
|
+
<li key={index} className={step.status === "done" ? "ax:text-muted-foreground" : undefined}>
|
|
65
|
+
{step.status === "running" ? <ShimmerText>{step.label}</ShimmerText> : step.label}
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ol>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
ThinkingSteps.displayName = "ThinkingSteps"
|
|
76
|
+
|
|
77
|
+
/** The blinking block caret at the end of text that is still being written. Decorative. */
|
|
78
|
+
export const StreamingCursor = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
79
|
+
({ className, ...props }, ref) => (
|
|
80
|
+
<span
|
|
81
|
+
ref={ref}
|
|
82
|
+
data-slot="streaming-cursor"
|
|
83
|
+
aria-hidden
|
|
84
|
+
className={cn(
|
|
85
|
+
"ax:ms-0.5 ax:inline-block ax:h-4 ax:w-1.5 ax:translate-y-0.5 ax:animate-pulse ax:rounded-xs ax:bg-foreground ax:motion-reduce:animate-none",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
StreamingCursor.displayName = "StreamingCursor"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Button } from "./button"
|
|
4
|
+
import { toast } from "./toaster"
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Toaster",
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "centered",
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"Application toast host (Sonner). A single Toaster is mounted in Storybook preview, same as placing it once near your app root. Call `toast()` from click handlers or other events; never call it during render.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta
|
|
19
|
+
|
|
20
|
+
export default meta
|
|
21
|
+
type Story = StoryObj
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<div className="ax:space-y-sm">
|
|
26
|
+
<p className="ax:t-body-sm ax:text-muted-foreground ax:max-w-sm">
|
|
27
|
+
Click the button to fire a toast. The host is mounted globally in Storybook preview.
|
|
28
|
+
</p>
|
|
29
|
+
<Button onClick={() => toast("Changes saved")}>Show toast</Button>
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const WithDescription: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<Button
|
|
37
|
+
onClick={() =>
|
|
38
|
+
toast("Invite sent", {
|
|
39
|
+
description: "We emailed a sign-in link to your teammate.",
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
Show toast with description
|
|
44
|
+
</Button>
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const Success: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<Button variant="secondary" onClick={() => toast.success("Payment received")}>
|
|
51
|
+
Success toast
|
|
52
|
+
</Button>
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Error: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<Button variant="destructive" onClick={() => toast.error("Could not save changes")}>
|
|
59
|
+
Error toast
|
|
60
|
+
</Button>
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const Action: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<Button
|
|
67
|
+
variant="outline"
|
|
68
|
+
onClick={() =>
|
|
69
|
+
toast("Draft deleted", {
|
|
70
|
+
action: {
|
|
71
|
+
label: "Undo",
|
|
72
|
+
onClick: () => toast("Draft restored"),
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
Toast with undo action
|
|
78
|
+
</Button>
|
|
79
|
+
),
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Toaster as SonnerToaster, toast } from "sonner"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Toaster - application-level toast host.
|
|
8
|
+
*
|
|
9
|
+
* Uses sonner under the hood. Mount once near the root of every app.
|
|
10
|
+
* Re-export `toast()` so consumers can call it without a second dependency.
|
|
11
|
+
*/
|
|
12
|
+
export function Toaster() {
|
|
13
|
+
return (
|
|
14
|
+
<SonnerToaster
|
|
15
|
+
position="bottom-right"
|
|
16
|
+
richColors={false}
|
|
17
|
+
closeButton
|
|
18
|
+
toastOptions={{
|
|
19
|
+
unstyled: false,
|
|
20
|
+
classNames: {
|
|
21
|
+
toast:
|
|
22
|
+
"ax:group ax:bg-card ax:text-card-foreground ax:border ax:border-border ax:rounded-md ax:shadow-md",
|
|
23
|
+
title: "ax:t-label-sm ax:text-foreground",
|
|
24
|
+
description: "ax:t-caption ax:text-muted-foreground",
|
|
25
|
+
actionButton:
|
|
26
|
+
"ax:bg-primary ax:text-primary-foreground ax:rounded-md ax:px-2.5 ax:py-1 ax:t-label-sm",
|
|
27
|
+
cancelButton:
|
|
28
|
+
"ax:bg-muted ax:text-muted-foreground ax:rounded-md ax:px-2.5 ax:py-1 ax:t-label-sm",
|
|
29
|
+
closeButton: "ax:bg-muted ax:text-muted-foreground ax:border ax:border-border",
|
|
30
|
+
},
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { toast }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Bold } from "lucide-react"
|
|
4
|
+
import { Toggle } from "./toggle"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Toggle> = {
|
|
7
|
+
title: "Components/Toggle",
|
|
8
|
+
component: Toggle,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof Toggle>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<Toggle aria-label="Toggle bold">
|
|
18
|
+
<Bold className="ax:size-4" />
|
|
19
|
+
</Toggle>
|
|
20
|
+
),
|
|
21
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TogglePrimitive from "@radix-ui/react-switch"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
export type ToggleSize = "sm" | "md" | "lg"
|
|
8
|
+
|
|
9
|
+
// Track / thumb / travel scale together. Travel = track inner width − thumb:
|
|
10
|
+
// sm 28−4(borders)−12 = 12 (translate-x-3)
|
|
11
|
+
// md 36−4−16 = 16 (translate-x-4)
|
|
12
|
+
// lg 44−4−20 = 20 (translate-x-5)
|
|
13
|
+
const TRACK: Record<ToggleSize, string> = {
|
|
14
|
+
sm: "ax:h-4 ax:w-7", // 16 × 28
|
|
15
|
+
md: "ax:h-5 ax:w-9", // 20 × 36
|
|
16
|
+
lg: "ax:h-6 ax:w-11", // 24 × 44
|
|
17
|
+
}
|
|
18
|
+
const THUMB: Record<ToggleSize, string> = {
|
|
19
|
+
sm: "ax:size-3", // 12
|
|
20
|
+
md: "ax:size-4", // 16
|
|
21
|
+
lg: "ax:size-5", // 20
|
|
22
|
+
}
|
|
23
|
+
const THUMB_TRANSLATE: Record<ToggleSize, string> = {
|
|
24
|
+
sm: "ax:data-[state=checked]:translate-x-3",
|
|
25
|
+
md: "ax:data-[state=checked]:translate-x-4",
|
|
26
|
+
lg: "ax:data-[state=checked]:translate-x-5",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToggleProps
|
|
30
|
+
extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> {
|
|
31
|
+
size?: ToggleSize
|
|
32
|
+
/**
|
|
33
|
+
* Optional caption rendered to the inline-start (left) of the switch. When set,
|
|
34
|
+
* the switch and text are wrapped in a <label> so the text is clickable and
|
|
35
|
+
* names the control - no separate `aria-label` needed.
|
|
36
|
+
*/
|
|
37
|
+
label?: React.ReactNode
|
|
38
|
+
/**
|
|
39
|
+
* Classes for the wrapping <label> (only used when `label` is set). `className`
|
|
40
|
+
* still targets the switch track itself.
|
|
41
|
+
*/
|
|
42
|
+
labelClassName?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A binary on/off toggle (built on Radix Switch).
|
|
47
|
+
*
|
|
48
|
+
* Pass `label` for a captioned control: the text renders to the LEFT of the
|
|
49
|
+
* switch and toggles it on click. Omit `label` to render the bare switch and
|
|
50
|
+
* compose your own layout (e.g. a spaced settings row with `justify-between`).
|
|
51
|
+
*/
|
|
52
|
+
export const Toggle = React.forwardRef<
|
|
53
|
+
React.ElementRef<typeof TogglePrimitive.Root>,
|
|
54
|
+
ToggleProps
|
|
55
|
+
>(({ className, size = "md", label, labelClassName, ...props }, ref) => {
|
|
56
|
+
const control = (
|
|
57
|
+
<TogglePrimitive.Root
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(
|
|
60
|
+
"ax:peer ax:inline-flex ax:shrink-0 ax:cursor-pointer ax:items-center ax:rounded-full ax:border-2 ax:border-transparent",
|
|
61
|
+
"ax:transition-colors ax:transition-snappy",
|
|
62
|
+
"ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2 ax:focus-visible:ring-offset-background",
|
|
63
|
+
"ax:disabled:cursor-not-allowed ax:disabled:opacity-50",
|
|
64
|
+
"ax:data-[state=checked]:bg-primary ax:data-[state=unchecked]:bg-input",
|
|
65
|
+
TRACK[size],
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<TogglePrimitive.Thumb
|
|
71
|
+
className={cn(
|
|
72
|
+
"ax:pointer-events-none ax:block ax:rounded-full ax:bg-card ax:shadow-sm",
|
|
73
|
+
"ax:transition-transform ax:transition-snappy",
|
|
74
|
+
"ax:data-[state=unchecked]:translate-x-0",
|
|
75
|
+
THUMB[size],
|
|
76
|
+
THUMB_TRANSLATE[size]
|
|
77
|
+
)}
|
|
78
|
+
/>
|
|
79
|
+
</TogglePrimitive.Root>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (label == null) return control
|
|
83
|
+
|
|
84
|
+
// Label on the inline-start (left). The switch dims itself when disabled
|
|
85
|
+
// (`disabled:opacity-50`); the text follows via `has-[button:disabled]`, so
|
|
86
|
+
// the two never double up on opacity.
|
|
87
|
+
return (
|
|
88
|
+
<label
|
|
89
|
+
className={cn(
|
|
90
|
+
"ax:inline-flex ax:items-center ax:gap-sm ax:cursor-pointer",
|
|
91
|
+
"ax:has-[button:disabled]:cursor-not-allowed",
|
|
92
|
+
"ax:[&:has(button:disabled)_[data-slot=toggle-label]]:opacity-50",
|
|
93
|
+
labelClassName
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<span data-slot="toggle-label" className="ax:t-label-sm ax:text-foreground ax:select-none">
|
|
97
|
+
{label}
|
|
98
|
+
</span>
|
|
99
|
+
{control}
|
|
100
|
+
</label>
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
Toggle.displayName = "Toggle"
|