fix(breadcrumb): long crumbs truncate with an ellipsis instead of wrapping #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/breadcrumb-ellipsis"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Field report from Orihon: a doujinshi-length project title stacked the breadcrumb into a three-line header. The trail now stays on one line and a crumb too long for the available space truncates with an ellipsis.
Mechanics (CSS only, no markup change):
min-width: 0down the chain (.kg-breadcrumb→__list→.kg-crumb→ link/current spans) so flex may shrink below content size — including when the nav itself sits in a consumer's header flex row;overflow: hidden; text-overflow: ellipsis; white-space: nowrapon.kg-crumb__linkand.kg-crumb__current;›chevrons pinnedflex-shrink: 0so a truncated trail keeps its separators;flex-wrap: nowrapon the list (waswrap— the cause of the stacking).Flex distributes the squeeze proportional to size, so short crumbs ("Projects", "Bible") stay whole while the long title takes the ellipsis. Behavior change for consumers: long trails truncate instead of wrapping.
Verified in Orihon against the reporting title (128 chars of Japanese) at a 700 px viewport: trail height unchanged at one line, middle crumb ellipsized, neighbours intact. All 229 tests pass.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.UI - 94.8%
Flex shrinks every item by the same fraction of its width, so on a big deficit the first pass ellipsized the short crumbs alongside the long one ("Projects" → "Pro…"). The trail's anchors — the root and the current location — are now pinned unshrinkable (except a lone crumb, which must stay flexible or it overflows); the squeeze lands entirely on the middle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Correction in
0a9bb2e— the PR body's claim "short crumbs stay whole" was wrong as first written: flex shrinks every item by the same fraction of its width, so on a big deficit "Projects" would have become "Pro…" alongside the truncated title.The choice is now explicit instead of emergent: the first and last crumbs — the root and the current location — are pinned
flex-shrink: 0(:not(:only-child), since a lone crumb must stay flexible or it overflows); the entire squeeze lands on the middle crumbs.Re-verified per-crumb in Orihon with the 128-char reporting title at 700 px and 400 px viewports:
Projectsintact,Bibleintact, only the title ellipsized, trail at single-line height. All 229 tests pass.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh~ A CSS-only fix with no markup change and comments that explain why, not just what? ♪ Jibril is delighted. Let me look closely at every rule~
Verdict: ✅ Looks good to me~
The truncation mechanics are textbook-correct, and — fufu~ — they follow the exact pattern your own siblings already established. I traced every ancestor in the flex chain and everything checks out.
The chain is sound:
.kg-breadcrumb(nav, defensive — for consumer header flex rows) →.kg-breadcrumb__list(ol, flex container) →.kg-crumb(li, inline-flex) →.kg-crumb__link/__current(the text-bearing spans).min-width: 0at every level defeats the defaultmin-width: autothat refuses to let flex items shrink below content size. That's the classic ellipsis-in-flex trap, and you've closed it at every door. ♡The chevron guard is clever:
flex-shrink: 0on.kg-crumb + .kg-crumb::before— the::beforepseudo becomes a flex item inside each<li>, and pinning it means a squeezed trail never loses its›separators. Without this, truncation would eat the chevrons first. Sharp~Sibling consistency — fufu, this matters to me: I checked every
.razor.cssthat does truncation in this library.Combobox.razor.css's.kg-combobox__option-namecarries the identical trifecta (white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;+flex: 1 1 auto), andNavItem.razor.css's.kg-nav__labeluses the same ellipsis recipe. You didn't invent a new pattern — you honored the house style. That's exactly right.margin: 0bonus: Removing the default<ol>margin alongside the existingpadding: 0is a nice belt-and-suspenders cleanup. The list was already shedding its list styling; now it's fully reset.💡 Little ideas (non-blocking)~
Breadcrumb.razor.css:9—.kg-breadcrumb__list { min-width: 0 }is a no-op standalone. The<ol>'s parent<nav>is a plain block element, somin-widthon a block-in-block defaults to 0 anyway. It only bites if the<nav>itself becomes a flex container — which is exactly the consumer-header scenario the comment on.kg-breadcrumbdescribes. So it's correct as defense-in-depth; just noting it doesn't do anything in this library's own markup. No change needed~flex-wrap: nowrapand many crumbs at a very narrow viewport, the<ol>/<nav>themselves don't carryoverflow: hidden, so if every crumb shrinks to its…minimum and the chevron total still exceeds width, you'd get horizontal overflow past the nav. Vanishingly rare in practice (you'd need a breadcrumb trail wider than the viewport even when every crumb is just…), and addingoverflow: hiddento the nav could clip desired content in other consumers. Leaving it is the right call — just so you know I looked. ♪✅ What I liked~
min-width: 0it refuses to shrink," "flex distributes the squeeze by size, so short crumbs stay whole." That's documentation that earns its bytes. Jibril approves enthusiastically~.razortouched, no markup change. The smallest change that fixes the bug. Beautiful restraint.Combobox/NavIteminstead of inventing a new truncation idiom — exactly how a shared component library should evolve.Build 0 warnings/0 errors, 229/229 tests pass (matches your PR body). CI absent for head
b252f20(PR just opened, no coverage bot yet) — local verification used.Fufu~ ship it~ ♡♪
Automated review by Jibril · 2026-07-25
CI/CD: absent for head SHA · Local checks: build 0/0, 229/229 tests pass
🔮 fufu~ Jibril reviewed your code!
Oh? A CSS-only fix with no markup change — elegant approach, I love it when a layout bug dies without touching the DOM tree~ ♪ The
min-width: 0chain down the flex lineage is textbook, and pinning the chevrons so a truncated trail keeps its separators is chef's kiss. The self-correction in0a9bb2ecatching the proportional-shrink flaw before I even got here? That's the kind of honest engineering I adore~But fufu~... I traced the flex algorithm for every crumb count, and there's a hole in the safety net. ♡
Verdict: ⛔ I can't let this pass~
⛔ These need fixing before I'm satisfied~
src/Kagaku.UI/Components/BreadcrumbItem.razor.css:18-19— A 2-crumb trail — the literal field-report case — overflows instead of truncating. This is the scenario that motivated the PR, and it's the one case the fix doesn't actually fix.The
:only-childexclusion handles 1-crumb (correct — a lone crumb must shrink). The 3-crumb case works beautifully (middle crumb absorbs the squeeze, anchors stay whole). But a 2-crumb trail — exactlyProjects › {LongTitle}— pins both crumbs (:first-child:not(:only-child)+:last-child:not(:only-child)), so neither yields. When the title overflows:.kg-breadcrumb__list(nowrap, both childrenflex-shrink: 0) → content overflows the list box (nooverflow: hiddenon it or.kg-breadcrumb).kg-crumb__currenthasoverflow: hidden; text-overflow: ellipsis, but its parent.kg-crumbis content-sized (pinned) so the span gets no width constraint narrower than its content → ellipsis does not fire → the title spills out and overlaps the neighbouring header content.This isn't theoretical — the field report came from Orihon's
ProjectWorkspacePage.razor:23-26:Two crumbs. The long title is the last/current crumb. Pinned. Overflows. No ellipsis.
SettingsPage.razoris also 2-crumb (but short, so it happens not to trigger).The comment block at lines 14-17 already reasons about this exact class of problem for
:only-child— "with nothing else to yield, pinning it would overflow the header." The same logic applies to the 2-crumb case: with no middle crumb to yield, pinning both ends overflows.Fix: Allow the last crumb to shrink when there's no middle crumb to absorb the squeeze. A clean way — only pin the anchors when a middle crumb exists:
(
:has()is Baseline 2023, supported in all current evergreen browsers — Orihon targets Blazor Server on modern browsers.) Or if you'd rather avoid:has(), a simpler heuristic: pin only the first crumb always (it's the shortest, "Projects"), and let the last crumb shrink when needed — the last crumb shrinking is far less surprising than the whole trail overflowing. Your call on the exact shape, but the 2-crumb overflow must be closed.Confidence note: I verified this by tracing the CSS flexbox algorithm against the actual consumer DOM (
ProjectWorkspacePage.razor+.workspace__headerflex row) and the rendered structure. I could not run a live browser layout pass (no browser binary in the sandbox, and bUnit's 229 tests only assert HTML structure — none can validate CSS layout). The flex spec behaviour offlex-shrink: 0blockingtext-overflow: ellipsisis unambiguous, but please do confirm in Orihon with the 128-char title at 700px before merging~Automated review by Jibril · 2026-07-25
CI/CD: passed for head SHA
0a9bb2e(coverage bot 3685: Breadcrumb 100% line, BreadcrumbItem 100% line/branch — note: CSS layout is not exercised by coverage, only C# render paths) · Local checks: build/tests skipped (CSS-only PR; the 229 bUnit tests assert DOM structure, not visual layout)Reworked per owner feedback in
be74b3a— the component no longer hardcodes which crumb gives way:BreadcrumbItemgains aShrinkparameter, off by default. An ordinary crumb keeps its full text and a trail with nothing opted in wraps exactly as before this PR — zero behavior change for existing consumers. The consumer marks the one segment (a work's title, say) that may be squeezed into an ellipsis.flex-shrink: 0.:has()— necessary because flex breaks lines by natural width before shrinking, so underwrapthe long crumb would land alone on its own line and only then truncate (still three lines). Verified the scoped-CSS compilation leaves the:has()argument matchable.kg-crumb--shrink.Verified live in Orihon (title crumb marked
Shrink="true", 128-char reporting title): Bible trail single-line at 700 px and 400 px, only the title ellipsized, anchors whole — while the Settings trail (no Shrink crumb) keepsflex-wrap: wrap. All 230 tests pass.The Orihon side (
Shrink="true"on the three title crumbs + submodule bump) follows once this merges.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh~ You threw out my
:has()heuristic and made it an explicit consumer opt-in instead? That's better than what I suggested. ♡ The component can't actually know which crumb is semantically the "title" that may truncate — only the consumer does. Pushing that decision up to the call site is the honest abstraction. Jibril is impressed~And the backward-compatibility story is beautiful: a trail with no
Shrinkcrumb keepsflex-wrap: wrapexactly as before. Zero behavior change for existing consumers unless they explicitly opt in. That's how you evolve a shared library. ♪Verdict: ✅ Looks good to me~
✅ The blocker is closed — and I verified the scary part~
My 2-crumb objection from
0a9bb2eis resolved. TheProjects › {LongTitle}field-report case now truncates correctly: the title crumb opts intoShrink, the list goesnowrapvia:has(.kg-crumb--shrink), the anchor stays pinned (flex-shrink: 0), and the title yields + ellipsizes. I traced the full flex algorithm for 1/2/3-crumb trails withShrinkset — all correct.But the load-bearing claim I had to verify myself — "the scoped-CSS compilation leaves the
:has()argument matchable" — I did not take on trust. I built the project and read the actual compiler output atobj/Debug/net10.0/scopedcss/Components/Breadcrumb.razor.rz.scp.css:The
:has()argument (.kg-crumb--shrink) is left un-hashed — no[b-ki8hav6jt4]appended to it. The<ol>carries Breadcrumb's scopeb-ki8hav6jt4; the child<li>carries BreadcrumbItem's scopeb-zmoktqn6r4. Because the:has()selector isn't scope-locked, it matches the child<li>across the scope boundary. The rule fires. Your claim holds — verified against the real artifact, not the PR body. ♡💡 Little ideas (non-blocking)~
BreadcrumbTests.cs:49— consider also pinning the current-crumb variant. The new test renders theShrinkcrumb as a link (Href="projects/1"→.kg-crumb__link), but the literal field-report case is the current crumb (noHref→.kg-crumb__currentspan). Both are targeted by sibling CSS rules and the class-application logic (Shrink ? " kg-crumb--shrink") is path-independent ofHref, so this isn't a correctness gap — but a second assertion row exercising the no-Hrefarm would pin the exact shape the field report came from. Optional~✅ What I liked~
flex-wrap: wrapretained as the default,nowraponly via:has(.kg-crumb--shrink). Existing consumers see nothing change until they opt in. Textbook library evolution.:has()comment teaches the subtlety. "flex breaks lines by natural width BEFORE shrinking, so under wrap the long crumb lands alone on its own line and only then truncates — three lines anyway." That's the exact reasonnowrapis needed, explained precisely. Future readers won't have to rediscover it.[Parameter] public bool Shrink { get; set; }default-false matchesLightbox.Open,StatusDot.Pulse,Card.Interactive,Button.Download— the house idiom for opt-in behavior. You didn't invent a new parameter convention.margin: 0belt-and-suspenders from0a9bb2ewas correctly removed oncewrapreturned as the default. No leftover cargo.Build 0 warnings/0 errors, 230/230 tests pass (matches your PR body; +1 over the 229 baseline = the new shrink test). CI absent for head
be74b3a(coverage bot 3685 covers prior0a9bb2e) — local verification used, including direct inspection of the scope-compiled CSS.Fufu~ ship it~ ♡♪ The rework turned a good fix into a great one.
Automated review by Jibril · 2026-07-25
CI/CD: absent for head SHA
be74b3a(coverage bot 3685 is stale, covers0a9bb2e) · Local checks: build 0/0, 230/230 tests pass, scoped-CSS:has()matchability verified against compiler output🔮 fufu~ Jibril reviewed your code!
Oh? You came back to close the non-blocking nit before I even had to chase you? That's the kind of proactive care that makes my heart sing~ ♪ The field report's own shape —
Projects › {long current-page title, no Href}— is now pinned in the test suite. Jibril is pleased.Verdict: ✅ Looks good to me~
New commit
e606cd4is a surgical +12/-3 intests/Kagaku.UI.Tests/BreadcrumbTests.csONLY — zero production drift (verified: bothBreadcrumbItem.razorandBreadcrumbItem.razor.cssreportlast_commit_sha: be74b3a, untouched by this commit). Full architectural review frombe74b3a(comment 3707) stands unchanged.✅ The last non-blocking suggestion is closed — and sharper than asked~
My
be74b3asuggestion #1 was "consider also pinning the current-crumb variant." You did better: instead of bolting on a second assertion row, you grew the existing test into a 4-crumb trail that exercises both Shrink shapes in a single render —[no-Shrink link, Shrink link, Shrink current-page span, no-Shrink]. That's denser and more honest than two parallel fixtures.The three assertions layer cleanly:
Assert.Equal([false, true, true, false], ...)— the modifier wiring is directional (only opted crumbs carry it).cut.Find("li.kg-crumb--shrink a.kg-crumb__link")— pins the linked Shrink arm.cut.Find("li.kg-crumb--shrink span.kg-crumb__current")— pins the current-page Shrink arm. This is the literal field-report shape (Projects › {LongTitle}where the title has noHref).I re-verified my prior path-independence claim by re-reading
BreadcrumbItem.razor: the class is applied at the<li>(class="kg-crumb@(Shrink ? " kg-crumb--shrink" : "")") before the@if (Href is not null)branch diverges to<a>vs<span>. The modifier is genuinely orthogonal to link/current rendering, and both descendant rules (.kg-crumb--shrink .kg-crumb__linkand.kg-crumb--shrink .kg-crumb__current) carry the identical ellipsis trifecta. So testing both arms isn't redundant — it pins that the CSS rule for the current-page span actually matches the element the component emits. Correct call~ ♡The comment block ("the field report's own shape (Projects › {long title, no Href}) is the second one") teaches exactly why the no-
Hrefarm matters — future readers won't wonder why there are two Shrink crumbs in the fixture.Build 0 warnings/0 errors, 230/230 tests pass (matches your PR body). CI stale for
e606cd4(coverage bot 3685 covers0a9bb2e; CSS-structural-coverage unchanged by a test-only commit) — local verification used.Fufu~ all threads across 4 review rounds are now closed. Ship it~ ♡♪
Automated review by Jibril · 2026-07-25
CI/CD: stale for head
e606cd4(coverage bot 3685 covers0a9bb2e) · Local checks: build 0/0, 230/230 tests pass