diff --git a/.agents/skills/frontend-code-review/references/dify-ui.md b/.agents/skills/frontend-code-review/references/dify-ui.md index 2e9ef7683cc..27eced33b6a 100644 --- a/.agents/skills/frontend-code-review/references/dify-ui.md +++ b/.agents/skills/frontend-code-review/references/dify-ui.md @@ -28,9 +28,12 @@ Flag: - Missing `package.json#exports` entry for a new primitive. - Internal package imports using workspace subpaths instead of relative paths. - Exported props using internal-only types that consumers cannot import from the component subpath. +- Canonical primitive boundaries or their associated public types using a redundant `Root` suffix when no higher-level convenience component exists in the same subpath. Consumers use subpath exports such as `@langgenius/dify-ui/button`. +Canonical boundaries use the primitive name and matching public types (`Select` / `SelectProps`). Keep `Root` only to distinguish a low-level anatomy root from a higher-level convenience component (`CheckboxRoot` / `Checkbox`); implementation aliases should still show their Base UI source (`BaseSelect.Root.Props`). + ## Props And State Flag: @@ -45,17 +48,17 @@ Flag: Prefer Base UI/Dify UI data attributes and CSS variables for visual state: `data-open`, `data-checked`, `data-disabled`, `data-highlighted`, `data-popup-open`, `group-data-*`, `peer-data-*`, `has-[:focus-visible]`, and primitive CSS variables such as anchor width or transform origin. Use JS conditional classes for product/business state that the primitive does not expose. -For non-string `Select` and `RadioGroup` values, prefer explicit domain generics at the root and at child value carriers. JSX children do not inherit the parent generic, so `RadioGroup` should compose with `Radio`, `RadioRoot`, or option values from a typed collection. For `Select`, prefer the Base UI `items` collection pattern for typed value-to-label rendering, and flag string coercion helpers used only to recover display labels. +For non-string `Select` and `RadioGroup` values, prefer explicit domain generics at the root and at child value carriers. JSX children do not inherit the parent generic, so `RadioGroup` should compose with `Radio`, `RadioItem`, or option values from a typed collection. For `Select`, prefer the Base UI `items` collection pattern for typed value-to-label rendering, and flag string coercion helpers used only to recover display labels. ## Forms Flag: - Form-like UI using unrelated `Input` and `Button` pieces without a submit boundary. -- Text-like fields not composed through `FieldRoot`, `FieldLabel`, and `FieldControl` when using Dify UI form semantics. +- Text-like fields not composed through `Field`, `FieldLabel`, and `FieldControl` when using Dify UI form semantics. - Select fields using `FieldLabel` instead of `SelectLabel`. - Slider fields using a generic label instead of `SliderLabel`. -- Checkbox/radio groups missing `FieldsetRoot` and `FieldsetLegend`. +- Checkbox/radio groups missing `Fieldset` and `FieldsetLegend`. - Field errors or descriptions rendered without `FieldDescription` / `FieldError` relationships. `Form` is the submit boundary. Dify UI form primitives are not a form state-management framework; business validation and schema-driven behavior belong in `web/`. diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md index 8800006f0a6..33f47cf3bab 100644 --- a/packages/dify-ui/AGENTS.md +++ b/packages/dify-ui/AGENTS.md @@ -8,6 +8,7 @@ Shared design tokens, the `cn()` utility, CSS-first Tailwind styles, and headles - Inside dify-ui, cross-component imports use relative paths (`../button`). External consumers use subpath exports (`@langgenius/dify-ui/button`). - No imports from `web/`. No dependencies on next / i18next / ky / jotai / zustand. - One component per folder: `src//index.tsx`, optional `index.stories.tsx` and `__tests__/index.spec.tsx`. Add a matching `./` subpath to `package.json#exports`. +- Name the canonical public boundary and its associated public types after the primitive without a `Root` suffix (`Select` / `SelectProps`). Keep `Root` only when the subpath also exports a higher-level convenience component that must be distinguished from the low-level anatomy root (`CheckboxRoot` / `Checkbox`). Preserve the upstream anatomy in implementation type sources such as `BaseSelect.Root.Props`. - Props pattern: `Omit & VariantProps & { /* custom */ }`. - Use plain `Omit<...>` only for non-union Base UI props. When a prop changes the valid shape of related props (for example `value` / `defaultValue`, `multiple` / `value`, or `clearable` / `onChange`), model that relationship with an explicit discriminated union or a distributive helper instead of flattening the props. - Preserve Base UI generic value contracts in wrappers. If the upstream primitive is generic, expose the same generic parameters and pass them through to the Base UI part, such as `Select.Root`, `RadioGroup`, or `Radio.Root`. diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index 732e62083b0..1740157c7aa 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -30,7 +30,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog' import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer' -import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' +import { Field, FieldControl, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' @@ -41,6 +41,8 @@ import '@langgenius/dify-ui/styles.css' // once, in the app root Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported — it keeps tree-shaking trivial and makes Storybook / test coverage attribution per-primitive. +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`. + ## Primitives | Category | Subpath | Notes | @@ -79,15 +81,15 @@ Dify UI's form primitives are a Base UI composition layer for native form semant Use `Form` for the submit boundary. It renders a native `
`, preserves Enter-to-submit and submit-button behavior, and adds Base UI's `onFormSubmit`, `errors`, `actionsRef`, and `validationMode` APIs for structured values and consolidated field validation. Prefer it over a bare `` when the form is composed with Dify UI fields. -Use `FieldRoot` for each standalone named field. A field must have a stable `name`, a label relationship, and either a `FieldControl` or another control that participates in the same Base UI field context. Prefer a visible label for normal form rows; when the surrounding UI already supplies the visible text, use the matching label primitive visually hidden or put `aria-label` on the actual interactive control. `FieldDescription` and `FieldError` provide the message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system. +Use `Field` for each standalone named field. A field must have a stable `name`, a label relationship, and either a `FieldControl` or another control that participates in the same Base UI field context. Prefer a visible label for normal form rows; when the surrounding UI already supplies the visible text, use the matching label primitive visually hidden or put `aria-label` on the actual interactive control. `FieldDescription` and `FieldError` provide the message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system. Choose the label primitive by the control semantics. Text-like inputs, `Textarea`, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels. -Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, multi-thumb sliders, or a section that combines several inputs. For checkbox and radio groups, wrap each option with `FieldItem` and give each option its own label: +Use `Fieldset` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, multi-thumb sliders, or a section that combines several inputs. For checkbox and radio groups, wrap each option with `FieldItem` and give each option its own label: ```tsx - - }> + +
}> Allowed network protocols @@ -95,11 +97,11 @@ Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group HTTPS - - +
+
``` -`FieldsetRoot` provides the group semantics and legend relationship. It does not own the interactive state of the grouped control. Pass `disabled`, `value`, `defaultValue`, and change handlers to the actual group primitive (`CheckboxGroup`, radio group, slider root, etc.) instead of relying on the fieldset wrapper to manage them. +`Fieldset` provides the group semantics and legend relationship. It does not own the interactive state of the grouped control. Pass `disabled`, `value`, `defaultValue`, and change handlers to the actual group primitive (`CheckboxGroup`, radio group, slider root, etc.) instead of relying on the fieldset wrapper to manage them. ## Typed value contracts diff --git a/packages/dify-ui/src/autocomplete/index.tsx b/packages/dify-ui/src/autocomplete/index.tsx index af8d5ca6767..360347ec0ba 100644 --- a/packages/dify-ui/src/autocomplete/index.tsx +++ b/packages/dify-ui/src/autocomplete/index.tsx @@ -17,26 +17,26 @@ import { parsePlacement } from '../placement' export type { Placement } -export type AutocompleteRootProps = BaseAutocomplete.Root.Props -export type AutocompleteRootGroupedProps< +export type AutocompleteProps = BaseAutocomplete.Root.Props +export type AutocompleteGroupedProps< Items extends readonly { items: readonly unknown[] }[], -> = Omit, 'items'> & { +> = Omit, 'items'> & { items: Items } -export type AutocompleteRootFlatProps - = Omit, 'items'> +export type AutocompleteFlatProps + = Omit, 'items'> & { items?: readonly ItemValue[] } export function Autocomplete( - props: AutocompleteRootGroupedProps, + props: AutocompleteGroupedProps, ): React.JSX.Element export function Autocomplete( - props: AutocompleteRootFlatProps, + props: AutocompleteFlatProps, ): React.JSX.Element export function Autocomplete( - props: AutocompleteRootProps, + props: AutocompleteProps, ): React.JSX.Element { return } @@ -48,8 +48,8 @@ export const AutocompleteRow = BaseAutocomplete.Row export const useAutocompleteFilter = BaseAutocomplete.useFilter export const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems -export type AutocompleteRootChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails -export type AutocompleteRootHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails +export type AutocompleteChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails +export type AutocompleteHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails 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', diff --git a/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx b/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx index f716a1eb343..2cb4dff52a3 100644 --- a/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import { render } from 'vitest-browser-react' import { Checkbox } from '../../checkbox' -import { FieldItem, FieldLabel, FieldRoot } from '../../field' -import { FieldsetLegend, FieldsetRoot } from '../../fieldset' +import { Field, FieldItem, FieldLabel } from '../../field' +import { Fieldset, FieldsetLegend } from '../../fieldset' import { CheckboxGroup } from '../index' const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement @@ -46,8 +46,8 @@ describe('CheckboxGroup', () => { it('should compose with Dify UI Field and Fieldset without losing labels', async () => { const onValueChange = vi.fn() const screen = await render( - - }> + +
}> Features @@ -61,8 +61,8 @@ describe('CheckboxGroup', () => { Analytics - - , +
+
, ) const analytics = screen.getByRole('checkbox', { name: 'Analytics' }) diff --git a/packages/dify-ui/src/checkbox-group/index.stories.tsx b/packages/dify-ui/src/checkbox-group/index.stories.tsx index 1fa2643a2e0..328b1be7bbd 100644 --- a/packages/dify-ui/src/checkbox-group/index.stories.tsx +++ b/packages/dify-ui/src/checkbox-group/index.stories.tsx @@ -3,12 +3,12 @@ import * as React from 'react' import { CheckboxGroup } from '.' import { Checkbox } from '../checkbox' import { + Field, FieldDescription, FieldItem, FieldLabel, - FieldRoot, } from '../field' -import { FieldsetLegend, FieldsetRoot } from '../fieldset' +import { Fieldset, FieldsetLegend } from '../fieldset' const meta = { title: 'Base/Form/CheckboxGroup', @@ -80,11 +80,11 @@ function DynamicFormFieldDemo() { const [selected, setSelected] = React.useState(['markdown']) return ( - + This mirrors Dify dynamic form fields where checkbox options are controlled by schema and persisted as a string array. - ))} - - + + ) } diff --git a/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx b/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx index bd1525d0d6b..26955b216bb 100644 --- a/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/collapsible/__tests__/index.spec.tsx @@ -1,7 +1,7 @@ import { render } from 'vitest-browser-react' import { + Collapsible, CollapsiblePanel, - CollapsibleRoot, CollapsibleTrigger, } from '../index' @@ -10,10 +10,10 @@ const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElem describe('Collapsible wrappers', () => { it('renders the Base UI anatomy with an accessible trigger', async () => { const screen = await render( - + Recovery keys Panel content - , + , ) await expect.element(screen.getByTestId('collapsible-root')).toBeInTheDocument() @@ -23,10 +23,10 @@ describe('Collapsible wrappers', () => { it('toggles open state through the trigger without caller-owned state', async () => { const screen = await render( - + Toggle section Hidden content - , + , ) const trigger = screen.getByRole('button', { name: 'Toggle section' }) @@ -40,10 +40,10 @@ describe('Collapsible wrappers', () => { it('forwards className to every compound part', async () => { const screen = await render( - + Custom Custom panel - , + , ) await expect.element(screen.getByRole('button', { name: 'Custom' })).toHaveClass('custom-trigger') @@ -53,10 +53,10 @@ describe('Collapsible wrappers', () => { it('passes Base UI panel props through to the panel', async () => { const screen = await render( - + Styled trigger Styled panel - , + , ) await expect.element(screen.getByRole('button', { name: 'Styled trigger' })).toHaveAttribute('data-panel-open', '') diff --git a/packages/dify-ui/src/collapsible/index.stories.tsx b/packages/dify-ui/src/collapsible/index.stories.tsx index 4376161beb7..97b593b7b69 100644 --- a/packages/dify-ui/src/collapsible/index.stories.tsx +++ b/packages/dify-ui/src/collapsible/index.stories.tsx @@ -1,15 +1,15 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import * as React from 'react' import { + Collapsible, CollapsiblePanel, - CollapsibleRoot, CollapsibleTrigger, } from '.' import { cn } from '../cn' const meta = { title: 'Base/UI/Collapsible', - component: CollapsibleRoot, + component: Collapsible, parameters: { layout: 'centered', docs: { @@ -19,7 +19,7 @@ const meta = { }, }, tags: ['autodocs'], -} satisfies Meta +} satisfies Meta export default meta type Story = StoryObj @@ -67,25 +67,25 @@ export const Anatomy: Story = { defaultOpen: true, }, render: args => ( - + - + ), } export const DefaultClosed: Story = { render: () => ( - + - + ), } export const DefaultOpen: Story = { render: () => ( - + - + ), } @@ -102,9 +102,9 @@ export const Controlled: Story = { > {open ? 'Close panel' : 'Open panel'} - + - + ) }, @@ -112,25 +112,25 @@ export const Controlled: Story = { export const Disabled: Story = { render: () => ( - + - + ), } export const KeepMounted: Story = { render: () => ( - + - + ), } export const HiddenUntilFound: Story = { render: () => ( - + - + ), } @@ -159,7 +159,7 @@ export const SettingsSections: Story = { render: () => (
{settingSections.map((section, index) => ( - 0 && 'mt-px')} @@ -176,7 +176,7 @@ export const SettingsSections: Story = { {section.description}
- + ))} ), diff --git a/packages/dify-ui/src/collapsible/index.tsx b/packages/dify-ui/src/collapsible/index.tsx index 73f54c4d2cc..b3ca9e496a0 100644 --- a/packages/dify-ui/src/collapsible/index.tsx +++ b/packages/dify-ui/src/collapsible/index.tsx @@ -4,16 +4,16 @@ 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 CollapsibleRootProps +export type CollapsibleProps = Omit & { className?: string } -export function CollapsibleRoot({ +export function Collapsible({ className, ...props -}: CollapsibleRootProps) { +}: CollapsibleProps) { return ( { : 'Try a different owner search.' return ( - + Owner { {emptyMessage} - + ) } @@ -523,7 +523,7 @@ const AsyncReviewerDemo = () => { : 'Try a different reviewer search.' return ( - + Async reviewers { Selected reviewers stay available while async matches change. - + ) } @@ -632,7 +632,7 @@ type Story = StoryObj export const Default: Story = { render: () => ( - + Connect source @@ -645,13 +645,13 @@ export const Default: Story = { {renderSimpleOptionItem} - + ), } export const FormField: Story = { render: () => ( - + Connect source @@ -665,7 +665,7 @@ export const FormField: Story = { Type to filter, then choose a remembered data source. - + ), } @@ -698,7 +698,7 @@ export const Sizes: Story = { render: () => (
{(['small', 'medium', 'large'] as const).map(size => ( - + {`${size[0]!.toUpperCase()}${size.slice(1)}`} @@ -711,7 +711,7 @@ export const Sizes: Story = { {renderOptionItem} - + ))}
), @@ -738,7 +738,7 @@ const MultipleChipsDemo = () => { const [value, setValue] = React.useState(defaultReviewers) return ( - + Reviewers @@ -763,7 +763,7 @@ const MultipleChipsDemo = () => { Selected reviewers wrap inside the input instead of scrolling horizontally. - + ) } @@ -786,7 +786,7 @@ export const VirtualizedLongList: Story = { export const EmptyAndStatus: Story = { render: () => ( - + Connector @@ -801,14 +801,14 @@ export const EmptyAndStatus: Story = { {renderSimpleOptionItem} - + ), } export const DisabledAndReadOnly: Story = { render: () => (
- + Disabled provider @@ -819,8 +819,8 @@ export const DisabledAndReadOnly: Story = { {renderOptionItem} - - + + Read-only source @@ -832,7 +832,7 @@ export const DisabledAndReadOnly: Story = { {renderOptionItem} - +
), } diff --git a/packages/dify-ui/src/combobox/index.tsx b/packages/dify-ui/src/combobox/index.tsx index 7504d684b61..4e04e6188e0 100644 --- a/packages/dify-ui/src/combobox/index.tsx +++ b/packages/dify-ui/src/combobox/index.tsx @@ -17,11 +17,11 @@ import { parsePlacement } from '../placement' export type { Placement } -export type ComboboxRootProps +export type ComboboxProps = BaseCombobox.Root.Props export function Combobox( - props: ComboboxRootProps, + props: ComboboxProps, ): React.JSX.Element { return } @@ -33,8 +33,8 @@ export const ComboboxRow = BaseCombobox.Row export const useComboboxFilter = BaseCombobox.useFilter export const useComboboxFilteredItems = BaseCombobox.useFilteredItems -export type ComboboxRootChangeEventDetails = BaseCombobox.Root.ChangeEventDetails -export type ComboboxRootHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails +export type ComboboxChangeEventDetails = BaseCombobox.Root.ChangeEventDetails +export type ComboboxHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails 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', diff --git a/packages/dify-ui/src/dialog/index.stories.tsx b/packages/dify-ui/src/dialog/index.stories.tsx index 5cf13713767..6e93b28c1cc 100644 --- a/packages/dify-ui/src/dialog/index.stories.tsx +++ b/packages/dify-ui/src/dialog/index.stories.tsx @@ -14,7 +14,7 @@ import { DialogViewport, } from '.' import { Button } from '../button' -import { FieldControl, FieldDescription, FieldError, FieldLabel, FieldRoot } from '../field' +import { Field, FieldControl, FieldDescription, FieldError, FieldLabel } from '../field' import { Form } from '../form' import { Input } from '../input' import { @@ -251,12 +251,12 @@ const FormDialogDemo = () => { className="grid gap-4 pt-5" onFormSubmit={() => setOpen(false)} > - + Name Name is required. - - + + Endpoint @@ -271,8 +271,8 @@ const FormDialogDemo = () => { Endpoint is required. Enter a valid URL. - - + { if (typeof value === 'string' && value.length > 0 && value.length < 5) @@ -285,7 +285,7 @@ const FormDialogDemo = () => { API key is required. - +
, ) @@ -55,10 +55,10 @@ describe('Field primitives', () => { const onFormSubmit = vi.fn() const screen = await render(
- + API key - +
, ) @@ -71,8 +71,8 @@ describe('Field primitives', () => { it('should support external invalid state without requiring FieldControl', async () => { const screen = await render( - - }> + +
}> Features @@ -81,8 +81,8 @@ describe('Field primitives', () => { Choose at least one feature. - - , +
+
, ) await expect.element(screen.getByRole('group', { name: 'Features' })).toBeInTheDocument() @@ -91,10 +91,10 @@ describe('Field primitives', () => { it('should expose the read-only state', async () => { const screen = await render( - + Token - , + , ) await expect.element(screen.getByRole('textbox', { name: 'Token' })).toHaveAttribute('readonly') diff --git a/packages/dify-ui/src/field/index.stories.tsx b/packages/dify-ui/src/field/index.stories.tsx index 84b6ac977d9..563be84ffe0 100644 --- a/packages/dify-ui/src/field/index.stories.tsx +++ b/packages/dify-ui/src/field/index.stories.tsx @@ -1,26 +1,26 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import { Button } from '../button' import { + Field, FieldControl, FieldDescription, FieldError, FieldLabel, - FieldRoot, } from './index' const meta = { title: 'Base/Form/Field', - component: FieldRoot, + component: Field, parameters: { layout: 'centered', docs: { description: { - component: 'Field primitives built on Base UI Field. Use FieldRoot with FieldLabel, FieldControl, FieldDescription, and FieldError for one named form field. External form libraries can control invalid, dirty, and touched on FieldRoot.', + component: 'Field primitives built on Base UI Field. Use Field with FieldLabel, FieldControl, FieldDescription, and FieldError for one named form field. External form libraries can control invalid, dirty, and touched on Field.', }, }, }, tags: ['autodocs'], -} satisfies Meta +} satisfies Meta export default meta @@ -29,13 +29,13 @@ type Story = StoryObj export const TextField: Story = { render: () => (
- + Endpoint Used as the base URL for extension requests. Endpoint is required. Enter a valid URL. - +
@@ -46,24 +46,24 @@ export const TextField: Story = { export const MultipleFields: Story = { render: () => ( - + Name Name is required. - - + + Endpoint Used as the base URL for extension requests. Endpoint is required. Enter a valid URL. - - + + API key Stored with the extension configuration. API key is required. - +
@@ -73,39 +73,39 @@ export const MultipleFields: Story = { export const ExternalInvalidState: Story = { render: () => ( - + API key API key has expired. - + ), } export const Sizes: Story = { render: () => (
- + Small - - + + Regular - - + + Large - +
), } export const ReadOnly: Story = { render: () => ( - + Endpoint This value is managed by the workspace owner. - + ), } diff --git a/packages/dify-ui/src/field/index.tsx b/packages/dify-ui/src/field/index.tsx index 26ab863b4a8..55868733257 100644 --- a/packages/dify-ui/src/field/index.tsx +++ b/packages/dify-ui/src/field/index.tsx @@ -6,18 +6,18 @@ import { Field as BaseField } from '@base-ui/react/field' import { cn } from '../cn' import { formLabelClassName, textControlVariants } from '../form-control-shared' -export type FieldRootProps +export type FieldProps = Omit & { className?: string } -export type FieldRootActions = BaseFieldNS.Root.Actions +export type FieldActions = BaseFieldNS.Root.Actions -export function FieldRoot({ +export function Field({ className, ...props -}: FieldRootProps) { +}: FieldProps) { return ( { it('should forward className to the fieldset and legend', async () => { const screen = await render( - +
Permissions - , +
, ) const legend = screen.getByText('Permissions').element() as HTMLElement diff --git a/packages/dify-ui/src/fieldset/index.stories.tsx b/packages/dify-ui/src/fieldset/index.stories.tsx index 51410ed24c7..774f1d8e194 100644 --- a/packages/dify-ui/src/fieldset/index.stories.tsx +++ b/packages/dify-ui/src/fieldset/index.stories.tsx @@ -1,25 +1,25 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import { Checkbox } from '../checkbox' import { CheckboxGroup } from '../checkbox-group' -import { FieldItem, FieldLabel, FieldRoot } from '../field' +import { Field, FieldItem, FieldLabel } from '../field' import { + Fieldset, FieldsetLegend, - FieldsetRoot, } from './index' const meta = { title: 'Base/Form/Fieldset', - component: FieldsetRoot, + component: Fieldset, parameters: { layout: 'centered', docs: { description: { - component: 'Fieldset primitives built on Base UI Fieldset. Use FieldsetRoot and FieldsetLegend when one field is represented by a group of related controls such as checkbox groups, radio groups, or multi-thumb sliders. Fieldset provides group semantics and labeling; pass interactive state such as disabled and value to the actual group primitive.', + component: 'Fieldset primitives built on Base UI Fieldset. Use Fieldset and FieldsetLegend when one field is represented by a group of related controls such as checkbox groups, radio groups, or multi-thumb sliders. Fieldset provides group semantics and labeling; pass interactive state such as disabled and value to the actual group primitive.', }, }, }, tags: ['autodocs'], -} satisfies Meta +} satisfies Meta export default meta @@ -27,8 +27,8 @@ type Story = StoryObj export const CheckboxGroupField: Story = { render: () => ( - - }> + +
}> Scopes
@@ -50,7 +50,7 @@ export const CheckboxGroupField: Story = {
- - +
+
), } diff --git a/packages/dify-ui/src/fieldset/index.tsx b/packages/dify-ui/src/fieldset/index.tsx index e51804509b4..b76edbf0129 100644 --- a/packages/dify-ui/src/fieldset/index.tsx +++ b/packages/dify-ui/src/fieldset/index.tsx @@ -4,16 +4,16 @@ 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 FieldsetRootProps +export type FieldsetProps = Omit & { className?: string } -export function FieldsetRoot({ +export function Fieldset({ className, ...props -}: FieldsetRootProps) { +}: FieldsetProps) { return ( element as HTMLElement @@ -18,7 +18,7 @@ function TestFileTree({ onPreview?: (itemId: string) => void }) { return ( - + @@ -53,7 +53,7 @@ function TestFileTree({ package.json - + ) } @@ -101,14 +101,14 @@ describe('FileTree', () => { it('does not activate disabled file buttons', async () => { const onPreview = vi.fn() const screen = await render( - + onPreview('disabled')}> disabled.txt - , + , ) asHTMLElement(screen.getByRole('button', { name: 'disabled.txt' }).element()).click() @@ -121,7 +121,7 @@ describe('FileTree', () => { it('resolves disabled folder triggers through the collapsible state', async () => { const onOpenChange = vi.fn() const screen = await render( - + @@ -136,7 +136,7 @@ describe('FileTree', () => { - , + , ) const trigger = screen.getByRole('button', { name: 'locked' }) diff --git a/packages/dify-ui/src/file-tree/index.stories.tsx b/packages/dify-ui/src/file-tree/index.stories.tsx index a75e40aa65c..61fc810b36a 100644 --- a/packages/dify-ui/src/file-tree/index.stories.tsx +++ b/packages/dify-ui/src/file-tree/index.stories.tsx @@ -3,6 +3,7 @@ import type { FileTreeIconType } from '.' import * as React from 'react' import { expect } from 'storybook/test' import { + FileTree, FileTreeBadge, FileTreeFile, FileTreeFolder, @@ -12,12 +13,11 @@ import { FileTreeLabel, FileTreeList, FileTreeMeta, - FileTreeRoot, } from '.' const meta = { title: 'Base/UI/FileTree', - component: FileTreeRoot, + component: FileTree, parameters: { layout: 'centered', docs: { @@ -28,7 +28,7 @@ const meta = { }, }, tags: ['autodocs'], -} satisfies Meta +} satisfies Meta export default meta type Story = StoryObj @@ -121,7 +121,7 @@ function ComposedFileTree() { const [selectedItemId, setSelectedItemId] = React.useState('button') return ( - @@ -172,7 +172,7 @@ function ComposedFileTree() { root - + ) } @@ -180,7 +180,7 @@ function DataDrivenFileTree() { const [selectedItemId, setSelectedItemId] = React.useState('app-components-file-tree') return ( - @@ -191,7 +191,7 @@ function DataDrivenFileTree() { onPreview={setSelectedItemId} /> - + ) } @@ -211,7 +211,7 @@ function IconGallery() { ] as const return ( - + {iconTypes.map(type => ( type === 'folder' @@ -232,7 +232,7 @@ function IconGallery() { ) ))} - + ) } @@ -246,11 +246,11 @@ function StateFrame({ return (
{label}
- + {children} - +
) } diff --git a/packages/dify-ui/src/file-tree/index.tsx b/packages/dify-ui/src/file-tree/index.tsx index e368364b7ec..d00d71bc115 100644 --- a/packages/dify-ui/src/file-tree/index.tsx +++ b/packages/dify-ui/src/file-tree/index.tsx @@ -45,14 +45,14 @@ function fileTreeRowClassName({ ) } -export type FileTreeRootProps = useRender.ComponentProps<'section'> +export type FileTreeProps = useRender.ComponentProps<'section'> -export function FileTreeRoot({ +export function FileTree({ render, className, children, ...props -}: FileTreeRootProps) { +}: FileTreeProps) { const defaultProps: useRender.ElementProps<'section'> = { className: cn('flex min-w-0 flex-col gap-px p-1', className), children: ( diff --git a/packages/dify-ui/src/form/__tests__/index.spec.tsx b/packages/dify-ui/src/form/__tests__/index.spec.tsx index 6ce3f6b7e2e..6fcccaeefa4 100644 --- a/packages/dify-ui/src/form/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/form/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render } from 'vitest-browser-react' -import { FieldControl, FieldLabel, FieldRoot } from '../../field' +import { Field, FieldControl, FieldLabel } from '../../field' import { Form } from '../index' const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement @@ -8,10 +8,10 @@ describe('Form primitive', () => { it('should render a native named form and merge custom class names', async () => { const screen = await render( - + Name - + , ) @@ -22,10 +22,10 @@ describe('Form primitive', () => { const onFormSubmit = vi.fn() const screen = await render(
- + Endpoint - +
, ) @@ -41,10 +41,10 @@ describe('Form primitive', () => { it('should expose externally supplied errors through FieldError consumers', async () => { const screen = await render(
- + Token - +
, ) diff --git a/packages/dify-ui/src/form/index.stories.tsx b/packages/dify-ui/src/form/index.stories.tsx index f1edac5e7ce..29cdbe88ebc 100644 --- a/packages/dify-ui/src/form/index.stories.tsx +++ b/packages/dify-ui/src/form/index.stories.tsx @@ -3,14 +3,14 @@ import { Button } from '../button' import { Checkbox } from '../checkbox' import { CheckboxGroup } from '../checkbox-group' import { + Field, FieldControl, FieldDescription, FieldError, FieldItem, FieldLabel, - FieldRoot, } from '../field' -import { FieldsetLegend, FieldsetRoot } from '../fieldset' +import { Fieldset, FieldsetLegend } from '../fieldset' import { Form } from './index' const meta = { @@ -28,22 +28,22 @@ type Story = StoryObj export const Basic: Story = { render: () => (
undefined}> - + Name Name is required. - + - + Email Used for account notifications. Email is required. Enter a valid email address. - + - - }> + +
}> Features
@@ -59,8 +59,8 @@ export const Basic: Story = {
- - +
+
diff --git a/packages/dify-ui/src/input/__tests__/index.spec.tsx b/packages/dify-ui/src/input/__tests__/index.spec.tsx index ecb7a5aadff..a44f5c95600 100644 --- a/packages/dify-ui/src/input/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/input/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render } from 'vitest-browser-react' -import { FieldError, FieldLabel, FieldRoot } from '../../field' +import { Field, FieldError, FieldLabel } from '../../field' import { Form } from '../../form' import { Input } from '../index' @@ -19,12 +19,12 @@ describe('Input', () => { await expect.element(input).toHaveValue('Dify') }) - it('should use FieldRoot invalid state', async () => { + it('should use Field invalid state', async () => { const screen = await render( - + Repository URL - , + , ) const input = screen.getByRole('textbox', { name: 'Repository URL' }) @@ -33,15 +33,15 @@ describe('Input', () => { await expect.element(input).toHaveAttribute('data-invalid') }) - it('should integrate with FieldRoot and Base UI Form validation', async () => { + it('should integrate with Field and Base UI Form validation', async () => { const onFormSubmit = vi.fn() const screen = await render( - + Email Email is required. - + , ) diff --git a/packages/dify-ui/src/input/index.stories.tsx b/packages/dify-ui/src/input/index.stories.tsx index 453d7ba02b9..f716863fabc 100644 --- a/packages/dify-ui/src/input/index.stories.tsx +++ b/packages/dify-ui/src/input/index.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import { Button } from '../button' import { + Field, FieldDescription, FieldError, FieldLabel, - FieldRoot, } from '../field' import { Form } from '../form' import { Input } from './index' @@ -16,7 +16,7 @@ const meta = { layout: 'centered', docs: { description: { - component: 'A standalone text input primitive built on Base UI Input. Use it for labelled text boxes outside FieldControl, and keep FieldControl for full FieldRoot form composition.', + component: 'A standalone text input primitive built on Base UI Input. Use it for labelled text boxes outside FieldControl, and keep FieldControl for full Field form composition.', }, }, }, @@ -74,7 +74,7 @@ export const States: Story = {
- + Invalid Enter a full URL including https://. - +
@@ -102,20 +102,20 @@ export const States: Story = { export const WithField: Story = { render: () => (
undefined}> - + Email Used for account notifications. Email is required. Enter a valid email address. - - + + Repository URL Use the full GitHub repository URL. Repository URL is required. Enter a valid URL. - +
diff --git a/packages/dify-ui/src/meter/__tests__/index.spec.tsx b/packages/dify-ui/src/meter/__tests__/index.spec.tsx index 5a5de3f6257..a19bb9f49d0 100644 --- a/packages/dify-ui/src/meter/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/meter/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import { render } from 'vitest-browser-react' import { + Meter, MeterIndicator, MeterLabel, - MeterRoot, MeterTrack, MeterValue, } from '../index' @@ -10,11 +10,11 @@ import { describe('Meter compound primitives', () => { it('exposes role="meter" with ARIA value metadata', async () => { const screen = await render( - + - , + , ) const meter = screen.getByLabelText('Quota') @@ -26,11 +26,11 @@ describe('Meter compound primitives', () => { it('respects custom min and max', async () => { const screen = await render( - + - , + , ) const meter = screen.getByLabelText('Quota') @@ -41,11 +41,11 @@ describe('Meter compound primitives', () => { it('sets indicator width from value/min/max', async () => { const screen = await render( - + - , + , ) const indicator = screen.getByTestId('indicator').element() as HTMLElement @@ -54,11 +54,11 @@ describe('Meter compound primitives', () => { it('forwards className to MeterTrack', async () => { const screen = await render( - + - , + , ) const track = screen.getByTestId('track').element() as HTMLElement @@ -67,7 +67,7 @@ describe('Meter compound primitives', () => { it('renders MeterLabel and MeterValue inside a compound layout', async () => { const screen = await render( - { - , + , ) await expect.element(screen.getByText('Score')).toBeInTheDocument() diff --git a/packages/dify-ui/src/meter/index.stories.tsx b/packages/dify-ui/src/meter/index.stories.tsx index f1b8bb46f73..aafd400b422 100644 --- a/packages/dify-ui/src/meter/index.stories.tsx +++ b/packages/dify-ui/src/meter/index.stories.tsx @@ -1,23 +1,23 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import { MeterIndicator, MeterLabel, MeterRoot, MeterTrack, MeterValue } from '.' +import { Meter, MeterIndicator, MeterLabel, MeterTrack, MeterValue } from '.' const meta = { title: 'Base/UI/Meter', - component: MeterRoot, + component: Meter, parameters: { layout: 'centered', docs: { description: { component: 'A graphical display of a numeric value within a known range. ' - + 'Use the compound primitives (`MeterRoot / MeterTrack / MeterIndicator / ' + + 'Use the compound primitives (`Meter / MeterTrack / MeterIndicator / ' + 'MeterValue / MeterLabel`) for quota, capacity, or score indicators; do ' + 'not use for task-completion progress.', }, }, }, tags: ['autodocs'], -} satisfies Meta +} satisfies Meta export default meta @@ -30,11 +30,11 @@ export const Default: Story = { }, render: args => (
- + - +
), } @@ -46,11 +46,11 @@ export const Warning: Story = { }, render: args => (
- + - +
), } @@ -62,11 +62,11 @@ export const Error: Story = { }, render: args => (
- + - +
), } @@ -78,7 +78,7 @@ export const ComposedWithLabelAndValue: Story = { }, render: args => (
- +
Storage @@ -86,7 +86,7 @@ export const ComposedWithLabelAndValue: Story = { - +
), } @@ -101,7 +101,7 @@ export const PercentFormatted: Story = { }, render: args => (
- +
Score @@ -109,7 +109,7 @@ export const PercentFormatted: Story = { - +
), } diff --git a/packages/dify-ui/src/meter/index.tsx b/packages/dify-ui/src/meter/index.tsx index c7dd7a8b9c0..92e6a8df68c 100644 --- a/packages/dify-ui/src/meter/index.tsx +++ b/packages/dify-ui/src/meter/index.tsx @@ -15,8 +15,8 @@ import { Meter as BaseMeter } from '@base-ui/react/meter' import { cva } from 'class-variance-authority' import { cn } from '../cn' -export const MeterRoot = BaseMeter.Root -export type MeterRootProps = BaseMeter.Root.Props +export const Meter = BaseMeter.Root +export type MeterProps = BaseMeter.Root.Props const meterTrackClassName = 'relative block h-1 w-full overflow-hidden rounded-md bg-components-progress-bar-bg' diff --git a/packages/dify-ui/src/number-field/__tests__/index.spec.tsx b/packages/dify-ui/src/number-field/__tests__/index.spec.tsx index 0273aa9d446..2bf94a5ee8c 100644 --- a/packages/dify-ui/src/number-field/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/number-field/__tests__/index.spec.tsx @@ -8,8 +8,8 @@ import type { import * as React from 'react' import { render } from 'vitest-browser-react' import { + Field, FieldLabel, - FieldRoot, } from '../../field' import { NumberField, @@ -79,14 +79,14 @@ describe('NumberField wrapper', () => { it('should surface field invalid state on the visual group', async () => { const screen = await render( - + Amount - , + , ) await expect.element(screen.getByTestId('group')).toHaveAttribute('data-invalid') diff --git a/packages/dify-ui/src/number-field/index.stories.tsx b/packages/dify-ui/src/number-field/index.stories.tsx index 284148c56ff..3ec70b71c0a 100644 --- a/packages/dify-ui/src/number-field/index.stories.tsx +++ b/packages/dify-ui/src/number-field/index.stories.tsx @@ -2,10 +2,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import * as React from 'react' import { Button } from '../button' import { + Field, FieldDescription, FieldError, FieldLabel, - FieldRoot, } from '../field' import { Form } from '../form' import { @@ -25,7 +25,7 @@ const meta = { layout: 'centered', docs: { description: { - component: 'Compound numeric input built on Base UI NumberField. Use it with FieldRoot for labelled, described, and validated form fields.', + component: 'Compound numeric input built on Base UI NumberField. Use it with Field for labelled, described, and validated form fields.', }, }, }, @@ -140,7 +140,7 @@ export const Sizes: Story = { export const States: Story = { render: () => (
- + Placeholder @@ -152,8 +152,8 @@ export const States: Story = { - - + + Filled @@ -165,8 +165,8 @@ export const States: Story = { - - + + Invalid @@ -179,8 +179,8 @@ export const States: Story = { Use a value from 0 to 100. - - + + Disabled @@ -191,8 +191,8 @@ export const States: Story = { - - + + Read-only @@ -204,7 +204,7 @@ export const States: Story = { - +
), } @@ -213,7 +213,7 @@ function ControlledDemo() { const [value, setValue] = React.useState(0.82) return ( - + Score threshold - + ) } @@ -261,7 +261,7 @@ function FormDemo() { setSavedValue(String(values.topK ?? '')) }} > - + Top K @@ -276,7 +276,7 @@ function FormDemo() { Top K is required. Use at least 1. Use 10 or fewer. - +
@@ -298,7 +298,7 @@ export const WithField: Story = { export const Formatting: Story = { render: () => (
- + Budget - - + + Temperature - +
), } diff --git a/packages/dify-ui/src/number-field/index.tsx b/packages/dify-ui/src/number-field/index.tsx index 435be738199..9a9c5060eee 100644 --- a/packages/dify-ui/src/number-field/index.tsx +++ b/packages/dify-ui/src/number-field/index.tsx @@ -8,7 +8,7 @@ import { cn } from '../cn' import { textControlCompoundFocusClassName } from '../form-control-shared' export const NumberField = BaseNumberField.Root -export type NumberFieldRootProps = BaseNumberField.Root.Props +export type NumberFieldProps = BaseNumberField.Root.Props export const numberFieldGroupVariants = cva( [ diff --git a/packages/dify-ui/src/radio/__tests__/index.spec.tsx b/packages/dify-ui/src/radio/__tests__/index.spec.tsx index c0e395ba6a3..7d66461ae54 100644 --- a/packages/dify-ui/src/radio/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/radio/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import type { RadioGroupProps } from '../index' import * as React from 'react' import { render } from 'vitest-browser-react' -import { FieldItem, FieldLabel, FieldRoot } from '../../field' -import { FieldsetLegend, FieldsetRoot } from '../../fieldset' +import { Field, FieldItem, FieldLabel } from '../../field' +import { Fieldset, FieldsetLegend } from '../../fieldset' import { Radio, RadioControl, RadioGroup, RadioItem, RadioSkeleton } from '../index' const clickElement = (element: HTMLElement | SVGElement) => { @@ -22,12 +22,12 @@ function TestRadioGroup({ ...props }: TestRadioGroupProps) { return ( - - {...props} />}> + +
{...props} />}> {label} {children} - - +
+
) } diff --git a/packages/dify-ui/src/radio/index.stories.tsx b/packages/dify-ui/src/radio/index.stories.tsx index ab741cf11ad..799e0c6335c 100644 --- a/packages/dify-ui/src/radio/index.stories.tsx +++ b/packages/dify-ui/src/radio/index.stories.tsx @@ -8,12 +8,12 @@ import { RadioSkeleton, } from '.' import { + Field, FieldDescription, FieldItem, FieldLabel, - FieldRoot, } from '../field' -import { FieldsetLegend, FieldsetRoot } from '../fieldset' +import { Fieldset, FieldsetLegend } from '../fieldset' const meta = { title: 'Base/Form/Radio', @@ -36,8 +36,8 @@ function StandardFormRowsDemo() { const [value, setValue] = React.useState('vector') return ( - - +
)} @@ -55,8 +55,8 @@ function StandardFormRowsDemo() { ))} - - +
+ ) } @@ -75,8 +75,8 @@ function BooleanInlineDemo() { const [value, setValue] = React.useState(true) return ( - - +
value={value} onValueChange={setValue} className="gap-3" /> )} @@ -96,8 +96,8 @@ function BooleanInlineDemo() {
- - + + ) } @@ -135,8 +135,8 @@ function OptionCardsDemo() { }> return ( - - +
value={value} onValueChange={setValue} className="flex-col items-stretch gap-3" /> )} @@ -164,8 +164,8 @@ function OptionCardsDemo() { ))} - - +
+ ) } @@ -189,11 +189,11 @@ function DynamicFormFieldDemo() { const [selected, setSelected] = React.useState('automatic') return ( - + This mirrors Dify dynamic form fields where radio options are controlled by schema and persisted as a single value. - ))} - - + + ) } @@ -232,8 +232,8 @@ export const DynamicFormField: Story = { export const StateMatrix: Story = { render: () => (
- - }> + +
}> Interactive radio states @@ -247,10 +247,10 @@ export const StateMatrix: Story = { Checked - - - - }> +
+
+ +
}> Disabled radio states @@ -264,8 +264,8 @@ export const StateMatrix: Story = { Disabled checked - - +
+
) @@ -335,7 +335,7 @@ const MutationLoadingDemo = () => { return (
- + Enable auto retry { /> Retry failed workflow runs without manual intervention. - + {statusText} diff --git a/packages/dify-ui/src/textarea/__tests__/index.spec.tsx b/packages/dify-ui/src/textarea/__tests__/index.spec.tsx index ef194ee7fb2..b6fbd74af30 100644 --- a/packages/dify-ui/src/textarea/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/textarea/__tests__/index.spec.tsx @@ -1,10 +1,10 @@ import type * as React from 'react' import { render } from 'vitest-browser-react' import { + Field, FieldDescription, FieldError, FieldLabel, - FieldRoot, } from '../../field' import { Form } from '../../form' import { Textarea } from '../index' @@ -20,11 +20,11 @@ const setTextareaValue = (element: HTMLElement | SVGElement, value: string) => { describe('Textarea', () => { it('should render a labelled textarea through Base UI Field.Control', async () => { const screen = await render( - + Description