Compare commits

...
Author SHA1 Message Date
Kristian Wind f89fb21502 Merge pull request #42 from kristianwind/sidebar-drop-chevrons
CI / OpenAPI is valid (push) Successful in 12s
CI / End-to-end (push) Successful in 8m44s
CI / Docker image builds (push) Successful in 1m49s
Release / Build and publish image (push) Failing after 5s
CI / Go (push) Failing after 14m26s
Drop the fold arrows from sidebar headers and groups
2026-09-03 13:39:35 +02:00
kristianwindandClaude Opus 4.8 ec147cd1ba feat(sidebar): drop the fold arrows from headers and groups
The chevron beside every section and group was saying "clickable" a
second time next to an icon and a bold name that already said it. Gone
now: a system section folds from anywhere on its heading, and a group
folds from its folder icon while the name still opens the group's page.
No arrow on the rail.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 13:34:33 +02:00
Kristian Wind 1f9f14099b Merge pull request #41 from kristianwind/calendar-lighter-pastels
CI / OpenAPI is valid (push) Successful in 53s
CI / End-to-end (push) Successful in 9m3s
CI / Docker image builds (push) Successful in 2m10s
Release / Build and publish image (push) Failing after 7s
CI / Go (push) Failing after 15m11s
Calendar: lighter grid, per-calendar pastels, today marked at top
2026-09-03 13:17:31 +02:00
kristianwindandClaude Opus 4.8 6cf63113df feat(calendar): lighter grid, per-calendar pastels, today marked at top
The calendar was a grey grid where every event was the same sunken block
edged in a colour you had to already know. Now the day itself is the
brightest surface and each event is a soft, dusty wash of its own
calendar's colour — so a screen of them reads as colour laid on white,
and two calendars are told apart at a glance. Tasks stay outlined cards,
the thing you can pick up, against the solid washes you cannot.

Today is marked where the day is named: its number sits in a filled disc
at the top of the column, the same one the month grid gives it, over a
faint wash down the whole column.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 13:09:07 +02:00
Kristian Wind e24a0fa849 Merge pull request #40 from kristianwind/sidebar-header-unify
Unify section and group headers with icons; scrollbar flush to the divider
2026-09-03 13:02:15 +02:00
kristianwindandClaude Opus 4.8 1c223a2c26 fix(sidebar): scrollbar flush to the divider, and match the group test
The scrollbar thumb centred a 4px bar in a 10px track behind a
transparent border, so it floated three pixels shy of the right edge.
The thumb is the track now — a thin 6px rounded bar sitting flush
against the divider line, where a scrollbar belongs.

The group heading is a heading now, not a project row: the smoke test
measured a group against the projects filed under it and read the
section icon as a dot. It measures a group against "Projekter" — the
tier it shares — and reads the icon both headings carry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 12:54:05 +02:00
kristianwindandClaude Opus 4.8 a9c90ee6f1 feat(ui): unify section and group headers with icons
System sections (Projects, Filters, Labels) and user groups (Work,
Home) now share one design: a bold label at menu size, a leading
folder/funnel/tag/people icon, and an always-visible fold chevron.
Groups drop their colour dot for the same folder icon, so the sidebar
reads as one consistent hierarchy instead of two visual languages.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 12:34:07 +02:00
Kristian Wind dc4a584d5f Merge pull request #39 from kristianwind/sidebar-scrollbar-flush
fix(sidebar): scrollbar flush to the edge, thin and dim
2026-09-03 12:22:58 +02:00
kristianwindandClaude Opus 4.8 378980de0c fix(sidebar): the scrollbar sits at the edge, thin and dim
It sat a pad-width inside the right edge because the scroller lives inside the
sidebar's padding, so it read as floating in the middle. Pull the scroller out
to the border and pad the content back — the rows do not move — and make the
bar thin and dim so it is present without drawing the eye.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 12:14:39 +02:00
Kristian Wind 04c7a52a5a Merge pull request #38 from kristianwind/sidebar-color-polish
Release / Build and publish image (push) Failing after 5s
feat(ui): colour fixed-view icons, lift notes surfaces, pin the create header
2026-09-03 12:08:32 +02:00
35 changed files with 203 additions and 177 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+31 -39
View File
@@ -371,9 +371,9 @@ 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');
// The name is a link to the group's own page; the chevron beside it is a
// The name is a link to the group's own page; the folder icon beside it is a
// separate button that folds. Two intentions, two targets — the single one they
// used to share could only ever serve the smaller.
// used to share could only ever serve the smaller, and neither wears an arrow.
//
// `exact` on the link, because a group's name is a user-chosen word that can
// turn up inside another control's name, and Playwright matches an accessible
@@ -383,19 +383,20 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
await expect(heading).toBeVisible();
await expect(sidebar.getByText('Træk et projekt herop')).toBeVisible();
// A group's name must be readable in full, and must start where a project's
// name starts.
// A group's name must be readable in full, and must line up with the other
// headings — not with the projects filed under it.
//
// Both halves are here because both were once broken at the same time and for
// different reasons. "Omdøb" and "Slet" sat in the flow reserving their width
// even while invisible, so ARBEJDE was drawn as "ARBE"; and the fold chevron
// sat before the name, pushing a group 22px further in than the projects it
// contains — so a group read as if it were filed under them. Every assertion
// in this file passed through both, because both buttons still worked.
// The clipping half is here because it was once broken: "Omdøb" and "Slet" sat
// in the flow reserving their width even while invisible, so ARBEJDE was drawn
// as "ARBE". The alignment half changed meaning with the redesign: a group now
// carries a section icon just like "Projekter" above it, and belongs to that
// heading tier rather than to the project rows below.
const shape = await sidebar.locator('.folder-head').evaluate((el) => {
const link = el.querySelector('h2 a');
const name = { ...link.querySelector('.dot').getBoundingClientRect().toJSON() };
// The last child of the link is the name itself; the dot is a span before it.
// The leading mark is the section icon in its own fold button, the same
// folder/funnel/tag glyph the system headings carry — a group and a section
// read identically, and the icon sits before the name, not inside the link.
const name = { ...el.querySelector('.head-icon').getBoundingClientRect().toJSON() };
return {
clipped: link.scrollWidth > link.clientWidth + 1,
left: name.left,
@@ -407,17 +408,17 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
expect(shape.clipped, 'gruppens navn bliver klippet').toBe(false);
expect(shape.count, 'navnet og tallet overlapper').toBeGreaterThanOrEqual(shape.right);
// Measured dot to dot, not box to box: both rows begin with the same coloured
// mark, and it is the only thing in them that means the same on both sides.
// Comparing the group's inner link with the project's outer box reported the
// row's own padding as a misalignment.
const projectLeft = await sidebar
.getByRole('link', { name: 'Regnskab' })
.evaluate((el) => el.querySelector('.dot').getBoundingClientRect().left);
// A group is a heading, not a project row: it belongs to the same tier as
// "Projekter" and the other system sections — icon, bold name, no arrow — and
// starts where they start, not where the projects filed under it do. Measured
// icon to icon, the one mark both headings share.
const sectionLeft = await sidebar
.getByRole('heading', { name: 'Projekter' })
.evaluate((el) => el.querySelector('.head-icon').getBoundingClientRect().left);
expect(
Math.abs(shape.left - projectLeft),
`gruppen starter ${Math.round(shape.left - projectLeft)}px fra projekterne`
).toBeLessThanOrEqual(2);
Math.abs(shape.left - sectionLeft),
`gruppen starter ${Math.round(shape.left - sectionLeft)}px fra sektionsoverskrifterne`
).toBeLessThanOrEqual(4);
// Dragged in, then dragged back out onto "Projekter" — which is the reason
// that heading is a drop target at all: with every project filed away there
@@ -440,18 +441,13 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
'false'
);
// En foldet gruppe står med sin chevron fremme permanent, og den lå oven i
// prikken med et felt i --surface bag sig — en lys firkant ud for navnet på
// den sænkede bjælke, hele tiden. Nu tones prikken ud i stedet, så der er
// ingenting at male henover den med.
const chevron = await sidebar
.locator('.folder-head .chevron-button')
.evaluate((el) => ({
background: getComputedStyle(el).backgroundColor,
dot: getComputedStyle(el.closest('.folder-head').querySelector('.group-dot')).opacity
}));
expect(chevron.background, 'chevronen har en firkant bag sig').toBe('rgba(0, 0, 0, 0)');
expect(chevron.dot, 'prikken ligger stadig under chevronen').toBe('0');
// The fold control is the bare folder icon now — no chevron, and no --surface
// field behind it, so a folded group leaves no pale square beside its name on
// the sunken bar.
const foldBg = await sidebar
.locator('.folder-head .fold-icon')
.evaluate((el) => getComputedStyle(el).backgroundColor);
expect(foldBg, 'foldeknappen har en firkant bag sig').toBe('rgba(0, 0, 0, 0)');
await sidebar.getByRole('button', { name: /^Fold Arbejde/ }).click();
@@ -467,11 +463,7 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
// button on click.
await expect(sidebar.getByLabel('Gruppens navn')).toBeVisible();
await sidebar.getByLabel('Gruppens navn').press('Enter');
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)');
await expect(sidebar.getByRole('link', { name: 'Arbejde', exact: true })).toBeVisible();
// The heading itself is *not* indented — only what is filed under it. A heading
// that starts further in than the rows above it says the heading is inside
+49 -17
View File
@@ -651,6 +651,7 @@
<div
class="allday"
data-allday={iso(date)}
class:today={iso(date) === todayISO}
class:over={over === iso(date)}
ondragover={(e) => onDragOver(e, date)}
ondragleave={() => (over = null)}
@@ -876,12 +877,16 @@
overflow: hidden;
}
/* White, not the sidebar's sunken ground: a calendar reads lightest when the
day itself is the brightest thing on the screen and the events are the soft
colour laid on it — the opposite of the grey grid it was, where the events
had to shout to be seen at all. */
.corner,
.head,
.gutter,
.allday,
.daycol {
background: var(--ground);
background: var(--surface);
}
.head {
@@ -889,6 +894,16 @@
text-align: center;
}
/* Today's whole column, faintly, so the eye finds it before it reads a number —
the marker on the date above is the label, this is the glow it sits in. Kept
under the events, so a pastel block still reads as its own calendar's colour
rather than as this wash tinted. */
.head.today,
.allday.today,
.daycol.today {
background: color-mix(in oklab, var(--accent) 6%, var(--surface));
}
.head .wd {
display: block;
font-size: var(--text-xs);
@@ -901,9 +916,19 @@
font-size: var(--text-sm);
}
/* The marker at the top the day is named by: today's number in a filled disc,
the same one the month grid gives today, so "this is today" is one glance in
either view rather than a colour you have to already know to read. */
.head.today .num {
color: var(--accent);
color: var(--accent-ink);
background: var(--accent);
border-radius: var(--radius-full);
width: 20px;
height: 20px;
display: inline-grid;
place-items: center;
font-weight: 560;
margin-top: 1px;
}
.gutter {
@@ -977,6 +1002,10 @@
/* Placeringen kommer fra `place()` som en inline `style`; alt andet står her.
To ting på samme tid deler bredden, så `margin-right` er den ene hårstrå
luft, der gør to kasser til to kasser. */
/* A soft wash of the calendar's own colour rather than a grey block edged in it:
each calendar keeps a dusty, unmistakable nuance, and a screen of them reads
as colour laid on white instead of a grid of grey bars. The left edge stays,
a shade stronger than the fill, so the block still has a spine. */
.tevent {
position: absolute;
margin-right: 2px;
@@ -984,16 +1013,20 @@
padding: 1px var(--s1);
border-radius: var(--radius-sm);
border-left: 3px solid var(--event-colour, var(--line-strong));
background: var(--surface-sunken);
color: var(--ink-muted);
background: color-mix(in oklab, var(--event-colour, var(--line-strong)) 16%, var(--surface));
color: var(--ink);
font-size: var(--text-xs);
line-height: 1.3;
text-align: left;
text-decoration: none;
}
/* A task is a card you can pick up, not an event you cannot — so on the white
column it is an outlined card, where an event is a solid wash. The priority
colour rides the left edge, as it does everywhere else. */
.tevent.task {
background: var(--surface-raised);
background: var(--surface);
box-shadow: inset 0 0 0 1px var(--line);
border-left-color: var(--line-strong);
color: var(--ink);
cursor: grab;
@@ -1101,7 +1134,8 @@
text-align: left;
padding: 1px var(--s1);
border-radius: var(--radius-sm);
background: var(--surface-raised);
background: var(--surface);
box-shadow: inset 0 0 0 1px var(--line);
border-left: 2px solid var(--line-strong);
color: var(--ink);
overflow: hidden;
@@ -1120,7 +1154,7 @@
}
.chip:hover {
background: var(--surface);
background: var(--surface-sunken);
}
/* An event reads as a different kind of thing at a glance, before any of the
@@ -1140,8 +1174,8 @@
padding: 1px var(--s1);
border-radius: var(--radius-sm);
border-left: 3px solid var(--event-colour);
background: var(--surface-sunken);
color: var(--ink-muted);
background: color-mix(in oklab, var(--event-colour) 16%, var(--surface));
color: var(--ink);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
@@ -1156,7 +1190,7 @@
}
.event:hover {
background: var(--surface);
background: color-mix(in oklab, var(--event-colour) 26%, var(--surface));
color: var(--ink);
}
@@ -1165,7 +1199,7 @@
every calendar anybody has used. */
.event.allday {
border-left: none;
background: color-mix(in oklab, var(--event-colour) 22%, var(--ground));
background: color-mix(in oklab, var(--event-colour) 22%, var(--surface));
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--event-colour) 45%, transparent);
}
@@ -1267,20 +1301,18 @@
padding: 1px var(--s1);
border-radius: var(--radius-sm);
border-left: 2px solid var(--line-strong);
background: var(--surface-raised);
background: var(--surface);
box-shadow: inset 0 0 0 1px var(--line);
}
.calendar.week .chip[data-priority='1'] {
border-left-color: var(--p1);
background: var(--surface-raised);
}
.calendar.week .chip[data-priority='2'] {
border-left-color: var(--p2);
background: var(--surface-raised);
}
.calendar.week .chip[data-priority='3'] {
border-left-color: var(--p3);
background: var(--surface-raised);
}
/* Readable again in the week, for the reason the task chips are: a
@@ -1292,12 +1324,12 @@
padding: 1px var(--s1);
border-radius: var(--radius-sm);
border-left: 3px solid var(--event-colour);
background: var(--surface-sunken);
background: color-mix(in oklab, var(--event-colour) 16%, var(--surface));
}
.calendar.week .event.allday {
border-left: none;
background: color-mix(in oklab, var(--event-colour) 22%, var(--ground));
background: color-mix(in oklab, var(--event-colour) 22%, var(--surface));
}
.calendar.week .at {
+123 -121
View File
@@ -682,28 +682,84 @@
<!--
A fixed heading that folds, the same way a project group's does.
One snippet for all four rather than four copies of a chevron and an aria
One snippet for all four rather than four copies of an icon and an aria
attribute: they have to behave identically, and the group heading is already
the proof that they drift otherwise — it grew a fold, a colour and two
actions while these stayed plain text.
The whole heading is the target, not the chevron. At this size a lone chevron
is something you miss, and the name is where the eye already is.
The whole heading is the target, and the icon is the mark: no arrow beside it.
At this size an extra glyph is clutter, and the name is where the eye already
is.
-->
<!-- One monochrome mark per kind of heading, so a section and a group read as the
same kind of thing — a folder of projects, a funnel of filters, a tag of
labels — instead of one being a faint label and the other a coloured dot. -->
{#snippet sectionIcon(key)}
{#if key === 'filters'}
<svg class="head-icon" viewBox="0 0 16 16" aria-hidden="true">
<path
d="M2.2 3.4h11.6L9.3 8.8v3.4L6.7 10.9V8.8z"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linejoin="round"
/>
</svg>
{:else if key === 'labels'}
<svg class="head-icon" viewBox="0 0 16 16" aria-hidden="true">
<path
d="M7.7 2.3H13v5.3l-5.3 5.3-5.3-5.3z"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linejoin="round"
/>
<circle cx="10.2" cy="4.9" r="0.95" fill="currentColor" />
</svg>
{:else if key === 'shared'}
<svg class="head-icon" viewBox="0 0 16 16" aria-hidden="true">
<circle cx="6.2" cy="5.6" r="2.1" fill="none" stroke="currentColor" stroke-width="1.4" />
<path
d="M2.6 12.8c0-2.1 1.7-3.3 3.6-3.3s3.6 1.2 3.6 3.3"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
/>
<path
d="M11 9.6c1.5.2 2.6 1.3 2.6 3.2"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
/>
<circle cx="11.2" cy="5.6" r="1.7" fill="none" stroke="currentColor" stroke-width="1.4" />
</svg>
{:else}
<!-- Projekter and every group: a folder of projects. -->
<svg class="head-icon" viewBox="0 0 16 16" aria-hidden="true">
<path
d="M2 4.6a1 1 0 011-1h3.1l1.4 1.5H13a1 1 0 011 1v6.3a1 1 0 01-1 1H3a1 1 0 01-1-1z"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linejoin="round"
/>
</svg>
{/if}
{/snippet}
{#snippet foldHeading(key, label)}
<!-- No chevron: a system section has no page and nothing to rename, so the
whole heading is one button that folds it. The icon and the bold name are
the whole control — an arrow beside them was only ever saying "clickable"
twice. -->
<h2 class="fold">
<button
onclick={() => app.toggleSection(key)}
aria-expanded={!app.sectionCollapsed(key)}
>
<svg
class="chevron"
class:collapsed={app.sectionCollapsed(key)}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="M6 9l6 6 6-6" />
</svg>
{@render sectionIcon(key)}
{label}
</button>
</h2>
@@ -871,31 +927,22 @@
</div>
</form>
{:else}
<!-- The button sits inside the heading, not around it, the same way a
project's title does: a group *is* a heading in the sidebar, and
wrapping an h2 in a button both invalidates the markup and takes
the heading away from a screen reader. The whole heading folds —
at this size a lone chevron is a target you miss, and the name is
where the eye already is. -->
<!-- The chevron folds; the name opens the group's own page. Two
targets in one heading, because they are two different intentions
and the old single one could only ever serve the smaller. -->
<!-- No chevron: the folder icon itself folds the group, and the name
beside it opens the group's own page. Two intentions, two targets,
but no arrow to clutter the rail — the icon a section carries is
the same one that folds it here. The button sits inside the
heading, not around it: a group *is* a heading, and wrapping an h2
in a button both invalidates the markup and takes the heading from
a screen reader. -->
<button
class="chevron-button"
class="fold-icon"
onclick={() => app.toggleGroup(group.id)}
aria-expanded={!group.collapsed}
aria-label={group.collapsed
? t('sidebar.unfold', { name: group.name })
: t('sidebar.fold', { name: group.name })}
>
<svg
class="chevron"
class:collapsed={group.collapsed}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="M6 9l6 6 6-6" />
</svg>
{@render sectionIcon('group')}
</button>
<h2 class="fold">
<!-- Navigation waits two tenths of a second for a second click. A
@@ -916,11 +963,6 @@
}, 200);
}}
>
<span
class="dot group-dot"
style="background: {colorVar(group.color)}"
aria-hidden="true"
></span>
{group.name}
</a>
</h2>
@@ -1075,6 +1117,32 @@
display: flex;
flex-direction: column;
gap: var(--s5);
/* The sidebar is padded, so the scrollbar sat a pad-width inside the edge and
read as floating in the middle. Pull the scroller out to the border and pad
the content back, so the bar sits against the edge where a scrollbar
belongs — the rows do not move. Thin and dim, so it is present without
drawing the eye. */
margin-right: calc(var(--s3) * -1);
padding-right: var(--s3);
scrollbar-width: thin;
scrollbar-color: var(--line-strong) transparent;
}
.scroller::-webkit-scrollbar {
width: 6px;
}
/* No transparent border and no padding-box clip: those centred a 4px bar in a
10px track, so the thumb floated three pixels shy of the divider. The bar is
the track now — a thin rounded thumb sitting flush against the line on the
right, which is where a scrollbar belongs. */
.scroller::-webkit-scrollbar-thumb {
background: var(--line-strong);
border-radius: 999px;
}
.scroller::-webkit-scrollbar-track {
background: transparent;
}
/* Wider than it looks: a 1px target is a target you miss. The visible line stays
@@ -1243,12 +1311,22 @@
background: var(--surface-sunken);
}
/* Every heading — the system sections and the user's groups — reads the same:
a mark, then a bold plainly-cased name. What used to set them apart (one a
faint uppercase label, the other a bold name with a coloured dot) is gone. */
h2 {
font-size: var(--text-xs);
font-weight: 560;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-faint);
font-size: var(--menu-size);
font-weight: 600;
color: var(--ink);
}
/* The heading's mark, in the muted ink so the bold name still leads. Sized and
nudged like a row's own mark, so the icons line up down the sidebar. */
.head-icon {
width: 14px;
height: 14px;
flex: none;
color: var(--ink-muted);
}
/* Only the square icon buttons — "+" and the folder — and asked for by class
@@ -1334,57 +1412,17 @@
min-width: 0;
}
/* The chevron on its own, so the name beside it can be a link to the group's
page. Sized as a target rather than as a glyph: 11px of arrow is something
you miss. */
/* The chevron sits on top of the group's dot rather than before it. In the flow
it pushed the name 22px further in than a project's, so groups read as if
they were filed under the projects above them — the opposite of what they
are. Out of the flow, a group's name starts exactly where a project's does.
The dot is the resting state; the chevron takes over on hover or focus, and
stays put while the group is folded, so a folded group still shows the way
back open. */
/* Uden baggrund. Knappen dækkede prikken med et felt i --surface, og på den
sænkede bjælke er det en lys firkant: en foldet gruppe stod med en hvid
klods ud for navnet permanent, fordi chevronen bliver stående, når gruppen
er lukket. Prikken tones ud i stedet for at blive dækket — det er samme
skifte, uden noget at male henover den med. */
.chevron-button {
position: absolute;
left: calc(var(--s2) - 5px);
width: 18px;
height: 18px;
/* The folder icon is the fold toggle now — no chevron beside it. A bare icon
button the size of the icon it holds: the name next to it opens the group's
page, this folds it, and neither wears an arrow to say so. */
.fold-icon {
flex: none;
display: grid;
place-items: center;
border-radius: var(--radius-sm);
color: inherit;
opacity: 0;
/* Fordi prikken herunder tones ud frem for at blive dækket. En `opacity`
under 1 laver en stakkontekst og maler elementet, som var det placeret
med `z-index: 0` — så prikken, der står senere i DOM'en, lagde sig oven
på knappen i det øjeblik den forsvandt. Den var usynlig og tog stadig
klikket: prøven brugte tredive sekunder på at folde en gruppe, der ikke
ville foldes. Ét lag op, og rækkefølgen er den samme som før. */
z-index: 1;
}
.folder-head:hover .chevron-button,
.chevron-button:focus-visible,
.chevron-button[aria-expanded='false'] {
opacity: 1;
}
/* Præcis de tre tilfælde ovenfor, læst nedad i stedet for opad: står chevronen
der, er prikken væk. `opacity` og ikke `display`, for de to ligger oven i
hinanden — forsvandt prikken ud af flowet, ville navnet rykke sig hver gang
pilen kom frem. */
.folder-head:hover .group-dot,
.folder-head:has(.chevron-button:focus-visible) .group-dot,
.folder-head:has(.chevron-button[aria-expanded='false']) .group-dot {
opacity: 0;
}
.chevron-button:hover {
.fold-icon:hover {
color: var(--ink);
}
@@ -1413,22 +1451,6 @@
text-decoration: none;
}
.chevron {
width: 11px;
height: 11px;
flex: none;
fill: none;
stroke: currentColor;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform var(--fast) var(--ease);
}
.chevron.collapsed {
transform: rotate(-90deg);
}
/* Hidden until the heading is hovered, like the section actions on a project
page: two buttons permanently beside a label stop it reading as a label. */
.folder-head .count {
@@ -1461,14 +1483,6 @@
white-space: nowrap;
}
/* The group's own mark, between the chevron and the name — inside the fold
button, so it cannot push the heading in. It is a mark now and not a
control: the colour is chosen while renaming, which is the other edit to
the same thing. */
.group-dot {
flex: none;
}
/* Projects sit at the same left margin whether or not they are in an area —
one straight edge down the whole sidebar reads calmer than a staircase, and
the bold area heading above already does the grouping the indent used to. */
@@ -1484,18 +1498,6 @@
padding-left: var(--s2);
}
/* An area is the sidebar's own heading — Things treats it as bold and plainly
cased, a thing the eye lands on, not another faint uppercase label like the
system sections. So an area's name is set apart while "Projekter", "Filtre"
and "Etiketter" stay quiet. */
.folder-head h2 {
font-size: var(--menu-size);
font-weight: 600;
text-transform: none;
letter-spacing: 0;
color: var(--ink);
}
/* Inside the rename form now, so it needs no padding of its own — the form
already sits where the heading did. */
.renaming {