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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user