mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-16 16:03:14 +00:00
feat: keep an item's summary and drop its path on a narrow page header
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5309bd2242
commit
60b4c8fdc5
@@ -74,7 +74,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
barWidth: pageHeader.barWidth,
|
||||
chrome: padLeft + padRight + (handleShown ? handleWidth + GAP : 0),
|
||||
trail: trailMeasures.trail,
|
||||
summary: BAR_STAGES.map((s) => (hasSummary && s.summary ? GAP + summaryFloorWidth : 0)),
|
||||
summary: BAR_STAGES.map(() => (hasSummary ? GAP + summaryFloorWidth : 0)),
|
||||
actions:
|
||||
actions.length === 0 ? [0] : actionsRigid ? actionWidths : [GAP + FLEXIBLE_ACTIONS_RESERVE],
|
||||
folds: actions.length > 0 && actionsRigid ? (content?.actionFolds ?? 0) : 0,
|
||||
@@ -236,13 +236,16 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
</svelte:boundary>
|
||||
</div>
|
||||
|
||||
{#if item && hasSummary && stage.summary}
|
||||
{#if item && hasSummary}
|
||||
<!-- No kind icon: the page below is the item, and saying "this is a flow" above a flow
|
||||
editor tells the reader what they can already see. -->
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<!-- A dot rather than a slash: the summary names the same item the path just located, it
|
||||
is not another level of it. -->
|
||||
<span class="shrink-0 text-hint/40 text-xs px-0.5" aria-hidden="true">·</span>
|
||||
<!-- A dot after the path: the summary names the same item the path just located, it is not
|
||||
another level of it. With the path dropped it follows the workspace, and the slash is
|
||||
the trail's own separator. Fixed width, so either fits the width measured below. -->
|
||||
<span class="shrink-0 w-2 text-center text-hint/40 text-xs" aria-hidden="true"
|
||||
>{stage.path ? '·' : '/'}</span
|
||||
>
|
||||
{#if item.summaryContent}
|
||||
<!-- Read only while uncapped: capped, the box says how wide the bar let it be, not how
|
||||
wide the page's control wants to be. -->
|
||||
@@ -315,7 +318,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
what the bar caps it from. A page's own control stands in at the width last seen. -->
|
||||
<div class="absolute -left-[99999px] top-0 invisible" aria-hidden="true" inert>
|
||||
<div class="flex items-center gap-1 w-max" bind:clientWidth={summaryFloorWidth}>
|
||||
<span class="shrink-0 text-hint/40 text-xs px-0.5">·</span>
|
||||
<span class="shrink-0 w-2 text-center text-hint/40 text-xs">·</span>
|
||||
{#if item.summaryContent}
|
||||
<span style="width: {Math.min(summaryContentWidth, FLOOR.summary)}px"></span>
|
||||
{:else}
|
||||
|
||||
@@ -34,18 +34,26 @@ describe('fitBar', () => {
|
||||
expect(fitBar({ ...unseen, actions: [300, undefined, 100] }).fold).toBe(1)
|
||||
})
|
||||
|
||||
it('takes the squeeze from the summary first, then the names, the item name last', () => {
|
||||
it("takes the squeeze from the path's name before the summary", () => {
|
||||
const fit = fitBar({
|
||||
...base,
|
||||
barWidth: 400,
|
||||
actions: [0],
|
||||
folds: 0,
|
||||
trail: trail.map((w) => w - 400),
|
||||
summary: BAR_STAGES.map((s) => (s.summary ? 100 : 0)),
|
||||
summary: BAR_STAGES.map(() => 100),
|
||||
natural: { family: 0, fork: 0, summary: 300, name: 300 }
|
||||
})
|
||||
// Stage 0 at its floors is 300px; whole, the summary and the name add 204 and 140, so 244
|
||||
// has to go: all 204 the summary has above its floor, then 40 from the name.
|
||||
expect(fit).toMatchObject({ stage: 0, caps: { summary: 96, name: 260 }, scopesGive: false })
|
||||
// Stage 0 at its floors is 300px; whole, the name and the summary add 140 and 204, so 244
|
||||
// has to go: all 140 the name has above its floor, then 104 from the summary.
|
||||
expect(fit).toMatchObject({ stage: 0, caps: { name: 160, summary: 196 }, scopesGive: false })
|
||||
})
|
||||
|
||||
it('keeps the summary and drops the path when only one of them fits', () => {
|
||||
const natural = { family: 0, fork: 0, summary: 300, name: 300 }
|
||||
// Stage 2 is the first without the path: the name is no longer a part the bar sizes.
|
||||
const fit = fitBar({ ...base, barWidth: 520, natural, actions: [0], folds: 0 })
|
||||
expect(fit.stage).toBe(2)
|
||||
expect(fit.caps.name).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* How the page header gives up width. Whole parts drop in a fixed order, and the bar shows the
|
||||
* first stage whose parts, each held at its floor width, fit beside the page's actions. Which
|
||||
* stage that is depends on the names in the bar rather than on a pixel breakpoint. Past the last
|
||||
* stage the page's actions fold, one level at a time; past their last level the item's name
|
||||
* truncates, then the folders above it.
|
||||
* stage the page's actions fold, one level at a time; past their last level the item's name and
|
||||
* its summary truncate, then the folders above it.
|
||||
*/
|
||||
|
||||
/** What the breadcrumb itself drops. */
|
||||
@@ -11,6 +11,10 @@ export type TrailStage = {
|
||||
/** Inside a fork, the family's name before the fork's. Outside one it is the workspace's own
|
||||
* name and stays until `wsButton`. */
|
||||
familyName: boolean
|
||||
/** The item's path. Dropped only for an item that has a summary: the summary is what a
|
||||
* person calls the thing, while its path is where it is filed. Without one, the path is the
|
||||
* item's only name and stays. */
|
||||
path: boolean
|
||||
forkName: boolean
|
||||
/** The workspace part as its disc alone, still leading home, beside its picker's chevron. */
|
||||
wsButton: boolean
|
||||
@@ -18,13 +22,13 @@ export type TrailStage = {
|
||||
middleLevels: boolean
|
||||
}
|
||||
|
||||
export type BarStage = TrailStage & { summary: boolean; label: string }
|
||||
export type BarStage = TrailStage & { label: string }
|
||||
|
||||
export const BAR_STAGES: BarStage[] = [
|
||||
{
|
||||
label: 'Full',
|
||||
familyName: true,
|
||||
summary: true,
|
||||
path: true,
|
||||
forkName: true,
|
||||
wsButton: false,
|
||||
middleLevels: true
|
||||
@@ -32,15 +36,15 @@ export const BAR_STAGES: BarStage[] = [
|
||||
{
|
||||
label: 'Family name dropped',
|
||||
familyName: false,
|
||||
summary: true,
|
||||
path: true,
|
||||
forkName: true,
|
||||
wsButton: false,
|
||||
middleLevels: true
|
||||
},
|
||||
{
|
||||
label: 'Summary dropped',
|
||||
label: 'Path dropped for the summary',
|
||||
familyName: false,
|
||||
summary: false,
|
||||
path: false,
|
||||
forkName: true,
|
||||
wsButton: false,
|
||||
middleLevels: true
|
||||
@@ -48,7 +52,7 @@ export const BAR_STAGES: BarStage[] = [
|
||||
{
|
||||
label: 'Fork name dropped',
|
||||
familyName: false,
|
||||
summary: false,
|
||||
path: false,
|
||||
forkName: false,
|
||||
wsButton: false,
|
||||
middleLevels: true
|
||||
@@ -56,7 +60,7 @@ export const BAR_STAGES: BarStage[] = [
|
||||
{
|
||||
label: 'Workspace as its disc',
|
||||
familyName: false,
|
||||
summary: false,
|
||||
path: false,
|
||||
forkName: false,
|
||||
wsButton: true,
|
||||
middleLevels: true
|
||||
@@ -64,7 +68,7 @@ export const BAR_STAGES: BarStage[] = [
|
||||
{
|
||||
label: 'Middle folders collapsed',
|
||||
familyName: false,
|
||||
summary: false,
|
||||
path: false,
|
||||
forkName: false,
|
||||
wsButton: true,
|
||||
middleLevels: false
|
||||
@@ -78,8 +82,9 @@ export type TrailMeasures = { trail: number[]; family: number; fork: number; nam
|
||||
|
||||
/** The width a part may truncate to before the next stage drops something whole. */
|
||||
export const FLOOR: Record<Part, number> = { family: 56, fork: 56, summary: 96, name: 160 }
|
||||
/** Past the last stage and fold, the name keeps this much before the folders give any. */
|
||||
export const SQUEEZED_NAME_FLOOR = 40
|
||||
/** Past the last stage and fold, the item's name and then its summary keep this much before the
|
||||
* folders give any. */
|
||||
export const SQUEEZED_FLOOR = 40
|
||||
|
||||
/** Middle folders collapsed to one `…`; the first folder and the name always stay. */
|
||||
export function shownPathLevels(levels: string[], stage: TrailStage): string[] {
|
||||
@@ -93,14 +98,14 @@ export function partVisible(part: Part, stage: BarStage, natural: Record<Part, n
|
||||
if (natural[part] <= 0) return false
|
||||
switch (part) {
|
||||
case 'summary':
|
||||
return stage.summary
|
||||
return true
|
||||
case 'family':
|
||||
// Outside a fork the family's name is the workspace's: it goes only with the whole part.
|
||||
return !stage.wsButton && (stage.familyName || natural.fork <= 0)
|
||||
case 'fork':
|
||||
return !stage.wsButton && stage.forkName
|
||||
case 'name':
|
||||
return true
|
||||
return stage.path || natural.summary <= 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +115,7 @@ export type BarMeasure = {
|
||||
chrome: number
|
||||
/** Per stage: the breadcrumb with each name at its floor. */
|
||||
trail: number[]
|
||||
/** Per stage: the summary at its floor with the gap before it, 0 where it is dropped. */
|
||||
/** Per stage: the summary at its floor with the separator before it, 0 where there is none. */
|
||||
summary: number[]
|
||||
/** Per fold level: the actions with the gap before them, undefined for a level not yet seen. */
|
||||
actions: (number | undefined)[]
|
||||
@@ -166,7 +171,8 @@ export function fitBar(m: BarMeasure): BarFit {
|
||||
}
|
||||
|
||||
const at = BAR_STAGES[stage]
|
||||
const parts = (['summary', 'family', 'fork', 'name'] as Part[]).filter((p) =>
|
||||
// The summary gives last: it is what a person calls the item, so the path's name goes first.
|
||||
const parts = (['family', 'fork', 'name', 'summary'] as Part[]).filter((p) =>
|
||||
partVisible(p, at, m.natural)
|
||||
)
|
||||
const slack = (p: Part) => Math.max(0, m.natural[p] - Math.min(m.natural[p], FLOOR[p]))
|
||||
@@ -181,6 +187,7 @@ export function fitBar(m: BarMeasure): BarFit {
|
||||
deficit -= give
|
||||
}
|
||||
for (const p of parts) take(p, FLOOR[p])
|
||||
take('name', SQUEEZED_NAME_FLOOR)
|
||||
take('name', SQUEEZED_FLOOR)
|
||||
take('summary', SQUEEZED_FLOOR)
|
||||
return { stage, fold, caps, scopesGive: deficit > 0 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user