banhaten 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/CONTRIBUTING.md +44 -0
- package/README.md +65 -13
- package/SECURITY.md +15 -0
- package/banhaten.config.example.json +0 -2
- package/docs/design-system/README.md +9 -0
- package/docs/design-system/adoption-quality-remediation.md +87 -0
- package/docs/design-system/agent-usage.md +13 -0
- package/docs/design-system/appearance-presets.md +6 -1
- package/docs/design-system/component-docs-pattern.md +81 -0
- package/docs/design-system/component-implementation-ownership-audit.md +244 -0
- package/docs/design-system/component-quality-audit-2026-08-20.md +223 -0
- package/docs/design-system/component-showcase-consistency-report.md +2 -0
- package/docs/design-system/component-token-consistency-audit.md +13 -3
- package/docs/design-system/components/README.md +4 -2
- package/docs/design-system/components/accordion.md +16 -0
- package/docs/design-system/components/activity-feed.md +17 -1
- package/docs/design-system/components/activity-heatmap.md +73 -0
- package/docs/design-system/components/alert-dialog.md +17 -0
- package/docs/design-system/components/alert.md +46 -14
- package/docs/design-system/components/aspect-ratio.md +15 -0
- package/docs/design-system/components/attribute.md +16 -0
- package/docs/design-system/components/autocomplete.md +19 -0
- package/docs/design-system/components/avatar.md +18 -0
- package/docs/design-system/components/badge.md +19 -0
- package/docs/design-system/components/banner.md +19 -1
- package/docs/design-system/components/breadcrumbs.md +17 -0
- package/docs/design-system/components/button-group.md +35 -0
- package/docs/design-system/components/button.md +18 -0
- package/docs/design-system/components/card.md +26 -1
- package/docs/design-system/components/carousel.md +20 -3
- package/docs/design-system/components/catalog-components.md +18 -1
- package/docs/design-system/components/chart.md +15 -0
- package/docs/design-system/components/checkbox.md +16 -0
- package/docs/design-system/components/collapsible.md +15 -0
- package/docs/design-system/components/command.md +15 -0
- package/docs/design-system/components/context-menu.md +15 -0
- package/docs/design-system/components/date-picker.md +21 -1
- package/docs/design-system/components/divider.md +16 -0
- package/docs/design-system/components/empty-state.md +17 -0
- package/docs/design-system/components/field.md +17 -0
- package/docs/design-system/components/file-upload.md +18 -0
- package/docs/design-system/components/hover-card.md +15 -0
- package/docs/design-system/components/input-group.md +17 -0
- package/docs/design-system/components/input-otp.md +18 -0
- package/docs/design-system/components/input.md +43 -7
- package/docs/design-system/components/item.md +72 -0
- package/docs/design-system/components/kbd.md +18 -0
- package/docs/design-system/components/label.md +16 -0
- package/docs/design-system/components/menu.md +17 -0
- package/docs/design-system/components/menubar.md +15 -0
- package/docs/design-system/components/modal.md +25 -4
- package/docs/design-system/components/native-select.md +18 -0
- package/docs/design-system/components/navigation-menu.md +15 -0
- package/docs/design-system/components/onboarding-step-list-item.md +16 -1
- package/docs/design-system/components/page-header.md +17 -1
- package/docs/design-system/components/pagination.md +31 -8
- package/docs/design-system/components/popover.md +19 -0
- package/docs/design-system/components/progress-slider.md +18 -0
- package/docs/design-system/components/progress.md +17 -0
- package/docs/design-system/components/radio-card.md +16 -0
- package/docs/design-system/components/radio-group.md +17 -0
- package/docs/design-system/components/resizable.md +15 -0
- package/docs/design-system/components/scroll-area.md +20 -1
- package/docs/design-system/components/segmented-control.md +71 -0
- package/docs/design-system/components/select.md +44 -7
- package/docs/design-system/components/sheet.md +17 -0
- package/docs/design-system/components/sidebar.md +17 -0
- package/docs/design-system/components/skeleton.md +18 -0
- package/docs/design-system/components/slideout.md +17 -0
- package/docs/design-system/components/slider.md +20 -0
- package/docs/design-system/components/social-button.md +21 -3
- package/docs/design-system/components/spinner.md +16 -0
- package/docs/design-system/components/steps.md +19 -0
- package/docs/design-system/components/table.md +36 -4
- package/docs/design-system/components/tabs.md +17 -0
- package/docs/design-system/components/tag.md +17 -0
- package/docs/design-system/components/textarea.md +16 -0
- package/docs/design-system/components/timeline.md +17 -0
- package/docs/design-system/components/toast.md +17 -0
- package/docs/design-system/components/toggle.md +17 -0
- package/docs/design-system/components/toolbar.md +21 -1
- package/docs/design-system/components/tooltip.md +18 -1
- package/docs/design-system/density-and-elevation.md +61 -0
- package/docs/design-system/direction-and-localization.md +23 -0
- package/docs/design-system/form-recipes.md +73 -0
- package/docs/design-system/gap-promotion-policy.md +21 -0
- package/docs/design-system/gap-promotion-report.md +100 -0
- package/docs/design-system/installation.md +68 -0
- package/docs/design-system/launch-comparison-shadcn.md +98 -0
- package/docs/design-system/registry-and-cli.md +30 -7
- package/docs/design-system/release-policy.md +30 -0
- package/docs/design-system/roadmap.md +14 -0
- package/docs/design-system/support-matrix.md +23 -0
- package/docs/design-system/tabs.md +1 -1
- package/docs/design-system/token-governance.md +23 -1
- package/package.json +30 -15
- package/registry/component-contract.json +40 -0
- package/registry/components/alert-dialog.tsx +2 -2
- package/registry/components/alert.tsx +101 -76
- package/registry/components/autocomplete.tsx +23 -9
- package/registry/components/avatar.tsx +14 -10
- package/registry/components/badge.tsx +18 -18
- package/registry/components/button-group.tsx +324 -5
- package/registry/components/button.tsx +38 -17
- package/registry/components/card.tsx +87 -62
- package/registry/components/carousel.tsx +67 -5
- package/registry/components/chart.tsx +36 -3
- package/registry/components/checkbox.tsx +3 -3
- package/registry/components/collapsible.tsx +1 -1
- package/registry/components/command.tsx +38 -4
- package/registry/components/context-menu.tsx +1 -1
- package/registry/components/date-picker.tsx +312 -135
- package/registry/components/expanded/ActivityFeed.tsx +9 -22
- package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
- package/registry/components/expanded/Banner.tsx +3 -4
- package/registry/components/expanded/Breadcrumbs.tsx +8 -5
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +16 -17
- package/registry/components/expanded/CatalogDivider.tsx +4 -3
- package/registry/components/expanded/EmptyState.tsx +1 -1
- package/registry/components/expanded/FileUpload.tsx +16 -7
- package/registry/components/expanded/OnboardingStepListItem.tsx +2 -6
- package/registry/components/expanded/PageHeader.tsx +47 -56
- package/registry/components/expanded/Slideout.tsx +29 -424
- package/registry/components/expanded/Steps.tsx +151 -13
- package/registry/components/expanded/Table.tsx +161 -82
- package/registry/components/expanded/Timeline.tsx +14 -27
- package/registry/components/expanded/activityFeed.css +9 -9
- package/registry/components/expanded/activityHeatmap.css +66 -0
- package/registry/components/expanded/banner.css +21 -11
- package/registry/components/expanded/breadcrumbs.css +2 -2
- package/registry/components/expanded/divider.css +1 -1
- package/registry/components/expanded/emptyState.css +1 -1
- package/registry/components/expanded/fileUpload.css +7 -7
- package/registry/components/expanded/onboardingStepListItem.css +2 -2
- package/registry/components/expanded/pageHeader.css +14 -11
- package/registry/components/expanded/slideout.css +10 -690
- package/registry/components/expanded/steps.css +78 -15
- package/registry/components/expanded/table.css +60 -69
- package/registry/components/expanded/timeline.css +19 -18
- package/registry/components/field.tsx +61 -8
- package/registry/components/input-content.ts +1 -0
- package/registry/components/input-otp.tsx +13 -3
- package/registry/components/input.tsx +68 -43
- package/registry/components/item.tsx +189 -0
- package/registry/components/kbd.tsx +111 -4
- package/registry/components/menu.tsx +87 -251
- package/registry/components/menubar.tsx +1 -1
- package/registry/components/modal.tsx +143 -103
- package/registry/components/native-select.tsx +2 -2
- package/registry/components/navigation-menu.tsx +1 -1
- package/registry/components/pagination.css +36 -0
- package/registry/components/pagination.tsx +88 -64
- package/registry/components/popover.tsx +162 -664
- package/registry/components/progress-slider.tsx +1 -1
- package/registry/components/radio-card.tsx +116 -87
- package/registry/components/resizable.tsx +36 -2
- package/registry/components/scroll-area.tsx +30 -6
- package/registry/components/segmented-control.tsx +87 -0
- package/registry/components/select-content.tsx +2 -2
- package/registry/components/select.tsx +86 -27
- package/registry/components/sheet.tsx +1 -1
- package/registry/components/sidebar.tsx +12 -6
- package/registry/components/skeleton.css +0 -3
- package/registry/components/social-button.tsx +59 -30
- package/registry/components/spinner.tsx +18 -55
- package/registry/components/table-elements.tsx +128 -0
- package/registry/components/tabs.css +1 -1
- package/registry/components/tabs.tsx +13 -14
- package/registry/components/tag.tsx +3 -3
- package/registry/components/textarea.tsx +3 -3
- package/registry/components/toast.tsx +16 -11
- package/registry/components/toggle.tsx +4 -4
- package/registry/components/toolbar.tsx +94 -22
- package/registry/components/tooltip.tsx +2 -2
- package/registry/examples/alert-demo.tsx +25 -5
- package/registry/examples/attribute-demo.tsx +1 -1
- package/registry/examples/autocomplete-demo.tsx +3 -3
- package/registry/examples/avatar-demo.tsx +14 -11
- package/registry/examples/badge-demo.tsx +1 -1
- package/registry/examples/button-demo.tsx +6 -6
- package/registry/examples/button-group-demo.tsx +23 -2
- package/registry/examples/card-demo.tsx +46 -8
- package/registry/examples/carousel-demo.tsx +20 -2
- package/registry/examples/chart-demo.tsx +1 -1
- package/registry/examples/checkbox-demo.tsx +5 -5
- package/registry/examples/collection-filter-bar-demo.tsx +135 -0
- package/registry/examples/command-demo.tsx +3 -3
- package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
- package/registry/examples/context-menu-demo.tsx +1 -1
- package/registry/examples/control-density-contract-demo.tsx +61 -0
- package/registry/examples/date-picker-demo.tsx +12 -12
- package/registry/examples/divider-demo.tsx +3 -3
- package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -0
- package/registry/examples/expanded/banner-demo.tsx +1 -1
- package/registry/examples/expanded/breadcrumbs-demo.tsx +4 -4
- package/registry/examples/expanded/empty-state-demo.tsx +1 -1
- package/registry/examples/expanded/file-upload-demo.tsx +1 -1
- package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
- package/registry/examples/expanded/slideout-demo.tsx +2 -2
- package/registry/examples/expanded/steps-demo.tsx +1 -1
- package/registry/examples/expanded/tabs-demo.tsx +1 -1
- package/registry/examples/expanded/timeline-demo.tsx +1 -1
- package/registry/examples/hover-card-demo.tsx +2 -2
- package/registry/examples/input-demo.tsx +6 -6
- package/registry/examples/input-otp-demo.tsx +4 -4
- package/registry/examples/item-demo.tsx +46 -0
- package/registry/examples/kbd-demo.tsx +1 -1
- package/registry/examples/label-demo.tsx +1 -1
- package/registry/examples/menu-demo.tsx +1 -1
- package/registry/examples/modal-demo.tsx +67 -9
- package/registry/examples/navigation-menu-demo.tsx +1 -1
- package/registry/examples/pagination-demo.tsx +10 -7
- package/registry/examples/popover-demo.tsx +19 -19
- package/registry/examples/progress-demo.tsx +1 -1
- package/registry/examples/progress-slider-demo.tsx +1 -1
- package/registry/examples/radio-card-demo.tsx +4 -4
- package/registry/examples/radio-group-demo.tsx +2 -2
- package/registry/examples/resizable-demo.tsx +1 -1
- package/registry/examples/results-toolbar-demo.tsx +44 -0
- package/registry/examples/scroll-area-demo.tsx +3 -3
- package/registry/examples/segmented-control-demo.tsx +39 -0
- package/registry/examples/select-demo.tsx +1 -1
- package/registry/examples/sidebar-demo.tsx +5 -3
- package/registry/examples/slider-demo.tsx +3 -3
- package/registry/examples/social-button-demo.tsx +18 -8
- package/registry/examples/spinner-demo.tsx +13 -13
- package/registry/examples/tabs-demo.tsx +1 -1
- package/registry/examples/tag-demo.tsx +1 -1
- package/registry/examples/textarea-demo.tsx +4 -4
- package/registry/examples/toast-demo.tsx +1 -1
- package/registry/examples/toggle-demo.tsx +6 -6
- package/registry/examples/tooltip-demo.tsx +4 -4
- package/registry/guidance/USAGE.md +27 -0
- package/registry/hooks/use-direction.ts +193 -13
- package/registry/index.json +5668 -375
- package/registry/styles/globals.css +2404 -3999
- package/registry.json +156 -149
- package/schema/config.schema.json +11 -8
- package/schema/registry.schema.json +150 -45
- package/schema/tokens.schema.json +134 -34
- package/src/cli/command-line.js +194 -0
- package/src/cli/config-editors.js +9 -0
- package/src/cli/doctor.js +399 -0
- package/src/cli/file-transaction.js +295 -0
- package/src/cli/global-css.js +153 -0
- package/src/cli/index.js +567 -1613
- package/src/cli/install-plan.js +592 -0
- package/src/cli/install-receipt.js +171 -0
- package/src/cli/jsonc.js +203 -0
- package/src/cli/package-install-state.js +254 -0
- package/src/cli/package-manager.js +238 -0
- package/src/cli/package-plan.js +185 -0
- package/src/cli/package-version.js +97 -0
- package/src/cli/registry-query.js +207 -0
- package/src/cli/reporting.js +55 -0
- package/src/cli/schema-validation.js +19 -0
- package/src/cli/source-scanner.js +686 -0
- package/src/cli/vite-config.js +478 -0
- package/tokens/banhaten.tokens.json +4444 -331
- package/docs/design-system/components/command-bar.md +0 -57
- package/registry/components/expanded/BannerBoard.tsx +0 -225
- package/registry/components/expanded/CatalogTag.tsx +0 -85
- package/registry/components/expanded/CommandBar.tsx +0 -386
- package/registry/components/expanded/IconExplorer.tsx +0 -612
- package/registry/components/expanded/TimelineShowcase.tsx +0 -158
- package/registry/components/expanded/commandBar.css +0 -470
- package/registry/components/expanded/iconExplorer.css +0 -764
- package/registry/components/expanded/iconPacks.ts +0 -866
- package/registry/components/expanded/tag.css +0 -148
- package/registry/examples/expanded/icons-demo.tsx +0 -5
- package/tokens/Color mode.zip +0 -0
- package/tokens/Numbers.zip +0 -0
- package/tokens/Radius.zip +0 -0
- package/tokens/Theme.zip +0 -0
|
@@ -6,19 +6,33 @@ import { cn } from "@/lib/utils"
|
|
|
6
6
|
|
|
7
7
|
type ButtonGroupSize = "md" | "sm" | "xs"
|
|
8
8
|
type ButtonGroupMode = "default" | "iconOnly"
|
|
9
|
+
type ButtonGroupItemWidth = "fixed" | "content"
|
|
10
|
+
type ControlDensity = "compact" | "default" | "comfortable"
|
|
11
|
+
type ToggleGroupType = "single" | "multiple"
|
|
12
|
+
type ToggleGroupValue = string | string[]
|
|
9
13
|
|
|
10
14
|
type ButtonGroupContextValue = {
|
|
15
|
+
itemWidth: ButtonGroupItemWidth
|
|
11
16
|
size: ButtonGroupSize
|
|
12
17
|
mode: ButtonGroupMode
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
const buttonGroupContext = React.createContext<ButtonGroupContextValue>({
|
|
21
|
+
itemWidth: "fixed",
|
|
16
22
|
size: "md",
|
|
17
23
|
mode: "default",
|
|
18
24
|
})
|
|
19
25
|
|
|
26
|
+
type ToggleGroupContextValue = {
|
|
27
|
+
disabled: boolean
|
|
28
|
+
isPressed: (value: string) => boolean
|
|
29
|
+
toggle: (value: string) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const toggleGroupContext = React.createContext<ToggleGroupContextValue | null>(null)
|
|
33
|
+
|
|
20
34
|
const buttonGroupVariants = cva(
|
|
21
|
-
"inline-flex w-fit items-center overflow-hidden rounded-[var(--bh-button-group-radius)] bg-[var(--bh-button-group-bg)] text-[var(--bh-button-group-content)] shadow-[var(--shadow-button-group)] [--shadow-button-group:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-button-group-border),var(--shadow-component-default)]",
|
|
35
|
+
"inline-flex w-fit max-w-full items-center overflow-x-auto overflow-y-hidden rounded-[var(--bh-button-group-radius)] bg-[var(--bh-button-group-bg)] text-[var(--bh-button-group-content)] shadow-[var(--shadow-button-group)] outline-none focus-visible:shadow-[var(--shadow-button-group-focus)] [--shadow-button-group:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-button-group-border),var(--shadow-component-default)]",
|
|
22
36
|
{
|
|
23
37
|
variants: {
|
|
24
38
|
size: {
|
|
@@ -46,26 +60,51 @@ const buttonGroupItemVariants = cva(
|
|
|
46
60
|
default: "",
|
|
47
61
|
iconOnly: "",
|
|
48
62
|
},
|
|
63
|
+
itemWidth: {
|
|
64
|
+
fixed: "",
|
|
65
|
+
content: "",
|
|
66
|
+
},
|
|
49
67
|
},
|
|
50
68
|
compoundVariants: [
|
|
51
69
|
{
|
|
52
70
|
size: "md",
|
|
53
71
|
mode: "default",
|
|
72
|
+
itemWidth: "fixed",
|
|
54
73
|
class:
|
|
55
74
|
"w-[var(--bh-button-group-md-item-width)] px-[var(--bh-button-group-md-padding-x)] rtl:w-[var(--bh-button-group-md-item-rtl-width)]",
|
|
56
75
|
},
|
|
57
76
|
{
|
|
58
77
|
size: "sm",
|
|
59
78
|
mode: "default",
|
|
79
|
+
itemWidth: "fixed",
|
|
60
80
|
class:
|
|
61
81
|
"w-[var(--bh-button-group-sm-item-width)] px-[var(--bh-button-group-sm-padding-x)] rtl:w-[var(--bh-button-group-sm-item-rtl-width)]",
|
|
62
82
|
},
|
|
63
83
|
{
|
|
64
84
|
size: "xs",
|
|
65
85
|
mode: "default",
|
|
86
|
+
itemWidth: "fixed",
|
|
66
87
|
class:
|
|
67
88
|
"w-[var(--bh-button-group-xs-item-width)] px-[var(--bh-button-group-xs-padding-x)] rtl:w-[var(--bh-button-group-xs-item-rtl-width)]",
|
|
68
89
|
},
|
|
90
|
+
{
|
|
91
|
+
size: "md",
|
|
92
|
+
mode: "default",
|
|
93
|
+
itemWidth: "content",
|
|
94
|
+
class: "w-auto px-[var(--bh-button-group-md-padding-x)]",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
size: "sm",
|
|
98
|
+
mode: "default",
|
|
99
|
+
itemWidth: "content",
|
|
100
|
+
class: "w-auto px-[var(--bh-button-group-sm-padding-x)]",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
size: "xs",
|
|
104
|
+
mode: "default",
|
|
105
|
+
itemWidth: "content",
|
|
106
|
+
class: "w-auto px-[var(--bh-button-group-xs-padding-x)]",
|
|
107
|
+
},
|
|
69
108
|
{
|
|
70
109
|
size: "md",
|
|
71
110
|
mode: "iconOnly",
|
|
@@ -88,39 +127,103 @@ const buttonGroupItemVariants = cva(
|
|
|
88
127
|
defaultVariants: {
|
|
89
128
|
size: "md",
|
|
90
129
|
mode: "default",
|
|
130
|
+
itemWidth: "fixed",
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const buttonGroupLabelVariants = cva(
|
|
136
|
+
"relative inline-flex shrink-0 items-center justify-center whitespace-nowrap bg-[var(--bh-button-group-item-bg)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] text-[var(--bh-button-group-content)] before:absolute before:inset-y-0 before:start-0 before:w-[var(--bh-border-width-default)] before:bg-[var(--bh-button-group-divider)] before:content-[''] first:before:hidden",
|
|
137
|
+
{
|
|
138
|
+
variants: {
|
|
139
|
+
size: {
|
|
140
|
+
md: "h-[var(--bh-button-group-md-height)]",
|
|
141
|
+
sm: "h-[var(--bh-button-group-sm-height)]",
|
|
142
|
+
xs: "h-[var(--bh-button-group-xs-height)]",
|
|
143
|
+
},
|
|
144
|
+
itemWidth: {
|
|
145
|
+
fixed: "",
|
|
146
|
+
content: "",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
compoundVariants: [
|
|
150
|
+
{
|
|
151
|
+
size: "md",
|
|
152
|
+
itemWidth: "fixed",
|
|
153
|
+
class: "w-[var(--bh-button-group-md-item-width)] px-[var(--bh-button-group-md-padding-x)] rtl:w-[var(--bh-button-group-md-item-rtl-width)]",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
size: "sm",
|
|
157
|
+
itemWidth: "fixed",
|
|
158
|
+
class: "w-[var(--bh-button-group-sm-item-width)] px-[var(--bh-button-group-sm-padding-x)] rtl:w-[var(--bh-button-group-sm-item-rtl-width)]",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
size: "xs",
|
|
162
|
+
itemWidth: "fixed",
|
|
163
|
+
class: "w-[var(--bh-button-group-xs-item-width)] px-[var(--bh-button-group-xs-padding-x)] rtl:w-[var(--bh-button-group-xs-item-rtl-width)]",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
size: "md",
|
|
167
|
+
itemWidth: "content",
|
|
168
|
+
class: "w-auto px-[var(--bh-button-group-md-padding-x)]",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
size: "sm",
|
|
172
|
+
itemWidth: "content",
|
|
173
|
+
class: "w-auto px-[var(--bh-button-group-sm-padding-x)]",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
size: "xs",
|
|
177
|
+
itemWidth: "content",
|
|
178
|
+
class: "w-auto px-[var(--bh-button-group-xs-padding-x)]",
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
defaultVariants: {
|
|
182
|
+
itemWidth: "content",
|
|
183
|
+
size: "sm",
|
|
91
184
|
},
|
|
92
185
|
}
|
|
93
186
|
)
|
|
94
187
|
|
|
95
188
|
type ButtonGroupProps = React.ComponentProps<"div"> &
|
|
96
189
|
VariantProps<typeof buttonGroupVariants> & {
|
|
190
|
+
density?: ControlDensity
|
|
191
|
+
itemWidth?: ButtonGroupItemWidth
|
|
97
192
|
mode?: ButtonGroupMode
|
|
98
193
|
}
|
|
99
194
|
|
|
100
195
|
const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(function ButtonGroup({
|
|
101
196
|
className,
|
|
102
|
-
|
|
197
|
+
density,
|
|
198
|
+
itemWidth = "fixed",
|
|
199
|
+
size,
|
|
103
200
|
mode = "default",
|
|
104
201
|
children,
|
|
105
202
|
role = "group",
|
|
203
|
+
tabIndex = 0,
|
|
106
204
|
...props
|
|
107
205
|
}, ref) {
|
|
206
|
+
const resolvedSize = resolveButtonGroupSize(density, size)
|
|
108
207
|
const contextValue = React.useMemo(
|
|
109
208
|
() => ({
|
|
110
|
-
|
|
209
|
+
itemWidth,
|
|
210
|
+
size: resolvedSize,
|
|
111
211
|
mode: mode || "default",
|
|
112
212
|
}),
|
|
113
|
-
[
|
|
213
|
+
[itemWidth, mode, resolvedSize]
|
|
114
214
|
)
|
|
115
215
|
|
|
116
216
|
return (
|
|
117
217
|
<buttonGroupContext.Provider value={contextValue}>
|
|
118
218
|
<div
|
|
119
219
|
data-mode={contextValue.mode}
|
|
220
|
+
data-density={density}
|
|
221
|
+
data-item-width={contextValue.itemWidth}
|
|
120
222
|
data-size={contextValue.size}
|
|
121
223
|
data-slot="button-group"
|
|
122
224
|
ref={ref}
|
|
123
225
|
role={role}
|
|
226
|
+
tabIndex={tabIndex}
|
|
124
227
|
className={cn(buttonGroupVariants({ size: contextValue.size }), className)}
|
|
125
228
|
{...props}
|
|
126
229
|
>
|
|
@@ -138,6 +241,7 @@ type ButtonGroupItemProps = React.ComponentProps<"button"> &
|
|
|
138
241
|
const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps>(function ButtonGroupItem({
|
|
139
242
|
className,
|
|
140
243
|
size,
|
|
244
|
+
itemWidth,
|
|
141
245
|
mode,
|
|
142
246
|
asChild = false,
|
|
143
247
|
children,
|
|
@@ -151,6 +255,7 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
|
|
|
151
255
|
const context = React.useContext(buttonGroupContext)
|
|
152
256
|
const selectedSize = (size || context.size) as ButtonGroupSize
|
|
153
257
|
const selectedMode = mode || context.mode
|
|
258
|
+
const selectedItemWidth = itemWidth || context.itemWidth
|
|
154
259
|
const Comp = asChild ? Slot : "button"
|
|
155
260
|
const accessibleLabel =
|
|
156
261
|
selectedMode === "iconOnly" ? ariaLabel || getTextAlternative(children) : ariaLabel
|
|
@@ -170,6 +275,7 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
|
|
|
170
275
|
aria-label={accessibleLabel}
|
|
171
276
|
aria-pressed={ariaPressed}
|
|
172
277
|
data-mode={selectedMode}
|
|
278
|
+
data-item-width={selectedItemWidth}
|
|
173
279
|
data-size={selectedSize}
|
|
174
280
|
data-slot="button-group-item"
|
|
175
281
|
ref={ref}
|
|
@@ -177,6 +283,7 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
|
|
|
177
283
|
buttonGroupItemVariants({
|
|
178
284
|
size: selectedSize,
|
|
179
285
|
mode: selectedMode,
|
|
286
|
+
itemWidth: selectedItemWidth,
|
|
180
287
|
className,
|
|
181
288
|
})
|
|
182
289
|
)}
|
|
@@ -192,6 +299,39 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
|
|
|
192
299
|
)
|
|
193
300
|
})
|
|
194
301
|
|
|
302
|
+
type ButtonGroupLabelProps = React.ComponentProps<"span"> &
|
|
303
|
+
VariantProps<typeof buttonGroupLabelVariants>
|
|
304
|
+
|
|
305
|
+
const ButtonGroupLabel = React.forwardRef<HTMLSpanElement, ButtonGroupLabelProps>(
|
|
306
|
+
function ButtonGroupLabel(
|
|
307
|
+
{ "aria-hidden": ariaHidden = true, className, itemWidth, size, ...props },
|
|
308
|
+
ref
|
|
309
|
+
) {
|
|
310
|
+
const context = React.useContext(buttonGroupContext)
|
|
311
|
+
const selectedSize = (size || context.size) as ButtonGroupSize
|
|
312
|
+
const selectedItemWidth = itemWidth || context.itemWidth
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<span
|
|
316
|
+
aria-hidden={ariaHidden}
|
|
317
|
+
className={cn(
|
|
318
|
+
buttonGroupLabelVariants({
|
|
319
|
+
itemWidth: selectedItemWidth,
|
|
320
|
+
size: selectedSize,
|
|
321
|
+
}),
|
|
322
|
+
className
|
|
323
|
+
)}
|
|
324
|
+
data-item-width={selectedItemWidth}
|
|
325
|
+
data-size={selectedSize}
|
|
326
|
+
data-slot="button-group-label"
|
|
327
|
+
dir="auto"
|
|
328
|
+
ref={ref}
|
|
329
|
+
{...props}
|
|
330
|
+
/>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
|
|
195
335
|
function ButtonGroupItemChildren({
|
|
196
336
|
children,
|
|
197
337
|
mode,
|
|
@@ -237,10 +377,189 @@ function getTextAlternative(children: React.ReactNode) {
|
|
|
237
377
|
return text.trim() || undefined
|
|
238
378
|
}
|
|
239
379
|
|
|
240
|
-
|
|
380
|
+
function resolveButtonGroupSize(
|
|
381
|
+
density: ControlDensity | undefined,
|
|
382
|
+
size: ButtonGroupSize | null | undefined
|
|
383
|
+
): ButtonGroupSize {
|
|
384
|
+
if (density === "compact") return "xs"
|
|
385
|
+
if (density === "default") return "sm"
|
|
386
|
+
if (density === "comfortable") return "md"
|
|
387
|
+
return size ?? "md"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
type ToggleGroupProps = Omit<ButtonGroupProps, "onChange"> & {
|
|
391
|
+
defaultValue?: ToggleGroupValue
|
|
392
|
+
disabled?: boolean
|
|
393
|
+
onValueChange?: (value: ToggleGroupValue) => void
|
|
394
|
+
type?: ToggleGroupType
|
|
395
|
+
value?: ToggleGroupValue
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
|
|
399
|
+
function ToggleGroup(
|
|
400
|
+
{
|
|
401
|
+
children,
|
|
402
|
+
defaultValue,
|
|
403
|
+
disabled = false,
|
|
404
|
+
onKeyDown,
|
|
405
|
+
onValueChange,
|
|
406
|
+
type = "single",
|
|
407
|
+
value,
|
|
408
|
+
...props
|
|
409
|
+
},
|
|
410
|
+
ref
|
|
411
|
+
) {
|
|
412
|
+
const controlled = value !== undefined
|
|
413
|
+
const [internalValue, setInternalValue] = React.useState<ToggleGroupValue>(
|
|
414
|
+
defaultValue ?? (type === "multiple" ? [] : "")
|
|
415
|
+
)
|
|
416
|
+
const selectedValue = normalizeToggleGroupValue(
|
|
417
|
+
controlled ? value : internalValue,
|
|
418
|
+
type
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
const updateValue = React.useCallback(
|
|
422
|
+
(nextValue: ToggleGroupValue) => {
|
|
423
|
+
if (!controlled) setInternalValue(nextValue)
|
|
424
|
+
onValueChange?.(nextValue)
|
|
425
|
+
},
|
|
426
|
+
[controlled, onValueChange]
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
const context = React.useMemo<ToggleGroupContextValue>(
|
|
430
|
+
() => ({
|
|
431
|
+
disabled,
|
|
432
|
+
isPressed: (itemValue) =>
|
|
433
|
+
Array.isArray(selectedValue)
|
|
434
|
+
? selectedValue.includes(itemValue)
|
|
435
|
+
: selectedValue === itemValue,
|
|
436
|
+
toggle: (itemValue) => {
|
|
437
|
+
if (disabled) return
|
|
438
|
+
if (type === "multiple") {
|
|
439
|
+
const values = Array.isArray(selectedValue) ? selectedValue : []
|
|
440
|
+
updateValue(
|
|
441
|
+
values.includes(itemValue)
|
|
442
|
+
? values.filter((entry) => entry !== itemValue)
|
|
443
|
+
: [...values, itemValue]
|
|
444
|
+
)
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
updateValue(selectedValue === itemValue ? "" : itemValue)
|
|
448
|
+
},
|
|
449
|
+
}),
|
|
450
|
+
[disabled, selectedValue, type, updateValue]
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<toggleGroupContext.Provider value={context}>
|
|
455
|
+
<ButtonGroup
|
|
456
|
+
aria-disabled={disabled || undefined}
|
|
457
|
+
data-selection-type={type}
|
|
458
|
+
data-slot="toggle-group"
|
|
459
|
+
onKeyDown={(event) => {
|
|
460
|
+
onKeyDown?.(event)
|
|
461
|
+
if (!event.defaultPrevented) moveToggleGroupFocus(event)
|
|
462
|
+
}}
|
|
463
|
+
ref={ref}
|
|
464
|
+
{...props}
|
|
465
|
+
>
|
|
466
|
+
{children}
|
|
467
|
+
</ButtonGroup>
|
|
468
|
+
</toggleGroupContext.Provider>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
type ToggleGroupItemProps = Omit<ButtonGroupItemProps, "aria-pressed"> & {
|
|
474
|
+
value: string
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const ToggleGroupItem = React.forwardRef<
|
|
478
|
+
HTMLButtonElement,
|
|
479
|
+
ToggleGroupItemProps
|
|
480
|
+
>(function ToggleGroupItem(
|
|
481
|
+
{ disabled, onClick, value, ...props },
|
|
482
|
+
ref
|
|
483
|
+
) {
|
|
484
|
+
const context = React.useContext(toggleGroupContext)
|
|
485
|
+
if (!context) {
|
|
486
|
+
throw new Error("ToggleGroupItem must be used within ToggleGroup")
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const pressed = context.isPressed(value)
|
|
490
|
+
const isDisabled = disabled || context.disabled
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<ButtonGroupItem
|
|
494
|
+
aria-pressed={pressed}
|
|
495
|
+
data-slot="toggle-group-item"
|
|
496
|
+
data-value={value}
|
|
497
|
+
disabled={isDisabled}
|
|
498
|
+
onClick={(event) => {
|
|
499
|
+
onClick?.(event)
|
|
500
|
+
if (!event.defaultPrevented) context.toggle(value)
|
|
501
|
+
}}
|
|
502
|
+
ref={ref}
|
|
503
|
+
{...props}
|
|
504
|
+
/>
|
|
505
|
+
)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
function normalizeToggleGroupValue(
|
|
509
|
+
value: ToggleGroupValue,
|
|
510
|
+
type: ToggleGroupType
|
|
511
|
+
): ToggleGroupValue {
|
|
512
|
+
if (type === "multiple") return Array.isArray(value) ? value : value ? [value] : []
|
|
513
|
+
return Array.isArray(value) ? value[0] ?? "" : value
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function moveToggleGroupFocus(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
517
|
+
const horizontalKeys = ["ArrowLeft", "ArrowRight"]
|
|
518
|
+
const verticalKeys = ["ArrowUp", "ArrowDown"]
|
|
519
|
+
if (![...horizontalKeys, ...verticalKeys, "Home", "End"].includes(event.key)) {
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>(
|
|
524
|
+
'[data-slot="toggle-group-item"]:not(:disabled)'
|
|
525
|
+
)]
|
|
526
|
+
if (!items.length) return
|
|
527
|
+
|
|
528
|
+
const currentIndex = Math.max(0, items.indexOf(document.activeElement as HTMLButtonElement))
|
|
529
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl"
|
|
530
|
+
let nextIndex = currentIndex
|
|
531
|
+
|
|
532
|
+
if (event.key === "Home") nextIndex = 0
|
|
533
|
+
else if (event.key === "End") nextIndex = items.length - 1
|
|
534
|
+
else if (event.key === "ArrowRight") nextIndex += rtl ? -1 : 1
|
|
535
|
+
else if (event.key === "ArrowLeft") nextIndex += rtl ? 1 : -1
|
|
536
|
+
else if (event.key === "ArrowDown") nextIndex += 1
|
|
537
|
+
else if (event.key === "ArrowUp") nextIndex -= 1
|
|
538
|
+
|
|
539
|
+
event.preventDefault()
|
|
540
|
+
items[(nextIndex + items.length) % items.length]?.focus()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export {
|
|
544
|
+
ButtonGroup,
|
|
545
|
+
ButtonGroupItem,
|
|
546
|
+
ButtonGroupLabel,
|
|
547
|
+
ToggleGroup,
|
|
548
|
+
ToggleGroupItem,
|
|
549
|
+
buttonGroupVariants,
|
|
550
|
+
buttonGroupItemVariants,
|
|
551
|
+
buttonGroupLabelVariants,
|
|
552
|
+
}
|
|
241
553
|
export type {
|
|
554
|
+
ButtonGroupLabelProps,
|
|
555
|
+
ButtonGroupItemWidth,
|
|
242
556
|
ButtonGroupItemProps,
|
|
243
557
|
ButtonGroupMode,
|
|
244
558
|
ButtonGroupProps,
|
|
245
559
|
ButtonGroupSize,
|
|
560
|
+
ControlDensity,
|
|
561
|
+
ToggleGroupItemProps,
|
|
562
|
+
ToggleGroupProps,
|
|
563
|
+
ToggleGroupType,
|
|
564
|
+
ToggleGroupValue,
|
|
246
565
|
}
|
|
@@ -4,38 +4,40 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "@/lib/utils"
|
|
6
6
|
|
|
7
|
+
type ControlDensity = "compact" | "default" | "comfortable"
|
|
8
|
+
|
|
7
9
|
const buttonVariants = cva(
|
|
8
|
-
"group/button inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-control-default)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:shadow-none aria-invalid:border aria-invalid:border-
|
|
10
|
+
"group/button inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-control-default)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:shadow-none aria-invalid:border-[length:var(--bh-border-width-default)] aria-invalid:border-[var(--bh-border-danger-strong)] aria-invalid:shadow-[var(--shadow-button-danger-focus)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[var(--bh-button-icon-size)] [&_svg[data-icon='inline-start']]:ms-[var(--bh-button-icon-offset)] [&_svg[data-icon='inline-end']]:me-[var(--bh-button-icon-offset)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
|
|
9
11
|
{
|
|
10
12
|
variants: {
|
|
11
13
|
variant: {
|
|
12
14
|
default:
|
|
13
|
-
"bg-
|
|
15
|
+
"bg-[var(--bh-interactive-brand-default)] text-[var(--bh-content-on-brand)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-brand-hover)] active:bg-[var(--bh-interactive-brand-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
14
16
|
danger:
|
|
15
|
-
"bg-
|
|
17
|
+
"bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
16
18
|
destructive:
|
|
17
|
-
"bg-
|
|
19
|
+
"bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
18
20
|
success:
|
|
19
|
-
"bg-success text-success
|
|
21
|
+
"bg-[var(--bh-interactive-success-default)] text-[var(--bh-content-on-success)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-success-hover)] active:bg-[var(--bh-interactive-success-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
20
22
|
warning:
|
|
21
|
-
"bg-warning text-warning
|
|
23
|
+
"bg-[var(--bh-interactive-warning-default)] text-[var(--bh-content-on-warning)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-warning-hover)] active:bg-[var(--bh-interactive-warning-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
22
24
|
secondary:
|
|
23
|
-
"bg-secondary text-
|
|
25
|
+
"bg-[var(--bh-interactive-secondary-default)] text-[var(--bh-content-default)] shadow-[var(--shadow-button-secondary)] [--bh-button-secondary-border:var(--bh-border-default)] hover:bg-[var(--bh-interactive-secondary-hover)] active:bg-[var(--bh-interactive-secondary-pressed)] focus-visible:[--bh-button-secondary-focus-border:var(--bh-border-brand-strong)] focus-visible:shadow-[var(--shadow-button-secondary-focus)] disabled:border-[length:var(--bh-border-width-default)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-secondary-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
24
26
|
soft:
|
|
25
|
-
"border border-transparent bg-[var(--soft)] text-
|
|
27
|
+
"border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] shadow-[var(--shadow-button-soft)] hover:bg-[var(--bh-interactive-soft-hover)] active:bg-[var(--bh-interactive-soft-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
26
28
|
outline:
|
|
27
|
-
"border border-[var(--bh-border-default)] bg-[var(--
|
|
29
|
+
"border-[length:var(--bh-border-width-default)] border-[var(--bh-border-default)] bg-[var(--bh-interactive-outlined-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-outlined-hover)] active:bg-[var(--bh-interactive-outlined-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-outlined-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
28
30
|
ghost:
|
|
29
|
-
"border border-transparent bg-[var(--ghost)] text-
|
|
31
|
+
"border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
30
32
|
"ghost-primary":
|
|
31
|
-
"border border-transparent bg-[var(--ghost)] text-[var(--bh-content-brand-default)] hover:bg-[var(--ghost-hover)] active:bg-[var(--ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
33
|
+
"border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-brand-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
32
34
|
white:
|
|
33
|
-
"border border-transparent bg-[var(--bh-bg-always-white)] text-[var(--bh-content-on-light)] focus-visible:border-[var(--bh-border-focus)] disabled:opacity-[var(--bh-opacity-30)]",
|
|
35
|
+
"border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-bg-always-white)] text-[var(--bh-content-on-light)] focus-visible:border-[var(--bh-border-focus)] disabled:opacity-[var(--bh-opacity-30)]",
|
|
34
36
|
"soft-danger":
|
|
35
|
-
"border border-transparent bg-[var(--
|
|
37
|
+
"border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
36
38
|
"soft-destructive":
|
|
37
|
-
"border border-transparent bg-[var(--
|
|
38
|
-
link: "bg-[var(--ghost)] text-[var(--bh-content-brand-default)] hover:bg-[var(--ghost-hover)] active:bg-[var(--ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:text-[var(--bh-content-disabled)]",
|
|
39
|
+
"border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
|
|
40
|
+
link: "bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-brand-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:text-[var(--bh-content-disabled)]",
|
|
39
41
|
},
|
|
40
42
|
size: {
|
|
41
43
|
xs: "h-[var(--bh-button-xs-height)] gap-[var(--bh-button-gap-xs)] px-[var(--bh-button-xs-padding-x)] py-[var(--bh-button-sm-padding-y)] has-[>svg]:px-[var(--bh-button-xs-padding-x)]",
|
|
@@ -60,23 +62,30 @@ const buttonVariants = cva(
|
|
|
60
62
|
type ButtonProps = React.ComponentProps<"button"> &
|
|
61
63
|
VariantProps<typeof buttonVariants> & {
|
|
62
64
|
asChild?: boolean
|
|
65
|
+
density?: ControlDensity
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>["size"]>
|
|
69
|
+
|
|
65
70
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button({
|
|
66
71
|
className,
|
|
67
72
|
variant,
|
|
68
73
|
size,
|
|
74
|
+
density,
|
|
69
75
|
asChild = false,
|
|
70
76
|
children,
|
|
71
77
|
...props
|
|
72
78
|
}, ref) {
|
|
73
79
|
const Comp = asChild ? Slot : "button"
|
|
80
|
+
const resolvedSize = resolveButtonSize(density, size)
|
|
74
81
|
|
|
75
82
|
return (
|
|
76
83
|
<Comp
|
|
84
|
+
data-density={density}
|
|
85
|
+
data-size={resolvedSize}
|
|
77
86
|
data-slot="button"
|
|
78
87
|
ref={ref}
|
|
79
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
88
|
+
className={cn(buttonVariants({ variant, size: resolvedSize, className }))}
|
|
80
89
|
{...props}
|
|
81
90
|
>
|
|
82
91
|
{asChild ? children : <ButtonChildren>{children}</ButtonChildren>}
|
|
@@ -98,5 +107,17 @@ function ButtonChildren({ children }: { children: React.ReactNode }) {
|
|
|
98
107
|
})
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
function resolveButtonSize(
|
|
111
|
+
density: ControlDensity | undefined,
|
|
112
|
+
size: ButtonSize | null | undefined
|
|
113
|
+
): ButtonSize | null | undefined {
|
|
114
|
+
if (!density) return size
|
|
115
|
+
|
|
116
|
+
const iconOnly = size?.startsWith("icon") ?? false
|
|
117
|
+
if (density === "compact") return iconOnly ? "icon-xs" : "xs"
|
|
118
|
+
if (density === "default") return iconOnly ? "icon-sm" : "sm"
|
|
119
|
+
return iconOnly ? "icon" : "default"
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
export { Button, buttonVariants }
|
|
102
|
-
export type { ButtonProps }
|
|
123
|
+
export type { ButtonProps, ControlDensity }
|