banhaten 0.1.0 → 0.1.2

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 (81) hide show
  1. package/README.md +21 -9
  2. package/package.json +8 -2
  3. package/registry/components/accordion.tsx +37 -1
  4. package/registry/components/alert.tsx +14 -28
  5. package/registry/components/attribute.tsx +6 -10
  6. package/registry/components/autocomplete.tsx +637 -0
  7. package/registry/components/avatar.tsx +259 -24
  8. package/registry/components/badge.tsx +97 -35
  9. package/registry/components/button-group.tsx +1 -1
  10. package/registry/components/card.tsx +1 -1
  11. package/registry/components/checkbox.tsx +19 -16
  12. package/registry/components/date-picker-state.ts +253 -0
  13. package/registry/components/date-picker.tsx +115 -158
  14. package/registry/components/expanded/ActivityFeed.tsx +37 -23
  15. package/registry/components/expanded/Banner.tsx +54 -19
  16. package/registry/components/expanded/Breadcrumbs.tsx +10 -38
  17. package/registry/components/expanded/CatalogComponentsShowcase.tsx +11 -16
  18. package/registry/components/expanded/CatalogTag.tsx +4 -11
  19. package/registry/components/expanded/CommandBar.tsx +33 -53
  20. package/registry/components/expanded/EmptyState.tsx +155 -0
  21. package/registry/components/expanded/FileUpload.tsx +362 -59
  22. package/registry/components/expanded/OnboardingStepListItem.tsx +6 -10
  23. package/registry/components/expanded/PageHeader.tsx +2 -11
  24. package/registry/components/expanded/Slideout.tsx +12 -23
  25. package/registry/components/expanded/Steps.tsx +6 -8
  26. package/registry/components/expanded/Table.tsx +18 -40
  27. package/registry/components/expanded/Timeline.tsx +5 -24
  28. package/registry/components/expanded/activityFeed.css +10 -54
  29. package/registry/components/expanded/banner.css +8 -75
  30. package/registry/components/expanded/breadcrumbs.css +1 -1
  31. package/registry/components/expanded/commandBar.css +23 -26
  32. package/registry/components/expanded/divider.css +1 -1
  33. package/registry/components/expanded/emptyState.css +111 -0
  34. package/registry/components/expanded/fileUpload.css +304 -75
  35. package/registry/components/expanded/pageHeader.css +1 -1
  36. package/registry/components/expanded/slideout.css +1 -0
  37. package/registry/components/expanded/steps.css +15 -51
  38. package/registry/components/expanded/table.css +6 -1
  39. package/registry/components/expanded/timeline.css +18 -15
  40. package/registry/components/input-otp.tsx +574 -0
  41. package/registry/components/input.tsx +140 -59
  42. package/registry/components/menu.tsx +470 -80
  43. package/registry/components/pagination.tsx +6 -18
  44. package/registry/components/popover.tsx +840 -0
  45. package/registry/components/radio-card.tsx +25 -31
  46. package/registry/components/select-content.tsx +28 -123
  47. package/registry/components/select.tsx +13 -9
  48. package/registry/components/skeleton.css +57 -0
  49. package/registry/components/skeleton.tsx +482 -0
  50. package/registry/components/social-button.tsx +24 -90
  51. package/registry/components/spinner.tsx +91 -7
  52. package/registry/components/textarea.tsx +21 -36
  53. package/registry/components/toggle.tsx +7 -23
  54. package/registry/components/tooltip.tsx +8 -4
  55. package/registry/examples/attribute-demo.tsx +2 -2
  56. package/registry/examples/autocomplete-demo.tsx +109 -0
  57. package/registry/examples/avatar-demo.tsx +102 -47
  58. package/registry/examples/badge-demo.tsx +16 -0
  59. package/registry/examples/checkbox-demo.tsx +3 -8
  60. package/registry/examples/date-picker-demo.tsx +75 -22
  61. package/registry/examples/expanded/banner-demo.tsx +31 -6
  62. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -0
  63. package/registry/examples/expanded/command-bar-demo.tsx +236 -0
  64. package/registry/examples/expanded/empty-state-demo.tsx +39 -0
  65. package/registry/examples/expanded/file-upload-demo.tsx +60 -0
  66. package/registry/examples/expanded/steps-demo.tsx +11 -0
  67. package/registry/examples/expanded/table-demo.tsx +142 -0
  68. package/registry/examples/input-demo.tsx +1 -1
  69. package/registry/examples/input-otp-demo.tsx +72 -0
  70. package/registry/examples/menu-demo.tsx +101 -88
  71. package/registry/examples/popover-demo.tsx +546 -0
  72. package/registry/examples/progress-demo.tsx +2 -2
  73. package/registry/examples/select-demo.tsx +32 -18
  74. package/registry/examples/skeleton-demo.tsx +56 -0
  75. package/registry/examples/social-button-demo.tsx +33 -33
  76. package/registry/examples/spinner-demo.tsx +59 -0
  77. package/registry/examples/tag-demo.tsx +1 -1
  78. package/registry/examples/textarea-demo.tsx +1 -1
  79. package/registry/index.json +266 -20
  80. package/registry/styles/globals.css +93 -3
  81. package/src/cli/index.js +997 -62
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { Button } from "@/components/ui/button";
2
3
  import "./activityFeed.css";
3
4
 
4
5
  const defaultAvatarSrc = new URL("../assets/activity-feed-avatar.png", import.meta.url).href;
@@ -85,26 +86,48 @@ function Indicator({
85
86
  }
86
87
 
87
88
  function ActionButton({ action }: { action: ActivityFeedAction }) {
88
- const className = classes("ds-activity-feed-item__button", `ds-activity-feed-item__button--${action.variant ?? "secondary"}`);
89
+ const variant = action.variant === "primary" ? "default" : "outline";
89
90
 
90
91
  if (action.href) {
91
92
  return (
92
- <a aria-label={action.ariaLabel} className={className} href={action.href}>
93
- {action.label}
94
- </a>
93
+ <Button
94
+ aria-label={action.ariaLabel}
95
+ asChild
96
+ className="ds-activity-feed-item__button"
97
+ size="sm"
98
+ variant={variant}
99
+ >
100
+ <a
101
+ aria-disabled={action.disabled || undefined}
102
+ href={action.disabled ? undefined : action.href}
103
+ onClick={(event) => {
104
+ if (action.disabled) {
105
+ event.preventDefault();
106
+ return;
107
+ }
108
+
109
+ action.onClick?.();
110
+ }}
111
+ tabIndex={action.disabled ? -1 : undefined}
112
+ >
113
+ {action.label}
114
+ </a>
115
+ </Button>
95
116
  );
96
117
  }
97
118
 
98
119
  return (
99
- <button
120
+ <Button
100
121
  aria-label={action.ariaLabel}
101
- className={className}
122
+ className="ds-activity-feed-item__button"
102
123
  disabled={action.disabled}
103
124
  onClick={action.onClick}
125
+ size="sm"
104
126
  type="button"
127
+ variant={variant}
105
128
  >
106
129
  {action.label}
107
- </button>
130
+ </Button>
108
131
  );
109
132
  }
110
133
 
@@ -135,17 +158,11 @@ export function ActivityFeedItem({
135
158
  const rtl = dir === "rtl";
136
159
  const nodeId = rtl ? (type === "caption" ? "587:18720" : "587:18701") : type === "caption" ? "584:18591" : "584:10411";
137
160
 
138
- const textParts = rtl
139
- ? [
140
- hasLink && link && <span className="ds-activity-feed-item__link" key="link">{link}</span>,
141
- hasSupportText && supportText && <span className="ds-activity-feed-item__support" key="support">{supportText}</span>,
142
- label && <strong key="label">{label}</strong>,
143
- ]
144
- : [
145
- label && <strong key="label">{label}</strong>,
146
- hasSupportText && supportText && <span className="ds-activity-feed-item__support" key="support">{supportText}</span>,
147
- hasLink && link && <span className="ds-activity-feed-item__link" key="link">{link}</span>,
148
- ];
161
+ const textParts = [
162
+ label && <strong key="label">{label}</strong>,
163
+ hasSupportText && supportText && <span className="ds-activity-feed-item__support" key="support">{supportText}</span>,
164
+ hasLink && link && <span className="ds-activity-feed-item__link" key="link">{link}</span>,
165
+ ];
149
166
 
150
167
  return (
151
168
  <article
@@ -158,14 +175,13 @@ export function ActivityFeedItem({
158
175
  data-node-id={nodeId}
159
176
  dir={rtl ? "rtl" : "ltr"}
160
177
  >
161
- {!rtl && <Indicator avatarAlt={avatarAlt} avatarSrc={avatarSrc} rtl={rtl} showLine={showLine} type={type} />}
178
+ <Indicator avatarAlt={avatarAlt} avatarSrc={avatarSrc} rtl={rtl} showLine={showLine} type={type} />
162
179
 
163
180
  <div className="ds-activity-feed-item__content">
164
181
  <div className="ds-activity-feed-item__container">
165
182
  <div className="ds-activity-feed-item__topline">
166
- {rtl && hasStatus && <StatusDot />}
167
183
  <div className="ds-activity-feed-item__text">{textParts}</div>
168
- {!rtl && hasStatus && <StatusDot />}
184
+ {hasStatus && <StatusDot />}
169
185
  </div>
170
186
 
171
187
  {type === "slot" ? (
@@ -191,8 +207,6 @@ export function ActivityFeedItem({
191
207
 
192
208
  {showPaddingBottom && <div className="ds-activity-feed-item__bottom-space" aria-hidden="true" />}
193
209
  </div>
194
-
195
- {rtl && <Indicator avatarAlt={avatarAlt} avatarSrc={avatarSrc} rtl={rtl} showLine={showLine} type={type} />}
196
210
  </article>
197
211
  );
198
212
  }
@@ -1,4 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
+ import { MegaphoneIcon as LucideMegaphoneIcon, XIcon } from "lucide-react";
3
+
4
+ import { Button, type ButtonProps } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+
2
7
  import "./banner.css";
3
8
 
4
9
  export type BannerType = "slim" | "single-action-inline" | "single-action" | "input";
@@ -30,19 +35,17 @@ export type BannerProps = {
30
35
  };
31
36
 
32
37
  function MegaphoneIcon() {
33
- return (
34
- <svg aria-hidden="true" className="ds-banner__icon" viewBox="0 0 24 24">
35
- <path d="M3 10.5v3a2.5 2.5 0 0 0 2.5 2.5h.63l1.15 3.44A1.5 1.5 0 0 0 8.7 20.5h1.1a1 1 0 0 0 .95-1.32L9.7 16H11l7.24 3.1A1.25 1.25 0 0 0 20 17.95V6.05a1.25 1.25 0 0 0-1.76-1.14L11 8H5.5A2.5 2.5 0 0 0 3 10.5Zm15 6.69-6-2.58V9.39l6-2.58v10.38ZM5 10.5A.5.5 0 0 1 5.5 10H10v4H5.5a.5.5 0 0 1-.5-.5v-3Z" />
36
- </svg>
37
- );
38
+ return <LucideMegaphoneIcon aria-hidden="true" className="ds-banner__icon" strokeWidth={2.1} />;
38
39
  }
39
40
 
40
41
  function CloseIcon() {
41
- return (
42
- <svg aria-hidden="true" className="ds-banner__close-icon" viewBox="0 0 24 24">
43
- <path d="m12 10.59 5.3-5.3 1.41 1.42-5.3 5.29 5.3 5.3-1.41 1.41-5.3-5.3-5.29 5.3-1.42-1.41 5.3-5.3-5.3-5.29 1.42-1.42 5.29 5.3Z" />
44
- </svg>
45
- );
42
+ return <XIcon aria-hidden="true" className="ds-banner__close-icon" strokeWidth={2.25} />;
43
+ }
44
+
45
+ function getBannerActionVariant(color: BannerColor): ButtonProps["variant"] {
46
+ if (color === "brand-light") return "default";
47
+ if (color === "grey") return "secondary";
48
+ return "white";
46
49
  }
47
50
 
48
51
  function cx(...classes: Array<string | false | undefined>) {
@@ -75,10 +78,18 @@ export function Banner({
75
78
  const isInlineAction = type === "slim" || type === "single-action-inline";
76
79
  const inlineActionLabel = type === "single-action-inline" ? actionLabel : linkLabel;
77
80
  const hasInlineAction = isInlineAction && (showInlineAction ?? true) && Boolean(inlineActionLabel);
81
+ const actionVariant = getBannerActionVariant(color);
78
82
  const closeButton = (
79
- <button className="ds-banner__close" onClick={onClose} type="button" aria-label={closeLabel}>
83
+ <Button
84
+ aria-label={closeLabel ?? "Dismiss banner"}
85
+ className="ds-banner__close"
86
+ onClick={onClose}
87
+ size="icon-xs"
88
+ type="button"
89
+ variant="ghost"
90
+ >
80
91
  <CloseIcon />
81
- </button>
92
+ </Button>
82
93
  );
83
94
 
84
95
  return (
@@ -110,18 +121,30 @@ export function Banner({
110
121
  </div>
111
122
 
112
123
  {hasInlineAction && (
113
- <button className="ds-banner__link" onClick={onAction} type="button">
124
+ <Button
125
+ className="ds-banner__link"
126
+ onClick={onAction}
127
+ size="sm"
128
+ type="button"
129
+ variant={type === "single-action-inline" ? actionVariant : "link"}
130
+ >
114
131
  {inlineActionLabel}
115
- </button>
132
+ </Button>
116
133
  )}
117
134
  </div>
118
135
 
119
136
  {type === "single-action" && (
120
137
  <div className="ds-banner__tail">
121
138
  {actionLabel && (
122
- <button className="ds-banner__action" onClick={onAction} type="button">
139
+ <Button
140
+ className="ds-banner__action"
141
+ onClick={onAction}
142
+ size="sm"
143
+ type="button"
144
+ variant={actionVariant}
145
+ >
123
146
  {actionLabel}
124
- </button>
147
+ </Button>
125
148
  )}
126
149
  {closeButton}
127
150
  </div>
@@ -129,11 +152,23 @@ export function Banner({
129
152
 
130
153
  {type === "input" && (
131
154
  <div className="ds-banner__tail ds-banner__tail--input">
132
- <input aria-label={inputAriaLabel} className="ds-banner__input" placeholder={inputPlaceholder} type="text" />
155
+ <Input
156
+ aria-label={inputAriaLabel}
157
+ className="ds-banner__input"
158
+ hasInformationIcon={false}
159
+ placeholder={inputPlaceholder}
160
+ size="md"
161
+ />
133
162
  {actionLabel && (
134
- <button className="ds-banner__action" onClick={onAction} type="button">
163
+ <Button
164
+ className="ds-banner__action"
165
+ onClick={onAction}
166
+ size="sm"
167
+ type="button"
168
+ variant={actionVariant}
169
+ >
135
170
  {actionLabel}
136
- </button>
171
+ </Button>
137
172
  )}
138
173
  {closeButton}
139
174
  </div>
@@ -1,4 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
+ import {
3
+ ChevronRightIcon,
4
+ EllipsisIcon,
5
+ FolderIcon,
6
+ HomeIcon,
7
+ } from "lucide-react";
2
8
  import "./breadcrumbs.css";
3
9
 
4
10
  export type BreadcrumbSeparator = "slash" | "chevron";
@@ -106,51 +112,17 @@ function Separator({ separator }: { separator: BreadcrumbSeparator }) {
106
112
  }
107
113
 
108
114
  export function BreadcrumbHomeIcon() {
109
- return (
110
- <svg aria-hidden="true" viewBox="0 0 20 20">
111
- <path
112
- d="M3.5 8.7 10 3.4l6.5 5.3v7.1a.9.9 0 0 1-.9.9h-3.2v-4.9H7.6v4.9H4.4a.9.9 0 0 1-.9-.9V8.7Z"
113
- fill="none"
114
- stroke="currentColor"
115
- strokeLinejoin="round"
116
- strokeWidth="1.7"
117
- />
118
- </svg>
119
- );
115
+ return <HomeIcon aria-hidden="true" strokeWidth={1.9} />;
120
116
  }
121
117
 
122
118
  export function BreadcrumbFolderIcon() {
123
- return (
124
- <svg aria-hidden="true" viewBox="0 0 20 20">
125
- <path
126
- d="M2.8 5.7c0-.9.7-1.6 1.6-1.6h3.8l1.5 1.8h5.9c.9 0 1.6.7 1.6 1.6v6.8c0 .9-.7 1.6-1.6 1.6H4.4c-.9 0-1.6-.7-1.6-1.6V5.7Z"
127
- fill="none"
128
- stroke="currentColor"
129
- strokeLinejoin="round"
130
- strokeWidth="1.7"
131
- />
132
- </svg>
133
- );
119
+ return <FolderIcon aria-hidden="true" strokeWidth={1.9} />;
134
120
  }
135
121
 
136
122
  function ChevronIcon() {
137
- return (
138
- <svg aria-hidden="true" viewBox="0 0 16 16">
139
- <path d="m6 3.5 4 4.5-4 4.5" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" />
140
- </svg>
141
- );
123
+ return <ChevronRightIcon aria-hidden="true" strokeWidth={2} />;
142
124
  }
143
125
 
144
126
  function OverflowIcon() {
145
- return (
146
- <svg aria-hidden="true" viewBox="0 0 20 20">
147
- <path
148
- d="M5.2 10h.1M10 10h.1M14.8 10h.1"
149
- fill="none"
150
- stroke="currentColor"
151
- strokeLinecap="round"
152
- strokeWidth="2.4"
153
- />
154
- </svg>
155
- );
127
+ return <EllipsisIcon aria-hidden="true" strokeWidth={2.25} />;
156
128
  }
@@ -36,17 +36,12 @@ const breadcrumbAriaLabel = "Catalog breadcrumbs";
36
36
  const rtlBreadcrumbAriaLabel = "\u0627\u0644\u0645\u0633\u0627\u0631";
37
37
 
38
38
  const uploadFiles: FileUploadFile[] = [
39
- { id: "tokens", name: "semantic-tokens.json", sizeLabel: "4 MB", versionLabel: "v3.0", status: "uploaded" },
40
- { id: "components", name: "component-inventory.csv", sizeLabel: "18 MB", versionLabel: "v2.4", status: "uploading" },
41
- { id: "icons", name: "icons-shortlist.pdf", sizeLabel: "7 MB", status: "error", errorLabel: "Needs retry" },
39
+ { id: "uploaded", name: "file-name.pdf", sizeLabel: "32mb", versionLabel: "v1.2.2", status: "uploaded" },
40
+ { id: "uploading", name: "file-name.pdf", sizeLabel: "32mb", versionLabel: "v1.2.2", status: "uploading" },
41
+ { id: "progress", name: "file-name.pdf", sizeLabel: "32 KB", progress: 40, status: "uploading-progress" },
42
+ { id: "error", name: "file-name.pdf", sizeLabel: "32mb", versionLabel: "v1.2.2", status: "error", errorLabel: "Upload failed" },
42
43
  ];
43
44
 
44
- const fileUploadCopy = {
45
- browseLabel: "Browse",
46
- helperText: "SVG, PNG, PDF, or CSV up to 50 MB",
47
- prompt: "Drag and drop files here, or",
48
- };
49
-
50
45
  const projectRows: ProjectRow[] = [
51
46
  { id: "1", name: "Component audit", leader: "Maya Chen", progress: 84, status: "On track", deadline: "Jun 12" },
52
47
  { id: "2", name: "Icon decision", leader: "Ahmed Galal", progress: 64, status: "Review", deadline: "Jun 18" },
@@ -121,7 +116,7 @@ export function CatalogComponentsShowcase() {
121
116
  aria-label={rtlBreadcrumbAriaLabel}
122
117
  dir="rtl"
123
118
  items={rtlBreadcrumbItems}
124
- overflowLabel="\u0627\u0644\u0645\u0632\u064a\u062f"
119
+ overflowLabel={"\u0627\u0644\u0645\u0632\u064a\u062f"}
125
120
  separator="chevron"
126
121
  style="raised"
127
122
  />
@@ -134,7 +129,7 @@ export function CatalogComponentsShowcase() {
134
129
  <CatalogDivider borderStyle="dotted" />
135
130
  <CatalogContentDivider label="Text" />
136
131
  <CatalogContentDivider actionLabel="Button" onAction={() => undefined} />
137
- <CatalogContentDivider dir="rtl" label="\u0646\u0635" borderStyle="dotted" />
132
+ <CatalogContentDivider dir="rtl" label={"\u0646\u0635"} borderStyle="dotted" />
138
133
  </div>
139
134
  </ShowcaseCard>
140
135
 
@@ -162,21 +157,21 @@ export function CatalogComponentsShowcase() {
162
157
  large
163
158
  </CatalogTag>
164
159
  <CatalogTag
165
- avatar="\u0645"
166
- closeLabel="\u0625\u0632\u0627\u0644\u0629 \u0648\u0633\u0645"
160
+ avatar={"\u0645"}
161
+ closeLabel={"\u0625\u0632\u0627\u0644\u0629 \u0648\u0633\u0645"}
167
162
  dir="rtl"
168
163
  type="avatar"
169
164
  showCloseButton
170
165
  >
171
- \u0645\u0644\u0635\u0642
166
+ {"\u0645\u0644\u0635\u0642"}
172
167
  </CatalogTag>
173
168
  </div>
174
169
  </ShowcaseCard>
175
170
 
176
171
  <ShowcaseCard title="File Upload" description="Large and small queued-file upload states.">
177
172
  <div className="catalog-showcase__upload-grid">
178
- <FileUpload {...fileUploadCopy} files={uploadFiles} multiple />
179
- <FileUpload {...fileUploadCopy} files={uploadFiles.slice(0, 1)} size="sm" />
173
+ <FileUpload files={uploadFiles} multiple />
174
+ <FileUpload files={uploadFiles.slice(0, 4)} size="sm" />
180
175
  </div>
181
176
  </ShowcaseCard>
182
177
 
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { PlusIcon as LucidePlusIcon, XIcon } from "lucide-react";
2
3
  import "./tag.css";
3
4
 
4
5
  export type TagType = "simple" | "dot" | "flag" | "avatar" | "icon";
@@ -61,7 +62,7 @@ export function CatalogTag({
61
62
  <span aria-hidden="true" className="ds-tag__leading">
62
63
  {type === "dot" && <span className="ds-tag__dot" />}
63
64
  {type === "flag" && (flag ?? <span className="ds-tag__flag" />)}
64
- {type === "avatar" && avatar}
65
+ {type === "avatar" && <span className="ds-tag__avatar">{avatar}</span>}
65
66
  {type === "icon" && (icon ?? <PlusIcon />)}
66
67
  </span>
67
68
  )}
@@ -76,17 +77,9 @@ export function CatalogTag({
76
77
  }
77
78
 
78
79
  function PlusIcon() {
79
- return (
80
- <svg aria-hidden="true" viewBox="0 0 16 16">
81
- <path d="M8 3.5v9M3.5 8h9" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
82
- </svg>
83
- );
80
+ return <LucidePlusIcon aria-hidden="true" strokeWidth={2.25} />;
84
81
  }
85
82
 
86
83
  function CloseIcon() {
87
- return (
88
- <svg aria-hidden="true" viewBox="0 0 16 16">
89
- <path d="m4.5 4.5 7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.6" />
90
- </svg>
91
- );
84
+ return <XIcon aria-hidden="true" strokeWidth={2.25} />;
92
85
  }
@@ -1,5 +1,16 @@
1
1
  import "./commandBar.css";
2
2
 
3
+ import {
4
+ ArrowUpDownIcon as LucideArrowUpDownIcon,
5
+ BookOpenIcon,
6
+ CommandIcon,
7
+ CornerDownLeftIcon,
8
+ SearchIcon as LucideSearchIcon,
9
+ XIcon,
10
+ } from "lucide-react";
11
+
12
+ import { Button } from "@/components/ui/button";
13
+
3
14
  export type CommandBarType = "default" | "recent" | "results" | "no-result";
4
15
  export type CommandBarBreakpoint = "desktop" | "mobile";
5
16
 
@@ -34,7 +45,7 @@ type CommandRow = {
34
45
  readonly visual: "avatar" | "icon";
35
46
  };
36
47
 
37
- const shortcutText = "\u2318K";
48
+ const shortcutLabel = "Command K";
38
49
 
39
50
  const ltrCopy: CommandBarCopy = {
40
51
  clear: "Clear search",
@@ -248,9 +259,15 @@ function FilterSection({ copy }: { copy: CommandBarCopy }) {
248
259
  <SectionHeading>{copy.searchFor}</SectionHeading>
249
260
  <div className="ds-command-bar__tags" aria-label={copy.searchFor}>
250
261
  {copy.tags.map((tag) => (
251
- <button className="ds-command-bar__tag" key={tag} type="button">
262
+ <Button
263
+ className="ds-command-bar__tag"
264
+ key={tag}
265
+ size="sm"
266
+ type="button"
267
+ variant="secondary"
268
+ >
252
269
  {tag}
253
- </button>
270
+ </Button>
254
271
  ))}
255
272
  </div>
256
273
  </div>
@@ -326,7 +343,14 @@ function CommandFooter({ copy }: { copy: CommandBarCopy }) {
326
343
  }
327
344
 
328
345
  function Shortcut() {
329
- return <kbd className="ds-command-bar__shortcut">{shortcutText}</kbd>;
346
+ return (
347
+ <kbd aria-label={shortcutLabel} className="ds-command-bar__shortcut">
348
+ <CommandIcon aria-hidden="true" className="ds-command-bar__shortcut-icon" />
349
+ <span aria-hidden="true" className="ds-command-bar__shortcut-key">
350
+ K
351
+ </span>
352
+ </kbd>
353
+ );
330
354
  }
331
355
 
332
356
  function IconWrap() {
@@ -342,65 +366,21 @@ function Avatar() {
342
366
  }
343
367
 
344
368
  function SearchIcon({ size = 24 }: { size?: number }) {
345
- return (
346
- <svg aria-hidden="true" fill="none" height={size} viewBox="0 0 24 24" width={size}>
347
- <path
348
- d="M10.75 18.5a7.75 7.75 0 1 1 5.48-2.27l3.02 3.02"
349
- stroke="currentColor"
350
- strokeLinecap="round"
351
- strokeLinejoin="round"
352
- strokeWidth="2"
353
- />
354
- </svg>
355
- );
369
+ return <LucideSearchIcon aria-hidden="true" height={size} strokeWidth={2} width={size} />;
356
370
  }
357
371
 
358
372
  function CloseIcon() {
359
- return (
360
- <svg aria-hidden="true" fill="none" height="20" viewBox="0 0 20 20" width="20">
361
- <path d="m5 5 10 10M15 5 5 15" stroke="currentColor" strokeLinecap="round" strokeWidth="1.8" />
362
- </svg>
363
- );
373
+ return <XIcon aria-hidden="true" height="20" strokeWidth={2.2} width="20" />;
364
374
  }
365
375
 
366
376
  function BookIcon() {
367
- return (
368
- <svg aria-hidden="true" fill="none" height="20" viewBox="0 0 20 20" width="20">
369
- <path
370
- d="M5.5 3.5h8.2c.9 0 1.6.7 1.6 1.6v10.4H6.2a1.5 1.5 0 0 0 0 3h9.1M5.5 3.5a2 2 0 0 0-2 2v11a2 2 0 0 1 2-2m0-11v11m2.4-7.8h4.8"
371
- stroke="currentColor"
372
- strokeLinecap="round"
373
- strokeLinejoin="round"
374
- strokeWidth="1.6"
375
- />
376
- </svg>
377
- );
377
+ return <BookOpenIcon aria-hidden="true" height="20" strokeWidth={1.8} width="20" />;
378
378
  }
379
379
 
380
380
  function ArrowUpDownIcon() {
381
- return (
382
- <svg aria-hidden="true" fill="none" height="16" viewBox="0 0 16 16" width="16">
383
- <path
384
- d="M5 2v12m0 0 2.5-2.5M5 14l-2.5-2.5M11 14V2m0 0 2.5 2.5M11 2 8.5 4.5"
385
- stroke="currentColor"
386
- strokeLinecap="round"
387
- strokeLinejoin="round"
388
- strokeWidth="1.45"
389
- />
390
- </svg>
391
- );
381
+ return <LucideArrowUpDownIcon aria-hidden="true" height="16" strokeWidth={1.9} width="16" />;
392
382
  }
393
383
 
394
384
  function EnterIcon() {
395
- return (
396
- <svg aria-hidden="true" fill="none" height="16" viewBox="0 0 16 16" width="16">
397
- <path
398
- d="M12.5 3.5v3.25a3 3 0 0 1-3 3H3.75m0 0L6.2 7.3M3.75 9.75 6.2 12.2"
399
- stroke="currentColor"
400
- strokeLinecap="round"
401
- strokeLinejoin="round"
402
- strokeWidth="1.45"
403
- />
404
- </svg>
405
- );
385
+ return <CornerDownLeftIcon aria-hidden="true" height="16" strokeWidth={1.9} width="16" />;
406
386
  }
@@ -0,0 +1,155 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { SearchIcon } from "lucide-react";
3
+
4
+ import { Button, type ButtonProps } from "@/components/ui/button";
5
+
6
+ import "./emptyState.css";
7
+
8
+ export type EmptyStateAlign = "center" | "start";
9
+ export type EmptyStateSize = "default" | "compact";
10
+
11
+ export type EmptyStateAction = {
12
+ ariaLabel?: string;
13
+ disabled?: boolean;
14
+ href?: string;
15
+ icon?: ReactNode;
16
+ label: ReactNode;
17
+ onAction?: () => void;
18
+ rel?: string;
19
+ size?: ButtonProps["size"];
20
+ target?: string;
21
+ type?: "button" | "submit" | "reset";
22
+ variant?: ButtonProps["variant"];
23
+ };
24
+
25
+ export type EmptyStateProps = Omit<ComponentPropsWithoutRef<"section">, "title"> & {
26
+ actions?: EmptyStateAction[];
27
+ align?: EmptyStateAlign;
28
+ description?: ReactNode;
29
+ dir?: "ltr" | "rtl" | "auto";
30
+ icon?: ReactNode;
31
+ iconLabel?: string;
32
+ size?: EmptyStateSize;
33
+ title: ReactNode;
34
+ };
35
+
36
+ export function EmptyState({
37
+ actions = [],
38
+ align = "center",
39
+ className,
40
+ description,
41
+ dir = "ltr",
42
+ icon,
43
+ iconLabel,
44
+ size = "default",
45
+ title,
46
+ ...props
47
+ }: EmptyStateProps) {
48
+ const hasActions = actions.length > 0;
49
+
50
+ return (
51
+ <section
52
+ className={cx(
53
+ "ds-empty-state",
54
+ `ds-empty-state--${align}`,
55
+ `ds-empty-state--${size}`,
56
+ className,
57
+ )}
58
+ dir={dir}
59
+ {...props}
60
+ >
61
+ <span
62
+ aria-hidden={iconLabel ? undefined : true}
63
+ aria-label={iconLabel}
64
+ className="ds-empty-state__icon-wrap"
65
+ role={iconLabel ? "img" : undefined}
66
+ >
67
+ <span className="ds-empty-state__icon-media">
68
+ {icon ?? <SearchIcon />}
69
+ </span>
70
+ </span>
71
+
72
+ <div className="ds-empty-state__copy">
73
+ <h3 className="ds-empty-state__title" dir="auto">
74
+ {title}
75
+ </h3>
76
+ {description && (
77
+ <p className="ds-empty-state__description" dir="auto">
78
+ {description}
79
+ </p>
80
+ )}
81
+ </div>
82
+
83
+ {hasActions && (
84
+ <div className="ds-empty-state__actions">
85
+ {actions.map((action, index) => (
86
+ <EmptyStateActionButton
87
+ action={action}
88
+ actionCount={actions.length}
89
+ index={index}
90
+ key={index}
91
+ />
92
+ ))}
93
+ </div>
94
+ )}
95
+ </section>
96
+ );
97
+ }
98
+
99
+ function EmptyStateActionButton({
100
+ action,
101
+ actionCount,
102
+ index,
103
+ }: {
104
+ action: EmptyStateAction;
105
+ actionCount: number;
106
+ index: number;
107
+ }) {
108
+ const variant =
109
+ action.variant ?? (actionCount > 1 && index === 0 ? "secondary" : "default");
110
+ const content = (
111
+ <>
112
+ {action.icon}
113
+ {action.label}
114
+ </>
115
+ );
116
+
117
+ if (action.href) {
118
+ const isDisabled = Boolean(action.disabled);
119
+
120
+ return (
121
+ <Button
122
+ aria-label={action.ariaLabel}
123
+ asChild
124
+ size={action.size ?? "default"}
125
+ variant={variant}
126
+ >
127
+ <a
128
+ aria-disabled={isDisabled || undefined}
129
+ href={isDisabled ? undefined : action.href}
130
+ rel={action.rel ?? (action.target === "_blank" ? "noreferrer" : undefined)}
131
+ target={action.target}
132
+ >
133
+ {content}
134
+ </a>
135
+ </Button>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <Button
141
+ aria-label={action.ariaLabel}
142
+ disabled={action.disabled}
143
+ onClick={action.onAction}
144
+ size={action.size ?? "default"}
145
+ type={action.type ?? "button"}
146
+ variant={variant}
147
+ >
148
+ {content}
149
+ </Button>
150
+ );
151
+ }
152
+
153
+ function cx(...classes: Array<string | false | null | undefined>) {
154
+ return classes.filter(Boolean).join(" ");
155
+ }