@boyernick/standard-ui-react 0.1.1-canary.3 → 0.1.1-canary.34
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 +12 -9
- package/src/badge.tsx +73 -9
- package/src/block-editor.tsx +1747 -0
- package/src/brand.tsx +2 -2
- package/src/breadcrumb.tsx +46 -21
- package/src/button.tsx +23 -12
- package/src/calendar.tsx +25 -11
- package/src/card.tsx +57 -14
- package/src/carousel.tsx +339 -15
- package/src/checkbox.tsx +36 -13
- package/src/code-block.tsx +66 -5
- package/src/collapsible.tsx +7 -3
- package/src/combobox.tsx +17 -15
- package/src/command.tsx +41 -14
- package/src/context-menu.tsx +6 -3
- package/src/date-picker.tsx +174 -0
- 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 +55 -1
- package/src/illustrations.tsx +219 -141
- package/src/index.ts +263 -18
- package/src/input.tsx +8 -5
- package/src/kbd.tsx +49 -0
- package/src/lib/focus.ts +41 -0
- package/src/lib/motion.ts +16 -1
- package/src/lib/popup.ts +45 -0
- 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/menu.tsx +7 -4
- package/src/menubar.tsx +9 -2
- package/src/minimap.tsx +296 -0
- package/src/modal.tsx +608 -0
- package/src/navigation-menu.tsx +340 -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 +3 -1
- package/src/preview-card.tsx +3 -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 +12 -7
- 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/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}
|
|
@@ -56,6 +58,8 @@ export const CollapsiblePanel = ({
|
|
|
56
58
|
)}
|
|
57
59
|
{...props}
|
|
58
60
|
>
|
|
59
|
-
|
|
61
|
+
{/* px-3 matches the trigger's own padding, so the panel copy lines up
|
|
62
|
+
under the trigger's label instead of starting at the panel edge. */}
|
|
63
|
+
<div className="px-3 pt-2 leading-relaxed">{children}</div>
|
|
60
64
|
</BaseCollapsible.Panel>
|
|
61
65
|
)
|
package/src/combobox.tsx
CHANGED
|
@@ -4,7 +4,9 @@ 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"
|
|
9
|
+
import { popupInset, popupItem, popupLabel, popupMessage, popupSurface } from "./lib/popup"
|
|
8
10
|
|
|
9
11
|
export type ComboboxProps<
|
|
10
12
|
Value,
|
|
@@ -76,10 +78,9 @@ export const ComboboxInputGroup = ({
|
|
|
76
78
|
}: ComboboxInputGroupProps) => (
|
|
77
79
|
<BaseCombobox.InputGroup
|
|
78
80
|
className={cn(
|
|
79
|
-
"relative flex min-h-9 w-full items-center rounded-md border border-border-secondary bg-surface inset-shadow-outline-top
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"aria-invalid:border-destructive aria-invalid:focus-within:border-destructive aria-invalid:focus-within:ring-destructive/20",
|
|
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,
|
|
@@ -139,7 +140,7 @@ export const ComboboxClear = ({
|
|
|
139
140
|
{...props}
|
|
140
141
|
>
|
|
141
142
|
{children ?? (
|
|
142
|
-
<IconCrossSmall size={
|
|
143
|
+
<IconCrossSmall size={16} className="size-4" aria-hidden />
|
|
143
144
|
)}
|
|
144
145
|
</BaseCombobox.Clear>
|
|
145
146
|
)
|
|
@@ -160,7 +161,7 @@ export const ComboboxBackdrop = ({
|
|
|
160
161
|
)
|
|
161
162
|
|
|
162
163
|
export const ComboboxPositioner = ({
|
|
163
|
-
sideOffset =
|
|
164
|
+
sideOffset = 6,
|
|
164
165
|
className,
|
|
165
166
|
...props
|
|
166
167
|
}: ComboboxPositionerProps) => (
|
|
@@ -174,7 +175,8 @@ export const ComboboxPositioner = ({
|
|
|
174
175
|
export const ComboboxPopup = ({ className, ...props }: ComboboxPopupProps) => (
|
|
175
176
|
<BaseCombobox.Popup
|
|
176
177
|
className={cn(
|
|
177
|
-
"z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden
|
|
178
|
+
"z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden",
|
|
179
|
+
popupSurface,
|
|
178
180
|
motion.popupAnchor,
|
|
179
181
|
className,
|
|
180
182
|
)}
|
|
@@ -199,7 +201,7 @@ export const ComboboxArrow = ({ className, ...props }: ComboboxArrowProps) => (
|
|
|
199
201
|
|
|
200
202
|
export const ComboboxList = ({ className, ...props }: ComboboxListProps) => (
|
|
201
203
|
<BaseCombobox.List
|
|
202
|
-
className={cn("max-h-[inherit] overflow-y-auto
|
|
204
|
+
className={cn("max-h-[inherit] overflow-y-auto outline-none", popupInset, className)}
|
|
203
205
|
{...props}
|
|
204
206
|
/>
|
|
205
207
|
)
|
|
@@ -207,7 +209,7 @@ export const ComboboxList = ({ className, ...props }: ComboboxListProps) => (
|
|
|
207
209
|
export const ComboboxItem = ({ className, ...props }: ComboboxItemProps) => (
|
|
208
210
|
<BaseCombobox.Item
|
|
209
211
|
className={cn(
|
|
210
|
-
|
|
212
|
+
popupItem,
|
|
211
213
|
motion.colors,
|
|
212
214
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary data-highlighted:text-fg-primary",
|
|
213
215
|
className,
|
|
@@ -238,14 +240,14 @@ export const ComboboxItemIndicator = ({
|
|
|
238
240
|
|
|
239
241
|
export const ComboboxEmpty = ({ className, ...props }: ComboboxEmptyProps) => (
|
|
240
242
|
<BaseCombobox.Empty
|
|
241
|
-
className={cn("
|
|
243
|
+
className={cn(cn(popupMessage, "empty:hidden"), className)}
|
|
242
244
|
{...props}
|
|
243
245
|
/>
|
|
244
246
|
)
|
|
245
247
|
|
|
246
248
|
export const ComboboxStatus = ({ className, ...props }: ComboboxStatusProps) => (
|
|
247
249
|
<BaseCombobox.Status
|
|
248
|
-
className={cn(
|
|
250
|
+
className={cn(popupMessage, className)}
|
|
249
251
|
{...props}
|
|
250
252
|
/>
|
|
251
253
|
)
|
|
@@ -259,7 +261,7 @@ export const ComboboxGroupLabel = ({
|
|
|
259
261
|
...props
|
|
260
262
|
}: ComboboxGroupLabelProps) => (
|
|
261
263
|
<BaseCombobox.GroupLabel
|
|
262
|
-
className={cn(
|
|
264
|
+
className={cn(popupLabel, className)}
|
|
263
265
|
{...props}
|
|
264
266
|
/>
|
|
265
267
|
)
|
|
@@ -291,7 +293,7 @@ export const ComboboxChips = ({ className, ...props }: ComboboxChipsProps) => (
|
|
|
291
293
|
export const ComboboxChip = ({ className, ...props }: ComboboxChipProps) => (
|
|
292
294
|
<BaseCombobox.Chip
|
|
293
295
|
className={cn(
|
|
294
|
-
"inline-flex h-6 items-center gap-1 rounded-xs bg-background-tertiary px-1.5 text-xs text-fg-primary",
|
|
296
|
+
"inline-flex h-6 items-center gap-1 rounded-xs bg-background-tertiary px-1.5 text-xs-strong text-fg-primary",
|
|
295
297
|
className,
|
|
296
298
|
)}
|
|
297
299
|
{...props}
|
|
@@ -305,13 +307,13 @@ export const ComboboxChipRemove = ({
|
|
|
305
307
|
}: ComboboxChipRemoveProps) => (
|
|
306
308
|
<BaseCombobox.ChipRemove
|
|
307
309
|
className={cn(
|
|
308
|
-
"inline-flex size-3.5 cursor-pointer items-center justify-center text-fg-
|
|
310
|
+
"inline-flex size-3.5 cursor-pointer items-center justify-center text-fg-primary outline-none focus-visible:outline-none",
|
|
309
311
|
className,
|
|
310
312
|
)}
|
|
311
313
|
{...props}
|
|
312
314
|
>
|
|
313
315
|
{children ?? (
|
|
314
|
-
<IconCrossSmall size={
|
|
316
|
+
<IconCrossSmall size={14} className="size-3.5" aria-hidden />
|
|
315
317
|
)}
|
|
316
318
|
</BaseCombobox.ChipRemove>
|
|
317
319
|
)
|
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
|
)}
|
|
@@ -147,7 +153,6 @@ export const CommandInput = ({
|
|
|
147
153
|
aria-activedescendant={activeOptionId}
|
|
148
154
|
className={cn(
|
|
149
155
|
"h-9 min-w-0 flex-1 cursor-text rounded-md bg-transparent px-2.5 text-base text-fg-primary outline-none placeholder:text-fg-quaternary",
|
|
150
|
-
"focus-visible:bg-background-tertiary/60 focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
151
156
|
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none",
|
|
152
157
|
className,
|
|
153
158
|
)}
|
|
@@ -174,9 +179,10 @@ export const CommandClear = ({
|
|
|
174
179
|
<button
|
|
175
180
|
type="button"
|
|
176
181
|
className={cn(
|
|
177
|
-
"cursor-pointer rounded-full px-3
|
|
182
|
+
"inline-flex h-8 cursor-pointer items-center rounded-full border border-transparent px-3 text-sm text-fg-quaternary",
|
|
178
183
|
motion.colors,
|
|
179
|
-
|
|
184
|
+
focusRing,
|
|
185
|
+
"hover:bg-background-tertiary hover:text-fg-primary data-[active=true]:text-fg-primary",
|
|
180
186
|
className,
|
|
181
187
|
)}
|
|
182
188
|
{...props}
|
|
@@ -203,9 +209,10 @@ export const CommandClose = ({
|
|
|
203
209
|
}: CommandCloseProps) => (
|
|
204
210
|
<BaseDialog.Close
|
|
205
211
|
className={cn(
|
|
206
|
-
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full text-fg-primary
|
|
212
|
+
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-fg-primary",
|
|
207
213
|
motion.colors,
|
|
208
|
-
|
|
214
|
+
focusRing,
|
|
215
|
+
"hover:bg-background-tertiary",
|
|
209
216
|
className,
|
|
210
217
|
)}
|
|
211
218
|
aria-label={props["aria-label"] ?? "Close"}
|
|
@@ -223,7 +230,7 @@ export const CommandContent = ({
|
|
|
223
230
|
}: CommandContentProps) => (
|
|
224
231
|
<div
|
|
225
232
|
className={cn(
|
|
226
|
-
"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",
|
|
227
234
|
className,
|
|
228
235
|
)}
|
|
229
236
|
{...props}
|
|
@@ -237,7 +244,8 @@ export const CommandFilters = ({
|
|
|
237
244
|
<ul
|
|
238
245
|
role="list"
|
|
239
246
|
className={cn(
|
|
240
|
-
|
|
247
|
+
filterGroupVariants({ variant: "pill" }),
|
|
248
|
+
"w-full shrink-0 justify-start py-1.5",
|
|
241
249
|
className,
|
|
242
250
|
)}
|
|
243
251
|
{...props}
|
|
@@ -255,11 +263,10 @@ export const CommandFilter = ({
|
|
|
255
263
|
type="button"
|
|
256
264
|
aria-pressed={selected}
|
|
257
265
|
data-selected={selected || undefined}
|
|
266
|
+
data-pressed={selected || undefined}
|
|
258
267
|
className={cn(
|
|
259
|
-
|
|
268
|
+
filterItemVariants({ variant: "pill", size: "md" }),
|
|
260
269
|
motion.colors,
|
|
261
|
-
"hover:text-fg-primary focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
262
|
-
"data-selected:bg-background-tertiary data-selected:text-fg-primary",
|
|
263
270
|
className,
|
|
264
271
|
)}
|
|
265
272
|
{...props}
|
|
@@ -277,7 +284,7 @@ export const CommandList = ({ className, ...props }: CommandListProps) => {
|
|
|
277
284
|
id={listboxId}
|
|
278
285
|
role="listbox"
|
|
279
286
|
className={cn(
|
|
280
|
-
"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",
|
|
281
288
|
className,
|
|
282
289
|
)}
|
|
283
290
|
{...props}
|
|
@@ -295,12 +302,21 @@ export const CommandItem = ({
|
|
|
295
302
|
onClick,
|
|
296
303
|
...props
|
|
297
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
|
+
|
|
298
314
|
const itemClassName = cn(
|
|
299
|
-
"flex h-15 min-h-15 w-full cursor-pointer items-center justify-between gap-4 rounded-lg 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",
|
|
300
316
|
motion.colors,
|
|
301
317
|
"hover:bg-background-tertiary data-selected:bg-background-tertiary",
|
|
302
318
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
303
|
-
|
|
319
|
+
focusRing,
|
|
304
320
|
className,
|
|
305
321
|
)
|
|
306
322
|
|
|
@@ -308,6 +324,7 @@ export const CommandItem = ({
|
|
|
308
324
|
<li role="presentation" className="m-0">
|
|
309
325
|
{href ? (
|
|
310
326
|
<a
|
|
327
|
+
ref={ref}
|
|
311
328
|
id={id}
|
|
312
329
|
href={disabled ? undefined : href}
|
|
313
330
|
role="option"
|
|
@@ -323,6 +340,7 @@ export const CommandItem = ({
|
|
|
323
340
|
</a>
|
|
324
341
|
) : (
|
|
325
342
|
<button
|
|
343
|
+
ref={ref}
|
|
326
344
|
id={id}
|
|
327
345
|
type="button"
|
|
328
346
|
role="option"
|
|
@@ -369,6 +387,15 @@ export const CommandTitle = ({ className, children }: CommandTitleProps) => (
|
|
|
369
387
|
<span className={cn("min-w-0 truncate", className)}>{children}</span>
|
|
370
388
|
)
|
|
371
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
|
+
|
|
372
399
|
export const CommandDialogTitle = ({
|
|
373
400
|
className,
|
|
374
401
|
...props
|
package/src/context-menu.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
|
|
|
5
5
|
import { IconCheckmark1, IconChevronRightSmall } from "./icons"
|
|
6
6
|
import { cn } from "./lib/cn"
|
|
7
7
|
import { motion } from "./lib/motion"
|
|
8
|
+
import { popupInset, popupItem, popupLabel, popupSurface } from "./lib/popup"
|
|
8
9
|
|
|
9
10
|
export type ContextMenuProps = ComponentProps<typeof BaseContextMenu.Root>
|
|
10
11
|
export type ContextMenuTriggerProps = ComponentProps<
|
|
@@ -53,7 +54,7 @@ export type ContextMenuSubmenuTriggerProps = ComponentProps<
|
|
|
53
54
|
>
|
|
54
55
|
|
|
55
56
|
const itemClassName = cn(
|
|
56
|
-
|
|
57
|
+
popupItem,
|
|
57
58
|
motion.colors,
|
|
58
59
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary",
|
|
59
60
|
)
|
|
@@ -100,7 +101,9 @@ export const ContextMenuPopup = ({
|
|
|
100
101
|
}: ContextMenuPopupProps) => (
|
|
101
102
|
<BaseContextMenu.Popup
|
|
102
103
|
className={cn(
|
|
103
|
-
"z-50 min-w-40 overflow-hidden
|
|
104
|
+
"z-50 min-w-40 overflow-hidden",
|
|
105
|
+
popupSurface,
|
|
106
|
+
popupInset,
|
|
104
107
|
motion.popupAnchor,
|
|
105
108
|
className,
|
|
106
109
|
)}
|
|
@@ -165,7 +168,7 @@ export const ContextMenuGroupLabel = ({
|
|
|
165
168
|
...props
|
|
166
169
|
}: ContextMenuGroupLabelProps) => (
|
|
167
170
|
<BaseContextMenu.GroupLabel
|
|
168
|
-
className={cn(
|
|
171
|
+
className={cn(popupLabel, className)}
|
|
169
172
|
{...props}
|
|
170
173
|
/>
|
|
171
174
|
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react"
|
|
4
|
+
import type { DateRange, PropsBase } from "react-day-picker"
|
|
5
|
+
import { Button } from "./button"
|
|
6
|
+
import { Calendar } from "./calendar"
|
|
7
|
+
import { IconCalendar1 } from "./icons"
|
|
8
|
+
import { cn } from "./lib/cn"
|
|
9
|
+
import {
|
|
10
|
+
Popover,
|
|
11
|
+
PopoverPopup,
|
|
12
|
+
PopoverPortal,
|
|
13
|
+
PopoverPositioner,
|
|
14
|
+
PopoverTrigger,
|
|
15
|
+
} from "./popover"
|
|
16
|
+
|
|
17
|
+
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
|
18
|
+
month: "short",
|
|
19
|
+
day: "numeric",
|
|
20
|
+
year: "numeric",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const formatDate = (date: Date) => dateFormatter.format(date)
|
|
24
|
+
|
|
25
|
+
type DatePickerBaseProps = {
|
|
26
|
+
/** Options forwarded to Calendar, excluding its selection mode. */
|
|
27
|
+
calendarProps?: Omit<PropsBase, "mode" | "required">
|
|
28
|
+
/** Classes applied to the trigger button. */
|
|
29
|
+
className?: string
|
|
30
|
+
/** Disables the trigger button. */
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
/** Placeholder shown when no date is selected. */
|
|
33
|
+
placeholder?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DatePickerSingleProps = DatePickerBaseProps & {
|
|
37
|
+
mode?: "single"
|
|
38
|
+
selected?: Date
|
|
39
|
+
onSelect?: (date: Date | undefined) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type DatePickerRangeProps = DatePickerBaseProps & {
|
|
43
|
+
mode: "range"
|
|
44
|
+
selected?: DateRange
|
|
45
|
+
onSelect?: (range: DateRange | undefined) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps
|
|
49
|
+
|
|
50
|
+
const DatePickerTrigger = ({
|
|
51
|
+
className,
|
|
52
|
+
disabled,
|
|
53
|
+
empty,
|
|
54
|
+
label,
|
|
55
|
+
}: {
|
|
56
|
+
className?: string
|
|
57
|
+
disabled?: boolean
|
|
58
|
+
empty: boolean
|
|
59
|
+
label: string
|
|
60
|
+
}) => (
|
|
61
|
+
<PopoverTrigger
|
|
62
|
+
render={
|
|
63
|
+
<Button
|
|
64
|
+
type="button"
|
|
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"
|
|
68
|
+
variant="outline"
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
prefix={<IconCalendar1 aria-hidden />}
|
|
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)}
|
|
75
|
+
/>
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
"min-w-0 flex-1 truncate text-left",
|
|
81
|
+
empty && "text-fg-tertiary",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{label}
|
|
85
|
+
</span>
|
|
86
|
+
</PopoverTrigger>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const DatePickerPopup = ({ children }: { children: ReactNode }) => (
|
|
90
|
+
<PopoverPortal>
|
|
91
|
+
<PopoverPositioner align="start" sideOffset={6}>
|
|
92
|
+
<PopoverPopup className="w-auto gap-0 p-0">{children}</PopoverPopup>
|
|
93
|
+
</PopoverPositioner>
|
|
94
|
+
</PopoverPortal>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const SingleDatePicker = ({
|
|
98
|
+
calendarProps,
|
|
99
|
+
className,
|
|
100
|
+
disabled,
|
|
101
|
+
onSelect,
|
|
102
|
+
placeholder = "Pick a date",
|
|
103
|
+
selected,
|
|
104
|
+
}: DatePickerSingleProps) => {
|
|
105
|
+
const [open, setOpen] = useState(false)
|
|
106
|
+
|
|
107
|
+
const handleSelect = (date: Date | undefined) => {
|
|
108
|
+
onSelect?.(date)
|
|
109
|
+
if (date) setOpen(false)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
114
|
+
<DatePickerTrigger
|
|
115
|
+
className={className}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
empty={!selected}
|
|
118
|
+
label={selected ? formatDate(selected) : placeholder}
|
|
119
|
+
/>
|
|
120
|
+
<DatePickerPopup>
|
|
121
|
+
<Calendar
|
|
122
|
+
{...calendarProps}
|
|
123
|
+
className={cn("border-0", calendarProps?.className)}
|
|
124
|
+
mode="single"
|
|
125
|
+
selected={selected}
|
|
126
|
+
onSelect={handleSelect}
|
|
127
|
+
/>
|
|
128
|
+
</DatePickerPopup>
|
|
129
|
+
</Popover>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const RangeDatePicker = ({
|
|
134
|
+
calendarProps,
|
|
135
|
+
className,
|
|
136
|
+
disabled,
|
|
137
|
+
onSelect,
|
|
138
|
+
placeholder = "Pick a date range",
|
|
139
|
+
selected,
|
|
140
|
+
}: DatePickerRangeProps) => {
|
|
141
|
+
const [open, setOpen] = useState(false)
|
|
142
|
+
const label = selected?.from
|
|
143
|
+
? selected.to
|
|
144
|
+
? `${formatDate(selected.from)} – ${formatDate(selected.to)}`
|
|
145
|
+
: `${formatDate(selected.from)} – Select end date`
|
|
146
|
+
: placeholder
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
150
|
+
<DatePickerTrigger
|
|
151
|
+
className={className}
|
|
152
|
+
disabled={disabled}
|
|
153
|
+
empty={!selected?.from}
|
|
154
|
+
label={label}
|
|
155
|
+
/>
|
|
156
|
+
<DatePickerPopup>
|
|
157
|
+
<Calendar
|
|
158
|
+
{...calendarProps}
|
|
159
|
+
className={cn("border-0", calendarProps?.className)}
|
|
160
|
+
mode="range"
|
|
161
|
+
selected={selected}
|
|
162
|
+
onSelect={onSelect}
|
|
163
|
+
/>
|
|
164
|
+
</DatePickerPopup>
|
|
165
|
+
</Popover>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const DatePicker = (props: DatePickerProps) =>
|
|
170
|
+
props.mode === "range" ? (
|
|
171
|
+
<RangeDatePicker {...props} />
|
|
172
|
+
) : (
|
|
173
|
+
<SingleDatePicker {...props} />
|
|
174
|
+
)
|
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
|
)}
|