feat: init

This commit is contained in:
hh committed 2026-08-08 16:30:22 +02:00
commit 8a609da778
59 files changed
+10053

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
<script lang="ts">
import Archive from "@lucide/svelte/icons/archive";
import { anchorDate, isOpen } from "../model/types";
import type { Task } from "../model/types";
import { draggable, dropZone } from "../lib/dnd.svelte";
import { labelHue } from "../lib/hue";
import { cn } from "../lib/utils";
import { STATUS_META } from "../lib/vocab";
import {
moveTask,
setDate,
setRecurrence,
setStatus,
setText,
} from "../vault/mutate";
import ContextArea from "./context-area.svelte";
import DuePicker from "./due-picker.svelte";
import LanePicker from "./lane-picker.svelte";
import PriorityBars from "./priority-bars.svelte";
import RowMenu from "./row-menu.svelte";
import { dropTarget, handleDrop } from "./drop";
import { followLink, markdown } from "./markdown";
import { taskMenu } from "./task-menu";
import { useView } from "./context";
interface Props {
showProject?: boolean;
showSphere?: boolean;
task: Task;
}
let { task, showSphere = true, showProject = true }: Props = $props();
const view = useView();
let editing = $state(false);
let draft = $state("");
const meta = $derived(STATUS_META[task.status]);
const target = $derived(dropTarget.task(task.id));
const zone = $derived(dropZone());
const isDropTarget = $derived(zone?.target === target);
const date = $derived(anchorDate(task));
const field = $derived(!task.due && task.scheduled ? "scheduled" : "due");
function startEdit() {
draft = task.text;
editing = true;
}
async function commitEdit() {
if (!editing) return;
editing = false;
await setText(view.app, task, draft);
}
function onkeydown(event: KeyboardEvent) {
if (event.key === "Enter") {
event.preventDefault();
void commitEdit();
} else if (event.key === "Escape") {
event.preventDefault();
editing = false;
}
}
async function toggle() {
await setStatus(view.app, task, isOpen(task.status) ? "x" : " ");
}
</script>
<ContextArea class="contents" items={taskMenu(view, task)}>
<div
class={cn(
"group/task grid select-none items-center gap-2 border-border/60 border-b px-3 transition-colors hover:bg-secondary/50",
!isOpen(task.status) && "opacity-60 hover:opacity-100",
isDropTarget &&
(zone?.after
? "shadow-[inset_0_-2px_0_0_var(--signal)]"
: "shadow-[inset_0_2px_0_0_var(--signal)]"),
)}
data-drop={target}
style="grid-template-columns: 1.25rem minmax(0, 1fr) auto;"
use:draggable={{
payload: () => ({ id: task.id, kind: "task" }),
ondrop: (to, after) => void handleDrop(view, task, to, after),
enabled: () => !editing,
}}
>
<button
aria-label={`Status: ${meta.label}`}
class="grid size-5 place-items-center rounded-md outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
data-nodrag
onclick={toggle}
title={meta.label}
type="button"
>
<meta.icon aria-hidden="true" class="size-4" style="color: {meta.color}" />
</button>
<div class="flex min-w-0 items-center gap-1.5 py-2.5">
{#if showSphere}
<span
class="hidden shrink-0 rounded-md border border-border bg-secondary/40 px-1.5 py-0.5 font-medium text-[11px] text-muted-foreground @min-[30rem]:inline-block"
>
{task.sphere}
</span>
{/if}
{#if task.inArchive}
<span
class="hidden shrink-0 items-center gap-1 rounded-md border border-border border-dashed px-1.5 py-0.5 text-[11px] text-muted-foreground @min-[24rem]:inline-flex"
title={task.project}
>
<Archive aria-hidden="true" class="size-3" />
{task.project}
</span>
{:else if showProject}
<LanePicker
class="hidden shrink-0 @min-[24rem]:inline-flex"
onpick={(path, project) =>
void moveTask(view.app, task, { path, project })}
path={task.path}
project={task.project}
>
{#snippet trigger()}
<span
class="hue-chip max-w-32 truncate rounded-md border px-1.5 py-0.5 text-[11px]"
style="--hue: {labelHue(task.project)}"
>
{task.project}
</span>
{/snippet}
</LanePicker>
{/if}
{#if editing}
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus
class="min-w-0 flex-1 rounded-md border border-ring bg-background px-1.5 py-0.5 text-sm outline-none"
onblur={commitEdit}
{onkeydown}
bind:value={draft}
/>
{:else}
<div
class={cn(
"bcal-md min-w-0 flex-1 cursor-text truncate font-medium text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
!isOpen(task.status) && "line-through decoration-muted-foreground/50",
)}
onclick={(event) => {
if (!followLink(view.app, event, task.path)) startEdit();
}}
onkeydown={(event) => {
if (event.key === "Enter") startEdit();
}}
role="button"
tabindex="0"
title={task.text}
use:markdown={{
app: view.app,
component: view.component,
sourcePath: task.path,
text: task.text,
}}
></div>
{/if}
</div>
<div class="flex items-center gap-1">
{#if task.priority !== 2}
<PriorityBars class="shrink-0" priority={task.priority} />
{/if}
<div class="flex justify-end @min-[22rem]:w-24">
<DuePicker
class={cn(
!date &&
"opacity-0 transition-opacity group-hover/task:opacity-100 focus-visible:opacity-100 touch-shown",
)}
{field}
onclear={() => void setDate(view.app, task, field, null)}
onpick={(key) => void setDate(view.app, task, field, key)}
onrepeat={(rule) => void setRecurrence(view.app, task, rule)}
recurrence={task.recurrence}
value={date}
/>
</div>
<RowMenu
class="opacity-0 transition-opacity focus-visible:opacity-100 group-hover/task:opacity-100 touch-shown"
items={taskMenu(view, task)}
/>
</div>
</div>
</ContextArea>