fix(dify-ui): preserve state-aware styling callbacks (#42631)

This commit is contained in:
yyh
2026-09-21 05:56:21 +00:00
committed by GitHub
parent 11038c5936
commit 22aff66cc0
47 changed files with 1010 additions and 770 deletions

View File

@@ -26,15 +26,17 @@ Prefer:
## Styling
For Dify UI/Base UI state styling and callback composition, apply the package's [Styling] contract,
including its documented reasons for direct state access.
Flag:
- New CSS modules or ad hoc CSS when Tailwind utilities and Dify tokens cover the need.
- Component-level plain `.css` files or component CSS imported through `globals.css`; use scoped `*.module.css` only when Tailwind and component variants cannot express the style.
- Generic color utilities where Dify semantic tokens exist.
- Hardcoded magic class values for colors, spacing, radius, shadow, z-index, or typography when Dify tokens, component variants, or documented radius mappings exist.
- Hardcoded magic class values for colors, spacing, radius, shadow, z-index, or typography when Dify tokens, component variants, or standard Tailwind radius classes match the required design value. Apply [Styling] when choosing radius classes.
- `!` important modifiers or important CSS overrides without a narrow, documented reason.
- Manual class-list assembly through string concatenation, template strings, array `.join(' ')`, or a custom conditional combiner instead of `cn(...)`. Conditional values passed to `cn(...)` remain valid.
- JS conditional class branches for primitive visual states already exposed by Dify UI/Base UI `data-*` selectors.
- Incoming `className` placed before default classes in `cn(...)`, preventing call-site overrides.
- Arbitrary z-index or one-off layering fixes on overlays.
@@ -43,7 +45,6 @@ Use:
- `cn(...)` from the local package or utility already used by the file.
- Dify semantic tokens and Tailwind v4 utilities.
- Existing component variants before one-off class forks.
- Primitive selectors such as `data-disabled:*`, `data-checked:*`, `data-highlighted:*`, `group-data-*`, `peer-data-*`, and `has-[:focus-visible]` before adding React state or boolean props solely for styling.
- Component-level variants, semantic tokens, and normal cascade/order before `!` overrides. Use `!` only for a contained compatibility override that cannot be expressed through the component API or local selector structure.
## Imports
@@ -66,3 +67,5 @@ Flag:
- Error messages that state only the failure and not the next step.
Use feature-local translation keys by default. Alias only when crossing namespaces. `pnpm i18n:check --file <name>` should pass for any touched translation namespace.
[Styling]: ../../../../packages/dify-ui/docs/styling.md

View File

@@ -16,7 +16,7 @@ owner:
| Forms and field semantics | `packages/dify-ui/docs/forms.md` |
| Selection and typed values | `packages/dify-ui/docs/selection.md` |
| Portals, layers, and floating surfaces | `packages/dify-ui/docs/overlays.md` |
| Tailwind and radius tokens | `packages/dify-ui/docs/styling.md` |
| State styling, callbacks, and Tailwind | `packages/dify-ui/docs/styling.md` |
| Package tests and Storybook | `packages/dify-ui/docs/testing.md` |
For consumer code under `web/`, also read `web/AGENTS.md` for application-owned reuse policy and

View File

@@ -10,6 +10,7 @@ Use this skill to route component architecture decisions to its bundled referenc
## Topic Routing
- Component moves, module boundaries, props, types, or owner placement: read [`references/ownership.md`][ownership].
- Dify UI wrappers or primitive state styling: read the package's [Public API authoring] and [Styling] contracts.
- Jotai, form drafts, route identity, URL state, or persistence: read [`references/state.md`][state].
- Generated contracts, nullable API data, Query, mutations, SSR, auth, or workspace state: read [`references/data.md`][data].
- Hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces: read [`references/interactions.md`][interactions] and the overlay guide it references when applicable. Also read [`references/state.md`][state] when the surface owns a draft or other local session state.
@@ -24,6 +25,8 @@ Identify the behavior owner, state lifetime, and public contract from the nearby
- Write canonical Tailwind v4 classes; prefer canonical utilities over equivalent arbitrary values.
- Common forms include `w-105` instead of `w-[420px]`, `px-2.25` instead of `px-[9px]`, `bg-linear-to-b` instead of `bg-gradient-to-b`, `wrap-break-word` instead of `break-words`, and `field-sizing-content` instead of `[field-sizing:content]`.
[Public API authoring]: ../../../packages/dify-ui/docs/authoring.md
[Styling]: ../../../packages/dify-ui/docs/styling.md
[data]: references/data.md
[interactions]: references/interactions.md
[ownership]: references/ownership.md

View File

@@ -12,8 +12,8 @@ This file owns the package boundary. Read the matching contract guide below dire
- Prefer `@base-ui/react` when it owns the required headless behavior. Style primitives with `cva`,
`cn`, and Dify design tokens. Keep one primitive per `src/<name>/` folder with optional colocated
stories and tests.
- Prefer Base UI data attributes and CSS variables for visual states. Do not mirror primitive state
in React solely to add classes.
- Prefer Base UI data attributes for state styling and CSS variables for exposed dynamic values.
Follow [Styling] for state callbacks; do not mirror primitive state in React solely to add classes.
- When an upstream API or selector contract is unclear, read the current official Base UI
documentation and installed `@base-ui/react` declarations before coding.
@@ -26,7 +26,7 @@ This file owns the package boundary. Read the matching contract guide below dire
- Form structure, labels, and value ownership: [Forms]
- Picker choice and typed values: [Selection]
- Portals, presence, layering, and floating-surface semantics: [Overlays]
- Tailwind integration and radius mapping: [Styling]
- State styling, callbacks, composition, Tailwind integration, and border radius: [Styling]
- Package test ownership and setup: [Testing and development]
A component needs a local README only when it owns a substantial Dify-specific contract that its

View File

@@ -68,15 +68,15 @@ Upstream behavior remains owned by the [Base UI documentation].
### Cross-component guides
| Guide | Scope |
| ----------------------------------- | ------------------------------------------------------------------------------ |
| [Accessible names and descriptions] | Naming sources, descriptions, overrides, and safe label removal. |
| [Forms] | Native submit boundaries, value ownership, fields, labels, and errors. |
| [Selection] | Typed values and choosing among segmented controls, pickers, and radio groups. |
| [Overlays] | Portals, presence lifecycles, layering, trigger composition, and semantics. |
| [Styling] | Tailwind CSS integration and the Figma radius mapping. |
| [Public API authoring] | Subpath exports, naming, public types, generics, and private helpers. |
| [Testing and development] | Package commands, test ownership, accessibility, and animation setup. |
| Guide | Scope |
| ----------------------------------- | ------------------------------------------------------------------------------- |
| [Accessible names and descriptions] | Naming sources, descriptions, overrides, and safe label removal. |
| [Forms] | Native submit boundaries, value ownership, fields, labels, and errors. |
| [Selection] | Typed values and choosing among segmented controls, pickers, and radio groups. |
| [Overlays] | Portals, presence lifecycles, layering, trigger composition, and semantics. |
| [Styling] | State styling, callbacks, composition, Tailwind integration, and border radius. |
| [Public API authoring] | Subpath exports, naming, public types, generics, and private helpers. |
| [Testing and development] | Package commands, test ownership, accessibility, and animation setup. |
## Contributing

View File

@@ -57,11 +57,15 @@ State, event details and reasons, actions, controlled-state helpers, context val
helpers, styling helpers, and upstream passthrough aliases are private by default. Public props
already provide contextual typing for inline render and event callbacks.
When a wrapper consumes `className` through `cn()`, omit the upstream state-callback form and
expose `className?: string`. Public types must describe behavior the wrapper actually implements.
Preserve upstream `className` and `style` callbacks. Resolve `className` with the owning Base UI
part's state before merging default classes with `cn()`, including through composite wrappers.
Forward `style` unchanged unless the wrapper needs to merge styles; then resolve its callback first.
Do not add state callbacks to native DOM props or unrelated custom APIs. See [Styling].
## Evidence
Use local public-subpath type tests to protect generic inference, required relationships, and
intentional errors. Read current official Base UI documentation and installed type declarations
before changing an upstream-derived contract.
[Styling]: ./styling.md

View File

@@ -1,5 +1,31 @@
# Styling
## Component state
Prefer the primitive's data attributes for state styling, such as `data-checked:` or
`data-disabled:`. Use its CSS variables for exposed dynamic values, such as popup anchor width
and available height. Do not mirror primitive state in React solely to style it.
Where supported by the upstream part, `className(state)` and `style(state)` read that part's state.
At call sites, before using a callback or React state to calculate styles, check whether data attributes or CSS
variables already express the requirement. When direct state access is needed, add a short comment
explaining why; for example, Tooltip and Popover can share `data-popup-open` on one trigger while
the style needs only Popover's open state. This does not restrict conditional rendering of content.
Composite content forwards these callbacks to its styled part: for example, `PopoverContent`
receives Popup state, not Positioner state. Native DOM props and custom convenience APIs do not
automatically support state callbacks.
Use `render` for element composition, not merely to change styles. A callback on `PopoverTrigger`
reads Popover state; one on a standalone `Button` reads Button state. Composition does not change
which state a part owns.
With `render={<Button />}`, Base UI automatically merges the target's props without resolving
its callbacks. Put callbacks on the outer primitive and use class strings and style objects on
that target. With `render={(props, state) => ...}`, forward the received props and ref, and explicitly
merge any custom classes, styles, or handlers. Do not switch to this form merely for state styling.
See Base UI's [styling] and [composition] guides for the upstream contracts.
## Tailwind CSS v4
Import Tailwind from the consumer's root stylesheet as described in the [Tailwind CSS v4 upgrade
@@ -21,26 +47,12 @@ stylesheet:
@source not '../../../packages/dify-ui/src/**/*.stories.{ts,tsx}';
```
## Figma radius mapping
## Border radius
Figma radius tokens are offset by one step from Tailwind CSS v4 defaults. Use this mapping instead
of adding custom theme values or `radius-*` utilities:
| Figma token | Tailwind class |
| --------------- | ---------------- |
| `--radius/2xs` | `rounded-xs` |
| `--radius/xs` | `rounded-sm` |
| `--radius/sm` | `rounded-md` |
| `--radius/md` | `rounded-lg` |
| `--radius/lg` | `rounded-[10px]` |
| `--radius/xl` | `rounded-xl` |
| `--radius/2xl` | `rounded-2xl` |
| `--radius/3xl` | `rounded-[20px]` |
| `--radius/6xl` | `rounded-[28px]` |
| `--radius/full` | `rounded-full` |
Convert Figma output such as `rounded-[var(--radius/sm, 6px)]` to the mapped Tailwind class. Use an
arbitrary value only when no standard class matches.
Check the actual radius value in Figma before choosing a `rounded-*` class; matching token names
do not imply matching values. For example, `radius/sm = 6px` requires `rounded-md`, not
`rounded-sm` (4px). Prefer a standard Tailwind class with the matching value; use an arbitrary
value only when none matches, such as `rounded-[10px]` for 10px.
Use semantic Dify tokens and existing component variants before hard-coded values or repeated
primitive classes. Use an important modifier only for a tightly scoped compatibility override
@@ -52,3 +64,5 @@ contains the native focus target, select that descendant state from the wrapper;
[Tailwind CSS functions and directives]: https://tailwindcss.com/docs/functions-and-directives
[Tailwind CSS v4 upgrade guide]: https://tailwindcss.com/docs/upgrade-guide
[composition]: https://base-ui.com/react/handbook/composition
[styling]: https://base-ui.com/react/handbook/styling

View File

@@ -5,6 +5,7 @@ import type { ButtonProps } from '../button'
import { AlertDialog as BaseAlertDialog } from '@base-ui/react/alert-dialog'
import { Button } from '../button'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { modalBackdropClassName, modalPopupAnimationClassName } from '../overlay-shared'
const AlertDialog = BaseAlertDialog.Root
@@ -17,17 +18,19 @@ type AlertDialogTriggerProps<Payload = unknown> = BaseAlertDialog.Trigger.Props<
type AlertDialogTitleProps = BaseAlertDialog.Title.Props
type AlertDialogDescriptionProps = BaseAlertDialog.Description.Props
type AlertDialogBackdropProps = Omit<BaseAlertDialog.Backdrop.Props, 'className'> & {
className?: string
}
type AlertDialogBackdropProps = BaseAlertDialog.Backdrop.Props
function AlertDialogBackdrop({ className, ...props }: AlertDialogBackdropProps) {
return <BaseAlertDialog.Backdrop {...props} className={cn(modalBackdropClassName, className)} />
return (
<BaseAlertDialog.Backdrop
{...props}
className={(state) => cn(modalBackdropClassName, resolveClassName(className, state))}
/>
)
}
type AlertDialogContentProps = Omit<BaseAlertDialog.Popup.Props, 'children' | 'className'> & {
type AlertDialogContentProps = Omit<BaseAlertDialog.Popup.Props, 'children'> & {
children: React.ReactNode
className?: string
backdropProps?: AlertDialogBackdropProps
}
@@ -41,11 +44,13 @@ function AlertDialogContent({
<BaseAlertDialog.Portal>
<AlertDialogBackdrop {...backdropProps} />
<BaseAlertDialog.Popup
className={cn(
'fixed top-1/2 left-1/2 z-50 max-h-[calc(100vh-2rem)] w-120 max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
modalPopupAnimationClassName,
className,
)}
className={(state) =>
cn(
'fixed top-1/2 left-1/2 z-50 max-h-[calc(100vh-2rem)] w-120 max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
modalPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
>
{children}

View File

@@ -7,6 +7,7 @@ import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlCompoundInputFocusClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
import {
floatingGroupLabelClassName,
floatingItemIndicatorClassName,
@@ -104,8 +105,8 @@ const autocompleteInputGroupVariants = cva(
},
)
type AutocompleteInputGroupProps = Omit<BaseAutocomplete.InputGroup.Props, 'className'> &
VariantProps<typeof autocompleteInputGroupVariants> & { className?: string }
type AutocompleteInputGroupProps = BaseAutocomplete.InputGroup.Props &
VariantProps<typeof autocompleteInputGroupVariants>
function AutocompleteInputGroup({
className,
@@ -114,7 +115,9 @@ function AutocompleteInputGroup({
}: AutocompleteInputGroupProps) {
return (
<BaseAutocomplete.InputGroup
className={cn(autocompleteInputGroupVariants({ size }), className)}
className={(state) =>
cn(autocompleteInputGroupVariants({ size }), resolveClassName(className, state))
}
{...props}
/>
)
@@ -141,8 +144,8 @@ const autocompleteInputVariants = cva(
},
)
type AutocompleteInputProps = Omit<BaseAutocomplete.Input.Props, 'className' | 'size'> &
VariantProps<typeof autocompleteInputVariants> & { className?: string }
type AutocompleteInputProps = Omit<BaseAutocomplete.Input.Props, 'size'> &
VariantProps<typeof autocompleteInputVariants>
function AutocompleteInput({
className,
@@ -153,7 +156,9 @@ function AutocompleteInput({
return (
<BaseAutocomplete.Input
autoComplete={autoComplete}
className={cn(autocompleteInputVariants({ size }), className)}
className={(state) =>
cn(autocompleteInputVariants({ size }), resolveClassName(className, state))
}
{...props}
/>
)
@@ -183,8 +188,8 @@ const autocompleteControlVariants = cva(
},
)
type AutocompleteTriggerProps = Omit<BaseAutocomplete.Trigger.Props, 'className'> &
VariantProps<typeof autocompleteControlVariants> & { className?: string }
type AutocompleteTriggerProps = BaseAutocomplete.Trigger.Props &
VariantProps<typeof autocompleteControlVariants>
function AutocompleteTrigger({
className,
@@ -200,7 +205,9 @@ function AutocompleteTrigger({
props['aria-label'] ??
(props['aria-labelledby'] ? undefined : 'Open autocomplete suggestions')
}
className={cn(autocompleteControlVariants({ size }), className)}
className={(state) =>
cn(autocompleteControlVariants({ size }), resolveClassName(className, state))
}
{...props}
>
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
@@ -208,8 +215,8 @@ function AutocompleteTrigger({
)
}
type AutocompleteClearProps = Omit<BaseAutocomplete.Clear.Props, 'className'> &
VariantProps<typeof autocompleteControlVariants> & { className?: string }
type AutocompleteClearProps = BaseAutocomplete.Clear.Props &
VariantProps<typeof autocompleteControlVariants>
function AutocompleteClear({
className,
@@ -224,11 +231,13 @@ function AutocompleteClear({
aria-label={
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Clear autocomplete')
}
className={cn(
autocompleteControlVariants({ size }),
'data-ending-style:opacity-0 data-starting-style:opacity-0',
className,
)}
className={(state) =>
cn(
autocompleteControlVariants({ size }),
'data-ending-style:opacity-0 data-starting-style:opacity-0',
resolveClassName(className, state),
)
}
{...props}
>
{children ?? <span className="i-ri-close-line size-4" aria-hidden="true" />}
@@ -236,14 +245,14 @@ function AutocompleteClear({
)
}
type AutocompleteIconProps = Omit<BaseAutocomplete.Icon.Props, 'className'> & {
className?: string
}
type AutocompleteIconProps = BaseAutocomplete.Icon.Props
function AutocompleteIcon({ className, children, ...props }: AutocompleteIconProps) {
return (
<BaseAutocomplete.Icon
className={cn('flex shrink-0 items-center text-text-tertiary', className)}
className={(state) =>
cn('flex shrink-0 items-center text-text-tertiary', resolveClassName(className, state))
}
{...props}
>
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
@@ -254,11 +263,7 @@ function AutocompleteIcon({ className, children, ...props }: AutocompleteIconPro
const AutocompletePortal = BaseAutocomplete.Portal
type AutocompletePortalProps = BaseAutocomplete.Portal.Props
type AutocompletePositionerProps = Omit<
BaseAutocomplete.Positioner.Props,
'className' | 'side' | 'align'
> & {
className?: string
type AutocompletePositionerProps = Omit<BaseAutocomplete.Positioner.Props, 'side' | 'align'> & {
placement?: Placement
}
@@ -275,47 +280,53 @@ function AutocompletePositioner({
side={side}
align={align}
sideOffset={sideOffset}
className={cn('z-50 outline-hidden', className)}
className={(state) => cn('z-50 outline-hidden', resolveClassName(className, state))}
{...props}
/>
)
}
type AutocompletePopupProps = Omit<BaseAutocomplete.Popup.Props, 'className'> & {
className?: string
}
type AutocompletePopupProps = BaseAutocomplete.Popup.Props
function AutocompletePopup({ className, ...props }: AutocompletePopupProps) {
return (
<BaseAutocomplete.Popup
className={cn(autocompletePopupClassName, floatingPopupAnimationClassName, className)}
className={(state) =>
cn(
autocompletePopupClassName,
floatingPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type AutocompleteListProps<Value = unknown> = Omit<
BaseAutocomplete.List.Props,
'children' | 'className'
> & {
type AutocompleteListProps<Value = unknown> = Omit<BaseAutocomplete.List.Props, 'children'> & {
children?: React.ReactNode | ((item: Value, index: number) => React.ReactNode)
className?: string
}
function AutocompleteList<Value = unknown>({ className, ...props }: AutocompleteListProps<Value>) {
return <BaseAutocomplete.List className={cn(autocompleteListClassName, className)} {...props} />
return (
<BaseAutocomplete.List
className={(state) => cn(autocompleteListClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type AutocompleteItemProps<Value = unknown> = Omit<
BaseAutocomplete.Item.Props,
'className' | 'value'
> & {
className?: string
type AutocompleteItemProps<Value = unknown> = Omit<BaseAutocomplete.Item.Props, 'value'> & {
value?: Value
}
function AutocompleteItem<Value = unknown>({ className, ...props }: AutocompleteItemProps<Value>) {
return <BaseAutocomplete.Item className={cn(autocompleteItemClassName, className)} {...props} />
return (
<BaseAutocomplete.Item
className={(state) => cn(autocompleteItemClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type AutocompleteItemTextProps = React.ComponentProps<'span'>
@@ -326,53 +337,52 @@ function AutocompleteItemText({ className, ...props }: AutocompleteItemTextProps
)
}
type AutocompleteGroupLabelProps = Omit<BaseAutocomplete.GroupLabel.Props, 'className'> & {
className?: string
}
type AutocompleteGroupLabelProps = BaseAutocomplete.GroupLabel.Props
function AutocompleteGroupLabel({ className, ...props }: AutocompleteGroupLabelProps) {
return (
<BaseAutocomplete.GroupLabel
className={cn(floatingGroupLabelClassName, className)}
className={(state) => cn(floatingGroupLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type AutocompleteSeparatorProps = Omit<BaseAutocomplete.Separator.Props, 'className'> & {
className?: string
}
type AutocompleteSeparatorProps = BaseAutocomplete.Separator.Props
function AutocompleteSeparator({ className, ...props }: AutocompleteSeparatorProps) {
return (
<BaseAutocomplete.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
<BaseAutocomplete.Separator
className={(state) => cn(floatingSeparatorClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type AutocompleteEmptyProps = Omit<BaseAutocomplete.Empty.Props, 'className'> & {
className?: string
}
type AutocompleteEmptyProps = BaseAutocomplete.Empty.Props
function AutocompleteEmpty({ className, ...props }: AutocompleteEmptyProps) {
return (
<BaseAutocomplete.Empty
className={cn(
'px-3 py-2 system-sm-regular text-text-tertiary empty:h-0 empty:p-0',
className,
)}
className={(state) =>
cn(
'px-3 py-2 system-sm-regular text-text-tertiary empty:h-0 empty:p-0',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type AutocompleteStatusProps = Omit<BaseAutocomplete.Status.Props, 'className'> & {
className?: string
}
type AutocompleteStatusProps = BaseAutocomplete.Status.Props
function AutocompleteStatus({ className, ...props }: AutocompleteStatusProps) {
return (
<BaseAutocomplete.Status
className={cn('px-3 py-2 system-sm-regular text-text-tertiary', className)}
className={(state) =>
cn('px-3 py-2 system-sm-regular text-text-tertiary', resolveClassName(className, state))
}
{...props}
/>
)

View File

@@ -3,6 +3,7 @@
import type { ImageLoadingStatus } from '@base-ui/react/avatar'
import { Avatar as BaseAvatar } from '@base-ui/react/avatar'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const avatarSizeClasses = {
xxs: { root: 'size-4', text: 'text-[7px]' },
@@ -25,47 +26,49 @@ type AvatarProps = {
onLoadingStatusChange?: (status: ImageLoadingStatus) => void
}
type AvatarRootProps = Omit<BaseAvatar.Root.Props, 'className'> & {
type AvatarRootProps = BaseAvatar.Root.Props & {
size?: AvatarSize
className?: string
}
function AvatarRoot({ size = 'md', className, ...props }: AvatarRootProps) {
return (
<BaseAvatar.Root
className={cn(
'relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-primary-600 select-none',
avatarSizeClasses[size].root,
className,
)}
className={(state) =>
cn(
'relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-primary-600 select-none',
avatarSizeClasses[size].root,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type AvatarFallbackProps = Omit<BaseAvatar.Fallback.Props, 'className'> & {
type AvatarFallbackProps = BaseAvatar.Fallback.Props & {
size?: AvatarSize
className?: string
}
function AvatarFallback({ size = 'md', className, ...props }: AvatarFallbackProps) {
return (
<BaseAvatar.Fallback
className={cn(
'flex size-full items-center justify-center font-medium text-white',
avatarSizeClasses[size].text,
className,
)}
className={(state) =>
cn(
'flex size-full items-center justify-center font-medium text-white',
avatarSizeClasses[size].text,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type AvatarImageProps = Omit<BaseAvatar.Image.Props, 'className'> & {
className?: string
}
type AvatarImageProps = BaseAvatar.Image.Props
function AvatarImage({ className, ...props }: AvatarImageProps) {
return (
<BaseAvatar.Image
className={cn('absolute inset-0 size-full object-cover', className)}
className={(state) =>
cn('absolute inset-0 size-full object-cover', resolveClassName(className, state))
}
{...props}
/>
)

View File

@@ -91,15 +91,17 @@ at call sites:
```
Regular (`medium`) and `large` sizes use 4px and 6px gaps. `small` uses 3px for `primary` and 4px
for the other variants. Use a `className` override only for a documented layout exception.
for the other variants. Override these gaps through `className` only for a documented layout exception.
## Related guides
- Read [`IconButton`] for icon-only actions.
- Read [Accessible names and descriptions] when choosing or changing a naming source.
- Read [Styling] for state styles, callbacks, and their composition boundary.
- Read [Base UI Button] for the upstream interaction and composition contract.
[Accessible names and descriptions]: ../../docs/accessible-names-and-descriptions.md
[Base UI Button]: https://base-ui.com/react/components/button
[Styling]: ../../docs/styling.md
[WAI-ARIA `aria-busy`]: https://www.w3.org/TR/wai-aria#aria-busy
[`IconButton`]: ../icon-button/README.md

View File

@@ -5,6 +5,7 @@ import type { VariantProps } from 'class-variance-authority'
import { Button as BaseButton } from '@base-ui/react/button'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const buttonVariants = cva(
'inline-flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-disabled:cursor-not-allowed',
@@ -103,7 +104,7 @@ const buttonVariants = cva(
},
)
type ButtonProps = Omit<BaseButtonNS.Props, 'className'> &
type ButtonProps = BaseButtonNS.Props &
VariantProps<typeof buttonVariants> & {
/**
* Marks an action as pending, blocks activation, and keeps the button focusable by default.
@@ -111,7 +112,6 @@ type ButtonProps = Omit<BaseButtonNS.Props, 'className'> &
* reference it with `aria-labelledby` so it remains the explicit accessible name.
*/
loading?: boolean
className?: string
}
function Button({
@@ -129,7 +129,9 @@ function Button({
return (
<BaseButton
type={type}
className={cn(buttonVariants({ variant, size, tone, className }))}
className={(state) =>
cn(buttonVariants({ variant, size, tone, className: resolveClassName(className, state) }))
}
disabled={disabled || loading}
focusableWhenDisabled={focusableWhenDisabled ?? loading}
{...props}

View File

@@ -4,6 +4,7 @@ import type { Checkbox as BaseCheckboxNS } from '@base-ui/react/checkbox'
import type * as React from 'react'
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const checkboxRootClassName = cn(
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-sm shadow-xs shadow-shadow-shadow-3 transition-colors motion-reduce:transition-none',
@@ -25,20 +26,21 @@ const checkboxIndicatorClassName =
const checkboxSkeletonClassName = 'size-4 shrink-0 rounded-sm bg-text-quaternary opacity-20'
type CheckboxRootProps = Omit<BaseCheckboxNS.Root.Props, 'className'> & {
className?: string
}
type CheckboxRootProps = BaseCheckboxNS.Root.Props
function CheckboxRoot({ className, ...props }: CheckboxRootProps) {
return <BaseCheckbox.Root className={cn(checkboxRootClassName, className)} {...props} />
return (
<BaseCheckbox.Root
className={(state) => cn(checkboxRootClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type CheckboxIndicatorProps = Omit<BaseCheckboxNS.Indicator.Props, 'className' | 'children'> & {
className?: string
}
type CheckboxIndicatorProps = Omit<BaseCheckboxNS.Indicator.Props, 'children'>
function CheckboxIndicator({ className, render, ...props }: CheckboxIndicatorProps) {
return (
<BaseCheckbox.Indicator
className={cn(checkboxIndicatorClassName, className)}
className={(state) => cn(checkboxIndicatorClassName, resolveClassName(className, state))}
render={
render ??
((indicatorProps, state) => (

View File

@@ -3,29 +3,33 @@
import type { Collapsible as BaseCollapsibleNS } from '@base-ui/react/collapsible'
import { Collapsible as BaseCollapsible } from '@base-ui/react/collapsible'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
type CollapsibleProps = Omit<BaseCollapsibleNS.Root.Props, 'className'> & {
className?: string
}
type CollapsibleProps = BaseCollapsibleNS.Root.Props
function Collapsible({ className, ...props }: CollapsibleProps) {
return <BaseCollapsible.Root className={cn('flex min-w-0 flex-col', className)} {...props} />
return (
<BaseCollapsible.Root
className={(state) => cn('flex min-w-0 flex-col', resolveClassName(className, state))}
{...props}
/>
)
}
const CollapsibleTrigger = BaseCollapsible.Trigger
type CollapsibleTriggerProps = BaseCollapsibleNS.Trigger.Props
type CollapsiblePanelProps = Omit<BaseCollapsibleNS.Panel.Props, 'className'> & {
className?: string
}
type CollapsiblePanelProps = BaseCollapsibleNS.Panel.Props
function CollapsiblePanel({ className, ...props }: CollapsiblePanelProps) {
return (
<BaseCollapsible.Panel
className={cn(
'h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none',
"[&[hidden]:not([hidden='until-found'])]:hidden",
'data-ending-style:h-0 data-starting-style:h-0',
className,
)}
className={(state) =>
cn(
'h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none',
"[&[hidden]:not([hidden='until-found'])]:hidden",
'data-ending-style:h-0 data-starting-style:h-0',
resolveClassName(className, state),
)
}
{...props}
/>
)

View File

@@ -7,6 +7,7 @@ import { Combobox as BaseCombobox } from '@base-ui/react/combobox'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { formLabelClassName, textControlCompoundInputFocusClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
import {
floatingGroupLabelClassName,
floatingItemIndicatorClassName,
@@ -113,9 +114,8 @@ const comboboxTriggerVariants = cva(
},
)
type ComboboxTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
type ComboboxTriggerProps = BaseCombobox.Trigger.Props &
VariantProps<typeof comboboxTriggerVariants> & {
className?: string
icon?: React.ReactNode | false
}
@@ -130,7 +130,9 @@ function ComboboxTrigger({
return (
<BaseCombobox.Trigger
type={type}
className={cn(comboboxTriggerVariants({ size, className }))}
className={(state) =>
cn(comboboxTriggerVariants({ size, className: resolveClassName(className, state) }))
}
{...props}
>
<span className="min-w-0 grow truncate">{children}</span>
@@ -169,13 +171,15 @@ const comboboxInputGroupVariants = cva(
},
)
type ComboboxInputGroupProps = Omit<BaseCombobox.InputGroup.Props, 'className'> &
VariantProps<typeof comboboxInputGroupVariants> & { className?: string }
type ComboboxInputGroupProps = BaseCombobox.InputGroup.Props &
VariantProps<typeof comboboxInputGroupVariants>
function ComboboxInputGroup({ className, size = 'medium', ...props }: ComboboxInputGroupProps) {
return (
<BaseCombobox.InputGroup
className={cn(comboboxInputGroupVariants({ size }), className)}
className={(state) =>
cn(comboboxInputGroupVariants({ size }), resolveClassName(className, state))
}
{...props}
/>
)
@@ -202,8 +206,8 @@ const comboboxInputVariants = cva(
},
)
type ComboboxInputProps = Omit<BaseCombobox.Input.Props, 'className' | 'size'> &
VariantProps<typeof comboboxInputVariants> & { className?: string }
type ComboboxInputProps = Omit<BaseCombobox.Input.Props, 'size'> &
VariantProps<typeof comboboxInputVariants>
function ComboboxInput({
className,
@@ -214,7 +218,7 @@ function ComboboxInput({
return (
<BaseCombobox.Input
autoComplete={autoComplete}
className={cn(comboboxInputVariants({ size }), className)}
className={(state) => cn(comboboxInputVariants({ size }), resolveClassName(className, state))}
{...props}
/>
)
@@ -244,8 +248,7 @@ const comboboxControlVariants = cva(
},
)
type ComboboxClearProps = Omit<BaseCombobox.Clear.Props, 'className'> &
VariantProps<typeof comboboxControlVariants> & { className?: string }
type ComboboxClearProps = BaseCombobox.Clear.Props & VariantProps<typeof comboboxControlVariants>
function ComboboxClear({
className,
@@ -258,11 +261,13 @@ function ComboboxClear({
<BaseCombobox.Clear
type={type}
aria-label={props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Clear combobox')}
className={cn(
comboboxControlVariants({ size }),
'data-ending-style:opacity-0 data-starting-style:opacity-0',
className,
)}
className={(state) =>
cn(
comboboxControlVariants({ size }),
'data-ending-style:opacity-0 data-starting-style:opacity-0',
resolveClassName(className, state),
)
}
{...props}
>
{children ?? <span className="i-ri-close-line size-4" aria-hidden="true" />}
@@ -270,8 +275,8 @@ function ComboboxClear({
)
}
type ComboboxInputTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
VariantProps<typeof comboboxControlVariants> & { className?: string }
type ComboboxInputTriggerProps = BaseCombobox.Trigger.Props &
VariantProps<typeof comboboxControlVariants>
function ComboboxInputTrigger({
className,
@@ -286,7 +291,9 @@ function ComboboxInputTrigger({
aria-label={
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Open combobox options')
}
className={cn(comboboxControlVariants({ size }), className)}
className={(state) =>
cn(comboboxControlVariants({ size }), resolveClassName(className, state))
}
{...props}
>
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
@@ -294,14 +301,14 @@ function ComboboxInputTrigger({
)
}
type ComboboxIconProps = Omit<BaseCombobox.Icon.Props, 'className'> & {
className?: string
}
type ComboboxIconProps = BaseCombobox.Icon.Props
function ComboboxIcon({ className, children, ...props }: ComboboxIconProps) {
return (
<BaseCombobox.Icon
className={cn('flex shrink-0 items-center text-text-tertiary', className)}
className={(state) =>
cn('flex shrink-0 items-center text-text-tertiary', resolveClassName(className, state))
}
{...props}
>
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
@@ -312,11 +319,7 @@ function ComboboxIcon({ className, children, ...props }: ComboboxIconProps) {
const ComboboxPortal = BaseCombobox.Portal
type ComboboxPortalProps = BaseCombobox.Portal.Props
type ComboboxPositionerProps = Omit<
BaseCombobox.Positioner.Props,
'className' | 'side' | 'align'
> & {
className?: string
type ComboboxPositionerProps = Omit<BaseCombobox.Positioner.Props, 'side' | 'align'> & {
placement?: Placement
}
@@ -333,41 +336,53 @@ function ComboboxPositioner({
side={side}
align={align}
sideOffset={sideOffset}
className={cn('z-50 outline-hidden', className)}
className={(state) => cn('z-50 outline-hidden', resolveClassName(className, state))}
{...props}
/>
)
}
type ComboboxPopupProps = Omit<BaseCombobox.Popup.Props, 'className'> & {
className?: string
}
type ComboboxPopupProps = BaseCombobox.Popup.Props
function ComboboxPopup({ className, ...props }: ComboboxPopupProps) {
return (
<BaseCombobox.Popup
className={cn(comboboxPopupClassName, floatingPopupAnimationClassName, className)}
className={(state) =>
cn(
comboboxPopupClassName,
floatingPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type ComboboxListProps<Item = unknown> = Omit<BaseCombobox.List.Props, 'children' | 'className'> & {
className?: string
type ComboboxListProps<Item = unknown> = Omit<BaseCombobox.List.Props, 'children'> & {
children?: React.ReactNode | ((item: Item, index: number) => React.ReactNode)
}
function ComboboxList<Item = unknown>({ className, ...props }: ComboboxListProps<Item>) {
return <BaseCombobox.List className={cn(comboboxListClassName, className)} {...props} />
return (
<BaseCombobox.List
className={(state) => cn(comboboxListClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ComboboxItemProps<Value = unknown> = Omit<BaseCombobox.Item.Props, 'className' | 'value'> & {
className?: string
type ComboboxItemProps<Value = unknown> = Omit<BaseCombobox.Item.Props, 'value'> & {
value?: Value
}
function ComboboxItem<Value = unknown>({ className, ...props }: ComboboxItemProps<Value>) {
return <BaseCombobox.Item className={cn(comboboxItemClassName, className)} {...props} />
return (
<BaseCombobox.Item
className={(state) => cn(comboboxItemClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ComboboxItemTextProps = React.ComponentProps<'span'>
@@ -381,7 +396,7 @@ function ComboboxItemText({ className, ...props }: ComboboxItemTextProps) {
function ComboboxItemIndicator({ className, children, ...props }: ComboboxItemIndicatorProps) {
return (
<BaseCombobox.ItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
className={(state) => cn(floatingItemIndicatorClassName, resolveClassName(className, state))}
{...props}
>
{children ?? <span className="i-ri-check-line h-4 w-4" aria-hidden="true" />}
@@ -389,93 +404,102 @@ function ComboboxItemIndicator({ className, children, ...props }: ComboboxItemIn
)
}
type ComboboxItemIndicatorProps = Omit<
BaseCombobox.ItemIndicator.Props,
'children' | 'className'
> & {
type ComboboxItemIndicatorProps = Omit<BaseCombobox.ItemIndicator.Props, 'children'> & {
children?: React.ReactNode
className?: string
}
type ComboboxLabelProps = Omit<BaseCombobox.Label.Props, 'className'> & {
className?: string
}
type ComboboxLabelProps = BaseCombobox.Label.Props
function ComboboxLabel({ className, ...props }: ComboboxLabelProps) {
return <BaseCombobox.Label className={cn(formLabelClassName, className)} {...props} />
}
type ComboboxGroupLabelProps = Omit<BaseCombobox.GroupLabel.Props, 'className'> & {
className?: string
}
function ComboboxGroupLabel({ className, ...props }: ComboboxGroupLabelProps) {
return (
<BaseCombobox.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
<BaseCombobox.Label
className={(state) => cn(formLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ComboboxSeparatorProps = Omit<BaseCombobox.Separator.Props, 'className'> & {
className?: string
type ComboboxGroupLabelProps = BaseCombobox.GroupLabel.Props
function ComboboxGroupLabel({ className, ...props }: ComboboxGroupLabelProps) {
return (
<BaseCombobox.GroupLabel
className={(state) => cn(floatingGroupLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ComboboxSeparatorProps = BaseCombobox.Separator.Props
function ComboboxSeparator({ className, ...props }: ComboboxSeparatorProps) {
return <BaseCombobox.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
return (
<BaseCombobox.Separator
className={(state) => cn(floatingSeparatorClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ComboboxEmptyProps = Omit<BaseCombobox.Empty.Props, 'className'> & {
className?: string
}
type ComboboxEmptyProps = BaseCombobox.Empty.Props
function ComboboxEmpty({ className, ...props }: ComboboxEmptyProps) {
return (
<BaseCombobox.Empty
className={cn(
'px-3 py-2 system-sm-regular text-text-tertiary empty:h-0 empty:p-0',
className,
)}
className={(state) =>
cn(
'px-3 py-2 system-sm-regular text-text-tertiary empty:h-0 empty:p-0',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type ComboboxStatusProps = Omit<BaseCombobox.Status.Props, 'className'> & {
className?: string
}
type ComboboxStatusProps = BaseCombobox.Status.Props
function ComboboxStatus({ className, ...props }: ComboboxStatusProps) {
return (
<BaseCombobox.Status
className={cn('px-3 py-2 system-sm-regular text-text-tertiary empty:p-0', className)}
className={(state) =>
cn(
'px-3 py-2 system-sm-regular text-text-tertiary empty:p-0',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type ComboboxChipsProps = Omit<BaseCombobox.Chips.Props, 'className'> & {
className?: string
}
type ComboboxChipsProps = BaseCombobox.Chips.Props
function ComboboxChips({ className, ...props }: ComboboxChipsProps) {
return (
<BaseCombobox.Chips
className={cn('flex w-full min-w-0 flex-wrap items-center gap-1 px-1', className)}
className={(state) =>
cn(
'flex w-full min-w-0 flex-wrap items-center gap-1 px-1',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type ComboboxChipProps = Omit<BaseCombobox.Chip.Props, 'className'> & {
className?: string
}
type ComboboxChipProps = BaseCombobox.Chip.Props
function ComboboxChip({ className, ...props }: ComboboxChipProps) {
return (
<BaseCombobox.Chip
className={cn(
'inline-flex max-w-full min-w-0 items-center gap-1 rounded-md bg-state-base-hover px-1.5 py-0.5 system-xs-medium text-text-secondary',
className,
)}
className={(state) =>
cn(
'inline-flex max-w-full min-w-0 items-center gap-1 rounded-md bg-state-base-hover px-1.5 py-0.5 system-xs-medium text-text-secondary',
resolveClassName(className, state),
)
}
{...props}
/>
)
@@ -493,10 +517,12 @@ function ComboboxChipRemove({
aria-label={
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Remove selected item')
}
className={cn(
'flex size-3.5 shrink-0 items-center justify-center rounded-sm text-text-tertiary outline-hidden hover:bg-state-base-hover-alt hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
className,
)}
className={(state) =>
cn(
'flex size-3.5 shrink-0 items-center justify-center rounded-sm text-text-tertiary outline-hidden hover:bg-state-base-hover-alt hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
resolveClassName(className, state),
)
}
{...props}
>
{children ?? <span className="i-ri-close-line size-3" aria-hidden="true" />}
@@ -504,9 +530,7 @@ function ComboboxChipRemove({
)
}
type ComboboxChipRemoveProps = Omit<BaseCombobox.ChipRemove.Props, 'className'> & {
className?: string
}
type ComboboxChipRemoveProps = BaseCombobox.ChipRemove.Props
export {
Combobox,

View File

@@ -5,6 +5,7 @@ import type { MenuItemVariant } from '../overlay-shared'
import type { Placement } from '../placement'
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import {
floatingGroupLabelClassName,
floatingItemIndicatorClassName,
@@ -45,11 +46,10 @@ function ContextMenuRadioGroup<Value = unknown>(
return <BaseContextMenu.RadioGroup {...props} />
}
type ContextMenuContentProps = Omit<BaseContextMenu.Popup.Props, 'children' | 'className'> &
type ContextMenuContentProps = Omit<BaseContextMenu.Popup.Props, 'children'> &
Pick<BaseContextMenu.Positioner.Props, 'sideOffset' | 'alignOffset'> & {
children: React.ReactNode
placement?: Placement
className?: string
}
function ContextMenuContent({
@@ -72,7 +72,13 @@ function ContextMenuContent({
className="z-50 outline-hidden"
>
<BaseContextMenu.Popup
className={cn(menuPopupClassName, floatingPopupAnimationClassName, className)}
className={(state) =>
cn(
menuPopupClassName,
floatingPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
>
{children}
@@ -82,24 +88,24 @@ function ContextMenuContent({
)
}
type ContextMenuItemProps = Omit<BaseContextMenu.Item.Props, 'className'> & {
type ContextMenuItemProps = BaseContextMenu.Item.Props & {
variant?: ContextMenuItemVariant
className?: string
}
function ContextMenuItem({ className, variant = 'default', ...props }: ContextMenuItemProps) {
return (
<BaseContextMenu.Item
data-variant={variant}
className={cn(menuItemClassName, menuItemDestructiveClassName, className)}
className={(state) =>
cn(menuItemClassName, menuItemDestructiveClassName, resolveClassName(className, state))
}
{...props}
/>
)
}
type ContextMenuLinkItemProps = Omit<BaseContextMenu.LinkItem.Props, 'className'> & {
type ContextMenuLinkItemProps = BaseContextMenu.LinkItem.Props & {
variant?: ContextMenuItemVariant
className?: string
}
function ContextMenuLinkItem({
@@ -111,18 +117,16 @@ function ContextMenuLinkItem({
return (
<BaseContextMenu.LinkItem
data-variant={variant}
className={cn(menuItemClassName, menuItemDestructiveClassName, className)}
className={(state) =>
cn(menuItemClassName, menuItemDestructiveClassName, resolveClassName(className, state))
}
closeOnClick={closeOnClick}
{...props}
/>
)
}
type ContextMenuRadioItemProps<Value = unknown> = Omit<
BaseContextMenu.RadioItem.Props,
'className' | 'value'
> & {
className?: string
type ContextMenuRadioItemProps<Value = unknown> = Omit<BaseContextMenu.RadioItem.Props, 'value'> & {
value: Value
}
@@ -130,16 +134,24 @@ function ContextMenuRadioItem<Value = unknown>({
className,
...props
}: ContextMenuRadioItemProps<Value>) {
return <BaseContextMenu.RadioItem className={cn(menuItemClassName, className)} {...props} />
return (
<BaseContextMenu.RadioItem
className={(state) => cn(menuItemClassName, resolveClassName(className, state))}
{...props}
/>
)
}
function ContextMenuCheckboxItem({ className, ...props }: ContextMenuCheckboxItemProps) {
return <BaseContextMenu.CheckboxItem className={cn(menuItemClassName, className)} {...props} />
return (
<BaseContextMenu.CheckboxItem
className={(state) => cn(menuItemClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ContextMenuCheckboxItemProps = Omit<BaseContextMenu.CheckboxItem.Props, 'className'> & {
className?: string
}
type ContextMenuCheckboxItemProps = BaseContextMenu.CheckboxItem.Props
function ContextMenuCheckboxItemIndicator({
className,
@@ -147,7 +159,7 @@ function ContextMenuCheckboxItemIndicator({
}: ContextMenuCheckboxItemIndicatorProps) {
return (
<BaseContextMenu.CheckboxItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
className={(state) => cn(floatingItemIndicatorClassName, resolveClassName(className, state))}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
@@ -157,8 +169,8 @@ function ContextMenuCheckboxItemIndicator({
type ContextMenuCheckboxItemIndicatorProps = Omit<
BaseContextMenu.CheckboxItemIndicator.Props,
'children' | 'className'
> & { className?: string }
'children'
>
function ContextMenuRadioItemIndicator({
className,
@@ -166,7 +178,7 @@ function ContextMenuRadioItemIndicator({
}: ContextMenuRadioItemIndicatorProps) {
return (
<BaseContextMenu.RadioItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
className={(state) => cn(floatingItemIndicatorClassName, resolveClassName(className, state))}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
@@ -174,14 +186,10 @@ function ContextMenuRadioItemIndicator({
)
}
type ContextMenuRadioItemIndicatorProps = Omit<
BaseContextMenu.RadioItemIndicator.Props,
'children' | 'className'
> & { className?: string }
type ContextMenuRadioItemIndicatorProps = Omit<BaseContextMenu.RadioItemIndicator.Props, 'children'>
type ContextMenuSubTriggerProps = Omit<BaseContextMenu.SubmenuTrigger.Props, 'className'> & {
type ContextMenuSubTriggerProps = BaseContextMenu.SubmenuTrigger.Props & {
variant?: ContextMenuItemVariant
className?: string
}
function ContextMenuSubTrigger({
@@ -193,7 +201,9 @@ function ContextMenuSubTrigger({
return (
<BaseContextMenu.SubmenuTrigger
data-variant={variant}
className={cn(menuItemClassName, menuItemDestructiveClassName, className)}
className={(state) =>
cn(menuItemClassName, menuItemDestructiveClassName, resolveClassName(className, state))
}
{...props}
>
{children}
@@ -237,23 +247,25 @@ function ContextMenuSubContent({
)
}
type ContextMenuLabelProps = Omit<BaseContextMenu.GroupLabel.Props, 'className'> & {
className?: string
}
type ContextMenuLabelProps = BaseContextMenu.GroupLabel.Props
function ContextMenuLabel({ className, ...props }: ContextMenuLabelProps) {
return (
<BaseContextMenu.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
<BaseContextMenu.GroupLabel
className={(state) => cn(floatingGroupLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ContextMenuSeparatorProps = Omit<BaseContextMenu.Separator.Props, 'className'> & {
className?: string
}
type ContextMenuSeparatorProps = BaseContextMenu.Separator.Props
function ContextMenuSeparator({ className, ...props }: ContextMenuSeparatorProps) {
return (
<BaseContextMenu.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
<BaseContextMenu.Separator
className={(state) => cn(floatingSeparatorClassName, resolveClassName(className, state))}
{...props}
/>
)
}

View File

@@ -3,6 +3,7 @@
import type * as React from 'react'
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { modalBackdropClassName, modalPopupAnimationClassName } from '../overlay-shared'
const Dialog = BaseDialog.Root
@@ -21,42 +22,47 @@ type DialogDescriptionProps = BaseDialog.Description.Props
type DialogPortalProps = BaseDialog.Portal.Props
type DialogCloseProps = BaseDialog.Close.Props
type DialogBackdropProps = Omit<BaseDialog.Backdrop.Props, 'className'> & {
className?: string
}
type DialogBackdropProps = BaseDialog.Backdrop.Props
function DialogBackdrop({ className, ...props }: DialogBackdropProps) {
return <BaseDialog.Backdrop {...props} className={cn(modalBackdropClassName, className)} />
return (
<BaseDialog.Backdrop
{...props}
className={(state) => cn(modalBackdropClassName, resolveClassName(className, state))}
/>
)
}
type DialogViewportProps = Omit<BaseDialog.Viewport.Props, 'className'> & {
className?: string
}
type DialogViewportProps = BaseDialog.Viewport.Props
function DialogViewport({ className, ...props }: DialogViewportProps) {
return <BaseDialog.Viewport className={cn('fixed inset-0 z-50', className)} {...props} />
}
type DialogPopupProps = Omit<BaseDialog.Popup.Props, 'className'> & {
className?: string
}
function DialogPopup({ className, ...props }: DialogPopupProps) {
return (
<BaseDialog.Popup
className={cn(
'z-50 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl',
modalPopupAnimationClassName,
className,
)}
<BaseDialog.Viewport
className={(state) => cn('fixed inset-0 z-50', resolveClassName(className, state))}
{...props}
/>
)
}
type DialogContentProps = Omit<DialogPopupProps, 'children' | 'className'> & {
type DialogPopupProps = BaseDialog.Popup.Props
function DialogPopup({ className, ...props }: DialogPopupProps) {
return (
<BaseDialog.Popup
className={(state) =>
cn(
'z-50 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl',
modalPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type DialogContentProps = Omit<DialogPopupProps, 'children'> & {
children: React.ReactNode
className?: string
backdropProps?: DialogBackdropProps
}
@@ -65,10 +71,12 @@ function DialogContent({ children, className, backdropProps, ...props }: DialogC
<DialogPortal>
<DialogBackdrop {...backdropProps} />
<DialogPopup
className={cn(
'fixed top-1/2 left-1/2 max-h-[80dvh] w-120 max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain p-6',
className,
)}
className={(state) =>
cn(
'fixed top-1/2 left-1/2 max-h-[80dvh] w-120 max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain p-6',
resolveClassName(className, state),
)
}
{...props}
>
{children}

View File

@@ -4,6 +4,7 @@ import type * as React from 'react'
import { Drawer as BaseDrawer } from '@base-ui/react/drawer'
import { cn } from '../cn'
import { iconButtonVariants } from '../icon-button/variants'
import { resolveClassName } from '../internals/resolve-class-name'
const Drawer = BaseDrawer.Root
const DrawerProvider = BaseDrawer.Provider
@@ -29,83 +30,82 @@ type DrawerTitleProps = BaseDrawer.Title.Props
type DrawerDescriptionProps = BaseDrawer.Description.Props
type DrawerCloseProps = BaseDrawer.Close.Props
type DrawerBackdropProps = Omit<BaseDrawer.Backdrop.Props, 'className'> & {
className?: string
}
type DrawerBackdropProps = BaseDrawer.Backdrop.Props
function DrawerBackdrop({ className, ...props }: DrawerBackdropProps) {
return (
<BaseDrawer.Backdrop
className={cn(
'absolute inset-0 z-50 bg-background-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))]',
'transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none',
className,
)}
className={(state) =>
cn(
'absolute inset-0 z-50 bg-background-overlay opacity-[calc(1-var(--drawer-swipe-progress,0))]',
'transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type DrawerViewportProps = Omit<BaseDrawer.Viewport.Props, 'className'> & {
className?: string
}
type DrawerViewportProps = BaseDrawer.Viewport.Props
function DrawerViewport({ className, ...props }: DrawerViewportProps) {
return (
<BaseDrawer.Viewport
className={cn(
'fixed inset-0 z-50 touch-none overflow-hidden overscroll-contain outline-hidden',
className,
)}
className={(state) =>
cn(
'fixed inset-0 z-50 touch-none overflow-hidden overscroll-contain outline-hidden',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type DrawerPopupProps = Omit<BaseDrawer.Popup.Props, 'className'> & {
className?: string
}
type DrawerPopupProps = BaseDrawer.Popup.Props
function DrawerPopup({ className, ...props }: DrawerPopupProps) {
return (
<BaseDrawer.Popup
className={cn(
'fixed z-50 flex min-h-0 touch-none flex-col overflow-hidden border-[0.5px] border-components-panel-border bg-components-panel-bg text-text-primary shadow-xl outline-hidden',
'transition-[transform,opacity,box-shadow] duration-200 data-swiping:duration-0 data-swiping:select-none motion-reduce:transition-none',
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:h-dvh data-[swipe-direction=right]:w-120 data-[swipe-direction=right]:max-w-[calc(100vw-2rem)] data-[swipe-direction=right]:transform-[translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=right]:rounded-l-2xl data-[swipe-direction=right]:border-r-0',
'data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))] data-starting-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))]',
'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:h-dvh data-[swipe-direction=left]:w-120 data-[swipe-direction=left]:max-w-[calc(100vw-2rem)] data-[swipe-direction=left]:transform-[translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=left]:rounded-r-2xl data-[swipe-direction=left]:border-l-0',
'data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(-100%-2px))] data-starting-style:data-[swipe-direction=left]:transform-[translateX(calc(-100%-2px))]',
'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:max-h-[calc(100dvh-2rem)] data-[swipe-direction=down]:w-full data-[swipe-direction=down]:transform-[translateY(calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y,0px)))] data-[swipe-direction=down]:rounded-t-2xl data-[swipe-direction=down]:border-b-0',
'data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(100%+2px))] data-starting-style:data-[swipe-direction=down]:transform-[translateY(calc(100%+2px))]',
'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:max-h-[calc(100dvh-2rem)] data-[swipe-direction=up]:w-full data-[swipe-direction=up]:transform-[translateY(var(--drawer-swipe-movement-y,0px))] data-[swipe-direction=up]:rounded-b-2xl data-[swipe-direction=up]:border-t-0',
'data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(-100%-2px))] data-starting-style:data-[swipe-direction=up]:transform-[translateY(calc(-100%-2px))]',
className,
)}
className={(state) =>
cn(
'fixed z-50 flex min-h-0 touch-none flex-col overflow-hidden border-[0.5px] border-components-panel-border bg-components-panel-bg text-text-primary shadow-xl outline-hidden',
'transition-[transform,opacity,box-shadow] duration-200 data-swiping:duration-0 data-swiping:select-none motion-reduce:transition-none',
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:h-dvh data-[swipe-direction=right]:w-120 data-[swipe-direction=right]:max-w-[calc(100vw-2rem)] data-[swipe-direction=right]:transform-[translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=right]:rounded-l-2xl data-[swipe-direction=right]:border-r-0',
'data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))] data-starting-style:data-[swipe-direction=right]:transform-[translateX(calc(100%+2px))]',
'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:h-dvh data-[swipe-direction=left]:w-120 data-[swipe-direction=left]:max-w-[calc(100vw-2rem)] data-[swipe-direction=left]:transform-[translateX(var(--drawer-swipe-movement-x,0px))] data-[swipe-direction=left]:rounded-r-2xl data-[swipe-direction=left]:border-l-0',
'data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(-100%-2px))] data-starting-style:data-[swipe-direction=left]:transform-[translateX(calc(-100%-2px))]',
'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:max-h-[calc(100dvh-2rem)] data-[swipe-direction=down]:w-full data-[swipe-direction=down]:transform-[translateY(calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y,0px)))] data-[swipe-direction=down]:rounded-t-2xl data-[swipe-direction=down]:border-b-0',
'data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(100%+2px))] data-starting-style:data-[swipe-direction=down]:transform-[translateY(calc(100%+2px))]',
'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:max-h-[calc(100dvh-2rem)] data-[swipe-direction=up]:w-full data-[swipe-direction=up]:transform-[translateY(var(--drawer-swipe-movement-y,0px))] data-[swipe-direction=up]:rounded-b-2xl data-[swipe-direction=up]:border-t-0',
'data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(-100%-2px))] data-starting-style:data-[swipe-direction=up]:transform-[translateY(calc(-100%-2px))]',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type DrawerContentProps = Omit<BaseDrawer.Content.Props, 'className'> & {
className?: string
}
type DrawerContentProps = BaseDrawer.Content.Props
function DrawerContent({ className, ...props }: DrawerContentProps) {
return (
<BaseDrawer.Content
className={cn(
'min-h-0 flex-1 overflow-y-auto overscroll-contain p-6 pb-[calc(1.5rem+env(safe-area-inset-bottom,0))]',
className,
)}
className={(state) =>
cn(
'min-h-0 flex-1 overflow-y-auto overscroll-contain p-6 pb-[calc(1.5rem+env(safe-area-inset-bottom,0))]',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type DrawerCloseButtonProps = Omit<BaseDrawer.Close.Props, 'children' | 'className'> & {
type DrawerCloseButtonProps = Omit<BaseDrawer.Close.Props, 'children'> & {
children?: React.ReactNode
className?: string
}
function DrawerCloseButton({
@@ -119,11 +119,13 @@ function DrawerCloseButton({
<BaseDrawer.Close
type={type}
aria-label={ariaLabel}
className={cn(
iconButtonVariants({ size: 'lg' }),
'focus-visible:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50 data-disabled:text-text-tertiary',
className,
)}
className={(state) =>
cn(
iconButtonVariants({ size: 'lg' }),
'focus-visible:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50 data-disabled:text-text-tertiary',
resolveClassName(className, state),
)
}
{...props}
>
{children ?? <span aria-hidden="true" className="i-ri-close-line size-4" />}

View File

@@ -5,6 +5,7 @@ import type { MenuItemVariant } from '../overlay-shared'
import type { Placement } from '../placement'
import { Menu } from '@base-ui/react/menu'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import {
floatingGroupLabelClassName,
floatingItemIndicatorClassName,
@@ -44,11 +45,7 @@ function DropdownMenuRadioGroup<Value = unknown>(
return <Menu.RadioGroup {...props} />
}
type DropdownMenuRadioItemProps<Value = unknown> = Omit<
Menu.RadioItem.Props,
'className' | 'value'
> & {
className?: string
type DropdownMenuRadioItemProps<Value = unknown> = Omit<Menu.RadioItem.Props, 'value'> & {
value: Value
}
@@ -56,7 +53,12 @@ function DropdownMenuRadioItem<Value = unknown>({
className,
...props
}: DropdownMenuRadioItemProps<Value>) {
return <Menu.RadioItem className={cn(menuItemClassName, className)} {...props} />
return (
<Menu.RadioItem
className={(state) => cn(menuItemClassName, resolveClassName(className, state))}
{...props}
/>
)
}
function DropdownMenuRadioItemIndicator({
@@ -64,23 +66,26 @@ function DropdownMenuRadioItemIndicator({
...props
}: DropdownMenuRadioItemIndicatorProps) {
return (
<Menu.RadioItemIndicator className={cn(floatingItemIndicatorClassName, className)} {...props}>
<Menu.RadioItemIndicator
className={(state) => cn(floatingItemIndicatorClassName, resolveClassName(className, state))}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
</Menu.RadioItemIndicator>
)
}
type DropdownMenuRadioItemIndicatorProps = Omit<
Menu.RadioItemIndicator.Props,
'children' | 'className'
> & { className?: string }
type DropdownMenuRadioItemIndicatorProps = Omit<Menu.RadioItemIndicator.Props, 'children'>
type DropdownMenuCheckboxItemProps = Omit<Menu.CheckboxItem.Props, 'className'> & {
className?: string
}
type DropdownMenuCheckboxItemProps = Menu.CheckboxItem.Props
function DropdownMenuCheckboxItem({ className, ...props }: DropdownMenuCheckboxItemProps) {
return <Menu.CheckboxItem className={cn(menuItemClassName, className)} {...props} />
return (
<Menu.CheckboxItem
className={(state) => cn(menuItemClassName, resolveClassName(className, state))}
{...props}
/>
)
}
function DropdownMenuCheckboxItemIndicator({
@@ -89,7 +94,7 @@ function DropdownMenuCheckboxItemIndicator({
}: DropdownMenuCheckboxItemIndicatorProps) {
return (
<Menu.CheckboxItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
className={(state) => cn(floatingItemIndicatorClassName, resolveClassName(className, state))}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
@@ -97,21 +102,20 @@ function DropdownMenuCheckboxItemIndicator({
)
}
type DropdownMenuCheckboxItemIndicatorProps = Omit<
Menu.CheckboxItemIndicator.Props,
'children' | 'className'
> & { className?: string }
type DropdownMenuCheckboxItemIndicatorProps = Omit<Menu.CheckboxItemIndicator.Props, 'children'>
type DropdownMenuLabelProps = Omit<Menu.GroupLabel.Props, 'className'> & {
className?: string
}
type DropdownMenuLabelProps = Menu.GroupLabel.Props
function DropdownMenuLabel({ className, ...props }: DropdownMenuLabelProps) {
return <Menu.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
return (
<Menu.GroupLabel
className={(state) => cn(floatingGroupLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type DropdownMenuPositionerProps = Omit<Menu.Positioner.Props, 'className' | 'side' | 'align'> & {
className?: string
type DropdownMenuPositionerProps = Omit<Menu.Positioner.Props, 'side' | 'align'> & {
placement?: Placement
}
@@ -130,29 +134,32 @@ function DropdownMenuPositioner({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-hidden', className)}
className={(state) => cn('z-50 outline-hidden', resolveClassName(className, state))}
{...props}
/>
)
}
type DropdownMenuPopupProps = Omit<Menu.Popup.Props, 'className'> & {
className?: string
}
type DropdownMenuPopupProps = Menu.Popup.Props
function DropdownMenuPopup({ className, ...props }: DropdownMenuPopupProps) {
return (
<Menu.Popup
className={cn(menuPopupBaseClassName, floatingPopupAnimationClassName, className)}
className={(state) =>
cn(
menuPopupBaseClassName,
floatingPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type DropdownMenuContentProps = Omit<DropdownMenuPopupProps, 'children' | 'className'> &
type DropdownMenuContentProps = Omit<DropdownMenuPopupProps, 'children'> &
Pick<DropdownMenuPositionerProps, 'alignOffset' | 'placement' | 'sideOffset'> & {
children: React.ReactNode
className?: string
}
function DropdownMenuContent({
@@ -170,7 +177,10 @@ function DropdownMenuContent({
sideOffset={sideOffset}
alignOffset={alignOffset}
>
<DropdownMenuPopup className={cn(menuPopupSurfaceClassName, className)} {...props}>
<DropdownMenuPopup
className={(state) => cn(menuPopupSurfaceClassName, resolveClassName(className, state))}
{...props}
>
{children}
</DropdownMenuPopup>
</DropdownMenuPositioner>
@@ -178,9 +188,8 @@ function DropdownMenuContent({
)
}
type DropdownMenuSubTriggerProps = Omit<Menu.SubmenuTrigger.Props, 'className'> & {
type DropdownMenuSubTriggerProps = Menu.SubmenuTrigger.Props & {
variant?: DropdownMenuItemVariant
className?: string
}
function DropdownMenuSubTrigger({
@@ -192,7 +201,9 @@ function DropdownMenuSubTrigger({
return (
<Menu.SubmenuTrigger
data-variant={variant}
className={cn(menuItemClassName, menuItemDestructiveClassName, className)}
className={(state) =>
cn(menuItemClassName, menuItemDestructiveClassName, resolveClassName(className, state))
}
{...props}
>
{children}
@@ -229,24 +240,24 @@ function DropdownMenuSubContent({
)
}
type DropdownMenuItemProps = Omit<Menu.Item.Props, 'className'> & {
type DropdownMenuItemProps = Menu.Item.Props & {
variant?: DropdownMenuItemVariant
className?: string
}
function DropdownMenuItem({ className, variant = 'default', ...props }: DropdownMenuItemProps) {
return (
<Menu.Item
data-variant={variant}
className={cn(menuItemClassName, menuItemDestructiveClassName, className)}
className={(state) =>
cn(menuItemClassName, menuItemDestructiveClassName, resolveClassName(className, state))
}
{...props}
/>
)
}
type DropdownMenuLinkItemProps = Omit<Menu.LinkItem.Props, 'className'> & {
type DropdownMenuLinkItemProps = Menu.LinkItem.Props & {
variant?: DropdownMenuItemVariant
className?: string
}
function DropdownMenuLinkItem({
@@ -258,19 +269,24 @@ function DropdownMenuLinkItem({
return (
<Menu.LinkItem
data-variant={variant}
className={cn(menuItemClassName, menuItemDestructiveClassName, className)}
className={(state) =>
cn(menuItemClassName, menuItemDestructiveClassName, resolveClassName(className, state))
}
closeOnClick={closeOnClick}
{...props}
/>
)
}
type DropdownMenuSeparatorProps = Omit<Menu.Separator.Props, 'className'> & {
className?: string
}
type DropdownMenuSeparatorProps = Menu.Separator.Props
function DropdownMenuSeparator({ className, ...props }: DropdownMenuSeparatorProps) {
return <Menu.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
return (
<Menu.Separator
className={(state) => cn(floatingSeparatorClassName, resolveClassName(className, state))}
{...props}
/>
)
}
export {

View File

@@ -4,52 +4,64 @@ import type { Field as BaseFieldNS } from '@base-ui/react/field'
import { Field as BaseField } from '@base-ui/react/field'
import { cn } from '../cn'
import { formLabelClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
type FieldProps = Omit<BaseFieldNS.Root.Props, 'className'> & {
className?: string
}
type FieldProps = BaseFieldNS.Root.Props
function Field({ className, ...props }: FieldProps) {
return <BaseField.Root className={cn('group/field grid min-w-0 gap-1', className)} {...props} />
}
type FieldItemProps = Omit<BaseFieldNS.Item.Props, 'className'> & {
className?: string
}
function FieldItem({ className, ...props }: FieldItemProps) {
return <BaseField.Item className={cn('grid min-w-0 gap-1', className)} {...props} />
}
type FieldLabelProps = Omit<BaseFieldNS.Label.Props, 'className'> & {
className?: string
}
function FieldLabel({ className, ...props }: FieldLabelProps) {
return <BaseField.Label className={cn(formLabelClassName, className)} {...props} />
}
type FieldDescriptionProps = Omit<BaseFieldNS.Description.Props, 'className'> & {
className?: string
}
function FieldDescription({ className, ...props }: FieldDescriptionProps) {
return (
<BaseField.Description
className={cn('py-0.5 body-xs-regular text-text-tertiary', className)}
<BaseField.Root
className={(state) =>
cn('group/field grid min-w-0 gap-1', resolveClassName(className, state))
}
{...props}
/>
)
}
type FieldErrorProps = Omit<BaseFieldNS.Error.Props, 'className'> & {
className?: string
type FieldItemProps = BaseFieldNS.Item.Props
function FieldItem({ className, ...props }: FieldItemProps) {
return (
<BaseField.Item
className={(state) => cn('grid min-w-0 gap-1', resolveClassName(className, state))}
{...props}
/>
)
}
type FieldLabelProps = BaseFieldNS.Label.Props
function FieldLabel({ className, ...props }: FieldLabelProps) {
return (
<BaseField.Label
className={(state) => cn(formLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type FieldDescriptionProps = BaseFieldNS.Description.Props
function FieldDescription({ className, ...props }: FieldDescriptionProps) {
return (
<BaseField.Description
className={(state) =>
cn('py-0.5 body-xs-regular text-text-tertiary', resolveClassName(className, state))
}
{...props}
/>
)
}
type FieldErrorProps = BaseFieldNS.Error.Props
function FieldError({ className, ...props }: FieldErrorProps) {
return (
<BaseField.Error
className={cn('py-0.5 body-xs-regular text-text-destructive', className)}
className={(state) =>
cn('py-0.5 body-xs-regular text-text-destructive', resolveClassName(className, state))
}
{...props}
/>
)

View File

@@ -3,23 +3,27 @@
import type { Fieldset as BaseFieldsetNS } from '@base-ui/react/fieldset'
import { Fieldset as BaseFieldset } from '@base-ui/react/fieldset'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
type FieldsetProps = Omit<BaseFieldsetNS.Root.Props, 'className'> & {
className?: string
}
type FieldsetProps = BaseFieldsetNS.Root.Props
function Fieldset({ className, ...props }: FieldsetProps) {
return <BaseFieldset.Root className={cn('m-0 min-w-0 border-0 p-0', className)} {...props} />
return (
<BaseFieldset.Root
className={(state) => cn('m-0 min-w-0 border-0 p-0', resolveClassName(className, state))}
{...props}
/>
)
}
type FieldsetLegendProps = Omit<BaseFieldsetNS.Legend.Props, 'className'> & {
className?: string
}
type FieldsetLegendProps = BaseFieldsetNS.Legend.Props
function FieldsetLegend({ className, ...props }: FieldsetLegendProps) {
return (
<BaseFieldset.Legend
className={cn('mb-1 py-1 system-sm-medium text-text-secondary', className)}
className={(state) =>
cn('mb-1 py-1 system-sm-medium text-text-secondary', resolveClassName(className, state))
}
{...props}
/>
)

View File

@@ -5,6 +5,7 @@ import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import * as React from 'react'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const FileTreeLevelContext = React.createContext(1)
@@ -66,17 +67,21 @@ function FileTreeList({ render, className, ...props }: FileTreeListProps) {
})
}
type FileTreeFolderProps = Omit<BaseCollapsible.Root.Props, 'className' | 'render'> & {
className?: string
type FileTreeFolderProps = Omit<BaseCollapsible.Root.Props, 'render'> & {
render?: BaseCollapsible.Root.Props['render']
}
function FileTreeFolder({ render = <li />, className, ...props }: FileTreeFolderProps) {
return <BaseCollapsible.Root render={render} className={cn('min-w-0', className)} {...props} />
return (
<BaseCollapsible.Root
render={render}
className={(state) => cn('min-w-0', resolveClassName(className, state))}
{...props}
/>
)
}
type FileTreeFolderTriggerProps = Omit<BaseCollapsible.Trigger.Props, 'className'> & {
className?: string
type FileTreeFolderTriggerProps = BaseCollapsible.Trigger.Props & {
level?: number
}
@@ -92,7 +97,7 @@ function FileTreeFolderTrigger({
return (
<BaseCollapsible.Trigger
className={fileTreeRowClassName({ className })}
className={(state) => fileTreeRowClassName({ className: resolveClassName(className, state) })}
disabled={disabled}
data-disabled={disabled || undefined}
{...props}
@@ -103,8 +108,7 @@ function FileTreeFolderTrigger({
)
}
type FileTreeFolderPanelProps = Omit<BaseCollapsible.Panel.Props, 'className' | 'render'> & {
className?: string
type FileTreeFolderPanelProps = Omit<BaseCollapsible.Panel.Props, 'render'> & {
render?: BaseCollapsible.Panel.Props['render']
}
@@ -119,7 +123,9 @@ function FileTreeFolderPanel({
return (
<BaseCollapsible.Panel
render={render}
className={cn('m-0 flex min-w-0 list-none flex-col gap-px p-0', className)}
className={(state) =>
cn('m-0 flex min-w-0 list-none flex-col gap-px p-0', resolveClassName(className, state))
}
{...props}
>
<FileTreeLevelContext.Provider value={level + 1}>{children}</FileTreeLevelContext.Provider>

View File

@@ -53,9 +53,11 @@ the icon button.
- Read [`Button`] for visible-label actions, submit semantics, and loading state.
- Read [Accessible names and descriptions] for the cross-component naming and description contract.
- Read [Styling] for state styles, callbacks, and their composition boundary.
- Read [Base UI Button] for the upstream interaction and composition contract.
[Accessible names and descriptions]: ../../docs/accessible-names-and-descriptions.md
[Base UI Button]: https://base-ui.com/react/components/button
[Styling]: ../../docs/styling.md
[`Button`]: ../button/README.md
[name, role, and value]: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html

View File

@@ -5,6 +5,7 @@ import type { VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { Button as BaseButton } from '@base-ui/react/button'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { iconButtonVariants } from './variants'
type AccessibleName =
@@ -17,14 +18,10 @@ type AccessibleName =
'aria-labelledby': string
}
type IconButtonProps = Omit<
BaseButtonNS.Props,
'aria-label' | 'aria-labelledby' | 'children' | 'className'
> &
type IconButtonProps = Omit<BaseButtonNS.Props, 'aria-label' | 'aria-labelledby' | 'children'> &
AccessibleName &
VariantProps<typeof iconButtonVariants> & {
children: React.ReactElement
className?: string
}
function IconButton({
@@ -39,7 +36,9 @@ function IconButton({
return (
<BaseButton
type={type}
className={cn(iconButtonVariants({ variant, tone, size }), className)}
className={(state) =>
cn(iconButtonVariants({ variant, tone, size }), resolveClassName(className, state))
}
{...props}
>
{children}

View File

@@ -6,6 +6,7 @@ import type * as React from 'react'
import { Input as BaseInput } from '@base-ui/react/input'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const interactiveElementSelector =
'button,a[href],[role="button"],[role="link"],select,[tabindex]:not([tabindex="-1"]),input:not([type="hidden"]):not([disabled]),[contenteditable]:not([contenteditable="false"]),textarea:not([disabled])'
@@ -55,23 +56,23 @@ function InputGroup({ className, onMouseDown, ...props }: InputGroupProps) {
)
}
type InputGroupInputProps = Omit<BaseInputNS.Props, 'className' | 'render' | 'size'> & {
className?: string
}
type InputGroupInputProps = Omit<BaseInputNS.Props, 'render' | 'size'>
function InputGroupInput({ className, ...props }: InputGroupInputProps) {
return (
<BaseInput
{...props}
className={cn(
[
'w-0 min-w-0 flex-1 appearance-none rounded-none border-0 bg-transparent px-3 py-1.75 system-sm-regular text-components-input-text-filled caret-primary-600 outline-hidden',
'placeholder:text-components-input-text-placeholder',
'read-only:cursor-default',
'disabled:cursor-not-allowed disabled:text-components-input-text-filled-disabled',
],
className,
)}
className={(state) =>
cn(
[
'w-0 min-w-0 flex-1 appearance-none rounded-none border-0 bg-transparent px-3 py-1.75 system-sm-regular text-components-input-text-filled caret-primary-600 outline-hidden',
'placeholder:text-components-input-text-placeholder',
'read-only:cursor-default',
'disabled:cursor-not-allowed disabled:text-components-input-text-filled-disabled',
],
resolveClassName(className, state),
)
}
/>
)
}

View File

@@ -4,28 +4,29 @@ import type { Input as BaseInputNS } from '@base-ui/react/input'
import { Input as BaseInput } from '@base-ui/react/input'
import { cn } from '../cn'
import { textControlFocusClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
type InputProps = Omit<BaseInputNS.Props, 'className' | 'size'> & {
className?: string
}
type InputProps = Omit<BaseInputNS.Props, 'size'>
function Input({ className, ...props }: InputProps) {
return (
<BaseInput
className={cn(
[
'w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 py-1.75 system-sm-regular text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color]',
'placeholder:text-components-input-text-placeholder',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
textControlFocusClassName,
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none read-only:focus-visible:ring-2 read-only:focus-visible:ring-state-accent-solid',
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',
'disabled:hover:border-transparent disabled:hover:bg-components-input-bg-disabled',
'motion-reduce:transition-none',
],
className,
)}
className={(state) =>
cn(
[
'w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 py-1.75 system-sm-regular text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color]',
'placeholder:text-components-input-text-placeholder',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
textControlFocusClassName,
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none read-only:focus-visible:ring-2 read-only:focus-visible:ring-state-accent-solid',
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',
'disabled:hover:border-transparent disabled:hover:bg-components-input-bg-disabled',
'motion-reduce:transition-none',
],
resolveClassName(className, state),
)
}
{...props}
/>
)

View File

@@ -0,0 +1,6 @@
export function resolveClassName<State>(
className: string | ((state: State) => string | undefined) | undefined,
state: State,
) {
return typeof className === 'function' ? className(state) : className
}

View File

@@ -14,6 +14,7 @@ import type { VariantProps } from 'class-variance-authority'
import { Meter as BaseMeter } from '@base-ui/react/meter'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const Meter = BaseMeter.Root
type MeterProps = BaseMeter.Root.Props
@@ -21,12 +22,15 @@ type MeterProps = BaseMeter.Root.Props
const meterTrackClassName =
'relative block h-1 w-full overflow-hidden rounded-md bg-components-progress-bar-bg'
type MeterTrackProps = Omit<BaseMeter.Track.Props, 'className'> & {
className?: string
}
type MeterTrackProps = BaseMeter.Track.Props
function MeterTrack({ className, ...props }: MeterTrackProps) {
return <BaseMeter.Track className={cn(meterTrackClassName, className)} {...props} />
return (
<BaseMeter.Track
className={(state) => cn(meterTrackClassName, resolveClassName(className, state))}
{...props}
/>
)
}
const meterIndicatorVariants = cva(
@@ -47,33 +51,43 @@ const meterIndicatorVariants = cva(
type MeterTone = NonNullable<VariantProps<typeof meterIndicatorVariants>['tone']>
type MeterIndicatorProps = Omit<BaseMeter.Indicator.Props, 'className'> & {
className?: string
type MeterIndicatorProps = BaseMeter.Indicator.Props & {
tone?: MeterTone
}
function MeterIndicator({ className, tone, ...props }: MeterIndicatorProps) {
return (
<BaseMeter.Indicator className={cn(meterIndicatorVariants({ tone }), className)} {...props} />
<BaseMeter.Indicator
className={(state) =>
cn(meterIndicatorVariants({ tone }), resolveClassName(className, state))
}
{...props}
/>
)
}
const meterValueClassName = 'system-xs-regular text-text-tertiary tabular-nums'
type MeterValueProps = Omit<BaseMeter.Value.Props, 'className'> & {
className?: string
}
type MeterValueProps = BaseMeter.Value.Props
function MeterValue({ className, ...props }: MeterValueProps) {
return <BaseMeter.Value className={cn(meterValueClassName, className)} {...props} />
return (
<BaseMeter.Value
className={(state) => cn(meterValueClassName, resolveClassName(className, state))}
{...props}
/>
)
}
const meterLabelClassName = 'system-xs-medium text-text-tertiary'
type MeterLabelProps = Omit<BaseMeter.Label.Props, 'className'> & {
className?: string
}
type MeterLabelProps = BaseMeter.Label.Props
function MeterLabel({ className, ...props }: MeterLabelProps) {
return <BaseMeter.Label className={cn(meterLabelClassName, className)} {...props} />
return (
<BaseMeter.Label
className={(state) => cn(meterLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
export { Meter, MeterIndicator, MeterLabel, MeterTrack, MeterValue }

View File

@@ -6,6 +6,7 @@ import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlCompoundInputFocusClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
const NumberField = BaseNumberField.Root
type NumberFieldProps = BaseNumberField.Root.Props
@@ -36,15 +37,15 @@ const numberFieldGroupVariants = cva(
)
type NumberFieldSize = NonNullable<VariantProps<typeof numberFieldGroupVariants>['size']>
type NumberFieldGroupProps = Omit<BaseNumberField.Group.Props, 'className'> &
VariantProps<typeof numberFieldGroupVariants> & {
className?: string
}
type NumberFieldGroupProps = BaseNumberField.Group.Props &
VariantProps<typeof numberFieldGroupVariants>
function NumberFieldGroup({ className, size = 'medium', ...props }: NumberFieldGroupProps) {
return (
<BaseNumberField.Group
className={cn(numberFieldGroupVariants({ size }), className)}
className={(state) =>
cn(numberFieldGroupVariants({ size }), resolveClassName(className, state))
}
{...props}
/>
)
@@ -70,15 +71,15 @@ const numberFieldInputVariants = cva(
},
)
type NumberFieldInputProps = Omit<BaseNumberField.Input.Props, 'className' | 'size'> &
VariantProps<typeof numberFieldInputVariants> & {
className?: string
}
type NumberFieldInputProps = Omit<BaseNumberField.Input.Props, 'size'> &
VariantProps<typeof numberFieldInputVariants>
function NumberFieldInput({ className, size = 'medium', ...props }: NumberFieldInputProps) {
return (
<BaseNumberField.Input
className={cn(numberFieldInputVariants({ size }), className)}
className={(state) =>
cn(numberFieldInputVariants({ size }), resolveClassName(className, state))
}
{...props}
/>
)
@@ -171,14 +172,8 @@ type NumberFieldButtonVariantProps = Omit<
'direction'
>
type NumberFieldIncrementProps = Omit<BaseNumberField.Increment.Props, 'className'> &
NumberFieldButtonVariantProps & {
className?: string
}
type NumberFieldDecrementProps = Omit<BaseNumberField.Decrement.Props, 'className'> &
NumberFieldButtonVariantProps & {
className?: string
}
type NumberFieldIncrementProps = BaseNumberField.Increment.Props & NumberFieldButtonVariantProps
type NumberFieldDecrementProps = BaseNumberField.Decrement.Props & NumberFieldButtonVariantProps
const incrementAriaLabel = 'Increment value'
const decrementAriaLabel = 'Decrement value'
@@ -195,7 +190,12 @@ function NumberFieldIncrement({
aria-label={
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : incrementAriaLabel)
}
className={cn(numberFieldControlButtonVariants({ size, direction: 'increment' }), className)}
className={(state) =>
cn(
numberFieldControlButtonVariants({ size, direction: 'increment' }),
resolveClassName(className, state),
)
}
>
{children ?? <span aria-hidden="true" className="i-ri-arrow-up-s-line size-3" />}
</BaseNumberField.Increment>
@@ -214,7 +214,12 @@ function NumberFieldDecrement({
aria-label={
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : decrementAriaLabel)
}
className={cn(numberFieldControlButtonVariants({ size, direction: 'decrement' }), className)}
className={(state) =>
cn(
numberFieldControlButtonVariants({ size, direction: 'decrement' }),
resolveClassName(className, state),
)
}
>
{children ?? <span aria-hidden="true" className="i-ri-arrow-down-s-line size-3" />}
</BaseNumberField.Decrement>

View File

@@ -6,6 +6,7 @@ import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import * as React from 'react'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { useIsoLayoutEffect } from '../internals/use-iso-layout-effect'
import { NumberField, NumberFieldGroup, NumberFieldInput } from '../number-field'
import { SegmentedControl, SegmentedControlItem } from '../segmented-control'
@@ -208,14 +209,12 @@ function PaginationNavigation({ render, className, ...props }: PaginationNavigat
})
}
type PaginationPreviousProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
type PaginationPreviousProps = Omit<BaseButtonNS.Props, 'children'> & {
children?: React.ReactNode
className?: string
}
type PaginationNextProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
type PaginationNextProps = Omit<BaseButtonNS.Props, 'children'> & {
children?: React.ReactNode
className?: string
}
const paginationArrowButtonClassName = [
@@ -243,7 +242,7 @@ function PaginationPrevious({
{...props}
type="button"
aria-label={ariaLabel ?? 'Previous page'}
className={cn(paginationArrowButtonClassName, className)}
className={(state) => cn(paginationArrowButtonClassName, resolveClassName(className, state))}
disabled={disabled}
onClick={(event) => {
props.onClick?.(event)
@@ -273,7 +272,7 @@ function PaginationNext({
{...props}
type="button"
aria-label={ariaLabel ?? 'Next page'}
className={cn(paginationArrowButtonClassName, className)}
className={(state) => cn(paginationArrowButtonClassName, resolveClassName(className, state))}
disabled={disabled}
onClick={(event) => {
props.onClick?.(event)
@@ -286,10 +285,9 @@ function PaginationNext({
)
}
type PaginationPageJumpProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
type PaginationPageJumpProps = Omit<BaseButtonNS.Props, 'children'> & {
inputLabel?: string
children?: React.ReactNode
className?: string
}
function PaginationPageJump({
@@ -378,10 +376,12 @@ function PaginationPageJump({
aria-label={
ariaLabel ?? `Edit page number, current page ${pagination.page} of ${pagination.totalPages}`
}
className={cn(
'inline-flex h-7 touch-manipulation items-center justify-center gap-0.5 rounded-lg px-2 py-1.5 system-xs-medium text-text-secondary tabular-nums outline-hidden transition-colors hover:cursor-text hover:bg-state-base-hover-alt focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none',
className,
)}
className={(state) =>
cn(
'inline-flex h-7 touch-manipulation items-center justify-center gap-0.5 rounded-lg px-2 py-1.5 system-xs-medium text-text-secondary tabular-nums outline-hidden transition-colors hover:cursor-text hover:bg-state-base-hover-alt focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none',
resolveClassName(className, state),
)
}
onClick={(event) => {
props.onClick?.(event)
@@ -424,10 +424,9 @@ function PaginationPageList({ render, className, ...props }: PaginationPageListP
})
}
type PaginationPageProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
type PaginationPageProps = Omit<BaseButtonNS.Props, 'children'> & {
page: number
children?: React.ReactNode
className?: string
}
function PaginationPage({
@@ -446,12 +445,14 @@ function PaginationPage({
type="button"
aria-current={current ? 'page' : undefined}
aria-label={ariaLabel ?? (current ? `Page ${page}, current page` : `Go to page ${page}`)}
className={cn(
'inline-flex h-8 min-w-8 touch-manipulation items-center justify-center rounded-lg px-1 py-2 system-sm-medium text-text-tertiary tabular-nums outline-hidden hover:bg-components-button-ghost-bg-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
current &&
'bg-components-button-tertiary-bg text-components-button-tertiary-text hover:bg-components-button-ghost-bg-hover',
className,
)}
className={(state) =>
cn(
'inline-flex h-8 min-w-8 touch-manipulation items-center justify-center rounded-lg px-1 py-2 system-sm-medium text-text-tertiary tabular-nums outline-hidden hover:bg-components-button-ghost-bg-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
current &&
'bg-components-button-tertiary-bg text-components-button-tertiary-text hover:bg-components-button-ghost-bg-hover',
resolveClassName(className, state),
)
}
onClick={(event) => {
props.onClick?.(event)

View File

@@ -84,7 +84,11 @@ describe('PopoverContent', () => {
const screen = await renderWithSafeViewport(
<Popover open>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>
<PopoverContent
// Verify both Dify wrapper layers resolve the popup callback before merging classes.
className={(state) => (state.open ? 'w-48' : undefined)}
render={<section className="p-2" />}
>
<PopoverTitle>Default popover</PopoverTitle>
<span>Default content</span>
</PopoverContent>
@@ -98,6 +102,8 @@ describe('PopoverContent', () => {
expect(popupStyle.borderTopLeftRadius).not.toBe('0px')
expect(popupStyle.backgroundColor).not.toBe('rgba(0, 0, 0, 0)')
expect(popupStyle.boxShadow).not.toBe('none')
expect(popupStyle.width).toBe('192px')
expect(popupStyle.paddingTop).toBe('8px')
})
})
})

View File

@@ -4,6 +4,7 @@ import type * as React from 'react'
import type { Placement } from '../placement'
import { Popover as BasePopover } from '@base-ui/react/popover'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { floatingPopupAnimationClassName } from '../overlay-shared'
import { parsePlacement } from '../placement'
@@ -25,16 +26,18 @@ type PopoverCloseProps = BasePopover.Close.Props
type PopoverTitleProps = BasePopover.Title.Props
type PopoverDescriptionProps = BasePopover.Description.Props
type PopoverBackdropProps = Omit<BasePopover.Backdrop.Props, 'className'> & {
className?: string
}
type PopoverBackdropProps = BasePopover.Backdrop.Props
function PopoverBackdrop({ className, ...props }: PopoverBackdropProps) {
return <BasePopover.Backdrop className={cn('fixed inset-0 z-50', className)} {...props} />
return (
<BasePopover.Backdrop
className={(state) => cn('fixed inset-0 z-50', resolveClassName(className, state))}
{...props}
/>
)
}
type PopoverPositionerProps = Omit<BasePopover.Positioner.Props, 'className' | 'side' | 'align'> & {
className?: string
type PopoverPositionerProps = Omit<BasePopover.Positioner.Props, 'side' | 'align'> & {
placement?: Placement
}
@@ -53,33 +56,32 @@ function PopoverPositioner({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-hidden', className)}
className={(state) => cn('z-50 outline-hidden', resolveClassName(className, state))}
{...props}
/>
)
}
type PopoverPopupProps = Omit<BasePopover.Popup.Props, 'className'> & {
className?: string
}
type PopoverPopupProps = BasePopover.Popup.Props
function PopoverPopup({ className, ...props }: PopoverPopupProps) {
return (
<BasePopover.Popup
className={cn(
'outline-hidden focus:outline-hidden focus-visible:outline-hidden',
floatingPopupAnimationClassName,
className,
)}
className={(state) =>
cn(
'outline-hidden focus:outline-hidden focus-visible:outline-hidden',
floatingPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type PopoverContentProps = Omit<PopoverPopupProps, 'children' | 'className'> &
type PopoverContentProps = Omit<PopoverPopupProps, 'children'> &
Pick<PopoverPositionerProps, 'alignOffset' | 'placement' | 'sideOffset'> & {
children: React.ReactNode
className?: string
}
function PopoverContent({
@@ -94,10 +96,12 @@ function PopoverContent({
<PopoverPortal>
<PopoverPositioner placement={placement} sideOffset={sideOffset} alignOffset={alignOffset}>
<PopoverPopup
className={cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
className,
)}
className={(state) =>
cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
resolveClassName(className, state),
)
}
{...props}
>
{children}

View File

@@ -4,6 +4,7 @@ import type * as React from 'react'
import type { Placement } from '../placement'
import { PreviewCard as BasePreviewCard } from '@base-ui/react/preview-card'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { floatingPopupAnimationClassName } from '../overlay-shared'
import { parsePlacement } from '../placement'
@@ -31,11 +32,7 @@ type PreviewCardTriggerProps<Payload = unknown> = BasePreviewCard.Trigger.Props<
type PreviewCardPortalProps = BasePreviewCard.Portal.Props
type PreviewCardViewportProps = BasePreviewCard.Viewport.Props
type PreviewCardPositionerProps = Omit<
BasePreviewCard.Positioner.Props,
'className' | 'side' | 'align'
> & {
className?: string
type PreviewCardPositionerProps = Omit<BasePreviewCard.Positioner.Props, 'side' | 'align'> & {
placement?: Placement
}
@@ -54,26 +51,26 @@ function PreviewCardPositioner({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('z-50 outline-hidden', className)}
className={(state) => cn('z-50 outline-hidden', resolveClassName(className, state))}
{...props}
/>
)
}
type PreviewCardPopupProps = Omit<BasePreviewCard.Popup.Props, 'className'> & {
className?: string
}
type PreviewCardPopupProps = BasePreviewCard.Popup.Props
function PreviewCardPopup({ className, ...props }: PreviewCardPopupProps) {
return (
<BasePreviewCard.Popup className={cn(floatingPopupAnimationClassName, className)} {...props} />
<BasePreviewCard.Popup
className={(state) => cn(floatingPopupAnimationClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type PreviewCardContentProps = Omit<PreviewCardPopupProps, 'children' | 'className'> &
type PreviewCardContentProps = Omit<PreviewCardPopupProps, 'children'> &
Pick<PreviewCardPositionerProps, 'alignOffset' | 'placement' | 'sideOffset'> & {
children: React.ReactNode
className?: string
}
function PreviewCardContent({
@@ -92,10 +89,12 @@ function PreviewCardContent({
alignOffset={alignOffset}
>
<PreviewCardPopup
className={cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
className,
)}
className={(state) =>
cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
resolveClassName(className, state),
)
}
{...props}
>
{children}

View File

@@ -4,6 +4,7 @@ import type { VariantProps } from 'class-variance-authority'
import { Progress as BaseProgress } from '@base-ui/react/progress'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const progressCircleRootVariants = cva('inline-flex shrink-0 items-center justify-center', {
variants: {
@@ -67,10 +68,9 @@ type ProgressCircleAccessibleNameProps =
type ProgressCircleProps = Omit<
BaseProgress.Root.Props,
'children' | 'className' | 'aria-label' | 'aria-labelledby'
'children' | 'aria-label' | 'aria-labelledby'
> &
ProgressCircleAccessibleNameProps & {
className?: string
color?: ProgressCircleColor
size?: ProgressCircleSize
circleStrokeWidth?: number
@@ -130,7 +130,9 @@ function ProgressCircle({
return (
<BaseProgress.Root
className={cn(progressCircleRootVariants({ size }), className)}
className={(state) =>
cn(progressCircleRootVariants({ size }), resolveClassName(className, state))
}
value={value}
min={min}
max={max}

View File

@@ -6,47 +6,46 @@ import type * as React from 'react'
import { Radio as BaseRadio } from '@base-ui/react/radio'
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
type RadioGroupProps<Value = string> = Omit<BaseRadioGroupNS.Props<Value>, 'className'> & {
className?: string
}
type RadioGroupProps<Value = string> = BaseRadioGroupNS.Props<Value>
function RadioGroup<Value = string>({ className, ...props }: RadioGroupProps<Value>) {
return <BaseRadioGroup<Value> className={cn('flex items-center gap-2', className)} {...props} />
return (
<BaseRadioGroup<Value>
className={(state) => cn('flex items-center gap-2', resolveClassName(className, state))}
{...props}
/>
)
}
type RadioItemProps<Value = string> = Omit<BaseRadioNS.Root.Props<Value>, 'className'> & {
className?: string
}
type RadioItemProps<Value = string> = BaseRadioNS.Root.Props<Value>
function RadioItem<Value = string>({ className, ...props }: RadioItemProps<Value>) {
return <BaseRadio.Root<Value> className={className} {...props} />
}
type RadioControlProps = Omit<
BaseRadioNS.Indicator.Props,
'className' | 'children' | 'keepMounted'
> & {
className?: string
}
type RadioControlProps = Omit<BaseRadioNS.Indicator.Props, 'children' | 'keepMounted'>
function RadioControl({ className, ...props }: RadioControlProps) {
return (
<BaseRadio.Indicator
{...props}
keepMounted
className={cn(
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-full p-0 transition-colors motion-reduce:transition-none',
'border border-components-radio-border bg-components-radio-bg shadow-xs shadow-shadow-shadow-3',
'hover:border-components-radio-border-hover hover:bg-components-radio-bg-hover',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-offset-0 focus-visible:outline-hidden',
'data-checked:border-[5px] data-checked:border-components-radio-border-checked data-checked:hover:border-components-radio-border-checked-hover',
'data-disabled:cursor-not-allowed data-disabled:border-components-radio-border-disabled data-disabled:bg-components-radio-bg-disabled',
'data-disabled:hover:border-components-radio-border-disabled data-disabled:hover:bg-components-radio-bg-disabled',
'data-disabled:data-checked:border-[5px] data-disabled:data-checked:border-components-radio-border-checked-disabled',
'data-disabled:data-checked:hover:border-components-radio-border-checked-disabled',
className,
)}
className={(state) =>
cn(
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-full p-0 transition-colors motion-reduce:transition-none',
'border border-components-radio-border bg-components-radio-bg shadow-xs shadow-shadow-shadow-3',
'hover:border-components-radio-border-hover hover:bg-components-radio-bg-hover',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-offset-0 focus-visible:outline-hidden',
'data-checked:border-[5px] data-checked:border-components-radio-border-checked data-checked:hover:border-components-radio-border-checked-hover',
'data-disabled:cursor-not-allowed data-disabled:border-components-radio-border-disabled data-disabled:bg-components-radio-bg-disabled',
'data-disabled:hover:border-components-radio-border-disabled data-disabled:hover:bg-components-radio-bg-disabled',
'data-disabled:data-checked:border-[5px] data-disabled:data-checked:border-components-radio-border-checked-disabled',
'data-disabled:data-checked:hover:border-components-radio-border-checked-disabled',
resolveClassName(className, state),
)
}
/>
)
}

View File

@@ -2,14 +2,17 @@
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
type ScrollAreaProps = Omit<BaseScrollArea.Root.Props, 'className'> & {
className?: string
}
type ScrollAreaProps = BaseScrollArea.Root.Props
function ScrollArea({ className, ...props }: ScrollAreaProps) {
return (
<BaseScrollArea.Root {...props} data-dify-scroll-area="" className={cn('isolate', className)} />
<BaseScrollArea.Root
{...props}
data-dify-scroll-area=""
className={(state) => cn('isolate', resolveClassName(className, state))}
/>
)
}
@@ -33,48 +36,52 @@ const scrollAreaThumbClassName = cn(
'active:bg-state-base-handle-hover',
)
type ScrollAreaViewportProps = Omit<BaseScrollArea.Viewport.Props, 'className'> & {
className?: string
}
type ScrollAreaViewportProps = BaseScrollArea.Viewport.Props
function ScrollAreaViewport({ className, ...props }: ScrollAreaViewportProps) {
return (
<BaseScrollArea.Viewport
{...props}
data-dify-scroll-area-viewport=""
className={cn('isolate size-full rounded-[inherit] outline-none', className)}
className={(state) =>
cn('isolate size-full rounded-[inherit] outline-none', resolveClassName(className, state))
}
/>
)
}
type ScrollAreaScrollbarProps = Omit<BaseScrollArea.Scrollbar.Props, 'className'> & {
className?: string
}
type ScrollAreaScrollbarProps = BaseScrollArea.Scrollbar.Props
function ScrollAreaScrollbar({ className, ...props }: ScrollAreaScrollbarProps) {
return (
<BaseScrollArea.Scrollbar
{...props}
data-dify-scroll-area-scrollbar=""
className={cn(scrollAreaScrollbarClassName, className)}
className={(state) => cn(scrollAreaScrollbarClassName, resolveClassName(className, state))}
/>
)
}
type ScrollAreaThumbProps = Omit<BaseScrollArea.Thumb.Props, 'className'> & {
className?: string
}
type ScrollAreaThumbProps = BaseScrollArea.Thumb.Props
function ScrollAreaThumb({ className, ...props }: ScrollAreaThumbProps) {
return <BaseScrollArea.Thumb className={cn(scrollAreaThumbClassName, className)} {...props} />
return (
<BaseScrollArea.Thumb
className={(state) => cn(scrollAreaThumbClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type ScrollAreaCornerProps = Omit<BaseScrollArea.Corner.Props, 'className'> & {
className?: string
}
type ScrollAreaCornerProps = BaseScrollArea.Corner.Props
function ScrollAreaCorner({ className, ...props }: ScrollAreaCornerProps) {
return <BaseScrollArea.Corner className={cn('bg-transparent', className)} {...props} />
return (
<BaseScrollArea.Corner
className={(state) => cn('bg-transparent', resolveClassName(className, state))}
{...props}
/>
)
}
export {

View File

@@ -6,6 +6,7 @@ import type * as React from 'react'
import { Radio as BaseRadio } from '@base-ui/react/radio'
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
type SegmentedControlSelectionProps<Value> =
| {
@@ -19,30 +20,25 @@ type SegmentedControlSelectionProps<Value> =
type SegmentedControlProps<Value = string> = Omit<
BaseRadioGroupNS.Props<Value>,
'className' | 'defaultValue' | 'value'
'defaultValue' | 'value'
> &
SegmentedControlSelectionProps<Value> & {
className?: string
}
SegmentedControlSelectionProps<Value>
function SegmentedControl<Value = string>({ className, ...props }: SegmentedControlProps<Value>) {
return (
<BaseRadioGroup<Value>
className={cn(
'inline-flex items-center gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5',
className,
)}
className={(state) =>
cn(
'inline-flex items-center gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type SegmentedControlItemProps<Value = string> = Omit<
BaseRadioNS.Root.Props<Value>,
'className'
> & {
className?: string
}
type SegmentedControlItemProps<Value = string> = BaseRadioNS.Root.Props<Value>
function SegmentedControlItem<Value = string>({
className,
@@ -54,10 +50,12 @@ function SegmentedControlItem<Value = string>({
<BaseRadio.Root<Value>
nativeButton={nativeButton}
render={render}
className={cn(
'relative flex h-7 min-w-0 touch-manipulation items-center justify-center gap-0.5 overflow-hidden rounded-lg border-[0.5px] border-transparent px-2 py-1 system-sm-medium whitespace-nowrap text-text-secondary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-checked:border-components-segmented-control-item-active-border data-checked:bg-components-segmented-control-item-active-bg data-checked:text-text-accent-light-mode-only data-checked:shadow-xs data-checked:shadow-shadow-shadow-3 data-disabled:cursor-not-allowed data-disabled:bg-transparent data-disabled:text-text-disabled data-disabled:shadow-none data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
className,
)}
className={(state) =>
cn(
'relative flex h-7 min-w-0 touch-manipulation items-center justify-center gap-0.5 overflow-hidden rounded-lg border-[0.5px] border-transparent px-2 py-1 system-sm-medium whitespace-nowrap text-text-secondary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-checked:border-components-segmented-control-item-active-border data-checked:bg-components-segmented-control-item-active-bg data-checked:text-text-accent-light-mode-only data-checked:shadow-xs data-checked:shadow-shadow-shadow-3 data-disabled:cursor-not-allowed data-disabled:bg-transparent data-disabled:text-text-disabled data-disabled:shadow-none data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
resolveClassName(className, state),
)
}
{...props}
/>
)

View File

@@ -8,6 +8,7 @@ import { Select as BaseSelect } from '@base-ui/react/select'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { formLabelClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
import {
floatingGroupLabelClassName,
floatingItemIndicatorClassName,
@@ -85,14 +86,18 @@ const selectTriggerVariants = cva(
)
type SelectSize = NonNullable<VariantProps<typeof selectTriggerVariants>['size']>
type SelectTriggerProps = Omit<BaseSelect.Trigger.Props, 'className'> & {
className?: string
type SelectTriggerProps = BaseSelect.Trigger.Props & {
size?: SelectSize
}
function SelectTrigger({ className, children, size, ...props }: SelectTriggerProps) {
return (
<BaseSelect.Trigger className={cn(selectTriggerVariants({ size, className }))} {...props}>
<BaseSelect.Trigger
className={(state) =>
cn(selectTriggerVariants({ size, className: resolveClassName(className, state) }))
}
{...props}
>
<span className="min-w-0 grow truncate">{children}</span>
<BaseSelect.Icon className="shrink-0 text-text-quaternary transition-colors group-data-readonly/select-trigger:hidden group-[:hover:not([data-disabled])]/select-trigger:text-text-secondary data-popup-open:text-text-secondary">
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden="true" />
@@ -101,31 +106,43 @@ function SelectTrigger({ className, children, size, ...props }: SelectTriggerPro
)
}
type SelectLabelProps = Omit<BaseSelect.Label.Props, 'className'> & { className?: string }
type SelectLabelProps = BaseSelect.Label.Props
function SelectLabel({ className, ...props }: SelectLabelProps) {
return <BaseSelect.Label className={cn(formLabelClassName, className)} {...props} />
return (
<BaseSelect.Label
className={(state) => cn(formLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type SelectGroupLabelProps = Omit<BaseSelect.GroupLabel.Props, 'className'> & {
className?: string
}
type SelectGroupLabelProps = BaseSelect.GroupLabel.Props
function SelectGroupLabel({ className, ...props }: SelectGroupLabelProps) {
return <BaseSelect.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
return (
<BaseSelect.GroupLabel
className={(state) => cn(floatingGroupLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type SelectSeparatorProps = Omit<BaseSelect.Separator.Props, 'className'> & { className?: string }
type SelectSeparatorProps = BaseSelect.Separator.Props
function SelectSeparator({ className, ...props }: SelectSeparatorProps) {
return <BaseSelect.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
return (
<BaseSelect.Separator
className={(state) => cn(floatingSeparatorClassName, resolveClassName(className, state))}
{...props}
/>
)
}
const SelectPortal = BaseSelect.Portal
type SelectPortalProps = BaseSelect.Portal.Props
type SelectPositionerProps = Omit<BaseSelect.Positioner.Props, 'className' | 'side' | 'align'> & {
className?: string
type SelectPositionerProps = Omit<BaseSelect.Positioner.Props, 'side' | 'align'> & {
placement?: Placement
}
@@ -146,46 +163,45 @@ function SelectPositioner({
sideOffset={sideOffset}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className={cn('z-50 outline-hidden', className)}
className={(state) => cn('z-50 outline-hidden', resolveClassName(className, state))}
{...props}
/>
)
}
type SelectPopupProps = Omit<BaseSelect.Popup.Props, 'className'> & {
className?: string
}
type SelectPopupProps = BaseSelect.Popup.Props
function SelectPopup({ className, ...props }: SelectPopupProps) {
return (
<BaseSelect.Popup
className={cn(
'max-w-(--available-width) min-w-[min(var(--anchor-width),var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
floatingPopupAnimationClassName,
className,
)}
className={(state) =>
cn(
'max-w-(--available-width) min-w-[min(var(--anchor-width),var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
floatingPopupAnimationClassName,
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type SelectListProps = Omit<BaseSelect.List.Props, 'className'> & {
className?: string
}
type SelectListProps = BaseSelect.List.Props
function SelectList({ className, ...props }: SelectListProps) {
return (
<BaseSelect.List
className={cn('max-h-80 overflow-auto p-1 outline-hidden', className)}
className={(state) =>
cn('max-h-80 overflow-auto p-1 outline-hidden', resolveClassName(className, state))
}
{...props}
/>
)
}
type SelectContentProps = Omit<SelectPopupProps, 'children' | 'className'> &
type SelectContentProps = Omit<SelectPopupProps, 'children'> &
Pick<SelectPositionerProps, 'alignOffset' | 'placement' | 'sideOffset'> & {
children: React.ReactNode
className?: string
}
function SelectContent({
@@ -207,39 +223,46 @@ function SelectContent({
)
}
type SelectItemProps<Value = unknown> = Omit<BaseSelect.Item.Props, 'className' | 'value'> & {
className?: string
type SelectItemProps<Value = unknown> = Omit<BaseSelect.Item.Props, 'value'> & {
value?: Value
}
function SelectItem<Value = unknown>({ className, ...props }: SelectItemProps<Value>) {
return (
<BaseSelect.Item
className={cn(
'flex h-8 cursor-pointer items-center rounded-lg px-2 system-sm-medium text-text-secondary outline-hidden',
'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-state-base-hover',
className,
)}
className={(state) =>
cn(
'flex h-8 cursor-pointer items-center rounded-lg px-2 system-sm-medium text-text-secondary outline-hidden',
'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-state-base-hover',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type SelectItemTextProps = Omit<BaseSelect.ItemText.Props, 'className'> & { className?: string }
type SelectItemTextProps = BaseSelect.ItemText.Props
function SelectItemText({ className, ...props }: SelectItemTextProps) {
return (
<BaseSelect.ItemText className={cn('me-1 min-w-0 grow truncate px-1', className)} {...props} />
<BaseSelect.ItemText
className={(state) =>
cn('me-1 min-w-0 grow truncate px-1', resolveClassName(className, state))
}
{...props}
/>
)
}
type SelectItemIndicatorProps = Omit<BaseSelect.ItemIndicator.Props, 'children' | 'className'> & {
className?: string
}
type SelectItemIndicatorProps = Omit<BaseSelect.ItemIndicator.Props, 'children'>
function SelectItemIndicator({ className, ...props }: SelectItemIndicatorProps) {
return (
<BaseSelect.ItemIndicator className={cn(floatingItemIndicatorClassName, className)} {...props}>
<BaseSelect.ItemIndicator
className={(state) => cn(floatingItemIndicatorClassName, resolveClassName(className, state))}
{...props}
>
<span className="i-ri-check-line h-4 w-4" aria-hidden />
</BaseSelect.ItemIndicator>
)

View File

@@ -18,8 +18,7 @@ does not implement the keyboard interaction or value semantics of a resizable se
Keep headings and labels beside the separator, since descendants of `role="separator"` have
presentational semantics. Use the owning menu family's separator inside menus.
Base UI owns `orientation`, `render`, refs, and callback-valued `style`; render callbacks receive
its orientation state. `className` accepts a string for Dify's class merging. Custom render
Follow the shared [styling contract] for state callbacks. Custom render
components must forward the received props and ref. Explicit DOM props and render overrides can
change the default semantics; callers are responsible for keeping those overrides consistent
with the intended role and `decorative` setting.
@@ -31,3 +30,4 @@ Use `variant` for the gradient rather than repeating its styles. To customize co
background token for solid lines or a gradient-start token for gradient lines.
[Base UI Separator]: https://base-ui.com/react/components/separator
[styling contract]: ../../docs/styling.md

View File

@@ -10,8 +10,9 @@ export function separatorTypeContracts(ref: React.Ref<HTMLDivElement>) {
style={(state) => ({ height: state.orientation === 'vertical' ? 24 : 1 })}
/>
)
// @ts-expect-error Dify merges string class names, not state callbacks.
const className = <Separator className={() => 'custom'} />
const className = (
<Separator className={(state) => (state.orientation === 'vertical' ? 'custom' : undefined)} />
)
// @ts-expect-error Orientation is inherited from Base UI.
const orientation = <Separator orientation="diagonal" />
return { decorative, semantic, render, className, orientation }

View File

@@ -3,6 +3,7 @@
import { Separator as BaseSeparator } from '@base-ui/react/separator'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const separatorVariants = cva('shrink-0', {
variants: {
@@ -17,8 +18,7 @@ const separatorVariants = cva('shrink-0', {
},
})
type SeparatorProps = Omit<BaseSeparator.Props, 'className'> & {
className?: string
type SeparatorProps = BaseSeparator.Props & {
/** Set true for visual lines that do not separate content groups. */
decorative?: boolean
variant?: 'solid' | 'gradient'
@@ -36,7 +36,9 @@ function Separator({
orientation={orientation}
role={decorative ? 'none' : 'separator'}
aria-orientation={decorative ? undefined : orientation}
className={cn(separatorVariants({ orientation, variant }), className)}
className={(state) =>
cn(separatorVariants({ orientation, variant }), resolveClassName(className, state))
}
{...props}
/>
)

View File

@@ -3,12 +3,10 @@
import { Slider as BaseSlider } from '@base-ui/react/slider'
import { cn } from '../cn'
import { formLabelClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
type SliderValue = number | readonly number[]
type SliderProps<Value extends SliderValue = SliderValue> = Omit<
BaseSlider.Root.Props<Value>,
'className'
> & { className?: string }
type SliderProps<Value extends SliderValue = SliderValue> = BaseSlider.Root.Props<Value>
function Slider<Value extends SliderValue = SliderValue>({
className,
@@ -16,83 +14,96 @@ function Slider<Value extends SliderValue = SliderValue>({
}: SliderProps<Value>) {
return (
<BaseSlider.Root
className={cn(
'group/slider relative inline-flex w-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
'data-disabled:opacity-30',
className,
)}
className={(state) =>
cn(
'group/slider relative inline-flex w-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
'data-disabled:opacity-30',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type SliderLabelProps = Omit<BaseSlider.Label.Props, 'className'> & { className?: string }
type SliderLabelProps = BaseSlider.Label.Props
function SliderLabel({ className, ...props }: SliderLabelProps) {
return <BaseSlider.Label className={cn(formLabelClassName, className)} {...props} />
return (
<BaseSlider.Label
className={(state) => cn(formLabelClassName, resolveClassName(className, state))}
{...props}
/>
)
}
type SliderControlProps = Omit<BaseSlider.Control.Props, 'className'> & { className?: string }
type SliderControlProps = BaseSlider.Control.Props
function SliderControl({ className, ...props }: SliderControlProps) {
return (
<BaseSlider.Control
className={cn(
'relative flex h-5 w-full touch-none items-center select-none',
'data-[orientation=vertical]:h-32 data-[orientation=vertical]:w-5 data-[orientation=vertical]:justify-center',
'data-disabled:cursor-not-allowed',
className,
)}
className={(state) =>
cn(
'relative flex h-5 w-full touch-none items-center select-none',
'data-[orientation=vertical]:h-32 data-[orientation=vertical]:w-5 data-[orientation=vertical]:justify-center',
'data-disabled:cursor-not-allowed',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type SliderTrackProps = Omit<BaseSlider.Track.Props, 'className'> & { className?: string }
type SliderTrackProps = BaseSlider.Track.Props
function SliderTrack({ className, ...props }: SliderTrackProps) {
return (
<BaseSlider.Track
className={cn(
'relative h-1 w-full rounded-full',
'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1',
'bg-components-slider-track',
className,
)}
className={(state) =>
cn(
'relative h-1 w-full rounded-full',
'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1',
'bg-components-slider-track',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type SliderIndicatorProps = Omit<BaseSlider.Indicator.Props, 'className'> & {
className?: string
}
type SliderIndicatorProps = BaseSlider.Indicator.Props
function SliderIndicator({ className, ...props }: SliderIndicatorProps) {
return (
<BaseSlider.Indicator
className={cn('h-full rounded-full', 'bg-components-slider-range', className)}
className={(state) =>
cn('h-full rounded-full', 'bg-components-slider-range', resolveClassName(className, state))
}
{...props}
/>
)
}
type SliderThumbProps = Omit<BaseSlider.Thumb.Props, 'className'> & { className?: string }
type SliderThumbProps = BaseSlider.Thumb.Props
function SliderThumb({ className, ...props }: SliderThumbProps) {
return (
<BaseSlider.Thumb
className={cn(
'block h-5 w-2 shrink-0 rounded-[3px] border-[0.5px]',
'data-[orientation=vertical]:h-2 data-[orientation=vertical]:w-5',
'border-components-slider-knob-border bg-components-slider-knob shadow-sm',
'transition-[background-color,border-color,box-shadow,opacity] motion-reduce:transition-none',
'hover:bg-components-slider-knob-hover',
'has-focus-visible:ring-2 has-focus-visible:ring-state-accent-solid has-focus-visible:ring-offset-0',
'active:shadow-md group-data-dragging/slider:has-focus:shadow-md',
'data-disabled:border-components-slider-knob-border data-disabled:bg-components-slider-knob-disabled data-disabled:shadow-none',
className,
)}
className={(state) =>
cn(
'block h-5 w-2 shrink-0 rounded-[3px] border-[0.5px]',
'data-[orientation=vertical]:h-2 data-[orientation=vertical]:w-5',
'border-components-slider-knob-border bg-components-slider-knob shadow-sm',
'transition-[background-color,border-color,box-shadow,opacity] motion-reduce:transition-none',
'hover:bg-components-slider-knob-hover',
'has-focus-visible:ring-2 has-focus-visible:ring-state-accent-solid has-focus-visible:ring-offset-0',
'active:shadow-md group-data-dragging/slider:has-focus:shadow-md',
'data-disabled:border-components-slider-knob-border data-disabled:bg-components-slider-knob-disabled data-disabled:shadow-none',
resolveClassName(className, state),
)
}
{...props}
/>
)

View File

@@ -6,6 +6,7 @@ import type * as React from 'react'
import { Switch as BaseSwitch } from '@base-ui/react/switch'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const switchRootStateClassName =
'bg-components-toggle-bg-unchecked hover:bg-components-toggle-bg-unchecked-hover data-checked:bg-components-toggle-bg data-checked:hover:bg-components-toggle-bg-hover data-disabled:cursor-not-allowed data-disabled:bg-components-toggle-bg-unchecked-disabled data-disabled:hover:bg-components-toggle-bg-unchecked-disabled data-disabled:data-checked:bg-components-toggle-bg-disabled data-disabled:data-checked:hover:bg-components-toggle-bg-disabled'
@@ -67,13 +68,12 @@ type SwitchControlProps = ControlledSwitchProps | UncontrolledSwitchProps
type SwitchProps = Omit<
BaseSwitchNS.Root.Props,
'checked' | 'defaultChecked' | 'className' | 'size' | 'onCheckedChange'
'checked' | 'defaultChecked' | 'size' | 'onCheckedChange'
> &
VariantProps<typeof switchRootVariants> &
SwitchControlProps & {
onCheckedChange?: (checked: boolean) => void
loading?: boolean
className?: string
}
function Switch({
@@ -92,7 +92,7 @@ function Switch({
checked={checked}
disabled={isDisabled}
aria-busy={loading || undefined}
className={cn(switchRootVariants({ size }), className)}
className={(state) => cn(switchRootVariants({ size }), resolveClassName(className, state))}
onCheckedChange={(value) => onCheckedChange?.(value)}
{...props}
>

View File

@@ -3,45 +3,49 @@
import type { Tabs as BaseTabsNS } from '@base-ui/react/tabs'
import { Tabs as BaseTabs } from '@base-ui/react/tabs'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
type TabsProps = BaseTabsNS.Root.Props
const Tabs = BaseTabs.Root
type TabsListProps = Omit<BaseTabsNS.List.Props, 'className'> & {
className?: string
}
type TabsListProps = BaseTabsNS.List.Props
function TabsList({ className, ...props }: TabsListProps) {
return <BaseTabs.List className={cn('flex gap-4', className)} {...props} />
}
type TabsTabProps = Omit<BaseTabsNS.Tab.Props, 'className'> & {
className?: string
}
function TabsTab({ className, ...props }: TabsTabProps) {
return (
<BaseTabs.Tab
className={cn(
'relative flex cursor-pointer touch-manipulation items-center border-b-2 border-transparent pt-2.5 pb-2 system-md-semibold text-text-tertiary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-active:border-components-tab-active data-active:text-text-primary data-disabled:cursor-not-allowed data-disabled:text-text-tertiary data-disabled:opacity-30 data-active:data-disabled:text-text-primary',
className,
)}
<BaseTabs.List
className={(state) => cn('flex gap-4', resolveClassName(className, state))}
{...props}
/>
)
}
type TabsPanelProps = Omit<BaseTabsNS.Panel.Props, 'className'> & {
className?: string
type TabsTabProps = BaseTabsNS.Tab.Props
function TabsTab({ className, ...props }: TabsTabProps) {
return (
<BaseTabs.Tab
className={(state) =>
cn(
'relative flex cursor-pointer touch-manipulation items-center border-b-2 border-transparent pt-2.5 pb-2 system-md-semibold text-text-tertiary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-active:border-components-tab-active data-active:text-text-primary data-disabled:cursor-not-allowed data-disabled:text-text-tertiary data-disabled:opacity-30 data-active:data-disabled:text-text-primary',
resolveClassName(className, state),
)
}
{...props}
/>
)
}
type TabsPanelProps = BaseTabsNS.Panel.Props
function TabsPanel({ className, ...props }: TabsPanelProps) {
return (
<BaseTabs.Panel
className={cn(
'outline-hidden focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
className,
)}
className={(state) =>
cn(
'outline-hidden focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
resolveClassName(className, state),
)
}
{...props}
/>
)

View File

@@ -37,15 +37,30 @@ describe('Textarea', () => {
expect(asHTMLElement(textarea.element()).tagName).toBe('TEXTAREA')
})
it('should apply custom classes', async () => {
it('should update callback styles from field state while preserving textarea variants', async () => {
const screen = await render(
<label>
Prompt
<Textarea size="large" className="resize-none" />
</label>,
<Field>
<FieldLabel>Prompt</FieldLabel>
<Textarea
size="large"
// Exercise Dify's callback merging across the Field.Control textarea adapter.
className={(state) => (state.filled ? 'resize-none' : undefined)}
style={(state) => ({ opacity: state.filled ? 0.5 : 1 })}
/>
</Field>,
)
const textarea = screen.getByRole('textbox', { name: 'Prompt' })
const initialPadding = getComputedStyle(textarea.element()).padding
await expect.element(screen.getByRole('textbox', { name: 'Prompt' })).toHaveClass('resize-none')
await expect.element(textarea).toHaveStyle({ opacity: '1' })
await textarea.fill('Draft')
await expect.poll(() => getComputedStyle(textarea.element()).resize).toBe('none')
await expect.element(textarea).toHaveStyle({ opacity: '0.5' })
expect(getComputedStyle(textarea.element()).padding).toBe(initialPadding)
await textarea.fill('')
await expect.element(textarea).toHaveStyle({ opacity: '1' })
await expect.poll(() => getComputedStyle(textarea.element()).resize).not.toBe('none')
})
it('should call onValueChange and stay controlled until value changes', async () => {

View File

@@ -7,6 +7,7 @@ import { Field as BaseField } from '@base-ui/react/field'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlFocusClassName } from '../form-control-shared'
import { resolveClassName } from '../internals/resolve-class-name'
const textareaVariants = cva(
[
@@ -61,6 +62,7 @@ type TextareaElementProps = Omit<
| 'onChange'
| 'rows'
| 'size'
| 'style'
| 'value'
| 'wrap'
>
@@ -73,11 +75,11 @@ type FieldControlTextareaProps = Omit<
>
type TextareaProps = TextareaElementProps &
Pick<BaseFieldNS.Control.Props, 'className' | 'style'> &
TextareaOnlyProps &
TextareaControlProps &
TextareaVariantProps & {
children?: never
className?: string
}
function Textarea({
@@ -98,7 +100,7 @@ function Textarea({
return (
<BaseField.Control
{...fieldControlProps}
className={cn(textareaVariants({ size }), className)}
className={(state) => cn(textareaVariants({ size }), resolveClassName(className, state))}
defaultValue={defaultValue}
onValueChange={onValueChange}
ref={ref}

View File

@@ -4,6 +4,7 @@ import type * as React from 'react'
import type { Placement } from '../placement'
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
import { parsePlacement } from '../placement'
/**
@@ -33,11 +34,10 @@ type TooltipProviderProps = BaseTooltip.Provider.Props
type TooltipProps<Payload = unknown> = BaseTooltip.Root.Props<Payload>
type TooltipTriggerProps<Payload = unknown> = BaseTooltip.Trigger.Props<Payload>
type TooltipContentProps = Omit<BaseTooltip.Popup.Props, 'children' | 'className'> &
type TooltipContentProps = Omit<BaseTooltip.Popup.Props, 'children'> &
Pick<BaseTooltip.Positioner.Props, 'sideOffset' | 'alignOffset'> & {
children: React.ReactNode
placement?: Placement
className?: string
}
function TooltipContent({
@@ -60,11 +60,13 @@ function TooltipContent({
className="z-50 outline-hidden"
>
<BaseTooltip.Popup
className={cn(
'max-w-75 rounded-md bg-components-panel-bg px-3 py-2 text-start system-xs-regular wrap-break-word text-text-tertiary shadow-lg',
'origin-(--transform-origin) transition-opacity data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0 motion-reduce:transition-none',
className,
)}
className={(state) =>
cn(
'max-w-75 rounded-md bg-components-panel-bg px-3 py-2 text-start system-xs-regular wrap-break-word text-text-tertiary shadow-lg',
'origin-(--transform-origin) transition-opacity data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0 motion-reduce:transition-none',
resolveClassName(className, state),
)
}
{...props}
>
{children}