mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
chore(dify-ui): align authoring rules and dependency optimization (#43233)
This commit is contained in:
38 files changed
+359
-316
No files matched your search
@@ -1193,15 +1193,29 @@ export const lintConfig = {
|
|||||||
{
|
{
|
||||||
files: ['packages/dify-ui/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
|
files: ['packages/dify-ui/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
|
||||||
rules: {
|
rules: {
|
||||||
|
'func-names': ['error', 'as-needed'],
|
||||||
|
'id-denylist': ['error', 'e'],
|
||||||
'unicorn/import-style': [
|
'unicorn/import-style': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
extendDefaultStyles: false,
|
extendDefaultStyles: false,
|
||||||
styles: {
|
styles: {
|
||||||
react: { namespace: true },
|
react: { namespace: true },
|
||||||
|
'react-dom': { namespace: true },
|
||||||
|
'react-dom/client': { namespace: true },
|
||||||
|
'react-dom/server': { namespace: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'react/function-component-definition': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
namedComponents: ['function-declaration', 'function-expression'],
|
||||||
|
unnamedComponents: ['arrow-function', 'function-expression'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': ['error', 'element'],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
'react/exhaustive-deps': [
|
'react/exhaustive-deps': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ Use React namespace imports throughout this package, including stories and tests
|
|||||||
import also covers React types; do not add separate named type imports. JSX alone does not
|
import also covers React types; do not add separate named type imports. JSX alone does not
|
||||||
require an explicit React import.
|
require an explicit React import.
|
||||||
|
|
||||||
|
Use namespace imports for `react-dom`, `react-dom/client`, and `react-dom/server` as well.
|
||||||
|
Write grouping fragments as `<React.Fragment>` and remove unnecessary fragment wrappers.
|
||||||
|
|
||||||
|
## Component writing style
|
||||||
|
|
||||||
|
Define named components with function declarations or function expressions. Arrow functions
|
||||||
|
remain appropriate for event handlers, render props, and Storybook render callbacks. Name function
|
||||||
|
expressions when JavaScript cannot infer their name. Use `event` or `error` instead of `e`.
|
||||||
|
|
||||||
## Subpaths and names
|
## Subpaths and names
|
||||||
|
|
||||||
Every public primitive needs a matching `package.json#exports` subpath. Import relatively between
|
Every public primitive needs a matching `package.json#exports` subpath. Import relatively between
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const NonDestructive: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
function ControlledDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = React.useState(0)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export const Controlled: Story = {
|
|||||||
render: () => <ControlledDemo />,
|
render: () => <ControlledDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingConfirmDemo = () => {
|
function LoadingConfirmDemo() {
|
||||||
const [pending, setPending] = React.useState(false)
|
const [pending, setPending] = React.useState(false)
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const confirmLabelId = React.useId()
|
const confirmLabelId = React.useId()
|
||||||
|
|||||||
@@ -293,27 +293,29 @@ async function searchSuggestions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SuggestionItem = ({ item, dense }: { item: Suggestion; dense?: boolean }) => (
|
function SuggestionItem({ item, dense }: { item: Suggestion; dense?: boolean }) {
|
||||||
<AutocompleteItem value={item}>
|
return (
|
||||||
{item.icon && (
|
<AutocompleteItem value={item}>
|
||||||
<span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" />
|
{item.icon && (
|
||||||
)}
|
<span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" />
|
||||||
<div className="flex min-w-0 grow flex-col">
|
|
||||||
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
|
|
||||||
{!dense && item.description && (
|
|
||||||
<span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex min-w-0 grow flex-col">
|
||||||
{item.meta && (
|
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
|
||||||
<span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary">
|
{!dense && item.description && (
|
||||||
{item.meta}
|
<span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
</AutocompleteItem>
|
{item.meta && (
|
||||||
)
|
<span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary">
|
||||||
|
{item.meta}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</AutocompleteItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Only virtualized items receive an explicit index; ordinary lists must let Base UI register items by DOM order for keyboard navigation.
|
// Only virtualized items receive an explicit index; ordinary lists must let Base UI register items by DOM order for keyboard navigation.
|
||||||
const VirtualizedSuggestionItem = ({
|
function VirtualizedSuggestionItem({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
dense,
|
dense,
|
||||||
@@ -321,70 +323,76 @@ const VirtualizedSuggestionItem = ({
|
|||||||
item: Suggestion
|
item: Suggestion
|
||||||
index: number
|
index: number
|
||||||
dense?: boolean
|
dense?: boolean
|
||||||
}) => (
|
}) {
|
||||||
<AutocompleteItem value={item} index={index}>
|
return (
|
||||||
{item.icon && (
|
<AutocompleteItem value={item} index={index}>
|
||||||
<span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" />
|
{item.icon && (
|
||||||
)}
|
<span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" />
|
||||||
<div className="flex min-w-0 grow flex-col">
|
|
||||||
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
|
|
||||||
{!dense && item.description && (
|
|
||||||
<span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex min-w-0 grow flex-col">
|
||||||
{item.meta && (
|
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
|
||||||
<span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary">
|
{!dense && item.description && (
|
||||||
{item.meta}
|
<span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
</AutocompleteItem>
|
{item.meta && (
|
||||||
)
|
<span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary">
|
||||||
|
{item.meta}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</AutocompleteItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TagSuggestionItem = ({ item }: { item: Suggestion }) => (
|
function TagSuggestionItem({ item }: { item: Suggestion }) {
|
||||||
<AutocompleteItem value={item}>
|
return (
|
||||||
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
|
<AutocompleteItem value={item}>
|
||||||
{item.description && (
|
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
|
||||||
<span className="ml-auto max-w-36 truncate system-xs-regular text-text-tertiary">
|
{item.description && (
|
||||||
{item.description}
|
<span className="ml-auto max-w-36 truncate system-xs-regular text-text-tertiary">
|
||||||
</span>
|
{item.description}
|
||||||
)}
|
</span>
|
||||||
</AutocompleteItem>
|
)}
|
||||||
)
|
</AutocompleteItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const BasicTagAutocomplete = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => (
|
function BasicTagAutocomplete({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
|
||||||
<Autocomplete
|
return (
|
||||||
items={tagSuggestions}
|
<Autocomplete
|
||||||
itemToStringValue={getSuggestionLabel}
|
items={tagSuggestions}
|
||||||
mode="list"
|
itemToStringValue={getSuggestionLabel}
|
||||||
openOnInputClick
|
mode="list"
|
||||||
>
|
openOnInputClick
|
||||||
<AutocompleteInputGroup size={size}>
|
>
|
||||||
<span
|
<AutocompleteInputGroup size={size}>
|
||||||
className="ml-2 i-ri-search-line size-4 shrink-0 text-text-tertiary"
|
<span
|
||||||
aria-hidden="true"
|
className="ml-2 i-ri-search-line size-4 shrink-0 text-text-tertiary"
|
||||||
/>
|
aria-hidden="true"
|
||||||
<AutocompleteInput
|
/>
|
||||||
size={size}
|
<AutocompleteInput
|
||||||
placeholder="Search tags or type a new one…"
|
size={size}
|
||||||
aria-label="Search tags or type a new one"
|
placeholder="Search tags or type a new one…"
|
||||||
/>
|
aria-label="Search tags or type a new one"
|
||||||
<AutocompleteClear size={size} />
|
/>
|
||||||
<AutocompleteTrigger size={size} />
|
<AutocompleteClear size={size} />
|
||||||
</AutocompleteInputGroup>
|
<AutocompleteTrigger size={size} />
|
||||||
<AutocompletePortal>
|
</AutocompleteInputGroup>
|
||||||
<AutocompletePositioner>
|
<AutocompletePortal>
|
||||||
<AutocompletePopup>
|
<AutocompletePositioner>
|
||||||
<AutocompleteList<Suggestion>>
|
<AutocompletePopup>
|
||||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
<AutocompleteList<Suggestion>>
|
||||||
</AutocompleteList>
|
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||||
<AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty>
|
</AutocompleteList>
|
||||||
</AutocompletePopup>
|
<AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty>
|
||||||
</AutocompletePositioner>
|
</AutocompletePopup>
|
||||||
</AutocompletePortal>
|
</AutocompletePositioner>
|
||||||
</Autocomplete>
|
</AutocompletePortal>
|
||||||
)
|
</Autocomplete>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const GroupedSuggestionList = () => {
|
function GroupedSuggestionList() {
|
||||||
const groups = useAutocompleteFilteredItems<SuggestionGroup>()
|
const groups = useAutocompleteFilteredItems<SuggestionGroup>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -402,7 +410,7 @@ const GroupedSuggestionList = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandPaletteList = () => {
|
function CommandPaletteList() {
|
||||||
const groups = useAutocompleteFilteredItems<SuggestionGroup>()
|
const groups = useAutocompleteFilteredItems<SuggestionGroup>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -450,7 +458,7 @@ const LimitedStatus = ({ total }: { total: number }) => {
|
|||||||
: `${items.length} suggestions available.`
|
: `${items.length} suggestions available.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const AsyncSearchDemo = () => {
|
function AsyncSearchDemo() {
|
||||||
const [searchValue, setSearchValue] = React.useState('')
|
const [searchValue, setSearchValue] = React.useState('')
|
||||||
const [searchResults, setSearchResults] = React.useState<Suggestion[]>([])
|
const [searchResults, setSearchResults] = React.useState<Suggestion[]>([])
|
||||||
const [error, setError] = React.useState<string | null>(null)
|
const [error, setError] = React.useState<string | null>(null)
|
||||||
@@ -532,11 +540,11 @@ const AsyncSearchDemo = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedSuggestionList = ({
|
function VirtualizedSuggestionList({
|
||||||
virtualizerRef,
|
virtualizerRef,
|
||||||
}: {
|
}: {
|
||||||
virtualizerRef: React.RefObject<StoryVirtualizer | null>
|
virtualizerRef: React.RefObject<StoryVirtualizer | null>
|
||||||
}) => {
|
}) {
|
||||||
const scrollRef = React.useRef<HTMLDivElement | null>(null)
|
const scrollRef = React.useRef<HTMLDivElement | null>(null)
|
||||||
const filteredItems = useAutocompleteFilteredItems<Suggestion>()
|
const filteredItems = useAutocompleteFilteredItems<Suggestion>()
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
@@ -586,7 +594,7 @@ const VirtualizedSuggestionList = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedStatus = () => {
|
function VirtualizedStatus() {
|
||||||
const filteredItems = useAutocompleteFilteredItems<Suggestion>()
|
const filteredItems = useAutocompleteFilteredItems<Suggestion>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -596,7 +604,7 @@ const VirtualizedStatus = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FuzzyHighlight = ({ text, query }: { text: string; query: string }) => {
|
function FuzzyHighlight({ text, query }: { text: string; query: string }) {
|
||||||
const parts = React.useMemo(() => {
|
const parts = React.useMemo(() => {
|
||||||
const trimmed = query.trim()
|
const trimmed = query.trim()
|
||||||
|
|
||||||
@@ -622,7 +630,7 @@ const FuzzyHighlight = ({ text, query }: { text: string; query: string }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FuzzyMatchingDemo = () => {
|
function FuzzyMatchingDemo() {
|
||||||
const [value, setValue] = React.useState('retr')
|
const [value, setValue] = React.useState('retr')
|
||||||
const { contains } = useAutocompleteFilter({ sensitivity: 'base' })
|
const { contains } = useAutocompleteFilter({ sensitivity: 'base' })
|
||||||
|
|
||||||
@@ -863,7 +871,7 @@ export const CommandPalette: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedLongSuggestionsDemo = () => {
|
function VirtualizedLongSuggestionsDemo() {
|
||||||
const virtualizerRef = React.useRef<StoryVirtualizer | null>(null)
|
const virtualizerRef = React.useRef<StoryVirtualizer | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function AvatarImage({ className, ...props }: AvatarImageProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar = ({ name, avatar, size = 'md', className, onLoadingStatusChange }: AvatarProps) => {
|
function Avatar({ name, avatar, size = 'md', className, onLoadingStatusChange }: AvatarProps) {
|
||||||
return (
|
return (
|
||||||
<AvatarRoot size={size} className={className}>
|
<AvatarRoot size={size} className={className}>
|
||||||
{avatar && (
|
{avatar && (
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function TriggerIcon() {
|
|||||||
|
|
||||||
function RecoveryKeys(props: Pick<CollapsiblePanelProps, 'keepMounted' | 'hiddenUntilFound'>) {
|
function RecoveryKeys(props: Pick<CollapsiblePanelProps, 'keepMounted' | 'hiddenUntilFound'>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
<CollapsibleTrigger className={cn(triggerClassName, 'justify-between gap-2')}>
|
<CollapsibleTrigger className={cn(triggerClassName, 'justify-between gap-2')}>
|
||||||
Recovery keys
|
Recovery keys
|
||||||
<TriggerIcon />
|
<TriggerIcon />
|
||||||
@@ -62,7 +62,7 @@ function RecoveryKeys(props: Pick<CollapsiblePanelProps, 'keepMounted' | 'hidden
|
|||||||
<div>horse-battery-staple</div>
|
<div>horse-battery-staple</div>
|
||||||
</div>
|
</div>
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
</>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -353,23 +353,25 @@ const renderVirtualizedOptionItem = (option: Option, index: number, itemCount: n
|
|||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
)
|
)
|
||||||
|
|
||||||
const PopupSearchInput = ({ label, placeholder }: { label: string; placeholder: string }) => (
|
function PopupSearchInput({ label, placeholder }: { label: string; placeholder: string }) {
|
||||||
<div className="p-1 pb-0">
|
return (
|
||||||
<ComboboxInputGroup className="h-8 min-h-8 px-2">
|
<div className="p-1 pb-0">
|
||||||
<span
|
<ComboboxInputGroup className="h-8 min-h-8 px-2">
|
||||||
aria-hidden
|
<span
|
||||||
className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder"
|
aria-hidden
|
||||||
/>
|
className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder"
|
||||||
<ComboboxInput
|
/>
|
||||||
aria-label={label}
|
<ComboboxInput
|
||||||
placeholder={`${placeholder}…`}
|
aria-label={label}
|
||||||
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
|
placeholder={`${placeholder}…`}
|
||||||
/>
|
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
|
||||||
</ComboboxInputGroup>
|
/>
|
||||||
</div>
|
</ComboboxInputGroup>
|
||||||
)
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const GroupedToolList = () => {
|
function GroupedToolList() {
|
||||||
const groups = useComboboxFilteredItems<OptionGroup>()
|
const groups = useComboboxFilteredItems<OptionGroup>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -385,11 +387,11 @@ const GroupedToolList = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedModelList = ({
|
function VirtualizedModelList({
|
||||||
virtualizerRef,
|
virtualizerRef,
|
||||||
}: {
|
}: {
|
||||||
virtualizerRef: React.RefObject<StoryVirtualizer | null>
|
virtualizerRef: React.RefObject<StoryVirtualizer | null>
|
||||||
}) => {
|
}) {
|
||||||
const scrollRef = React.useRef<HTMLDivElement | null>(null)
|
const scrollRef = React.useRef<HTMLDivElement | null>(null)
|
||||||
const filteredItems = useComboboxFilteredItems<Option>()
|
const filteredItems = useComboboxFilteredItems<Option>()
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
@@ -451,7 +453,7 @@ const VirtualizedModelList = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilteredModelStatus = () => {
|
function FilteredModelStatus() {
|
||||||
const filteredItems = useComboboxFilteredItems<Option>()
|
const filteredItems = useComboboxFilteredItems<Option>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -461,7 +463,7 @@ const FilteredModelStatus = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedLongListDemo = () => {
|
function VirtualizedLongListDemo() {
|
||||||
const [value, setValue] = React.useState<Option | null>(modelCatalogOptions[137]!)
|
const [value, setValue] = React.useState<Option | null>(modelCatalogOptions[137]!)
|
||||||
const virtualizerRef = React.useRef<StoryVirtualizer | null>(null)
|
const virtualizerRef = React.useRef<StoryVirtualizer | null>(null)
|
||||||
|
|
||||||
@@ -495,7 +497,7 @@ const VirtualizedLongListDemo = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AsyncDirectoryDemo = () => {
|
function AsyncDirectoryDemo() {
|
||||||
const [searchResults, setSearchResults] = React.useState<Option[]>([])
|
const [searchResults, setSearchResults] = React.useState<Option[]>([])
|
||||||
const [selectedValue, setSelectedValue] = React.useState<Option | null>(null)
|
const [selectedValue, setSelectedValue] = React.useState<Option | null>(null)
|
||||||
const [searchValue, setSearchValue] = React.useState('')
|
const [searchValue, setSearchValue] = React.useState('')
|
||||||
@@ -599,7 +601,7 @@ const AsyncDirectoryDemo = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InlinePopoverDemo = () => {
|
function InlinePopoverDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const [value, setValue] = React.useState<Option | null>(null)
|
const [value, setValue] = React.useState<Option | null>(null)
|
||||||
const [inputValue, setInputValue] = React.useState('')
|
const [inputValue, setInputValue] = React.useState('')
|
||||||
@@ -829,7 +831,7 @@ export const Grouped: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultipleChipsDemo = () => {
|
function MultipleChipsDemo() {
|
||||||
const [value, setValue] = React.useState<Option[]>(defaultReviewers)
|
const [value, setValue] = React.useState<Option[]>(defaultReviewers)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1004,7 +1006,7 @@ export const ReadOnly: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
function ControlledDemo() {
|
||||||
const [value, setValue] = React.useState<string | null>(defaultTag.value)
|
const [value, setValue] = React.useState<string | null>(defaultTag.value)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,14 +19,16 @@ import {
|
|||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from '.'
|
} from '.'
|
||||||
|
|
||||||
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => (
|
function TriggerArea({ label = 'Right-click inside this area' }: { label?: string }) {
|
||||||
<ContextMenuTrigger
|
return (
|
||||||
aria-label="context menu trigger area"
|
<ContextMenuTrigger
|
||||||
className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary select-none"
|
aria-label="context menu trigger area"
|
||||||
>
|
className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary select-none"
|
||||||
{label}
|
>
|
||||||
</ContextMenuTrigger>
|
{label}
|
||||||
)
|
</ContextMenuTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Base/UI/ContextMenu',
|
title: 'Base/UI/ContextMenu',
|
||||||
@@ -102,7 +104,7 @@ export const WithGroupLabel: Story = {
|
|||||||
|
|
||||||
type Density = 'compact' | 'comfortable' | 'spacious'
|
type Density = 'compact' | 'comfortable' | 'spacious'
|
||||||
|
|
||||||
const WithRadioItemsDemo = () => {
|
function WithRadioItemsDemo() {
|
||||||
const [density, setDensity] = React.useState<Density>('comfortable')
|
const [density, setDensity] = React.useState<Density>('comfortable')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -132,7 +134,7 @@ export const WithRadioItems: Story = {
|
|||||||
render: () => <WithRadioItemsDemo />,
|
render: () => <WithRadioItemsDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const WithCheckboxItemsDemo = () => {
|
function WithCheckboxItemsDemo() {
|
||||||
const [showToolbar, setShowToolbar] = React.useState(true)
|
const [showToolbar, setShowToolbar] = React.useState(true)
|
||||||
const [showSidebar, setShowSidebar] = React.useState(false)
|
const [showSidebar, setShowSidebar] = React.useState(false)
|
||||||
const [showStatusBar, setShowStatusBar] = React.useState(true)
|
const [showStatusBar, setShowStatusBar] = React.useState(true)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type * as React from 'react'
|
|
||||||
import {
|
import {
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DatePickerContent,
|
DatePickerContent,
|
||||||
@@ -17,7 +16,8 @@ import {
|
|||||||
TimePickerLabel,
|
TimePickerLabel,
|
||||||
TimePickerTrigger,
|
TimePickerTrigger,
|
||||||
} from '@langgenius/dify-ui/time-picker'
|
} from '@langgenius/dify-ui/time-picker'
|
||||||
import { createPortal } from 'react-dom'
|
import * as React from 'react'
|
||||||
|
import * as ReactDOM from 'react-dom'
|
||||||
import { userEvent } from 'vite-plus/test/browser'
|
import { userEvent } from 'vite-plus/test/browser'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ it.each(pickers)(
|
|||||||
const help = vi.fn()
|
const help = vi.fn()
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
wrap(
|
wrap(
|
||||||
<>
|
<React.Fragment>
|
||||||
<Label>
|
<Label>
|
||||||
<span>Departure</span>
|
<span>Departure</span>
|
||||||
<button type="button" onClick={help}>
|
<button type="button" onClick={help}>
|
||||||
@@ -60,7 +60,7 @@ it.each(pickers)(
|
|||||||
</Label>
|
</Label>
|
||||||
<Trigger />
|
<Trigger />
|
||||||
<Content />
|
<Content />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const trigger = screen.getByRole('button', { name: 'Departure Help' })
|
const trigger = screen.getByRole('button', { name: 'Departure Help' })
|
||||||
@@ -113,7 +113,7 @@ it('does not forward clicks from portalled label content to the trigger', async
|
|||||||
<DatePicker>
|
<DatePicker>
|
||||||
<DatePickerLabel>
|
<DatePickerLabel>
|
||||||
Departure
|
Departure
|
||||||
{createPortal(<input aria-label="Help search" />, document.body)}
|
{ReactDOM.createPortal(<input aria-label="Help search" />, document.body)}
|
||||||
</DatePickerLabel>
|
</DatePickerLabel>
|
||||||
<DatePickerTrigger />
|
<DatePickerTrigger />
|
||||||
<DatePickerContent />
|
<DatePickerContent />
|
||||||
@@ -127,14 +127,14 @@ it('does not forward clicks from portalled label content to the trigger', async
|
|||||||
|
|
||||||
it('allows a consumer to cancel label focus forwarding', async () => {
|
it('allows a consumer to cancel label focus forwarding', async () => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<button type="button">Previous control</button>
|
<button type="button">Previous control</button>
|
||||||
<DatePicker>
|
<DatePicker>
|
||||||
<DatePickerLabel onClick={(event) => event.preventDefault()}>Departure</DatePickerLabel>
|
<DatePickerLabel onClick={(event) => event.preventDefault()}>Departure</DatePickerLabel>
|
||||||
<DatePickerTrigger />
|
<DatePickerTrigger />
|
||||||
<DatePickerContent />
|
<DatePickerContent />
|
||||||
</DatePicker>
|
</DatePicker>
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
const previous = screen.getByRole('button', { name: 'Previous control' })
|
const previous = screen.getByRole('button', { name: 'Previous control' })
|
||||||
await previous.click()
|
await previous.click()
|
||||||
@@ -148,10 +148,10 @@ it.each(pickers)(
|
|||||||
async ({ wrap, Trigger, Content }) => {
|
async ({ wrap, Trigger, Content }) => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
wrap(
|
wrap(
|
||||||
<>
|
<React.Fragment>
|
||||||
<Trigger aria-label="Departure" />
|
<Trigger aria-label="Departure" />
|
||||||
<Content aria-label="Choose departure" />
|
<Content aria-label="Choose departure" />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const trigger = screen.getByRole('button', { name: 'Departure' })
|
const trigger = screen.getByRole('button', { name: 'Departure' })
|
||||||
@@ -164,7 +164,7 @@ it.each(pickers)(
|
|||||||
|
|
||||||
it('combines external label, value and descriptions without losing validation feedback', async () => {
|
it('combines external label, value and descriptions without losing validation feedback', async () => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<span id="departure-label">Departure</span>
|
<span id="departure-label">Departure</span>
|
||||||
<p id="departure-help">Choose a working day.</p>
|
<p id="departure-help">Choose a working day.</p>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -176,7 +176,7 @@ it('combines external label, value and descriptions without losing validation fe
|
|||||||
<DatePickerTrigger aria-labelledby="departure-label" aria-describedby="departure-help" />
|
<DatePickerTrigger aria-labelledby="departure-label" aria-describedby="departure-help" />
|
||||||
<DatePickerContent aria-labelledby="departure-label" />
|
<DatePickerContent aria-labelledby="departure-label" />
|
||||||
</DatePicker>
|
</DatePicker>
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
const trigger = screen.getByRole('button', { name: 'Departure Jan 15, 2025' })
|
const trigger = screen.getByRole('button', { name: 'Departure Jan 15, 2025' })
|
||||||
await expect
|
await expect
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
|
|
||||||
function Parts() {
|
function Parts() {
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
<DatePickerTrigger />
|
<DatePickerTrigger />
|
||||||
<DatePickerClear aria-label="Clear date" />
|
<DatePickerClear aria-label="Clear date" />
|
||||||
<DatePickerContent />
|
<DatePickerContent />
|
||||||
</>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ describe('DatePicker keyboard and form contract', () => {
|
|||||||
function Example() {
|
function Example() {
|
||||||
const [visible, setVisible] = React.useState(true)
|
const [visible, setVisible] = React.useState(true)
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
<button type="button" onClick={() => setVisible(!visible)}>
|
<button type="button" onClick={() => setVisible(!visible)}>
|
||||||
Toggle label
|
Toggle label
|
||||||
</button>
|
</button>
|
||||||
@@ -36,7 +36,7 @@ describe('DatePicker keyboard and form contract', () => {
|
|||||||
<DatePickerTrigger id="custom-departure" aria-label={visible ? undefined : 'Date'} />
|
<DatePickerTrigger id="custom-departure" aria-label={visible ? undefined : 'Date'} />
|
||||||
<DatePickerContent aria-label={visible ? undefined : 'Choose date'} />
|
<DatePickerContent aria-label={visible ? undefined : 'Choose date'} />
|
||||||
</DatePicker>
|
</DatePicker>
|
||||||
</>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const screen = await render(<Example />)
|
const screen = await render(<Example />)
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ function CalendarPanel({
|
|||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
choosingMonth ? (
|
choosingMonth ? (
|
||||||
<>
|
<React.Fragment>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -183,7 +183,7 @@ function CalendarPanel({
|
|||||||
>
|
>
|
||||||
{labels.apply}
|
{labels.apply}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
footer
|
footer
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ function Session() {
|
|||||||
isDateUnavailable={isDateUnavailable}
|
isDateUnavailable={isDateUnavailable}
|
||||||
labels={text}
|
labels={text}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<React.Fragment>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary-accent"
|
variant="secondary-accent"
|
||||||
@@ -285,14 +285,14 @@ function Session() {
|
|||||||
</Button>
|
</Button>
|
||||||
{apply}
|
{apply}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PickerPanel
|
<PickerPanel
|
||||||
header={<PickerPanelHeader>{text.pickTime}</PickerPanelHeader>}
|
header={<PickerPanelHeader>{text.pickTime}</PickerPanelHeader>}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<React.Fragment>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary-accent"
|
variant="secondary-accent"
|
||||||
@@ -311,7 +311,7 @@ function Session() {
|
|||||||
</Button>
|
</Button>
|
||||||
{apply}
|
{apply}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TimePanel
|
<TimePanel
|
||||||
|
|||||||
@@ -283,13 +283,13 @@ function PickerTrigger({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<React.Fragment>
|
||||||
<PickerValue />
|
<PickerValue />
|
||||||
<span
|
<span
|
||||||
className="i-ri-calendar-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none"
|
className="i-ri-calendar-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -296,9 +296,9 @@ it('announces the complete custom displayed value including its timezone', async
|
|||||||
<TimePickerTrigger>
|
<TimePickerTrigger>
|
||||||
<TimePickerValue>
|
<TimePickerValue>
|
||||||
{(displayValue) => (
|
{(displayValue) => (
|
||||||
<>
|
<React.Fragment>
|
||||||
<span>{displayValue}</span> <span>UTC+08:00</span>
|
<span>{displayValue}</span> <span>UTC+08:00</span>
|
||||||
</>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</TimePickerValue>
|
</TimePickerValue>
|
||||||
</TimePickerTrigger>
|
</TimePickerTrigger>
|
||||||
|
|||||||
@@ -174,9 +174,9 @@ export const Schedule: Story = {
|
|||||||
<TimePickerTrigger>
|
<TimePickerTrigger>
|
||||||
<TimePickerValue>
|
<TimePickerValue>
|
||||||
{(value) => (
|
{(value) => (
|
||||||
<>
|
<React.Fragment>
|
||||||
{value} <span className="text-text-tertiary">UTC+0</span>
|
{value} <span className="text-text-tertiary">UTC+0</span>
|
||||||
</>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</TimePickerValue>
|
</TimePickerValue>
|
||||||
</TimePickerTrigger>
|
</TimePickerTrigger>
|
||||||
|
|||||||
@@ -136,13 +136,13 @@ function TimePickerTrigger({ children, ...props }: TimePickerTriggerProps) {
|
|||||||
return (
|
return (
|
||||||
<PickerTrigger {...props}>
|
<PickerTrigger {...props}>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<React.Fragment>
|
||||||
<PickerValue />
|
<PickerValue />
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="i-ri-time-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none"
|
className="i-ri-time-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none"
|
||||||
/>
|
/>
|
||||||
</>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</PickerTrigger>
|
</PickerTrigger>
|
||||||
)
|
)
|
||||||
@@ -196,7 +196,7 @@ function Session() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<React.Fragment>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost-accent"
|
variant="ghost-accent"
|
||||||
@@ -224,7 +224,7 @@ function Session() {
|
|||||||
>
|
>
|
||||||
{text.apply}
|
{text.apply}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TimePanel
|
<TimePanel
|
||||||
|
|||||||
@@ -75,14 +75,14 @@ describe('Dialog wrapper', () => {
|
|||||||
it('should connect a detached trigger to the dialog', async () => {
|
it('should connect a detached trigger to the dialog', async () => {
|
||||||
const handle = createDialogHandle()
|
const handle = createDialogHandle()
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<DialogTrigger handle={handle}>Open dialog</DialogTrigger>
|
<DialogTrigger handle={handle}>Open dialog</DialogTrigger>
|
||||||
<Dialog handle={handle}>
|
<Dialog handle={handle}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Detached dialog</DialogTitle>
|
<DialogTitle>Detached dialog</DialogTitle>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Open dialog' }).click()
|
await screen.getByRole('button', { name: 'Open dialog' }).click()
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const WithoutCloseButton: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
function ControlledDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -274,7 +274,7 @@ type ApiExtensionFormValues = {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormDialogDemo = () => {
|
function FormDialogDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const nameInputRef = React.useRef<HTMLInputElement>(null)
|
const nameInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ export const FormDialog: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const OutsideScrollingContentDemo = () => {
|
function OutsideScrollingContentDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const popupRef = React.useRef<HTMLDivElement>(null)
|
const popupRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -465,7 +465,7 @@ export const OutsidePopupElements: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const InsideScrollingContentDemo = () => {
|
function InsideScrollingContentDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,18 +19,20 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '.'
|
} from '.'
|
||||||
|
|
||||||
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
|
function TriggerButton({ label = 'Open Menu' }: { label?: string }) {
|
||||||
<DropdownMenuTrigger
|
return (
|
||||||
render={
|
<DropdownMenuTrigger
|
||||||
<button
|
render={
|
||||||
type="button"
|
<button
|
||||||
className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
type="button"
|
||||||
/>
|
className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
{label}
|
>
|
||||||
</DropdownMenuTrigger>
|
{label}
|
||||||
)
|
</DropdownMenuTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Base/UI/DropdownMenu',
|
title: 'Base/UI/DropdownMenu',
|
||||||
@@ -140,7 +142,7 @@ export const WithSubmenu: Story = {
|
|||||||
|
|
||||||
type Density = 'compact' | 'comfortable' | 'spacious'
|
type Density = 'compact' | 'comfortable' | 'spacious'
|
||||||
|
|
||||||
const WithRadioItemsDemo = () => {
|
function WithRadioItemsDemo() {
|
||||||
const [density, setDensity] = React.useState<Density>('comfortable')
|
const [density, setDensity] = React.useState<Density>('comfortable')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -170,7 +172,7 @@ export const WithRadioItems: Story = {
|
|||||||
render: () => <WithRadioItemsDemo />,
|
render: () => <WithRadioItemsDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const WithCheckboxItemsDemo = () => {
|
function WithCheckboxItemsDemo() {
|
||||||
const [showToolbar, setShowToolbar] = React.useState(true)
|
const [showToolbar, setShowToolbar] = React.useState(true)
|
||||||
const [showSidebar, setShowSidebar] = React.useState(false)
|
const [showSidebar, setShowSidebar] = React.useState(false)
|
||||||
const [showStatusBar, setShowStatusBar] = React.useState(true)
|
const [showStatusBar, setShowStatusBar] = React.useState(true)
|
||||||
@@ -265,7 +267,7 @@ export const WithLinkItems: Story = {
|
|||||||
|
|
||||||
type SortOrder = 'newest' | 'oldest' | 'name'
|
type SortOrder = 'newest' | 'oldest' | 'name'
|
||||||
|
|
||||||
const ComplexDemo = () => {
|
function ComplexDemo() {
|
||||||
const [sortOrder, setSortOrder] = React.useState<SortOrder>('newest')
|
const [sortOrder, setSortOrder] = React.useState<SortOrder>('newest')
|
||||||
const [showArchived, setShowArchived] = React.useState(false)
|
const [showArchived, setShowArchived] = React.useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import * as ReactDOM from 'react-dom'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import { Infotip, InfotipContent, InfotipTrigger } from '..'
|
import { Infotip, InfotipContent, InfotipTrigger } from '..'
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ describe('Infotip composition', () => {
|
|||||||
const screen = await render(
|
const screen = await render(
|
||||||
<button type="button" onClick={parentClick}>
|
<button type="button" onClick={parentClick}>
|
||||||
Parent action
|
Parent action
|
||||||
{createPortal(
|
{ReactDOM.createPortal(
|
||||||
<Infotip>
|
<Infotip>
|
||||||
<InfotipTrigger aria-label="Details" onClick={triggerClick} ref={triggerRef} />
|
<InfotipTrigger aria-label="Details" onClick={triggerClick} ref={triggerRef} />
|
||||||
<InfotipContent aria-label="Details">Explanation</InfotipContent>
|
<InfotipContent aria-label="Details">Explanation</InfotipContent>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type * as React from 'react'
|
|
||||||
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
|
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
export function infotipTypeContracts(
|
export function infotipTypeContracts(
|
||||||
triggerRef: React.Ref<HTMLButtonElement>,
|
triggerRef: React.Ref<HTMLButtonElement>,
|
||||||
@@ -8,7 +8,7 @@ export function infotipTypeContracts(
|
|||||||
const composition = (
|
const composition = (
|
||||||
<Infotip<{ title: string }>>
|
<Infotip<{ title: string }>>
|
||||||
{({ payload }) => (
|
{({ payload }) => (
|
||||||
<>
|
<React.Fragment>
|
||||||
<InfotipTrigger
|
<InfotipTrigger
|
||||||
aria-label="Details"
|
aria-label="Details"
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
@@ -28,7 +28,7 @@ export function infotipTypeContracts(
|
|||||||
>
|
>
|
||||||
{payload?.title}
|
{payload?.title}
|
||||||
</InfotipContent>
|
</InfotipContent>
|
||||||
</>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</Infotip>
|
</Infotip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as React from 'react'
|
||||||
import { userEvent } from 'vite-plus/test/browser'
|
import { userEvent } from 'vite-plus/test/browser'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import { Button } from '../../button'
|
import { Button } from '../../button'
|
||||||
@@ -157,7 +158,7 @@ describe('Invalid focus colors', () => {
|
|||||||
document.documentElement.dataset.theme = theme
|
document.documentElement.dataset.theme = theme
|
||||||
try {
|
try {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<Button>Before</Button>
|
<Button>Before</Button>
|
||||||
<Field invalid>
|
<Field invalid>
|
||||||
<FieldLabel>Invalid value</FieldLabel>
|
<FieldLabel>Invalid value</FieldLabel>
|
||||||
@@ -166,7 +167,7 @@ describe('Invalid focus colors', () => {
|
|||||||
<InputGroupAddon>suffix</InputGroupAddon>
|
<InputGroupAddon>suffix</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Field>
|
</Field>
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
const input = screen.getByRole('textbox', { name: 'Invalid value' })
|
const input = screen.getByRole('textbox', { name: 'Invalid value' })
|
||||||
const surface = screen.getByTestId('invalid-surface').element()
|
const surface = screen.getByTestId('invalid-surface').element()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const meta = {
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
const HotkeyKbdGroup = ({
|
function HotkeyKbdGroup({
|
||||||
hotkey,
|
hotkey,
|
||||||
color = 'gray',
|
color = 'gray',
|
||||||
platform = 'mac',
|
platform = 'mac',
|
||||||
@@ -51,16 +51,18 @@ const HotkeyKbdGroup = ({
|
|||||||
hotkey: DisplayHotkey
|
hotkey: DisplayHotkey
|
||||||
color?: 'gray' | 'white'
|
color?: 'gray' | 'white'
|
||||||
platform?: FormatDisplayOptions['platform']
|
platform?: FormatDisplayOptions['platform']
|
||||||
}) => (
|
}) {
|
||||||
<KbdGroup>
|
return (
|
||||||
{formatForDisplay(hotkey, { platform, parts: true }).map((key, index) => (
|
<KbdGroup>
|
||||||
// oxlint-disable-next-line react/no-array-index-key -- Repeated display keys are static, ordered tokens with no component state.
|
{formatForDisplay(hotkey, { platform, parts: true }).map((key, index) => (
|
||||||
<Kbd key={`${key}-${index}`} color={color}>
|
// oxlint-disable-next-line react/no-array-index-key -- Repeated display keys are static, ordered tokens with no component state.
|
||||||
{key}
|
<Kbd key={`${key}-${index}`} color={color}>
|
||||||
</Kbd>
|
{key}
|
||||||
))}
|
</Kbd>
|
||||||
</KbdGroup>
|
))}
|
||||||
)
|
</KbdGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: () => <HotkeyKbdGroup hotkey="Mod+K" />,
|
render: () => <HotkeyKbdGroup hotkey="Mod+K" />,
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ describe('Invalid focus colors', () => {
|
|||||||
document.documentElement.dataset.theme = theme
|
document.documentElement.dataset.theme = theme
|
||||||
try {
|
try {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<Button>Before</Button>
|
<Button>Before</Button>
|
||||||
<Field invalid>
|
<Field invalid>
|
||||||
<FieldLabel>Invalid value</FieldLabel>
|
<FieldLabel>Invalid value</FieldLabel>
|
||||||
@@ -278,7 +278,7 @@ describe('Invalid focus colors', () => {
|
|||||||
</NumberFieldGroup>
|
</NumberFieldGroup>
|
||||||
</NumberField>
|
</NumberField>
|
||||||
</Field>
|
</Field>
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
const input = screen.getByRole('textbox', { name: 'Invalid value' })
|
const input = screen.getByRole('textbox', { name: 'Invalid value' })
|
||||||
const surface = screen.getByTestId('invalid-surface').element()
|
const surface = screen.getByTestId('invalid-surface').element()
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ const PLACEMENTS: PopoverPlacement[] = [
|
|||||||
'left-end',
|
'left-end',
|
||||||
]
|
]
|
||||||
|
|
||||||
const PlacementsDemo = () => {
|
function PlacementsDemo() {
|
||||||
const [placement, setPlacement] = React.useState<PopoverPlacement>('bottom')
|
const [placement, setPlacement] = React.useState<PopoverPlacement>('bottom')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -254,7 +254,7 @@ export const Placements: Story = {
|
|||||||
render: () => <PlacementsDemo />,
|
render: () => <PlacementsDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
function ControlledDemo() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ const PLACEMENTS: PreviewCardPlacement[] = [
|
|||||||
'left-end',
|
'left-end',
|
||||||
]
|
]
|
||||||
|
|
||||||
const PlacementsDemo = () => {
|
function PlacementsDemo() {
|
||||||
const [placement, setPlacement] = React.useState<PreviewCardPlacement>('bottom')
|
const [placement, setPlacement] = React.useState<PreviewCardPlacement>('bottom')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -172,27 +172,29 @@ export const Placements: Story = {
|
|||||||
render: () => <PlacementsDemo />,
|
render: () => <PlacementsDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomDelayDemo = () => (
|
function CustomDelayDemo() {
|
||||||
<PreviewCard>
|
return (
|
||||||
<PreviewCardTrigger
|
<PreviewCard>
|
||||||
delay={100}
|
<PreviewCardTrigger
|
||||||
closeDelay={100}
|
delay={100}
|
||||||
href="#preview-card-delay"
|
closeDelay={100}
|
||||||
className={triggerButtonClassName}
|
href="#preview-card-delay"
|
||||||
>
|
className={triggerButtonClassName}
|
||||||
Snappy trigger
|
>
|
||||||
</PreviewCardTrigger>
|
Snappy trigger
|
||||||
<PreviewCardContent className="w-64 p-3">
|
</PreviewCardTrigger>
|
||||||
<div className="flex flex-col gap-1">
|
<PreviewCardContent className="w-64 p-3">
|
||||||
<div className="text-sm font-semibold text-text-primary">Fast hover</div>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="text-xs text-text-secondary">
|
<div className="text-sm font-semibold text-text-primary">Fast hover</div>
|
||||||
Base UI defaults (600ms / 300ms) are tuned for link previews. Override per trigger for
|
<div className="text-xs text-text-secondary">
|
||||||
denser UIs.
|
Base UI defaults (600ms / 300ms) are tuned for link previews. Override per trigger for
|
||||||
|
denser UIs.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PreviewCardContent>
|
||||||
</PreviewCardContent>
|
</PreviewCard>
|
||||||
</PreviewCard>
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export const CustomDelays: Story = {
|
export const CustomDelays: Story = {
|
||||||
render: () => <CustomDelayDemo />,
|
render: () => <CustomDelayDemo />,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type * as React from 'react'
|
import * as React from 'react'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import {
|
import {
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
@@ -105,7 +105,7 @@ describe('scroll area', () => {
|
|||||||
|
|
||||||
it('should keep accessible region semantics on the viewport', async () => {
|
it('should keep accessible region semantics on the viewport', async () => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<p id="installed-apps-label">Installed apps</p>
|
<p id="installed-apps-label">Installed apps</p>
|
||||||
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
|
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
|
||||||
<ScrollAreaViewport
|
<ScrollAreaViewport
|
||||||
@@ -121,7 +121,7 @@ describe('scroll area', () => {
|
|||||||
<ScrollAreaThumb />
|
<ScrollAreaThumb />
|
||||||
</ScrollAreaScrollbar>
|
</ScrollAreaScrollbar>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
const viewport = screen.getByRole('region', { name: 'Installed apps' })
|
const viewport = screen.getByRole('region', { name: 'Installed apps' })
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type * as React from 'react'
|
import * as React from 'react'
|
||||||
import { userEvent } from 'vite-plus/test/browser'
|
import { userEvent } from 'vite-plus/test/browser'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '../index'
|
import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '../index'
|
||||||
|
|
||||||
function SegmentedControlTypeExamples() {
|
function SegmentedControlTypeExamples() {
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
<SegmentedControl<number> value={10} onValueChange={() => {}} aria-label="Page size">
|
<SegmentedControl<number> value={10} onValueChange={() => {}} aria-label="Page size">
|
||||||
<SegmentedControlItem<number> value={10}>10</SegmentedControlItem>
|
<SegmentedControlItem<number> value={10}>10</SegmentedControlItem>
|
||||||
<SegmentedControlItem<number> value={20}>20</SegmentedControlItem>
|
<SegmentedControlItem<number> value={20}>20</SegmentedControlItem>
|
||||||
@@ -14,7 +14,7 @@ function SegmentedControlTypeExamples() {
|
|||||||
<SegmentedControl aria-label="Missing value">
|
<SegmentedControl aria-label="Missing value">
|
||||||
<SegmentedControlItem value="one">One</SegmentedControlItem>
|
<SegmentedControlItem value="one">One</SegmentedControlItem>
|
||||||
</SegmentedControl>
|
</SegmentedControl>
|
||||||
</>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,18 @@ type SegmentedControlProps = {
|
|||||||
noPadding?: boolean
|
noPadding?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = () => <i className="i-ri-information-line size-4 shrink-0" aria-hidden="true" />
|
function Icon() {
|
||||||
|
return <i className="i-ri-information-line size-4 shrink-0" aria-hidden="true" />
|
||||||
|
}
|
||||||
|
|
||||||
const Item = () => (
|
function Item() {
|
||||||
<React.Fragment>
|
return (
|
||||||
<Icon />
|
<React.Fragment>
|
||||||
<span className="px-0.5">Item</span>
|
<Icon />
|
||||||
</React.Fragment>
|
<span className="px-0.5">Item</span>
|
||||||
)
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function SegmentedControlExample({
|
function SegmentedControlExample({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export const ReadOnly: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
function ControlledDemo() {
|
||||||
const [value, setValue] = React.useState<string | null>('balanced')
|
const [value, setValue] = React.useState<string | null>('balanced')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -336,7 +336,7 @@ export const Controlled: Story = {
|
|||||||
render: () => <ControlledDemo />,
|
render: () => <ControlledDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultipleControlledDemo = () => {
|
function MultipleControlledDemo() {
|
||||||
const [value, setValue] = React.useState<DeploymentRegion[]>(['us-east', 'eu-west'])
|
const [value, setValue] = React.useState<DeploymentRegion[]>(['us-east', 'eu-west'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import * as React from 'react'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import { StatusDot, StatusDotSkeleton } from '../index'
|
import { StatusDot, StatusDotSkeleton } from '../index'
|
||||||
|
|
||||||
describe('StatusDot', () => {
|
describe('StatusDot', () => {
|
||||||
it('hides the visual indicator and its loading placeholder from assistive technology', async () => {
|
it('hides the visual indicator and its loading placeholder from assistive technology', async () => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<StatusDot data-testid="dot" />
|
<StatusDot data-testid="dot" />
|
||||||
<StatusDotSkeleton data-testid="skeleton" />
|
<StatusDotSkeleton data-testid="skeleton" />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
|
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
|
||||||
@@ -16,14 +17,14 @@ describe('StatusDot', () => {
|
|||||||
|
|
||||||
it('does not allow consumers to override the decorative contract', async () => {
|
it('does not allow consumers to override the decorative contract', async () => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
{/* @ts-expect-error StatusDot is always decorative */}
|
{/* @ts-expect-error StatusDot is always decorative */}
|
||||||
<StatusDot aria-hidden={false} data-testid="dot" />
|
<StatusDot aria-hidden={false} data-testid="dot" />
|
||||||
{/* @ts-expect-error Status semantics belong to the surrounding component */}
|
{/* @ts-expect-error Status semantics belong to the surrounding component */}
|
||||||
<StatusDot aria-label="Active" data-testid="labelled-dot" />
|
<StatusDot aria-label="Active" data-testid="labelled-dot" />
|
||||||
{/* @ts-expect-error StatusDotSkeleton is always decorative */}
|
{/* @ts-expect-error StatusDotSkeleton is always decorative */}
|
||||||
<StatusDotSkeleton aria-hidden={false} data-testid="skeleton" />
|
<StatusDotSkeleton aria-hidden={false} data-testid="skeleton" />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
|
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { SwitchProps } from '@langgenius/dify-ui/switch'
|
import type { SwitchProps } from '@langgenius/dify-ui/switch'
|
||||||
import { Switch } from '@langgenius/dify-ui/switch'
|
import { Switch } from '@langgenius/dify-ui/switch'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
function SwitchTypeExamples() {
|
function SwitchTypeExamples() {
|
||||||
const onCheckedChange: NonNullable<SwitchProps['onCheckedChange']> = (checked, details) => {
|
const onCheckedChange: NonNullable<SwitchProps['onCheckedChange']> = (checked, details) => {
|
||||||
@@ -7,12 +8,12 @@ function SwitchTypeExamples() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
<Switch defaultChecked onCheckedChange={onCheckedChange} />
|
<Switch defaultChecked onCheckedChange={onCheckedChange} />
|
||||||
<Switch checked={false} onCheckedChange={(checked) => void checked} />
|
<Switch checked={false} onCheckedChange={(checked) => void checked} />
|
||||||
{/* @ts-expect-error the composite owns its thumb and loading indicator */}
|
{/* @ts-expect-error the composite owns its thumb and loading indicator */}
|
||||||
<Switch>Custom thumb</Switch>
|
<Switch>Custom thumb</Switch>
|
||||||
</>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type SwitchDemoProps = Partial<
|
|||||||
checked?: boolean
|
checked?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const SwitchDemo = (args: SwitchDemoProps) => {
|
function SwitchDemo(args: SwitchDemoProps) {
|
||||||
const [enabled, setEnabled] = React.useState(args.checked ?? false)
|
const [enabled, setEnabled] = React.useState(args.checked ?? false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -113,7 +113,7 @@ export const DisabledOn: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const AllStatesDemo = () => {
|
function AllStatesDemo() {
|
||||||
const sizes = ['xs', 'sm', 'md', 'lg'] as const
|
const sizes = ['xs', 'sm', 'md', 'lg'] as const
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -202,7 +202,7 @@ export const AllStates: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const SizeComparisonDemo = () => {
|
function SizeComparisonDemo() {
|
||||||
const [states, setStates] = React.useState({
|
const [states, setStates] = React.useState({
|
||||||
xs: false,
|
xs: false,
|
||||||
sm: false,
|
sm: false,
|
||||||
@@ -260,7 +260,7 @@ export const SizeComparison: Story = {
|
|||||||
render: () => <SizeComparisonDemo />,
|
render: () => <SizeComparisonDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingDemo = () => {
|
function LoadingDemo() {
|
||||||
const [loading, setLoading] = React.useState(true)
|
const [loading, setLoading] = React.useState(true)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -362,7 +362,7 @@ function useMockUpdateAutoRetrySettingMutation({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MutationLoadingDemo = () => {
|
function MutationLoadingDemo() {
|
||||||
const autoRetrySetting = useMockAutoRetrySettingQuery()
|
const autoRetrySetting = useMockAutoRetrySettingQuery()
|
||||||
const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({
|
const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({
|
||||||
onSuccess: autoRetrySetting.setData,
|
onSuccess: autoRetrySetting.setData,
|
||||||
@@ -406,26 +406,28 @@ export const MutationLoadingGuard: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const SkeletonDemo = () => (
|
function SkeletonDemo() {
|
||||||
<div className="flex flex-col items-center space-y-4">
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<SwitchSkeleton size="xs" aria-hidden="true" />
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-gray-700">Extra Small skeleton</span>
|
<SwitchSkeleton size="xs" aria-hidden="true" />
|
||||||
|
<span className="text-sm text-gray-700">Extra Small skeleton</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SwitchSkeleton size="sm" aria-hidden="true" />
|
||||||
|
<span className="text-sm text-gray-700">Small skeleton</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SwitchSkeleton size="md" aria-hidden="true" />
|
||||||
|
<span className="text-sm text-gray-700">Regular skeleton</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SwitchSkeleton size="lg" aria-hidden="true" />
|
||||||
|
<span className="text-sm text-gray-700">Large skeleton</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
)
|
||||||
<SwitchSkeleton size="sm" aria-hidden="true" />
|
}
|
||||||
<span className="text-sm text-gray-700">Small skeleton</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<SwitchSkeleton size="md" aria-hidden="true" />
|
|
||||||
<span className="text-sm text-gray-700">Regular skeleton</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<SwitchSkeleton size="lg" aria-hidden="true" />
|
|
||||||
<span className="text-sm text-gray-700">Large skeleton</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Skeleton: Story = {
|
export const Skeleton: Story = {
|
||||||
render: () => <SkeletonDemo />,
|
render: () => <SkeletonDemo />,
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const States: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormDemo = () => {
|
function FormDemo() {
|
||||||
const [savedDescription, setSavedDescription] = React.useState<string | null>(null)
|
const [savedDescription, setSavedDescription] = React.useState<string | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -153,7 +153,7 @@ export const WithField: Story = {
|
|||||||
render: () => <FormDemo />,
|
render: () => <FormDemo />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
function ControlledDemo() {
|
||||||
const [value, setValue] = React.useState(
|
const [value, setValue] = React.useState(
|
||||||
'Summarize customer feedback into actionable product themes.',
|
'Summarize customer feedback into actionable product themes.',
|
||||||
)
|
)
|
||||||
@@ -180,7 +180,7 @@ export const Controlled: Story = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const CharacterCounterDemo = () => {
|
function CharacterCounterDemo() {
|
||||||
const maxLength = 120
|
const maxLength = 120
|
||||||
const [value, setValue] = React.useState(
|
const [value, setValue] = React.useState(
|
||||||
'Summarize customer feedback into actionable product themes.',
|
'Summarize customer feedback into actionable product themes.',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ToastManager, ToastViewportProps } from '../index'
|
import type { ToastManager, ToastViewportProps } from '../index'
|
||||||
|
import * as React from 'react'
|
||||||
import { userEvent } from 'vite-plus/test/browser'
|
import { userEvent } from 'vite-plus/test/browser'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import {
|
import {
|
||||||
@@ -88,7 +89,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
|
|
||||||
it('should not intercept pointer events below a collapsed top-anchored stack', async () => {
|
it('should not intercept pointer events below a collapsed top-anchored stack', async () => {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<style>{'[role="dialog"] { transition: none !important; }'}</style>
|
<style>{'[role="dialog"] { transition: none !important; }'}</style>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,7 +104,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
Underlying action
|
Underlying action
|
||||||
</button>
|
</button>
|
||||||
<ExampleToastHost manager={manager} timeout={0} />
|
<ExampleToastHost manager={manager} timeout={0} />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
toast('Older notification')
|
toast('Older notification')
|
||||||
@@ -134,7 +135,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
[role="dialog"]:not([data-ending-style]) {
|
[role="dialog"]:not([data-ending-style]) {
|
||||||
@@ -170,7 +171,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ExampleToastHost manager={manager} timeout={0} offset={{ top: 120 }} />
|
<ExampleToastHost manager={manager} timeout={0} offset={{ top: 120 }} />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
toast('Directional notification')
|
toast('Directional notification')
|
||||||
@@ -209,7 +210,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
[role="dialog"][data-ending-style] {
|
[role="dialog"][data-ending-style] {
|
||||||
@@ -230,7 +231,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ExampleToastHost manager={manager} timeout={0} />
|
<ExampleToastHost manager={manager} timeout={0} />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
toast('Background notification')
|
toast('Background notification')
|
||||||
@@ -269,10 +270,10 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
const localManager = createToastManager()
|
const localManager = createToastManager()
|
||||||
const localToast = createToast(localManager)
|
const localToast = createToast(localManager)
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<ExampleToastHost manager={manager} />
|
<ExampleToastHost manager={manager} />
|
||||||
<ExampleToastHost manager={localManager} />
|
<ExampleToastHost manager={localManager} />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
localToast.error('Local error')
|
localToast.error('Local error')
|
||||||
@@ -361,7 +362,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const screen = await render(
|
const screen = await render(
|
||||||
<>
|
<React.Fragment>
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
[role="dialog"][data-ending-style] {
|
[role="dialog"][data-ending-style] {
|
||||||
@@ -391,7 +392,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
|||||||
Underlying action
|
Underlying action
|
||||||
</button>
|
</button>
|
||||||
<ExampleToastHost manager={manager} />
|
<ExampleToastHost manager={manager} />
|
||||||
</>,
|
</React.Fragment>,
|
||||||
)
|
)
|
||||||
|
|
||||||
toast('Dismiss me', {
|
toast('Dismiss me', {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const buttonClassName =
|
|||||||
const cardClassName =
|
const cardClassName =
|
||||||
'flex min-h-[220px] flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6 shadow-sm shadow-shadow-shadow-3'
|
'flex min-h-[220px] flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6 shadow-sm shadow-shadow-shadow-3'
|
||||||
|
|
||||||
const ExampleCard = ({
|
function ExampleCard({
|
||||||
eyebrow,
|
eyebrow,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -63,7 +63,7 @@ const ExampleCard = ({
|
|||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className={cardClassName}>
|
<section className={cardClassName}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -76,7 +76,7 @@ const ExampleCard = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const VariantExamples = () => {
|
function VariantExamples() {
|
||||||
const createVariantToast = (type: 'success' | 'error' | 'warning' | 'info') => {
|
const createVariantToast = (type: 'success' | 'error' | 'warning' | 'info') => {
|
||||||
const copy = {
|
const copy = {
|
||||||
success: {
|
success: {
|
||||||
@@ -132,7 +132,7 @@ const VariantExamples = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StackExamples = () => {
|
function StackExamples() {
|
||||||
const createStack = () => {
|
const createStack = () => {
|
||||||
;[
|
;[
|
||||||
{
|
{
|
||||||
@@ -194,7 +194,7 @@ const StackExamples = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PromiseExamples = () => {
|
function PromiseExamples() {
|
||||||
const [pendingExample, setPendingExample] = React.useState<'success' | 'error' | null>(null)
|
const [pendingExample, setPendingExample] = React.useState<'success' | 'error' | null>(null)
|
||||||
|
|
||||||
const exportDsl = async (outcome: 'success' | 'error') => {
|
const exportDsl = async (outcome: 'success' | 'error') => {
|
||||||
@@ -255,7 +255,7 @@ const PromiseExamples = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionExamples = () => {
|
function ActionExamples() {
|
||||||
const createActionToast = () => {
|
const createActionToast = () => {
|
||||||
let archivedToastId = ''
|
let archivedToastId = ''
|
||||||
archivedToastId = toast.warning('Project archived', {
|
archivedToastId = toast.warning('Project archived', {
|
||||||
@@ -301,7 +301,7 @@ const ActionExamples = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeduplicateExamples = () => {
|
function DeduplicateExamples() {
|
||||||
const saveCountRef = React.useRef(0)
|
const saveCountRef = React.useRef(0)
|
||||||
|
|
||||||
const saveDraft = () => {
|
const saveDraft = () => {
|
||||||
@@ -328,7 +328,7 @@ const DeduplicateExamples = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateExamples = () => {
|
function UpdateExamples() {
|
||||||
const createUpdatableToast = () => {
|
const createUpdatableToast = () => {
|
||||||
const toastId = toast.info('Import started', {
|
const toastId = toast.info('Import started', {
|
||||||
description: 'Preparing assets and metadata for processing.',
|
description: 'Preparing assets and metadata for processing.',
|
||||||
@@ -365,7 +365,7 @@ const UpdateExamples = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToastDocsDemo = () => {
|
function ToastDocsDemo() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ExampleToastHost manager={manager} />
|
<ExampleToastHost manager={manager} />
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const PLACEMENTS: TooltipPlacement[] = [
|
|||||||
'left-end',
|
'left-end',
|
||||||
]
|
]
|
||||||
|
|
||||||
const PlacementsDemo = () => {
|
function PlacementsDemo() {
|
||||||
const [placement, setPlacement] = React.useState<TooltipPlacement>('top')
|
const [placement, setPlacement] = React.useState<TooltipPlacement>('top')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,24 +158,26 @@ const DELAY_PRESETS: Array<{ label: string; delay: number }> = [
|
|||||||
{ label: 'Default', delay: 600 },
|
{ label: 'Default', delay: 600 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const DelayDemo = () => (
|
function DelayDemo() {
|
||||||
<div className="flex items-center gap-3">
|
return (
|
||||||
{DELAY_PRESETS.map(({ label, delay }) => (
|
<div className="flex items-center gap-3">
|
||||||
<TooltipProvider key={delay} delay={delay}>
|
{DELAY_PRESETS.map(({ label, delay }) => (
|
||||||
<Tooltip>
|
<TooltipProvider key={delay} delay={delay}>
|
||||||
<TooltipTrigger
|
<Tooltip>
|
||||||
render={
|
<TooltipTrigger
|
||||||
<DifyIconButton aria-label={`${label} (${delay}ms)`} size="lg" variant="secondary">
|
render={
|
||||||
<span aria-hidden className="i-ri-timer-line size-4" />
|
<DifyIconButton aria-label={`${label} (${delay}ms)`} size="lg" variant="secondary">
|
||||||
</DifyIconButton>
|
<span aria-hidden className="i-ri-timer-line size-4" />
|
||||||
}
|
</DifyIconButton>
|
||||||
/>
|
}
|
||||||
<TooltipContent>{`${label} (${delay}ms)`}</TooltipContent>
|
/>
|
||||||
</Tooltip>
|
<TooltipContent>{`${label} (${delay}ms)`}</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
))}
|
</TooltipProvider>
|
||||||
</div>
|
))}
|
||||||
)
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const WithDelay: Story = {
|
export const WithDelay: Story = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|||||||
@@ -15,16 +15,6 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
tsconfigPaths: true,
|
tsconfigPaths: true,
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
|
||||||
include: [
|
|
||||||
'vite-plus/test/browser',
|
|
||||||
'@daypicker/react/locale/en-US',
|
|
||||||
'@daypicker/react/locale/ar-TN',
|
|
||||||
'@daypicker/react/locale/fa-IR',
|
|
||||||
'@base-ui/utils/useStableCallback',
|
|
||||||
'@base-ui/utils/useTimeout',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
test: {
|
test: {
|
||||||
browser: {
|
browser: {
|
||||||
expect: { toMatchScreenshot: { screenshotDirectory: './.vitest-browser/screenshots' } },
|
expect: { toMatchScreenshot: { screenshotDirectory: './.vitest-browser/screenshots' } },
|
||||||
|
|||||||
Reference in new issue
Block a user