banhaten 0.2.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 (120) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +5 -5
  3. package/docs/design-system/component-implementation-ownership-audit.md +245 -0
  4. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -0
  5. package/docs/design-system/component-showcase-consistency-report.md +2 -0
  6. package/docs/design-system/components/README.md +1 -0
  7. package/docs/design-system/components/activity-feed.md +2 -0
  8. package/docs/design-system/components/activity-heatmap.md +73 -0
  9. package/docs/design-system/components/alert-dialog.md +1 -1
  10. package/docs/design-system/components/alert.md +3 -3
  11. package/docs/design-system/components/attribute.md +1 -1
  12. package/docs/design-system/components/avatar.md +2 -2
  13. package/docs/design-system/components/badge.md +1 -1
  14. package/docs/design-system/components/banner.md +6 -2
  15. package/docs/design-system/components/breadcrumbs.md +5 -2
  16. package/docs/design-system/components/button-group.md +3 -2
  17. package/docs/design-system/components/button.md +2 -2
  18. package/docs/design-system/components/card.md +11 -3
  19. package/docs/design-system/components/catalog-components.md +3 -0
  20. package/docs/design-system/components/date-picker.md +3 -1
  21. package/docs/design-system/components/file-upload.md +5 -1
  22. package/docs/design-system/components/input.md +8 -6
  23. package/docs/design-system/components/item.md +3 -1
  24. package/docs/design-system/components/kbd.md +11 -8
  25. package/docs/design-system/components/menu.md +2 -2
  26. package/docs/design-system/components/modal.md +1 -1
  27. package/docs/design-system/components/navigation-menu.md +1 -1
  28. package/docs/design-system/components/onboarding-step-list-item.md +3 -2
  29. package/docs/design-system/components/page-header.md +3 -1
  30. package/docs/design-system/components/pagination.md +3 -2
  31. package/docs/design-system/components/radio-card.md +1 -1
  32. package/docs/design-system/components/resizable.md +1 -1
  33. package/docs/design-system/components/scroll-area.md +7 -3
  34. package/docs/design-system/components/select.md +1 -1
  35. package/docs/design-system/components/sidebar.md +60 -18
  36. package/docs/design-system/components/slideout.md +4 -1
  37. package/docs/design-system/components/steps.md +7 -2
  38. package/docs/design-system/components/table.md +25 -7
  39. package/docs/design-system/components/tag.md +3 -3
  40. package/docs/design-system/components/timeline.md +4 -1
  41. package/docs/design-system/components/toggle.md +1 -1
  42. package/docs/design-system/components/toolbar.md +1 -1
  43. package/docs/design-system/components/tooltip.md +1 -1
  44. package/docs/design-system/density-and-elevation.md +23 -2
  45. package/docs/design-system/gap-promotion-report.md +1 -67
  46. package/docs/design-system/icons.md +6 -1
  47. package/docs/design-system/installation.md +1 -1
  48. package/docs/design-system/launch-comparison-shadcn.md +10 -10
  49. package/docs/design-system/token-governance.md +7 -8
  50. package/package.json +3 -2
  51. package/registry/components/alert-dialog.tsx +1 -1
  52. package/registry/components/alert.tsx +17 -13
  53. package/registry/components/attribute.tsx +8 -2
  54. package/registry/components/avatar.tsx +28 -20
  55. package/registry/components/badge.tsx +17 -17
  56. package/registry/components/button-group.tsx +4 -2
  57. package/registry/components/button.tsx +3 -3
  58. package/registry/components/card.tsx +19 -3
  59. package/registry/components/carousel.tsx +14 -0
  60. package/registry/components/command.tsx +37 -3
  61. package/registry/components/date-picker.tsx +83 -35
  62. package/registry/components/expanded/ActivityFeed.tsx +3 -7
  63. package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
  64. package/registry/components/expanded/Banner.tsx +0 -1
  65. package/registry/components/expanded/Breadcrumbs.tsx +3 -0
  66. package/registry/components/expanded/CatalogComponentsShowcase.tsx +3 -4
  67. package/registry/components/expanded/CatalogDivider.tsx +3 -2
  68. package/registry/components/expanded/FileUpload.tsx +7 -5
  69. package/registry/components/expanded/OnboardingStepListItem.tsx +6 -7
  70. package/registry/components/expanded/PageHeader.tsx +35 -44
  71. package/registry/components/expanded/Slideout.tsx +18 -13
  72. package/registry/components/expanded/Steps.tsx +147 -9
  73. package/registry/components/expanded/Table.tsx +50 -18
  74. package/registry/components/expanded/Timeline.tsx +11 -24
  75. package/registry/components/expanded/activityFeed.css +8 -8
  76. package/registry/components/expanded/activityHeatmap.css +66 -0
  77. package/registry/components/expanded/banner.css +30 -15
  78. package/registry/components/expanded/breadcrumbs.css +5 -5
  79. package/registry/components/expanded/fileUpload.css +4 -4
  80. package/registry/components/expanded/onboardingStepListItem.css +16 -7
  81. package/registry/components/expanded/pageHeader.css +14 -11
  82. package/registry/components/expanded/slideout.css +3 -7
  83. package/registry/components/expanded/steps.css +75 -12
  84. package/registry/components/expanded/table.css +63 -69
  85. package/registry/components/expanded/timeline.css +17 -16
  86. package/registry/components/field.tsx +61 -8
  87. package/registry/components/input.tsx +12 -5
  88. package/registry/components/item.tsx +1 -1
  89. package/registry/components/kbd.tsx +109 -15
  90. package/registry/components/menu.tsx +57 -7
  91. package/registry/components/modal.tsx +1 -1
  92. package/registry/components/navigation-menu.tsx +1 -1
  93. package/registry/components/pagination.css +36 -0
  94. package/registry/components/pagination.tsx +20 -13
  95. package/registry/components/progress-slider.tsx +1 -1
  96. package/registry/components/radio-card.tsx +1 -2
  97. package/registry/components/resizable.tsx +36 -2
  98. package/registry/components/scroll-area.tsx +30 -6
  99. package/registry/components/select.tsx +35 -12
  100. package/registry/components/sidebar.tsx +940 -62
  101. package/registry/components/social-button.tsx +12 -5
  102. package/registry/components/table-elements.tsx +128 -0
  103. package/registry/components/tabs.tsx +5 -10
  104. package/registry/components/tag.tsx +2 -2
  105. package/registry/components/toggle.tsx +2 -2
  106. package/registry/components/toolbar.tsx +6 -6
  107. package/registry/components/tooltip.tsx +1 -1
  108. package/registry/examples/attribute-demo.tsx +1 -1
  109. package/registry/examples/avatar-demo.tsx +7 -4
  110. package/registry/examples/command-palette-demo.tsx +0 -1
  111. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -0
  112. package/registry/examples/expanded/breadcrumbs-demo.tsx +3 -3
  113. package/registry/examples/kbd-demo.tsx +2 -1
  114. package/registry/examples/pagination-demo.tsx +5 -5
  115. package/registry/examples/sidebar-demo.tsx +59 -13
  116. package/registry/guidance/USAGE.md +1 -1
  117. package/registry/index.json +621 -60
  118. package/registry/styles/globals.css +211 -151
  119. package/registry.json +44 -6
  120. package/tokens/banhaten.tokens.json +325 -95
@@ -58,7 +58,7 @@ const cardVariants = cva(
58
58
  orientation: "horizontal",
59
59
  breakpoint: "desktop",
60
60
  class:
61
- "min-h-[var(--bh-card-horizontal-min-height)] w-[min(100%,var(--bh-card-horizontal-width))]",
61
+ "min-h-[var(--bh-card-horizontal-min-height)] w-[min(100%,var(--bh-card-horizontal-width))] flex-wrap",
62
62
  },
63
63
  {
64
64
  orientation: "horizontal",
@@ -109,6 +109,20 @@ type CardActionsProps = React.ComponentProps<"div"> & {
109
109
  }
110
110
 
111
111
  type CardFooterProps = React.ComponentProps<"div">
112
+ type CardSurfaceProps = React.ComponentPropsWithoutRef<"article">
113
+
114
+ const CardSurface = React.forwardRef<HTMLElement, CardSurfaceProps>(
115
+ function CardSurface({ className, ...props }, ref) {
116
+ return (
117
+ <article
118
+ data-slot="card-surface"
119
+ ref={ref}
120
+ className={cn(cardVariants(), className)}
121
+ {...props}
122
+ />
123
+ )
124
+ }
125
+ )
112
126
 
113
127
  const Card = React.forwardRef<HTMLElement, CardProps>(function Card({
114
128
  actions,
@@ -362,7 +376,7 @@ function CardBody({
362
376
  className={cn(
363
377
  "flex min-w-0 flex-col",
364
378
  horizontal
365
- ? "flex-1 gap-[var(--bh-space-5xl-24)] p-[var(--bh-card-horizontal-padding)]"
379
+ ? "min-w-[min(100%,var(--bh-card-vertical-width))] flex-[1_1_var(--bh-card-vertical-width)] gap-[var(--bh-space-5xl-24)] p-[var(--bh-card-horizontal-padding)]"
366
380
  : "w-full gap-[var(--bh-space-5xl-24)] p-[var(--bh-card-content-padding)]"
367
381
  )}
368
382
  >
@@ -566,7 +580,7 @@ function getCardMediaClassName(
566
580
  breakpoint: CardBreakpoint
567
581
  ) {
568
582
  if (orientation === "horizontal" && breakpoint === "desktop") {
569
- return "h-auto w-[var(--bh-card-horizontal-media-width)]"
583
+ return "min-h-[var(--bh-card-media-height)] w-[min(100%,var(--bh-card-horizontal-media-width))] shrink flex-[1_1_var(--bh-card-horizontal-media-width)]"
570
584
  }
571
585
 
572
586
  return "h-[var(--bh-card-media-height)] w-full"
@@ -611,6 +625,7 @@ export {
611
625
  CardActions,
612
626
  CardFooter,
613
627
  CardMedia,
628
+ CardSurface,
614
629
  cardVariants,
615
630
  type CardActionsProps,
616
631
  type CardBreakpoint,
@@ -618,5 +633,6 @@ export {
618
633
  type CardMediaProps,
619
634
  type CardOrientation,
620
635
  type CardProps,
636
+ type CardSurfaceProps,
621
637
  type CardType,
622
638
  }
@@ -17,16 +17,22 @@ type CarouselPlugin = UseCarouselParameters[1]
17
17
  type CarouselOrientation = "horizontal" | "vertical"
18
18
 
19
19
  type CarouselMessages = {
20
+ carouselLabel: string
21
+ carouselRoleDescription: string
20
22
  nextSlide: string
21
23
  previousSlide: string
22
24
  }
23
25
 
24
26
  const defaultCarouselMessages: CarouselMessages = {
27
+ carouselLabel: "Carousel",
28
+ carouselRoleDescription: "carousel",
25
29
  nextSlide: "Next slide",
26
30
  previousSlide: "Previous slide",
27
31
  }
28
32
 
29
33
  const arabicCarouselMessages: CarouselMessages = {
34
+ carouselLabel: "\u0639\u0627\u0631\u0636 \u0627\u0644\u0634\u0631\u0627\u0626\u062d",
35
+ carouselRoleDescription: "\u0639\u0627\u0631\u0636 \u0634\u0631\u0627\u0626\u062d",
30
36
  nextSlide: "\u0627\u0644\u0634\u0631\u064a\u062d\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629",
31
37
  previousSlide: "\u0627\u0644\u0634\u0631\u064a\u062d\u0629 \u0627\u0644\u0633\u0627\u0628\u0642\u0629",
32
38
  }
@@ -58,6 +64,9 @@ const CarouselContext = React.createContext<CarouselContextValue | null>(null)
58
64
  const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
59
65
  function Carousel(
60
66
  {
67
+ "aria-label": ariaLabel,
68
+ "aria-labelledby": ariaLabelledBy,
69
+ "aria-roledescription": ariaRoleDescription,
61
70
  children,
62
71
  className,
63
72
  dir,
@@ -138,6 +147,11 @@ const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
138
147
  dir={direction}
139
148
  ref={setRootRef}
140
149
  role="region"
150
+ aria-label={ariaLabelledBy ? undefined : (ariaLabel ?? messages.carouselLabel)}
151
+ aria-labelledby={ariaLabelledBy}
152
+ aria-roledescription={
153
+ ariaRoleDescription ?? messages.carouselRoleDescription
154
+ }
141
155
  className={cn("relative", className)}
142
156
  {...props}
143
157
  >
@@ -63,10 +63,27 @@ const CommandInput = React.forwardRef<HTMLInputElement, CommandInputProps>(
63
63
 
64
64
  const CommandList = React.forwardRef<HTMLDivElement, CommandListProps>(
65
65
  function CommandList({ className, ...props }, ref) {
66
+ const listRef = React.useRef<HTMLDivElement | null>(null)
67
+ React.useLayoutEffect(() => {
68
+ listRef.current
69
+ ?.querySelector<HTMLElement>("[cmdk-list-sizer]")
70
+ ?.setAttribute("role", "presentation")
71
+ })
72
+
73
+ const setListRef = React.useCallback(
74
+ (node: HTMLDivElement | null) => {
75
+ listRef.current = node
76
+
77
+ if (typeof ref === "function") ref(node)
78
+ else if (ref) ref.current = node
79
+ },
80
+ [ref]
81
+ )
82
+
66
83
  return (
67
84
  <CommandPrimitive.List
68
85
  data-slot="command-list"
69
- ref={ref}
86
+ ref={setListRef}
70
87
  className={cn(
71
88
  "max-h-[calc(var(--bh-space-19xl-384)+var(--bh-space-8xl-48))] overflow-y-auto overflow-x-hidden p-[var(--bh-space-md-8)]",
72
89
  className
@@ -79,10 +96,25 @@ const CommandList = React.forwardRef<HTMLDivElement, CommandListProps>(
79
96
 
80
97
  const CommandEmpty = React.forwardRef<HTMLDivElement, CommandEmptyProps>(
81
98
  function CommandEmpty({ className, ...props }, ref) {
99
+ const emptyRef = React.useRef<HTMLDivElement | null>(null)
100
+ React.useLayoutEffect(() => {
101
+ emptyRef.current?.setAttribute("role", "option")
102
+ emptyRef.current?.setAttribute("aria-disabled", "true")
103
+ emptyRef.current?.setAttribute("aria-selected", "false")
104
+ })
105
+ const setEmptyRef = React.useCallback(
106
+ (node: HTMLDivElement | null) => {
107
+ emptyRef.current = node
108
+ if (typeof ref === "function") ref(node)
109
+ else if (ref) ref.current = node
110
+ },
111
+ [ref]
112
+ )
113
+
82
114
  return (
83
115
  <CommandPrimitive.Empty
84
116
  data-slot="command-empty"
85
- ref={ref}
117
+ ref={setEmptyRef}
86
118
  className={cn(
87
119
  "py-[var(--bh-space-8xl-48)] text-center text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-text-body-sm-regular-font-weight)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)] text-[var(--bh-content-subtle)]",
88
120
  className
@@ -130,9 +162,11 @@ const CommandSeparator = React.forwardRef<
130
162
  CommandSeparatorProps
131
163
  >(function CommandSeparator({ className, ...props }, ref) {
132
164
  return (
133
- <CommandPrimitive.Separator
165
+ <div
134
166
  data-slot="command-separator"
135
167
  ref={ref}
168
+ role="presentation"
169
+ aria-hidden="true"
136
170
  className={cn("-mx-[var(--bh-space-md-8)] my-[var(--bh-space-md-8)] h-[var(--bh-border-width-default)] bg-[var(--bh-border-subtle)]", className)}
137
171
  {...props}
138
172
  />
@@ -159,7 +159,15 @@ type DateRangePickerProps = Omit<
159
159
  }
160
160
 
161
161
  const defaultTimeValue = getCurrentTimeValue()
162
- const defaultDayLabels = ["S", "M", "T", "W", "T", "F", "S"]
162
+ const defaultDayLabels = [
163
+ { full: "Sunday", short: "S" },
164
+ { full: "Monday", short: "M" },
165
+ { full: "Tuesday", short: "T" },
166
+ { full: "Wednesday", short: "W" },
167
+ { full: "Thursday", short: "T" },
168
+ { full: "Friday", short: "F" },
169
+ { full: "Saturday", short: "S" },
170
+ ] as const
163
171
 
164
172
  const calendarSurface = cva(
165
173
  [
@@ -216,7 +224,7 @@ const monthButton = cva(
216
224
  "flex size-[var(--bh-date-nav-button-size)] shrink-0 items-center justify-center rounded-[var(--bh-control-default)]",
217
225
  "bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] outline-none transition-[background-color,box-shadow,color]",
218
226
  "hover:bg-[var(--bh-interactive-soft-hover)] focus-visible:shadow-[var(--shadow-button-focus)] disabled:opacity-[var(--bh-opacity-0)]",
219
- "[&_svg]:size-[var(--bh-date-nav-icon-size)]",
227
+ "[&_[data-slot=date-picker-nav-icon]>svg]:size-[var(--bh-date-nav-icon-size)]",
220
228
  ]
221
229
  )
222
230
 
@@ -262,7 +270,7 @@ const dayCellButton = cva(
262
270
  event:
263
271
  "rounded-[var(--bh-control-default)] text-[var(--bh-content-brand-default)] after:absolute after:bottom-[var(--bh-date-event-dot-offset-bottom)] after:left-1/2 after:size-[var(--bh-date-event-dot-size)] after:-translate-x-1/2 after:rounded-[var(--bh-radius-full)] after:bg-[var(--bh-content-brand-default)] rtl:after:translate-x-1/2",
264
272
  selected:
265
- "rounded-[var(--bh-control-default)] bg-[var(--bh-bg-brand-default)] text-[var(--bh-content-on-color)] hover:bg-[var(--bh-bg-brand-default)]",
273
+ "rounded-[var(--bh-control-default)] bg-[var(--bh-bg-brand-bold)] text-[var(--bh-content-on-brand)] hover:bg-[var(--bh-bg-brand-bold)] hover:shadow-[inset_0_0_0_var(--bh-border-width-default)_var(--bh-border-focus)]",
266
274
  range:
267
275
  "rounded-none bg-[var(--bh-bg-brand-subtle)] text-[var(--bh-content-brand-strong)] hover:bg-[var(--bh-bg-brand-subtle)]",
268
276
  },
@@ -756,7 +764,7 @@ function DatePickerTrigger({
756
764
  <span
757
765
  aria-hidden="true"
758
766
  data-slot="date-picker-trigger-icon"
759
- className="flex size-[var(--bh-date-trigger-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)] group-disabled/date-trigger:text-[var(--bh-content-disabled)] [&_svg:not([class*='size-'])]:size-[var(--bh-date-trigger-icon-size)]"
767
+ className="flex size-[var(--bh-date-trigger-icon-slot-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)] group-disabled/date-trigger:text-[var(--bh-content-disabled)] [&_svg:not([class*='size-'])]:size-[var(--bh-date-trigger-icon-size)]"
760
768
  >
761
769
  {icon ?? <CalendarDaysIcon strokeWidth="var(--bh-icon-stroke-180)" />}
762
770
  </span>
@@ -816,6 +824,7 @@ function CalendarMonth({
816
824
  }) {
817
825
  const cells = React.useMemo(() => getCalendarCells(displayMonth), [displayMonth])
818
826
  const gridRef = React.useRef<HTMLDivElement>(null)
827
+ const titleId = React.useId()
819
828
  const pendingFocusDateRef = React.useRef<Date | undefined>(undefined)
820
829
  const monthTitle = title ?? formatMonthTitle(displayMonth)
821
830
  const hasMonthToggle = mode === "single"
@@ -864,13 +873,20 @@ function CalendarMonth({
864
873
  className={monthButton()}
865
874
  onClick={() => onDisplayMonthChange(addMonths(displayMonth, -1))}
866
875
  >
867
- <ChevronLeftIcon aria-hidden="true" data-rtl-flip="true" />
876
+ <span
877
+ aria-hidden="true"
878
+ data-slot="date-picker-nav-icon"
879
+ className="inline-flex size-[var(--bh-date-nav-icon-slot-size)] items-center justify-center"
880
+ >
881
+ <ChevronLeftIcon data-rtl-flip="true" />
882
+ </span>
868
883
  </button>
869
884
 
870
885
  {hasMonthToggle ? (
871
886
  <button
872
887
  aria-expanded={view === "month-year"}
873
888
  data-slot="calendar-title"
889
+ id={titleId}
874
890
  type="button"
875
891
  className={calendarTitleButton({
876
892
  active: view === "month-year",
@@ -888,6 +904,7 @@ function CalendarMonth({
888
904
  ) : (
889
905
  <div
890
906
  data-slot="calendar-title"
907
+ id={titleId}
891
908
  className={calendarTitleButton({ framed: false })}
892
909
  >
893
910
  <span className="min-w-0 truncate">{monthTitle}</span>
@@ -901,46 +918,67 @@ function CalendarMonth({
901
918
  className={monthButton()}
902
919
  onClick={() => onDisplayMonthChange(addMonths(displayMonth, 1))}
903
920
  >
904
- <ChevronRightIcon aria-hidden="true" data-rtl-flip="true" />
921
+ <span
922
+ aria-hidden="true"
923
+ data-slot="date-picker-nav-icon"
924
+ className="inline-flex size-[var(--bh-date-nav-icon-slot-size)] items-center justify-center"
925
+ >
926
+ <ChevronRightIcon data-rtl-flip="true" />
927
+ </span>
905
928
  </button>
906
929
  </div>
907
930
 
908
931
  {view === "days" ? (
909
932
  <>
910
933
  <div
911
- aria-hidden="true"
912
- data-slot="calendar-weekdays"
913
- className="grid w-full grid-cols-7"
914
- >
915
- {defaultDayLabels.map((day, index) => (
916
- <div
917
- className="flex h-[var(--bh-date-day-cell-height)] min-w-0 items-center justify-center rounded-[var(--bh-radius-lg-8)] text-center text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
918
- key={`${day}-${index}`}
919
- >
920
- {day}
921
- </div>
922
- ))}
923
- </div>
924
-
925
- <div
934
+ aria-colcount={7}
935
+ aria-labelledby={titleId}
936
+ aria-rowcount={Math.ceil(cells.length / 7) + 1}
926
937
  data-slot="calendar-grid"
927
938
  role="grid"
928
939
  ref={gridRef}
929
- className="grid w-full grid-cols-7"
940
+ className="grid w-full gap-[var(--bh-space-md-8)]"
930
941
  >
931
- {cells.map((cell) => (
932
- <CalendarDayCell
933
- cell={cell}
934
- displayMonth={displayMonth}
935
- eventDates={eventDates}
936
- key={cell.date.toISOString()}
937
- mode={mode}
938
- onKeyboardNavigateDate={handleKeyboardNavigateDate}
939
- onSelectDate={onSelectDate}
940
- range={range}
941
- selectedDate={selectedDate}
942
- />
943
- ))}
942
+ <div
943
+ data-slot="calendar-weekdays"
944
+ role="row"
945
+ className="grid w-full grid-cols-7"
946
+ >
947
+ {defaultDayLabels.map((day) => (
948
+ <div
949
+ aria-label={day.full}
950
+ className="flex h-[var(--bh-date-day-cell-height)] min-w-0 items-center justify-center rounded-[var(--bh-radius-lg-8)] text-center text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
951
+ key={day.full}
952
+ role="columnheader"
953
+ >
954
+ {day.short}
955
+ </div>
956
+ ))}
957
+ </div>
958
+
959
+ <div role="rowgroup" className="grid w-full">
960
+ {chunkCalendarRows(cells).map((row, rowIndex) => (
961
+ <div
962
+ className="grid w-full grid-cols-7"
963
+ key={row[0]?.date.toISOString() ?? rowIndex}
964
+ role="row"
965
+ >
966
+ {row.map((cell) => (
967
+ <CalendarDayCell
968
+ cell={cell}
969
+ displayMonth={displayMonth}
970
+ eventDates={eventDates}
971
+ key={cell.date.toISOString()}
972
+ mode={mode}
973
+ onKeyboardNavigateDate={handleKeyboardNavigateDate}
974
+ onSelectDate={onSelectDate}
975
+ range={range}
976
+ selectedDate={selectedDate}
977
+ />
978
+ ))}
979
+ </div>
980
+ ))}
981
+ </div>
944
982
  </div>
945
983
 
946
984
  {showTime ? (
@@ -955,6 +993,16 @@ function CalendarMonth({
955
993
  )
956
994
  }
957
995
 
996
+ function chunkCalendarRows(cells: CalendarCell[]) {
997
+ const rows: CalendarCell[][] = []
998
+
999
+ for (let index = 0; index < cells.length; index += 7) {
1000
+ rows.push(cells.slice(index, index + 7))
1001
+ }
1002
+
1003
+ return rows
1004
+ }
1005
+
958
1006
  function CalendarDayCell({
959
1007
  cell,
960
1008
  displayMonth,
@@ -149,7 +149,6 @@ export function ActivityFeedItem({
149
149
  type = "caption",
150
150
  }: ActivityFeedItemProps) {
151
151
  const actionItems = actions === false || !hasActionButtons ? [] : (actions ?? []).slice(0, hasTwoActionButtons ? 2 : 1);
152
- const rtl = dir === "rtl";
153
152
 
154
153
  const textParts = [
155
154
  label && <strong key="label">{label}</strong>,
@@ -162,10 +161,9 @@ export function ActivityFeedItem({
162
161
  className={classes(
163
162
  "ds-activity-feed-item",
164
163
  `ds-activity-feed-item--${type}`,
165
- rtl ? "ds-activity-feed-item--rtl" : "ds-activity-feed-item--ltr",
166
164
  className,
167
165
  )}
168
- dir={rtl ? "rtl" : "ltr"}
166
+ dir={dir}
169
167
  >
170
168
  <Indicator avatarAlt={avatarAlt} avatarSrc={avatarSrc} showLine={showLine} />
171
169
 
@@ -210,13 +208,11 @@ export function ActivityFeed({
210
208
  dir,
211
209
  items = [],
212
210
  }: ActivityFeedProps) {
213
- const rtl = dir === "rtl";
214
-
215
211
  return (
216
212
  <section
217
213
  aria-label={ariaLabel}
218
- className={classes("ds-activity-feed", rtl ? "ds-activity-feed--rtl" : "ds-activity-feed--ltr", className)}
219
- dir={rtl ? "rtl" : "ltr"}
214
+ className={classes("ds-activity-feed", className)}
215
+ dir={dir}
220
216
  >
221
217
  {items.map((item, index) => (
222
218
  <ActivityFeedItem
@@ -0,0 +1,64 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import "./activityHeatmap.css";
3
+
4
+ export type ActivityHeatmapLevel = 0 | 1 | 2 | 3 | 4;
5
+
6
+ export type ActivityHeatmapItem = {
7
+ id?: string;
8
+ label?: string;
9
+ level: ActivityHeatmapLevel;
10
+ };
11
+
12
+ export type ActivityHeatmapProps = Omit<
13
+ ComponentPropsWithoutRef<"div">,
14
+ "children" | "dir"
15
+ > & {
16
+ ariaLabel?: string;
17
+ dir?: "ltr" | "rtl";
18
+ items?: readonly ActivityHeatmapItem[];
19
+ };
20
+
21
+ function classes(...names: Array<string | false | undefined>) {
22
+ return names.filter(Boolean).join(" ");
23
+ }
24
+
25
+ function normalizeLevel(level: ActivityHeatmapLevel): ActivityHeatmapLevel {
26
+ return Math.max(0, Math.min(4, Math.round(level))) as ActivityHeatmapLevel;
27
+ }
28
+
29
+ export function ActivityHeatmap({
30
+ ariaLabel = "Activity heatmap",
31
+ className,
32
+ dir,
33
+ items = [],
34
+ tabIndex = 0,
35
+ ...props
36
+ }: ActivityHeatmapProps) {
37
+ return (
38
+ <div
39
+ {...props}
40
+ aria-label={ariaLabel}
41
+ className={classes("ds-activity-heatmap", className)}
42
+ data-slot="activity-heatmap"
43
+ dir={dir}
44
+ role="img"
45
+ tabIndex={tabIndex}
46
+ >
47
+ <span
48
+ aria-hidden="true"
49
+ className="ds-activity-heatmap__grid"
50
+ data-slot="activity-heatmap-grid"
51
+ >
52
+ {items.map((item, index) => (
53
+ <span
54
+ className="ds-activity-heatmap__cell"
55
+ data-level={normalizeLevel(item.level)}
56
+ data-slot="activity-heatmap-cell"
57
+ key={item.id ?? index}
58
+ title={item.label}
59
+ />
60
+ ))}
61
+ </span>
62
+ </div>
63
+ );
64
+ }
@@ -101,7 +101,6 @@ export function Banner({
101
101
  `ds-banner--${size}`,
102
102
  `ds-banner--${color}`,
103
103
  `ds-banner--${content}`,
104
- dir === "rtl" && "ds-banner--rtl",
105
104
  className,
106
105
  )}
107
106
  dir={dir}
@@ -19,6 +19,7 @@ export type BreadcrumbItem = {
19
19
 
20
20
  export type BreadcrumbsProps = {
21
21
  "aria-label"?: string;
22
+ className?: string;
22
23
  dir?: "ltr" | "rtl" | "auto";
23
24
  iconOnly?: boolean;
24
25
  items: BreadcrumbItem[];
@@ -49,6 +50,7 @@ function visibleItems(
49
50
 
50
51
  export function Breadcrumbs({
51
52
  "aria-label": ariaLabel,
53
+ className,
52
54
  dir,
53
55
  iconOnly = false,
54
56
  items,
@@ -67,6 +69,7 @@ export function Breadcrumbs({
67
69
  `ds-breadcrumbs--${surfaceStyle}`,
68
70
  `ds-breadcrumbs--${separator}`,
69
71
  iconOnly && "ds-breadcrumbs--icon-only",
72
+ className,
70
73
  )}
71
74
  dir={dir}
72
75
  >
@@ -32,7 +32,6 @@ const rtlBreadcrumbItems: BreadcrumbItem[] = [
32
32
  { label: "\u0627\u0644\u0645\u0643\u0648\u0646\u0627\u062a", current: true },
33
33
  ];
34
34
 
35
- const breadcrumbAriaLabel = "Catalog breadcrumbs";
36
35
  const rtlBreadcrumbAriaLabel = "\u0627\u0644\u0645\u0633\u0627\u0631";
37
36
 
38
37
  const uploadFiles: FileUploadFile[] = [
@@ -109,9 +108,9 @@ export function CatalogComponentsShowcase() {
109
108
  <div className="catalog-showcase">
110
109
  <ShowcaseCard title="Breadcrumbs" description="Default, raised, icon-only, separator, and RTL variants.">
111
110
  <div className="catalog-showcase__stack">
112
- <Breadcrumbs aria-label={breadcrumbAriaLabel} items={breadcrumbItems} overflowLabel="More" />
113
- <Breadcrumbs aria-label={breadcrumbAriaLabel} items={breadcrumbItems} overflowLabel="More" separator="chevron" style="raised" />
114
- <Breadcrumbs aria-label={breadcrumbAriaLabel} iconOnly items={breadcrumbItems} overflowLabel="More" separator="chevron" style="raised" />
111
+ <Breadcrumbs aria-label="Default catalog breadcrumbs" items={breadcrumbItems} overflowLabel="More" />
112
+ <Breadcrumbs aria-label="Raised catalog breadcrumbs" items={breadcrumbItems} overflowLabel="More" separator="chevron" style="raised" />
113
+ <Breadcrumbs aria-label="Icon-only catalog breadcrumbs" iconOnly items={breadcrumbItems} overflowLabel="More" separator="chevron" style="raised" />
115
114
  <Breadcrumbs
116
115
  aria-label={rtlBreadcrumbAriaLabel}
117
116
  dir="rtl"
@@ -1,3 +1,4 @@
1
+ import { Button } from "@/components/ui/button";
1
2
  import "./divider.css";
2
3
 
3
4
  export type DividerBorderStyle = "solid" | "dotted";
@@ -36,9 +37,9 @@ export function CatalogContentDivider({
36
37
  <div className={`ds-content-divider ds-content-divider--${borderStyle}`} dir={dir}>
37
38
  <span aria-hidden="true" className="ds-content-divider__rule" />
38
39
  {actionLabel && onAction ? (
39
- <button className="ds-content-divider__button" disabled={disabled} onClick={onAction} type="button">
40
+ <Button className="ds-content-divider__button" disabled={disabled} onClick={onAction} size="sm" type="button" variant="soft">
40
41
  {actionLabel}
41
- </button>
42
+ </Button>
42
43
  ) : label ? (
43
44
  <span className="ds-content-divider__label">{label}</span>
44
45
  ) : null}
@@ -154,9 +154,9 @@ export function FileUpload({
154
154
  <p className="ds-file-upload__title">
155
155
  {resolvedPrompt}{" "}
156
156
  {resolvedBrowseLabel && (
157
- <button className="ds-file-upload__browse" disabled={disabled} onClick={browse} type="button">
157
+ <Button className="ds-file-upload__browse" disabled={disabled} onClick={browse} size="xs" type="button" variant="link">
158
158
  {resolvedBrowseLabel}
159
- </button>
159
+ </Button>
160
160
  )}
161
161
  </p>
162
162
  )}
@@ -267,9 +267,9 @@ function FileUploadRow({
267
267
  )}
268
268
 
269
269
  {!isSmall && file.status === "error" && (
270
- <button className="ds-file-upload-row__retry" onClick={() => onRetry?.(file.id)} type="button">
270
+ <Button className="ds-file-upload-row__retry" onClick={() => onRetry?.(file.id)} size="xs" type="button" variant="link">
271
271
  {retryLabel}
272
- </button>
272
+ </Button>
273
273
  )}
274
274
  </div>
275
275
  </div>
@@ -404,12 +404,14 @@ function StatusIcon({
404
404
  }) {
405
405
  return (
406
406
  <span
407
+ aria-hidden={standalone ? undefined : "true"}
408
+ aria-label={standalone ? getStatusLabel(status) : undefined}
407
409
  className={cx(
408
410
  "ds-file-upload-row__status",
409
411
  standalone && "ds-file-upload-row__status--standalone",
410
412
  `ds-file-upload-row__status--${status}`
411
413
  )}
412
- aria-label={getStatusLabel(status)}
414
+ role={standalone ? "img" : undefined}
413
415
  >
414
416
  {status === "uploaded" && <CheckboxCircleFillIcon />}
415
417
  {status === "uploading" && <Loader2LineIcon />}
@@ -21,7 +21,6 @@ export function OnboardingStepListItem({
21
21
  showTrailingLine = true,
22
22
  state = "finished",
23
23
  }: OnboardingStepListItemProps) {
24
- const rtl = dir === "rtl";
25
24
  const isFinished = state === "finished";
26
25
  const isActive = state === "active";
27
26
  const isInactive = state === "inactive";
@@ -31,7 +30,6 @@ export function OnboardingStepListItem({
31
30
  className={[
32
31
  "ds-onboarding-step",
33
32
  `ds-onboarding-step--${state}`,
34
- rtl ? "ds-onboarding-step--rtl" : "ds-onboarding-step--ltr",
35
33
  className,
36
34
  ]
37
35
  .filter(Boolean)
@@ -52,10 +50,11 @@ export function OnboardingStepListItem({
52
50
 
53
51
  function CheckCircleIcon() {
54
52
  return (
55
- <CircleCheckIcon
56
- aria-hidden="true"
57
- className="ds-onboarding-step__check"
58
- strokeWidth="var(--bh-icon-stroke-225)"
59
- />
53
+ <span className="ds-onboarding-step__check-slot" aria-hidden="true">
54
+ <CircleCheckIcon
55
+ className="ds-onboarding-step__check"
56
+ strokeWidth="var(--bh-icon-stroke-225)"
57
+ />
58
+ </span>
60
59
  );
61
60
  }