Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 78 additions & 15 deletions e2e/tag-shelf-resize.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,75 @@ import {
const TAG_COUNT = 10
const RESTING_CHIPS = 8 // MIN_TAG_ROWS (2) × TAGS_PER_ROW (4)
const DRAGGED_CHIPS = 12 // one row deeper
const OLD_CEILING_CHIPS = 48 // what twelve rows used to be worth
// var(--space-3) under the last chip row, matching the air over the shelf's
// separator: the strip's border used to sit 5px under the chips at every depth.
const BOTTOM_GAP = 10
// Whatever the shelf is dragged to, the sections keep this much of the column.
const LIST_FLOOR = 150
// MAX_TAGS is 20 per snippet, so a deep shelf takes several.
const PER_SNIPPET = 18

async function seedTags(page) {
async function seedSnippet(page, name, tags) {
await newSnippetButton(page).click()
const editor = page.getByRole('dialog', { name: 'New Snippet' })
await editor.getByPlaceholder('Snippet name…').fill('Tag farm')
await editor.getByPlaceholder('Snippet name…').fill(name)
await editor.locator('.editor').click()
await page.keyboard.type('seeded body')
for (let i = 1; i <= TAG_COUNT; i++) {
await editor.getByPlaceholder('add a tag…').fill(`tag-${String(i).padStart(2, '0')}`)
for (const tag of tags) {
await editor.getByPlaceholder('add a tag…').fill(tag)
await editor.getByPlaceholder('add a tag…').press('Enter')
}
await editor.getByRole('button', { name: 'Save', exact: true }).click()
await expect(editor).toBeHidden()
}

async function dragShelfDownOneRow(page) {
const rowStep = (await page.locator('.usb-tag').first().boundingBox()).height + 4
async function seedTags(page, count = TAG_COUNT) {
const names = Array.from({ length: count }, (_, i) => `tag-${String(i + 1).padStart(2, '0')}`)
for (let at = 0; at < names.length; at += PER_SNIPPET) {
await seedSnippet(page, `Tag farm ${at}`, names.slice(at, at + PER_SNIPPET))
}
}

async function dragShelf(page, toY) {
const box = await page.locator('.usb-shelf-grip').boundingBox()
const x = box.x + box.width / 2
const y = box.y + box.height / 2
await page.mouse.move(x, y)
await page.mouse.down()
await page.mouse.move(x, y + rowStep, { steps: 6 })
await page.mouse.move(x, toY(y), { steps: 6 })
await page.mouse.up()
}

async function dragShelfDownOneRow(page) {
const rowStep = (await page.locator('.usb-tag').first().boundingBox()).height + 4
await dragShelf(page, (y) => y + rowStep)
}

// The air between the last chip row and the strip's border, which is the same
// number at every depth.
async function bottomGap(page) {
const tags = await page.locator('.usb-tags').boundingBox()
const strip = await page.locator('.usb-controls').boundingBox()
return strip.y + strip.height - (tags.y + tags.height)
}

// The divider under the shelf is the ONE thing that looks draggable, so it has
// to be the thing that is: both the paint and the hit area straddle it. Adrift
// in the air above, the handle lights up somewhere the pointer is not and the
// seam itself grabs nothing.
async function expectHandleOnTheSeam(page) {
const strip = await page.locator('.usb-controls').boundingBox()
const grip = await page.locator('.usb-shelf-grip').boundingBox()
const seam = strip.y + strip.height
expect(Math.abs(grip.y + grip.height / 2 - seam)).toBeLessThanOrEqual(1)
const onSeam = await page.evaluate(
([x, y]) => document.elementFromPoint(x, y)?.className ?? '',
[strip.x + strip.width / 2, seam - 0.5]
)
expect(onSeam).toContain('usb-shelf-grip')
}

test('the shelf grip deepens the tag rows, and the depth survives a relaunch', async () => {
test.setTimeout(90_000)
const dir = freshUserDataDir()
Expand All @@ -56,14 +99,9 @@ test('the shelf grip deepens the tag rows, and the depth survives a relaunch', a
await expect(chips).toHaveCount(RESTING_CHIPS)
await expect(page.locator('.usb-more')).toBeVisible()

// The handle IS the section's bottom edge: flush under the chips AND flush
// with the strip's own boundary — floating anywhere between, it reads as
// some other divider's handle.
const tagsBox = await page.locator('.usb-tags').boundingBox()
const gripBox = await page.locator('.usb-shelf-grip').boundingBox()
const stripBox = await page.locator('.usb-controls').boundingBox()
expect(gripBox.y - (tagsBox.y + tagsBox.height)).toBeLessThanOrEqual(1)
expect(stripBox.y + stripBox.height - (gripBox.y + gripBox.height)).toBeLessThanOrEqual(1)
await expectHandleOnTheSeam(page)
const restingGap = await bottomGap(page)
expect(restingGap).toBeGreaterThanOrEqual(BOTTOM_GAP)

// "+6 more" opens the SIX that did not fit — not the whole registry with
// the promised six lost inside it.
Expand All @@ -81,6 +119,8 @@ test('the shelf grip deepens the tag rows, and the depth survives a relaunch', a
// One row deeper: four more chips show and the overflow count falls in step.
await expect(chips).toHaveCount(DRAGGED_CHIPS)
await expect(page.locator('.usb-more')).toContainText('+2')
// And the air under the shelf is the air it had before the drag.
expect(await bottomGap(page)).toBeCloseTo(restingGap, 0)
} finally {
await app.close()
}
Expand All @@ -94,3 +134,26 @@ test('the shelf grip deepens the tag rows, and the depth survives a relaunch', a
await app.close()
}
})

// Dragged to the floor of the window: the shelf goes far deeper than the dozen
// rows it used to stop at, and the sections it shares the column with keep a
// usable slice of it — squeezed out, they take the grip off the bottom edge with
// them and there is no way left to make the shelf small again.
test('the shelf deepens past the old ceiling without swallowing the sidebar', async ({ page }) => {
test.setTimeout(120_000)
await seedTags(page, 108)
await expect(page.locator('.usb-tag')).toHaveCount(RESTING_CHIPS)

const floor = await page.evaluate(() => window.innerHeight - 4)
await dragShelf(page, () => floor)

expect(await page.locator('.usb-tag').count()).toBeGreaterThan(OLD_CEILING_CHIPS)

const aside = await page.locator('.saved').boundingBox()
const list = await page.locator('.usb-scroll').boundingBox()
const grip = await page.locator('.usb-shelf-grip').boundingBox()
expect(list.height).toBeGreaterThanOrEqual(LIST_FLOOR)
expect(grip.y + grip.height).toBeLessThanOrEqual(aside.y + aside.height)
expect(await bottomGap(page)).toBeGreaterThanOrEqual(BOTTOM_GAP)
await expectHandleOnTheSeam(page)
})
40 changes: 26 additions & 14 deletions src/renderer/src/components/styles/SavedDiffs.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

/* Fixed control strip: search + the segmented group filter. */
.usb-controls {
position: relative; /* the shelf grip pins to its bottom edge */
flex: none;
flex-direction: column;
align-items: stretch;
Expand All @@ -58,13 +59,19 @@
padding: 0 var(--space-3) var(--space-2);
border-bottom: 1px solid var(--border);
background: var(--bg-elevated);
/* The sections keep this much of the column whatever the shelf is dragged to,
and whatever the window is resized to afterwards. Squeezed out they take
the grip off the bottom edge with them, and a shelf with no reachable grip
cannot be made small again. Past the cap the chips scroll instead. */
max-height: calc(100% - 150px);
}
/* Section toggles — independent pills (MULTI-select), so several can be on at
once. "On" fills accent-tinted; "off" is a quiet outline. All + ★ are helpers,
set slightly apart with a divider. */
/* The search field and its one modifier share a row, so the band keeps the
height the rail lines up against. */
.usb-find {
flex: none;
gap: var(--space-2);
}
.usb-find > :first-child {
Expand All @@ -90,10 +97,14 @@
color: var(--pin-ink);
border-color: color-mix(in srgb, var(--favorite) 40%, var(--border));
}
/* The one part of the strip that gives way when the shelf is deeper than the
column can hold: the chips scroll rather than pushing the sections out. */
.usb-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 0;
overflow-y: auto;
margin-top: var(--space-1);
margin-bottom: var(--space-1);
}
Expand Down Expand Up @@ -138,16 +149,21 @@
padding-bottom: var(--space-4);
}

/* Drag-to-deepen: the width grip's horizontal twin, on the shelf's bottom
edge. Unmarked at rest; the depth's only affordance, persisted as
tagShelfRows. Full-bleed across the control strip so the seam is the
target, not a sliver of it. */
/* Drag-to-deepen: the width grip's horizontal twin, straddling the strip's own
border. Unmarked at rest; the depth's only affordance, persisted as
tagShelfRows. Full-bleed so the seam is the target, not a sliver of it.

OUT of the flow, which is the whole trick: in it, the handle can either hang
off the chips or sit on the border, and each attempt at one broke the other —
held to the chips it lights up in mid-air well above the divider, and welded
to the border it glues the last chip row to the edge. Pinned here it is the
divider, and the strip's bottom padding is free to be the air above it. */
.usb-shelf-grip {
height: 4px;
/* Pulled up over the column gap + the shelf's own margin so the handle sits
ON the chips' bottom edge — floating 10px below, it read as some other
divider's handle. */
margin: calc(-1 * (var(--space-2) + var(--space-1))) calc(-1 * var(--space-3)) 0;
position: absolute;
left: 0;
right: 0;
bottom: -3px; /* centres the 5px band on the 1px border */
height: 5px;
cursor: row-resize;
user-select: none;
touch-action: none;
Expand All @@ -156,11 +172,6 @@
.usb-shelf-grip.resizing {
background: color-mix(in srgb, var(--accent) 45%, transparent);
}
/* With no filter row after it, the grip IS the strip's boundary — it rides
down over the strip's bottom padding to sit on the border itself. */
.usb-shelf-grip:last-child {
margin-bottom: calc(-1 * var(--space-2));
}

/* The overflow keeps the chip shape — it stands in for tags, so it belongs in
their row — but carries no colour dot, because it is not one. */
Expand All @@ -177,6 +188,7 @@
selected tag whatever shape it took. It belongs on its own line, beside the
thing it undoes: how many tags are on. */
.usb-filtering {
flex: none;
display: flex;
align-items: baseline;
gap: var(--space-2);
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/src/utils/settingsDefaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export const FILE_TYPE_LIMITS = {
export const DEFAULT_MAX_SNIPPET_SIZE_KB = 512
// The sidebar tag shelf's depth. Two rows is the FLOOR as well as the default:
// diffs and snippets are what the sidebar is for, and an unbounded tag wall
// pushed them into a sliver. A tall screen may spend up to twelve.
// pushed them into a sliver. The ceiling can afford to be this generous because
// the strip's max-height (SavedDiffs.css) keeps the sections a slice of the
// column whatever is asked for — past it the chips scroll.
export const TAGS_PER_ROW = 4
export const MIN_TAG_ROWS = 2
export const MAX_TAG_ROWS = 12
export const MAX_TAG_ROWS = 24

export const MAX_SNIPPET_SIZE_KB_CAP = 8192

Expand Down
Loading