general ui patches
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { ViewHeader, CardGrid, Card } from '$lib/components';
|
||||
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
|
||||
import { formatHumanDate } from '$lib/format';
|
||||
import { TEMPLATE_COLORS, ICON_NAMES, accentBg, outlineColor } from '$lib/templateIcons';
|
||||
import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types';
|
||||
|
||||
// ── Chevron SVG icons ──
|
||||
@@ -25,6 +26,10 @@
|
||||
// Assign-from-template modal (platform templates are copied into the family
|
||||
// as assigned chores; the fam admin amends before saving).
|
||||
let assignModal = $state<{ t: ChoreTemplate; memberId: string } | null>(null);
|
||||
let assignName = $state('');
|
||||
let assignDescription = $state('');
|
||||
let assignColor = $state('');
|
||||
let assignIcon = $state('');
|
||||
let assignFreq = $state('daily');
|
||||
let assignType = $state('points');
|
||||
let assignValue = $state(10);
|
||||
@@ -50,6 +55,9 @@
|
||||
let editType = $state('points');
|
||||
let editValue = $state(10);
|
||||
let editCustomName = $state('');
|
||||
let editDescription = $state('');
|
||||
let editColor = $state('');
|
||||
let editIcon = $state('');
|
||||
let editEmoji = $state('🎉');
|
||||
let editTodoCompleteBy = $state('');
|
||||
let editTodoStartDate = $state('');
|
||||
@@ -73,6 +81,8 @@
|
||||
);
|
||||
|
||||
let seasons = $state(famStore.initialized ? famStore.seasons : data.seasons || []);
|
||||
// Only active seasons are selectable for assignment / filtering.
|
||||
let activeSeasons = $derived((seasons || []).filter((s) => s.active !== false));
|
||||
let completions = $state((data.completions as Completion[]) || []);
|
||||
|
||||
let filteredAssigned = $derived(
|
||||
@@ -104,6 +114,10 @@
|
||||
return seasons.find((s) => s.id === id)?.name || id;
|
||||
}
|
||||
|
||||
function seasonColor(id: string): string {
|
||||
return seasons.find((s) => s.id === id)?.color || '#9ca3af';
|
||||
}
|
||||
|
||||
function seasonNames(a: AssignedChore): string {
|
||||
return (a.seasonIds || []).map((id) => seasonName(id)).join(', ') || 'Global';
|
||||
}
|
||||
@@ -133,6 +147,10 @@
|
||||
const t = templates.find((x) => x.id === tid);
|
||||
if (!t) return;
|
||||
assignModal = { t, memberId };
|
||||
assignName = t.name || '';
|
||||
assignDescription = t.description || '';
|
||||
assignColor = t.color || '';
|
||||
assignIcon = t.icon || '';
|
||||
assignFreq = t.defaultFrequency;
|
||||
assignType = t.defaultType;
|
||||
assignValue = t.defaultValue;
|
||||
@@ -146,6 +164,10 @@
|
||||
const body: Record<string, unknown> = {
|
||||
memberId: assignModal.memberId,
|
||||
templateId: assignModal.t.id,
|
||||
customName: assignName,
|
||||
description: assignDescription,
|
||||
color: assignColor,
|
||||
icon: assignIcon,
|
||||
frequency: assignFreq,
|
||||
type: assignType,
|
||||
value: assignValue
|
||||
@@ -227,6 +249,9 @@
|
||||
editType = a.type;
|
||||
editValue = a.value;
|
||||
editCustomName = a.customName || '';
|
||||
editDescription = a.description || '';
|
||||
editColor = a.color || '';
|
||||
editIcon = a.icon || '';
|
||||
editEmoji = a.emoji || '🎉';
|
||||
editSeasonIds = a.seasonIds ? [...a.seasonIds] : [];
|
||||
editTodoCompleteBy = a.completeBy || '';
|
||||
@@ -239,7 +264,14 @@
|
||||
editingAssignment = null;
|
||||
}
|
||||
|
||||
function enhanceUpdateAssigned() {
|
||||
function enhanceUpdateAssigned(e: any) {
|
||||
const data = e?.data;
|
||||
if (data && editingAssignment && !editingAssignment.isTodo) {
|
||||
data.set('seasonIds', editSeasonIds.join(','));
|
||||
data.set('color', editColor);
|
||||
data.set('icon', editIcon);
|
||||
data.set('description', editDescription);
|
||||
}
|
||||
return async ({ result }: any) => {
|
||||
if (result.type === 'success' && editingAssignment) {
|
||||
const idx = assigned.findIndex((a: any) => a.id === editingAssignment!.id);
|
||||
@@ -283,7 +315,7 @@
|
||||
<label>Season:</label>
|
||||
<select bind:value={seasonFilter}>
|
||||
<option value="all">All Chores</option>
|
||||
{#each seasons as s}
|
||||
{#each activeSeasons as s}
|
||||
<option value={s.id}>{s.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -291,9 +323,14 @@
|
||||
</div>
|
||||
<div class="template-list">
|
||||
{#each templates as t}
|
||||
<div class="card template" draggable="true" ondragstart={(e) => handleDragStart(e, t.id)} style="border-left-color:{t.color}">
|
||||
<div
|
||||
class="card template"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleDragStart(e, t.id)}
|
||||
style="border-left-color:{outlineColor(t.color)}; background:{accentBg(t.color)}"
|
||||
>
|
||||
<div class="card-body">
|
||||
<TemplateIcon name={t.icon} size={18} color={t.color} />
|
||||
<TemplateIcon name={t.icon} size={18} color={outlineColor(t.color)} />
|
||||
<strong>{t.name}</strong>
|
||||
<span class="badge">{t.defaultFrequency}</span>
|
||||
<span class="badge type">{t.defaultType}</span>
|
||||
@@ -407,13 +444,25 @@
|
||||
<div class="accordion-body">
|
||||
{#each assignedForMember(m.id) as a}
|
||||
{@const tName = templateName(a.templateId)}
|
||||
<div class="card assigned" onclick={() => openEdit(a)} role="button" tabindex="0">
|
||||
<div
|
||||
class="card assigned"
|
||||
onclick={() => openEdit(a)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
style="border-left-color:{outlineColor(a.color)}; background:{accentBg(a.color)}"
|
||||
>
|
||||
<div class="card-body">
|
||||
<TemplateIcon name={a.icon} size={16} color={outlineColor(a.color)} />
|
||||
<strong>{a.customName || tName}</strong>
|
||||
<span class="badge">{a.frequency}</span>
|
||||
<span class="badge type">{a.type}</span>
|
||||
{#if seasonFilter !== 'all' && isGlobalChore(a)}
|
||||
<span class="badge season">Add to</span>
|
||||
{#each a.seasonIds || [] as sid}
|
||||
<span class="badge season" style="border-color:{seasonColor(sid)}; color:{seasonColor(sid)}"
|
||||
>{seasonName(sid)}</span
|
||||
>
|
||||
{/each}
|
||||
{#if (a.seasonIds || []).length === 0}
|
||||
<span class="badge season muted">All year</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card-value">
|
||||
@@ -451,15 +500,67 @@
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
<!-- Season pill selector (reused by assign + edit modals) -->
|
||||
{#snippet seasonPills(list: Season[], selected: string[], set: (v: string[]) => void)}
|
||||
<div class="season-pills">
|
||||
<button
|
||||
type="button"
|
||||
class="season-pill"
|
||||
class:selected={selected.length === 0}
|
||||
style="--pill:#9ca3af"
|
||||
onclick={() => set([])}>All year</button
|
||||
>
|
||||
{#each list as s}
|
||||
<button
|
||||
type="button"
|
||||
class="season-pill"
|
||||
class:selected={selected[0] === s.id}
|
||||
style="--pill:{s.color}"
|
||||
onclick={() => set([s.id])}>{s.name}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Assign from Template Modal -->
|
||||
{#if assignModal}
|
||||
<div class="overlay" onclick={() => (assignModal = null)} role="presentation">
|
||||
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) assignModal = null; }} role="presentation">
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||
<h3>Assign: {assignModal.t.name}</h3>
|
||||
<p class="hint">
|
||||
Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)?.name}'s
|
||||
chores. Amend, then Assign.
|
||||
</p>
|
||||
<label>Name <input bind:value={assignName} placeholder="Task name" /></label>
|
||||
<label
|
||||
>Description <input bind:value={assignDescription} placeholder="Optional description" /></label
|
||||
>
|
||||
<div class="color-field">
|
||||
<span class="field-label">Colour</span>
|
||||
<div class="color-swatches">
|
||||
{#each TEMPLATE_COLORS as c}
|
||||
<button
|
||||
type="button"
|
||||
class="swatch"
|
||||
class:selected={assignColor === c.value}
|
||||
style="background:{c.value || 'transparent'}; border-color:{c.chromatic ===
|
||||
'transparent'
|
||||
? '#e5e7eb'
|
||||
: c.value}"
|
||||
title={c.label}
|
||||
onclick={() => (assignColor = c.value)}></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<label class="icon-row"
|
||||
><span class="icon-preview"
|
||||
><TemplateIcon name={assignIcon} size={22} color={outlineColor(assignColor)} /></span
|
||||
>
|
||||
<select bind:value={assignIcon}>
|
||||
<option value="">None</option>
|
||||
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Frequency
|
||||
<select bind:value={assignFreq}>
|
||||
@@ -478,6 +579,10 @@
|
||||
Value
|
||||
<input type="number" step="any" bind:value={assignValue} />
|
||||
</label>
|
||||
<div class="season-field">
|
||||
<span class="field-label">Seasons</span>
|
||||
<seasonPills activeSeasons selected={assignSeasonIds} set={(v: string[]) => (assignSeasonIds = v)} />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" onclick={() => (assignModal = null)}>Cancel</button>
|
||||
<button type="button" onclick={assignChore}>Assign</button>
|
||||
@@ -488,7 +593,7 @@
|
||||
|
||||
<!-- Edit Assignment Modal -->
|
||||
{#if showEditModal && editingAssignment}
|
||||
<div class="overlay" onclick={closeEdit} role="presentation">
|
||||
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) closeEdit(); }} role="presentation">
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||
<h3>
|
||||
{#if editingAssignment.isTodo}
|
||||
@@ -504,6 +609,10 @@
|
||||
Name
|
||||
<input name="customName" bind:value={editCustomName} placeholder="Name" />
|
||||
</label>
|
||||
<label
|
||||
>Description
|
||||
<input name="description" bind:value={editDescription} placeholder="Optional description" /></label
|
||||
>
|
||||
|
||||
{#if editingAssignment.isTodo}
|
||||
<label>
|
||||
@@ -561,6 +670,33 @@
|
||||
</label>
|
||||
{:else}
|
||||
<input name="seasonIds" type="hidden" value={editSeasonIds.join(',')} />
|
||||
<div class="color-field">
|
||||
<span class="field-label">Colour</span>
|
||||
<div class="color-swatches">
|
||||
{#each TEMPLATE_COLORS as c}
|
||||
<button
|
||||
type="button"
|
||||
class="swatch"
|
||||
class:selected={editColor === c.value}
|
||||
style="background:{c.value || 'transparent'}; border-color:{c.chromatic ===
|
||||
'transparent'
|
||||
? '#e5e7eb'
|
||||
: c.value}"
|
||||
title={c.label}
|
||||
onclick={() => (editColor = c.value)}></button>
|
||||
{/each}
|
||||
</div>
|
||||
<input type="hidden" name="color" value={editColor} />
|
||||
</div>
|
||||
<label
|
||||
><span class="icon-preview"
|
||||
><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span
|
||||
>
|
||||
<select name="icon" bind:value={editIcon}>
|
||||
<option value="">None</option>
|
||||
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Frequency
|
||||
<select name="frequency" bind:value={editFreq}>
|
||||
@@ -579,36 +715,10 @@
|
||||
Value
|
||||
<input name="value" type="number" step="any" bind:value={editValue} required />
|
||||
</label>
|
||||
<label>
|
||||
Seasons
|
||||
<div class="season-checklist">
|
||||
{#each seasons as s}
|
||||
<label class="check-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editSeasonIds.includes(s.id)}
|
||||
onchange={() => {
|
||||
if (editSeasonIds.includes(s.id)) {
|
||||
editSeasonIds = editSeasonIds.filter((x) => x !== s.id);
|
||||
} else {
|
||||
editSeasonIds = [...editSeasonIds, s.id];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span class="dot" style="background:{s.color}"></span>
|
||||
{s.name}
|
||||
</label>
|
||||
{/each}
|
||||
<label class="check-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editSeasonIds.length === 0}
|
||||
onchange={() => (editSeasonIds = [])}
|
||||
/>
|
||||
Global (no season)
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
<div class="season-field">
|
||||
<span class="field-label">Seasons</span>
|
||||
<seasonPills activeSeasons selected={editSeasonIds} set={(v: string[]) => (editSeasonIds = v)} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-actions">
|
||||
<button type="button" onclick={closeEdit}>Cancel</button>
|
||||
@@ -621,7 +731,7 @@
|
||||
|
||||
<!-- Add Todo Modal -->
|
||||
{#if showTodoModal && todoMemberId}
|
||||
<div class="overlay" onclick={() => (showTodoModal = false)} role="presentation">
|
||||
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) showTodoModal = false; }} role="presentation">
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||
<h3>Add Todo for {members.find((m) => m.id === todoMemberId)?.name || 'Member'}</h3>
|
||||
<form
|
||||
@@ -941,9 +1051,14 @@
|
||||
background: #fff;
|
||||
}
|
||||
.badge.season {
|
||||
background: #e0f2fe;
|
||||
background: transparent;
|
||||
border: 1px solid #0369a1;
|
||||
color: #0369a1;
|
||||
}
|
||||
.badge.season.muted {
|
||||
color: #9ca3af;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
.card-meta {
|
||||
font-size: 0.7rem;
|
||||
color: #9ca3af;
|
||||
@@ -971,6 +1086,76 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.color-field,
|
||||
.season-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.field-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
.modal label.icon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.modal label.icon-row select {
|
||||
flex: 1;
|
||||
}
|
||||
.icon-preview {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
.color-swatches {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.swatch {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.swatch.selected {
|
||||
outline: 2px solid #111827;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.season-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.season-pill {
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--pill, #e5e7eb);
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
.season-pill.selected {
|
||||
background: var(--pill, #8b5cf6);
|
||||
color: #fff;
|
||||
border-color: var(--pill, #8b5cf6);
|
||||
}
|
||||
|
||||
.template {
|
||||
cursor: grab;
|
||||
border-left: 4px solid #8b5cf6;
|
||||
|
||||
Reference in New Issue
Block a user