Give each habit completion type a distinct layout

This commit is contained in:
syntaxbullet
2026-09-05 08:11:00 +02:00
parent 2f67b775bf
commit 63e1573dae
2 changed files with 52 additions and 29 deletions

View File

@@ -3,7 +3,7 @@ import { ChevronDown, ListChecks } from "lucide-react";
import { CalendarHeatmap } from "./CalendarHeatmap"; import { CalendarHeatmap } from "./CalendarHeatmap";
import { demoCalendar } from "./calendar-model"; import { demoCalendar } from "./calendar-model";
/** Open chart section: intentionally no card surface or enclosing border. */ /** Completion controls define each goal surface; history stays a separate full-width disclosure. */
export function HabitChart({ export function HabitChart({
name, name,
method, method,
@@ -47,22 +47,27 @@ export function HabitChart({
const Heading = `h${headingLevel}` as const; const Heading = `h${headingLevel}` as const;
if (trackingMethod) return ( if (trackingMethod) return (
<section className={`ds-habit-chart ds-goal ds-goal--${trackingMethod}`} id={id} aria-labelledby={headingId} style={{ "--habit-color": color } as CSSProperties}> <section className={`ds-habit-chart ds-goal ds-goal--${trackingMethod}`} id={id} aria-labelledby={headingId} style={{ "--habit-color": color } as CSSProperties}>
<header className="ds-goal-heading"> <div className="ds-goal-body">
<div className="ds-goal-title"> <div className="ds-goal-overview">
{trackingMethod === "manual" && children} <header className="ds-goal-heading">
<Heading id={headingId} className="type-title"> <div className="ds-goal-title">
{trackingMethod === "manual" ? <label htmlFor={checkInId}>{name}</label> : name} {trackingMethod === "manual" && children}
</Heading> <Heading id={headingId} className="type-title">
{trackingMethod === "manual" ? <label htmlFor={checkInId}>{name}</label> : name}
</Heading>
</div>
{headingActions}
</header>
{(!due || schedule !== "Every day") && <p className="ds-goal-schedule">{due ? schedule : `Not scheduled today · ${schedule}`}</p>}
{trackingMethod === "tasks" && due && target > 0 && <div className="ds-goal-task-progress">
<progress aria-label={`${name} tasks completed`} value={Math.max(0, Math.min(value, target))} max={target} />
<span role="status">{value} / {target}</span>
</div>}
</div> </div>
{headingActions} {trackingMethod === "count" && <div className="ds-goal-count">{children}<span className="ds-goal-unit">{unit}</span></div>}
</header> {editor}
{(!due || schedule !== "Every day") && <p className="ds-goal-schedule">{due ? schedule : `Not scheduled today · ${schedule}`}</p>} {trackingMethod === "tasks" && <div className="ds-goal-tasks">{tasks}</div>}
{editor} </div>
{trackingMethod === "count" && <div className="ds-goal-count">{children}<span className="ds-goal-unit">{unit}</span></div>}
{trackingMethod === "tasks" && <div className="ds-goal-tasks">
{due && target > 0 && <p className="ds-goal-task-progress" role="status">{value} / {target} completed</p>}
{tasks}
</div>}
{calendar} {calendar}
</section> </section>
); );

View File

@@ -1591,33 +1591,51 @@
/* Each goal puts its completion control before secondary history. */ /* Each goal puts its completion control before secondary history. */
.ds-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } .ds-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.ds-habit-list > .ds-goal { display: flex; flex-direction: column; align-items: stretch; gap: 0; padding: 24px 0; } .ds-habit-list > .ds-goal { display: flex; flex-direction: column; align-items: stretch; gap: 0; padding: 20px 0; }
.ds-goal-body { min-width: 0; }
.ds-goal-overview { min-width: 0; }
.ds-goal-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .ds-goal-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.ds-goal-title { display: flex; align-items: center; gap: 12px; min-width: 0; } .ds-goal-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
.ds-goal-title h3 { overflow-wrap: anywhere; min-width: 0; } .ds-goal-title :is(h3, h4) { overflow-wrap: anywhere; min-width: 0; }
.ds-goal-title label { cursor: pointer; } .ds-goal-title label { cursor: pointer; }
.ds-goal-title > .ds-checkbox { flex: 0 0 44px; justify-content: center; gap: 0; } .ds-goal-title > .ds-checkbox { flex: 0 0 44px; justify-content: center; gap: 0; }
.ds-goal--manual .ds-goal-title:has(input:checked) h3 { color: var(--ds-secondary); } .ds-goal--manual .ds-goal-body { padding: 4px 0; }
.ds-goal-schedule { @apply type-small; color: var(--ds-secondary); margin-top: 4px; } .ds-goal--manual .ds-goal-title:has(input:checked) :is(h3, h4) { color: var(--ds-secondary); }
.ds-goal--manual .ds-goal-title input { width: 26px; height: 26px; }
.ds-goal--manual .ds-goal-title input:checked { background: var(--habit-color); border-color: var(--habit-color); }
.ds-goal-schedule { @apply type-small; color: var(--ds-secondary); margin-top: 6px; }
.ds-goal--manual .ds-goal-schedule { padding-left: 56px; } .ds-goal--manual .ds-goal-schedule { padding-left: 56px; }
.ds-goal-count { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; } .ds-goal--count .ds-goal-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, auto); align-items: center; gap: 24px; padding: 24px; border-radius: 8px; border: 1px solid color-mix(in srgb, var(--habit-color) 20%, var(--ds-rule)); background: color-mix(in srgb, var(--habit-color) 5%, var(--ds-paper)); }
.ds-goal-count .ds-counter { padding: 6px; gap: 8px; background: var(--ds-surface); } .ds-goal--count .ds-goal-body > .ds-inline-editor { grid-column: 1 / -1; }
.ds-goal-count .ds-counter-input { @apply type-title; width: 5ch; min-height: 52px; background: var(--ds-paper); } .ds-goal-count { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; }
.ds-goal-count .ds-counter .ds-button { min-height: 48px; min-width: 48px; } .ds-goal-count .ds-counter { justify-content: center; padding: 6px; gap: 6px; background: var(--ds-paper); border-color: color-mix(in srgb, var(--habit-color) 20%, var(--ds-rule)); }
.ds-goal-unit { @apply type-body; color: var(--ds-secondary); overflow-wrap: anywhere; } .ds-goal-count .ds-counter-input { font-size: 36px; line-height: 1.2; width: 4ch; min-height: 60px; background: transparent; }
.ds-habit-list .ds-goal .ds-habit-history { align-self: flex-end; margin-top: 8px; } .ds-goal-count .ds-counter .ds-button { min-height: 44px; min-width: 44px; }
.ds-goal-count .ds-counter > .ds-muted { font-size: 18px; padding-right: 0; }
.ds-goal-unit { @apply type-small; color: var(--ds-secondary); overflow-wrap: anywhere; text-align: center; }
.ds-habit-list .ds-goal .ds-habit-history { align-self: flex-end; margin-top: 10px; }
.ds-habit-list .ds-goal .ds-habit-history:has([aria-expanded="true"]) { align-self: stretch; } .ds-habit-list .ds-goal .ds-habit-history:has([aria-expanded="true"]) { align-self: stretch; }
.ds-goal-task-progress { @apply type-small; color: var(--ds-secondary); margin: 8px 0; } .ds-goal--tasks .ds-goal-body { border: 1px solid var(--ds-rule); border-radius: 8px; overflow: clip; }
.ds-goal--tasks .ds-goal-overview { padding: 20px 24px 16px; background: var(--ds-surface); border-bottom: 1px solid var(--ds-rule); }
.ds-goal-task-progress { display: flex; align-items: center; gap: 12px; color: var(--ds-secondary); margin-top: 12px; @apply type-small; font-variant-numeric: tabular-nums; }
.ds-goal-task-progress progress { appearance: none; width: 100px; height: 4px; border: 0; border-radius: 4px; overflow: hidden; background: var(--ds-rule); accent-color: var(--habit-color); }
.ds-goal-task-progress progress::-webkit-progress-bar { background: var(--ds-rule); }
.ds-goal-task-progress progress::-webkit-progress-value { background: var(--habit-color); }
.ds-goal-task-progress progress::-moz-progress-bar { background: var(--habit-color); }
.ds-goal-tasks { padding: 4px 16px 8px; }
.ds-routine-items { list-style: none; padding: 0; margin: 0; } .ds-routine-items { list-style: none; padding: 0; margin: 0; }
.ds-routine-items .ds-editable-task:first-child { border-top: 0; } .ds-routine-items .ds-editable-task:first-child { border-top: 0; }
.ds-routine-items .ds-editable-task-row { padding: 4px 8px; gap: 12px; } .ds-routine-items .ds-editable-task-row { padding: 4px 8px; gap: 12px; border-radius: 4px; }
.ds-routine-items .ds-editable-task-row > .ds-checkbox { min-height: 48px; } .ds-routine-items .ds-editable-task-row > .ds-checkbox { min-height: 48px; }
.ds-routine-items .ds-editable-task-row:has(input:checked) .ds-checkbox-label { color: var(--ds-secondary); text-decoration: line-through; text-decoration-color: var(--ds-rule); } .ds-routine-items .ds-editable-task-row:has(input:checked) .ds-checkbox-label { color: var(--ds-secondary); text-decoration: line-through; text-decoration-color: var(--ds-rule); }
.ds-routine-items .ds-editable-task-row:hover { background: var(--ds-surface); } .ds-routine-items .ds-editable-task-row:hover { background: var(--ds-surface); }
.ds-goal-tasks .ds-task-add-action { padding-bottom: 0; } .ds-goal-tasks .ds-task-add-action { padding: 8px; border-top: 1px solid var(--ds-rule); }
.ds-habit-starter-options { margin-top: 0; } .ds-habit-starter-options { margin-top: 0; }
@media (max-width: 700px) { @media (max-width: 700px) {
.ds-goal-heading { gap: 8px; } .ds-goal-heading { gap: 8px; }
.ds-goal--count .ds-goal-body { grid-template-columns: minmax(0, 1fr); gap: 20px; padding: 20px 16px; }
.ds-goal--tasks .ds-goal-overview { padding: 16px; }
.ds-goal-tasks { padding-inline: 8px; }
.ds-goal-count { gap: 8px; } .ds-goal-count { gap: 8px; }
.ds-goal-count .ds-counter { gap: 4px; } .ds-goal-count .ds-counter { gap: 4px; }
.ds-habit-list .ds-goal .ds-habit-history { align-self: flex-start; } .ds-habit-list .ds-goal .ds-habit-history { align-self: flex-start; }