fix(web): stop chunk length/overlap inputs collapsing to an unusable width in narrow containers (#39600)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Park 2026-07-27 02:03:17 +00:00 committed by GitHub
parent b81737cfb3
commit d5c0e927c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 4 deletions

View File

@ -197,4 +197,30 @@ describe('GeneralChunkingOptions', () => {
expect(onSummaryIndexSettingChange).toHaveBeenCalledWith({ enable: true })
})
})
// Regression: dify issue 39592 — the delimiter/max-length/overlap row must
// stack (one field per row) in a narrow card and sit three-across when wide,
// driven by a container query rather than the viewport. Fails before this
// change (row was flex-wrap; no @container ancestor).
describe('#39592 narrow-container regression', () => {
it('stacks by default and becomes a row across the 552px container query', () => {
render(<GeneralChunkingOptions {...defaultProps} />)
const delimiterLabel = screen.getByText(`${ns}.stepTwo.separator`)
const row = delimiterLabel.closest('.gap-3')
expect(row).not.toBeNull()
// stacked by default (below threshold)
expect(row!.className).toContain('flex-col')
// three-across at/above the container threshold
expect(row!.className).toContain('@min-[552px]/chunkfields:flex-row')
// the previous flex-wrap approach is gone
expect(row!.className).not.toContain('flex-wrap')
})
it('marks an ancestor as the query container', () => {
render(<GeneralChunkingOptions {...defaultProps} />)
const delimiterLabel = screen.getByText(`${ns}.stepTwo.separator`)
const container = delimiterLabel.closest('[class*="@container/chunkfields"]')
expect(container).not.toBeNull()
})
})
})

View File

@ -136,3 +136,38 @@ describe('OverlapInput', () => {
expect(onChange).toHaveBeenLastCalledWith(100)
})
})
// Regression: dify issue 39592 — in a narrow card the number inputs collapsed
// to a 32px, unusable sliver. The fix reflows on the container (a
// @container/chunkfields ancestor, see general-chunking-options): below 552px
// each field stacks and is capped at max-w-[288px] so the input reads as a form
// field; at/above 552px it restores flex-1 (three across, pixel-identical to
// stock). The input keeps a min-width floor as the belt against re-collapse.
// jsdom has no layout engine, so we cannot assert pixel widths — we assert the
// structural classes. These fail before this change and pass after.
describe('#39592 narrow-container regression (structural)', () => {
it('gives the MaxLength input a min-width floor so it can never collapse to a sliver', () => {
render(<MaxLengthInput onChange={vi.fn()} />)
const input = screen.getByRole('textbox')
expect(input.className).toContain('min-w-[64px]')
})
it('gives the OverlapInput input a min-width floor too', () => {
render(<OverlapInput onChange={vi.fn()} />)
const input = screen.getByRole('textbox')
expect(input.className).toContain('min-w-[64px]')
})
it('caps each field width when stacked and restores flex-1 across the 552px container query', () => {
render(<MaxLengthInput onChange={vi.fn()} />)
const field = screen.getByRole('textbox').closest('.space-y-2')
expect(field).not.toBeNull()
// stacked (default / below threshold): constrained width, no stretch
expect(field!.className).toContain('max-w-[288px]')
// three-across (at/above threshold): restore flex-1 and drop the cap
expect(field!.className).toContain('@min-[552px]/chunkfields:flex-1')
expect(field!.className).toContain('@min-[552px]/chunkfields:max-w-none')
// the previous flex-wrap approach is gone
expect(field!.className).not.toContain('basis-[176px]')
})
})

View File

@ -127,8 +127,10 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
}
noHighlight={isInUpload && isNotUploadInEmptyDataset}
>
<div className="flex flex-col gap-y-4">
<div className="flex gap-3">
<div className="@container/chunkfields flex flex-col gap-y-4">
{/* Container query, not a viewport breakpoint: three across at/above a
552px container, stacked one-per-row below (see inputs.tsx FormField). */}
<div className="flex flex-col gap-3 @min-[552px]/chunkfields:flex-row">
<DelimiterInput
value={segmentIdentifier}
onChange={(e) => onSegmentIdentifierChange(e.target.value)}

View File

@ -5,6 +5,7 @@ import type {
} from '@langgenius/dify-ui/number-field'
import type { FC, PropsWithChildren, ReactNode } from 'react'
import type { InputProps } from '@/app/components/base/input'
import { cn } from '@langgenius/dify-ui/cn'
import {
NumberField,
NumberFieldControls,
@ -30,7 +31,12 @@ const TextLabel: FC<PropsWithChildren> = (props) => {
const FormField: FC<PropsWithChildren<{ label: ReactNode }>> = (props) => {
return (
<div className="flex-1 space-y-2">
// Reflow on the container (a @container/chunkfields ancestor), not the
// viewport. Below 552px the fields stack one per row, each capped at
// max-w-[288px] so the input reads as a form field, not a full-bleed bar.
// At/above 552px this restores flex-1 with no cap, so three columns resolve
// to (container - gaps)/3 — pixel-identical to the stock flex-1 layout.
<div className="max-w-[288px] space-y-2 @min-[552px]/chunkfields:max-w-none @min-[552px]/chunkfields:flex-1">
<TextLabel>{props.label}</TextLabel>
{props.children}
</div>
@ -142,7 +148,10 @@ function CompoundNumberInput({
{...inputProps}
aria-label={label}
size={size}
className={className}
// min-w-[64px] overrides the component's default min-w-0 so the input
// can never collapse to an unusable sliver, even in an unforeseen
// container; belt to the row's flex-wrap braces.
className={cn('min-w-[64px]', className)}
onBlur={onBlur}
/>
{Boolean(unit) && <NumberFieldUnit size={size}>{unit}</NumberFieldUnit>}