refactor(desktop): migrate workspace invitations onto the design system
D19 of the design-system migration (Desktop stack, base feature/desktop-d18-workspace-member-list, #3394).
What
- Settings › Workspace › Invitations uses
@luke/ui'sSettingsSection, titled "Invitations", in place of the desktopSettingsGroup. Its search anchor (SETTINGS_SECTION.INVITATIONS) is on a wrapperdiv, 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 theCompatChipwithclassName="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), replacingtoolbar-button bordered-button
- the address:
- Each pending invitation is a
SettingsRowthroughMenuRow'srowform:- 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/TextAreatakerequired?: 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 asWindowChip.
Selectors
- Removed:
.workspace-invite .settings-input(the field is now.luke-text-field), and the old.workspace-invite-rolechip rule..workspace-invite-roleis now the chip's container. .workspace-inviteno 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-rowwrapper (the form plus its refusal). It joins.api-key, .workspace-identity, .workspace-memberand their> .error-messagerule. - The narrow-container rule (
@container settings (max-width: 379px)) now targets.workspace-inviteand.workspace-invite > .luke-text-field. .settings-inputis 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-stylesettings-inputoverride. - Invite uses the shared outline
Buttonface (regular weight, like Rename and Copy link), replacing the boldtoolbar-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 ofsmall(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:invitationRowsnow findsMenuRows whose description starts "Invited as ". The address is read from the row's label (addressOf) instead of the retiredstrong/small. No behaviour assertion changed.text-field.test.ts: new test. A required empty field holds back its form's submit and reportsvalueMissing; a field that isn't required does not.pnpm check:affectedexits 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.
| Light | Dark | |
|---|---|---|
| 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
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
TextFieldandTextAreanow acceptrequired, 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
CompatChipuse on this surface is gone; the leave flow's heir chip keeps it, imported asWindowChip. - 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-roleis now the chip's container;.settings-inputstays 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.



