// Pure fit decisions for a single-line chip lane that collapses its overflow into a "+N" pill. // Lifted out of the components so the math is unit-testable without a layout engine (jsdom reports // 0-width boxes), the same split as lib/dateFilter and lib/columnSort. The shared DOM-measurement // half — the layout-effect + ResizeObserver that reads real rem boxes off a hidden mirror — lives in // lib/useChipOverflowFit; it hands the measured geometry to one of these policies: // - decideChipLayout: the feed's applied-filter row, which also shares its width with fixed trailing // controls (Sort/View), reserves a leading label + a trailing "Clear all", and collapses to a // single count pill on a narrow lane. // - fitChipsWithPill: the chips-only channel tag cell (ChannelTagsControl) — just "how many fit // before the +N pill". export interface ChipFitInput { /** Number of chips. */ n: number; /** Right edge (offsetLeft + offsetWidth) of the leading label, in the measurer. */ labelRight: number; /** Right edge of each chip, in the measurer (index-aligned to the chips). */ chipRights: number[]; /** Width of the "+N" overflow pill. */ plusW: number; /** Width the trailing "Clear all" button needs INCLUDING its leading gap (0 when n <= 1, since it * only renders with more than one chip). Reserved only in the "all" branch, which renders it. */ clearW: number; /** Available lane width. */ avail: number; /** The real (rem-scaled) inter-item gap. */ gap: number; /** At or below this lane width an overflow collapses to a single count pill instead of "+N". */ narrowPx: number; } export type ChipFit = { mode: "all" | "some" | "count"; visible: number }; export function decideChipLayout(p: ChipFitInput): ChipFit { const { n, labelRight, chipRights, plusW, clearW, avail, gap, narrowPx } = p; if (n <= 0) return { mode: "all", visible: 0 }; // Do all chips (plus the Clear all button it renders) fit? Use the last chip's right edge. const allRight = chipRights[n - 1] ?? labelRight; if (allRight + clearW <= avail) return { mode: "all", visible: n }; // Overflow. On a narrow lane, collapse everything into one count pill. if (avail < narrowPx) return { mode: "count", visible: 0 }; // Otherwise fit as many chips as leave room for the "+N" pill after them. let k = 0; for (let i = 0; i < n; i++) { const right = chipRights[i] ?? 0; if (right + gap + plusW <= avail) k = i + 1; else break; } // If not even one chip fits beside the pill, fall back to the count pill. if (k === 0 && labelRight + gap + plusW > avail) return { mode: "count", visible: 0 }; return { mode: "some", visible: k }; } // The chips-only policy (ChannelTagsControl): how many chips fit on the line, reserving the "+N" pill's // width whenever any overflow. Returns the visible count. export interface ChipsWithPillInput { /** Right edge (offsetLeft + offsetWidth) of each chip, index-aligned. */ chipRights: number[]; /** Width of the "+N" pill. */ pillW: number; /** Available lane width. */ avail: number; /** The real (rem-scaled) inter-item gap. */ gap: number; } export function fitChipsWithPill(p: ChipsWithPillInput): number { const { chipRights, pillW, avail, gap } = p; const n = chipRights.length; if (n === 0) return 0; // Whole-set fast path: if the LAST chip's right edge already fits, every chip fits and NO pill is // needed. The reservation loop below reserves the pill for every chip except the last and breaks at // the first overflow, so on its own it would collapse a short trailing chip (narrower than the pill) // that would actually have fitted, leaving "+N" beside empty space. if ((chipRights[n - 1] ?? 0) <= avail) return n; let count = n; for (let i = 0; i < n; i++) { const right = chipRights[i] ?? 0; const moreAfter = i < n - 1; if (right > avail - (moreAfter ? pillW + gap : 0)) { count = i; break; } } return count; }