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>
220 lines
4.5 KiB
Svelte
220 lines
4.5 KiB
Svelte
<script>
|
|
import PropertyPills from './PropertyPills.svelte';
|
|
import FilterPills from './FilterPills.svelte';
|
|
import { activeFilter } from '$lib/stores/filters.js';
|
|
import { mergeInputWithFilter } from '$lib/utils/filters.js';
|
|
|
|
/**
|
|
* @type {(input: string) => Promise<void>}
|
|
*/
|
|
export let onSubmit;
|
|
|
|
export let error = '';
|
|
export let loading = false;
|
|
|
|
let value = '';
|
|
let focused = false;
|
|
|
|
/** @type {HTMLInputElement|null} */
|
|
let inputEl = null;
|
|
|
|
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
let blurTimer = null;
|
|
|
|
async function handleSubmit() {
|
|
const trimmed = value.trim();
|
|
if (!trimmed || loading) return;
|
|
|
|
const merged = mergeInputWithFilter(trimmed, $activeFilter || '');
|
|
|
|
try {
|
|
await onSubmit(merged);
|
|
value = '';
|
|
} catch {
|
|
// Value preserved for retry
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {KeyboardEvent} e
|
|
*/
|
|
function handleKeydown(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
}
|
|
|
|
function handleFocus() {
|
|
if (blurTimer) {
|
|
clearTimeout(blurTimer);
|
|
blurTimer = null;
|
|
}
|
|
focused = true;
|
|
}
|
|
|
|
function handleBlur() {
|
|
blurTimer = setTimeout(() => {
|
|
focused = false;
|
|
blurTimer = null;
|
|
}, 150);
|
|
}
|
|
|
|
/** @param {string} text */
|
|
function insertAtCursor(text) {
|
|
if (!inputEl) return;
|
|
const start = inputEl.selectionStart ?? value.length;
|
|
const end = inputEl.selectionEnd ?? value.length;
|
|
|
|
const needsSpace = start > 0 && value[start - 1] !== ' ';
|
|
const insert = (needsSpace ? ' ' : '') + text;
|
|
|
|
value = value.slice(0, start) + insert + value.slice(end);
|
|
|
|
const newPos = start + insert.length;
|
|
requestAnimationFrame(() => {
|
|
if (inputEl) {
|
|
inputEl.focus();
|
|
inputEl.setSelectionRange(newPos, newPos);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** @returns {string} */
|
|
export function getInputValue() {
|
|
return value;
|
|
}
|
|
|
|
/** @param {string} newValue */
|
|
export function setInputValue(newValue) {
|
|
value = newValue;
|
|
}
|
|
</script>
|
|
|
|
<div class="input-bar">
|
|
<PropertyPills visible={focused} onInsert={insertAtCursor} inputValue={value} onInputChange={(v) => { value = v; }} />
|
|
|
|
{#if error}
|
|
<div class="error">{error}</div>
|
|
{/if}
|
|
|
|
<div class="input-row" class:focused>
|
|
{#if $activeFilter}
|
|
<FilterPills />
|
|
<div class="separator"></div>
|
|
{/if}
|
|
<input
|
|
bind:this={inputEl}
|
|
bind:value
|
|
on:keydown={handleKeydown}
|
|
on:focus={handleFocus}
|
|
on:blur={handleBlur}
|
|
type="text"
|
|
placeholder={$activeFilter ? "Add task..." : "Add task... (e.g. Buy milk due:tomorrow priority:H)"}
|
|
disabled={loading}
|
|
class="input"
|
|
/>
|
|
<button
|
|
class="submit-btn"
|
|
on:click={handleSubmit}
|
|
disabled={!value.trim() || loading}
|
|
type="button"
|
|
aria-label="Add task"
|
|
>
|
|
{#if loading}
|
|
<span class="loading"></span>
|
|
{:else}
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.input-bar {
|
|
grid-area: input;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background-color: var(--bg-primary);
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.input-row {
|
|
display: flex;
|
|
align-items: stretch;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
background-color: var(--bg-secondary);
|
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
}
|
|
|
|
.input-row.focused {
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
|
}
|
|
|
|
.separator {
|
|
width: 1px;
|
|
align-self: stretch;
|
|
background-color: var(--border-color);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.input {
|
|
flex: 1;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: none;
|
|
border-radius: 0;
|
|
font-size: var(--font-size-base);
|
|
font-family: inherit;
|
|
background: transparent;
|
|
color: var(--text-primary);
|
|
min-width: 0;
|
|
}
|
|
|
|
.input::placeholder {
|
|
color: var(--text-tertiary);
|
|
}
|
|
|
|
.input:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.submit-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 44px;
|
|
height: 44px;
|
|
background-color: var(--color-primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 0 calc(var(--border-radius) - 1px) calc(var(--border-radius) - 1px) 0;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
min-width: 44px;
|
|
transition: background-color 0.15s;
|
|
}
|
|
|
|
.submit-btn:hover:not(:disabled) {
|
|
background-color: var(--color-primary-dark);
|
|
}
|
|
|
|
.submit-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.icon {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
}
|
|
|
|
.error {
|
|
padding: var(--spacing-xs) 0;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-danger);
|
|
}
|
|
</style>
|