@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,301 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
5
|
+
import { cva } from "class-variance-authority"
|
|
6
|
+
import { cn } from "../lib/cn"
|
|
7
|
+
|
|
8
|
+
export type SegmentedControlSize = "sm" | "md" | "lg"
|
|
9
|
+
|
|
10
|
+
/** Colour theme for the whole control. Drives the track background (a weak
|
|
11
|
+
* `{variant}` tint) and the selected thumb's drop shadow (`{variant}-strong`),
|
|
12
|
+
* so the control picks up the brand's neutral, primary, or secondary hue. */
|
|
13
|
+
export type SegmentedControlVariant = "neutral" | "primary" | "secondary"
|
|
14
|
+
|
|
15
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
interface SegmentedControlContextValue {
|
|
18
|
+
size: SegmentedControlSize
|
|
19
|
+
currentValue: string
|
|
20
|
+
variant: SegmentedControlVariant
|
|
21
|
+
registerItem: (value: string, el: HTMLButtonElement | null) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SegmentedControlContext = React.createContext<SegmentedControlContextValue>({
|
|
25
|
+
size: "md",
|
|
26
|
+
currentValue: "",
|
|
27
|
+
variant: "neutral",
|
|
28
|
+
registerItem: () => {},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// ─── Variants ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
// Outer heights mirror Button/Input/Dropdown so a SegmentedControl sits at
|
|
34
|
+
// the same height as those primitives when placed side-by-side, and item
|
|
35
|
+
// padding mirrors Button so each item has identical type metrics to a
|
|
36
|
+
// standalone Button at the same size:
|
|
37
|
+
// sm → 32px (h-2xl) md → 40px (h-3xl) lg → 48px (h-4xl)
|
|
38
|
+
//
|
|
39
|
+
// No root inset: items go edge-to-edge so the total outer width matches the
|
|
40
|
+
// sum of equivalent standalone Buttons (no extra 2px frame).
|
|
41
|
+
//
|
|
42
|
+
// Uses --input-radius (not --radius) so the control stays squared in pill
|
|
43
|
+
// brands. Only Button/Badge should pick up the brand's button-pill setting.
|
|
44
|
+
const rootVariants = cva(
|
|
45
|
+
"ax:relative ax:inline-flex ax:items-stretch ax:rounded-(--input-radius)",
|
|
46
|
+
{
|
|
47
|
+
variants: {
|
|
48
|
+
size: {
|
|
49
|
+
sm: "ax:h-xl ax:gap-px",
|
|
50
|
+
md: "ax:h-2xl ax:gap-px",
|
|
51
|
+
lg: "ax:h-3xl ax:gap-px",
|
|
52
|
+
},
|
|
53
|
+
// Track fill tint per theme. Neutral keeps the established `bg-muted`;
|
|
54
|
+
// primary/secondary use a weak brand tint. Applied via compoundVariants so
|
|
55
|
+
// it's suppressed when `transparent`.
|
|
56
|
+
variant: {
|
|
57
|
+
neutral: "",
|
|
58
|
+
primary: "",
|
|
59
|
+
secondary: "",
|
|
60
|
+
},
|
|
61
|
+
// `transparent` drops the track fill, for placing the control on an
|
|
62
|
+
// already-tinted surface where the track tint would clash.
|
|
63
|
+
transparent: {
|
|
64
|
+
true: "ax:bg-transparent",
|
|
65
|
+
false: "",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
compoundVariants: [
|
|
69
|
+
{ variant: "neutral", transparent: false, class: "ax:bg-neutral-weakest" },
|
|
70
|
+
{ variant: "primary", transparent: false, class: "ax:bg-primary-weakest" },
|
|
71
|
+
{ variant: "secondary", transparent: false, class: "ax:bg-secondary-weakest" },
|
|
72
|
+
],
|
|
73
|
+
defaultVariants: { size: "md", variant: "neutral", transparent: false },
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const itemVariants = cva(
|
|
78
|
+
[
|
|
79
|
+
"ax:relative ax:z-10 ax:flex ax:items-center ax:justify-center ax:cursor-pointer ax:select-none ax:whitespace-nowrap",
|
|
80
|
+
"ax:rounded-(--input-radius)",
|
|
81
|
+
"ax:transition-colors ax:transition-snappy",
|
|
82
|
+
"ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-1 ax:focus-visible:ring-offset-muted",
|
|
83
|
+
"ax:disabled:cursor-not-allowed ax:disabled:opacity-50",
|
|
84
|
+
],
|
|
85
|
+
{
|
|
86
|
+
variants: {
|
|
87
|
+
size: {
|
|
88
|
+
sm: "ax:px-sm ax:t-label-xs",
|
|
89
|
+
md: "ax:px-md ax:t-label-sm",
|
|
90
|
+
lg: "ax:px-xl ax:t-label-md",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
defaultVariants: { size: "md" },
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// Colours the floating indicator's shadow per theme. The default `--shadow-sm`
|
|
98
|
+
// token bakes its colour in at ~4% alpha, so simply retinting it reads as a
|
|
99
|
+
// plain neutral shadow. Instead we rebuild the token's diffuse layer falloff
|
|
100
|
+
// tinted with the raw `{variant}-strong` triplet (which auto-inverts in dark
|
|
101
|
+
// mode) at a visible alpha, so the selected thumb's lift clearly carries the hue.
|
|
102
|
+
const tintedShadow = (colorVar: string): string =>
|
|
103
|
+
[
|
|
104
|
+
`hsl(${colorVar} / 0.10) 0px 0px 0px 1px`,
|
|
105
|
+
`hsl(${colorVar} / 0.20) 0px 0.5625px 0.5625px -0.28125px`,
|
|
106
|
+
`hsl(${colorVar} / 0.20) 0px 1.6875px 1.6875px -0.84375px`,
|
|
107
|
+
`hsl(${colorVar} / 0.18) 0px 3.375px 3.375px -1.6875px`,
|
|
108
|
+
`hsl(${colorVar} / 0.16) 0px 6.75px 6.75px -3.375px`,
|
|
109
|
+
`hsl(${colorVar} / 0.14) 0px 13.5px 13.5px -6.75px`,
|
|
110
|
+
].join(", ")
|
|
111
|
+
|
|
112
|
+
// Neutral keeps the default `--shadow-sm` token (unchanged subtle look, no
|
|
113
|
+
// regression for existing usages); branded variants get the tinted shadow.
|
|
114
|
+
const variantBoxShadow: Record<SegmentedControlVariant, string | undefined> = {
|
|
115
|
+
neutral: undefined,
|
|
116
|
+
primary: tintedShadow("var(--primary)"),
|
|
117
|
+
secondary: tintedShadow("var(--secondary)"),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Selected item's label colour, following the control's variant: neutral reads
|
|
121
|
+
// as plain `foreground`; branded variants tint the label `{variant}-stronger`
|
|
122
|
+
// and bump the weight to reinforce the selection.
|
|
123
|
+
const activeLabelClass: Record<SegmentedControlVariant, string> = {
|
|
124
|
+
neutral: "ax:text-foreground",
|
|
125
|
+
primary: "ax:text-primary-foreground ax:font-medium",
|
|
126
|
+
secondary: "ax:text-secondary-foreground ax:font-medium",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Unselected items' label colour, following the control's variant: the
|
|
130
|
+
// `{variant}-strong` hue held back at half opacity, lifting toward full on
|
|
131
|
+
// hover so the option still reads as interactive.
|
|
132
|
+
const inactiveLabelClass: Record<SegmentedControlVariant, string> = {
|
|
133
|
+
neutral: "ax:text-muted-foreground ax:hover:opacity-75",
|
|
134
|
+
primary: "ax:text-primary-foreground ax:hover:opacity-85",
|
|
135
|
+
secondary: "ax:text-secondary-foreground ax:hover:opacity-85",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Root ─────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export interface SegmentedControlProps
|
|
141
|
+
extends Omit<
|
|
142
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
|
|
143
|
+
"orientation"
|
|
144
|
+
> {
|
|
145
|
+
size?: SegmentedControlSize
|
|
146
|
+
/** When true, the control stretches to fill its container and each item
|
|
147
|
+
* takes an equal share of the width. Use for view toggles embedded in
|
|
148
|
+
* toolbars, modal footers, or any layout where the control should fill
|
|
149
|
+
* the available space rather than hug its content. */
|
|
150
|
+
fullWidth?: boolean
|
|
151
|
+
/** When true, the track background is transparent (drops the `bg-muted` fill).
|
|
152
|
+
* Use when the control sits on an already-tinted surface. */
|
|
153
|
+
transparent?: boolean
|
|
154
|
+
/** Colour theme for the whole control: tints the track background (weak
|
|
155
|
+
* `{variant}`), the selected thumb's shadow (`{variant}-strong`), and the
|
|
156
|
+
* selected item's label (`{variant}-stronger`). Defaults to `neutral`. */
|
|
157
|
+
variant?: SegmentedControlVariant
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const SegmentedControl = React.forwardRef<
|
|
161
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
162
|
+
SegmentedControlProps
|
|
163
|
+
>(({ className, size = "md", fullWidth = false, transparent = false, variant = "neutral", value, defaultValue, onValueChange, children, ...props }, ref) => {
|
|
164
|
+
const rootRef = React.useRef<HTMLDivElement>(null)
|
|
165
|
+
const itemRefs = React.useRef(new Map<string, HTMLButtonElement>())
|
|
166
|
+
const positioned = React.useRef(false)
|
|
167
|
+
|
|
168
|
+
const [internalValue, setInternalValue] = React.useState<string>(
|
|
169
|
+
value ?? defaultValue ?? ""
|
|
170
|
+
)
|
|
171
|
+
const currentValue = value ?? internalValue
|
|
172
|
+
|
|
173
|
+
const [indicatorStyle, setIndicatorStyle] = React.useState<React.CSSProperties>({ opacity: 0 })
|
|
174
|
+
|
|
175
|
+
const updateIndicator = React.useCallback((val: string) => {
|
|
176
|
+
const el = itemRefs.current.get(val)
|
|
177
|
+
const root = rootRef.current
|
|
178
|
+
if (!el || !root) return
|
|
179
|
+
|
|
180
|
+
const rootRect = root.getBoundingClientRect()
|
|
181
|
+
const itemRect = el.getBoundingClientRect()
|
|
182
|
+
|
|
183
|
+
setIndicatorStyle({
|
|
184
|
+
left: itemRect.left - rootRect.left,
|
|
185
|
+
width: itemRect.width,
|
|
186
|
+
top: itemRect.top - rootRect.top,
|
|
187
|
+
height: itemRect.height,
|
|
188
|
+
opacity: 1,
|
|
189
|
+
transition: positioned.current ? "left 150ms ease, width 150ms ease" : undefined,
|
|
190
|
+
})
|
|
191
|
+
positioned.current = true
|
|
192
|
+
}, [])
|
|
193
|
+
|
|
194
|
+
// Reposition when value or children change.
|
|
195
|
+
React.useLayoutEffect(() => {
|
|
196
|
+
updateIndicator(currentValue)
|
|
197
|
+
}, [currentValue, updateIndicator])
|
|
198
|
+
|
|
199
|
+
// Reposition on container resize (theme panel open/close shifts layout).
|
|
200
|
+
React.useEffect(() => {
|
|
201
|
+
const root = rootRef.current
|
|
202
|
+
if (!root) return
|
|
203
|
+
const observer = new ResizeObserver(() => updateIndicator(currentValue))
|
|
204
|
+
observer.observe(root)
|
|
205
|
+
return () => observer.disconnect()
|
|
206
|
+
}, [currentValue, updateIndicator])
|
|
207
|
+
|
|
208
|
+
const handleValueChange = React.useCallback(
|
|
209
|
+
(val: string) => {
|
|
210
|
+
setInternalValue(val)
|
|
211
|
+
onValueChange?.(val)
|
|
212
|
+
},
|
|
213
|
+
[onValueChange]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const registerItem = React.useCallback(
|
|
217
|
+
(val: string, el: HTMLButtonElement | null) => {
|
|
218
|
+
if (el) itemRefs.current.set(val, el)
|
|
219
|
+
else itemRefs.current.delete(val)
|
|
220
|
+
},
|
|
221
|
+
[]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// Merge the forwarded ref with our local root ref.
|
|
225
|
+
const mergedRef = React.useCallback(
|
|
226
|
+
(node: HTMLDivElement | null) => {
|
|
227
|
+
(rootRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
228
|
+
if (typeof ref === "function") ref(node)
|
|
229
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
230
|
+
},
|
|
231
|
+
[ref]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<SegmentedControlContext.Provider value={{ size, currentValue, variant, registerItem }}>
|
|
236
|
+
<RadioGroupPrimitive.Root
|
|
237
|
+
ref={mergedRef}
|
|
238
|
+
orientation="horizontal"
|
|
239
|
+
value={currentValue}
|
|
240
|
+
onValueChange={handleValueChange}
|
|
241
|
+
className={cn(
|
|
242
|
+
rootVariants({ size, variant, transparent }),
|
|
243
|
+
fullWidth && "ax:flex ax:w-full *:flex-1",
|
|
244
|
+
className
|
|
245
|
+
)}
|
|
246
|
+
{...props}
|
|
247
|
+
>
|
|
248
|
+
{/* Floating indicator - slides behind the active item */}
|
|
249
|
+
<div
|
|
250
|
+
aria-hidden="true"
|
|
251
|
+
// Dark mode: a raised neutral-stronger thumb, lighter than the `bg-muted`
|
|
252
|
+
// track, so the selection pops off the track (mirrors the light-mode
|
|
253
|
+
// white-on-grey lift) consistently across brands. Light stays --card.
|
|
254
|
+
className="ax:absolute ax:bg-card ax:shadow-sm ax:rounded-(--input-radius) ax:pointer-events-none"
|
|
255
|
+
style={{ ...indicatorStyle, boxShadow: variantBoxShadow[variant] }}
|
|
256
|
+
/>
|
|
257
|
+
{children}
|
|
258
|
+
</RadioGroupPrimitive.Root>
|
|
259
|
+
</SegmentedControlContext.Provider>
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
SegmentedControl.displayName = "SegmentedControl"
|
|
263
|
+
|
|
264
|
+
// ─── Item ─────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
export interface SegmentedControlItemProps
|
|
267
|
+
extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {}
|
|
268
|
+
|
|
269
|
+
export const SegmentedControlItem = React.forwardRef<
|
|
270
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
271
|
+
SegmentedControlItemProps
|
|
272
|
+
>(({ className, value, children, ...props }, ref) => {
|
|
273
|
+
const { size, currentValue, variant, registerItem } = React.useContext(SegmentedControlContext)
|
|
274
|
+
const isActive = currentValue === value
|
|
275
|
+
const localRef = React.useRef<HTMLButtonElement>(null)
|
|
276
|
+
|
|
277
|
+
React.useImperativeHandle(ref, () => localRef.current!)
|
|
278
|
+
|
|
279
|
+
React.useLayoutEffect(() => {
|
|
280
|
+
registerItem(value, localRef.current)
|
|
281
|
+
return () => registerItem(value, null)
|
|
282
|
+
}, [value, registerItem])
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<RadioGroupPrimitive.Item
|
|
286
|
+
ref={localRef}
|
|
287
|
+
value={value}
|
|
288
|
+
className={cn(
|
|
289
|
+
itemVariants({ size }),
|
|
290
|
+
isActive
|
|
291
|
+
? activeLabelClass[variant]
|
|
292
|
+
: inactiveLabelClass[variant],
|
|
293
|
+
className
|
|
294
|
+
)}
|
|
295
|
+
{...props}
|
|
296
|
+
>
|
|
297
|
+
{children}
|
|
298
|
+
</RadioGroupPrimitive.Item>
|
|
299
|
+
)
|
|
300
|
+
})
|
|
301
|
+
SegmentedControlItem.displayName = "SegmentedControlItem"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { SelectButton } from "./select-button"
|
|
4
|
+
import { SelectItem } from "./select"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof SelectButton> = {
|
|
7
|
+
title: "Components/SelectButton",
|
|
8
|
+
component: SelectButton,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
args: {
|
|
11
|
+
defaultValue: "all",
|
|
12
|
+
placeholder: "Choose status",
|
|
13
|
+
buttonLabel: "Apply",
|
|
14
|
+
children: (
|
|
15
|
+
<>
|
|
16
|
+
<SelectItem value="all">All statuses</SelectItem>
|
|
17
|
+
<SelectItem value="active">Active</SelectItem>
|
|
18
|
+
<SelectItem value="archived">Archived</SelectItem>
|
|
19
|
+
</>
|
|
20
|
+
),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof SelectButton>
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
5
|
+
import { ChevronDown } from "lucide-react"
|
|
6
|
+
import { Button, type ButtonProps } from "./button"
|
|
7
|
+
import { SelectContent } from "./select"
|
|
8
|
+
import { cn } from "../lib/cn"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SelectButton - a Select + Button compactly joined into a single inline group.
|
|
12
|
+
*
|
|
13
|
+
* The two halves share the same height (sm/md/lg align with Button, Input,
|
|
14
|
+
* and Dropdown so they line up when placed side-by-side). The Select trigger
|
|
15
|
+
* occupies the left half; the Button occupies the right half and accepts any
|
|
16
|
+
* Button `variant`. SelectItem children render inside the Select's popover.
|
|
17
|
+
*
|
|
18
|
+
* <SelectButton
|
|
19
|
+
* size="md"
|
|
20
|
+
* variant="primary"
|
|
21
|
+
* placeholder="Choose status"
|
|
22
|
+
* value={value}
|
|
23
|
+
* onValueChange={setValue}
|
|
24
|
+
* buttonLabel="Apply"
|
|
25
|
+
* onButtonClick={handleApply}
|
|
26
|
+
* >
|
|
27
|
+
* <SelectItem value="active">Active</SelectItem>
|
|
28
|
+
* <SelectItem value="archived">Archived</SelectItem>
|
|
29
|
+
* </SelectButton>
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type SelectButtonSize = "sm" | "md" | "lg"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Variant whitelist for the right-hand action button.
|
|
36
|
+
* Excludes Button variants that don't fit the joined-pill composite:
|
|
37
|
+
* ghost - transparent background reads as "no button"
|
|
38
|
+
* accent - wrong semantics for a commit action
|
|
39
|
+
* destructive - wrong semantics for a select+commit toolbar
|
|
40
|
+
* link - underline-on-hover text breaks the joined-pill shape
|
|
41
|
+
*/
|
|
42
|
+
export type SelectButtonVariant = Exclude<NonNullable<ButtonProps["variant"]>, "ghost" | "accent" | "destructive">
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const TRIGGER_HEIGHT: Record<SelectButtonSize, string> = {
|
|
46
|
+
sm: "ax:h-2xl", // 32px
|
|
47
|
+
md: "ax:h-3xl", // 40px
|
|
48
|
+
lg: "ax:h-4xl", // 48px
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mirrors the standalone Select / Input trigger scale so a SelectButton's
|
|
52
|
+
// left half has identical horizontal padding + type metrics to a standalone
|
|
53
|
+
// Select at the same size.
|
|
54
|
+
const TRIGGER_PADDING_X: Record<SelectButtonSize, string> = {
|
|
55
|
+
sm: "ax:px-sm",
|
|
56
|
+
md: "ax:px-md",
|
|
57
|
+
lg: "ax:px-xl",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TRIGGER_TEXT: Record<SelectButtonSize, string> = {
|
|
61
|
+
sm: "ax:t-label-sm",
|
|
62
|
+
md: "ax:t-label-sm",
|
|
63
|
+
lg: "ax:text-base",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SelectButtonProps {
|
|
67
|
+
/** Shared height/typography ladder - matches Button, Input, Dropdown. */
|
|
68
|
+
size?: SelectButtonSize
|
|
69
|
+
/** Visual treatment of the right-hand action button. */
|
|
70
|
+
variant?: SelectButtonVariant
|
|
71
|
+
|
|
72
|
+
// ─── Select half ────────────────────────────────────────────────────────────
|
|
73
|
+
value?: string
|
|
74
|
+
defaultValue?: string
|
|
75
|
+
onValueChange?: (value: string) => void
|
|
76
|
+
placeholder?: string
|
|
77
|
+
/** Disables BOTH halves. Use `buttonDisabled` to disable only the button. */
|
|
78
|
+
disabled?: boolean
|
|
79
|
+
name?: string
|
|
80
|
+
/** SelectItem (and SelectLabel/SelectSeparator) children for the popover. */
|
|
81
|
+
children: React.ReactNode
|
|
82
|
+
|
|
83
|
+
// ─── Button half ────────────────────────────────────────────────────────────
|
|
84
|
+
buttonLabel: React.ReactNode
|
|
85
|
+
onButtonClick?: React.MouseEventHandler<HTMLButtonElement>
|
|
86
|
+
buttonDisabled?: boolean
|
|
87
|
+
buttonType?: "button" | "submit" | "reset"
|
|
88
|
+
/** Extra className appended to the inner Button. */
|
|
89
|
+
buttonClassName?: string
|
|
90
|
+
|
|
91
|
+
className?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const SelectButton = React.forwardRef<HTMLDivElement, SelectButtonProps>(
|
|
95
|
+
(
|
|
96
|
+
{
|
|
97
|
+
size = "md",
|
|
98
|
+
variant = "primary",
|
|
99
|
+
value,
|
|
100
|
+
defaultValue,
|
|
101
|
+
onValueChange,
|
|
102
|
+
placeholder,
|
|
103
|
+
disabled,
|
|
104
|
+
name,
|
|
105
|
+
children,
|
|
106
|
+
buttonLabel,
|
|
107
|
+
onButtonClick,
|
|
108
|
+
buttonDisabled,
|
|
109
|
+
buttonType = "button",
|
|
110
|
+
buttonClassName,
|
|
111
|
+
className,
|
|
112
|
+
},
|
|
113
|
+
ref
|
|
114
|
+
) => (
|
|
115
|
+
<div
|
|
116
|
+
ref={ref}
|
|
117
|
+
data-slot="select-button"
|
|
118
|
+
className={cn(
|
|
119
|
+
"ax:inline-flex ax:items-stretch ax:isolate",
|
|
120
|
+
disabled && "ax:opacity-50 ax:pointer-events-none",
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<SelectPrimitive.Root
|
|
125
|
+
value={value}
|
|
126
|
+
defaultValue={defaultValue}
|
|
127
|
+
onValueChange={onValueChange}
|
|
128
|
+
disabled={disabled}
|
|
129
|
+
name={name}
|
|
130
|
+
>
|
|
131
|
+
<SelectPrimitive.Trigger
|
|
132
|
+
className={cn(
|
|
133
|
+
"ax:inline-flex ax:items-center ax:justify-between ax:gap-xs ax:whitespace-nowrap ax:cursor-pointer ax:select-none",
|
|
134
|
+
"ax:bg-transparent ax:text-foreground",
|
|
135
|
+
"ax:border ax:border-input ax:rounded-l-var(--input-radius) ax:rounded-r-none ax:-mr-px",
|
|
136
|
+
"ax:transition-[border-color,box-shadow] ax:transition-snappy",
|
|
137
|
+
"ax:focus:outline-none ax:focus:z-10 ax:focus:ring-2 ax:focus:ring-ring ax:focus:ring-offset-1 ax:focus:ring-offset-background ax:focus:border-transparent",
|
|
138
|
+
"ax:disabled:cursor-not-allowed ax:disabled:opacity-50",
|
|
139
|
+
"ax:data-placeholder:text-muted-foreground",
|
|
140
|
+
// leading-snug on the value span: t-label-sm is leading-none, and line-clamp-1
|
|
141
|
+
// sets overflow-hidden: a 1.0 line-height would shave descenders (g, y, p).
|
|
142
|
+
"ax:[&>span]:line-clamp-1 ax:[&>span]:leading-snug",
|
|
143
|
+
TRIGGER_HEIGHT[size],
|
|
144
|
+
TRIGGER_PADDING_X[size],
|
|
145
|
+
TRIGGER_TEXT[size]
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<SelectPrimitive.Value placeholder={placeholder} />
|
|
149
|
+
<SelectPrimitive.Icon asChild>
|
|
150
|
+
<ChevronDown className="ax:size-4 ax:shrink-0 ax:text-muted-foreground" />
|
|
151
|
+
</SelectPrimitive.Icon>
|
|
152
|
+
</SelectPrimitive.Trigger>
|
|
153
|
+
<SelectContent>{children}</SelectContent>
|
|
154
|
+
</SelectPrimitive.Root>
|
|
155
|
+
<Button
|
|
156
|
+
type={buttonType}
|
|
157
|
+
size={size}
|
|
158
|
+
variant={variant}
|
|
159
|
+
onClick={onButtonClick}
|
|
160
|
+
disabled={buttonDisabled || disabled}
|
|
161
|
+
className={cn(
|
|
162
|
+
// Use --input-radius (not --radius) on the right corner so the
|
|
163
|
+
// composite control stays squared in pill brands. Only the
|
|
164
|
+
// Button/Badge tokens should pick up the brand button-pill.
|
|
165
|
+
"ax:rounded-l-none ax:rounded-r-[var(--input-radius)] ax:focus-visible:z-10",
|
|
166
|
+
buttonClassName
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
{buttonLabel}
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
SelectButton.displayName = "SelectButton"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Select> = {
|
|
6
|
+
title: "Components/Select",
|
|
7
|
+
component: Select,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof Select>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<Select defaultValue="apple">
|
|
17
|
+
<SelectTrigger className="ax:w-[200px]">
|
|
18
|
+
<SelectValue placeholder="Select a fruit" />
|
|
19
|
+
</SelectTrigger>
|
|
20
|
+
<SelectContent>
|
|
21
|
+
<SelectItem value="apple">Apple</SelectItem>
|
|
22
|
+
<SelectItem value="banana">Banana</SelectItem>
|
|
23
|
+
<SelectItem value="orange">Orange</SelectItem>
|
|
24
|
+
</SelectContent>
|
|
25
|
+
</Select>
|
|
26
|
+
),
|
|
27
|
+
}
|