@gentleduck/registry-ui 0.2.4 → 0.2.6

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.
@@ -0,0 +1 @@
1
+ $ tsc -p tsconfig.json --noEmit --pretty false --skipLibCheck
@@ -0,0 +1,23 @@
1
+ $ bun test
2
+ bun test v1.3.5 (1e86cebd)
3
+
4
+ ::group::src/chart/__test__/chart.test.tsx:
5
+ (pass) registry-ui chart > ChartContainer server render does not emit invalid size warnings [47.00ms]
6
+
7
+ ::endgroup::
8
+
9
+ ::group::src/button/__test__/button.test.tsx:
10
+ (pass) registry-ui button > buttonVariants returns the shared base styles and defaults
11
+ (pass) registry-ui button > buttonVariants applies explicit variant and size overrides [1.00ms]
12
+ (pass) registry-ui button > button exports keep stable display names
13
+ (pass) registry-ui button > Button renders loading state as a busy disabled native button [9.00ms]
14
+ (pass) registry-ui button > Button preserves explicit disabled state even when loading is false
15
+ (pass) registry-ui button > Button collapses into icon-only mode and hides secondary content [4.00ms]
16
+ (pass) registry-ui button > AnimationIcon renders left and right placements around children [1.00ms]
17
+
18
+ ::endgroup::
19
+
20
+ 8 pass
21
+ 0 fail
22
+ 25 expect() calls
23
+ Ran 8 tests across 2 files. [742.00ms]
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @gentleduck/registry-ui
2
2
 
3
+ ## 0.2.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 2b6e8d0: Resolve all biome lint warnings, improve type safety, and add test coverage across the monorepo.
8
+ - Updated dependencies [2b6e8d0]
9
+ - @gentleduck/primitives@0.2.5
10
+ - @gentleduck/variants@0.1.20
11
+ - @gentleduck/hooks@0.1.12
12
+ - @gentleduck/motion@0.1.17
13
+ - @gentleduck/libs@0.1.15
14
+ - @gentleduck/vim@0.1.16
15
+
16
+ ## 0.2.5
17
+
18
+ ### Patch Changes
19
+
20
+ - fix(search): disable primitive's built-in filter when using custom lunr search
21
+
22
+ The command menu had two competing filtering systems: lunr-based search and the primitive's
23
+ substring filter. The primitive's filter was hiding items via `el.hidden = true` even when
24
+ lunr correctly found them, causing search results to not appear. Added `shouldFilter` prop
25
+ to the Command primitive to allow disabling the built-in filter.
26
+
27
+ - Updated dependencies
28
+ - @gentleduck/primitives@0.2.4
29
+
3
30
  ## 0.2.4
4
31
 
5
32
  ### Patch Changes
package/package.json CHANGED
@@ -52,8 +52,9 @@
52
52
  "access": "public"
53
53
  },
54
54
  "scripts": {
55
- "check-types": "tsc -p tsconfig.json --noEmit --pretty false --skipLibCheck"
55
+ "check-types": "tsc -p tsconfig.json --noEmit --pretty false --skipLibCheck",
56
+ "test": "bun test"
56
57
  },
57
58
  "type": "module",
58
- "version": "0.2.4"
59
+ "version": "0.2.6"
59
60
  }
@@ -1,5 +1,14 @@
1
+ /**
2
+ * @deprecated These table components are deprecated and no longer maintained.
3
+ * Use the components from `@duck-ui/registry-ui/table` instead.
4
+ * This module will be removed in a future release.
5
+ */
1
6
  export * from './table'
7
+ /** @deprecated Use components from `@duck-ui/registry-ui/table` instead. */
2
8
  export * from './table.constants'
9
+ /** @deprecated Use components from `@duck-ui/registry-ui/table` instead. */
3
10
  export * from './table.hook'
11
+ /** @deprecated Use components from `@duck-ui/registry-ui/table` instead. */
4
12
  export * from './table.lib'
13
+ /** @deprecated Use components from `@duck-ui/registry-ui/table` instead. */
5
14
  export * from './table.types'
@@ -206,7 +206,7 @@
206
206
  // DuckTableSearchInputProps
207
207
  // >(({ trigger, label, badge, keys }, ref) => {
208
208
  // const {
209
- // children: badgeChildren = '⌃+⇧+F',
209
+ // children: badgeChildren = 'Ctrl+Shift+F',
210
210
  // className: badgeClassName,
211
211
  // ...badgeProps
212
212
  // } = badge ?? {}
@@ -415,7 +415,7 @@
415
415
  // children: 'View',
416
416
  // command: {
417
417
  // key: 'ctrl+shift+v',
418
- // label: '⌃+⇧+V',
418
+ // label: 'Ctrl+Shift+V',
419
419
  // },
420
420
  // icon: {
421
421
  // children: MixerHorizontalIcon as LucideIcon,
@@ -527,7 +527,7 @@
527
527
  // // }),
528
528
  // // command: {
529
529
  // // key: 'ctrl+shift+down',
530
- // // label: '⌃+⇧+↓',
530
+ // // label: 'Ctrl+Shift+Down',
531
531
  // // action: () =>
532
532
  // // setPaginationState({
533
533
  // // ...paginationState,
@@ -546,7 +546,7 @@
546
546
  // // onClick: () => setPaginationState({ ...paginationState, activePage: 0 }),
547
547
  // // command: {
548
548
  // // key: 'ctrl+shift+left',
549
- // // label: '⌃+⇧+←',
549
+ // // label: 'Ctrl+Shift+Left',
550
550
  // // action: () => setPaginationState({ ...paginationState, activePage: 0 }),
551
551
  // // },
552
552
  // label: {
@@ -561,7 +561,7 @@
561
561
  // // onClick: () => setPaginationState({ ...paginationState, activePage: resultArrays.length - 1 }),
562
562
  // // command: {
563
563
  // // key: 'ctrl+shift+right',
564
- // // label: '⌃+⇧+->',
564
+ // // label: 'Ctrl+Shift+Right',
565
565
  // // action: () => setPaginationState({ ...paginationState, activePage: resultArrays.length - 1 }),
566
566
  // // },
567
567
  // label: {
@@ -575,7 +575,7 @@
575
575
  // right={{
576
576
  // command: {
577
577
  // key: 'ctrl+shift+up',
578
- // label: '⌃+⇧+↑',
578
+ // label: 'Ctrl+Shift+Up',
579
579
  // // action: () =>
580
580
  // // setPaginationState({
581
581
  // // ...paginationState,
@@ -677,7 +677,7 @@
677
677
  // className: 'w-[4.5rem] h-[32px] gap-0',
678
678
  // command: {
679
679
  // key: 'ctrl+shift+c',
680
- // label: '⌃+⇧+C',
680
+ // label: 'Ctrl+Shift+C',
681
681
  // },
682
682
  // label: {
683
683
  // children: 'Rows per page',
@@ -1,9 +1,22 @@
1
+ /**
2
+ * @deprecated These upload components are deprecated and no longer maintained.
3
+ * Use the components from `@duck-ui/registry-ui/upload` instead.
4
+ * This module will be removed in a future release.
5
+ */
1
6
  export * from './upload'
7
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
2
8
  export * from './upload.assets'
9
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
3
10
  export * from './upload.constants'
11
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
4
12
  export * from './upload.dto'
13
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
5
14
  export * from './upload.lib'
15
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
6
16
  export * from './upload.types'
17
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
7
18
  export * from './upload-advanced'
19
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
8
20
  export * from './upload-advanced-chunks'
21
+ /** @deprecated Use components from `@duck-ui/registry-ui/upload` instead. */
9
22
  export * from './upload-sonner'
@@ -34,7 +34,7 @@
34
34
  // </p>
35
35
  // <div className="flex items-center gap-2">
36
36
  // {remainingTime && (
37
- // <p className="text-foreground-light text-sm font-mono">{`${remainingTime && !isNaN(remainingTime) && isFinite(remainingTime) && remainingTime !== 0 ? `${formatTime(remainingTime)} remaining ` : ''}`}</p>
37
+ // <p className="text-foreground-light text-sm font-mono">{`${remainingTime && !isNaN(remainingTime) && isFinite(remainingTime) && remainingTime !== 0 ? `${formatTime(remainingTime)} remaining -` : ''}`}</p>
38
38
  // )}
39
39
  // <p className="text-foreground-light text-sm font-mono">{`${progress}%`}</p>
40
40
  // </div>
@@ -337,7 +337,19 @@ const AudioVisualizer: React.FC<AudioVisualizerProps> = ({
337
337
  setLoading,
338
338
  width,
339
339
  })
340
- }, [blob])
340
+ }, [
341
+ blob,
342
+ barWidth,
343
+ currentColors.backgroundColor,
344
+ currentColors.barColor,
345
+ currentColors.barPlayedColor,
346
+ gap,
347
+ height,
348
+ minBarHeight,
349
+ process_audio,
350
+ setLoading,
351
+ width,
352
+ ])
341
353
 
342
354
  React.useEffect(() => {
343
355
  if (!canvasRef.current) return
@@ -359,7 +371,19 @@ const AudioVisualizer: React.FC<AudioVisualizerProps> = ({
359
371
  gap,
360
372
  minBarHeight,
361
373
  })
362
- }, [data, width, height, currentTime, duration, animationProgress, theme])
374
+ }, [
375
+ data,
376
+ width,
377
+ currentTime,
378
+ duration,
379
+ animationProgress,
380
+ barWidth,
381
+ currentColors.backgroundColor,
382
+ currentColors.barColor,
383
+ currentColors.barPlayedColor,
384
+ gap,
385
+ minBarHeight,
386
+ ])
363
387
 
364
388
  return (
365
389
  <canvas
@@ -6,8 +6,7 @@ export interface RecordingParams {
6
6
 
7
7
  export interface StopRecordingHandlerParam {
8
8
  setRecording: React.Dispatch<React.SetStateAction<boolean>>
9
- // @ts-ignore
10
- intervalRef: React.RefObject<NodeJS.Timeout | null>
9
+ intervalRef: React.RefObject<ReturnType<typeof setInterval> | null>
11
10
  mediaRecorderRef: React.RefObject<MediaRecorder | null>
12
11
  durationRef: React.RefObject<number>
13
12
  }
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import * as React from 'react'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
4
+ import { AnimationIcon, Button } from '../button'
5
+ import { buttonVariants } from '../button.constants'
6
+
7
+ describe('registry-ui button', () => {
8
+ test('buttonVariants returns the shared base styles and defaults', () => {
9
+ const classes = buttonVariants()
10
+
11
+ expect(classes).toContain('inline-flex')
12
+ expect(classes).toContain('bg-primary')
13
+ expect(classes).toContain('h-9')
14
+ })
15
+
16
+ test('buttonVariants applies explicit variant and size overrides', () => {
17
+ const classes = buttonVariants({ size: 'sm', variant: 'ghost' })
18
+
19
+ expect(classes).toContain('h-8')
20
+ expect(classes).toContain('hover:bg-accent')
21
+ })
22
+
23
+ test('button exports keep stable display names', () => {
24
+ expect(Button).toBeDefined()
25
+ expect(Button.displayName).toBe('Button')
26
+ expect(AnimationIcon.displayName).toBe('AnimationIcon')
27
+ })
28
+
29
+ test('Button renders loading state as a busy disabled native button', () => {
30
+ const html = renderToStaticMarkup(<Button loading>Save</Button>)
31
+
32
+ expect(html).toContain('type="button"')
33
+ expect(html).toContain('aria-busy="true"')
34
+ expect(html).toContain('disabled=""')
35
+ expect(html).toContain('animate-spin')
36
+ expect(html).toContain('Save')
37
+ })
38
+
39
+ test('Button preserves explicit disabled state even when loading is false', () => {
40
+ const html = renderToStaticMarkup(
41
+ <Button disabled loading={false}>
42
+ Save
43
+ </Button>,
44
+ )
45
+
46
+ expect(html).toContain('disabled=""')
47
+ expect(html).not.toContain('aria-busy="true"')
48
+ })
49
+
50
+ test('Button collapses into icon-only mode and hides secondary content', () => {
51
+ const html = renderToStaticMarkup(
52
+ <Button icon={<span data-icon="left">L</span>} isCollapsed secondIcon={<span data-icon="right">R</span>}>
53
+ Save
54
+ </Button>,
55
+ )
56
+
57
+ expect(html).toContain('data-icon="left"')
58
+ expect(html).toContain('size-9')
59
+ expect(html).not.toContain('Save')
60
+ expect(html).not.toContain('data-icon="right"')
61
+ })
62
+
63
+ test('AnimationIcon renders left and right placements around children', () => {
64
+ const leftHtml = renderToStaticMarkup(
65
+ <AnimationIcon animationIcon={{ icon: <span data-icon="left">L</span>, iconPlacement: 'left' }}>
66
+ Label
67
+ </AnimationIcon>,
68
+ )
69
+ const rightHtml = renderToStaticMarkup(
70
+ <AnimationIcon animationIcon={{ icon: <span data-icon="right">R</span>, iconPlacement: 'right' }}>
71
+ Label
72
+ </AnimationIcon>,
73
+ )
74
+
75
+ expect(leftHtml).toContain('data-icon="left"')
76
+ expect(leftHtml.indexOf('data-icon="left"')).toBeLessThan(leftHtml.indexOf('Label'))
77
+ expect(rightHtml).toContain('data-icon="right"')
78
+ expect(rightHtml.indexOf('Label')).toBeLessThan(rightHtml.indexOf('data-icon="right"'))
79
+ })
80
+ })
@@ -42,7 +42,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
42
  variant,
43
43
  }),
44
44
  )}
45
- disabled={loading ?? disabled}
45
+ disabled={Boolean(loading) || disabled}
46
46
  ref={ref}
47
47
  type={type}>
48
48
  {loading ? <Loader aria-hidden="true" className="animate-spin" /> : icon}
@@ -12,6 +12,7 @@ const ButtonGroup = React.forwardRef<
12
12
  >(({ className, orientation = 'horizontal', dir, ...props }, ref) => {
13
13
  const direction = useDirection(dir as Direction)
14
14
  return (
15
+ // biome-ignore lint/a11y/useSemanticElements: group role is semantically correct for button groups
15
16
  <div
16
17
  className={cn(buttonGroupVariants({ orientation }), className)}
17
18
  data-orientation={orientation}
@@ -100,6 +100,7 @@ const Carousel = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLDivEleme
100
100
  scrollNext,
101
101
  scrollPrev,
102
102
  }}>
103
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: carousel is a custom widget needing roledescription */}
103
104
  <section
104
105
  aria-roledescription="carousel"
105
106
  className={cn('relative', className)}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import * as React from 'react'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
4
+ import { Bar, BarChart, XAxis } from 'recharts'
5
+ import { ChartContainer } from '../chart'
6
+
7
+ const data = [{ name: 'alpha', value: 12 }]
8
+
9
+ describe('registry-ui chart', () => {
10
+ test('ChartContainer server render does not emit invalid size warnings', () => {
11
+ const originalWarn = console.warn
12
+ const warnings: string[] = []
13
+
14
+ console.warn = (...args: unknown[]) => {
15
+ warnings.push(args.map((value) => String(value)).join(' '))
16
+ }
17
+
18
+ try {
19
+ const html = renderToStaticMarkup(
20
+ <ChartContainer
21
+ config={{
22
+ value: {
23
+ color: 'hsl(var(--chart-1))',
24
+ label: 'Value',
25
+ },
26
+ }}>
27
+ <BarChart accessibilityLayer data={data}>
28
+ <XAxis dataKey="name" hide />
29
+ <Bar dataKey="value" fill="var(--color-value)" radius={8} />
30
+ </BarChart>
31
+ </ChartContainer>,
32
+ )
33
+
34
+ expect(html).toContain('data-slot="chart-container"')
35
+ expect(warnings.some((message) => message.includes('The width(') && message.includes('height('))).toBe(false)
36
+ } finally {
37
+ console.warn = originalWarn
38
+ }
39
+ })
40
+ })
@@ -15,6 +15,7 @@ import type {
15
15
 
16
16
  // Format: { THEME_NAME: CSS_SELECTOR }
17
17
  export const THEMES = { dark: '.dark', light: '' } as const
18
+ const DEFAULT_CHART_INITIAL_DIMENSION = { width: 640, height: 360 } as const
18
19
 
19
20
  const ChartContext = React.createContext<ChartContextProps | null>(null)
20
21
 
@@ -46,7 +47,9 @@ const ChartContainer = ({ id, className, children, config, ref, dir, ...props }:
46
47
  dir={direction}
47
48
  ref={ref}>
48
49
  <ChartStyle config={config} id={chartId} />
49
- <RechartsPrimitive.ResponsiveContainer minWidth={0}>{children}</RechartsPrimitive.ResponsiveContainer>
50
+ <RechartsPrimitive.ResponsiveContainer initialDimension={DEFAULT_CHART_INITIAL_DIMENSION} minWidth={0}>
51
+ {children}
52
+ </RechartsPrimitive.ResponsiveContainer>
50
53
  </div>
51
54
  </ChartContext.Provider>
52
55
  )
@@ -61,6 +64,7 @@ const ChartStyle = ({ id, config }: ChartStyleProps) => {
61
64
 
62
65
  return (
63
66
  <style
67
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: controlled CSS injection for chart color themes
64
68
  dangerouslySetInnerHTML={{
65
69
  __html: Object.entries(THEMES)
66
70
  .map(
@@ -37,6 +37,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
37
37
  onCheckedChange?.(next)
38
38
  }
39
39
 
40
+ // biome-ignore lint/correctness/useExhaustiveDependencies: changeCheckedState is stable and defined in render scope
40
41
  React.useEffect(() => {
41
42
  if (ref && typeof ref !== 'function' && checked === 'indeterminate' && ref.current) {
42
43
  ref.current.indeterminate = true
@@ -44,6 +44,7 @@ const Collapsible = React.forwardRef<
44
44
  onOpenChange?.(state)
45
45
  }
46
46
 
47
+ // biome-ignore lint/correctness/useExhaustiveDependencies: handleOpenChange and triggerRef are stable refs
47
48
  React.useEffect(() => {
48
49
  if (open) {
49
50
  handleOpenChange(open)
@@ -56,7 +57,7 @@ const Collapsible = React.forwardRef<
56
57
 
57
58
  triggerRef.current?.addEventListener('click', handleClick)
58
59
  return () => triggerRef.current?.removeEventListener('click', handleClick)
59
- }, [open])
60
+ }, [open, onOpenChange])
60
61
 
61
62
  return (
62
63
  <CollapsibleContext.Provider
@@ -162,13 +162,19 @@ function CommandShortcut({
162
162
  )
163
163
  }
164
164
 
165
- function CommandDialog({ children, ...props }: React.ComponentPropsWithRef<typeof Dialog>): React.JSX.Element {
165
+ function CommandDialog({
166
+ children,
167
+ shouldFilter,
168
+ ...props
169
+ }: React.ComponentPropsWithRef<typeof Dialog> & { shouldFilter?: boolean }): React.JSX.Element {
166
170
  return (
167
171
  <Dialog {...props}>
168
172
  <DialogContent className="h-125 max-w-full p-0 lg:w-[700px]">
169
173
  <DialogTitle className="sr-only">Command palette</DialogTitle>
170
174
  <DialogDescription className="sr-only">Search for commands and navigation items</DialogDescription>
171
- <Command className="max-w-full">{children}</Command>
175
+ <Command className="max-w-full" shouldFilter={shouldFilter}>
176
+ {children}
177
+ </Command>
172
178
  </DialogContent>
173
179
  </Dialog>
174
180
  )
@@ -53,7 +53,9 @@ function DialogContentResponsive({
53
53
  const isDesktop = useMediaQuery('(min-width: 768px)')
54
54
 
55
55
  if (isDesktop) {
56
- return <DialogContent {...(props as any)}>{children}</DialogContent>
56
+ return (
57
+ <DialogContent {...(props as React.ComponentPropsWithoutRef<typeof DialogContent>)}>{children}</DialogContent>
58
+ )
57
59
  }
58
60
 
59
61
  return <DrawerContent {...(props as React.ComponentPropsWithoutRef<typeof DrawerContent>)}>{children}</DrawerContent>
@@ -58,6 +58,7 @@ function Field({
58
58
  ...props
59
59
  }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
60
60
  return (
61
+ // biome-ignore lint/a11y/useSemanticElements: field group role is semantically correct for form field grouping
61
62
  <div
62
63
  className={cn(fieldVariants({ orientation }), className)}
63
64
  data-orientation={orientation}
@@ -169,6 +170,7 @@ function FieldError({
169
170
 
170
171
  return (
171
172
  <ul className="ms-4 flex list-disc flex-col gap-1">
173
+ {/* biome-ignore lint/suspicious/noArrayIndexKey: error messages have no stable unique id */}
172
174
  {errors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
173
175
  </ul>
174
176
  )
@@ -12,6 +12,7 @@ const InputGroup = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutR
12
12
  ({ className, dir, children, ...props }, ref) => {
13
13
  const direction = useDirection(dir as Direction)
14
14
  return (
15
+ // biome-ignore lint/a11y/useSemanticElements: group role is semantically correct for input grouping
15
16
  <div
16
17
  className={cn(
17
18
  'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30',
@@ -66,6 +67,8 @@ const InputGroupAddon = React.forwardRef<
66
67
  React.ComponentPropsWithoutRef<'div'> & VariantProps<typeof inputGroupAddonVariants>
67
68
  >(({ className, align = 'inline-start', ...props }, ref) => {
68
69
  return (
70
+ // biome-ignore lint/a11y/useSemanticElements: group role is semantically correct for addon grouping
71
+ // biome-ignore lint/a11y/useKeyWithClickEvents: click handler delegates focus to input, not interactive itself
69
72
  <div
70
73
  className={cn(inputGroupAddonVariants({ align }), className)}
71
74
  data-align={align}
package/src/item/item.tsx CHANGED
@@ -10,6 +10,7 @@ const ItemGroup = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRe
10
10
  ({ className, dir, ...props }, ref) => {
11
11
  const direction = useDirection(dir as Direction)
12
12
  return (
13
+ // biome-ignore lint/a11y/useSemanticElements: list role on div is intentional for composed item patterns
13
14
  <div
14
15
  className={cn('group/item-group flex flex-col', className)}
15
16
  dir={direction}
@@ -243,7 +243,7 @@ export function JsonTextareaField<TFieldValues extends FieldValues>(
243
243
 
244
244
  const oneLine = committedText.replace(/\s+/g, ' ').trim()
245
245
  return oneLine.length > 120 ? `${oneLine.slice(0, 117)}...` : oneLine
246
- }, [committedText])
246
+ }, [committedText, t.nullPreview])
247
247
 
248
248
  const inlineEditor = (
249
249
  <div className="space-y-2" data-slot="json-editor-inline">
@@ -335,67 +335,65 @@ export function JsonTextareaField<TFieldValues extends FieldValues>(
335
335
  {fieldState.error ? <FieldError errors={[fieldState.error]} /> : null}
336
336
 
337
337
  {expandMode === 'sheet' ? (
338
- <>
339
- <Sheet
340
- onOpenChange={(nextOpen) => {
341
- if (nextOpen) {
342
- openSheet()
343
- return
344
- }
345
-
346
- requestCloseSheet()
347
- }}
348
- open={sheetOpen}>
349
- <SheetContent className="w-full sm:max-w-3xl" dir={dir} side={sheetSide}>
350
- <SheetHeader>
351
- <SheetTitle>{sheetTitle}</SheetTitle>
352
- </SheetHeader>
353
-
354
- <div className="mt-4 space-y-3" data-slot="json-editor-sheet-content">
355
- <JsonEditorView
356
- dir={dir}
357
- lang={lang}
358
- lineHeightPx={lineHeightPx}
359
- lineNumbers={lineNumbers}
360
- onChange={(value) => {
361
- setSheetDraft(value)
362
- setSheetDirty(true)
363
- }}
364
- onKeyDown={sheetHotkeys}
365
- onScroll={setSheetScrollTop}
366
- placeholder={placeholder}
367
- readOnly={!isEditable}
368
- rows={24}
369
- scrollTop={sheetScrollTop}
370
- value={sheetDraft}
371
- />
372
-
373
- <div className="flex items-center justify-between gap-2" data-slot="json-editor-sheet-actions">
374
- <div className="text-muted-foreground text-xs">{t.sheetStatusHint}</div>
375
-
376
- <div className="flex items-center gap-2">
377
- <Button
378
- disabled={!isEditable || !canFormatSheet}
379
- onClick={formatSheet}
380
- size="sm"
381
- type="button"
382
- variant="outline">
383
- {t.format}
384
- </Button>
385
-
386
- <Button onClick={requestCloseSheet} size="sm" type="button" variant="outline">
387
- {t.close}
388
- </Button>
389
-
390
- <Button disabled={!isEditable} onClick={saveSheet} size="sm" type="button">
391
- {t.save}
392
- </Button>
393
- </div>
338
+ <Sheet
339
+ onOpenChange={(nextOpen) => {
340
+ if (nextOpen) {
341
+ openSheet()
342
+ return
343
+ }
344
+
345
+ requestCloseSheet()
346
+ }}
347
+ open={sheetOpen}>
348
+ <SheetContent className="w-full sm:max-w-3xl" dir={dir} side={sheetSide}>
349
+ <SheetHeader>
350
+ <SheetTitle>{sheetTitle}</SheetTitle>
351
+ </SheetHeader>
352
+
353
+ <div className="mt-4 space-y-3" data-slot="json-editor-sheet-content">
354
+ <JsonEditorView
355
+ dir={dir}
356
+ lang={lang}
357
+ lineHeightPx={lineHeightPx}
358
+ lineNumbers={lineNumbers}
359
+ onChange={(value) => {
360
+ setSheetDraft(value)
361
+ setSheetDirty(true)
362
+ }}
363
+ onKeyDown={sheetHotkeys}
364
+ onScroll={setSheetScrollTop}
365
+ placeholder={placeholder}
366
+ readOnly={!isEditable}
367
+ rows={24}
368
+ scrollTop={sheetScrollTop}
369
+ value={sheetDraft}
370
+ />
371
+
372
+ <div className="flex items-center justify-between gap-2" data-slot="json-editor-sheet-actions">
373
+ <div className="text-muted-foreground text-xs">{t.sheetStatusHint}</div>
374
+
375
+ <div className="flex items-center gap-2">
376
+ <Button
377
+ disabled={!isEditable || !canFormatSheet}
378
+ onClick={formatSheet}
379
+ size="sm"
380
+ type="button"
381
+ variant="outline">
382
+ {t.format}
383
+ </Button>
384
+
385
+ <Button onClick={requestCloseSheet} size="sm" type="button" variant="outline">
386
+ {t.close}
387
+ </Button>
388
+
389
+ <Button disabled={!isEditable} onClick={saveSheet} size="sm" type="button">
390
+ {t.save}
391
+ </Button>
394
392
  </div>
395
393
  </div>
396
- </SheetContent>
397
- </Sheet>
398
- </>
394
+ </div>
395
+ </SheetContent>
396
+ </Sheet>
399
397
  ) : null}
400
398
 
401
399
  <Portal>
@@ -10,6 +10,7 @@ const Label = React.forwardRef<HTMLLabelElement, LabelProps>(({ className, htmlF
10
10
  const direction = useDirection(dir as Direction)
11
11
 
12
12
  return (
13
+ // biome-ignore lint/a11y/noLabelWithoutControl: label is composed with form controls externally via htmlFor
13
14
  <label
14
15
  className={cn(
15
16
  'text-balance font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
@@ -304,8 +304,8 @@ function PreviewPanel({
304
304
  if (!el) return
305
305
 
306
306
  const dist = (e: TouchEvent) => {
307
- const a = e.touches[0]!
308
- const b = e.touches[1]!
307
+ const a = e.touches[0] ?? { clientX: 0, clientY: 0 }
308
+ const b = e.touches[1] ?? { clientX: 0, clientY: 0 }
309
309
  return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY)
310
310
  }
311
311
 
@@ -17,7 +17,6 @@ const Separator = React.forwardRef<
17
17
  aria-orientation={orientation}
18
18
  className={cn('shrink-0 bg-border', orientation === 'horizontal' ? 'h-px w-full' : 'min-h-full w-px', className)}
19
19
  dir={direction}
20
- role="separator"
21
20
  {...props}
22
21
  data-slot="separator"
23
22
  />
@@ -61,7 +61,7 @@ function SidebarProvider({
61
61
  // Helper to toggle the sidebar.
62
62
  const toggleSidebar = React.useCallback(() => {
63
63
  return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
64
- }, [isMobile, setOpen, setOpenMobile])
64
+ }, [isMobile, setOpen])
65
65
 
66
66
  // Adds a keyboard shortcut to toggle the sidebar.
67
67
  React.useEffect(() => {
@@ -91,7 +91,7 @@ function SidebarProvider({
91
91
  toggleSidebar,
92
92
  dir: direction,
93
93
  }),
94
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, direction],
94
+ [state, open, setOpen, isMobile, openMobile, toggleSidebar, direction],
95
95
  )
96
96
 
97
97
  return (
@@ -46,6 +46,7 @@ function Slider({
46
46
  <SliderPrimitive.Thumb
47
47
  data-orientation={orientation}
48
48
  data-slot="slider-thumb"
49
+ // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional, index is the stable key
49
50
  key={index}
50
51
  className="relative block size-4 shrink-0 select-none rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:outline-hidden focus-visible:ring-3 active:ring-3 disabled:pointer-events-none disabled:opacity-50"
51
52
  />
@@ -31,6 +31,7 @@ const SonnerUpload = ({
31
31
  )}
32
32
  />
33
33
  <div className="flex w-full flex-col gap-2">
34
+ {/* biome-ignore lint/a11y/useSemanticElements: status role on div is intentional for live region announcements */}
34
35
  <div className="flex w-full justify-between" role="status">
35
36
  <p className="text-foreground text-sm">
36
37
  {progress >= 100
@@ -46,6 +46,7 @@ const Switch = React.forwardRef<
46
46
  onChange?.(e)
47
47
  onCheckedChange?.(e.target.checked)
48
48
  }}
49
+ aria-checked={props.checked}
49
50
  ref={ref}
50
51
  role="switch"
51
52
  dir={direction}
package/src/tabs/tabs.tsx CHANGED
@@ -35,7 +35,7 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
35
35
 
36
36
  React.useEffect(() => {
37
37
  if (onValueChange) onValueChange(activeItem)
38
- }, [activeItem])
38
+ }, [activeItem, onValueChange])
39
39
 
40
40
  return (
41
41
  <TabsContext.Provider value={{ activeItem, setActiveItem, tabsId }}>
@@ -74,7 +74,7 @@ const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
74
74
 
75
75
  React.useEffect(() => {
76
76
  if (defaultChecked) setActiveItem(value)
77
- }, [defaultChecked])
77
+ }, [defaultChecked, setActiveItem, value])
78
78
 
79
79
  return (
80
80
  <button
package/tsconfig.json CHANGED
@@ -19,7 +19,17 @@
19
19
  ],
20
20
  "rootDir": "./"
21
21
  },
22
- "exclude": ["node_modules", "dist", "./src/_old", "**/*.tsbuildinfo", "tsconfig.tsbuildinfo"],
22
+ "exclude": [
23
+ "node_modules",
24
+ "dist",
25
+ "./src/_old",
26
+ "**/__test__/**",
27
+ "**/__test__/**",
28
+ "**/*.test.ts",
29
+ "**/*.test.tsx",
30
+ "**/*.tsbuildinfo",
31
+ "tsconfig.tsbuildinfo"
32
+ ],
23
33
  "extends": "@gentleduck/typescript-config/base.json",
24
34
  "include": ["./**/*.ts", "./**/*.tsx"]
25
35
  }