refactor(dify-ui): govern public component APIs (#39806)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-07-30 18:12:34 +08:00 committed by GitHub
parent 3aa937def4
commit 563cf2602f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 2381 additions and 978 deletions

View File

@ -1686,11 +1686,6 @@
"count": 5
}
},
"web/app/components/base/markdown-blocks/button.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"web/app/components/base/markdown-blocks/code-block.tsx": {
"typescript/no-explicit-any": {
"count": 9
@ -5757,11 +5752,6 @@
"count": 1
}
},
"web/app/components/workflow/variable-inspect/display-content.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"web/app/components/workflow/variable-inspect/group.tsx": {
"typescript/no-explicit-any": {
"count": 2

View File

@ -43,6 +43,22 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
The canonical boundary exported from a primitive subpath uses the primitive name without a `Root` suffix, and its public types follow the same name (`Select` / `SelectProps`, `Drawer` / `DrawerProps`). Keep `Root` only when the subpath exposes both a low-level anatomy root and a higher-level convenience component, such as `CheckboxRoot` / `Checkbox` or `PaginationRoot` / `Pagination`. Implementation code should continue to reference the upstream Base UI anatomy explicitly through names such as `BaseSelect.Root.Props`.
### Public type contracts
Every runtime component exported from a primitive subpath must have an accurate, importable props type with the matching name (`DialogContent` / `DialogContentProps`). Define Dify-authored composite props at the Dify UI boundary; use direct aliases for unchanged Base UI parts instead of copying their shapes.
Treat each `src/<primitive>/index.tsx` as an explicit public API boundary. Keep component, factory, hook, and type declarations module-local, then publish the complete surface through separate `export { ... }` and `export type { ... }` manifests at the bottom of the file. Do not mix scattered inline exports with the manifest or use a wildcard export; omission from the manifest keeps implementation helpers private.
Preserve generic relationships end to end. Generic public components and their props must carry the same caller-owned type parameters, including picker `Value` / `Multiple`, form values, radio values, slider values, and overlay payloads and handles. Never erase those relationships with `any` or a hard-coded `string`; use `unknown` only as the safe default for an independently consumed anatomy part whose value cannot be inferred from its parent through JSX.
Do not add a root-only generic when independently rendered JSX anatomy can produce values outside that root's inferred type. Preserve the upstream contract until the complete component family can enforce one value type; otherwise the generic gives callbacks a narrower type than the runtime can guarantee. `Tabs` intentionally follows Base UI's value contract for this reason.
Keep the public type surface smaller than the upstream Base UI namespace. A type is not public merely because Base UI provides a name for it, because Dify gives an internal contract a descriptive alias, or because an earlier implementation happened to export it. In addition to matching component props, export a type only when it pairs with a public factory or has a concrete workspace consumer that cannot express the contract clearly through the matching props type. Remove legacy aliases that have no matching runtime API or real consumer; package-local tests and stories can derive narrow values from canonical props instead of preserving an otherwise unused export. Dify-authored options, states, and controlled/uncontrolled branches stay private when the matching props type already expresses them.
State, event details and reasons, actions, and controlled/uncontrolled composition helpers are private by default. Public props already provide contextual typing for inline render and event callbacks. Export one of these narrower contracts only when Dify defines the state or event itself, a public factory requires the named type, or an external consumer needs to name it independently. Apply the same rule to Dify-authored components such as FileTree, Pagination, ProgressCircle, StatusDot, and Toast; being implemented locally does not justify a broader API.
Keep implementation-only render helpers, context values, styling helpers, and upstream passthrough aliases private. 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.
## Primitives
| Category | Subpath | Notes |
@ -107,7 +123,7 @@ Use `Fieldset` and `FieldsetLegend` when one field is represented by a group of
Selection primitives should preserve the caller's domain value type instead of widening values to `string`. Use `Select<Value, Multiple>`, `RadioGroup<Value>`, `Radio<Value>`, and `RadioItem<Value>` when the selected value is an enum, union, boolean, number, object, or nullable placeholder value.
Root-level generics type `value`, `defaultValue`, `onValueChange`, and collection props such as `items`, but JSX children do not automatically inherit the parent generic. For non-string radio groups, type the child item too:
Root-level generics type `value`, `defaultValue`, and value-dependent callbacks such as `onValueChange`. Upstream collection props such as `items` can still be broader than the root value type, and JSX children do not automatically inherit the parent generic. Type independently consumed anatomy parts when their value cannot be inferred locally; for non-string radio groups, type the child item too:
```tsx
<RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}>
@ -121,7 +137,22 @@ Root-level generics type `value`, `defaultValue`, `onValueChange`, and collectio
Use `Radio` for the default Dify control. Use `RadioItem` when custom UI should be the radio item; place `RadioControl` inside it for the standard visual dot. `RadioControl` is a Dify visual part, not a Base UI anatomy export.
For select labels and display values, prefer the Base UI `items` collection pattern so the root, value display, and item list share the same typed values. Avoid helpers that stringify values only to recover labels later; convert values to strings only at real boundaries such as form submission, URL/search params, or legacy APIs that require strings.
For `Select` and `Combobox`, a literal multiple-value contract must match the runtime mode: `<Combobox<Subject, true> multiple>`. Their value display can still receive `null` before a value is selected, including in multiple mode. Because JSX does not pass the root's generic to its children, repeat the domain type on independently consumed render anatomy instead of annotating callback parameters:
```tsx
<Combobox<Subject, true> multiple value={subjects} onValueChange={setSubjects}>
<ComboboxValue<Subject, true>>
{(selectedSubjects) => selectedSubjects?.map((subject) => subject.name).join(', ') ?? 'Anyone'}
</ComboboxValue>
<ComboboxList<Subject>>
{(subject) => <ComboboxItem value={subject}>{subject.name}</ComboboxItem>}
</ComboboxList>
</Combobox>
```
`AutocompleteList` follows the same rule. A `ComboboxGroup` or `AutocompleteGroup` can infer its local value type from `items`; a nested `Collection` is a separate JSX boundary and should use `<Value>` when its render callback needs the domain type. Dynamic `multiple={condition}` remains supported and gives callbacks the corresponding single-or-multiple union.
For select labels and display values, prefer the Base UI `items` collection pattern so the root, value display, and item list share one runtime source of truth for values and labels. Avoid helpers that stringify values only to recover labels later; convert values to strings only at real boundaries such as form submission, URL/search params, or legacy APIs that require strings.
`CheckboxGroup` follows the Base UI contract and uses `string[]`. Do not add a generic checkbox-group wrapper unless the underlying primitive contract changes; if different business IDs need stronger separation, model that at the feature/domain type boundary.

View File

@ -7,10 +7,15 @@ import { Button } from '../button'
import { cn } from '../cn'
import { modalBackdropClassName, modalPopupAnimationClassName } from '../overlay-shared'
export const AlertDialog = BaseAlertDialog.Root
export const AlertDialogTrigger = BaseAlertDialog.Trigger
export const AlertDialogTitle = BaseAlertDialog.Title
export const AlertDialogDescription = BaseAlertDialog.Description
const AlertDialog = BaseAlertDialog.Root
const AlertDialogTrigger = BaseAlertDialog.Trigger
const AlertDialogTitle = BaseAlertDialog.Title
const AlertDialogDescription = BaseAlertDialog.Description
type AlertDialogProps<Payload = unknown> = BaseAlertDialog.Root.Props<Payload>
type AlertDialogTriggerProps<Payload = unknown> = BaseAlertDialog.Trigger.Props<Payload>
type AlertDialogTitleProps = BaseAlertDialog.Title.Props
type AlertDialogDescriptionProps = BaseAlertDialog.Description.Props
type AlertDialogContentProps = {
children: React.ReactNode
@ -19,7 +24,7 @@ type AlertDialogContentProps = {
backdropProps?: Omit<BaseAlertDialog.Backdrop.Props, 'className'>
}
export function AlertDialogContent({
function AlertDialogContent({
children,
className,
backdropClassName,
@ -46,7 +51,7 @@ export function AlertDialogContent({
type AlertDialogActionsProps = React.ComponentProps<'div'>
export function AlertDialogActions({ className, ...props }: AlertDialogActionsProps) {
function AlertDialogActions({ className, ...props }: AlertDialogActionsProps) {
return (
<div
className={cn('flex items-start justify-end gap-2 self-stretch p-6', className)}
@ -60,7 +65,7 @@ type AlertDialogCancelButtonProps = Omit<ButtonProps, 'children'> & {
closeProps?: Omit<BaseAlertDialog.Close.Props, 'children' | 'render'>
}
export function AlertDialogCancelButton({
function AlertDialogCancelButton({
children,
closeProps,
...buttonProps
@ -74,10 +79,32 @@ export function AlertDialogCancelButton({
type AlertDialogConfirmButtonProps = ButtonProps
export function AlertDialogConfirmButton({
function AlertDialogConfirmButton({
variant = 'primary',
tone = 'destructive',
...props
}: AlertDialogConfirmButtonProps) {
return <Button variant={variant} tone={tone} {...props} />
}
export {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
AlertDialogTrigger,
}
export type {
AlertDialogActionsProps,
AlertDialogCancelButtonProps,
AlertDialogConfirmButtonProps,
AlertDialogContentProps,
AlertDialogDescriptionProps,
AlertDialogProps,
AlertDialogTitleProps,
AlertDialogTriggerProps,
}

View File

@ -245,8 +245,8 @@ describe('Autocomplete wrappers', () => {
<AutocompleteInput aria-label="Search resources" />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: string) => (
<AutocompleteList<string>>
{(item) => (
<AutocompleteItem key={item} value={item}>
<AutocompleteItemText>{item}</AutocompleteItemText>
</AutocompleteItem>

View File

@ -370,8 +370,8 @@ const BasicTagAutocomplete = ({ size = 'medium' }: { size?: 'small' | 'medium' |
<AutocompleteTrigger size={size} />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: Suggestion) => <TagSuggestionItem key={item.value} item={item} />}
<AutocompleteList<Suggestion>>
{(item) => <TagSuggestionItem key={item.value} item={item} />}
</AutocompleteList>
<AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty>
</AutocompleteContent>
@ -387,8 +387,8 @@ const GroupedSuggestionList = () => {
<AutocompleteGroup key={group.label} items={group.items}>
{groupIndex > 0 && <AutocompleteSeparator />}
<AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(item: Suggestion) => <SuggestionItem key={item.value} item={item} />}
<AutocompleteCollection<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} />}
</AutocompleteCollection>
</AutocompleteGroup>
))}
@ -408,8 +408,8 @@ const CommandPaletteList = () => {
>
{group.label}
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(item: Suggestion) => (
<AutocompleteCollection<Suggestion>>
{(item) => (
<AutocompleteItem key={item.value} value={item} className="grid grid-cols-[1fr_auto]">
<span className="flex min-w-0 items-center gap-2">
{item.icon && (
@ -516,8 +516,8 @@ const AsyncSearchDemo = () => {
popupProps={{ 'aria-busy': isPending || undefined }}
>
<AutocompleteStatus>{status}</AutocompleteStatus>
<AutocompleteList>
{(item: Suggestion) => <SuggestionItem key={item.value} item={item} />}
<AutocompleteList<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} />}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
@ -643,8 +643,8 @@ const FuzzyMatchingDemo = () => {
<AutocompleteTrigger />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: Suggestion) => (
<AutocompleteList<Suggestion>>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.icon && (
<span
@ -730,8 +730,8 @@ export const InlineAutocomplete: Story = {
<AutocompleteTrigger />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: Suggestion) => <SuggestionItem key={item.value} item={item} dense />}
<AutocompleteList<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} dense />}
</AutocompleteList>
<AutocompleteEmpty>No inline completion. Continue typing freely.</AutocompleteEmpty>
</AutocompleteContent>
@ -800,8 +800,8 @@ export const LimitResults: Story = {
<AutocompleteStatus className="border-b border-divider-subtle">
<LimitedStatus total={workflowSuggestions.length} />
</AutocompleteStatus>
<AutocompleteList>
{(item: Suggestion) => <SuggestionItem key={item.value} item={item} />}
<AutocompleteList<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} />}
</AutocompleteList>
<AutocompleteEmpty>No suggestion. Submit the typed text instead.</AutocompleteEmpty>
</AutocompleteContent>
@ -908,8 +908,8 @@ export const Empty: Story = {
<AutocompleteTrigger />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: Suggestion) => <TagSuggestionItem key={item.value} item={item} />}
<AutocompleteList<Suggestion>>
{(item) => <TagSuggestionItem key={item.value} item={item} />}
</AutocompleteList>
<AutocompleteEmpty>No tag suggestion. The custom text remains valid.</AutocompleteEmpty>
</AutocompleteContent>
@ -934,8 +934,8 @@ export const DisabledAndReadOnly: Story = {
<AutocompleteTrigger />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: Suggestion) => <TagSuggestionItem key={item.value} item={item} />}
<AutocompleteList<Suggestion>>
{(item) => <TagSuggestionItem key={item.value} item={item} />}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
@ -952,8 +952,8 @@ export const DisabledAndReadOnly: Story = {
<AutocompleteTrigger />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteList>
{(item: Suggestion) => <SuggestionItem key={item.value} item={item} />}
<AutocompleteList<Suggestion>>
{(item) => <SuggestionItem key={item.value} item={item} />}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>

View File

@ -15,36 +15,52 @@ import {
} from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
export type AutocompleteProps<ItemValue> = BaseAutocomplete.Root.Props<ItemValue>
export type AutocompleteGroupedProps<Items extends readonly { items: readonly unknown[] }[]> = Omit<
type AutocompleteProps<ItemValue> = BaseAutocomplete.Root.Props<ItemValue>
type AutocompleteChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails
type AutocompleteGroupedProps<Items extends readonly { items: readonly unknown[] }[]> = Omit<
AutocompleteProps<Items[number]['items'][number]>,
'items'
> & {
items: Items
}
export type AutocompleteFlatProps<ItemValue> = Omit<AutocompleteProps<ItemValue>, 'items'> & {
type AutocompleteFlatProps<ItemValue> = Omit<AutocompleteProps<ItemValue>, 'items'> & {
items?: readonly ItemValue[]
}
export function Autocomplete<Items extends readonly { items: readonly unknown[] }[]>(
function Autocomplete<Items extends readonly { items: readonly unknown[] }[]>(
props: AutocompleteGroupedProps<Items>,
): React.JSX.Element
export function Autocomplete<ItemValue>(props: AutocompleteFlatProps<ItemValue>): React.JSX.Element
export function Autocomplete(props: AutocompleteProps<unknown>): React.JSX.Element {
function Autocomplete<ItemValue>(props: AutocompleteFlatProps<ItemValue>): React.JSX.Element
function Autocomplete(props: AutocompleteProps<unknown>): React.JSX.Element {
return <BaseAutocomplete.Root {...props} />
}
export const AutocompleteValue = BaseAutocomplete.Value
export const AutocompleteGroup = BaseAutocomplete.Group
export const AutocompleteCollection = BaseAutocomplete.Collection
export const AutocompleteRow = BaseAutocomplete.Row
export const useAutocompleteFilter = BaseAutocomplete.useFilter
export const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems
const AutocompleteValue = BaseAutocomplete.Value
const AutocompleteRow = BaseAutocomplete.Row
const useAutocompleteFilter = BaseAutocomplete.useFilter
const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems
export type AutocompleteChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails
export type AutocompleteHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails
type AutocompleteValueProps = BaseAutocomplete.Value.Props
type AutocompleteRowProps = BaseAutocomplete.Row.Props
type AutocompleteGroupProps<Value = unknown> = Omit<BaseAutocomplete.Group.Props, 'items'> & {
items?: readonly Value[]
}
function AutocompleteGroup<Value = unknown>(props: AutocompleteGroupProps<Value>) {
return <BaseAutocomplete.Group {...props} />
}
type AutocompleteCollectionProps<Value = unknown> = Omit<
BaseAutocomplete.Collection.Props,
'children'
> & {
children: (item: Value, index: number) => React.ReactNode
}
function AutocompleteCollection<Value = unknown>(props: AutocompleteCollectionProps<Value>) {
return <BaseAutocomplete.Collection {...props} />
}
const autocompletePopupClassName = [
'w-(--anchor-width) max-w-[min(28rem,var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
@ -88,14 +104,10 @@ const autocompleteInputGroupVariants = cva(
},
)
export type AutocompleteSize = NonNullable<
VariantProps<typeof autocompleteInputGroupVariants>['size']
>
type AutocompleteInputGroupProps = Omit<BaseAutocomplete.InputGroup.Props, 'className'> &
VariantProps<typeof autocompleteInputGroupVariants> & { className?: string }
export type AutocompleteInputGroupProps = BaseAutocomplete.InputGroup.Props &
VariantProps<typeof autocompleteInputGroupVariants>
export function AutocompleteInputGroup({
function AutocompleteInputGroup({
className,
size = 'medium',
...props
@ -129,10 +141,10 @@ const autocompleteInputVariants = cva(
},
)
export type AutocompleteInputProps = Omit<BaseAutocomplete.Input.Props, 'size'> &
VariantProps<typeof autocompleteInputVariants>
type AutocompleteInputProps = Omit<BaseAutocomplete.Input.Props, 'className' | 'size'> &
VariantProps<typeof autocompleteInputVariants> & { className?: string }
export function AutocompleteInput({
function AutocompleteInput({
className,
size = 'medium',
type = 'text',
@ -173,16 +185,16 @@ const autocompleteControlVariants = cva(
},
)
export type AutocompleteControlProps = Omit<BaseAutocomplete.Trigger.Props, 'className'> &
type AutocompleteTriggerProps = Omit<BaseAutocomplete.Trigger.Props, 'className'> &
VariantProps<typeof autocompleteControlVariants> & { className?: string }
export function AutocompleteTrigger({
function AutocompleteTrigger({
className,
children,
size = 'medium',
type = 'button',
...props
}: AutocompleteControlProps) {
}: AutocompleteTriggerProps) {
return (
<BaseAutocomplete.Trigger
type={type}
@ -198,10 +210,10 @@ export function AutocompleteTrigger({
)
}
export type AutocompleteClearProps = Omit<BaseAutocomplete.Clear.Props, 'className'> &
type AutocompleteClearProps = Omit<BaseAutocomplete.Clear.Props, 'className'> &
VariantProps<typeof autocompleteControlVariants> & { className?: string }
export function AutocompleteClear({
function AutocompleteClear({
className,
children,
size = 'medium',
@ -226,7 +238,11 @@ export function AutocompleteClear({
)
}
export function AutocompleteIcon({ className, children, ...props }: BaseAutocomplete.Icon.Props) {
type AutocompleteIconProps = Omit<BaseAutocomplete.Icon.Props, 'className'> & {
className?: string
}
function AutocompleteIcon({ className, children, ...props }: AutocompleteIconProps) {
return (
<BaseAutocomplete.Icon
className={cn('flex shrink-0 items-center text-text-tertiary', className)}
@ -252,7 +268,7 @@ type AutocompleteContentProps = {
popupProps?: Omit<BaseAutocomplete.Popup.Props, 'children' | 'className'>
}
export function AutocompleteContent({
function AutocompleteContent({
children,
placement = 'bottom-start',
sideOffset = 4,
@ -290,23 +306,43 @@ export function AutocompleteContent({
)
}
export function AutocompleteList({ className, ...props }: BaseAutocomplete.List.Props) {
type AutocompleteListProps<Value = unknown> = Omit<
BaseAutocomplete.List.Props,
'children' | 'className'
> & {
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} />
}
export function AutocompleteItem({ className, ...props }: BaseAutocomplete.Item.Props) {
type AutocompleteItemProps<Value = unknown> = Omit<
BaseAutocomplete.Item.Props,
'className' | 'value'
> & {
className?: string
value?: Value
}
function AutocompleteItem<Value = unknown>({ className, ...props }: AutocompleteItemProps<Value>) {
return <BaseAutocomplete.Item className={cn(autocompleteItemClassName, className)} {...props} />
}
export type AutocompleteItemTextProps = React.ComponentProps<'span'>
type AutocompleteItemTextProps = React.ComponentProps<'span'>
export function AutocompleteItemText({ className, ...props }: AutocompleteItemTextProps) {
function AutocompleteItemText({ className, ...props }: AutocompleteItemTextProps) {
return (
<span className={cn('min-w-0 grow truncate px-1 system-sm-medium', className)} {...props} />
)
}
export function AutocompleteGroupLabel({ className, ...props }: BaseAutocomplete.GroupLabel.Props) {
type AutocompleteGroupLabelProps = Omit<BaseAutocomplete.GroupLabel.Props, 'className'> & {
className?: string
}
function AutocompleteGroupLabel({ className, ...props }: AutocompleteGroupLabelProps) {
return (
<BaseAutocomplete.GroupLabel
className={cn(floatingGroupLabelClassName, className)}
@ -315,13 +351,21 @@ export function AutocompleteGroupLabel({ className, ...props }: BaseAutocomplete
)
}
export function AutocompleteSeparator({ className, ...props }: BaseAutocomplete.Separator.Props) {
type AutocompleteSeparatorProps = Omit<BaseAutocomplete.Separator.Props, 'className'> & {
className?: string
}
function AutocompleteSeparator({ className, ...props }: AutocompleteSeparatorProps) {
return (
<BaseAutocomplete.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
)
}
export function AutocompleteEmpty({ className, ...props }: BaseAutocomplete.Empty.Props) {
type AutocompleteEmptyProps = Omit<BaseAutocomplete.Empty.Props, 'className'> & {
className?: string
}
function AutocompleteEmpty({ className, ...props }: AutocompleteEmptyProps) {
return (
<BaseAutocomplete.Empty
className={cn(
@ -333,7 +377,11 @@ export function AutocompleteEmpty({ className, ...props }: BaseAutocomplete.Empt
)
}
export function AutocompleteStatus({ className, ...props }: BaseAutocomplete.Status.Props) {
type AutocompleteStatusProps = Omit<BaseAutocomplete.Status.Props, 'className'> & {
className?: string
}
function AutocompleteStatus({ className, ...props }: AutocompleteStatusProps) {
return (
<BaseAutocomplete.Status
className={cn('px-3 py-2 system-sm-regular text-text-tertiary', className)}
@ -342,14 +390,65 @@ export function AutocompleteStatus({ className, ...props }: BaseAutocomplete.Sta
)
}
export function AutocompleteItemIndicator({
function AutocompleteItemIndicator({
className,
children,
...props
}: React.ComponentProps<'span'>) {
}: AutocompleteItemIndicatorProps) {
return (
<span className={cn(floatingItemIndicatorClassName, className)} {...props}>
{children ?? <span className="i-ri-arrow-right-line size-4" aria-hidden="true" />}
</span>
)
}
type AutocompleteItemIndicatorProps = React.ComponentProps<'span'>
export {
Autocomplete,
AutocompleteClear,
AutocompleteCollection,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteIcon,
AutocompleteInput,
AutocompleteInputGroup,
AutocompleteItem,
AutocompleteItemIndicator,
AutocompleteItemText,
AutocompleteList,
AutocompleteRow,
AutocompleteSeparator,
AutocompleteStatus,
AutocompleteTrigger,
AutocompleteValue,
useAutocompleteFilter,
useAutocompleteFilteredItems,
}
export type {
AutocompleteChangeEventDetails,
AutocompleteClearProps,
AutocompleteCollectionProps,
AutocompleteContentProps,
AutocompleteEmptyProps,
AutocompleteFlatProps,
AutocompleteGroupedProps,
AutocompleteGroupLabelProps,
AutocompleteGroupProps,
AutocompleteIconProps,
AutocompleteInputGroupProps,
AutocompleteInputProps,
AutocompleteItemIndicatorProps,
AutocompleteItemProps,
AutocompleteItemTextProps,
AutocompleteListProps,
AutocompleteProps,
AutocompleteRowProps,
AutocompleteSeparatorProps,
AutocompleteStatusProps,
AutocompleteTriggerProps,
AutocompleteValueProps,
}

View File

@ -15,9 +15,9 @@ const avatarSizeClasses = {
'3xl': { root: 'size-16', text: 'text-2xl' },
} as const
export type AvatarSize = keyof typeof avatarSizeClasses
type AvatarSize = keyof typeof avatarSizeClasses
export type AvatarProps = {
type AvatarProps = {
name: string
avatar: string | null
size?: AvatarSize
@ -25,11 +25,11 @@ export type AvatarProps = {
onLoadingStatusChange?: (status: ImageLoadingStatus) => void
}
type AvatarRootProps = BaseAvatar.Root.Props & {
type AvatarRootProps = Omit<BaseAvatar.Root.Props, 'className'> & {
size?: AvatarSize
className?: string
}
export function AvatarRoot({ size = 'md', className, ...props }: AvatarRootProps) {
function AvatarRoot({ size = 'md', className, ...props }: AvatarRootProps) {
return (
<BaseAvatar.Root
className={cn(
@ -42,11 +42,11 @@ export function AvatarRoot({ size = 'md', className, ...props }: AvatarRootProps
)
}
type AvatarFallbackProps = BaseAvatar.Fallback.Props & {
type AvatarFallbackProps = Omit<BaseAvatar.Fallback.Props, 'className'> & {
size?: AvatarSize
className?: string
}
export function AvatarFallback({ size = 'md', className, ...props }: AvatarFallbackProps) {
function AvatarFallback({ size = 'md', className, ...props }: AvatarFallbackProps) {
return (
<BaseAvatar.Fallback
className={cn(
@ -59,9 +59,10 @@ export function AvatarFallback({ size = 'md', className, ...props }: AvatarFallb
)
}
type AvatarImageProps = BaseAvatar.Image.Props
export function AvatarImage({ className, ...props }: AvatarImageProps) {
type AvatarImageProps = Omit<BaseAvatar.Image.Props, 'className'> & {
className?: string
}
function AvatarImage({ className, ...props }: AvatarImageProps) {
return (
<BaseAvatar.Image
className={cn('absolute inset-0 size-full object-cover', className)}
@ -70,13 +71,7 @@ export function AvatarImage({ className, ...props }: AvatarImageProps) {
)
}
export const Avatar = ({
name,
avatar,
size = 'md',
className,
onLoadingStatusChange,
}: AvatarProps) => {
const Avatar = ({ name, avatar, size = 'md', className, onLoadingStatusChange }: AvatarProps) => {
return (
<AvatarRoot size={size} className={className}>
{avatar && (
@ -86,3 +81,7 @@ export const Avatar = ({
</AvatarRoot>
)
}
export { Avatar, AvatarFallback, AvatarImage, AvatarRoot }
export type { AvatarFallbackProps, AvatarImageProps, AvatarProps, AvatarRootProps, AvatarSize }

View File

@ -6,7 +6,7 @@ import { Button as BaseButton } from '@base-ui/react/button'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
export const buttonVariants = cva(
const buttonVariants = cva(
'inline-flex cursor-pointer items-center justify-center whitespace-nowrap outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid data-disabled:cursor-not-allowed',
{
variants: {
@ -100,13 +100,13 @@ export const buttonVariants = cva(
},
)
export type ButtonProps = Omit<BaseButtonNS.Props, 'className'> &
type ButtonProps = Omit<BaseButtonNS.Props, 'className'> &
VariantProps<typeof buttonVariants> & {
loading?: boolean
className?: string
}
export function Button({
function Button({
className,
variant,
size,
@ -136,3 +136,7 @@ export function Button({
</BaseButton>
)
}
export { Button, buttonVariants }
export type { ButtonProps }

View File

@ -3,8 +3,12 @@
import type { CheckboxGroup as BaseCheckboxGroupNS } from '@base-ui/react/checkbox-group'
import { CheckboxGroup as BaseCheckboxGroup } from '@base-ui/react/checkbox-group'
export type CheckboxGroupProps = BaseCheckboxGroupNS.Props
type CheckboxGroupProps = BaseCheckboxGroupNS.Props
export function CheckboxGroup(props: CheckboxGroupProps) {
function CheckboxGroup(props: CheckboxGroupProps) {
return <BaseCheckboxGroup {...props} />
}
export { CheckboxGroup }
export type { CheckboxGroupProps }

View File

@ -25,22 +25,17 @@ const checkboxIndicatorClassName =
const checkboxSkeletonClassName = 'size-4 shrink-0 rounded-sm bg-text-quaternary opacity-20'
export type CheckboxRootProps = Omit<BaseCheckboxNS.Root.Props, 'className'> & {
type CheckboxRootProps = Omit<BaseCheckboxNS.Root.Props, 'className'> & {
className?: string
}
export function CheckboxRoot({ className, ...props }: CheckboxRootProps) {
function CheckboxRoot({ className, ...props }: CheckboxRootProps) {
return <BaseCheckbox.Root className={cn(checkboxRootClassName, className)} {...props} />
}
export type CheckboxIndicatorProps = Omit<
BaseCheckboxNS.Indicator.Props,
'className' | 'children'
> & {
type CheckboxIndicatorProps = Omit<BaseCheckboxNS.Indicator.Props, 'className' | 'children'> & {
className?: string
}
export function CheckboxIndicator({ className, render, ...props }: CheckboxIndicatorProps) {
function CheckboxIndicator({ className, render, ...props }: CheckboxIndicatorProps) {
return (
<BaseCheckbox.Indicator
className={cn(checkboxIndicatorClassName, className)}
@ -61,9 +56,9 @@ export function CheckboxIndicator({ className, render, ...props }: CheckboxIndic
)
}
export type CheckboxProps = Omit<CheckboxRootProps, 'children'>
type CheckboxProps = Omit<CheckboxRootProps, 'children'>
export function Checkbox({ ...props }: CheckboxProps) {
function Checkbox({ ...props }: CheckboxProps) {
return (
<CheckboxRoot {...props}>
<CheckboxIndicator />
@ -71,10 +66,14 @@ export function Checkbox({ ...props }: CheckboxProps) {
)
}
export type CheckboxSkeletonProps = Omit<React.ComponentProps<'div'>, 'className'> & {
type CheckboxSkeletonProps = Omit<React.ComponentProps<'div'>, 'className'> & {
className?: string
}
export function CheckboxSkeleton({ className, ...props }: CheckboxSkeletonProps) {
function CheckboxSkeleton({ className, ...props }: CheckboxSkeletonProps) {
return <div className={cn(checkboxSkeletonClassName, className)} {...props} />
}
export { Checkbox, CheckboxIndicator, CheckboxRoot, CheckboxSkeleton }
export type { CheckboxIndicatorProps, CheckboxProps, CheckboxRootProps, CheckboxSkeletonProps }

View File

@ -2,6 +2,8 @@ import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export { cn }

View File

@ -4,19 +4,17 @@ import type { Collapsible as BaseCollapsibleNS } from '@base-ui/react/collapsibl
import { Collapsible as BaseCollapsible } from '@base-ui/react/collapsible'
import { cn } from '../cn'
export type CollapsibleProps = Omit<BaseCollapsibleNS.Root.Props, 'className'> & {
type CollapsibleProps = Omit<BaseCollapsibleNS.Root.Props, 'className'> & {
className?: string
}
export function Collapsible({ className, ...props }: CollapsibleProps) {
function Collapsible({ className, ...props }: CollapsibleProps) {
return <BaseCollapsible.Root className={cn('flex min-w-0 flex-col', className)} {...props} />
}
export type CollapsibleTriggerProps = Omit<BaseCollapsibleNS.Trigger.Props, 'className'> & {
type CollapsibleTriggerProps = Omit<BaseCollapsibleNS.Trigger.Props, 'className'> & {
className?: string
}
export function CollapsibleTrigger({ className, ...props }: CollapsibleTriggerProps) {
function CollapsibleTrigger({ className, ...props }: CollapsibleTriggerProps) {
return (
<BaseCollapsible.Trigger
className={cn(
@ -32,11 +30,10 @@ export function CollapsibleTrigger({ className, ...props }: CollapsibleTriggerPr
)
}
export type CollapsiblePanelProps = Omit<BaseCollapsibleNS.Panel.Props, 'className'> & {
type CollapsiblePanelProps = Omit<BaseCollapsibleNS.Panel.Props, 'className'> & {
className?: string
}
export function CollapsiblePanel({ className, ...props }: CollapsiblePanelProps) {
function CollapsiblePanel({ className, ...props }: CollapsiblePanelProps) {
return (
<BaseCollapsible.Panel
className={cn(
@ -49,3 +46,7 @@ export function CollapsiblePanel({ className, ...props }: CollapsiblePanelProps)
/>
)
}
export { Collapsible, CollapsiblePanel, CollapsibleTrigger }
export type { CollapsiblePanelProps, CollapsibleProps, CollapsibleTriggerProps }

View File

@ -265,8 +265,8 @@ describe('Combobox wrappers', () => {
<ComboboxInput aria-label="Search resources" />
</ComboboxInputGroup>
<ComboboxContent>
<ComboboxList>
{(item: string) => (
<ComboboxList<string>>
{(item) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemText>{item}</ComboboxItemText>
<ComboboxItemIndicator />
@ -300,15 +300,32 @@ describe('Combobox wrappers', () => {
})
describe('Multiple selection chips', () => {
it('should expose a controlled null value to a typed multiple value renderer', async () => {
const screen = await renderWithSafeViewport(
<Combobox<string, true> multiple value={null}>
<ComboboxInputGroup>
<ComboboxValue<string, true>>
{(selectedValue) =>
selectedValue === null ? 'No reviewers selected' : selectedValue.join(', ')
}
</ComboboxValue>
<ComboboxInput aria-label="Reviewers" />
</ComboboxInputGroup>
</Combobox>,
)
await expect.element(screen.getByText('No reviewers selected')).toBeInTheDocument()
})
it('should render chip wrappers and default remove button label', async () => {
const screen = await renderWithSafeViewport(
<Combobox multiple defaultValue={['maya']} items={['maya', 'nora']}>
<ComboboxInputGroup>
<ComboboxChips className="custom-chips" data-testid="chips">
<ComboboxValue>
{(selectedValue: string[]) => (
<ComboboxValue<string, true>>
{(selectedValue) => (
<React.Fragment>
{selectedValue.map((item) => (
{selectedValue?.map((item) => (
<ComboboxChip key={item} className="custom-chip">
<span>{item}</span>
<ComboboxChipRemove data-testid="remove-chip" />
@ -337,10 +354,10 @@ describe('Combobox wrappers', () => {
<Combobox multiple defaultValue={['maya']} items={['maya']}>
<ComboboxInputGroup>
<ComboboxChips>
<ComboboxValue>
{(selectedValue: string[]) => (
<ComboboxValue<string, true>>
{(selectedValue) => (
<React.Fragment>
{selectedValue.map((item) => (
{selectedValue?.map((item) => (
<ComboboxChip key={item}>
<span id="remove-maya">Remove Maya</span>
<ComboboxChipRemove aria-labelledby="remove-maya" />

View File

@ -362,7 +362,7 @@ const GroupedToolList = () => {
<ComboboxGroup key={group.label} items={group.items}>
{groupIndex > 0 && <ComboboxSeparator />}
<ComboboxGroupLabel>{group.label}</ComboboxGroupLabel>
<ComboboxCollection>{(option: Option) => renderOptionItem(option)}</ComboboxCollection>
<ComboboxCollection<Option>>{(option) => renderOptionItem(option)}</ComboboxCollection>
</ComboboxGroup>
))}
</ComboboxList>
@ -560,7 +560,7 @@ const AsyncDirectoryDemo = () => {
popupProps={{ 'aria-busy': isPending || undefined }}
>
<ComboboxStatus className="border-b border-divider-subtle">{status}</ComboboxStatus>
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
<ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
</ComboboxContent>
</Combobox>
@ -671,17 +671,17 @@ const AsyncReviewerDemo = () => {
>
<ComboboxInputGroup className="h-auto min-h-8 items-start py-1">
<ComboboxChips>
<ComboboxValue>
{(selectedValue: Option[]) => (
<ComboboxValue<Option, true>>
{(selectedValue) => (
<React.Fragment>
{selectedValue.map((item) => (
{selectedValue?.map((item) => (
<ComboboxChip key={item.value} aria-label={item.label}>
<span className="max-w-32 truncate">{item.label}</span>
<ComboboxChipRemove aria-label={`Remove ${item.label}`} />
</ComboboxChip>
))}
<ComboboxInput
placeholder={selectedValue.length ? '' : 'Search reviewers…'}
placeholder={selectedValue?.length ? '' : 'Search reviewers…'}
className="min-w-24 px-1 py-0.5"
/>
</React.Fragment>
@ -694,7 +694,7 @@ const AsyncReviewerDemo = () => {
popupProps={{ 'aria-busy': isPending || undefined }}
>
<ComboboxStatus className="border-b border-divider-subtle">{status}</ComboboxStatus>
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
<ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
</ComboboxContent>
</Combobox>
@ -741,7 +741,7 @@ export const Default: Story = {
<ComboboxInputTrigger className="mr-0" />
</ComboboxInputGroup>
<ComboboxContent>
<ComboboxList>{renderSimpleOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderSimpleOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
@ -766,7 +766,7 @@ export const FormField: Story = {
<ComboboxInputTrigger className="mr-0" />
</ComboboxInputGroup>
<ComboboxContent>
<ComboboxList>{renderSimpleOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderSimpleOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
<FieldDescription>Type to filter, then choose a remembered data source.</FieldDescription>
@ -784,7 +784,7 @@ export const CompactTriggerWithPopupSearch: Story = {
</ComboboxTrigger>
<ComboboxContent>
<PopupSearchInput label="Search data sources" placeholder="Search sources" />
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
@ -816,7 +816,7 @@ export const Sizes: Story = {
<ComboboxInputTrigger size={size} className="mr-0" />
</ComboboxInputGroup>
<ComboboxContent>
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
@ -851,17 +851,17 @@ const MultipleChipsDemo = () => {
<Combobox items={reviewerOptions} multiple value={value} onValueChange={setValue}>
<ComboboxInputGroup className="h-auto min-h-8 items-start py-1">
<ComboboxChips>
<ComboboxValue>
{(selectedValue: Option[]) => (
<ComboboxValue<Option, true>>
{(selectedValue) => (
<React.Fragment>
{selectedValue.map((item) => (
{selectedValue?.map((item) => (
<ComboboxChip key={item.value}>
<span className="max-w-32 truncate">{item.label}</span>
<ComboboxChipRemove aria-label={`Remove ${item.label}`} />
</ComboboxChip>
))}
<ComboboxInput
placeholder={selectedValue.length ? '' : 'Assign reviewers…'}
placeholder={selectedValue?.length ? '' : 'Assign reviewers…'}
className="min-w-24 px-1 py-0.5"
/>
</React.Fragment>
@ -870,7 +870,7 @@ const MultipleChipsDemo = () => {
</ComboboxChips>
</ComboboxInputGroup>
<ComboboxContent>
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
<FieldDescription>
@ -917,7 +917,7 @@ export const EmptyAndStatus: Story = {
<ComboboxContent>
<ComboboxStatus>Search workspace connectors</ComboboxStatus>
<ComboboxEmpty>No connectors found</ComboboxEmpty>
<ComboboxList>{renderSimpleOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderSimpleOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
@ -935,7 +935,7 @@ export const DisabledAndReadOnly: Story = {
</ComboboxTrigger>
<ComboboxContent>
<PopupSearchInput label="Search disabled providers" placeholder="Search providers" />
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
@ -951,7 +951,7 @@ export const DisabledAndReadOnly: Story = {
<ComboboxInputTrigger className="mr-0" />
</ComboboxInputGroup>
<ComboboxContent>
<ComboboxList>{renderOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
@ -972,7 +972,7 @@ const ControlledDemo = () => {
</ComboboxTrigger>
<ComboboxContent>
<PopupSearchInput label="Search app tags" placeholder="Search tags" />
<ComboboxList>{renderSimpleOptionItem}</ComboboxList>
<ComboboxList<Option>>{renderSimpleOptionItem}</ComboboxList>
</ComboboxContent>
</Combobox>
</div>

View File

@ -15,28 +15,59 @@ import {
} from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
export type ComboboxProps<
type ComboboxProps<Value, Multiple extends boolean | undefined = false> = BaseCombobox.Root.Props<
Value,
Multiple extends boolean | undefined = false,
> = BaseCombobox.Root.Props<Value, Multiple>
Multiple
> &
([Multiple] extends [true] ? { multiple: true } : unknown)
type ComboboxChangeEventDetails = BaseCombobox.Root.ChangeEventDetails
export function Combobox<Value, Multiple extends boolean | undefined = false>(
function Combobox<Value, Multiple extends boolean | undefined = false>(
props: ComboboxProps<Value, Multiple>,
): React.JSX.Element {
return <BaseCombobox.Root {...props} />
}
export const ComboboxValue = BaseCombobox.Value
export const ComboboxGroup = BaseCombobox.Group
export const ComboboxCollection = BaseCombobox.Collection
export const ComboboxRow = BaseCombobox.Row
export const useComboboxFilter = BaseCombobox.useFilter
export const useComboboxFilteredItems = BaseCombobox.useFilteredItems
const ComboboxRow = BaseCombobox.Row
const useComboboxFilter = BaseCombobox.useFilter
const useComboboxFilteredItems = BaseCombobox.useFilteredItems
export type ComboboxChangeEventDetails = BaseCombobox.Root.ChangeEventDetails
export type ComboboxHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails
type ComboboxSelectedValue<Value, Multiple extends boolean | undefined = false> =
| (Multiple extends true ? Value[] : Value)
| null
type ComboboxValueProps<Value = unknown, Multiple extends boolean | undefined = false> = Omit<
BaseCombobox.Value.Props,
'children'
> & {
children?:
| React.ReactNode
| ((selectedValue: ComboboxSelectedValue<Value, Multiple>) => React.ReactNode)
}
function ComboboxValue<Value = unknown, Multiple extends boolean | undefined = false>(
props: ComboboxValueProps<Value, Multiple>,
): React.JSX.Element
function ComboboxValue(props: BaseCombobox.Value.Props): React.JSX.Element {
return <BaseCombobox.Value {...props} />
}
type ComboboxGroupProps<Value = unknown> = Omit<BaseCombobox.Group.Props, 'items'> & {
items?: readonly Value[]
}
function ComboboxGroup<Value = unknown>(props: ComboboxGroupProps<Value>) {
return <BaseCombobox.Group {...props} />
}
type ComboboxCollectionProps<Value = unknown> = Omit<BaseCombobox.Collection.Props, 'children'> & {
children: (item: Value, index: number) => React.ReactNode
}
function ComboboxCollection<Value = unknown>(props: ComboboxCollectionProps<Value>) {
return <BaseCombobox.Collection {...props} />
}
type ComboboxRowProps = BaseCombobox.Row.Props
const comboboxPopupClassName = [
'w-(--anchor-width) max-w-[min(28rem,var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
@ -81,15 +112,13 @@ const comboboxTriggerVariants = cva(
},
)
export type ComboboxSize = NonNullable<VariantProps<typeof comboboxTriggerVariants>['size']>
type ComboboxTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
VariantProps<typeof comboboxTriggerVariants> & {
className?: string
icon?: React.ReactNode | false
}
export function ComboboxTrigger({
function ComboboxTrigger({
className,
children,
icon,
@ -139,14 +168,10 @@ const comboboxInputGroupVariants = cva(
},
)
export type ComboboxInputGroupProps = BaseCombobox.InputGroup.Props &
VariantProps<typeof comboboxInputGroupVariants>
type ComboboxInputGroupProps = Omit<BaseCombobox.InputGroup.Props, 'className'> &
VariantProps<typeof comboboxInputGroupVariants> & { className?: string }
export function ComboboxInputGroup({
className,
size = 'medium',
...props
}: ComboboxInputGroupProps) {
function ComboboxInputGroup({ className, size = 'medium', ...props }: ComboboxInputGroupProps) {
return (
<BaseCombobox.InputGroup
className={cn(comboboxInputGroupVariants({ size }), className)}
@ -176,10 +201,10 @@ const comboboxInputVariants = cva(
},
)
export type ComboboxInputProps = Omit<BaseCombobox.Input.Props, 'size'> &
VariantProps<typeof comboboxInputVariants>
type ComboboxInputProps = Omit<BaseCombobox.Input.Props, 'className' | 'size'> &
VariantProps<typeof comboboxInputVariants> & { className?: string }
export function ComboboxInput({
function ComboboxInput({
className,
size = 'medium',
type = 'text',
@ -220,10 +245,10 @@ const comboboxControlVariants = cva(
},
)
export type ComboboxClearProps = Omit<BaseCombobox.Clear.Props, 'className'> &
type ComboboxClearProps = Omit<BaseCombobox.Clear.Props, 'className'> &
VariantProps<typeof comboboxControlVariants> & { className?: string }
export function ComboboxClear({
function ComboboxClear({
className,
children,
size = 'medium',
@ -246,10 +271,10 @@ export function ComboboxClear({
)
}
export type ComboboxInputTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
type ComboboxInputTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
VariantProps<typeof comboboxControlVariants> & { className?: string }
export function ComboboxInputTrigger({
function ComboboxInputTrigger({
className,
children,
size = 'medium',
@ -270,7 +295,11 @@ export function ComboboxInputTrigger({
)
}
export function ComboboxIcon({ className, children, ...props }: BaseCombobox.Icon.Props) {
type ComboboxIconProps = Omit<BaseCombobox.Icon.Props, 'className'> & {
className?: string
}
function ComboboxIcon({ className, children, ...props }: ComboboxIconProps) {
return (
<BaseCombobox.Icon
className={cn('flex shrink-0 items-center text-text-tertiary', className)}
@ -296,7 +325,7 @@ type ComboboxContentProps = {
popupProps?: Omit<BaseCombobox.Popup.Props, 'children' | 'className'>
}
export function ComboboxContent({
function ComboboxContent({
children,
placement = 'bottom-start',
sideOffset = 4,
@ -330,27 +359,36 @@ export function ComboboxContent({
)
}
export function ComboboxList({ className, ...props }: BaseCombobox.List.Props) {
type ComboboxListProps<Value = unknown> = Omit<
BaseCombobox.List.Props,
'children' | 'className'
> & {
className?: string
children?: React.ReactNode | ((item: Value, index: number) => React.ReactNode)
}
function ComboboxList<Value = unknown>({ className, ...props }: ComboboxListProps<Value>) {
return <BaseCombobox.List className={cn(comboboxListClassName, className)} {...props} />
}
export function ComboboxItem({ className, ...props }: BaseCombobox.Item.Props) {
type ComboboxItemProps<Value = unknown> = Omit<BaseCombobox.Item.Props, 'className' | 'value'> & {
className?: string
value?: Value
}
function ComboboxItem<Value = unknown>({ className, ...props }: ComboboxItemProps<Value>) {
return <BaseCombobox.Item className={cn(comboboxItemClassName, className)} {...props} />
}
export type ComboboxItemTextProps = React.ComponentProps<'span'>
type ComboboxItemTextProps = React.ComponentProps<'span'>
export function ComboboxItemText({ className, ...props }: ComboboxItemTextProps) {
function ComboboxItemText({ className, ...props }: ComboboxItemTextProps) {
return (
<span className={cn('min-w-0 grow truncate px-1 system-sm-medium', className)} {...props} />
)
}
export function ComboboxItemIndicator({
className,
children,
...props
}: Omit<BaseCombobox.ItemIndicator.Props, 'children'> & { children?: React.ReactNode }) {
function ComboboxItemIndicator({ className, children, ...props }: ComboboxItemIndicatorProps) {
return (
<BaseCombobox.ItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
@ -361,21 +399,45 @@ export function ComboboxItemIndicator({
)
}
export function ComboboxLabel({ className, ...props }: BaseCombobox.Label.Props) {
type ComboboxItemIndicatorProps = Omit<
BaseCombobox.ItemIndicator.Props,
'children' | 'className'
> & {
children?: React.ReactNode
className?: string
}
type ComboboxLabelProps = Omit<BaseCombobox.Label.Props, 'className'> & {
className?: string
}
function ComboboxLabel({ className, ...props }: ComboboxLabelProps) {
return <BaseCombobox.Label className={cn(formLabelClassName, className)} {...props} />
}
export function ComboboxGroupLabel({ className, ...props }: BaseCombobox.GroupLabel.Props) {
type ComboboxGroupLabelProps = Omit<BaseCombobox.GroupLabel.Props, 'className'> & {
className?: string
}
function ComboboxGroupLabel({ className, ...props }: ComboboxGroupLabelProps) {
return (
<BaseCombobox.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
)
}
export function ComboboxSeparator({ className, ...props }: BaseCombobox.Separator.Props) {
type ComboboxSeparatorProps = Omit<BaseCombobox.Separator.Props, 'className'> & {
className?: string
}
function ComboboxSeparator({ className, ...props }: ComboboxSeparatorProps) {
return <BaseCombobox.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
}
export function ComboboxEmpty({ className, ...props }: BaseCombobox.Empty.Props) {
type ComboboxEmptyProps = Omit<BaseCombobox.Empty.Props, 'className'> & {
className?: string
}
function ComboboxEmpty({ className, ...props }: ComboboxEmptyProps) {
return (
<BaseCombobox.Empty
className={cn(
@ -387,7 +449,11 @@ export function ComboboxEmpty({ className, ...props }: BaseCombobox.Empty.Props)
)
}
export function ComboboxStatus({ className, ...props }: BaseCombobox.Status.Props) {
type ComboboxStatusProps = Omit<BaseCombobox.Status.Props, 'className'> & {
className?: string
}
function ComboboxStatus({ className, ...props }: ComboboxStatusProps) {
return (
<BaseCombobox.Status
className={cn('px-3 py-2 system-sm-regular text-text-tertiary', className)}
@ -396,7 +462,11 @@ export function ComboboxStatus({ className, ...props }: BaseCombobox.Status.Prop
)
}
export function ComboboxChips({ className, ...props }: BaseCombobox.Chips.Props) {
type ComboboxChipsProps = Omit<BaseCombobox.Chips.Props, 'className'> & {
className?: string
}
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)}
@ -405,7 +475,11 @@ export function ComboboxChips({ className, ...props }: BaseCombobox.Chips.Props)
)
}
export function ComboboxChip({ className, ...props }: BaseCombobox.Chip.Props) {
type ComboboxChipProps = Omit<BaseCombobox.Chip.Props, 'className'> & {
className?: string
}
function ComboboxChip({ className, ...props }: ComboboxChipProps) {
return (
<BaseCombobox.Chip
className={cn(
@ -417,12 +491,12 @@ export function ComboboxChip({ className, ...props }: BaseCombobox.Chip.Props) {
)
}
export function ComboboxChipRemove({
function ComboboxChipRemove({
className,
children,
type = 'button',
...props
}: BaseCombobox.ChipRemove.Props) {
}: ComboboxChipRemoveProps) {
return (
<BaseCombobox.ChipRemove
type={type}
@ -439,3 +513,65 @@ export function ComboboxChipRemove({
</BaseCombobox.ChipRemove>
)
}
type ComboboxChipRemoveProps = Omit<BaseCombobox.ChipRemove.Props, 'className'> & {
className?: string
}
export {
Combobox,
ComboboxChip,
ComboboxChipRemove,
ComboboxChips,
ComboboxClear,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxIcon,
ComboboxInput,
ComboboxInputGroup,
ComboboxInputTrigger,
ComboboxItem,
ComboboxItemIndicator,
ComboboxItemText,
ComboboxLabel,
ComboboxList,
ComboboxRow,
ComboboxSeparator,
ComboboxStatus,
ComboboxTrigger,
ComboboxValue,
useComboboxFilter,
useComboboxFilteredItems,
}
export type {
ComboboxChangeEventDetails,
ComboboxChipProps,
ComboboxChipRemoveProps,
ComboboxChipsProps,
ComboboxClearProps,
ComboboxCollectionProps,
ComboboxContentProps,
ComboboxEmptyProps,
ComboboxGroupLabelProps,
ComboboxGroupProps,
ComboboxIconProps,
ComboboxInputGroupProps,
ComboboxInputProps,
ComboboxInputTriggerProps,
ComboboxItemIndicatorProps,
ComboboxItemProps,
ComboboxItemTextProps,
ComboboxLabelProps,
ComboboxListProps,
ComboboxProps,
ComboboxRowProps,
ComboboxSeparatorProps,
ComboboxStatusProps,
ComboboxTriggerProps,
ComboboxValueProps,
Placement,
}

View File

@ -100,23 +100,25 @@ export const WithGroupLabel: Story = {
),
}
type Density = 'compact' | 'comfortable' | 'spacious'
const WithRadioItemsDemo = () => {
const [value, setValue] = React.useState('comfortable')
const [density, setDensity] = React.useState<Density>('comfortable')
return (
<ContextMenu>
<TriggerArea label={`Right-click to set density: ${value}`} />
<TriggerArea label={`Right-click to set density: ${density}`} />
<ContextMenuContent popupClassName="w-44">
<ContextMenuRadioGroup value={value} onValueChange={setValue}>
<ContextMenuRadioItem value="compact">
<ContextMenuRadioGroup<Density> value={density} onValueChange={setDensity}>
<ContextMenuRadioItem<Density> value="compact">
Compact
<ContextMenuRadioItemIndicator />
</ContextMenuRadioItem>
<ContextMenuRadioItem value="comfortable">
<ContextMenuRadioItem<Density> value="comfortable">
Comfortable
<ContextMenuRadioItemIndicator />
</ContextMenuRadioItem>
<ContextMenuRadioItem value="spacious">
<ContextMenuRadioItem<Density> value="spacious">
Spacious
<ContextMenuRadioItemIndicator />
</ContextMenuRadioItem>

View File

@ -16,16 +16,35 @@ import {
} from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
export const ContextMenu = BaseContextMenu.Root
export const ContextMenuTrigger = BaseContextMenu.Trigger
export const ContextMenuSub = BaseContextMenu.SubmenuRoot
export const ContextMenuGroup = BaseContextMenu.Group
export const ContextMenuRadioGroup = BaseContextMenu.RadioGroup
export type ContextMenuActions = BaseContextMenu.Root.Actions
const ContextMenu = BaseContextMenu.Root
const ContextMenuTrigger = BaseContextMenu.Trigger
const ContextMenuSub = BaseContextMenu.SubmenuRoot
const ContextMenuGroup = BaseContextMenu.Group
type ContextMenuProps = BaseContextMenu.Root.Props
type ContextMenuActions = BaseContextMenu.Root.Actions
type ContextMenuTriggerProps = BaseContextMenu.Trigger.Props
type ContextMenuSubProps = BaseContextMenu.SubmenuRoot.Props
type ContextMenuGroupProps = BaseContextMenu.Group.Props
type ContextMenuRadioGroupProps<Value = unknown> = Omit<
BaseContextMenu.RadioGroup.Props,
'defaultValue' | 'onValueChange' | 'value'
> & {
defaultValue?: Value
onValueChange?: (
value: Value,
eventDetails: BaseContextMenu.RadioGroup.ChangeEventDetails,
) => void
value?: Value
}
type ContextMenuItemVariant = MenuItemVariant
// Intentionally no public Backdrop export; Base UI handles context-menu modal dismissal internally.
function ContextMenuRadioGroup<Value = unknown>(
props: ContextMenuRadioGroupProps<Value>,
): React.JSX.Element {
return <BaseContextMenu.RadioGroup {...props} />
}
type ContextMenuContentProps = {
children: React.ReactNode
placement?: Placement
@ -83,7 +102,7 @@ function renderContextMenuPopup({
)
}
export function ContextMenuContent({
function ContextMenuContent({
children,
placement = 'bottom-start',
sideOffset = 0,
@ -105,15 +124,12 @@ export function ContextMenuContent({
})
}
type ContextMenuItemProps = BaseContextMenu.Item.Props & {
variant?: MenuItemVariant
type ContextMenuItemProps = Omit<BaseContextMenu.Item.Props, 'className'> & {
variant?: ContextMenuItemVariant
className?: string
}
export function ContextMenuItem({
className,
variant = 'default',
...props
}: ContextMenuItemProps) {
function ContextMenuItem({ className, variant = 'default', ...props }: ContextMenuItemProps) {
return (
<BaseContextMenu.Item
data-variant={variant}
@ -123,11 +139,12 @@ export function ContextMenuItem({
)
}
type ContextMenuLinkItemProps = BaseContextMenu.LinkItem.Props & {
variant?: MenuItemVariant
type ContextMenuLinkItemProps = Omit<BaseContextMenu.LinkItem.Props, 'className'> & {
variant?: ContextMenuItemVariant
className?: string
}
export function ContextMenuLinkItem({
function ContextMenuLinkItem({
className,
variant = 'default',
closeOnClick = true,
@ -143,21 +160,33 @@ export function ContextMenuLinkItem({
)
}
export function ContextMenuRadioItem({ className, ...props }: BaseContextMenu.RadioItem.Props) {
type ContextMenuRadioItemProps<Value = unknown> = Omit<
BaseContextMenu.RadioItem.Props,
'className' | 'value'
> & {
className?: string
value: Value
}
function ContextMenuRadioItem<Value = unknown>({
className,
...props
}: ContextMenuRadioItemProps<Value>) {
return <BaseContextMenu.RadioItem className={cn(menuItemClassName, className)} {...props} />
}
export function ContextMenuCheckboxItem({
className,
...props
}: BaseContextMenu.CheckboxItem.Props) {
function ContextMenuCheckboxItem({ className, ...props }: ContextMenuCheckboxItemProps) {
return <BaseContextMenu.CheckboxItem className={cn(menuItemClassName, className)} {...props} />
}
export function ContextMenuCheckboxItemIndicator({
type ContextMenuCheckboxItemProps = Omit<BaseContextMenu.CheckboxItem.Props, 'className'> & {
className?: string
}
function ContextMenuCheckboxItemIndicator({
className,
...props
}: Omit<BaseContextMenu.CheckboxItemIndicator.Props, 'children'>) {
}: ContextMenuCheckboxItemIndicatorProps) {
return (
<BaseContextMenu.CheckboxItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
@ -168,10 +197,15 @@ export function ContextMenuCheckboxItemIndicator({
)
}
export function ContextMenuRadioItemIndicator({
type ContextMenuCheckboxItemIndicatorProps = Omit<
BaseContextMenu.CheckboxItemIndicator.Props,
'children' | 'className'
> & { className?: string }
function ContextMenuRadioItemIndicator({
className,
...props
}: Omit<BaseContextMenu.RadioItemIndicator.Props, 'children'>) {
}: ContextMenuRadioItemIndicatorProps) {
return (
<BaseContextMenu.RadioItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
@ -182,11 +216,17 @@ export function ContextMenuRadioItemIndicator({
)
}
type ContextMenuSubTriggerProps = BaseContextMenu.SubmenuTrigger.Props & {
variant?: MenuItemVariant
type ContextMenuRadioItemIndicatorProps = Omit<
BaseContextMenu.RadioItemIndicator.Props,
'children' | 'className'
> & { className?: string }
type ContextMenuSubTriggerProps = Omit<BaseContextMenu.SubmenuTrigger.Props, 'className'> & {
variant?: ContextMenuItemVariant
className?: string
}
export function ContextMenuSubTrigger({
function ContextMenuSubTrigger({
className,
variant = 'default',
children,
@ -218,7 +258,7 @@ type ContextMenuSubContentProps = {
popupProps?: ContextMenuContentProps['popupProps']
}
export function ContextMenuSubContent({
function ContextMenuSubContent({
children,
placement = 'right-start',
sideOffset = 4,
@ -240,14 +280,61 @@ export function ContextMenuSubContent({
})
}
export function ContextMenuLabel({ className, ...props }: BaseContextMenu.GroupLabel.Props) {
type ContextMenuLabelProps = Omit<BaseContextMenu.GroupLabel.Props, 'className'> & {
className?: string
}
function ContextMenuLabel({ className, ...props }: ContextMenuLabelProps) {
return (
<BaseContextMenu.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
)
}
export function ContextMenuSeparator({ className, ...props }: BaseContextMenu.Separator.Props) {
type ContextMenuSeparatorProps = Omit<BaseContextMenu.Separator.Props, 'className'> & {
className?: string
}
function ContextMenuSeparator({ className, ...props }: ContextMenuSeparatorProps) {
return (
<BaseContextMenu.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
)
}
export {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuCheckboxItemIndicator,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuLinkItem,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuRadioItemIndicator,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
}
export type {
ContextMenuActions,
ContextMenuCheckboxItemIndicatorProps,
ContextMenuCheckboxItemProps,
ContextMenuContentProps,
ContextMenuGroupProps,
ContextMenuItemProps,
ContextMenuLabelProps,
ContextMenuLinkItemProps,
ContextMenuProps,
ContextMenuRadioGroupProps,
ContextMenuRadioItemIndicatorProps,
ContextMenuRadioItemProps,
ContextMenuSeparatorProps,
ContextMenuSubContentProps,
ContextMenuSubProps,
ContextMenuSubTriggerProps,
ContextMenuTriggerProps,
}

View File

@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect, waitFor, within } from 'storybook/test'
import {
createDialogHandle,
Dialog,
DialogBackdrop,
DialogCloseButton,
@ -191,6 +192,59 @@ export const Controlled: Story = {
render: () => <ControlledDemo />,
}
type WorkspaceDialogPayload = {
name: string
memberCount: number
}
const workspaceDialogPayloads = [
{ name: 'Design', memberCount: 8 },
{ name: 'Engineering', memberCount: 24 },
] as const satisfies readonly WorkspaceDialogPayload[]
function DetachedTriggersDemo() {
const [dialogHandle] = React.useState(() => createDialogHandle<WorkspaceDialogPayload>())
return (
<div className="flex flex-col items-center gap-3">
<div className="flex gap-2">
{workspaceDialogPayloads.map((payload) => (
<DialogTrigger
key={payload.name}
handle={dialogHandle}
payload={payload}
render={<Button variant="secondary" />}
>
Edit {payload.name}
</DialogTrigger>
))}
</div>
<Dialog handle={dialogHandle}>
{({ payload }) => (
<DialogContent>
<DialogCloseButton />
<div className="grid gap-2 pr-8">
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
{payload ? `Edit ${payload.name}` : 'Edit workspace'}
</DialogTitle>
<DialogDescription className="text-sm leading-5 text-text-secondary">
{payload
? `${payload.memberCount} members currently have access to this workspace.`
: 'Choose a workspace to edit.'}
</DialogDescription>
</div>
</DialogContent>
)}
</Dialog>
</div>
)
}
export const DetachedTriggers: Story = {
render: () => <DetachedTriggersDemo />,
}
type ApiExtensionFormValues = {
name: string
endpoint: string

View File

@ -5,18 +5,25 @@ import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { cn } from '../cn'
import { modalBackdropClassName, modalPopupAnimationClassName } from '../overlay-shared'
export const Dialog = BaseDialog.Root
export const DialogTrigger = BaseDialog.Trigger
export const DialogTitle = BaseDialog.Title
export const DialogDescription = BaseDialog.Description
export const DialogPortal = BaseDialog.Portal
export const createDialogHandle = BaseDialog.createHandle
const Dialog = BaseDialog.Root
const DialogTrigger = BaseDialog.Trigger
const DialogTitle = BaseDialog.Title
const DialogDescription = BaseDialog.Description
const DialogPortal = BaseDialog.Portal
const createDialogHandle = BaseDialog.createHandle
type DialogProps<Payload = unknown> = BaseDialog.Root.Props<Payload>
type DialogHandle<Payload = unknown> = BaseDialog.Handle<Payload>
type DialogTriggerProps<Payload = unknown> = BaseDialog.Trigger.Props<Payload>
type DialogTitleProps = BaseDialog.Title.Props
type DialogDescriptionProps = BaseDialog.Description.Props
type DialogPortalProps = BaseDialog.Portal.Props
type DialogBackdropProps = Omit<BaseDialog.Backdrop.Props, 'className'> & {
className?: string
}
export function DialogBackdrop({ className, ...props }: DialogBackdropProps) {
function DialogBackdrop({ className, ...props }: DialogBackdropProps) {
return <BaseDialog.Backdrop {...props} className={cn(modalBackdropClassName, className)} />
}
@ -24,7 +31,7 @@ type DialogViewportProps = Omit<BaseDialog.Viewport.Props, 'className'> & {
className?: string
}
export function DialogViewport({ className, ...props }: DialogViewportProps) {
function DialogViewport({ className, ...props }: DialogViewportProps) {
return <BaseDialog.Viewport className={cn('fixed inset-0 z-50', className)} {...props} />
}
@ -32,7 +39,7 @@ type DialogPopupProps = Omit<BaseDialog.Popup.Props, 'className'> & {
className?: string
}
export function DialogPopup({ className, ...props }: DialogPopupProps) {
function DialogPopup({ className, ...props }: DialogPopupProps) {
return (
<BaseDialog.Popup
className={cn(
@ -45,9 +52,11 @@ export function DialogPopup({ className, ...props }: DialogPopupProps) {
)
}
type DialogCloseButtonProps = Omit<BaseDialog.Close.Props, 'children'>
type DialogCloseButtonProps = Omit<BaseDialog.Close.Props, 'children' | 'className'> & {
className?: string
}
export function DialogCloseButton({
function DialogCloseButton({
className,
'aria-label': ariaLabel = 'Close',
...props
@ -73,7 +82,7 @@ type DialogContentProps = {
backdropProps?: Omit<BaseDialog.Backdrop.Props, 'className'>
}
export function DialogContent({
function DialogContent({
children,
className,
backdropClassName,
@ -93,3 +102,31 @@ export function DialogContent({
</DialogPortal>
)
}
export {
createDialogHandle,
Dialog,
DialogBackdrop,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogPopup,
DialogPortal,
DialogTitle,
DialogTrigger,
DialogViewport,
}
export type {
DialogBackdropProps,
DialogCloseButtonProps,
DialogContentProps,
DialogDescriptionProps,
DialogHandle,
DialogPopupProps,
DialogPortalProps,
DialogProps,
DialogTitleProps,
DialogTriggerProps,
DialogViewportProps,
}

View File

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { DrawerSnapPoint } from '.'
import type { DrawerProps } from '.'
import * as React from 'react'
import {
createDrawerHandle,
@ -308,6 +308,7 @@ export const Positions: Story = {
const snapTopMarginRem = 1
const visibleSnapPointRem = 30
type DrawerSnapPoint = NonNullable<DrawerProps['snapPoints']>[number]
const initialSnapPoint: DrawerSnapPoint = `${visibleSnapPointRem + snapTopMarginRem}rem`
const snapPoints = [initialSnapPoint, 1] satisfies DrawerSnapPoint[]

View File

@ -4,28 +4,35 @@ import type * as React from 'react'
import { Drawer as BaseDrawer } from '@base-ui/react/drawer'
import { cn } from '../cn'
export const Drawer = BaseDrawer.Root
export const DrawerProvider = BaseDrawer.Provider
export const DrawerIndent = BaseDrawer.Indent
export const DrawerIndentBackground = BaseDrawer.IndentBackground
export const DrawerTrigger = BaseDrawer.Trigger
export const DrawerSwipeArea = BaseDrawer.SwipeArea
export const DrawerPortal = BaseDrawer.Portal
export const DrawerTitle = BaseDrawer.Title
export const DrawerDescription = BaseDrawer.Description
export const DrawerClose = BaseDrawer.Close
export const createDrawerHandle = BaseDrawer.createHandle
const Drawer = BaseDrawer.Root
const DrawerProvider = BaseDrawer.Provider
const DrawerIndent = BaseDrawer.Indent
const DrawerIndentBackground = BaseDrawer.IndentBackground
const DrawerTrigger = BaseDrawer.Trigger
const DrawerSwipeArea = BaseDrawer.SwipeArea
const DrawerPortal = BaseDrawer.Portal
const DrawerTitle = BaseDrawer.Title
const DrawerDescription = BaseDrawer.Description
const DrawerClose = BaseDrawer.Close
const createDrawerHandle = BaseDrawer.createHandle
export type DrawerProps<Payload = unknown> = BaseDrawer.Root.Props<Payload>
export type DrawerActions = BaseDrawer.Root.Actions
export type DrawerChangeEventDetails = BaseDrawer.Root.ChangeEventDetails
export type DrawerChangeEventReason = BaseDrawer.Root.ChangeEventReason
export type DrawerSnapPoint = BaseDrawer.Root.SnapPoint
export type DrawerSnapPointChangeEventDetails = BaseDrawer.Root.SnapPointChangeEventDetails
export type DrawerSnapPointChangeEventReason = BaseDrawer.Root.SnapPointChangeEventReason
export type DrawerTriggerProps<Payload = unknown> = BaseDrawer.Trigger.Props<Payload>
type DrawerProps<Payload = unknown> = BaseDrawer.Root.Props<Payload>
type DrawerHandle<Payload = unknown> = BaseDrawer.Handle<Payload>
type DrawerProviderProps = BaseDrawer.Provider.Props
type DrawerIndentProps = BaseDrawer.Indent.Props
type DrawerIndentBackgroundProps = BaseDrawer.IndentBackground.Props
type DrawerTriggerProps<Payload = unknown> = BaseDrawer.Trigger.Props<Payload>
type DrawerSwipeAreaProps = BaseDrawer.SwipeArea.Props
type DrawerPortalProps = BaseDrawer.Portal.Props
type DrawerTitleProps = BaseDrawer.Title.Props
type DrawerDescriptionProps = BaseDrawer.Description.Props
type DrawerCloseProps = BaseDrawer.Close.Props
export function DrawerBackdrop({ className, ...props }: BaseDrawer.Backdrop.Props) {
type DrawerBackdropProps = Omit<BaseDrawer.Backdrop.Props, 'className'> & {
className?: string
}
function DrawerBackdrop({ className, ...props }: DrawerBackdropProps) {
return (
<BaseDrawer.Backdrop
className={cn(
@ -38,7 +45,11 @@ export function DrawerBackdrop({ className, ...props }: BaseDrawer.Backdrop.Prop
)
}
export function DrawerViewport({ className, ...props }: BaseDrawer.Viewport.Props) {
type DrawerViewportProps = Omit<BaseDrawer.Viewport.Props, 'className'> & {
className?: string
}
function DrawerViewport({ className, ...props }: DrawerViewportProps) {
return (
<BaseDrawer.Viewport
className={cn(
@ -50,7 +61,11 @@ export function DrawerViewport({ className, ...props }: BaseDrawer.Viewport.Prop
)
}
export function DrawerPopup({ className, ...props }: BaseDrawer.Popup.Props) {
type DrawerPopupProps = Omit<BaseDrawer.Popup.Props, 'className'> & {
className?: string
}
function DrawerPopup({ className, ...props }: DrawerPopupProps) {
return (
<BaseDrawer.Popup
className={cn(
@ -71,7 +86,11 @@ export function DrawerPopup({ className, ...props }: BaseDrawer.Popup.Props) {
)
}
export function DrawerContent({ className, ...props }: BaseDrawer.Content.Props) {
type DrawerContentProps = Omit<BaseDrawer.Content.Props, 'className'> & {
className?: string
}
function DrawerContent({ className, ...props }: DrawerContentProps) {
return (
<BaseDrawer.Content
className={cn(
@ -83,11 +102,12 @@ export function DrawerContent({ className, ...props }: BaseDrawer.Content.Props)
)
}
type DrawerCloseButtonProps = Omit<BaseDrawer.Close.Props, 'children'> & {
type DrawerCloseButtonProps = Omit<BaseDrawer.Close.Props, 'children' | 'className'> & {
children?: React.ReactNode
className?: string
}
export function DrawerCloseButton({
function DrawerCloseButton({
className,
children,
type = 'button',
@ -108,3 +128,41 @@ export function DrawerCloseButton({
</BaseDrawer.Close>
)
}
export {
createDrawerHandle,
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerCloseButton,
DrawerContent,
DrawerDescription,
DrawerIndent,
DrawerIndentBackground,
DrawerPopup,
DrawerPortal,
DrawerProvider,
DrawerSwipeArea,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
}
export type {
DrawerBackdropProps,
DrawerCloseButtonProps,
DrawerCloseProps,
DrawerContentProps,
DrawerDescriptionProps,
DrawerHandle,
DrawerIndentBackgroundProps,
DrawerIndentProps,
DrawerPopupProps,
DrawerPortalProps,
DrawerProps,
DrawerProviderProps,
DrawerSwipeAreaProps,
DrawerTitleProps,
DrawerTriggerProps,
DrawerViewportProps,
}

View File

@ -138,23 +138,25 @@ export const WithSubmenu: Story = {
),
}
type Density = 'compact' | 'comfortable' | 'spacious'
const WithRadioItemsDemo = () => {
const [value, setValue] = React.useState('comfortable')
const [density, setDensity] = React.useState<Density>('comfortable')
return (
<DropdownMenu>
<TriggerButton label={`Density: ${value}`} />
<TriggerButton label={`Density: ${density}`} />
<DropdownMenuContent>
<DropdownMenuRadioGroup value={value} onValueChange={setValue}>
<DropdownMenuRadioItem value="compact">
<DropdownMenuRadioGroup<Density> value={density} onValueChange={setDensity}>
<DropdownMenuRadioItem<Density> value="compact">
Compact
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="comfortable">
<DropdownMenuRadioItem<Density> value="comfortable">
Comfortable
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="spacious">
<DropdownMenuRadioItem<Density> value="spacious">
Spacious
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
@ -261,8 +263,10 @@ export const WithLinkItems: Story = {
),
}
type SortOrder = 'newest' | 'oldest' | 'name'
const ComplexDemo = () => {
const [sortOrder, setSortOrder] = React.useState('newest')
const [sortOrder, setSortOrder] = React.useState<SortOrder>('newest')
const [showArchived, setShowArchived] = React.useState(false)
return (
@ -308,16 +312,16 @@ const ComplexDemo = () => {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuRadioGroup value={sortOrder} onValueChange={setSortOrder}>
<DropdownMenuRadioItem value="newest">
<DropdownMenuRadioGroup<SortOrder> value={sortOrder} onValueChange={setSortOrder}>
<DropdownMenuRadioItem<SortOrder> value="newest">
Newest first
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="oldest">
<DropdownMenuRadioItem<SortOrder> value="oldest">
Oldest first
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name">
<DropdownMenuRadioItem<SortOrder> value="name">
Name
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>

View File

@ -16,22 +16,50 @@ import {
} from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
const DropdownMenu = Menu.Root
const DropdownMenuTrigger = Menu.Trigger
const DropdownMenuSub = Menu.SubmenuRoot
const DropdownMenuGroup = Menu.Group
export const DropdownMenu = Menu.Root
export const DropdownMenuTrigger = Menu.Trigger
export const DropdownMenuSub = Menu.SubmenuRoot
export const DropdownMenuGroup = Menu.Group
export const DropdownMenuRadioGroup = Menu.RadioGroup
type DropdownMenuProps<Payload = unknown> = Menu.Root.Props<Payload>
type DropdownMenuTriggerProps<Payload = unknown> = Menu.Trigger.Props<Payload>
type DropdownMenuSubProps = Menu.SubmenuRoot.Props
type DropdownMenuGroupProps = Menu.Group.Props
type DropdownMenuRadioGroupProps<Value = unknown> = Omit<
Menu.RadioGroup.Props,
'defaultValue' | 'onValueChange' | 'value'
> & {
defaultValue?: Value
onValueChange?: (value: Value, eventDetails: Menu.RadioGroup.ChangeEventDetails) => void
value?: Value
}
type DropdownMenuItemVariant = MenuItemVariant
export function DropdownMenuRadioItem({ className, ...props }: Menu.RadioItem.Props) {
function DropdownMenuRadioGroup<Value = unknown>(
props: DropdownMenuRadioGroupProps<Value>,
): React.JSX.Element {
return <Menu.RadioGroup {...props} />
}
type DropdownMenuRadioItemProps<Value = unknown> = Omit<
Menu.RadioItem.Props,
'className' | 'value'
> & {
className?: string
value: Value
}
function DropdownMenuRadioItem<Value = unknown>({
className,
...props
}: DropdownMenuRadioItemProps<Value>) {
return <Menu.RadioItem className={cn(menuItemClassName, className)} {...props} />
}
export function DropdownMenuRadioItemIndicator({
function DropdownMenuRadioItemIndicator({
className,
...props
}: Omit<Menu.RadioItemIndicator.Props, 'children'>) {
}: DropdownMenuRadioItemIndicatorProps) {
return (
<Menu.RadioItemIndicator className={cn(floatingItemIndicatorClassName, className)} {...props}>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
@ -39,14 +67,23 @@ export function DropdownMenuRadioItemIndicator({
)
}
export function DropdownMenuCheckboxItem({ className, ...props }: Menu.CheckboxItem.Props) {
type DropdownMenuRadioItemIndicatorProps = Omit<
Menu.RadioItemIndicator.Props,
'children' | 'className'
> & { className?: string }
type DropdownMenuCheckboxItemProps = Omit<Menu.CheckboxItem.Props, 'className'> & {
className?: string
}
function DropdownMenuCheckboxItem({ className, ...props }: DropdownMenuCheckboxItemProps) {
return <Menu.CheckboxItem className={cn(menuItemClassName, className)} {...props} />
}
export function DropdownMenuCheckboxItemIndicator({
function DropdownMenuCheckboxItemIndicator({
className,
...props
}: Omit<Menu.CheckboxItemIndicator.Props, 'children'>) {
}: DropdownMenuCheckboxItemIndicatorProps) {
return (
<Menu.CheckboxItemIndicator
className={cn(floatingItemIndicatorClassName, className)}
@ -57,7 +94,16 @@ export function DropdownMenuCheckboxItemIndicator({
)
}
export function DropdownMenuLabel({ className, ...props }: Menu.GroupLabel.Props) {
type DropdownMenuCheckboxItemIndicatorProps = Omit<
Menu.CheckboxItemIndicator.Props,
'children' | 'className'
> & { className?: string }
type DropdownMenuLabelProps = Omit<Menu.GroupLabel.Props, 'className'> & {
className?: string
}
function DropdownMenuLabel({ className, ...props }: DropdownMenuLabelProps) {
return <Menu.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
}
@ -118,7 +164,7 @@ function renderDropdownMenuPopup({
)
}
export function DropdownMenuContent({
function DropdownMenuContent({
children,
placement = 'bottom-end',
sideOffset = 4,
@ -140,11 +186,12 @@ export function DropdownMenuContent({
})
}
type DropdownMenuSubTriggerProps = Menu.SubmenuTrigger.Props & {
variant?: MenuItemVariant
type DropdownMenuSubTriggerProps = Omit<Menu.SubmenuTrigger.Props, 'className'> & {
variant?: DropdownMenuItemVariant
className?: string
}
export function DropdownMenuSubTrigger({
function DropdownMenuSubTrigger({
className,
variant = 'default',
children,
@ -176,7 +223,7 @@ type DropdownMenuSubContentProps = {
popupProps?: DropdownMenuContentProps['popupProps']
}
export function DropdownMenuSubContent({
function DropdownMenuSubContent({
children,
placement = 'left-start',
sideOffset = 4,
@ -198,15 +245,12 @@ export function DropdownMenuSubContent({
})
}
type DropdownMenuItemProps = Menu.Item.Props & {
variant?: MenuItemVariant
type DropdownMenuItemProps = Omit<Menu.Item.Props, 'className'> & {
variant?: DropdownMenuItemVariant
className?: string
}
export function DropdownMenuItem({
className,
variant = 'default',
...props
}: DropdownMenuItemProps) {
function DropdownMenuItem({ className, variant = 'default', ...props }: DropdownMenuItemProps) {
return (
<Menu.Item
data-variant={variant}
@ -216,11 +260,12 @@ export function DropdownMenuItem({
)
}
type DropdownMenuLinkItemProps = Menu.LinkItem.Props & {
variant?: MenuItemVariant
type DropdownMenuLinkItemProps = Omit<Menu.LinkItem.Props, 'className'> & {
variant?: DropdownMenuItemVariant
className?: string
}
export function DropdownMenuLinkItem({
function DropdownMenuLinkItem({
className,
variant = 'default',
closeOnClick = true,
@ -236,6 +281,49 @@ export function DropdownMenuLinkItem({
)
}
export function DropdownMenuSeparator({ className, ...props }: Menu.Separator.Props) {
type DropdownMenuSeparatorProps = Omit<Menu.Separator.Props, 'className'> & {
className?: string
}
function DropdownMenuSeparator({ className, ...props }: DropdownMenuSeparatorProps) {
return <Menu.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
}
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuCheckboxItemIndicator,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuLinkItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuRadioItemIndicator,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
}
export type {
DropdownMenuCheckboxItemIndicatorProps,
DropdownMenuCheckboxItemProps,
DropdownMenuContentProps,
DropdownMenuGroupProps,
DropdownMenuItemProps,
DropdownMenuLabelProps,
DropdownMenuLinkItemProps,
DropdownMenuProps,
DropdownMenuRadioGroupProps,
DropdownMenuRadioItemIndicatorProps,
DropdownMenuRadioItemProps,
DropdownMenuSeparatorProps,
DropdownMenuSubContentProps,
DropdownMenuSubProps,
DropdownMenuSubTriggerProps,
DropdownMenuTriggerProps,
Placement,
}

View File

@ -6,50 +6,44 @@ import { Field as BaseField } from '@base-ui/react/field'
import { cn } from '../cn'
import { formLabelClassName, textControlVariants } from '../form-control-shared'
export type FieldProps = Omit<BaseFieldNS.Root.Props, 'className'> & {
type FieldProps = Omit<BaseFieldNS.Root.Props, 'className'> & {
className?: string
}
export type FieldActions = BaseFieldNS.Root.Actions
export function Field({ className, ...props }: FieldProps) {
function Field({ className, ...props }: FieldProps) {
return <BaseField.Root className={cn('group/field grid min-w-0 gap-1', className)} {...props} />
}
export type FieldItemProps = Omit<BaseFieldNS.Item.Props, 'className'> & {
type FieldItemProps = Omit<BaseFieldNS.Item.Props, 'className'> & {
className?: string
}
export function FieldItem({ className, ...props }: FieldItemProps) {
function FieldItem({ className, ...props }: FieldItemProps) {
return <BaseField.Item className={cn('grid min-w-0 gap-1', className)} {...props} />
}
export type FieldLabelProps = Omit<BaseFieldNS.Label.Props, 'className'> & {
type FieldLabelProps = Omit<BaseFieldNS.Label.Props, 'className'> & {
className?: string
}
export function FieldLabel({ className, ...props }: FieldLabelProps) {
function FieldLabel({ className, ...props }: FieldLabelProps) {
return <BaseField.Label className={cn(formLabelClassName, className)} {...props} />
}
export type FieldControlSize = NonNullable<VariantProps<typeof textControlVariants>['size']>
export type FieldControlProps = Omit<BaseFieldNS.Control.Props, 'className' | 'size'> &
type FieldControlProps = Omit<BaseFieldNS.Control.Props, 'className' | 'size'> &
VariantProps<typeof textControlVariants> & {
className?: string
}
export type FieldControlChangeEventDetails = BaseFieldNS.Control.ChangeEventDetails
export function FieldControl({ className, size = 'medium', ...props }: FieldControlProps) {
function FieldControl({ className, size = 'medium', ...props }: FieldControlProps) {
return <BaseField.Control className={cn(textControlVariants({ size }), className)} {...props} />
}
export type FieldDescriptionProps = Omit<BaseFieldNS.Description.Props, 'className'> & {
type FieldDescriptionProps = Omit<BaseFieldNS.Description.Props, 'className'> & {
className?: string
}
export function FieldDescription({ className, ...props }: FieldDescriptionProps) {
function FieldDescription({ className, ...props }: FieldDescriptionProps) {
return (
<BaseField.Description
className={cn('py-0.5 body-xs-regular text-text-tertiary', className)}
@ -58,11 +52,11 @@ export function FieldDescription({ className, ...props }: FieldDescriptionProps)
)
}
export type FieldErrorProps = Omit<BaseFieldNS.Error.Props, 'className'> & {
type FieldErrorProps = Omit<BaseFieldNS.Error.Props, 'className'> & {
className?: string
}
export function FieldError({ className, ...props }: FieldErrorProps) {
function FieldError({ className, ...props }: FieldErrorProps) {
return (
<BaseField.Error
className={cn('py-0.5 body-xs-regular text-text-destructive', className)}
@ -71,7 +65,18 @@ export function FieldError({ className, ...props }: FieldErrorProps) {
)
}
export type FieldValidityProps = BaseFieldNS.Validity.Props
export type FieldValidityState = BaseFieldNS.Validity.State
type FieldValidityProps = BaseFieldNS.Validity.Props
export const FieldValidity = BaseField.Validity
const FieldValidity = BaseField.Validity
export { Field, FieldControl, FieldDescription, FieldError, FieldItem, FieldLabel, FieldValidity }
export type {
FieldControlProps,
FieldDescriptionProps,
FieldErrorProps,
FieldItemProps,
FieldLabelProps,
FieldProps,
FieldValidityProps,
}

View File

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

View File

@ -20,7 +20,7 @@ function renderGuides(level: number) {
return Array.from({ length: Math.max(level - 1, 0) }, (_, index) => <FileTreeGuide key={index} />)
}
type FileTreeRowState = {
type FileTreeFileState = {
selected: boolean
disabled: boolean
level: number
@ -37,9 +37,9 @@ function fileTreeRowClassName({ className }: { className?: string }) {
)
}
export type FileTreeProps = useRender.ComponentProps<'section'>
type FileTreeProps = useRender.ComponentProps<'section'>
export function FileTree({ render, className, children, ...props }: FileTreeProps) {
function FileTree({ render, className, children, ...props }: FileTreeProps) {
const defaultProps: useRender.ElementProps<'section'> = {
className: cn('flex min-w-0 flex-col gap-px p-1', className),
children: <FileTreeLevelContext.Provider value={1}>{children}</FileTreeLevelContext.Provider>,
@ -52,9 +52,9 @@ export function FileTree({ render, className, children, ...props }: FileTreeProp
})
}
export type FileTreeListProps = useRender.ComponentProps<'ul'>
type FileTreeListProps = useRender.ComponentProps<'ul'>
export function FileTreeList({ render, className, ...props }: FileTreeListProps) {
function FileTreeList({ render, className, ...props }: FileTreeListProps) {
const defaultProps: useRender.ElementProps<'ul'> = {
className: cn('m-0 flex min-w-0 list-none flex-col gap-px p-0', className),
}
@ -66,20 +66,21 @@ export function FileTreeList({ render, className, ...props }: FileTreeListProps)
})
}
export type FileTreeFolderProps = Omit<BaseCollapsible.Root.Props, 'render'> & {
type FileTreeFolderProps = Omit<BaseCollapsible.Root.Props, 'className' | 'render'> & {
className?: string
render?: BaseCollapsible.Root.Props['render']
}
export function FileTreeFolder({ render = <li />, className, ...props }: FileTreeFolderProps) {
function FileTreeFolder({ render = <li />, className, ...props }: FileTreeFolderProps) {
return <BaseCollapsible.Root render={render} className={cn('min-w-0', className)} {...props} />
}
export type FileTreeFolderTriggerProps = Omit<BaseCollapsible.Trigger.Props, 'className'> & {
type FileTreeFolderTriggerProps = Omit<BaseCollapsible.Trigger.Props, 'className'> & {
className?: string
level?: number
}
export function FileTreeFolderTrigger({
function FileTreeFolderTrigger({
className,
children,
disabled,
@ -102,11 +103,12 @@ export function FileTreeFolderTrigger({
)
}
export type FileTreeFolderPanelProps = Omit<BaseCollapsible.Panel.Props, 'render'> & {
type FileTreeFolderPanelProps = Omit<BaseCollapsible.Panel.Props, 'className' | 'render'> & {
className?: string
render?: BaseCollapsible.Panel.Props['render']
}
export function FileTreeFolderPanel({
function FileTreeFolderPanel({
render = <ul />,
className,
children,
@ -125,15 +127,12 @@ export function FileTreeFolderPanel({
)
}
export type FileTreeFileProps = Omit<
useRender.ComponentProps<'button', FileTreeRowState>,
'type'
> & {
type FileTreeFileProps = Omit<useRender.ComponentProps<'button', FileTreeFileState>, 'type'> & {
level?: number
selected?: boolean
}
export function FileTreeFile({
function FileTreeFile({
render,
className,
children,
@ -144,7 +143,7 @@ export function FileTreeFile({
}: FileTreeFileProps) {
const contextLevel = useFileTreeLevel()
const level = levelProp ?? contextLevel
const state: FileTreeRowState = {
const state: FileTreeFileState = {
selected,
disabled,
level,
@ -174,9 +173,9 @@ export function FileTreeFile({
return <li className="min-w-0">{file}</li>
}
export type FileTreeGuideProps = useRender.ComponentProps<'span'>
type FileTreeGuideProps = useRender.ComponentProps<'span'>
export function FileTreeGuide({ render, className, ...props }: FileTreeGuideProps) {
function FileTreeGuide({ render, className, ...props }: FileTreeGuideProps) {
const defaultProps: useRender.ElementProps<'span'> = {
'aria-hidden': true,
className: cn(
@ -192,7 +191,7 @@ export function FileTreeGuide({ render, className, ...props }: FileTreeGuideProp
})
}
export type FileTreeIconType =
type FileTreeIconType =
| 'folder'
| 'file'
| 'markdown'
@ -218,18 +217,12 @@ const fileTreeIconClassNames: Record<Exclude<FileTreeIconType, 'folder'>, string
archive: 'i-ri-file-zip-fill text-[#A4AABF]',
}
export type FileTreeIconProps = Omit<useRender.ComponentProps<'span'>, 'children'> & {
type FileTreeIconProps = Omit<useRender.ComponentProps<'span'>, 'children'> & {
type?: FileTreeIconType
children?: React.ReactNode
}
export function FileTreeIcon({
type = 'file',
render,
className,
children,
...props
}: FileTreeIconProps) {
function FileTreeIcon({ type = 'file', render, className, children, ...props }: FileTreeIconProps) {
const defaultProps: useRender.ElementProps<'span'> = {
'aria-hidden': true,
className: cn(
@ -258,12 +251,12 @@ export function FileTreeIcon({
})
}
export type FileTreeLabelProps = useRender.ComponentProps<'span'>
type FileTreeLabelProps = useRender.ComponentProps<'span'>
type FileTreeLabelElementProps = useRender.ElementProps<'span'> & {
'data-label'?: string
}
export function FileTreeLabel({ render, className, children, ...props }: FileTreeLabelProps) {
function FileTreeLabel({ render, className, children, ...props }: FileTreeLabelProps) {
const labelText = getLabelText(children)
const defaultProps = {
'data-label': labelText,
@ -284,9 +277,9 @@ export function FileTreeLabel({ render, className, children, ...props }: FileTre
})
}
export type FileTreeMetaProps = useRender.ComponentProps<'span'>
type FileTreeMetaProps = useRender.ComponentProps<'span'>
export function FileTreeMeta({ render, className, ...props }: FileTreeMetaProps) {
function FileTreeMeta({ render, className, ...props }: FileTreeMetaProps) {
const defaultProps: useRender.ElementProps<'span'> = {
className: cn('min-w-0 shrink truncate system-xs-regular text-text-tertiary', className),
}
@ -298,9 +291,9 @@ export function FileTreeMeta({ render, className, ...props }: FileTreeMetaProps)
})
}
export type FileTreeBadgeProps = useRender.ComponentProps<'span'>
type FileTreeBadgeProps = useRender.ComponentProps<'span'>
export function FileTreeBadge({ render, className, ...props }: FileTreeBadgeProps) {
function FileTreeBadge({ render, className, ...props }: FileTreeBadgeProps) {
const defaultProps: useRender.ElementProps<'span'> = {
className: cn(
'ms-1 inline-flex min-w-4 shrink-0 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary',
@ -314,3 +307,31 @@ export function FileTreeBadge({ render, className, ...props }: FileTreeBadgeProp
props: mergeProps<'span'>(defaultProps, props),
})
}
export {
FileTree,
FileTreeBadge,
FileTreeFile,
FileTreeFolder,
FileTreeFolderPanel,
FileTreeFolderTrigger,
FileTreeGuide,
FileTreeIcon,
FileTreeLabel,
FileTreeList,
FileTreeMeta,
}
export type {
FileTreeBadgeProps,
FileTreeFileProps,
FileTreeFolderPanelProps,
FileTreeFolderProps,
FileTreeFolderTriggerProps,
FileTreeGuideProps,
FileTreeIconProps,
FileTreeIconType,
FileTreeLabelProps,
FileTreeListProps,
FileTreeMetaProps,
FileTreeProps,
}

View File

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { Button } from '../button'
import { Checkbox } from '../checkbox'
import { CheckboxGroup } from '../checkbox-group'
@ -18,48 +19,71 @@ export default meta
type Story = StoryObj<typeof meta>
export const Basic: Story = {
render: () => (
<Form className="grid w-96 gap-4" onFormSubmit={() => undefined}>
<Field name="name">
<FieldLabel>Name</FieldLabel>
<FieldControl required placeholder="Enter a name" />
<FieldError match="valueMissing">Name is required.</FieldError>
</Field>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" required placeholder="name@example.com" />
<FieldDescription>Used for account notifications.</FieldDescription>
<FieldError match="valueMissing">Email is required.</FieldError>
<FieldError match="typeMismatch">Enter a valid email address.</FieldError>
</Field>
<Field name="features">
<Fieldset render={<CheckboxGroup defaultValue={['search']} />}>
<FieldsetLegend>Features</FieldsetLegend>
<div className="grid gap-2">
<FieldItem>
<FieldLabel className="flex items-center gap-2">
<Checkbox value="search" />
Search
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-2">
<Checkbox value="analytics" />
Analytics
</FieldLabel>
</FieldItem>
</div>
</Fieldset>
</Field>
<div className="flex justify-end">
<Button type="submit" variant="primary">
Save
</Button>
</div>
</Form>
),
type WorkspaceFormValues = {
name: string
email: string
features: string[]
}
function BasicFormDemo() {
const [submittedValues, setSubmittedValues] = React.useState<WorkspaceFormValues>()
return (
<div className="grid gap-4">
<Form<WorkspaceFormValues>
className="grid w-96 gap-4"
onFormSubmit={(formValues) => setSubmittedValues(formValues)}
>
<Field name="name">
<FieldLabel>Name</FieldLabel>
<FieldControl required placeholder="Enter a name" />
<FieldError match="valueMissing">Name is required.</FieldError>
</Field>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" required placeholder="name@example.com" />
<FieldDescription>Used for account notifications.</FieldDescription>
<FieldError match="valueMissing">Email is required.</FieldError>
<FieldError match="typeMismatch">Enter a valid email address.</FieldError>
</Field>
<Field name="features">
<Fieldset render={<CheckboxGroup defaultValue={['search']} />}>
<FieldsetLegend>Features</FieldsetLegend>
<div className="grid gap-2">
<FieldItem>
<FieldLabel className="flex items-center gap-2">
<Checkbox value="search" />
Search
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-2">
<Checkbox value="analytics" />
Analytics
</FieldLabel>
</FieldItem>
</div>
</Fieldset>
</Field>
<div className="flex justify-end">
<Button type="submit" variant="primary">
Save
</Button>
</div>
</Form>
{submittedValues && (
<output className="w-96 rounded-lg bg-background-default-subtle p-3 font-mono text-xs text-text-secondary">
{JSON.stringify(submittedValues, null, 2)}
</output>
)}
</div>
)
}
export const Basic: Story = {
render: () => <BasicFormDemo />,
}

View File

@ -3,9 +3,10 @@
import type { Form as BaseFormNS } from '@base-ui/react/form'
import { Form as BaseForm } from '@base-ui/react/form'
export const Form = BaseForm
const Form = BaseForm
export type FormProps = BaseFormNS.Props
export type FormActions = BaseFormNS.Actions
export type FormValidationMode = BaseFormNS.ValidationMode
export type FormSubmitEventDetails = BaseFormNS.SubmitEventDetails
type FormProps<FormValues extends BaseFormNS.Values = BaseFormNS.Values> =
BaseFormNS.Props<FormValues>
export { Form }
export type { FormProps }

View File

@ -6,15 +6,14 @@ import { Input as BaseInput } from '@base-ui/react/input'
import { cn } from '../cn'
import { textControlVariants } from '../form-control-shared'
export type InputSize = NonNullable<VariantProps<typeof textControlVariants>['size']>
export type InputProps = Omit<BaseInputNS.Props, 'className' | 'size'> &
type InputProps = Omit<BaseInputNS.Props, 'className' | 'size'> &
VariantProps<typeof textControlVariants> & {
className?: string
}
export type InputChangeEventDetails = BaseInputNS.ChangeEventDetails
export function Input({ className, size = 'medium', ...props }: InputProps) {
function Input({ className, size = 'medium', ...props }: InputProps) {
return <BaseInput className={cn(textControlVariants({ size }), className)} {...props} />
}
export { Input }
export type { InputProps }

View File

@ -25,11 +25,11 @@ const kbdVariants = cva(
},
)
export type KbdColor = NonNullable<VariantProps<typeof kbdVariants>['color']>
type KbdColor = NonNullable<VariantProps<typeof kbdVariants>['color']>
export type KbdProps = Omit<React.ComponentProps<'kbd'>, 'color'> & VariantProps<typeof kbdVariants>
type KbdProps = Omit<React.ComponentProps<'kbd'>, 'color'> & VariantProps<typeof kbdVariants>
export function Kbd({ className, color, disabled, ...props }: KbdProps) {
function Kbd({ className, color, disabled, ...props }: KbdProps) {
return (
<kbd
data-disabled={disabled ? '' : undefined}
@ -39,10 +39,13 @@ export function Kbd({ className, color, disabled, ...props }: KbdProps) {
)
}
export type KbdGroupProps = React.ComponentProps<'span'>
type KbdGroupProps = React.ComponentProps<'span'>
export function KbdGroup({ className, ...props }: KbdGroupProps) {
function KbdGroup({ className, ...props }: KbdGroupProps) {
return (
<span className={cn('inline-flex items-center gap-0.5 align-middle', className)} {...props} />
)
}
export { Kbd, KbdGroup }
export type { KbdColor, KbdGroupProps, KbdProps }

View File

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

View File

@ -1,7 +1,8 @@
import type {
NumberFieldButtonProps,
NumberFieldControlsProps,
NumberFieldDecrementProps,
NumberFieldGroupProps,
NumberFieldIncrementProps,
NumberFieldInputProps,
NumberFieldUnitProps,
} from '../index'
@ -24,8 +25,8 @@ type RenderNumberFieldOptions = {
inputProps?: Partial<NumberFieldInputProps>
unitProps?: Partial<NumberFieldUnitProps> & { children?: React.ReactNode }
controlsProps?: Partial<NumberFieldControlsProps>
incrementProps?: Partial<NumberFieldButtonProps>
decrementProps?: Partial<NumberFieldButtonProps>
incrementProps?: Partial<NumberFieldIncrementProps>
decrementProps?: Partial<NumberFieldDecrementProps>
}
const renderNumberField = ({

View File

@ -7,10 +7,10 @@ import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlCompoundFocusClassName } from '../form-control-shared'
export const NumberField = BaseNumberField.Root
export type NumberFieldProps = BaseNumberField.Root.Props
const NumberField = BaseNumberField.Root
type NumberFieldProps = BaseNumberField.Root.Props
export const numberFieldGroupVariants = cva(
const numberFieldGroupVariants = cva(
[
'group/number-field flex w-full min-w-0 items-stretch overflow-hidden border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
@ -33,14 +33,14 @@ export const numberFieldGroupVariants = cva(
},
},
)
export type NumberFieldSize = NonNullable<VariantProps<typeof numberFieldGroupVariants>['size']>
type NumberFieldSize = NonNullable<VariantProps<typeof numberFieldGroupVariants>['size']>
export type NumberFieldGroupProps = Omit<BaseNumberField.Group.Props, 'className'> &
type NumberFieldGroupProps = Omit<BaseNumberField.Group.Props, 'className'> &
VariantProps<typeof numberFieldGroupVariants> & {
className?: string
}
export function NumberFieldGroup({ className, size = 'medium', ...props }: NumberFieldGroupProps) {
function NumberFieldGroup({ className, size = 'medium', ...props }: NumberFieldGroupProps) {
return (
<BaseNumberField.Group
className={cn(numberFieldGroupVariants({ size }), className)}
@ -49,7 +49,7 @@ export function NumberFieldGroup({ className, size = 'medium', ...props }: Numbe
)
}
export const numberFieldInputVariants = cva(
const numberFieldInputVariants = cva(
[
'w-0 min-w-0 flex-1 appearance-none border-0 bg-transparent text-components-input-text-filled caret-primary-600 outline-hidden',
'placeholder:text-components-input-text-placeholder',
@ -69,12 +69,12 @@ export const numberFieldInputVariants = cva(
},
)
export type NumberFieldInputProps = Omit<BaseNumberField.Input.Props, 'className' | 'size'> &
type NumberFieldInputProps = Omit<BaseNumberField.Input.Props, 'className' | 'size'> &
VariantProps<typeof numberFieldInputVariants> & {
className?: string
}
export function NumberFieldInput({ className, size = 'medium', ...props }: NumberFieldInputProps) {
function NumberFieldInput({ className, size = 'medium', ...props }: NumberFieldInputProps) {
return (
<BaseNumberField.Input
className={cn(numberFieldInputVariants({ size }), className)}
@ -83,7 +83,7 @@ export function NumberFieldInput({ className, size = 'medium', ...props }: Numbe
)
}
export const numberFieldUnitVariants = cva(
const numberFieldUnitVariants = cva(
'flex shrink-0 items-center self-stretch system-sm-regular text-text-tertiary',
{
variants: {
@ -98,10 +98,10 @@ export const numberFieldUnitVariants = cva(
},
)
export type NumberFieldUnitProps = React.ComponentProps<'span'> &
type NumberFieldUnitProps = React.ComponentProps<'span'> &
VariantProps<typeof numberFieldUnitVariants>
export function NumberFieldUnit({ className, size = 'medium', ...props }: NumberFieldUnitProps) {
function NumberFieldUnit({ className, size = 'medium', ...props }: NumberFieldUnitProps) {
return <span className={cn(numberFieldUnitVariants({ size }), className)} {...props} />
}
@ -109,9 +109,9 @@ const numberFieldControlsVariants = cva(
'flex shrink-0 flex-col items-stretch border-l border-divider-subtle bg-transparent text-text-tertiary',
)
export type NumberFieldControlsProps = React.ComponentProps<'div'>
type NumberFieldControlsProps = React.ComponentProps<'div'>
export function NumberFieldControls({ className, ...props }: NumberFieldControlsProps) {
function NumberFieldControls({ className, ...props }: NumberFieldControlsProps) {
return <div className={cn(numberFieldControlsVariants(), className)} {...props} />
}
@ -170,7 +170,11 @@ type NumberFieldButtonVariantProps = Omit<
'direction'
>
export type NumberFieldButtonProps = Omit<BaseNumberField.Increment.Props, 'className'> &
type NumberFieldIncrementProps = Omit<BaseNumberField.Increment.Props, 'className'> &
NumberFieldButtonVariantProps & {
className?: string
}
type NumberFieldDecrementProps = Omit<BaseNumberField.Decrement.Props, 'className'> &
NumberFieldButtonVariantProps & {
className?: string
}
@ -178,12 +182,12 @@ export type NumberFieldButtonProps = Omit<BaseNumberField.Increment.Props, 'clas
const incrementAriaLabel = 'Increment value'
const decrementAriaLabel = 'Decrement value'
export function NumberFieldIncrement({
function NumberFieldIncrement({
className,
children,
size = 'medium',
...props
}: NumberFieldButtonProps) {
}: NumberFieldIncrementProps) {
return (
<BaseNumberField.Increment
{...props}
@ -197,12 +201,12 @@ export function NumberFieldIncrement({
)
}
export function NumberFieldDecrement({
function NumberFieldDecrement({
className,
children,
size = 'medium',
...props
}: NumberFieldButtonProps) {
}: NumberFieldDecrementProps) {
return (
<BaseNumberField.Decrement
{...props}
@ -215,3 +219,23 @@ export function NumberFieldDecrement({
</BaseNumberField.Decrement>
)
}
export {
NumberField,
NumberFieldControls,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldUnit,
}
export type {
NumberFieldControlsProps,
NumberFieldDecrementProps,
NumberFieldGroupProps,
NumberFieldIncrementProps,
NumberFieldInputProps,
NumberFieldProps,
NumberFieldSize,
NumberFieldUnitProps,
}

View File

@ -110,7 +110,7 @@ type PaginationRootState = {
disabled: boolean
}
export type PaginationRootProps = Omit<
type PaginationRootProps = Omit<
useRender.ComponentProps<'nav', PaginationRootState>,
'onChange'
> & {
@ -122,7 +122,7 @@ export type PaginationRootProps = Omit<
visiblePageCount?: number
}
export function PaginationRoot({
function PaginationRoot({
page,
totalPages,
onPageChange,
@ -184,11 +184,11 @@ export function PaginationRoot({
})
}
export type PaginationNavigationProps = useRender.ComponentProps<'div'>
type PaginationNavigationProps = useRender.ComponentProps<'div'>
export type PaginationContentProps = useRender.ComponentProps<'div'>
type PaginationContentProps = useRender.ComponentProps<'div'>
export function PaginationContent({ render, className, ...props }: PaginationContentProps) {
function PaginationContent({ render, className, ...props }: PaginationContentProps) {
const defaultProps: useRender.ElementProps<'div'> = {
className: cn(
'grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2',
@ -203,7 +203,7 @@ export function PaginationContent({ render, className, ...props }: PaginationCon
})
}
export function PaginationNavigation({ render, className, ...props }: PaginationNavigationProps) {
function PaginationNavigation({ render, className, ...props }: PaginationNavigationProps) {
const defaultProps: useRender.ElementProps<'div'> = {
className: cn(
'flex shrink-0 items-center gap-0.5 justify-self-start rounded-[10px] bg-background-section-burn p-0.5',
@ -218,8 +218,14 @@ export function PaginationNavigation({ render, className, ...props }: Pagination
})
}
type PaginationButtonProps = Omit<BaseButtonNS.Props, 'children'> & {
type PaginationPreviousProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
children?: React.ReactNode
className?: string
}
type PaginationNextProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
children?: React.ReactNode
className?: string
}
const paginationArrowButtonClassName = [
@ -230,12 +236,12 @@ const paginationArrowButtonClassName = [
'motion-reduce:transition-none',
]
export function PaginationPrevious({
function PaginationPrevious({
className,
children,
'aria-label': ariaLabel,
...props
}: PaginationButtonProps) {
}: PaginationPreviousProps) {
const pagination = usePaginationContext('PaginationPrevious')
if (!pagination.hasPages) return null
@ -260,12 +266,12 @@ export function PaginationPrevious({
)
}
export function PaginationNext({
function PaginationNext({
className,
children,
'aria-label': ariaLabel,
...props
}: PaginationButtonProps) {
}: PaginationNextProps) {
const pagination = usePaginationContext('PaginationNext')
if (!pagination.hasPages) return null
@ -290,12 +296,13 @@ export function PaginationNext({
)
}
export type PaginationPageJumpProps = Omit<BaseButtonNS.Props, 'children'> & {
type PaginationPageJumpProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
inputLabel?: string
children?: React.ReactNode
className?: string
}
export function PaginationPageJump({
function PaginationPageJump({
className,
inputLabel = 'Page number',
children,
@ -402,9 +409,9 @@ export function PaginationPageJump({
)
}
export type PaginationPageListProps = useRender.ComponentProps<'ol'>
type PaginationPageListProps = useRender.ComponentProps<'ol'>
export function PaginationPageList({ render, className, ...props }: PaginationPageListProps) {
function PaginationPageList({ render, className, ...props }: PaginationPageListProps) {
const pagination = usePaginationContext('PaginationPageList')
const defaultProps: useRender.ElementProps<'ol'> = {
@ -427,12 +434,13 @@ export function PaginationPageList({ render, className, ...props }: PaginationPa
})
}
export type PaginationPageProps = Omit<BaseButtonNS.Props, 'children'> & {
type PaginationPageProps = Omit<BaseButtonNS.Props, 'children' | 'className'> & {
page: number
children?: React.ReactNode
className?: string
}
export function PaginationPage({
function PaginationPage({
page,
className,
children,
@ -465,9 +473,9 @@ export function PaginationPage({
)
}
export type PaginationEllipsisProps = useRender.ComponentProps<'span'>
type PaginationEllipsisProps = useRender.ComponentProps<'span'>
export function PaginationEllipsis({ render, className, ...props }: PaginationEllipsisProps) {
function PaginationEllipsis({ render, className, ...props }: PaginationEllipsisProps) {
const defaultProps: useRender.ElementProps<'span'> = {
'aria-hidden': true,
className: cn(
@ -484,7 +492,7 @@ export function PaginationEllipsis({ render, className, ...props }: PaginationEl
})
}
export type PaginationPageSizeProps<Value extends number = number> = {
type PaginationPageSizeProps<Value extends number = number> = {
value: Value
options: readonly Value[]
onValueChange: (value: Value) => void
@ -493,7 +501,7 @@ export type PaginationPageSizeProps<Value extends number = number> = {
className?: string
}
export function PaginationPageSize<Value extends number = number>({
function PaginationPageSize<Value extends number = number>({
value,
options,
onValueChange,
@ -538,14 +546,14 @@ export function PaginationPageSize<Value extends number = number>({
)
}
export type PaginationLabels = {
type PaginationLabels = {
previous?: string
next?: string
editPageNumber?: (page: number, totalPages: number) => string
pageNumberInput?: string
}
export type PaginationPageSizeConfig<Value extends number = number> = {
type PaginationPageSizeConfig<Value extends number = number> = {
value: Value
options: readonly Value[]
onValueChange: (value: Value) => void
@ -553,15 +561,12 @@ export type PaginationPageSizeConfig<Value extends number = number> = {
ariaLabel?: string
}
export type PaginationProps<Value extends number = number> = Omit<
PaginationRootProps,
'children'
> & {
type PaginationProps<Value extends number = number> = Omit<PaginationRootProps, 'children'> & {
labels?: PaginationLabels
pageSize?: PaginationPageSizeConfig<Value>
}
export function Pagination<Value extends number = number>({
function Pagination<Value extends number = number>({
labels,
pageSize,
page,
@ -598,9 +603,9 @@ export function Pagination<Value extends number = number>({
)
}
export type PaginationSkeletonProps = useRender.ComponentProps<'div'>
type PaginationSkeletonProps = useRender.ComponentProps<'div'>
export function PaginationSkeleton({ render, className, ...props }: PaginationSkeletonProps) {
function PaginationSkeleton({ render, className, ...props }: PaginationSkeletonProps) {
const defaultProps: useRender.ElementProps<'div'> = {
'aria-hidden': true,
className: cn(
@ -635,3 +640,32 @@ export function PaginationSkeleton({ render, className, ...props }: PaginationSk
props: mergeProps<'div'>(defaultProps, props),
})
}
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationNavigation,
PaginationNext,
PaginationPage,
PaginationPageJump,
PaginationPageList,
PaginationPageSize,
PaginationPrevious,
PaginationRoot,
PaginationSkeleton,
}
export type {
PaginationContentProps,
PaginationEllipsisProps,
PaginationNavigationProps,
PaginationNextProps,
PaginationPageJumpProps,
PaginationPageListProps,
PaginationPageProps,
PaginationPageSizeProps,
PaginationPreviousProps,
PaginationProps,
PaginationRootProps,
PaginationSkeletonProps,
}

View File

@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Placement } from '.'
import * as React from 'react'
import {
createPopoverHandle,
Popover,
PopoverClose,
PopoverContent,
@ -91,6 +92,60 @@ export const WithActions: Story = {
),
}
type ResourcePopoverPayload = {
label: string
description: string
}
const resourcePopoverPayloads = [
{
label: 'Knowledge base',
description: 'Searches indexed documents before the model generates an answer.',
},
{
label: 'HTTP request',
description: 'Calls an external endpoint with values from the current workflow.',
},
] as const satisfies readonly ResourcePopoverPayload[]
function DetachedTriggersDemo() {
const [popoverHandle] = React.useState(() => createPopoverHandle<ResourcePopoverPayload>())
return (
<div className="flex gap-2">
{resourcePopoverPayloads.map((payload) => (
<PopoverTrigger
key={payload.label}
handle={popoverHandle}
payload={payload}
render={<button type="button" className={triggerButtonClassName} />}
>
{payload.label}
</PopoverTrigger>
))}
<Popover handle={popoverHandle}>
{({ payload }) => (
<PopoverContent>
<div className="flex w-72 flex-col gap-1 p-4">
<PopoverTitle className="text-sm font-semibold text-text-primary">
{payload?.label ?? 'Resource'}
</PopoverTitle>
<PopoverDescription className="text-xs text-text-secondary">
{payload?.description ?? 'Choose a resource to learn more.'}
</PopoverDescription>
</div>
</PopoverContent>
)}
</Popover>
</div>
)
}
export const DetachedTriggers: Story = {
render: () => <DetachedTriggersDemo />,
}
export const Infotip: Story = {
parameters: {
docs: {

View File

@ -7,14 +7,19 @@ import { cn } from '../cn'
import { floatingPopupAnimationClassName } from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
const Popover = BasePopover.Root
const PopoverTrigger = BasePopover.Trigger
const PopoverClose = BasePopover.Close
const PopoverTitle = BasePopover.Title
const PopoverDescription = BasePopover.Description
const createPopoverHandle = BasePopover.createHandle
export const Popover = BasePopover.Root
export const PopoverTrigger = BasePopover.Trigger
export const PopoverClose = BasePopover.Close
export const PopoverTitle = BasePopover.Title
export const PopoverDescription = BasePopover.Description
export const createPopoverHandle = BasePopover.createHandle
type PopoverProps<Payload = unknown> = BasePopover.Root.Props<Payload>
type PopoverHandle<Payload = unknown> = BasePopover.Handle<Payload>
type PopoverTriggerProps<Payload = unknown> = BasePopover.Trigger.Props<Payload>
type PopoverCloseProps = BasePopover.Close.Props
type PopoverTitleProps = BasePopover.Title.Props
type PopoverDescriptionProps = BasePopover.Description.Props
type PopoverContentProps = {
children: React.ReactNode
@ -30,7 +35,7 @@ type PopoverContentProps = {
popupProps?: Omit<BasePopover.Popup.Props, 'children' | 'className'>
}
export function PopoverContent({
function PopoverContent({
children,
placement = 'bottom',
sideOffset = 8,
@ -67,3 +72,23 @@ export function PopoverContent({
</BasePopover.Portal>
)
}
export {
createPopoverHandle,
Popover,
PopoverClose,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
}
export type {
Placement,
PopoverCloseProps,
PopoverContentProps,
PopoverDescriptionProps,
PopoverHandle,
PopoverProps,
PopoverTitleProps,
PopoverTriggerProps,
}

View File

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Placement } from '.'
import type { PreviewCardContentProps } from '.'
import * as React from 'react'
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '.'
@ -33,7 +33,68 @@ type Story = StoryObj<typeof meta>
// destination. The Wikipedia URL and Unsplash image are the exact assets used
// in base-ui.com's public docs so the story renders a real preview.
// https://base-ui.com/react/components/preview-card
const typographyPreview = createPreviewCardHandle()
type TypographyPreviewPayload = {
title: string
description: string
image: {
src: string
alt: string
}
}
const typographyPreviewPayload = {
title: 'Typography',
description:
'Typography is the art and science of arranging type to make written language legible, readable, and visually appealing.',
image: {
src: 'https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300',
alt: 'Station Hofplein signage in Rotterdam, Netherlands',
},
} satisfies TypographyPreviewPayload
function LinkPreviewDemo() {
const [previewCardHandle] = React.useState(() =>
createPreviewCardHandle<TypographyPreviewPayload>(),
)
return (
<div className="max-w-md p-6 text-sm leading-6 text-text-secondary">
<p>
The principles of good{' '}
<PreviewCardTrigger
handle={previewCardHandle}
payload={typographyPreviewPayload}
href="https://en.wikipedia.org/wiki/Typography"
target="_blank"
rel="noreferrer"
className={inlineLinkClassName}
>
typography
</PreviewCardTrigger>{' '}
remain in the digital age.
</p>
<PreviewCard handle={previewCardHandle}>
{({ payload = typographyPreviewPayload }) => (
<PreviewCardContent popupClassName="w-[240px] p-2">
<div className="flex flex-col gap-2">
<img
width="224"
height="150"
className="block max-w-none rounded-md"
src={payload.image.src}
alt={payload.image.alt}
/>
<p className="m-0 text-xs leading-5 text-text-secondary">
<strong className="text-text-primary">{payload.title}</strong> {payload.description}
</p>
</div>
</PreviewCardContent>
)}
</PreviewCard>
</div>
)
}
export const LinkPreview: Story = {
name: 'Link preview (canonical)',
@ -45,44 +106,12 @@ export const LinkPreview: Story = {
},
},
},
render: () => (
<div className="max-w-md p-6 text-sm leading-6 text-text-secondary">
<p>
The principles of good{' '}
<PreviewCardTrigger
handle={typographyPreview}
href="https://en.wikipedia.org/wiki/Typography"
target="_blank"
rel="noreferrer"
className={inlineLinkClassName}
>
typography
</PreviewCardTrigger>{' '}
remain in the digital age.
</p>
<PreviewCard handle={typographyPreview}>
<PreviewCardContent popupClassName="w-[240px] p-2">
<div className="flex flex-col gap-2">
<img
width="224"
height="150"
className="block max-w-none rounded-md"
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className="m-0 text-xs leading-5 text-text-secondary">
<strong className="text-text-primary">Typography</strong> is the art and science of
arranging type to make written language legible, readable, and visually appealing.
</p>
</div>
</PreviewCardContent>
</PreviewCard>
</div>
),
render: () => <LinkPreviewDemo />,
}
const PLACEMENTS: Placement[] = [
type PreviewCardPlacement = NonNullable<PreviewCardContentProps['placement']>
const PLACEMENTS: PreviewCardPlacement[] = [
'top-start',
'top',
'top-end',
@ -98,7 +127,7 @@ const PLACEMENTS: Placement[] = [
]
const PlacementsDemo = () => {
const [placement, setPlacement] = React.useState<Placement>('bottom')
const [placement, setPlacement] = React.useState<PreviewCardPlacement>('bottom')
return (
<div className="flex flex-col items-center gap-4 p-20">

View File

@ -7,8 +7,6 @@ import { cn } from '../cn'
import { floatingPopupAnimationClassName } from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
/**
* PreviewCard follows Base UI's canonical semantics: a hover/focus-triggered
* visual enhancement for a link that previews its destination.
@ -21,10 +19,15 @@ export type { Placement }
* opening the popup is itself the trigger's purpose or its content must be
* accessible across input modes.
*/
export const PreviewCard = BasePreviewCard.Root
export const PreviewCardTrigger = BasePreviewCard.Trigger
export const PreviewCardViewport = BasePreviewCard.Viewport
export const createPreviewCardHandle = BasePreviewCard.createHandle
const PreviewCard = BasePreviewCard.Root
const PreviewCardTrigger = BasePreviewCard.Trigger
const PreviewCardViewport = BasePreviewCard.Viewport
const createPreviewCardHandle = BasePreviewCard.createHandle
type PreviewCardProps<Payload = unknown> = BasePreviewCard.Root.Props<Payload>
type PreviewCardHandle<Payload = unknown> = BasePreviewCard.Handle<Payload>
type PreviewCardTriggerProps<Payload = unknown> = BasePreviewCard.Trigger.Props<Payload>
type PreviewCardViewportProps = BasePreviewCard.Viewport.Props
type PreviewCardContentProps = {
children: React.ReactNode
@ -40,7 +43,7 @@ type PreviewCardContentProps = {
popupProps?: Omit<BasePreviewCard.Popup.Props, 'children' | 'className'>
}
export function PreviewCardContent({
function PreviewCardContent({
children,
placement = 'bottom',
sideOffset = 8,
@ -76,3 +79,18 @@ export function PreviewCardContent({
</BasePreviewCard.Portal>
)
}
export {
createPreviewCardHandle,
PreviewCard,
PreviewCardContent,
PreviewCardTrigger,
PreviewCardViewport,
}
export type {
PreviewCardContentProps,
PreviewCardHandle,
PreviewCardProps,
PreviewCardTriggerProps,
PreviewCardViewportProps,
}

View File

@ -1,8 +1,11 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { ProgressCircleColor, ProgressCircleSize } from '.'
import type { ProgressCircleProps } from '.'
import * as React from 'react'
import { ProgressCircle } from '.'
type ProgressCircleColor = NonNullable<ProgressCircleProps['color']>
type ProgressCircleSize = NonNullable<ProgressCircleProps['size']>
const colors: ProgressCircleColor[] = ['gray', 'white', 'blue', 'warning', 'error']
const sizes: ProgressCircleSize[] = ['small', 'medium', 'large']

View File

@ -46,10 +46,8 @@ const progressCircleColorClasses = {
},
} as const
export type ProgressCircleSize = NonNullable<
VariantProps<typeof progressCircleRootVariants>['size']
>
export type ProgressCircleColor = keyof typeof progressCircleColorClasses
type ProgressCircleSize = NonNullable<VariantProps<typeof progressCircleRootVariants>['size']>
type ProgressCircleColor = keyof typeof progressCircleColorClasses
const progressCircleSizeValues = {
small: 12,
@ -67,7 +65,7 @@ type ProgressCircleAccessibleNameProps =
'aria-labelledby': string
}
export type ProgressCircleProps = Omit<
type ProgressCircleProps = Omit<
BaseProgress.Root.Props,
'children' | 'className' | 'aria-label' | 'aria-labelledby'
> &
@ -113,7 +111,7 @@ function getSectorPath(size: number, percentage: number | null) {
`
}
export function ProgressCircle({
function ProgressCircle({
className,
color = 'blue',
size = 'small',
@ -163,3 +161,6 @@ export function ProgressCircle({
</BaseProgress.Root>
)
}
export { ProgressCircle }
export type { ProgressCircleProps }

View File

@ -0,0 +1,188 @@
// Compile-time public API contracts; included by package typecheck and excluded from package exports.
import type {
AutocompleteCollection,
AutocompleteCollectionProps,
AutocompleteGroup,
AutocompleteGroupProps,
AutocompleteItemProps,
AutocompleteList,
AutocompleteListProps,
AutocompleteProps,
} from './autocomplete'
import type {
ComboboxCollection,
ComboboxCollectionProps,
ComboboxGroup,
ComboboxGroupProps,
ComboboxItemProps,
ComboboxList,
ComboboxListProps,
ComboboxProps,
ComboboxValue,
ComboboxValueProps,
} from './combobox'
import type { ContextMenuRadioGroupProps, ContextMenuRadioItemProps } from './context-menu'
import type { DialogHandle, DialogTriggerProps } from './dialog'
import type { DrawerHandle, DrawerTriggerProps } from './drawer'
import type { DropdownMenuRadioGroupProps, DropdownMenuRadioItemProps } from './dropdown-menu'
import type { FormProps } from './form'
import type { PopoverHandle, PopoverTriggerProps } from './popover'
import type { PreviewCardHandle, PreviewCardTriggerProps } from './preview-card'
import type { RadioGroupProps, RadioItemProps } from './radio'
import type { SelectItemProps, SelectProps, SelectValue, SelectValueProps } from './select'
import type { SliderRootProps } from './slider'
type Equal<Left, Right> =
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2
? true
: false
type Expect<Condition extends true> = Condition
type Arguments<Callback> = Callback extends (...args: infer Values) => unknown ? Values : never
type FirstArgument<Callback> = Arguments<Callback>[0]
type SecondArgument<Callback> = Arguments<Callback>[1]
type IsRequired<Props, Key extends keyof Props> =
Record<never, never> extends Pick<Props, Key> ? false : true
type Callable<Member> = Member extends (...args: infer Values) => infer Result
? (...args: Values) => Result
: never
type DomainValue = { id: string }
type FormValues = { enabled: boolean; name: string }
type RangeValue = readonly [number, number]
type OverlayPayload = { sourceId: string }
type SelectSingleValueProps = Parameters<typeof SelectValue<DomainValue>>[0]
type SelectMultipleValueProps = Parameters<typeof SelectValue<DomainValue, true>>[0]
type SelectDynamicValueProps = Parameters<typeof SelectValue<DomainValue, boolean>>[0]
type ComboboxSingleValueProps = Parameters<typeof ComboboxValue<DomainValue>>[0]
type ComboboxMultipleValueProps = Parameters<typeof ComboboxValue<DomainValue, true>>[0]
type ComboboxDynamicValueProps = Parameters<typeof ComboboxValue<DomainValue, boolean>>[0]
type AutocompleteListRuntimeProps = Parameters<typeof AutocompleteList<DomainValue>>[0]
type AutocompleteCollectionRuntimeProps = Parameters<typeof AutocompleteCollection<DomainValue>>[0]
type AutocompleteGroupRuntimeProps = Parameters<typeof AutocompleteGroup<DomainValue>>[0]
type ComboboxListRuntimeProps = Parameters<typeof ComboboxList<DomainValue>>[0]
type ComboboxCollectionRuntimeProps = Parameters<typeof ComboboxCollection<DomainValue>>[0]
type ComboboxGroupRuntimeProps = Parameters<typeof ComboboxGroup<DomainValue>>[0]
type PublicTypeAssertions = [
Expect<Equal<FirstArgument<NonNullable<FormProps<FormValues>['onFormSubmit']>>, FormValues>>,
Expect<
Equal<
FirstArgument<NonNullable<SelectProps<DomainValue, false>['onValueChange']>>,
DomainValue | null
>
>,
Expect<
Equal<
FirstArgument<NonNullable<SelectProps<DomainValue, true>['onValueChange']>>,
DomainValue[]
>
>,
Expect<Equal<IsRequired<SelectProps<DomainValue, true>, 'multiple'>, true>>,
Expect<Equal<IsRequired<SelectProps<DomainValue, boolean>, 'multiple'>, false>>,
Expect<Equal<SelectItemProps<DomainValue>['value'], DomainValue | undefined>>,
Expect<
Equal<
FirstArgument<Callable<SelectValueProps<DomainValue, true>['children']>>,
DomainValue[] | null
>
>,
Expect<Equal<FirstArgument<Callable<SelectSingleValueProps['children']>>, DomainValue | null>>,
Expect<
Equal<FirstArgument<Callable<SelectMultipleValueProps['children']>>, DomainValue[] | null>
>,
Expect<
Equal<
FirstArgument<Callable<SelectDynamicValueProps['children']>>,
DomainValue | DomainValue[] | null
>
>,
Expect<
Equal<
FirstArgument<NonNullable<ComboboxProps<DomainValue, true>['onValueChange']>>,
DomainValue[]
>
>,
Expect<Equal<IsRequired<ComboboxProps<DomainValue, true>, 'multiple'>, true>>,
Expect<Equal<IsRequired<ComboboxProps<DomainValue, boolean>, 'multiple'>, false>>,
Expect<Equal<ComboboxItemProps<DomainValue>['value'], DomainValue | undefined>>,
Expect<
Equal<
FirstArgument<Callable<ComboboxValueProps<DomainValue, true>['children']>>,
DomainValue[] | null
>
>,
Expect<Equal<FirstArgument<Callable<ComboboxSingleValueProps['children']>>, DomainValue | null>>,
Expect<
Equal<FirstArgument<Callable<ComboboxMultipleValueProps['children']>>, DomainValue[] | null>
>,
Expect<
Equal<
FirstArgument<Callable<ComboboxDynamicValueProps['children']>>,
DomainValue | DomainValue[] | null
>
>,
Expect<
Equal<
FirstArgument<NonNullable<AutocompleteProps<DomainValue>['onItemHighlighted']>>,
DomainValue | undefined
>
>,
Expect<Equal<AutocompleteItemProps<DomainValue>['value'], DomainValue | undefined>>,
Expect<
Equal<FirstArgument<Callable<AutocompleteListProps<DomainValue>['children']>>, DomainValue>
>,
Expect<Equal<SecondArgument<Callable<AutocompleteListProps<DomainValue>['children']>>, number>>,
Expect<Equal<FirstArgument<Callable<AutocompleteListRuntimeProps['children']>>, DomainValue>>,
Expect<Equal<FirstArgument<AutocompleteCollectionProps<DomainValue>['children']>, DomainValue>>,
Expect<Equal<SecondArgument<AutocompleteCollectionProps<DomainValue>['children']>, number>>,
Expect<Equal<FirstArgument<AutocompleteCollectionRuntimeProps['children']>, DomainValue>>,
Expect<Equal<AutocompleteGroupProps<DomainValue>['items'], readonly DomainValue[] | undefined>>,
Expect<Equal<AutocompleteGroupRuntimeProps['items'], readonly DomainValue[] | undefined>>,
Expect<Equal<FirstArgument<Callable<ComboboxListProps<DomainValue>['children']>>, DomainValue>>,
Expect<Equal<SecondArgument<Callable<ComboboxListProps<DomainValue>['children']>>, number>>,
Expect<Equal<FirstArgument<Callable<ComboboxListRuntimeProps['children']>>, DomainValue>>,
Expect<Equal<FirstArgument<ComboboxCollectionProps<DomainValue>['children']>, DomainValue>>,
Expect<Equal<SecondArgument<ComboboxCollectionProps<DomainValue>['children']>, number>>,
Expect<Equal<FirstArgument<ComboboxCollectionRuntimeProps['children']>, DomainValue>>,
Expect<Equal<ComboboxGroupProps<DomainValue>['items'], readonly DomainValue[] | undefined>>,
Expect<Equal<ComboboxGroupRuntimeProps['items'], readonly DomainValue[] | undefined>>,
Expect<
Equal<
FirstArgument<NonNullable<ContextMenuRadioGroupProps<DomainValue>['onValueChange']>>,
DomainValue
>
>,
Expect<Equal<ContextMenuRadioItemProps<DomainValue>['value'], DomainValue>>,
Expect<
Equal<
FirstArgument<NonNullable<DropdownMenuRadioGroupProps<DomainValue>['onValueChange']>>,
DomainValue
>
>,
Expect<Equal<DropdownMenuRadioItemProps<DomainValue>['value'], DomainValue>>,
Expect<
Equal<FirstArgument<NonNullable<RadioGroupProps<DomainValue>['onValueChange']>>, DomainValue>
>,
Expect<Equal<RadioItemProps<DomainValue>['value'], DomainValue>>,
Expect<Equal<SliderRootProps<RangeValue>['value'], RangeValue | undefined>>,
Expect<
Equal<FirstArgument<NonNullable<SliderRootProps<RangeValue>['onValueChange']>>, RangeValue>
>,
Expect<
Equal<NonNullable<DialogTriggerProps<OverlayPayload>['handle']>, DialogHandle<OverlayPayload>>
>,
Expect<
Equal<NonNullable<DrawerTriggerProps<OverlayPayload>['handle']>, DrawerHandle<OverlayPayload>>
>,
Expect<
Equal<NonNullable<PopoverTriggerProps<OverlayPayload>['handle']>, PopoverHandle<OverlayPayload>>
>,
Expect<
Equal<
NonNullable<PreviewCardTriggerProps<OverlayPayload>['handle']>,
PreviewCardHandle<OverlayPayload>
>
>,
]
export type { PublicTypeAssertions }

View File

@ -7,30 +7,30 @@ import { Radio as BaseRadio } from '@base-ui/react/radio'
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group'
import { cn } from '../cn'
export type RadioGroupProps<Value = string> = Omit<BaseRadioGroupNS.Props<Value>, 'className'> & {
type RadioGroupProps<Value = string> = Omit<BaseRadioGroupNS.Props<Value>, 'className'> & {
className?: string
}
export function RadioGroup<Value = string>({ className, ...props }: RadioGroupProps<Value>) {
function RadioGroup<Value = string>({ className, ...props }: RadioGroupProps<Value>) {
return <BaseRadioGroup<Value> className={cn('flex items-center gap-2', className)} {...props} />
}
export type RadioItemProps<Value = string> = Omit<BaseRadioNS.Root.Props<Value>, 'className'> & {
type RadioItemProps<Value = string> = Omit<BaseRadioNS.Root.Props<Value>, 'className'> & {
className?: string
}
export function RadioItem<Value = string>({ className, ...props }: RadioItemProps<Value>) {
function RadioItem<Value = string>({ className, ...props }: RadioItemProps<Value>) {
return <BaseRadio.Root<Value> className={className} {...props} />
}
export type RadioControlProps = Omit<
type RadioControlProps = Omit<
BaseRadioNS.Indicator.Props,
'className' | 'children' | 'keepMounted'
> & {
className?: string
}
export function RadioControl({ className, ...props }: RadioControlProps) {
function RadioControl({ className, ...props }: RadioControlProps) {
return (
<BaseRadio.Indicator
{...props}
@ -51,9 +51,9 @@ export function RadioControl({ className, ...props }: RadioControlProps) {
)
}
export type RadioProps<Value = string> = Omit<RadioItemProps<Value>, 'children'>
type RadioProps<Value = string> = Omit<RadioItemProps<Value>, 'children'>
export function Radio<Value = string>({ className, ...props }: RadioProps<Value>) {
function Radio<Value = string>({ className, ...props }: RadioProps<Value>) {
return (
<BaseRadio.Root<Value>
className={cn(
@ -73,9 +73,9 @@ export function Radio<Value = string>({ className, ...props }: RadioProps<Value>
)
}
export type RadioSkeletonProps = React.ComponentProps<'div'>
type RadioSkeletonProps = React.ComponentProps<'div'>
export function RadioSkeleton({ className, ...props }: RadioSkeletonProps) {
function RadioSkeleton({ className, ...props }: RadioSkeletonProps) {
return (
<div
className={cn('size-4 shrink-0 rounded-full bg-text-quaternary opacity-20', className)}
@ -83,3 +83,7 @@ export function RadioSkeleton({ className, ...props }: RadioSkeletonProps) {
/>
)
}
export { Radio, RadioControl, RadioGroup, RadioItem, RadioSkeleton }
export type { RadioControlProps, RadioGroupProps, RadioItemProps, RadioProps, RadioSkeletonProps }

View File

@ -4,20 +4,22 @@ import type * as React from 'react'
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
import { cn } from '../cn'
export const ScrollAreaRoot = BaseScrollArea.Root
const ScrollAreaRoot = BaseScrollArea.Root
type ScrollAreaRootProps = BaseScrollArea.Root.Props
export const ScrollAreaContent = BaseScrollArea.Content
const ScrollAreaContent = BaseScrollArea.Content
type ScrollAreaContentProps = BaseScrollArea.Content.Props
type ScrollAreaSlotClassNames = {
viewport?: string
content?: string
scrollbar?: string
}
type ScrollAreaOrientation = NonNullable<BaseScrollArea.Scrollbar.Props['orientation']>
type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
children: React.ReactNode
orientation?: 'vertical' | 'horizontal'
orientation?: ScrollAreaOrientation
slotClassNames?: ScrollAreaSlotClassNames
label?: string
labelledBy?: string
@ -47,17 +49,21 @@ const scrollAreaViewportClassName = cn(
const scrollAreaCornerClassName = 'bg-transparent'
type ScrollAreaViewportProps = BaseScrollArea.Viewport.Props
type ScrollAreaViewportProps = Omit<BaseScrollArea.Viewport.Props, 'className'> & {
className?: string
}
export function ScrollAreaViewport({ className, ...props }: ScrollAreaViewportProps) {
function ScrollAreaViewport({ className, ...props }: ScrollAreaViewportProps) {
return (
<BaseScrollArea.Viewport className={cn(scrollAreaViewportClassName, className)} {...props} />
)
}
type ScrollAreaScrollbarProps = BaseScrollArea.Scrollbar.Props
type ScrollAreaScrollbarProps = Omit<BaseScrollArea.Scrollbar.Props, 'className'> & {
className?: string
}
export function ScrollAreaScrollbar({ className, ...props }: ScrollAreaScrollbarProps) {
function ScrollAreaScrollbar({ className, ...props }: ScrollAreaScrollbarProps) {
return (
<BaseScrollArea.Scrollbar
data-dify-scrollbar=""
@ -67,19 +73,23 @@ export function ScrollAreaScrollbar({ className, ...props }: ScrollAreaScrollbar
)
}
type ScrollAreaThumbProps = BaseScrollArea.Thumb.Props
type ScrollAreaThumbProps = Omit<BaseScrollArea.Thumb.Props, 'className'> & {
className?: string
}
export function ScrollAreaThumb({ className, ...props }: ScrollAreaThumbProps) {
function ScrollAreaThumb({ className, ...props }: ScrollAreaThumbProps) {
return <BaseScrollArea.Thumb className={cn(scrollAreaThumbClassName, className)} {...props} />
}
type ScrollAreaCornerProps = BaseScrollArea.Corner.Props
type ScrollAreaCornerProps = Omit<BaseScrollArea.Corner.Props, 'className'> & {
className?: string
}
export function ScrollAreaCorner({ className, ...props }: ScrollAreaCornerProps) {
function ScrollAreaCorner({ className, ...props }: ScrollAreaCornerProps) {
return <BaseScrollArea.Corner className={cn(scrollAreaCornerClassName, className)} {...props} />
}
export function ScrollArea({
function ScrollArea({
children,
className,
orientation = 'vertical',
@ -104,3 +114,23 @@ export function ScrollArea({
</ScrollAreaRoot>
)
}
export {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
}
export type {
ScrollAreaContentProps,
ScrollAreaCornerProps,
ScrollAreaProps,
ScrollAreaRootProps,
ScrollAreaScrollbarProps,
ScrollAreaThumbProps,
ScrollAreaViewportProps,
}

View File

@ -7,14 +7,14 @@ import { Toggle as BaseToggle } from '@base-ui/react/toggle'
import { ToggleGroup as BaseToggleGroup } from '@base-ui/react/toggle-group'
import { cn } from '../cn'
export type SegmentedControlProps<Value extends string = string> = Omit<
type SegmentedControlProps<Value extends string = string> = Omit<
BaseToggleGroupNS.Props<Value>,
'className'
> & {
className?: string
}
export function SegmentedControl<Value extends string = string>({
function SegmentedControl<Value extends string = string>({
className,
...props
}: SegmentedControlProps<Value>) {
@ -29,14 +29,14 @@ export function SegmentedControl<Value extends string = string>({
)
}
export type SegmentedControlItemProps<Value extends string = string> = Omit<
type SegmentedControlItemProps<Value extends string = string> = Omit<
BaseToggleNS.Props<Value>,
'className'
> & {
className?: string
}
export function SegmentedControlItem<Value extends string = string>({
function SegmentedControlItem<Value extends string = string>({
className,
...props
}: SegmentedControlItemProps<Value>) {
@ -51,11 +51,11 @@ export function SegmentedControlItem<Value extends string = string>({
)
}
export type SegmentedControlDividerProps = Omit<React.ComponentProps<'span'>, 'className'> & {
type SegmentedControlDividerProps = Omit<React.ComponentProps<'span'>, 'className'> & {
className?: string
}
export function SegmentedControlDivider({ className, ...props }: SegmentedControlDividerProps) {
function SegmentedControlDivider({ className, ...props }: SegmentedControlDividerProps) {
return (
<span
role="presentation"
@ -65,3 +65,7 @@ export function SegmentedControlDivider({ className, ...props }: SegmentedContro
/>
)
}
export { SegmentedControl, SegmentedControlDivider, SegmentedControlItem }
export type { SegmentedControlDividerProps, SegmentedControlItemProps, SegmentedControlProps }

View File

@ -89,6 +89,27 @@ describe('Select wrappers', () => {
expect(hiddenInput).toHaveAttribute('autocomplete', 'address-level2')
expect(new FormData(form).get('city')).toBe('seattle')
})
it('should expose a controlled null value to a typed multiple value renderer', async () => {
const screen = await renderWithSafeViewport(
<Select<string, true> multiple value={null}>
<SelectTrigger aria-label="city select">
<SelectValue<string, true>>
{(selectedValue) =>
selectedValue === null ? 'No cities selected' : selectedValue.join(', ')
}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem<string> value="seattle">
<SelectItemText>Seattle</SelectItemText>
</SelectItem>
</SelectContent>
</Select>,
)
await expect.element(screen.getByText('No cities selected')).toBeInTheDocument()
})
})
describe('SelectTrigger', () => {

View File

@ -27,6 +27,14 @@ const cityItems = [
{ label: 'Paris', value: 'paris' },
]
const deploymentRegionItems = [
{ label: 'US East', value: 'us-east' },
{ label: 'Europe West', value: 'eu-west' },
{ label: 'Asia Pacific', value: 'ap-southeast' },
] as const
type DeploymentRegion = (typeof deploymentRegionItems)[number]['value']
const meta = {
title: 'Base/Form/Select',
component: Select,
@ -35,7 +43,7 @@ const meta = {
docs: {
description: {
component:
'Compound select built on Base UI Select. Compose `SelectTrigger`, `SelectContent`, and `SelectItem` to build accessible single-value pickers with groups, labels, separators, and keyboard selection.',
'Compound select built on Base UI Select. Compose `SelectTrigger`, `SelectContent`, and `SelectItem` to build accessible single- or multiple-value pickers with groups, labels, separators, and keyboard selection.',
},
},
},
@ -328,6 +336,68 @@ export const Controlled: Story = {
render: () => <ControlledDemo />,
}
const MultipleControlledDemo = () => {
const [value, setValue] = React.useState<DeploymentRegion[]>(['us-east', 'eu-west'])
return (
<div className={triggerWidth}>
<Select<DeploymentRegion, true>
items={deploymentRegionItems}
multiple
value={value}
onValueChange={setValue}
>
<SelectLabel>Deployment regions</SelectLabel>
<SelectTrigger>
<SelectValue<DeploymentRegion, true>>
{(selectedRegions) => {
if (!selectedRegions?.length) return 'Choose regions'
const [firstSelectedRegion] = selectedRegions
if (!firstSelectedRegion) return 'Choose regions'
const firstRegion = deploymentRegionItems.find(
(item) => item.value === firstSelectedRegion,
)
const additionalRegionCount = selectedRegions.length - 1
const firstRegionLabel = firstRegion?.label ?? firstSelectedRegion
return additionalRegionCount > 0
? `${firstRegionLabel} (+${additionalRegionCount} more)`
: firstRegionLabel
}}
</SelectValue>
</SelectTrigger>
<SelectContent listProps={{ 'aria-label': 'Deployment region options' }}>
{deploymentRegionItems.map((item) => (
<SelectItem<DeploymentRegion> key={item.value} value={item.value}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
export const MultipleControlled: Story = {
render: () => <MultipleControlledDemo />,
play: async ({ canvas, canvasElement, userEvent }) => {
const trigger = canvas.getByRole('combobox', { name: 'Deployment regions' })
const body = within(canvasElement.ownerDocument.body)
await expect(trigger).toHaveTextContent('US East (+1 more)')
await userEvent.click(trigger)
const asiaPacificOption = await body.findByRole('option', { name: 'Asia Pacific' })
await userEvent.click(asiaPacificOption)
await expect(trigger).toHaveTextContent('US East (+2 more)')
await expect(asiaPacificOption).toHaveAttribute('aria-selected', 'true')
},
}
export const InForm: Story = {
render: () => (
<Form aria-label="Timezone form" className="grid w-72 gap-3" onFormSubmit={() => undefined}>

View File

@ -1,5 +1,6 @@
'use client'
import type { ComponentRenderFn, HTMLProps } from '@base-ui/react/types'
import type { VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import type { Placement } from '../placement'
@ -15,21 +16,49 @@ import {
} from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
export type SelectProps<
type SelectProps<Value, Multiple extends boolean | undefined = false> = BaseSelect.Root.Props<
Value,
Multiple extends boolean | undefined = false,
> = BaseSelect.Root.Props<Value, Multiple>
Multiple
> &
([Multiple] extends [true] ? { multiple: true } : unknown)
export function Select<Value, Multiple extends boolean | undefined = false>(
function Select<Value, Multiple extends boolean | undefined = false>(
props: SelectProps<Value, Multiple>,
): React.JSX.Element {
return <BaseSelect.Root {...props} />
}
export const SelectValue = BaseSelect.Value
export const SelectGroup = BaseSelect.Group
const SelectGroup = BaseSelect.Group
type SelectSelectedValue<Value, Multiple extends boolean | undefined = false> =
| (Multiple extends true ? Value[] : Value)
| null
type SelectValueState<Value = unknown, Multiple extends boolean | undefined = false> = Omit<
BaseSelect.Value.State,
'value'
> & {
value: SelectSelectedValue<Value, Multiple>
}
type SelectValueProps<Value = unknown, Multiple extends boolean | undefined = false> = Omit<
BaseSelect.Value.Props,
'children' | 'className' | 'render' | 'style'
> & {
children?: React.ReactNode | ((value: SelectSelectedValue<Value, Multiple>) => React.ReactNode)
className?: string | ((state: SelectValueState<Value, Multiple>) => string | undefined)
render?:
| React.ReactElement
| ComponentRenderFn<HTMLProps<HTMLSpanElement>, SelectValueState<Value, Multiple>>
style?:
| React.CSSProperties
| ((state: SelectValueState<Value, Multiple>) => React.CSSProperties | undefined)
}
function SelectValue<Value = unknown, Multiple extends boolean | undefined = false>(
props: SelectValueProps<Value, Multiple>,
): React.JSX.Element
function SelectValue(props: BaseSelect.Value.Props): React.JSX.Element {
return <BaseSelect.Value {...props} />
}
type SelectGroupProps = BaseSelect.Group.Props
const selectTriggerVariants = cva(
[
@ -55,10 +84,13 @@ const selectTriggerVariants = cva(
},
)
type SelectTriggerProps = Omit<BaseSelect.Trigger.Props, 'className'> &
VariantProps<typeof selectTriggerVariants> & { className?: string }
type SelectSize = NonNullable<VariantProps<typeof selectTriggerVariants>['size']>
type SelectTriggerProps = Omit<BaseSelect.Trigger.Props, 'className'> & {
className?: string
size?: SelectSize
}
export function SelectTrigger({ className, children, size, ...props }: SelectTriggerProps) {
function SelectTrigger({ className, children, size, ...props }: SelectTriggerProps) {
return (
<BaseSelect.Trigger className={cn(selectTriggerVariants({ size, className }))} {...props}>
<span className="min-w-0 grow truncate">{children}</span>
@ -69,18 +101,33 @@ export function SelectTrigger({ className, children, size, ...props }: SelectTri
)
}
export function SelectLabel({ className, ...props }: BaseSelect.Label.Props) {
type SelectLabelProps = Omit<BaseSelect.Label.Props, 'className'> & { className?: string }
function SelectLabel({ className, ...props }: SelectLabelProps) {
return <BaseSelect.Label className={cn(formLabelClassName, className)} {...props} />
}
export function SelectGroupLabel({ className, ...props }: BaseSelect.GroupLabel.Props) {
type SelectGroupLabelProps = Omit<BaseSelect.GroupLabel.Props, 'className'> & {
className?: string
}
function SelectGroupLabel({ className, ...props }: SelectGroupLabelProps) {
return <BaseSelect.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
}
export function SelectSeparator({ className, ...props }: BaseSelect.Separator.Props) {
type SelectSeparatorProps = Omit<BaseSelect.Separator.Props, 'className'> & { className?: string }
function SelectSeparator({ className, ...props }: SelectSeparatorProps) {
return <BaseSelect.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
}
type SelectContentPositionerProps = Omit<
BaseSelect.Positioner.Props,
'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset'
>
type SelectContentPopupProps = Omit<BaseSelect.Popup.Props, 'children' | 'className'>
type SelectContentListProps = Omit<BaseSelect.List.Props, 'children' | 'className'>
type SelectContentProps = {
children: React.ReactNode
placement?: Placement
@ -89,15 +136,12 @@ type SelectContentProps = {
className?: string
popupClassName?: string
listClassName?: string
positionerProps?: Omit<
BaseSelect.Positioner.Props,
'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset'
>
popupProps?: Omit<BaseSelect.Popup.Props, 'children' | 'className'>
listProps?: Omit<BaseSelect.List.Props, 'children' | 'className'>
positionerProps?: SelectContentPositionerProps
popupProps?: SelectContentPopupProps
listProps?: SelectContentListProps
}
export function SelectContent({
function SelectContent({
children,
placement = 'bottom-start',
sideOffset = 4,
@ -142,7 +186,12 @@ export function SelectContent({
)
}
export function SelectItem({ className, ...props }: BaseSelect.Item.Props) {
type SelectItemProps<Value = unknown> = Omit<BaseSelect.Item.Props, 'className' | 'value'> & {
className?: string
value?: Value
}
function SelectItem<Value = unknown>({ className, ...props }: SelectItemProps<Value>) {
return (
<BaseSelect.Item
className={cn(
@ -155,19 +204,50 @@ export function SelectItem({ className, ...props }: BaseSelect.Item.Props) {
)
}
export function SelectItemText({ className, ...props }: BaseSelect.ItemText.Props) {
type SelectItemTextProps = Omit<BaseSelect.ItemText.Props, 'className'> & { className?: string }
function SelectItemText({ className, ...props }: SelectItemTextProps) {
return (
<BaseSelect.ItemText className={cn('me-1 min-w-0 grow truncate px-1', className)} {...props} />
)
}
export function SelectItemIndicator({
className,
...props
}: Omit<BaseSelect.ItemIndicator.Props, 'children'>) {
type SelectItemIndicatorProps = Omit<BaseSelect.ItemIndicator.Props, 'children' | 'className'> & {
className?: string
}
function SelectItemIndicator({ className, ...props }: SelectItemIndicatorProps) {
return (
<BaseSelect.ItemIndicator className={cn(floatingItemIndicatorClassName, className)} {...props}>
<span className="i-ri-check-line h-4 w-4" aria-hidden />
</BaseSelect.ItemIndicator>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
}
export type {
SelectContentProps,
SelectGroupLabelProps,
SelectGroupProps,
SelectItemIndicatorProps,
SelectItemProps,
SelectItemTextProps,
SelectLabelProps,
SelectProps,
SelectSeparatorProps,
SelectTriggerProps,
SelectValueProps,
}

View File

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { SliderProps } from '.'
import * as React from 'react'
import {
Slider,
@ -49,7 +50,7 @@ function SliderDemo({
value: initialValue = 50,
defaultValue: _defaultValue,
...args
}: React.ComponentProps<typeof Slider>) {
}: SliderProps) {
const [value, setValue] = React.useState(initialValue)
return (
@ -107,3 +108,37 @@ export const ComposedWithLabel: Story = {
</SliderRoot>
),
}
type PriceRange = readonly [number, number]
function RangeSliderDemo() {
const [range, setRange] = React.useState<PriceRange>([25, 75])
return (
<div className="w-[320px] space-y-3">
<SliderRoot<PriceRange>
value={range}
onValueChange={setRange}
min={0}
max={100}
className="group/slider relative inline-flex w-full flex-col gap-1 data-disabled:opacity-30"
>
<SliderLabel>Price range</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderIndicator />
</SliderTrack>
<SliderThumb aria-label="Minimum price" />
<SliderThumb aria-label="Maximum price" />
</SliderControl>
</SliderRoot>
<div className="text-center system-sm-medium text-text-secondary">
{range[0]} {range[1]}
</div>
</div>
)
}
export const Range: Story = {
render: () => <RangeSliderDemo />,
}

View File

@ -4,22 +4,27 @@ import { Slider as BaseSlider } from '@base-ui/react/slider'
import { cn } from '../cn'
import { formLabelClassName } from '../form-control-shared'
export const SliderRoot = BaseSlider.Root
const SliderRoot = BaseSlider.Root
export function SliderLabel({ className, ...props }: BaseSlider.Label.Props) {
type SliderValue = number
type SliderRangeValue = readonly number[]
type SliderRootValue = SliderValue | SliderRangeValue
type SliderRootProps<Value extends SliderRootValue = SliderRootValue> = BaseSlider.Root.Props<Value>
type SliderLabelProps = Omit<BaseSlider.Label.Props, 'className'> & { className?: string }
function SliderLabel({ className, ...props }: SliderLabelProps) {
return <BaseSlider.Label className={cn(formLabelClassName, className)} {...props} />
}
type SliderRootProps = BaseSlider.Root.Props<number>
const sliderControlClassName = cn(
'relative flex h-5 w-full touch-none items-center select-none',
'data-disabled:cursor-not-allowed',
)
type SliderControlProps = BaseSlider.Control.Props
type SliderControlProps = Omit<BaseSlider.Control.Props, 'className'> & { className?: string }
export function SliderControl({ className, ...props }: SliderControlProps) {
function SliderControl({ className, ...props }: SliderControlProps) {
return <BaseSlider.Control className={cn(sliderControlClassName, className)} {...props} />
}
@ -28,17 +33,19 @@ const sliderTrackClassName = cn(
'bg-components-slider-track',
)
type SliderTrackProps = BaseSlider.Track.Props
type SliderTrackProps = Omit<BaseSlider.Track.Props, 'className'> & { className?: string }
export function SliderTrack({ className, ...props }: SliderTrackProps) {
function SliderTrack({ className, ...props }: SliderTrackProps) {
return <BaseSlider.Track className={cn(sliderTrackClassName, className)} {...props} />
}
const sliderIndicatorClassName = cn('h-full rounded-full', 'bg-components-slider-range')
type SliderIndicatorProps = BaseSlider.Indicator.Props
type SliderIndicatorProps = Omit<BaseSlider.Indicator.Props, 'className'> & {
className?: string
}
export function SliderIndicator({ className, ...props }: SliderIndicatorProps) {
function SliderIndicator({ className, ...props }: SliderIndicatorProps) {
return <BaseSlider.Indicator className={cn(sliderIndicatorClassName, className)} {...props} />
}
@ -52,9 +59,9 @@ const sliderThumbClassName = cn(
'group-data-disabled/slider:border-components-slider-knob-border group-data-disabled/slider:bg-components-slider-knob-disabled group-data-disabled/slider:shadow-none',
)
type SliderThumbProps = BaseSlider.Thumb.Props
type SliderThumbProps = Omit<BaseSlider.Thumb.Props, 'className'> & { className?: string }
export function SliderThumb({ className, ...props }: SliderThumbProps) {
function SliderThumb({ className, ...props }: SliderThumbProps) {
return <BaseSlider.Thumb className={cn(sliderThumbClassName, className)} {...props} />
}
@ -66,7 +73,7 @@ type SliderSlotClassNames = {
}
type SliderBaseProps = Pick<
SliderRootProps,
SliderRootProps<SliderValue>,
'onValueChange' | 'min' | 'max' | 'step' | 'disabled' | 'name'
> &
Pick<SliderThumbProps, 'aria-label' | 'aria-labelledby'> & {
@ -75,13 +82,13 @@ type SliderBaseProps = Pick<
}
type ControlledSliderProps = SliderBaseProps & {
value: number
value: SliderValue
defaultValue?: never
}
type UncontrolledSliderProps = SliderBaseProps & {
value?: never
defaultValue?: number
defaultValue?: SliderValue
}
type SliderProps = ControlledSliderProps | UncontrolledSliderProps
@ -94,7 +101,7 @@ const getSafeValue = (value: number | undefined, min: number) => {
return Number.isFinite(value) ? value : min
}
export function Slider({
function Slider({
value,
defaultValue,
onValueChange,
@ -134,3 +141,15 @@ export function Slider({
</SliderRoot>
)
}
export { Slider, SliderControl, SliderIndicator, SliderLabel, SliderRoot, SliderThumb, SliderTrack }
export type {
SliderControlProps,
SliderIndicatorProps,
SliderLabelProps,
SliderProps,
SliderRootProps,
SliderThumbProps,
SliderTrackProps,
}

View File

@ -1,8 +1,10 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { StatusDotSize, StatusDotStatus } from '.'
import type { StatusDotProps, StatusDotStatus } from '.'
import * as React from 'react'
import { StatusDot, StatusDotSkeleton } from '.'
type StatusDotSize = NonNullable<StatusDotProps['size']>
const statuses: StatusDotStatus[] = ['success', 'warning', 'error', 'normal', 'disabled']
const sizes: StatusDotSize[] = ['small', 'medium']

View File

@ -47,19 +47,19 @@ const statusDotSkeletonVariants = cva(
type StatusDotVariants = VariantProps<typeof statusDotVariants>
export type StatusDotStatus = NonNullable<StatusDotVariants['status']>
export type StatusDotSize = NonNullable<StatusDotVariants['size']>
type StatusDotStatus = NonNullable<StatusDotVariants['status']>
type StatusDotSize = NonNullable<StatusDotVariants['size']>
export type StatusDotProps = Omit<React.ComponentProps<'span'>, 'children'> & {
type StatusDotProps = Omit<React.ComponentProps<'span'>, 'children'> & {
status?: StatusDotStatus
size?: StatusDotSize
}
export type StatusDotSkeletonProps = Omit<React.ComponentProps<'span'>, 'children'> & {
type StatusDotSkeletonProps = Omit<React.ComponentProps<'span'>, 'children'> & {
size?: StatusDotSize
}
export function StatusDot({
function StatusDot({
className,
status = 'success',
size = 'medium',
@ -81,7 +81,7 @@ export function StatusDot({
)
}
export function StatusDotSkeleton({
function StatusDotSkeleton({
className,
size = 'medium',
'aria-hidden': ariaHidden,
@ -101,3 +101,7 @@ export function StatusDotSkeleton({
/>
)
}
export { StatusDot, StatusDotSkeleton }
export type { StatusDotProps, StatusDotSkeletonProps, StatusDotStatus }

View File

@ -44,8 +44,6 @@ const switchThumbVariants = cva(
},
)
export type SwitchSize = NonNullable<VariantProps<typeof switchRootVariants>['size']>
const switchSpinnerVariants = cva('absolute top-1/2 -translate-x-1/2 -translate-y-1/2', {
variants: {
size: {
@ -67,7 +65,7 @@ type UncontrolledSwitchProps = {
type SwitchControlProps = ControlledSwitchProps | UncontrolledSwitchProps
export type SwitchProps = Omit<
type SwitchProps = Omit<
BaseSwitchNS.Root.Props,
'checked' | 'defaultChecked' | 'className' | 'size' | 'onCheckedChange'
> &
@ -78,7 +76,7 @@ export type SwitchProps = Omit<
className?: string
}
export function Switch({
function Switch({
checked,
size = 'md',
disabled,
@ -122,9 +120,12 @@ const switchSkeletonVariants = cva('bg-text-quaternary opacity-20', {
},
})
export type SwitchSkeletonProps = React.ComponentProps<'div'> &
VariantProps<typeof switchSkeletonVariants>
type SwitchSkeletonProps = React.ComponentProps<'div'> & VariantProps<typeof switchSkeletonVariants>
export function SwitchSkeleton({ size = 'md', className, ...props }: SwitchSkeletonProps) {
function SwitchSkeleton({ size = 'md', className, ...props }: SwitchSkeletonProps) {
return <div className={cn(switchSkeletonVariants({ size }), className)} {...props} />
}
export { Switch, SwitchSkeleton }
export type { SwitchProps, SwitchSkeletonProps }

View File

@ -4,23 +4,22 @@ import type { Tabs as BaseTabsNS } from '@base-ui/react/tabs'
import { Tabs as BaseTabs } from '@base-ui/react/tabs'
import { cn } from '../cn'
export type TabsProps = BaseTabsNS.Root.Props
type TabsProps = BaseTabsNS.Root.Props
const Tabs = BaseTabs.Root
export const Tabs = BaseTabs.Root
export type TabsListProps = Omit<BaseTabsNS.List.Props, 'className'> & {
type TabsListProps = Omit<BaseTabsNS.List.Props, 'className'> & {
className?: string
}
export function TabsList({ className, ...props }: TabsListProps) {
function TabsList({ className, ...props }: TabsListProps) {
return <BaseTabs.List className={cn('flex gap-4', className)} {...props} />
}
export type TabsTabProps = Omit<BaseTabsNS.Tab.Props, 'className'> & {
type TabsTabProps = Omit<BaseTabsNS.Tab.Props, 'className'> & {
className?: string
}
export function TabsTab({ className, ...props }: TabsTabProps) {
function TabsTab({ className, ...props }: TabsTabProps) {
return (
<BaseTabs.Tab
className={cn(
@ -32,12 +31,12 @@ export function TabsTab({ className, ...props }: TabsTabProps) {
)
}
export type TabsPanelProps = Omit<BaseTabsNS.Panel.Props, 'className'> & {
className?: string
}
type TabsPanelProps = BaseTabsNS.Panel.Props
const TabsPanel = BaseTabs.Panel
export function TabsPanel({ className, ...props }: TabsPanelProps) {
return <BaseTabs.Panel className={className} {...props} />
}
const TabsIndicator = BaseTabs.Indicator
type TabsIndicatorProps = BaseTabsNS.Indicator.Props
export const TabsIndicator = BaseTabs.Indicator
export { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab }
export type { TabsIndicatorProps, TabsListProps, TabsPanelProps, TabsProps, TabsTabProps }

View File

@ -35,8 +35,7 @@ const textareaVariants = cva(
)
type TextareaValue = string | number
export type TextareaSize = NonNullable<VariantProps<typeof textareaVariants>['size']>
export type TextareaChangeEventDetails = BaseFieldNS.Control.ChangeEventDetails
type TextareaChangeEventDetails = BaseFieldNS.Control.ChangeEventDetails
type TextareaOnValueChange = (value: string, eventDetails: TextareaChangeEventDetails) => void
type ControlledTextareaProps = {
@ -73,7 +72,7 @@ type FieldControlTextareaProps = Omit<
'className' | 'defaultValue' | 'onValueChange' | 'render' | 'value'
>
export type TextareaProps = TextareaElementProps &
type TextareaProps = TextareaElementProps &
TextareaOnlyProps &
TextareaControlProps &
TextareaVariantProps & {
@ -81,7 +80,7 @@ export type TextareaProps = TextareaElementProps &
className?: string
}
export function Textarea({
function Textarea({
className,
cols,
defaultValue,
@ -108,3 +107,7 @@ export function Textarea({
/>
)
}
export { Textarea }
export type { TextareaProps }

View File

@ -141,7 +141,7 @@ function promiseToast<Value>(promiseValue: Promise<Value>, options: ToastPromise
return toastManager.promise(promiseValue, options)
}
export const toast: ToastApi = Object.assign(showToast, {
const toast: ToastApi = Object.assign(showToast, {
success: createTypedToast('success'),
error: createTypedToast('error'),
warning: createTypedToast('warning'),
@ -251,7 +251,7 @@ function ToastViewport() {
)
}
export function ToastHost({ timeout, limit }: ToastHostProps) {
function ToastHost({ timeout, limit }: ToastHostProps) {
return (
<BaseToast.Provider toastManager={toastManager} timeout={timeout} limit={limit}>
<BaseToast.Portal>
@ -260,3 +260,7 @@ export function ToastHost({ timeout, limit }: ToastHostProps) {
</BaseToast.Provider>
)
}
export { toast, ToastHost }
export type { ToastHostProps }

View File

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Placement } from '.'
import type { TooltipContentProps } from '.'
import * as React from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.'
@ -91,7 +91,9 @@ export const KeyboardShortcut: Story = {
),
}
const PLACEMENTS: Placement[] = [
type TooltipPlacement = NonNullable<TooltipContentProps['placement']>
const PLACEMENTS: TooltipPlacement[] = [
'top-start',
'top',
'top-end',
@ -107,7 +109,7 @@ const PLACEMENTS: Placement[] = [
]
const PlacementsDemo = () => {
const [placement, setPlacement] = React.useState<Placement>('top')
const [placement, setPlacement] = React.useState<TooltipPlacement>('top')
return (
<div className="flex flex-col items-center gap-4 p-24">

View File

@ -6,8 +6,6 @@ import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
import { cn } from '../cn'
import { parsePlacement } from '../placement'
export type { Placement }
/**
* Tooltip is an **ephemeral hint** tied to a trigger (typically an icon button,
* badge, or short label). It follows Base UI's Tooltip semantics:
@ -27,9 +25,13 @@ export type { Placement }
*
* If you need interactive affordances (buttons, links, forms) use `Popover`.
*/
export const TooltipProvider = BaseTooltip.Provider
export const Tooltip = BaseTooltip.Root
export const TooltipTrigger = BaseTooltip.Trigger
const TooltipProvider = BaseTooltip.Provider
const Tooltip = BaseTooltip.Root
const TooltipTrigger = BaseTooltip.Trigger
type TooltipProviderProps = BaseTooltip.Provider.Props
type TooltipProps<Payload = unknown> = BaseTooltip.Root.Props<Payload>
type TooltipTriggerProps<Payload = unknown> = BaseTooltip.Trigger.Props<Payload>
type TooltipContentProps = {
children: React.ReactNode
@ -40,7 +42,7 @@ type TooltipContentProps = {
className?: string
} & Omit<BaseTooltip.Popup.Props, 'children' | 'className'>
export function TooltipContent({
function TooltipContent({
children,
placement = 'top',
sideOffset = 8,
@ -74,3 +76,7 @@ export function TooltipContent({
</BaseTooltip.Portal>
)
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
export type { TooltipContentProps, TooltipProps, TooltipProviderProps, TooltipTriggerProps }

View File

@ -1,4 +1,3 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { FormData } from './form'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
@ -99,7 +98,7 @@ const LoadedFormContent = ({
<Button
key={action.id}
disabled={isActionDisabled}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
variant={getButtonStyle(action.button_style)}
onClick={() => submit(action.id)}
>
{action.title}

View File

@ -163,10 +163,8 @@ export default function AddMemberOrGroupDialog() {
)}
{hasResults ? (
<>
<ComboboxList className="max-h-none p-1">
{(subject: Subject) => (
<SubjectItem key={getSubjectValue(subject)} subject={subject} />
)}
<ComboboxList<Subject> className="max-h-none p-1">
{(subject) => <SubjectItem key={getSubjectValue(subject)} subject={subject} />}
</ComboboxList>
{isFetchingNextPage && <Loading />}
<div ref={anchorRef} className="h-0" />

View File

@ -20,16 +20,6 @@ type AppSortFilterProps = {
onChange: (value: AppListSortBy) => void
}
const appListSortByValues: AppListSortBy[] = [
'last_modified',
'recently_created',
'earliest_created',
]
function isAppListSortBy(value: string): value is AppListSortBy {
return appListSortByValues.includes(value as AppListSortBy)
}
export function AppSortFilter({ value, onChange }: AppSortFilterProps) {
const { t } = useTranslation()
@ -65,12 +55,16 @@ export function AppSortFilter({ value, onChange }: AppSortFilterProps) {
<span aria-hidden className="i-ri-arrow-down-s-line size-4 shrink-0 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-start" sideOffset={4} popupClassName="w-[220px]">
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<AppListSortBy>
value={value}
onValueChange={(nextValue) => isAppListSortBy(nextValue) && onChange(nextValue)}
onValueChange={(nextValue) => onChange(nextValue)}
>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value} closeOnClick>
<DropdownMenuRadioItem<AppListSortBy>
key={option.value}
value={option.value}
closeOnClick
>
<span className="min-w-0 flex-1 truncate">{option.text}</span>
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>

View File

@ -2,14 +2,7 @@ import { parseAsStringLiteral } from 'nuqs'
import { AppModes } from '@/types/app'
const APP_LIST_CATEGORY_VALUES = ['all', ...AppModes] as const
type AppListCategory = (typeof APP_LIST_CATEGORY_VALUES)[number]
export type { AppListCategory }
const appListCategorySet = new Set<string>(APP_LIST_CATEGORY_VALUES)
export const isAppListCategory = (value: string): value is AppListCategory => {
return appListCategorySet.has(value)
}
export type AppListCategory = (typeof APP_LIST_CATEGORY_VALUES)[number]
export const parseAsAppListCategory = parseAsStringLiteral(APP_LIST_CATEGORY_VALUES)
.withDefault('all')

View File

@ -13,7 +13,6 @@ import {
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { AppModeEnum } from '@/types/app'
import { isAppListCategory } from './app-type-filter-shared'
const chipClassName =
'flex h-8 items-center whitespace-nowrap rounded-lg border-[0.5px] px-2 text-[13px] leading-4 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid'
@ -27,38 +26,39 @@ export function AppTypeFilter({ value, onChange }: AppTypeFilterProps) {
const { t } = useTranslation()
const options = useMemo(
() => [
{
value: 'all',
text: t(($) => $['types.all'], { ns: 'app' }),
iconClassName: 'i-ri-apps-2-line',
},
{
value: AppModeEnum.WORKFLOW,
text: t(($) => $['types.workflow'], { ns: 'app' }),
iconClassName: 'i-ri-exchange-2-line',
},
{
value: AppModeEnum.ADVANCED_CHAT,
text: t(($) => $['types.advanced'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.CHAT,
text: t(($) => $['types.chatbot'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.AGENT_CHAT,
text: t(($) => $['types.agent'], { ns: 'app' }),
iconClassName: 'i-ri-robot-3-line',
},
{
value: AppModeEnum.COMPLETION,
text: t(($) => $['newApp.completeApp'], { ns: 'app' }),
iconClassName: 'i-ri-file-4-line',
},
],
() =>
[
{
value: 'all',
text: t(($) => $['types.all'], { ns: 'app' }),
iconClassName: 'i-ri-apps-2-line',
},
{
value: AppModeEnum.WORKFLOW,
text: t(($) => $['types.workflow'], { ns: 'app' }),
iconClassName: 'i-ri-exchange-2-line',
},
{
value: AppModeEnum.ADVANCED_CHAT,
text: t(($) => $['types.advanced'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.CHAT,
text: t(($) => $['types.chatbot'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.AGENT_CHAT,
text: t(($) => $['types.agent'], { ns: 'app' }),
iconClassName: 'i-ri-robot-3-line',
},
{
value: AppModeEnum.COMPLETION,
text: t(($) => $['newApp.completeApp'], { ns: 'app' }),
iconClassName: 'i-ri-file-4-line',
},
] satisfies Array<{ value: AppListCategory; text: string; iconClassName: string }>,
[t],
)
@ -87,12 +87,12 @@ export function AppTypeFilter({ value, onChange }: AppTypeFilterProps) {
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-start" popupClassName="w-[220px]">
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<AppListCategory>
value={value}
onValueChange={(nextValue) => isAppListCategory(nextValue) && onChange(nextValue)}
onValueChange={(nextValue) => onChange(nextValue)}
>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
<DropdownMenuRadioItem<AppListCategory> key={option.value} value={option.value}>
<span
aria-hidden
className={cn('h-4 w-4 shrink-0 text-text-tertiary', option.iconClassName)}

View File

@ -103,7 +103,7 @@ const InputsFormContent = ({ showTip }: Props) => {
/>
)}
{form.type === InputVarType.select && (
<Select
<Select<string>
value={(inputsFormValue?.[form.variable] ?? form.default ?? '') || null}
onValueChange={(value) => value && handleFormChange(form.variable, value)}
>

View File

@ -1,5 +1,4 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from './field-renderer'
import type { HumanInputFormProps } from './type'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
@ -63,7 +62,7 @@ const HumanInputForm = ({ formData, onSubmit }: HumanInputFormProps) => {
<Button
key={action.id}
disabled={isActionDisabled}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
variant={getButtonStyle(action.button_style)}
onClick={() => formToken && submit(formToken, action.id, inputs)}
>
{action.title}

View File

@ -1,3 +1,4 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from './field-renderer'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
@ -24,7 +25,7 @@ dayjs.extend(utc)
dayjs.extend(relativeTime)
dayjs.extend(isSameOrAfter)
export const getButtonStyle = (style: UserActionButtonType) => {
export const getButtonStyle = (style: UserActionButtonType): ButtonProps['variant'] => {
if (style === UserActionButtonType.Primary) return 'primary'
if (style === UserActionButtonType.Default) return 'secondary'
if (style === UserActionButtonType.Accent) return 'secondary-accent'

View File

@ -107,7 +107,7 @@ const InputsFormContent = ({ showTip }: Props) => {
/>
)}
{form.type === InputVarType.select && (
<Select
<Select<string>
value={(inputsFormValue?.[form.variable] ?? form.default ?? '') || null}
onValueChange={(value) => value && handleFormChange(form.variable, value)}
>

View File

@ -13,7 +13,7 @@ import { useStore } from '../store'
type FileFromLinkOrLocalProps = {
showFromLink?: boolean
showFromLocal?: boolean
trigger: (open: boolean) => React.ReactNode
trigger: (open: boolean) => React.ReactElement
fileConfig: FileUpload
}
const FileFromLinkOrLocal = ({
@ -49,7 +49,7 @@ const FileFromLinkOrLocal = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={trigger(open) as React.ReactElement} />
<PopoverTrigger render={trigger(open)} />
<PopoverContent
placement="top"
sideOffset={4}

View File

@ -277,7 +277,7 @@ const BaseField = ({
)}
{formItemType === FormTypeEnum.select &&
(multiple ? (
<Select
<Select<string, true>
multiple
items={memorizedOptions}
value={Array.isArray(value) ? value : []}
@ -289,9 +289,9 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={translatedPlaceholder}>
{(selectedValue: string[]) =>
selectedValue.length
<SelectValue<string, true> placeholder={translatedPlaceholder}>
{(selectedValue) =>
selectedValue?.length
? t(($) => $['dynamicSelect.selected'], {
ns: 'common',
count: selectedValue.length,
@ -310,7 +310,7 @@ const BaseField = ({
</SelectContent>
</Select>
) : (
<Select
<Select<string>
items={memorizedOptions}
value={getSingleSelectValue(value, memorizedOptions)}
disabled={disabled}
@ -324,7 +324,7 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={translatedPlaceholder}>
<SelectValue<string> placeholder={translatedPlaceholder}>
{(nextValue) =>
getSingleSelectLabel(nextValue, memorizedOptions, translatedPlaceholder)
}
@ -352,7 +352,7 @@ const BaseField = ({
)}
{formItemType === FormTypeEnum.dynamicSelect &&
(multiple ? (
<Select
<Select<string, true>
multiple
items={dynamicOptions}
value={Array.isArray(value) ? value : []}
@ -364,9 +364,9 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={dynamicPlaceholder}>
{(selectedValue: string[]) =>
selectedValue.length
<SelectValue<string, true> placeholder={dynamicPlaceholder}>
{(selectedValue) =>
selectedValue?.length
? t(($) => $['dynamicSelect.selected'], {
ns: 'common',
count: selectedValue.length,
@ -395,7 +395,7 @@ const BaseField = ({
</SelectContent>
</Select>
) : (
<Select
<Select<string>
items={dynamicOptions}
value={getSingleSelectValue(value, dynamicOptions)}
disabled={disabled || isDynamicOptionsLoading}
@ -409,7 +409,7 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={dynamicPlaceholder}>
<SelectValue<string> placeholder={dynamicPlaceholder}>
{(nextValue) =>
getSingleSelectLabel(nextValue, dynamicOptions, dynamicPlaceholder)
}

View File

@ -71,8 +71,8 @@ const SelectField = ({
}}
>
<SelectTrigger id={field.name} className="px-2">
<SelectValue placeholder={placeholderText}>
{(nextValue: string | null) => getDisplayLabel(nextValue, options, placeholderText)}
<SelectValue<string> placeholder={placeholderText}>
{(nextValue) => getDisplayLabel(nextValue, options, placeholderText)}
</SelectValue>
</SelectTrigger>
<SelectContent popupClassName={cn('bg-components-panel-bg-blur', popupProps?.className)}>

View File

@ -1,3 +1,4 @@
import type { ExtraProps } from 'streamdown'
import type { ChatContextValue } from '@/app/components/base/chat/chat/context'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@ -13,14 +14,15 @@ vi.mock('../utils', () => ({
isValidUrl: (u: string) => isValidUrlSpy(u),
})) // test subject
type TestNode = {
properties?: {
dataVariant?: string
dataMessage?: string
dataLink?: string
dataSize?: string
type TestNode = NonNullable<ExtraProps['node']>
function createButtonNode(properties: TestNode['properties'], label?: string): TestNode {
return {
type: 'element',
tagName: 'button',
properties,
children: label === undefined ? [] : [{ type: 'text', value: label }],
}
children?: Array<{ value?: string }>
}
describe('MarkdownButton (integration)', () => {
@ -39,13 +41,13 @@ describe('MarkdownButton (integration)', () => {
return render(
<ChatContextProvider {...ctx}>
<MarkdownButton node={node as unknown as Record<string, unknown>} />
<MarkdownButton node={node} />
</ChatContextProvider>,
)
}
it('renders button text from node children', () => {
const node: TestNode = { children: [{ value: 'Click me' }], properties: {} }
const node = createButtonNode({}, 'Click me')
renderWithCtx(node)
expect(screen.getByRole('button')).toHaveTextContent('Click me')
})
@ -55,10 +57,7 @@ describe('MarkdownButton (integration)', () => {
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
const user = userEvent.setup()
const node: TestNode = {
properties: { dataLink: 'https://example.com' },
children: [{ value: 'Go' }],
}
const node = createButtonNode({ dataLink: 'https://example.com' }, 'Go')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@ -74,10 +73,7 @@ describe('MarkdownButton (integration)', () => {
isValidUrlSpy.mockReturnValue(false)
const user = userEvent.setup()
const node: TestNode = {
properties: { dataLink: 'not-a-url', dataMessage: 'hello!' },
children: [{ value: 'Send' }],
}
const node = createButtonNode({ dataLink: 'not-a-url', dataMessage: 'hello!' }, 'Send')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@ -91,7 +87,7 @@ describe('MarkdownButton (integration)', () => {
isValidUrlSpy.mockReturnValue(false)
const user = userEvent.setup()
const node: TestNode = { properties: {}, children: [{ value: 'Empty' }] }
const node = createButtonNode({}, 'Empty')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@ -101,10 +97,7 @@ describe('MarkdownButton (integration)', () => {
it('calls onSend when message present and no link', async () => {
const user = userEvent.setup()
const node: TestNode = {
properties: { dataMessage: 'msg-only' },
children: [{ value: 'Msg' }],
}
const node = createButtonNode({ dataMessage: 'msg-only' }, 'Msg')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@ -113,9 +106,17 @@ describe('MarkdownButton (integration)', () => {
})
it('falls back to empty label when first child value is missing', () => {
const node: TestNode = { properties: {}, children: [{}] }
const node = createButtonNode({})
renderWithCtx(node)
expect(screen.getByRole('button')).toHaveTextContent('')
})
it('maps the legacy warning variant to a destructive primary button', () => {
renderWithCtx(createButtonNode({ dataVariant: 'warning' }, 'Delete'))
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass(
'bg-components-button-destructive-primary-bg',
)
})
})

View File

@ -704,6 +704,18 @@ describe('MarkdownForm', () => {
expect(button)!.toBeInTheDocument()
})
it('should map the legacy warning variant to a destructive primary button', () => {
const node = createRootNode([
createElementNode('button', { dataVariant: 'warning' }, [createTextNode('Delete')]),
])
render(<MarkdownForm node={node} />)
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass(
'bg-components-button-destructive-primary-bg',
)
})
it('should ignore invalid variant and size values', () => {
const node = createRootNode([
createElementNode('button', { dataVariant: 'danger', dataSize: 'xl' }, [

View File

@ -0,0 +1,46 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
type MarkdownButtonAppearance = Pick<ButtonProps, 'size' | 'tone' | 'variant'>
const MARKDOWN_BUTTON_APPEARANCES = {
primary: { variant: 'primary' },
warning: { tone: 'destructive', variant: 'primary' },
secondary: { variant: 'secondary' },
'secondary-accent': { variant: 'secondary-accent' },
ghost: { variant: 'ghost' },
'ghost-accent': { variant: 'ghost-accent' },
tertiary: { variant: 'tertiary' },
} satisfies Record<string, MarkdownButtonAppearance>
const VALID_BUTTON_SIZES: ReadonlySet<string> = new Set([
'small',
'medium',
'large',
] satisfies Array<NonNullable<ButtonProps['size']>>)
function isMarkdownButtonVariant(value: string): value is keyof typeof MARKDOWN_BUTTON_APPEARANCES {
return Object.hasOwn(MARKDOWN_BUTTON_APPEARANCES, value)
}
function isButtonSize(value: string): value is NonNullable<ButtonProps['size']> {
return VALID_BUTTON_SIZES.has(value)
}
function normalizeAttribute(value: unknown) {
return value == null ? '' : String(value)
}
function getMarkdownButtonAppearance(
dataVariant: unknown,
dataSize: unknown,
): MarkdownButtonAppearance {
const variant = normalizeAttribute(dataVariant)
const size = normalizeAttribute(dataSize)
return {
...(isMarkdownButtonVariant(variant) ? MARKDOWN_BUTTON_APPEARANCES[variant] : undefined),
size: isButtonSize(size) ? size : undefined,
}
}
export { getMarkdownButtonAppearance }

View File

@ -1,19 +1,31 @@
import type { ComponentProps } from 'react'
import type { ExtraProps } from 'streamdown'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import { getMarkdownButtonAppearance } from './button-appearance'
import { isValidUrl } from './utils'
const MarkdownButton = ({ node }: any) => {
type MarkdownButtonProps = ComponentProps<'button'> & ExtraProps
function getStringProperty(value: unknown) {
return typeof value === 'string' ? value : undefined
}
const MarkdownButton = ({ node }: MarkdownButtonProps) => {
const { onSend } = useChatContext()
const variant = node.properties.dataVariant
const message = node.properties.dataMessage
const link = node.properties.dataLink
const size = node.properties.dataSize
const appearance = getMarkdownButtonAppearance(
node?.properties.dataVariant,
node?.properties.dataSize,
)
const message = getStringProperty(node?.properties.dataMessage)
const link = getStringProperty(node?.properties.dataLink)
const firstChild = node?.children[0]
const label = firstChild?.type === 'text' ? firstChild.value : ''
return (
<Button
variant={variant}
size={size}
{...appearance}
className={cn('h-auto! min-h-8 px-3! whitespace-normal select-none')}
onClick={() => {
if (link && isValidUrl(link)) {
@ -24,7 +36,7 @@ const MarkdownButton = ({ node }: any) => {
onSend?.(message)
}}
>
<span className="text-[13px]">{node.children[0]?.value || ''}</span>
<span className="text-[13px]">{label}</span>
</Button>
)
}

View File

@ -1,4 +1,3 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { Dayjs } from 'dayjs'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
@ -22,6 +21,7 @@ import {
toDayjs,
} from '@/app/components/base/date-and-time-picker/utils/dayjs'
import Input from '@/app/components/base/input'
import { getMarkdownButtonAppearance } from './button-appearance'
const DATA_FORMAT = {
TEXT: 'text',
@ -72,17 +72,6 @@ function isSafeName(name: unknown): name is string {
)
}
const VALID_BUTTON_VARIANTS = new Set<string>([
'primary',
'warning',
'secondary',
'secondary-accent',
'ghost',
'ghost-accent',
'tertiary',
])
const VALID_BUTTON_SIZES = new Set<string>(['small', 'medium', 'large'])
type HastText = {
type: 'text'
value: string
@ -391,21 +380,16 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
}
if (child.tagName === SUPPORTED_TAGS.BUTTON) {
const rawVariant = str(child.properties.dataVariant)
const rawSize = str(child.properties.dataSize)
const variant = VALID_BUTTON_VARIANTS.has(rawVariant)
? (rawVariant as ButtonProps['variant'])
: undefined
const size = VALID_BUTTON_SIZES.has(rawSize)
? (rawSize as ButtonProps['size'])
: undefined
const appearance = getMarkdownButtonAppearance(
child.properties.dataVariant,
child.properties.dataSize,
)
return (
<Button
variant={variant}
size={size}
className="mt-4"
key={key}
{...appearance}
className="mt-4"
disabled={isSubmitting}
onClick={onSubmit}
>

View File

@ -22,14 +22,7 @@ const isTheme = (value: string): value is Theme => {
export default function ThemeSelector() {
const { t } = useTranslation()
const { theme, setTheme } = useTheme()
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
}
const handleThemeValueChange = (value: string) => {
if (isTheme(value)) handleThemeChange(value)
}
const currentTheme: Theme = theme && isTheme(theme) ? theme : 'system'
const getCurrentIcon = () => {
switch (theme) {
@ -55,22 +48,25 @@ export default function ThemeSelector() {
{getCurrentIcon()}
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={6} popupClassName="w-[144px]">
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={handleThemeValueChange}>
<DropdownMenuRadioItem value="light" closeOnClick>
<DropdownMenuRadioGroup<Theme>
value={currentTheme}
onValueChange={(nextTheme) => setTheme(nextTheme)}
>
<DropdownMenuRadioItem<Theme> value="light" closeOnClick>
<span className="i-ri-sun-line size-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">
{t(($) => $['theme.light'], { ns: 'common' })}
</span>
<DropdownMenuRadioItemIndicator data-testid="light-icon" />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" closeOnClick>
<DropdownMenuRadioItem<Theme> value="dark" closeOnClick>
<span className="i-ri-moon-line size-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">
{t(($) => $['theme.dark'], { ns: 'common' })}
</span>
<DropdownMenuRadioItemIndicator data-testid="dark-icon" />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" closeOnClick>
<DropdownMenuRadioItem<Theme> value="system" closeOnClick>
<span className="i-ri-computer-line size-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">
{t(($) => $['theme.auto'], { ns: 'common' })}

View File

@ -20,8 +20,8 @@ function getDocumentExtension(document: SimpleDocumentDetail) {
export default function DocumentList({ className }: Props) {
return (
<ComboboxList className={cn('max-h-[calc(100vh-120px)] p-0', className)}>
{(item: SimpleDocumentDetail) => {
<ComboboxList<SimpleDocumentDetail> className={cn('max-h-[calc(100vh-120px)] p-0', className)}>
{(item) => {
const extension = getDocumentExtension(item)
return (
<ComboboxItem

View File

@ -148,10 +148,8 @@ export function DocumentPicker({ datasetId, value, parentMode, onChange }: Props
'ml-1 flex size-auto rounded-lg border-0 bg-transparent px-2 py-1 hover:bg-state-base-hover focus-visible:bg-state-base-hover data-popup-open:bg-state-base-hover',
)}
>
<ComboboxValue>
{(document: SimpleDocumentDetail | null) => (
<DocumentPickerTriggerValue document={document} parentMode={parentMode} />
)}
<ComboboxValue<SimpleDocumentDetail>>
{(document) => <DocumentPickerTriggerValue document={document} parentMode={parentMode} />}
</ComboboxValue>
</ComboboxTrigger>
<ComboboxContent

View File

@ -27,7 +27,7 @@ export function CreateMetadataModal({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={triggerElement as React.ReactElement} />
<PopoverTrigger render={triggerElement} />
<PopoverContent
placement="left-start"
sideOffset={popupLeft}

View File

@ -202,8 +202,8 @@ function MetadataPickerSelectPanel({
{query && <ComboboxClear aria-label={t(($) => $['operation.clear'], { ns: 'common' })} />}
</ComboboxInputGroup>
</div>
<ComboboxList>
{(metadata: MetadataItem) => <MetadataOption key={metadata.id} metadata={metadata} />}
<ComboboxList<MetadataItem>>
{(metadata) => <MetadataOption key={metadata.id} metadata={metadata} />}
</ComboboxList>
<ComboboxEmpty>{t(($) => $.noData, { ns: 'common' })}</ComboboxEmpty>
<ComboboxSeparator />

View File

@ -477,8 +477,8 @@ function GotoAnythingDialog() {
? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' })
: t(($) => $['gotoAnything.selectSearchType'], { ns: 'app' })}
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(option: CommandOption) => (
<AutocompleteCollection<CommandOption>>
{(option) => (
<AutocompleteItem
key={option.shortcut}
value={option}
@ -542,8 +542,8 @@ function GotoAnythingDialog() {
{ ns: 'app' },
)}
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(result: SearchResult) => (
<AutocompleteCollection<SearchResult>>
{(result) => (
<AutocompleteItem
key={`${result.type}-${result.id}`}
value={result}

View File

@ -57,6 +57,8 @@ function MainNavRadioItemContent({ iconClassName, label }: MainNavRadioItemConte
function AppearanceSubmenu() {
const { t } = useTranslation()
const { theme, setTheme } = useTheme()
const currentTheme: Theme =
theme === 'light' || theme === 'dark' || theme === 'system' ? theme : 'system'
return (
<DropdownMenuSub>
@ -71,23 +73,35 @@ function AppearanceSubmenu() {
sideOffset={6}
popupClassName="w-[139px] max-h-[360px] bg-components-panel-bg-blur p-1 backdrop-blur-[5px]"
>
<DropdownMenuRadioGroup
value={theme || 'system'}
onValueChange={(value) => setTheme(value as Theme)}
<DropdownMenuRadioGroup<Theme>
value={currentTheme}
onValueChange={(nextTheme) => setTheme(nextTheme)}
>
<DropdownMenuRadioItem value="light" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
<DropdownMenuRadioItem<Theme>
value="light"
closeOnClick
className="mx-0 h-8 gap-1 px-2 py-1"
>
<MainNavRadioItemContent
iconClassName="i-ri-sun-line"
label={t(($) => $['account.appearanceLight'], { ns: 'common' })}
/>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
<DropdownMenuRadioItem<Theme>
value="dark"
closeOnClick
className="mx-0 h-8 gap-1 px-2 py-1"
>
<MainNavRadioItemContent
iconClassName="i-ri-moon-line"
label={t(($) => $['account.appearanceDark'], { ns: 'common' })}
/>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
<DropdownMenuRadioItem<Theme>
value="system"
closeOnClick
className="mx-0 h-8 gap-1 px-2 py-1"
>
<MainNavRadioItemContent
iconClassName="i-ri-computer-line"
label={t(($) => $['account.appearanceSystem'], { ns: 'common' })}

View File

@ -1,5 +1,6 @@
import type { ReactElement, ReactNode } from 'react'
import type { DefaultModel, Model, ModelItem } from '../../declarations'
import type { ModelSelectorPreviewPayload } from '../popup-item'
import { Combobox } from '@langgenius/dify-ui/combobox'
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { fireEvent, screen } from '@testing-library/react'
@ -138,7 +139,7 @@ const makeProvider = (overrides: Record<string, unknown> = {}) => ({
})
const previewCardProps = () => ({
previewCardHandle: createPreviewCardHandle(),
previewCardHandle: createPreviewCardHandle<ModelSelectorPreviewPayload>(),
onPreviewCardClose: vi.fn(),
})
@ -206,7 +207,7 @@ describe('PopupItem', () => {
const onPreviewCardClose = vi.fn()
renderWithCombobox(
<PopupItem
previewCardHandle={createPreviewCardHandle()}
previewCardHandle={createPreviewCardHandle<ModelSelectorPreviewPayload>()}
onPreviewCardClose={onPreviewCardClose}
model={makeModel()}
onHide={vi.fn()}

View File

@ -1,4 +1,4 @@
import type { ComponentProps } from 'react'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { DefaultModel, Model, ModelItem } from '../declarations'
import type { ModelSelectorModelPredicate } from './types'
import { cn } from '@langgenius/dify-ui/cn'
@ -26,14 +26,12 @@ export type ModelSelectorPreviewPayload = {
modelItem: ModelItem
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
type PopupItemProps = {
defaultModel?: DefaultModel
model: Model
modelPredicate?: ModelSelectorModelPredicate
modelSuggestionPredicate?: ModelSelectorModelPredicate
previewCardHandle: PreviewCardHandle
previewCardHandle: PreviewCardHandle<ModelSelectorPreviewPayload>
onPreviewCardClose: () => void
onHide: () => void
}

View File

@ -325,7 +325,7 @@ function Popup({
<ModelSelectorPreviewCard
capabilitiesLabel={t(($) => $['model.capabilities'], { ns: 'common' })}
language={language}
payload={payload as ModelSelectorPreviewPayload | undefined}
payload={payload}
/>
)}
</PreviewCard>

View File

@ -89,15 +89,15 @@ function WorkspaceSwitchControls({
sideOffset={4}
popupClassName="w-40 bg-components-panel-bg-blur! p-1! backdrop-blur-[5px]"
>
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<WorkspaceSort>
value={sort}
onValueChange={(value) => {
onSortChange(value as WorkspaceSort)
onSortChange(value)
setSortMenuOpen(false)
}}
>
{sortOptions.map((option) => (
<DropdownMenuRadioItem
<DropdownMenuRadioItem<WorkspaceSort>
key={option.value}
value={option.value}
className="mx-0 h-8 gap-1 px-2 py-1"

View File

@ -168,8 +168,8 @@ export function AppPicker({
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-1">
{isLoading && <ComboboxStatus>{t(($) => $.loading, { ns: 'common' })}</ComboboxStatus>}
<ComboboxList className="max-h-none p-0">
{(app: App) => <AppPickerOption key={app.id} app={app} />}
<ComboboxList<App> className="max-h-none p-0">
{(app) => <AppPickerOption key={app.id} app={app} />}
</ComboboxList>
<ComboboxEmpty>{t(($) => $.noData, { ns: 'common' })}</ComboboxEmpty>
{hasMore && (

View File

@ -1,6 +1,7 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { Placement } from '@langgenius/dify-ui/popover'
import type { ComponentProps, ReactNode } from 'react'
import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
@ -16,7 +17,7 @@ type DebugInfoProps = {
popupPlacement?: Placement
triggerClassName?: string
triggerContent?: ReactNode
triggerVariant?: ComponentProps<typeof Button>['variant']
triggerVariant?: ButtonProps['variant']
}
function DebugInfo({

View File

@ -21,11 +21,6 @@ type SnippetPublishStatusFilterProps = {
const chipClassName =
'flex h-8 items-center rounded-lg border-[0.5px] px-2 text-[13px] leading-4 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid'
const snippetPublishStatusValues: SnippetPublishStatus[] = ['all', 'published', 'draft']
const isSnippetPublishStatus = (value: string): value is SnippetPublishStatus => {
return snippetPublishStatusValues.includes(value as SnippetPublishStatus)
}
const SnippetPublishStatusFilter = ({ value, onChange }: SnippetPublishStatusFilterProps) => {
const { t } = useTranslation()
@ -64,12 +59,16 @@ const SnippetPublishStatusFilter = ({ value, onChange }: SnippetPublishStatusFil
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-start" popupClassName="w-[220px]">
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<SnippetPublishStatus>
value={value}
onValueChange={(nextValue) => isSnippetPublishStatus(nextValue) && onChange(nextValue)}
onValueChange={(nextValue) => onChange(nextValue)}
>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value} closeOnClick>
<DropdownMenuRadioItem<SnippetPublishStatus>
key={option.value}
value={option.value}
closeOnClick
>
<span>{option.text}</span>
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>

View File

@ -1,5 +1,5 @@
import type { DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu'
import type { ComponentProps, SyntheticEvent } from 'react'
import type { DropdownMenuContentProps } from '@langgenius/dify-ui/dropdown-menu'
import type { SyntheticEvent } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useCallback, useState } from 'react'
@ -8,7 +8,6 @@ const STEP_BY_STEP_TOUR_MENU_POPUP_NO_MOTION_CLASS_NAME =
'transition-none data-starting-style:scale-100 data-starting-style:opacity-100 data-ending-style:scale-100 data-ending-style:opacity-100'
const STEP_BY_STEP_TOUR_MENU_PRESENTATION_CLASS_NAME = 'pointer-events-none cursor-default'
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuContent>
type DropdownMenuPositionerProps = DropdownMenuContentProps['positionerProps']
type DropdownMenuPopupProps = DropdownMenuContentProps['popupProps']

View File

@ -194,7 +194,7 @@ const Blocks = ({
)}
{!isEmpty && BLOCK_CLASSIFICATIONS.map(renderGroup)}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => <BlockPreviewCard payload={payload as BlockPreviewPayload | undefined} />}
{({ payload }) => <BlockPreviewCard payload={payload} />}
</PreviewCard>
</div>
)

View File

@ -1,4 +1,5 @@
'use client'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { TFunction } from 'i18next'
import type { ToolWithProvider } from '../types'
import type { ToolDefaultValue, ToolValue } from './types'
@ -227,9 +228,7 @@ const FeaturedTools = ({
)}
</CollapsiblePanel>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<FeaturedToolPreviewCard payload={payload as FeaturedToolPreviewPayload | undefined} />
)}
{({ payload }) => <FeaturedToolPreviewCard payload={payload} />}
</PreviewCard>
</Collapsible>
)
@ -238,7 +237,7 @@ const FeaturedTools = ({
type FeaturedToolUninstalledItemProps = {
plugin: Plugin
language: Locale
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedToolPreviewPayload>>
previewCardHandle: PreviewCardHandle<FeaturedToolPreviewPayload>
onInstallSuccess?: () => Promise<void> | void
t: TFunction
}

View File

@ -1,4 +1,5 @@
'use client'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { TFunction } from 'i18next'
import type { TriggerPluginActionPreviewPayload } from './trigger-plugin/action-item'
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
@ -227,18 +228,10 @@ const FeaturedTriggers = ({
)}
</CollapsiblePanel>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<FeaturedTriggerPreviewCard
payload={payload as FeaturedTriggerPreviewPayload | undefined}
/>
)}
{({ payload }) => <FeaturedTriggerPreviewCard payload={payload} />}
</PreviewCard>
<PreviewCard handle={triggerActionPreviewCardHandle}>
{({ payload }) => (
<TriggerPluginActionPreviewCard
payload={payload as TriggerPluginActionPreviewPayload | undefined}
/>
)}
{({ payload }) => <TriggerPluginActionPreviewCard payload={payload} />}
</PreviewCard>
</Collapsible>
)
@ -247,7 +240,7 @@ const FeaturedTriggers = ({
type FeaturedTriggerUninstalledItemProps = {
plugin: Plugin
language: Locale
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedTriggerPreviewPayload>>
previewCardHandle: PreviewCardHandle<FeaturedTriggerPreviewPayload>
onInstallSuccess?: () => Promise<void> | void
t: TFunction
}

View File

@ -66,9 +66,7 @@ const List = ({ onSelect, tools, viewType, unInstalledPlugins, className }: List
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
)}
{({ payload }) => <ToolActionPreviewCard payload={payload} />}
</PreviewCard>
{unInstalledPlugins.map((item) => {
return <UninstalledItem key={item.plugin_id} payload={item} />

View File

@ -196,7 +196,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
return (
<BlockSelectorPreviewCardContent>
<SnippetDetailCard snippet={payload as SnippetListItemData} />
<SnippetDetailCard snippet={payload} />
</BlockSelectorPreviewCardContent>
)
}}

View File

@ -214,9 +214,7 @@ const StartBlocks = ({
))}
</div>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<StartBlockPreviewCard payload={payload as StartBlockPreviewPayload | undefined} t={t} />
)}
{({ payload }) => <StartBlockPreviewCard payload={payload} t={t} />}
</PreviewCard>
</div>
)

View File

@ -1,5 +1,6 @@
'use client'
import type { ComponentProps, FC } from 'react'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { FC } from 'react'
import type { ToolWithProvider } from '../../types'
import type { ToolDefaultValue } from '../types'
import type { Tool } from '@/app/components/tools/types'
@ -30,23 +31,22 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
return icon
}
type Props = Readonly<{
provider: ToolWithProvider
payload: Tool
previewCardHandle: PreviewCardHandle
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
}>
export type ToolActionPreviewPayload = {
providerIcon: ToolWithProvider['icon']
payload: Tool
language: ReturnType<typeof useGetLanguage>
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
export type ToolActionPreviewCardHandle = PreviewCardHandle
export type ToolActionPreviewCardHandle = PreviewCardHandle<ToolActionPreviewPayload>
type Props = Readonly<{
provider: ToolWithProvider
payload: Tool
previewCardHandle: ToolActionPreviewCardHandle
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
}>
const ToolItem: FC<Props> = ({
provider,

View File

@ -91,9 +91,7 @@ const Tools = ({
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
)}
{({ payload }) => <ToolActionPreviewCard payload={payload} />}
</PreviewCard>
</div>
)

View File

@ -1,5 +1,6 @@
'use client'
import type { ComponentProps, FC } from 'react'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { FC } from 'react'
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
import type { Event } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
@ -26,8 +27,8 @@ export type TriggerPluginActionPreviewPayload = {
language: ReturnType<typeof useGetLanguage>
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
export type TriggerPluginActionPreviewCardHandle = PreviewCardHandle
export type TriggerPluginActionPreviewCardHandle =
PreviewCardHandle<TriggerPluginActionPreviewPayload>
const TriggerPluginActionItem: FC<Props> = ({
provider,

View File

@ -102,11 +102,7 @@ const TriggerPluginList = ({
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<TriggerPluginActionPreviewCard
payload={payload as TriggerPluginActionPreviewPayload | undefined}
/>
)}
{({ payload }) => <TriggerPluginActionPreviewCard payload={payload} />}
</PreviewCard>
</div>
)

View File

@ -190,7 +190,7 @@ const FormItem: FC<Props> = ({
)}
{type === InputVarType.select && (
<Select
<Select<string>
value={value || payload.default || null}
onValueChange={(nextValue) => {
if (!nextValue) return

Some files were not shown because too many files have changed in this diff Show More