8693681660
- Deduplicate API_BASE (was defined in both client.js and endpoints.js) - Extract EMPTY_STATE and persist() helper in auth store (DRY) - Extract updateByUuid() in tasks store, normalize to .map() pattern - Remove unused getQueueSize(), Select.svelte, and lib/index.js - Modernize uuid.js to prefer crypto.randomUUID() - Strip ~60 redundant comments that restated self-evident code No behavior changes. Build passes, pre-existing type errors unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1010 lines
25 KiB
Svelte
1010 lines
25 KiB
Svelte
<script>
|
|
import { formatDate, formatRelative, fromUnix, toUnix } from '$lib/utils/dates.js';
|
|
import { format } from 'date-fns';
|
|
import { tags as tagsAPI } from '$lib/api/endpoints.js';
|
|
|
|
/**
|
|
* @typedef {import('$lib/api/types.js').Task} Task
|
|
*/
|
|
|
|
/** @type {Task} */
|
|
export let task;
|
|
|
|
/** @type {(uuid: string, updates: Partial<Task>) => Promise<void>} */
|
|
export let onUpdate;
|
|
|
|
/** @type {(uuid: string) => Promise<void>} */
|
|
export let onStart;
|
|
|
|
/** @type {(uuid: string) => Promise<void>} */
|
|
export let onStop;
|
|
|
|
/** @type {(uuid: string) => void} */
|
|
export let onDelete;
|
|
|
|
/** @type {(uuid: string) => void} */
|
|
export let onComplete;
|
|
|
|
/** @type {() => void} */
|
|
export let onClose;
|
|
|
|
/** @type {string|null} */
|
|
let editingField = null;
|
|
|
|
// Edit values
|
|
let editDescription = '';
|
|
let editProject = '';
|
|
let editTagInput = '';
|
|
|
|
/** @type {'instance'|'template'|null} */
|
|
let recurringChoice = null;
|
|
|
|
$: isRecurringInstance = task.parent_uuid !== null && task.parent_uuid !== undefined;
|
|
$: isTemplate = task.status === 'R';
|
|
$: isCompleted = task.status === 'C';
|
|
$: isActive = task.start !== null && task.start !== undefined;
|
|
|
|
const statusLabels = /** @type {Record<string, string>} */ ({
|
|
P: 'Pending',
|
|
C: 'Completed',
|
|
D: 'Deleted',
|
|
R: 'Recurring'
|
|
});
|
|
|
|
const priorityLabels = /** @type {Record<number, string>} */ ({
|
|
3: 'High',
|
|
2: 'Medium',
|
|
1: 'Default',
|
|
0: 'Low'
|
|
});
|
|
|
|
const priorityCycle = /** @type {Record<number, number>} */ ({
|
|
3: 2,
|
|
2: 1,
|
|
1: 0,
|
|
0: 3
|
|
});
|
|
|
|
/** @param {number} ts @returns {string} */
|
|
function tsToDateValue(ts) {
|
|
return format(fromUnix(ts), 'yyyy-MM-dd');
|
|
}
|
|
|
|
/**
|
|
* @param {string} field
|
|
* @returns {boolean}
|
|
*/
|
|
function needsRecurringPrompt(field) {
|
|
if (!isRecurringInstance) return false;
|
|
if (recurringChoice) return false;
|
|
const editableOnBoth = ['description', 'priority', 'project', 'due', 'tags', 'scheduled', 'wait', 'until'];
|
|
return editableOnBoth.includes(field);
|
|
}
|
|
|
|
/**
|
|
* @param {'instance'|'template'} choice
|
|
*/
|
|
function handleRecurringChoice(choice) {
|
|
recurringChoice = choice;
|
|
showRecurringPrompt = false;
|
|
}
|
|
|
|
let showRecurringPrompt = false;
|
|
/** @type {string|null} */
|
|
let pendingEditField = null;
|
|
|
|
/**
|
|
* @param {string} field
|
|
*/
|
|
function startEdit(field) {
|
|
if (needsRecurringPrompt(field)) {
|
|
pendingEditField = field;
|
|
showRecurringPrompt = true;
|
|
return;
|
|
}
|
|
|
|
// Save current field if editing
|
|
if (editingField) saveCurrentEdit();
|
|
|
|
editingField = field;
|
|
|
|
if (field === 'description') {
|
|
editDescription = task.description;
|
|
} else if (field === 'project') {
|
|
editProject = task.project || '';
|
|
}
|
|
}
|
|
|
|
function saveCurrentEdit() {
|
|
if (!editingField) return;
|
|
editingField = null;
|
|
}
|
|
|
|
/** @returns {string} */
|
|
function getTargetUuid() {
|
|
if (recurringChoice === 'template' && task.parent_uuid) {
|
|
return task.parent_uuid;
|
|
}
|
|
return task.uuid;
|
|
}
|
|
|
|
async function saveDescription() {
|
|
const trimmed = editDescription.trim();
|
|
if (trimmed && trimmed !== task.description) {
|
|
await onUpdate(getTargetUuid(), { description: trimmed });
|
|
}
|
|
editingField = null;
|
|
}
|
|
|
|
async function saveProject() {
|
|
const trimmed = editProject.trim();
|
|
const newVal = trimmed || null;
|
|
if (newVal !== task.project) {
|
|
await onUpdate(getTargetUuid(), { project: newVal });
|
|
}
|
|
editingField = null;
|
|
}
|
|
|
|
async function cyclePriority() {
|
|
if (needsRecurringPrompt('priority')) {
|
|
pendingEditField = 'priority-cycle';
|
|
showRecurringPrompt = true;
|
|
return;
|
|
}
|
|
const next = priorityCycle[task.priority] ?? 1;
|
|
await onUpdate(getTargetUuid(), { priority: /** @type {import('$lib/api/types.js').TaskPriority} */ (next) });
|
|
}
|
|
|
|
/**
|
|
* @param {Event} e
|
|
* @param {string} field
|
|
*/
|
|
async function handleDateChange(e, field) {
|
|
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
const value = input.value;
|
|
if (value) {
|
|
const date = new Date(value + 'T00:00:00');
|
|
await onUpdate(getTargetUuid(), { [field]: toUnix(date) });
|
|
}
|
|
editingField = null;
|
|
}
|
|
|
|
/**
|
|
* @param {string} field
|
|
*/
|
|
async function clearDate(field) {
|
|
await onUpdate(getTargetUuid(), { [field]: null });
|
|
editingField = null;
|
|
}
|
|
|
|
/**
|
|
* @param {string} tag
|
|
*/
|
|
async function removeTag(tag) {
|
|
try {
|
|
await tagsAPI.remove(getTargetUuid(), tag);
|
|
task = { ...task, tags: task.tags.filter(t => t !== tag) };
|
|
} catch (error) {
|
|
console.error('Failed to remove tag:', error);
|
|
}
|
|
}
|
|
|
|
async function addTag() {
|
|
const tag = editTagInput.trim();
|
|
if (!tag) return;
|
|
editTagInput = '';
|
|
try {
|
|
await tagsAPI.add(getTargetUuid(), tag);
|
|
task = { ...task, tags: [...task.tags, tag] };
|
|
} catch (error) {
|
|
console.error('Failed to add tag:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {KeyboardEvent} e
|
|
*/
|
|
function handleTagKeydown(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
addTag();
|
|
} else if (e.key === 'Escape') {
|
|
editingField = null;
|
|
editTagInput = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {KeyboardEvent} e
|
|
*/
|
|
function handleDescriptionKeydown(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
saveDescription();
|
|
} else if (e.key === 'Escape') {
|
|
editingField = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {KeyboardEvent} e
|
|
*/
|
|
function handleProjectKeydown(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
saveProject();
|
|
} else if (e.key === 'Escape') {
|
|
editingField = null;
|
|
}
|
|
}
|
|
|
|
async function copyUuid() {
|
|
try {
|
|
await navigator.clipboard.writeText(task.uuid);
|
|
} catch {
|
|
// Fallback: do nothing
|
|
}
|
|
}
|
|
|
|
$: if (recurringChoice && pendingEditField) {
|
|
const field = pendingEditField;
|
|
pendingEditField = null;
|
|
if (field === 'priority-cycle') {
|
|
const next = priorityCycle[task.priority] ?? 1;
|
|
onUpdate(getTargetUuid(), { priority: /** @type {import('$lib/api/types.js').TaskPriority} */ (next) });
|
|
} else {
|
|
startEdit(field);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="task-detail">
|
|
<!-- Recurring instance prompt -->
|
|
{#if showRecurringPrompt}
|
|
<div class="recurring-prompt">
|
|
<button class="recurring-option" on:click={() => handleRecurringChoice('instance')} type="button">
|
|
Edit this instance only
|
|
</button>
|
|
<button class="recurring-option" on:click={() => handleRecurringChoice('template')} type="button">
|
|
Edit template (all future)
|
|
</button>
|
|
<button class="recurring-option recurring-cancel" on:click={() => { showRecurringPrompt = false; pendingEditField = null; }} type="button">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Description -->
|
|
<div class="description-section">
|
|
{#if editingField === 'description'}
|
|
<div class="description-edit">
|
|
<textarea
|
|
bind:value={editDescription}
|
|
on:keydown={handleDescriptionKeydown}
|
|
class="description-input"
|
|
rows="2"
|
|
></textarea>
|
|
<div class="edit-actions">
|
|
<button class="edit-btn save" on:click={saveDescription} type="button" aria-label="Save">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="18" height="18">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</button>
|
|
<button class="edit-btn cancel" on:click={() => editingField = null} type="button" aria-label="Cancel">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="18" height="18">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<div class="description-display" on:click={() => startEdit('description')}>
|
|
<h2 class="description-text">{task.description}</h2>
|
|
<button class="edit-icon-btn" type="button" aria-label="Edit description">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Editable fields -->
|
|
<div class="fields">
|
|
<!-- Status -->
|
|
<div class="field-row">
|
|
<span class="field-label">Status</span>
|
|
<span class="field-value">
|
|
<span class="status-badge status-{task.status.toLowerCase()}">{statusLabels[task.status] || task.status}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Priority -->
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<div class="field-row editable" on:click={cyclePriority}>
|
|
<span class="field-label">Priority</span>
|
|
<span class="field-value">
|
|
<span class="priority-dot priority-{task.priority}"></span>
|
|
{priorityLabels[task.priority] || 'Default'}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Project -->
|
|
<div class="field-row" class:editable={editingField !== 'project'}>
|
|
<span class="field-label">Project</span>
|
|
{#if editingField === 'project'}
|
|
<input
|
|
class="field-input"
|
|
type="text"
|
|
bind:value={editProject}
|
|
on:keydown={handleProjectKeydown}
|
|
on:blur={saveProject}
|
|
placeholder="Project name"
|
|
/>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<span class="field-value clickable" on:click={() => startEdit('project')}>
|
|
{task.project || 'Add...'}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Due -->
|
|
{#if task.due || editingField === 'due'}
|
|
<div class="field-row" class:editable={editingField !== 'due'}>
|
|
<span class="field-label">Due</span>
|
|
{#if editingField === 'due'}
|
|
<div class="date-edit">
|
|
<input
|
|
class="field-input date-input"
|
|
type="date"
|
|
value={task.due ? tsToDateValue(task.due) : ''}
|
|
on:change={(e) => handleDateChange(e, 'due')}
|
|
/>
|
|
<button class="clear-btn" on:click={() => clearDate('due')} type="button">Clear</button>
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<span class="field-value clickable" on:click={() => startEdit('due')}>
|
|
{formatRelative(task.due)} ({formatDate(task.due)})
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<div class="field-row editable" on:click={() => startEdit('due')}>
|
|
<span class="field-label">Due</span>
|
|
<span class="field-value clickable">Set...</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Scheduled -->
|
|
{#if task.scheduled || editingField === 'scheduled'}
|
|
<div class="field-row" class:editable={editingField !== 'scheduled'}>
|
|
<span class="field-label">Scheduled</span>
|
|
{#if editingField === 'scheduled'}
|
|
<div class="date-edit">
|
|
<input
|
|
class="field-input date-input"
|
|
type="date"
|
|
value={task.scheduled ? tsToDateValue(task.scheduled) : ''}
|
|
on:change={(e) => handleDateChange(e, 'scheduled')}
|
|
/>
|
|
<button class="clear-btn" on:click={() => clearDate('scheduled')} type="button">Clear</button>
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<span class="field-value clickable" on:click={() => startEdit('scheduled')}>
|
|
{formatRelative(task.scheduled)} ({formatDate(task.scheduled)})
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Wait -->
|
|
{#if task.wait || editingField === 'wait'}
|
|
<div class="field-row" class:editable={editingField !== 'wait'}>
|
|
<span class="field-label">Wait</span>
|
|
{#if editingField === 'wait'}
|
|
<div class="date-edit">
|
|
<input
|
|
class="field-input date-input"
|
|
type="date"
|
|
value={task.wait ? tsToDateValue(task.wait) : ''}
|
|
on:change={(e) => handleDateChange(e, 'wait')}
|
|
/>
|
|
<button class="clear-btn" on:click={() => clearDate('wait')} type="button">Clear</button>
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<span class="field-value clickable" on:click={() => startEdit('wait')}>
|
|
{formatRelative(task.wait)} ({formatDate(task.wait)})
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Until -->
|
|
{#if task.until || editingField === 'until'}
|
|
<div class="field-row" class:editable={editingField !== 'until'}>
|
|
<span class="field-label">Until</span>
|
|
{#if editingField === 'until'}
|
|
<div class="date-edit">
|
|
<input
|
|
class="field-input date-input"
|
|
type="date"
|
|
value={task.until ? tsToDateValue(task.until) : ''}
|
|
on:change={(e) => handleDateChange(e, 'until')}
|
|
/>
|
|
<button class="clear-btn" on:click={() => clearDate('until')} type="button">Clear</button>
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<span class="field-value clickable" on:click={() => startEdit('until')}>
|
|
{formatRelative(task.until)} ({formatDate(task.until)})
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Active since -->
|
|
{#if isActive}
|
|
<div class="field-row">
|
|
<span class="field-label">Active since</span>
|
|
<span class="field-value">{formatRelative(task.start)}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Recurrence -->
|
|
{#if task.recurrence_duration}
|
|
<div class="field-row">
|
|
<span class="field-label">Recurrence</span>
|
|
<span class="field-value">{task.recurrence_duration}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Parent -->
|
|
{#if isRecurringInstance}
|
|
<div class="field-row">
|
|
<span class="field-label">Parent</span>
|
|
<span class="field-value muted">Recurring instance</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Tags -->
|
|
<div class="field-row tags-row">
|
|
<span class="field-label">Tags</span>
|
|
<div class="tags-container">
|
|
{#each task.tags as tag}
|
|
<span class="tag-pill">
|
|
{tag}
|
|
<button class="tag-remove" on:click={() => removeTag(tag)} type="button" aria-label="Remove tag {tag}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="12" height="12">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
{/each}
|
|
{#if editingField === 'tags'}
|
|
<input
|
|
class="tag-input"
|
|
type="text"
|
|
bind:value={editTagInput}
|
|
on:keydown={handleTagKeydown}
|
|
on:blur={() => { addTag(); editingField = null; }}
|
|
placeholder="tag name"
|
|
/>
|
|
{:else}
|
|
<button class="tag-add" on:click={() => startEdit('tags')} type="button">
|
|
+ Add
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Read-only metadata -->
|
|
<div class="fields metadata">
|
|
<div class="field-row">
|
|
<span class="field-label">Created</span>
|
|
<span class="field-value muted">{formatDate(task.created)}</span>
|
|
</div>
|
|
<div class="field-row">
|
|
<span class="field-label">Modified</span>
|
|
<span class="field-value muted">{formatDate(task.modified)}</span>
|
|
</div>
|
|
{#if task.end}
|
|
<div class="field-row">
|
|
<span class="field-label">End</span>
|
|
<span class="field-value muted">{formatDate(task.end)}</span>
|
|
</div>
|
|
{/if}
|
|
{#if task.urgency > 0}
|
|
<div class="field-row">
|
|
<span class="field-label">Urgency</span>
|
|
<span class="field-value muted">{task.urgency.toFixed(1)}</span>
|
|
</div>
|
|
{/if}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
<div class="field-row editable" on:click={copyUuid}>
|
|
<span class="field-label">UUID</span>
|
|
<span class="field-value muted uuid">{task.uuid.substring(0, 8)}...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="actions">
|
|
{#if isCompleted}
|
|
<button class="action-btn action-primary" on:click={() => onUpdate(task.uuid, { status: 'P', end: null })} type="button">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
|
</svg>
|
|
Uncomplete
|
|
</button>
|
|
{:else}
|
|
{#if isActive}
|
|
<button class="action-btn action-primary" on:click={() => onStop(task.uuid)} type="button">
|
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
|
<rect x="6" y="6" width="12" height="12" rx="1" fill="currentColor" />
|
|
</svg>
|
|
Stop
|
|
</button>
|
|
{:else}
|
|
<button class="action-btn action-primary" on:click={() => onStart(task.uuid)} type="button">
|
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
|
<polygon points="6,4 20,12 6,20" fill="currentColor" />
|
|
</svg>
|
|
Start
|
|
</button>
|
|
{/if}
|
|
|
|
<button class="action-btn action-success" on:click={() => { onComplete(task.uuid); onClose(); }} type="button">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Complete
|
|
</button>
|
|
{/if}
|
|
|
|
<button class="action-btn action-danger" on:click={() => onDelete(task.uuid)} type="button">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.task-detail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
/* Description section */
|
|
.description-section {
|
|
padding: var(--spacing-xs) 0;
|
|
}
|
|
|
|
.description-display {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--spacing-sm);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.description-text {
|
|
flex: 1;
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
word-break: break-word;
|
|
margin: 0;
|
|
}
|
|
|
|
.edit-icon-btn {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
min-width: 32px;
|
|
min-height: 32px;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
color: var(--text-tertiary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.edit-icon-btn:hover {
|
|
color: var(--text-secondary);
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.description-edit {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.description-input {
|
|
width: 100%;
|
|
padding: var(--spacing-sm);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
font-size: var(--font-size-lg);
|
|
font-family: inherit;
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
resize: vertical;
|
|
min-height: 44px;
|
|
}
|
|
|
|
.description-input:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
|
}
|
|
|
|
.edit-actions {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.edit-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
min-width: 32px;
|
|
min-height: 32px;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
background: none;
|
|
}
|
|
|
|
.edit-btn.save {
|
|
color: var(--color-success);
|
|
}
|
|
|
|
.edit-btn.cancel {
|
|
color: var(--text-tertiary);
|
|
}
|
|
|
|
/* Divider */
|
|
.divider {
|
|
height: 1px;
|
|
background-color: var(--border-color);
|
|
margin: var(--spacing-xs) 0;
|
|
}
|
|
|
|
/* Fields */
|
|
.fields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.field-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: var(--spacing-sm) 0;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.field-row.editable {
|
|
cursor: pointer;
|
|
border-radius: 0.25rem;
|
|
margin: 0 calc(-1 * var(--spacing-xs));
|
|
padding-left: var(--spacing-xs);
|
|
padding-right: var(--spacing-xs);
|
|
}
|
|
|
|
.field-row.editable:hover {
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.field-label {
|
|
width: 6rem;
|
|
flex-shrink: 0;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.field-value {
|
|
flex: 1;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.field-value.clickable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.field-value.muted {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.field-value.uuid {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.field-input {
|
|
flex: 1;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 0.25rem;
|
|
font-size: var(--font-size-sm);
|
|
font-family: inherit;
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
min-height: 36px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.field-input:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
|
}
|
|
|
|
/* Date editing */
|
|
.date-edit {
|
|
flex: 1;
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
align-items: center;
|
|
}
|
|
|
|
.date-input {
|
|
flex: 1;
|
|
}
|
|
|
|
.clear-btn {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: var(--spacing-xs);
|
|
min-width: unset;
|
|
min-height: unset;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.clear-btn:hover {
|
|
color: var(--color-danger);
|
|
}
|
|
|
|
/* Status badge */
|
|
.status-badge {
|
|
font-size: var(--font-size-xs);
|
|
padding: 0.125rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-p {
|
|
background-color: var(--color-due-bg);
|
|
color: var(--color-due-text);
|
|
}
|
|
|
|
.status-c {
|
|
background-color: rgba(var(--color-success), 0.15);
|
|
color: var(--color-success);
|
|
}
|
|
|
|
.status-r {
|
|
background-color: var(--color-project-bg);
|
|
color: var(--color-project-text);
|
|
}
|
|
|
|
/* Priority dot */
|
|
.priority-dot {
|
|
display: inline-block;
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
border-radius: 50%;
|
|
margin-right: var(--spacing-xs);
|
|
}
|
|
|
|
.priority-3 {
|
|
background-color: var(--color-priority-high-text);
|
|
}
|
|
|
|
.priority-2 {
|
|
background-color: var(--color-priority-medium-text);
|
|
}
|
|
|
|
.priority-1 {
|
|
background-color: var(--text-secondary);
|
|
}
|
|
|
|
.priority-0 {
|
|
background-color: var(--text-tertiary);
|
|
}
|
|
|
|
/* Tags */
|
|
.tags-row {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.tags-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-xs);
|
|
align-items: center;
|
|
}
|
|
|
|
.tag-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: var(--font-size-xs);
|
|
padding: 0.125rem 0.5rem;
|
|
background-color: var(--color-tag-bg);
|
|
color: var(--color-tag-text);
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.tag-remove {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
min-width: unset;
|
|
min-height: unset;
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-tertiary);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.tag-remove:hover {
|
|
color: var(--color-danger);
|
|
}
|
|
|
|
.tag-add {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--color-primary);
|
|
background: none;
|
|
border: 1px dashed var(--border-color);
|
|
border-radius: 0.25rem;
|
|
padding: 0.125rem 0.5rem;
|
|
cursor: pointer;
|
|
min-width: unset;
|
|
min-height: unset;
|
|
}
|
|
|
|
.tag-add:hover {
|
|
border-color: var(--color-primary);
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.tag-input {
|
|
padding: 0.125rem 0.5rem;
|
|
border: 1px solid var(--color-primary);
|
|
border-radius: 0.25rem;
|
|
font-size: var(--font-size-xs);
|
|
font-family: inherit;
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
min-height: unset;
|
|
min-width: 80px;
|
|
max-width: 120px;
|
|
}
|
|
|
|
.tag-input:focus {
|
|
outline: none;
|
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
|
}
|
|
|
|
/* Metadata section */
|
|
.metadata .field-row {
|
|
min-height: 32px;
|
|
padding: var(--spacing-xs) 0;
|
|
}
|
|
|
|
/* Recurring prompt */
|
|
.recurring-prompt {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-sm);
|
|
background-color: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.recurring-option {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: none;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
font-size: var(--font-size-sm);
|
|
font-family: inherit;
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
min-height: 40px;
|
|
min-width: unset;
|
|
}
|
|
|
|
.recurring-option:hover {
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.recurring-cancel {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Actions */
|
|
.actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
padding: var(--spacing-sm) 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: none;
|
|
border-radius: var(--border-radius);
|
|
font-size: var(--font-size-sm);
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
min-height: 40px;
|
|
min-width: unset;
|
|
}
|
|
|
|
.action-primary {
|
|
background-color: var(--color-primary);
|
|
color: white;
|
|
}
|
|
|
|
.action-primary:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.action-success {
|
|
background-color: var(--color-success);
|
|
color: white;
|
|
}
|
|
|
|
.action-success:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.action-danger {
|
|
background: none;
|
|
color: var(--color-danger);
|
|
}
|
|
|
|
.action-danger:hover {
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
</style>
|