diff options
| author | Adam Malczewski <[email protected]> | 2026-10-21 20:32:41 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-21 20:32:41 +0900 |
| commit | bac1e1c39398ddcb0866db510c11bed75ac3a9a8 (patch) | |
| tree | 5b2bde4e1db4683290ea0c1a42eb61c498a06402 /src | |
| parent | 564b2c1d03cf477d7cfc23414b60d1ec76904ecc (diff) | |
| download | dispatch-web-bac1e1c39398ddcb0866db510c11bed75ac3a9a8.tar.gz dispatch-web-bac1e1c39398ddcb0866db510c11bed75ac3a9a8.zip | |
feat(tabs): rotate three theme colours per group nesting level
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/tabs/index.ts | 4 | ||||
| -rw-r--r-- | src/features/tabs/tab-groups.test.ts | 31 | ||||
| -rw-r--r-- | src/features/tabs/tab-groups.ts | 16 | ||||
| -rw-r--r-- | src/features/tabs/ui.test.ts | 8 | ||||
| -rw-r--r-- | src/features/tabs/ui/TabGroupBox.svelte | 29 |
5 files changed, 79 insertions, 9 deletions
diff --git a/src/features/tabs/index.ts b/src/features/tabs/index.ts index 980e8ee..86ed83f 100644 --- a/src/features/tabs/index.ts +++ b/src/features/tabs/index.ts @@ -1,11 +1,13 @@ -export type { TabGroupForest, TabGroupNode } from "./tab-groups"; +export type { GroupTone, TabGroupForest, TabGroupNode } from "./tab-groups"; export { buildTabGroups, directGroupChildren, + GROUP_TONES, groupBleedPx, groupDescendants, groupGutterPx, groupParentCandidates, + groupTone, lastRowIsGroup, visibleGroupParents, } from "./tab-groups"; diff --git a/src/features/tabs/tab-groups.test.ts b/src/features/tabs/tab-groups.test.ts index 83f273c..a09dbc8 100644 --- a/src/features/tabs/tab-groups.test.ts +++ b/src/features/tabs/tab-groups.test.ts @@ -6,6 +6,7 @@ import { groupDescendants, groupGutterPx, groupParentCandidates, + groupTone, lastRowIsGroup, visibleGroupParents, } from "./tab-groups"; @@ -103,6 +104,36 @@ describe("groupDescendants / directGroupChildren / groupParentCandidates", () => }); }); +describe("groupTone", () => { + it("rotates three colours by level, starting at the outermost box", () => { + expect([1, 2, 3, 4, 5, 6, 7].map(groupTone)).toEqual([ + "secondary", + "success", + "accent", + "secondary", + "success", + "accent", + "secondary", + ]); + }); + + it("never gives a box the colour of its parent or grandparent", () => { + for (let level = 3; level <= 20; level++) { + expect(groupTone(level)).not.toBe(groupTone(level - 1)); + expect(groupTone(level)).not.toBe(groupTone(level - 2)); + } + }); + + it("never uses primary (reserved for the active tab)", () => { + for (let level = 1; level <= 6; level++) expect(groupTone(level)).not.toBe("primary"); + }); + + it("stays in range for out-of-range levels", () => { + expect(groupTone(0)).toBe("accent"); + expect(groupTone(-1)).toBe("success"); + }); +}); + describe("groupBleedPx / groupGutterPx", () => { it("is zero without groups", () => { expect(groupGutterPx(0)).toBe(0); diff --git a/src/features/tabs/tab-groups.ts b/src/features/tabs/tab-groups.ts index 12870ee..c7406d8 100644 --- a/src/features/tabs/tab-groups.ts +++ b/src/features/tabs/tab-groups.ts @@ -153,6 +153,22 @@ export function lastRowIsGroup(children: readonly TabGroupNode[]): boolean { } /** + * The DaisyUI theme colour a group box at nesting `level` (1 = outermost) is + * tinted with. Three colours ROTATE by level, so a box always differs from + * the box around it AND from the one around that (with two colours a box + * would match its grandparent). `primary` is deliberately excluded — it marks + * the active tab, which must stay legible inside any box. + */ +export const GROUP_TONES = ["secondary", "success", "accent"] as const; +export type GroupTone = (typeof GROUP_TONES)[number]; + +export function groupTone(level: number): GroupTone { + const n = GROUP_TONES.length; + const i = (((Math.trunc(level) - 1) % n) + n) % n; // non-negative even for level < 1 + return GROUP_TONES[i] ?? GROUP_TONES[0]; +} + +/** * Horizontal bleed (px) of a group box at nesting `level` (1 = outermost) * when the deepest box is `maxDepth`. A box bleeds OUTWARD by this much (a * negative margin + equal padding), so the cards inside every box sit on diff --git a/src/features/tabs/ui.test.ts b/src/features/tabs/ui.test.ts index 2569438..8785ea9 100644 --- a/src/features/tabs/ui.test.ts +++ b/src/features/tabs/ui.test.ts @@ -383,9 +383,11 @@ describe("TabList", () => { s?.paddingLeft, ]); } - // Every box paints a 15% lavender tint, so nested boxes compound per level. - expect(outer).toHaveClass("bg-secondary/15"); - expect(inner).toHaveClass("bg-secondary/15"); + // Each level is tinted with its own rotating theme colour. + expect(outer?.dataset.tone).toBe("secondary"); + expect(inner?.dataset.tone).toBe("success"); + expect(outer?.style.getPropertyValue("--group-tone")).toBe("var(--color-secondary)"); + expect(inner?.style.getPropertyValue("--group-tone")).toBe("var(--color-success)"); expect(outer?.style.getPropertyValue("margin-inline")).toBe(`-${outer?.style.paddingLeft}`); }); diff --git a/src/features/tabs/ui/TabGroupBox.svelte b/src/features/tabs/ui/TabGroupBox.svelte index a941814..f9a3ae2 100644 --- a/src/features/tabs/ui/TabGroupBox.svelte +++ b/src/features/tabs/ui/TabGroupBox.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import type { Snippet } from "svelte"; import type { TabGroupNode } from "../tab-groups"; - import { groupBleedPx, lastRowIsGroup } from "../tab-groups"; + import { groupBleedPx, groupTone, lastRowIsGroup } from "../tab-groups"; import type { Tab } from "../tabs"; import TabGroupBox from "./TabGroupBox.svelte"; @@ -13,9 +13,11 @@ * inside stay on the same two columns as top-level cards: every card keeps * identical dimensions at every depth. The padding is EQUAL on all four * sides (= the bleed) so the box looks even — except below a nested box on - * the last row, where it is trimmed to match that box's side gaps. Every - * box paints a 15% secondary (lavender) tint, which compounds: each - * nesting level reads a little stronger than the one around it. + * the last row, where it is trimmed to match that box's side gaps. Each + * box is tinted with one of three theme colours ROTATING by level + * (`groupTone`), painted OPAQUE (mixed into the sidebar well colour) so a + * nested box never blends with the box around it, plus a border in the same + * colour so every box edge reads at a glance. */ let { node, @@ -32,6 +34,7 @@ } = $props(); const bleed = $derived(groupBleedPx(level, maxDepth)); + const tone = $derived(groupTone(level)); // Even spacing: when the last row is a nested box, the gap below it would // be this box's full padding while its side gaps are only the DIFFERENCE // in bleed (the nested box bleeds outward too) — trim the bottom to match. @@ -41,13 +44,15 @@ </script> <div - class="col-span-2 rounded-lg bg-secondary/15" + class="tab-group-box col-span-2 rounded-lg" + style:--group-tone="var(--color-{tone})" style:margin-inline="-{bleed}px" style:padding="{bleed}px {bleed}px {padBottom}px" role="group" aria-label={`Subagents of ${node.tab.title}`} data-tab-group={node.tab.conversationId} data-level={level} + data-tone={tone} > <div class="grid grid-flow-row-dense grid-cols-2 gap-1.5"> {@render card(node.tab, true, node.children.length)} @@ -60,3 +65,17 @@ {/each} </div> </div> + +<style> + /* Opaque tint: the tone mixed into the sidebar's `.inset-well` colour (the + surface the tab grid sits on), so nested boxes replace — never compound + with — the colour around them. */ + .tab-group-box { + background-color: color-mix( + in oklab, + var(--group-tone) 16%, + var(--inset-well-bg, var(--color-base-200)) + ); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--group-tone) 55%, transparent); + } +</style> |
