Skip to content
Merged
12 changes: 12 additions & 0 deletions docs/design/artboards/d215/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,12 @@
.authcard .facts dd { margin: 0; font: 500 12.5px/1.4 var(--f-mono); color: var(--ink); text-align: right; overflow-wrap: anywhere; }
.authcard h1 .host { white-space: nowrap; }
.authcard .keys2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.authcard .inv { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
.authcard .inv h1 { margin: 6px 0 0; text-wrap: balance; }
.authcard .inv .ey { font: 500 11.5px/1 var(--f-mono); color: var(--mu); }
.authcard .pt { flex: none; width: 44px; height: 44px; border-radius: 11px; display: grid; place-items: center; color: #fff; font: 700 15px/1 var(--f-body); box-shadow: 0 0 0 1px var(--av-ring); }
.authcard .ds { margin: 0; font-size: 14px; line-height: 21px; color: var(--ink2); }
.authcard .mt { margin: 0; font-size: 13px; line-height: 19px; color: var(--se); }
.authcard .sub { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--se); }
.authcard .hint { margin: 0; font-size: 13px; line-height: 1.5; color: var(--se); }
.authcard .hint a, .authcard .legal a { color: var(--c-text); font-weight: 600; }
Expand Down Expand Up @@ -1358,6 +1364,12 @@ <h1>Same look. <em>Less said.</em></h1>
<div class="rowa" style="grid-template-columns:repeat(3,minmax(0,1fr));margin-top:14px"><figure><figcaption><b>Enter code · 390</b><span></span></figcaption><div class="frame" style="--fw:390;--fh:844" data-surface="cli-authorize" data-vp="390" data-state="default"><div class="app m"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><h1>Authorize Commonly CLI</h1><p class="sub">Enter the code shown in your terminal.</p><div class="fl"><div class="lab">Device code</div><div class="field code"><span>WDJB-MJHT</span></div></div><button class="b b-p" type="button">Continue</button></div></div></div></div></figure><figure><figcaption><b>Approve · 390</b><span>a pending request</span></figcaption><div class="frame" style="--fw:390;--fh:844" data-surface="cli-authorize" data-vp="390" data-state="confirm"><div class="app m"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><h1>Authorize <span class="host">alex-mbp</span> as @alex?</h1><p class="sub">This device is asking to use your Commonly account.</p><dl class="facts"><div><dt>Client</dt><dd>commonly-cli 0.1.84</dd></div><div><dt>Instance</dt><dd>api.commonly.me</dd></div><div><dt>Requested</dt><dd>just now</dd></div></dl><div class="alrt" role="note"><svg class="i" viewBox="0 0 24 24"><use href="#i-info"/></svg><span>Only approve a code you requested from your own terminal.</span></div><div class="keys2"><button class="b b-s" type="button">Deny</button><button class="b b-p" type="button">Authorize</button></div></div></div></div></div></figure><figure><figcaption><b>Authorized · 390</b><span></span></figcaption><div class="frame" style="--fw:390;--fh:844" data-surface="cli-authorize" data-vp="390" data-state="done"><div class="app m"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><h1>Device authorized</h1><div class="alrt" role="status"><svg class="i" viewBox="0 0 24 24"><use href="#i-check"/></svg><span>You can return to your terminal.</span></div></div></div></div></div></figure></div>
</section>

<section class="surf" id="s-invite">
<div class="shead"><h2>Invite link</h2><span class="route">/v2/invite/:token</span></div>
<div class="row1"><div class="notes"><ul><li><b>The sign-in card, holding a pod.</b> Same brand row, 400px card and keys as sign-in, so a person who follows the link and then signs up never changes page family. Left-aligned like every auth card; the centred card and its 18px title are gone.</li><li><b>The pod is the title.</b> Its mark (the same seeded tile it has in the sidebar) sits beside a mono “You’re invited to” and the pod name in the 28px title.</li><li><b>One key per state.</b> Signed out: Sign up to join, with Log in under it. Signed in: Join the pod, or Go to pod for a member. A dead link: the alert row and one plain key.</li></ul><h3>Each state, one line</h3><table class="ctl"><tr><td>Signed out</td><td><b>Drawn.</b> Pod, member line, Sign up to join, Log in.</td></tr><tr><td>Signed in</td><td><b>Drawn.</b> Description when the pod has one, member count, Join.</td></tr><tr><td>Already a member</td><td><b>Not drawn.</b> The key reads Go to pod.</td></tr><tr><td>Joining</td><td><b>Not drawn.</b> The key reads Joining… and is disabled.</td></tr><tr><td>Join failed</td><td><b>Not drawn.</b> The alert row sits above the key; the key stays.</td></tr><tr><td>Unavailable</td><td><b>Drawn signed out:</b> the plain key reads What is Commonly?. Signed in, it reads Go to your pods.</td></tr><tr><td>Loading</td><td><b>Not drawn.</b> The card holds the brand row and one muted line.</td></tr></table></div><figure><figcaption><b>Signed out · 1440</b><span>from a shared link</span></figcaption><div class="frame" style="--fw:1440;--fh:900" data-surface="invite" data-vp="1440" data-state="default"><div class="app d"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><div class="inv"><span class="pt" style="background:linear-gradient(215deg, #0f766e 0%, #0b5c56 100%)">DR</span><div><div class="ey">You’re invited to</div><h1>Design review</h1></div></div><p class="mt">4 members · humans and agents working together on Commonly</p><button class="b b-p" type="button">Sign up to join</button><div class="alt">Already have an account? <a href="#">Log in</a></div></div></div></div></div></figure></div>
<div class="rowa" style="grid-template-columns:repeat(3,minmax(0,1fr));margin-top:14px"><figure><figcaption><b>Signed out · 390</b><span></span></figcaption><div class="frame" style="--fw:390;--fh:844" data-surface="invite" data-vp="390" data-state="default"><div class="app m"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><div class="inv"><span class="pt" style="background:linear-gradient(215deg, #0f766e 0%, #0b5c56 100%)">DR</span><div><div class="ey">You’re invited to</div><h1>Design review</h1></div></div><p class="mt">4 members · humans and agents working together on Commonly</p><button class="b b-p" type="button">Sign up to join</button><div class="alt">Already have an account? <a href="#">Log in</a></div></div></div></div></div></figure><figure><figcaption><b>Signed in · 390</b><span></span></figcaption><div class="frame" style="--fw:390;--fh:844" data-surface="invite" data-vp="390" data-state="join"><div class="app m"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><div class="inv"><span class="pt" style="background:linear-gradient(215deg, #0f766e 0%, #0b5c56 100%)">DR</span><div><div class="ey">You’re invited to</div><h1>Design review</h1></div></div><p class="ds">Crits on the launch screens before they ship. Agents post the first pass.</p><p class="mt">4 members</p><button class="b b-p" type="button">Join Design review</button></div></div></div></div></figure><figure><figcaption><b>Unavailable · 390</b><span>signed out; expired or revoked</span></figcaption><div class="frame" style="--fw:390;--fh:844" data-surface="invite" data-vp="390" data-state="unavailable"><div class="app m"><div class="authpage"><div class="authcard"><div class="brand"><span class="logo"><svg class="i" viewBox="0 0 24 24"><use href="#i-mark"/></svg></span>commonly</div><h1>Invite unavailable</h1><div class="alrt" role="alert"><svg class="i" viewBox="0 0 24 24"><use href="#i-alert"/></svg><span>This invite is no longer valid.</span></div><button class="b b-s" type="button">What is Commonly?</button></div></div></div></div></figure></div>
</section>

<section class="surf" id="s-dark">
<div class="shead"><h2>Dark mode</h2><span class="route">one dark value per token</span></div>
<div class="dm">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/i18n/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ describe('i18n configuration', () => {
lng: 'zh-CN',
provider: 'GitHub',
})).toBe('使用 GitHub 继续');
expect(i18n.t('inviteRedeem.invitedTo', {
expect(i18n.t('inviteRedeem.joinPod', {
lng: 'zh-CN',
podName: 'Commonly HQ',
})).toBe('你受邀加入 Commonly HQ');
})).toBe('加入 Commonly HQ');
});

it('keeps the verification copy glossary-clean in Simplified Chinese', () => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@
"whatIsCommonly": "What is Commonly?",
"fallbackPod": "a pod",
"untitledPod": "Untitled pod",
"invitedTo": "You've been invited to {{podName}}",
"invitedEyebrow": "You’re invited to",
"sharedWorkspaceDescription": "A private workspace shared with you",
"previewMeta_one": "{{formattedCount}} member · humans and agents working together on Commonly",
"previewMeta_other": "{{formattedCount}} members · humans and agents working together on Commonly",
Expand All @@ -491,7 +491,7 @@
"signUpToJoin": "Sign up to join",
"alreadyHaveAccount": "Already have an account? <login>Log in</login>",
"goToPods": "Go to your pods",
"goToPod": "Go to pod →",
"goToPod": "Go to pod",
"joining": "Joining…",
"joinPod": "Join {{podName}}",
"errors": {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,12 @@
"whatIsCommonly": "Commonly 是什么?",
"fallbackPod": "一个 Pod",
"untitledPod": "未命名 Pod",
"invitedTo": "你受邀加入 {{podName}}",
"invitedEyebrow": "邀请你加入",
"sharedWorkspaceDescription": "与你共享的私人工作空间",
"signUpToJoin": "注册并加入",
"alreadyHaveAccount": "已有账号?<login>登录</login>",
"goToPods": "前往你的 Pod",
"goToPod": "前往 Pod →",
"goToPod": "前往 Pod",
"joining": "正在加入…",
"joinPod": "加入 {{podName}}",
"errors": {
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/v2/__tests__/V2InviteRedeemLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @ts-nocheck
// The d215 invite frames: the auth card holding a pod, one key per state.
import React from 'react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import axios from 'axios';
import V2InviteRedeem from '../components/V2InviteRedeem';

const mockApi = { get: jest.fn(), post: jest.fn(), patch: jest.fn(), del: jest.fn() };
let mockAuthenticated = false;

jest.mock('axios', () => ({ get: jest.fn() }));
jest.mock('../../context/AuthContext', () => ({
useAuth: () => ({ isAuthenticated: mockAuthenticated, loading: false }),
}));
jest.mock('../hooks/useV2Api', () => ({ useV2Api: () => mockApi }));

const TOKEN = 'b'.repeat(32);
const renderPage = () => render(
<MemoryRouter initialEntries={[`/v2/invite/${TOKEN}`]}>
<Routes><Route path="/v2/invite/:token" element={<V2InviteRedeem />} /></Routes>
</MemoryRouter>,
);

describe('V2InviteRedeem layout', () => {
beforeEach(() => { jest.clearAllMocks(); mockAuthenticated = false; });

test('signed out: the pod is the title inside the auth card, with sign up and log in', async () => {
axios.get.mockResolvedValue({ data: { pod: { name: 'Design review', memberCount: 4 } } });
const { container } = renderPage();
const title = await screen.findByRole('heading', { level: 1, name: 'Design review' });
const card = container.querySelector('.v2-login__card.v2-login__card--stack');
expect(card).toContainElement(title);
expect(card.querySelector('.v2-login__brand')).not.toBeNull();
expect(within(card).getByText('You’re invited to')).toHaveClass('v2-invite__eyebrow');
expect(within(card).getByText(/4 members/)).toHaveClass('v2-invite__meta');
expect(within(card).getByRole('button', { name: 'Sign up to join' })).toHaveClass('v2-login__submit');
expect(within(card).getByRole('link', { name: 'Log in' })).toHaveAttribute('href', `/v2/login?next=${encodeURIComponent(`/v2/invite/${TOKEN}`)}`);
expect(container.querySelector('.v2-invite-card, .v2-invite-page')).toBeNull();
});

test('a dead link: the alert row and one plain key', async () => {
axios.get.mockRejectedValue(new Error('gone'));
renderPage();
expect(await screen.findByRole('heading', { level: 1, name: 'Invite unavailable' })).toBeInTheDocument();
expect(screen.getByRole('alert')).toHaveTextContent('This invite is no longer valid.');
const key = screen.getByRole('button', { name: 'What is Commonly?' });
expect(key).toHaveClass('v2-login__submit', 'v2-login__submit--ghost');
});

test('signed in: description, member count and Join; a member gets Go to pod', async () => {
mockAuthenticated = true;
mockApi.get.mockResolvedValue({
pod: { _id: 'p1', name: 'Design review', description: 'Crits on the launch screens.', memberCount: 4 },
alreadyMember: false,
});
const { unmount } = renderPage();
expect(await screen.findByRole('heading', { level: 1, name: 'Design review' })).toBeInTheDocument();
expect(screen.getByText('Crits on the launch screens.')).toHaveClass('v2-invite__description');
expect(screen.getByRole('button', { name: 'Join Design review' })).toHaveClass('v2-login__submit');
unmount();

mockApi.get.mockResolvedValue({ pod: { _id: 'p1', name: 'Design review', memberCount: 4 }, alreadyMember: true });
renderPage();
expect(await screen.findByRole('button', { name: 'Go to pod' })).toBeInTheDocument();
});
test('signed in: a dead link and a failed join both show the alert row inside the card', async () => {
mockAuthenticated = true;
mockApi.get.mockRejectedValue({ response: { data: { msg: 'Invite expired' } } });
const { container, unmount } = renderPage();
expect(await screen.findByRole('heading', { level: 1, name: 'Invite unavailable' })).toBeInTheDocument();
const card = container.querySelector('.v2-login__card');
expect(within(card).getByRole('alert')).toHaveTextContent('Invite expired');
expect(within(card).getByRole('button', { name: 'Go to your pods' })).toHaveClass('v2-login__submit--ghost');
unmount();

mockApi.get.mockResolvedValue({ pod: { _id: 'p1', name: 'Design review', memberCount: 4 }, alreadyMember: false });
mockApi.post.mockRejectedValue({ response: { data: { msg: 'Pod is full' } } });
const second = renderPage();
fireEvent.click(await screen.findByRole('button', { name: 'Join Design review' }));
const alert = await screen.findByRole('alert');
expect(second.container.querySelector('.v2-login__card')).toContainElement(alert);
expect(alert).toHaveTextContent('Pod is full');
expect(screen.getByRole('button', { name: 'Join Design review' })).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion frontend/src/v2/__tests__/V2Login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,8 @@ describe('V2 routing', () => {
try {
renderAt('/v2/community');

expect(await screen.findByText(/invited to Commonly HQ/)).toBeInTheDocument();
expect(await screen.findByRole('heading', { level: 1, name: 'Commonly HQ' })).toBeInTheDocument();
expect(screen.getByText('You’re invited to')).toBeInTheDocument();
expect(axios.get).toHaveBeenCalledWith('/api/pods/6a5fe677306155f677c26abf');
expect(axios.get).toHaveBeenCalledWith('/api/invites/7b91255f18ae3c0ae3721707a6613731/preview');
} finally {
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(phone).toMatch(/\.v2-profile \.v2-profile__follow, \.v2-profile \.v2-profile__edit \{[^}]*flex: 1 0 100%/);
});

test('/invite: a long pod name or description wraps inside the auth card (d215 s-invite)', () => {
const invite = read('../components/V2InviteRedeem.css');
expect(ruleBody(readV2Css(), '.v2-login__card--stack')).toContain('grid-template-columns: minmax(0, 1fr)');
expect(invite).toMatch(/\.v2-invite__heading \{[^}]*min-width: 0/);
expect(invite).toMatch(/\.v2-invite__heading \.v2-login__title \{[^}]*overflow-wrap: anywhere/);
expect(invite).toMatch(/\.v2-invite__description \{[^}]*overflow-wrap: anywhere/);
// The join key reads "Join <pod>": it grows instead of clipping a long name.
const key = ruleBody(readV2Css(), '.v2-root .v2-login__card--stack button.v2-login__submit');
expect(key).toContain('overflow-wrap: anywhere');
expect(key).toContain('height: auto');
// A min-height here outranks the phone rule's 44px on every auth page.
expect(key).not.toMatch(/^\s*min-height:/m);
});

test('/cli/authorize scrolls inside the .v2-root shell on a short viewport (TASK-682)', () => {
// .v2-root is height 100vh + overflow hidden; the page must sit in a
// scroller or the lower controls (Deny / Authorize) are unreachable on a
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/v2/__tests__/v2-type-floors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import { readV2Css } from '../__testkit__/v2Css';
*/
// v2.css plus the surface files lifted out of it (D223): the chat sheets V2App
// loads, and the per-surface sheets their components import (R04 BYO, S4 board,
// R05 Connect, R06 Your team).
const CSS = [readV2Css(), ...['../components/V2AgentBYO.css', '../components/V2PodBoard.css', '../components/V2ConnectPage.css', '../components/V2YourTeamPage.css']
// R05 Connect, R06 Your team, the invite page).
const CSS = [readV2Css(), ...['../components/V2AgentBYO.css', '../components/V2PodBoard.css', '../components/V2ConnectPage.css', '../components/V2YourTeamPage.css', '../components/V2InviteRedeem.css']
.map((rel) => fs.readFileSync(path.join(__dirname, rel), 'utf8'))]
.join('\n');

Expand Down Expand Up @@ -141,7 +141,8 @@ const ALLOWLIST: string[] = [
// with the Connect lift, so its entry left this list.
'.v2-approval__badge', // 10.5px — mono-below-floor
'.v2-approval__time', // 11.5px — no-mono-family
'.v2-invite-card__meta', // 11.0px — no-mono-family
// The invite link lift dropped `.v2-invite-card__meta` (11.0px, no family)
// with the old centred card, so its entry left this list.
'.v2-invite-link', // 11.0px — no-mono-family
'.v2-invite-manage__empty', // 11.0px — no-mono-family
'.v2-invite-manage__meta', // 10.0px — no-mono-family
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/v2/components/V2InviteRedeem.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* Invite link, the d215 invite frames: the auth card holding a pod. The pod's
seeded mark sits beside a mono eyebrow and the pod name as the 28px title. */
.v2-invite__pod {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.v2-root .v2-avatar.v2-invite__mark {
flex: none;
width: 44px;
height: 44px;
border-radius: 11px;
font-size: 15px;
}
.v2-invite__heading {
min-width: 0;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
.v2-invite__eyebrow {
display: block;
font: 500 11.5px/1 var(--v2-font-mono);
color: var(--v2-text-muted);
}
/* Outranks .v2-login__card--stack .v2-login__title (4px) on specificity, not
on the order the lazy chunk's stylesheet lands. */
.v2-login__card--stack .v2-invite__heading .v2-login__title {
margin: 6px 0 0;
overflow-wrap: anywhere;
}
.v2-invite__description {
margin: 0;
overflow-wrap: anywhere;
font-size: 14px;
line-height: 21px;
color: var(--v2-text-secondary);
}
.v2-invite__meta {
margin: 0;
font-size: 13px;
line-height: 19px;
color: var(--v2-text-secondary);
}
Loading
Loading