From 3b521b463ca70b0a82d0a7ffa51de109f56dffcd Mon Sep 17 00:00:00 2001 From: Kristian Wind Date: Mon, 17 Aug 2026 16:58:35 +0200 Subject: [PATCH] feat(projects): add, rename and delete sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The board has drawn a column per section since it was written and there was no way to make one. Sections could only be created by the Todoist importer or a template — so an ordinary project could never have a heading in it at all. Add, rename and delete, in the list view where the headings already are. The controls appear on hover rather than sitting beside the heading permanently: a heading with two buttons always next to it stops reading as a heading. Deleting says how many tasks are in the section and that they will survive it, because the API keeps them and drops their section — which is the right behaviour and not what "delete section" sounds like. The list is re-read afterwards, since those tasks move to the unsectioned group and nothing else would tell the view they had. Co-Authored-By: Claude Opus 5 --- web/src/lib/components/TaskDetail.svelte | 6 - web/src/routes/projekt/[id]/+page.svelte | 187 ++++++++++++++++++++++- 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/web/src/lib/components/TaskDetail.svelte b/web/src/lib/components/TaskDetail.svelte index 694fd90..aff1d3f 100644 --- a/web/src/lib/components/TaskDetail.svelte +++ b/web/src/lib/components/TaskDetail.svelte @@ -699,12 +699,6 @@ gap: var(--s3); } - .repeats { - margin: 0; - font-size: var(--text-sm); - color: var(--ink-muted); - } - section { display: flex; flex-direction: column; diff --git a/web/src/routes/projekt/[id]/+page.svelte b/web/src/routes/projekt/[id]/+page.svelte index 7943362..73ac469 100644 --- a/web/src/routes/projekt/[id]/+page.svelte +++ b/web/src/routes/projekt/[id]/+page.svelte @@ -101,6 +101,61 @@ } } + // --- sections ------------------------------------------------------------------- + + let addingSection = $state(false); + let renamingSection = $state(null); + let sectionName = $state(''); + + async function addSection(event) { + event.preventDefault(); + const name = sectionName.trim(); + if (!name) return; + addingSection = false; + sectionName = ''; + try { + sections = [...sections, await api.createSection(project.id, name)]; + } catch (e) { + app.toast(humanMessage(e)); + } + } + + async function renameSection(event, section) { + event.preventDefault(); + const name = sectionName.trim(); + renamingSection = null; + if (!name || name === section.name) return; + + const previous = sections; + sections = sections.map((s) => (s.id === section.id ? { ...s, name } : s)); + try { + await api.updateSection(section.id, { name }); + } catch (e) { + sections = previous; + app.toast(humanMessage(e)); + } + } + + async function removeSection(section) { + const count = open.filter((t) => t.section_id === section.id).length; + const warning = count + ? `Slet sektionen "${section.name}"? De ${count} opgaver bliver, men mister deres sektion.` + : `Slet sektionen "${section.name}"?`; + if (!confirm(warning)) return; + + const previous = sections; + sections = sections.filter((s) => s.id !== section.id); + try { + await api.deleteSection(section.id); + // The tasks are not deleted with it; they come back unsectioned, and the + // list has to be re-read to show them in the right place. + await app.loadTasks({ project_id: project.id }); + } catch (e) { + sections = previous; + app.toast(humanMessage(e)); + } + } + async function invite(event) { event.preventDefault(); inviteError = ''; @@ -249,7 +304,34 @@ {#each sections as section (section.id)} {@const tasks = open.filter((t) => t.section_id === section.id)}
-

{section.name}

+
+ {#if renamingSection === section.id} +
renameSection(e, section)}> + + (renamingSection = null)} + onkeydown={(e) => e.key === 'Escape' && (renamingSection = null)} + /> +
+ {:else} +

{section.name}

+ {#if canEdit} + + + {/if} + {/if} +
{#if !tasks.length}

Tom

@@ -257,7 +339,35 @@
{/each} - {#if !open.length} + {#if canEdit} +
+ {#if addingSection} +
+ + !sectionName.trim() && (addingSection = false)} + onkeydown={(e) => e.key === 'Escape' && (addingSection = false)} + /> +
+ {:else} + + {/if} +
+ {/if} + + {#if !open.length && !sections.length}

Ingenting her endnu. @@ -548,6 +658,79 @@ margin-bottom: var(--s2); } + .section-head { + display: flex; + align-items: baseline; + gap: var(--s3); + border-bottom: 1px solid var(--line); + margin-bottom: var(--s2); + } + + .section-head h2 { + border-bottom: 0; + margin-bottom: 0; + flex: 1; + min-width: 0; + } + + /* Hidden until the section is hovered or something in it has focus: a heading + with two buttons permanently beside it stops reading as a heading. */ + .section-action { + font-size: var(--text-xs); + color: var(--ink-faint); + padding: 0 var(--s1); + opacity: 0; + transition: opacity var(--fast) var(--ease); + } + + .section-head:hover .section-action, + .section-action:focus-visible { + opacity: 1; + } + + .section-action:hover { + color: var(--ink); + } + + .section-action.remove:hover { + color: var(--danger); + } + + .section-head form { + flex: 1; + } + + .section-head input, + .add-section input { + width: 100%; + padding: var(--s1) var(--s2); + background: var(--surface-raised); + border: 1px solid var(--line); + border-radius: var(--radius); + font-size: var(--text-sm); + outline: none; + } + + .section-head input:focus, + .add-section input:focus { + border-color: var(--accent); + } + + .add-section { + margin-top: var(--s5); + } + + .add-section .add { + font-size: var(--text-sm); + color: var(--ink-faint); + padding: var(--s2); + transition: color var(--fast) var(--ease); + } + + .add-section .add:hover { + color: var(--accent); + } + .empty, .readonly { margin: 0;