Rework the dashboard into two looks over one behaviour
A dashboard is a wall panel somebody hangs in their own hallway, so it now wears what they choose: a look, and a palette of their own colours. Two complete component sets live under `Dashboard/ui/` — `glass` (translucent panes over a slowly moving ground) and `material` (Material 3 tonal cards) — behind one prop contract. Every control's state, keyboard and `aria-` live in `ui/core` and are shared, so the two sets are the same dashboard drawn twice rather than two products: a set only decides what a control looks like while doing it. Four settings join the channel, each drivable by a flow like any other: `look`, `palette`, `background` and `touch`. A palette is an ordered list of hex colours — background, surface, primary, accent, text, then more chart colours — pasted from a coolors.co link or typed, written onto the canvas as the token variables everything already reads. Trailing roles are derived, so three colours are a whole dashboard, and derived text is held to AA rather than trusted (`theme.check.ts` measures it). A palette also decides light or dark, since its first colour is the ground. Widgets are measured against their own tile with container queries rather than against the viewport, animate through `motion`, and can be drawn without their title. The three reworks: - a bar draws a row per reading, up to eight, each in the dashboard's own data colours and each able to carry its own scale — replacing readings nested in one fill, which could only ever share one colour and stop at three. Documents written the old way are read as rows. - a chart's range picker moved to a column down its right-hand edge, which gives the plot back a whole row of a short tile. - the colour wheel became a disc: hue is the angle and saturation the distance from the middle, so a colour is one gesture rather than three, with brightness on a slider beside it. `index.css` and `lib/motion.ts` are untouched — the dashboard overrides token *values* on its canvas, never the blocks the two repos share.
This commit is contained in:
@@ -1,28 +1,13 @@
|
||||
import { useQueries } from "@tanstack/react-query"
|
||||
import { Link, type LinkProps } from "@tanstack/react-router"
|
||||
import type { LinkProps } from "@tanstack/react-router"
|
||||
|
||||
import { ICONS } from "@/components/Dashboard/icons"
|
||||
import { dashboardQueryOptions } from "@/components/Dashboard/queries"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useUi } from "./ui"
|
||||
|
||||
/** How much room the rail takes, canvas insets included. Mirrors the side
|
||||
* panel's `27rem`: the button plus the gutters either side of it. */
|
||||
export const RAIL_INSET = "4.5rem"
|
||||
|
||||
/** Two letters off the title, so a rail of four reads as four different things. */
|
||||
function initials(label: string): string {
|
||||
const words = label.split(/[\s_-]+/).filter(Boolean)
|
||||
if (words.length === 0) return "?"
|
||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
||||
return (words[0][0] + words[1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Switching between the dashboards one panel was assigned.
|
||||
*
|
||||
@@ -32,6 +17,9 @@ function initials(label: string): string {
|
||||
* the grid itself never changes, so an arrangement made without the rail still
|
||||
* fits with it.
|
||||
*
|
||||
* Drawn in the dashboard's own look, because it hangs on the same screen: a
|
||||
* rail wearing the app's chrome beside a glass panel is two designs at once.
|
||||
*
|
||||
* Reading the dashboards it links to is also what fills the labels, and it
|
||||
* warms the cache for the neighbours so a switch draws immediately.
|
||||
*/
|
||||
@@ -39,68 +27,23 @@ export function PanelRail({
|
||||
dashboards,
|
||||
current,
|
||||
linkFor,
|
||||
className,
|
||||
}: {
|
||||
dashboards: string[]
|
||||
current: string
|
||||
linkFor: (name: string) => LinkProps
|
||||
className?: string
|
||||
}) {
|
||||
const { Rail } = useUi()
|
||||
const entries = useQueries({
|
||||
queries: dashboards.map((name) => dashboardQueryOptions(name)),
|
||||
combine: (results) =>
|
||||
results.map((result, index) => ({
|
||||
name: dashboards[index],
|
||||
label: result.data?.title || dashboards[index],
|
||||
icon: result.data?.icon ?? "",
|
||||
active: dashboards[index] === current,
|
||||
link: linkFor(dashboards[index]),
|
||||
})),
|
||||
})
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid="panel-rail"
|
||||
className={cn(
|
||||
"pointer-events-auto absolute inset-y-4 left-4 z-10 flex w-12 flex-col items-center gap-1 rounded-lg border border-border bg-card/80 p-1 shadow-e2 backdrop-blur-md",
|
||||
// More dashboards than the column is tall still scroll, but no bar is
|
||||
// ever drawn: a wall panel is swiped, and there is no room for one
|
||||
// anyway. `w-12` less `p-1` either side is exactly the 40px button, so
|
||||
// a classic vertical bar claiming its gutter is what pushed the buttons
|
||||
// out sideways and put a horizontal bar under them — `overflow-y` alone
|
||||
// computes `overflow-x` to `auto` rather than leaving it visible.
|
||||
"overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{dashboards.map((name, index) => {
|
||||
const { label, icon } = entries[index]
|
||||
const Glyph = ICONS[icon]
|
||||
const active = name === current
|
||||
return (
|
||||
<Tooltip key={name}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
// `icon-lg` at every width on purpose: a rail is a touch
|
||||
// surface whatever the screen's size, and the panel it hangs
|
||||
// on is 1024 wide as often as it is 400.
|
||||
size="icon-lg"
|
||||
className={cn(
|
||||
"shrink-0 text-xs font-medium text-muted-foreground",
|
||||
active && "bg-accent text-foreground",
|
||||
)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
data-testid={`panel-rail-${name}`}
|
||||
>
|
||||
<Link {...linkFor(name)} aria-label={label}>
|
||||
{Glyph ? <Glyph className="size-5" /> : initials(label)}
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
return <Rail entries={entries} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user