Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ac53bce
docs: plan the Ignite v3 Focus surface
malinfossum Aug 13, 2026
12323bc
refactor(route): rename the landing route to focus and redirect #area…
malinfossum Aug 13, 2026
7775273
feat(focus): bucket tasks into the four Focus tabs
malinfossum Aug 13, 2026
6c4bf0b
fix(focus): pick the Next hero from the Today bucket, not every dated…
malinfossum Aug 13, 2026
699fc7b
feat(focus): add the day greeting and the overdue/due-today summary
malinfossum Aug 13, 2026
696ddc7
feat(focus): resolve a task's area for the row badge
malinfossum Aug 13, 2026
16a52e2
refactor(toast): let a toast carry any action, not only Undo
malinfossum Aug 13, 2026
1edf8e2
feat(focus): give the task row an area badge slot
malinfossum Aug 13, 2026
c5a4323
feat(focus): add the tab strip template
malinfossum Aug 13, 2026
db001d3
refactor(focus): rename the Today view to the Focus view
malinfossum Aug 13, 2026
364ee52
feat(focus): put Today, Tomorrow, Starred and the notepad on tabs
malinfossum Aug 13, 2026
d7c9b47
docs(plan): move the tab touch-target check to the task that ships th…
malinfossum Aug 13, 2026
5505d20
feat(focus): add empty states, area badges and one-tap filing
malinfossum Aug 13, 2026
6f9aaad
feat(focus): head the page with Focus, the day and the day's shape
malinfossum Aug 13, 2026
bcffe4b
feat(focus): confirm captures with a View action and unlist Focus fro…
malinfossum Aug 13, 2026
0e3c4d8
fix(focus): clear the pending tab selection on every mount
malinfossum Aug 13, 2026
33ca036
feat(focus): lay out the tab strip and the badge and time columns
malinfossum Aug 13, 2026
d935489
fix(focus): stop horizontal scroll and fix tab strip contrast, ring a…
malinfossum Aug 13, 2026
2e599b2
fix(focus): restore panel focus, fix stranded renames, and finish the…
malinfossum Aug 13, 2026
c5f4739
docs(focus): correct five comments that misdescribed their code
malinfossum Aug 13, 2026
24cf1ba
fix(focus): move the badge and time columns to 1024px
malinfossum Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

An ADHD-friendly task app — open source, local-first, zero bloat.

**Status:** v0.4.0 — 228 tests passing. **Live:** [malinfossum.github.io/ignite](https://malinfossum.github.io/ignite/)
**Status:** v0.4.0 — 247 tests passing. **Live:** [malinfossum.github.io/ignite](https://malinfossum.github.io/ignite/)

![Ignite in dark mode — the Today view showing a Next card and a Starred group, with the area sidebar and theme control on the left](docs/desktop_preview.png)
![Ignite in dark mode — the Focus view showing the tab strip, a Next card and a Today group, with the area sidebar and theme control on the left](docs/desktop_preview.png)

---

Expand All @@ -32,10 +32,11 @@ Built to replace a cluttered subscription task app with something minimal, free,
## Features

- **Quick capture** — type and go; the bar shows where the task will land, and asks which section when an area has more than one
- **Today view** — what's next, with relative time labels that tick live
- **Focus** — the landing surface, on four tabs: Today (what's overdue and what's due, led by what's next), Tomorrow, Starred, and Focus itself — the notepad every capture lands in until you file it
- **One-tap filing** — a note moves out of the notepad into any section without going through a menu
- **Areas & sections** — organize with a two-level hierarchy; create, rename, reorder, and delete
- **Add in place** — every section has its own add row, so a run of tasks goes in without leaving the keyboard
- **Tasks** — complete, star, reorder, rename in place, and move between sections; tasks in Focus file into any section in one tap
- **Tasks** — complete, star, reorder, rename in place, and move between sections
- **Scheduling** — give any task a date, and a time of day if it needs one; a task with no time reads as due that day rather than at midnight, and sorts after the day's timed tasks
- **Recurring tasks** — repeat daily, weekly, monthly, or yearly (every N); completing one advances it to the next date, keeping its time of day, and records when you last did it
- **Undo everything** — deletes (including a whole area and its contents) are undoable from the toast
Expand All @@ -53,7 +54,7 @@ Vanilla HTML, CSS, and JavaScript — no frameworks. Strict MVC with a `subscrib
- **Build:** Vite
- **Persistence:** IndexedDB (hand-rolled wrapper)
- **Offline:** hand-rolled service worker + web app manifest
- **Test:** Vitest + fake-indexeddb (228 tests)
- **Test:** Vitest + fake-indexeddb (247 tests)
- **Format / lint:** Biome
- **Deploy:** GitHub Pages via GitHub Actions

Expand Down
2,453 changes: 2,453 additions & 0 deletions docs/superpowers/plans/2026-08-13-ignite-v3-focus-surface.md

Large diffs are not rendered by default.

132 changes: 126 additions & 6 deletions main.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
--sidebar-width: 240px;
--sidebar-rail-width: 48px;
--main-max-width: var(--reading-width);
--main-padding: var(--space-5);
--main-padding: var(--space-4);
--radius: var(--radius-sm);
}

Expand Down Expand Up @@ -106,6 +106,12 @@ body {

#main {
width: 100%;
/* #main is a grid item in body's 1fr column. Without this, its automatic
minimum size is its content's min-content width — and the task row's
non-shrinking badge/time columns plus a nowrap title push that past
600px, forcing the column wider than the viewport between 768px and
~911px and causing horizontal page scroll. */
min-width: 0;
max-width: var(--main-max-width);
margin-inline: auto;
padding: var(--main-padding);
Expand Down Expand Up @@ -289,6 +295,70 @@ body {
position: static;
}

/* --- Focus tabs --- */
.focus-tabs {
display: flex;
gap: var(--space-1);
margin: 0 0 var(--space-4);
border-bottom: 1px solid var(--border);
/* Four 44px targets plus their counts do not fit 375px. The strip scrolls;
the targets do NOT shrink (spec §8.1).
Deliberately NOT `scrollbar-width: none`: at 375px the fourth tab sits off
screen, and the scrollbar is the only cue that it is there at all. Mobile
browsers auto-hide overlay scrollbars anyway, and the strip does not
overflow on desktop, so hiding it buys nothing and costs the affordance. */
overflow-x: auto;
}
.focus-tab {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: var(--space-2);
min-height: 44px;
padding: 0 var(--space-3);
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-muted);
font: inherit;
font-size: var(--text-sm);
cursor: pointer;
}
.focus-tab:hover {
color: var(--text);
}
/* The strip's own overflow-x: auto forces overflow-y to compute to auto too,
which clips painting to the padding box. A positive outline-offset (the
design system's default :focus-visible) draws 2px outside that box and gets
cut off on the first/last tab. Negative offset keeps the outline inside, but
the design system's box-shadow: var(--shadow-focus) glow is not offset, so it
still clips at the strip's edge on first/last tabs. The outline is the
WCAG-relevant indicator and is fully visible; this is accepted. */
.focus-tab:focus-visible {
outline-offset: -2px;
}
.focus-tab.is-active {
color: var(--text);
border-bottom-color: var(--accent);
}
/* A real token colour, never opacity: opacity compounds against whatever is
behind it, so a value that passes contrast on one surface fails on another.
Spec §8.2. `--text-sm` matches `.group__count`, which is the same idea in the
same palette — `--text-xs` (12px) would make this the smallest text anywhere
in the app, below even the 13.6px time label. `--text-muted` (not
`--text-faint`, which measures 4.21:1 against --surface-1 at 14px — under
WCAG AA's 4.5:1) — measured 6.70:1 light / 10.11:1 dark. The element-level
rule beats inheritance, so even in the active tab (which inherits --text), the
count renders --text-muted. No separate active-state override needed. */
.focus-tab__count {
color: var(--text-muted);
font-size: var(--text-sm);
}
.focus-panel:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

/* --- NEXT card --- */
.next-card {
margin: 0 0 var(--space-5);
Expand Down Expand Up @@ -345,7 +415,7 @@ body {
border-top: 1px solid var(--border-soft);
}
/* Round the end rows individually. Do NOT reach for `overflow: hidden` here — the
task ⋯ menu injects INSIDE the <li> as its last child (see today.js), so a
task ⋯ menu injects INSIDE the <li> as its last child (see focus.js), so a
clipping ancestor cuts the menu off. It bites worst on the last row of a group,
which is exactly where a menu most needs to overflow. */
.group__list > .task:first-child {
Expand Down Expand Up @@ -411,6 +481,47 @@ body {
color: var(--color-text-muted);
font-size: 0.85rem;
}
/* Phone and tablet: the row is title-first, so the badge drops out entirely
(spec §7). display:none takes it out of the accessibility tree too — accepted,
because the area is not conveyed to the sighted user at these widths either. */
.task__area-badge {
display: none;
color: var(--text-muted);
font-size: var(--text-sm);
}
/* Fixed bases so the columns line up across every group. `.task__title` is the
only flex:1 child, which is what absorbs the conditional ⟲ glyph; every OTHER
child must refuse to shrink or the alignment drifts row to row.
`.task__check` and `.task__recurring` are in this list on purpose: both
default to `flex: 0 1 auto`, so under pressure they compress by a fraction and
every column to their right moves with them. A partially-pinned row is the
version that looks right on seeded data and wrong on a real list, and §7 calls
this alignment the work. */
.task__check,
.task__recurring,
.task__star,
.task__menu-btn {
flex: 0 0 auto;
}
/* 1024px, not the 768px of spec §7: at 768 the sidebar already claims 240px, so
reserving 8rem + 6rem for these two columns left the title 6–9 characters.
Malin's call 2026-08-14 — desktop-only columns, tablet keeps the phone row. */
@media (min-width: 1024px) {
.task__area-badge {
display: block;
flex: 0 0 8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task__time-label {
flex: 0 0 6rem;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.task__file {
flex: 0 0 auto;
min-height: 44px;
Expand Down Expand Up @@ -543,15 +654,15 @@ body {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
pointer-events: auto;
}
.toast__undo {
.toast__action {
background: none;
border: none;
color: var(--color-accent);
cursor: pointer;
font: inherit;
padding: 0.2rem 0.4rem;
}
.toast__undo:hover {
.toast__action:hover {
text-decoration: underline;
}

Expand Down Expand Up @@ -1392,11 +1503,20 @@ body.is-drawer-open #scrim {
design system's reset.css already zeroes every margin globally. The area
route's default h1 weight/color already match what `.area__title` used to
set explicitly, so there's nothing left to fold in. */
.page-header__date {
.page-header__greeting {
margin: var(--space-1) 0 0;
color: var(--text-faint);
color: var(--text-muted);
font-size: var(--text-sm);
}
.page-header__summary {
margin: var(--space-1) 0 0;
color: var(--text-muted);
font-size: var(--text-sm);
}
.page-header__overdue {
color: var(--color-overdue);
font-weight: 600;
}

/* --- Area icon picker --- */
/* Auto-fit rather than a fixed column count. A fixed count cannot be right at
Expand Down
Loading
Loading