feat(sidebar): colours you can choose, and projects indented under their group
`projects.color` has been in the schema since the first migration, has defaulted to 'graphite' on every row ever written, and nothing has ever read it. There was no palette, no name-to-colour mapping, and the dot in the sidebar was hardcoded to a border grey. So this is not "add a picker"; it is implementing colour for the first time. Names, not hex values. A hex in ten thousand rows is a decision nobody can revisit; a name is resolved when the interface paints and can be retuned for a new theme or a new display. It is also the difference between a database that stores a fact and one that stores a rendering. The set is closed and refused at the edge — an unknown name would be stored happily and painted as the default, which looks like a colour that did not save. One palette across all five themes, and this is the one place in app.css that is not redefined per theme. Everything else there is text, a ground or a border and has to be measured against what sits behind it. These are small solid marks beside a label that already carries the meaning: what they owe is to be told apart, and to be visible on both a near-black and a white ground, which is why all ten sit in the mid-tone band. It also keeps them out of the trap the themes are in — a colour added to four blocks and forgotten in the fifth looks finished and falls back silently. There is no fifth block to forget. The list is written down three times: Go for validation, colors.js for the picker, app.css for the tokens that paint. So a test reads the other two — the same small-scanner approach openapi_test.go takes to the spec — and fails if the server accepts a colour nothing can draw, or the picker offers one the server refuses. Groups get a colour of their own (migration 0006) and projects inside a group are indented. The indent is to the group's *name*, not to its dot: the eye follows the text, and lining the projects up under the dot leaves them looking a step short of where the heading starts. The drop line is indented with them, so a drop inside a group reads as landing in the group. The group's dot is the control as well as the preview — you press the thing you are about to change, as on the project page. That was not the first design: a third text action, "Farve", sat beside Omdøb and Slet, and those carry `opacity: 0` rather than `display: none`, so they take their width whether or not you can see them. A group called "Arbejde" rendered as "ARBEJD". Moving the control onto the dot gave the name its room back. The smoke test measures the indent at the content edge rather than the element's, because the indent is padding and the box does not move — which is how the first version of that assertion passed 12 against 12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e8ebd0fa78
commit
550f420c59
+35
-5
@@ -256,7 +256,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
||||
await sidebar.getByLabel('Gruppens navn').fill('Arbejde');
|
||||
await sidebar.getByLabel('Gruppens navn').press('Enter');
|
||||
|
||||
const heading = sidebar.getByRole('button', { name: 'Arbejde' });
|
||||
// `exact`, because the heading's colour dot is a button too and its label is
|
||||
// "Farve på Arbejde" — and Playwright matches an accessible name by substring
|
||||
// unless told otherwise.
|
||||
const heading = sidebar.getByRole('button', { name: 'Arbejde', exact: true });
|
||||
await expect(heading).toBeVisible();
|
||||
await expect(sidebar.getByText('Træk et projekt herop')).toBeVisible();
|
||||
|
||||
@@ -295,11 +298,38 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
||||
await heading.click();
|
||||
await expect(heading).toHaveAttribute('aria-expanded', 'false');
|
||||
await page.reload();
|
||||
await expect(sidebar.getByRole('button', { name: 'Arbejde' })).toHaveAttribute(
|
||||
await expect(sidebar.getByRole('button', { name: 'Arbejde', exact: true })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false'
|
||||
);
|
||||
await sidebar.getByRole('button', { name: 'Arbejde' }).click();
|
||||
await sidebar.getByRole('button', { name: 'Arbejde', exact: true }).click();
|
||||
|
||||
// Colour, and indentation. The dot is the control as well as the preview, so
|
||||
// the thing you press is the thing you are about to change.
|
||||
await sidebar.getByRole('button', { name: 'Farve på Arbejde' }).click();
|
||||
await sidebar.getByRole('button', { name: 'Petrol' }).click();
|
||||
|
||||
const painted = await sidebar.locator('.folder-head .group-dot').evaluate((el) =>
|
||||
getComputedStyle(el).backgroundColor
|
||||
);
|
||||
expect(painted, 'gruppens prik fik ikke sin farve').not.toBe('rgba(0, 0, 0, 0)');
|
||||
|
||||
// A project inside a group starts further in than a row outside one. Measured
|
||||
// rather than asserted on a class, because indentation is a fact about where
|
||||
// the row's contents land — and measured at the *content* edge, not the box:
|
||||
// the indent is padding, so the element's own left edge does not move.
|
||||
await sidebar.getByRole('link', { name: 'Regnskab' }).dragTo(heading);
|
||||
const contentLeft = (el) =>
|
||||
el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft);
|
||||
const [inside, outside] = await Promise.all([
|
||||
sidebar.locator('.folder > a').first().evaluate(contentLeft),
|
||||
sidebar.locator('.views a').first().evaluate(contentLeft)
|
||||
]);
|
||||
expect(inside, 'et projekt i en gruppe er ikke rykket ind').toBeGreaterThan(outside);
|
||||
|
||||
await sidebar
|
||||
.getByRole('link', { name: 'Regnskab' })
|
||||
.dragTo(sidebar.getByRole('heading', { name: 'Projekter' }));
|
||||
|
||||
// Renaming writes one row rather than one per project, which is the reason a
|
||||
// group is a table and not a string repeated on every project.
|
||||
@@ -307,14 +337,14 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
||||
const name = sidebar.getByLabel('Gruppens navn');
|
||||
await name.fill('Kontoret');
|
||||
await name.press('Enter');
|
||||
await expect(sidebar.getByRole('button', { name: 'Kontoret' })).toBeVisible();
|
||||
await expect(sidebar.getByRole('button', { name: 'Kontoret', exact: true })).toBeVisible();
|
||||
|
||||
// Deleting the heading must not take the project filed under it. It is
|
||||
// ungrouped here, which is the case that would look identical either way if
|
||||
// the assertion were only "the group is gone".
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
await sidebar.getByRole('button', { name: 'Slet' }).click();
|
||||
await expect(sidebar.getByRole('button', { name: 'Kontoret' })).toBeHidden();
|
||||
await expect(sidebar.getByRole('button', { name: 'Kontoret', exact: true })).toBeHidden();
|
||||
await expect(sidebar.getByRole('link', { name: 'Regnskab' })).toBeVisible();
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user