feat: extrae TaskText y TaskMeta para TaskItem

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 1 month ago
parent 415548cdce
commit b02ca36383

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { success, error as toastError } from "$lib/stores/toasts"; import { success, error as toastError } from "$lib/stores/toasts";
import { tick, createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { normalizeDigits } from "$lib/utils/phone"; import { normalizeDigits } from "$lib/utils/phone";
import { colorForGroup } from "$lib/utils/groupColor"; import { colorForGroup } from "$lib/utils/groupColor";
@ -8,6 +8,8 @@
import TaskAssignees from "$lib/ui/data/task/TaskAssignees.svelte"; import TaskAssignees from "$lib/ui/data/task/TaskAssignees.svelte";
import TaskCompleteButton from "$lib/ui/data/task/TaskCompleteButton.svelte"; import TaskCompleteButton from "$lib/ui/data/task/TaskCompleteButton.svelte";
import TaskActions from "$lib/ui/data/task/TaskActions.svelte"; import TaskActions from "$lib/ui/data/task/TaskActions.svelte";
import TaskText from "$lib/ui/data/task/TaskText.svelte";
import TaskMeta from "$lib/ui/data/task/TaskMeta.svelte";
export let id: number; export let id: number;
export let description: string; export let description: string;
@ -43,7 +45,7 @@
// Edición de texto (inline) // Edición de texto (inline)
let editingText = false; let editingText = false;
let descEl: HTMLElement | null = null; let taskText: any | null = null;
async function doClaim() { async function doClaim() {
if (busy) return; if (busy) return;
@ -193,31 +195,14 @@
editingText = !editingText; editingText = !editingText;
if (editingText) { if (editingText) {
editing = false; editing = false;
// Asegurar que el elemento refleja el texto actual y enfocarlo
if (descEl) {
descEl.textContent = description;
}
tick().then(() => {
if (descEl) {
descEl.focus();
placeCaretAtEnd(descEl);
}
});
} }
} }
function placeCaretAtEnd(el: HTMLElement) {
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
async function saveText() { async function saveText(text?: string) {
if (busy) return; if (busy) return;
const raw = (descEl?.textContent || "").replace(/\s+/g, " ").trim(); const rawSource = typeof text === "string" ? text : (taskText?.getCurrentText?.() ?? "");
const raw = String(rawSource).replace(/\s+/g, " ").trim();
if (raw.length < 1 || raw.length > 1000) { if (raw.length < 1 || raw.length > 1000) {
toastError("La descripción debe tener entre 1 y 1000 caracteres."); toastError("La descripción debe tener entre 1 y 1000 caracteres.");
return; return;
@ -250,51 +235,25 @@
} }
function cancelText() { function cancelText() {
if (descEl) {
descEl.textContent = description;
}
editingText = false; editingText = false;
} }
</script> </script>
<li class="task" class:completed in:fade={{ duration: 180 }} out:fade={{ duration: 180 }}> <li class="task" class:completed in:fade={{ duration: 180 }} out:fade={{ duration: 180 }}>
<div class="code">{codeStr}</div> <div class="code">{codeStr}</div>
<div <TaskText
tabindex="0" description={description}
class="desc" {completed}
class:editing={editingText} editing={editingText}
class:completed {busy}
contenteditable={editingText && !completed} bind:this={taskText}
role="textbox" on:toggleEdit={toggleEditText}
aria-label="Descripción de la tarea" on:saveText={(e) => saveText((e as CustomEvent<{ text: string }>).detail.text)}
spellcheck="true" on:cancelText={cancelText}
bind:this={descEl} />
on:keydown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
cancelText();
} else if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
saveText();
} else if (e.key === "Enter") {
e.preventDefault();
}
}}
>
{description}
</div>
<div class="meta"> <div class="meta">
<span <TaskMeta {groupLabel} {gc} {due_date} />
class="group-badge"
title="Grupo"
style={gc
? `--gc-border: ${gc.border}; --gc-bg: ${gc.bg}; --gc-text: ${gc.text};`
: undefined}>{groupLabel}</span
>
{#if due_date}
<TaskDueBadge {due_date} />
{/if}
</div> </div>
<div class="complete"> <div class="complete">
<TaskCompleteButton <TaskCompleteButton
@ -356,27 +315,6 @@
.completed { .completed {
opacity: 0.5; opacity: 0.5;
} }
.desc {
padding: 8px 4px;
grid-column: 1/3;
grid-row: 2/3;
}
.desc.editing {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
background: var(--color-surface);
padding: 2px 4px;
border-radius: 4px;
white-space: normal;
text-overflow: clip;
grid-column: 1/3;
grid-row: 2/3;
margin: 16px 0;
}
.desc.completed {
text-decoration: line-through;
}
.meta { .meta {
justify-self: end; justify-self: end;
align-items: start; align-items: start;
@ -389,14 +327,6 @@
.muted { .muted {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.group-badge {
padding: 2px 6px;
border-radius: 6px;
border: 1px solid var(--gc-border, var(--color-border));
background: var(--gc-bg, transparent);
color: var(--gc-text, inherit);
font-size: 12px;
}
.assignees-container { .assignees-container {
grid-row: 4/5; grid-row: 4/5;
grid-column: 1/2; grid-column: 1/2;

@ -0,0 +1,31 @@
<script lang="ts">
import TaskDueBadge from "$lib/ui/data/task/TaskDueBadge.svelte";
export let groupLabel: string;
export let gc: { border?: string; bg?: string; text?: string } | null = null;
export let due_date: string | null = null;
</script>
<span
class="group-badge"
title="Grupo"
style={gc
? `--gc-border: ${gc.border}; --gc-bg: ${gc.bg}; --gc-text: ${gc.text};`
: undefined}
>
{groupLabel}
</span>
{#if due_date}
<TaskDueBadge {due_date} />
{/if}
<style>
.group-badge {
padding: 2px 6px;
border-radius: 6px;
border: 1px solid var(--gc-border, var(--color-border));
background: var(--gc-bg, transparent);
color: var(--gc-text, inherit);
font-size: 12px;
}
</style>

@ -0,0 +1,99 @@
<script lang="ts">
import { tick, createEventDispatcher } from "svelte";
export let description: string;
export let completed: boolean;
export let editing: boolean;
export let busy: boolean;
const dispatch = createEventDispatcher<{
toggleEdit: void;
saveText: { text: string };
cancelText: void;
}>();
let el: HTMLElement | null = null;
// Mantener el DOM sincronizado cuando se cierra la edición o cambia la descripción
$: if (el && !editing) {
el.textContent = description;
}
// Enfocar al entrar en modo edición
$: if (editing) {
tick().then(() => {
if (el) {
el.focus();
placeCaretAtEnd(el);
}
});
}
function placeCaretAtEnd(node: HTMLElement) {
const range = document.createRange();
range.selectNodeContents(node);
range.collapse(false);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
function normalizeText(s: string): string {
return s.replace(/\s+/g, " ").trim();
}
export function getCurrentText(): string {
return normalizeText(el?.textContent || "");
}
</script>
<div
tabindex="0"
class="desc"
class:editing={editing}
class:completed
contenteditable={editing && !completed}
role="textbox"
aria-label="Descripción de la tarea"
spellcheck="true"
bind:this={el}
on:dblclick={() => !busy && !completed && dispatch('toggleEdit')}
on:keydown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
dispatch('cancelText');
} else if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
dispatch('saveText', { text: getCurrentText() });
} else if (e.key === "Enter") {
e.preventDefault();
}
}}
>
{description}
</div>
<style>
.desc {
padding: 8px 4px;
grid-column: 1/3;
grid-row: 2/3;
}
.desc.editing {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
background: var(--color-surface);
padding: 2px 4px;
border-radius: 4px;
white-space: normal;
text-overflow: clip;
grid-column: 1/3;
grid-row: 2/3;
margin: 16px 0;
}
.desc.completed {
text-decoration: line-through;
}
</style>
Loading…
Cancel
Save