@boyernick/standard-ui-react 0.1.1-canary.9 → 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.
Files changed (82) hide show
  1. package/package.json +5 -3
  2. package/src/accordion.tsx +5 -2
  3. package/src/alert-dialog.tsx +1 -1
  4. package/src/attachment.tsx +5 -2
  5. package/src/autocomplete.tsx +4 -3
  6. package/src/block-editor.tsx +1747 -0
  7. package/src/brand.tsx +2 -2
  8. package/src/breadcrumb.tsx +14 -7
  9. package/src/button.tsx +22 -11
  10. package/src/calendar.tsx +6 -3
  11. package/src/carousel.tsx +220 -53
  12. package/src/checkbox.tsx +3 -2
  13. package/src/code-block.tsx +66 -5
  14. package/src/collapsible.tsx +4 -2
  15. package/src/combobox.tsx +4 -3
  16. package/src/command.tsx +27 -12
  17. package/src/dialog.tsx +33 -10
  18. package/src/empty.tsx +4 -4
  19. package/src/field.tsx +4 -3
  20. package/src/filter-group.tsx +143 -0
  21. package/src/icons.tsx +28 -1
  22. package/src/illustrations.tsx +219 -141
  23. package/src/index.ts +238 -18
  24. package/src/input.tsx +8 -5
  25. package/src/kbd.tsx +10 -4
  26. package/src/lib/focus.ts +41 -0
  27. package/src/lib/popup.ts +1 -1
  28. package/src/lifeline/company-icon.tsx +71 -0
  29. package/src/lifeline/index.ts +42 -0
  30. package/src/lifeline/lifeline-data.ts +97 -0
  31. package/src/lifeline/lifeline-desktop.tsx +204 -0
  32. package/src/lifeline/lifeline-event.tsx +108 -0
  33. package/src/lifeline/lifeline-fireworks.tsx +302 -0
  34. package/src/lifeline/lifeline-hover-image.tsx +269 -0
  35. package/src/lifeline/lifeline-icons.tsx +39 -0
  36. package/src/lifeline/lifeline-intro-timing.ts +105 -0
  37. package/src/lifeline/lifeline-labels.tsx +24 -0
  38. package/src/lifeline/lifeline-layout.ts +1 -0
  39. package/src/lifeline/lifeline-legend.tsx +31 -0
  40. package/src/lifeline/lifeline-lightbox.tsx +309 -0
  41. package/src/lifeline/lifeline-marker.tsx +183 -0
  42. package/src/lifeline/lifeline-people.tsx +99 -0
  43. package/src/lifeline/lifeline-photos.tsx +366 -0
  44. package/src/lifeline/lifeline-shell.tsx +135 -0
  45. package/src/lifeline/lifeline-utils.ts +83 -0
  46. package/src/lifeline/lifeline-vertical.tsx +482 -0
  47. package/src/lifeline/lifeline.tsx +69 -0
  48. package/src/lifeline/types.ts +112 -0
  49. package/src/lifeline/use-lifeline-intro.ts +130 -0
  50. package/src/lifeline/use-lifeline-scroll.ts +1011 -0
  51. package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
  52. package/src/menubar.tsx +9 -2
  53. package/src/minimap.tsx +296 -0
  54. package/src/modal.tsx +608 -0
  55. package/src/navigation-menu.tsx +338 -67
  56. package/src/number-field.tsx +336 -61
  57. package/src/otp-field.tsx +4 -3
  58. package/src/pagination.tsx +262 -42
  59. package/src/password-protection.tsx +345 -0
  60. package/src/popover.tsx +1 -1
  61. package/src/progress.tsx +5 -0
  62. package/src/questionnaire.tsx +284 -0
  63. package/src/radio.tsx +4 -2
  64. package/src/scroll-area.tsx +4 -1
  65. package/src/select.tsx +5 -2
  66. package/src/sidebar.tsx +113 -28
  67. package/src/slider.tsx +66 -6
  68. package/src/sounds.tsx +6 -10
  69. package/src/spinner.tsx +105 -32
  70. package/src/switch.tsx +4 -1
  71. package/src/table.tsx +82 -15
  72. package/src/tabs.tsx +189 -38
  73. package/src/text-animate.tsx +71 -28
  74. package/src/textarea.tsx +6 -1
  75. package/src/timeline.tsx +378 -0
  76. package/src/toast.tsx +214 -35
  77. package/src/toggle.tsx +4 -2
  78. package/src/toolbar.tsx +12 -7
  79. package/src/tooltip.tsx +43 -3
  80. package/src/video-player.tsx +396 -127
  81. package/src/image-modal.tsx +0 -110
  82. package/src/markdown-editor.tsx +0 -302
@@ -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 p-3",
21
- md: "text-xs p-4",
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 html = highlight(code, { lang: getHighlightLanguage(lang) })
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={`sh-code overflow-x-auto font-mono ${sizeClass[size]} ${frameClass}`}
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
- className={`overflow-hidden rounded-xl border border-border-primary bg-surface ${className}`}
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>
@@ -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 outline-none",
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
- "outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-disabled:cursor-not-allowed data-disabled:opacity-50",
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 outline-none transition-[color,box-shadow]",
81
- "focus-within:border-ring focus-within:ring-[3px] focus-within:ring-offset-1 focus-within:ring-offset-background-primary focus-within:ring-ring/20",
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,
package/src/command.tsx CHANGED
@@ -15,7 +15,9 @@ import {
15
15
  type ReactNode,
16
16
  } from "react"
17
17
  import { IconCrossSmall, IconMagnifyingGlass } from "./icons"
18
+ import { filterGroupVariants, filterItemVariants } from "./filter-group"
18
19
  import { cn } from "./lib/cn"
20
+ import { focusRing } from "./lib/focus"
19
21
  import { motion } from "./lib/motion"
20
22
 
21
23
  type CommandContextValue = {
@@ -51,7 +53,9 @@ export type CommandClearProps = ButtonHTMLAttributes<HTMLButtonElement>
51
53
  export type CommandCloseProps = ComponentProps<typeof BaseDialog.Close>
52
54
  export type CommandDividerProps = HTMLAttributes<HTMLDivElement>
53
55
  export type CommandContentProps = HTMLAttributes<HTMLDivElement>
56
+ /** @deprecated Use FilterGroup for new filter controls. */
54
57
  export type CommandFiltersProps = HTMLAttributes<HTMLUListElement>
58
+ /** @deprecated Use FilterItem for new filter controls. */
55
59
  export type CommandFilterProps = ButtonHTMLAttributes<HTMLButtonElement> & {
56
60
  selected?: boolean
57
61
  }
@@ -110,7 +114,7 @@ export const CommandBackdrop = ({
110
114
  export const CommandPopup = ({ className, ...props }: CommandPopupProps) => (
111
115
  <BaseDialog.Popup
112
116
  className={cn(
113
- "fixed top-1/2 left-1/2 z-50 flex h-[min(80vh,32rem)] w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl bg-surface-raised shadow-ambient outline-none max-sm:max-w-[calc(100vw-2rem)]",
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",
114
118
  motion.popupCenter,
115
119
  className,
116
120
  )}
@@ -175,9 +179,10 @@ export const CommandClear = ({
175
179
  <button
176
180
  type="button"
177
181
  className={cn(
178
- "inline-flex h-8 cursor-pointer items-center rounded-full border border-transparent px-3 text-sm text-fg-quaternary outline-none",
182
+ "inline-flex h-8 cursor-pointer items-center rounded-full border border-transparent px-3 text-sm text-fg-quaternary",
179
183
  motion.colors,
180
- "hover:bg-background-tertiary 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 data-[active=true]:text-fg-primary",
184
+ focusRing,
185
+ "hover:bg-background-tertiary hover:text-fg-primary data-[active=true]:text-fg-primary",
181
186
  className,
182
187
  )}
183
188
  {...props}
@@ -204,9 +209,10 @@ export const CommandClose = ({
204
209
  }: CommandCloseProps) => (
205
210
  <BaseDialog.Close
206
211
  className={cn(
207
- "inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-fg-primary outline-none",
212
+ "inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-fg-primary",
208
213
  motion.colors,
209
- "hover:bg-background-tertiary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
214
+ focusRing,
215
+ "hover:bg-background-tertiary",
210
216
  className,
211
217
  )}
212
218
  aria-label={props["aria-label"] ?? "Close"}
@@ -238,7 +244,8 @@ export const CommandFilters = ({
238
244
  <ul
239
245
  role="list"
240
246
  className={cn(
241
- "flex shrink-0 flex-wrap justify-start gap-1.5 py-1.5",
247
+ filterGroupVariants({ variant: "pill" }),
248
+ "w-full shrink-0 justify-start py-1.5",
242
249
  className,
243
250
  )}
244
251
  {...props}
@@ -256,11 +263,10 @@ export const CommandFilter = ({
256
263
  type="button"
257
264
  aria-pressed={selected}
258
265
  data-selected={selected || undefined}
266
+ data-pressed={selected || undefined}
259
267
  className={cn(
260
- "inline-flex h-9 cursor-pointer items-center rounded-full border border-transparent px-4 text-sm text-fg-quaternary outline-none",
268
+ filterItemVariants({ variant: "pill", size: "md" }),
261
269
  motion.colors,
262
- "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",
263
- "data-selected:bg-background-tertiary data-selected:text-fg-primary",
264
270
  className,
265
271
  )}
266
272
  {...props}
@@ -278,7 +284,7 @@ export const CommandList = ({ className, ...props }: CommandListProps) => {
278
284
  id={listboxId}
279
285
  role="listbox"
280
286
  className={cn(
281
- "grid min-h-0 flex-1 content-start gap-0 overflow-y-auto overscroll-contain py-0.5",
287
+ "grid min-h-0 flex-1 content-start gap-0 overflow-y-auto overscroll-contain p-1",
282
288
  className,
283
289
  )}
284
290
  {...props}
@@ -306,11 +312,11 @@ export const CommandItem = ({
306
312
  }, [selected])
307
313
 
308
314
  const itemClassName = cn(
309
- "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 outline-none select-none",
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",
310
316
  motion.colors,
311
317
  "hover:bg-background-tertiary data-selected:bg-background-tertiary",
312
318
  "data-disabled:cursor-not-allowed data-disabled:opacity-50",
313
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
319
+ focusRing,
314
320
  className,
315
321
  )
316
322
 
@@ -381,6 +387,15 @@ export const CommandTitle = ({ className, children }: CommandTitleProps) => (
381
387
  <span className={cn("min-w-0 truncate", className)}>{children}</span>
382
388
  )
383
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
+
384
399
  export const CommandDialogTitle = ({
385
400
  className,
386
401
  ...props
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 = ({ className, ...props }: DialogPopupProps) => (
45
- <BaseDialog.Popup
46
- className={cn(
47
- "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-sm:max-w-[calc(100vw-2rem)]",
48
- motion.popupCenter,
49
- className,
50
- )}
51
- {...props}
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 gap-3 rounded-xl border border-dashed border-border-primary bg-surface p-8 text-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-1 flex flex-wrap items-center justify-center gap-2",
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 outline-none transition-[color,box-shadow] duration-[var(--duration-sm)] ease-enter placeholder:text-fg-quaternary",
53
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
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 }
package/src/icons.tsx CHANGED
@@ -1,9 +1,11 @@
1
1
  "use client"
2
2
 
3
3
  import type { CentralIconBaseProps } from "@central-icons-react/round-outlined-radius-2-stroke-2/CentralIconBase"
4
+ import { IconArrowDown as IconArrowDownBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconArrowDown"
4
5
  import { IconBell as IconBellBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconBell"
5
6
  import { IconCalendar1 as IconCalendar1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCalendar1"
6
7
  import { IconChainLink1 as IconChainLink1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChainLink1"
8
+ import { IconChainLink3 as IconChainLink3Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChainLink3"
7
9
  import { IconCheckmark1 as IconCheckmark1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCheckmark1"
8
10
  import { IconChevronBottom as IconChevronBottomBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChevronBottom"
9
11
  import { IconChevronDownSmall as IconChevronDownSmallBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChevronDownSmall"
@@ -13,18 +15,26 @@ import { IconCircleInfo as IconCircleInfoBase } from "@central-icons-react/round
13
15
  import { IconClipboard as IconClipboardBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconClipboard"
14
16
  import { IconCrossSmall as IconCrossSmallBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCrossSmall"
15
17
  import { IconDotGrid1x3Horizontal as IconDotGrid1x3HorizontalBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconDotGrid1x3Horizontal"
18
+ import { IconDotGrid2x3 as IconDotGrid2x3Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconDotGrid2x3"
16
19
  import { IconExclamationCircle as IconExclamationCircleBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconExclamationCircle"
17
20
  import { IconExclamationTriangle as IconExclamationTriangleBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconExclamationTriangle"
18
21
  import { IconHome as IconHomeBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconHome"
22
+ import { IconFullScreen as IconFullScreenBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconFullScreen"
23
+ import { IconHighlight as IconHighlightBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconHighlight"
19
24
  import { IconMagnifyingGlass as IconMagnifyingGlassBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconMagnifyingGlass"
20
25
  import { IconMinusMedium as IconMinusBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconMinusMedium"
26
+ import { IconPause as IconPauseBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconPause"
21
27
  import { IconMoon as IconMoonBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconMoon"
22
28
  import { IconPeople as IconPeopleBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconPeople"
29
+ import { IconPlay as IconPlayBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconPlay"
23
30
  import { IconPlusMedium as IconPlusBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconPlusMedium"
24
31
  import { IconSettingsGear1 as IconSettingsGear1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconSettingsGear1"
25
32
  import { IconSquareBehindSquare6 as IconSquareBehindSquare6Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconSquareBehindSquare6"
26
33
  import { IconStar as IconStarBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconStar"
34
+ import { IconStrikeThrough as IconStrikeThroughBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconStrikeThrough"
27
35
  import { IconSun as IconSunBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconSun"
36
+ import { IconVolumeFull as IconVolumeFullBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconVolumeFull"
37
+ import { IconVolumeOff as IconVolumeOffBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconVolumeOff"
28
38
  import { IconX as IconXBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconX"
29
39
  import type { ComponentType } from "react"
30
40
 
@@ -42,16 +52,21 @@ export const withCentralIconDefaults = (
42
52
  ): CentralIconComponent => {
43
53
  const CentralIcon = ({
44
54
  size = CENTRAL_ICON_SIZE,
55
+ // Central's "masked" mode hardcodes one maskId per icon name. A second
56
+ // instance on the page paints as a solid square. Raw mode avoids that.
57
+ mode = "raw",
45
58
  ...props
46
- }: CentralIconProps) => <Icon size={size} {...props} />
59
+ }: CentralIconProps) => <Icon {...props} size={size} mode={mode} />
47
60
 
48
61
  CentralIcon.displayName = Icon.displayName ?? Icon.name
49
62
  return CentralIcon
50
63
  }
51
64
 
65
+ export const IconArrowDown = withCentralIconDefaults(IconArrowDownBase)
52
66
  export const IconBell = withCentralIconDefaults(IconBellBase)
53
67
  export const IconCalendar1 = withCentralIconDefaults(IconCalendar1Base)
54
68
  export const IconChainLink1 = withCentralIconDefaults(IconChainLink1Base)
69
+ export const IconChainLink3 = withCentralIconDefaults(IconChainLink3Base)
55
70
  export const IconCheckmark1 = withCentralIconDefaults(IconCheckmark1Base)
56
71
  export const IconChevronBottom = withCentralIconDefaults(IconChevronBottomBase)
57
72
  export const IconChevronDownSmall = withCentralIconDefaults(
@@ -67,26 +82,34 @@ export const IconCrossSmall = withCentralIconDefaults(IconCrossSmallBase)
67
82
  export const IconDotGrid1x3Horizontal = withCentralIconDefaults(
68
83
  IconDotGrid1x3HorizontalBase,
69
84
  )
85
+ export const IconDotGrid2x3 = withCentralIconDefaults(IconDotGrid2x3Base)
70
86
  export const IconExclamationCircle = withCentralIconDefaults(
71
87
  IconExclamationCircleBase,
72
88
  )
73
89
  export const IconExclamationTriangle = withCentralIconDefaults(
74
90
  IconExclamationTriangleBase,
75
91
  )
92
+ export const IconFullScreen = withCentralIconDefaults(IconFullScreenBase)
76
93
  export const IconHome = withCentralIconDefaults(IconHomeBase)
77
94
  export const IconMagnifyingGlass = withCentralIconDefaults(
78
95
  IconMagnifyingGlassBase,
79
96
  )
80
97
  export const IconMinus = withCentralIconDefaults(IconMinusBase)
98
+ export const IconPause = withCentralIconDefaults(IconPauseBase)
81
99
  export const IconMoon = withCentralIconDefaults(IconMoonBase)
82
100
  export const IconPeople = withCentralIconDefaults(IconPeopleBase)
101
+ export const IconPlay = withCentralIconDefaults(IconPlayBase)
83
102
  export const IconPlus = withCentralIconDefaults(IconPlusBase)
84
103
  export const IconSettingsGear1 = withCentralIconDefaults(IconSettingsGear1Base)
85
104
  export const IconSquareBehindSquare6 = withCentralIconDefaults(
86
105
  IconSquareBehindSquare6Base,
87
106
  )
88
107
  export const IconStar = withCentralIconDefaults(IconStarBase)
108
+ export const IconStrikeThrough = withCentralIconDefaults(IconStrikeThroughBase)
109
+ export const IconHighlight = withCentralIconDefaults(IconHighlightBase)
89
110
  export const IconSun = withCentralIconDefaults(IconSunBase)
111
+ export const IconVolumeFull = withCentralIconDefaults(IconVolumeFullBase)
112
+ export const IconVolumeOff = withCentralIconDefaults(IconVolumeOffBase)
90
113
  export const IconX = withCentralIconDefaults(IconXBase)
91
114
 
92
115
  export const iconGallery = [
@@ -110,9 +133,13 @@ export const iconGallery = [
110
133
  { name: "IconCircleInfo", Icon: IconCircleInfo },
111
134
  { name: "IconClipboard", Icon: IconClipboard },
112
135
  { name: "IconDotGrid1x3Horizontal", Icon: IconDotGrid1x3Horizontal },
136
+ { name: "IconDotGrid2x3", Icon: IconDotGrid2x3 },
113
137
  { name: "IconSquareBehindSquare6", Icon: IconSquareBehindSquare6 },
114
138
  { name: "IconChainLink1", Icon: IconChainLink1 },
139
+ { name: "IconChainLink3", Icon: IconChainLink3 },
115
140
  { name: "IconExclamationCircle", Icon: IconExclamationCircle },
116
141
  { name: "IconExclamationTriangle", Icon: IconExclamationTriangle },
117
142
  { name: "IconStar", Icon: IconStar },
143
+ { name: "IconStrikeThrough", Icon: IconStrikeThrough },
144
+ { name: "IconHighlight", Icon: IconHighlight },
118
145
  ] as const