wayBetter for X

This commit is contained in:
CRTkafa 2026-10-01 06:01:13 +07:00
commit 434f193217
720 changed files with 147465 additions and 0 deletions

View file

@ -0,0 +1,283 @@
// X's chat (XChat, /i/chat) for the theme matrix: the token layer, the utilities and the markup
// shapes read live from x.com (Lights out, logged in, 2026-09-30; .omc/research/chat-survey.md has
// the test ids). Names, messages and links are placeholders.
//
// What is exact: the primitives and semantic tokens of both X modes, the utility declarations
// (including Tailwind 4's opacity form with its nested `@supports`, the shape that turned the
// pinned-message pill solid), the container query that splits inbox and conversation at 918px, the
// floating header / pinned pill / composer over a full-height scroller, the row markup (received,
// sent, reply quote, reaction chip, date separator) and the inbox rows.
// What is approximated (not seen in the two conversations in scope): the link card's inner layout,
// the typing indicator, the unread dot, the send button (modelled as X's primary button) and the
// menu's own surface class (its computed colors are live: X's sheet color with the normal border).
// Light-mode values of `--x-btn-tertiary-opaque*` mirror the dark ones (unverified on x.com Default).
const ALPHAS = ['00', '02', '04', '08', '10', '15', '20', '30', '40', '50', '60', '70', '80', '85', '90', '95'];
const STEPS = ['00', '05', '10', '15', '20', '30', '40', '50', '60', '70', '80', '85', '90', '95', '100'];
const alphaVars = (name, rgb, list) => list.map(step => `--x-${name}-${step}: rgba(${rgb}, ${+(Number(step) / 100).toFixed(2)});`).join(' ');
/** X's chat stylesheet: tokens for both modes and the utilities the fixtures use. */
export const CHAT_CSS = `
:root { --x-neutral-000: #f2f2f2; --x-neutral-050: #ebebeb; --x-neutral-100: #d9d9d9; --x-neutral-200: #cccccc; --x-neutral-300: #b2b2b2; --x-neutral-700: #4d4d4d;
--x-neutral-800: #333333; --x-neutral-900: #262626; --x-neutral-1000: #141414; --x-blue-600: #43b3f6; --x-blue-700: #6bc9fb; --x-red-500: #f4212e; --x-red-050: #440004;
${alphaVars('white', '255, 255, 255', ALPHAS)} ${alphaVars('black', '0, 0, 0', ALPHAS)}
--container-xl: 36rem; --container-5xl: 64rem; --blur-md: 12px; }
:root, :root[data-theme="light"], [data-theme="light"], .light-theme {
--x-bg-primary: var(--x-white); --x-bg-secondary: var(--x-neutral-000); --x-bg-tertiary: var(--x-neutral-050); --x-bg-quaternary: var(--x-black-04);
--x-bg-sheets: var(--x-white); --x-bg-inputs: var(--x-black-04); --x-bg-modal: var(--x-white);
--x-fg-primary: var(--x-black); --x-fg-secondary: var(--x-black-60); --x-fg-tertiary: var(--x-black-30); --x-fg-inverted: var(--x-white); --x-fg-on-color: var(--x-white);
--x-fg-brand: var(--x-blue-500); --x-fg-destructive: var(--x-red-500);
--x-border-normal: var(--x-black-15); --x-border-hover: var(--x-black-20); --x-border-active: var(--x-black-30);
--x-btn-primary: var(--x-black); --x-btn-primary-hover: var(--x-black-80); --x-btn-secondary: var(--x-neutral-050); --x-btn-secondary-hover: var(--x-neutral-100);
--x-btn-tertiary: var(--x-black-04); --x-btn-tertiary-hover: var(--x-black-08); --x-btn-tertiary-pressed: var(--x-black-15);
--x-btn-tertiary-opaque: color-mix(in srgb, var(--x-black) 4%, var(--x-bg-primary)); --x-btn-tertiary-opaque-hover: color-mix(in srgb, var(--x-black) 8%, var(--x-bg-primary));
--x-btn-ghost-hover: var(--x-black-04); --x-btn-ghost-pressed: var(--x-black-15); --x-btn-brand: var(--x-blue-500); --x-hover-subtle: var(--x-black-04);
--x-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); --x-shadow-popup: 0 4px 20px rgba(0, 0, 0, 0.08);
${alphaVars('fg-alpha', '0, 0, 0', STEPS)} ${alphaVars('bg-alpha', '255, 255, 255', STEPS)} }
:root[data-theme="dark"], [data-theme="dark"], .dark-theme {
--x-bg-primary: var(--x-black); --x-bg-secondary: var(--x-neutral-1000); --x-bg-tertiary: var(--x-neutral-900); --x-bg-quaternary: var(--x-white-04);
--x-bg-sheets: var(--x-neutral-1000); --x-bg-inputs: var(--x-white-08); --x-bg-modal: var(--x-neutral-1000);
--x-fg-primary: var(--x-white); --x-fg-secondary: var(--x-white-60); --x-fg-tertiary: var(--x-white-30); --x-fg-inverted: var(--x-black); --x-fg-on-color: var(--x-white);
--x-fg-brand: var(--x-blue-500); --x-fg-destructive: var(--x-red-500);
--x-border-normal: var(--x-white-15); --x-border-hover: var(--x-white-20); --x-border-active: var(--x-white-30);
--x-btn-primary: var(--x-white); --x-btn-primary-hover: var(--x-white-80); --x-btn-secondary: var(--x-neutral-900); --x-btn-secondary-hover: var(--x-neutral-800);
--x-btn-tertiary: var(--x-white-10); --x-btn-tertiary-hover: var(--x-white-15); --x-btn-tertiary-pressed: var(--x-white-20);
--x-btn-tertiary-opaque: color-mix(in srgb, var(--x-white) 10%, var(--x-bg-primary)); --x-btn-tertiary-opaque-hover: color-mix(in srgb, var(--x-white) 15%, var(--x-bg-primary));
--x-btn-ghost-hover: var(--x-white-08); --x-btn-ghost-pressed: var(--x-white-15); --x-btn-brand: var(--x-blue-500); --x-hover-subtle: var(--x-white-08);
--x-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2); --x-shadow-popup: 0 4px 20px rgba(0, 0, 0, 0.4);
${alphaVars('fg-alpha', '255, 255, 255', STEPS)} ${alphaVars('bg-alpha', '0, 0, 0', STEPS)} }
@layer base {
:root, :root[data-theme="light"], [data-theme="light"] { --color-background: 0 0% 100%; --color-blue-500: 204 88% 53%; --color-brand: var(--color-blue-500); --chat-accent: var(--color-blue-500);
--chat-accent-foreground: 0 0% 98%; --card: var(--color-gray-0); --popover: 0 0% 100%; --secondary: 197 16% 91%; --muted: 197 16% 91%; --muted-foreground: 206 15% 38%; --input: 197 16% 91%; --ring: var(--color-blue-500); }
:root[data-theme="dark"], [data-theme="dark"] { --color-background: 0 0% 0%; --color-blue-500: 204 88% 53%; --color-brand: var(--color-blue-500); --chat-accent: var(--color-blue-500);
--chat-accent-foreground: 240 5.9% 10%; --card: var(--color-gray-0); --popover: 0 0% 0%; --secondary: 210 7% 18%; --muted: 210 7% 18%; --muted-foreground: 240 5% 64.9%; --input: 210 7% 18%; --ring: var(--color-blue-500); }
[data-testid="dm-container"] *, [data-slot="xds-menu-content"], [data-slot="xds-menu-content"] * { box-sizing: border-box; border: 0 solid hsl(var(--border)); }
[data-testid="dm-container"] button, [data-testid="dm-container"] input, [data-testid="dm-container"] textarea { font: inherit; color: inherit; background-color: transparent; margin: 0; padding: 0; }
[data-testid="dm-container"] a { color: inherit; text-decoration: inherit; }
}
.bg-bg-alpha-70 { background-color: var(--x-bg-alpha-70); }
.border-normal { border-color: var(--x-border-normal); }
.shadow-popup { box-shadow: var(--x-shadow-popup); } .shadow-lg { box-shadow: var(--x-shadow-lg); }
.bg-btn-tertiary { background-color: var(--x-btn-tertiary); } .bg-btn-secondary { background-color: var(--x-btn-secondary); }
.bg-btn-primary { background-color: var(--x-btn-primary); } .bg-btn-brand { background-color: var(--x-btn-brand); }
.text-primary { color: var(--x-fg-primary); } .text-secondary { color: var(--x-fg-secondary); } .text-tertiary { color: var(--x-fg-tertiary); }
.text-inverted { color: var(--x-fg-inverted); } .text-on-color { color: var(--x-fg-on-color); } .text-brand { color: var(--x-fg-brand); } .text-destructive { color: var(--x-fg-destructive); }
.bg-inputs { background-color: var(--x-bg-inputs); } .bg-sheets { background-color: var(--x-bg-sheets); }
.bg-gray-0 { background-color: hsl(var(--color-gray-0)); } .bg-gray-50 { background-color: hsl(var(--color-gray-50)); }
.bg-gray-100 { background-color: hsl(var(--color-gray-100)); } .bg-gray-300 { background-color: hsl(var(--color-gray-300)); }
.bg-brand { background-color: hsl(var(--color-brand)); }
.text-text { color: hsl(var(--color-text)); } .text-gray-700 { color: hsl(var(--color-gray-700)); } .text-chat-accent { color: hsl(var(--chat-accent)); }
.text-\\[color\\:inherit\\] { color: inherit; }
.border-background { border-color: hsl(var(--background)); } .border-gray-200 { border-color: hsl(var(--color-gray-200)); } .border-transparent { border-color: transparent; }
.border { border-width: 1px; } .border-e { border-inline-end-width: 1px; } .border-\\[0\\.5px\\] { border-width: 0.5px; }
.bg-fg-alpha-05\\! { background-color: var(--x-fg-alpha-05) !important; }
.dark\\:bg-fg-alpha-10\\!:where([data-theme="dark"], [data-theme="dark"] *) { background-color: var(--x-fg-alpha-10) !important; }
.dark\\:bg-white\\/15:where([data-theme="dark"], [data-theme="dark"] *) { background-color: var(--x-white); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--x-white) 15%, transparent); } }
.border-chat-accent\\/50 { border-color: hsl(var(--chat-accent)); @supports (color: color-mix(in lab, red, red)) { border-color: color-mix(in oklab, hsl(var(--chat-accent)) 50%, transparent); } }
@media (hover: hover) {
.hover\\:bg-btn-ghost-hover:hover { background-color: var(--x-btn-ghost-hover); }
.hover\\:bg-btn-tertiary-hover:hover { background-color: var(--x-btn-tertiary-hover); }
.hover\\:bg-fg-alpha-05\\/50:hover { background-color: var(--x-fg-alpha-05); }
.data-highlighted\\:hover\\:bg-\\[var\\(--x-hover-subtle\\)\\][data-highlighted]:hover { background-color: var(--x-hover-subtle); }
}
@supports (color: color-mix(in lab, red, red)) { @media (hover: hover) { .hover\\:bg-fg-alpha-05\\/50:hover { background-color: color-mix(in oklab, var(--x-fg-alpha-05) 50%, transparent); } } }
.placeholder\\:text-tertiary::placeholder { color: var(--x-fg-tertiary); }
.scrollbar-thin-custom { scrollbar-width: thin; scrollbar-color: rgba(156, 163, 175, 0.5) transparent; }
.backdrop-blur-md { backdrop-filter: blur(var(--blur-md)); }
.rounded-chat { border-radius: 16px; } .rounded-full { border-radius: 9999px; }
.max-w-5xl { max-width: var(--container-5xl); } .min-w-\\[400px\\] { min-width: 400px; } .w-full { width: 100%; }
.\\@container { container-type: inline-size; }
@container (width >= 918px) { .\\@wide\\:block { display: block; } .\\@wide\\:w-\\[35\\%\\] { width: 35%; } .\\@wide\\:max-w-xl { max-width: var(--container-xl); } .\\@wide\\:shrink-0 { flex-shrink: 0; } }
@container (width < 918px) { .\\@max-wide\\:w-full { width: 100%; } }
/* X's \`[--x-btn-tertiary:var(--x-btn-tertiary-opaque)]\` arbitrary-property classes on the floating header and the composer. */
.fx-opaque-btn { --x-btn-tertiary: var(--x-btn-tertiary-opaque); --x-btn-tertiary-hover: var(--x-btn-tertiary-opaque-hover); }
/* Fixture-only geometry (Tailwind's layout utilities, written out). */
.fx-f { font-family: TwitterChirp, -apple-system, "Segoe UI", sans-serif; }
.fx-body { font-size: 15px; line-height: 20px; } .fx-sub2 { font-size: 13px; line-height: 16px; } .fx-sub3 { font-size: 11px; line-height: 12px; }
.fx-h2 { font-size: 17px; line-height: 20px; font-weight: 500; } .fx-h1 { font-size: 20px; line-height: 24px; font-weight: 700; }
.fx-clamp1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.fx-btn { display: inline-flex; align-items: center; justify-content: center; border-radius: 9999px; cursor: pointer; flex: 0 0 auto; }
.fx-glyph { width: 20px; height: 20px; border: 2px solid currentColor; border-radius: 5px; }
`;
const avatar = (size, extra = '') => `<div class="bg-gray-300" style="width: ${size}px; height: ${size}px; min-width: ${size}px; border-radius: 9999px; overflow: hidden; ${extra}"><div style="width: 100%; height: 100%; background: linear-gradient(135deg, #8aa4b8, #4f6b80);"></div></div>`;
const iconButton = (testid, size = 48, classes = 'bg-btn-tertiary text-primary hover:bg-btn-tertiary-hover') => `<button type="button" data-slot="xds-button"${testid ? ` data-testid="${testid}"` : ''} class="fx-btn ${classes}" style="width: ${size}px; height: ${size}px;"><span class="fx-glyph"></span></button>`;
const stamp = (time, mine) => `<div style="position: absolute; bottom: 0; inset-inline-end: 0;"><div class="${mine ? 'text-white' : 'text-gray700'}" style="display: flex; align-items: center; gap: 4px;"><span class="fx-f fx-sub3 text-[color:inherit]">${time}</span></div></div>`;
/** One message row; `mine` is a sent message (accent bubble, right), otherwise received (avatar, left). `min` is the contrast the fixture asks for on the message text. */
function row({ mine = false, text, time = '20:14', quote = null, reaction = null, link = null, mention = null, avatarShown = true }) {
const bubble = mine ? 'bg-chat-accent' : 'bg-gray-50';
const quoteBlock = quote ? `<div style="margin-top: 2px; display: flex; flex-direction: column; gap: 4px; align-items: ${mine ? 'flex-end' : 'flex-start'}; cursor: pointer;">
<div style="display: flex; align-items: center; gap: 4px; padding: 0 8px;"><svg data-icon="icon-reply-alt" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M10 9V5l-7 7 7 7v-4c5 0 8 1.5 11 5-1-6-4-10-11-11z"/></svg><div class="fx-f fx-sub2 text-gray-700 fx-clamp1">${quote.from}</div></div>
<div class="rounded-chat ${mine ? 'border border-chat-accent/50 text-chat-accent' : 'bg-gray-50 text-gray-700'}" style="padding: 6px 12px;"><span class="fx-f fx-sub2 text-[color:inherit]">${quote.text}</span></div></div>` : '';
const card = link ? `<div class="rounded-chat border border-gray-200" style="overflow: hidden;"><a href="#" class="bg-gray-50 hover:bg-gray-50/50" style="display: flex; align-items: center; gap: 12px; padding: 12px;"><div class="bg-gray-100" style="width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center;"><svg data-icon="icon-link" aria-hidden="true" width="18" height="18" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="2"/></svg></div><div class="fx-f fx-body text-text" data-fx-min="7" style="font-weight: 700;">${link}</div></a></div>` : '';
const body = `${mention ? `<a href="#" class="text-chat-accent" data-fx-min="4.5">${mention}</a> ` : ''}${text}`;
const chip = reaction ? `<div style="grid-area: message-info; display: flex; padding-bottom: 4px;"><div style="margin: -10px 12px 0;"><button type="button" data-slot="hover-card-trigger" class="rounded-full border border-background bg-gray-50" style="display: inline-flex; align-items: center; gap: 4px; min-width: 40px; height: 22px; justify-content: center; padding: 0 8px; cursor: pointer;"><span style="font-size: 13px;">${reaction[0]}</span><span class="fx-f fx-sub3 text-primary">${reaction[1]}</span></button></div></div>` : '';
return `<div data-dm-message-row="fx" data-testid="message-fx" data-send-status="sent" style="position: relative; display: flex; width: 100%; padding: 4px 16px; justify-content: ${mine ? 'flex-end' : 'flex-start'};">
<div style="position: relative; z-index: 10; display: grid; column-gap: 8px; row-gap: 2px; max-width: calc(100% - 4rem); grid-template-columns: auto auto; align-items: center; grid-template-areas: ${mine ? '&quot;content avatar&quot; &quot;message-info .&quot;' : '&quot;avatar content&quot; &quot;. message-info&quot;'};">
<div style="grid-area: avatar; align-self: end; margin-bottom: 2px;">${!mine && avatarShown ? `<a href="#">${avatar(32)}</a>` : ''}</div>
<div style="grid-area: content; display: flex; min-width: 0; flex-direction: column; gap: 4px; align-items: ${mine ? 'flex-end' : 'flex-start'};">
${quoteBlock}${card}
<div role="article" tabindex="-1" class="rounded-chat" style="position: relative;"><div data-testid="message-text-fx" class="rounded-chat ${bubble}" style="position: relative; display: flex; justify-content: flex-end; padding: 8px 16px;">
<div class="${mine ? 'text-white' : 'text-text'}" style="position: relative; display: inline-flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; overflow: hidden;"><span class="fx-f fx-body text-[color:inherit]"><span dir="auto" data-fx-min="${mine ? 4.5 : 7}">${body}</span><span aria-hidden="true" style="display: inline-block; padding-left: 8px; opacity: 0;"><span class="fx-f fx-sub3">${time}</span></span></span>${stamp(time, mine)}</div>
</div></div>
</div>
${chip}
</div></div>`;
}
const separator = text => `<div><div style="margin: 16px 0 8px; display: flex; align-items: center; justify-content: center;"><div class="fx-f fx-sub2 text-secondary" style="font-weight: 500; text-align: center;">${text}</div></div></div>`;
function inbox() {
const rows = [
{ name: 'AI Game Lobby', preview: 'Rowan: the build is almost ready', time: '2m', selected: true },
{ name: 'Avery Stone', preview: 'see you there', time: '1h', unread: true },
{ name: 'Kai Morgan', preview: 'You: sounds good', time: '3h' },
{ name: 'Remy Hart', preview: 'sent a photo', time: 'Sun', muted: true },
{ name: 'Sana Iqbal', preview: 'You: split panes at 1275px are the real stress test', time: 'Sat' },
];
return `<div data-testid="dm-inbox-panel" class="@wide:block @max-wide:w-full @wide:w-[35%] @wide:max-w-xl @wide:shrink-0 min-w-[400px]"><div class="border-e" style="display: flex; flex-direction: column; height: 100%; position: relative; width: 100%;">
<header data-testid="dm-inbox-header" style="display: flex; flex-direction: column;"><div data-testid="dm-inbox-header-main" style="display: flex; height: 64px; align-items: center; gap: 8px; padding: 8px 16px;">
<div data-testid="dm-inbox-title" class="fx-f fx-h1 text-[color:inherit]" data-fx-min="7">Chat</div>
<div style="margin-inline-start: auto; display: flex; align-items: center; gap: 6px;">
<button type="button" data-testid="dm-inbox-dropdown-trigger" data-slot="xds-menu-trigger" class="fx-btn border border-normal text-primary hover:bg-btn-ghost-hover" style="height: 36px; padding: 0 12px;"><span class="fx-f fx-body">All</span></button>
${iconButton('dm-new-chat-button', 36)}
</div></div>
<div data-testid="dm-search-bar" style="padding: 4px 16px;"><div style="position: relative;"><span class="text-tertiary" style="position: absolute; inset-inline-start: 12px; top: 50%; transform: translateY(-50%);"><svg data-icon="icon-search-stroke" aria-hidden="true" width="18" height="18" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
<input data-testid="dm-search-input" data-slot="xds-input" placeholder="Search" class="fx-f fx-body rounded-full w-full text-primary border border-transparent bg-inputs placeholder:text-tertiary" style="height: 40px; padding: 0 40px; outline: 0;"></div></div>
</header><div style="padding: 8px 0;"></div>
<div role="listbox" style="display: flex; min-height: 0; flex: 1; flex-direction: column;"><div data-testid="dm-conversation-scroller" class="scrollbar-thin-custom border-transparent" style="height: 100%; overflow: hidden auto; width: 100%; border-top-width: 1px;"><ul style="margin: 0; padding: 0; list-style: none;">
${rows.map((r, i) => `<li><div data-testid="dm-conversation-item-fx${i}"><a href="#" role="option" aria-selected="${!!r.selected}" style="display: block;"><div class="hover:bg-fg-alpha-05/50${r.selected ? ' bg-fg-alpha-05! dark:bg-fg-alpha-10!' : ''}" style="margin: 0 16px; display: flex; gap: 16px; border-radius: 12px; padding: 0 16px 0 8px; cursor: pointer;">
<div style="padding: 12px 0;">${avatar(48)}</div>
<div style="display: flex; min-width: 0; flex: 1; flex-direction: column; justify-content: center; gap: 4px;">
<div class="fx-f fx-body" style="display: flex; align-items: center; justify-content: space-between; gap: 6px; font-weight: 500;"><div style="min-width: 0; flex: 1;"><div class="text-text fx-clamp1" data-fx-min="7">${r.name}</div></div><div class="${r.unread ? 'text-primary' : 'text-secondary'}" style="flex-shrink: 0;"><div class="fx-sub2 text-[color:inherit]">${r.time}</div></div></div>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 16px;"><div style="min-width: 0; flex: 1; overflow: hidden;"><div class="${r.unread ? 'text-primary' : 'text-secondary'}" style="font-weight: ${r.unread ? 500 : 400};"><div class="fx-f fx-body text-[color:inherit] fx-clamp1">${r.preview}</div></div></div>
${r.unread ? '<div class="bg-brand" style="width: 10px; height: 10px; border-radius: 9999px; flex-shrink: 0;"></div>' : r.muted ? '<div class="text-secondary"><svg data-icon="icon-bell-off" width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="2"/></svg></div>' : ''}</div>
</div></div></a></div></li>`).join('')}
</ul></div></div></div></div>`;
}
function conversation(kind) {
const group = kind === 'group';
const title = group ? 'AI Game Lobby' : 'Avery Stone';
const header = `<div data-testid="dm-conversation-header" style="position: relative; width: 100%; padding: 16px; gap: 16px; display: flex; align-items: center; justify-content: space-between;">
<div style="position: absolute; inset: 0; z-index: -10;"><div style="pointer-events: none; position: absolute; inset: 0; background: linear-gradient(360deg, transparent 0%, hsl(var(--color-background)) 100%);"></div></div>
<div style="display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px;">
<span class="fx-back">${iconButton('dm-conversation-back-button')}</span>
<a href="#" style="display: flex; min-width: 0; align-items: center; gap: 8px;">${avatar(48)}<div style="display: flex; flex-direction: column;"><div data-testid="dm-conversation-username"><div class="fx-f fx-h2 text-text fx-clamp1" data-fx-min="7">${title}</div></div>
${group ? '' : '<div class="text-gray-700" style="display: flex; align-items: center; gap: 4px;"><svg data-icon="icon-unlock-stroke" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" fill="none" stroke="currentColor" stroke-width="2"/></svg><span class="fx-f fx-sub2 text-[color:inherit]">Unencrypted</span></div>'}</div></a>
</div><div style="display: flex; align-items: center; gap: 8px;"></div></div>`;
const pinned = group ? `<div class="max-w-5xl w-full" style="margin: 0 auto;"><div data-testid="pinned-message-banner" style="pointer-events: none; width: 100%; padding: 4px 16px 8px;">
<div class="border-normal bg-bg-alpha-70 shadow-popup rounded-full border backdrop-blur-md dark:bg-white/15" style="pointer-events: auto; display: flex; align-items: center; padding: 4px;">
<button type="button" class="hover:bg-btn-ghost-hover rounded-full" style="display: flex; min-width: 0; flex: 1; cursor: pointer; align-items: center; gap: 8px; padding: 6px 8px; text-align: start;"><div style="min-width: 0; flex: 1;">
<span class="fx-f fx-body text-primary fx-clamp1" data-fx-min="7" style="font-weight: 500;">Pinned message</span>
<span class="fx-f fx-body text-secondary fx-clamp1"><span>Welcome in. Closing the room for a couple of hours while the new build goes out, back soon.</span></span></div></button>
${iconButton('', 48, 'text-primary hover:bg-btn-ghost-hover')}
</div></div></div>` : '';
const rows = group ? [
separator('Today 20:14'),
row({ text: 'first message of the day, scrolled to the top', time: '20:14', mention: '@rowan' }),
row({ text: 'great job, the lip sync demo holds up', time: '20:15', reaction: ['👍', '3'] }),
row({ text: 'Impressive for sure. How does it handle a voice line?', time: '20:17', link: 'example.github.io', quote: { from: 'Rowan Vale', text: 'I have not tried proper voice syncing yet' } }),
row({ mine: true, text: 'I can share the rig later tonight', time: '20:18' }),
row({ text: 'Thank you, I will have to try it', time: '20:25' }),
`<div data-fx-typing style="display: flex; align-items: center; gap: 8px; padding: 4px 24px;">${avatar(16)}<span class="fx-f fx-sub2 text-secondary">Typing …</span></div>`,
] : [
`<div style="display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px 0;"><div class="fx-f fx-body text-secondary">Joined August 2026</div><div class="fx-f fx-body text-secondary">22 Followers</div>
<button type="button" data-slot="xds-button" class="fx-btn bg-btn-secondary text-primary" style="height: 36px; padding: 0 16px;"><span class="fx-f fx-body" data-fx-min="7" style="font-weight: 500;">View profile</span></button></div>`,
separator('Sunday 23:26'),
row({ mine: true, text: 'chat test under a theme', time: '23:26' }),
row({ text: 'got it, reads fine here', time: '23:27', avatarShown: false, reaction: ['🔥', '1'] }),
separator('Today 18:11'),
row({ mine: true, text: 'reply with a quote', time: '18:15', quote: { from: 'You', text: 'chat test under a theme' } }),
row({ mine: true, text: 'see https://example.com for the notes', time: '18:16' }).replace('https://example.com', '<a href="#" data-fx-min="4.5">https://example.com</a>'),
];
const typed = group ? '' : 'a message being written';
const composer = `<div data-testid="dm-composer-container" class="max-w-5xl w-full fx-opaque-btn" style="position: absolute; bottom: 0; left: 50%; isolation: isolate; z-index: 10; transform: translateX(-50%); padding: 16px;">
<div style="display: flex; align-items: flex-end; gap: 8px;">${iconButton('dm-composer-attachment-button')}
<form data-testid="dm-composer-form" style="position: relative; min-width: 0; flex: 1; margin: 0;"><div data-testid="dm-composer-input-container" class="fx-f fx-body bg-btn-tertiary" style="display: flex; align-items: flex-end; overflow: hidden; border-radius: 24px; padding: 8px; line-height: 24px;">
<textarea data-testid="dm-composer-textarea" rows="1" placeholder="${group ? 'Message' : 'Unencrypted message'}" class="text-primary placeholder:text-tertiary" style="flex: 1; min-width: 0; border: 0; outline: 0; resize: none; height: 32px; padding: 4px 8px;">${typed}</textarea>
${typed ? `<button type="button" data-testid="dm-composer-send-button" class="fx-btn bg-btn-primary text-inverted" style="width: 32px; height: 32px;"><span class="fx-f fx-sub2" style="font-weight: 700;">↑</span></button>` : iconButton('dm-composer-voice-button', 32, 'bg-gray-0 text-primary')}
</div></form></div></div>`;
return `<div data-testid="dm-conversation-panel" class="@wide:block w-full" style="min-width: 0;"><div style="position: relative; display: flex; height: 100%; flex: 1;"><div data-testid="dm-conversation-content" style="position: relative; display: flex; height: 100%; flex-grow: 1; flex-direction: column; min-width: 0;">
<div class="fx-overlay fx-opaque-btn" style="position: absolute; top: 0; right: 0; left: 0; z-index: 10; display: flex; flex-direction: column;">${header}${pinned}</div>
<div data-testid="dm-message-list-container" style="isolation: isolate; flex: 1; overflow: hidden;"><div data-testid="dm-message-list" style="position: relative; height: 100%;">
<div data-testid="dm-message-scroller" role="log" data-virtualizer="tanstack" class="scrollbar-thin-custom" style="height: 100%; width: 100%; overflow-y: auto; overflow-anchor: none; scroll-padding-bottom: 92px;">
<div class="fx-top-spacer"></div><div style="position: relative; width: 100%;">${rows.join('')}</div><div data-testid="dm-message-list-composer-spacer" style="height: 92px;"></div>
</div></div></div>
<div style="pointer-events: none; position: absolute; left: 0; right: 0; bottom: 0; height: 92px; background-image: linear-gradient(to bottom, transparent, var(--x-bg-primary) 20%, var(--x-bg-primary));"></div>
${composer}
</div></div></div>`;
}
/** XChat's message menu (Base UI, portaled into `<body>`): Reply, Forward, Copy, Info, Delete. */
function menu() {
// The second item is the one under the pointer (X paints it with `--x-hover-subtle`).
const item = 'display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 12px; cursor: pointer;';
return `<div style="position: fixed; right: 96px; bottom: 150px; z-index: 50;"><div role="menu" data-slot="xds-menu-content" class="bg-sheets border-normal border-[0.5px] shadow-lg" style="position: relative; padding: 4px; border-radius: 16px; min-width: 220px;">
${['Reply', 'Forward', 'Copy message text', 'Info'].map((t, i) => `<div role="menuitem" data-slot="xds-menu-item" class="text-primary data-highlighted:hover:bg-[var(--x-hover-subtle)]"${i === 1 ? ' data-highlighted' : ''} style="${item}${i === 1 ? ' background-color: var(--x-hover-subtle);' : ''}"><span class="fx-glyph"></span><div class="fx-f fx-body" data-fx-min="7" style="font-weight: 500;">${t}</div></div>`).join('')}
<div role="menuitem" data-slot="xds-menu-item" class="text-destructive" style="${item}"><span class="fx-glyph"></span><div class="fx-f fx-body" style="font-weight: 500;">Delete for me</div></div>
</div></div>`;
}
/**
* The chat route: X's chat row (fixed width, capped at the pane) with the two-panel app inside
* `primaryColumn`. `m` is the matrix's X mode (inline colors X writes on its own elements).
*/
export function chatRoute(m, kind, withMenu = false, embed = false) {
const app = `<div data-testid="dm-container" class="@container" style="display: flex; width: 100%; height: 100vh;">${inbox()}${conversation(kind)}</div>`;
// The embed (live since 2026-10-01): the chat app inside the open shadow root of X's XChatEmbed host,
// styled only by the root's own adopted sheets (EMBED_SCRIPT attaches both).
const body = embed
? `<div data-testid="xchatEmbedRoute" style="display: block; width: 100%; height: 100vh;"></div><template id="fx-embed"><div data-theme="${m.dataTheme}" class="bg-background text-foreground" style="height: 100%;">${app}${withMenu ? menu() : ''}</div></template>`
: app;
return `<main role="main" class="css-175oi2r r-16y2uox r-1wbh5a2 r-1habvwh"><div class="css-175oi2r r-16y2uox r-1wbh5a2 r-chatrow r-maxw100"><div class="css-175oi2r r-16y2uox"><div class="css-175oi2r r-18u37iz r-16y2uox r-13qz1uu">
<div data-testid="primaryColumn" class="css-175oi2r r-kemksi r-1kqtdi0 r-16y2uox r-13qz1uu" style="color: ${m.text};"><div class="css-175oi2r"><div class="css-175oi2r r-13qz1uu">
${body}
</div></div></div></div></div></div>${withMenu && !embed ? menu() : ''}</main>`;
}
/**
* Runs first in the fixture: the embed host gets its open shadow root with the chat inside and X's
* chat sheet adopted by the root, its tokens on `:host` as x.com declares them there.
*/
// The chat's primitives and label utilities live in the embed's own sheet on x.com, not in the document.
const EMBED_PRIMITIVES = ':root { --x-white: #ffffff; --x-black: #000000; --x-blue-500: #1d9bf0; }';
const EMBED_UTILITIES = '.bg-chat-accent { background-color: hsl(var(--chat-accent)); }\n.text-white { color: var(--x-white); }\n.text-white-60 { color: var(--x-white-60); }\n.text-inherit { color: inherit; }';
export const EMBED_SCRIPT = `
(function () {
var host = document.querySelector('[data-testid="xchatEmbedRoute"]'), tpl = document.getElementById('fx-embed');
if (!host || !tpl) return;
// x.com's document no longer declares the chat's primitives (--x-white is empty outside the root).
var x = document.getElementById('x-css'); if (x) x.textContent = x.textContent.replace(${JSON.stringify(CHAT_CSS)}, '').replace(${JSON.stringify(EMBED_PRIMITIVES)}, '');
var root = host.attachShadow({ mode: 'open' });
root.appendChild(tpl.content.cloneNode(true));
var sheet = new CSSStyleSheet();
sheet.replaceSync(${JSON.stringify(CHAT_CSS)}.replace(/:root\\[data-theme="(dark|light)"\\]/g, ':host([data-theme="$1"])').replace(/:root/g, ':host'));
sheet.insertRule(${JSON.stringify(EMBED_PRIMITIVES.replace(':root', ':host'))}, sheet.cssRules.length);
${JSON.stringify(EMBED_UTILITIES)}.split('\\n').forEach(function (rule) { if (rule.trim()) sheet.insertRule(rule, sheet.cssRules.length); });
root.adoptedStyleSheets = [sheet];
})();
`;
/** Runs in the fixture: what X's own script does (one panel below the wide container width, the top spacer as tall as the floating header). */
export const CHAT_SCRIPT = `
(function () {
var embed = document.querySelector('[data-testid="xchatEmbedRoute"]');
var container = document.querySelector('[data-testid="dm-container"]') || (embed && embed.shadowRoot && embed.shadowRoot.querySelector('[data-testid="dm-container"]')); if (!container) return;
function layout() {
// On the chat route X keeps its nav as the icon rail at every width.
var navInner = document.querySelector('.fx-nav');
if (navInner) { navInner.classList.remove('r-nav275'); navInner.classList.add('r-nav88'); document.querySelectorAll('.fx-label').forEach(function (l) { l.style.display = 'none'; }); var post = document.querySelector('.fx-post span'); if (post) post.textContent = '+'; }
var wide = container.clientWidth >= 918;
var inboxPanel = container.querySelector('[data-testid="dm-inbox-panel"]');
inboxPanel.style.display = wide ? '' : 'none';
container.querySelector('.fx-back').style.display = wide ? 'none' : '';
var overlay = container.querySelector('.fx-overlay'), scroller = container.querySelector('[data-testid="dm-message-scroller"]');
var top = Math.ceil(overlay.getBoundingClientRect().height) + 3;
container.querySelector('.fx-top-spacer').style.height = top + 'px';
scroller.style.scrollPaddingTop = top + 'px';
}
layout(); window.addEventListener('resize', layout);
if (window.ResizeObserver) new ResizeObserver(layout).observe(container.querySelector('.fx-overlay'));
})();
`;