Something went wrong. Try again.
a local-first atmospheric chalky task planner
Something went wrong. Try again.
6.8 kB · 221 lines
Svelte
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222<script lang="ts"> import { X, GripVertical } from "@lucide/svelte"; import { dragHandleZone, dragHandle, type DndEvent } from "svelte-dnd-action"; import { flip } from "svelte/animate"; import { settings } from "../store/settings.svelte"; import { PRESETS, type PaletteColor } from "../palette";
const FLIP_MS = 180; const MAX_ACTIVE = 9;
let { onOpenPicker }: { onOpenPicker: (id: string | null) => void } = $props();
// Local mirror of the active (reorderable) palette — svelte-dnd-action owns // this array mid-drag via consider/finalize, same pattern as DayColumn. let items = $state<PaletteColor[]>(settings.activePalette.map((c) => ({ ...c }))); // Resync whenever the palette changes from elsewhere (add/remove/restore, or // a remote edit hydrating in). Harmless to also run right after our own // `finalize` writes back — it'll just resync to the same order. $effect(() => { items = settings.activePalette.map((c) => ({ ...c })); });
let full = $derived(items.length >= MAX_ACTIVE);
let dndOptions = $derived({ items, flipDurationMs: FLIP_MS, type: "palette", dropTargetStyle: {}, zoneTabIndex: -1, zoneItemTabIndex: -1, });
function consider(e: CustomEvent<DndEvent<PaletteColor>>) { items = e.detail.items; } function finalize(e: CustomEvent<DndEvent<PaletteColor>>) { items = e.detail.items; // Reassemble: the freshly-ordered active colors, then every hidden entry // (kept, just appended) — see settings.svelte.ts's PaletteColor.hidden. const hidden = settings.palette.filter((c) => c.hidden); settings.setPalette([...items, ...hidden]); }
function commitName(id: string, raw: string) { settings.updateColor(id, { name: raw }); }
// "Add color" opens the picker page in "new color" mode (id = null): the // entry is only created when its Save commits, so cancelling never leaves a // half-made color behind. The picker seeds its draft from the first preset // not already used by any current entry. function addNewColor() { if (full) return; onOpenPicker(null); }</script>
<div class="colors-page"> <div class="sect"> <div class="sect-title">Your palette</div> <p class="hint">Drag to reorder — the number is its hotkey (press it to filter by that color).</p> <!-- svelte-ignore a11y_no_static_element_interactions --> <div class="list" use:dragHandleZone={dndOptions} onconsider={consider} onfinalize={finalize}> {#each items as c, i (c.id)} <div class="row" animate:flip={{ duration: FLIP_MS }}> <span class="badge">{i + 1}</span> <!-- svelte-ignore a11y_no_static_element_interactions --> <span class="grip" use:dragHandle aria-label="Drag to reorder"> <GripVertical size={14} aria-hidden="true" /> </span> <button class="swatch-btn" aria-label={`${c.name ?? `Color ${i + 1}`} — change color`} onclick={() => onOpenPicker(c.id)} > <span class="swatch-dot" style:background={c.hex}></span> <span class="hex-chip">{c.hex}</span> </button> <input class="name-input" type="text" placeholder="Name" value={c.name ?? ""} aria-label={`${c.name ?? `Color ${i + 1}`} — name`} onchange={(e) => commitName(c.id, (e.currentTarget as HTMLInputElement).value)} /> <button class="remove" aria-label={`Remove ${c.name ?? `color ${i + 1}`}`} onclick={() => settings.removeColor(c.id)} > <X size={13} aria-hidden="true" /> </button> </div> {/each} </div> </div>
<div class="sep"></div>
<div class="sect"> <div class="sect-title">Add color</div> <button class="add-color-btn" disabled={full} onclick={addNewColor}>Add color</button> {#if full}<p class="hint">Palette is full (9 colors) — remove one to add another.</p>{/if} </div></div>
<style> .colors-page { display: flex; flex-direction: column; gap: 16px; } .sect { display: flex; flex-direction: column; gap: 10px; } .sect-title { font-size: var(--fs-small); font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: 0.05em; } .sep { height: 1px; background: var(--line); } .hint { margin: 0; font-size: var(--fs-small); color: var(--text); line-height: 1.4; }
.list { display: flex; flex-direction: column; gap: 6px; } .row { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--line); } .badge { flex: none; display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; background: var(--surface-alt); color: var(--text); font-size: 11px; font-weight: 700; } .grip { flex: none; display: grid; place-items: center; width: 18px; height: 24px; color: var(--text); cursor: var(--cur-pointer); touch-action: none; }
.swatch-btn { flex: none; display: flex; align-items: center; gap: 6px; padding: 3px 8px 3px 3px; border: 1px solid var(--line-strong); border-radius: 999px; background: var(--surface); } @media (hover: hover) { .swatch-btn:hover { background: var(--surface-alt); } } .swatch-dot { flex: none; width: 20px; height: 20px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12); } .hex-chip { font-size: var(--fs-small); color: var(--text); text-transform: lowercase; }
.name-input { flex: 1 1 auto; font: inherit; color: var(--text); background-color: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 5px 8px; outline: none; min-width: 0; } .name-input:focus-visible { border-color: var(--text); } .name-input::placeholder { color: var(--text-dim); }
.remove { flex: none; display: grid; place-items: center; width: 24px; height: 24px; border-radius: 6px; color: var(--text); } @media (hover: hover) { .remove:hover { background: var(--surface-alt); color: var(--danger); } }
.add-color-btn { align-self: flex-start; padding: 6px 14px; border-radius: var(--radius-sm); background: var(--text); color: var(--bg); font-size: var(--fs-body); font-weight: 600; } .add-color-btn:disabled { opacity: 0.5; cursor: default; } @media (hover: hover) { .add-color-btn:hover:not(:disabled) { filter: brightness(0.92); } }</style>