banhaten 0.2.0 → 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.
Files changed (101) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/docs/design-system/component-implementation-ownership-audit.md +244 -0
  4. package/docs/design-system/component-quality-audit-2026-08-20.md +223 -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 +2 -2
  11. package/docs/design-system/components/avatar.md +1 -1
  12. package/docs/design-system/components/badge.md +1 -1
  13. package/docs/design-system/components/banner.md +5 -1
  14. package/docs/design-system/components/breadcrumbs.md +3 -0
  15. package/docs/design-system/components/button-group.md +1 -0
  16. package/docs/design-system/components/button.md +1 -1
  17. package/docs/design-system/components/card.md +11 -3
  18. package/docs/design-system/components/catalog-components.md +3 -0
  19. package/docs/design-system/components/date-picker.md +1 -1
  20. package/docs/design-system/components/file-upload.md +4 -0
  21. package/docs/design-system/components/modal.md +1 -1
  22. package/docs/design-system/components/navigation-menu.md +1 -1
  23. package/docs/design-system/components/onboarding-step-list-item.md +1 -0
  24. package/docs/design-system/components/page-header.md +3 -1
  25. package/docs/design-system/components/pagination.md +1 -0
  26. package/docs/design-system/components/resizable.md +1 -1
  27. package/docs/design-system/components/scroll-area.md +7 -3
  28. package/docs/design-system/components/sidebar.md +3 -1
  29. package/docs/design-system/components/slideout.md +4 -1
  30. package/docs/design-system/components/steps.md +7 -2
  31. package/docs/design-system/components/table.md +25 -7
  32. package/docs/design-system/components/tag.md +1 -1
  33. package/docs/design-system/components/timeline.md +4 -1
  34. package/docs/design-system/components/toggle.md +1 -1
  35. package/docs/design-system/components/toolbar.md +1 -1
  36. package/docs/design-system/components/tooltip.md +1 -1
  37. package/docs/design-system/density-and-elevation.md +18 -1
  38. package/docs/design-system/gap-promotion-report.md +1 -67
  39. package/docs/design-system/launch-comparison-shadcn.md +10 -10
  40. package/package.json +3 -2
  41. package/registry/components/alert-dialog.tsx +1 -1
  42. package/registry/components/alert.tsx +15 -11
  43. package/registry/components/avatar.tsx +7 -3
  44. package/registry/components/badge.tsx +17 -17
  45. package/registry/components/button-group.tsx +3 -1
  46. package/registry/components/button.tsx +2 -2
  47. package/registry/components/card.tsx +19 -3
  48. package/registry/components/carousel.tsx +14 -0
  49. package/registry/components/command.tsx +37 -3
  50. package/registry/components/date-picker.tsx +67 -31
  51. package/registry/components/expanded/ActivityFeed.tsx +3 -7
  52. package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
  53. package/registry/components/expanded/Banner.tsx +0 -1
  54. package/registry/components/expanded/Breadcrumbs.tsx +3 -0
  55. package/registry/components/expanded/CatalogComponentsShowcase.tsx +3 -4
  56. package/registry/components/expanded/CatalogDivider.tsx +3 -2
  57. package/registry/components/expanded/FileUpload.tsx +7 -5
  58. package/registry/components/expanded/OnboardingStepListItem.tsx +0 -2
  59. package/registry/components/expanded/PageHeader.tsx +35 -44
  60. package/registry/components/expanded/Slideout.tsx +18 -13
  61. package/registry/components/expanded/Steps.tsx +147 -9
  62. package/registry/components/expanded/Table.tsx +50 -18
  63. package/registry/components/expanded/Timeline.tsx +11 -24
  64. package/registry/components/expanded/activityFeed.css +8 -8
  65. package/registry/components/expanded/activityHeatmap.css +66 -0
  66. package/registry/components/expanded/banner.css +21 -11
  67. package/registry/components/expanded/breadcrumbs.css +1 -1
  68. package/registry/components/expanded/fileUpload.css +1 -1
  69. package/registry/components/expanded/onboardingStepListItem.css +2 -2
  70. package/registry/components/expanded/pageHeader.css +14 -11
  71. package/registry/components/expanded/slideout.css +3 -7
  72. package/registry/components/expanded/steps.css +75 -12
  73. package/registry/components/expanded/table.css +58 -67
  74. package/registry/components/expanded/timeline.css +17 -16
  75. package/registry/components/field.tsx +61 -8
  76. package/registry/components/menu.tsx +56 -6
  77. package/registry/components/modal.tsx +1 -1
  78. package/registry/components/navigation-menu.tsx +1 -1
  79. package/registry/components/pagination.css +36 -0
  80. package/registry/components/pagination.tsx +8 -6
  81. package/registry/components/progress-slider.tsx +1 -1
  82. package/registry/components/resizable.tsx +36 -2
  83. package/registry/components/scroll-area.tsx +30 -6
  84. package/registry/components/select.tsx +34 -11
  85. package/registry/components/sidebar.tsx +11 -5
  86. package/registry/components/social-button.tsx +12 -5
  87. package/registry/components/table-elements.tsx +128 -0
  88. package/registry/components/tabs.tsx +5 -10
  89. package/registry/components/tag.tsx +1 -1
  90. package/registry/components/toggle.tsx +2 -2
  91. package/registry/components/toolbar.tsx +5 -5
  92. package/registry/components/tooltip.tsx +1 -1
  93. package/registry/examples/avatar-demo.tsx +7 -4
  94. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -0
  95. package/registry/examples/expanded/breadcrumbs-demo.tsx +3 -3
  96. package/registry/examples/pagination-demo.tsx +5 -5
  97. package/registry/examples/sidebar-demo.tsx +4 -2
  98. package/registry/index.json +392 -33
  99. package/registry/styles/globals.css +68 -42
  100. package/registry.json +40 -3
  101. package/tokens/banhaten.tokens.json +165 -55
@@ -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
  [
@@ -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
  },
@@ -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"
@@ -871,6 +880,7 @@ function CalendarMonth({
871
880
  <button
872
881
  aria-expanded={view === "month-year"}
873
882
  data-slot="calendar-title"
883
+ id={titleId}
874
884
  type="button"
875
885
  className={calendarTitleButton({
876
886
  active: view === "month-year",
@@ -888,6 +898,7 @@ function CalendarMonth({
888
898
  ) : (
889
899
  <div
890
900
  data-slot="calendar-title"
901
+ id={titleId}
891
902
  className={calendarTitleButton({ framed: false })}
892
903
  >
893
904
  <span className="min-w-0 truncate">{monthTitle}</span>
@@ -908,39 +919,54 @@ function CalendarMonth({
908
919
  {view === "days" ? (
909
920
  <>
910
921
  <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
922
+ aria-colcount={7}
923
+ aria-labelledby={titleId}
924
+ aria-rowcount={Math.ceil(cells.length / 7) + 1}
926
925
  data-slot="calendar-grid"
927
926
  role="grid"
928
927
  ref={gridRef}
929
- className="grid w-full grid-cols-7"
928
+ className="grid w-full gap-[var(--bh-space-md-8)]"
930
929
  >
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
- ))}
930
+ <div
931
+ data-slot="calendar-weekdays"
932
+ role="row"
933
+ className="grid w-full grid-cols-7"
934
+ >
935
+ {defaultDayLabels.map((day) => (
936
+ <div
937
+ aria-label={day.full}
938
+ 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)]"
939
+ key={day.full}
940
+ role="columnheader"
941
+ >
942
+ {day.short}
943
+ </div>
944
+ ))}
945
+ </div>
946
+
947
+ <div role="rowgroup" className="grid w-full">
948
+ {chunkCalendarRows(cells).map((row, rowIndex) => (
949
+ <div
950
+ className="grid w-full grid-cols-7"
951
+ key={row[0]?.date.toISOString() ?? rowIndex}
952
+ role="row"
953
+ >
954
+ {row.map((cell) => (
955
+ <CalendarDayCell
956
+ cell={cell}
957
+ displayMonth={displayMonth}
958
+ eventDates={eventDates}
959
+ key={cell.date.toISOString()}
960
+ mode={mode}
961
+ onKeyboardNavigateDate={handleKeyboardNavigateDate}
962
+ onSelectDate={onSelectDate}
963
+ range={range}
964
+ selectedDate={selectedDate}
965
+ />
966
+ ))}
967
+ </div>
968
+ ))}
969
+ </div>
944
970
  </div>
945
971
 
946
972
  {showTime ? (
@@ -955,6 +981,16 @@ function CalendarMonth({
955
981
  )
956
982
  }
957
983
 
984
+ function chunkCalendarRows(cells: CalendarCell[]) {
985
+ const rows: CalendarCell[][] = []
986
+
987
+ for (let index = 0; index < cells.length; index += 7) {
988
+ rows.push(cells.slice(index, index + 7))
989
+ }
990
+
991
+ return rows
992
+ }
993
+
958
994
  function CalendarDayCell({
959
995
  cell,
960
996
  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)
@@ -1,5 +1,7 @@
1
- import type { ReactNode } from "react";
1
+ import type { ElementType, ReactNode } from "react";
2
2
  import { HomeIcon as LucideHomeIcon } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Breadcrumbs as OwnedBreadcrumbs } from "./Breadcrumbs";
3
5
  import { Tabs, type TabsProps } from "../tabs";
4
6
  import "./pageHeader.css";
5
7
 
@@ -31,6 +33,7 @@ export type PageHeaderProps = {
31
33
  description?: string;
32
34
  dir?: "ltr" | "rtl";
33
35
  icon?: ReactNode | false;
36
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
34
37
  metaInfo?: string;
35
38
  tabs?: PageHeaderTabs | false;
36
39
  title?: string;
@@ -44,64 +47,43 @@ function HomeIcon() {
44
47
  return <LucideHomeIcon aria-hidden="true" className="ds-page-header__home-icon" strokeWidth="var(--bh-icon-stroke-190)" />;
45
48
  }
46
49
 
47
- function Breadcrumbs({ items, label }: { items: PageHeaderBreadcrumb[]; label?: string }) {
48
- return (
49
- <nav aria-label={label} className="ds-page-header__breadcrumbs">
50
- <ol>
51
- {items.map((item, index) => {
52
- const isLast = index === items.length - 1;
53
- const isCurrent = item.current === true;
54
- const content = (
55
- <>
56
- {item.icon && <span className="ds-page-header__breadcrumb-icon">{item.icon}</span>}
57
- <span className="ds-page-header__breadcrumb-label">{item.label}</span>
58
- </>
59
- );
60
-
61
- return (
62
- <li key={`${item.label}-${index}`}>
63
- {item.href && !isCurrent ? (
64
- <a href={item.href}>{content}</a>
65
- ) : (
66
- <span aria-current={isCurrent ? "page" : undefined} className={isCurrent ? "is-current" : undefined}>
67
- {content}
68
- </span>
69
- )}
70
- {!isLast && (
71
- <span aria-hidden="true" className="ds-page-header__breadcrumb-separator">
72
- /
73
- </span>
74
- )}
75
- </li>
76
- );
77
- })}
78
- </ol>
79
- </nav>
80
- );
81
- }
82
-
83
50
  function Action({ action }: { action: PageHeaderAction }) {
84
51
  const className = "ds-page-header__action";
85
52
  const label = <span>{action.label}</span>;
86
53
 
87
54
  if (action.href) {
88
55
  return (
89
- <a aria-label={action.ariaLabel} className={className} href={action.href}>
90
- {label}
91
- </a>
56
+ <Button asChild className={className} variant="secondary">
57
+ <a
58
+ aria-disabled={action.disabled || undefined}
59
+ aria-label={action.ariaLabel}
60
+ href={action.disabled ? undefined : action.href}
61
+ onClick={(event) => {
62
+ if (action.disabled) {
63
+ event.preventDefault();
64
+ return;
65
+ }
66
+ action.onClick?.();
67
+ }}
68
+ tabIndex={action.disabled ? -1 : undefined}
69
+ >
70
+ {label}
71
+ </a>
72
+ </Button>
92
73
  );
93
74
  }
94
75
 
95
76
  return (
96
- <button
77
+ <Button
97
78
  aria-label={action.ariaLabel}
98
79
  className={className}
99
80
  disabled={action.disabled}
100
81
  onClick={action.onClick}
101
82
  type="button"
83
+ variant="secondary"
102
84
  >
103
85
  {label}
104
- </button>
86
+ </Button>
105
87
  );
106
88
  }
107
89
 
@@ -112,6 +94,7 @@ export function PageHeader({
112
94
  className,
113
95
  description,
114
96
  dir,
97
+ headingLevel = 1,
115
98
  icon,
116
99
  metaInfo,
117
100
  tabs = {},
@@ -120,10 +103,18 @@ export function PageHeader({
120
103
  const breadcrumbItems = breadcrumbs === false ? [] : breadcrumbs;
121
104
  const actionItems = actions === false ? [] : actions;
122
105
  const showIcon = icon !== false;
106
+ const Heading = `h${headingLevel}` as ElementType;
123
107
 
124
108
  return (
125
109
  <header className={classes("ds-page-header", className)} dir={dir}>
126
- {breadcrumbItems.length > 0 && <Breadcrumbs items={breadcrumbItems} label={breadcrumbLabel} />}
110
+ {breadcrumbItems.length > 0 && (
111
+ <OwnedBreadcrumbs
112
+ aria-label={breadcrumbLabel ?? "Breadcrumb"}
113
+ className="ds-page-header__breadcrumbs"
114
+ dir={dir}
115
+ items={breadcrumbItems}
116
+ />
117
+ )}
127
118
 
128
119
  <div className="ds-page-header__section">
129
120
  <div className="ds-page-header__topline">
@@ -131,7 +122,7 @@ export function PageHeader({
131
122
  <div className="ds-page-header__heading-row">
132
123
  <div className="ds-page-header__heading-main">
133
124
  {showIcon && <span className="ds-page-header__icon">{icon ?? <HomeIcon />}</span>}
134
- {title && <h1>{title}</h1>}
125
+ {title && <Heading className="ds-page-header__title">{title}</Heading>}
135
126
  </div>
136
127
  {metaInfo && (
137
128
  <span className="ds-page-header__meta">
@@ -1,5 +1,6 @@
1
- import type { ReactNode } from "react";
1
+ import { useId, type ReactNode } from "react";
2
2
  import { XIcon } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
3
4
  import "./slideout.css";
4
5
 
5
6
  export type SlideoutSize = "small" | "medium" | "large";
@@ -48,7 +49,7 @@ export function Slideout({
48
49
  size = "small",
49
50
  title,
50
51
  }: SlideoutProps) {
51
- const rtl = dir === "rtl";
52
+ const titleId = useId();
52
53
  const hasFooterActions = Boolean(primaryActionLabel || (secondaryAction && secondaryActionLabel));
53
54
 
54
55
  return (
@@ -57,39 +58,43 @@ export function Slideout({
57
58
  "ds-slideout",
58
59
  `ds-slideout--${size}`,
59
60
  `ds-slideout--${breakpoint}`,
60
- rtl && "ds-slideout--rtl",
61
61
  className,
62
62
  )}
63
63
  dir={dir}
64
64
  >
65
- <section aria-label={title} className="ds-slideout__panel" role="dialog">
65
+ <section
66
+ aria-labelledby={title ? titleId : undefined}
67
+ className="ds-slideout__panel"
68
+ data-slot="slideout-panel"
69
+ role={title ? "region" : undefined}
70
+ >
66
71
  <header className="ds-slideout__header">
67
72
  <div className="ds-slideout__heading-wrap">
68
- <h2 className="ds-slideout__title">{title}</h2>
73
+ {title ? <h2 className="ds-slideout__title" id={titleId}>{title}</h2> : null}
69
74
  {headerAction && actionLabel && (
70
- <button className="ds-slideout__text-button" type="button" onClick={onAction}>
75
+ <Button className="ds-slideout__text-button" size="sm" type="button" variant="link" onClick={onAction}>
71
76
  {actionLabel}
72
- </button>
77
+ </Button>
73
78
  )}
74
79
  </div>
75
80
  {closeLabel && (
76
- <button aria-label={closeLabel} className="ds-slideout__close" type="button" onClick={onClose}>
81
+ <Button aria-label={closeLabel} className="ds-slideout__close" size="icon-sm" type="button" variant="ghost" onClick={onClose}>
77
82
  <CloseIcon />
78
- </button>
83
+ </Button>
79
84
  )}
80
85
  </header>
81
86
  <div className="ds-slideout__body">{children}</div>
82
87
  {footer && hasFooterActions && (
83
88
  <footer className="ds-slideout__footer">
84
89
  {primaryActionLabel && (
85
- <button className="ds-slideout__button ds-slideout__button--primary" type="button" onClick={onPrimaryAction}>
90
+ <Button className="ds-slideout__button ds-slideout__button--primary" size="sm" type="button" onClick={onPrimaryAction}>
86
91
  {primaryActionLabel}
87
- </button>
92
+ </Button>
88
93
  )}
89
94
  {secondaryAction && secondaryActionLabel && (
90
- <button className="ds-slideout__button ds-slideout__button--secondary" type="button" onClick={onSecondaryAction}>
95
+ <Button className="ds-slideout__button ds-slideout__button--secondary" size="sm" type="button" variant="secondary" onClick={onSecondaryAction}>
91
96
  {secondaryActionLabel}
92
- </button>
97
+ </Button>
93
98
  )}
94
99
  </footer>
95
100
  )}