Skip to content
31 changes: 31 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
Expand Down
65 changes: 65 additions & 0 deletions client/src/app/test/checkbox/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { CheckboxGroup, CheckboxItem } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";

const DATES = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];

export default function CheckboxTestPage() {
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
const [dates, setDates] = useState<string[]>([]);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert(
`Submitted data:\nacceptCondition: ${acceptTerms.toString()}\ndates: ${JSON.stringify(dates)}`,
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name in the alert message is inconsistent: it shows "acceptCondition" but the actual state variable is named "acceptTerms" (line 19). This should be "acceptTerms" to match the variable name.

Suggested change
`Submitted data:\nacceptCondition: ${acceptTerms.toString()}\ndates: ${JSON.stringify(dates)}`,
`Submitted data:\nacceptTerms: ${acceptTerms.toString()}\ndates: ${JSON.stringify(dates)}`,

Copilot uses AI. Check for mistakes.
);
};

return (
<div className="min-h-screen p-8">
<div className="mx-auto my-5 flex w-full max-w-lg flex-col items-center justify-center">
<form onSubmit={handleSubmit} className="w-full rounded-md border p-4">
{/* a single checkbox */}
<p className="title mb-3">A single checkbox</p>
<CheckboxItem
id="acceptTerms"
name="acceptTerms"
checked={acceptTerms}
onCheckedChange={(checked) => setAcceptTerms(checked === true)} // Radix Output can be true | false | "indeterminate"
>
<Label htmlFor="acceptTerms"> Accept terms and conditions. </Label>
</CheckboxItem>
{/* a checkbox group */}
<p className="title mb-3 mt-5">A checkbox group</p>
<CheckboxGroup value={dates} onValueChange={setDates}>
{DATES.map((date) => (
<CheckboxItem key={date} value={date} id={date}>
<Label htmlFor={date}> {date} </Label>
</CheckboxItem>
))}
</CheckboxGroup>
<Button type="submit" className="mx-auto mt-4">
Submit
</Button>
</form>
<p className="mt-4 text-center">
Current acceptTerms value: {acceptTerms.toString()}
</p>
<p className="mt-4 text-center">
Current dates message: {JSON.stringify(dates, null, 1)}
</p>
</div>
</div>
);
}
127 changes: 127 additions & 0 deletions client/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Usage:
// A single checkbox: use [checked, setChecked] = useState<boolean>(initialValue) to get the checkbox status
// <CheckboxItem
// checked={checked}
// onCheckedChange={(checked) => setChecked(checked === true)} // As Radix Output can be true | false | "indeterminate", simple pass setChecked will cause Typescript complaint
// // optional props can be passed as in standard html checkbox
// >
// {children} // add label here
//</CheckboxItem>

// A group of checkbox: use [value, setValue] = useState<string[]>(initialValue) to get the selected value
// <CheckboxGroup value={value} onValueChange={setValue}>
// <CheckboxItem value={value1}>{children}</CheckboxItem> // value is a necessary field in this case
// <CheckboxItem value={value2}>{children}</CheckboxItem>
// </CheckboxGroup>

Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage documentation at the top of the file should be formatted as JSDoc comments (/** ... */) instead of regular comments (// ...), consistent with the documentation pattern used in other UI components like radio-group.tsx. This improves IDE support and discoverability.

Suggested change
// Usage:
// A single checkbox: use [checked, setChecked] = useState<boolean>(initialValue) to get the checkbox status
// <CheckboxItem
// checked={checked}
// onCheckedChange={(checked) => setChecked(checked === true)} // As Radix Output can be true | false | "indeterminate", simple pass setChecked will cause Typescript complaint
// // optional props can be passed as in standard html checkbox
// >
// {children} // add label here
//</CheckboxItem>
// A group of checkbox: use [value, setValue] = useState<string[]>(initialValue) to get the selected value
// <CheckboxGroup value={value} onValueChange={setValue}>
// <CheckboxItem value={value1}>{children}</CheckboxItem> // value is a necessary field in this case
// <CheckboxItem value={value2}>{children}</CheckboxItem>
// </CheckboxGroup>
/**
* Usage:
*
* A single checkbox: use `[checked, setChecked] = useState<boolean>(initialValue)` to get the checkbox status
* ```tsx
* <CheckboxItem
* checked={checked}
* onCheckedChange={(checked) => setChecked(checked === true)} // As Radix Output can be true | false | "indeterminate", simple pass setChecked will cause Typescript complaint
* // optional props can be passed as in standard html checkbox
* >
* {children} // add label here
* </CheckboxItem>
* ```
*
* A group of checkbox: use `[value, setValue] = useState<string[]>(initialValue)` to get the selected value
* ```tsx
* <CheckboxGroup value={value} onValueChange={setValue}>
* <CheckboxItem value={value1}>{children}</CheckboxItem> // value is a necessary field in this case
* <CheckboxItem value={value2}>{children}</CheckboxItem>
* </CheckboxGroup>
* ```
*/

Copilot uses AI. Check for mistakes.
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "use client" directive is missing at the top of this file. Other similar Radix UI component files in this codebase (e.g., accordion.tsx, select.tsx) include this directive, which is necessary for components that use React hooks like useCallback and useContext in Next.js App Router.

Copilot uses AI. Check for mistakes.
import React, { useCallback, useContext } from "react";
import { FaCheck } from "react-icons/fa";

import { cn } from "@/lib/utils";

type CheckboxGroupContextValue = {
value: string[];
onChange: (itemValue: string, checked: boolean) => void;
};
const CheckboxGroupContext =
React.createContext<CheckboxGroupContextValue | null>(null);

interface CheckboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
value: string[];
onValueChange: (value: string[]) => void;
}

const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
({ className, value, onValueChange, children, ...props }, ref) => {
const onItemValueChange = useCallback(
(itemValue: string, checked: boolean) => {
if (checked) {
onValueChange([...value, itemValue]);
} else {
onValueChange(value.filter((v) => v !== itemValue));
}
},
[value, onValueChange],
);

return (
<CheckboxGroupContext.Provider
value={{ value, onChange: onItemValueChange }}
>
<div ref={ref} className={cn("grid gap-2", className)} {...props}>
{children}
</div>
</CheckboxGroupContext.Provider>
);
},
);

CheckboxGroup.displayName = "CheckboxGroup";

interface CheckboxItemProps
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
value?: string;
}

// checkbox item
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This single-line comment should either be removed or converted to JSDoc format (/** ... */) to document the component, consistent with other UI components in the codebase like radio-group.tsx.

Suggested change
// checkbox item
/**
* CheckboxItem component for rendering a single checkbox.
* Can be used standalone or within a CheckboxGroup.
*/

Copilot uses AI. Check for mistakes.
const CheckboxItem = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
CheckboxItemProps
>(
(
{
className,
id: propsId,
value,
checked,
onCheckedChange,
children,
...props
},
ref,
) => {
const context = useContext(CheckboxGroupContext);
const id = propsId || value;

// if checkbox is used in a group, value must be provided
if (context && !value) {
throw new Error("Value is required when checkbox is put in a group");
}

// if checkbox is not used in a group, use the default checked and onCheckedChange
const isGrouped = !!value && !!context;
const isChecked = isGrouped ? context.value.includes(value) : checked;
const handleCheckedChange = isGrouped
? (checked: boolean) => context.onChange(value, checked)
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleCheckedChange callback assumes checked is a boolean, but Radix UI's CheckedState type can be true | false | "indeterminate". This may cause runtime errors when an indeterminate state is passed. Consider handling the indeterminate state or casting to boolean: (checked: CheckedState) => context.onChange(value, checked === true)

Suggested change
? (checked: boolean) => context.onChange(value, checked)
? (checked: CheckboxPrimitive.CheckedState) => context.onChange(value, checked === true)

Copilot uses AI. Check for mistakes.
: onCheckedChange;

return (
<div className="flex items-center gap-1">
<CheckboxPrimitive.Root
ref={ref}
id={id}
className={cn(
"h-6 w-6 rounded-sm",
"border-2 border-gray-300",
"data-[state=checked]:border-none data-[state=checked]:bg-bloom-orbit data-[state=checked]:text-primary-foreground",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
checked={isChecked}
onCheckedChange={handleCheckedChange}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center">
<FaCheck className="h-4 w-4 text-white" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
{children}
</div>
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkbox and label are rendered in separate elements without an explicit wrapper label element. While the htmlFor/id association works, wrapping both in a <label> element would improve click target area and accessibility. Consider wrapping the entire div content in a label element or document that labels should be passed as children and associated via htmlFor.

Copilot uses AI. Check for mistakes.
);
},
);

CheckboxItem.displayName = "CheckboxItem";

export { CheckboxGroup, CheckboxItem };