banhaten 0.3.0 → 0.4.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +4 -4
  3. package/docs/design-system/component-implementation-ownership-audit.md +2 -1
  4. package/docs/design-system/component-quality-audit-2026-08-20.md +4 -2
  5. package/docs/design-system/components/alert-dialog.md +1 -1
  6. package/docs/design-system/components/alert.md +2 -2
  7. package/docs/design-system/components/attribute.md +1 -1
  8. package/docs/design-system/components/avatar.md +2 -2
  9. package/docs/design-system/components/banner.md +2 -2
  10. package/docs/design-system/components/breadcrumbs.md +2 -2
  11. package/docs/design-system/components/button-group.md +2 -2
  12. package/docs/design-system/components/button.md +2 -2
  13. package/docs/design-system/components/date-picker.md +3 -1
  14. package/docs/design-system/components/file-upload.md +1 -1
  15. package/docs/design-system/components/input.md +8 -6
  16. package/docs/design-system/components/item.md +3 -1
  17. package/docs/design-system/components/kbd.md +11 -8
  18. package/docs/design-system/components/menu.md +2 -2
  19. package/docs/design-system/components/modal.md +1 -1
  20. package/docs/design-system/components/onboarding-step-list-item.md +2 -2
  21. package/docs/design-system/components/pagination.md +2 -2
  22. package/docs/design-system/components/radio-card.md +1 -1
  23. package/docs/design-system/components/select.md +1 -1
  24. package/docs/design-system/components/sidebar.md +59 -19
  25. package/docs/design-system/components/table.md +1 -1
  26. package/docs/design-system/components/tag.md +3 -3
  27. package/docs/design-system/components/toolbar.md +1 -1
  28. package/docs/design-system/density-and-elevation.md +16 -12
  29. package/docs/design-system/icons.md +6 -1
  30. package/docs/design-system/installation.md +1 -1
  31. package/docs/design-system/token-governance.md +7 -8
  32. package/package.json +1 -1
  33. package/registry/components/alert-dialog.tsx +1 -1
  34. package/registry/components/alert.tsx +3 -3
  35. package/registry/components/attribute.tsx +8 -2
  36. package/registry/components/avatar.tsx +21 -17
  37. package/registry/components/button-group.tsx +1 -1
  38. package/registry/components/button.tsx +1 -1
  39. package/registry/components/date-picker.tsx +16 -4
  40. package/registry/components/expanded/OnboardingStepListItem.tsx +6 -5
  41. package/registry/components/expanded/banner.css +10 -5
  42. package/registry/components/expanded/breadcrumbs.css +4 -4
  43. package/registry/components/expanded/fileUpload.css +3 -3
  44. package/registry/components/expanded/onboardingStepListItem.css +14 -5
  45. package/registry/components/expanded/table.css +5 -2
  46. package/registry/components/input.tsx +12 -5
  47. package/registry/components/item.tsx +1 -1
  48. package/registry/components/kbd.tsx +109 -15
  49. package/registry/components/menu.tsx +1 -1
  50. package/registry/components/modal.tsx +1 -1
  51. package/registry/components/pagination.tsx +12 -7
  52. package/registry/components/radio-card.tsx +1 -2
  53. package/registry/components/select.tsx +1 -1
  54. package/registry/components/sidebar.tsx +929 -57
  55. package/registry/components/tag.tsx +1 -1
  56. package/registry/components/toolbar.tsx +1 -1
  57. package/registry/examples/attribute-demo.tsx +1 -1
  58. package/registry/examples/command-palette-demo.tsx +0 -1
  59. package/registry/examples/kbd-demo.tsx +2 -1
  60. package/registry/examples/sidebar-demo.tsx +60 -16
  61. package/registry/guidance/USAGE.md +1 -1
  62. package/registry/index.json +233 -31
  63. package/registry/styles/globals.css +152 -118
  64. package/registry.json +4 -3
  65. package/tokens/banhaten.tokens.json +169 -49
@@ -13,7 +13,8 @@
13
13
  --onboarding-step-width: calc(var(--bh-space-17xl-288) - var(--bh-space-md-8));
14
14
  --onboarding-step-height: var(--bh-space-7xl-40);
15
15
  --onboarding-step-line-wrap-size: var(--bh-space-5xl-24);
16
- --onboarding-step-check-size: var(--bh-space-4xl-20);
16
+ --onboarding-step-check-size: var(--bh-icon-size-default);
17
+ --onboarding-step-check-slot-size: var(--bh-icon-slot-size-default);
17
18
  --onboarding-step-line-width: var(--bh-space-xxs-2);
18
19
  --onboarding-step-line-offset: calc(-1 * var(--bh-space-md-8));
19
20
  --onboarding-step-line-segment-height: calc(var(--bh-space-2xl-14) - var(--bh-space-xxxs-1));
@@ -82,14 +83,22 @@
82
83
  text-align: right;
83
84
  }
84
85
 
85
- .ds-onboarding-step__check {
86
+ .ds-onboarding-step__check-slot {
86
87
  position: relative;
87
88
  z-index: 1;
88
- display: block;
89
+ display: inline-flex;
89
90
  flex: 0 0 auto;
91
+ align-items: center;
92
+ justify-content: center;
93
+ width: var(--onboarding-step-check-slot-size);
94
+ height: var(--onboarding-step-check-slot-size);
95
+ color: var(--onboarding-step-content-success);
96
+ }
97
+
98
+ .ds-onboarding-step__check {
99
+ display: block;
90
100
  width: var(--onboarding-step-check-size);
91
101
  height: var(--onboarding-step-check-size);
92
- color: var(--onboarding-step-content-success);
93
102
  }
94
103
 
95
104
  .ds-onboarding-step__line {
@@ -113,7 +122,7 @@
113
122
  .ds-onboarding-step__line--active {
114
123
  position: relative;
115
124
  left: auto;
116
- height: var(--onboarding-step-check-size);
125
+ height: var(--onboarding-step-check-slot-size);
117
126
  transform: none;
118
127
  background: var(--onboarding-step-border-brand-strong);
119
128
  }
@@ -349,6 +349,7 @@
349
349
  display: inline-flex;
350
350
  align-items: center;
351
351
  gap: var(--bh-space-md-8);
352
+ max-width: 100%;
352
353
  }
353
354
 
354
355
  .ds-table-avatar-text > span:last-child {
@@ -509,14 +510,16 @@
509
510
 
510
511
  .ds-table-icon-button svg,
511
512
  .ds-table-icon-label svg {
512
- width: var(--bh-space-4xl-20);
513
- height: var(--bh-space-4xl-20);
513
+ width: var(--bh-icon-size-default);
514
+ height: var(--bh-icon-size-default);
514
515
  }
515
516
 
516
517
  .ds-table-icon-label > span:first-child {
517
518
  flex: 0 0 auto;
518
519
  display: inline-grid;
519
520
  place-items: center;
521
+ width: var(--bh-icon-slot-size-default);
522
+ height: var(--bh-icon-slot-size-default);
520
523
  }
521
524
 
522
525
  .ds-table-icon-label > span:last-child {
@@ -9,7 +9,7 @@ import {
9
9
  } from "lucide-react"
10
10
 
11
11
  import { cn } from "@/lib/utils"
12
- import { Kbd, KbdShortcut } from "@/components/ui/kbd"
12
+ import { Shortcut } from "@/components/ui/kbd"
13
13
  import {
14
14
  getInputDefaultValue,
15
15
  getInputHelperText,
@@ -419,7 +419,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
419
419
 
420
420
  <span
421
421
  data-slot="input-content"
422
- className={cn(inputContent({ size: resolvedSize }))}
422
+ className={cn(
423
+ inputContent({ size: resolvedSize }),
424
+ kind === "shortcut" &&
425
+ resolvedSize === "lg" &&
426
+ "px-[var(--bh-input-md-padding-x)]"
427
+ )}
423
428
  >
424
429
  {shouldShowLeadingIcon ? (
425
430
  <InputLeadingIcon disabled={isDisabled} icon={leadingIcon} />
@@ -470,9 +475,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
470
475
 
471
476
  {kind === "shortcut" ? (
472
477
  hasRenderableContent(shortcut) ? (
473
- <Kbd disabled={isDisabled}>{shortcut}</Kbd>
478
+ <Shortcut disabled={isDisabled}>{shortcut}</Shortcut>
474
479
  ) : (
475
- <KbdShortcut disabled={isDisabled} keys={shortcutKeys} />
480
+ <Shortcut disabled={isDisabled} keys={shortcutKeys} />
476
481
  )
477
482
  ) : null}
478
483
  </span>
@@ -618,7 +623,9 @@ function InputLeadingIcon({
618
623
  aria-hidden="true"
619
624
  data-slot="input-leading-icon"
620
625
  className={cn(
621
- "flex size-[var(--bh-input-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
626
+ "flex size-[var(--bh-input-icon-slot-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
627
+ "[&>img]:size-[var(--bh-input-icon-size)] [&>img]:shrink-0 [&>img]:object-contain",
628
+ "[&>svg]:size-[var(--bh-input-icon-size)] [&>svg]:shrink-0",
622
629
  disabled && "text-[var(--bh-content-disabled)]"
623
630
  )}
624
631
  >
@@ -99,7 +99,7 @@ const ItemMedia = React.forwardRef<HTMLDivElement, ItemMediaProps>(
99
99
  ref={ref}
100
100
  className={cn(
101
101
  "flex size-[var(--bh-item-media-size)] shrink-0 items-center justify-center overflow-hidden",
102
- "[&_svg]:size-[var(--bh-item-icon-size)] [&_img]:size-full [&_img]:object-cover",
102
+ "[&_svg]:size-[var(--bh-item-icon-size)] [&_svg]:m-[calc((var(--bh-item-icon-slot-size)-var(--bh-item-icon-size))/2)] [&_img]:size-full [&_img]:object-cover",
103
103
  variant === "icon" &&
104
104
  "rounded-[var(--bh-item-media-radius)] bg-[var(--bh-bg-neutral-subtle)] text-[var(--bh-content-subtle)]",
105
105
  variant === "avatar" &&
@@ -9,7 +9,7 @@ const kbdVariants = cva(
9
9
  [
10
10
  "inline-flex min-w-[var(--bh-space-5xl-24)] items-center justify-center whitespace-nowrap rounded-[var(--bh-radius-md-6)]",
11
11
  "border border-[var(--bh-border-default)] bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-subtle)]",
12
- "font-[var(--bh-font-family)] text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)]",
12
+ "[font-family:var(--bh-font-family)] text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)]",
13
13
  "leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] shadow-[var(--shadow-button-soft)]",
14
14
  "data-[disabled=true]:border-[var(--bh-border-disabled)] data-[disabled=true]:bg-[var(--bh-interactive-soft-disabled)] data-[disabled=true]:text-[var(--bh-content-disabled)] data-[disabled=true]:shadow-none",
15
15
  ],
@@ -26,6 +26,14 @@ const kbdVariants = cva(
26
26
  }
27
27
  )
28
28
 
29
+ const shortcutVariants = cva([
30
+ "inline-flex h-[var(--bh-space-4xl-20)] min-w-[var(--bh-space-4xl-20)] shrink-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-radius-sm-4)] px-[var(--bh-space-xs-4)]",
31
+ "border border-[var(--bh-border-default)] bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-subtle)]",
32
+ "[font-family:var(--bh-font-family)] text-[length:var(--bh-text-body-2xs-medium-font-size)] font-[var(--bh-text-body-2xs-medium-font-weight)]",
33
+ "leading-[var(--bh-text-body-2xs-medium-line-height)] tracking-[var(--bh-text-body-2xs-medium-letter-spacing)]",
34
+ "data-[disabled=true]:border-[var(--bh-border-disabled)] data-[disabled=true]:bg-[var(--bh-interactive-soft-disabled)] data-[disabled=true]:text-[var(--bh-content-disabled)]",
35
+ ])
36
+
29
37
  type KbdProps = React.ComponentProps<"kbd"> &
30
38
  VariantProps<typeof kbdVariants> & {
31
39
  disabled?: boolean
@@ -48,6 +56,55 @@ const Kbd = React.forwardRef<HTMLElement, KbdProps>(function Kbd(
48
56
 
49
57
  type ShortcutPlatform = "auto" | "mac" | "windows" | "linux"
50
58
 
59
+ type ShortcutProps = Omit<React.ComponentProps<"kbd">, "children"> & {
60
+ children?: React.ReactNode
61
+ disabled?: boolean
62
+ keys?: readonly string[]
63
+ platform?: ShortcutPlatform
64
+ }
65
+
66
+ const Shortcut = React.forwardRef<HTMLElement, ShortcutProps>(function Shortcut(
67
+ {
68
+ "aria-label": ariaLabel,
69
+ children,
70
+ className,
71
+ disabled = false,
72
+ keys = [],
73
+ platform = "auto",
74
+ ...props
75
+ },
76
+ ref
77
+ ) {
78
+ const resolvedPlatform = useShortcutPlatform(platform)
79
+ const resolvedKeys = keys.map((key) =>
80
+ resolveShortcutKey(key, resolvedPlatform)
81
+ )
82
+ const content =
83
+ children ?? formatShortcutKeys(resolvedKeys, resolvedPlatform)
84
+ const accessibleLabel =
85
+ ariaLabel ??
86
+ (resolvedKeys.length > 0
87
+ ? resolvedKeys.map(readableShortcutKey).join(" plus ")
88
+ : typeof children === "string"
89
+ ? readableShortcutText(children)
90
+ : undefined)
91
+
92
+ return (
93
+ <kbd
94
+ aria-label={accessibleLabel}
95
+ className={cn(shortcutVariants(), className)}
96
+ data-disabled={disabled ? "true" : undefined}
97
+ data-platform={resolvedPlatform}
98
+ data-slot="shortcut"
99
+ dir="ltr"
100
+ ref={ref}
101
+ {...props}
102
+ >
103
+ {content}
104
+ </kbd>
105
+ )
106
+ })
107
+
51
108
  type KbdShortcutProps = Omit<React.ComponentProps<"span">, "children"> & {
52
109
  disabled?: boolean
53
110
  keys: readonly string[]
@@ -68,18 +125,7 @@ const KbdShortcut = React.forwardRef<HTMLSpanElement, KbdShortcutProps>(
68
125
  },
69
126
  ref
70
127
  ) {
71
- const [resolvedPlatform, setResolvedPlatform] = React.useState<
72
- Exclude<ShortcutPlatform, "auto">
73
- >(platform === "auto" ? "mac" : platform)
74
-
75
- React.useEffect(() => {
76
- if (platform !== "auto") {
77
- setResolvedPlatform(platform)
78
- return
79
- }
80
-
81
- setResolvedPlatform(detectShortcutPlatform())
82
- }, [platform])
128
+ const resolvedPlatform = useShortcutPlatform(platform)
83
129
 
84
130
  const resolvedKeys = keys.map((key) =>
85
131
  resolveShortcutKey(key, resolvedPlatform)
@@ -117,6 +163,25 @@ const KbdShortcut = React.forwardRef<HTMLSpanElement, KbdShortcutProps>(
117
163
  }
118
164
  )
119
165
 
166
+ function useShortcutPlatform(
167
+ platform: ShortcutPlatform
168
+ ): Exclude<ShortcutPlatform, "auto"> {
169
+ const [resolvedPlatform, setResolvedPlatform] = React.useState<
170
+ Exclude<ShortcutPlatform, "auto">
171
+ >(platform === "auto" ? "mac" : platform)
172
+
173
+ React.useEffect(() => {
174
+ if (platform !== "auto") {
175
+ setResolvedPlatform(platform)
176
+ return
177
+ }
178
+
179
+ setResolvedPlatform(detectShortcutPlatform())
180
+ }, [platform])
181
+
182
+ return resolvedPlatform
183
+ }
184
+
120
185
  function detectShortcutPlatform(): Exclude<ShortcutPlatform, "auto"> {
121
186
  if (typeof navigator === "undefined") return "mac"
122
187
  const platform = navigator.platform.toLowerCase()
@@ -147,5 +212,34 @@ function readableShortcutKey(key: string) {
147
212
  return key
148
213
  }
149
214
 
150
- export { Kbd, KbdShortcut, kbdVariants }
151
- export type { KbdProps, KbdShortcutProps, ShortcutPlatform }
215
+ function formatShortcutKeys(
216
+ keys: readonly string[],
217
+ platform: Exclude<ShortcutPlatform, "auto">
218
+ ) {
219
+ if (platform === "mac" && keys.slice(0, -1).every(isMacShortcutSymbol)) {
220
+ return keys.join("")
221
+ }
222
+
223
+ return keys.join(" ")
224
+ }
225
+
226
+ function isMacShortcutSymbol(key: string) {
227
+ return ["⌘", "⌥", "⇧", "⌃"].includes(key)
228
+ }
229
+
230
+ function readableShortcutText(value: string) {
231
+ return value
232
+ .replaceAll("⌘", "Command ")
233
+ .replaceAll("⌥", "Option ")
234
+ .replaceAll("⇧", "Shift ")
235
+ .replaceAll("⌃", "Control ")
236
+ .trim()
237
+ }
238
+
239
+ export { Kbd, KbdShortcut, Shortcut, kbdVariants, shortcutVariants }
240
+ export type {
241
+ KbdProps,
242
+ KbdShortcutProps,
243
+ ShortcutPlatform,
244
+ ShortcutProps,
245
+ }
@@ -90,7 +90,7 @@ const menuItemIconVariants = cva(
90
90
  variants: {
91
91
  position: {
92
92
  leading:
93
- "h-[var(--bh-menu-leading-icon-size)] ps-[var(--bh-menu-icon-padding)] [&_svg]:size-[var(--bh-menu-leading-icon-size)]",
93
+ "h-[var(--bh-menu-leading-icon-slot-size)] ps-[var(--bh-menu-icon-padding)] [&_svg]:size-[var(--bh-menu-leading-icon-size)]",
94
94
  trailing:
95
95
  "size-[var(--bh-menu-trailing-icon-size)] text-[var(--bh-content-muted)] [&_svg]:size-[var(--bh-menu-trailing-icon-size)]",
96
96
  },
@@ -39,7 +39,7 @@ type ConfirmModalMessages = {
39
39
 
40
40
  const modalSurfaceBaseClassName = [
41
41
  "flex max-h-[calc(100vh-(var(--bh-space-8xl-48)*2))] w-[var(--bh-modal-width)] max-w-[calc(100vw-(var(--bh-space-4xl-20)*2))] flex-col overflow-hidden rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--shadow-modal)] outline-none",
42
- "[--bh-modal-border:var(--bh-border-subtle)] [--shadow-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-modal-border),var(--shadow-lg)]",
42
+ "[--bh-modal-border:var(--bh-border-subtle)] [--shadow-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-modal-border),var(--shadow-2xl)]",
43
43
  ]
44
44
 
45
45
  const modalSizeVariants = {
@@ -111,7 +111,7 @@ const paginationControlVariants = cva(
111
111
  "outline-none transition-[background-color,border-color,color,box-shadow]",
112
112
  "focus-visible:shadow-[var(--shadow-pagination-focus)]",
113
113
  "disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
114
- "[&_svg]:pointer-events-none [&_svg]:size-[var(--bh-pagination-icon-size)] [&_svg]:shrink-0",
114
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0",
115
115
  "rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
116
116
  ],
117
117
  {
@@ -507,13 +507,18 @@ function PaginationArrowIcon({ type }: { type: "previous" | "next" }) {
507
507
  const Icon = type === "previous" ? ChevronLeftIcon : ChevronRightIcon
508
508
 
509
509
  return (
510
- <Icon
511
- aria-hidden="true"
512
- data-rtl-flip="true"
510
+ <span
513
511
  data-slot="pagination-icon"
514
- focusable="false"
515
- strokeWidth="var(--bh-icon-stroke-225)"
516
- />
512
+ className="inline-flex size-[var(--bh-pagination-icon-slot-size)] shrink-0 items-center justify-center"
513
+ >
514
+ <Icon
515
+ aria-hidden="true"
516
+ className="size-[var(--bh-pagination-icon-size)]"
517
+ data-rtl-flip="true"
518
+ focusable="false"
519
+ strokeWidth="var(--bh-icon-stroke-225)"
520
+ />
521
+ </span>
517
522
  )
518
523
  }
519
524
 
@@ -579,7 +579,7 @@ function RadioCardMedia({
579
579
  <span
580
580
  data-slot="radio-card-icon"
581
581
  className={cn(
582
- "flex size-[var(--bh-space-4xl-20)] shrink-0 items-center justify-center text-[var(--bh-content-subtle)]",
582
+ "flex size-[var(--bh-icon-slot-size-default)] shrink-0 items-center justify-center text-[var(--bh-content-subtle)] [&_svg]:size-[var(--bh-icon-size-default)]",
583
583
  disabled && "text-[var(--bh-content-disabled)]"
584
584
  )}
585
585
  >
@@ -592,7 +592,6 @@ function RadioCardUserIcon() {
592
592
  return (
593
593
  <UserIcon
594
594
  aria-hidden="true"
595
- className="size-[var(--bh-space-4xl-20)]"
596
595
  strokeWidth="var(--bh-icon-stroke-200)"
597
596
  />
598
597
  )
@@ -783,7 +783,7 @@ const SelectMenuItem = React.forwardRef<HTMLButtonElement, SelectMenuItemProps>(
783
783
  {selectedMedia ? (
784
784
  <span
785
785
  data-slot="select-menu-item-media"
786
- className="flex shrink-0 items-center ps-[var(--bh-select-media-inset)] text-[var(--bh-content-subtle)] group-data-[disabled=true]/select-menu-item:text-[var(--bh-content-disabled)] group-data-[disabled=true]/select-menu-item:opacity-[var(--bh-opacity-60)] [&_svg:not([class*='size-'])]:size-[var(--bh-select-icon-slot-size)]"
786
+ className="flex shrink-0 items-center ps-[var(--bh-select-media-inset)] text-[var(--bh-content-subtle)] group-data-[disabled=true]/select-menu-item:text-[var(--bh-content-disabled)] group-data-[disabled=true]/select-menu-item:opacity-[var(--bh-opacity-60)] [&_svg:not([class*='size-'])]:size-[var(--bh-icon-size-default)]"
787
787
  >
788
788
  {selectedMedia}
789
789
  </span>