Skip to content

Commit a6491c8

Browse files
committed
chore: sweep ui fixes
1 parent 4fe8864 commit a6491c8

24 files changed

Lines changed: 249 additions & 69 deletions

File tree

.changeset/sweep-ui-fixes.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@cloudflare/create-nimbus-docs": patch
3+
---
4+
5+
Fix a batch of UI stress-sweep defects in the starter components:
6+
7+
- **TOC scroll-spy** no longer desyncs when a heading slugs to an empty id (e.g. an emoji-only `## 🎉`). The active-heading index now stays aligned with the full link/rail set instead of a resolvable-only subset, so every section below an unresolvable heading highlights correctly.
8+
- **Mobile sidebar** hamburger survives client-side navigation — the toggle re-binds on `astro:page-load` and tears down on `astro:before-swap` (via `mount()`), fixing a dead button after the first view transition, with the scroll lock balanced on a mid-open swap.
9+
- **Dialog** content taller than the cap now scrolls inside the panel (`overflow-y-auto`) so the close button stays reachable.
10+
- **Banner** long unbroken strings (including the framework deprecation banner's version URL) wrap instead of overflowing.
11+
- **PackageManagers** blocks with identical props on one page now get unique, incremental-build-stable DOM ids (per-page counter), fixing duplicate `id`/`aria-controls`.
12+
- Dev-only warnings: `<Steps>` around a bullet list, and duplicate labels within a `<Tabs syncKey>` group.

AGENT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ The architecture splits into three tiers, one test per tier:
6868

6969
**When in doubt, default to framework; the starter should grow slowly.**
7070

71+
**The sharper cut — when a test above is ambiguous, or a single file mixes both:** *one correct answer → framework; a user might reasonably want a different answer → their repo.* A scroll-spy's active-heading math has exactly one right answer (framework); the rail's *look* does not (starter). When a `.client.ts` inlines both mechanism and taste, split it at that line — expose the mechanism as a `nimbus-docs/client` primitive and keep the taste in the user file — rather than filing the whole file under one tier. `tabs.client.ts` (thin glue over `initTabs`) is the target shape; a fat, all-mechanism `.client.ts` in the starter is the smell.
72+
7173
## Derived templates
7274

7375
**Drift discipline: canonical source → generator → orphan branch → tagged.** Hand-edits happen in one place, `packages/nimbus-starter-source/`. The generator (`packages/create-nimbus-docs/scripts/copy-template.mjs`) emits one directory per variant from that source plus the manifest. The CLI tarball carries **no templates**; distribution lives in this repo — the variants live on an orphan `templates` branch (no shared history with `main`), synced and **tagged `templates-v<create-nimbus-docs version>`** by the release job. At scaffold time `create-nimbus-docs` fetches its matching tag via giget (`github:cloudflare/nimbus/<variant>#templates-v<version>`); the tag's tree is templates-only, so the tarball stays small even though the repo also holds all of `main`. A starter edit therefore still produces a diff touching only `packages/nimbus-starter-source/**`; the `templates` branch is sync output, never hand-edited (a branch ruleset rejects human pushes, and `templates-v*` tags are immutable for everyone — including the bot).

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ The architecture splits into three tiers, one test per tier:
6868

6969
**When in doubt, default to framework; the starter should grow slowly.**
7070

71+
**The sharper cut — when a test above is ambiguous, or a single file mixes both:** *one correct answer → framework; a user might reasonably want a different answer → their repo.* A scroll-spy's active-heading math has exactly one right answer (framework); the rail's *look* does not (starter). When a `.client.ts` inlines both mechanism and taste, split it at that line — expose the mechanism as a `nimbus-docs/client` primitive and keep the taste in the user file — rather than filing the whole file under one tier. `tabs.client.ts` (thin glue over `initTabs`) is the target shape; a fat, all-mechanism `.client.ts` in the starter is the smell.
72+
7173
## Derived templates
7274

7375
**Drift discipline: canonical source → generator → orphan branch → tagged.** Hand-edits happen in one place, `packages/nimbus-starter-source/`. The generator (`packages/create-nimbus-docs/scripts/copy-template.mjs`) emits one directory per variant from that source plus the manifest. The CLI tarball carries **no templates**; distribution lives in this repo — the variants live on an orphan `templates` branch (no shared history with `main`), synced and **tagged `templates-v<create-nimbus-docs version>`** by the release job. At scaffold time `create-nimbus-docs` fetches its matching tag via giget (`github:cloudflare/nimbus/<variant>#templates-v<version>`); the tag's tree is templates-only, so the tarball stays small even though the repo also holds all of `main`. A starter edit therefore still produces a diff touching only `packages/nimbus-starter-source/**`; the `templates` branch is sync output, never hand-edited (a branch ruleset rejects human pushes, and `templates-v*` tags are immutable for everyone — including the bot).

apps/www/src/components/ui/banner/Banner.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const safeContent = sanitizeBannerHtml(content);
6767
background: var(--_t);
6868
}
6969

70+
.banner-card-body { min-width: 0; overflow-wrap: break-word; }
7071
.banner-card-body :global(p) { margin: 0; }
7172
.banner-card-body :global(p + p) { margin-top: 0.375rem; }
7273
.banner-card-body :global(a) { text-decoration: underline; text-underline-offset: 2px; }

apps/www/src/components/ui/dialog/DialogContent.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const { class: className, ...attrs } = Astro.props;
1313

1414
<div class="flex items-start justify-center pt-[10vh] px-4 pointer-events-none">
1515
<div
16-
class={cn("pointer-events-auto flex max-h-[70vh] w-full flex-col overflow-hidden rounded-lg bg-muted shadow-lg ring-1 ring-border", className)}
16+
class={cn("pointer-events-auto flex max-h-[70vh] w-full flex-col overflow-y-auto rounded-lg bg-muted shadow-lg ring-1 ring-border", className)}
1717
{...attrs}
1818
>
1919
<slot />

apps/www/src/components/ui/embed/Embed.astro

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ const {
3030
class: className,
3131
...attrs
3232
} = Astro.props;
33+
34+
// `title` is typed as required, but MDX doesn't enforce prop types — a missing
35+
// title would ship a nameless iframe (WCAG 4.1.2). Fall back to an honest
36+
// host-derived name and warn the author in dev.
37+
let resolvedTitle = title;
38+
if (!resolvedTitle) {
39+
let host = "";
40+
try {
41+
host = new URL(src).hostname.replace(/^www\./, "");
42+
} catch {}
43+
resolvedTitle = host ? `Embedded content from ${host}` : "Embedded content";
44+
if (import.meta.env.DEV) {
45+
console.warn(
46+
`[nimbus] <Embed src="${src}"> has no title; using a fallback accessible ` +
47+
"name. Set `title` to describe the embed.",
48+
);
49+
}
50+
}
3351
---
3452

3553
<div
@@ -38,7 +56,7 @@ const {
3856
>
3957
<iframe
4058
src={src}
41-
title={title}
59+
title={resolvedTitle}
4260
loading="lazy"
4361
allow={allow}
4462
allowfullscreen={allowfullscreen}

apps/www/src/components/ui/frame/Frame.astro

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,20 @@ const sizes = "(min-width: 768px) 768px, 100vw";
9292
border-radius: 0.5rem;
9393
}
9494

95-
/* Aspect-locked variant — locks the media box to the given ratio and
96-
absolutely positions any direct child so it fills (img / video /
97-
iframe / svg / div / etc). */
95+
/* Aspect-locked variant — locks the media box to the given ratio. `aspect`
96+
is single-media by design: only replaced media (img/video/iframe/svg) is
97+
filled, so a stray non-media child isn't stretched or stacked. */
9898
.frame[data-frame-aspect] .frame-media {
9999
position: relative;
100100
display: block;
101101
width: 100%;
102102
aspect-ratio: var(--frame-aspect);
103103
}
104104

105-
.frame[data-frame-aspect] .frame-media > :global(*) {
105+
.frame[data-frame-aspect] .frame-media > :global(img),
106+
.frame[data-frame-aspect] .frame-media > :global(video),
107+
.frame[data-frame-aspect] .frame-media > :global(iframe),
108+
.frame[data-frame-aspect] .frame-media > :global(svg) {
106109
position: absolute;
107110
inset: 0;
108111
width: 100%;

apps/www/src/components/ui/package-managers/PackageManagers.astro

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ interface Props extends CommandOptions {
1717
1818
const { pkg, type = "add", args, dev, comment } = Astro.props;
1919
const tabs = getTabs(type, pkg, { args, dev, comment });
20-
// Deterministic ID — hash the props so two builds with identical content
21-
// produce identical IDs. Required for incremental builds: a per-render
22-
// `crypto.randomUUID()` would make warm output differ from cold.
20+
// Prop hash + a per-page counter (on Astro.locals) so two blocks with
21+
// identical props still get distinct ids. Counter order is stable within a
22+
// render, so incremental/warm builds match cold — unlike crypto.randomUUID().
23+
const localsAny = Astro.locals as Record<string, unknown>;
24+
const counters =
25+
(localsAny.__nbCounters as Map<string, number>) ??
26+
(localsAny.__nbCounters = new Map<string, number>());
27+
const n = (counters.get("package-managers") ?? 0) + 1;
28+
counters.set("package-managers", n);
2329
const uid =
2430
"pm-" +
2531
createHash("sha256")
2632
.update(JSON.stringify({ pkg, type, args, dev, comment }))
2733
.digest("hex")
28-
.slice(0, 12);
34+
.slice(0, 12) +
35+
"-" +
36+
n.toString(16).padStart(4, "0");
2937
---
3038

3139
<script is:inline>

apps/www/src/components/ui/popover/PopoverContent.astro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,10 @@ const { placement = "bottom-start", class: className, ...attrs } = Astro.props;
3434
position: fixed;
3535
margin: 0;
3636
inset: auto;
37+
/* Never exceed the viewport (16px = 2× the anchor's 8px pad, absorbing
38+
the scrollbar width); overflow scrolls inside the panel. */
39+
max-width: calc(100vw - 16px);
40+
max-height: calc(100vh - 16px);
41+
overflow: auto;
3742
}
3843
</style>

apps/www/src/components/ui/steps/steps.client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import { mount } from "@cloudflare/nimbus-docs/client";
1010

1111
function initSteps(root: HTMLElement): () => void {
1212
const lists = root.querySelectorAll<HTMLOListElement>("ol");
13+
if (
14+
import.meta.env.DEV &&
15+
lists.length === 0 &&
16+
root.querySelector("[data-step]") === null &&
17+
root.children.length > 0
18+
) {
19+
console.warn(
20+
"[nimbus] <Steps> expects an ordered list (`1.` items) or <Step> " +
21+
"children. A bullet list renders with no numbers or connectors — " +
22+
"use an ordered list.",
23+
);
24+
}
1325
lists.forEach((ol) => ol.setAttribute("role", "list"));
1426

1527
return () => {

0 commit comments

Comments
 (0)