Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
272c4918b0 | ||
|
|
ff866cc514 | ||
|
|
e94d731c30 | ||
|
|
0aa0ec2989 | ||
|
|
21b60b007a | ||
|
|
7066e2e06e | ||
|
|
2b76a172e8 | ||
|
|
15495255ef | ||
|
|
3b9b68f4b1 | ||
|
|
7dcd475036 | ||
|
|
fc4877c3bd | ||
|
|
2da679d629 |
@@ -24,9 +24,10 @@ Documentation: [`docs/`](docs/), built with Material for MkDocs.
|
|||||||
|
|
||||||
## Why this exists
|
## Why this exists
|
||||||
|
|
||||||
Todoist Pro, self-hosted, on your own hardware, with your own data — and with the
|
A fast, self-hosted task manager — projects, quick add, filters, notes and shared
|
||||||
integrations that a hosted product will not build for you: a real CalDAV server, a
|
lists — on your own hardware, with your own data, and with the integrations that a
|
||||||
mail-to-task address, and an MCP endpoint so Claude can work your task list directly.
|
hosted product will not build for you: a real CalDAV server, a mail-to-task address,
|
||||||
|
and an MCP endpoint so Claude can work your task list directly.
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Saved filters
|
# Saved filters
|
||||||
|
|
||||||
A filter is a small expression describing a slice of your tasks. The grammar is
|
A filter is a small expression describing a slice of your tasks. The grammar will
|
||||||
Todoist's, so if you are coming from there you already know it.
|
feel familiar if you have used query-based filters in other task apps.
|
||||||
|
|
||||||
```
|
```
|
||||||
today & p1
|
today & p1
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ of. Which is what a to-do list is.
|
|||||||
|
|
||||||
## What it is for
|
## What it is for
|
||||||
|
|
||||||
Todoist Pro, on your own hardware, with your own data. And with the integrations a
|
A fast, self-hosted task manager — projects, quick add, filters, notes and shared
|
||||||
|
lists — on your own hardware, with your own data. And with the integrations a
|
||||||
hosted product will not build for you: a real CalDAV server, an address you can
|
hosted product will not build for you: a real CalDAV server, an address you can
|
||||||
forward mail to, and an MCP endpoint so Claude can work your task list directly.
|
forward mail to, and an MCP endpoint so Claude can work your task list directly.
|
||||||
|
|
||||||
@@ -22,15 +23,22 @@ forward mail to, and an MCP endpoint so Claude can work your task list directly.
|
|||||||
Firma, labelled regnskab. Danish and English, mixed freely in one line.
|
Firma, labelled regnskab. Danish and English, mixed freely in one line.
|
||||||
- **Projects, sections, sub-tasks, labels, [saved filters](filters.md).** List,
|
- **Projects, sections, sub-tasks, labels, [saved filters](filters.md).** List,
|
||||||
board and calendar views, and foldable groups over the projects in the sidebar.
|
board and calendar views, and foldable groups over the projects in the sidebar.
|
||||||
- **Notes, beside the work.** Rich text to write in, Markdown on disk. A note is
|
The calendar draws each task to its length — drag the foot to resize, the body
|
||||||
shared by filing it in a project; `#project` and `[[another note]]` are links,
|
to move it.
|
||||||
and a task shows what has been written about it.
|
- **Notes, beside the work.** Rich text to write in, Markdown on disk. Share a
|
||||||
|
single note with a person, or file it in a project; `#project` and
|
||||||
|
`[[another note]]` are links, and a task shows what has been written about it.
|
||||||
|
- **Delegate, and snooze.** Hand a task to somebody and it is marked with an
|
||||||
|
arrow to them, gathered on a *Waiting on others* page. Snooze one to park it,
|
||||||
|
greyed, at the foot of the list until a moment you pick — without touching when
|
||||||
|
it is due.
|
||||||
- **Drag where it means something** — reorder a list or a board, file a project
|
- **Drag where it means something** — reorder a list or a board, file a project
|
||||||
under a group, drop a task on another project, or drop it on another day.
|
under a group, drop a task on another project, or drop it on another day.
|
||||||
- **Sharing** with owner, editor and viewer roles. Anyone outside your instance
|
- **Sharing** with owner, editor and viewer roles. Anyone outside your instance
|
||||||
joins through an invite link.
|
joins through an invite link.
|
||||||
- **Live sync** — a change made by somebody else appears without a refresh.
|
- **Live sync** — a change made by somebody else appears without a refresh.
|
||||||
- **Repeating tasks** as RRULE, reminders, comments and attachments.
|
- **Repeating tasks** as RRULE, comments and attachments, and reminders that go
|
||||||
|
out as Web Push to an iPhone or a Mac.
|
||||||
- **[Calendar](caldav.md), [mail](mail.md) and [Claude](mcp.md)** connected over
|
- **[Calendar](caldav.md), [mail](mail.md) and [Claude](mcp.md)** connected over
|
||||||
open standards rather than private formats.
|
open standards rather than private formats.
|
||||||
|
|
||||||
@@ -48,7 +56,9 @@ columns.*
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
*What you have handed to somebody else, grouped by whoever has it.*
|
*What you have handed to somebody else, grouped by whoever has it — and each row
|
||||||
|
marked with an arrow to them, so a delegated task reads as delegated wherever you
|
||||||
|
meet it.*
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 217 KiB |
@@ -1,7 +1,8 @@
|
|||||||
# Self-hosting
|
# With Docker
|
||||||
|
|
||||||
verdande is one static binary and one directory. There is no database server, no
|
Running it yourself outside a panel: one static binary and one directory. There is
|
||||||
cache, no queue and no second process.
|
no database server, no cache, no queue and no second process. (Yggdrasil is
|
||||||
|
self-hosting too — for that route see [As a Rune](rune.md).)
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ extra_css:
|
|||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Setting it up:
|
- Setting it up:
|
||||||
- Self-hosting: self-hosting.md
|
|
||||||
- As a Rune: rune.md
|
- As a Rune: rune.md
|
||||||
|
- With Docker: self-hosting.md
|
||||||
- Configuration: configuration.md
|
- Configuration: configuration.md
|
||||||
- Using it:
|
- Using it:
|
||||||
- Signing in: signing-in.md
|
- Signing in: signing-in.md
|
||||||
|
|||||||
@@ -856,8 +856,11 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="button primary" href="https://kristianwind.github.io/verdande/self-hosting/">
|
<a class="button primary" href="https://kristianwind.github.io/verdande/rune/">
|
||||||
Self-host it
|
As a Rune
|
||||||
|
</a>
|
||||||
|
<a class="button" href="https://kristianwind.github.io/verdande/self-hosting/">
|
||||||
|
With Docker
|
||||||
</a>
|
</a>
|
||||||
<a class="button" href="https://github.com/kristianwind/verdande">Source</a>
|
<a class="button" href="https://github.com/kristianwind/verdande">Source</a>
|
||||||
<a class="button" href="https://kristianwind.github.io/verdande/">Documentation</a>
|
<a class="button" href="https://kristianwind.github.io/verdande/">Documentation</a>
|
||||||
@@ -949,12 +952,12 @@
|
|||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<img
|
<img
|
||||||
src="screenshots/board.webp"
|
src="screenshots/board.webp"
|
||||||
alt="A project in board view: one column per section, with task cards showing due dates, labels and repetition."
|
alt="A project in board view: one column per section, with task cards showing due dates, labels, an arrow to whoever a task is delegated to, and a snoozed card greyed at the foot of its column."
|
||||||
width="1400"
|
width="1400"
|
||||||
height="900"
|
height="900"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<figcaption>Board view — one column per section.</figcaption>
|
<figcaption>Board view — one column per section. A delegated card carries an arrow to whoever has it; a snoozed one greys at the foot.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<img
|
<img
|
||||||
@@ -1009,12 +1012,12 @@
|
|||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<img
|
<img
|
||||||
src="screenshots/delegated.webp"
|
src="screenshots/delegated.webp"
|
||||||
alt="Waiting on others: tasks that have been handed to somebody else, grouped under the person who has them."
|
alt="Waiting on others: tasks that have been handed to somebody else, grouped under the person who has them, each row carrying an arrow to that person."
|
||||||
width="1400"
|
width="1400"
|
||||||
height="900"
|
height="900"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<figcaption>Waiting on others — grouped by who has it.</figcaption>
|
<figcaption>Waiting on others — grouped by who has it, each marked with an arrow to them.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<img
|
<img
|
||||||
@@ -1070,8 +1073,8 @@
|
|||||||
<h3>Repeats, and says so in words</h3>
|
<h3>Repeats, and says so in words</h3>
|
||||||
<p>
|
<p>
|
||||||
Recurring tasks are RRULE underneath and "every other Tuesday" on the
|
Recurring tasks are RRULE underneath and "every other Tuesday" on the
|
||||||
screen. Reminders are absolute or relative, and go out to the devices
|
screen. Reminders are absolute or relative, and go out as Web Push to
|
||||||
you have allowed them on.
|
an iPhone or a Mac you have allowed them on.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
@@ -1104,8 +1107,17 @@
|
|||||||
<h3>Waiting on others</h3>
|
<h3>Waiting on others</h3>
|
||||||
<p>
|
<p>
|
||||||
What you have handed to somebody else, on its own page, grouped under
|
What you have handed to somebody else, on its own page, grouped under
|
||||||
the person who has it. The thing a shared list stops being able to tell
|
the person who has it — and marked where you meet it too, with an arrow
|
||||||
you the moment it is shared.
|
out of your hands, so a list reads at a glance as which tasks are yours
|
||||||
|
and which you are waiting on.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<h3>Snooze it out of the way</h3>
|
||||||
|
<p>
|
||||||
|
Park a task to this evening, tomorrow, next week, or a moment you pick.
|
||||||
|
It greys and sinks to the bottom of the list until then, and comes back
|
||||||
|
by itself — without touching when it is actually due.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1225,6 +1237,23 @@
|
|||||||
<a href="https://github.com/kristianwind/verdande/releases">the releases page</a>.
|
<a href="https://github.com/kristianwind/verdande/releases">the releases page</a>.
|
||||||
</p>
|
</p>
|
||||||
<ul class="lately">
|
<ul class="lately">
|
||||||
|
<li>
|
||||||
|
<span class="ver">0.41</span>
|
||||||
|
A task you have delegated is marked where you meet it, not only on the
|
||||||
|
"Waiting on others" page. A task can be snoozed to the bottom of the list
|
||||||
|
until a moment you choose, and comes back by itself.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="ver">0.41</span>
|
||||||
|
Web Push reminders reach an iPhone and a Mac again, and every dated task
|
||||||
|
now shows up in a calendar you have subscribed to — not only the timed ones.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="ver">0.41</span>
|
||||||
|
The calendar draws each task to its length: drag the foot to resize, or the
|
||||||
|
body to move it. A single note can be shared with one person, and a
|
||||||
|
<code>[[note]]</code> is a link you follow inline.
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="ver">0.34</span>
|
<span class="ver">0.34</span>
|
||||||
Sections can be dragged into the order the work actually happens in. The
|
Sections can be dragged into the order the work actually happens in. The
|
||||||
@@ -1236,21 +1265,6 @@
|
|||||||
or drag in, a source view, and a copy button on code blocks. Search
|
or drag in, a source view, and a copy button on code blocks. Search
|
||||||
stopped answering by date and started answering by relevance.
|
stopped answering by date and started answering by relevance.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<span class="ver">0.32</span>
|
|
||||||
A calendar of its own as a menu item, with a read-only Google calendar
|
|
||||||
that can be laid over it.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="ver">0.31</span>
|
|
||||||
Mailboxes over IMAP, so flagging a mail in iCloud or Fastmail makes a
|
|
||||||
task — not only Gmail.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="ver">0.30</span>
|
|
||||||
An installation count that says exactly what it sends, and can be turned
|
|
||||||
off in one click.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -937,7 +937,7 @@ test('en rolle kan rettes uden at fjerne personen', async ({ browser, page }) =>
|
|||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
const row = page.locator('.row').filter({ hasText: 'brænde kaffe' });
|
const row = page.locator('.row').filter({ hasText: 'brænde kaffe' });
|
||||||
await expect(row.getByTitle('Ansvarlig: andreas')).toBeVisible();
|
await expect(row.getByTitle('Uddelegeret til andreas')).toBeVisible();
|
||||||
// And not on a task that is nobody's.
|
// And not on a task that is nobody's.
|
||||||
await page.getByLabel('Ny opgave').fill('min egen');
|
await page.getByLabel('Ny opgave').fill('min egen');
|
||||||
await page.getByLabel('Ny opgave').press('Enter');
|
await page.getByLabel('Ny opgave').press('Enter');
|
||||||
|
|||||||
@@ -483,6 +483,17 @@ async function seed({ colleague, noteTitle }) {
|
|||||||
const made = {};
|
const made = {};
|
||||||
for (const task of tasks) made[task.content] = await api('/tasks', task);
|
for (const task of tasks) made[task.content] = await api('/tasks', task);
|
||||||
|
|
||||||
|
// One task snoozed, so the board and the list show a card parked at the bottom
|
||||||
|
// of its section — greyed, with a "snoozed until" mark — set aside rather than
|
||||||
|
// forgotten, and without touching when it is due.
|
||||||
|
const snoozeUntil = (() => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + 2);
|
||||||
|
d.setHours(9, 0, 0, 0);
|
||||||
|
return d.toISOString();
|
||||||
|
})();
|
||||||
|
await api(`/tasks/${made['Write the job advert'].id}/snooze`, { until: snoozeUntil });
|
||||||
|
|
||||||
// A saved filter, because the sidebar shows them and an empty heading reads as
|
// A saved filter, because the sidebar shows them and an empty heading reads as
|
||||||
// a feature that does not work.
|
// a feature that does not work.
|
||||||
await api('/filters', { name: 'Important this week', query: 'p1 & 7 days' });
|
await api('/filters', { name: 'Important this week', query: 'p1 & 7 days' });
|
||||||
|
|||||||
@@ -1115,5 +1115,21 @@
|
|||||||
width: auto;
|
width: auto;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Two columns is half a phone each: Prioritet collapses to "Ing" and the
|
||||||
|
due-row (date + fixed-width time + duration) runs off the right edge. One
|
||||||
|
column per field gives each its full width; the due-row wraps if even that
|
||||||
|
is not enough. */
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due input[type='date'] {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -113,8 +113,14 @@
|
|||||||
{#if assignee}
|
{#if assignee}
|
||||||
<!-- First in the row, because "who" changes what the rest of the line
|
<!-- First in the row, because "who" changes what the rest of the line
|
||||||
means: a date on somebody else's task is their deadline, not
|
means: a date on somebody else's task is their deadline, not
|
||||||
yours. -->
|
yours. The hand-off arrow marks it as delegated — out of your
|
||||||
<span class="assignee" title={t('task.assignee', { name: assignee.name })}>
|
hands and into theirs — so a list reads at a glance as which are
|
||||||
|
yours to do and which you are waiting on. -->
|
||||||
|
<span class="assignee" title={t('task.delegatedTo', { name: assignee.name })}>
|
||||||
|
<svg class="handoff" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M13 5l7 7-7 7" />
|
||||||
|
<path d="M20 12H4" />
|
||||||
|
</svg>
|
||||||
<span class="face" style="background: {assignee.avatar_color}">
|
<span class="face" style="background: {assignee.avatar_color}">
|
||||||
{assignee.name[0]?.toUpperCase() ?? '?'}
|
{assignee.name[0]?.toUpperCase() ?? '?'}
|
||||||
</span>
|
</span>
|
||||||
@@ -358,8 +364,25 @@
|
|||||||
.assignee {
|
.assignee {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--s1);
|
gap: 4px;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 1px var(--s2) 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The delegation mark: an arrow leaving, in the accent so the chip reads as
|
||||||
|
handed-off rather than as just another name. */
|
||||||
|
.handoff {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--accent);
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.face {
|
.face {
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export const da = {
|
|||||||
'repeat.everyN.month': 'hver {n}. måned',
|
'repeat.everyN.month': 'hver {n}. måned',
|
||||||
'repeat.everyN.year': 'hvert {n}. år',
|
'repeat.everyN.year': 'hvert {n}. år',
|
||||||
'task.assignee': 'Ansvarlig: {name}',
|
'task.assignee': 'Ansvarlig: {name}',
|
||||||
|
'task.delegatedTo': 'Uddelegeret til {name}',
|
||||||
'task.subtasks': '{done} af {total} undertasks er lukket',
|
'task.subtasks': '{done} af {total} undertasks er lukket',
|
||||||
'task.attachments': '{n} vedhæftet',
|
'task.attachments': '{n} vedhæftet',
|
||||||
'task.priority': 'Prioritet {n}',
|
'task.priority': 'Prioritet {n}',
|
||||||
@@ -265,6 +266,10 @@ export const da = {
|
|||||||
'Hvor du er logget ind. Genkender du ikke en af dem, så log den ud og skift adgangskode.',
|
'Hvor du er logget ind. Genkender du ikke en af dem, så log den ud og skift adgangskode.',
|
||||||
'account.thisDevice': 'denne enhed',
|
'account.thisDevice': 'denne enhed',
|
||||||
'account.signOutDevice': 'Log ud',
|
'account.signOutDevice': 'Log ud',
|
||||||
|
'account.support': 'Støt projektet',
|
||||||
|
'account.supportHint':
|
||||||
|
'verdande er fri, open source-software under MIT-licensen — ingen betalt version, intet holdt tilbage. Sparede den dig en eftermiddag, kan du give vedligeholderen en kop kaffe. Helt frivilligt, og intet i appen ændrer sig.',
|
||||||
|
'account.buyCoffee': 'Buy me a coffee',
|
||||||
|
|
||||||
'account.totpEnabled': 'Slået til.',
|
'account.totpEnabled': 'Slået til.',
|
||||||
'account.recoveryLeftOne': '{n} gendannelseskode tilbage.',
|
'account.recoveryLeftOne': '{n} gendannelseskode tilbage.',
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export const en = {
|
|||||||
'repeat.everyN.month': 'every {n} months',
|
'repeat.everyN.month': 'every {n} months',
|
||||||
'repeat.everyN.year': 'every {n} years',
|
'repeat.everyN.year': 'every {n} years',
|
||||||
'task.assignee': 'Assigned to {name}',
|
'task.assignee': 'Assigned to {name}',
|
||||||
|
'task.delegatedTo': 'Delegated to {name}',
|
||||||
'task.subtasks': '{done} of {total} sub-tasks are done',
|
'task.subtasks': '{done} of {total} sub-tasks are done',
|
||||||
'task.attachments': '{n} attached',
|
'task.attachments': '{n} attached',
|
||||||
'task.priority': 'Priority {n}',
|
'task.priority': 'Priority {n}',
|
||||||
@@ -261,6 +262,10 @@ export const en = {
|
|||||||
'Where you are signed in. If you do not recognise one, sign it out and change your password.',
|
'Where you are signed in. If you do not recognise one, sign it out and change your password.',
|
||||||
'account.thisDevice': 'this device',
|
'account.thisDevice': 'this device',
|
||||||
'account.signOutDevice': 'Sign out',
|
'account.signOutDevice': 'Sign out',
|
||||||
|
'account.support': 'Support the project',
|
||||||
|
'account.supportHint':
|
||||||
|
'verdande is free, open-source software under the MIT licence — no paid tier, nothing held back. If it saved you an afternoon, you can buy the maintainer a coffee. Entirely optional, and nothing in the app changes either way.',
|
||||||
|
'account.buyCoffee': 'Buy me a coffee',
|
||||||
|
|
||||||
'account.totpEnabled': 'On.',
|
'account.totpEnabled': 'On.',
|
||||||
'account.recoveryLeftOne': '{n} recovery code left.',
|
'account.recoveryLeftOne': '{n} recovery code left.',
|
||||||
|
|||||||
@@ -571,6 +571,42 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Last, and quietly: a donate link belongs in Settings, never in the way of the
|
||||||
|
work. A plain anchor with an inline SVG — no third-party script, no webfont,
|
||||||
|
no tracking — so it works offline and cannot phone home. See the coffee-cup
|
||||||
|
snippet for why not their generated button. -->
|
||||||
|
<section class="panel">
|
||||||
|
<header>
|
||||||
|
<h2>{t('account.support')}</h2>
|
||||||
|
<p class="hint">{t('account.supportHint')}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="bmc"
|
||||||
|
href="https://buymeacoffee.com/kristianwind"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path d="M4 9h13v6a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V9Z" />
|
||||||
|
<path d="M17 10h1.5a2.5 2.5 0 0 1 0 5H17" />
|
||||||
|
<path d="M7.5 5.6c0-.9.8-1.1.8-2M11 5.6c0-.9.8-1.1.8-2M14.5 5.6c0-.9.8-1.1.8-2" />
|
||||||
|
</svg>
|
||||||
|
{t('account.buyCoffee')} ↗
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.looks {
|
.looks {
|
||||||
margin-top: var(--s4);
|
margin-top: var(--s4);
|
||||||
@@ -771,4 +807,36 @@
|
|||||||
.uri {
|
.uri {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The understated variant: present, not loud. Bordered and in the muted ink so
|
||||||
|
it reads as a footnote, brightening on hover. The cup is outline-only. */
|
||||||
|
.bmc {
|
||||||
|
align-self: flex-start;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--s2);
|
||||||
|
padding: var(--s2) var(--s3);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
transition:
|
||||||
|
color var(--fast) var(--ease),
|
||||||
|
border-color var(--fast) var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bmc:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bmc:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bmc svg {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||