Give a short meeting enough room to say what it is
A quarter of an hour is 1/96th of the day column — about twelve pixels at the height the grid usually gets — and the chip inside needs twenty-four for one line of "9:00 Standup". So a fifteen-minute meeting drew a sliver with its own label hanging out of it. It reached the README screenshot, which is where it was noticed. The floor is 1.75rem, which is the all-day lane's floor directly above it: one number for "the smallest a chip may be drawn" rather than a second one that agrees with it today. A floored block is then taller than its duration and reaches over the one after it. That is the trade, taken deliberately: the block's TOP is where the meeting starts and stays honest, only the padded bottom is a fiction, and the alternative — reflowing so short events push their neighbours down — moves a 10:00 meeting off the 10:00 line, which is the one thing a time grid exists to get right. Blocks are emitted in start order, so the later one paints over the fiction rather than under it. Presentational only, and worth saying because it looks like it should not be: the drag reads data-starts-at and data-ends-at, never the rendered box, so a floored block still resizes from its real duration. The resize preview writes a pixel height computed from minutes, which the floor clamps — the value posted is unaffected. Also corrects this file's own header. It claimed the screenshot suite cannot run against the E2E stack because nothing seeds the demo mailbox. app:test:seed-demo does, so it runs — and leaves a you@example.com account behind, which makes account-scope.spec.ts fail on the FIRST account in the sidebar and read as a regression in account scoping. Found by doing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
@@ -178,9 +178,37 @@ away and is a single full-width column. Worth revisiting; not worth guessing at.
|
||||
{% set event = occurrence.event %}
|
||||
{% set readOnly = event.calendar.isReadOnly %}
|
||||
|
||||
{# min-height is the floor a short meeting needs to
|
||||
be readable. A quarter of an hour is 1/96th of
|
||||
the column — about twelve pixels at the height
|
||||
this grid usually gets — and the chip inside
|
||||
wants twenty-four for one line of "9:00 Standup",
|
||||
so a fifteen-minute event drew a sliver with its
|
||||
own label spilling out of it. 1.75rem is the
|
||||
all-day lane's floor above, deliberately: one
|
||||
number for "the smallest a chip may be drawn".
|
||||
|
||||
The floored block is then taller than its
|
||||
duration and can reach over the one after it.
|
||||
That is the right trade and not a compromise: the
|
||||
block's TOP is where the meeting starts and stays
|
||||
honest, only the padded bottom is a fiction, and
|
||||
the alternative — reflowing so short events push
|
||||
their neighbours down — would move a 10:00
|
||||
meeting off the 10:00 line, which is the one
|
||||
thing a time grid exists to get right. The
|
||||
overlap is drawn under the later block, because
|
||||
they are emitted in start order and the fiction
|
||||
is what gets covered.
|
||||
|
||||
Presentational only. The drag reads
|
||||
data-starts-at and data-ends-at, never the
|
||||
rendered box, so a floored block resizes from its
|
||||
real duration. #}
|
||||
<div class="absolute p-px"
|
||||
style="top: {{ (placed.top * 100)|round(4) }}%;
|
||||
height: {{ (placed.height * 100)|round(4) }}%;
|
||||
min-height: 1.75rem;
|
||||
left: {{ (placed.lane * 100 / placed.lanes)|round(4) }}%;
|
||||
width: {{ (100 / placed.lanes)|round(4) }}%;"
|
||||
data-calendar--time-grid-target="block"
|
||||
|
||||
@@ -31,6 +31,9 @@ import { seed } from "./support/config";
|
||||
|
||||
const TIMED = "E2E grid timed";
|
||||
const ALL_DAY = "E2E grid all day";
|
||||
// Not from the seeder: this one is made through the dialog, so it is cleared by
|
||||
// the editor rather than by `seed-grid-events --clear`.
|
||||
const SHORT = "E2E grid quarter hour";
|
||||
const DAILY = "E2E grid daily";
|
||||
|
||||
/** The positioned blocks holding one title. */
|
||||
@@ -142,6 +145,59 @@ test.describe("calendar time-grid", () => {
|
||||
await expect(blocks(page, ALL_DAY)).toHaveCount(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* A quarter of an hour is 1/96th of the column — about twelve pixels at the
|
||||
* height this grid gets — and the chip inside needs twenty-four for one line
|
||||
* of "9:00 Standup". Without a floor the block was a sliver with its own
|
||||
* label hanging out of it, which is how it reached the README screenshot.
|
||||
*
|
||||
* Created through the dialog rather than seeded, for the reason the
|
||||
* screenshot suite gives: an event is a JSCalendar object whose occurrences
|
||||
* are materialised on write, and putting rows in around the writer is how a
|
||||
* test comes to assert a state the app cannot produce.
|
||||
*/
|
||||
test("a fifteen-minute meeting is still tall enough to read", async ({ page }) => {
|
||||
const day = new Date().toISOString().slice(0, 10);
|
||||
|
||||
await page.goto("/calendar/day");
|
||||
|
||||
await page.getByRole("button", { name: "New event", exact: true }).click();
|
||||
|
||||
const modal = page.locator("#modal-backdrop");
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
await modal.locator("#event-title").fill(SHORT);
|
||||
await modal.locator("#event-starts").fill(`${day}T09:00`);
|
||||
await modal.locator("#event-ends").fill(`${day}T09:15`);
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
const block = blocks(page, SHORT).first();
|
||||
await expect(block).toBeVisible();
|
||||
|
||||
const box = await block.boundingBox();
|
||||
|
||||
// 28px is the 1.75rem floor the template sets, which is also the all-day
|
||||
// lane's — one number for "the smallest a chip may be drawn".
|
||||
expect(box?.height ?? 0).toBeGreaterThanOrEqual(28);
|
||||
|
||||
// And the label is inside it rather than spilling: the title's own box
|
||||
// has to fit within the block it belongs to.
|
||||
const label = await block.getByText(SHORT).boundingBox();
|
||||
|
||||
expect(label).not.toBeNull();
|
||||
expect((label?.y ?? 0) + (label?.height ?? 0)).toBeLessThanOrEqual(
|
||||
(box?.y ?? 0) + (box?.height ?? 0) + 1,
|
||||
);
|
||||
|
||||
// Taken away again. `seed-grid-events --clear` removes the three titles
|
||||
// the seeder owns and this is not one of them, so without this the
|
||||
// fixture user collects a meeting per run for ever.
|
||||
await block.click();
|
||||
await expect(modal).toBeVisible();
|
||||
await modal.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(blocks(page, SHORT)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dragging a block down the column moves the event later", async ({ page }) => {
|
||||
await page.goto("/calendar/day");
|
||||
|
||||
|
||||
@@ -6,14 +6,21 @@ import { TEST_ADMIN, consoleCommand, login, seed, seedUser } from "./support/con
|
||||
*
|
||||
* Not part of the regression suite — it asserts nothing about behaviour, it
|
||||
* just drives the UI to the states worth showing and writes PNGs into
|
||||
* docs/screenshots/. It also cannot run on the E2E fixtures: it looks for the
|
||||
* demo mailbox by subject and sender, and no seed command in this repo
|
||||
* produces that data, so on the test stack it can only fail and overwrite the
|
||||
* committed PNGs with pictures of an empty inbox.
|
||||
*
|
||||
* Hence the opt-in. Point the suite at an app holding the demo mailbox and:
|
||||
* docs/screenshots/. Hence the opt-in:
|
||||
*
|
||||
* npm run test:e2e:screenshots
|
||||
*
|
||||
* **It will run against the E2E stack, and it poisons it.** This used to say
|
||||
* the suite could not run there at all, which is not true — app:test:seed-demo
|
||||
* makes exactly the mailbox it wants — and the truth is worse, because it works
|
||||
* and then leaves an account called you@example.com on the shared fixture user.
|
||||
* account-scope.spec.ts asserts on the FIRST account in the sidebar, so it
|
||||
* fails afterwards with "expected E2E Mailbox, received you@example.com", which
|
||||
* looks like a regression in account scoping and is not.
|
||||
*
|
||||
* Point it at an app of its own, or clear the demo account afterwards:
|
||||
*
|
||||
* php bin/console dbal:run-sql "DELETE FROM account WHERE email = 'you@example.com'"
|
||||
*/
|
||||
const OUT = "docs/screenshots";
|
||||
|
||||
|
||||