diff --git a/apps/web/src/page/dashboards/view/view.ts b/apps/web/src/page/dashboards/view/view.ts index 7de0b3fdef..4003ba301e 100644 --- a/apps/web/src/page/dashboards/view/view.ts +++ b/apps/web/src/page/dashboards/view/view.ts @@ -287,8 +287,9 @@ export const view = Submodel.defineView((model, dashboardPage( h, model.document === null - ? {} + ? { width: "full" } : { + width: "full", titleContent: title(h, model, model.document.name), actions: toolbar(h, model, inputs.context), ...(model.history.isOpen diff --git a/apps/web/src/page/dashboards/widget/view.ts b/apps/web/src/page/dashboards/widget/view.ts index 829b215c5c..63a7b025ea 100644 --- a/apps/web/src/page/dashboards/widget/view.ts +++ b/apps/web/src/page/dashboards/widget/view.ts @@ -174,7 +174,7 @@ export const view = Submodel.defineView((model, h.div( [h.Class("contents"), h.OnMount(ResolveThemeColors({ tokens: WIDGET_CHART_TOKENS }))], [ - dashboardPage(h, {}, [ + dashboardPage(h, { width: "full" }, [ model.status === "Ready" && model.state !== null ? editor(h, model, inputs.context.timeZone) : model.status === "Missing" diff --git a/apps/web/src/page/kit/dashboard-page.ts b/apps/web/src/page/kit/dashboard-page.ts index 79bf85f485..af3b948e45 100644 --- a/apps/web/src/page/kit/dashboard-page.ts +++ b/apps/web/src/page/kit/dashboard-page.ts @@ -37,7 +37,10 @@ export type DashboardPageOptions = Readonly<{ /** Page tabs pinned under the header. */ tabs?: Html scrollClassName?: string - /** A centred capped column (`page-column`). */ + /** + * The body's column (`page-column`). Defaults to `wide`, a 1536px cap; `full` is for a + * page whose content is the canvas (traces, the service map, a dashboard grid). + */ width?: PageWidth /** The gap between the body's top-level blocks. */ gap?: PageGap @@ -106,23 +109,31 @@ export const pageHeader = ( actions === undefined ? [] : [pageHeaderActions(h, [actions])], ) -const stickyArea = (h: HtmlBuilder, children: ReadonlyArray, isScrolled = false): Html => +const stickyArea = ( + h: HtmlBuilder, + children: ReadonlyArray, + isScrolled: boolean, + width: PageWidth, +): Html => h.div( [ h.DataAttribute("slot", "page-sticky-area"), h.Class(cn(pageStickyAreaStyles, isScrolled && pageStickyAreaScrolledStyles)), ], - children, + // The header caps at the body's width, so the title sits over the column it heads. + PAGE_WIDTH[width] === null + ? children + : [h.div([h.Class(cn("w-full space-y-4", PAGE_WIDTH[width]))], children)], ) const fillArea = (h: HtmlBuilder, children: ReadonlyArray): Html => h.div([h.DataAttribute("slot", "page-fill"), h.Class(pageFillStyles)], children) -// `settings` caps the header and the column at the same left-aligned width (settings, integrations, account). +// Every capped width is left-aligned on the page gutter, so the header, body and top bar share one edge. const PAGE_WIDTH = { full: null, - reading: "mx-auto max-w-4xl", - narrow: "mx-auto max-w-3xl", + wide: "max-w-(--breakpoint-2xl)", + narrow: "max-w-3xl", settings: "max-w-4xl", } as const const PAGE_GAP = { none: null, sm: "gap-3", md: "gap-4", lg: "gap-6" } as const @@ -133,7 +144,7 @@ const scrollArea = ( h: HtmlBuilder, children: ReadonlyArray, className?: string, - width: PageWidth = "full", + width: PageWidth = "wide", gap: PageGap = "none", ): Html => width === "full" @@ -183,9 +194,9 @@ export const dashboardPage = ( [heading, pageDescription(h, [options.description], "text-sm")], )) const hasHeader = titleContent !== undefined || options.actions !== undefined - const headerWidth = options.width === "settings" ? PAGE_WIDTH.settings : undefined - const header = - options.header ?? (hasHeader ? pageHeader(h, titleContent, options.actions, headerWidth) : undefined) + // A page that owns its scrolling is a canvas: its header spans the full width with it. + const width = options.width ?? (options.fill === true ? "full" : "wide") + const header = options.header ?? (hasHeader ? pageHeader(h, titleContent, options.actions) : undefined) const sticky = [ ...(header === undefined ? [] : [header]), ...(options.tabs === undefined @@ -198,10 +209,10 @@ export const dashboardPage = ( ? [] : [filterRail(h, options.filters, options.filtersWidth, options.filtersLabel)]), pageContent(h, [ - ...(sticky.length > 0 ? [stickyArea(h, sticky, options.isScrolled)] : []), + ...(sticky.length > 0 ? [stickyArea(h, sticky, options.isScrolled ?? false, width)] : []), options.fill === true ? fillArea(h, children) - : scrollArea(h, children, options.scrollClassName, options.width, options.gap), + : scrollArea(h, children, options.scrollClassName, width, options.gap), ]), ...(options.rightPanel === undefined ? [] diff --git a/apps/web/src/page/metrics/detail/view.ts b/apps/web/src/page/metrics/detail/view.ts index 0dc98ea311..b7fa80bbc6 100644 --- a/apps/web/src/page/metrics/detail/view.ts +++ b/apps/web/src/page/metrics/detail/view.ts @@ -121,6 +121,7 @@ export const view = Submodel.defineView((model, dashboardPage( h, { + width: "full", // A metric name is an identifier, so it is set in mono like elsewhere. titleContent: detailTitle(h, { kind: ["Metric", typeLabelOf(model)], diff --git a/apps/web/src/page/service-map/view.ts b/apps/web/src/page/service-map/view.ts index d7f285a27a..3ba559576e 100644 --- a/apps/web/src/page/service-map/view.ts +++ b/apps/web/src/page/service-map/view.ts @@ -234,7 +234,7 @@ export const view = Submodel.defineView((model, h.div( [h.Class("contents"), h.OnMount(ResolveThemeColors({ tokens: DB_CHART_TOKENS }))], [ - dashboardPage(h, { title: "Service map", actions: headerActions(h, model) }, [ + dashboardPage(h, { title: "Service map", width: "full", actions: headerActions(h, model) }, [ Option.match(AsyncData.getError(model.facets.data), { onSome: (failure) => errorState(h, { diff --git a/apps/web/src/page/traces/detail/view.ts b/apps/web/src/page/traces/detail/view.ts index cf967f238d..ffbb345939 100644 --- a/apps/web/src/page/traces/detail/view.ts +++ b/apps/web/src/page/traces/detail/view.ts @@ -255,6 +255,7 @@ export const view = Submodel.defineView((model, return dashboardPage( h, { + width: "full", // A failed load still gets a header naming the trace, so the error sits under a title. titleContent: trace !== undefined diff --git a/apps/web/src/page/traces/list/view.ts b/apps/web/src/page/traces/list/view.ts index b97e08f845..146f93dad8 100644 --- a/apps/web/src/page/traces/list/view.ts +++ b/apps/web/src/page/traces/list/view.ts @@ -42,6 +42,7 @@ export const view = Submodel.defineView((model, h, { title: "Traces", + width: "full", filters: filtersRail(h, model, inputs.context.pinnedNamespace), actions: hstack(h, { gap: "2", wrap: true }, [ advancedFilterButton(h, { diff --git a/apps/web/src/shell/view-topbar.ts b/apps/web/src/shell/view-topbar.ts index 3eeff37758..0dfe769a3b 100644 --- a/apps/web/src/shell/view-topbar.ts +++ b/apps/web/src/shell/view-topbar.ts @@ -80,7 +80,7 @@ export const topbar = ( gap: "2", align: "center", as: "header", - className: "h-12 shrink-0 border-b px-4", + className: "h-12 shrink-0 border-b px-(--page-gutter)", attributes: [h.DataAttribute("slot", "app-topbar")], }, [ diff --git a/packages/views/src/ui/page-layout.styles.ts b/packages/views/src/ui/page-layout.styles.ts index dda87dd2c9..774c393ad4 100644 --- a/packages/views/src/ui/page-layout.styles.ts +++ b/packages/views/src/ui/page-layout.styles.ts @@ -1,4 +1,5 @@ -export const pageLayoutStyles = "flex min-h-0 flex-1 flex-col" +/** `--page-gutter` is the one page edge: the top bar, header, filter rail and body all sit on it. */ +export const pageLayoutStyles = "flex min-h-0 flex-1 flex-col [--page-gutter:--spacing(4)]" export const pageTitleStyles = "font-display text-2xl font-semibold tracking-tight truncate leading-6.5" @@ -14,9 +15,10 @@ export const pageHeaderTextStyles = "min-w-0 flex-1" export const pageHeaderActionsStyles = "min-w-0 @2xl/page:shrink-0" -export const pageStickyAreaStyles = "shrink-0 space-y-4 p-4 transition-shadow" +export const pageStickyAreaStyles = + "shrink-0 space-y-4 border-b border-transparent px-(--page-gutter) pt-(--page-gutter) pb-4" -export const pageStickyAreaScrolledStyles = "shadow-popup" +export const pageStickyAreaScrolledStyles = "border-border-subtle" export const pageBodyStyles = "flex min-h-0 flex-1 overflow-hidden" @@ -26,11 +28,13 @@ export const pageFilterSheetStyles = "w-72 overflow-y-auto p-4 pt-12" export const pageFilterSheetHeaderStyles = "sr-only" -export const pageFilterSidebarStyles = "sticky top-0 h-full shrink-0 overflow-y-auto border-r p-4" +export const pageFilterSidebarStyles = + "sticky top-0 h-full shrink-0 overflow-y-auto border-r px-4 pt-(--page-gutter) pb-4" export const pageContentStyles = "@container/page flex min-h-0 min-w-0 flex-1 flex-col" -export const pageScrollAreaStyles = "flex min-h-0 flex-1 flex-col overflow-auto p-4" +export const pageScrollAreaStyles = + "flex min-h-0 flex-1 flex-col overflow-auto px-(--page-gutter) pt-4 pb-(--page-gutter)" export const pageFillStyles = "flex min-h-0 min-w-0 flex-1 flex-col"