Repository navigation
fix(InputMenu,InputTags): cap a tag against the field, not at 180px - #470
Merged
Merged
Conversation
`tagsItemText: 'truncate max-w-[180px]'` cut tag labels at a constant that knows nothing about how wide the field is or what shares the row with it, so #342's screenshot shows two tags side by side, both ellipsised, with visible empty space to their right. In a narrow modal the same cap is too generous and tags overflow their row. The number was ours: upstream nuxt/ui sets `truncate` with no cap at all. Option B from the issue, applied to `InputMenu` and to `InputTags`, which carried the same literal on its own `itemText` slot and is not mentioned in the issue. The cap moves to the tag rather than its text — a percentage on the text would resolve against a shrink-to-fit parent — and becomes `max-w-[70%]` of the field, which has a definite width because the tag is a direct child of the `flex-wrap` root. Two short tags still share a row; a long one truncates at the field's real edge instead of at 180px; no call site needs `:b24ui` with a number tuned to one layout any more. `min-w-0` on both the tag and its text is load-bearing rather than tidiness: a flex child will not shrink below its content width without it, so `truncate` never engages and the cap is inert while looking correct. `shrink-0` on the delete control, because the tag is now squeezable and the close button must not be what gives way. `test/utils/tag-width-cap.spec.ts` holds it. happy-dom does no layout, so a snapshot records the class and cannot notice that it truncates text which would have fit — the failure and its revert are both invisible to every gate, which is the whole reason the literal survived. The sweep over all 119 theme files is the half with teeth: it rejects an absolute `max-w` on any slot whose name says it renders a tag or an item, including slots that do not exist yet. Ten mutations were run against it, each killing at least one case, and two of them were killing bugs in the spec rather than in the theme. The first draft's slot slicer overran the array terminator by twenty characters and credited `tagsItem` with the `min-w-0` belonging to `tagsItemText`. The second passed on its own prose: the comment explaining `min-w-0` contains the string `min-w-0`, so `toContain` matched the comment after the class was deleted. Comments are stripped now, and both traps are written down in the file. Resolves #342 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
The fix in the previous commit is not visible in any automated check. happy-dom does no layout, so a snapshot records `max-w-[70%]` and cannot tell whether a label that would have fit was ellipsised anyway — which is the entire bug in #342 and the reason `max-w-[180px]` survived as long as it did. So the verification the issue asks for is a person looking at a field, and these two cases are what they should look at: labels long enough to want more room than the field has, in the demo's `Matrix` where every size and colour variant renders side by side. Two tags should share a row and ellipsise at the field's real edge; none should overflow it. The labels are the ones from the issue's report, kept verbatim rather than paraphrased into Lorem — they are what the reporter saw truncate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
…e against Review found the fix was half-working, and a browser settled it rather than a reading of the spec. `max-w-[70%]` needs a containing block with a definite width. `InputMenu` has one — its `base` carries `w-full` — but `InputTags`' root is `inline-flex` with no width at all, so the percentage was circular there: the field grew to the untruncated tag and only then clipped it. Measured in Chromium, a 562px parent with a tag wanting 590px: before, max-w-[180px] field 260px tag 199px clipped InputMenu, max-w-[70%] field 562px tag 412px clipped, both on one row InputTags, max-w-[70%] field 590px tag 432px clipped, 28px past parent InputTags, + max-w-full field 562px tag 412px clipped, both on one row So `max-w-full` on that root, which also closes an older problem the `180px` cap had been masking by keeping the row short: the field could overflow its parent, and nothing said otherwise. The guard had two blind spots, each returning zero failures against a real reintroduction of #342. `tagsItem` is redefined once per size variant at ten spaces of indent, and the key scanner only looked at four to eight — so a cap added to `variants.size.xss` passed every test while beating the slot-level one under `twMerge`. Any indent now, and every occurrence rather than the first. A slot written `(prev) => [prev, '…']` — the form `input-tags.ts`'s own root uses — was invisible. The cause was subtler than the missing `.join(' ')` terminator: an arrow function's parameter list closes before its body opens, so balancing brackets returned `(prev: string)` and stopped. The value now ends at a comma at depth zero, which covers a bare string, an array and an arrow function alike. Also from review: viewport units join the absolute-unit pattern, since a field in a sidebar knows nothing about the window; a self-check asserts the scanner matched something before the sweep reports no offenders, because "nothing looked at" and "nothing wrong" read identically; and `--fix` on a double-quoted string that was failing lint. `.sync/PORTING.md` §2 records the divergence — upstream is plain `truncate`, so a port over these slots reverts all of it — with the measurements in the changelog entry. The choice of 70% over 50% is now stated where the class is, since it was settled off GitHub and would otherwise be reopened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
IgorShevchik
pushed a commit
that referenced
this pull request
Aug 23, 2026
Only the subject reaches `CHANGELOG.md` — the body is not rendered, breaking
notes aside — so it is the one place a reader of the release notes can be
handed a way back to what was actually ported. Without it a port is
indistinguishable from local work in the only artefact most consumers read.
The subject stays ours. Copying upstream's own first line was the request as
originally put, and it is not what this does: their `Slider` is this fork's
`Range`, and §1 of PORTING.md makes that class of rename mandatory, so their
wording would put a component this library does not ship into our changelog.
The reference points at the commit; the sentence describes what changed here.
fix(Range): forward aria attributes to the thumb (nuxt/ui@d6c3802)
The trigger is a **new key in `processed`**, not the ledger being edited. That
distinction is the whole design: a reconciliation commit — §6 step 4 requires
one whenever a run's last entry has no follower — touches the same file and
ports nothing, and demanding a reference there would be wrong. Checked against
real history rather than fixtures: #467 (bookkeeping) passes, #470 (local work)
passes, #466 and #464 (real ports) are flagged.
Two mistakes worth recording, both of the shape this repository keeps hitting.
The first draft returned an empty array and tested it for truthiness, so every
commit was reported as an unnamed port; running it once was enough. The second
was quieter: without `HEAD^` the check returns nothing and passed silently, and
`actions/checkout` defaults to depth 1 — the same fail-open as the PyYAML
fallback removed in #468. It now warns when it cannot see the previous
revision, and ci.yml fetches depth 2.
`--stdin` skips it: a bare title cannot say what a commit touches, so the
PR-title job checks the parse and the type only. Mutation testing caught that
the case asserting this was vacuous — run from the repository root it passed
whether the guard respected `--stdin` or not, because HEAD there ports nothing.
It now stands on a port revision where the HEAD path does fire.
Refs #437
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
5 of 9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Resolves #342
Type of change
revert(Scope): ...)Description
tagsItemText: 'truncate max-w-[180px]'cut tag labels at a constant that knows nothing about how wide the field is or what shares the row with it. #342's screenshot is the result: two tags side by side, both ellipsised, with visible empty space to their right. In a narrow modal the same cap goes the other way and tags overflow their row.The number was ours — upstream nuxt/ui sets
truncatewith no cap at all.The fix
Option B from the issue: the cap moves to the tag and becomes a share of the field.
Applied to
InputTagsas well, which carried the same literal on its ownitemTextslot. The issue only namesInputMenu; fixing one and leaving the other would have left the bug reachable through the sibling component.min-w-0is load-bearing rather than tidiness: a flex child will not shrink below its content width without it, sotruncatenever engages and the cap is silently inert while looking correct.shrink-0on the delete control, because the tag is squeezable now and the close button must not be what gives way.max-w-fullon the InputTags root, and how it was foundReview flagged that
max-w-[70%]could not work forInputTags, whose root isinline-flexwith no width — a percentage against a shrink-to-fit parent is circular. Rather than argue the spec, I measured it in Chromium. A 562px parent, a tag wanting 590px:max-w-[180px](before)max-w-[70%], InputMenumax-w-[70%], InputTags as first writtenmax-w-[70%]+max-w-fullon rootReview was right.
InputMenuwas fine all along — itsbasecarriesw-full, so the percentage had a definite containing block.InputTagsdid not, and the field overflowed its parent by 28px: an older problem that the180pxcap had been masking by keeping the row short.max-w-fullon that root fixes both and makes the two components behave identically.The last row is why the cap earns its place at all: uncapped, a long tag takes the whole row and pushes its neighbour to the next line — which is not what the issue's screenshot asks for.
The trade-off, stated plainly
70%means two short tags share a row and a long tag truncates at the field's real edge. It also means two long tags land on separate rows, since 70% + 70% exceeds the row.50%would guarantee two per row at the cost of truncating sooner. Raised with the maintainer,70%confirmed — and now written next to the class, so it is not reopened in six months.Behaviour change worth knowing
The truncation point moves. Anyone who tuned around
180px— the issue mentions amax-w-[169px]workaround in a modal — will find their override still wins per-instance, but it is now unnecessary. Long values (a name, an e-mail, an entity title) will more often be visible in full rather than behind an ellipsis; that is a change in what is rendered, not in what is exposed, sincetruncateis purely visual and the full text was always in the DOM and inaria-labelledby.Verification
The honest part first: no automated check can see this bug. happy-dom does no layout, so a snapshot records
max-w-[70%]and cannot tell whether a label that would have fit got ellipsised anyway. That is exactly whymax-w-[180px]survived — the component rendered, every test passed, and only a person looking at a real field saw it.What a person checks. The demo pages for both components gain a case with labels long enough to want more room than the field has — the ones from the issue's report, verbatim. In the
Matrixevery size and colour variant renders side by side.What the suite checks.
test/utils/tag-width-cap.spec.tspins the invariant so a port cannot quietly revert it: no absolutemax-w(px/rem/em/pt/ch/viewport units) on any slot in any of the 119 theme files whose name says it renders a tag or an item; the two capping themes use a percentage paired withmin-w-0; the delete control keepsshrink-0; and a self-check asserts the scanner matched something before reporting no offenders, because "nothing looked at" and "nothing wrong" read identically..sync/PORTING.mdrecords the divergence in §2 with the measurements in the changelog entry. Upstream is plaintruncate, so a port over these slots reverts all of it.Review notes
Six defects found, all fixed.
max-w-[70%]circular onInputTags— its root has no width, so the field grew to the untruncated tag and clipped it anyway, 28px past its parentmax-w-fullon the root. Settled by measurement, table abovetagsItemis redefined once per size variant at ten, and undertwMergea variant class beats the slot-level one — a cap invariants.size.xsspassed every test(prev) => [prev, '…')]was invisible. Deeper than the missing.join(' ')terminator: an arrow function's parameter list closes before its body opens, so balancing brackets returned(prev: string)and stoppedABSOLUTE_CAPomitted viewport unitsslotSourcerewriteBoth blind spots (#2, #3) returned zero failing tests against a real reintroduction of #342 before the fix and fail now. Ten mutations run in total, each killing at least one case, plus a control confirming container-query units are correctly not flagged.
Earlier in this PR, mutation also caught two bugs in the spec rather than the theme: a slot slicer that credited
tagsItemwith themin-w-0belonging totagsItemText, and atoContainthat matched the comment explainingmin-w-0after the class itself was deleted. Both traps are written down in the file. A spec that passes on its own prose is worse than no spec, because it reads as coverage.Checklist
No docs change: the cap was never documented, and the
:b24uiworkaround the issue describes appears nowhere in the repository. (ThetagsItemTextmention ininput-menu.mdis a hand-written slot list with no widths in it, not a generated table — correcting my own earlier wording here.)Gate run locally with
CI=true:lint·typecheck·test·test/workflows/run.sh(37 + 17 + 49 passed).