diff --git a/specs/016-visual-redesign/tasks.md b/specs/016-visual-redesign/tasks.md
index 4e501e0..955f326 100644
--- a/specs/016-visual-redesign/tasks.md
+++ b/specs/016-visual-redesign/tasks.md
@@ -167,3 +167,4 @@ Items from the redesign review, one pull request each, in order.
- [x] R-03 Nav wording: the primary and footer navigation read Servers, Standards, Explorer, Breaches, Contribute (Scanner stays in the footer); every `` trail, the page-foot cross link on `/standards/` and the 404 buttons use the same words; `/mcp/` and `/standards/` take Servers and Standards as `` and eyebrow. "Atlas" stays the product word in ledes and body copy
- [x] R-04 Standards map rework (PR #62): the head is "The standards map" with the counts in the lede. A rail beside the drawing is legend, filter and reading pane in one: relation types as line-sample toggles (references off by default), publishing bodies as toggles (a band label in the drawing toggles the same), maturity chips, the live count, a reset, and a card that names the hovered or focused node's relations in words. The toggles are checkboxes that filter the drawing and the outline through `:has()` rules in the stylesheet, so filtering works without JavaScript; the island keeps the count, mirrors the state in the URL, shows the card and highlights the neighbourhood. "Recently revised" is a view of the map (dated nodes, the rest fades), not a list in the head. The drawing is laid out by `elkjs` at build time (`src/lib/graph.ts`): a spec sits a column right of what it builds on, columns group by body behind tinted bands, dependency edges are routed orthogonally, cross links stay curves. Below 40rem the drawing gives way to `StandardsOutline.astro`: the dependency forest (`buildForest`) as an indented outline, cross links as `` chips, the spine as a hairline; the outline replaces the flat list on every screen and carries body, family, revision and both links (FR-015). Baselines through the `refresh-screenshots` label
- [x] R-05 Matrix on phones: below 40rem the matrix is read one way at a time instead of the desktop table squeezed. A row of plain anchors (All and one per requirement group, with counts) picks the view through `:target`, so it works without JavaScript and every group stays in the HTML; the island turns the choice into `data-view` with `aria-current`, so switching keeps the scroll and adds no history entry. "All" draws every requirement as a strip of small glyphs per server, sized from the column with container units, so the whole atlas fits without sideways scroll; its glyphs are a picture and a group's view opens them. A group shows its own columns (one generated `nth-child` range each) with two-line horizontal labels instead of `vertical-rl`, two and a half columns in view so the next one peeks in, and scroll shadows at the frame edges. The server column is 7rem, names wrap to two lines and link to the server page, the transposed view. Desktop is unchanged. Baselines through the `refresh-screenshots` label
+- [x] R-06 Matrix on desktop: the table comes straight after the head, and the status key and the live count sit under it. "Filter and group" folds into one quiet button at the end of the head's meta line; opened, the form runs under the whole line. The revision is named once, in the meta line: the lede says "the MCP Authorization specification", the link to its text is "Specification text", and the published date shows only when it differs from the revision id. On wide screens the table fits the column instead of scrolling past its edge (at 1440 px three groups were hidden behind a clipped "Tra…"): its widths come from the group row, a group whose label is wider than its columns (`data-narrow`, from an estimate of the label's length) takes the label's width, and the other columns share the rest down to a 2.25rem floor, below which the frame scrolls. The island redoes the sums when a filter changes the groups. Phones keep R-05. Baselines through the `refresh-screenshots` label
diff --git a/src/components/Matrix.svelte b/src/components/Matrix.svelte
index 5270526..d8d0c7d 100644
--- a/src/components/Matrix.svelte
+++ b/src/components/Matrix.svelte
@@ -13,6 +13,8 @@ let opener: HTMLElement | null = null;
const PARAMS = ["level", "category", "party", "q", "access", "auth", "group"] as const;
type Param = (typeof PARAMS)[number];
+// the width in rem below which a group's label counts as wider than its columns (MatrixTable)
+const NARROW_COLUMN_REM = 2.75;
onMount(() => {
const table = document.getElementById("matrix");
@@ -117,10 +119,20 @@ onMount(() => {
perCategory.set(c, (perCategory.get(c) ?? 0) + 1);
}
}
+ // on wide screens a group whose label outgrows its columns takes the label's width
+ let narrowRem = 0;
+ let wideCols = 0;
for (const gh of groupHeads) {
const n = perCategory.get(gh.dataset.category ?? "") ?? 0;
gh.hidden = n === 0;
- if (n > 0) gh.colSpan = n;
+ if (n === 0) continue;
+ gh.colSpan = n;
+ gh.style.setProperty("--n", String(n));
+ const label = Number(gh.dataset.labelRem ?? 0);
+ const narrow = label > n * NARROW_COLUMN_REM;
+ gh.toggleAttribute("data-narrow", narrow);
+ if (narrow) narrowRem += label;
+ else wideCols += n;
}
const featureKey = [...visibleFeatures].join(" ");
const columnsChanged = featureKey !== shownFeatures;
@@ -141,6 +153,8 @@ onMount(() => {
if (columnsChanged) {
// the table's layout is fixed: its width and the feature column follow the columns shown
table.style.setProperty("--cols", String(visibleFeatures.size));
+ table.style.setProperty("--wide-cols", String(wideCols));
+ table.style.setProperty("--narrow-w", `${narrowRem}rem`);
if (featureCol) featureCol.span = Math.max(visibleFeatures.size, 1);
}
regroup(s.group, visibleFeatures.size);
diff --git a/src/components/MatrixPage.astro b/src/components/MatrixPage.astro
index 9d69a9d..c2fe9c9 100644
--- a/src/components/MatrixPage.astro
+++ b/src/components/MatrixPage.astro
@@ -68,20 +68,24 @@ const classWords = (counts: { value: string; count: number }[], label: RecordServers
Who supports what in MCP Authorization
- The public MCP servers we list, against what MCP Authorization {matrix.revision.id} requires
+ The public MCP servers we list, against what the MCP Authorization specification requires
of servers and their authorization servers. Each cell opens the observation behind it.
-
+
Revision {matrix.revision.id}{" "}
{isCurrent ? "current" : matrix.revision.status}
-
- published
-
- {spec.name} {matrix.revision.id}
+ {
+ matrix.revision.date !== matrix.revision.id && (
+
+ published
+
+ )
+ }
+ Specification textAbout this specification
{
revisions
@@ -93,73 +97,76 @@ const classWords = (counts: { value: string; count: number }[], label: RecordWhat changed since {diff.from.id}}
-
+
+ Filter and group
+
+
+
-
-
- Filter and group
-
-
+
+
+
+
{
STATUS_ORDER.map((s) => (
@@ -172,15 +179,12 @@ const classWords = (counts: { value: string; count: number }[], label: Record
-
+
{matrix.rows.length} of {matrix.rows.length} servers, {matrix.columns.length} of{" "}
{matrix.columns.length} features
-
-
-
MUST, SHOULD
@@ -277,16 +281,11 @@ const classWords = (counts: { value: string; count: number }[], label: Record summary {
list-style: none;
@@ -303,18 +302,32 @@ const classWords = (counts: { value: string; count: number }[], label: Record summary::after {
margin-top: 0.2em;
rotate: -135deg;
}
.filters[open] > .toolbar {
+ align-self: stretch;
margin: var(--space-3) 0 0;
+ color: var(--ink);
+ }
+
+ /* under the matrix: the status key and the live count */
+ .matrix-key {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--space-3) var(--space-5);
+ margin: var(--space-4) 0 0;
}
- .matrix-bar__count {
+ .matrix-key__count {
margin: 0 0 0 auto;
}
@@ -403,10 +416,10 @@ const classWords = (counts: { value: string; count: number }[], label: Record `:nth-child(${r.from + 1})`)
.join(",")}){border-left:1px solid var(--rule)}}}`;
+/*
+ * Wide screens (follow-up 6): the table fits the column instead of scrolling past its edge. A
+ * group whose label is wider than its columns, such as a one-column "Transport", takes the
+ * label's width; the other columns share what is left. The label width is an estimate from its
+ * length in --tiny Geist, a little generous; the island redoes the sums when a filter changes
+ * the groups.
+ */
+const NARROW_COLUMN_REM = 2.75;
+const labelOf = (c: string) => CATEGORY_LABEL[c as keyof typeof CATEGORY_LABEL] ?? c;
+const labelRem = (c: string) => Math.ceil((labelOf(c).length * 0.42 + 0.875) * 4) / 4;
+const narrow = groups.filter((g) => labelRem(g.category) > g.count * NARROW_COLUMN_REM);
+const narrowRem = narrow.reduce((sum, g) => sum + labelRem(g.category), 0);
+const wideCols = columns.length - narrow.reduce((sum, g) => sum + g.count, 0);
+
const changedIds = new Map(diff?.changed.map((c) => [c.feature.id, c]) ?? []);
const addedIds = new Set(diff?.added.map((f) => f.id) ?? []);
const changesHref = diff ? `/mcp/changes/${diff.from.id}/${diff.to.id}/` : undefined;
@@ -87,7 +101,7 @@ const shortLabel = (id: string) => id.split(".").slice(2).join(".") || id;
class="matrix"
id="matrix"
data-matrix-revision={revision.id}
- style={`--cols: ${columns.length}`}
+ style={`--cols: ${columns.length}; --wide-cols: ${wideCols}; --narrow-w: ${narrowRem}rem`}
>
Support for each feature of MCP Authorization {revision.id} by server. Rows are servers in
@@ -102,8 +116,15 @@ const shortLabel = (id: string) => id.split(".").slice(2).join(".") || id;
{
groups.map((g) => (
-
- {CATEGORY_LABEL[g.category as keyof typeof CATEGORY_LABEL] ?? g.category}
+
+ {labelOf(g.category)}
))
}
@@ -629,6 +650,36 @@ const shortLabel = (id: string) => id.split(".").slice(2).join(".") || id;
translate: none;
}
+ /*
+ * Wide screens: the table fits the column. Its widths come from the group row, so a group
+ * whose label needs more room than its columns (data-narrow) takes the label's width, and
+ * the other columns share the rest, down to a floor below which the frame scrolls.
+ */
+ @media (width > 40rem) {
+ .matrix {
+ --feature-w: max(
+ 2.25rem,
+ calc((100cqi - var(--server-w) - var(--narrow-w, 0rem)) / var(--wide-cols, var(--cols)))
+ );
+ width: calc(
+ var(--server-w) + var(--wide-cols, var(--cols)) * var(--feature-w) + var(--narrow-w, 0rem)
+ );
+ }
+
+ .matrix .matrix__feature-col {
+ width: auto;
+ }
+
+ .matrix .groups th {
+ width: calc(var(--n, 1) * var(--feature-w));
+ }
+
+ .matrix .groups th[data-narrow] {
+ width: var(--label);
+ padding-inline: var(--space-1);
+ }
+ }
+
/* the group picker and its hint exist for phones only */
.matrix-tabs,
.matrix-tabs__hint {
@@ -642,10 +693,13 @@ const shortLabel = (id: string) => id.split(".").slice(2).join(".") || id;
* peek of the next column and shadows at the edges that scroll. The names link to the server
* page, which is the same data transposed. .matrix-view:is([data-view="all"], :not([data-view]):not(:has(.matrix-tabs a:not(#mx-all):target))) is the "All" view, .matrix-view:is([data-view]:not([data-view="all"]), :not([data-view]):has(.matrix-tabs a:not(#mx-all):target)) a group's.
*/
+ .matrix-view {
+ container-type: inline-size;
+ }
+
@media (max-width: 40rem) {
.matrix-view {
--server-w: 7rem;
- container-type: inline-size;
}
.matrix-tabs {
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-dark.png b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-dark.png
index 20f117c..62f8f09 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-dark.png and b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-dark.png differ
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-light.png b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-light.png
index b6984dc..1beef7a 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-light.png and b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-light.png differ
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png
index fb5cb87..9dd84a9 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png and b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png differ
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png
index 24af30c..9729baa 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png and b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png differ
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png
index 949017a..c3c8dc9 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png and b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png differ
diff --git a/tests/e2e/matrix.spec.ts b/tests/e2e/matrix.spec.ts
index 009466b..6805007 100644
--- a/tests/e2e/matrix.spec.ts
+++ b/tests/e2e/matrix.spec.ts
@@ -227,6 +227,22 @@ test.describe("matrix page (US1)", () => {
expect(scroll).toBeLessThanOrEqual(inner ?? 0);
});
+ test("on a wide screen the table fits its frame and no group label is clipped (follow-up 6)", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/mcp/");
+ await expect(page.locator("#matrix[data-enhanced]")).toBeAttached();
+ const region = page.locator(".matrix-scroll");
+ expect(await region.evaluate((el) => el.scrollWidth - el.clientWidth)).toBeLessThanOrEqual(1);
+ const clipped = await page
+ .locator("#matrix thead tr.groups th[data-category] span")
+ .evaluateAll((els) =>
+ els.filter((el) => el.scrollWidth > el.clientWidth).map((el) => el.textContent),
+ );
+ expect(clipped).toEqual([]);
+ });
+
test("a group scrolls inside its region at 320px with the server column sticky (016 WP4)", async ({
page,
}) => {