@boyernick/standard-ui-react 0.1.1-canary.8 → 0.2.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/package.json +5 -3
- package/src/accordion.tsx +5 -2
- package/src/alert-dialog.tsx +1 -1
- package/src/attachment.tsx +5 -2
- package/src/autocomplete.tsx +4 -3
- package/src/block-editor.tsx +1747 -0
- package/src/brand.tsx +2 -2
- package/src/breadcrumb.tsx +14 -7
- package/src/button.tsx +22 -11
- package/src/calendar.tsx +6 -3
- package/src/carousel.tsx +220 -53
- package/src/checkbox.tsx +3 -2
- package/src/code-block.tsx +66 -5
- package/src/collapsible.tsx +4 -2
- package/src/combobox.tsx +4 -3
- package/src/command.tsx +41 -13
- package/src/date-picker.tsx +7 -2
- package/src/dialog.tsx +33 -10
- package/src/empty.tsx +4 -4
- package/src/field.tsx +4 -3
- package/src/filter-group.tsx +143 -0
- package/src/icons.tsx +28 -1
- package/src/illustrations.tsx +219 -141
- package/src/index.ts +245 -18
- package/src/input.tsx +8 -5
- package/src/kbd.tsx +49 -0
- package/src/lib/focus.ts +41 -0
- package/src/lib/popup.ts +1 -1
- package/src/lifeline/company-icon.tsx +71 -0
- package/src/lifeline/index.ts +42 -0
- package/src/lifeline/lifeline-data.ts +97 -0
- package/src/lifeline/lifeline-desktop.tsx +204 -0
- package/src/lifeline/lifeline-event.tsx +108 -0
- package/src/lifeline/lifeline-fireworks.tsx +302 -0
- package/src/lifeline/lifeline-hover-image.tsx +269 -0
- package/src/lifeline/lifeline-icons.tsx +39 -0
- package/src/lifeline/lifeline-intro-timing.ts +105 -0
- package/src/lifeline/lifeline-labels.tsx +24 -0
- package/src/lifeline/lifeline-layout.ts +1 -0
- package/src/lifeline/lifeline-legend.tsx +31 -0
- package/src/lifeline/lifeline-lightbox.tsx +309 -0
- package/src/lifeline/lifeline-marker.tsx +183 -0
- package/src/lifeline/lifeline-people.tsx +99 -0
- package/src/lifeline/lifeline-photos.tsx +366 -0
- package/src/lifeline/lifeline-shell.tsx +135 -0
- package/src/lifeline/lifeline-utils.ts +83 -0
- package/src/lifeline/lifeline-vertical.tsx +482 -0
- package/src/lifeline/lifeline.tsx +69 -0
- package/src/lifeline/types.ts +112 -0
- package/src/lifeline/use-lifeline-intro.ts +130 -0
- package/src/lifeline/use-lifeline-scroll.ts +1011 -0
- package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
- package/src/menubar.tsx +9 -2
- package/src/minimap.tsx +296 -0
- package/src/modal.tsx +608 -0
- package/src/navigation-menu.tsx +338 -67
- package/src/number-field.tsx +336 -61
- package/src/otp-field.tsx +4 -3
- package/src/pagination.tsx +262 -42
- package/src/password-protection.tsx +345 -0
- package/src/popover.tsx +1 -1
- package/src/progress.tsx +5 -0
- package/src/questionnaire.tsx +284 -0
- package/src/radio.tsx +4 -2
- package/src/scroll-area.tsx +4 -1
- package/src/select.tsx +5 -2
- package/src/sidebar.tsx +113 -28
- package/src/slider.tsx +66 -6
- package/src/sounds.tsx +6 -10
- package/src/spinner.tsx +105 -32
- package/src/switch.tsx +4 -1
- package/src/table.tsx +82 -15
- package/src/tabs.tsx +189 -38
- package/src/text-animate.tsx +71 -28
- package/src/textarea.tsx +6 -1
- package/src/timeline.tsx +378 -0
- package/src/toast.tsx +214 -35
- package/src/toggle.tsx +4 -2
- package/src/toolbar.tsx +12 -7
- package/src/tooltip.tsx +43 -3
- package/src/video-player.tsx +396 -127
- package/src/image-modal.tsx +0 -110
- package/src/markdown-editor.tsx +0 -302
package/src/code-block.tsx
CHANGED
|
@@ -4,6 +4,10 @@ import { useState } from "react"
|
|
|
4
4
|
import { highlight, type LanguageName } from "sugar-high"
|
|
5
5
|
import { Button } from "./button"
|
|
6
6
|
import { IconCheckmark1, IconSquareBehindSquare6 } from "./icons"
|
|
7
|
+
import { cn } from "./lib/cn"
|
|
8
|
+
|
|
9
|
+
/** 1-based line number, or an inclusive `[start, end]` range. */
|
|
10
|
+
export type CodeBlockHighlightLine = number | readonly [number, number]
|
|
7
11
|
|
|
8
12
|
export type CodeBlockProps = {
|
|
9
13
|
code: string
|
|
@@ -13,12 +17,18 @@ export type CodeBlockProps = {
|
|
|
13
17
|
showHeader?: boolean
|
|
14
18
|
/** Removes border and radius chrome for nesting inside another frame. */
|
|
15
19
|
bare?: boolean
|
|
20
|
+
/** 1-based lines (or ranges) to emphasize inside the block. */
|
|
21
|
+
highlightLines?: readonly CodeBlockHighlightLine[]
|
|
22
|
+
/** 1-based lines added in a review (green). */
|
|
23
|
+
addedLines?: readonly CodeBlockHighlightLine[]
|
|
24
|
+
/** 1-based lines removed in a review (red). */
|
|
25
|
+
removedLines?: readonly CodeBlockHighlightLine[]
|
|
16
26
|
className?: string
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
const sizeClass = {
|
|
20
|
-
sm: "text-2xs
|
|
21
|
-
md: "text-xs
|
|
30
|
+
sm: "text-2xs py-3",
|
|
31
|
+
md: "text-xs py-4",
|
|
22
32
|
} as const
|
|
23
33
|
|
|
24
34
|
const languages = new Set<LanguageName>([
|
|
@@ -59,17 +69,60 @@ const getHighlightLanguage = (lang: string): LanguageName => {
|
|
|
59
69
|
return "typescript"
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
const toHighlightedLines = (
|
|
73
|
+
lines?: readonly CodeBlockHighlightLine[],
|
|
74
|
+
): Set<number> => {
|
|
75
|
+
const set = new Set<number>()
|
|
76
|
+
if (!lines) return set
|
|
77
|
+
|
|
78
|
+
for (const entry of lines) {
|
|
79
|
+
if (typeof entry === "number") {
|
|
80
|
+
set.add(entry)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const [start, end] = entry
|
|
85
|
+
const from = Math.min(start, end)
|
|
86
|
+
const to = Math.max(start, end)
|
|
87
|
+
for (let line = from; line <= to; line += 1) set.add(line)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return set
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
export const CodeBlock = ({
|
|
63
94
|
code: rawCode,
|
|
64
95
|
lang = "tsx",
|
|
65
96
|
size = "md",
|
|
66
97
|
showHeader = true,
|
|
67
98
|
bare = false,
|
|
99
|
+
highlightLines,
|
|
100
|
+
addedLines,
|
|
101
|
+
removedLines,
|
|
68
102
|
className = "",
|
|
69
103
|
}: CodeBlockProps) => {
|
|
70
104
|
const [copied, setCopied] = useState(false)
|
|
71
105
|
const code = rawCode.replace(/^\n/, "").replace(/\n$/, "")
|
|
72
|
-
const
|
|
106
|
+
const focused = toHighlightedLines(highlightLines)
|
|
107
|
+
const added = toHighlightedLines(addedLines)
|
|
108
|
+
const removed = toHighlightedLines(removedLines)
|
|
109
|
+
const html = highlight(code, {
|
|
110
|
+
lang: getHighlightLanguage(lang),
|
|
111
|
+
markLine: (line) => {
|
|
112
|
+
const number = line.index + 1
|
|
113
|
+
if (added.has(number)) {
|
|
114
|
+
line.className += " sh__line--added"
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if (removed.has(number)) {
|
|
118
|
+
line.className += " sh__line--removed"
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (focused.has(number)) {
|
|
122
|
+
line.className += " sh__line--highlighted"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
})
|
|
73
126
|
const frameClass =
|
|
74
127
|
!showHeader && !bare
|
|
75
128
|
? "rounded-lg border border-border-primary bg-surface"
|
|
@@ -87,7 +140,11 @@ export const CodeBlock = ({
|
|
|
87
140
|
|
|
88
141
|
const codeContent = (
|
|
89
142
|
<pre
|
|
90
|
-
className={
|
|
143
|
+
className={cn(
|
|
144
|
+
"sh-code overflow-x-auto font-mono",
|
|
145
|
+
sizeClass[size],
|
|
146
|
+
frameClass,
|
|
147
|
+
)}
|
|
91
148
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
92
149
|
/>
|
|
93
150
|
)
|
|
@@ -98,7 +155,11 @@ export const CodeBlock = ({
|
|
|
98
155
|
|
|
99
156
|
return (
|
|
100
157
|
<div
|
|
101
|
-
|
|
158
|
+
data-slot="code-block"
|
|
159
|
+
className={cn(
|
|
160
|
+
"overflow-hidden rounded-xl border border-border-primary bg-surface",
|
|
161
|
+
className,
|
|
162
|
+
)}
|
|
102
163
|
>
|
|
103
164
|
<div className="flex items-center justify-between border-b border-border-primary bg-background-secondary px-3 py-1.5 pl-4">
|
|
104
165
|
<p className="text-xs font-mono text-fg-quaternary">{lang}</p>
|
package/src/collapsible.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible"
|
|
|
4
4
|
import type { ComponentProps } from "react"
|
|
5
5
|
import { IconChevronDownSmall } from "./icons"
|
|
6
6
|
import { cn } from "./lib/cn"
|
|
7
|
+
import { focusRing } from "./lib/focus"
|
|
7
8
|
import { motion } from "./lib/motion"
|
|
8
9
|
|
|
9
10
|
export type CollapsibleProps = ComponentProps<typeof BaseCollapsible.Root>
|
|
@@ -23,9 +24,10 @@ export const CollapsibleTrigger = ({
|
|
|
23
24
|
}: CollapsibleTriggerProps) => (
|
|
24
25
|
<BaseCollapsible.Trigger
|
|
25
26
|
className={cn(
|
|
26
|
-
"group flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border-secondary bg-surface px-3 text-sm text-fg-primary inset-shadow-outline-top
|
|
27
|
+
"group flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border-secondary bg-surface px-3 text-sm text-fg-primary inset-shadow-outline-top",
|
|
27
28
|
motion.colors,
|
|
28
|
-
|
|
29
|
+
focusRing,
|
|
30
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
29
31
|
className,
|
|
30
32
|
)}
|
|
31
33
|
{...props}
|
package/src/combobox.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { Combobox as BaseCombobox } from "@base-ui/react/combobox"
|
|
|
4
4
|
import type { ComponentProps, ReactElement } from "react"
|
|
5
5
|
import { IconChevronDownSmall, IconCrossSmall } from "./icons"
|
|
6
6
|
import { cn } from "./lib/cn"
|
|
7
|
+
import { focusRingWithin, focusRingInvalidWithin } from "./lib/focus"
|
|
7
8
|
import { motion } from "./lib/motion"
|
|
8
9
|
import { popupInset, popupItem, popupLabel, popupMessage, popupSurface } from "./lib/popup"
|
|
9
10
|
|
|
@@ -77,9 +78,9 @@ export const ComboboxInputGroup = ({
|
|
|
77
78
|
}: ComboboxInputGroupProps) => (
|
|
78
79
|
<BaseCombobox.InputGroup
|
|
79
80
|
className={cn(
|
|
80
|
-
"relative flex min-h-9 w-full items-center rounded-md border border-border-secondary bg-surface inset-shadow-outline-top
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
"relative flex min-h-9 w-full items-center rounded-md border border-border-secondary bg-surface inset-shadow-outline-top transition-[color,box-shadow]",
|
|
82
|
+
focusRingWithin,
|
|
83
|
+
focusRingInvalidWithin,
|
|
83
84
|
"has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:focus-within:border-destructive has-[[aria-invalid=true]]:focus-within:ring-destructive/20",
|
|
84
85
|
"has-[:disabled]:opacity-50",
|
|
85
86
|
className,
|
package/src/command.tsx
CHANGED
|
@@ -4,8 +4,10 @@ import { Dialog as BaseDialog } from "@base-ui/react/dialog"
|
|
|
4
4
|
import {
|
|
5
5
|
createContext,
|
|
6
6
|
useContext,
|
|
7
|
+
useEffect,
|
|
7
8
|
useId,
|
|
8
9
|
useMemo,
|
|
10
|
+
useRef,
|
|
9
11
|
type ButtonHTMLAttributes,
|
|
10
12
|
type ComponentProps,
|
|
11
13
|
type HTMLAttributes,
|
|
@@ -13,7 +15,9 @@ import {
|
|
|
13
15
|
type ReactNode,
|
|
14
16
|
} from "react"
|
|
15
17
|
import { IconCrossSmall, IconMagnifyingGlass } from "./icons"
|
|
18
|
+
import { filterGroupVariants, filterItemVariants } from "./filter-group"
|
|
16
19
|
import { cn } from "./lib/cn"
|
|
20
|
+
import { focusRing } from "./lib/focus"
|
|
17
21
|
import { motion } from "./lib/motion"
|
|
18
22
|
|
|
19
23
|
type CommandContextValue = {
|
|
@@ -49,7 +53,9 @@ export type CommandClearProps = ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
49
53
|
export type CommandCloseProps = ComponentProps<typeof BaseDialog.Close>
|
|
50
54
|
export type CommandDividerProps = HTMLAttributes<HTMLDivElement>
|
|
51
55
|
export type CommandContentProps = HTMLAttributes<HTMLDivElement>
|
|
56
|
+
/** @deprecated Use FilterGroup for new filter controls. */
|
|
52
57
|
export type CommandFiltersProps = HTMLAttributes<HTMLUListElement>
|
|
58
|
+
/** @deprecated Use FilterItem for new filter controls. */
|
|
53
59
|
export type CommandFilterProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
54
60
|
selected?: boolean
|
|
55
61
|
}
|
|
@@ -108,7 +114,7 @@ export const CommandBackdrop = ({
|
|
|
108
114
|
export const CommandPopup = ({ className, ...props }: CommandPopupProps) => (
|
|
109
115
|
<BaseDialog.Popup
|
|
110
116
|
className={cn(
|
|
111
|
-
"fixed top-1/2 left-1/2 z-50 flex h-[min(80vh,32rem)] w-
|
|
117
|
+
"fixed top-1/2 left-1/2 z-50 flex h-[min(80vh,32rem)] w-[calc(100vw-2rem)] max-w-3xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl bg-surface-raised shadow-ambient outline-none",
|
|
112
118
|
motion.popupCenter,
|
|
113
119
|
className,
|
|
114
120
|
)}
|
|
@@ -173,9 +179,10 @@ export const CommandClear = ({
|
|
|
173
179
|
<button
|
|
174
180
|
type="button"
|
|
175
181
|
className={cn(
|
|
176
|
-
"inline-flex h-8 cursor-pointer items-center rounded-full border border-transparent px-3 text-sm text-fg-quaternary
|
|
182
|
+
"inline-flex h-8 cursor-pointer items-center rounded-full border border-transparent px-3 text-sm text-fg-quaternary",
|
|
177
183
|
motion.colors,
|
|
178
|
-
|
|
184
|
+
focusRing,
|
|
185
|
+
"hover:bg-background-tertiary hover:text-fg-primary data-[active=true]:text-fg-primary",
|
|
179
186
|
className,
|
|
180
187
|
)}
|
|
181
188
|
{...props}
|
|
@@ -202,9 +209,10 @@ export const CommandClose = ({
|
|
|
202
209
|
}: CommandCloseProps) => (
|
|
203
210
|
<BaseDialog.Close
|
|
204
211
|
className={cn(
|
|
205
|
-
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-fg-primary
|
|
212
|
+
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-fg-primary",
|
|
206
213
|
motion.colors,
|
|
207
|
-
|
|
214
|
+
focusRing,
|
|
215
|
+
"hover:bg-background-tertiary",
|
|
208
216
|
className,
|
|
209
217
|
)}
|
|
210
218
|
aria-label={props["aria-label"] ?? "Close"}
|
|
@@ -222,7 +230,7 @@ export const CommandContent = ({
|
|
|
222
230
|
}: CommandContentProps) => (
|
|
223
231
|
<div
|
|
224
232
|
className={cn(
|
|
225
|
-
"flex min-h-0 flex-1 flex-col gap-3 overflow-
|
|
233
|
+
"flex min-h-0 flex-1 flex-col gap-3 overflow-hidden px-4 pt-1 pb-5",
|
|
226
234
|
className,
|
|
227
235
|
)}
|
|
228
236
|
{...props}
|
|
@@ -236,7 +244,8 @@ export const CommandFilters = ({
|
|
|
236
244
|
<ul
|
|
237
245
|
role="list"
|
|
238
246
|
className={cn(
|
|
239
|
-
|
|
247
|
+
filterGroupVariants({ variant: "pill" }),
|
|
248
|
+
"w-full shrink-0 justify-start py-1.5",
|
|
240
249
|
className,
|
|
241
250
|
)}
|
|
242
251
|
{...props}
|
|
@@ -254,11 +263,10 @@ export const CommandFilter = ({
|
|
|
254
263
|
type="button"
|
|
255
264
|
aria-pressed={selected}
|
|
256
265
|
data-selected={selected || undefined}
|
|
266
|
+
data-pressed={selected || undefined}
|
|
257
267
|
className={cn(
|
|
258
|
-
|
|
268
|
+
filterItemVariants({ variant: "pill", size: "md" }),
|
|
259
269
|
motion.colors,
|
|
260
|
-
"hover:text-fg-primary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
261
|
-
"data-selected:bg-background-tertiary data-selected:text-fg-primary",
|
|
262
270
|
className,
|
|
263
271
|
)}
|
|
264
272
|
{...props}
|
|
@@ -276,7 +284,7 @@ export const CommandList = ({ className, ...props }: CommandListProps) => {
|
|
|
276
284
|
id={listboxId}
|
|
277
285
|
role="listbox"
|
|
278
286
|
className={cn(
|
|
279
|
-
"grid min-h-0 flex-1 content-start gap-0 overflow-
|
|
287
|
+
"grid min-h-0 flex-1 content-start gap-0 overflow-y-auto overscroll-contain p-1",
|
|
280
288
|
className,
|
|
281
289
|
)}
|
|
282
290
|
{...props}
|
|
@@ -294,12 +302,21 @@ export const CommandItem = ({
|
|
|
294
302
|
onClick,
|
|
295
303
|
...props
|
|
296
304
|
}: CommandItemProps) => {
|
|
305
|
+
const ref = useRef<HTMLAnchorElement & HTMLButtonElement>(null)
|
|
306
|
+
|
|
307
|
+
// Keyboard selection moves the highlight without moving focus, so nothing
|
|
308
|
+
// scrolls the list on its own. `nearest` only acts when the row is actually
|
|
309
|
+
// out of view, which leaves hover-driven selection alone.
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (selected) ref.current?.scrollIntoView({ block: "nearest" })
|
|
312
|
+
}, [selected])
|
|
313
|
+
|
|
297
314
|
const itemClassName = cn(
|
|
298
|
-
"flex h-15 min-h-15 w-full cursor-pointer items-center justify-between gap-4 rounded-lg border border-transparent px-4 text-left text-sm font-medium text-fg-primary
|
|
315
|
+
"flex h-15 min-h-15 w-full cursor-pointer items-center justify-between gap-4 rounded-lg border border-transparent px-4 text-left text-sm font-medium text-fg-primary select-none",
|
|
299
316
|
motion.colors,
|
|
300
317
|
"hover:bg-background-tertiary data-selected:bg-background-tertiary",
|
|
301
318
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
302
|
-
|
|
319
|
+
focusRing,
|
|
303
320
|
className,
|
|
304
321
|
)
|
|
305
322
|
|
|
@@ -307,6 +324,7 @@ export const CommandItem = ({
|
|
|
307
324
|
<li role="presentation" className="m-0">
|
|
308
325
|
{href ? (
|
|
309
326
|
<a
|
|
327
|
+
ref={ref}
|
|
310
328
|
id={id}
|
|
311
329
|
href={disabled ? undefined : href}
|
|
312
330
|
role="option"
|
|
@@ -322,6 +340,7 @@ export const CommandItem = ({
|
|
|
322
340
|
</a>
|
|
323
341
|
) : (
|
|
324
342
|
<button
|
|
343
|
+
ref={ref}
|
|
325
344
|
id={id}
|
|
326
345
|
type="button"
|
|
327
346
|
role="option"
|
|
@@ -368,6 +387,15 @@ export const CommandTitle = ({ className, children }: CommandTitleProps) => (
|
|
|
368
387
|
<span className={cn("min-w-0 truncate", className)}>{children}</span>
|
|
369
388
|
)
|
|
370
389
|
|
|
390
|
+
export type CommandMetaProps = HTMLAttributes<HTMLSpanElement>
|
|
391
|
+
|
|
392
|
+
export const CommandMeta = ({ className, ...props }: CommandMetaProps) => (
|
|
393
|
+
<span
|
|
394
|
+
className={cn("shrink-0 text-xs font-normal text-fg-tertiary", className)}
|
|
395
|
+
{...props}
|
|
396
|
+
/>
|
|
397
|
+
)
|
|
398
|
+
|
|
371
399
|
export const CommandDialogTitle = ({
|
|
372
400
|
className,
|
|
373
401
|
...props
|
package/src/date-picker.tsx
CHANGED
|
@@ -62,11 +62,16 @@ const DatePickerTrigger = ({
|
|
|
62
62
|
render={
|
|
63
63
|
<Button
|
|
64
64
|
type="button"
|
|
65
|
-
|
|
65
|
+
// Default md height, so the trigger lines up with every other field
|
|
66
|
+
// in a form rather than sitting 4px short of them.
|
|
67
|
+
size="md"
|
|
66
68
|
variant="outline"
|
|
67
69
|
disabled={disabled}
|
|
68
70
|
prefix={<IconCalendar1 aria-hidden />}
|
|
69
|
-
|
|
71
|
+
// The outline button fills on hover, which is right for a button and
|
|
72
|
+
// wrong here: this reads as a field, and fields in the system keep
|
|
73
|
+
// their surface. The border and the popover carry the state.
|
|
74
|
+
className={cn("w-56 justify-start hover:bg-surface", className)}
|
|
70
75
|
/>
|
|
71
76
|
}
|
|
72
77
|
>
|
package/src/dialog.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Dialog as BaseDialog } from "@base-ui/react/dialog"
|
|
4
4
|
import type { ComponentProps, HTMLAttributes } from "react"
|
|
5
|
+
import { useRef } from "react"
|
|
5
6
|
import { cn } from "./lib/cn"
|
|
6
7
|
import { motion } from "./lib/motion"
|
|
7
8
|
|
|
@@ -41,16 +42,38 @@ export const DialogBackdrop = ({
|
|
|
41
42
|
/>
|
|
42
43
|
)
|
|
43
44
|
|
|
44
|
-
export const DialogPopup = ({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
export const DialogPopup = ({
|
|
46
|
+
className,
|
|
47
|
+
initialFocus,
|
|
48
|
+
ref,
|
|
49
|
+
...props
|
|
50
|
+
}: DialogPopupProps) => {
|
|
51
|
+
const popupRef = useRef<HTMLDivElement | null>(null)
|
|
52
|
+
|
|
53
|
+
const setRef = (node: HTMLDivElement | null) => {
|
|
54
|
+
popupRef.current = node
|
|
55
|
+
if (typeof ref === "function") ref(node)
|
|
56
|
+
else if (ref) ref.current = node
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<BaseDialog.Popup
|
|
61
|
+
ref={setRef}
|
|
62
|
+
// Base UI focuses the first tabbable element by default. In a dialog long
|
|
63
|
+
// enough to scroll that is usually the action at the bottom, and focusing
|
|
64
|
+
// it scrolls the body to the end before the reader has seen the start —
|
|
65
|
+
// so the popup takes focus itself, which is what its `tabindex="-1"` is
|
|
66
|
+
// for. Callers can still name their own target.
|
|
67
|
+
initialFocus={initialFocus ?? popupRef}
|
|
68
|
+
className={cn(
|
|
69
|
+
"fixed top-1/2 left-1/2 z-50 flex w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col gap-6 rounded-xl border border-border-primary bg-surface-raised p-5 shadow-md outline-none max-h-[calc(100dvh-2rem)] overflow-y-auto max-sm:max-w-[calc(100vw-2rem)]",
|
|
70
|
+
motion.popupCenter,
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
54
77
|
|
|
55
78
|
export const DialogTitle = ({ className, ...props }: DialogTitleProps) => (
|
|
56
79
|
<BaseDialog.Title
|
package/src/empty.tsx
CHANGED
|
@@ -10,7 +10,7 @@ export type EmptyActionsProps = ComponentProps<"div">;
|
|
|
10
10
|
export const Empty = ({ className, ...props }: EmptyProps) => (
|
|
11
11
|
<div
|
|
12
12
|
className={cn(
|
|
13
|
-
"flex min-h-48 flex-col items-center justify-center
|
|
13
|
+
"flex min-h-48 flex-col items-center justify-center rounded-xl border border-dashed border-border-primary bg-surface p-8 text-center",
|
|
14
14
|
className,
|
|
15
15
|
)}
|
|
16
16
|
{...props}
|
|
@@ -20,7 +20,7 @@ export const Empty = ({ className, ...props }: EmptyProps) => (
|
|
|
20
20
|
export const EmptyIcon = ({ className, ...props }: EmptyIconProps) => (
|
|
21
21
|
<div
|
|
22
22
|
className={cn(
|
|
23
|
-
"flex size-10 items-center justify-center rounded-lg bg-background-tertiary text-fg-secondary [&_svg]:size-5",
|
|
23
|
+
"mb-4 flex size-10 items-center justify-center rounded-lg bg-background-tertiary text-fg-secondary [&_svg]:size-5",
|
|
24
24
|
className,
|
|
25
25
|
)}
|
|
26
26
|
{...props}
|
|
@@ -36,7 +36,7 @@ export const EmptyDescription = ({
|
|
|
36
36
|
...props
|
|
37
37
|
}: EmptyDescriptionProps) => (
|
|
38
38
|
<p
|
|
39
|
-
className={cn("max-w-sm text-sm text-fg-secondary", className)}
|
|
39
|
+
className={cn("mt-1 max-w-sm text-sm text-fg-secondary", className)}
|
|
40
40
|
{...props}
|
|
41
41
|
/>
|
|
42
42
|
);
|
|
@@ -44,7 +44,7 @@ export const EmptyDescription = ({
|
|
|
44
44
|
export const EmptyActions = ({ className, ...props }: EmptyActionsProps) => (
|
|
45
45
|
<div
|
|
46
46
|
className={cn(
|
|
47
|
-
"mt-
|
|
47
|
+
"mt-4 flex flex-wrap items-center justify-center gap-2",
|
|
48
48
|
className,
|
|
49
49
|
)}
|
|
50
50
|
{...props}
|
package/src/field.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Field as BaseField } from "@base-ui/react/field"
|
|
4
4
|
import type { ComponentProps } from "react"
|
|
5
5
|
import { cn } from "./lib/cn"
|
|
6
|
+
import { focusRing, focusRingInvalid } from "./lib/focus"
|
|
6
7
|
|
|
7
8
|
export type FieldProps = ComponentProps<typeof BaseField.Root>
|
|
8
9
|
export type FieldLabelProps = ComponentProps<typeof BaseField.Label>
|
|
@@ -49,10 +50,10 @@ export const FieldError = ({ className, ...props }: FieldErrorProps) => (
|
|
|
49
50
|
export const FieldControl = ({ className, ...props }: FieldControlProps) => (
|
|
50
51
|
<BaseField.Control
|
|
51
52
|
className={cn(
|
|
52
|
-
"text-sm flex h-9 w-full cursor-text rounded-md border border-border-secondary bg-surface px-3 text-fg-primary inset-shadow-outline-top
|
|
53
|
-
|
|
53
|
+
"text-sm flex h-9 w-full cursor-text rounded-md border border-border-secondary bg-surface px-3 text-fg-primary inset-shadow-outline-top transition-[color,box-shadow] duration-[var(--duration-sm)] ease-enter placeholder:text-fg-quaternary",
|
|
54
|
+
focusRing,
|
|
55
|
+
focusRingInvalid,
|
|
54
56
|
"data-invalid:border-destructive data-invalid:focus-visible:border-destructive data-invalid:focus-visible:ring-destructive/20",
|
|
55
|
-
"aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20",
|
|
56
57
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
57
58
|
className,
|
|
58
59
|
)}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Toggle as BaseToggle } from "@base-ui/react/toggle"
|
|
4
|
+
import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group"
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
|
+
import { createContext, type ComponentProps, useContext } from "react"
|
|
7
|
+
import { cn } from "./lib/cn"
|
|
8
|
+
import { focusRing } from "./lib/focus"
|
|
9
|
+
import { motion } from "./lib/motion"
|
|
10
|
+
|
|
11
|
+
const filterGroupVariants = cva("flex w-fit items-center", {
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
pill: "flex-wrap gap-1",
|
|
15
|
+
segmented: "gap-0.5 rounded-lg bg-background-tertiary p-0.5",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "pill",
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const filterItemVariants = cva(
|
|
24
|
+
cn(
|
|
25
|
+
"group/filter-item inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap border border-transparent text-fg-tertiary hover:text-fg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 data-pressed:text-fg-primary",
|
|
26
|
+
focusRing,
|
|
27
|
+
),
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
pill: "rounded-full hover:bg-background-tertiary data-pressed:bg-background-tertiary",
|
|
32
|
+
segmented:
|
|
33
|
+
"rounded-md hover:bg-background-quaternary data-pressed:bg-surface-raised data-pressed:shadow-hairline",
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
sm: "text-xs",
|
|
37
|
+
md: "text-sm",
|
|
38
|
+
lg: "text-sm",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
compoundVariants: [
|
|
42
|
+
{ variant: "pill", size: "sm", class: "h-8 px-3" },
|
|
43
|
+
{ variant: "pill", size: "md", class: "h-9 px-4" },
|
|
44
|
+
{ variant: "pill", size: "lg", class: "h-10 px-5" },
|
|
45
|
+
{ variant: "segmented", size: "sm", class: "h-7 px-2.5" },
|
|
46
|
+
{ variant: "segmented", size: "md", class: "h-8 px-3" },
|
|
47
|
+
{ variant: "segmented", size: "lg", class: "h-9 px-3.5" },
|
|
48
|
+
],
|
|
49
|
+
defaultVariants: {
|
|
50
|
+
variant: "pill",
|
|
51
|
+
size: "md",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export type FilterGroupVariant = NonNullable<
|
|
57
|
+
VariantProps<typeof filterGroupVariants>["variant"]
|
|
58
|
+
>
|
|
59
|
+
export type FilterGroupSize = NonNullable<
|
|
60
|
+
VariantProps<typeof filterItemVariants>["size"]
|
|
61
|
+
>
|
|
62
|
+
|
|
63
|
+
type FilterGroupStyleContextValue = {
|
|
64
|
+
variant: FilterGroupVariant
|
|
65
|
+
size: FilterGroupSize
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const FilterGroupStyleContext = createContext<FilterGroupStyleContextValue>({
|
|
69
|
+
variant: "pill",
|
|
70
|
+
size: "md",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export type FilterGroupProps = ComponentProps<typeof BaseToggleGroup> &
|
|
74
|
+
VariantProps<typeof filterGroupVariants> & {
|
|
75
|
+
/** Whether pressing the active item can clear a single-select group. */
|
|
76
|
+
allowEmpty?: boolean
|
|
77
|
+
size?: FilterGroupSize
|
|
78
|
+
}
|
|
79
|
+
export type FilterItemProps = ComponentProps<typeof BaseToggle> &
|
|
80
|
+
VariantProps<typeof filterItemVariants>
|
|
81
|
+
export type FilterCountProps = ComponentProps<"span">
|
|
82
|
+
|
|
83
|
+
export const FilterGroup = ({
|
|
84
|
+
className,
|
|
85
|
+
variant = "pill",
|
|
86
|
+
size = "md",
|
|
87
|
+
allowEmpty = false,
|
|
88
|
+
multiple = false,
|
|
89
|
+
onValueChange,
|
|
90
|
+
...props
|
|
91
|
+
}: FilterGroupProps) => (
|
|
92
|
+
<FilterGroupStyleContext.Provider value={{ variant: variant ?? "pill", size }}>
|
|
93
|
+
<BaseToggleGroup
|
|
94
|
+
className={cn(filterGroupVariants({ variant }), className)}
|
|
95
|
+
multiple={multiple}
|
|
96
|
+
onValueChange={(value, eventDetails) => {
|
|
97
|
+
if (!multiple && !allowEmpty && value.length === 0) {
|
|
98
|
+
eventDetails.cancel()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
onValueChange?.(value, eventDetails)
|
|
102
|
+
}}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
</FilterGroupStyleContext.Provider>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const FilterItem = ({
|
|
109
|
+
className,
|
|
110
|
+
variant,
|
|
111
|
+
size,
|
|
112
|
+
type = "button",
|
|
113
|
+
...props
|
|
114
|
+
}: FilterItemProps) => {
|
|
115
|
+
const styles = useContext(FilterGroupStyleContext)
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<BaseToggle
|
|
119
|
+
type={type}
|
|
120
|
+
className={cn(
|
|
121
|
+
filterItemVariants({
|
|
122
|
+
variant: variant ?? styles.variant,
|
|
123
|
+
size: size ?? styles.size,
|
|
124
|
+
}),
|
|
125
|
+
motion.colors,
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const FilterCount = ({ className, ...props }: FilterCountProps) => (
|
|
134
|
+
<span
|
|
135
|
+
className={cn(
|
|
136
|
+
"text-2xs-strong inline-flex min-w-4 items-center justify-center rounded-full bg-background-quaternary px-1 py-0.5 text-fg-secondary tabular-nums group-data-pressed/filter-item:bg-surface",
|
|
137
|
+
className,
|
|
138
|
+
)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
export { filterGroupVariants, filterItemVariants }
|