diff --git a/apps/web/src/lab/alerts-hub-lab.ts b/apps/web/src/lab/alerts-hub-lab.ts index b8c92d6d45..8a4e55067a 100644 --- a/apps/web/src/lab/alerts-hub-lab.ts +++ b/apps/web/src/lab/alerts-hub-lab.ts @@ -9,7 +9,7 @@ import { defineMessageUnion } from "foldkit/message" import * as HubMessage from "../page/alerts/hub/message" import * as HubModel from "../page/alerts/hub/model" import * as HubUpdate from "../page/alerts/hub/update" -import { view as hubView } from "../page/alerts/hub/view" +import { breadcrumbs as hubBreadcrumbs, view as hubView } from "../page/alerts/hub/view" import * as SilenceDialog from "../page/alerts/silence-dialog/dialog" import { destinationRow, NOW, ORG, ruleRow, stateRow } from "../page/alerts/shared/test-rows" import type { PageContext } from "../page/contract" @@ -199,7 +199,7 @@ export const update = (model: Model, message: Message): Return => }, }) -export const breadcrumbs = () => [{ label: "Alerts" }] +export const breadcrumbs = (model: Model) => hubBreadcrumbs(model.hub) export const view = (model: Model, h: HtmlBuilder): Html => h.submodel({ diff --git a/apps/web/src/lab/anomaly-alerts-lab.ts b/apps/web/src/lab/anomaly-alerts-lab.ts index 8950a95878..6a9e917e01 100644 --- a/apps/web/src/lab/anomaly-alerts-lab.ts +++ b/apps/web/src/lab/anomaly-alerts-lab.ts @@ -17,7 +17,7 @@ import { breadcrumbsOf as editorCrumbs, view as editorView } from "../page/alert import * as HubMessage from "../page/alerts/hub/message" import * as HubModel from "../page/alerts/hub/model" import * as HubUpdate from "../page/alerts/hub/update" -import { view as hubView } from "../page/alerts/hub/view" +import { breadcrumbs as hubBreadcrumbs, view as hubView } from "../page/alerts/hub/view" import type { Incident } from "../page/overview/model" import { alertFiringHero, heroCountsOf } from "../page/overview/view-hero" import { vstack } from "@maple/views/ui/stack" @@ -417,7 +417,7 @@ export const breadcrumbs = (model: Model) => ? editorCrumbs(model.editor) : isIncident(model.state) ? incidentCrumbs(model.incident) - : [{ label: "Alerts" }] + : hubBreadcrumbs(model.hub) const openIncident = (severity: Incident["severity"], signalType: Incident["signalType"]): Incident => ({ status: "open", diff --git a/apps/web/src/lab/escalation-lab.ts b/apps/web/src/lab/escalation-lab.ts index 91f0462182..08d3416e53 100644 --- a/apps/web/src/lab/escalation-lab.ts +++ b/apps/web/src/lab/escalation-lab.ts @@ -17,7 +17,7 @@ import { chainSelectIdOf } from "../page/alerts/editor/form-steps" import * as HubMessage from "../page/alerts/hub/message" import * as HubModel from "../page/alerts/hub/model" import * as HubUpdate from "../page/alerts/hub/update" -import { view as hubView } from "../page/alerts/hub/view" +import { breadcrumbs as hubBreadcrumbs, view as hubView } from "../page/alerts/hub/view" import * as IncidentMessage from "../page/alerts/incident/message" import * as IncidentModel from "../page/alerts/incident/model" import * as IncidentUpdate from "../page/alerts/incident/update" @@ -311,7 +311,7 @@ export const update = (model: Model, message: Message): Return => export const breadcrumbs = (model: Model) => isHub(model.state) - ? [{ label: "Alerts" }] + ? hubBreadcrumbs(model.hub) : isIncident(model.state) ? incidentBreadcrumbs(model.incident) : editorBreadcrumbs(model.editor) diff --git a/apps/web/src/lab/maintenance-lab.ts b/apps/web/src/lab/maintenance-lab.ts index 6c83ac933f..b8a04cb397 100644 --- a/apps/web/src/lab/maintenance-lab.ts +++ b/apps/web/src/lab/maintenance-lab.ts @@ -8,7 +8,7 @@ import * as WindowDialog from "../page/alerts/maintenance-dialog/dialog" import * as HubMessage from "../page/alerts/hub/message" import * as HubModel from "../page/alerts/hub/model" import * as HubUpdate from "../page/alerts/hub/update" -import { view as hubView } from "../page/alerts/hub/view" +import { breadcrumbs as hubBreadcrumbs, view as hubView } from "../page/alerts/hub/view" import { populatedWindows, silencesWithWindows, @@ -155,7 +155,7 @@ export const update = (model: Model, message: Message): Return => }, }) -export const breadcrumbs = () => [{ label: "Alerts" }] +export const breadcrumbs = (model: Model) => hubBreadcrumbs(model.hub) export const view = (model: Model, h: HtmlBuilder): Html => h.submodel({ diff --git a/apps/web/src/lab/oncall-lab.ts b/apps/web/src/lab/oncall-lab.ts index c5e488aba3..e94302533f 100644 --- a/apps/web/src/lab/oncall-lab.ts +++ b/apps/web/src/lab/oncall-lab.ts @@ -12,7 +12,7 @@ import { keepLocal } from "./local-commands" import * as HubMessage from "../page/alerts/hub/message" import * as HubModel from "../page/alerts/hub/model" import * as HubUpdate from "../page/alerts/hub/update" -import { view as hubView } from "../page/alerts/hub/view" +import { breadcrumbs as hubBreadcrumbs, view as hubView } from "../page/alerts/hub/view" import * as IncidentMessage from "../page/alerts/incident/message" import * as IncidentModel from "../page/alerts/incident/model" import * as IncidentUpdate from "../page/alerts/incident/update" @@ -450,7 +450,7 @@ export const update = (model: Model, message: Message): Return => export const breadcrumbs = (model: Model) => isHub(model.state) - ? [{ label: "Alerts" }] + ? hubBreadcrumbs(model.hub) : isForm(model.state) ? [{ label: "Alerts", href: "/alerts" }, { label: "On-call" }] : isSchedule(model.state) diff --git a/apps/web/src/lab/slos-lab.ts b/apps/web/src/lab/slos-lab.ts index 94669d26a7..58b043672d 100644 --- a/apps/web/src/lab/slos-lab.ts +++ b/apps/web/src/lab/slos-lab.ts @@ -10,7 +10,7 @@ import { toLoadFailure } from "@maple/views/data/load-failure" import * as HubMessage from "../page/alerts/hub/message" import * as HubModel from "../page/alerts/hub/model" import * as HubUpdate from "../page/alerts/hub/update" -import { view as hubView } from "../page/alerts/hub/view" +import { breadcrumbs as hubBreadcrumbs, view as hubView } from "../page/alerts/hub/view" import { checkoutSlo, SLO_RULE_UUID, @@ -283,7 +283,7 @@ const isForm = (state: LabState) => state === "form-new" || state === "form-inva export const breadcrumbs = (model: Model) => isTab(model.state) - ? [{ label: "Alerts" }] + ? hubBreadcrumbs(model.hub) : isForm(model.state) ? [{ label: "Alerts" }, { label: "SLOs" }] : breadcrumbsOf() diff --git a/apps/web/src/page/alerts/hub/hub.scene.test.ts b/apps/web/src/page/alerts/hub/hub.scene.test.ts index 95b91532b4..1327330cb8 100644 --- a/apps/web/src/page/alerts/hub/hub.scene.test.ts +++ b/apps/web/src/page/alerts/hub/hub.scene.test.ts @@ -29,7 +29,7 @@ import { destinationRow, incidentRow, NOW, ORG, ruleId, ruleRow, stateRow } from import { Message } from "./message" import type { Model } from "./model" import { init, ruleMenuId, update } from "./update" -import { view as pageView } from "./view" +import { breadcrumbs, view as pageView } from "./view" const ENGINEER = Policy.grantsOf(["org:engineer"]) @@ -104,6 +104,33 @@ describe("alerts hub", () => { ) }) + test("the section rail marks the open section, which titles the page and its crumbs", () => { + scene( + { update: pageUpdate, view }, + given(loaded({ tab: "rules" })), + expectView(role("link", { name: /^Rules\s*3$/ })).toHaveAttr("aria-current", "page"), + expectView(role("link", { name: /^Inbox\s*1$/ })).not.toHaveAttr("aria-current", "page"), + expectView(role("heading", { name: "Rules" })).toExist(), + ) + // The anomaly filters belong to the Inbox: its row keeps them, every other row drops them. + scene( + { update: pageUpdate, view }, + given(loaded({ tab: "inbox", signal: "anomaly", serviceName: "checkout" })), + expectView(role("link", { name: /^Inbox\s*1$/ })).toHaveAttr( + "href", + "/alerts?tab=inbox&serviceName=checkout&signal=anomaly", + ), + expectView(role("link", { name: "Silences" })).toHaveAttr( + "href", + "/alerts?tab=silences&serviceName=checkout", + ), + ) + expect(breadcrumbs(loaded({ tab: "silences" }))).toEqual([ + { label: "Alerts", href: "/alerts" }, + { label: "Silences" }, + ]) + }) + const v2Incident = Schema.decodeSync(V2AlertIncident)({ id: encodePublicId(PublicIdPrefixes.alertIncident, incidentRow().id), object: "alert_incident", diff --git a/apps/web/src/page/alerts/hub/index.ts b/apps/web/src/page/alerts/hub/index.ts index 7aa9a2a678..d004453ea2 100644 --- a/apps/web/src/page/alerts/hub/index.ts +++ b/apps/web/src/page/alerts/hub/index.ts @@ -3,9 +3,9 @@ import { Message } from "./message" import { Model } from "./model" import { subscriptions } from "./subscription" import { init, routeChanged, update } from "./update" -import { view } from "./view" +import { breadcrumbs, view } from "./view" -/** `/alerts`: the incident-first hub (Inbox, Rules, Routing). One instance per org; tabs are search. */ +/** `/alerts`: the incident-first hub and its sections. One instance per org; the section is search. */ export const page = definePage( "AlertsHub", { Model, Message }, @@ -16,6 +16,6 @@ export const page = definePage( routeChanged, view, subscriptions, - breadcrumbs: () => [{ label: "Alerts", isPageHeading: true }], + breadcrumbs, }, ) diff --git a/apps/web/src/page/alerts/hub/view.ts b/apps/web/src/page/alerts/hub/view.ts index f8fcf1e68f..d786835982 100644 --- a/apps/web/src/page/alerts/hub/view.ts +++ b/apps/web/src/page/alerts/hub/view.ts @@ -13,7 +13,8 @@ import * as MenuView from "@maple/views/ui/menu-view" import { skeleton, skeletonList } from "@maple/views/ui/skeleton" import type { PageViewInputs } from "../../contract" import { dashboardPage } from "../../kit/dashboard-page" -import { linkTabStrip } from "../../kit/underline-tabs" +import { navShell, type NavShellCount, type NavShellSection } from "../../kit/nav-shell" +import type { BreadcrumbEntry } from "../../../shell" import { alertsHref, createHref } from "../shared/links" import { newSloHref } from "../shared/slo-commands" import { @@ -54,75 +55,122 @@ import { import { hstack, vstack } from "@maple/views/ui/stack" import { typography } from "@maple/views/ui/typography" -// `AlertsPage` and `AlertsHub`: the `/alerts` page and its tabs. +// `AlertsPage` and `AlertsHub`: the `/alerts` page and the rail of its sections. type H = HtmlBuilder -const ALERTS_TABS: ReadonlyArray<{ value: AlertsTab; label: string }> = [ - { value: "inbox", label: "Inbox" }, - { value: "rules", label: "Rules" }, - { value: "uptime", label: "Uptime" }, - { value: "slos", label: "SLOs" }, - { value: "silences", label: "Silences" }, - { value: "maintenance", label: "Maintenance" }, - { value: "oncall", label: "On-call" }, - { value: "routing", label: "Routing" }, +const TAB_LABEL = { + inbox: "Inbox", + rules: "Rules", + uptime: "Uptime", + slos: "SLOs", + silences: "Silences", + maintenance: "Maintenance", + oncall: "On-call", + routing: "Routing", +} as const satisfies Record + +type RailRow = Readonly<{ tab: AlertsTab; icon: IconName }> + +const RAIL: ReadonlyArray }>> = [ + { id: "triage", title: "Triage", rows: [{ tab: "inbox", icon: "BellIcon" }] }, + { + id: "monitors", + title: "Monitors", + rows: [ + { tab: "rules", icon: "ChartLineIcon" }, + { tab: "uptime", icon: "GlobeIcon" }, + { tab: "slos", icon: "GaugeIcon" }, + ], + }, + { + id: "quiet", + title: "Quiet", + rows: [ + { tab: "silences", icon: "BellSlashIcon" }, + { tab: "maintenance", icon: "CalendarIcon" }, + ], + }, + { + id: "response", + title: "Response", + rows: [ + { tab: "oncall", icon: "UsersIcon" }, + { tab: "routing", icon: "BranchForkIcon" }, + ], + }, ] const HUB_SKELETON_ROWS = ["h-14 w-1/2", "h-40 w-full", "h-48 w-full"] -const tabBar = ( - h: H, +/** `Alerts ›
`: the rail's active row names the page. */ +export const breadcrumbs = (model: Model): ReadonlyArray => [ + { label: "Alerts", href: alertsHref({}) }, + { label: TAB_LABEL[resolveTab(model.search.tab)] }, +] + +const countOf = ( model: Model, overview: Option.Option, - active: AlertsTab, -): Html => { - const monitorCount = Option.map(AsyncData.getData(model.monitors.data), (monitors) => monitors.length) - const sloCount = Option.map(AsyncData.getData(model.slos.data), (slos) => slos.length) - const scheduleCount = Option.map(AsyncData.getData(model.schedules.data), (rows) => rows.length) - return linkTabStrip( + tab: AlertsTab, +): number | undefined => { + const lengthOf = (rows: Option.Option>) => + Option.getOrUndefined(Option.map(rows, Arr.length)) + switch (tab) { + case "uptime": + return lengthOf(AsyncData.getData(model.monitors.data)) + case "slos": + return lengthOf(AsyncData.getData(model.slos.data)) + case "oncall": + return lengthOf(AsyncData.getData(model.schedules.data)) + case "inbox": + return Option.getOrUndefined( + Option.map(overview, (data) => unacknowledgedCount(data.openIncidents)), + ) + case "rules": + return Option.getOrUndefined(Option.map(overview, (data) => data.rules.length)) + case "silences": + case "maintenance": + case "routing": + return undefined + } +} + +/** The section rail in `dashboardPage`'s side slot, which folds into a sheet below `lg`. */ +const rail = (h: H, model: Model, overview: Option.Option, active: AlertsTab): Html => + navShell( h, + Arr.map(RAIL, (section): NavShellSection => ({ + id: section.id, + title: section.title, + items: Arr.map(section.rows, (row) => { + const count = countOf(model, overview, row.tab) + const tone: NavShellCount["tone"] = row.tab === "inbox" ? "error" : "muted" + return { + kind: "link", + id: row.tab, + label: TAB_LABEL[row.tab], + icon: row.icon, + count: count !== undefined && count > 0 ? { value: count, tone } : undefined, + // The Anomalies view belongs to the Inbox: other sections drop its filters. + href: alertsHref( + row.tab === "inbox" + ? { ...model.search, tab: row.tab } + : { + ...model.search, + tab: row.tab, + signal: undefined, + anomalyStatus: undefined, + window: undefined, + }, + ), + } + }), + })), + active, + (tab) => tab, "Alerts sections", - Arr.map(ALERTS_TABS, (tab) => { - const count = - tab.value === "uptime" - ? Option.getOrUndefined(monitorCount) - : tab.value === "slos" - ? Option.getOrUndefined(sloCount) - : tab.value === "oncall" - ? Option.getOrUndefined(scheduleCount) - : Option.match(overview, { - onNone: () => undefined, - onSome: (data) => - tab.value === "inbox" - ? unacknowledgedCount(data.openIncidents) - : tab.value === "rules" - ? data.rules.length - : undefined, - }) - return { - key: tab.value, - // The Anomalies view belongs to the Inbox: other tabs drop its filters. - href: alertsHref( - tab.value === "inbox" - ? { ...model.search, tab: tab.value } - : { - ...model.search, - tab: tab.value, - signal: undefined, - anomalyStatus: undefined, - window: undefined, - }, - ), - label: tab.label, - count: count !== undefined && count > 0 ? count : undefined, - countTone: tab.value === "inbox" ? "error" : "muted", - isCurrent: tab.value === active, - isRouteActive: tab.value === active, - } - }), ) -} /** A "New rule" menu row: icon, label and one line of what it watches. */ const newRuleItem = ( @@ -405,9 +453,11 @@ export const view = Submodel.defineView((model, dashboardPage( h, { - title: "Alerts", + title: TAB_LABEL[tab], actions: headerActions(h, model, tab), - tabs: tabBar(h, model, overview, tab), + filters: rail(h, model, overview, tab), + filtersLabel: "Alerts", + filtersWidth: "w-56", }, [ tab === "routing" diff --git a/apps/web/src/page/integrations/view.ts b/apps/web/src/page/integrations/view.ts index dfefe0d82b..652823eb6c 100644 --- a/apps/web/src/page/integrations/view.ts +++ b/apps/web/src/page/integrations/view.ts @@ -8,7 +8,7 @@ import { catalogEntry, chatConnectorOf, integrationOfSearch, type IntegrationId import { ConnectProvider } from "./connect" import { Message } from "./message" import { canManage, type Model } from "./model" -import { navShell } from "../settings/kit/nav-shell" +import { navShell } from "../kit/nav-shell" import { isClerkAuthEnabled } from "../../lib/services/common/auth-mode" import { visibleSections } from "../settings/nav" import { settingsNavVisibility } from "./derive" diff --git a/apps/web/src/page/settings/kit/nav-shell.ts b/apps/web/src/page/kit/nav-shell.ts similarity index 58% rename from apps/web/src/page/settings/kit/nav-shell.ts rename to apps/web/src/page/kit/nav-shell.ts index 4d74bf286d..f39211b74b 100644 --- a/apps/web/src/page/settings/kit/nav-shell.ts +++ b/apps/web/src/page/kit/nav-shell.ts @@ -6,12 +6,17 @@ import { vstack, vstackClass } from "@maple/views/ui/stack" import { Array as Arr } from "effect" import { typography } from "@maple/views/ui/typography" -// `SettingsNavShell`: the rail of `/settings`, `/account` and `/integrations`. Tabs are buttons, sibling -// pages are links. +// `NavShell`: a page's own nav rail of titled sections (settings, security, alerts). Tabs are buttons, +// links go to sibling pages or search-param sections. + +/** A row's trailing count; `error` for one that needs attention (open incidents). */ +export type NavShellCount = Readonly<{ value: number; tone?: "muted" | "error" }> + +type RowBase = Readonly<{ label: string; icon: IconName; count?: NavShellCount }> export type NavShellRow = - | Readonly<{ kind: "tab"; id: Id; label: string; icon: IconName }> - | Readonly<{ kind: "link"; id: string; label: string; icon: IconName; href: string }> + | (RowBase & Readonly<{ kind: "tab"; id: Id }>) + | (RowBase & Readonly<{ kind: "link"; id: string; href: string }>) export type NavShellSection = Readonly<{ id: string @@ -19,9 +24,24 @@ export type NavShellSection = Readonly<{ items: ReadonlyArray> }> -const rowChildren = (h: HtmlBuilder, name: IconName, label: string) => [ - icon(h, name, { size: 16, className: "shrink-0" }), - typography(h, { truncate: true }, [label]), +const rowChildren = (h: HtmlBuilder, row: RowBase) => [ + icon(h, row.icon, { size: 16, className: "shrink-0" }), + typography(h, { truncate: true, className: "flex-1" }, [row.label]), + ...(row.count === undefined + ? [] + : [ + typography( + h, + { + size: "xs", + mono: true, + numeric: true, + tone: row.count.tone ?? "muted", + className: "shrink-0", + }, + [String(row.count.value)], + ), + ]), ] export const navShell = ( @@ -45,12 +65,12 @@ export const navShell = ( ? navRailRow( h, { isActive, href: row.href, attributes: [h.Key(row.id)] }, - rowChildren(h, row.icon, row.label), + rowChildren(h, row), ) : navRailRow( h, { isActive, onClick: onSelectTab(row.id), attributes: [h.Key(row.id)] }, - rowChildren(h, row.icon, row.label), + rowChildren(h, row), ) }), ), diff --git a/apps/web/src/page/security/kit/layout.ts b/apps/web/src/page/security/kit/layout.ts index f281a7ad4b..6ed4099422 100644 --- a/apps/web/src/page/security/kit/layout.ts +++ b/apps/web/src/page/security/kit/layout.ts @@ -4,7 +4,7 @@ import type { BreadcrumbEntry } from "../../../shell" import type { IconName } from "@maple/views/ui/icon" import { dashboardPage, type DashboardPageOptions } from "../../kit/dashboard-page" import { linkTabStrip } from "../../kit/underline-tabs" -import { navShell, type NavShellSection } from "../../settings/kit/nav-shell" +import { navShell, type NavShellSection } from "../../kit/nav-shell" import { Array as Arr, Option } from "effect" import { vstack } from "@maple/views/ui/stack" // The frame of each Security source's page: the rail of sources, the source's title block, content. diff --git a/apps/web/src/page/settings/account/nav.ts b/apps/web/src/page/settings/account/nav.ts index e462668784..c978094be1 100644 --- a/apps/web/src/page/settings/account/nav.ts +++ b/apps/web/src/page/settings/account/nav.ts @@ -1,5 +1,5 @@ import { accountTabValues } from "../../../route/search" -import type { NavShellSection } from "../kit/nav-shell" +import type { NavShellSection } from "../../kit/nav-shell" import { Array as Arr, Option } from "effect" // `accountNavSections`. diff --git a/apps/web/src/page/settings/account/view.ts b/apps/web/src/page/settings/account/view.ts index e8d712faa8..d497b64573 100644 --- a/apps/web/src/page/settings/account/view.ts +++ b/apps/web/src/page/settings/account/view.ts @@ -3,7 +3,7 @@ import type { Html, HtmlBuilder } from "foldkit/html" import type { BreadcrumbEntry } from "../../../shell" import type { PageViewInputs } from "../../contract" import { dashboardPage } from "../../kit/dashboard-page" -import { navShell } from "../kit/nav-shell" +import { navShell } from "../../kit/nav-shell" import { Message, type Model } from "./model" import { accountNavSections, accountTabLabels } from "./nav" import * as ProfileView from "./profile/view" diff --git a/apps/web/src/page/settings/nav.ts b/apps/web/src/page/settings/nav.ts index 5c65c3a820..9d0cc2451a 100644 --- a/apps/web/src/page/settings/nav.ts +++ b/apps/web/src/page/settings/nav.ts @@ -1,6 +1,6 @@ import * as Policy from "@maple/domain/policy" import { Array, Option } from "effect" -import type { NavShellRow, NavShellSection } from "./kit/nav-shell" +import type { NavShellRow, NavShellSection } from "../kit/nav-shell" import { legacySettingsTabValues, settingsTabValues } from "../../route/search" // The tab set, labels, retired-tab redirects, nav groups and their visibility. diff --git a/apps/web/src/page/settings/view.ts b/apps/web/src/page/settings/view.ts index f1645dc288..48b05c4571 100644 --- a/apps/web/src/page/settings/view.ts +++ b/apps/web/src/page/settings/view.ts @@ -18,7 +18,7 @@ import { skeleton } from "@maple/views/ui/skeleton" import type { PageViewInputs } from "../contract" import { dashboardPage } from "../kit/dashboard-page" import type { BreadcrumbEntry } from "../../shell" -import { navShell } from "./kit/nav-shell" +import { navShell } from "../kit/nav-shell" import { INLINE_LINK_CLASS } from "./kit/section" import * as IngestionView from "./ingestion/view" import * as KeysView from "./keys/view" diff --git a/packages/views/src/ui/INDEX.md b/packages/views/src/ui/INDEX.md index 4338452405..d84fb675eb 100644 --- a/packages/views/src/ui/INDEX.md +++ b/packages/views/src/ui/INDEX.md @@ -109,7 +109,7 @@ existing copies onto it (see `apps/web/CLAUDE.md`). - `packages/views/src/ui/middle-truncate.ts` `middleTruncate`: Port of packages/ui/src/components/ui/middle-truncate.tsx: keeps the last `tail` characters visible and ellipsizes the middle. - `packages/views/src/ui/mini-bars.ts` `miniBars`: Port of packages/ui/src/components/ui/mini-bars.tsx: a static bar sparkline in one . - `packages/views/src/ui/native-select.ts` `nativeOption`, `nativeOptGroup`, `nativeSelect`: ``: a styled native ``, Base UI's outside marking, optional scroll lock and focus moves, t... @@ -200,6 +200,7 @@ existing copies onto it (see `apps/web/CLAUDE.md`). - `apps/web/src/page/kit/list-toolbar.ts` `listToolbar`, `toolbarCount`, `toolbarStat`: `ListToolbar`: a list page's row of view tabs, then search, filters and the count readout. - `apps/web/src/page/kit/markdown.ts` `escapeDirectives`, `parseMarkdownTexts`: The one markdown parse every page uses (chat, agent sessions). - `apps/web/src/page/kit/message-response.ts` `glyph`, `messageResponse`: Lightweight markdown over Streamdown: laid out with Streamdown's element classes. +- `apps/web/src/page/kit/nav-shell.ts` `navShell`: `NavShell`: a page's own nav rail of titled sections (settings, security, alerts). - `apps/web/src/page/kit/page-rail-sheet.ts` `pageRailSheet`: `PageLayout.FilterSidebar` / `RightSidebar` below `lg`: the page's rail in a side sheet (``) with an sr-only header. - `apps/web/src/page/kit/peek-keys.ts` `peekStepFor`: The list peeks' step keys: down/J forward, up/K back, except in a text field or with a modifier. - `apps/web/src/page/kit/pick-list.ts` `pickList`: `PickList`: what a form has picked (destinations, people, schedules), each removable and, in an ordered list, movable, then an "Add" menu of the rest. diff --git a/packages/views/src/ui/nav-rail-row.ts b/packages/views/src/ui/nav-rail-row.ts index caf85d3f32..f7ccdb6d09 100644 --- a/packages/views/src/ui/nav-rail-row.ts +++ b/packages/views/src/ui/nav-rail-row.ts @@ -2,7 +2,7 @@ import { cn } from "@maple/ui/lib/utils" import { ROW_LANE } from "@maple/ui/styles/ui/list-row" import type { Attribute, Html, HtmlBuilder } from "foldkit/html" -// `NavRailRow`: a page's own nav rail row (the settings rail), with the app sidebar row's height, +// `NavRailRow`: a page's own nav rail row (settings, security, alerts), with the app sidebar row's height, // type, radius and flush 2px active lane. An `href` renders a router link, an `onClick` a tab button. const rowClass = (isActive: boolean) =>