Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/floppy-spoons-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@radui/ui": patch
---

detach prop support for all comps
4 changes: 3 additions & 1 deletion src/components/ui/Accordion/fragments/AccordionRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

export type AccordionRootProps = Omit<React.ComponentPropsWithoutRef<'div'>, 'defaultValue'> & {
customRootClass?: string;
detach?: boolean;
transitionDuration?: number;
transitionTimingFunction?: string;
orientation?: 'horizontal' | 'vertical';
Expand All @@ -31,6 +32,7 @@
transitionDuration = 0,
transitionTimingFunction = 'linear',
customRootClass,
detach = false,
loop = true,
openMultiple = false,
value,
Expand All @@ -39,7 +41,7 @@
...props
}, forwardedRef) => {
const accordionRef = useRef<HTMLDivElement | null>(null);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detach mode still outputs -root class in AccordionRoot.

Line 149 unconditionally appends -root. With detach=true (Line 44 path), this still emits a generated token (-root) instead of no class output.

Also applies to: 149-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Accordion/fragments/AccordionRoot.tsx` at line 44, The
AccordionRoot currently always appends the "-root" token because rootClass is
computed unconditionally by calling customClassSwitcher(customRootClass,
COMPONENT_NAME, detach); update the logic so that when detach is true no
generated "-root" token is emitted: either change the call site in AccordionRoot
to set rootClass = '' when detach === true, or modify customClassSwitcher to
early-return an empty string when the detach flag is true; reference the symbols
AccordionRoot, customClassSwitcher, customRootClass, COMPONENT_NAME and the
detach variable when making the change so the rootClass no longer contains the
"-root" suffix in detach mode.

const [fixedHeight, setFixedHeight] = useState<number | null>(null);
const heightLockTimeoutRef = useRef<NodeJS.Timeout>();
const previousActiveItemsRef = useRef<(number | string)[]>([]);
Expand Down Expand Up @@ -94,7 +96,7 @@
// Temporarily unlock to measure the target height
setFixedHeight(null);
// Force reflow to get accurate measurement
const _ = accordionRef.current.offsetHeight;

Check warning on line 99 in src/components/ui/Accordion/fragments/AccordionRoot.tsx

View workflow job for this annotation

GitHub Actions / lint

'_' is assigned a value but never used

requestAnimationFrame(() => {
if (accordionRef.current) {
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/AlertDialog/fragments/AlertDialogRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type DialogPrimitiveRootProps = React.ComponentPropsWithoutRef<typeof DialogPrim

export type AlertDialogRootProps = Omit<DialogPrimitiveRootProps, 'open' | 'onOpenChange'> & {
customRootClass?: string;
detach?: boolean;
className?: string;
defaultOpen?: boolean;
open?: boolean;
Expand All @@ -24,12 +25,13 @@ const AlertDialogRoot = forwardRef<AlertDialogRootElement, AlertDialogRootProps>
children,
className = '',
customRootClass = '',
detach = false,
defaultOpen = false,
open,
onOpenChange,
...props
}, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const [isOpen, setIsOpen] = useControllableState(open, defaultOpen, onOpenChange);
const [titleId, setTitleId] = useState<string | undefined>(undefined);
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/AspectRatio/AspectRatio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ const COMPONENT_NAME = 'AspectRatio';

export type AspectRatioProps = ComponentPropsWithoutRef<'div'> & {
customRootClass?: string;
detach?: boolean;
ratio?: string;
};

const AspectRatio = React.forwardRef<ElementRef<'div'>, AspectRatioProps>(({ children, customRootClass, className, ratio = '1', ...props }, ref) => {
const AspectRatio = React.forwardRef<ElementRef<'div'>, AspectRatioProps>(({ children, customRootClass, detach = false, className, ratio = '1', ...props }, ref) => {
if (isNaN(Number(ratio)) && !ratio.match(/^(\d+)\/(\d+)$/)) ratio = '1';
if (Number(ratio) <= 0) ratio = '1';

const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
return (
<div ref={ref} style={{ aspectRatio: ratio }} className={clsx(rootClass, className)} {...props}>
{children}
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Avatar/fragments/AvatarRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ const COMPONENT_NAME = 'Avatar';

export type AvatarRootProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitiveRoot> & {
customRootClass?: string;
detach?: boolean;
className?: string;
size?: string;
variant?: string;
color?: string;
}

const AvatarRoot = React.forwardRef<React.ElementRef<typeof AvatarPrimitiveRoot>, AvatarRootProps>(({ children, customRootClass = '', className = '', size = '', variant = '', color = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const AvatarRoot = React.forwardRef<React.ElementRef<typeof AvatarPrimitiveRoot>, AvatarRootProps>(({ children, customRootClass = '', detach = false, className = '', size = '', variant = '', color = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
Comment on lines +19 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

detach currently leaks dash-prefixed Avatar child classes.

When Line [20] yields rootClass = '', AvatarImage / AvatarFallback still build ${rootClass}-image and ${rootClass}-fallback, producing -image / -fallback instead of no generated classes.

Proposed fix for descendant class guards
# src/components/ui/Avatar/fragments/AvatarImage.tsx
- className={clsx(`${rootClass}-image`)}
+ className={clsx(rootClass && `${rootClass}-image`)}

# src/components/ui/Avatar/fragments/AvatarFallback.tsx
- className={clsx(`${rootClass}-fallback`, className)}
+ className={clsx(rootClass && `${rootClass}-fallback`, className)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Avatar/fragments/AvatarRoot.tsx` around lines 19 - 20,
AvatarRoot can produce dash-prefixed child classes when rootClass is empty
(e.g., AvatarImage/AvatarFallback end up with "-image"/"-fallback") because
rootClass is blindly concatenated; update AvatarRoot to ensure rootClass is
non-empty before it is used to build descendant class names (use
customClassSwitcher(customRootClass, COMPONENT_NAME, detach) but guard/normalize
its return to '' when it would produce a lone dash), and update any places that
derive `${rootClass}-image` / `${rootClass}-fallback` so they only append the
dash when rootClass is truthy; focus changes in the AvatarRoot component
(variables: rootClass, customRootClass, detach, and function
customClassSwitcher) and ensure AvatarImage and AvatarFallback class
construction logic checks rootClass before adding the '-' prefix.


const dataAttributes = useCreateDataAttribute('avatar', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/AvatarGroup/fragments/AvatarGroupRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { useCreateDataAttribute, useComposeAttributes } from '~/core/hooks/creat

export type AvatarGroupRootProps = React.ComponentPropsWithoutRef<'div'> & {
customRootClass?: string | '';
detach?: boolean;
size?: string;
variant?: string;
};

const COMPONENT_NAME = 'AvatarGroup';

const AvatarGroupRoot = React.forwardRef<HTMLDivElement, AvatarGroupRootProps>(({ customRootClass = '', size = '', variant = '', children, className = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const AvatarGroupRoot = React.forwardRef<HTMLDivElement, AvatarGroupRootProps>(({ customRootClass = '', detach = false, size = '', variant = '', children, className = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
const dataAttributes = useCreateDataAttribute('avatar', { variant, size });
const composedAttributes = useComposeAttributes(dataAttributes());

Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import { useCreateDataAttribute, useComposeAttributes, useCreateDataAccentColorA
const COMPONENT_NAME = 'Badge';
export type BadgeProps = React.ComponentPropsWithoutRef<'div'> & {
customRootClass?: string;
detach?: boolean;
variant?: string;
size?: string;
color?: string;
};

const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(({ children, customRootClass = '', className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(({ children, customRootClass = '', detach = false, className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const dataAttributes = useCreateDataAttribute('badge', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/BlockQuote/BlockQuote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ const COMPONENT_NAME = 'BlockQuote';

export type BlockQuoteProps = React.ComponentPropsWithoutRef<'blockquote'> & {
customRootClass?: string;
detach?: boolean;
color?: string;
variant?: string;
size?: string;
};

const BlockQuote = React.forwardRef<React.ElementRef<'blockquote'>, BlockQuoteProps>(
({ children, customRootClass = '', className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
({ children, customRootClass = '', detach = false, className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const data_attributes: Record<string, string> = {};

Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const COMPONENT_NAME = 'Button';

export type ButtonProps = {
customRootClass?: string;
detach?: boolean;
variant?: string;
size?: string;
color?: string;
Expand All @@ -24,6 +25,7 @@ const Button = forwardRef<ElementRef<typeof ButtonPrimitive>, ButtonProps>(
children,
type = 'button',
customRootClass = '',
detach = false,
className = '',
variant = '',
size = '',
Expand All @@ -32,7 +34,7 @@ const Button = forwardRef<ElementRef<typeof ButtonPrimitive>, ButtonProps>(
onClick,
...props
}, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
// apply data attribute for accent color
// apply attribute only if color is present
const dataAttributes = useCreateDataAttribute('button', { variant, size });
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Callout/fragments/CalloutRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ type CalloutRootProps = PrimitiveDivProps & {
intent?: string; // Semantic intent: 'destructive' | 'warning' | 'info' | etc.
size?: string;
customRootClass?: string;
detach?: boolean;
};

const CalloutRoot = React.forwardRef<CalloutRootElement, CalloutRootProps>(
(
{ children, asChild = false, className = '', color = '', variant = '', intent = '', size = '', customRootClass = '', ...props },
{ children, asChild = false, className = '', color = '', variant = '', intent = '', size = '', customRootClass = '', detach = false, ...props },
ref
) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

// Backward compatibility: if variant is "destructive", treat it as intent
// This allows existing code to continue working while migrating to intent/variant separation
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Card/fragments/CardRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { useCreateDataAttribute } from '~/core/hooks/createDataAttribute';
const COMPONENT_NAME = 'Card';
export type CardRootProps = React.ComponentPropsWithoutRef<'div'> & {
customRootClass?: string;
detach?: boolean;
variant?: string;
size?: string;
};

const CardRoot = React.forwardRef<HTMLDivElement, CardRootProps>(({ children, customRootClass, className = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const CardRoot = React.forwardRef<HTMLDivElement, CardRootProps>(({ children, customRootClass, detach = false, className = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
const dataAttributes = useCreateDataAttribute('card', { variant, size });

return (
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Checkbox/fragments/CheckboxRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ const COMPONENT_NAME = 'Checkbox';
export type CheckboxRootElement = ElementRef<typeof CheckboxPrimitiveRoot>;
export type CheckboxRootProps = {
customRootClass?: string;
detach?: boolean;
color?: string;
variant?: string;
size?: string;
} & ComponentPropsWithoutRef<typeof CheckboxPrimitiveRoot>;

const CheckboxRoot = forwardRef<CheckboxRootElement, CheckboxRootProps>(({ children, className = '', customRootClass, color = '', variant, size, ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const CheckboxRoot = forwardRef<CheckboxRootElement, CheckboxRootProps>(({ children, className = '', customRootClass, detach = false, color = '', variant, size, ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const dataAttributes = useCreateDataAttribute('checkbox', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ const COMPONENT_NAME = 'CheckboxCards';
export type CheckboxCardsRootElement = ElementRef<typeof CheckboxGroupPrimitive.Root>;
export type CheckboxCardsRootProps = {
customRootClass?: string;
detach?: boolean;
color?: string;
variant?: string;
size?: string;
} & ComponentPropsWithoutRef<typeof CheckboxGroupPrimitive.Root>;

const CheckboxCardsRoot = forwardRef<CheckboxCardsRootElement, CheckboxCardsRootProps>(({ children, customRootClass = '', className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const CheckboxCardsRoot = forwardRef<CheckboxCardsRootElement, CheckboxCardsRootProps>(({ children, customRootClass = '', detach = false, className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const dataAttributes = useCreateDataAttribute('checkbox-cards', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ const COMPONENT_NAME = 'CheckboxGroup';
export type CheckboxGroupRootElement = ElementRef<typeof CheckboxGroupPrimitive.Root>;
export type CheckboxGroupRootProps = {
customRootClass?: string;
detach?: boolean;
color?: string;
variant?: string;
size?: string;
} & ComponentPropsWithoutRef<typeof CheckboxGroupPrimitive.Root>;

const CheckboxGroupRoot = forwardRef<CheckboxGroupRootElement, CheckboxGroupRootProps>(({ children, customRootClass = '', className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const CheckboxGroupRoot = forwardRef<CheckboxGroupRootElement, CheckboxGroupRootProps>(({ children, customRootClass = '', detach = false, className = '', color = '', variant = '', size = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
Comment on lines +19 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

detach=true still emits a generated -root class.

Line [20] can set rootClass to empty, but Line [28] always adds ${rootClass}-root, which becomes -root.

Proposed fix
- <CheckboxGroupPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, rootClass, className)} {...props} {...composedAttributes()}>
+ <CheckboxGroupPrimitive.Root
+   ref={ref}
+   className={clsx(rootClass && `${rootClass}-root`, rootClass, className)}
+   {...props}
+   {...composedAttributes()}
+ >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/CheckboxGroup/fragments/CheckboxGroupRoot.tsx` around lines
19 - 20, CheckboxGroupRoot builds rootClass via
customClassSwitcher(customRootClass, COMPONENT_NAME, detach) but later
unconditionally appends `${rootClass}-root`, which yields `-root` when detach
true (rootClass empty); update the JSX/className assembly in the
CheckboxGroupRoot component so the `${rootClass}-root` token is only added when
rootClass is non-empty (e.g., guard the addition based on rootClass truthiness)
and continue to respect props customRootClass and detach so no stray `-root`
class is emitted.


const dataAttributes = useCreateDataAttribute('checkbox-group', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/Code/Code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const COMPONENT_NAME = 'Code';

export type CodeProps = React.ComponentPropsWithoutRef<'code'> & {
customRootClass?: string;
detach?: boolean;
variant?: string;
size?: string;
color?: string;
Expand All @@ -15,13 +16,14 @@ export type CodeProps = React.ComponentPropsWithoutRef<'code'> & {
const Code = React.forwardRef<React.ElementRef<'code'>, CodeProps>(({
children,
customRootClass = '',
detach = false,
color = '',
variant = '',
size = '',
className,
...props
}, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const data_attributes: Record<string, string> = {};

Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Collapsible/fragments/CollapsibleRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ export type CollapsibleRootProps = React.ComponentPropsWithoutRef<
typeof CollapsiblePrimitive.Root
> & {
customRootClass?: string;
detach?: boolean;
};

const CollapsibleRoot = React.forwardRef<
CollapsibleRootElement,
CollapsibleRootProps
>(({ children, className = '', transitionDuration = 0, disabled, customRootClass, ...props }, forwardedRef) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
>(({ children, className = '', transitionDuration = 0, disabled, customRootClass, detach = false, ...props }, forwardedRef) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
return (
<CollapsibleContext.Provider value={{ rootClass }}>
<CollapsiblePrimitive.Root
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Combobox/fragments/ComboboxRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ const COMPONENT_NAME = 'Combobox';
type ComboboxRootElement = React.ElementRef<typeof ComboboxPrimitive.Root>;
type ComboboxRootProps = React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Root> & {
customRootClass?: string;
detach?: boolean;
};

const ComboboxRoot = React.forwardRef<ComboboxRootElement, ComboboxRootProps>(
({ customRootClass, children, defaultValue, value, onValueChange, shift, ...props }, forwardedRef) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
({ customRootClass, detach = false, children, defaultValue, value, onValueChange, shift, ...props }, forwardedRef) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

return (
<ComboboxRootContext.Provider value={{ rootClass }}>
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/ContextMenu/fragments/ContextMenuRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ export type ContextMenuRootElement = ElementRef<typeof MenuPrimitive.Root>;
export type ContextMenuRootProps = {
children: React.ReactNode;
customRootClass?: string;
detach?: boolean;
className?: string;
} & ComponentPropsWithoutRef<typeof MenuPrimitive.Root>;

const COMPONENT_NAME = 'ContextMenu';

const ContextMenuRoot = forwardRef<ContextMenuRootElement, ContextMenuRootProps>(({ children, customRootClass, className, open, defaultOpen = false, onOpenChange, ...props }, ref) => {
const ContextMenuRoot = forwardRef<ContextMenuRootElement, ContextMenuRootProps>(({ children, customRootClass, detach = false, className, open, defaultOpen = false, onOpenChange, ...props }, ref) => {
const [isOpen, setIsOpen] = useControllableState(
open,
defaultOpen,
onOpenChange
);
const [coords, setCoords] = React.useState({ x: 0, y: 0 });
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
return (
<ContextMenuContext.Provider value={{ rootClass, setCoords, setIsOpen }} >
<MenuPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, className)} mainAxisOffset={-coords.y} crossAxisOffset={coords.x} open={isOpen} onOpenChange={setIsOpen} {...props}>
Comment on lines +18 to 28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detach is incomplete: generated class tokens are still emitted.

Line 28 builds ${rootClass}-root; when detached this becomes -root. The same pattern exists in ContextMenuContent, ContextMenuTrigger, and ContextMenuItem, producing -content/-trigger/-item. That violates the requested detach behavior.

Proposed root-level fix
-            <MenuPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, className)} mainAxisOffset={-coords.y} crossAxisOffset={coords.x} open={isOpen} onOpenChange={setIsOpen} {...props}>
+            <MenuPrimitive.Root ref={ref} className={clsx(rootClass && `${rootClass}-root`, className)} mainAxisOffset={-coords.y} crossAxisOffset={coords.x} open={isOpen} onOpenChange={setIsOpen} {...props}>

Please mirror this guard in the sibling fragments that append -${part} to rootClass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/ContextMenu/fragments/ContextMenuRoot.tsx` around lines 18
- 28, The string interpolation that appends part suffixes (e.g.,
`${rootClass}-root`) emits leading hyphens when rootClass is empty due to
detach; update ContextMenuRoot (and sibling fragments ContextMenuContent,
ContextMenuTrigger, ContextMenuItem) to only append
`-root`/`-content`/`-trigger`/`-item` when rootClass is non-empty—use the
existing customClassSwitcher/COMPONENT_NAME logic to compute rootClass and guard
the template concatenation so that when detach produces an empty rootClass you
render just `rootClass` (empty) plus any className instead of `-root` (i.e.,
conditionally join the suffix only if rootClass !== "").

Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/DataList/fragments/DataListRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ const COMPONENT_NAME = 'DataList';
type DataListRootElement = ElementRef<'div'>;
export interface DataListRootProps extends ComponentPropsWithoutRef<'div'> {
customRootClass?: string;
detach?: boolean;
}

const DataListRoot = forwardRef<DataListRootElement, DataListRootProps>(({ children, className = '', customRootClass = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const DataListRoot = forwardRef<DataListRootElement, DataListRootProps>(({ children, className = '', customRootClass = '', detach = false, ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
Comment on lines +11 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for string interpolation patterns with rootClass in DataList
rg -n '\$\{rootClass\}' src/components/ui/DataList/

Repository: rad-ui/ui

Length of output: 535


🏁 Script executed:

#!/bin/bash
# Find customClassSwitcher implementation to understand what it returns
rg -n 'customClassSwitcher' src/components/ui/DataList/ -A 3 -B 3

Repository: rad-ui/ui

Length of output: 1360


🏁 Script executed:

#!/bin/bash
# Find the definition of customClassSwitcher function
fd -t f -e tsx -e ts . | xargs rg -l 'export.*customClassSwitcher|const customClassSwitcher|function customClassSwitcher'

Repository: rad-ui/ui

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check DataListRoot to see what COMPONENT_NAME and customRootClass values might be
rg -n 'COMPONENT_NAME\|customRootClass' src/components/ui/DataList/fragments/DataListRoot.tsx -B 2 -A 2

Repository: rad-ui/ui

Length of output: 35


🏁 Script executed:

#!/bin/bash
# Read the customClassSwitcher implementation
cat src/core/customClassSwitcher/index.ts

Repository: rad-ui/ui

Length of output: 779


🏁 Script executed:

#!/bin/bash
# Also check what's exported from core to understand the function signature
head -50 src/core/index.ts

Repository: rad-ui/ui

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Search for usage of DataList component with detach prop
rg -n 'detach\s*=' src/components/ui/DataList/ -A 2 -B 2

Repository: rad-ui/ui

Length of output: 622


🏁 Script executed:

#!/bin/bash
# Search for DataList usage in stories or examples
fd -t f -e tsx -e ts . | xargs rg -l 'DataList' | head -20

Repository: rad-ui/ui

Length of output: 528


🏁 Script executed:

#!/bin/bash
# Check if there are tests for DataList detach behavior
fd -t f \( -name '*.test.tsx' -o -name '*.spec.tsx' -o -name '*.stories.tsx' \) | xargs rg -l 'DataList' 2>/dev/null

Repository: rad-ui/ui

Length of output: 899


🏁 Script executed:

#!/bin/bash
# Check test file for detach usage
rg -n 'detach' src/components/ui/DataList/tests/DataList.test.tsx

Repository: rad-ui/ui

Length of output: 35


🏁 Script executed:

#!/bin/bash
# Check stories file for detach examples
rg -n 'detach' src/components/ui/DataList/stories/DataList.stories.tsx

Repository: rad-ui/ui

Length of output: 35


🏁 Script executed:

#!/bin/bash
# Read the child component implementations to confirm the pattern
cat src/components/ui/DataList/fragments/DataListValue.tsx

Repository: rad-ui/ui

Length of output: 706


🏁 Script executed:

#!/bin/bash
# Check the README or docs for DataList component purpose and detach usage
fd -t f \( -name 'README*' -o -name '*.md' \) . | xargs rg -l 'DataList' | head -5

Repository: rad-ui/ui

Length of output: 573


🏁 Script executed:

#!/bin/bash
# Look at all child components to see the pattern consistency
cat src/components/ui/DataList/fragments/DataListLabel.tsx src/components/ui/DataList/fragments/DataListItem.tsx

Repository: rad-ui/ui

Length of output: 1355


🏁 Script executed:

#!/bin/bash
# Check if detach is exported or documented in the public API
rg -n 'detach' src/components/ui/DataList/DataList.tsx

Repository: rad-ui/ui

Length of output: 35


Malformed CSS class names when detach=true is used.

The implementation produces invalid class names. When detach=true, customClassSwitcher returns an empty string, causing child components to generate malformed classes like -value, -label, -item (with leading hyphens). This occurs because all child components use ${rootClass}-suffix interpolation without handling the empty rootClass case.

Since detach is not used in tests, stories, or the public API, either:

  1. Remove the unused detach property entirely, or
  2. Fix child components to handle empty rootClass (e.g., ${rootClass ? rootClass + '-' : ''}value), or
  3. Modify customClassSwitcher to return a sensible default instead of empty string when detach=true.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/DataList/fragments/DataListRoot.tsx` around lines 11 - 15,
The DataListRoot component's detach prop causes customClassSwitcher to return an
empty rootClass which makes child components that build classes like
`${rootClass}-value`, `${rootClass}-label`, `${rootClass}-item` produce
malformed names (e.g., `-value`); fix by either removing the unused detach prop
from DataListRoot and its type, or updating one of two spots: (A) change
customClassSwitcher so when detach=true it returns a sensible default base class
(not an empty string), or (B) update child components that reference rootClass
(the templates producing `${rootClass}-...`) to guard against empty rootClass by
using a prefix expression such as `${rootClass ? rootClass + '-' : ''}value` so
no leading hyphen is produced. Ensure references include DataListRoot, detach,
customClassSwitcher and the child class interpolation sites.

return <DataListContext.Provider
value={{
rootClass
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Dialog/fragments/DialogRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ type DialogPrimitiveRootProps = React.ComponentPropsWithoutRef<typeof DialogPrim

export type DialogRootProps = DialogPrimitiveRootProps & {
customRootClass?: string;
detach?: boolean;
className?: string;
};

const DialogRoot = forwardRef<DialogRootElement, DialogRootProps>(({ children, customRootClass = '', className = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const DialogRoot = forwardRef<DialogRootElement, DialogRootProps>(({ children, customRootClass = '', detach = false, className = '', ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);

const contextProps = { rootClass };
Comment on lines +20 to 23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detach leaks generated suffix classes in Dialog children.

When detach is true, rootClass becomes empty, but downstream fragments still build ${rootClass}-*, yielding classes like -trigger instead of removing generated classes entirely.

Suggested fix (context + child guard)
// src/components/ui/Dialog/fragments/DialogRoot.tsx
- const contextProps = { rootClass };
+ const contextProps = { rootClass, detach };
// Example pattern for Dialog child fragments
- className={clsx(`${rootClass}-trigger`, className)}
+ className={clsx(detach ? undefined : `${rootClass}-trigger`, className)}

Apply the same guard pattern to Trigger/Content/Overlay/Close.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Dialog/fragments/DialogRoot.tsx` around lines 20 - 23,
DialogRoot currently sets contextProps.rootClass to an empty string when detach
is true, which lets downstream fragments (Trigger/Content/Overlay/Close) build
invalid classes like "-trigger"; update DialogRoot and the fragments so that the
suffixes are only generated when rootClass is non-empty: compute and expose
either an empty string or full suffixes (e.g., `${rootClass}-trigger`) or expose
rootClass and have each fragment guard when composing their class (use a
conditional like rootClass ? `${rootClass}-<suffix>` : ''), and apply this same
guard pattern to Trigger, Content, Overlay and Close so no leading hyphen
classes are emitted when detach is true.


Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Disclosure/fragments/DisclosureRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ const COMPONENT_NAME = 'Disclosure';

export type DisclosureRootProps = React.ComponentPropsWithoutRef<'div'> & {
customRootClass?: string;
detach?: boolean;
defaultOpen?: number | null;
};

const DisclosureRoot = React.forwardRef<React.ElementRef<'div'>, DisclosureRootProps>(({ children, customRootClass, 'aria-label': ariaLabel, ...props }, forwardedRef) => {
const DisclosureRoot = React.forwardRef<React.ElementRef<'div'>, DisclosureRootProps>(({ children, customRootClass, detach = false, 'aria-label': ariaLabel, ...props }, forwardedRef) => {
const disclosureRef = useRef<React.ElementRef<'div'> | null>(null);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Empty rootClass produces -root class names when detach=true.

When detach=true, rootClass is '', causing both line 42 and line 45 to produce `-root` as the class name instead of no class output.

Additionally, the same ${rootClass}-root class is applied to both RovingFocusGroup.Group (line 42) and the inner div (line 45). This duplication may be intentional, but worth confirming.

Also applies to: 42-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Disclosure/fragments/DisclosureRoot.tsx` at line 18,
rootClass (computed by customClassSwitcher with arguments customRootClass,
COMPONENT_NAME, detach) can be an empty string causing className templates like
`${rootClass}-root` to produce `-root`; update the DisclosureRoot render so you
only append `-root` when rootClass is non-empty (e.g., conditionally add
`${rootClass}-root`), and confirm whether the `${rootClass}-root` should be
applied to RovingFocusGroup.Group or the inner div (RovingFocusGroup.Group and
the inner div currently both receive it) — remove the duplicate application if
it was unintentional and apply the conditional logic to the className
expressions for RovingFocusGroup.Group and the inner div so no `-root` appears
when detach=true.


const [activeItem, setActiveItem] = useState<number | null>(null);

Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ export type DropdownMenuRootElement = ElementRef<typeof MenuPrimitive.Root>;
export type DropdownMenuRootProps = {
children: React.ReactNode;
customRootClass?: string;
detach?: boolean;
className?: string;
} & ComponentPropsWithoutRef<typeof MenuPrimitive.Root>;

const COMPONENT_NAME = 'DropdownMenu';

const DropdownMenuRoot = forwardRef<DropdownMenuRootElement, DropdownMenuRootProps>(({ children, customRootClass, className, ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const DropdownMenuRoot = forwardRef<DropdownMenuRootElement, DropdownMenuRootProps>(({ children, customRootClass, detach = false, className, ...props }, ref) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach);
return (
<DropdownMenuContext.Provider value={{ rootClass }} >
<MenuPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, className)} {...props}>
Comment on lines +17 to 21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

detach still emits a generated class token on root.

Line 21 still produces -root when detach=true (rootClass === ''), which breaks the “no generated classes” contract.

Proposed fix
-            <MenuPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, className)} {...props}>
+            <MenuPrimitive.Root ref={ref} className={clsx(rootClass && `${rootClass}-root`, className)} {...props}>

Also apply the same guard in child fragments that build ${rootClass}-... classes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/DropdownMenu/fragments/DropdownMenuRoot.tsx` around lines
17 - 21, The root currently concatenates `${rootClass}-root` even when detach is
true and rootClass is empty, producing a stray "-root"; update DropdownMenuRoot
to only append the suffix when rootClass is non-empty (e.g., compute the
className as rootClass ? `${rootClass}-root` : undefined or omit the generated
token) before passing it to MenuPrimitive.Root and into
DropdownMenuContext.Provider, and apply the same guard in all child fragments
that construct `${rootClass}-...` class names (use the same conditional
concatenation pattern wherever rootClass is used).

Expand Down
Loading
Loading