@gentleduck/registry-ui 0.2.6 → 0.2.8

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.
@@ -3,63 +3,72 @@
3
3
  import { cn } from '@gentleduck/libs/cn'
4
4
  import { type Direction, useDirection } from '@gentleduck/primitives/direction'
5
5
  import type { VariantProps } from '@gentleduck/variants'
6
- import { useMemo } from 'react'
6
+ import React, { useMemo } from 'react'
7
7
  import { Label } from '../label'
8
8
  import { Separator } from '../separator'
9
9
  import { fieldVariants } from './field.constants'
10
10
 
11
- function FieldSet({ className, dir, ...props }: React.ComponentProps<'fieldset'>) {
12
- const direction = useDirection(dir as Direction)
13
- return (
14
- <fieldset
15
- className={cn(
16
- 'flex flex-col gap-6',
17
- 'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
18
- className,
19
- )}
20
- dir={direction}
21
- data-slot="field-set"
22
- {...props}
23
- />
24
- )
25
- }
26
-
27
- function FieldLegend({
28
- className,
29
- variant = 'legend',
30
- ...props
31
- }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
11
+ const FieldSet = React.forwardRef<HTMLFieldSetElement, React.ComponentPropsWithoutRef<'fieldset'>>(
12
+ ({ className, dir, ...props }, ref) => {
13
+ const direction = useDirection(dir as Direction)
14
+ return (
15
+ <fieldset
16
+ ref={ref}
17
+ className={cn(
18
+ 'flex flex-col gap-6',
19
+ 'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
20
+ className,
21
+ )}
22
+ dir={direction}
23
+ data-slot="field-set"
24
+ {...props}
25
+ />
26
+ )
27
+ },
28
+ )
29
+ FieldSet.displayName = 'FieldSet'
30
+
31
+ const FieldLegend = React.forwardRef<
32
+ HTMLLegendElement,
33
+ React.ComponentPropsWithoutRef<'legend'> & { variant?: 'legend' | 'label' }
34
+ >(({ className, variant = 'legend', ...props }, ref) => {
32
35
  return (
33
36
  <legend
37
+ ref={ref}
34
38
  className={cn('mb-3 font-medium', 'data-[variant=legend]:text-base', 'data-[variant=label]:text-sm', className)}
35
39
  data-slot="field-legend"
36
40
  data-variant={variant}
37
41
  {...props}
38
42
  />
39
43
  )
40
- }
41
-
42
- function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
43
- return (
44
- <div
45
- className={cn(
46
- 'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
47
- className,
48
- )}
49
- data-slot="field-group"
50
- {...props}
51
- />
52
- )
53
- }
44
+ })
45
+ FieldLegend.displayName = 'FieldLegend'
54
46
 
55
- function Field({
56
- className,
57
- orientation = 'vertical',
58
- ...props
59
- }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
47
+ const FieldGroup = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
48
+ ({ className, ...props }, ref) => {
49
+ return (
50
+ <div
51
+ ref={ref}
52
+ className={cn(
53
+ 'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
54
+ className,
55
+ )}
56
+ data-slot="field-group"
57
+ {...props}
58
+ />
59
+ )
60
+ },
61
+ )
62
+ FieldGroup.displayName = 'FieldGroup'
63
+
64
+ const Field = React.forwardRef<
65
+ HTMLDivElement,
66
+ React.ComponentPropsWithoutRef<'div'> & VariantProps<typeof fieldVariants>
67
+ >(({ className, orientation = 'vertical', ...props }, ref) => {
60
68
  return (
61
69
  // biome-ignore lint/a11y/useSemanticElements: field group role is semantically correct for form field grouping
62
70
  <div
71
+ ref={ref}
63
72
  className={cn(fieldVariants({ orientation }), className)}
64
73
  data-orientation={orientation}
65
74
  data-slot="field"
@@ -67,70 +76,87 @@ function Field({
67
76
  {...props}
68
77
  />
69
78
  )
70
- }
79
+ })
80
+ Field.displayName = 'Field'
71
81
 
72
- function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
73
- return (
74
- <div
75
- className={cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', className)}
76
- data-slot="field-content"
77
- {...props}
78
- />
79
- )
80
- }
81
-
82
- function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
83
- return (
84
- <Label
85
- className={cn(
86
- 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
87
- 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
88
- 'has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10',
89
- className,
90
- )}
91
- data-slot="field-label"
92
- {...props}
93
- />
94
- )
95
- }
82
+ const FieldContent = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
83
+ ({ className, ...props }, ref) => {
84
+ return (
85
+ <div
86
+ ref={ref}
87
+ className={cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', className)}
88
+ data-slot="field-content"
89
+ {...props}
90
+ />
91
+ )
92
+ },
93
+ )
94
+ FieldContent.displayName = 'FieldContent'
96
95
 
97
- function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
98
- return (
99
- <div
100
- className={cn(
101
- 'flex w-fit items-center gap-2 font-medium text-sm leading-snug group-data-[disabled=true]/field:opacity-50',
102
- className,
103
- )}
104
- data-slot="field-label"
105
- {...props}
106
- />
107
- )
108
- }
96
+ const FieldLabel = React.forwardRef<HTMLLabelElement, React.ComponentPropsWithoutRef<typeof Label>>(
97
+ ({ className, ...props }, ref) => {
98
+ return (
99
+ <Label
100
+ ref={ref}
101
+ className={cn(
102
+ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
103
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
104
+ 'has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10',
105
+ className,
106
+ )}
107
+ data-slot="field-label"
108
+ {...props}
109
+ />
110
+ )
111
+ },
112
+ )
113
+ FieldLabel.displayName = 'FieldLabel'
109
114
 
110
- function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
111
- return (
112
- <p
113
- className={cn(
114
- 'font-normal text-muted-foreground text-sm leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
115
- 'nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5',
116
- '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
117
- className,
118
- )}
119
- data-slot="field-description"
120
- {...props}
121
- />
122
- )
123
- }
115
+ const FieldTitle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
116
+ ({ className, ...props }, ref) => {
117
+ return (
118
+ <div
119
+ ref={ref}
120
+ className={cn(
121
+ 'flex w-fit items-center gap-2 font-medium text-sm leading-snug group-data-[disabled=true]/field:opacity-50',
122
+ className,
123
+ )}
124
+ data-slot="field-label"
125
+ {...props}
126
+ />
127
+ )
128
+ },
129
+ )
130
+ FieldTitle.displayName = 'FieldTitle'
124
131
 
125
- function FieldSeparator({
126
- children,
127
- className,
128
- ...props
129
- }: React.ComponentProps<'div'> & {
130
- children?: React.ReactNode
131
- }) {
132
+ const FieldDescription = React.forwardRef<HTMLParagraphElement, React.ComponentPropsWithoutRef<'p'>>(
133
+ ({ className, ...props }, ref) => {
134
+ return (
135
+ <p
136
+ ref={ref}
137
+ className={cn(
138
+ 'font-normal text-muted-foreground text-sm leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
139
+ 'nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5',
140
+ '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
141
+ className,
142
+ )}
143
+ data-slot="field-description"
144
+ {...props}
145
+ />
146
+ )
147
+ },
148
+ )
149
+ FieldDescription.displayName = 'FieldDescription'
150
+
151
+ const FieldSeparator = React.forwardRef<
152
+ HTMLDivElement,
153
+ React.ComponentPropsWithoutRef<'div'> & {
154
+ children?: React.ReactNode
155
+ }
156
+ >(({ children, className, ...props }, ref) => {
132
157
  return (
133
158
  <div
159
+ ref={ref}
134
160
  className={cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', className)}
135
161
  data-content={!!children}
136
162
  data-slot="field-separator"
@@ -145,16 +171,15 @@ function FieldSeparator({
145
171
  )}
146
172
  </div>
147
173
  )
148
- }
174
+ })
175
+ FieldSeparator.displayName = 'FieldSeparator'
149
176
 
150
- function FieldError({
151
- className,
152
- children,
153
- errors,
154
- ...props
155
- }: React.ComponentProps<'div'> & {
156
- errors?: Array<{ message?: string } | undefined>
157
- }) {
177
+ const FieldError = React.forwardRef<
178
+ HTMLDivElement,
179
+ React.ComponentPropsWithoutRef<'div'> & {
180
+ errors?: Array<{ message?: string } | undefined>
181
+ }
182
+ >(({ className, children, errors, ...props }, ref) => {
158
183
  const content = useMemo(() => {
159
184
  if (children) {
160
185
  return children
@@ -182,6 +207,7 @@ function FieldError({
182
207
 
183
208
  return (
184
209
  <div
210
+ ref={ref}
185
211
  className={cn('font-normal text-destructive text-sm', className)}
186
212
  data-slot="field-error"
187
213
  role="alert"
@@ -189,7 +215,8 @@ function FieldError({
189
215
  {content}
190
216
  </div>
191
217
  )
192
- }
218
+ })
219
+ FieldError.displayName = 'FieldError'
193
220
 
194
221
  export {
195
222
  Field,
@@ -421,3 +421,4 @@ export function JsonTextareaField<TFieldValues extends FieldValues>(
421
421
  </Field>
422
422
  )
423
423
  }
424
+ JsonTextareaField.displayName = 'JsonTextareaField'
@@ -108,3 +108,4 @@ export function JsonEditorView({
108
108
  </div>
109
109
  )
110
110
  }
111
+ JsonEditorView.displayName = 'JsonEditorView'
@@ -7,18 +7,23 @@ import { Check, ChevronRight, Circle } from 'lucide-react'
7
7
  import * as React from 'react'
8
8
 
9
9
  const MenubarMenu: typeof MenubarPrimitive.Menu = MenubarPrimitive.Menu
10
+ MenubarMenu.displayName = 'MenubarMenu'
10
11
 
11
12
  const MenubarGroup = MenubarPrimitive.Group
13
+ MenubarGroup.displayName = 'MenubarGroup'
12
14
 
13
15
  const MenubarPortal = MenubarPrimitive.Portal
16
+ MenubarPortal.displayName = 'MenubarPortal'
14
17
 
15
18
  function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
16
19
  return <MenubarPrimitive.RadioGroup {...props} />
17
20
  }
21
+ MenubarRadioGroup.displayName = 'MenubarRadioGroup'
18
22
 
19
23
  function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
20
24
  return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
21
25
  }
26
+ MenubarSub.displayName = 'MenubarSub'
22
27
 
23
28
  const Menubar = React.forwardRef<
24
29
  React.ComponentRef<typeof MenubarPrimitive.Root>,
@@ -188,9 +193,11 @@ const MenubarSeparator = React.forwardRef<
188
193
  ))
189
194
  MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
190
195
 
191
- const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
192
- return <span className={cn('ms-auto text-muted-foreground text-xs tracking-widest', className)} {...props} />
193
- }
196
+ const MenubarShortcut = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
197
+ ({ className, ...props }, ref) => (
198
+ <span ref={ref} className={cn('ms-auto text-muted-foreground text-xs tracking-widest', className)} {...props} />
199
+ ),
200
+ )
194
201
  MenubarShortcut.displayName = 'MenubarShortcut'
195
202
 
196
203
  export {
@@ -5,10 +5,13 @@ import * as PopoverPrimitive from '@gentleduck/primitives/popover'
5
5
  import * as React from 'react'
6
6
 
7
7
  const Popover = PopoverPrimitive.Root
8
+ Popover.displayName = 'Popover'
8
9
 
9
10
  const PopoverTrigger: typeof PopoverPrimitive.Trigger = PopoverPrimitive.Trigger
11
+ PopoverTrigger.displayName = 'PopoverTrigger'
10
12
 
11
13
  const PopoverAnchor: typeof PopoverPrimitive.Anchor = PopoverPrimitive.Anchor
14
+ PopoverAnchor.displayName = 'PopoverAnchor'
12
15
 
13
16
  const PopoverContent = React.forwardRef<
14
17
  React.ComponentRef<typeof PopoverPrimitive.Content>,
@@ -31,5 +34,6 @@ const PopoverContent = React.forwardRef<
31
34
  PopoverContent.displayName = PopoverPrimitive.Content.displayName
32
35
 
33
36
  export const PopoverClose: typeof PopoverPrimitive.Close = PopoverPrimitive.Close
37
+ PopoverClose.displayName = 'PopoverClose'
34
38
 
35
39
  export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
@@ -2,98 +2,104 @@
2
2
 
3
3
  import { cn } from '@gentleduck/libs/cn'
4
4
  import { Maximize2 } from 'lucide-react'
5
- import { useCallback, useMemo, useRef, useState } from 'react'
5
+ import React, { useCallback, useMemo, useRef, useState } from 'react'
6
6
  import { Button } from '../button'
7
7
  import { Dialog, DialogContent, DialogTrigger } from '../dialog'
8
8
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip'
9
9
  import { PreviewPanel } from './preview-panel'
10
10
  import type { PreviewPanelDialogProps, PreviewPanelState } from './preview-panel.types'
11
11
 
12
- function PreviewPanelDialog({
13
- children,
14
- html,
15
- className,
16
- panelClassName,
17
- maxHeight,
18
- minZoom = 0.25,
19
- maxZoom = 4,
20
- initialZoom = 1,
21
- showControls = true,
22
- syncPanels = true,
23
- fullscreenText = 'Open fullscreen',
24
- }: PreviewPanelDialogProps & { fullscreenText?: string }) {
25
- const [sharedState, setSharedState] = useState<PreviewPanelState | undefined>(undefined)
12
+ const PreviewPanelDialog = React.forwardRef<HTMLDivElement, PreviewPanelDialogProps & { fullscreenText?: string }>(
13
+ (
14
+ {
15
+ children,
16
+ html,
17
+ className,
18
+ panelClassName,
19
+ maxHeight,
20
+ minZoom = 0.25,
21
+ maxZoom = 4,
22
+ initialZoom = 1,
23
+ showControls = true,
24
+ syncPanels = true,
25
+ fullscreenText = 'Open fullscreen',
26
+ },
27
+ ref,
28
+ ) => {
29
+ const [sharedState, setSharedState] = useState<PreviewPanelState | undefined>(undefined)
26
30
 
27
- // Ref tracks whether a state update is already scheduled this frame.
28
- // Prevents multiple setState calls per animation frame when both
29
- // panels emit state changes simultaneously.
30
- const pendingRef = useRef(false)
31
+ // Ref tracks whether a state update is already scheduled this frame.
32
+ // Prevents multiple setState calls per animation frame when both
33
+ // panels emit state changes simultaneously.
34
+ const pendingRef = useRef(false)
31
35
 
32
- const handleStateChange = useCallback(
33
- (state: PreviewPanelState) => {
34
- if (!syncPanels) return
35
- if (pendingRef.current) return
36
- pendingRef.current = true
37
- requestAnimationFrame(() => {
38
- pendingRef.current = false
39
- setSharedState(state)
40
- })
41
- },
42
- [syncPanels],
43
- )
36
+ const handleStateChange = useCallback(
37
+ (state: PreviewPanelState) => {
38
+ if (!syncPanels) return
39
+ if (pendingRef.current) return
40
+ pendingRef.current = true
41
+ requestAnimationFrame(() => {
42
+ pendingRef.current = false
43
+ setSharedState(state)
44
+ })
45
+ },
46
+ [syncPanels],
47
+ )
44
48
 
45
- const contentProps = useMemo(() => (html ? { html } : { children }), [html, children])
49
+ const contentProps = useMemo(() => (html ? { html } : { children }), [html, children])
46
50
 
47
- const dialogPanelClassName = useMemo(() => cn('min-h-[70vh]', panelClassName), [panelClassName])
51
+ const dialogPanelClassName = useMemo(() => cn('min-h-[70vh]', panelClassName), [panelClassName])
48
52
 
49
- return (
50
- <div className={cn('group relative', className)}>
51
- <div className="relative overflow-hidden rounded-lg border bg-card">
52
- <PreviewPanel
53
- {...contentProps}
54
- maxHeight={maxHeight}
55
- minZoom={minZoom}
56
- maxZoom={maxZoom}
57
- initialZoom={initialZoom}
58
- showControls={showControls}
59
- className={panelClassName}
60
- onStateChange={handleStateChange}
61
- syncState={syncPanels ? sharedState : undefined}
62
- />
53
+ return (
54
+ <div ref={ref} className={cn('group relative', className)}>
55
+ <div className="relative overflow-hidden rounded-lg border bg-card">
56
+ <PreviewPanel
57
+ {...contentProps}
58
+ maxHeight={maxHeight}
59
+ minZoom={minZoom}
60
+ maxZoom={maxZoom}
61
+ initialZoom={initialZoom}
62
+ showControls={showControls}
63
+ className={panelClassName}
64
+ onStateChange={handleStateChange}
65
+ syncState={syncPanels ? sharedState : undefined}
66
+ />
63
67
 
64
- <TooltipProvider>
65
- <Dialog>
66
- <Tooltip>
67
- <TooltipTrigger asChild>
68
- <DialogTrigger asChild>
69
- <Button
70
- variant="ghost"
71
- size="icon-sm"
72
- icon={<Maximize2 aria-hidden="true" />}
73
- aria-label={fullscreenText}
74
- className="absolute end-3 bottom-3 z-10 border bg-background/80 backdrop-blur-sm"
75
- />
76
- </DialogTrigger>
77
- </TooltipTrigger>
78
- <TooltipContent>{fullscreenText}</TooltipContent>
79
- </Tooltip>
80
- <DialogContent className="max-h-[85vh] max-w-[90vw] overflow-auto p-0">
81
- <PreviewPanel
82
- {...contentProps}
83
- minZoom={minZoom}
84
- maxZoom={maxZoom}
85
- initialZoom={initialZoom}
86
- showControls={showControls}
87
- className={dialogPanelClassName}
88
- onStateChange={handleStateChange}
89
- syncState={syncPanels ? sharedState : undefined}
90
- />
91
- </DialogContent>
92
- </Dialog>
93
- </TooltipProvider>
68
+ <TooltipProvider>
69
+ <Dialog>
70
+ <Tooltip>
71
+ <TooltipTrigger asChild>
72
+ <DialogTrigger asChild>
73
+ <Button
74
+ variant="ghost"
75
+ size="icon-sm"
76
+ icon={<Maximize2 aria-hidden="true" />}
77
+ aria-label={fullscreenText}
78
+ className="absolute end-3 bottom-3 z-10 border bg-background/80 backdrop-blur-sm"
79
+ />
80
+ </DialogTrigger>
81
+ </TooltipTrigger>
82
+ <TooltipContent>{fullscreenText}</TooltipContent>
83
+ </Tooltip>
84
+ <DialogContent className="max-h-[85vh] max-w-[90vw] overflow-auto p-0">
85
+ <PreviewPanel
86
+ {...contentProps}
87
+ minZoom={minZoom}
88
+ maxZoom={maxZoom}
89
+ initialZoom={initialZoom}
90
+ showControls={showControls}
91
+ className={dialogPanelClassName}
92
+ onStateChange={handleStateChange}
93
+ syncState={syncPanels ? sharedState : undefined}
94
+ />
95
+ </DialogContent>
96
+ </Dialog>
97
+ </TooltipProvider>
98
+ </div>
94
99
  </div>
95
- </div>
96
- )
97
- }
100
+ )
101
+ },
102
+ )
103
+ PreviewPanelDialog.displayName = 'PreviewPanelDialog'
98
104
 
99
105
  export { PreviewPanelDialog }