Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c223a2c26 | ||
|
|
a9c90ee6f1 | ||
|
|
dc4a584d5f | ||
|
|
378980de0c | ||
|
|
04c7a52a5a | ||
|
|
52df528994 | ||
|
|
2f00246d6f | ||
|
|
acec77a20c | ||
|
|
59413a8b64 | ||
|
|
cf69cac410 |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
@@ -383,19 +383,19 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
await expect(heading).toBeVisible();
|
await expect(heading).toBeVisible();
|
||||||
await expect(sidebar.getByText('Træk et projekt herop')).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
|
// A group's name must be readable in full, and must line up with the other
|
||||||
// name starts.
|
// headings — not with the projects filed under it.
|
||||||
//
|
//
|
||||||
// Both halves are here because both were once broken at the same time and for
|
// The clipping half is here because it was once broken: "Omdøb" and "Slet" sat
|
||||||
// different reasons. "Omdøb" and "Slet" sat in the flow reserving their width
|
// in the flow reserving their width even while invisible, so ARBEJDE was drawn
|
||||||
// even while invisible, so ARBEJDE was drawn as "ARBE"; and the fold chevron
|
// as "ARBE". The alignment half changed meaning with the redesign: a group now
|
||||||
// sat before the name, pushing a group 22px further in than the projects it
|
// carries a fold chevron and a section icon just like "Projekter" above it, and
|
||||||
// contains — so a group read as if it were filed under them. Every assertion
|
// belongs to that heading tier rather than to the project rows below.
|
||||||
// in this file passed through both, because both buttons still worked.
|
|
||||||
const shape = await sidebar.locator('.folder-head').evaluate((el) => {
|
const shape = await sidebar.locator('.folder-head').evaluate((el) => {
|
||||||
const link = el.querySelector('h2 a');
|
const link = el.querySelector('h2 a');
|
||||||
const name = { ...link.querySelector('.dot').getBoundingClientRect().toJSON() };
|
// The leading mark is now the section icon, the same folder/funnel/tag glyph
|
||||||
// The last child of the link is the name itself; the dot is a span before it.
|
// the system headings carry — a group and a section read identically.
|
||||||
|
const name = { ...link.querySelector('.head-icon').getBoundingClientRect().toJSON() };
|
||||||
return {
|
return {
|
||||||
clipped: link.scrollWidth > link.clientWidth + 1,
|
clipped: link.scrollWidth > link.clientWidth + 1,
|
||||||
left: name.left,
|
left: name.left,
|
||||||
@@ -407,17 +407,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.clipped, 'gruppens navn bliver klippet').toBe(false);
|
||||||
expect(shape.count, 'navnet og tallet overlapper').toBeGreaterThanOrEqual(shape.right);
|
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
|
// A group is a heading, not a project row: it belongs to the same tier as
|
||||||
// mark, and it is the only thing in them that means the same on both sides.
|
// "Projekter" and the other system sections — chevron, icon, bold name — and
|
||||||
// Comparing the group's inner link with the project's outer box reported the
|
// starts where they start, not where the projects filed under it do. Measured
|
||||||
// row's own padding as a misalignment.
|
// icon to icon, the one mark both headings share.
|
||||||
const projectLeft = await sidebar
|
const sectionLeft = await sidebar
|
||||||
.getByRole('link', { name: 'Regnskab' })
|
.getByRole('heading', { name: 'Projekter' })
|
||||||
.evaluate((el) => el.querySelector('.dot').getBoundingClientRect().left);
|
.evaluate((el) => el.querySelector('.head-icon').getBoundingClientRect().left);
|
||||||
expect(
|
expect(
|
||||||
Math.abs(shape.left - projectLeft),
|
Math.abs(shape.left - sectionLeft),
|
||||||
`gruppen starter ${Math.round(shape.left - projectLeft)}px fra projekterne`
|
`gruppen starter ${Math.round(shape.left - sectionLeft)}px fra sektionsoverskrifterne`
|
||||||
).toBeLessThanOrEqual(2);
|
).toBeLessThanOrEqual(4);
|
||||||
|
|
||||||
// Dragged in, then dragged back out onto "Projekter" — which is the reason
|
// 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
|
// that heading is a drop target at all: with every project filed away there
|
||||||
@@ -440,18 +440,13 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
'false'
|
'false'
|
||||||
);
|
);
|
||||||
|
|
||||||
// En foldet gruppe står med sin chevron fremme permanent, og den lå oven i
|
// A group's chevron is in the flow now, the same one the system sections carry —
|
||||||
// prikken med et felt i --surface bag sig — en lys firkant ud for navnet på
|
// no absolute mark with a --surface field behind it, so a folded group no longer
|
||||||
// den sænkede bjælke, hele tiden. Nu tones prikken ud i stedet, så der er
|
// leaves a pale square beside its name on the sunken bar.
|
||||||
// ingenting at male henover den med.
|
const chevronBg = await sidebar
|
||||||
const chevron = await sidebar
|
|
||||||
.locator('.folder-head .chevron-button')
|
.locator('.folder-head .chevron-button')
|
||||||
.evaluate((el) => ({
|
.evaluate((el) => getComputedStyle(el).backgroundColor);
|
||||||
background: getComputedStyle(el).backgroundColor,
|
expect(chevronBg, 'chevronen har en firkant bag sig').toBe('rgba(0, 0, 0, 0)');
|
||||||
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');
|
|
||||||
|
|
||||||
await sidebar.getByRole('button', { name: /^Fold Arbejde/ }).click();
|
await sidebar.getByRole('button', { name: /^Fold Arbejde/ }).click();
|
||||||
|
|
||||||
@@ -467,11 +462,7 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
// button on click.
|
// button on click.
|
||||||
await expect(sidebar.getByLabel('Gruppens navn')).toBeVisible();
|
await expect(sidebar.getByLabel('Gruppens navn')).toBeVisible();
|
||||||
await sidebar.getByLabel('Gruppens navn').press('Enter');
|
await sidebar.getByLabel('Gruppens navn').press('Enter');
|
||||||
|
await expect(sidebar.getByRole('link', { name: 'Arbejde', exact: true })).toBeVisible();
|
||||||
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)');
|
|
||||||
|
|
||||||
// The heading itself is *not* indented — only what is filed under it. A heading
|
// 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
|
// that starts further in than the rows above it says the heading is inside
|
||||||
@@ -485,10 +476,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
.evaluate((el) => el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft));
|
.evaluate((el) => el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft));
|
||||||
expect(headLeft, 'gruppehovedet er rykket ind').toBeLessThanOrEqual(looseLeft);
|
expect(headLeft, 'gruppehovedet er rykket ind').toBeLessThanOrEqual(looseLeft);
|
||||||
|
|
||||||
// A project inside a group starts further in than a row outside one. Measured
|
// A project sits at the same left margin whether or not it is in a group — one
|
||||||
// rather than asserted on a class, because indentation is a fact about where
|
// straight edge down the sidebar, with the bold heading doing the grouping the
|
||||||
// the row's contents land — and measured at the *content* edge, not the box:
|
// indent used to. Measured at the *content* edge, not the box, because what the
|
||||||
// the indent is padding, so the element's own left edge does not move.
|
// eye follows is where the text lands.
|
||||||
await sidebar.getByRole('link', { name: 'Regnskab' }).dragTo(heading);
|
await sidebar.getByRole('link', { name: 'Regnskab' }).dragTo(heading);
|
||||||
const contentLeft = (el) =>
|
const contentLeft = (el) =>
|
||||||
el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft);
|
el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft);
|
||||||
@@ -496,7 +487,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
sidebar.locator('.folder > a').first().evaluate(contentLeft),
|
sidebar.locator('.folder > a').first().evaluate(contentLeft),
|
||||||
sidebar.locator('.views 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
|
await sidebar
|
||||||
.getByRole('link', { name: 'Regnskab' })
|
.getByRole('link', { name: 'Regnskab' })
|
||||||
@@ -1522,9 +1516,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
|
// 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
|
// now. The chevron beside it still folds; they are two intentions and the old
|
||||||
// single target could only serve the smaller one.
|
// single target could only serve the smaller one.
|
||||||
await sidebar
|
//
|
||||||
.getByRole('link', { name: 'Drejebænk' })
|
// The drop is retried until the project actually nests under the group: a native
|
||||||
.dragTo(sidebar.getByRole('link', { name: 'Værksted' }));
|
// 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 sidebar.getByRole('link', { name: 'Værksted' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Værksted', level: 1 })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Værksted', level: 1 })).toBeVisible();
|
||||||
|
|||||||
@@ -487,6 +487,7 @@
|
|||||||
it, which is the second time that drop has earned its test. -->
|
it, which is the second time that drop has earned its test. -->
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
data-view={item.key}
|
||||||
class:active={current === item.href}
|
class:active={current === item.href}
|
||||||
class:receiving={item.key === 'today' && overToday}
|
class:receiving={item.key === 'today' && overToday}
|
||||||
onclick={onnavigate}
|
onclick={onnavigate}
|
||||||
@@ -529,6 +530,52 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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}
|
{:else}
|
||||||
<span class="dot" class:today={item.key === 'today'} aria-hidden="true"></span>
|
<span class="dot" class:today={item.key === 'today'} aria-hidden="true"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -570,6 +617,7 @@
|
|||||||
{#snippet projectRow(project, sortable)}
|
{#snippet projectRow(project, sortable)}
|
||||||
<a
|
<a
|
||||||
href="/projekt/{project.id}"
|
href="/projekt/{project.id}"
|
||||||
|
data-view={project.is_inbox ? 'inbox' : undefined}
|
||||||
class:sortable
|
class:sortable
|
||||||
class:active={current === `/projekt/${project.id}`}
|
class:active={current === `/projekt/${project.id}`}
|
||||||
class:dragging={draggingId === project.id}
|
class:dragging={draggingId === project.id}
|
||||||
@@ -593,7 +641,29 @@
|
|||||||
? onTaskDrop(e, project)
|
? onTaskDrop(e, project)
|
||||||
: sortable && onDrop(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)}
|
{projectName(project)}
|
||||||
<!-- Open tasks, not people. The same grey number meant three things in this
|
<!-- 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
|
one column — people at a project, projects at a group, tasks at a label
|
||||||
@@ -620,6 +690,64 @@
|
|||||||
The whole heading is the target, not the chevron. At this size a lone chevron
|
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.
|
is something you miss, 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)}
|
{#snippet foldHeading(key, label)}
|
||||||
<h2 class="fold">
|
<h2 class="fold">
|
||||||
<button
|
<button
|
||||||
@@ -634,6 +762,7 @@
|
|||||||
>
|
>
|
||||||
<path d="M6 9l6 6 6-6" />
|
<path d="M6 9l6 6 6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{@render sectionIcon(key)}
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -656,7 +785,7 @@
|
|||||||
and with every project filed there would be no loose row left to aim at. -->
|
and with every project filed there would be no loose row left to aim at. -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="group-head"
|
class="group-head sticky-head"
|
||||||
class:over={overGroup === ''}
|
class:over={overGroup === ''}
|
||||||
ondragover={(e) => {
|
ondragover={(e) => {
|
||||||
if (!carries(e, PROJECT)) return;
|
if (!carries(e, PROJECT)) return;
|
||||||
@@ -846,11 +975,7 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{@render sectionIcon('group')}
|
||||||
class="dot group-dot"
|
|
||||||
style="background: {colorVar(group.color)}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
{group.name}
|
{group.name}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1005,6 +1130,32 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--s5);
|
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
|
/* Wider than it looks: a 1px target is a target you miss. The visible line stays
|
||||||
@@ -1133,8 +1284,26 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active .mark {
|
/* The fixed views carry a colour of their own, the way Things does — the star
|
||||||
color: var(--accent);
|
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 {
|
.group-head {
|
||||||
@@ -1144,12 +1313,33 @@
|
|||||||
padding: 0 var(--s2) var(--s2);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
h2 {
|
||||||
font-size: var(--text-xs);
|
font-size: var(--menu-size);
|
||||||
font-weight: 560;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
color: var(--ink);
|
||||||
letter-spacing: 0.06em;
|
}
|
||||||
color: var(--ink-faint);
|
|
||||||
|
/* 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
|
/* Only the square icon buttons — "+" and the folder — and asked for by class
|
||||||
@@ -1250,39 +1440,17 @@
|
|||||||
klods ud for navnet permanent, fordi chevronen bliver stående, når gruppen
|
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
|
er lukket. Prikken tones ud i stedet for at blive dækket — det er samme
|
||||||
skifte, uden noget at male henover den med. */
|
skifte, uden noget at male henover den med. */
|
||||||
|
/* A group folds from its own chevron, the same one the system sections carry —
|
||||||
|
in the flow before the name now, not an absolute mark that appeared on hover,
|
||||||
|
so a group and a section read identically at rest. The name beside it is still
|
||||||
|
a link to the group's page; the chevron is the second intention. */
|
||||||
.chevron-button {
|
.chevron-button {
|
||||||
position: absolute;
|
flex: none;
|
||||||
left: calc(var(--s2) - 5px);
|
width: 14px;
|
||||||
width: 18px;
|
height: 14px;
|
||||||
height: 18px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: inherit;
|
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 {
|
.chevron-button:hover {
|
||||||
@@ -1362,29 +1530,19 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The group's own mark, between the chevron and the name — inside the fold
|
/* Projects sit at the same left margin whether or not they are in an area —
|
||||||
button, so it cannot push the heading in. It is a mark now and not a
|
one straight edge down the whole sidebar reads calmer than a staircase, and
|
||||||
control: the colour is chosen while renaming, which is the other edit to
|
the bold area heading above already does the grouping the indent used to. */
|
||||||
the same thing. */
|
|
||||||
.group-dot {
|
|
||||||
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. */
|
|
||||||
.folder > a {
|
.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 {
|
.folder > a.sortable::before {
|
||||||
left: calc(var(--s2) + var(--s4));
|
left: var(--s2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder > .empty {
|
.folder > .empty {
|
||||||
padding-left: calc(var(--s2) + var(--s4));
|
padding-left: var(--s2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inside the rename form now, so it needs no padding of its own — the form
|
/* Inside the rename form now, so it needs no padding of its own — the form
|
||||||
@@ -1455,8 +1613,12 @@
|
|||||||
color: var(--ink);
|
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 {
|
a.active {
|
||||||
background: var(--surface-raised);
|
background: var(--accent-sunken);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1081,6 +1081,11 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
padding-right: var(--s3);
|
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 {
|
.head {
|
||||||
@@ -1546,7 +1551,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 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: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
/* Luft nok til at teksten ikke rører kanten.
|
/* Luft nok til at teksten ikke rører kanten.
|
||||||
|
|||||||