@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.
- package/README.md +9 -0
- package/package.json +46 -0
- package/src/accordion.tsx +84 -0
- package/src/alert-dialog.tsx +101 -0
- package/src/attachment.tsx +138 -0
- package/src/autocomplete.tsx +281 -0
- package/src/avatar.tsx +56 -0
- package/src/badge.tsx +28 -0
- package/src/brand.tsx +70 -0
- package/src/breadcrumb.tsx +81 -0
- package/src/button.tsx +129 -0
- package/src/calendar.tsx +84 -0
- package/src/card.tsx +54 -0
- package/src/carousel.tsx +253 -0
- package/src/chart.tsx +185 -0
- package/src/checkbox-group.tsx +14 -0
- package/src/checkbox.tsx +31 -0
- package/src/code-block.tsx +124 -0
- package/src/collapsible.tsx +61 -0
- package/src/combobox.tsx +324 -0
- package/src/command.tsx +377 -0
- package/src/context-menu.tsx +250 -0
- package/src/dialog.tsx +78 -0
- package/src/drawer.tsx +156 -0
- package/src/empty.tsx +52 -0
- package/src/field.tsx +75 -0
- package/src/fieldset.tsx +25 -0
- package/src/form.tsx +14 -0
- package/src/icons.tsx +91 -0
- package/src/illustrations.tsx +167 -0
- package/src/image-modal.tsx +110 -0
- package/src/index.ts +833 -0
- package/src/input.tsx +68 -0
- package/src/lib/cn.ts +3 -0
- package/src/lib/motion.ts +23 -0
- package/src/markdown-editor.tsx +302 -0
- package/src/menu.tsx +205 -0
- package/src/menubar.tsx +22 -0
- package/src/meter.tsx +61 -0
- package/src/navigation-menu.tsx +217 -0
- package/src/number-field.tsx +119 -0
- package/src/orb.tsx +33 -0
- package/src/otp-field.tsx +45 -0
- package/src/pagination.tsx +91 -0
- package/src/popover.tsx +92 -0
- package/src/preview-card.tsx +103 -0
- package/src/progress.tsx +60 -0
- package/src/radio.tsx +43 -0
- package/src/scroll-area.tsx +67 -0
- package/src/select.tsx +161 -0
- package/src/separator.tsx +17 -0
- package/src/sidebar.tsx +82 -0
- package/src/skeleton.tsx +32 -0
- package/src/slider.tsx +56 -0
- package/src/sounds.tsx +270 -0
- package/src/spinner.tsx +45 -0
- package/src/switch.tsx +19 -0
- package/src/table.tsx +81 -0
- package/src/tabs.tsx +62 -0
- package/src/text-animate.tsx +155 -0
- package/src/textarea.tsx +58 -0
- package/src/ticker.tsx +74 -0
- package/src/toast.tsx +142 -0
- package/src/toggle.tsx +32 -0
- package/src/toolbar.tsx +75 -0
- package/src/tooltip.tsx +55 -0
- 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,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
|
+
)
|
package/src/menubar.tsx
ADDED
|
@@ -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
|
+
)
|