Files
gems/opal-web/src/lib/components/TaskDetail.svelte
T
joakim 8693681660 refactor: clean up opal-web duplication, dead code, and comment noise
- 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>
2026-02-21 01:03:08 +01:00

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>