From 1155352436dd157c71681f3e7c9a381422a51ac0 Mon Sep 17 00:00:00 2001 From: Zach Dunn Date: Sat, 22 Aug 2026 20:19:50 -0400 Subject: [PATCH 1/2] feat(web): migrate workspace people list off bespoke CSS Move renderMembersHtml/renderInvitesHtml/renderMembersPlaceholderHtml (workspace-ui.ts) from .member-row* classes to Tailwind utilities, and delete the now-unused .member-list/.member-row* rules from account-content.css (exclusive to the people tab; .ul-select styling for the role picker is unaffected, it's shared). --- apps/web/src/lib/workspace-ui.test.ts | 4 +- apps/web/src/lib/workspace-ui.ts | 35 +++-- apps/web/src/styles/account-content.css | 167 ------------------------ 3 files changed, 25 insertions(+), 181 deletions(-) diff --git a/apps/web/src/lib/workspace-ui.test.ts b/apps/web/src/lib/workspace-ui.test.ts index 8a47a158..143366cc 100644 --- a/apps/web/src/lib/workspace-ui.test.ts +++ b/apps/web/src/lib/workspace-ui.test.ts @@ -51,7 +51,7 @@ describe("renderMembersHtml", () => { it("leads with the email when there is no display name, without a sub-line", () => { const html = renderMembersHtml([{ email: "c@d.com", name: "", role: "member" }]); - expect(html).toContain('member-row__name">c@d.com<'); + expect(html).toMatch(/member-row__name[^"]*">c@d\.com { it("mirrors the real member row's two-column structure", () => { const html = renderMembersPlaceholderHtml(2); - expect(html.match(/class="member-row"/g)).toHaveLength(2); + expect(html.match(/class="member-row /g)).toHaveLength(2); // Both halves present, or the flex row collapses and the swap rearranges. expect(html).toContain("member-row__who"); expect(html).toContain("member-row__name"); diff --git a/apps/web/src/lib/workspace-ui.ts b/apps/web/src/lib/workspace-ui.ts index 42e83a60..237cb845 100644 --- a/apps/web/src/lib/workspace-ui.ts +++ b/apps/web/src/lib/workspace-ui.ts @@ -158,21 +158,32 @@ export function canManageMemberRow(member: MemberRow, opts: MemberRowOptions): b * People-tab member list. Name leads when set (email sub-line); manageable * rows get role `` + + ? `` + + `` + `` + `` - : `${escapeHtml(m.role)}`; - return `
${escapeHtml(lead)}${sub}${controls}
`; + : `${escapeHtml(m.role)}`; + return `
${escapeHtml(lead)}${sub}${controls}
`; }) .join(""); } @@ -191,9 +202,9 @@ export function renderMembersPlaceholderHtml(rows = 2): string { return Array.from( { length: rows }, (_, i) => - `
` + - `${skeletonBarHtml(widths[i % widths.length])}` + - `${skeletonBarHtml("42px")}` + + `
` + + `${skeletonBarHtml(widths[i % widths.length])}` + + `${skeletonBarHtml("42px")}` + `
`, ).join(""); } @@ -209,10 +220,10 @@ export function renderInvitesHtml( .map((inv) => { const status = inv.status || "pending"; return ( - `
` + - `${escapeHtml(inv.email)}` + - `` + - `${escapeHtml(status)}` + + `
` + + `${escapeHtml(inv.email)}` + + `` + + `${escapeHtml(status)}` + `` + `
` ); diff --git a/apps/web/src/styles/account-content.css b/apps/web/src/styles/account-content.css index 9322f3f0..ac2ba0c8 100644 --- a/apps/web/src/styles/account-content.css +++ b/apps/web/src/styles/account-content.css @@ -1,40 +1,5 @@ /* Content styles for /account/* cards (profile kv, workspace list, developer list). */ -/* Billing tab — plan comparison cards. */ -.plan-cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; - margin: 4px 0 14px; -} -.plan-card { - display: flex; - flex-direction: column; - gap: 8px; - padding: 16px; - border: 1px solid var(--line); - border-radius: 10px; -} -.plan-card.is-current { - border-color: var(--fg); -} -.plan-card__badge { - align-self: flex-start; - padding: 2px 8px; - border-radius: 999px; - background: var(--fg); - color: var(--bg); - font-size: var(--text-micro); - font-weight: var(--weight-semibold); - text-transform: uppercase; - letter-spacing: 0.04em; -} -.plan-card__name { - margin: 0; - font-size: var(--text-body); - font-weight: var(--weight-semibold); -} - /* Compact "Pro" marker shown outside the billing tab (issue #365 follow-up) — same uppercase/pill idiom as .plan-card__badge, but a muted accent outline rather than an inverse fg/bg fill so it reads as a small aside @@ -51,53 +16,6 @@ letter-spacing: 0.04em; line-height: 1.4; } -.plan-card__price { - margin: 0; - font-size: var(--text-meta); - color: var(--muted); -} -.plan-card__blurb { - margin: 0; - font-size: var(--text-meta); -} -.plan-card__limits { - margin: 4px 0 0; - padding: 0; - list-style: none; - display: grid; - gap: 4px; - font-size: var(--text-micro); -} -.plan-card__cta { - margin-top: auto; - align-self: flex-start; -} -/* Billing action buttons share the account area's standard button treatment - (same recipe as the invite-form submit) instead of native browser chrome. */ -.plan-card__cta, -#ws-manage-billing-btn { - font: var(--text-micro) var(--sans); - letter-spacing: 0.03em; - color: var(--accent); - border: 1px solid var(--line); - border-radius: 6px; - padding: 7px 12px; - background: var(--panel); - cursor: pointer; -} -.plan-card__cta:hover, -.plan-card__cta:focus-visible, -#ws-manage-billing-btn:hover, -#ws-manage-billing-btn:focus-visible { - border-color: var(--accent); - outline: none; -} -.plan-card__cta:disabled, -#ws-manage-billing-btn:disabled { - opacity: 0.6; - cursor: default; -} - /* Settings pages: one card, hairline sections — no stacked cards / nested boxes. */ section.card.settings-page { padding: 20px 22px 8px; @@ -1742,63 +1660,6 @@ button.wft-card__name:focus-visible { } } -/* People tab — member rows (innerHTML-injected by renderMembersHtml). */ -.member-list { - display: grid; -} -.member-row { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 14px; - padding: 9px 2px; - border-top: 1px solid var(--line); - font-size: var(--text-meta); -} -.member-row:first-child { - border-top: 0; -} -.member-row__who { - display: flex; - align-items: baseline; - gap: 10px; - min-width: 0; -} -.member-row__name { - font-weight: var(--weight-semibold); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.member-row__email { - color: var(--muted); - font-size: var(--text-micro); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -/* A row's height must not depend on which of three shapes its role cell - happens to render: a plain label (read-only viewer), a , so the caret/background-image would be wrong. */ -.member-row__role { - flex: none; - font: var(--text-micro) var(--sans); - padding: 5px 9px; - border: 1px solid transparent; - border-radius: 6px; - color: var(--muted); - letter-spacing: 0.08em; - text-transform: uppercase; -} - /* --bp-file-grid: the five fixed columns (64/64/92/32 + gaps) leave the name track nothing to render in, so filenames ellipsize to zero width. Drop the size and type columns (their values still live in the file's own page) and @@ -1818,34 +1679,6 @@ button.wft-card__name:focus-visible { } } -/* People tab: pending invites share the member-row surface + revoke control. */ -.member-row__actions { - display: flex; - align-items: center; - gap: 8px; -} -/* Styling now comes from .ul-select .ul-select--sm (markup in workspace-ui.ts) - — that primitive's `appearance: none` takes full control of the box, which - turns out to be exactly what removes the old rendering 2px taller than a ` - : ``; + ? `` + : ``; } return ` -
- ${isCurrent ? `Current plan` : ""} -

${escapeHtml(plan.name)}

- ${priceLine ? `

${escapeHtml(priceLine)}

` : ""} -

${escapeHtml(plan.blurb)}

-
    ${planCardLimitRows(plan)}
+
+ ${isCurrent ? `Current plan` : ""} +

${escapeHtml(plan.name)}

+ ${priceLine ? `

${escapeHtml(priceLine)}

` : ""} +

${escapeHtml(plan.blurb)}

+
    ${planCardLimitRows(plan)}
${footer}
`; }) diff --git a/apps/web/src/pages/account/workspaces/[name]/billing.astro b/apps/web/src/pages/account/workspaces/[name]/billing.astro index cd7f4405..4cd8dc2a 100644 --- a/apps/web/src/pages/account/workspaces/[name]/billing.astro +++ b/apps/web/src/pages/account/workspaces/[name]/billing.astro @@ -96,7 +96,7 @@ const cta: BillingCtaState = billing

Plans

@@ -113,6 +113,7 @@ const cta: BillingCtaState = billing