un poquito mejor los estilos
parent
05900ef977
commit
e34df4c370
@ -1,462 +1,461 @@
|
||||
<script lang="ts">
|
||||
import Badge from "$lib/ui/atoms/Badge.svelte";
|
||||
import {
|
||||
compareYmd,
|
||||
todayYmdUTC,
|
||||
ymdToDmy,
|
||||
isToday,
|
||||
isTomorrow,
|
||||
} from "$lib/utils/date";
|
||||
import { success, error as toastError } from "$lib/stores/toasts";
|
||||
import { tick } from "svelte";
|
||||
import Badge from "$lib/ui/atoms/Badge.svelte";
|
||||
import {
|
||||
compareYmd,
|
||||
todayYmdUTC,
|
||||
ymdToDmy,
|
||||
isToday,
|
||||
isTomorrow,
|
||||
} from "$lib/utils/date";
|
||||
import { success, error as toastError } from "$lib/stores/toasts";
|
||||
import { tick } from "svelte";
|
||||
|
||||
export let id: number;
|
||||
export let description: string;
|
||||
export let due_date: string | null = null;
|
||||
export let display_code: number | null = null;
|
||||
export let assignees: string[] = [];
|
||||
export let currentUserId: string | null | undefined = null;
|
||||
export let completed: boolean = false;
|
||||
export let completed_at: string | null = null;
|
||||
export let id: number;
|
||||
export let description: string;
|
||||
export let due_date: string | null = null;
|
||||
export let display_code: number | null = null;
|
||||
export let assignees: string[] = [];
|
||||
export let currentUserId: string | null | undefined = null;
|
||||
export let completed: boolean = false;
|
||||
export let completed_at: string | null = null;
|
||||
|
||||
const code = display_code ?? id;
|
||||
const codeStr = String(code).padStart(4, "0");
|
||||
$: isAssigned = currentUserId ? assignees.includes(currentUserId) : false;
|
||||
$: today = todayYmdUTC();
|
||||
$: overdue = !!due_date && compareYmd(due_date, today) < 0;
|
||||
$: imminent = !!due_date && (isToday(due_date) || isTomorrow(due_date));
|
||||
$: dateDmy = due_date ? ymdToDmy(due_date) : "";
|
||||
const code = display_code ?? id;
|
||||
const codeStr = String(code).padStart(4, "0");
|
||||
$: isAssigned = currentUserId ? assignees.includes(currentUserId) : false;
|
||||
$: today = todayYmdUTC();
|
||||
$: overdue = !!due_date && compareYmd(due_date, today) < 0;
|
||||
$: imminent = !!due_date && (isToday(due_date) || isTomorrow(due_date));
|
||||
$: dateDmy = due_date ? ymdToDmy(due_date) : "";
|
||||
|
||||
let editing = false;
|
||||
let dateValue: string = due_date ?? "";
|
||||
let busy = false;
|
||||
let editing = false;
|
||||
let dateValue: string = due_date ?? "";
|
||||
let busy = false;
|
||||
|
||||
// Edición de texto (inline)
|
||||
let editingText = false;
|
||||
let descEl: HTMLElement | null = null;
|
||||
// Edición de texto (inline)
|
||||
let editingText = false;
|
||||
let descEl: HTMLElement | null = null;
|
||||
|
||||
async function doClaim() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}/claim`, { method: "POST" });
|
||||
if (res.ok) {
|
||||
success("Tarea reclamada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo reclamar");
|
||||
}
|
||||
} catch (e) {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
async function doClaim() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}/claim`, { method: "POST" });
|
||||
if (res.ok) {
|
||||
success("Tarea reclamada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo reclamar");
|
||||
}
|
||||
} catch (e) {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function doComplete() {
|
||||
if (busy || completed) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}/complete`, { method: "POST" });
|
||||
if (res.ok) {
|
||||
success("Tarea completada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo completar la tarea");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
async function doComplete() {
|
||||
if (busy || completed) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}/complete`, { method: "POST" });
|
||||
if (res.ok) {
|
||||
success("Tarea completada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo completar la tarea");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function doUnassign() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}/unassign`, { method: "POST" });
|
||||
if (res.ok) {
|
||||
success("Asignación eliminada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo soltar");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
async function doUnassign() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}/unassign`, { method: "POST" });
|
||||
if (res.ok) {
|
||||
success("Asignación eliminada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo soltar");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDate() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const body = {
|
||||
due_date:
|
||||
dateValue && dateValue.trim() !== "" ? dateValue.trim() : null,
|
||||
};
|
||||
const res = await fetch(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.ok) {
|
||||
success("Fecha actualizada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo actualizar la fecha");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
async function saveDate() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const body = {
|
||||
due_date:
|
||||
dateValue && dateValue.trim() !== "" ? dateValue.trim() : null,
|
||||
};
|
||||
const res = await fetch(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.ok) {
|
||||
success("Fecha actualizada");
|
||||
location.reload();
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo actualizar la fecha");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEdit() {
|
||||
editing = !editing;
|
||||
if (editing) editingText = false;
|
||||
dateValue = due_date ?? "";
|
||||
}
|
||||
function toggleEdit() {
|
||||
editing = !editing;
|
||||
if (editing) editingText = false;
|
||||
dateValue = due_date ?? "";
|
||||
}
|
||||
|
||||
function clearDate() {
|
||||
if (busy) return;
|
||||
if (!confirm("¿Quitar la fecha de vencimiento?")) return;
|
||||
dateValue = "";
|
||||
saveDate();
|
||||
}
|
||||
function clearDate() {
|
||||
if (busy) return;
|
||||
if (!confirm("¿Quitar la fecha de vencimiento?")) return;
|
||||
dateValue = "";
|
||||
saveDate();
|
||||
}
|
||||
|
||||
function toggleEditText() {
|
||||
editingText = !editingText;
|
||||
if (editingText) {
|
||||
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 toggleEditText() {
|
||||
editingText = !editingText;
|
||||
if (editingText) {
|
||||
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);
|
||||
}
|
||||
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() {
|
||||
if (busy) return;
|
||||
const raw = (descEl?.textContent || "").replace(/\s+/g, " ").trim();
|
||||
if (raw.length < 1 || raw.length > 1000) {
|
||||
toastError("La descripción debe tener entre 1 y 1000 caracteres.");
|
||||
return;
|
||||
}
|
||||
if (raw === description) {
|
||||
editingText = false;
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ description: raw }),
|
||||
});
|
||||
if (res.ok) {
|
||||
description = raw;
|
||||
success("Descripción actualizada");
|
||||
editingText = false;
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo actualizar la descripción");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
async function saveText() {
|
||||
if (busy) return;
|
||||
const raw = (descEl?.textContent || "").replace(/\s+/g, " ").trim();
|
||||
if (raw.length < 1 || raw.length > 1000) {
|
||||
toastError("La descripción debe tener entre 1 y 1000 caracteres.");
|
||||
return;
|
||||
}
|
||||
if (raw === description) {
|
||||
editingText = false;
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ description: raw }),
|
||||
});
|
||||
if (res.ok) {
|
||||
description = raw;
|
||||
success("Descripción actualizada");
|
||||
editingText = false;
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
toastError(txt || "No se pudo actualizar la descripción");
|
||||
}
|
||||
} catch {
|
||||
toastError("Error de red");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelText() {
|
||||
if (descEl) {
|
||||
descEl.textContent = description;
|
||||
}
|
||||
editingText = false;
|
||||
}
|
||||
function cancelText() {
|
||||
if (descEl) {
|
||||
descEl.textContent = description;
|
||||
}
|
||||
editingText = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<li class="task" class:completed>
|
||||
<div class="left">
|
||||
<span class="code">#{codeStr}</span>
|
||||
<span
|
||||
class="desc"
|
||||
class:editing={editingText}
|
||||
class:completed
|
||||
contenteditable={editingText && !completed}
|
||||
role="textbox"
|
||||
aria-label="Descripción de la tarea"
|
||||
spellcheck="true"
|
||||
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}</span
|
||||
>
|
||||
</div>
|
||||
<div class="code">#{codeStr}</div>
|
||||
<div
|
||||
class="desc"
|
||||
class:editing={editingText}
|
||||
class:completed
|
||||
contenteditable={editingText && !completed}
|
||||
role="textbox"
|
||||
aria-label="Descripción de la tarea"
|
||||
spellcheck="true"
|
||||
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">
|
||||
{#if due_date}
|
||||
<span
|
||||
class="date-badge"
|
||||
class:overdue
|
||||
class:soon={imminent}
|
||||
title={overdue ? "Vencida" : imminent ? "Próxima" : "Fecha"}
|
||||
>
|
||||
📅 {dateDmy}{#if overdue}
|
||||
⚠{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if assignees?.length}
|
||||
<div class="assignees">
|
||||
{#each assignees as a}
|
||||
<span class="assignee" title={a}>👤</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
{#if !completed}
|
||||
{#if !isAssigned}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Reclamar"
|
||||
on:click|preventDefault={doClaim}
|
||||
disabled={busy}>Reclamar</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Soltar"
|
||||
title="Soltar"
|
||||
on:click|preventDefault={doUnassign}
|
||||
disabled={busy}>🫳</button
|
||||
>
|
||||
{/if}
|
||||
<div class="meta">
|
||||
{#if due_date}
|
||||
<span
|
||||
class="date-badge"
|
||||
class:overdue
|
||||
class:soon={imminent}
|
||||
title={overdue ? "Vencida" : imminent ? "Próxima" : "Fecha"}
|
||||
>
|
||||
📅 {dateDmy}{#if overdue}
|
||||
⚠{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if assignees?.length}
|
||||
<div class="assignees">
|
||||
{#each assignees as a}
|
||||
<span class="assignee" title={a}>👤</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
{#if !completed}
|
||||
{#if !isAssigned}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Reclamar"
|
||||
on:click|preventDefault={doClaim}
|
||||
disabled={busy}>Reclamar</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Soltar"
|
||||
title="Soltar"
|
||||
on:click|preventDefault={doUnassign}
|
||||
disabled={busy}>🫳</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Completar"
|
||||
title="Completar"
|
||||
on:click|preventDefault={doComplete}
|
||||
disabled={busy}>✅</button
|
||||
>
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Completar"
|
||||
title="Completar"
|
||||
on:click|preventDefault={doComplete}
|
||||
disabled={busy}>✅</button
|
||||
>
|
||||
|
||||
{#if !editingText}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Editar texto"
|
||||
title="Editar texto"
|
||||
on:click|preventDefault={toggleEditText}
|
||||
disabled={busy}>✍️</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click|preventDefault={saveText}
|
||||
disabled={busy}>Guardar</button
|
||||
>
|
||||
<button
|
||||
class="btn ghost"
|
||||
on:click|preventDefault={cancelText}
|
||||
disabled={busy}>Cancelar</button
|
||||
>
|
||||
{/if}
|
||||
{#if !editingText}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Editar texto"
|
||||
title="Editar texto"
|
||||
on:click|preventDefault={toggleEditText}
|
||||
disabled={busy}>✍️</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click|preventDefault={saveText}
|
||||
disabled={busy}>Guardar</button
|
||||
>
|
||||
<button
|
||||
class="btn ghost"
|
||||
on:click|preventDefault={cancelText}
|
||||
disabled={busy}>Cancelar</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if !editing}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Editar fecha"
|
||||
title="Editar fecha"
|
||||
on:click|preventDefault={toggleEdit}
|
||||
disabled={busy}>🗓️</button
|
||||
>
|
||||
{:else}
|
||||
<input class="date" type="date" bind:value={dateValue} />
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click|preventDefault={saveDate}
|
||||
disabled={busy}>Guardar</button
|
||||
>
|
||||
<button
|
||||
class="btn danger"
|
||||
on:click|preventDefault={clearDate}
|
||||
disabled={busy}>Quitar</button
|
||||
>
|
||||
<button
|
||||
class="btn ghost"
|
||||
on:click|preventDefault={toggleEdit}
|
||||
disabled={busy}>Cancelar</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if !editing}
|
||||
<button
|
||||
class="icon-btn"
|
||||
aria-label="Editar fecha"
|
||||
title="Editar fecha"
|
||||
on:click|preventDefault={toggleEdit}
|
||||
disabled={busy}>🗓️</button
|
||||
>
|
||||
{:else}
|
||||
<input class="date" type="date" bind:value={dateValue} />
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click|preventDefault={saveDate}
|
||||
disabled={busy}>Guardar</button
|
||||
>
|
||||
<button
|
||||
class="btn danger"
|
||||
on:click|preventDefault={clearDate}
|
||||
disabled={busy}>Quitar</button
|
||||
>
|
||||
<button
|
||||
class="btn ghost"
|
||||
on:click|preventDefault={toggleEdit}
|
||||
disabled={busy}>Cancelar</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.task {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-template-rows: max-content max-content;
|
||||
grid-gap: 2px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
position: relative;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
.task:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
.code {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 2px;
|
||||
}
|
||||
.desc {
|
||||
padding: 8px;
|
||||
grid-column: 1/2;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
.task {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-template-rows: min-content max-content max-content;
|
||||
grid-gap: 2px;
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
position: relative;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
.task:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.code {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/2;
|
||||
}
|
||||
.completed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.desc {
|
||||
padding: 8px;
|
||||
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: 2/3;
|
||||
grid-row: 2/3;
|
||||
}
|
||||
.desc.completed {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.meta {
|
||||
justify-self: end;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.date-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 12px;
|
||||
}
|
||||
.date-badge.overdue {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
.date-badge.soon {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
.assignees {
|
||||
grid-column: 1/2;
|
||||
text-align: left;
|
||||
align-self: end;
|
||||
}
|
||||
.assignee {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.task.completed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.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 {
|
||||
justify-self: end;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
grid-row: 1/2;
|
||||
grid-column: 2/3;
|
||||
}
|
||||
.muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.date-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 12px;
|
||||
}
|
||||
.date-badge.overdue {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
.date-badge.soon {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
.assignees {
|
||||
grid-row: 3/4;
|
||||
grid-column: 1/2;
|
||||
text-align: left;
|
||||
align-self: end;
|
||||
}
|
||||
.assignee {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.task.completed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
justify-self: end;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
grid-column: 2/3;
|
||||
grid-row: 2/3;
|
||||
}
|
||||
.btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn[disabled] {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn.primary {
|
||||
border-color: transparent;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn.secondary {
|
||||
}
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
.btn.danger {
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
.icon-btn {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.date {
|
||||
padding: 4px 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.actions {
|
||||
display: block;
|
||||
gap: 6px;
|
||||
justify-self: end;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
grid-column: 2/3;
|
||||
grid-row: 3/4;
|
||||
}
|
||||
.btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn[disabled] {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn.primary {
|
||||
border-color: transparent;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn.secondary {
|
||||
}
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
.btn.danger {
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
.icon-btn {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.date {
|
||||
padding: 4px 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue