refactor(desktop): migrate workspace invitations onto the design system

Merged
CH
5d ago
+124-72

D19 of the design-system migration (Desktop stack, base feature/desktop-d18-workspace-member-list, #3394).

What

  • Settings › Workspace › Invitations uses @luke/ui's SettingsSection, titled "Invitations", in place of the desktop SettingsGroup. Its search anchor (SETTINGS_SECTION.INVITATIONS) is on a wrapper div, as D18 did for Members.
  • The invite form uses the strict barrel controls:
    • the address: TextField (TEXT_FIELD_TYPE.EMAIL, required, autofill off, spell check off), replacing the raw <input class="settings-input">
    • the role: the barrel Chip (field look, chevron) with its menu unchanged, replacing the CompatChip with className="workspace-invite-role". The chip's fixed 104px width now comes from its container (.workspace-invite-role > .luke-chip), the same pattern as .settings-picker > .luke-chip
    • Invite: the outline Button (BUTTON_TYPE.SUBMIT), replacing toolbar-button bordered-button
  • Each pending invitation is a SettingsRow through MenuRow's row form:
    • leading: the invited face
    • label: the address, kept to one line by OneLine
    • description: "Invited as … · Expires …"
    • control: Copy link (outline Button), then the ⋯ with Cancel invitation
  • Shared API (approved by the orchestrator): TextField/TextArea take required?: boolean, passed through to the element. Without it, the address field would lose the browser's own check, which stops an empty or malformed address before anything is sent. It has a jsdom test.
  • Behaviour is unchanged:
    • invite by address and role, with Member as the default and Admin dimmed for an admin
    • the new invitation is listed at once, then reconciled with the service
    • one write runs at a time, so a second Invite or Cancel while one is in flight sends nothing
    • Copy link copies the accept URL; Cancel invitation asks nothing first
    • a refusal is shown under the form (role="alert"); a first read that fails offers Retry
  • This removes the last ../chip (CompatChip) use on this surface. The leave flow's heir chip (D22) keeps it, imported as WindowChip.

Selectors

  • Removed: .workspace-invite .settings-input (the field is now .luke-text-field), and the old .workspace-invite-role chip rule. .workspace-invite-role is now the chip's container.
  • .workspace-invite no longer relies on .settings-row/.settings-card > *. It has its own row layout on contract tokens (--luke-settings-row-min-height, --luke-settings-row-block/inline, --luke-control-gap).
  • New .workspace-invite-row wrapper (the form plus its refusal). It joins .api-key, .workspace-identity, .workspace-member and their > .error-message rule.
  • The narrow-container rule (@container settings (max-width: 379px)) now targets .workspace-invite and .workspace-invite > .luke-text-field.
  • .settings-input is not removed, because 11 other callers still use it.

Visible changes

  • The address field is the shared TextField: strong hairline, input fill, control inset padding. It replaces the API-keys-style settings-input override.
  • Invite uses the shared outline Button face (regular weight, like Rename and Copy link), replacing the bold toolbar-button bordered-button.
  • The gap between the field, chip and Invite is 8px (--luke-control-gap), down from 12px.
  • In a pending invitation, the address uses the label role (13/18, 500) instead of strong (13/1.35). The role and expiry line uses the caption role (12/16, subtle) instead of small (12/1.4, tertiary). This matches the Members rows above it.
  • Below a 380px row, Copy link and the ⋯ drop below the words, as the shared row contract requires.

Tests

  • workspace-page.test.ts: invitationRows now finds MenuRows whose description starts "Invited as ". The address is read from the row's label (addressOf) instead of the retired strong/small. No behaviour assertion changed.
  • text-field.test.ts: new test. A required empty field holds back its form's submit and reports valueMissing; a field that isn't required does not.
  • pnpm check:affected exits 0.

Evidence

Captured on Linux with headless Chrome at 1280×840. The real WorkspacePage was rendered in jsdom by a throwaway, uncommitted copy of the page's test, as owner of a synthetic workspace "acme". The output was placed in the built desktop stylesheet inside the SettingsPage frame.

LightDark
Two pending invitations, one with a long address
Address already invited, shown under the form

Not verified on a Mac

  • SF Pro rendering of the form and rows
  • The browser's own "fill in this field" bubble on an empty or malformed address in Electron
  • Inviting, copying and cancelling against a live workspace, which were tested only through the tests
  • The search-result ring landing on the Invitations wrapper in the real window

🤖 Generated with Claude Code


Open workspace in Conductor


Devin Review


Summary by cubic

Migrates Settings › Workspace › Invitations onto the design system, matching the Members section from D18. The section now lives in @luke/ui's SettingsSection, the invite form uses the shared TextField, field Chip, and outline Button, and each pending invitation is a MenuRow row — the invited face, the address as a one-line label, "Invited as … · Expires …" as the description, and Copy link before the ⋯ menu. Inviting by address and role, the role defaults, one write at a time, and the refusal shown under the form are unchanged; the tests now locate rows by their new roles.

API

  • TextField and TextArea now accept required, passed through to the element, so the email field keeps the browser's empty-or-malformed check before anything is sent; covered by a jsdom test.

Selectors and layout

  • The last CompatChip use on this surface is gone; the leave flow's heir chip keeps it, imported as WindowChip.
  • The invite form no longer relies on .settings-row/.settings-card > *, drawing its row on contract tokens with the field gap down from 12px to 8px.
  • .workspace-invite-role is now the chip's container; .settings-input stays because 11 other callers still use it.

The two merges from main only resolved conflicts in unrelated migrated surfaces; the invitation migration itself is unchanged.