Draw the panel as a panel
Five things a wall panel showed that a wall panel should not. **A tile's body no longer clips.** It scrolled, and a box that scrolls also cuts whatever crosses its edge — which took the glow off a lit button at exactly the width where the button filled its tile, and off a gauge's arc at exactly the height where the dial filled its own. Only what is written or listed asks for a scroller now; everything else is a picture drawn to fit, and what overflows is left to the frame, which clips at the tile's edge where a shadow has already faded out. The slider's phantom scrollbar goes with it. **The selector is a selector.** Named for what it does rather than what it is, and the choice it is holding is held in the dashboard's own primary — a pill that slides between the options rather than a grey one that had to be looked for. The stored type is untouched, so no document changes meaning. **The arrangement is held off the panel's edges**, by the same distance it holds between two widgets. The ground is not held off with it: a background covers the whole panel, and only what is arranged on it has a margin. No stored panel loses a row to it. **The rail is drawn on the panel.** It was chrome bolted to the edge of the screen beside the canvas — in the app's own design rather than the dashboard's, and on a scaled canvas not even lined up with it. It now takes a column out of the canvas the way the margin does, scaled with it and wearing its look. Which cell each widget sits in is unchanged; only how big a cell is. Two of these were the same mistake twice: an unlayered rule stating `position` for everything wearing a class — `.gl-surface` on a rail placed by a utility, and a blanket lift over every child of a pressable on a pill placed by `layoutId`. Both now say it one element at a time.
This commit is contained in:
@@ -54,8 +54,8 @@ import { appPath } from "@/lib/portal"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { handleError } from "@/utils"
|
||||
import {
|
||||
areaOf,
|
||||
CanvasSurface,
|
||||
canvasOf,
|
||||
columnsOf,
|
||||
type Dashboard,
|
||||
DashboardView,
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
sectionsOf,
|
||||
widgetsOf,
|
||||
} from "./DashboardView"
|
||||
import { PanelRail, RAIL_INSET } from "./PanelRail"
|
||||
import { PanelRail } from "./PanelRail"
|
||||
import { DashboardPanel, WidgetPanel } from "./panels"
|
||||
import {
|
||||
dashboardKeys,
|
||||
@@ -327,7 +327,6 @@ export function DashboardEditor({
|
||||
// Every widget the panel shows, not just the first section's — a document
|
||||
// written with several sections is one arrangement here, as it is there.
|
||||
const widgets = page ? flatWidgets(page) : []
|
||||
const canvas = canvasOf(draft)
|
||||
|
||||
const updateWidgets = (next: WidgetDef[]) => {
|
||||
if (!page) return
|
||||
@@ -381,8 +380,25 @@ export function DashboardEditor({
|
||||
),
|
||||
)
|
||||
|
||||
// A dashboard hanging on a panel beside others is drawn with a rail on it,
|
||||
// which takes room off the canvas. Show that here rather than letting someone
|
||||
// arrange against a width the wall does not have.
|
||||
// ponytail: the first such panel wins — the others differ only in scale.
|
||||
const { data: panels } = useQuery(panelsQueryOptions())
|
||||
const railDashboards =
|
||||
(panels?.panels ?? []).find(
|
||||
(panel) =>
|
||||
(panel.dashboards ?? []).length > 1 &&
|
||||
(panel.dashboards ?? []).includes(draft.name),
|
||||
)?.dashboards ?? null
|
||||
const hasRail = Boolean(railDashboards) && !stacked
|
||||
// What the arrangement is laid out in: the canvas less its margin, and less
|
||||
// the rail's column when the panel carries one.
|
||||
const area = areaOf(draft, hasRail)
|
||||
|
||||
const layout = layoutOf(widgets, columns)
|
||||
const rows = rowsOf(draft)
|
||||
const rows = rowsOf(draft, hasRail)
|
||||
|
||||
// maxRows below only constrains a *drag*. A stored placement is corrected
|
||||
// against `cols` and nothing else, so a canvas that shrank leaves whatever
|
||||
// now falls past its bottom edge drawn under the clip.
|
||||
@@ -428,18 +444,6 @@ export function DashboardEditor({
|
||||
const active = widgets.find((widget) => widget.id === selected) ?? null
|
||||
const panelOpen = Boolean(active) || settingsOpen
|
||||
|
||||
// A dashboard hanging on a panel beside others is drawn with a rail over it,
|
||||
// which takes room off the canvas. Show that here rather than letting someone
|
||||
// arrange against a width the wall does not have.
|
||||
// ponytail: the first such panel wins — the others differ only in scale.
|
||||
const { data: panels } = useQuery(panelsQueryOptions())
|
||||
const railDashboards =
|
||||
(panels?.panels ?? []).find(
|
||||
(panel) =>
|
||||
(panel.dashboards ?? []).length > 1 &&
|
||||
(panel.dashboards ?? []).includes(draft.name),
|
||||
)?.dashboards ?? null
|
||||
|
||||
const setEdit = (next: boolean) => {
|
||||
setSelected(null)
|
||||
setSettingsOpen(false)
|
||||
@@ -492,19 +496,37 @@ export function DashboardEditor({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CanvasSurface dashboard={draft} dots={edit}>
|
||||
<CanvasSurface
|
||||
dashboard={draft}
|
||||
dots={edit}
|
||||
rail={
|
||||
railDashboards ? (
|
||||
<PanelRail
|
||||
dashboards={railDashboards}
|
||||
current={draft.name}
|
||||
// Editing one dashboard of a panel and editing its neighbour are
|
||||
// the same job, so the rail keeps whichever mode this one is in.
|
||||
linkFor={(name) => ({
|
||||
to: "/dashboards/$name",
|
||||
params: { name },
|
||||
search: edit ? { edit: true } : {},
|
||||
})}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{(scale) =>
|
||||
!edit ? (
|
||||
<DashboardView dashboard={draft} />
|
||||
<DashboardView dashboard={draft} rail={hasRail} />
|
||||
) : (
|
||||
<GridLayout
|
||||
width={canvas.width}
|
||||
width={area.width}
|
||||
layout={layout}
|
||||
onLayoutChange={applyLayout}
|
||||
positionStrategy={scaledStrategy(scale)}
|
||||
gridConfig={{
|
||||
cols: columns,
|
||||
rowHeight: rowHeightOf(draft),
|
||||
rowHeight: rowHeightOf(draft, hasRail),
|
||||
margin: [GRID_GAP, GRID_GAP],
|
||||
containerPadding: [0, 0],
|
||||
// The canvas is the constraint: nothing may be dragged off the
|
||||
@@ -534,28 +556,11 @@ export function DashboardEditor({
|
||||
|
||||
return (
|
||||
<LookProvider dashboard={draft}>
|
||||
{railDashboards && !stacked ? (
|
||||
<PanelRail
|
||||
dashboards={railDashboards}
|
||||
current={draft.name}
|
||||
// Editing one dashboard of a panel and editing its neighbour are the
|
||||
// same job, so the rail keeps whichever mode this one is in.
|
||||
linkFor={(name) => ({
|
||||
to: "/dashboards/$name",
|
||||
params: { name },
|
||||
search: edit ? { edit: true } : {},
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 overflow-hidden px-4 pb-24 pt-20 transition-[padding] duration-200",
|
||||
panelOpen && "md:pr-[27rem]",
|
||||
)}
|
||||
style={
|
||||
railDashboards && !stacked ? { paddingLeft: RAIL_INSET } : undefined
|
||||
}
|
||||
data-testid="dashboard-canvas"
|
||||
>
|
||||
<PaletteProvider dashboard={draft}>{body}</PaletteProvider>
|
||||
@@ -566,7 +571,6 @@ export function DashboardEditor({
|
||||
"pointer-events-none absolute inset-0 transition-[right] duration-200",
|
||||
panelOpen && "md:right-[27rem]",
|
||||
)}
|
||||
style={railDashboards && !stacked ? { left: RAIL_INSET } : undefined}
|
||||
>
|
||||
<CanvasTitle>
|
||||
<span className="truncate px-3 py-1.5 text-sm font-medium">
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useDashboardBackground,
|
||||
} from "./settings"
|
||||
import { useUi } from "./ui"
|
||||
import { showTitle } from "./ui/core/config"
|
||||
import { scrolls, showTitle } from "./ui/core/config"
|
||||
import { LookProvider, useCanvasRoot, useLook } from "./ui/core/look"
|
||||
import { gridStagger, LOOK } from "./ui/core/motion"
|
||||
import { WidgetBody, widgetIssue } from "./widgets"
|
||||
@@ -44,6 +44,26 @@ export const CANVAS_PRESETS = [
|
||||
/** The gap between widgets, in pixels. Matches the `gap-3` view mode uses. */
|
||||
export const GRID_GAP = 12
|
||||
|
||||
/**
|
||||
* How far the arrangement keeps from the panel's own edges.
|
||||
*
|
||||
* The same as the gap between two widgets, so a tile sits as far from the edge
|
||||
* of the screen as it does from its neighbour. The ground is not inset with it
|
||||
* — a background covers the whole panel, and only what is arranged on it is
|
||||
* held off the edge.
|
||||
*/
|
||||
export const GRID_INSET = GRID_GAP
|
||||
|
||||
/**
|
||||
* The column a dashboard-switching rail takes out of the canvas, in canvas
|
||||
* pixels.
|
||||
*
|
||||
* The rail is drawn on the panel rather than beside it — it is part of what
|
||||
* the screen shows — so it is inside the canvas, scaled with it, wearing the
|
||||
* dashboard's own look.
|
||||
*/
|
||||
export const RAIL_INSET = 72
|
||||
|
||||
/**
|
||||
* The generated client marks every list optional, because the server fills
|
||||
* them in. These three keep that from spreading through the components.
|
||||
@@ -80,22 +100,43 @@ const DEFAULT_COLUMN_WIDTH =
|
||||
*/
|
||||
const CELL_ASPECT = DEFAULT_COLUMN_WIDTH / DEFAULT_ROW_HEIGHT
|
||||
|
||||
const columnWidthOf = (dashboard: Dashboard) =>
|
||||
(canvasOf(dashboard).width + GRID_GAP) / columnsOf(dashboard) - GRID_GAP
|
||||
/**
|
||||
* The part of the canvas an arrangement is laid out in.
|
||||
*
|
||||
* The margin off every edge, plus the rail's column when the panel carries
|
||||
* one: a rail is part of the screen rather than something beside it, so the
|
||||
* room it takes comes out of the canvas the same way the margin does. Which
|
||||
* cell each widget sits in never changes — only how big a cell is.
|
||||
*/
|
||||
export const areaOf = (dashboard: Dashboard, rail = false) => {
|
||||
const { width, height } = canvasOf(dashboard)
|
||||
const left = GRID_INSET + (rail ? RAIL_INSET : 0)
|
||||
return {
|
||||
left,
|
||||
top: GRID_INSET,
|
||||
width: Math.max(1, width - left - GRID_INSET),
|
||||
height: Math.max(1, height - 2 * GRID_INSET),
|
||||
}
|
||||
}
|
||||
|
||||
const columnWidthOf = (dashboard: Dashboard, rail = false) =>
|
||||
(areaOf(dashboard, rail).width + GRID_GAP) / columnsOf(dashboard) - GRID_GAP
|
||||
|
||||
/** One grid row, in pixels — the unit widget heights are multiples of. */
|
||||
export const rowHeightOf = (dashboard: Dashboard) =>
|
||||
Math.round(columnWidthOf(dashboard) / CELL_ASPECT)
|
||||
export const rowHeightOf = (dashboard: Dashboard, rail = false) =>
|
||||
Math.round(columnWidthOf(dashboard, rail) / CELL_ASPECT)
|
||||
|
||||
/** Distance between two row origins — what a widget's `y` is counted in. */
|
||||
export const rowPitchOf = (dashboard: Dashboard) =>
|
||||
rowHeightOf(dashboard) + GRID_GAP
|
||||
export const rowPitchOf = (dashboard: Dashboard, rail = false) =>
|
||||
rowHeightOf(dashboard, rail) + GRID_GAP
|
||||
|
||||
/** How many rows of the grid fit in the canvas; the rest is off the panel. */
|
||||
export const rowsOf = (dashboard: Dashboard) =>
|
||||
export const rowsOf = (dashboard: Dashboard, rail = false) =>
|
||||
Math.max(
|
||||
1,
|
||||
Math.floor((canvasOf(dashboard).height + GRID_GAP) / rowPitchOf(dashboard)),
|
||||
Math.floor(
|
||||
(areaOf(dashboard, rail).height + GRID_GAP) / rowPitchOf(dashboard, rail),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -114,11 +155,14 @@ export const rowsOf = (dashboard: Dashboard) =>
|
||||
export function CanvasSurface({
|
||||
dashboard,
|
||||
dots,
|
||||
rail,
|
||||
children,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
/** Draw the placement grid. A wall panel is not being arranged, so: no. */
|
||||
dots?: boolean
|
||||
/** The dashboard-switching rail, drawn on the panel rather than beside it. */
|
||||
rail?: React.ReactNode
|
||||
children: (scale: number) => React.ReactNode
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
@@ -140,6 +184,7 @@ export function CanvasSurface({
|
||||
const { width, height } = canvasOf(dashboard)
|
||||
const scale = Math.min(box.width / width, box.height / height)
|
||||
const root = useCanvasRoot(dashboard)
|
||||
const area = areaOf(dashboard, Boolean(rail))
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative size-full overflow-hidden">
|
||||
@@ -165,20 +210,33 @@ export function CanvasSurface({
|
||||
top: (box.height - height * scale) / 2,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
// One dot per cell corner. The column pitch the grid snaps to
|
||||
// is (width + gap) / columns; a row is one row plus the gap.
|
||||
"--dot-x": `${(width + GRID_GAP) / columnsOf(dashboard)}px`,
|
||||
"--dot-y": `${rowPitchOf(dashboard)}px`,
|
||||
// One dot per cell corner, counted across the arranged area
|
||||
// rather than the whole panel: the margin is not a cell.
|
||||
"--dot-x": `${(area.width + GRID_GAP) / columnsOf(dashboard)}px`,
|
||||
"--dot-y": `${rowPitchOf(dashboard, Boolean(rail))}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* Full bleed, and under everything: a background covers the
|
||||
panel, margin included. */}
|
||||
<CanvasGround dashboard={dashboard} />
|
||||
{/* Over the ground rather than under it: the dots are what is
|
||||
being arranged against, and a wallpaper must not hide them. */}
|
||||
{dots ? (
|
||||
<div className="dot-canvas pointer-events-none absolute inset-0" />
|
||||
) : null}
|
||||
{children(scale)}
|
||||
{rail}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
left: area.left,
|
||||
top: area.top,
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
}}
|
||||
>
|
||||
{/* Over the ground rather than under it: the dots are what is
|
||||
being arranged against, and a wallpaper must not hide them. */}
|
||||
{dots ? (
|
||||
<div className="dot-canvas pointer-events-none absolute inset-0" />
|
||||
) : null}
|
||||
{children(scale)}
|
||||
</div>
|
||||
</div>
|
||||
</LookProvider>
|
||||
) : null}
|
||||
@@ -293,11 +351,14 @@ export function DashboardView({
|
||||
dashboard,
|
||||
renderWidget,
|
||||
stacked,
|
||||
rail,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
renderWidget?: (widget: WidgetDef) => React.ReactNode
|
||||
/** One column at the viewport's width, for a phone. */
|
||||
stacked?: boolean
|
||||
/** Whether the panel carries a rail, which takes a column of the canvas. */
|
||||
rail?: boolean
|
||||
}) {
|
||||
// ponytail: the first page only. A panel carries several whole dashboards
|
||||
// — that is what the rail is for — so nothing writes a second page.
|
||||
@@ -343,6 +404,7 @@ export function DashboardView({
|
||||
widgets={widgets}
|
||||
columns={columns}
|
||||
stacked={stacked}
|
||||
rail={rail}
|
||||
renderWidget={renderWidget}
|
||||
/>
|
||||
</PaletteProvider>
|
||||
@@ -363,12 +425,14 @@ function WidgetGrid({
|
||||
widgets,
|
||||
columns,
|
||||
stacked,
|
||||
rail,
|
||||
renderWidget,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
widgets: WidgetDef[]
|
||||
columns: number
|
||||
stacked?: boolean
|
||||
rail?: boolean
|
||||
renderWidget?: (widget: WidgetDef) => React.ReactNode
|
||||
}) {
|
||||
const { Frame } = useUi()
|
||||
@@ -385,7 +449,7 @@ function WidgetGrid({
|
||||
"--widget-cols": columns,
|
||||
// The row height follows the canvas, so the CSS grid and the
|
||||
// editor's grid library cannot drift apart.
|
||||
"--row-height": `${rowHeightOf(dashboard)}px`,
|
||||
"--row-height": `${rowHeightOf(dashboard, rail)}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
@@ -403,6 +467,7 @@ function WidgetGrid({
|
||||
<Frame
|
||||
title={showTitle(widget) ? widget.title : undefined}
|
||||
issue={widgetIssue(widget)}
|
||||
scroll={scrolls(widget)}
|
||||
>
|
||||
<WidgetBody widget={widget} dashboard={dashboard.name} />
|
||||
</Frame>
|
||||
|
||||
@@ -4,10 +4,6 @@ import type { LinkProps } from "@tanstack/react-router"
|
||||
import { dashboardQueryOptions } from "@/components/Dashboard/queries"
|
||||
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"
|
||||
|
||||
/**
|
||||
* Switching between the dashboards one panel was assigned.
|
||||
*
|
||||
|
||||
@@ -21,11 +21,14 @@ import { LookProvider } from "@/components/Dashboard/ui/core/look"
|
||||
export function PanelSurface({
|
||||
dashboard,
|
||||
stacked,
|
||||
rail,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
/** A landscape arrangement scaled onto a phone comes out at about a fifth of
|
||||
* its size, which reads as nothing at all. Stack it instead. */
|
||||
stacked?: boolean
|
||||
/** The way between this panel's dashboards, drawn on the canvas itself. */
|
||||
rail?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="relative size-full">
|
||||
@@ -34,8 +37,8 @@ export function PanelSurface({
|
||||
) : (
|
||||
// The panel's own surface, scaled to fit. No dots: nothing is being
|
||||
// arranged here.
|
||||
<CanvasSurface dashboard={dashboard}>
|
||||
{() => <DashboardView dashboard={dashboard} />}
|
||||
<CanvasSurface dashboard={dashboard} rail={rail}>
|
||||
{() => <DashboardView dashboard={dashboard} rail={Boolean(rail)} />}
|
||||
</CanvasSurface>
|
||||
)}
|
||||
{/* Outside the canvas, so it needs the look stated for it. */}
|
||||
|
||||
@@ -67,6 +67,19 @@ export const fractionOf = (
|
||||
export const showTitle = (widget: WidgetDef): boolean =>
|
||||
config(widget).show_title !== false
|
||||
|
||||
/**
|
||||
* Whether this widget's body may scroll.
|
||||
*
|
||||
* Only what is written or listed: a note is as long as it is, and an agenda
|
||||
* has as many entries as the week does. Everything else is a picture drawn to
|
||||
* fit the tile, and a picture that scrolls is one nobody can see the rest of —
|
||||
* as well as a box that clips whatever glow or shadow crosses its edge.
|
||||
*/
|
||||
export const scrolls = (widget: WidgetDef): boolean =>
|
||||
widget.type === "markdown" ||
|
||||
widget.type === "agenda" ||
|
||||
widget.type === "notification"
|
||||
|
||||
/** Radix hands back a string; the message wants whatever was configured. */
|
||||
export function asOriginal(
|
||||
selected: string,
|
||||
|
||||
@@ -36,6 +36,8 @@ export type FrameProps = {
|
||||
issue?: string | null
|
||||
/** Make this widget draggable in the editor. */
|
||||
grip?: boolean
|
||||
/** Let the body scroll. Only what is written or listed asks for it. */
|
||||
scroll?: boolean
|
||||
selected?: boolean
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>
|
||||
children: React.ReactNode
|
||||
|
||||
@@ -51,7 +51,7 @@ export function Button({
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="gl-glow" aria-hidden />
|
||||
<span className="min-w-0 truncate">{children}</span>
|
||||
<span className="gl-lift min-w-0 truncate">{children}</span>
|
||||
</motion.button>
|
||||
)
|
||||
}
|
||||
@@ -181,11 +181,14 @@ export function Select({
|
||||
className="gl-field gl-pressable justify-between"
|
||||
>
|
||||
<span className="gl-glow" aria-hidden />
|
||||
<span className="min-w-0 truncate">
|
||||
<span className="gl-lift min-w-0 truncate">
|
||||
<SelectPrimitive.Value placeholder="Choose" />
|
||||
</span>
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="size-4 shrink-0 opacity-60" aria-hidden />
|
||||
<ChevronDown
|
||||
className="gl-lift size-4 shrink-0 opacity-60"
|
||||
aria-hidden
|
||||
/>
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
|
||||
@@ -94,6 +94,7 @@ export function Frame({
|
||||
title,
|
||||
issue,
|
||||
grip,
|
||||
scroll,
|
||||
selected,
|
||||
onClick,
|
||||
children,
|
||||
@@ -121,7 +122,9 @@ export function Frame({
|
||||
{issue ? <Issue issue={issue} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="dui-frame-body">{children}</div>
|
||||
<div className={cn("dui-frame-body", scroll && "dui-frame-scroll")}>
|
||||
{children}
|
||||
</div>
|
||||
{/* No header to sit in, so the fault takes the corner instead. */}
|
||||
{!title && issue ? (
|
||||
<div className="dui-frame-corner">
|
||||
@@ -155,7 +158,7 @@ export function Rail({ entries }: RailProps) {
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid={TESTID.rail}
|
||||
className={cn(
|
||||
"gl-surface gl-rail pointer-events-auto absolute inset-y-4 left-4 z-10 flex w-14 flex-col items-center gap-1 p-2",
|
||||
"gl-surface gl-rail pointer-events-auto absolute inset-y-3 left-3 z-10 flex w-12 flex-col items-center gap-1 p-1",
|
||||
"overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -95,8 +95,11 @@
|
||||
|
||||
/* --- surfaces ------------------------------------------------------------ */
|
||||
|
||||
/* No `position` of its own: this is worn by a tile, a rail and a notice alike,
|
||||
and each of those is placed by whatever puts it there. Stating it here won
|
||||
over the utility that placed them — an unlayered rule beats a layered one —
|
||||
and collapsed the rail into the flow. */
|
||||
.gl-surface {
|
||||
position: relative;
|
||||
border: 1px solid var(--gl-border);
|
||||
border-radius: var(--gl-radius);
|
||||
background: var(--gl-bg);
|
||||
@@ -181,7 +184,12 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gl-pressable > :not(.gl-glow) {
|
||||
/* Lifted one at a time rather than by a blanket rule over every child: that
|
||||
one restated `position` on things already placed — a sliding pill, a switch's
|
||||
thumb — and dropped them back into the flow. */
|
||||
.gl-switch-thumb,
|
||||
.gl-segment-label,
|
||||
.gl-lift {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -316,7 +324,7 @@
|
||||
}
|
||||
|
||||
.gl-segment[data-active] {
|
||||
color: var(--foreground);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.gl-segment-thumb {
|
||||
@@ -324,13 +332,11 @@
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
border-radius: 9999px;
|
||||
background: var(--gl-bg-strong);
|
||||
box-shadow: var(--gl-inset);
|
||||
background: color-mix(in srgb, var(--primary) 85%, transparent);
|
||||
box-shadow: var(--gl-glow), var(--gl-inset);
|
||||
}
|
||||
|
||||
.gl-segment-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -80,7 +80,7 @@ export function Button({
|
||||
onClick={onClick}
|
||||
>
|
||||
<Skin presses={presses} done={done} />
|
||||
<span className="min-w-0 truncate">{children}</span>
|
||||
<span className="m3-lift min-w-0 truncate">{children}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -207,11 +207,14 @@ export function Select({
|
||||
disabled={disabled}
|
||||
className="m3-field m3-pressable justify-between"
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
<span className="m3-lift min-w-0 truncate">
|
||||
<SelectPrimitive.Value placeholder="Choose" />
|
||||
</span>
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="size-4 shrink-0 opacity-60" aria-hidden />
|
||||
<ChevronDown
|
||||
className="m3-lift size-4 shrink-0 opacity-60"
|
||||
aria-hidden
|
||||
/>
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
|
||||
@@ -58,6 +58,7 @@ export function Frame({
|
||||
title,
|
||||
issue,
|
||||
grip,
|
||||
scroll,
|
||||
selected,
|
||||
onClick,
|
||||
children,
|
||||
@@ -85,7 +86,9 @@ export function Frame({
|
||||
{issue ? <Issue issue={issue} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="dui-frame-body">{children}</div>
|
||||
<div className={cn("dui-frame-body", scroll && "dui-frame-scroll")}>
|
||||
{children}
|
||||
</div>
|
||||
{/* No header to sit in, so the fault takes the corner instead — a widget
|
||||
drawn without its title still says when it is mis-wired. */}
|
||||
{!title && issue ? (
|
||||
@@ -121,7 +124,7 @@ export function Rail({ entries }: RailProps) {
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid={TESTID.rail}
|
||||
className={cn(
|
||||
"m3-rail pointer-events-auto absolute inset-y-4 left-4 z-10 flex w-14 flex-col items-center gap-1 p-2",
|
||||
"m3-rail pointer-events-auto absolute inset-y-3 left-3 z-10 flex w-12 flex-col items-center gap-1 p-1",
|
||||
// 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.
|
||||
"overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
||||
|
||||
@@ -100,7 +100,12 @@
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.m3-pressable > :not(.m3-state):not(.m3-ripple) {
|
||||
/* Lifted one at a time rather than by a blanket rule over every child: that
|
||||
one restated `position` on things already placed — a sliding pill, a switch's
|
||||
thumb — and dropped them back into the flow. */
|
||||
.m3-switch-thumb,
|
||||
.m3-segment-label,
|
||||
.m3-lift {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -233,7 +238,7 @@
|
||||
}
|
||||
|
||||
.m3-segment[data-active] {
|
||||
color: var(--card-foreground);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.m3-segment-thumb {
|
||||
@@ -241,12 +246,10 @@
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
border-radius: 9999px;
|
||||
background: var(--m3-sc);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.m3-segment-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -76,7 +76,7 @@ export const WIDGET_LABELS: Record<WidgetKind, string> = {
|
||||
switch: "Switch",
|
||||
slider: "Slider",
|
||||
input: "Input",
|
||||
dropdown: "Dropdown",
|
||||
dropdown: "Selector",
|
||||
color: "Colour",
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router"
|
||||
|
||||
import type { Dashboard } from "@/components/Dashboard/DashboardView"
|
||||
import { PanelRail, RAIL_INSET } from "@/components/Dashboard/PanelRail"
|
||||
import { PanelRail } from "@/components/Dashboard/PanelRail"
|
||||
import { PanelSurface } from "@/components/Dashboard/PanelSurface"
|
||||
import {
|
||||
dashboardQueryOptions,
|
||||
@@ -85,25 +85,29 @@ function PanelRoute() {
|
||||
root.className,
|
||||
stacked ? "overflow-y-auto" : "overflow-hidden",
|
||||
)}
|
||||
style={{
|
||||
...root.style,
|
||||
...(rail ? { paddingLeft: RAIL_INSET } : {}),
|
||||
}}
|
||||
style={root.style}
|
||||
>
|
||||
<LookProvider dashboard={dashboard as Dashboard | undefined}>
|
||||
{rail && current ? (
|
||||
<PanelRail
|
||||
dashboards={dashboards}
|
||||
current={current}
|
||||
linkFor={(name) => ({
|
||||
to: "/panel/$id",
|
||||
params: { id },
|
||||
search: { d: name },
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
{dashboard ? (
|
||||
<PanelSurface dashboard={dashboard as Dashboard} stacked={stacked} />
|
||||
<PanelSurface
|
||||
dashboard={dashboard as Dashboard}
|
||||
stacked={stacked}
|
||||
// Drawn on the panel rather than beside it: a screen showing four
|
||||
// dashboards shows the way between them too.
|
||||
rail={
|
||||
rail && current ? (
|
||||
<PanelRail
|
||||
dashboards={dashboards}
|
||||
current={current}
|
||||
linkFor={(name) => ({
|
||||
to: "/panel/$id",
|
||||
params: { id },
|
||||
search: { d: name },
|
||||
})}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</LookProvider>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user