@boyernick/standard-ui-react 0.1.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 (67) hide show
  1. package/README.md +9 -0
  2. package/package.json +46 -0
  3. package/src/accordion.tsx +84 -0
  4. package/src/alert-dialog.tsx +101 -0
  5. package/src/attachment.tsx +138 -0
  6. package/src/autocomplete.tsx +281 -0
  7. package/src/avatar.tsx +56 -0
  8. package/src/badge.tsx +28 -0
  9. package/src/brand.tsx +70 -0
  10. package/src/breadcrumb.tsx +81 -0
  11. package/src/button.tsx +129 -0
  12. package/src/calendar.tsx +84 -0
  13. package/src/card.tsx +54 -0
  14. package/src/carousel.tsx +253 -0
  15. package/src/chart.tsx +185 -0
  16. package/src/checkbox-group.tsx +14 -0
  17. package/src/checkbox.tsx +31 -0
  18. package/src/code-block.tsx +124 -0
  19. package/src/collapsible.tsx +61 -0
  20. package/src/combobox.tsx +324 -0
  21. package/src/command.tsx +377 -0
  22. package/src/context-menu.tsx +250 -0
  23. package/src/dialog.tsx +78 -0
  24. package/src/drawer.tsx +156 -0
  25. package/src/empty.tsx +52 -0
  26. package/src/field.tsx +75 -0
  27. package/src/fieldset.tsx +25 -0
  28. package/src/form.tsx +14 -0
  29. package/src/icons.tsx +91 -0
  30. package/src/illustrations.tsx +167 -0
  31. package/src/image-modal.tsx +110 -0
  32. package/src/index.ts +833 -0
  33. package/src/input.tsx +68 -0
  34. package/src/lib/cn.ts +3 -0
  35. package/src/lib/motion.ts +23 -0
  36. package/src/markdown-editor.tsx +302 -0
  37. package/src/menu.tsx +205 -0
  38. package/src/menubar.tsx +22 -0
  39. package/src/meter.tsx +61 -0
  40. package/src/navigation-menu.tsx +217 -0
  41. package/src/number-field.tsx +119 -0
  42. package/src/orb.tsx +33 -0
  43. package/src/otp-field.tsx +45 -0
  44. package/src/pagination.tsx +91 -0
  45. package/src/popover.tsx +92 -0
  46. package/src/preview-card.tsx +103 -0
  47. package/src/progress.tsx +60 -0
  48. package/src/radio.tsx +43 -0
  49. package/src/scroll-area.tsx +67 -0
  50. package/src/select.tsx +161 -0
  51. package/src/separator.tsx +17 -0
  52. package/src/sidebar.tsx +82 -0
  53. package/src/skeleton.tsx +32 -0
  54. package/src/slider.tsx +56 -0
  55. package/src/sounds.tsx +270 -0
  56. package/src/spinner.tsx +45 -0
  57. package/src/switch.tsx +19 -0
  58. package/src/table.tsx +81 -0
  59. package/src/tabs.tsx +62 -0
  60. package/src/text-animate.tsx +155 -0
  61. package/src/textarea.tsx +58 -0
  62. package/src/ticker.tsx +74 -0
  63. package/src/toast.tsx +142 -0
  64. package/src/toggle.tsx +32 -0
  65. package/src/toolbar.tsx +75 -0
  66. package/src/tooltip.tsx +55 -0
  67. package/src/video-player.tsx +241 -0
package/src/input.tsx ADDED
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { forwardRef, type InputHTMLAttributes } from "react"
5
+ import { cn } from "./lib/cn"
6
+
7
+ const inputVariants = cva(
8
+ "text-sm flex w-full cursor-text rounded-md text-fg-primary transition-[color,box-shadow] duration-150 ease-out placeholder:text-fg-quaternary outline-none aria-invalid:border-destructive disabled:cursor-not-allowed disabled:opacity-50",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border bg-surface inset-shadow-outline-top focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20",
14
+ ghost: "border border-transparent bg-transparent",
15
+ },
16
+ size: {
17
+ sm: "h-8 px-2.5",
18
+ md: "h-9 px-3",
19
+ lg: "h-10 px-3.5",
20
+ },
21
+ invalid: {
22
+ true: "",
23
+ false: "",
24
+ },
25
+ },
26
+ compoundVariants: [
27
+ {
28
+ variant: "default",
29
+ invalid: false,
30
+ class: "border-border-secondary",
31
+ },
32
+ {
33
+ variant: "default",
34
+ invalid: true,
35
+ class: "border-destructive",
36
+ },
37
+ {
38
+ variant: "ghost",
39
+ invalid: true,
40
+ class:
41
+ "border-destructive focus-visible:border-destructive focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-destructive/20",
42
+ },
43
+ ],
44
+ defaultVariants: {
45
+ variant: "default",
46
+ size: "md",
47
+ invalid: false,
48
+ },
49
+ },
50
+ )
51
+
52
+ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> &
53
+ VariantProps<typeof inputVariants>
54
+
55
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
56
+ ({ className, variant, size, invalid, ...props }, ref) => (
57
+ <input
58
+ ref={ref}
59
+ className={cn(inputVariants({ variant, size, invalid }), className)}
60
+ aria-invalid={invalid || undefined}
61
+ {...props}
62
+ />
63
+ ),
64
+ )
65
+
66
+ Input.displayName = "Input"
67
+
68
+ export { inputVariants }
package/src/lib/cn.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+
3
+ export const cn = (...inputs: ClassValue[]) => clsx(inputs)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared motion classes for StandardUI.
3
+ * Prefer CSS `scale` on centered modals so `translate` centering stays intact.
4
+ * Anchored popups use `transform-origin` from Base UI.
5
+ */
6
+ export const motion = {
7
+ backdrop:
8
+ "transition-opacity duration-150 ease-out motion-reduce:transition-none data-starting-style:opacity-0 data-ending-style:opacity-0",
9
+ /** Centered dialog / alert — scale property, not transform */
10
+ popupCenter:
11
+ "transition-[scale,opacity] duration-150 ease-out motion-reduce:transition-none data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.96] data-ending-style:opacity-0",
12
+ /** Select, tooltip, menus — transform-origin from Base UI */
13
+ popupAnchor:
14
+ "origin-[var(--transform-origin)] transition-[transform,scale,opacity] duration-150 ease-out motion-reduce:transition-none data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.96] data-ending-style:opacity-0",
15
+ accordionPanel:
16
+ "transition-[height] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none data-starting-style:h-0 data-ending-style:h-0",
17
+ tabsIndicator:
18
+ "transition-[translate,width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none",
19
+ colors: "transition-colors duration-150 ease-out motion-reduce:transition-none",
20
+ transform:
21
+ "transition-transform duration-150 ease-out motion-reduce:transition-none",
22
+ all: "transition-all duration-150 ease-out motion-reduce:transition-none",
23
+ } as const
@@ -0,0 +1,302 @@
1
+ "use client"
2
+
3
+ import {
4
+ createContext,
5
+ forwardRef,
6
+ useContext,
7
+ useRef,
8
+ useState,
9
+ type ChangeEvent,
10
+ type ComponentPropsWithoutRef,
11
+ type HTMLAttributes,
12
+ type ReactNode,
13
+ type Ref,
14
+ type TextareaHTMLAttributes,
15
+ } from "react"
16
+ import { cn } from "./lib/cn"
17
+ import { textareaVariants } from "./textarea"
18
+ import { Toggle } from "./toggle"
19
+ import { Toolbar } from "./toolbar"
20
+
21
+ type MarkdownEditorContextValue = {
22
+ value: string
23
+ setValue: (value: string) => void
24
+ inputRef: React.RefObject<HTMLTextAreaElement | null>
25
+ selection: { start: number; end: number }
26
+ setSelection: (selection: { start: number; end: number }) => void
27
+ }
28
+
29
+ const MarkdownEditorContext = createContext<MarkdownEditorContextValue | null>(
30
+ null,
31
+ )
32
+
33
+ const useMarkdownEditor = () => {
34
+ const context = useContext(MarkdownEditorContext)
35
+
36
+ if (!context) {
37
+ throw new Error(
38
+ "MarkdownEditor parts must be rendered inside MarkdownEditor",
39
+ )
40
+ }
41
+
42
+ return context
43
+ }
44
+
45
+ const assignRef = <Value,>(ref: Ref<Value> | undefined, value: Value) => {
46
+ if (typeof ref === "function") {
47
+ ref(value)
48
+ return
49
+ }
50
+
51
+ if (ref) ref.current = value
52
+ }
53
+
54
+ export type MarkdownEditorProps = Omit<
55
+ HTMLAttributes<HTMLDivElement>,
56
+ "onChange"
57
+ > & {
58
+ value?: string
59
+ defaultValue?: string
60
+ onValueChange?: (value: string) => void
61
+ }
62
+
63
+ export const MarkdownEditor = ({
64
+ value: controlledValue,
65
+ defaultValue = "",
66
+ onValueChange,
67
+ className,
68
+ children,
69
+ ...props
70
+ }: MarkdownEditorProps) => {
71
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)
72
+ const [selection, setSelection] = useState({ start: 0, end: 0 })
73
+ const inputRef = useRef<HTMLTextAreaElement>(null)
74
+ const value = controlledValue ?? uncontrolledValue
75
+
76
+ const setValue = (nextValue: string) => {
77
+ if (controlledValue === undefined) setUncontrolledValue(nextValue)
78
+ onValueChange?.(nextValue)
79
+ }
80
+
81
+ return (
82
+ <MarkdownEditorContext.Provider
83
+ value={{
84
+ value,
85
+ setValue,
86
+ inputRef,
87
+ selection,
88
+ setSelection,
89
+ }}
90
+ >
91
+ <div
92
+ className={cn(
93
+ "overflow-hidden rounded-xl border border-border-primary bg-surface",
94
+ className,
95
+ )}
96
+ {...props}
97
+ >
98
+ {children}
99
+ </div>
100
+ </MarkdownEditorContext.Provider>
101
+ )
102
+ }
103
+
104
+ type Format = {
105
+ label: string
106
+ marker: "*" | "**" | "`"
107
+ className?: string
108
+ }
109
+
110
+ const formats: Format[] = [
111
+ { label: "Bold", marker: "**", className: "font-semibold" },
112
+ { label: "Italic", marker: "*", className: "italic" },
113
+ { label: "Code", marker: "`", className: "font-mono" },
114
+ ]
115
+
116
+ export type MarkdownEditorToolbarProps = ComponentPropsWithoutRef<
117
+ typeof Toolbar
118
+ >
119
+
120
+ export const MarkdownEditorToolbar = ({
121
+ className,
122
+ children,
123
+ ...props
124
+ }: MarkdownEditorToolbarProps) => {
125
+ const { value, setValue, inputRef, selection, setSelection } =
126
+ useMarkdownEditor()
127
+
128
+ const isFormatActive = (marker: Format["marker"]) => {
129
+ const { start, end } = selection
130
+ return (
131
+ value.slice(start - marker.length, start) === marker &&
132
+ value.slice(end, end + marker.length) === marker
133
+ )
134
+ }
135
+
136
+ const handleFormat = (marker: Format["marker"]) => {
137
+ const input = inputRef.current
138
+
139
+ if (!input) return
140
+
141
+ const start = input.selectionStart
142
+ const end = input.selectionEnd
143
+ const selectedText = value.slice(start, end)
144
+ const isWrapped =
145
+ value.slice(start - marker.length, start) === marker &&
146
+ value.slice(end, end + marker.length) === marker
147
+
148
+ if (isWrapped) {
149
+ const nextValue =
150
+ value.slice(0, start - marker.length) +
151
+ selectedText +
152
+ value.slice(end + marker.length)
153
+
154
+ setValue(nextValue)
155
+ requestAnimationFrame(() => {
156
+ input.focus()
157
+ input.setSelectionRange(start - marker.length, end - marker.length)
158
+ setSelection({
159
+ start: start - marker.length,
160
+ end: end - marker.length,
161
+ })
162
+ })
163
+ return
164
+ }
165
+
166
+ const nextValue =
167
+ value.slice(0, start) +
168
+ marker +
169
+ selectedText +
170
+ marker +
171
+ value.slice(end)
172
+
173
+ setValue(nextValue)
174
+ requestAnimationFrame(() => {
175
+ const selectionStart = start + marker.length
176
+ input.focus()
177
+ input.setSelectionRange(selectionStart, selectionStart + selectedText.length)
178
+ setSelection({
179
+ start: selectionStart,
180
+ end: selectionStart + selectedText.length,
181
+ })
182
+ })
183
+ }
184
+
185
+ return (
186
+ <Toolbar
187
+ aria-label="Text formatting"
188
+ className={cn(
189
+ "flex w-full rounded-none border-0 border-b border-border-primary px-2 py-1",
190
+ className,
191
+ )}
192
+ {...props}
193
+ >
194
+ {children ??
195
+ formats.map((format) => (
196
+ <Toggle
197
+ key={format.label}
198
+ aria-label={format.label}
199
+ pressed={isFormatActive(format.marker)}
200
+ onPressedChange={() => handleFormat(format.marker)}
201
+ className={format.className}
202
+ >
203
+ {format.label}
204
+ </Toggle>
205
+ ))}
206
+ </Toolbar>
207
+ )
208
+ }
209
+
210
+ export type MarkdownEditorInputProps = TextareaHTMLAttributes<HTMLTextAreaElement>
211
+
212
+ export const MarkdownEditorInput = forwardRef<
213
+ HTMLTextAreaElement,
214
+ MarkdownEditorInputProps
215
+ >(({ className, onChange, onSelect, ...props }, forwardedRef) => {
216
+ const { value, setValue, inputRef, setSelection } = useMarkdownEditor()
217
+
218
+ const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
219
+ setValue(event.target.value)
220
+ onChange?.(event)
221
+ }
222
+
223
+ return (
224
+ <textarea
225
+ ref={(node) => {
226
+ inputRef.current = node
227
+ assignRef(forwardedRef, node)
228
+ }}
229
+ value={value}
230
+ className={cn(
231
+ textareaVariants({ variant: "ghost" }),
232
+ "min-h-56 resize-y rounded-none border-0 p-4 font-mono text-sm focus-visible:ring-0",
233
+ className,
234
+ )}
235
+ onChange={handleChange}
236
+ onSelect={(event) => {
237
+ setSelection({
238
+ start: event.currentTarget.selectionStart,
239
+ end: event.currentTarget.selectionEnd,
240
+ })
241
+ onSelect?.(event)
242
+ }}
243
+ {...props}
244
+ />
245
+ )
246
+ })
247
+
248
+ MarkdownEditorInput.displayName = "MarkdownEditorInput"
249
+
250
+ const renderBasicMarkdown = (value: string): ReactNode[] =>
251
+ value
252
+ .split(/(\*\*[^*\n]+\*\*|`[^`\n]+`|\*[^*\n]+\*)/g)
253
+ .map((part, index) => {
254
+ if (part.startsWith("**") && part.endsWith("**")) {
255
+ return <strong key={index}>{part.slice(2, -2)}</strong>
256
+ }
257
+
258
+ if (part.startsWith("`") && part.endsWith("`")) {
259
+ return (
260
+ <code
261
+ key={index}
262
+ className="rounded-xs bg-background-tertiary px-1 py-0.5 font-mono text-sm"
263
+ >
264
+ {part.slice(1, -1)}
265
+ </code>
266
+ )
267
+ }
268
+
269
+ if (part.startsWith("*") && part.endsWith("*")) {
270
+ return <em key={index}>{part.slice(1, -1)}</em>
271
+ }
272
+
273
+ return part
274
+ })
275
+
276
+ export type MarkdownEditorPreviewProps = HTMLAttributes<HTMLDivElement> & {
277
+ emptyText?: string
278
+ }
279
+
280
+ export const MarkdownEditorPreview = ({
281
+ className,
282
+ emptyText = "Nothing to preview",
283
+ ...props
284
+ }: MarkdownEditorPreviewProps) => {
285
+ const { value } = useMarkdownEditor()
286
+
287
+ return (
288
+ <div
289
+ className={cn(
290
+ "min-h-56 whitespace-pre-wrap border-t border-border-primary p-4 text-sm text-fg-primary",
291
+ className,
292
+ )}
293
+ {...props}
294
+ >
295
+ {value ? (
296
+ renderBasicMarkdown(value)
297
+ ) : (
298
+ <span className="text-fg-quaternary">{emptyText}</span>
299
+ )}
300
+ </div>
301
+ )
302
+ }
package/src/menu.tsx ADDED
@@ -0,0 +1,205 @@
1
+ "use client"
2
+
3
+ import { Menu as BaseMenu } from "@base-ui/react/menu"
4
+ import type { ComponentProps } from "react"
5
+ import { IconCheckmark1, IconChevronRightSmall } from "./icons"
6
+ import { cn } from "./lib/cn"
7
+ import { motion } from "./lib/motion"
8
+
9
+ export type MenuProps = ComponentProps<typeof BaseMenu.Root>
10
+ export type MenuTriggerProps = ComponentProps<typeof BaseMenu.Trigger>
11
+ export type MenuPortalProps = ComponentProps<typeof BaseMenu.Portal>
12
+ export type MenuBackdropProps = ComponentProps<typeof BaseMenu.Backdrop>
13
+ export type MenuPositionerProps = ComponentProps<typeof BaseMenu.Positioner>
14
+ export type MenuPopupProps = ComponentProps<typeof BaseMenu.Popup>
15
+ export type MenuArrowProps = ComponentProps<typeof BaseMenu.Arrow>
16
+ export type MenuItemProps = ComponentProps<typeof BaseMenu.Item>
17
+ export type MenuLinkItemProps = ComponentProps<typeof BaseMenu.LinkItem>
18
+ export type MenuSeparatorProps = ComponentProps<typeof BaseMenu.Separator>
19
+ export type MenuGroupProps = ComponentProps<typeof BaseMenu.Group>
20
+ export type MenuGroupLabelProps = ComponentProps<typeof BaseMenu.GroupLabel>
21
+ export type MenuCheckboxItemProps = ComponentProps<typeof BaseMenu.CheckboxItem>
22
+ export type MenuCheckboxItemIndicatorProps = ComponentProps<
23
+ typeof BaseMenu.CheckboxItemIndicator
24
+ >
25
+ export type MenuRadioGroupProps = ComponentProps<typeof BaseMenu.RadioGroup>
26
+ export type MenuRadioItemProps = ComponentProps<typeof BaseMenu.RadioItem>
27
+ export type MenuRadioItemIndicatorProps = ComponentProps<
28
+ typeof BaseMenu.RadioItemIndicator
29
+ >
30
+ export type MenuSubmenuRootProps = ComponentProps<typeof BaseMenu.SubmenuRoot>
31
+ export type MenuSubmenuTriggerProps = ComponentProps<
32
+ typeof BaseMenu.SubmenuTrigger
33
+ >
34
+ export type MenuViewportProps = ComponentProps<typeof BaseMenu.Viewport>
35
+
36
+ const menuItemClassName = cn(
37
+ "flex min-h-8 cursor-default items-center gap-2 rounded-xs px-2.5 py-1.5 text-sm text-fg-primary outline-none select-none",
38
+ motion.colors,
39
+ "data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary",
40
+ )
41
+
42
+ const menuIndicatorItemClassName = cn(menuItemClassName, "pl-2")
43
+
44
+ const menuIndicatorClassName =
45
+ "mr-0.5 flex size-4 shrink-0 items-center justify-center text-fg-primary data-unchecked:invisible"
46
+
47
+ export const Menu = (props: MenuProps) => <BaseMenu.Root {...props} />
48
+
49
+ export const MenuTrigger = ({ className, ...props }: MenuTriggerProps) => (
50
+ <BaseMenu.Trigger className={cn("cursor-pointer", className)} {...props} />
51
+ )
52
+
53
+ export const MenuPortal = (props: MenuPortalProps) => (
54
+ <BaseMenu.Portal {...props} />
55
+ )
56
+
57
+ export const MenuBackdrop = ({ className, ...props }: MenuBackdropProps) => (
58
+ <BaseMenu.Backdrop className={cn(className)} {...props} />
59
+ )
60
+
61
+ export const MenuPositioner = ({
62
+ sideOffset = 4,
63
+ className,
64
+ ...props
65
+ }: MenuPositionerProps) => (
66
+ <BaseMenu.Positioner
67
+ sideOffset={sideOffset}
68
+ className={cn("z-50 outline-none", className)}
69
+ {...props}
70
+ />
71
+ )
72
+
73
+ export const MenuPopup = ({ className, ...props }: MenuPopupProps) => (
74
+ <BaseMenu.Popup
75
+ className={cn(
76
+ "z-50 min-w-40 overflow-hidden rounded-md border border-border-primary bg-surface p-1 shadow-md outline-none",
77
+ motion.popupAnchor,
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+
84
+ export const MenuArrow = ({ className, ...props }: MenuArrowProps) => (
85
+ <BaseMenu.Arrow
86
+ className={cn(
87
+ "data-[side=bottom]:top-[-6px] data-[side=left]:right-[-6px] data-[side=right]:left-[-6px] data-[side=top]:bottom-[-6px]",
88
+ "size-2.5 rotate-45 border border-border-primary bg-surface",
89
+ "data-[side=bottom]:border-r-0 data-[side=bottom]:border-b-0",
90
+ "data-[side=top]:border-t-0 data-[side=top]:border-l-0",
91
+ "data-[side=left]:border-b-0 data-[side=left]:border-l-0",
92
+ "data-[side=right]:border-t-0 data-[side=right]:border-r-0",
93
+ className,
94
+ )}
95
+ {...props}
96
+ />
97
+ )
98
+
99
+ export const MenuItem = ({ className, ...props }: MenuItemProps) => (
100
+ <BaseMenu.Item className={cn(menuItemClassName, className)} {...props} />
101
+ )
102
+
103
+ export const MenuLinkItem = ({ className, ...props }: MenuLinkItemProps) => (
104
+ <BaseMenu.LinkItem
105
+ className={cn(menuItemClassName, "cursor-pointer", className)}
106
+ {...props}
107
+ />
108
+ )
109
+
110
+ export const MenuSeparator = ({ className, ...props }: MenuSeparatorProps) => (
111
+ <BaseMenu.Separator
112
+ className={cn("my-1 h-px bg-border-primary", className)}
113
+ {...props}
114
+ />
115
+ )
116
+
117
+ export const MenuGroup = ({ className, ...props }: MenuGroupProps) => (
118
+ <BaseMenu.Group className={cn(className)} {...props} />
119
+ )
120
+
121
+ export const MenuGroupLabel = ({
122
+ className,
123
+ ...props
124
+ }: MenuGroupLabelProps) => (
125
+ <BaseMenu.GroupLabel
126
+ className={cn("px-2.5 py-1.5 text-xs text-fg-tertiary", className)}
127
+ {...props}
128
+ />
129
+ )
130
+
131
+ export const MenuCheckboxItem = ({
132
+ className,
133
+ ...props
134
+ }: MenuCheckboxItemProps) => (
135
+ <BaseMenu.CheckboxItem
136
+ className={cn(menuIndicatorItemClassName, className)}
137
+ {...props}
138
+ />
139
+ )
140
+
141
+ export const MenuCheckboxItemIndicator = ({
142
+ className,
143
+ children,
144
+ keepMounted = true,
145
+ ...props
146
+ }: MenuCheckboxItemIndicatorProps) => (
147
+ <BaseMenu.CheckboxItemIndicator
148
+ keepMounted={keepMounted}
149
+ className={cn(menuIndicatorClassName, className)}
150
+ {...props}
151
+ >
152
+ {children ?? <IconCheckmark1 size={14} className="size-3.5" aria-hidden />}
153
+ </BaseMenu.CheckboxItemIndicator>
154
+ )
155
+
156
+ export const MenuRadioGroup = ({
157
+ className,
158
+ ...props
159
+ }: MenuRadioGroupProps) => (
160
+ <BaseMenu.RadioGroup className={cn(className)} {...props} />
161
+ )
162
+
163
+ export const MenuRadioItem = ({ className, ...props }: MenuRadioItemProps) => (
164
+ <BaseMenu.RadioItem
165
+ className={cn(menuIndicatorItemClassName, className)}
166
+ {...props}
167
+ />
168
+ )
169
+
170
+ export const MenuRadioItemIndicator = ({
171
+ className,
172
+ children,
173
+ keepMounted = true,
174
+ ...props
175
+ }: MenuRadioItemIndicatorProps) => (
176
+ <BaseMenu.RadioItemIndicator
177
+ keepMounted={keepMounted}
178
+ className={cn(menuIndicatorClassName, className)}
179
+ {...props}
180
+ >
181
+ {children ?? <IconCheckmark1 size={14} className="size-3.5" aria-hidden />}
182
+ </BaseMenu.RadioItemIndicator>
183
+ )
184
+
185
+ export const MenuSubmenuRoot = (props: MenuSubmenuRootProps) => (
186
+ <BaseMenu.SubmenuRoot {...props} />
187
+ )
188
+
189
+ export const MenuSubmenuTrigger = ({
190
+ className,
191
+ children,
192
+ ...props
193
+ }: MenuSubmenuTriggerProps) => (
194
+ <BaseMenu.SubmenuTrigger
195
+ className={cn(menuItemClassName, "w-full justify-between", className)}
196
+ {...props}
197
+ >
198
+ {children}
199
+ <IconChevronRightSmall size={16} className="size-4 text-fg-tertiary" aria-hidden />
200
+ </BaseMenu.SubmenuTrigger>
201
+ )
202
+
203
+ export const MenuViewport = ({ className, ...props }: MenuViewportProps) => (
204
+ <BaseMenu.Viewport className={cn(className)} {...props} />
205
+ )
@@ -0,0 +1,22 @@
1
+ "use client"
2
+
3
+ import { Menubar as BaseMenubar } from "@base-ui/react/menubar"
4
+ import type { ComponentProps } from "react"
5
+ import { cn } from "./lib/cn"
6
+
7
+ export type MenubarProps = ComponentProps<typeof BaseMenubar>
8
+
9
+ export const Menubar = ({ className, ...props }: MenubarProps) => (
10
+ <BaseMenubar
11
+ className={cn(
12
+ "inline-flex items-center gap-0.5 rounded-lg border border-border-primary bg-background-secondary p-0.5",
13
+ "[&_button]:inline-flex [&_button]:h-8 [&_button]:cursor-pointer [&_button]:items-center [&_button]:justify-center [&_button]:gap-1.5 [&_button]:rounded-md [&_button]:px-2.5 [&_button]:text-sm [&_button]:text-fg-secondary [&_button]:outline-none",
14
+ "[&_button]:hover:bg-background-tertiary [&_button]:hover:text-fg-primary",
15
+ "[&_button]:focus-visible:border-ring [&_button]:focus-visible:ring-[3px] [&_button]:focus-visible:ring-offset-1 [&_button]:focus-visible:ring-offset-background-primary [&_button]:focus-visible:ring-ring/20",
16
+ "[&_button]:data-popup-open:bg-background-tertiary [&_button]:data-popup-open:text-fg-primary",
17
+ "[&_button]:data-disabled:cursor-not-allowed [&_button]:data-disabled:opacity-50",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ )
package/src/meter.tsx ADDED
@@ -0,0 +1,61 @@
1
+ "use client"
2
+
3
+ import { Meter as BaseMeter } from "@base-ui/react/meter"
4
+ import type { ComponentProps } from "react"
5
+ import { cn } from "./lib/cn"
6
+
7
+ export type MeterProps = ComponentProps<typeof BaseMeter.Root>
8
+ export type MeterLabelProps = ComponentProps<typeof BaseMeter.Label>
9
+ export type MeterValueProps = ComponentProps<typeof BaseMeter.Value>
10
+ export type MeterTrackProps = ComponentProps<typeof BaseMeter.Track>
11
+ export type MeterIndicatorProps = ComponentProps<typeof BaseMeter.Indicator>
12
+
13
+ export const Meter = ({ className, ...props }: MeterProps) => (
14
+ <BaseMeter.Root
15
+ className={cn(
16
+ "grid w-full grid-cols-[1fr_auto] items-center gap-x-2 gap-y-2",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+
23
+ export const MeterLabel = ({ className, ...props }: MeterLabelProps) => (
24
+ <BaseMeter.Label
25
+ className={cn("text-sm text-fg-primary", className)}
26
+ {...props}
27
+ />
28
+ )
29
+
30
+ export const MeterValue = ({ className, ...props }: MeterValueProps) => (
31
+ <BaseMeter.Value
32
+ className={cn(
33
+ "text-right text-sm text-fg-secondary tabular-nums",
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ )
39
+
40
+ export const MeterTrack = ({ className, ...props }: MeterTrackProps) => (
41
+ <BaseMeter.Track
42
+ className={cn(
43
+ "col-span-2 h-1.5 overflow-hidden rounded-full bg-background-quaternary",
44
+ className,
45
+ )}
46
+ {...props}
47
+ />
48
+ )
49
+
50
+ export const MeterIndicator = ({
51
+ className,
52
+ ...props
53
+ }: MeterIndicatorProps) => (
54
+ <BaseMeter.Indicator
55
+ className={cn(
56
+ "h-full rounded-full bg-brand-primary transition-[width] duration-300 ease-out motion-reduce:transition-none",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ )