1359 lines
38 KiB
Svelte
1359 lines
38 KiB
Svelte
<script lang="ts">
|
|
import type { PageData } from './$types';
|
|
import { page } from '$app/state';
|
|
import { enhance } from '$app/forms';
|
|
import { famStore } from '$lib/stores/fam.svelte';
|
|
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
|
|
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
|
|
import { resolveTz, todayInTz, addDaysStr } from '@shared/timezone';
|
|
import { chevronRight } from '$lib/components/icons';
|
|
import Clock from '@lucide/svelte/icons/clock';
|
|
import Tag from '@lucide/svelte/icons/tag';
|
|
import UsersRound from '@lucide/svelte/icons/users-round';
|
|
import Target from '@lucide/svelte/icons/target';
|
|
import Settings2 from '@lucide/svelte/icons/settings-2';
|
|
import CalendarDays from '@lucide/svelte/icons/calendar-days';
|
|
import Coins from '@lucide/svelte/icons/coins';
|
|
import Gift from '@lucide/svelte/icons/gift';
|
|
import PoundSterling from '@lucide/svelte/icons/pound-sterling';
|
|
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
|
import Save from '@lucide/svelte/icons/save';
|
|
import Trash2 from '@lucide/svelte/icons/trash-2';
|
|
import Crown from '@lucide/svelte/icons/crown';
|
|
import Ban from '@lucide/svelte/icons/ban';
|
|
import type {
|
|
BonusConfig,
|
|
BonusTemplate,
|
|
Member,
|
|
BonusConfigWithProgress,
|
|
BonusProgress,
|
|
Reward
|
|
} from '$lib/types';
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
// Today in the family's timezone (used for the default start date of a
|
|
// standalone reward — progress must not count points from before it began).
|
|
function todayIso(): string {
|
|
const famTz = resolveTz((famStore.fam as any)?.timezone || 'auto');
|
|
return todayInTz(famTz);
|
|
}
|
|
|
|
// Numbered-circle marker used for the Who / How / What step tabs and field
|
|
// labels (SVG string, same style as lib/components/icons.ts).
|
|
function stepIcon(n: number): string {
|
|
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><text x="12" y="16" text-anchor="middle" font-size="12" font-family="inherit" fill="currentColor" stroke="none">${n}</text></svg>`;
|
|
}
|
|
|
|
let showEditModal = $state(false);
|
|
let editingConfig = $state<BonusConfig | null>(null);
|
|
let creatingFromTemplate = $state<BonusTemplate | null>(null);
|
|
// Multi-step rule editor: 1 = who, 2 = how, 3 = what.
|
|
let step = $state(1);
|
|
|
|
// Kanban columns can collapse (accordion) so templates can be dragged onto
|
|
// the desired area on tablet/mobile without cramming three columns.
|
|
let collapsed = $state({ member: false, comp: false, collab: false });
|
|
function toggleCol(key: 'member' | 'comp' | 'collab') {
|
|
collapsed[key] = !collapsed[key];
|
|
}
|
|
|
|
let createVals = $state({
|
|
name: '',
|
|
description: '',
|
|
target: 'individual' as string,
|
|
memberId: '' as string,
|
|
type: 'threshold' as string,
|
|
thresholdType: 'points' as string,
|
|
occurrence: 'recurring' as string,
|
|
rewardType: 'points' as string,
|
|
rewardValue: '100',
|
|
criteriaValue: 10,
|
|
period: '' as string,
|
|
startMode: 'today' as string
|
|
});
|
|
|
|
let editVals = $state({
|
|
name: '',
|
|
description: '',
|
|
target: 'individual' as string,
|
|
memberId: '' as string,
|
|
type: 'threshold' as string,
|
|
thresholdType: 'points' as string,
|
|
occurrence: 'recurring' as string,
|
|
rewardType: 'points' as string,
|
|
rewardValue: '100',
|
|
criteriaValue: 10,
|
|
period: '' as string,
|
|
startMode: 'today' as string,
|
|
completeBy: 'unlimited' as string,
|
|
startDate: '' as string,
|
|
completeByDate: '' as string
|
|
});
|
|
|
|
let configs = $derived(
|
|
famStore.initialized
|
|
? (famStore.bonusConfigs as BonusConfig[])
|
|
: (data.configs as BonusConfig[]) || []
|
|
);
|
|
let templates = $derived(
|
|
famStore.initialized
|
|
? (famStore.bonusTemplates as BonusTemplate[])
|
|
: (data.templates as BonusTemplate[]) || []
|
|
);
|
|
let members = $derived(
|
|
famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[]) || []
|
|
);
|
|
let allRewards = $derived(
|
|
famStore.initialized ? (famStore.rewards as Reward[]) : (data.rewards as Reward[]) || []
|
|
);
|
|
let progressData = $state((data.progress as BonusConfigWithProgress[]) || []);
|
|
|
|
let showPeriod = $derived(editVals.occurrence !== 'once');
|
|
let showCriteria = $derived(editVals.type !== 'manual');
|
|
let editIsPercent = $derived(editVals.type === 'threshold' && editVals.thresholdType === 'percent');
|
|
// Standalone (no period) rewards can scope progress by completeBy + dates.
|
|
let showProgressWindow = $derived(editVals.type !== 'manual' && !editVals.period);
|
|
|
|
let templateConfigs = $derived((templates as BonusTemplate[]).filter((t) => t.global));
|
|
let allBonusConfigs = $derived(
|
|
configs.filter((c: BonusConfig) => c.status !== 'completed') as BonusConfig[]
|
|
);
|
|
|
|
// Source of truth: which IDs are disabled (greyed-out on kanban)
|
|
let disabledIds = $derived(
|
|
new Set(configs.filter((c: BonusConfig) => c.status === 'disabled').map((c) => c.id))
|
|
);
|
|
|
|
let progressByConfigId = $derived.by(() => {
|
|
const map = new Map<string, BonusConfigWithProgress>();
|
|
for (const p of progressData) map.set(p.config.id, p);
|
|
return map;
|
|
});
|
|
|
|
const username = $derived(page.params.username);
|
|
|
|
// Pocket money: seeded for every child, but the amount (rewardValue) starts
|
|
// empty until the parent sets it on this page. The admin dashboard links
|
|
// here with ?s=pocketmoney to surface this mandatory teaching notice.
|
|
let pocketNotice = $derived(page.url.searchParams.get('s') === 'pocketmoney');
|
|
let pocketConfigs = $derived(
|
|
(configs as BonusConfig[]).filter((c: BonusConfig) => c.isPocketMoney)
|
|
);
|
|
let hasUnsetPocket = $derived(pocketConfigs.some((c: BonusConfig) => !c.rewardValue));
|
|
|
|
function memberName(id: string): string {
|
|
const m = members.find((m) => m.id === id);
|
|
return m?.name || 'Unknown';
|
|
}
|
|
|
|
function badgeClass(cfg: BonusConfig | BonusTemplate): string {
|
|
const classes: string[] = [];
|
|
if (cfg.target === 'individual') classes.push('indiv');
|
|
if (cfg.target === 'competitive') classes.push('comp');
|
|
if (cfg.period) classes.push('period');
|
|
if (cfg.type === 'manual') classes.push('manual');
|
|
return classes.join(' ');
|
|
}
|
|
|
|
function formatReward(type: string, value: string): string {
|
|
if (type === 'cash') return `£${value}`;
|
|
if (type === 'points') return `${value} pts`;
|
|
return value;
|
|
}
|
|
|
|
function handleDragStart(e: DragEvent, tpl: BonusTemplate) {
|
|
e.dataTransfer?.setData('text/plain', JSON.stringify({ id: tpl.id }));
|
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
|
|
}
|
|
|
|
function handleDragOver(e: DragEvent) {
|
|
e.preventDefault();
|
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
}
|
|
|
|
function openCreateFromTemplate(raw: string, targetType: string) {
|
|
const { id } = JSON.parse(raw);
|
|
const tpl = templates.find((t) => t.id === id);
|
|
if (!tpl) return;
|
|
creatingFromTemplate = tpl;
|
|
editingConfig = null;
|
|
editVals = {
|
|
name: tpl.name,
|
|
description: tpl.description || '',
|
|
target: targetType,
|
|
memberId: '',
|
|
type: tpl.type,
|
|
thresholdType: tpl.thresholdType || 'points',
|
|
occurrence: tpl.occurrence,
|
|
rewardType: tpl.rewardType,
|
|
rewardValue: tpl.rewardValue,
|
|
criteriaValue: tpl.criteriaValue || 10,
|
|
period: tpl.occurrence === 'once' ? '' : tpl.period || '',
|
|
startMode: 'today',
|
|
completeBy: 'unlimited',
|
|
startDate: todayIso(),
|
|
completeByDate: ''
|
|
};
|
|
step = 1;
|
|
showEditModal = true;
|
|
}
|
|
|
|
async function handleDropToMember(e: DragEvent) {
|
|
e.preventDefault();
|
|
const raw = e.dataTransfer?.getData('text/plain');
|
|
if (raw) openCreateFromTemplate(raw, 'individual');
|
|
}
|
|
|
|
async function handleDropToCompetition(e: DragEvent) {
|
|
e.preventDefault();
|
|
const raw = e.dataTransfer?.getData('text/plain');
|
|
if (raw) openCreateFromTemplate(raw, 'competitive');
|
|
}
|
|
|
|
async function handleDropToCollaboration(e: DragEvent) {
|
|
e.preventDefault();
|
|
const raw = e.dataTransfer?.getData('text/plain');
|
|
if (raw) openCreateFromTemplate(raw, 'collaborative');
|
|
}
|
|
|
|
function handleUpdate() {
|
|
return async ({ result }: any) => {
|
|
if (result.type === 'success') {
|
|
showEditModal = false;
|
|
creatingFromTemplate = null;
|
|
editingConfig = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
function openEditConfig(cfg: BonusConfig) {
|
|
editingConfig = cfg;
|
|
creatingFromTemplate = null;
|
|
editVals = {
|
|
name: cfg.name,
|
|
description: cfg.description || '',
|
|
target: cfg.target,
|
|
memberId: cfg.memberId || '',
|
|
type: cfg.type,
|
|
thresholdType: cfg.thresholdType || 'points',
|
|
occurrence: cfg.occurrence,
|
|
rewardType: cfg.rewardType,
|
|
rewardValue: cfg.rewardValue,
|
|
criteriaValue: cfg.criteriaValue || 10,
|
|
period: cfg.occurrence === 'once' ? '' : cfg.period || '',
|
|
startMode: cfg.status === 'disabled' ? 'disabled' : 'today',
|
|
completeBy: cfg.completeBy || 'unlimited',
|
|
startDate: cfg.startDate || todayIso(),
|
|
completeByDate: cfg.completeByDate || ''
|
|
};
|
|
step = 1;
|
|
showEditModal = true;
|
|
}
|
|
|
|
const showPeriodForCreate = $derived(createVals.occurrence !== 'once');
|
|
const showCriteriaForCreate = $derived(createVals.type !== 'manual');
|
|
|
|
let doneConfigs = $derived(
|
|
configs.filter((c: BonusConfig) => c.status === 'completed') as BonusConfig[]
|
|
);
|
|
</script>
|
|
|
|
<ViewHeader title="Rewards" hero />
|
|
|
|
{#if pocketNotice}
|
|
<CardGrid stackTablet>
|
|
<Card cols={3} accent={hasUnsetPocket ? '#f59e0b' : '#10b981'}>
|
|
<div class="pocket-notice">
|
|
<strong>Pocket Money</strong>
|
|
{#if hasUnsetPocket}
|
|
<p>
|
|
Every child has a <em>Pocket Money</em> droplet. Set how it's awarded — pick the
|
|
amount (£) and the % of weekly chores they must complete to earn it — then save. Until
|
|
you set the amount, pocket money is paused for that child.
|
|
</p>
|
|
{:else}
|
|
<p>All Pocket Money droplets are set. Children will earn pocket money as they complete chores.</p>
|
|
{/if}
|
|
</div>
|
|
</Card>
|
|
</CardGrid>
|
|
{/if}
|
|
|
|
<CardGrid stackTablet>
|
|
<Card cols={3}>
|
|
<div class="template-section">
|
|
<h3>Templates — Drag to assign</h3>
|
|
<div class="template-list">
|
|
{#each templateConfigs as cfg}
|
|
<div class="template-card" draggable="true" ondragstart={(e) => handleDragStart(e, cfg)} style="border-left-color:{cfg.color}">
|
|
<div class="card-head">
|
|
<TemplateIcon name={cfg.icon} size={18} color={cfg.color} />
|
|
<strong>{cfg.name}</strong>
|
|
</div>
|
|
<div class="badges">
|
|
<span class="badge {badgeClass(cfg)}">{cfg.target}</span>
|
|
<span class="badge">{cfg.type}</span>
|
|
<span class="badge">{cfg.occurrence}</span>
|
|
{#if cfg.period}
|
|
<span class="badge period">{cfg.period}</span>
|
|
{/if}
|
|
{#if cfg.isPocketMoney}
|
|
<span class="badge pocket">Pocket Money</span>
|
|
{/if}
|
|
</div>
|
|
<div class="reward-preview">
|
|
{formatReward(cfg.rewardType, cfg.rewardValue)}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{#if templateConfigs.length === 0}
|
|
<p class="empty">No templates yet</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<Card cols={3} scrollX>
|
|
<div class="kanban">
|
|
<!-- Member Column -->
|
|
<div class="column member-col" class:collapsed={collapsed.member} ondragover={handleDragOver} ondrop={handleDropToMember}>
|
|
<button class="col-header" type="button" onclick={() => toggleCol('member')}>
|
|
<span class="col-title">Member</span>
|
|
<span class="chev" class:open={!collapsed.member}>{@html chevronRight}</span>
|
|
</button>
|
|
<div class="col-body">
|
|
{#each allBonusConfigs.filter((c) => c.target === 'individual' && !doneConfigs.includes(c)) as cfg}
|
|
<div
|
|
class="col-card"
|
|
class:disabled={disabledIds.has(cfg.id)}
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={() => openEditConfig(cfg)}
|
|
onkeydown={(e) => e.key === 'Enter' && openEditConfig(cfg)}
|
|
>
|
|
<div class="col-card-head">
|
|
<strong>{cfg.name}</strong>
|
|
<div class="badges">
|
|
<span class="badge {badgeClass(cfg)}">{cfg.target}</span>
|
|
<span class="badge">{cfg.type}</span>
|
|
<span class="badge">{cfg.occurrence}</span>
|
|
{#if cfg.isPocketMoney}
|
|
<span class="badge pocket">Pocket Money</span>
|
|
{/if}
|
|
</div>
|
|
<span class="reward-total {cfg.rewardType}">
|
|
{cfg.rewardType === 'cash' ? `£${Number(cfg.rewardValue).toFixed(2)}` : `${cfg.rewardValue} pts`}
|
|
</span>
|
|
</div>
|
|
{#each members as m}
|
|
{#if !cfg.memberId || cfg.memberId === m.id}
|
|
{@const p = progressByConfigId
|
|
.get(cfg.id)
|
|
?.progress.find((pr: BonusProgress) => pr.memberId === m.id)}
|
|
{#if cfg.type === 'manual'}
|
|
<div class="member-progress">
|
|
<span class="mname" style="color:{m.color}">{m.name}</span>
|
|
</div>
|
|
{:else}
|
|
{@const criteria = p?.criteriaValue ?? cfg.criteriaValue ?? 0}
|
|
{@const current = p?.current ?? 0}
|
|
{@const achieved = p?.achieved ?? false}
|
|
{@const isPct = cfg.thresholdType === 'percent'}
|
|
{@const pct = isPct
|
|
? Math.min(100, current)
|
|
: criteria > 0
|
|
? Math.min(100, (current / criteria) * 100)
|
|
: 0}
|
|
<div class="member-progress">
|
|
<span class="mname" style="color:{m.color}">{m.name}</span>
|
|
{#if criteria > 0}
|
|
<div class="bar-wrap">
|
|
<div class="bar-fill" style="width:{pct}%" class:met={achieved}></div>
|
|
</div>
|
|
{/if}
|
|
<span class="stat {p?.state ?? 'pending'}">
|
|
{#if !p || p.state === 'pending'}<Clock size={13} />{:else}{p.state}{/if}
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{/each}
|
|
<div class="col-card-foot">
|
|
<span class="click-edit">Click to edit</span>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{#if allBonusConfigs.filter((c) => c.target === 'individual' && !doneConfigs.includes(c)).length === 0}
|
|
<p class="empty">No bonuses</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Competition Column -->
|
|
<div class="column comp-col" class:collapsed={collapsed.comp} ondragover={handleDragOver} ondrop={handleDropToCompetition}>
|
|
<button class="col-header" type="button" onclick={() => toggleCol('comp')}>
|
|
<span class="col-title">Competition</span>
|
|
<span class="chev" class:open={!collapsed.comp}>{@html chevronRight}</span>
|
|
</button>
|
|
<div class="col-body">
|
|
{#each allBonusConfigs.filter((c) => c.target === 'competitive' && !doneConfigs.includes(c)) as cfg}
|
|
{@const prog = progressByConfigId.get(cfg.id)}
|
|
<div
|
|
class="col-card"
|
|
class:disabled={disabledIds.has(cfg.id)}
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={() => openEditConfig(cfg)}
|
|
onkeydown={(e) => e.key === 'Enter' && openEditConfig(cfg)}
|
|
>
|
|
<div class="col-card-head">
|
|
<strong>{cfg.name}</strong>
|
|
<div class="badges">
|
|
<span class="badge {badgeClass(cfg)}">{cfg.target}</span>
|
|
<span class="badge">{cfg.type}</span>
|
|
<span class="badge">{cfg.occurrence}</span>
|
|
</div>
|
|
<span class="reward-total {cfg.rewardType}">
|
|
{cfg.rewardType === 'cash' ? `£${Number(cfg.rewardValue).toFixed(2)}` : `${cfg.rewardValue} pts`}
|
|
</span>
|
|
</div>
|
|
{#if prog}
|
|
<div class="leaderboard">
|
|
{#each [...prog.progress].sort((a, b) => b.current - a.current) as p, i}
|
|
<div class="lb-row" class:winner={i === 0 && p.current > 0}>
|
|
<span class="lb-rank">#{i + 1}</span>
|
|
<span class="mname" style="color:{p.memberColor}">{p.memberName}</span>
|
|
{#if p.criteriaValue > 0}
|
|
<div class="bar-wrap sm">
|
|
<div
|
|
class="bar-fill"
|
|
style="width:{Math.min(
|
|
100,
|
|
cfg.thresholdType === 'percent'
|
|
? p.current
|
|
: (p.current / Math.max(1, p.criteriaValue)) * 100
|
|
)}%"
|
|
class:met={p.achieved}
|
|
></div>
|
|
</div>
|
|
{/if}
|
|
<span class="stat {p.state}">
|
|
{#if p.state === 'pending'}<Clock size={13} />{:else}{p.state}{/if}
|
|
</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
<div class="col-card-foot">
|
|
<span class="click-edit">Click to edit</span>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{#if allBonusConfigs.filter((c) => c.target === 'competitive' && !doneConfigs.includes(c)).length === 0}
|
|
<p class="empty">No bonuses</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collaboration Column -->
|
|
<div class="column collab-col" class:collapsed={collapsed.collab} ondragover={handleDragOver} ondrop={handleDropToCollaboration}>
|
|
<button class="col-header" type="button" onclick={() => toggleCol('collab')}>
|
|
<span class="col-title">Collaboration</span>
|
|
<span class="chev" class:open={!collapsed.collab}>{@html chevronRight}</span>
|
|
</button>
|
|
<div class="col-body">
|
|
{#each allBonusConfigs.filter((c) => c.target === 'collaborative') as cfg}
|
|
{@const prog = progressByConfigId.get(cfg.id)}
|
|
<div
|
|
class="col-card"
|
|
class:disabled={disabledIds.has(cfg.id)}
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={() => openEditConfig(cfg)}
|
|
onkeydown={(e) => e.key === 'Enter' && openEditConfig(cfg)}
|
|
>
|
|
<div class="col-card-head">
|
|
<strong>{cfg.name}</strong>
|
|
<div class="badges">
|
|
<span class="badge {badgeClass(cfg)}">{cfg.target}</span>
|
|
<span class="badge">{cfg.type}</span>
|
|
<span class="badge">{cfg.occurrence}</span>
|
|
</div>
|
|
<span class="reward-total {cfg.rewardType}">
|
|
{cfg.rewardType === 'cash' ? `£${Number(cfg.rewardValue).toFixed(2)}` : `${cfg.rewardValue} pts`}
|
|
</span>
|
|
</div>
|
|
{#if prog}
|
|
{@const team = prog.progress[0]}
|
|
{#if team}
|
|
<div class="team-progress">
|
|
<span class="mname" style="color:{team.memberColor}">{team.memberName}</span>
|
|
{#if team.criteriaValue > 0}
|
|
<div class="bar-wrap">
|
|
<div
|
|
class="bar-fill"
|
|
style="width:{Math.min(
|
|
100,
|
|
cfg.thresholdType === 'percent'
|
|
? team.current
|
|
: (team.current / Math.max(1, team.criteriaValue)) * 100
|
|
)}%"
|
|
class:met={team.achieved}
|
|
></div>
|
|
</div>
|
|
{/if}
|
|
<span class="stat">
|
|
{team.current}/{team.criteriaValue}
|
|
{cfg.type === 'threshold' ? 'pts' : 'chores'}</span
|
|
>
|
|
<span class="stat {team.state}">
|
|
{#if team.state === 'pending'}<Clock size={13} />{:else}{team.state}{/if}
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
<div class="col-card-foot">
|
|
<span class="click-edit">Click to edit</span>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{#if allBonusConfigs.filter((c) => c.target === 'collaborative' && !doneConfigs.includes(c)).length === 0}
|
|
<p class="empty">No bonuses</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit / Create-from-Template Modal -->
|
|
{#if showEditModal && (editingConfig || creatingFromTemplate)}
|
|
{@const targetCfg = creatingFromTemplate || editingConfig!}
|
|
<div
|
|
class="overlay"
|
|
onclick={() => {
|
|
showEditModal = false;
|
|
creatingFromTemplate = null;
|
|
editingConfig = null;
|
|
}}
|
|
role="presentation"
|
|
>
|
|
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
|
<h3 class="modal-title">
|
|
{creatingFromTemplate ? 'Create from: ' : 'Edit: '}{targetCfg.name}
|
|
</h3>
|
|
|
|
<!-- Step tabs -->
|
|
<div class="step-tabs" role="tablist">
|
|
<button
|
|
type="button"
|
|
class="step-tab"
|
|
class:active={step === 1}
|
|
onclick={() => (step = 1)}
|
|
>{@html stepIcon(1)}<span>Who</span></button>
|
|
<button
|
|
type="button"
|
|
class="step-tab"
|
|
class:active={step === 2}
|
|
onclick={() => (step = 2)}
|
|
>{@html stepIcon(2)}<span>How</span></button>
|
|
<button
|
|
type="button"
|
|
class="step-tab"
|
|
class:active={step === 3}
|
|
onclick={() => (step = 3)}
|
|
>{@html stepIcon(3)}<span>What</span></button>
|
|
</div>
|
|
|
|
<form
|
|
method="POST"
|
|
action={creatingFromTemplate ? '?/createFromTemplate' : '?/updateConfig'}
|
|
use:enhance={handleUpdate}
|
|
>
|
|
{#if editingConfig}
|
|
<input type="hidden" name="id" value={editingConfig?.id} />
|
|
{/if}
|
|
{#if creatingFromTemplate}
|
|
<input type="hidden" name="isPocketMoney" value={creatingFromTemplate.isPocketMoney ? '1' : ''} />
|
|
{/if}
|
|
|
|
<!-- STEP 1 — Who (basics) -->
|
|
{#if step === 1}
|
|
<div class="step-body">
|
|
<label>
|
|
<span class="field-label">{@html stepIcon(1)} Name</span>
|
|
<input name="name" bind:value={editVals.name} required />
|
|
</label>
|
|
<label>
|
|
<span class="field-label">{@html stepIcon(1)} Description</span>
|
|
<input name="description" bind:value={editVals.description} />
|
|
</label>
|
|
<label>
|
|
<span class="field-label"><UsersRound size={14} /> Target (who)</span>
|
|
<select name="target" bind:value={editVals.target}>
|
|
<option value="individual">Individual</option>
|
|
<option value="competitive">Competition</option>
|
|
<option value="collaborative">Collaborative</option>
|
|
</select>
|
|
</label>
|
|
{#if (editingConfig || creatingFromTemplate) && editVals.target === 'individual'}
|
|
<label>
|
|
<span class="field-label"><UsersRound size={14} /> Member</span>
|
|
<select name="memberId" bind:value={editVals.memberId}>
|
|
<option value="">All (everyone)</option>
|
|
{#each members as m}
|
|
<option value={m.id}>{m.name}</option>
|
|
{/each}
|
|
</select>
|
|
</label>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- STEP 2 — How (rules + progress) -->
|
|
{#if step === 2}
|
|
<div class="step-body">
|
|
<label>
|
|
<span class="field-label"><Settings2 size={14} /> Type</span>
|
|
<select name="type" bind:value={editVals.type}>
|
|
<option value="threshold">Threshold</option>
|
|
<option value="count">Count</option>
|
|
<option value="manual">Manual</option>
|
|
</select>
|
|
</label>
|
|
{#if editVals.type === 'threshold'}
|
|
<label>
|
|
<span class="field-label"><Target size={14} /> Threshold by</span>
|
|
<select name="thresholdType" bind:value={editVals.thresholdType}>
|
|
<option value="points">Points</option>
|
|
<option value="percent">% of chores done</option>
|
|
</select>
|
|
</label>
|
|
{/if}
|
|
{#if showCriteria}
|
|
{#if editIsPercent}
|
|
<label>
|
|
<span class="field-label"><Target size={14} /> Chores completed (%)</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
name="criteriaValue"
|
|
bind:value={editVals.criteriaValue}
|
|
/>
|
|
<span class="hint">{editVals.criteriaValue}% of chores this week</span>
|
|
</label>
|
|
{:else}
|
|
<label>
|
|
<span class="field-label"><Target size={14} /> Target Value</span>
|
|
<input
|
|
type="number"
|
|
name="criteriaValue"
|
|
bind:value={editVals.criteriaValue}
|
|
required
|
|
/>
|
|
<span class="hint"
|
|
>{editVals.type === 'threshold'
|
|
? 'Points to reach'
|
|
: editVals.type === 'count'
|
|
? 'Number of chores'
|
|
: 'N/A'}</span
|
|
>
|
|
</label>
|
|
{/if}
|
|
{/if}
|
|
<label>
|
|
<span class="field-label"><Settings2 size={14} /> Occurrence</span>
|
|
<select name="occurrence" bind:value={editVals.occurrence}>
|
|
<option value="recurring">Recurring</option>
|
|
<option value="once">Once</option>
|
|
</select>
|
|
</label>
|
|
{#if showPeriod}
|
|
<label>
|
|
<span class="field-label"><CalendarDays size={14} /> Period</span>
|
|
<select name="period" bind:value={editVals.period}>
|
|
<option value="">None</option>
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly">Weekly</option>
|
|
<option value="monthly">Monthly</option>
|
|
</select>
|
|
<span class="hint"
|
|
>Recurring bonuses reset and re-earn each period. Leave as "None" for a
|
|
one-off reward measured once.</span
|
|
>
|
|
</label>
|
|
{/if}
|
|
{#if showProgressWindow}
|
|
<div class="start-mode">
|
|
<label
|
|
><span class="field-label"><CalendarDays size={14} /> Progress counts</span
|
|
></label>
|
|
<div class="radio-group">
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="completeBy"
|
|
value="unlimited"
|
|
bind:group={editVals.completeBy}
|
|
/>
|
|
<span>Unlimited (until reached)</span>
|
|
</label>
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="completeBy"
|
|
value="custom"
|
|
bind:group={editVals.completeBy}
|
|
/>
|
|
<span>Until a date</span>
|
|
</label>
|
|
</div>
|
|
{#if editVals.completeBy === 'custom'}
|
|
<label>
|
|
<span class="field-label"><CalendarDays size={14} /> Complete by</span>
|
|
<input type="date" name="completeByDate" bind:value={editVals.completeByDate} />
|
|
</label>
|
|
{/if}
|
|
<label>
|
|
<span class="field-label"><CalendarDays size={14} /> Progress starts on</span>
|
|
<input type="date" name="startDate" bind:value={editVals.startDate} />
|
|
<span class="hint"
|
|
>Points before this date won't count toward the reward.</span
|
|
>
|
|
</label>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- STEP 3 — What (reward) -->
|
|
{#if step === 3}
|
|
<div class="step-body">
|
|
<label>
|
|
<span class="field-label">
|
|
{#if editVals.rewardType === 'cash'}
|
|
<PoundSterling size={14} />
|
|
{:else if editVals.rewardType === 'points'}
|
|
<Coins size={14} />
|
|
{:else}
|
|
<Gift size={14} />
|
|
{/if}
|
|
Reward Type
|
|
</span>
|
|
<select name="rewardType" bind:value={editVals.rewardType}>
|
|
<option value="points">Points</option>
|
|
<option value="cash">Cash</option>
|
|
<option value="prize">Prize</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span class="field-label">
|
|
{#if editVals.rewardType === 'cash'}
|
|
<PoundSterling size={14} />
|
|
{:else if editVals.rewardType === 'points'}
|
|
<Coins size={14} />
|
|
{:else}
|
|
<Gift size={14} />
|
|
{/if}
|
|
Reward Value
|
|
</span>
|
|
<input name="rewardValue" bind:value={editVals.rewardValue} required />
|
|
<span class="hint"
|
|
>{editVals.rewardType === 'cash'
|
|
? 'Amount in £'
|
|
: editVals.rewardType === 'points'
|
|
? 'Points to award'
|
|
: 'Prize description'}</span
|
|
>
|
|
</label>
|
|
{#if creatingFromTemplate}
|
|
<div class="start-mode">
|
|
<label
|
|
><span class="field-label"><CalendarDays size={14} /> When to start</span
|
|
></label>
|
|
<div class="radio-group">
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="startMode"
|
|
value="disabled"
|
|
bind:group={editVals.startMode}
|
|
/>
|
|
<span>Disabled</span>
|
|
</label>
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="startMode"
|
|
value="today"
|
|
bind:group={editVals.startMode}
|
|
onchange={() => (editVals.startDate = todayIso())}
|
|
/>
|
|
<span>Today</span>
|
|
</label>
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="startMode"
|
|
value="nextWeek"
|
|
bind:group={editVals.startMode}
|
|
onchange={() => (editVals.startDate = addDaysStr(todayIso(), 7))}
|
|
/>
|
|
<span>Next week</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Footer actions -->
|
|
<div class="modal-actions">
|
|
{#if step === 2}
|
|
<button type="button" class="nav-btn" onclick={() => (step = 1)}>
|
|
<ArrowLeft size={14} /> Back
|
|
</button>
|
|
<button type="button" class="next-btn" onclick={() => (step = 3)}>
|
|
Next <ArrowRight size={14} />
|
|
</button>
|
|
{:else if step === 3}
|
|
<button type="button" class="nav-btn" onclick={() => (step = 2)}>
|
|
<ArrowLeft size={14} /> Back
|
|
</button>
|
|
{#if editingConfig}
|
|
<button
|
|
type="button"
|
|
class="toggle-btn"
|
|
onclick={async () => {
|
|
if (!editingConfig) return;
|
|
const fd = new FormData();
|
|
fd.append('id', editingConfig.id);
|
|
fd.append('currentStatus', editingConfig.status);
|
|
const res = await fetch('?/toggleConfig', { method: 'POST', body: fd });
|
|
if (res.ok) showEditModal = false;
|
|
}}
|
|
>
|
|
<Ban size={14} />
|
|
{editingConfig.status === 'disabled' ? 'Enable' : 'Disable'}
|
|
</button>
|
|
{#if editingConfig.status !== 'completed'}
|
|
<button
|
|
type="button"
|
|
class="complete-btn"
|
|
onclick={async () => {
|
|
if (!editingConfig) return;
|
|
if (!confirm('Mark this bonus as completed?')) return;
|
|
const fd = new FormData();
|
|
fd.append('id', editingConfig.id);
|
|
const res = await fetch('?/completeConfig', { method: 'POST', body: fd });
|
|
if (res.ok) showEditModal = false;
|
|
}}
|
|
>
|
|
<Crown size={14} /> Complete
|
|
</button>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
class="delete-btn"
|
|
onclick={async () => {
|
|
if (!editingConfig) return;
|
|
if (!confirm('Delete this bonus?')) return;
|
|
const fd = new FormData();
|
|
fd.append('id', editingConfig.id);
|
|
const res = await fetch('?/destroyConfig', { method: 'POST', body: fd });
|
|
if (res.ok) {
|
|
showEditModal = false;
|
|
editingConfig = null;
|
|
}
|
|
}}
|
|
>
|
|
<Trash2 size={14} /> Delete
|
|
</button>
|
|
{/if}
|
|
<button type="submit" class="save-btn">
|
|
<Save size={14} /> {creatingFromTemplate ? 'Assign' : 'Save'}
|
|
</button>
|
|
{:else}
|
|
<button type="button" class="next-btn" onclick={() => (step = 2)}>
|
|
Next <ArrowRight size={14} />
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</Card>
|
|
</CardGrid>
|
|
|
|
<style>
|
|
.template-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.template-section h3 {
|
|
margin: 0 0 0.5rem;
|
|
font-size: 1rem;
|
|
}
|
|
.template-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.template-card {
|
|
flex: 0 1 auto;
|
|
min-width: 160px;
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-left: 4px solid #8b5cf6;
|
|
border-radius: 10px;
|
|
padding: 0.65rem 0.85rem;
|
|
cursor: grab;
|
|
transition: all 0.2s;
|
|
}
|
|
.template-card:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.template-card:active {
|
|
cursor: grabbing;
|
|
opacity: 0.8;
|
|
}
|
|
@media (max-width: 1023px) {
|
|
.template-list {
|
|
flex-direction: column;
|
|
}
|
|
.template-card {
|
|
width: 100%;
|
|
}
|
|
}
|
|
.kanban {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.75rem;
|
|
}
|
|
@media (max-width: 1023px) {
|
|
.kanban {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
.column {
|
|
border: 1px solid #e5e7eb;
|
|
border-top: 4px solid #6366f1;
|
|
border-radius: 12px;
|
|
padding: 0.75rem;
|
|
min-height: 200px;
|
|
background: #fafbff;
|
|
}
|
|
.member-col { border-top-color: #6366f1; background: #eef2ff; }
|
|
.comp-col { border-top-color: #f59e0b; background: #fffbeb; }
|
|
.collab-col { border-top-color: #10b981; background: #ecfdf5; }
|
|
.column h3 {
|
|
margin: 0 0 0.5rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
.col-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
margin: 0 0 0.5rem;
|
|
padding: 0;
|
|
background: none;
|
|
border: none;
|
|
font: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
color: inherit;
|
|
}
|
|
.col-header .col-title {
|
|
font-size: 0.95rem;
|
|
font-weight: 700;
|
|
}
|
|
.chev {
|
|
display: inline-flex;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
.chev.open {
|
|
transform: rotate(90deg);
|
|
}
|
|
.column.collapsed {
|
|
min-height: 0;
|
|
}
|
|
.column.collapsed .col-body {
|
|
display: none;
|
|
}
|
|
.card-head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
.card-head strong {
|
|
font-size: 0.85rem;
|
|
color: #1f2937;
|
|
}
|
|
.badges {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
.badge {
|
|
font-size: 0.65rem;
|
|
padding: 0.1rem 0.3rem;
|
|
border-radius: 0.25rem;
|
|
background: #e5e7eb;
|
|
}
|
|
.badge.indiv {
|
|
background: #dbeafe;
|
|
}
|
|
.badge.comp {
|
|
background: #dcfce7;
|
|
}
|
|
.badge.period {
|
|
background: #fef3c7;
|
|
}
|
|
.badge.manual {
|
|
background: #f3e8ff;
|
|
}
|
|
.badge.pocket {
|
|
background: #fde68a;
|
|
color: #92400e;
|
|
font-weight: 600;
|
|
}
|
|
.pocket-notice strong {
|
|
display: block;
|
|
margin-bottom: 0.35rem;
|
|
font-size: 0.95rem;
|
|
color: #1f2937;
|
|
}
|
|
.pocket-notice p {
|
|
margin: 0;
|
|
font-size: 0.85rem;
|
|
color: #6b7280;
|
|
line-height: 1.45;
|
|
}
|
|
.reward-preview {
|
|
font-size: 0.75rem;
|
|
color: #6b7280;
|
|
margin-top: 0.25rem;
|
|
}
|
|
.del-btn {
|
|
background: transparent;
|
|
border: none;
|
|
font-size: 1.2rem;
|
|
cursor: pointer;
|
|
color: #9ca3af;
|
|
padding: 0 0.25rem;
|
|
line-height: 1;
|
|
border-radius: 4px;
|
|
}
|
|
.del-btn:hover {
|
|
color: #ef4444;
|
|
background: #fef2f2;
|
|
}
|
|
.empty {
|
|
color: #9ca3af;
|
|
font-size: 0.85rem;
|
|
text-align: center;
|
|
padding: 1.5rem;
|
|
}
|
|
.col-card {
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-left: 4px solid #6366f1;
|
|
border-radius: 10px;
|
|
padding: 0.65rem 0.85rem;
|
|
margin-bottom: 0.6rem;
|
|
cursor: pointer;
|
|
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
|
|
}
|
|
.col-card:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);
|
|
border-color: #c7d2fe;
|
|
}
|
|
.col-card:focus-visible {
|
|
outline: 2px solid #6366f1;
|
|
outline-offset: 2px;
|
|
}
|
|
.reward-total {
|
|
margin-left: auto;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
}
|
|
.reward-total.cash {
|
|
color: #16a34a;
|
|
}
|
|
.reward-total.points {
|
|
color: #9333ea;
|
|
}
|
|
.click-edit {
|
|
margin-left: auto;
|
|
font-size: 0.68rem;
|
|
color: #9ca3af;
|
|
}
|
|
.col-card.done {
|
|
opacity: 0.75;
|
|
}
|
|
.col-card.disabled {
|
|
opacity: 0.5;
|
|
filter: grayscale(0.5);
|
|
}
|
|
.col-card-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
flex-wrap: wrap;
|
|
gap: 0.3rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.col-card-head strong {
|
|
font-size: 0.85rem;
|
|
color: #1f2937;
|
|
}
|
|
.col-card-foot {
|
|
margin-top: 0.5rem;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.3rem;
|
|
}
|
|
.member-progress {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
.mname {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.bar-wrap {
|
|
flex: 1;
|
|
height: 8px;
|
|
background: #f3f4f6;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.bar-wrap.sm {
|
|
width: 60px;
|
|
flex: none;
|
|
}
|
|
.bar-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
border-radius: 4px;
|
|
transition: width 0.3s;
|
|
}
|
|
.bar-fill.met {
|
|
background: linear-gradient(90deg, #10b981, #34d399);
|
|
}
|
|
.stat {
|
|
font-size: 0.7rem;
|
|
padding: 0.1rem 0.25rem;
|
|
border-radius: 0.25rem;
|
|
background: #e5e7eb;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.15rem;
|
|
}
|
|
.stat.pending {
|
|
background: #e5e7eb;
|
|
}
|
|
.stat.unclaimed {
|
|
background: #fde68a;
|
|
}
|
|
.stat.claimed {
|
|
background: #a7f3d0;
|
|
}
|
|
.leaderboard {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.lb-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
font-size: 0.75rem;
|
|
padding: 0.15rem 0;
|
|
}
|
|
.lb-row.winner {
|
|
background: #fef3c7;
|
|
border-radius: 0.25rem;
|
|
padding: 0.15rem 0.25rem;
|
|
}
|
|
.lb-rank {
|
|
width: 20px;
|
|
font-weight: 600;
|
|
}
|
|
.overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
.modal {
|
|
background: white;
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
max-width: 400px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
.modal h3 {
|
|
margin: 0 0 1rem;
|
|
}
|
|
.modal label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
.modal input,
|
|
.modal select {
|
|
width: 100%;
|
|
padding: 0.4rem;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 0.25rem;
|
|
margin-top: 0.2rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
.modal .hint {
|
|
font-size: 0.75rem;
|
|
color: #6b7280;
|
|
display: block;
|
|
margin-top: 0.15rem;
|
|
}
|
|
.modal-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 1rem;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
}
|
|
.modal-actions button {
|
|
padding: 0.5rem 1rem;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
}
|
|
.modal-actions button.nav-btn,
|
|
.modal-actions button[type='button'] {
|
|
background: #e5e7eb;
|
|
color: #374151;
|
|
}
|
|
.modal-actions button.next-btn,
|
|
.modal-actions button[type='submit'].save-btn {
|
|
background: #6366f1;
|
|
color: white;
|
|
}
|
|
.modal-actions button.toggle-btn {
|
|
background: #f59e0b;
|
|
color: white;
|
|
}
|
|
.modal-actions button.complete-btn {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
.modal-actions button.delete-btn {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
/* Step tabs (Who / How / What) */
|
|
.step-tabs {
|
|
display: flex;
|
|
gap: 2px;
|
|
background: #f3f4f6;
|
|
border-radius: 6px;
|
|
padding: 2px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.step-tab {
|
|
flex: 1;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.3rem;
|
|
padding: 0.4rem 0.5rem;
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: 5px;
|
|
font-size: 0.85rem;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
}
|
|
.step-tab.active {
|
|
background: #fff;
|
|
color: #4338ca;
|
|
font-weight: 600;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.step-tab :global(svg) {
|
|
flex: none;
|
|
}
|
|
.field-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
.field-label :global(svg) {
|
|
flex: none;
|
|
color: #6366f1;
|
|
}
|
|
.step-body {
|
|
min-height: 120px;
|
|
}
|
|
.start-mode {
|
|
margin-top: 0.75rem;
|
|
}
|
|
.start-mode > label {
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
margin-bottom: 0.3rem;
|
|
display: block;
|
|
}
|
|
.radio-group {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
}
|
|
.radio-group label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
flex-wrap: wrap;
|
|
}
|
|
/* Progress-counts date inputs must shrink rather than blow the label width. */
|
|
.start-mode input[type='date'] {
|
|
flex-shrink: 1;
|
|
min-width: 0;
|
|
}
|
|
.step-body input[type='date'] {
|
|
flex-shrink: 1;
|
|
min-width: 0;
|
|
}
|
|
.radio-group label input[type='date'] {
|
|
flex-shrink: 1;
|
|
min-width: 0;
|
|
}
|
|
</style>
|