feat(ux): B3.6 — editable energy labels (global + per-profile) + first-tap prompt

Settings → Energy: edit global energy labels (label + description per
level) with reset-to-defaults. Per-profile override toggle in the
Profiles section ("Customise for this profile" / "Use global").
EnergyChip and Tasks-page energy controls now resolve labels via
resolveEnergyLabels(profile, settings) — global by default, profile
override when present.

First-time prompt: on first energy-chip tap of the day (per profile)
or first-ever tap to Zero, an inline non-modal prompt asks "What does
Zero mean for you today?" with example chips. Per-day gate via
energyPromptLastShownDate; one-shot first-Zero gate via
energyPromptZeroSeen; Settings escape resets both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 19:30:06 +01:00
parent 7daf5677c9
commit fc3a56c0dc
6 changed files with 375 additions and 19 deletions

View File

@@ -3,7 +3,7 @@
import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { settings, saveSettings, profiles, saveProfiles, templates, createTemplate as createTemplateRow, updateTemplate as updateTemplateRow, deleteTemplate as deleteTemplateRow, page, addProfileTaskList, removeProfileTaskList } from "$lib/stores/page.svelte.js";
import { settings, saveSettings, profiles, saveProfiles, templates, createTemplate as createTemplateRow, updateTemplate as updateTemplateRow, deleteTemplate as deleteTemplateRow, page, addProfileTaskList, removeProfileTaskList, defaultEnergyLabels } from "$lib/stores/page.svelte.js";
import Card from "$lib/components/Card.svelte";
import Toggle from "$lib/components/Toggle.svelte";
import SegmentedButton from "$lib/components/SegmentedButton.svelte";
@@ -1625,6 +1625,94 @@
</p>
</div>
{/if}
<!-- B3.6 — Energy labels (global). Three rows for the three
energy levels: edit the display label and a one-line
description that surfaces in the first-tap prompt. The
internal enum values (high/medium/brain_dead) never
move; only the user-facing strings.
Per-profile overrides live in the Profiles section
below — that's the right home because the override is
a property of the profile, not of the Tasks page. -->
<div class="mt-6 pt-5 border-t border-border-subtle">
<div class="flex items-center justify-between mb-2">
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider">Energy labels</p>
<button
type="button"
class="text-[11px] text-text-tertiary hover:text-text"
onclick={() => {
settings.energyLabels = defaultEnergyLabels();
saveSettings();
}}
title="Restore the default High / Medium / Zero labels and descriptions"
>Reset to defaults</button>
</div>
<p class="text-[11px] text-text-tertiary mb-3">
Rename the three energy levels and the short description shown when you set one. Profiles can override these — see the Profiles section below.
</p>
{#each ["high", "medium", "brain_dead"] as level}
<div class="mb-3">
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-1">
{level === "high" ? "High" : level === "medium" ? "Medium" : "Zero / Low"} <span class="text-text-tertiary normal-case">({level})</span>
</p>
<div class="flex items-center gap-2">
<input
type="text"
class="w-[140px] bg-bg-input border border-border-subtle rounded-lg px-2 py-1 text-[13px] text-text placeholder:text-text-tertiary focus:outline-none focus:border-accent"
placeholder="Label"
bind:value={settings.energyLabels[level].label}
oninput={() => saveSettings()}
data-no-transition
/>
<input
type="text"
class="flex-1 bg-bg-input border border-border-subtle rounded-lg px-2 py-1 text-[13px] text-text placeholder:text-text-tertiary focus:outline-none focus:border-accent"
placeholder="Description (e.g. Sharp focus, big lifts)"
bind:value={settings.energyLabels[level].description}
oninput={() => saveSettings()}
data-no-transition
/>
</div>
</div>
{/each}
</div>
<!-- B3.6 — first-tap prompt controls. The energy-prompt
fires at most once per local calendar day, plus once
the very first time the user taps Zero. This block
holds the kill-switch and the "re-introduce" escape
so the user is never stuck with a state they can't
reset. -->
<div class="mt-6 pt-5 border-t border-border-subtle">
<Toggle
bind:checked={settings.energyPromptsEnabled}
label="Show energy prompts"
description="On the first energy tap of the day (or the first ever Zero), Kon asks what the level means for you today, with example chips you can save into the description. Never modal."
/>
<div class="mt-3 flex items-center gap-3">
<button
type="button"
class="px-3 py-1.5 rounded-lg bg-bg-input border border-border-subtle text-[12px] text-text-secondary hover:text-text hover:border-accent"
style="transition-duration: var(--duration-ui)"
onclick={() => {
settings.energyPromptLastShownDate = null;
settings.energyPromptZeroSeen = false;
saveSettings();
toasts.info(
"Energy prompts re-introduced",
"The next time you set an energy level (and the next time you pick Zero), Kon will ask what it means for you today.",
);
}}
>Re-introduce energy prompts</button>
<span class="text-[11px] text-text-tertiary">
{settings.energyPromptLastShownDate
? `Last shown ${settings.energyPromptLastShownDate}`
: "Not shown yet"}
</span>
</div>
</div>
</div>
{/if}
</div>
@@ -2022,6 +2110,98 @@
oninput={() => saveProfiles()}
data-no-transition
></textarea>
<!-- B3.6 — per-profile energy label override.
OFF = profile.energyLabelsOverride is null
and the profile uses settings.energyLabels.
ON = override is populated with a copy of
the current global (or the existing
override) and three rows render below.
Inlined toggle (rather than the <Toggle>
component) so we can run the deep-clone
on the same click that flips the switch
— the shared Toggle has no onchange hook. -->
<div class="mt-3 pt-3 border-t border-border-subtle">
<div class="flex items-start justify-between gap-2">
<div class="flex items-start gap-3 py-2.5 flex-1">
<button
type="button"
class="relative mt-0.5 w-[38px] min-w-[38px] h-[22px] rounded-full flex-shrink-0
{profile.energyLabelsOverride ? 'bg-accent shadow-[0_0_8px_rgba(232,168,124,0.25)]' : 'bg-bg-elevated'}
active:scale-95"
style="transition-duration: var(--duration-ui)"
role="switch"
aria-checked={!!profile.energyLabelsOverride}
aria-label="Customise energy labels for this profile"
onclick={() => {
if (profile.energyLabelsOverride) {
profile.energyLabelsOverride = null;
} else {
const base = profile.energyLabelsOverride ?? settings.energyLabels;
profile.energyLabelsOverride = {
high: { ...base.high },
medium: { ...base.medium },
brain_dead: { ...base.brain_dead },
};
}
saveProfiles();
}}
>
<span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white shadow-sm ease-[cubic-bezier(0.34,1.56,0.64,1)]
{profile.energyLabelsOverride ? 'translate-x-[16px]' : 'translate-x-0'}"
style="transition: transform var(--duration-ui) cubic-bezier(0.34, 1.56, 0.64, 1)"
></span>
</button>
<div class="flex-1 min-w-0">
<p class="text-[13px] text-text leading-tight">Customise energy labels for this profile</p>
<p class="text-[11px] text-text-tertiary mt-0.5 leading-snug">When on, this profile uses its own High / Medium / Zero names instead of the global ones.</p>
</div>
</div>
{#if profile.energyLabelsOverride}
<button
type="button"
class="text-[11px] text-text-tertiary hover:text-text whitespace-nowrap mt-3"
onclick={() => {
profile.energyLabelsOverride = null;
saveProfiles();
}}
title="Drop the override and fall back to the global labels"
>Use global</button>
{/if}
</div>
{#if profile.energyLabelsOverride}
<div class="mt-3 pl-1 animate-fade-in">
{#each ["high", "medium", "brain_dead"] as level}
<div class="mb-2">
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-1">
{level === "high" ? "High" : level === "medium" ? "Medium" : "Zero / Low"} <span class="text-text-tertiary normal-case">({level})</span>
</p>
<div class="flex items-center gap-2">
<input
type="text"
class="w-[140px] bg-bg-input border border-border-subtle rounded-lg px-2 py-1 text-[13px] text-text placeholder:text-text-tertiary focus:outline-none focus:border-accent"
placeholder="Label"
bind:value={profile.energyLabelsOverride[level].label}
oninput={() => saveProfiles()}
data-no-transition
/>
<input
type="text"
class="flex-1 bg-bg-input border border-border-subtle rounded-lg px-2 py-1 text-[13px] text-text placeholder:text-text-tertiary focus:outline-none focus:border-accent"
placeholder="Description"
bind:value={profile.energyLabelsOverride[level].description}
oninput={() => saveProfiles()}
data-no-transition
/>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
</div>