Update Convex with no payload to be just like convex with payload but without payload

This commit is contained in:
Gabriel Brown
2026-06-21 15:35:42 -05:00
parent 13b8b36c4c
commit fba73a92ce
130 changed files with 15637 additions and 32018 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18 -9
View File
@@ -12,23 +12,27 @@
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
"test:unit": "vitest run --project unit --passWithNoTests",
"test:integration": "vitest run --project integration --passWithNoTests",
"test:component": "NODE_ENV=test vitest run --project component",
"ui-add": "bunx --bun shadcn@latest add && prettier src --write --list-different"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@tabler/icons-react": "^3.41.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -36,26 +40,31 @@
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.577.0",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react-day-picker": "^9.14.0",
"react-hook-form": "^7.65.0",
"react-hook-form": "^7.72.0",
"react-image-crop": "^11.0.10",
"react-resizable-panels": "^4",
"recharts": "^3.8.0",
"react-resizable-panels": "^4.7.6",
"recharts": "^3.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwind-merge": "^3.5.0",
"vaul": "^1.1.2"
},
"devDependencies": {
"@gib/eslint-config": "workspace:*",
"@gib/prettier-config": "workspace:*",
"@gib/tsconfig": "workspace:*",
"@gib/vitest-config": "workspace:*",
"@testing-library/react": "catalog:test",
"@types/react": "catalog:react19",
"eslint": "catalog:",
"jsdom": "catalog:test",
"prettier": "catalog:",
"react": "catalog:react19",
"typescript": "catalog:",
"vitest": "catalog:test",
"zod": "catalog:"
},
"peerDependencies": {
+3 -3
View File
@@ -4,12 +4,12 @@ import type { ComponentProps } from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { User } from 'lucide-react';
import { AvatarImage, cn } from '@gib/ui';
import { cn } from '@gib/ui';
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
src?: string | null;
fullName?: string | null;
imageProps?: Omit<ComponentProps<typeof AvatarImage>, 'data-slot'>;
imageProps?: Omit<ComponentProps<typeof AvatarPrimitive.Image>, 'data-slot'>;
fallbackProps?: ComponentProps<typeof AvatarPrimitive.Fallback>;
userIconProps?: ComponentProps<typeof User>;
};
@@ -35,7 +35,7 @@ const BasedAvatar = ({
{...props}
>
{src ? (
<AvatarImage
<AvatarPrimitive.Image
{...imageProps}
src={src}
className={imageProps?.className}
+1 -1
View File
@@ -45,7 +45,7 @@ const BasedProgress = ({
<ProgressPrimitive.Indicator
data-slot='progress-indicator'
className='bg-primary h-full w-full flex-1 transition-all'
style={{ transform: `translateX(-${100 - (progress ?? 0)}%)` }}
style={{ transform: `translateX(-${100 - progress}%)` }}
/>
</ProgressPrimitive.Root>
);
+2 -3
View File
@@ -98,7 +98,7 @@ const Carousel = ({
api.on('select', onSelect);
return () => {
api?.off('select', onSelect);
api.off('select', onSelect);
};
}, [api, onSelect]);
@@ -108,8 +108,7 @@ const Carousel = ({
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
orientation: orientation,
scrollPrev,
scrollNext,
canScrollPrev,
+51 -26
View File
@@ -48,7 +48,7 @@ const ChartContainer = ({
>['children'];
}) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`;
return (
<ChartContext.Provider value={{ config }}>
@@ -72,7 +72,7 @@ const ChartContainer = ({
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
([, config]) => config.theme ?? config.color,
);
if (!colorConfig.length) {
@@ -89,7 +89,7 @@ ${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
@@ -105,6 +105,15 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip;
type ChartPayloadItem = {
name?: string | number;
value?: number | string;
dataKey?: string | number;
type?: string;
color?: string;
payload?: Record<string, unknown> & { fill?: string };
};
const ChartTooltipContent = ({
active,
payload,
@@ -119,14 +128,29 @@ const ChartTooltipContent = ({
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<'div'> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}) => {
}: React.ComponentProps<'div'> & {
active?: boolean;
payload?: ChartPayloadItem[];
label?: React.ReactNode;
labelFormatter?: (
value: React.ReactNode,
payload: ChartPayloadItem[],
) => React.ReactNode;
formatter?: (
value: number | string,
name: string | number,
item: ChartPayloadItem,
index: number,
itemPayload: ChartPayloadItem['payload'],
) => React.ReactNode;
color?: string;
labelClassName?: string;
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
@@ -135,11 +159,11 @@ const ChartTooltipContent = ({
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === 'string'
? config[label]?.label || label
? (config[label]?.label ?? label)
: itemConfig?.label;
if (labelFormatter) {
@@ -183,19 +207,19 @@ const ChartTooltipContent = ({
{payload
.filter((item) => item.type !== 'none')
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
const indicatorColor = color ?? item.payload?.fill ?? item.color;
return (
<div
key={item.dataKey}
key={item.dataKey ?? index}
className={cn(
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
indicator === 'dot' && 'items-center',
)}
>
{formatter && item?.value !== undefined && item.name ? (
{formatter && item.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
@@ -232,7 +256,7 @@ const ChartTooltipContent = ({
<div className='grid gap-1.5'>
{nestLabel ? tooltipLabel : null}
<span className='text-muted-foreground'>
{itemConfig?.label || item.name}
{itemConfig?.label ?? item.name}
</span>
</div>
{item.value && (
@@ -259,11 +283,12 @@ const ChartLegendContent = ({
payload,
verticalAlign = 'bottom',
nameKey,
}: React.ComponentProps<'div'> &
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean;
nameKey?: string;
}) => {
}: React.ComponentProps<'div'> & {
payload?: ChartPayloadItem[];
verticalAlign?: 'top' | 'bottom';
hideIcon?: boolean;
nameKey?: string;
}) => {
const { config } = useChart();
if (!payload?.length) {
@@ -280,13 +305,13 @@ const ChartLegendContent = ({
>
{payload
.filter((item) => item.type !== 'none')
.map((item) => {
const key = `${nameKey || item.dataKey || 'value'}`;
.map((item, index) => {
const key = `${nameKey ?? item.dataKey ?? 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
key={item.value ?? index}
className={cn(
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
)}
+10 -7
View File
@@ -68,13 +68,16 @@ const ComboboxInput = ({
/>
<InputGroupAddon align='inline-end'>
{showTrigger && (
<InputGroupButton
size='icon-xs'
variant='ghost'
render={<ComboboxTrigger />}
data-slot='input-group-button'
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
disabled={disabled}
<ComboboxTrigger
render={
<InputGroupButton
size='icon-xs'
variant='ghost'
data-slot='input-group-button'
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
disabled={disabled}
/>
}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
+1
View File
@@ -0,0 +1 @@
declare module '*.css';
+1 -5
View File
@@ -46,10 +46,6 @@ const useFormField = () => {
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
@@ -138,7 +134,7 @@ const FormDescription = ({
const FormMessage = ({ className, ...props }: React.ComponentProps<'p'>) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? '') : props.children;
const body = error ? String(error.message ?? '') : props.children;
if (!body) {
return null;
@@ -1,5 +1,4 @@
import * as React from 'react';
import { MousePointerClick, X } from 'lucide-react';
type EventType =
| 'mousedown'
+23 -18
View File
@@ -153,7 +153,7 @@ export const ImageCrop = ({
useEffect(() => {
const reader = new FileReader();
reader.addEventListener('load', () =>
setImgSrc(reader.result?.toString() || ''),
setImgSrc(typeof reader.result === 'string' ? reader.result : ''),
);
reader.readAsDataURL(file);
}, [file]);
@@ -173,12 +173,13 @@ export const ImageCrop = ({
onChange?.(pixelCrop, percentCrop);
};
const handleComplete = async (
const handleComplete = (
pixelCrop: PixelCrop,
percentCrop: PercentCrop,
) => {
): Promise<void> => {
setCompletedCrop(pixelCrop);
onComplete?.(pixelCrop, percentCrop);
return Promise.resolve();
};
const applyCrop = async () => {
@@ -293,19 +294,17 @@ export const ImageCropApply = ({
if (asChild) {
return (
<Slot.Root onClick={handleClick} {...(props as any)}>
<Slot.Root
onClick={handleClick}
{...(props as ComponentProps<typeof Slot.Root>)}
>
{children}
</Slot.Root>
);
}
return (
<Button
onClick={handleClick}
size='icon'
variant='ghost'
{...(props as any)}
>
<Button onClick={handleClick} size='icon' variant='ghost' {...props}>
{children ?? <CropIcon className='size-4' />}
</Button>
);
@@ -330,19 +329,17 @@ export const ImageCropReset = ({
if (asChild) {
return (
<Slot.Root onClick={handleClick} {...(props as any)}>
<Slot.Root
onClick={handleClick}
{...(props as ComponentProps<typeof Slot.Root>)}
>
{children}
</Slot.Root>
);
}
return (
<Button
onClick={handleClick}
size='icon'
variant='ghost'
{...(props as any)}
>
<Button onClick={handleClick} size='icon' variant='ghost' {...props}>
{children ?? <RotateCcwIcon className='size-4' />}
</Button>
);
@@ -372,7 +369,15 @@ export const Cropper = ({
onChange={onChange}
onComplete={onComplete}
onCrop={onCrop}
{...(props as any)}
{...(props as Omit<
ImageCropProps,
| 'file'
| 'maxImageSize'
| 'onChange'
| 'onComplete'
| 'onCrop'
| 'children'
>)}
>
<ImageCropContent className={className} style={style} />
</ImageCrop>
+1 -1
View File
@@ -47,7 +47,7 @@ const InputOTPSlot = ({
index: number;
}) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] ?? {};
return (
<div
+1 -1
View File
@@ -21,7 +21,7 @@ const Progress = ({
<ProgressPrimitive.Indicator
data-slot='progress-indicator'
className='bg-primary size-full flex-1 transition-all'
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
+3 -3
View File
@@ -602,9 +602,9 @@ const SidebarMenuSkeleton = ({
showIcon?: boolean;
}) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
const [width] = React.useState(
() => `${Math.floor(Math.random() * 40) + 50}%`,
);
return (
<div
+4 -4
View File
@@ -58,13 +58,13 @@ const ToggleGroupItem = ({
return (
<ToggleGroupPrimitive.Item
data-slot='toggle-group-item'
data-variant={context.variant || variant}
data-size={context.size || size}
data-variant={context.variant ?? variant}
data-size={context.size ?? size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
variant: context.variant ?? variant,
size: context.size ?? size,
}),
'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10',
'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l',
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Button } from '../../src/button';
describe('Button', () => {
it('renders its children', () => {
render(<Button>Click me</Button>);
expect(
screen.getByRole('button', { name: 'Click me' }),
).toBeInTheDocument();
});
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+1 -1
View File
@@ -5,6 +5,6 @@
"jsx": "preserve",
"rootDir": "."
},
"include": ["src"],
"include": ["src", "tests", "vitest.config.ts"],
"exclude": ["node_modules"]
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';
import { jsdomProject, nodeProject } from '@gib/vitest-config';
export default defineConfig({
test: {
projects: [
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
],
},
});