Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion apps/web/src/build-plan-button.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,66 @@
transform: translate(-50%, -50%);
}

/* Phone: the glyph alone, without the pill, so it reads as status rather than a control. */
/* Phone: the build's state and its action read as links in the top bar's subtitle. */
.build-status {
display: inline-flex;
min-width: 0;
align-items: center;
gap: var(--spacing);
}

.build-status-link {
position: relative;
display: inline-flex;
min-width: 0;
align-items: center;
gap: var(--spacing);
color: inherit;
font: inherit;
font-variant-numeric: tabular-nums;
white-space: nowrap;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
}

/* A thumb-sized target around the small text, as the capsule has. */
.build-status-link::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: max(100%, var(--control-target-coarse));
height: var(--control-target-coarse);
transform: translate(-50%, -50%);
}

.build-status-link:active {
opacity: 0.6;
}

/* Build plan and the action are things to do, so they take the brand's ink. */
.build-status-link:not(:has(.build-capsule-label)) {
color: var(--color-brand-ink);
font-weight: var(--font-weight-medium);
}

.build-status-link[aria-disabled="true"] {
color: inherit;
font-weight: inherit;
}

.build-status-link .build-capsule-label {
overflow: hidden;
text-overflow: ellipsis;
}

.build-status-link .build-glyph {
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
}

/* Compact: the glyph alone, without the pill, so it reads as status rather than a control. */
.build-capsule[data-compact] {
justify-content: center;
width: var(--control-height-compact);
Expand Down
32 changes: 20 additions & 12 deletions apps/web/src/build-plan-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClockIcon, LoaderIcon, ProgressRingIcon, WarningIcon, WrenchIcon } from "@chopin/icons";
import { ClockIcon, LoaderIcon, ProgressRingIcon, WarningIcon } from "@chopin/icons";
import { useEffect, useId, useLayoutEffect, useReducer, useRef, useState } from "react";

import {
Expand Down Expand Up @@ -342,7 +342,7 @@ export function BuildPlanButton(
onOpen: () => void;
/** Phone: touch has no tooltip, so a tap that cannot act shows this reason instead. */
onExplain?: (reason: string) => void;
/** A phone's top bar: Build plan shows as its glyph. */
/** A phone's top bar: the status and its action read as links in the title's subtitle. */
phone?: boolean;
/** Build can open; until then a phone tap on the glyph explains instead. */
tabReady?: boolean;
Expand Down Expand Up @@ -394,9 +394,8 @@ export function BuildPlanButton(
<button
aria-describedby={blocked ? reason : undefined}
aria-disabled={blocked ? true : undefined}
aria-label={phone ? "Build plan" : undefined}
className={phone
? "btn btn-icon btn-primary build-plan shrink-0"
? "build-status-link"
: "btn btn-compact btn-primary build-plan shrink-0"}
data-tooltip={blocked}
data-tooltip-detail={blocked ? "" : undefined}
Expand All @@ -411,8 +410,7 @@ export function BuildPlanButton(
}}
type="button"
>
{/* A phone's top bar keeps the title centred, so Build plan is its glyph there. */}
{phone ? <WrenchIcon aria-hidden="true" size={18} /> : "Build plan"}
Build plan
</button>
{blocked && <span className="sr-only" id={reason}>{blocked}</span>}
{live}
Expand All @@ -426,18 +424,18 @@ export function BuildPlanButton(
: look.kind === "failed" && look.retry && canEdit
? { name: "Retry", run: build.rebuild }
: look.kind === "stale" && look.build && canEdit
// On a phone the Build tab sits just below, so the action names what it builds.
? { name: compact ? "Build changes" : "Build", run: build.rebuild }
// Compact names what it builds; a phone's subtitle follows the status, which already says.
? { name: compact && !phone ? "Build changes" : "Build", run: build.rebuild }
: undefined;
let refused = action?.run === build.rebuild ? build.rebuildError : undefined;
let busy = build.busy || build.rebuilding;
return (
<>
<span className="build-capsule-row" key="capsule">
<span className={phone ? "build-status" : "build-capsule-row"} key="capsule">
<button
aria-describedby={look.tip ? reason : undefined}
aria-label={compact ? label : undefined}
className="build-capsule"
className={phone ? "build-status-link" : "build-capsule"}
data-compact={compact || undefined}
data-hot={hot || undefined}
data-tooltip={compact ? `${label} · ${look.tip}` : look.tip}
Expand All @@ -456,17 +454,27 @@ export function BuildPlanButton(
<span className="build-capsule-glyph" ref={glyph}>
<BuildGlyph look={look} />
</span>
{!compact && <span className="build-capsule-label" key={label}>{label}</span>}
{(!compact || phone) && (
<span className="build-capsule-label" key={label}>
{/* Beside its Build, a phone's narrow subtitle needs only the count. */}
{phone && action && look.kind === "stale"
? `${look.changes} ${look.changes === 1 ? "change" : "changes"}`
: label}
</span>
)}
</button>
{look.tip && <span className="sr-only" id={reason}>{look.tip}</span>}
{action && (
<>
{phone && <span aria-hidden="true">·</span>}
{/* Stays focusable while blocked or busy; aria-disabled and aria-busy say why. */}
<button
aria-busy={busy || undefined}
aria-describedby={blocked ? actionReason : undefined}
aria-disabled={blocked || busy ? true : undefined}
className="btn btn-compact btn-outline build-capsule-action"
className={phone
? "build-status-link build-capsule-action"
: "btn btn-compact btn-outline build-capsule-action"}
data-tooltip={blocked}
data-tooltip-detail={blocked ? "" : undefined}
data-tooltip-verbatim={blocked ? "" : undefined}
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/child-title.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Where an open child's title goes in the split workspace.
* Where an open child's title goes in the split workspace, or in a phone's top bar.
*
* Under Zoom the child replaces its parent in place, so its title, rename and actions join the
* end of the parent's path in the parent's title bar. Under Lift the child's sheet carries a
Expand All @@ -14,6 +14,10 @@ export type ChildTitle = {
motion: ChildMotionStyle;
/** The end of the parent's path under Zoom; null until it mounts, and under Lift. */
slot: HTMLElement | null;
/** A phone's top bar: the child renders its whole title and trailing controls into the slot. */
phone?: boolean;
/** The parent's title, for the child's subtitle on a phone. */
parent?: string;
};

export const ChildTitleContext = createContext<ChildTitle | undefined>(undefined);
Loading
Loading