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
1 change: 1 addition & 0 deletions specs/016-visual-redesign/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Crumbs>` trail, the page-foot cross link on `/standards/` and the 404 buttons use the same words; `/mcp/` and `/standards/` take Servers and Standards as `<title>` 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 `<details>` 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
16 changes: 15 additions & 1 deletion src/components/Matrix.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
189 changes: 101 additions & 88 deletions src/components/MatrixPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,24 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st
<p class="eyebrow">Servers</p>
<h1>Who supports what in <span class="nowrap">MCP Authorization</span></h1>
<p class="lede">
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.
</p>
<p class="page-head__meta revision-line" aria-label="Specification revision">
<div class="page-head__meta revision-line">
<span>
Revision <strong class="revision-id" data-revision>{matrix.revision.id}</strong>{" "}
<span class:list={["chip", { signal: isCurrent }]}>
{isCurrent ? "current" : matrix.revision.status}
</span>
</span>
<span>
published <time datetime={matrix.revision.date}>{matrix.revision.date}</time>
</span>
<a href={matrix.revision.url} rel="noopener">{spec.name} {matrix.revision.id}</a>
{
matrix.revision.date !== matrix.revision.id && (
<span>
published <time datetime={matrix.revision.date}>{matrix.revision.date}</time>
</span>
)
}
<a href={matrix.revision.url} rel="noopener">Specification text</a>
<a href="/standards/mcp-authorization/">About this specification</a>
{
revisions
Expand All @@ -93,73 +97,76 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st
))
}
{diff && <a href={`/mcp/changes/${diff.from.id}/${diff.to.id}/`}>What changed since {diff.from.id}</a>}
</p>
<details class="filters" id="matrix-filter-panel">
<summary class="button small quiet">Filter and group</summary>
<form id="matrix-filters" class="toolbar explainer" method="get" action={hrefFor(revisionId)}>
<div class="field search">
<label for="f-q">Search servers</label>
<div class="search__box">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg>
<input id="f-q" name="q" type="search" placeholder="e.g. stripe" autocomplete="off" />
</div>
</div>
<div class="field">
<label for="f-access">Access</label>
<select id="f-access" name="access">
<option value="">Any access</option>
{accessCounts.map((c) => <option value={c.value}>{ACCESS_LABEL[c.value]} ({c.count})</option>)}
</select>
</div>
<div class="field">
<label for="f-auth">Auth</label>
<select id="f-auth" name="auth">
<option value="">Any auth</option>
{authCounts.map((c) => <option value={c.value}>{AUTH_LABEL[c.value]} ({c.count})</option>)}
</select>
</div>
<div class="field">
<label for="f-group">Group rows</label>
<select id="f-group" name="group">
<option value="">A to Z</option>
<option value="access">By access</option>
<option value="auth">By auth</option>
</select>
</div>
<div class="field">
<label for="f-level">Level</label>
<select id="f-level" name="level">
<option value="">All levels</option>
{levels.filter((l) => usedLevels.has(l)).map((l) => <option value={l}>{l}</option>)}
</select>
</div>
<div class="field">
<label for="f-category">Category</label>
<select id="f-category" name="category">
<option value="">All categories</option>
{usedCategories.map((c) => <option value={c}>{CATEGORY_LABEL[c]}</option>)}
</select>
</div>
<div class="field">
<label for="f-party">Party</label>
<select id="f-party" name="party">
<option value="">Both parties</option>
<option value="mcp-server">{PARTY_LABEL["mcp-server"]}</option>
<option value="authorization-server">{PARTY_LABEL["authorization-server"]}</option>
</select>
</div>
<div class="field actions">
<button type="submit" class="button small">Apply</button>
<button type="reset" class="button small quiet">Reset</button>
</div>
</form>
</details>
</div>
</header>

<div class="matrix-bar">
<details class="filters" id="matrix-filter-panel">
<summary class="button small quiet">Filter and group</summary>
<form id="matrix-filters" class="toolbar explainer" method="get" action={hrefFor(revisionId)}>
<div class="field search">
<label for="f-q">Search servers</label>
<div class="search__box">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg>
<input id="f-q" name="q" type="search" placeholder="e.g. stripe" autocomplete="off" />
</div>
</div>
<div class="field">
<label for="f-access">Access</label>
<select id="f-access" name="access">
<option value="">Any access</option>
{accessCounts.map((c) => <option value={c.value}>{ACCESS_LABEL[c.value]} ({c.count})</option>)}
</select>
</div>
<div class="field">
<label for="f-auth">Auth</label>
<select id="f-auth" name="auth">
<option value="">Any auth</option>
{authCounts.map((c) => <option value={c.value}>{AUTH_LABEL[c.value]} ({c.count})</option>)}
</select>
</div>
<div class="field">
<label for="f-group">Group rows</label>
<select id="f-group" name="group">
<option value="">A to Z</option>
<option value="access">By access</option>
<option value="auth">By auth</option>
</select>
</div>
<div class="field">
<label for="f-level">Level</label>
<select id="f-level" name="level">
<option value="">All levels</option>
{levels.filter((l) => usedLevels.has(l)).map((l) => <option value={l}>{l}</option>)}
</select>
</div>
<div class="field">
<label for="f-category">Category</label>
<select id="f-category" name="category">
<option value="">All categories</option>
{usedCategories.map((c) => <option value={c}>{CATEGORY_LABEL[c]}</option>)}
</select>
</div>
<div class="field">
<label for="f-party">Party</label>
<select id="f-party" name="party">
<option value="">Both parties</option>
<option value="mcp-server">{PARTY_LABEL["mcp-server"]}</option>
<option value="authorization-server">{PARTY_LABEL["authorization-server"]}</option>
</select>
</div>
<div class="field actions">
<button type="submit" class="button small">Apply</button>
<button type="reset" class="button small quiet">Reset</button>
</div>
</form>
</details>
<MatrixTable matrix={matrix} diff={diff} scans={scans} />
<Matrix client:idle />

<div class="matrix-key">
<ul class="legend__statuses" aria-label="Legend">
{
STATUS_ORDER.map((s) => (
Expand All @@ -172,15 +179,12 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st
<StatusCell status="unknown" derived variant="label" />
</li>
</ul>
<p id="matrix-count" class="count matrix-bar__count" aria-live="polite">
<p id="matrix-count" class="count matrix-key__count" aria-live="polite">
{matrix.rows.length} of {matrix.rows.length} servers, {matrix.columns.length} of{" "}
{matrix.columns.length} features
</p>
</div>

<MatrixTable matrix={matrix} diff={diff} scans={scans} />
<Matrix client:idle />

<section class="legend" aria-label="Reading the matrix">
<p class="legend__note">
<span class="level level-MUST">MUST</span>, <span class="level level-SHOULD">SHOULD</span>
Expand Down Expand Up @@ -277,16 +281,11 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st
}

/*
* The bar right above the matrix: the filters folded into one button, the status legend and
* the live count. The matrix stays the first thing under the head; opened, the filters take
* the bar's full width.
* The filters fold into one quiet button at the end of the head's meta line, so the matrix is
* the first thing under the head; opened, they take the line's full width.
*/
.matrix-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3) var(--space-5);
margin: var(--space-6) 0 var(--space-4);
.filters {
margin-left: auto;
}
.filters > summary {
list-style: none;
Expand All @@ -303,18 +302,32 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st
border-width: 0 1.5px 1.5px 0;
rotate: 45deg;
}
/* opened, the button stays where it was and the form runs under the whole line */
.filters[open] {
order: 2;
flex-basis: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.filters[open] > 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;
}

Expand Down Expand Up @@ -403,10 +416,10 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st
}

@media (max-width: 40rem) {
.matrix-bar {
.matrix-key {
gap: var(--space-2) var(--space-4);
}
.matrix-bar__count {
.matrix-key__count {
margin-left: 0;
}
.legend__statuses {
Expand Down
Loading
Loading