@preset.nz/ux-kit 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +90 -2
- package/package.json +51 -3
- package/src/components/avatar.tsx +35 -0
- package/src/components/badge.tsx +42 -0
- package/src/components/breadcrumb.tsx +102 -0
- package/src/components/button.tsx +56 -0
- package/src/components/card.tsx +92 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/color-field.tsx +248 -0
- package/src/components/color-swatch.tsx +52 -0
- package/src/components/context-menu.tsx +249 -0
- package/src/components/dialog.tsx +137 -0
- package/src/components/dropdown-menu.tsx +249 -0
- package/src/components/empty-state.tsx +32 -0
- package/src/components/input.tsx +20 -0
- package/src/components/label.tsx +18 -0
- package/src/components/navigation-menu.tsx +144 -0
- package/src/components/number-field.tsx +428 -0
- package/src/components/popover.tsx +82 -0
- package/src/components/select.tsx +201 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/sheet.tsx +124 -0
- package/src/components/side-panel.tsx +166 -0
- package/src/components/sidebar.tsx +735 -0
- package/src/components/skeleton.tsx +14 -0
- package/src/components/slider.tsx +75 -0
- package/src/components/snackbar.tsx +141 -0
- package/src/components/status-bar.tsx +38 -0
- package/src/components/tabs.tsx +78 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +86 -0
- package/src/components/toggle.tsx +43 -0
- package/src/components/toolbar.tsx +307 -0
- package/src/components/tooltip.tsx +54 -0
- package/src/graphics/adsr.ts +70 -0
- package/src/graphics/curve-editor.tsx +525 -0
- package/src/graphics/curve-edits.ts +129 -0
- package/src/graphics/index.ts +16 -0
- package/src/graphics/scale.ts +22 -0
- package/src/graphics/use-canvas.ts +56 -0
- package/src/graphics/use-tokens.ts +32 -0
- package/src/icons.ts +3 -0
- package/src/index.css +199 -0
- package/src/index.ts +37 -0
- package/src/lib/use-mobile.ts +18 -0
- package/src/lib/utils.ts +6 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
import { ColorSwatch } from "./color-swatch"
|
|
5
|
+
import { Input } from "./input"
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
|
7
|
+
|
|
8
|
+
// ColorField: swatch, hex text, optional alpha, preset grid, native picker.
|
|
9
|
+
// Union of M&T's ColorField (presets, hex input), Oblique's colour renderers
|
|
10
|
+
// (native picker, read-only hex, one picker session = one undo step via
|
|
11
|
+
// onPickStart/onPickEnd, optional colour), and Strata's swatch popover
|
|
12
|
+
// (clearable, no-colour chip). Values are hex strings: #rrggbb, or #rrggbbaa
|
|
13
|
+
// when `alpha` is set. The palette engine (presets and derivation) is app
|
|
14
|
+
// domain data and stays in the app; it feeds `presets` and nothing more.
|
|
15
|
+
|
|
16
|
+
interface Rgba {
|
|
17
|
+
r: number
|
|
18
|
+
g: number
|
|
19
|
+
b: number
|
|
20
|
+
a: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Parses #rgb, #rgba, #rrggbb or #rrggbbaa (leading # optional). Alpha is 0 to 1. */
|
|
24
|
+
function parseHex(input: string): Rgba | null {
|
|
25
|
+
let h = input.trim().replace(/^#/, "")
|
|
26
|
+
if (!/^[0-9a-f]+$/i.test(h)) return null
|
|
27
|
+
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("")
|
|
28
|
+
if (h.length !== 6 && h.length !== 8) return null
|
|
29
|
+
const n = (i: number) => parseInt(h.slice(i, i + 2), 16)
|
|
30
|
+
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) / 255 : 1 }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toHex({ r, g, b, a }: Rgba, withAlpha: boolean): string {
|
|
34
|
+
const p = (v: number) => Math.round(v).toString(16).padStart(2, "0")
|
|
35
|
+
return `#${p(r)}${p(g)}${p(b)}${withAlpha ? p(a * 255) : ""}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ColorFieldProps
|
|
39
|
+
extends Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> {
|
|
40
|
+
/** #rrggbb or #rrggbbaa. `null` shows the empty chip and a dash. */
|
|
41
|
+
value: string | null
|
|
42
|
+
/** Called with a normalised lowercase hex, or null from the clear button. */
|
|
43
|
+
onChange?: (value: string | null) => void
|
|
44
|
+
label?: string
|
|
45
|
+
/** Shows an alpha percentage field; output is #rrggbbaa. Default false. */
|
|
46
|
+
alpha?: boolean
|
|
47
|
+
/** Preset colours as hex. Omit for none. */
|
|
48
|
+
presets?: string[]
|
|
49
|
+
/** Adds a "no colour" chip to the popover that calls `onChange(null)`. */
|
|
50
|
+
clearable?: boolean
|
|
51
|
+
/** Shows the colour and hex but cannot change them. Also the state when `onChange` is missing. */
|
|
52
|
+
readOnly?: boolean
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
/** The colour a null value falls back to for the native picker. Default #000000. */
|
|
55
|
+
fallback?: string
|
|
56
|
+
/** The native picker opened or closed: the app's undo transaction boundary. */
|
|
57
|
+
onPickStart?: () => void
|
|
58
|
+
onPickEnd?: () => void
|
|
59
|
+
/** Extra controls after the fields, e.g. an eyedropper button. */
|
|
60
|
+
trailing?: React.ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ColorField({
|
|
64
|
+
value: valueProp,
|
|
65
|
+
onChange,
|
|
66
|
+
label,
|
|
67
|
+
alpha = false,
|
|
68
|
+
presets,
|
|
69
|
+
clearable = false,
|
|
70
|
+
readOnly,
|
|
71
|
+
disabled,
|
|
72
|
+
fallback = "#000000",
|
|
73
|
+
onPickStart,
|
|
74
|
+
onPickEnd,
|
|
75
|
+
trailing,
|
|
76
|
+
id,
|
|
77
|
+
className,
|
|
78
|
+
...props
|
|
79
|
+
}: ColorFieldProps) {
|
|
80
|
+
const generatedId = React.useId()
|
|
81
|
+
const inputId = id ?? generatedId
|
|
82
|
+
const locked = !!readOnly || !onChange
|
|
83
|
+
// During a picker session the field shows only the colour picked so far and ignores incoming
|
|
84
|
+
// `value` props (an app that round-trips each change would fight the picker); on the picker
|
|
85
|
+
// closing it re-syncs to the prop.
|
|
86
|
+
const [live, setLive] = React.useState<string | null>(null)
|
|
87
|
+
const value = live ?? valueProp
|
|
88
|
+
const rgba = value ? parseHex(value) : null
|
|
89
|
+
const shown = value ? (rgba ? toHex(rgba, alpha) : value) : ""
|
|
90
|
+
|
|
91
|
+
const [draft, setDraft] = React.useState<string | null>(null)
|
|
92
|
+
const [alphaDraft, setAlphaDraft] = React.useState<string | null>(null)
|
|
93
|
+
|
|
94
|
+
const emit = (next: Rgba) => onChange?.(toHex(next, alpha))
|
|
95
|
+
|
|
96
|
+
const commitHex = () => {
|
|
97
|
+
if (draft === null) return
|
|
98
|
+
const parsed = parseHex(draft)
|
|
99
|
+
setDraft(null)
|
|
100
|
+
if (!parsed) return // invalid text reverts to the current value
|
|
101
|
+
// An entry without alpha digits keeps the existing alpha.
|
|
102
|
+
const digits = draft.trim().replace(/^#/, "").length
|
|
103
|
+
const hasAlpha = digits === 4 || digits === 8
|
|
104
|
+
emit(alpha && rgba && !hasAlpha ? { ...parsed, a: rgba.a } : parsed)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const commitAlpha = () => {
|
|
108
|
+
if (alphaDraft === null) return
|
|
109
|
+
const pct = Number(alphaDraft)
|
|
110
|
+
setAlphaDraft(null)
|
|
111
|
+
if (!Number.isFinite(pct)) return
|
|
112
|
+
emit({ ...(rgba ?? parseHex(fallback)!), a: Math.min(100, Math.max(0, pct)) / 100 })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pick = (hex: string, session = false) => {
|
|
116
|
+
const parsed = parseHex(hex)
|
|
117
|
+
if (!parsed) return
|
|
118
|
+
if (session) setLive(toHex(parsed, false))
|
|
119
|
+
// Presets and the native picker carry no alpha of their own unless 8-digit.
|
|
120
|
+
emit(hex.replace(/^#/, "").length === 8 || !rgba ? parsed : { ...parsed, a: rgba.a })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const swatchLabel = label ? `${label} colour` : "Colour"
|
|
124
|
+
const chip = <ColorSwatch color={value} size="lg" className="size-7" />
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div data-slot="color-field" className={cn("flex flex-col gap-1", className)} {...props}>
|
|
128
|
+
{label ? (
|
|
129
|
+
<label htmlFor={inputId} className="text-[11px] font-medium tracking-wide text-muted-foreground">
|
|
130
|
+
{label}
|
|
131
|
+
</label>
|
|
132
|
+
) : null}
|
|
133
|
+
<div className={cn("flex items-center gap-2", disabled && "opacity-50")}>
|
|
134
|
+
{locked || disabled ? (
|
|
135
|
+
chip
|
|
136
|
+
) : !presets?.length && !clearable ? (
|
|
137
|
+
// Nothing to offer but the picker: the swatch opens it directly.
|
|
138
|
+
<label
|
|
139
|
+
className="relative shrink-0 cursor-pointer rounded-sm focus-within:ring-1 focus-within:ring-ring"
|
|
140
|
+
title={swatchLabel}
|
|
141
|
+
>
|
|
142
|
+
{chip}
|
|
143
|
+
<input
|
|
144
|
+
type="color"
|
|
145
|
+
aria-label={`${swatchLabel} picker`}
|
|
146
|
+
value={rgba ? toHex(rgba, false) : toHex(parseHex(fallback) ?? { r: 0, g: 0, b: 0, a: 1 }, false)}
|
|
147
|
+
onChange={(e) => pick(e.target.value, true)}
|
|
148
|
+
onFocus={onPickStart}
|
|
149
|
+
onBlur={() => {
|
|
150
|
+
setLive(null)
|
|
151
|
+
onPickEnd?.()
|
|
152
|
+
}}
|
|
153
|
+
className="absolute inset-0 size-full cursor-pointer opacity-0"
|
|
154
|
+
/>
|
|
155
|
+
</label>
|
|
156
|
+
) : (
|
|
157
|
+
<Popover>
|
|
158
|
+
<PopoverTrigger
|
|
159
|
+
aria-label={swatchLabel}
|
|
160
|
+
className="shrink-0 rounded-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
161
|
+
>
|
|
162
|
+
{chip}
|
|
163
|
+
</PopoverTrigger>
|
|
164
|
+
<PopoverContent align="start" className="w-auto gap-2 p-2">
|
|
165
|
+
{presets?.length || clearable ? (
|
|
166
|
+
<div className="grid grid-cols-7 gap-1" role="group" aria-label="Presets">
|
|
167
|
+
{presets?.map((c) => (
|
|
168
|
+
<button
|
|
169
|
+
key={c}
|
|
170
|
+
type="button"
|
|
171
|
+
aria-label={c}
|
|
172
|
+
aria-pressed={value?.toLowerCase() === c.toLowerCase()}
|
|
173
|
+
onClick={() => pick(c)}
|
|
174
|
+
className="rounded-xs outline-none hover:scale-110 focus-visible:ring-1 focus-visible:ring-ring aria-pressed:ring-2 aria-pressed:ring-foreground/40"
|
|
175
|
+
>
|
|
176
|
+
<ColorSwatch color={c} size="lg" />
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
{clearable ? (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
aria-label="No colour"
|
|
183
|
+
onClick={() => onChange?.(null)}
|
|
184
|
+
className="rounded-xs outline-none hover:scale-110 focus-visible:ring-1 focus-visible:ring-ring"
|
|
185
|
+
>
|
|
186
|
+
<ColorSwatch color={null} size="lg" />
|
|
187
|
+
</button>
|
|
188
|
+
) : null}
|
|
189
|
+
</div>
|
|
190
|
+
) : null}
|
|
191
|
+
<input
|
|
192
|
+
type="color"
|
|
193
|
+
aria-label={`${swatchLabel} picker`}
|
|
194
|
+
value={rgba ? toHex(rgba, false) : toHex(parseHex(fallback) ?? { r: 0, g: 0, b: 0, a: 1 }, false)}
|
|
195
|
+
onChange={(e) => pick(e.target.value, true)}
|
|
196
|
+
onFocus={onPickStart}
|
|
197
|
+
onBlur={() => {
|
|
198
|
+
setLive(null)
|
|
199
|
+
onPickEnd?.()
|
|
200
|
+
}}
|
|
201
|
+
className="h-8 w-full cursor-pointer rounded-xs border border-border bg-transparent"
|
|
202
|
+
/>
|
|
203
|
+
</PopoverContent>
|
|
204
|
+
</Popover>
|
|
205
|
+
)}
|
|
206
|
+
<Input
|
|
207
|
+
id={inputId}
|
|
208
|
+
value={draft ?? (value ? shown : "")}
|
|
209
|
+
placeholder="—"
|
|
210
|
+
readOnly={locked}
|
|
211
|
+
disabled={disabled}
|
|
212
|
+
spellCheck={false}
|
|
213
|
+
aria-label={label ? `${label} hex` : "Hex"}
|
|
214
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
215
|
+
onBlur={commitHex}
|
|
216
|
+
onKeyDown={(e) => {
|
|
217
|
+
if (e.key === "Enter") commitHex()
|
|
218
|
+
if (e.key === "Escape") setDraft(null)
|
|
219
|
+
}}
|
|
220
|
+
className="flex-1 font-mono"
|
|
221
|
+
/>
|
|
222
|
+
{alpha ? (
|
|
223
|
+
<div className="flex w-16 shrink-0 items-center gap-1 text-xs text-muted-foreground">
|
|
224
|
+
<Input
|
|
225
|
+
inputMode="numeric"
|
|
226
|
+
value={alphaDraft ?? (rgba ? String(Math.round(rgba.a * 100)) : "")}
|
|
227
|
+
readOnly={locked}
|
|
228
|
+
disabled={disabled}
|
|
229
|
+
aria-label={label ? `${label} alpha` : "Alpha"}
|
|
230
|
+
onChange={(e) => setAlphaDraft(e.target.value)}
|
|
231
|
+
onBlur={commitAlpha}
|
|
232
|
+
onKeyDown={(e) => {
|
|
233
|
+
if (e.key === "Enter") commitAlpha()
|
|
234
|
+
if (e.key === "Escape") setAlphaDraft(null)
|
|
235
|
+
}}
|
|
236
|
+
className="text-right tabular-nums"
|
|
237
|
+
/>
|
|
238
|
+
%
|
|
239
|
+
</div>
|
|
240
|
+
) : null}
|
|
241
|
+
{trailing}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { ColorField, parseHex, toHex }
|
|
248
|
+
export type { ColorFieldProps }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
// A colour chip. Any CSS colour string works; `null` draws the dashed "no
|
|
6
|
+
// colour" chip (Strata's colour label, Oblique's disabled optional colour).
|
|
7
|
+
// A checkerboard sits under the colour so transparency reads as transparency.
|
|
8
|
+
|
|
9
|
+
const CHECKER =
|
|
10
|
+
"conic-gradient(var(--muted) 25%, transparent 0 50%, var(--muted) 0 75%, transparent 0) 0 0 / 8px 8px"
|
|
11
|
+
|
|
12
|
+
interface ColorSwatchProps extends Omit<React.ComponentProps<"span">, "color"> {
|
|
13
|
+
/** A CSS colour (#rrggbbaa, rgb(), a variable), or null for none. */
|
|
14
|
+
color?: string | null
|
|
15
|
+
/** `sm` 12px, `md` 16px, `lg` 24px. */
|
|
16
|
+
size?: "sm" | "md" | "lg"
|
|
17
|
+
shape?: "square" | "round"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ColorSwatch({
|
|
21
|
+
color,
|
|
22
|
+
size = "md",
|
|
23
|
+
shape = "square",
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
...props
|
|
27
|
+
}: ColorSwatchProps) {
|
|
28
|
+
return (
|
|
29
|
+
<span
|
|
30
|
+
data-slot="color-swatch"
|
|
31
|
+
data-empty={color ? undefined : ""}
|
|
32
|
+
role={props["aria-label"] ? "img" : undefined}
|
|
33
|
+
aria-hidden={props["aria-label"] ? undefined : true}
|
|
34
|
+
className={cn(
|
|
35
|
+
"relative inline-block shrink-0 overflow-hidden border border-border",
|
|
36
|
+
size === "sm" && "size-3",
|
|
37
|
+
size === "md" && "size-4",
|
|
38
|
+
size === "lg" && "size-6",
|
|
39
|
+
shape === "round" ? "rounded-full" : "rounded-xs",
|
|
40
|
+
!color && "border-dashed border-muted-foreground/60",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
style={color ? { background: CHECKER, ...style } : style}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{color ? <span className="absolute inset-0" style={{ backgroundColor: color }} /> : null}
|
|
47
|
+
</span>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { ColorSwatch }
|
|
52
|
+
export type { ColorSwatchProps }
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
|
|
3
|
+
import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
|
|
8
|
+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
|
|
12
|
+
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ContextMenuTrigger({ className, ...props }: ContextMenuPrimitive.Trigger.Props) {
|
|
16
|
+
return (
|
|
17
|
+
<ContextMenuPrimitive.Trigger
|
|
18
|
+
data-slot="context-menu-trigger"
|
|
19
|
+
className={cn("select-none", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ContextMenuContent({
|
|
26
|
+
align = "start",
|
|
27
|
+
alignOffset = 0,
|
|
28
|
+
side = "bottom",
|
|
29
|
+
sideOffset = 4,
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: ContextMenuPrimitive.Popup.Props &
|
|
33
|
+
Pick<ContextMenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
|
34
|
+
return (
|
|
35
|
+
<ContextMenuPrimitive.Portal>
|
|
36
|
+
<ContextMenuPrimitive.Positioner
|
|
37
|
+
className="isolate z-50 outline-none"
|
|
38
|
+
align={align}
|
|
39
|
+
alignOffset={alignOffset}
|
|
40
|
+
side={side}
|
|
41
|
+
sideOffset={sideOffset}
|
|
42
|
+
>
|
|
43
|
+
<ContextMenuPrimitive.Popup
|
|
44
|
+
data-slot="context-menu-content"
|
|
45
|
+
className={cn(
|
|
46
|
+
"z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
</ContextMenuPrimitive.Positioner>
|
|
52
|
+
</ContextMenuPrimitive.Portal>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
|
|
57
|
+
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ContextMenuLabel({
|
|
61
|
+
className,
|
|
62
|
+
inset,
|
|
63
|
+
...props
|
|
64
|
+
}: ContextMenuPrimitive.GroupLabel.Props & { inset?: boolean }) {
|
|
65
|
+
return (
|
|
66
|
+
<ContextMenuPrimitive.GroupLabel
|
|
67
|
+
data-slot="context-menu-label"
|
|
68
|
+
data-inset={inset}
|
|
69
|
+
className={cn(
|
|
70
|
+
"px-2 py-1.5 text-xs font-medium text-muted-foreground data-inset:pl-8",
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ContextMenuItem({
|
|
79
|
+
className,
|
|
80
|
+
inset,
|
|
81
|
+
variant = "default",
|
|
82
|
+
...props
|
|
83
|
+
}: ContextMenuPrimitive.Item.Props & {
|
|
84
|
+
inset?: boolean
|
|
85
|
+
variant?: "default" | "destructive"
|
|
86
|
+
}) {
|
|
87
|
+
return (
|
|
88
|
+
<ContextMenuPrimitive.Item
|
|
89
|
+
data-slot="context-menu-item"
|
|
90
|
+
data-inset={inset}
|
|
91
|
+
data-variant={variant}
|
|
92
|
+
className={cn(
|
|
93
|
+
"group/context-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
|
94
|
+
className
|
|
95
|
+
)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
|
|
102
|
+
return <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ContextMenuSubTrigger({
|
|
106
|
+
className,
|
|
107
|
+
inset,
|
|
108
|
+
children,
|
|
109
|
+
...props
|
|
110
|
+
}: ContextMenuPrimitive.SubmenuTrigger.Props & { inset?: boolean }) {
|
|
111
|
+
return (
|
|
112
|
+
<ContextMenuPrimitive.SubmenuTrigger
|
|
113
|
+
data-slot="context-menu-sub-trigger"
|
|
114
|
+
data-inset={inset}
|
|
115
|
+
className={cn(
|
|
116
|
+
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
<CaretRightIcon className="ml-auto" />
|
|
123
|
+
</ContextMenuPrimitive.SubmenuTrigger>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ContextMenuSubContent({
|
|
128
|
+
align = "start",
|
|
129
|
+
alignOffset = -3,
|
|
130
|
+
side = "right",
|
|
131
|
+
sideOffset = 0,
|
|
132
|
+
className,
|
|
133
|
+
...props
|
|
134
|
+
}: React.ComponentProps<typeof ContextMenuContent>) {
|
|
135
|
+
return (
|
|
136
|
+
<ContextMenuContent
|
|
137
|
+
data-slot="context-menu-sub-content"
|
|
138
|
+
className={cn("min-w-32 shadow-lg", className)}
|
|
139
|
+
align={align}
|
|
140
|
+
alignOffset={alignOffset}
|
|
141
|
+
side={side}
|
|
142
|
+
sideOffset={sideOffset}
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ContextMenuCheckboxItem({
|
|
149
|
+
className,
|
|
150
|
+
children,
|
|
151
|
+
inset,
|
|
152
|
+
...props
|
|
153
|
+
}: ContextMenuPrimitive.CheckboxItem.Props & { inset?: boolean }) {
|
|
154
|
+
return (
|
|
155
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
156
|
+
data-slot="context-menu-checkbox-item"
|
|
157
|
+
data-inset={inset}
|
|
158
|
+
className={cn(
|
|
159
|
+
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
>
|
|
164
|
+
<span
|
|
165
|
+
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
|
166
|
+
data-slot="context-menu-checkbox-item-indicator"
|
|
167
|
+
>
|
|
168
|
+
<ContextMenuPrimitive.CheckboxItemIndicator>
|
|
169
|
+
<CheckIcon />
|
|
170
|
+
</ContextMenuPrimitive.CheckboxItemIndicator>
|
|
171
|
+
</span>
|
|
172
|
+
{children}
|
|
173
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function ContextMenuRadioGroup({ ...props }: ContextMenuPrimitive.RadioGroup.Props) {
|
|
178
|
+
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ContextMenuRadioItem({
|
|
182
|
+
className,
|
|
183
|
+
children,
|
|
184
|
+
inset,
|
|
185
|
+
...props
|
|
186
|
+
}: ContextMenuPrimitive.RadioItem.Props & { inset?: boolean }) {
|
|
187
|
+
return (
|
|
188
|
+
<ContextMenuPrimitive.RadioItem
|
|
189
|
+
data-slot="context-menu-radio-item"
|
|
190
|
+
data-inset={inset}
|
|
191
|
+
className={cn(
|
|
192
|
+
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
193
|
+
className
|
|
194
|
+
)}
|
|
195
|
+
{...props}
|
|
196
|
+
>
|
|
197
|
+
<span
|
|
198
|
+
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
|
199
|
+
data-slot="context-menu-radio-item-indicator"
|
|
200
|
+
>
|
|
201
|
+
<ContextMenuPrimitive.RadioItemIndicator>
|
|
202
|
+
<CheckIcon />
|
|
203
|
+
</ContextMenuPrimitive.RadioItemIndicator>
|
|
204
|
+
</span>
|
|
205
|
+
{children}
|
|
206
|
+
</ContextMenuPrimitive.RadioItem>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ContextMenuSeparator({ className, ...props }: ContextMenuPrimitive.Separator.Props) {
|
|
211
|
+
return (
|
|
212
|
+
<ContextMenuPrimitive.Separator
|
|
213
|
+
data-slot="context-menu-separator"
|
|
214
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
215
|
+
{...props}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
|
221
|
+
return (
|
|
222
|
+
<span
|
|
223
|
+
data-slot="context-menu-shortcut"
|
|
224
|
+
className={cn(
|
|
225
|
+
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
|
|
226
|
+
className
|
|
227
|
+
)}
|
|
228
|
+
{...props}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
ContextMenu,
|
|
235
|
+
ContextMenuPortal,
|
|
236
|
+
ContextMenuTrigger,
|
|
237
|
+
ContextMenuContent,
|
|
238
|
+
ContextMenuGroup,
|
|
239
|
+
ContextMenuLabel,
|
|
240
|
+
ContextMenuItem,
|
|
241
|
+
ContextMenuCheckboxItem,
|
|
242
|
+
ContextMenuRadioGroup,
|
|
243
|
+
ContextMenuRadioItem,
|
|
244
|
+
ContextMenuSeparator,
|
|
245
|
+
ContextMenuShortcut,
|
|
246
|
+
ContextMenuSub,
|
|
247
|
+
ContextMenuSubTrigger,
|
|
248
|
+
ContextMenuSubContent,
|
|
249
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
|
3
|
+
import { XIcon } from "@phosphor-icons/react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { Button } from "./button"
|
|
7
|
+
|
|
8
|
+
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
9
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
|
13
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
|
17
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
|
21
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
|
|
25
|
+
return (
|
|
26
|
+
<DialogPrimitive.Backdrop
|
|
27
|
+
data-slot="dialog-overlay"
|
|
28
|
+
className={cn(
|
|
29
|
+
"fixed inset-0 isolate z-50 bg-foreground/15 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function DialogContent({
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
showCloseButton = true,
|
|
41
|
+
...props
|
|
42
|
+
}: DialogPrimitive.Popup.Props & {
|
|
43
|
+
showCloseButton?: boolean
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<DialogPortal>
|
|
47
|
+
<DialogOverlay />
|
|
48
|
+
<DialogPrimitive.Popup
|
|
49
|
+
data-slot="dialog-content"
|
|
50
|
+
className={cn(
|
|
51
|
+
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
{showCloseButton && (
|
|
58
|
+
<DialogPrimitive.Close
|
|
59
|
+
data-slot="dialog-close"
|
|
60
|
+
render={<Button variant="ghost" className="absolute top-4 right-4" size="icon-sm" />}
|
|
61
|
+
>
|
|
62
|
+
<XIcon />
|
|
63
|
+
<span className="sr-only">Close</span>
|
|
64
|
+
</DialogPrimitive.Close>
|
|
65
|
+
)}
|
|
66
|
+
</DialogPrimitive.Popup>
|
|
67
|
+
</DialogPortal>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-slot="dialog-header"
|
|
75
|
+
className={cn("flex flex-col gap-2", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function DialogFooter({
|
|
82
|
+
className,
|
|
83
|
+
showCloseButton = false,
|
|
84
|
+
children,
|
|
85
|
+
...props
|
|
86
|
+
}: React.ComponentProps<"div"> & {
|
|
87
|
+
showCloseButton?: boolean
|
|
88
|
+
}) {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
data-slot="dialog-footer"
|
|
92
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
{showCloseButton && (
|
|
97
|
+
<DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
104
|
+
return (
|
|
105
|
+
<DialogPrimitive.Title
|
|
106
|
+
data-slot="dialog-title"
|
|
107
|
+
className={cn("font-heading leading-none font-medium", className)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
|
|
114
|
+
return (
|
|
115
|
+
<DialogPrimitive.Description
|
|
116
|
+
data-slot="dialog-description"
|
|
117
|
+
className={cn(
|
|
118
|
+
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
Dialog,
|
|
128
|
+
DialogClose,
|
|
129
|
+
DialogContent,
|
|
130
|
+
DialogDescription,
|
|
131
|
+
DialogFooter,
|
|
132
|
+
DialogHeader,
|
|
133
|
+
DialogOverlay,
|
|
134
|
+
DialogPortal,
|
|
135
|
+
DialogTitle,
|
|
136
|
+
DialogTrigger,
|
|
137
|
+
}
|