Files
siftlode/frontend/src/lib/chipOverflow.ts
T
peter e90603d4db fix(feed): deep-review remediations for the filter-bar overhaul
Correctness / behaviour:
- Move the Personal Views modal from inside the collapsible filter rail to the
  feed page (moduleRegistry FeedPage), so opening it from the always-visible
  toolbar view chip works while the rail is unpinned (its default) instead of
  no-op'ing and latching modalOpen into a later stale pop-open.
- Migrate a persisted legacy global `tagMode` into per-category `tagModes` on the
  localStorage seed paths too (loadAccountFilters), matching the saved-view and
  URL paths — an upgrading user's topic any/all no longer silently reverts to 'or'.
- DateFilterSelect: reset the custom reveal on an external clear while the popover
  is closed (guard on !open, not maxAgeDays), so the trigger reverts to "Any time"
  instead of lingering on "Custom range" with empty fields.
- PersonalViewsModal delete: only advance the detail selection when the delete
  actually happened (remove() now resolves a boolean) — cancelling the confirm no
  longer switches the selected view.
- SavedViewsProvider: prefer the explicitly-applied (sticky) view among several
  that capture identical filters, so apply(B) isn't re-attributed to an earlier
  duplicate A (wrong "modified" chip name).

A11y:
- Give the clickable panel title a descriptive accessible name (titleActionLabel,
  wired to "Personal views") instead of announcing only "Filters".
- Give the "+N" overflow pill an aria-label ("N more filters").
- Use t("views.reorder") for the saved-view drag handle instead of a hardcoded
  English "reorder" literal.

Also extract the chip-fit decision into a pure, testable lib/chipOverflow.ts and
measure the inter-chip gap live (rem-safe) instead of a frozen 6px constant.
2026-08-22 06:33:48 +02:00

54 lines
2.5 KiB
TypeScript

// Pure fit decision for a single-line chip lane that must share its width with fixed trailing
// controls (the feed's applied-filter row + Sort/View). Lifted out of ActiveFilterChips so the
// all/some/count math is unit-testable without a layout engine (jsdom reports 0-width boxes), the
// same split as lib/dateFilter and lib/columnSort. The caller measures real widths off a hidden
// mirror (including the live rem gap, so nothing is frozen in px) and hands them here.
//
// A sibling one-line-fit measurer lives in ChannelTags (ChannelTagsControl); if a third appears,
// promote the DOM-measurement half to a shared hook and keep this as its policy core.
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 };
}