raft-ui 0.0.1

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 (134) hide show
  1. package/README.md +24 -0
  2. package/dist/cn.d.mts +2 -0
  3. package/dist/cn.mjs +2 -0
  4. package/dist/index.d.mts +2710 -0
  5. package/dist/index.mjs +4821 -0
  6. package/dist/label-DWq4ia5I.mjs +115 -0
  7. package/dist/styled-props-B3SNekJg.d.mts +16 -0
  8. package/dist/wip.d.mts +1238 -0
  9. package/dist/wip.mjs +1416 -0
  10. package/package.json +70 -0
  11. package/src/components/avatar/avatar.tsx +375 -0
  12. package/src/components/avatar/index.ts +17 -0
  13. package/src/components/badge/badge.tsx +393 -0
  14. package/src/components/badge/index.ts +2 -0
  15. package/src/components/banner/banner.tsx +269 -0
  16. package/src/components/banner/index.ts +8 -0
  17. package/src/components/bottom-sheet/bottom-sheet.tsx +152 -0
  18. package/src/components/bottom-sheet/index.ts +22 -0
  19. package/src/components/button/button.tsx +497 -0
  20. package/src/components/button/index.ts +2 -0
  21. package/src/components/checkbox/checkbox-indicator.tsx +128 -0
  22. package/src/components/checkbox/checkbox.tsx +350 -0
  23. package/src/components/checkbox/index.ts +2 -0
  24. package/src/components/combobox/combobox.tsx +603 -0
  25. package/src/components/combobox/index.ts +52 -0
  26. package/src/components/context-menu/context-menu.tsx +380 -0
  27. package/src/components/context-menu/index.ts +34 -0
  28. package/src/components/conversation-preview-card/conversation-preview-card.tsx +336 -0
  29. package/src/components/conversation-preview-card/index.ts +32 -0
  30. package/src/components/description-list/description-list.tsx +116 -0
  31. package/src/components/description-list/index.ts +14 -0
  32. package/src/components/dropdown-menu/dropdown-menu.tsx +405 -0
  33. package/src/components/dropdown-menu/index.ts +38 -0
  34. package/src/components/empty-state/empty-state.tsx +177 -0
  35. package/src/components/empty-state/index.ts +16 -0
  36. package/src/components/field/field.tsx +125 -0
  37. package/src/components/field/index.ts +18 -0
  38. package/src/components/inline-code/index.ts +2 -0
  39. package/src/components/inline-code/inline-code.tsx +33 -0
  40. package/src/components/input/index.ts +2 -0
  41. package/src/components/input/input.tsx +76 -0
  42. package/src/components/input-group/index.ts +7 -0
  43. package/src/components/input-group/input-group.tsx +215 -0
  44. package/src/components/kbd/index.ts +2 -0
  45. package/src/components/kbd/kbd.tsx +43 -0
  46. package/src/components/label/index.ts +7 -0
  47. package/src/components/label/label.tsx +99 -0
  48. package/src/components/lightbox/index.ts +24 -0
  49. package/src/components/lightbox/lightbox.tsx +245 -0
  50. package/src/components/list-item/index.ts +20 -0
  51. package/src/components/list-item/list-item.tsx +263 -0
  52. package/src/components/media-list-item/index.ts +20 -0
  53. package/src/components/media-list-item/media-list-item.tsx +169 -0
  54. package/src/components/notification-center/index.ts +39 -0
  55. package/src/components/notification-center/notification-center.tsx +558 -0
  56. package/src/components/panel-header/index.ts +22 -0
  57. package/src/components/panel-header/panel-header.tsx +199 -0
  58. package/src/components/popover/index.ts +30 -0
  59. package/src/components/popover/popover.tsx +248 -0
  60. package/src/components/preview-shell/index.ts +2 -0
  61. package/src/components/preview-shell/preview-shell.tsx +51 -0
  62. package/src/components/quoted-message-card/index.ts +36 -0
  63. package/src/components/quoted-message-card/quoted-message-card.tsx +391 -0
  64. package/src/components/radio-group/index.ts +6 -0
  65. package/src/components/radio-group/radio-group.tsx +119 -0
  66. package/src/components/section-header/index.ts +16 -0
  67. package/src/components/section-header/section-header.tsx +117 -0
  68. package/src/components/section-label/index.ts +2 -0
  69. package/src/components/section-label/section-label.tsx +38 -0
  70. package/src/components/segmented-control/index.ts +12 -0
  71. package/src/components/segmented-control/segmented-control.tsx +129 -0
  72. package/src/components/select/index.ts +40 -0
  73. package/src/components/select/select.tsx +382 -0
  74. package/src/components/spinner/index.ts +2 -0
  75. package/src/components/spinner/spinner.tsx +228 -0
  76. package/src/components/status/index.ts +2 -0
  77. package/src/components/status/status.tsx +152 -0
  78. package/src/components/tabs/index.ts +24 -0
  79. package/src/components/tabs/tabs.tsx +417 -0
  80. package/src/components/text/index.ts +2 -0
  81. package/src/components/text/text.tsx +130 -0
  82. package/src/components/textarea/index.ts +2 -0
  83. package/src/components/textarea/textarea.tsx +136 -0
  84. package/src/components/toggle-group/index.ts +12 -0
  85. package/src/components/toggle-group/toggle-group.tsx +111 -0
  86. package/src/components/tooltip/index.ts +14 -0
  87. package/src/components/tooltip/tooltip.tsx +186 -0
  88. package/src/fonts/brutal.css +2 -0
  89. package/src/index.ts +404 -0
  90. package/src/legacy/AttentionDot.tsx +28 -0
  91. package/src/legacy/AvatarListRow.tsx +87 -0
  92. package/src/legacy/AvatarSlot.tsx +157 -0
  93. package/src/legacy/Badge.tsx +95 -0
  94. package/src/legacy/Banner.tsx +74 -0
  95. package/src/legacy/BottomSheet.tsx +76 -0
  96. package/src/legacy/CheckMarker.tsx +72 -0
  97. package/src/legacy/Checkbox.tsx +81 -0
  98. package/src/legacy/ConversationPreviewCard.tsx +169 -0
  99. package/src/legacy/DismissBackdrop.tsx +68 -0
  100. package/src/legacy/EmptyState.tsx +63 -0
  101. package/src/legacy/ExperimentalBadge.tsx +19 -0
  102. package/src/legacy/FormField.tsx +106 -0
  103. package/src/legacy/KeyValueRow.tsx +67 -0
  104. package/src/legacy/Lightbox.tsx +119 -0
  105. package/src/legacy/NotificationCenter.tsx +217 -0
  106. package/src/legacy/PanelHeader.tsx +94 -0
  107. package/src/legacy/PreviewShell.tsx +40 -0
  108. package/src/legacy/QuotedMessageCard.tsx +181 -0
  109. package/src/legacy/ReorderablePanelTabs.tsx +151 -0
  110. package/src/legacy/SandboxedPreviewFrame.tsx +59 -0
  111. package/src/legacy/SearchInput.tsx +23 -0
  112. package/src/legacy/SectionEyebrow.tsx +60 -0
  113. package/src/legacy/SectionHeader.tsx +68 -0
  114. package/src/legacy/SegmentedControl.tsx +74 -0
  115. package/src/legacy/SlugInput.tsx +29 -0
  116. package/src/legacy/Spinner.tsx +89 -0
  117. package/src/legacy/StatusDot.tsx +51 -0
  118. package/src/legacy/SurfaceListItem.tsx +32 -0
  119. package/src/legacy/Textarea.tsx +137 -0
  120. package/src/legacy/button.tsx +100 -0
  121. package/src/legacy/context-menu-divider.tsx +12 -0
  122. package/src/legacy/menu-item.tsx +66 -0
  123. package/src/legacy/selection-popover.tsx +212 -0
  124. package/src/legacy/server-switcher-menu.tsx +203 -0
  125. package/src/legacy/source.css +240 -0
  126. package/src/lib/cn.ts +1 -0
  127. package/src/lib/styled-props.ts +11 -0
  128. package/src/lib/theme/theme-context.ts +28 -0
  129. package/src/lib/theme/theme-provider.tsx +181 -0
  130. package/src/lib/theme/use-theme.ts +12 -0
  131. package/src/lib/tv.ts +29 -0
  132. package/src/lib/without-children.ts +6 -0
  133. package/src/styles.css +467 -0
  134. package/src/wip.ts +255 -0
@@ -0,0 +1,405 @@
1
+ import type { ComponentPropsWithRef } from "react";
2
+ import { Menu as BaseMenu } from "@base-ui/react/menu";
3
+ import { tv } from "../../lib/tv.ts";
4
+ import { Badge, type BadgeProps } from "../badge/index.ts";
5
+ import { Kbd, type KbdProps } from "../kbd/index.ts";
6
+ import type { StyledProps } from "../../lib/styled-props.ts";
7
+ import { useThemeFamily } from "../../lib/theme/use-theme.ts";
8
+
9
+ const dropdownMenu = tv({
10
+ slots: {
11
+ trigger: ["select-none"],
12
+ positioner: ["isolate z-50 outline-none"],
13
+ content: [
14
+ "max-h-[var(--available-height)] min-w-48 overflow-hidden bg-layer-panel",
15
+ "outline-none",
16
+ ],
17
+ item: [
18
+ "flex w-full cursor-default select-none items-center gap-2 px-3 py-2 [@media(max-height:600px)]:py-1",
19
+ "text-left text-sm outline-none transition-colors",
20
+ ],
21
+ radioItem: [
22
+ "flex w-full cursor-default select-none items-center gap-2 px-3",
23
+ "text-left outline-none transition-colors",
24
+ ],
25
+ submenuTrigger: [
26
+ "flex w-full cursor-default select-none items-center gap-2 px-3 py-2 [@media(max-height:600px)]:py-1",
27
+ "text-left text-sm outline-none transition-colors",
28
+ ],
29
+ label: [],
30
+ shortcut: ["ml-auto"],
31
+ itemCount: ["ml-auto tabular-nums"],
32
+ separator: ["border-t-2"],
33
+ indicator: ["pointer-events-none ml-auto flex size-3 shrink-0 items-center justify-center"],
34
+ },
35
+ variants: {
36
+ theme: {
37
+ brutal: {
38
+ trigger: [
39
+ "data-[popup-open]:translate-x-0 data-[popup-open]:translate-y-0",
40
+ "data-[popup-open]:hover:translate-x-0 data-[popup-open]:hover:translate-y-0",
41
+ ],
42
+ content: ["border-2 border-black bg-white shadow-lg"],
43
+ item: [
44
+ "py-1.5 font-sans text-sm font-semibold leading-6 text-black hover:bg-brutal-yellow data-[highlighted]:bg-brutal-yellow",
45
+ "data-[disabled]:pointer-events-none data-[disabled]:text-black/30",
46
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-2 [&_svg_*]:stroke-2 [&_svg:not([class*='size-'])]:size-3.5",
47
+ ],
48
+ radioItem: [
49
+ "h-9 overflow-hidden font-sans text-sm font-semibold leading-6 text-black",
50
+ "border-b border-black/10 last:border-b-0",
51
+ "hover:bg-brutal-yellow/30 data-[highlighted]:bg-brutal-yellow/30",
52
+ "data-[disabled]:pointer-events-none data-[disabled]:text-black/30",
53
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-2 [&_svg_*]:stroke-2 [&_svg:not([class*='size-'])]:size-3.5",
54
+ ],
55
+ submenuTrigger: [
56
+ "py-1.5 font-sans text-sm font-semibold leading-6 text-black hover:bg-brutal-yellow data-[highlighted]:bg-brutal-yellow data-[popup-open]:bg-brutal-yellow",
57
+ "data-[disabled]:pointer-events-none data-[disabled]:text-black/30",
58
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-2 [&_svg_*]:stroke-2 [&_svg:not([class*='size-'])]:size-3.5",
59
+ ],
60
+ label: [
61
+ "border-b border-black px-3 py-2",
62
+ "font-sans text-[10px] font-bold uppercase leading-none tracking-wide text-black/60",
63
+ ],
64
+ shortcut: ["font-sans text-sm font-semibold leading-6"],
65
+ itemCount: [
66
+ "rounded border border-black bg-brutal-pink px-1.5 py-0.5",
67
+ "font-sans text-[10px] leading-none font-bold text-white",
68
+ ],
69
+ separator: ["border-black"],
70
+ indicator: [],
71
+ },
72
+ elegant: {
73
+ content: [
74
+ "gap-0.5 rounded-md bg-layer-popover p-1",
75
+ "shadow-lg outline-none",
76
+ "flex flex-col",
77
+ ],
78
+ item: [
79
+ "gap-2.5 rounded-sm px-3 py-2 text-[13px] font-medium tracking-[-0.005em]",
80
+ "text-foreground-muted",
81
+ "transition-[background-color,color] duration-150",
82
+ "hover:bg-foreground/[0.04] hover:text-foreground-strong",
83
+ "data-[highlighted]:bg-foreground/[0.04] data-[highlighted]:text-foreground-strong",
84
+ "data-[disabled]:pointer-events-none data-[disabled]:text-foreground-disabled",
85
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-[1.5] [&_svg_*]:stroke-[1.5] [&>svg]:size-4",
86
+ "[&>svg:last-child]:size-3",
87
+ "[&>svg]:text-foreground-muted",
88
+ "hover:[&>svg]:text-foreground-strong",
89
+ "data-[highlighted]:[&>svg]:text-foreground-strong",
90
+ ],
91
+ radioItem: [
92
+ "gap-2.5 rounded-sm px-3 py-2 text-[13px] font-medium tracking-[-0.005em]",
93
+ "text-foreground-muted",
94
+ "transition-[background-color,color] duration-150",
95
+ "hover:bg-foreground/[0.04] hover:text-foreground-strong",
96
+ "data-[highlighted]:bg-foreground/[0.04] data-[highlighted]:text-foreground-strong",
97
+ "data-[disabled]:pointer-events-none data-[disabled]:text-foreground-disabled",
98
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-[1.5] [&_svg_*]:stroke-[1.5] [&>svg]:size-4",
99
+ "[&>svg:last-child]:size-3",
100
+ "[&>svg]:text-foreground-muted",
101
+ "hover:[&>svg]:text-foreground-strong",
102
+ "data-[highlighted]:[&>svg]:text-foreground-strong",
103
+ ],
104
+ submenuTrigger: [
105
+ "gap-2.5 rounded-sm px-3 py-2 text-[13px] font-medium tracking-[-0.005em]",
106
+ "text-foreground-muted",
107
+ "transition-[background-color,color] duration-150",
108
+ "hover:bg-foreground/[0.04] hover:text-foreground-strong",
109
+ "data-[highlighted]:bg-foreground/[0.04] data-[highlighted]:text-foreground-strong",
110
+ "data-[popup-open]:bg-foreground/[0.04] data-[popup-open]:text-foreground-strong",
111
+ "data-[disabled]:pointer-events-none data-[disabled]:text-foreground-disabled",
112
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-[1.5] [&_svg_*]:stroke-[1.5] [&>svg]:size-4",
113
+ "[&>svg:last-child]:size-3",
114
+ "[&>svg]:text-foreground-muted",
115
+ "hover:[&>svg]:text-foreground-strong",
116
+ "data-[highlighted]:[&>svg]:text-foreground-strong",
117
+ "data-[popup-open]:[&>svg]:text-foreground-strong",
118
+ ],
119
+ label: [
120
+ "-mx-1 border-b border-line-muted px-3 py-2",
121
+ "text-[11px] font-medium leading-none tracking-[-0.005em] text-foreground/45",
122
+ ],
123
+ shortcut: ["text-foreground-muted"],
124
+ itemCount: [],
125
+ separator: ["my-1 -mx-1 h-px border-0 bg-line-muted"],
126
+ indicator: [],
127
+ },
128
+ },
129
+ },
130
+ });
131
+
132
+ export type DropdownMenuProps = BaseMenu.Root.Props;
133
+
134
+ export function DropdownMenu(props: DropdownMenuProps) {
135
+ return <BaseMenu.Root data-slot="dropdown-menu" {...props} />;
136
+ }
137
+
138
+ export type DropdownMenuSubmenuProps = BaseMenu.SubmenuRoot.Props;
139
+
140
+ export function DropdownMenuSubmenu(props: DropdownMenuSubmenuProps) {
141
+ return <BaseMenu.SubmenuRoot data-slot="dropdown-menu-submenu" {...props} />;
142
+ }
143
+
144
+ export type DropdownMenuTriggerProps = StyledProps<BaseMenu.Trigger.Props>;
145
+
146
+ export function DropdownMenuTrigger({ className, ...props }: DropdownMenuTriggerProps) {
147
+ const theme = useThemeFamily();
148
+ const { trigger } = dropdownMenu({ theme });
149
+ return (
150
+ <BaseMenu.Trigger
151
+ data-slot="dropdown-menu-trigger"
152
+ className={trigger({ className })}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ export type DropdownMenuPortalProps = BaseMenu.Portal.Props;
159
+
160
+ export function DropdownMenuPortal(props: DropdownMenuPortalProps) {
161
+ return <BaseMenu.Portal data-slot="dropdown-menu-portal" {...props} />;
162
+ }
163
+
164
+ export type DropdownMenuPopupProps = ComponentPropsWithRef<"div">;
165
+
166
+ export function DropdownMenuPopup({ className, ...props }: DropdownMenuPopupProps) {
167
+ const theme = useThemeFamily();
168
+ const { content } = dropdownMenu({ theme });
169
+ return <div data-slot="dropdown-menu-content" className={content({ className })} {...props} />;
170
+ }
171
+
172
+ export type DropdownMenuContentProps = StyledProps<BaseMenu.Popup.Props> &
173
+ Pick<
174
+ BaseMenu.Positioner.Props,
175
+ | "align"
176
+ | "alignOffset"
177
+ | "anchor"
178
+ | "arrowPadding"
179
+ | "collisionAvoidance"
180
+ | "collisionBoundary"
181
+ | "collisionPadding"
182
+ | "disableAnchorTracking"
183
+ | "positionMethod"
184
+ | "side"
185
+ | "sideOffset"
186
+ | "sticky"
187
+ > & {
188
+ portalProps?: DropdownMenuPortalProps;
189
+ };
190
+
191
+ export function DropdownMenuContent({
192
+ align = "start",
193
+ alignOffset = 0,
194
+ anchor,
195
+ arrowPadding,
196
+ collisionAvoidance,
197
+ collisionBoundary,
198
+ collisionPadding,
199
+ disableAnchorTracking = false,
200
+ positionMethod = "fixed",
201
+ side = "bottom",
202
+ sideOffset = 4,
203
+ sticky,
204
+ portalProps,
205
+ className,
206
+ ...props
207
+ }: DropdownMenuContentProps) {
208
+ const theme = useThemeFamily();
209
+ const { positioner, content } = dropdownMenu({ theme });
210
+ return (
211
+ <BaseMenu.Portal {...portalProps}>
212
+ <BaseMenu.Positioner
213
+ align={align}
214
+ alignOffset={alignOffset}
215
+ anchor={anchor}
216
+ arrowPadding={arrowPadding}
217
+ collisionAvoidance={collisionAvoidance}
218
+ collisionBoundary={collisionBoundary}
219
+ collisionPadding={collisionPadding}
220
+ disableAnchorTracking={disableAnchorTracking}
221
+ positionMethod={positionMethod}
222
+ side={side}
223
+ sideOffset={sideOffset}
224
+ sticky={sticky}
225
+ className={positioner()}
226
+ >
227
+ <BaseMenu.Popup
228
+ data-slot="dropdown-menu-content"
229
+ className={content({ className })}
230
+ {...props}
231
+ />
232
+ </BaseMenu.Positioner>
233
+ </BaseMenu.Portal>
234
+ );
235
+ }
236
+
237
+ export type DropdownMenuItemProps = StyledProps<BaseMenu.Item.Props>;
238
+
239
+ export function DropdownMenuItem({ className, ...props }: DropdownMenuItemProps) {
240
+ const theme = useThemeFamily();
241
+ const { item } = dropdownMenu({ theme });
242
+ return (
243
+ <BaseMenu.Item data-slot="dropdown-menu-item" className={item({ className })} {...props} />
244
+ );
245
+ }
246
+
247
+ export type DropdownMenuSubmenuTriggerProps = StyledProps<BaseMenu.SubmenuTrigger.Props>;
248
+
249
+ export function DropdownMenuSubmenuTrigger({
250
+ className,
251
+ ...props
252
+ }: DropdownMenuSubmenuTriggerProps) {
253
+ const theme = useThemeFamily();
254
+ const { submenuTrigger } = dropdownMenu({ theme });
255
+ return (
256
+ <BaseMenu.SubmenuTrigger
257
+ data-slot="dropdown-menu-submenu-trigger"
258
+ className={submenuTrigger({ className })}
259
+ {...props}
260
+ />
261
+ );
262
+ }
263
+
264
+ export type DropdownMenuLabelProps = StyledProps<ComponentPropsWithRef<"div">>;
265
+
266
+ export function DropdownMenuLabel({ className, ...props }: DropdownMenuLabelProps) {
267
+ const theme = useThemeFamily();
268
+ const { label } = dropdownMenu({ theme });
269
+ return <div data-slot="dropdown-menu-label" className={label({ className })} {...props} />;
270
+ }
271
+
272
+ export type DropdownMenuShortcutProps = KbdProps;
273
+
274
+ export function DropdownMenuShortcut({ className, ...props }: DropdownMenuShortcutProps) {
275
+ const theme = useThemeFamily();
276
+ const { shortcut } = dropdownMenu({ theme });
277
+ return <Kbd data-slot="dropdown-menu-shortcut" className={shortcut({ className })} {...props} />;
278
+ }
279
+
280
+ export type DropdownMenuItemCountProps = BadgeProps;
281
+
282
+ export function DropdownMenuItemCount({ className, ...props }: DropdownMenuItemCountProps) {
283
+ const theme = useThemeFamily();
284
+ const { itemCount } = dropdownMenu({ theme });
285
+ return (
286
+ <Badge data-slot="dropdown-menu-item-count" className={itemCount({ className })} {...props} />
287
+ );
288
+ }
289
+
290
+ export type DropdownMenuSeparatorProps = StyledProps<BaseMenu.Separator.Props>;
291
+
292
+ export function DropdownMenuSeparator({ className, ...props }: DropdownMenuSeparatorProps) {
293
+ const theme = useThemeFamily();
294
+ const { separator } = dropdownMenu({ theme });
295
+ return (
296
+ <BaseMenu.Separator
297
+ data-slot="dropdown-menu-separator"
298
+ className={separator({ className })}
299
+ {...props}
300
+ />
301
+ );
302
+ }
303
+
304
+ function DropdownMenuCheckIcon() {
305
+ return (
306
+ <svg width="12" height="12" viewBox="0 0 12 12" aria-hidden className="size-3 shrink-0">
307
+ <g
308
+ fill="none"
309
+ strokeLinecap="round"
310
+ strokeLinejoin="round"
311
+ strokeWidth="1"
312
+ stroke="currentColor"
313
+ >
314
+ <path d="M1.75391 6.41392L4.4001 9.24985L10.2467 2.98532" />
315
+ </g>
316
+ </svg>
317
+ );
318
+ }
319
+
320
+ export type DropdownMenuRadioGroupProps = StyledProps<BaseMenu.RadioGroup.Props>;
321
+
322
+ export function DropdownMenuRadioGroup({ ...props }: DropdownMenuRadioGroupProps) {
323
+ return <BaseMenu.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
324
+ }
325
+
326
+ export type DropdownMenuRadioItemProps = StyledProps<BaseMenu.RadioItem.Props>;
327
+
328
+ export function DropdownMenuRadioItem({
329
+ className,
330
+ children,
331
+ ...props
332
+ }: DropdownMenuRadioItemProps) {
333
+ const theme = useThemeFamily();
334
+ const { radioItem, indicator } = dropdownMenu({ theme });
335
+ return (
336
+ <BaseMenu.RadioItem
337
+ data-slot="dropdown-menu-radio-item"
338
+ className={radioItem({ className })}
339
+ {...props}
340
+ >
341
+ {children}
342
+ <span className={indicator()}>
343
+ <BaseMenu.RadioItemIndicator>
344
+ <DropdownMenuCheckIcon />
345
+ </BaseMenu.RadioItemIndicator>
346
+ </span>
347
+ </BaseMenu.RadioItem>
348
+ );
349
+ }
350
+
351
+ export type DropdownMenuRadioItemIndicatorProps = StyledProps<BaseMenu.RadioItemIndicator.Props>;
352
+
353
+ export function DropdownMenuRadioItemIndicator({
354
+ className,
355
+ ...props
356
+ }: DropdownMenuRadioItemIndicatorProps) {
357
+ return (
358
+ <BaseMenu.RadioItemIndicator
359
+ data-slot="dropdown-menu-radio-item-indicator"
360
+ className={className}
361
+ {...props}
362
+ />
363
+ );
364
+ }
365
+
366
+ export type DropdownMenuCheckboxItemProps = StyledProps<BaseMenu.CheckboxItem.Props>;
367
+
368
+ export function DropdownMenuCheckboxItem({
369
+ className,
370
+ children,
371
+ ...props
372
+ }: DropdownMenuCheckboxItemProps) {
373
+ const theme = useThemeFamily();
374
+ const { item, indicator } = dropdownMenu({ theme });
375
+ return (
376
+ <BaseMenu.CheckboxItem
377
+ data-slot="dropdown-menu-checkbox-item"
378
+ className={item({ className })}
379
+ {...props}
380
+ >
381
+ {children}
382
+ <span className={indicator()}>
383
+ <BaseMenu.CheckboxItemIndicator>
384
+ <DropdownMenuCheckIcon />
385
+ </BaseMenu.CheckboxItemIndicator>
386
+ </span>
387
+ </BaseMenu.CheckboxItem>
388
+ );
389
+ }
390
+
391
+ export type DropdownMenuCheckboxItemIndicatorProps =
392
+ StyledProps<BaseMenu.CheckboxItemIndicator.Props>;
393
+
394
+ export function DropdownMenuCheckboxItemIndicator({
395
+ className,
396
+ ...props
397
+ }: DropdownMenuCheckboxItemIndicatorProps) {
398
+ return (
399
+ <BaseMenu.CheckboxItemIndicator
400
+ data-slot="dropdown-menu-checkbox-item-indicator"
401
+ className={className}
402
+ {...props}
403
+ />
404
+ );
405
+ }
@@ -0,0 +1,38 @@
1
+ export {
2
+ DropdownMenu,
3
+ DropdownMenuCheckboxItem,
4
+ DropdownMenuCheckboxItemIndicator,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuItemCount,
8
+ DropdownMenuLabel,
9
+ DropdownMenuPortal,
10
+ DropdownMenuRadioGroup,
11
+ DropdownMenuRadioItem,
12
+ DropdownMenuRadioItemIndicator,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuShortcut,
15
+ DropdownMenuSubmenu,
16
+ DropdownMenuSubmenuTrigger,
17
+ DropdownMenuPopup,
18
+ DropdownMenuTrigger,
19
+ } from "./dropdown-menu.tsx";
20
+ export type {
21
+ DropdownMenuCheckboxItemIndicatorProps,
22
+ DropdownMenuCheckboxItemProps,
23
+ DropdownMenuContentProps,
24
+ DropdownMenuItemCountProps,
25
+ DropdownMenuItemProps,
26
+ DropdownMenuLabelProps,
27
+ DropdownMenuPortalProps,
28
+ DropdownMenuProps,
29
+ DropdownMenuRadioGroupProps,
30
+ DropdownMenuRadioItemIndicatorProps,
31
+ DropdownMenuRadioItemProps,
32
+ DropdownMenuSeparatorProps,
33
+ DropdownMenuShortcutProps,
34
+ DropdownMenuSubmenuProps,
35
+ DropdownMenuSubmenuTriggerProps,
36
+ DropdownMenuPopupProps,
37
+ DropdownMenuTriggerProps,
38
+ } from "./dropdown-menu.tsx";
@@ -0,0 +1,177 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { tv } from "../../lib/tv.ts";
4
+ import { useThemeFamily } from "../../lib/theme/use-theme.ts";
5
+
6
+ // EmptyState family: a quiet "nothing here yet" surface — muted icon, muted
7
+ // title, optional description + action. Composed from a centered root, a
8
+ // content group, and slot parts (`EmptyStateIcon`, `EmptyStateTitle`,
9
+ // `EmptyStateDescription`, `EmptyStateActions`). All parts share one `tv()`
10
+ // slots config so theme styling lives in a single place. There is no icon frame
11
+ // in brutal; elegant uses a compact neutral icon chip. The title is
12
+ // sentence-case (callers pass the literal string).
13
+ const emptyState = tv({
14
+ slots: {
15
+ root: ["flex w-full min-w-0 flex-col items-center justify-center px-6 py-12 text-center"],
16
+ content: [
17
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-2 text-center",
18
+ "[&>[data-slot=empty-state-icon]]:mb-0",
19
+ "[&>[data-slot=empty-state-title]]:mb-0",
20
+ ],
21
+ icon: ["mb-4 inline-flex items-center justify-center [&_svg]:shrink-0"],
22
+ title: ["mb-2 w-full max-w-full text-center text-balance font-heading text-lg font-semibold"],
23
+ description: [
24
+ "mx-auto w-full max-w-[32ch] text-center text-sm leading-relaxed text-pretty",
25
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-foreground-strong",
26
+ ],
27
+ actions: ["mt-5"],
28
+ },
29
+ variants: {
30
+ theme: {
31
+ brutal: {
32
+ icon: "text-foreground-muted [&_svg]:size-9",
33
+ title: "text-foreground/60",
34
+ description: "text-foreground/60",
35
+ },
36
+ elegant: {
37
+ icon: "rounded-md bg-layer-muted p-2 text-foreground-placeholder [&_svg]:size-4.5",
38
+ title: "text-sm font-medium text-foreground-strong",
39
+ description: "text-foreground-muted",
40
+ },
41
+ },
42
+ },
43
+ });
44
+
45
+ type EmptyStateSlotProps = useRender.ElementProps<"div"> & {
46
+ "data-slot": string;
47
+ };
48
+
49
+ export type EmptyStateProps = useRender.ComponentProps<"div">;
50
+
51
+ // Root: a centered, padded column. Compose `EmptyStateContent` and
52
+ // `EmptyStateActions` as children. Flat icon/title/description children are
53
+ // still supported for legacy callsites.
54
+ export function EmptyState({ render, className, ...props }: EmptyStateProps) {
55
+ const { root } = emptyState();
56
+ const defaultProps: EmptyStateSlotProps = {
57
+ "data-slot": "empty-state",
58
+ className: root({ className }),
59
+ };
60
+
61
+ return useRender({
62
+ defaultTagName: "div",
63
+ render,
64
+ props: mergeProps<"div">(defaultProps, props),
65
+ state: {
66
+ slot: "empty-state",
67
+ },
68
+ });
69
+ }
70
+
71
+ export type EmptyStateContentProps = useRender.ComponentProps<"div">;
72
+
73
+ // Content: groups media, title, and description so empty-state copy keeps a
74
+ // stable measure and balanced line breaks.
75
+ export function EmptyStateContent({ render, className, ...props }: EmptyStateContentProps) {
76
+ const { content } = emptyState();
77
+ const defaultProps: EmptyStateSlotProps = {
78
+ "data-slot": "empty-state-content",
79
+ className: content({ className }),
80
+ };
81
+
82
+ return useRender({
83
+ defaultTagName: "div",
84
+ render,
85
+ props: mergeProps<"div">(defaultProps, props),
86
+ state: {
87
+ slot: "empty-state-content",
88
+ },
89
+ });
90
+ }
91
+
92
+ export type EmptyStateIconProps = useRender.ComponentProps<"div">;
93
+
94
+ // Muted icon slot. The slot owns default SVG sizing per theme; callers can
95
+ // still override it with className for custom media.
96
+ export function EmptyStateIcon({ render, className, ...props }: EmptyStateIconProps) {
97
+ const theme = useThemeFamily();
98
+ const { icon } = emptyState({ theme });
99
+ const defaultProps: EmptyStateSlotProps = {
100
+ "data-slot": "empty-state-icon",
101
+ className: icon({ className }),
102
+ };
103
+
104
+ return useRender({
105
+ defaultTagName: "div",
106
+ render,
107
+ props: mergeProps<"div">(defaultProps, props),
108
+ state: {
109
+ slot: "empty-state-icon",
110
+ },
111
+ });
112
+ }
113
+
114
+ export type EmptyStateTitleProps = useRender.ComponentProps<"div">;
115
+
116
+ // Muted, sentence-case title line. Pass `render` to override the element (e.g. a
117
+ // real heading for landmark semantics).
118
+ export function EmptyStateTitle({ render, className, ...props }: EmptyStateTitleProps) {
119
+ const theme = useThemeFamily();
120
+ const { title } = emptyState({ theme });
121
+ const defaultProps: EmptyStateSlotProps = {
122
+ "data-slot": "empty-state-title",
123
+ className: title({ className }),
124
+ };
125
+
126
+ return useRender({
127
+ defaultTagName: "div",
128
+ render,
129
+ props: mergeProps<"div">(defaultProps, props),
130
+ state: {
131
+ slot: "empty-state-title",
132
+ },
133
+ });
134
+ }
135
+
136
+ export type EmptyStateDescriptionProps = useRender.ComponentProps<"div">;
137
+
138
+ // Optional supporting copy, capped to ~32ch for a tidy measure. Pass `render` to
139
+ // override the element.
140
+ export function EmptyStateDescription({ render, className, ...props }: EmptyStateDescriptionProps) {
141
+ const theme = useThemeFamily();
142
+ const { description } = emptyState({ theme });
143
+ const defaultProps: EmptyStateSlotProps = {
144
+ "data-slot": "empty-state-description",
145
+ className: description({ className }),
146
+ };
147
+
148
+ return useRender({
149
+ defaultTagName: "div",
150
+ render,
151
+ props: mergeProps<"div">(defaultProps, props),
152
+ state: {
153
+ slot: "empty-state-description",
154
+ },
155
+ });
156
+ }
157
+
158
+ export type EmptyStateActionsProps = useRender.ComponentProps<"div">;
159
+
160
+ // Optional action slot rendered below the copy. Pass `render` to override the
161
+ // element.
162
+ export function EmptyStateActions({ render, className, ...props }: EmptyStateActionsProps) {
163
+ const { actions } = emptyState();
164
+ const defaultProps: EmptyStateSlotProps = {
165
+ "data-slot": "empty-state-actions",
166
+ className: actions({ className }),
167
+ };
168
+
169
+ return useRender({
170
+ defaultTagName: "div",
171
+ render,
172
+ props: mergeProps<"div">(defaultProps, props),
173
+ state: {
174
+ slot: "empty-state-actions",
175
+ },
176
+ });
177
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ EmptyState,
3
+ EmptyStateContent,
4
+ EmptyStateIcon,
5
+ EmptyStateTitle,
6
+ EmptyStateDescription,
7
+ EmptyStateActions,
8
+ } from "./empty-state.tsx";
9
+ export type {
10
+ EmptyStateProps,
11
+ EmptyStateContentProps,
12
+ EmptyStateIconProps,
13
+ EmptyStateTitleProps,
14
+ EmptyStateDescriptionProps,
15
+ EmptyStateActionsProps,
16
+ } from "./empty-state.tsx";