Compare commits

..
Author SHA1 Message Date
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
kristianwindandClaude Opus 4.8 52df528994 test(e2e): retry the project→group drop until it nests
A native HTML5 drag occasionally does not register under CI load, leaving
the group empty and the group-page assertion failing for a reason unrelated
to the test. Retry the drop until the project actually sits under the group.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 12:00:02 +02:00
kristianwindandClaude Opus 4.8 2f00246d6f feat(ui): colour the fixed-view icons, lift the notes surfaces, fix the create header
Following the Things pass, four bits of polish:

- The fixed views carry their own colour — amber star for Today, blue tray
  for the Inbox, red Upcoming, violet Waiting on others, teal Notes, indigo
  Calendar — and keep it whether or not they are the active row.
- The active row was all but invisible on the dark sidebar (a half-step of
  surface); it is now an accent-tinted selection that reads in both themes.
- The Projekter heading — the only place to add a project or a group — no
  longer scrolls away with a long list; it is pinned to the top of the
  scroller.
- The note list and the editor lift off the page: white in light, a nuance
  lighter than the ground in dark, so the two read as one writing surface.

Screenshots regenerated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 11:47:17 +02:00
Kristian Wind acec77a20c Merge pull request #37 from kristianwind/sidebar-things-nav
Release / Build and publish image (push) Failing after 6s
feat(sidebar): icons for fixed views, bold areas, one straight edge
2026-09-03 10:17:56 +02:00
kristianwindandClaude Opus 4.8 59413a8b64 test(e2e): a grouped project now shares the left margin, not indented
The sidebar redesign puts every project at the same left edge; flip the
assertion from "indented more than a fixed view" to "in line with it".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 10:09:32 +02:00
kristianwindandClaude Opus 4.8 cf69cac410 feat(sidebar): icons for the fixed views, bold areas, one straight edge
Toward Things' calm: the fixed views carry icons instead of dots — a star
for Today, a calendar for Upcoming, a person for Waiting on others, a tray
for the Inbox. Areas (the user's groups) are set in bold, plainly cased, so
the eye lands on them the way it lands on an area in Things; the system
sections (Projekter, Filtre, Etiketter) stay quiet. And projects sit at the
same left margin whether or not they are in an area — one straight edge down
the sidebar rather than a staircase, with the bold heading doing the
grouping the indent used to.

Screenshots regenerated to match.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-03 10:02:53 +02:00
Kristian Wind b0998fe524 Merge pull request #36 from kristianwind/settings-font-sizes
Release / Build and publish image (push) Failing after 11s
feat(settings): menu and body-text font size options
2026-09-02 15:39:06 +02:00
35 changed files with 154 additions and 22 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 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: 38 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+20 -8
View File
@@ -485,10 +485,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
.evaluate((el) => el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft));
expect(headLeft, 'gruppehovedet er rykket ind').toBeLessThanOrEqual(looseLeft);
// 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.
// A project sits at the same left margin whether or not it is in a group — one
// straight edge down the sidebar, with the bold heading doing the grouping the
// indent used to. Measured at the *content* edge, not the box, because what the
// eye follows is where the text lands.
await sidebar.getByRole('link', { name: 'Regnskab' }).dragTo(heading);
const contentLeft = (el) =>
el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft);
@@ -496,7 +496,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
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);
expect(
Math.abs(inside - outside),
'et projekt i en gruppe står ikke i flugt med de faste visninger'
).toBeLessThan(2);
await sidebar
.getByRole('link', { name: 'Regnskab' })
@@ -1522,9 +1525,18 @@ test('en gruppe er en side med sine projekter, en beskrivelse og filer', async (
// Into the group, then onto its page — which is what the heading is a link to
// now. The chevron beside it still folds; they are two intentions and the old
// single target could only serve the smaller one.
await sidebar
.getByRole('link', { name: 'Drejebænk' })
.dragTo(sidebar.getByRole('link', { name: 'Værksted' }));
//
// The drop is retried until the project actually nests under the group: a native
// HTML5 drag occasionally does not register the first time under CI load, and a
// single dragTo then leaves the group empty and the page assertion failing for a
// reason that has nothing to do with what the test is about.
const nested = sidebar.locator('.folder > a').filter({ hasText: 'Drejebænk' });
await expect(async () => {
await sidebar
.getByRole('link', { name: 'Drejebænk' })
.dragTo(sidebar.getByRole('link', { name: 'Værksted' }));
await expect(nested).toHaveCount(1, { timeout: 1000 });
}).toPass({ timeout: 10000 });
await sidebar.getByRole('link', { name: 'Værksted' }).click();
await expect(page.getByRole('heading', { name: 'Værksted', level: 1 })).toBeVisible();
+126 -13
View File
@@ -487,6 +487,7 @@
it, which is the second time that drop has earned its test. -->
<a
href={item.href}
data-view={item.key}
class:active={current === item.href}
class:receiving={item.key === 'today' && overToday}
onclick={onnavigate}
@@ -529,6 +530,52 @@
stroke-linecap="round"
/>
</svg>
{:else if item.key === 'today'}
<!-- A star, the way Things marks Today: the one view you open first. -->
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
<path
d="M8 2l1.7 3.6 3.9.5-2.9 2.7.7 3.9L8 10.9 4.6 13.4l.7-3.9L2.4 6.1l3.9-.5z"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linejoin="round"
/>
</svg>
{:else if item.key === 'upcoming'}
<!-- A calendar with a day marked — the Calendar view carries the plain
grid, so the marked day keeps the two apart. -->
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
<rect
x="2.2"
y="3.2"
width="11.6"
height="10.6"
rx="1.4"
fill="none"
stroke="currentColor"
stroke-width="1.4"
/>
<path
d="M2.2 6.4h11.6M5.6 2.2v2M10.4 2.2v2"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
/>
<rect x="8.8" y="8.6" width="2.6" height="2.6" rx="0.5" fill="currentColor" />
</svg>
{:else if item.key === 'delegated'}
<!-- A person: the tasks you are waiting on someone else for. -->
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
<circle cx="8" cy="5.4" r="2.5" fill="none" stroke="currentColor" stroke-width="1.4" />
<path
d="M3.4 13.4c0-2.6 2.1-4.2 4.6-4.2s4.6 1.6 4.6 4.2"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
/>
</svg>
{:else}
<span class="dot" class:today={item.key === 'today'} aria-hidden="true"></span>
{/if}
@@ -570,6 +617,7 @@
{#snippet projectRow(project, sortable)}
<a
href="/projekt/{project.id}"
data-view={project.is_inbox ? 'inbox' : undefined}
class:sortable
class:active={current === `/projekt/${project.id}`}
class:dragging={draggingId === project.id}
@@ -593,7 +641,29 @@
? onTaskDrop(e, project)
: sortable && onDrop(e, project)}
>
<span class="dot" style="background: {colorVar(project.color)}" aria-hidden="true"></span>
{#if project.is_inbox}
<!-- A tray, not a coloured dot: the Inbox is a fixed view that happens to
be a project, and it reads as one of the views above, not as the first
of the projects below. -->
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
<path
d="M2.3 9.2L4 3.4a1.1 1.1 0 011-.8h6a1.1 1.1 0 011 .8l1.7 5.8"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linejoin="round"
/>
<path
d="M2.3 9.2v2.9A1.3 1.3 0 003.6 13.4h8.8a1.3 1.3 0 001.3-1.3V9.2h-3.3l-1 1.7H6.6l-1-1.7z"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linejoin="round"
/>
</svg>
{:else}
<span class="dot" style="background: {colorVar(project.color)}" aria-hidden="true"></span>
{/if}
{projectName(project)}
<!-- Open tasks, not people. The same grey number meant three things in this
one column — people at a project, projects at a group, tasks at a label
@@ -656,7 +726,7 @@
and with every project filed there would be no loose row left to aim at. -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="group-head"
class="group-head sticky-head"
class:over={overGroup === ''}
ondragover={(e) => {
if (!carries(e, PROJECT)) return;
@@ -1133,8 +1203,26 @@
flex: none;
}
a.active .mark {
color: var(--accent);
/* The fixed views carry a colour of their own, the way Things does — the star
amber, the Inbox blue — and keep it whether or not they are the active row.
So no active-state override here; the colour is the icon's identity. */
[data-view='inbox'] .mark {
color: var(--color-blue);
}
[data-view='today'] .mark {
color: var(--color-amber);
}
[data-view='upcoming'] .mark {
color: var(--color-tomato);
}
[data-view='delegated'] .mark {
color: var(--color-violet);
}
[data-view='notes'] .mark {
color: var(--color-teal);
}
[data-view='calendar'] .mark {
color: var(--color-indigo);
}
.group-head {
@@ -1144,6 +1232,17 @@
padding: 0 var(--s2) var(--s2);
}
/* The Projekter heading holds the only way to add a project or a group, so it
must not scroll away with a long list. Pinned to the top of the scroller, on
the sidebar's own ground so the rows pass under it. */
.sticky-head {
position: sticky;
top: 0;
z-index: 3;
padding-top: var(--s2);
background: var(--surface-sunken);
}
h2 {
font-size: var(--text-xs);
font-weight: 560;
@@ -1370,21 +1469,31 @@
flex: none;
}
/* Projects inside a group are indented to the group's name, not to its dot:
the eye follows the text, and lining up under the dot leaves the names
looking a step short of where the heading starts. */
/* 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. */
.folder > a {
padding-left: calc(var(--s2) + var(--s4));
padding-left: var(--s2);
}
/* The line is indented with them, so a drop between two projects in a group
reads as landing in the group rather than across the whole sidebar. */
.folder > a.sortable::before {
left: calc(var(--s2) + var(--s4));
left: var(--s2);
}
.folder > .empty {
padding-left: calc(var(--s2) + var(--s4));
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
@@ -1455,8 +1564,12 @@
color: var(--ink);
}
/* An accent-tinted selection, not a half-step of surface: on the sunken dark
sidebar --surface-raised was all but invisible, so the active row read as no
different from the rest. --accent-sunken carries the accent hue and stands out
in both themes. */
a.active {
background: var(--surface-raised);
background: var(--accent-sunken);
color: var(--ink);
font-weight: 600;
}
+8 -1
View File
@@ -1081,6 +1081,11 @@
min-width: 0;
border-right: 1px solid var(--line);
padding-right: var(--s3);
/* White in light, a nuance lighter than the ground in dark — the note list
is a surface you read down, so it lifts off the page like the editor does.
--surface-raised is #fff in the light themes and one step up in the dark. */
background: var(--surface-raised);
padding-left: var(--s3);
}
.head {
@@ -1546,7 +1551,9 @@
flex-direction: column;
min-height: 0;
min-width: 0;
background: var(--surface);
/* A nuance lighter than the ground in dark, still white in light — matches
the note list beside it so the two read as one writing surface. */
background: var(--surface-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
/* Luft nok til at teksten ikke rører kanten.