feat(theme): polish Lines & Zen; soft gradient page backdrop

Lines theme:
- Row hover tint (light + dark), wrapped in @media (hover: hover) so
  it doesn't stick on touch, and gated to :not(.isSelected)
- Drop placeholder: 2px accent top line at 30% ghost opacity
- Subtasks get separator lines between them (not at block boundaries)
- Boards: hairline separators between cards in board columns
- Worklog year/month dividers: dashed -> solid
- Issue panel suggestions: hairline-separated list
- Suppress inherited dashed accent border on current task; faint accent
  tint for planner/board current task
- :focus-visible outline on task/planner-task

Zen + Lines:
- Banner: neutral elevated bg (6% black light, 5% white dark) so it
  reads as distinct without re-introducing card chrome

Page backdrop (page.scss body:before):
- Replace flat primary-color tint with a soft primary-tinted gradient
- Light: two glows in different primary shades (primary-200 top-left,
  primary-400 bottom-right) for tonal variation on white
- Dark: single primary corner glow + faint linear wash
- Themes that hide body:before (dark-base, arc, nord-snow-storm,
  dracula, velvet) keep their own backdrops
This commit is contained in:
Johannes Millan 2026-05-08 13:03:12 +02:00
parent b72c24eeff
commit 08d195a15d
3 changed files with 170 additions and 34 deletions

View file

@ -16,10 +16,15 @@ body task:last-of-type {
border-bottom: 1px solid var(--separator-color);
}
/* No separators on sub-tasks — they form a connected indented block under the parent */
body .sub-tasks task,
body .sub-tasks task:last-of-type {
/* Subtasks get separator lines between them.
Suppress only the boundaries so the indented block reads as part of its parent:
no top line on the first subtask (header block flows together), no bottom line
on the last subtask (parent's bottom border or next parent's top closes it). */
body .sub-tasks task:first-of-type {
border-top: none;
}
body .sub-tasks task:last-of-type {
border-bottom: none;
}
@ -44,16 +49,19 @@ body task.isSelected .sub-tasks .box {
background: transparent !important;
}
/* === Planner: tight list with horizontal separator lines === */
/* === Planner & Boards: tight list with horizontal separator lines === */
/* Remove the per-row gap so lines act as the only separator */
/* Remove the per-row gap so lines act as the only separator.
`.task-items` is the board column list same treatment. */
body .normal-tasks-items > *,
body .deadline-items > *,
body .scheduled-items > * {
body .scheduled-items > *,
body .task-items > planner-task {
margin-bottom: 0 !important;
margin-right: 0 !important;
}
/* Strip box-style chrome from every planner item host */
/* Strip box-style chrome from every planner/board item host */
body planner-task,
body planner-deadline-task,
body planner-repeat-projection,
@ -71,14 +79,16 @@ body planner-repeat-projection > div {
border-radius: 0 !important;
}
/* Top separator on every direct child of the normal/deadline lists */
/* Top separator on every direct child of the normal/deadline/board lists */
body .normal-tasks-items > *,
body .deadline-items > * {
body .deadline-items > *,
body .task-items > planner-task {
border-top: 1px solid var(--separator-color) !important;
}
body .normal-tasks-items > *:last-child,
body .deadline-items > *:last-child {
body .deadline-items > *:last-child,
body .task-items > planner-task:last-child {
border-bottom: 1px solid var(--separator-color) !important;
}
@ -91,3 +101,102 @@ body .scheduled-items > .scheduled-item {
body .scheduled-items > .scheduled-item:last-child {
border-bottom: 1px solid var(--separator-color);
}
/* === Row hover: subtle tint so the list feels responsive === */
/* Wrap in @media (hover: hover) — on touch, :hover sticks on the last-tapped row. */
@media (hover: hover) {
/* Use :has() so hovering a subtask doesn't also highlight its parent row.
The .box is the absolute-positioned background layer of a task. */
body task:hover:not(:has(task:hover)) > swipe-block > .inner-wrapper > .box {
background: rgba(0, 0, 0, 0.04) !important;
}
body.isDarkTheme
task:hover:not(:has(task:hover))
> swipe-block
> .inner-wrapper
> .box {
background: rgba(255, 255, 255, 0.03) !important;
}
body planner-task:hover {
background: rgba(0, 0, 0, 0.04) !important;
}
body.isDarkTheme planner-task:hover {
background: rgba(255, 255, 255, 0.03) !important;
}
/* Selection wins over hover */
body task.isSelected:hover > swipe-block > .inner-wrapper > .box {
background: var(--c-dark-10) !important;
}
body.isDarkTheme task.isSelected:hover > swipe-block > .inner-wrapper > .box {
background: var(--c-light-05) !important;
}
}
/* === Current task: suppress inherited border === */
/* The base theme paints a dashed accent border around the current task's box
too noisy for a hairline-only list. The task component already renders a
progress bar below the title, which is enough indication. */
body task.isCurrent .box,
body task.isCurrent.isCurrent.isCurrent.isCurrent.isCurrent .box {
border-color: transparent !important;
box-shadow: none !important;
}
/* Planner/board tasks don't render a progress bar without an indicator the
running task disappears. Use a faint accent tint instead of a border. */
body planner-task.isCurrent {
border: none !important;
background: color-mix(in srgb, var(--c-accent) 10%, transparent) !important;
}
/* === Drop indicator: ghost slot with an accent top line === */
/* Default opacity is 0.1 too faint between hairlines. Bump to 0.3 so the
ghost is visible and add a 2px accent top line marking the drop position. */
body task.cdk-drag-placeholder,
body planner-task.cdk-drag-placeholder {
opacity: 0.3 !important;
border-top: 2px solid var(--c-accent) !important;
}
/* === Keyboard focus: explicit outline so focus is never invisible === */
body task:focus-visible {
outline: 2px solid var(--palette-primary-400) !important;
outline-offset: -2px;
}
body planner-task:focus-visible {
outline: 2px solid var(--palette-primary-400) !important;
outline-offset: -2px;
}
/* === Worklog: switch dashed dividers to solid for visual consistency === */
body worklog .year,
body worklog .month {
border-top-style: solid;
}
/* === Issue panel suggestions: hairline-separated list === */
body issue-provider-tab .item-list {
gap: 0 !important;
}
body issue-preview-item {
background: transparent !important;
box-shadow: none !important;
border-radius: 0 !important;
border-top: 1px solid var(--separator-color);
}
body issue-preview-item:last-of-type {
border-bottom: 1px solid var(--separator-color);
}

View file

@ -255,3 +255,14 @@ body .week-month-btn:not(.active) {
app-root.isWorkViewScrolled task.isCurrent .box {
background: var(--bg) !important;
}
/* Banner: zen normally hides chrome; give it a neutral elevated bg so it
reads as distinct from the page without re-introducing primary color. */
body banner .content-wrapper {
background: rgba(0, 0, 0, 0.06) !important;
box-shadow: none !important;
}
body.isDarkTheme banner .content-wrapper {
background: rgba(255, 255, 255, 0.05) !important;
}

View file

@ -1,4 +1,3 @@
@use 'angular-material-css-vars' as mat-css-vars;
@use '../_common';
@use './mixins/media-queries';
@ -139,16 +138,17 @@ body {
opacity: 0;
}
&.isLightTheme {
// Soft primary-tinted gradient backdrop. Light and dark want different
// recipes the same single corner-glow that reads as warm on dark looks
// smudgy on white, so light gets two larger diffuse glows (primary +
// accent) for subtle depth without flat-tint feel.
// Themes that want a different/no backdrop hide body:before (dark-base,
// arc, nord-snow-storm, dracula, velvet do this).
&.isLightTheme,
&.isDarkTheme {
color: var(--text-color);
background: var(--bg);
&:before {
display: block;
opacity: 1;
filter: saturate(0.8);
background: mat-css-vars.mat-css-color(50, 1);
}
&.isDisableBackgroundTint {
background: var(--bg);
&:before {
@ -158,23 +158,39 @@ body {
}
}
&.isDarkTheme {
color: var(--text-color);
background: var(--bg);
&.isLightTheme:before {
display: block;
opacity: 1;
// Two glows in different primary shades primary-200 (soft pastel)
// for the top-left highlight, primary-400 (deeper) for bottom-right
// depth. Single-hue keeps the light-mode wash calm.
background:
radial-gradient(
ellipse 120% 80% at 0% -10%,
color-mix(in srgb, var(--palette-primary-200) 40%, transparent) 0%,
transparent 65%
),
radial-gradient(
ellipse 80% 60% at 100% 100%,
color-mix(in srgb, var(--palette-primary-400) 15%, transparent) 0%,
transparent 60%
);
}
&:before {
display: block;
opacity: 1;
filter: brightness(0.1);
background: mat-css-vars.mat-css-color(900, 1);
}
&.isDisableBackgroundTint {
background: var(--bg);
&:before {
display: none;
opacity: 0;
}
}
&.isDarkTheme:before {
display: block;
opacity: 1;
background:
radial-gradient(
ellipse 80% 60% at 10% -10%,
color-mix(in srgb, var(--c-primary) 10%, transparent) 0%,
transparent 55%
),
linear-gradient(
180deg,
color-mix(in srgb, var(--c-primary) 4%, transparent) 0%,
transparent 100%
);
}
// also hide material dialogs while import is in progress