Fix the Stage 1 Dashboard UX defects found in assembled review
CI / verify (pull_request) Successful in 1m13s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m22s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m38s
CI / verify (pull_request) Successful in 1m13s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m22s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m38s
The assembled Stage 1 Dashboard was reviewed by hand on a desktop, with the sidebar collapsed, and on an iPhone. This fixes what that review found, and records the decisions in ADR 0064: - Discover is its own page at /settings/discover, with Installed and Updates beneath it as children. There is no Browse item any more. /settings/discover/browse is still read, as an alias, and is respelled in place. Parents and children now read as a hierarchy: a guide line, text-only children, a disclosure chevron, and parent and current states that look different. - The sidebar is navigation only. Display pairing moves from the sidebar and drawer to Settings -> Displays: the paired displays, a deliberate Add display dialog, and revocation. Nothing takes focus on arrival; the code field is focused only once Add display is chosen. - Collapsed icons are centred. Nav-link layout moves from inline styles to the shell stylesheet, and every collapsed item is one square centred on the rail. - /settings is an overview of the installation: displays paired, modules installed and running, and a line for each other area. It reads only existing endpoints. - Phones get a sticky, safe-area-aware header holding the menu trigger and the page name. New pages start at the top, and focus no longer scrolls content under the header. - @ekklesia/ui draws checkbox and radio indicators from tokens rather than accent-color, so the selected state is the strong accent in Light and Dark alike. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
186668915b
commit
405e509bcb
@@ -1,4 +1,8 @@
|
||||
import { designToken } from "@ekklesia/design-tokens";
|
||||
import { SHELL_ICON_SIZE } from "./shell-icons.js";
|
||||
|
||||
/** The phone header's own height: one large control plus a little room above and below it. */
|
||||
const MOBILE_BAR_HEIGHT = `calc(${designToken("control.height-lg")} + 2 * ${designToken("spacing.xs")})`;
|
||||
|
||||
/**
|
||||
* The Dashboard shell's own stylesheet (ADR 0058): what inline styles cannot express — hover and
|
||||
@@ -23,6 +27,28 @@ export const SHELL_STYLESHEET = `
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/* The authenticated shell's frame. dvh where the browser has it, so a phone's collapsing browser
|
||||
chrome never leaves the page a toolbar's height too tall. */
|
||||
.ek-shell-root {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
/* The page's own region. A phone gets a slimmer margin than a desktop, and on a device with a notch
|
||||
or a home indicator the page's edges keep clear of them. */
|
||||
.ek-shell-main {
|
||||
padding: ${designToken("spacing.xl")};
|
||||
padding-bottom: calc(${designToken("spacing.xl")} + env(safe-area-inset-bottom, 0px));
|
||||
padding-right: calc(${designToken("spacing.xl")} + env(safe-area-inset-right, 0px));
|
||||
}
|
||||
|
||||
.ek-shell-root[data-layout="compact"] .ek-shell-main {
|
||||
padding: ${designToken("spacing.lg")};
|
||||
padding-bottom: calc(${designToken("spacing.lg")} + env(safe-area-inset-bottom, 0px));
|
||||
padding-left: calc(${designToken("spacing.lg")} + env(safe-area-inset-left, 0px));
|
||||
padding-right: calc(${designToken("spacing.lg")} + env(safe-area-inset-right, 0px));
|
||||
}
|
||||
|
||||
.ek-shell-sidebar {
|
||||
background: ${designToken("background.canvas")};
|
||||
border-right: ${designToken("border.width")} solid ${designToken("border.divider")};
|
||||
@@ -33,6 +59,10 @@ export const SHELL_STYLESHEET = `
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
padding: ${designToken("spacing.md")};
|
||||
/* A landscape phone wide enough for the sidebar puts its left edge beside the notch, and its
|
||||
account area above the home indicator. */
|
||||
padding-bottom: calc(${designToken("spacing.md")} + env(safe-area-inset-bottom, 0px));
|
||||
padding-left: calc(${designToken("spacing.md")} + env(safe-area-inset-left, 0px));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
transition: width ${designToken("motion.duration-short")} ease;
|
||||
@@ -59,6 +89,17 @@ export const SHELL_STYLESHEET = `
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* The rail is exactly as wide as its items. A scrollbar would take its width from one side and push
|
||||
every icon off centre, so the rail scrolls without one (wheel, touch, and keyboard focus still
|
||||
scroll it). */
|
||||
.ek-shell-sidebar[data-sidebar="collapsed"] .ek-shell-sidebar__scroll {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.ek-shell-sidebar[data-sidebar="collapsed"] .ek-shell-sidebar__scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ek-shell-sidebar__bottom {
|
||||
border-top: ${designToken("border.width")} solid ${designToken("border.divider")};
|
||||
padding-top: ${designToken("spacing.sm")};
|
||||
@@ -114,11 +155,49 @@ export const SHELL_STYLESHEET = `
|
||||
padding-inline-start: ${designToken("spacing.xs")};
|
||||
}
|
||||
|
||||
/* A parent's children — a module's pages, Discover's Installed and Updates (ADR 0064). The guide
|
||||
line runs down from beneath the parent's icon, and the children's text starts past the parent's
|
||||
label, so the relationship reads at a glance. */
|
||||
.ek-shell-nav__children {
|
||||
border-inline-start: ${designToken("border.width")} solid ${designToken("border.divider")};
|
||||
display: grid;
|
||||
gap: ${designToken("spacing.2xs")};
|
||||
margin: ${designToken("spacing.2xs")} 0 ${designToken("spacing.xs")}
|
||||
calc(${designToken("control.padding-inline")} + ${SHELL_ICON_SIZE / 2}px);
|
||||
padding: 0 0 0 ${designToken("spacing.lg")};
|
||||
}
|
||||
|
||||
.ek-shell-nav__divider {
|
||||
border-top: ${designToken("border.width")} solid ${designToken("border.divider")};
|
||||
margin: 0 ${designToken("spacing.xs")};
|
||||
}
|
||||
|
||||
/* A navigation link (DashboardLink). Its layout and open-page look live here, never inline, so
|
||||
the collapsed rail below can override them (ADR 0064). */
|
||||
.ek-shell-link {
|
||||
justify-content: flex-start;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ek-shell-link[aria-current="page"],
|
||||
.ek-shell-link[aria-current="page"]:hover:not(:disabled) {
|
||||
background: ${designToken("state.selected-background")};
|
||||
color: ${designToken("state.selected-foreground")};
|
||||
font-weight: ${designToken("typography.weight-bold")};
|
||||
}
|
||||
|
||||
/* The parent of the open page: named in the primary colour, its icon in the accent, and no fill —
|
||||
the fill belongs to the open page alone. */
|
||||
.ek-shell-link[data-nav-ancestor] {
|
||||
color: ${designToken("text.primary")};
|
||||
font-weight: ${designToken("typography.weight-bold")};
|
||||
}
|
||||
|
||||
.ek-shell-link[data-nav-ancestor] > svg {
|
||||
color: ${designToken("accent.strong")};
|
||||
}
|
||||
|
||||
.ek-shell-nav-item {
|
||||
box-sizing: border-box;
|
||||
gap: ${designToken("spacing.sm")};
|
||||
@@ -131,16 +210,40 @@ export const SHELL_STYLESHEET = `
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.ek-shell-nav-item[data-nav-level="child"]:not([aria-current="page"]) {
|
||||
font-weight: ${designToken("typography.weight-regular")};
|
||||
}
|
||||
|
||||
.ek-shell-nav-item__label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ek-shell-nav-item__disclosure {
|
||||
color: ${designToken("text.muted")};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
transition: transform ${designToken("motion.duration-short")} ease;
|
||||
}
|
||||
|
||||
.ek-shell-nav-item__disclosure[data-open="true"] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Collapsed, every item is the same square, centred on the rail, with its glyph centred in it —
|
||||
whatever its level, and whatever the expanded layout's padding, gap, or alignment would say. */
|
||||
.ek-shell-sidebar[data-sidebar="collapsed"] .ek-shell-nav-item {
|
||||
/* A block-level flex box, so the auto margins centre it on the rail. */
|
||||
display: flex;
|
||||
gap: 0;
|
||||
height: ${designToken("control.height-md")};
|
||||
justify-content: center;
|
||||
padding-inline: 0;
|
||||
margin-inline: auto;
|
||||
padding: 0;
|
||||
width: ${designToken("control.height-md")};
|
||||
}
|
||||
|
||||
.ek-shell-settings-link {
|
||||
@@ -183,6 +286,142 @@ export const SHELL_STYLESHEET = `
|
||||
font-size: ${designToken("typography.size-sm")};
|
||||
}
|
||||
|
||||
/* Settings → Displays: one card per paired screen, wrapping onto two lines on a phone. */
|
||||
.ek-shell-display-list {
|
||||
display: grid;
|
||||
gap: ${designToken("spacing.sm")};
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ek-shell-display-card {
|
||||
align-items: center;
|
||||
border: ${designToken("border.width")} solid ${designToken("border.base")};
|
||||
border-radius: ${designToken("radius.md")};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${designToken("spacing.sm")} ${designToken("spacing.md")};
|
||||
padding: ${designToken("spacing.md")};
|
||||
}
|
||||
|
||||
.ek-shell-display-card__icon {
|
||||
align-items: center;
|
||||
background: ${designToken("background.inset")};
|
||||
border-radius: ${designToken("radius.md")};
|
||||
color: ${designToken("text.secondary")};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
height: ${designToken("control.height-md")};
|
||||
justify-content: center;
|
||||
width: ${designToken("control.height-md")};
|
||||
}
|
||||
|
||||
.ek-shell-display-card__copy {
|
||||
display: grid;
|
||||
flex: 1 1 12rem;
|
||||
gap: ${designToken("spacing.2xs")};
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ek-shell-display-card__title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${designToken("spacing.sm")};
|
||||
}
|
||||
|
||||
.ek-shell-display-card__name {
|
||||
color: ${designToken("text.primary")};
|
||||
font-weight: ${designToken("typography.weight-bold")};
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ek-shell-display-card__facts {
|
||||
color: ${designToken("text.secondary")};
|
||||
font-size: ${designToken("typography.size-sm")};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* The Settings overview (ADR 0064): what state the installation is in, a card per area. */
|
||||
.ek-shell-overview {
|
||||
display: grid;
|
||||
gap: ${designToken("spacing.md")};
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
||||
}
|
||||
|
||||
.ek-shell-overview--compact {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
}
|
||||
|
||||
.ek-shell-summary {
|
||||
background: ${designToken("background.surface")};
|
||||
border: ${designToken("border.width")} solid ${designToken("border.base")};
|
||||
border-radius: ${designToken("radius.lg")};
|
||||
box-shadow: ${designToken("shadow.sm")};
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
gap: ${designToken("spacing.sm")};
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-width: 0;
|
||||
padding: ${designToken("spacing.lg")};
|
||||
}
|
||||
|
||||
.ek-shell-summary__heading {
|
||||
align-items: center;
|
||||
color: ${designToken("text.secondary")};
|
||||
display: flex;
|
||||
font-size: ${designToken("typography.size-sm")};
|
||||
font-weight: ${designToken("typography.weight-medium")};
|
||||
gap: ${designToken("spacing.sm")};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ek-shell-summary__status {
|
||||
color: ${designToken("text.primary")};
|
||||
font-size: ${designToken("typography.size-lg")};
|
||||
font-weight: ${designToken("typography.weight-bold")};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ek-shell-summary--compact .ek-shell-summary__status {
|
||||
font-size: ${designToken("typography.size-md")};
|
||||
font-weight: ${designToken("typography.weight-medium")};
|
||||
}
|
||||
|
||||
.ek-shell-summary__detail {
|
||||
color: ${designToken("text.secondary")};
|
||||
font-size: ${designToken("typography.size-sm")};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ek-shell-summary__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${designToken("spacing.xs")} ${designToken("spacing.md")};
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ek-shell-summary__link {
|
||||
border-radius: ${designToken("radius.sm")};
|
||||
color: ${designToken("accent.strong")};
|
||||
font-size: ${designToken("typography.size-sm")};
|
||||
font-weight: ${designToken("typography.weight-medium")};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ek-shell-summary__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ek-shell-summary__link:focus-visible {
|
||||
outline: ${designToken("state.focus-visible-width")} solid ${designToken("accent.focus")};
|
||||
outline-offset: ${designToken("state.focus-visible-offset")};
|
||||
}
|
||||
|
||||
.ek-shell-tooltip {
|
||||
background: ${designToken("background.raised")};
|
||||
border: ${designToken("border.width")} solid ${designToken("border.base")};
|
||||
@@ -314,12 +553,16 @@ export const SHELL_STYLESHEET = `
|
||||
color: ${designToken("state.selected-foreground")};
|
||||
}
|
||||
|
||||
.ek-shell-choice__input {
|
||||
accent-color: ${designToken("accent.base")};
|
||||
flex: 0 0 auto;
|
||||
height: 1.25rem;
|
||||
/* The radio itself is @ekklesia/ui's shared indicator (ek-radio__input), larger here because the
|
||||
whole row is the target and the radio is its one visual anchor. */
|
||||
.ek-shell-choice .ek-shell-choice__input {
|
||||
--ek-choice-size: 1.25rem;
|
||||
margin: 0.125rem 0 0;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
/* The whole row carries the focus ring (above), so the radio inside it draws none of its own. */
|
||||
.ek-shell-choice .ek-shell-choice__input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ek-shell-choice__copy {
|
||||
@@ -350,8 +593,39 @@ export const SHELL_STYLESHEET = `
|
||||
font-size: ${designToken("typography.size-xs")};
|
||||
}
|
||||
|
||||
/* The phone header (ADR 0064): the menu trigger and the open page's name, kept at the top while the
|
||||
page scrolls, below the status bar or notch (viewport-fit=cover). Not a desktop top bar: it
|
||||
holds nothing else. */
|
||||
.ek-shell-mobile-bar {
|
||||
padding: ${designToken("spacing.sm")} ${designToken("spacing.md")} 0;
|
||||
align-items: center;
|
||||
background: ${designToken("background.canvas")};
|
||||
border-bottom: ${designToken("border.width")} solid ${designToken("border.divider")};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${designToken("spacing.sm")};
|
||||
min-height: calc(${MOBILE_BAR_HEIGHT} + env(safe-area-inset-top, 0px));
|
||||
padding: calc(${designToken("spacing.xs")} + env(safe-area-inset-top, 0px))
|
||||
calc(${designToken("spacing.sm")} + env(safe-area-inset-right, 0px))
|
||||
${designToken("spacing.xs")}
|
||||
calc(${designToken("spacing.sm")} + env(safe-area-inset-left, 0px));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ek-shell-mobile-bar__title {
|
||||
color: ${designToken("text.primary")};
|
||||
font-size: ${designToken("typography.size-md")};
|
||||
font-weight: ${designToken("typography.weight-medium")};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Something scrolled into view — a focused field, a fragment — lands below the header, not under it. */
|
||||
html:has(.ek-shell-mobile-bar) {
|
||||
scroll-padding-top: calc(${MOBILE_BAR_HEIGHT} + env(safe-area-inset-top, 0px) + ${designToken("spacing.sm")});
|
||||
}
|
||||
|
||||
.ek-shell-drawer-backdrop {
|
||||
@@ -372,6 +646,9 @@ export const SHELL_STYLESHEET = `
|
||||
max-width: 85vw;
|
||||
outline: none;
|
||||
padding: ${designToken("spacing.md")};
|
||||
padding-bottom: calc(${designToken("spacing.md")} + env(safe-area-inset-bottom, 0px));
|
||||
padding-left: calc(${designToken("spacing.md")} + env(safe-area-inset-left, 0px));
|
||||
padding-top: calc(${designToken("spacing.md")} + env(safe-area-inset-top, 0px));
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
z-index: 41;
|
||||
|
||||
Reference in New Issue
Block a user