waybetter-for-x/apps/desktop/scripts/theme-matrix/fixtures.mjs

434 lines
52 KiB
JavaScript

// Static x.com fixtures for the theme regression matrix (scripts/theme-matrix.mjs).
//
// They reproduce the parts of X's markup the theme engine reads and the shapes that broke it,
// with values read from live x.com (Lights out, logged in, 2026-09-27) and X's Default palette:
// - react-native-web atomic classes (`.css-175oi2r` base with `background-color:rgba(0,0,0,0)`,
// `.r-kemksi` page black, `.r-cl2sl0` = rgb(20,20,20), the surface of every Lights out layer),
// inline `style="color: rgb(...)"` text, X's 1050px row / 600px column cap / 350px sidebar;
// - the Tailwind shells (chat, settings) that color through `--color-gray-*` / `--x-fg-*`
// variables declared on `:root[data-theme="dark"]`;
// - Creator Studio's jetfuel neutrals with variable alpha (`rgba(0, 0, 0, var(--jbgo))`);
// - Articles, whose row content has no intrinsic width (it collapsed to 0 under the old rules);
// - `#layers`: More menu, hover card, modal sheets, mask.
// X's own breakpoints (nav 275/88px, sidebar hidden below 1000px) are emulated by swapping the
// same atomic classes on resize, as react-native-web does. Names and text are placeholders.
// X's chat (XChat) has its own token layer and markup: see chat-fixtures.mjs.
import { CHAT_CSS, CHAT_SCRIPT, EMBED_SCRIPT, chatRoute } from './chat-fixtures.mjs';
const MODES = {
light: {
page: 'rgb(255, 255, 255)', layer: 'rgb(255, 255, 255)', border: 'rgb(239, 243, 244)', text: 'rgb(15, 20, 25)', muted: 'rgb(83, 100, 113)',
hover: 'rgba(15, 20, 25, 0.1)', search: 'rgb(239, 243, 244)', module: 'rgb(247, 249, 249)', fill: 'rgb(15, 20, 25)', onFill: 'rgb(255, 255, 255)',
mask: 'rgba(0, 0, 0, 0.4)', shadow: 'rgba(101, 119, 134, 0.2) 0px 0px 15px, rgba(101, 119, 134, 0.15) 0px 0px 3px 1px',
// Mirrored from Lights out (X's border gray as the toolbar icon color): unverified on x.com Default.
dimIcon: 'rgb(207, 217, 222)', iconButton: 'rgb(15, 20, 25)',
theme: '#ffffff', dataTheme: 'light',
},
dark: {
page: 'rgb(0, 0, 0)', layer: 'rgb(20, 20, 20)', border: 'rgb(47, 51, 54)', text: 'rgb(231, 233, 234)', muted: 'rgb(113, 118, 123)',
hover: 'rgba(231, 233, 234, 0.1)', search: 'rgb(32, 35, 39)', module: 'rgb(22, 24, 28)', fill: 'rgb(239, 243, 244)', onFill: 'rgb(15, 20, 25)',
mask: 'rgba(0, 0, 0, 0.5)', shadow: 'rgba(0, 0, 0, 0.5) 0px 4px 12px, rgba(0, 0, 0, 0.35) 0px 0px 2px',
// Read live (2026-09-30): the composer toolbar icons are `svg.r-1otekoa` inside a div with this inline color.
dimIcon: 'rgb(51, 54, 57)', iconButton: 'rgb(239, 243, 244)',
theme: '#000000', dataTheme: 'dark',
},
};
// X's Tailwind variable blocks, as declared on x.com (HSL triples).
const TAILWIND = `
:root, :root[data-theme="light"], [data-theme="light"] { --background: 0 0% 100%; --foreground: 210 25% 8%; --color-text: 210 25% 8%; --border: 197 16% 91%;
--color-modal-background: 0 0% 100%; --color-nested-border: 200 19% 84%; --x-fg-primary: #0f1419; --x-fg-secondary: #536471; --x-bg-primary: #ffffff;
--color-gray-0: 180 14% 97%; --color-gray-50: 192 19% 95%; --color-gray-100: 197 16% 91%; --color-gray-200: 200 19% 84%; --color-gray-300: 201 23% 78%;
--color-gray-400: 203 23% 69%; --color-gray-500: 205 20% 59%; --color-gray-600: 206 14% 49%; --color-gray-700: 206 15% 38%; --color-gray-800: 206 19% 31%;
--color-gray-900: 207 17% 26%; --color-gray-1000: 207 20% 18%; --color-gray-1100: 210 25% 8%; }
:root[data-theme="dark"], [data-theme="dark"], .dark { --background: 0 0% 0%; --foreground: 200 7% 91%; --color-text: 200 7% 91%; --border: 210 7% 18%;
--color-modal-background: 0 0% 8%; --color-nested-border: 210 4% 46%; --x-fg-primary: #e7e9ea; --x-fg-secondary: #71767b; --x-bg-primary: #000000;
--color-gray-0: 220 12% 10%; --color-gray-50: 214 10% 14%; --color-gray-100: 210 7% 18%; --color-gray-200: 210 6% 21%; --color-gray-300: 210 5% 25%;
--color-gray-400: 214 4% 31%; --color-gray-500: 213 5% 37%; --color-gray-600: 207 4% 41%; --color-gray-700: 210 4% 46%; --color-gray-800: 207 4% 59%;
--color-gray-900: 210 4% 73%; --color-gray-1000: 204 6% 85%; --color-gray-1100: 200 7% 91%; }
.tw-name { color: hsl(var(--color-gray-1000)); font-weight: 700; }
.tw-preview { color: hsl(var(--color-gray-800)); }
.tw-time { color: hsl(var(--color-gray-700)); }
.tw-title { color: hsl(var(--foreground)); }
.tw-pane { background-color: hsl(var(--background)); }
.tw-list { background-color: hsl(var(--color-gray-0)); }
.tw-row:hover, .tw-row.is-active { background-color: hsl(var(--color-gray-50)); }
.tw-divider { border-color: hsl(var(--color-gray-100)); }
.tw-search { background-color: hsl(var(--color-gray-50)); color: hsl(var(--color-gray-700)); }
:root { --x-white: #ffffff; --x-black: #000000; --x-blue-500: #1d9bf0; }
:root { --chat-accent: 204 88% 53%; --x-white-60: rgba(255, 255, 255, 0.6); }
/* XChat (live, 2026-09-27): the sent bubble is an HSL variable fill, its label X's white variable. */
.bg-chat-accent { background-color: hsl(var(--chat-accent)); } .text-white { color: var(--x-white); } .text-white-60 { color: var(--x-white-60); } .text-inherit { color: inherit; }
.tw-fg { color: var(--x-fg-primary); } .tw-fg2 { color: var(--x-fg-secondary); }
`;
// Creator Studio (jetfuel): neutral colors with variable alpha, dark rules scoped under data-theme.
const JETFUEL = `
.jetfuel-style-root { --jbgo: 1; --jtxo: 1; }
.jetfuel-style-root .jf-surface { background-color: rgba(255, 255, 255, var(--jbgo)); --jf-bg-color: rgba(255, 255, 255, var(--jbgo)); }
.jetfuel-style-root .jf-text { color: rgba(0, 0, 0, var(--jtxo)); --jf-text-color: rgba(0, 0, 0, var(--jtxo)); }
.jetfuel-style-root .jf-muted { color: rgba(115, 115, 115, var(--jtxo)); }
.jetfuel-style-root .jf-pill { background-color: rgba(229, 229, 229, var(--jbgo)); color: rgba(82, 82, 82, var(--jtxo)); }
[data-theme="dark"] .jetfuel-style-root .jf-surface { background-color: rgba(0, 0, 0, var(--jbgo)); --jf-bg-color: rgba(0, 0, 0, var(--jbgo)); }
[data-theme="dark"] .jetfuel-style-root .jf-text { color: rgba(255, 255, 255, var(--jtxo)); --jf-text-color: rgba(255, 255, 255, var(--jtxo)); }
[data-theme="dark"] .jetfuel-style-root .jf-muted { color: rgba(115, 115, 115, var(--jtxo)); }
[data-theme="dark"] .jetfuel-style-root .jf-pill { background-color: rgba(38, 38, 38, var(--jbgo)); color: rgba(115, 115, 115, var(--jtxo)); }
`;
function xCss(m) {
return `
html, body { margin: 0; height: 100%; }
body { font-family: TwitterChirp, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
.css-175oi2r { align-items: stretch; background-color: rgba(0, 0, 0, 0); border: 0 solid black; box-sizing: border-box; display: flex; flex-basis: auto; flex-direction: column; flex-shrink: 0; list-style: none; margin: 0; min-height: 0; min-width: 0; padding: 0; position: relative; z-index: 0; }
.css-1jxf684 { background-color: rgba(0, 0, 0, 0); border: 0 solid black; box-sizing: border-box; color: inherit; display: inline; font: inherit; margin: 0; padding: 0; white-space: inherit; overflow-wrap: break-word; }
.r-chirp { font-family: "TwitterChirp", -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
.r-a023e6 { font-size: 15px; } .r-rjixqe { line-height: 20px; } .r-adyw6z { font-size: 20px; } .r-135wba7 { line-height: 24px; }
.r-b88u0q { font-weight: 700; } .r-16dba41 { font-weight: 400; }
.r-kemksi { background-color: ${m.page}; }
.r-cl2sl0 { background-color: ${m.layer}; }
.r-1jyoszn { box-shadow: ${m.shadow}; }
.r-1kqtdi0 { border-color: ${m.border}; }
.r-1cvl2hr { color: rgb(29, 155, 240); }
.r-l5o3uw { background-color: rgb(29, 155, 240); }
.r-jwli3a { color: rgb(255, 255, 255); }
.r-hover { background-color: ${m.hover}; }
.r-yyyyoo { fill: currentcolor; }
.r-1otekoa { color: ${m.dimIcon}; }
/* Read live 2026-10-01: X brightens the dim tool icons with a filter on their wrapper. */
.r-1jodnrb { filter: brightness(2.5); }
.r-module { background-color: ${m.module}; }
.r-search { background-color: ${m.search}; }
.r-sdzlij { border-top-left-radius: 9999px; border-top-right-radius: 9999px; border-bottom-right-radius: 9999px; border-bottom-left-radius: 9999px; }
.r-1q9bdsx { border-top-left-radius: 16px; } .r-card16 { border-top-left-radius: 16px; border-top-right-radius: 16px; border-bottom-right-radius: 16px; border-bottom-left-radius: 16px; }
.r-ttdzmv { padding-top: 12px; } .r-kzbkwu { padding-bottom: 12px; } .r-qklmqi { border-bottom-width: 1px; }
.r-lrvibr { border-left-width: 1px; } .r-thb0q2 { border-right-width: 1px; }
/* Layout (X's own widths) */
.r-1ye8kvj { max-width: 600px; }
.r-13qz1uu { width: 100%; }
.r-16y2uox { flex-grow: 1; }
.r-1wbh5a2 { flex-shrink: 1; }
.r-1habvwh { align-items: flex-start; }
.r-18u37iz { flex-direction: row; }
.r-113js5t { width: 1050px; }
.r-row990 { width: 990px; }
.r-row920 { width: 920px; }
.r-row600 { width: 600px; }
.r-1hycxz { width: 350px; }
.r-side290 { width: 290px; }
.r-10f7w94 { margin-right: 70px; }
.r-nav275 { width: 275px; }
.r-nav88 { width: 88px; }
.r-chatrow { width: 1257px; }
.r-maxw100 { max-width: 100%; }
.r-sticky { position: sticky; top: 0; }
${TAILWIND}
${JETFUEL}
${CHAT_CSS}
/* Fixture-only helpers (not X classes): pure geometry, no colors. */
.fx-avatar { width: 40px; height: 40px; border-radius: 9999px; background: linear-gradient(135deg, #8aa4b8, #4f6b80); flex: 0 0 40px; }
.fx-media { height: 180px; border-radius: 16px; background: linear-gradient(135deg, #3b5b76, #9fb6c8); margin-top: 10px; }
.fx-icon { width: 26px; height: 26px; border-radius: 6px; border: 2px solid currentColor; box-sizing: border-box; }
`;
}
const T = (color, text, extra = '') => `<span class="css-1jxf684 r-chirp r-a023e6 r-rjixqe${extra}" style="color: ${color};">${text}</span>`;
function tweet(m, name, handle, body, media = false) {
return `<div data-testid="cellInnerDiv" class="css-175oi2r"><div class="css-175oi2r r-qklmqi r-1kqtdi0" style="border-bottom-style: solid;">
<article data-testid="tweet" class="css-175oi2r r-18u37iz" style="padding: 0 16px;" tabindex="0">
<div class="css-175oi2r r-ttdzmv" style="margin-right: 8px;"><div data-testid="UserAvatar-Container-x" class="css-175oi2r"><div class="fx-avatar"></div></div></div>
<div class="css-175oi2r r-16y2uox r-1wbh5a2 r-ttdzmv r-kzbkwu">
<div data-testid="User-Name" class="css-175oi2r r-18u37iz" style="gap: 4px;">${T(MODES_TEXT(m), name, ' r-b88u0q')}<svg data-testid="icon-verified" aria-label="Verified account" viewBox="0 0 22 22" width="18" height="18" class="r-1cvl2hr" style="color: rgb(29, 155, 240);"><circle cx="11" cy="11" r="9" fill="currentColor"/></svg>${T(m.muted, `@${handle} · 2h`)}</div>
<div data-testid="tweetText" class="css-175oi2r" style="margin-top: 2px;">${T(m.text, body)}</div>
${media ? '<div class="fx-media"></div>' : ''}
<div role="group" class="css-175oi2r r-18u37iz" style="justify-content: space-between; max-width: 425px; margin-top: 12px;">${['reply', 'retweet', 'like', 'views'].map((name, i) => { const inner = `<div class="css-175oi2r r-18u37iz" style="align-items: center; gap: 4px;"><div class="css-175oi2r">${icon()}</div><div class="css-175oi2r"><span data-testid="app-text-transition-container">${T(m.muted, ['12', '48', '1.2K', '9K'][i])}</span></div></div>`; return name === 'views' ? `<a href="/placeholder/status/1/analytics" aria-label="views" class="css-175oi2r" style="text-decoration: none; color: ${m.muted};">${inner}</a>` : `<button type="button" role="button" data-testid="${name}" class="css-175oi2r" style="border: 0; background: transparent; padding: 0; color: ${m.muted};">${inner}</button>`; }).join('')}</div>
</div>
</article></div></div>`;
}
const MODES_TEXT = m => m.text;
/** One of X's icons: an svg that fills with the current color (`r-yyyyoo`); `extra` adds X's color class. */
function icon(extra = '') { return `<svg viewBox="0 0 24 24" aria-hidden="true" width="18" height="18" class="r-yyyyoo${extra}"><g><path d="M4 4h16v12H9l-5 4z"></path></g></svg>`; }
/**
* X's composer (read live, Lights out, 2026-09-30): the editor, the audience row in X's blue, and
* the toolbar, whose icons are `svg.r-1otekoa` (one of X's border grays, 1.7:1 on X's own black)
* inside a button whose content color is X's primary text. The Post button is X's filled button.
*/
function composer(m, { inline = true } = {}) {
const tools = ['Add photos or video', 'Add a GIF', 'Enhance your post with Grok', 'Add poll', 'Add emoji', 'Schedule post', 'Tag location'];
return `<div class="css-175oi2r r-18u37iz${inline ? ' r-qklmqi r-1kqtdi0' : ''}" style="padding: 12px 16px; gap: 12px;${inline ? ' border-bottom-style: solid;' : ''}"><div class="fx-avatar"></div><div class="css-175oi2r r-16y2uox r-1wbh5a2" style="gap: 12px;">
<div data-testid="tweetTextarea_0" role="textbox" class="r-chirp r-adyw6z" style="color: ${m.muted}; min-height: ${inline ? 28 : 96}px;">What is happening?!</div>
<div class="css-175oi2r r-18u37iz r-qklmqi r-1kqtdi0" style="border-bottom-style: solid; padding-bottom: 12px;"><div role="button" class="css-175oi2r r-18u37iz" style="align-items: center; gap: 4px; color: rgb(29, 155, 240);">${icon()}${T('rgb(29, 155, 240)', 'Everyone can reply', ' r-b88u0q')}</div></div>
<div class="css-175oi2r r-18u37iz" style="align-items: center; justify-content: space-between;"><div data-testid="toolBar" class="css-175oi2r r-18u37iz r-16y2uox" style="align-items: center;"><nav class="css-175oi2r r-18u37iz" style="align-items: center;">
${tools.map(label => `<button aria-label="${label}" role="button" type="button" class="css-175oi2r r-sdzlij" style="background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0); min-width: 36px; min-height: 36px; align-items: center; justify-content: center;"><div dir="ltr" class="css-175oi2r r-1jodnrb" style="color: ${m.iconButton};">${icon(' r-1otekoa')}</div></button>`).join('')}
</nav></div><button data-testid="${inline ? 'tweetButtonInline' : 'tweetButton'}" class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 8px 18px; border: 0; opacity: 0.5;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">Post</span></button></div>
</div></div>`;
}
function nav(m) {
// X's rail as read live (2026-09-30): header > width wrapper > sticky column > [logo + nav + Post, account].
// Nav items: a[data-testid=AppTabBar_*] > div (the pill) > [div > svg, div > span].
const items = [['AppTabBar_Home_Link', '/home', 'Home'], ['AppTabBar_Explore_Link', '/explore', 'Explore'], ['AppTabBar_Notifications_Link', '/notifications', 'Notifications'],
['AppTabBar_DirectMessage_Link', '/i/chat', 'Chat'], ['', '/i/grok', 'Grok'], ['premium-hub-tab', '/i/premium_sign_up', 'Premium'], ['', '/i/bookmarks', 'Bookmarks'],
// Read live 2026-10-02: X shows up to 12 rail items (History, Creator Studio, Articles join the usual ones).
['', '/i/history', 'History'], ['', '/i/jf/creators/studio', 'Creator Studio'], ['', '/compose/articles', 'Articles'],
['AppTabBar_Profile_Link', '/placeholder', 'Profile'], ['AppTabBar_More_Menu', '', 'More']];
const glyph = `<svg viewBox="0 0 24 24" aria-hidden="true" width="26" height="26" class="r-yyyyoo" style="color: ${m.text};"><g><path d="M12 3l9 8h-3v9H6v-9H3z"></path></g></svg>`;
const item = ([testid, href, label]) => {
const tag = href ? 'a' : 'button';
return `<${tag} ${href ? `href="${href}" role="link"` : 'type="button" role="button"'}${testid ? ` data-testid="${testid}"` : ''} aria-label="${label}" class="css-175oi2r r-16y2uox r-13qz1uu" style="text-decoration: none; border: 0; background: transparent; padding: 4px 0;"><div class="css-175oi2r r-18u37iz r-sdzlij" style="align-items: center; gap: 18px; padding: 12px;"><div class="css-175oi2r">${glyph}</div><div class="css-175oi2r fx-label-box"><span class="css-1jxf684 r-chirp r-adyw6z fx-label" style="color: ${m.text};">${label}</span></div></div></${tag}>`;
};
return `<header role="banner" class="css-175oi2r" style="align-items: flex-end; flex-grow: 1;">
<div class="css-175oi2r"><div class="css-175oi2r fx-nav" style="position: sticky; top: 0; height: 100vh; padding: 0 8px; box-sizing: border-box;"><div class="css-175oi2r" style="height: 100%; justify-content: space-between;">
<div class="css-175oi2r" style="gap: 4px; padding-top: 8px;">
<h1 role="heading" class="css-175oi2r" style="margin: 0;"><a href="/home" aria-label="X" role="link" class="css-175oi2r" style="padding: 12px;"><div class="fx-icon" style="color: ${m.text};"></div></a></h1>
<div class="css-175oi2r r-13qz1uu"><nav role="navigation" aria-label="Primary" class="css-175oi2r r-13qz1uu">${items.map(item).join('')}</nav></div>
<div class="css-175oi2r r-13qz1uu" style="align-items: center;"><a href="#" role="link" data-testid="SideNav_NewTweet_Button" class="css-175oi2r r-sdzlij fx-post" style="background-color: ${m.fill}; border-color: rgba(0, 0, 0, 0); margin-top: 12px; min-height: 52px; align-items: center; justify-content: center; text-decoration: none;"><div class="css-175oi2r" style="color: ${m.onFill};"><span class="css-1jxf684 r-chirp r-b88u0q r-a023e6">Post</span></div></a></div>
</div>
<div class="css-175oi2r" style="padding: 12px 0;"><button type="button" data-testid="SideNav_AccountSwitcher_Button" aria-label="Account menu" class="css-175oi2r" style="border: 0; background: transparent;"><div class="fx-avatar"></div></button></div>
</div></div></div></header>`;
}
function sidebar(m, extra = '') {
return `<div data-testid="sidebarColumn" class="css-175oi2r r-1hycxz r-10f7w94 fx-sidebar">
<div class="css-175oi2r" style="gap: 16px; padding-top: 8px;">
${extra}<form role="search" class="css-175oi2r"><div class="css-175oi2r"><div class="css-175oi2r r-search r-sdzlij" style="padding: 10px 16px;">${T(m.muted, 'Search')}</div></div></form>
<div class="css-175oi2r r-1kqtdi0 r-card16" style="border-width: 1px; border-style: solid;"><div class="css-175oi2r"><aside role="complementary" class="css-175oi2r" style="padding: 12px 16px; gap: 10px;">
<span class="css-1jxf684 r-chirp r-adyw6z r-b88u0q" style="color: ${m.text};">You might like</span>
${['Avery Stone', 'Kai Morgan', 'Remy Hart'].map(n => `<div class="css-175oi2r r-18u37iz" style="align-items: center; gap: 10px;"><div class="fx-avatar"></div><div class="css-175oi2r r-16y2uox">${T(m.text, n, ' r-b88u0q')}${T(m.muted, '@' + n.split(' ')[0].toLowerCase())}</div><button data-testid="1-follow" class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 6px 14px; border: 0;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">Follow</span></button></div>`).join('')}
</aside></div></div>
</div></div>`;
}
function columnHeader(m, title, tabs = []) {
return `<div class="css-175oi2r r-sticky r-kemksi r-qklmqi r-1kqtdi0" style="border-bottom-style: solid; z-index: 3;">
<div class="css-175oi2r" style="padding: 12px 16px;"><h2 class="css-1jxf684 r-chirp r-adyw6z r-b88u0q" style="margin: 0; color: ${m.text};">${title}</h2></div>
${tabs.length ? `<div role="tablist" class="css-175oi2r r-18u37iz">${tabs.map((t, i) => `<a role="tab" href="#" class="css-175oi2r r-16y2uox" style="align-items: center; padding: 14px 0; text-decoration: none;">${T(i ? m.muted : m.text, t, i ? '' : ' r-b88u0q')}</a>`).join('')}</div>` : ''}
</div>`;
}
function timeline(m, count = 5) {
const posts = [
['Lena Ortiz', 'lena', 'Shipping the new build tonight. Themes should never fight the layout.', true],
['Theo Park', 'theo', 'The best interfaces feel inevitable once you see them.'],
['Mira Chen', 'mira', 'Hot take: hover cards should be opaque. Always.'],
['Jonas Weber', 'jonas', 'Wrote 2k words on typographic rhythm and nobody asked.', true],
['Sana Iqbal', 'sana', 'Split panes at 1275px are the real stress test.'],
];
return `<section role="region" aria-label="Timeline" class="css-175oi2r"><div class="css-175oi2r">${posts.slice(0, count).map(p => tweet(m, ...p)).join('')}</div></section>`;
}
function newPostsPill() {
return `<div role="status" class="css-175oi2r" style="position: absolute; left: 0; right: 0; top: 64px; align-items: center; z-index: 4;"><button class="css-175oi2r r-l5o3uw r-sdzlij" style="padding: 8px 16px; border: 0;"><div data-testid="pillLabel" class="css-175oi2r" style="color: rgb(255, 255, 255);"><span class="css-1jxf684 r-chirp r-b88u0q r-a023e6">See new posts</span></div></button></div>`;
}
/** X's usual shell: nav + main row (primary column + sidebar). */
function standardShell(m, primary, { sidebarShown = true, sidebarExtra = '' } = {}) {
return `<main role="main" class="css-175oi2r r-16y2uox r-1wbh5a2 r-1habvwh">
<div class="css-175oi2r r-16y2uox r-1wbh5a2 fx-row r-113js5t" style="min-width: 0;"><div class="css-175oi2r r-16y2uox r-1wbh5a2"><div class="css-175oi2r r-18u37iz r-16y2uox" style="justify-content: space-between;">
<div data-testid="primaryColumn" class="css-175oi2r r-16y2uox r-13qz1uu r-1ye8kvj r-kemksi r-lrvibr r-thb0q2 r-1kqtdi0" style="border-style: solid; margin: 0 auto; min-height: 100vh;">${primary}</div>
${sidebarShown ? sidebar(m, sidebarExtra) : ''}
</div></div></div></main>`;
}
const ROUTES = {
home: m => standardShell(m, `${columnHeader(m, 'Home', ['For you', 'Following'])}${newPostsPill()}${timeline(m)}`),
// Home with X's inline composer on top of the timeline.
'home-composer': m => standardShell(m, `${columnHeader(m, 'Home', ['For you', 'Following'])}${composer(m)}${timeline(m, 3)}`),
explore: m => standardShell(m, `${columnHeader(m, 'Explore', ['For You', 'Trending', 'News'])}<div class="css-175oi2r" style="padding: 12px 16px; gap: 14px;">${['Design', 'Tauri 3.0', 'Typography', 'Split panes'].map((t, i) => `<div class="css-175oi2r">${T(m.muted, `${i + 1} · Trending in Technology`)}${T(m.text, t, ' r-b88u0q')}${T(m.muted, `${(i + 2) * 3}K posts`)}</div>`).join('')}</div>${timeline(m, 2)}`),
notifications: m => standardShell(m, `${columnHeader(m, 'Notifications', ['All', 'Verified', 'Mentions'])}<div class="css-175oi2r">${[1, 2, 3].map(i => `<div data-testid="cellInnerDiv" class="css-175oi2r"><div class="css-175oi2r r-18u37iz r-qklmqi r-1kqtdi0 r-ttdzmv r-kzbkwu" style="border-bottom-style: solid; padding: 12px 16px; gap: 12px;"><div class="fx-icon" style="color: rgb(249, 24, 128);"></div><div class="css-175oi2r r-16y2uox"><div class="css-175oi2r r-18u37iz" style="gap: 4px;">${[1, 2, 3, 4].map(() => '<div class="fx-avatar" style="width: 32px; height: 32px; flex-basis: 32px;"></div>').join('')}</div>${T(m.text, `<b>Person ${i}</b> and ${i * 7} others liked your reply · 1m`)}${T(m.muted, 'Procedural olarak generate etmek için yöntemler var ...')}</div></div></div>`).join('')}</div>${timeline(m, 2)}`),
bookmarks: m => standardShell(m, `${columnHeader(m, 'Bookmarks')}${timeline(m, 4)}`),
profile: m => standardShell(m, `${columnHeader(m, 'Placeholder User')}<div class="fx-media" style="margin: 0; border-radius: 0; height: 160px;"></div><div class="css-175oi2r" style="padding: 12px 16px; gap: 6px;"><div class="css-175oi2r r-18u37iz" style="justify-content: flex-end;"><a href="/settings/profile" role="link" class="css-175oi2r r-sdzlij r-1kqtdi0" style="border-width: 1px; border-style: solid; padding: 6px 14px; text-decoration: none;">${T(m.text, 'Edit profile', ' r-b88u0q')}</a></div>${T(m.text, 'Placeholder User', ' r-b88u0q r-adyw6z')}${T(m.muted, '@placeholder')}${T(m.text, 'Computer scientist. Writes about interfaces.')}<div class="css-175oi2r r-18u37iz" style="gap: 12px;">${T(m.text, '<b>544</b>')}${T(m.muted, 'Following')}${T(m.text, '<b>240</b>')}${T(m.muted, 'Followers')}</div></div><div role="tablist" class="css-175oi2r r-18u37iz r-qklmqi r-1kqtdi0" style="border-bottom-style: solid;">${['Posts', 'Replies', 'Media'].map((t, i) => `<a role="tab" href="#" class="css-175oi2r r-16y2uox" style="align-items: center; padding: 14px 0; text-decoration: none;">${T(i ? m.muted : m.text, t)}</a>`).join('')}</div>${timeline(m, 3)}`),
status: m => standardShell(m, `${columnHeader(m, 'Post')}<article data-testid="tweet" class="css-175oi2r" style="padding: 12px 16px;"><div class="css-175oi2r r-18u37iz" style="gap: 10px;"><div class="fx-avatar"></div><div data-testid="User-Name" class="css-175oi2r">${T(m.text, 'Lena Ortiz', ' r-b88u0q')}${T(m.muted, '@lena')}</div></div><div data-testid="tweetText" class="css-175oi2r" style="margin-top: 12px;"><span class="css-1jxf684 r-chirp r-adyw6z" style="color: ${m.text};">A focused status page: long text, the reply box and replies below. <a href="#" class="css-1jxf684 r-1cvl2hr" style="color: rgb(29, 155, 240);">example.com</a></span></div><div class="fx-media"></div>${T(m.muted, '7:14 PM · Sep 27, 2026 · 12K Views')}</article><div class="css-175oi2r r-18u37iz r-qklmqi r-1kqtdi0" style="border-bottom-style: solid; border-top: 1px solid; padding: 12px 16px; gap: 10px; align-items: center;"><div class="fx-avatar"></div>${T(m.muted, 'Post your reply')}<div class="css-175oi2r r-16y2uox"></div><button data-testid="tweetButtonInline" class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 8px 16px; border: 0; opacity: 0.5;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">Reply</span></button></div>${timeline(m, 2)}`),
// Creator Studio: X's primary column hosts a jetfuel root with its own neutrals.
'creator-studio': m => standardShell(m, `<div class="jetfuel-style-root"><div class="jf-surface" style="padding: 12px 16px 24px;"><div style="display: flex; align-items: center; justify-content: center; padding: 8px 0 18px;"><span class="jf-text" style="font: 700 18px/24px TwitterChirp, sans-serif;">Creator Studio</span></div>${[['Programs', [['Original Content Rewards', 'Earn from your posts', 'Ineligible'], ['Subscriptions', '', 'Ineligible']]], ['Tools', [['Live Studio', 'Go live professionally', 'New'], ['Analytics', '', ''], ['Inspiration', 'Top posts by engagement', '']]]].map(([h, rows]) => `<div class="jf-text" style="font: 700 15px/20px TwitterChirp, sans-serif; margin: 14px 0 8px;">${h}</div>${rows.map(([t, s, pill]) => `<div style="display: flex; align-items: center; gap: 14px; padding: 12px 0;"><div class="fx-icon jf-text"></div><div style="flex: 1;"><div class="jf-text" style="font: 15px/20px TwitterChirp, sans-serif;">${t}</div>${s ? `<div class="jf-muted" style="font: 13px/16px TwitterChirp, sans-serif;">${s}</div>` : ''}</div>${pill ? `<span class="jf-pill" style="font: 12px/16px TwitterChirp, sans-serif; padding: 2px 8px; border-radius: 6px;">${pill}</span>` : ''}</div>`).join('')}`).join('')}</div></div>`),
// Chat: X's primary column holds a full-width Tailwind app (list + conversation), no 600px cap, no sidebar.
chat: m => `<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 data-testid="primaryColumn" class="css-175oi2r r-16y2uox r-13qz1uu" style="min-height: 100vh;">
<div style="display: flex; height: 100vh;"><div class="tw-list tw-divider" style="width: 400px; border-right-width: 1px; border-right-style: solid; display: flex; flex-direction: column;">
<div style="display: flex; align-items: center; padding: 14px 16px;"><span class="tw-title" style="font: 700 20px/24px TwitterChirp, sans-serif; flex: 1;">Chat</span><span class="tw-fg2" style="font: 14px TwitterChirp, sans-serif;">All</span></div>
<div class="tw-search" style="margin: 0 16px 8px; padding: 10px 14px; border-radius: 9999px; font: 15px TwitterChirp, sans-serif;">Search</div>
${['AI Game Lobby', 'Servet Carey', 'Casie Nakamura', 'mrdoob', 'robot 2.0', 'Jorrit Boumann'].map((n, i) => `<div class="tw-row${i === 1 ? ' is-active' : ''}" style="display: flex; gap: 12px; padding: 12px 16px;"><div class="fx-avatar" style="width: 48px; height: 48px; flex-basis: 48px;"></div><div style="flex: 1; min-width: 0;"><div style="display: flex;"><span class="tw-name" style="font: 700 15px/20px TwitterChirp, sans-serif; flex: 1;">${n}</span><span class="tw-time" style="font: 13px TwitterChirp, sans-serif;">${i + 1}h</span></div><div class="tw-preview" style="font: 14px/18px TwitterChirp, sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">You: see you there, the build is almost ready</div></div></div>`).join('')}
</div><div class="tw-pane" style="flex: 1; display: flex; flex-direction: column; justify-content: flex-end; padding: 16px; gap: 8px;">
<div class="tw-list" style="align-self: flex-start; padding: 10px 14px; border-radius: 18px;"><span class="tw-fg" style="font: 15px TwitterChirp, sans-serif;">hey, did the theme fix land?</span></div>
<div class="bg-chat-accent rounded-chat" style="align-self: flex-end; padding: 10px 14px; border-radius: 18px;"><div class="text-white" style="display: flex; gap: 8px; align-items: flex-end;"><span class="text-inherit" style="font: 15px TwitterChirp, sans-serif;">yes, layers are opaque now</span><span class="text-inherit" style="font: 12px TwitterChirp, sans-serif;">23:26</span></div></div>
</div></div></div></div></main>`,
// XChat as it is on x.com (chat-fixtures.mjs): a group with a pinned message, a 1:1, and the message menu.
'chat-group': m => chatRoute(m, 'group'),
'chat-direct': m => chatRoute(m, 'direct'),
'chat-direct-menu': m => chatRoute(m, 'direct', true),
// X's chat embed: the same app inside an open shadow root (x.com since 2026-10-01).
'chat-embed': m => chatRoute(m, 'direct', false, true),
// Articles: X's row with content that has no intrinsic width (absolutely positioned editor).
articles: m => `<main role="main" class="css-175oi2r r-16y2uox r-1wbh5a2 r-1habvwh"><div class="css-175oi2r r-16y2uox r-1wbh5a2 r-113js5t fx-row fx-articles" style="min-width: 0; min-height: 100vh;"><div class="css-175oi2r r-16y2uox r-kemksi r-lrvibr r-1kqtdi0" style="border-style: solid;"><div data-testid="articles-editor" style="position: absolute; inset: 0; display: flex;"><div class="r-1kqtdi0" style="width: 38%; border-right-width: 1px; border-right-style: solid; padding: 16px; box-sizing: border-box;">${T(m.text, 'Articles', ' r-b88u0q r-adyw6z')}<div style="height: 12px;"></div>${T(m.muted, 'Drafts')}<div style="height: 8px;"></div>${T(m.text, 'Why opaque layers matter')}</div><div style="flex: 1; padding: 32px; box-sizing: border-box;">${T(m.text, 'Write a title', ' r-b88u0q r-adyw6z')}<div style="height: 12px;"></div>${T(m.muted, 'Start writing your article…')}</div></div></div></div></main>`,
// Theme v3 fixture cells (live failures, 2026-10-02). Creator Studio /i/monetization: jetfuel's "New" pill on its
// indigo fill rgb(30,27,75), the label coloured through --jf-text-color (1.19:1 live under Windows 98).
monetization: m => standardShell(m, `${columnHeader(m, 'Monetization')}<div class="jetfuel-style-root"><div class="jf-surface jf-text" style="padding: 16px; gap: 12px; display: flex; flex-direction: column;">${['Subscriptions', 'Creator Revenue Sharing'].map((t, i) => `<div class="jf-element flex" style="display: flex; align-items: center; gap: 8px;"><p class="jf-element" style="margin: 0; color: var(--jf-text-color); font: 600 15px TwitterChirp, sans-serif;">${t}</p>${i === 0 ? '<div class="jf-element" style="background-color: rgb(30, 27, 75); border-radius: 9999px; padding: 2px 8px;"><p class="jf-element jf-truncate" style="margin: 0; color: var(--jf-text-color); font: 700 12px TwitterChirp, sans-serif;">New</p></div>' : ''}</div>`).join('')}</div></div>`),
// The empty sidebar module (trends hidden: X keeps the framed section with nothing in it) and the nested
// wrapper of 1.0.2 (div > div > section, which a :has() framed as a whole).
'sidebar-empty': m => standardShell(m, `${columnHeader(m, 'Home', ['For you', 'Following'])}${timeline(m, 2)}`, { sidebarExtra: `<div class="css-175oi2r r-1kqtdi0 r-card16" style="border-width: 1px; border-style: solid; background-color: ${m.module};"><section role="region" aria-labelledby="accessible-list-x" class="css-175oi2r" style="min-height: 240px;"><h1 id="accessible-list-x" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0);"></h1></section></div>` }),
'sidebar-nested': m => standardShell(m, `${columnHeader(m, 'Home', ['For you', 'Following'])}${timeline(m, 2)}`, { sidebarExtra: `<div class="css-175oi2r"><div class="css-175oi2r"><section role="region" class="css-175oi2r r-1kqtdi0 r-card16" style="border-width: 1px; border-style: solid; padding: 12px 16px;">${T(m.text, 'What�s happening', ' r-b88u0q')}<div class="css-175oi2r" style="padding-top: 8px;">${T(m.muted, 'Placeholder trend')}</div></section></div></div>` }),
// Settings: two panes inside X's row, no primaryColumn testid on x.com.
settings: m => `<main role="main" class="css-175oi2r r-16y2uox r-1wbh5a2 r-1habvwh"><div class="css-175oi2r r-16y2uox r-1wbh5a2 r-113js5t fx-row" style="min-width: 0;"><div class="css-175oi2r r-18u37iz r-16y2uox" style="min-height: 100vh;"><section class="css-175oi2r r-kemksi r-lrvibr r-thb0q2 r-1kqtdi0" style="width: 400px; border-style: solid;">${columnHeader(m, 'Settings')}${['Your account', 'Security and account access', 'Privacy and safety', 'Notifications', 'Accessibility, display, and languages'].map((t, i) => `<a href="#" role="tab" class="css-175oi2r r-18u37iz${i === 0 ? ' r-hover' : ''}" style="padding: 14px 16px; text-decoration: none; justify-content: space-between;">${T(m.text, t)}${T(m.muted, '›')}</a>`).join('')}</section><section class="css-175oi2r r-16y2uox r-1wbh5a2 r-kemksi r-thb0q2 r-1kqtdi0" style="border-style: solid;">${columnHeader(m, 'Your account')}<div class="css-175oi2r" style="padding: 12px 16px; gap: 14px;">${T(m.muted, 'See information about your account, download an archive of your data, or learn about your account deactivation options.')}${['Account information', 'Change your password', 'Download an archive of your data'].map(t => `<div class="css-175oi2r">${T(m.text, t)}${T(m.muted, 'Placeholder description')}</div>`).join('')}</div></section></div></div></main>`,
};
// `#layers`: portaled surfaces. Real X gives them the layer color (rgb(20,20,20) in Lights out).
const LAYERS = {
'more-menu': m => `<div class="css-175oi2r" style="position: fixed; inset: 0; z-index: 10;"><div role="menu" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="position: fixed; left: 24px; top: 150px; width: 318px; padding: 8px 0;">${['Lists', 'Communities', 'Creator Studio', 'Business', 'Ads', 'Create your Space', 'Settings and privacy'].map(t => `<a role="menuitem" href="#" class="css-175oi2r r-18u37iz" style="gap: 14px; padding: 12px 16px; align-items: center; text-decoration: none;"><div class="fx-icon" style="color: ${m.text};"></div>${T(m.text, t, ' r-b88u0q')}</a>`).join('')}</div></div>`,
'hover-card': m => `<div data-testid="hoverCardParent" class="css-175oi2r" style="position: fixed; left: 360px; top: 190px; z-index: 10;"><div data-testid="HoverCard" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="width: 300px; padding: 16px; gap: 8px;"><div class="css-175oi2r r-18u37iz" style="justify-content: space-between; align-items: flex-start;"><div class="fx-avatar" style="width: 64px; height: 64px; flex-basis: 64px;"></div><button data-testid="1-follow" class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 8px 16px; border: 0;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">Follow</span></button></div>${T(m.text, 'Fishies', ' r-b88u0q')}${T(m.muted, '@playfishies')}${T(m.text, 'The social aquarium that lives while you’re away.')}<div class="css-175oi2r r-18u37iz" style="gap: 12px;">${T(m.text, '<b>30</b>')}${T(m.muted, 'Following')}${T(m.text, '<b>21</b>')}${T(m.muted, 'Followers')}</div></div></div>`,
'edit-profile': m => modal(m, 'Edit profile', `<div class="fx-media" style="margin: 0; border-radius: 0; height: 150px;"></div><div class="css-175oi2r" style="padding: 16px; gap: 16px;">${['Name', 'Bio', 'Location', 'Website'].map(label => `<label class="css-175oi2r r-1kqtdi0 r-card16" style="border-width: 1px; border-style: solid; padding: 8px 12px;">${T(m.muted, label)}<input class="r-chirp r-a023e6" style="border: 0; outline: 0; background: transparent; color: ${m.text}; padding: 4px 0;" value="${label === 'Name' ? 'Placeholder User' : ''}"></label>`).join('')}</div>`, 'Save'),
compose: m => modal(m, '', composer(m, { inline: false })),
// X's tooltip under a post's caret ("More"): X's grey tooltip fill with the label in X's text colour.
'hover-label': m => `<div class="css-175oi2r" style="position: fixed; left: 640px; top: 120px; z-index: 10;"><div data-testid="HoverLabel" class="css-175oi2r" style="background-color: rgba(91, 112, 131, 0.8); padding: 2px 6px; border-radius: 4px;"><span class="css-1jxf684 r-chirp" style="color: rgb(255, 255, 255); font-size: 11px;">More</span></div></div>`,
// The account menu as X places it: from the account button's rect (above it, left-aligned), wherever a theme puts
// the button (the right end of a top or bottom bar pushed it 52px past the edge live).
'account-menu': m => `<div data-testid="hoverCardParent" data-fx-anchor="SideNav_AccountSwitcher_Button" data-fx-place="above" class="css-175oi2r" style="position: fixed; z-index: 10;"><div role="menu" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="width: 300px; box-shadow: ${m.shadow};">${['Add an existing account', 'Log out @placeholder'].map(t => `<a role="menuitem" class="css-175oi2r" style="padding: 12px 16px;">${T(m.text, t, ' r-b88u0q')}</a>`).join('')}</div></div>`,
// A hover card anchored near the bottom edge (X opens it below the anchor: cut off live).
'hovercard-bottom': m => `<div data-testid="hoverCardParent" data-fx-place="bottom" class="css-175oi2r" style="position: fixed; left: 360px; z-index: 10;"><div data-testid="HoverCard" class="css-175oi2r r-cl2sl0 r-1jyoszn r-card16" style="width: 300px; padding: 16px; gap: 8px; box-shadow: ${m.shadow};"><div class="fx-avatar"></div>${T(m.text, 'Placeholder User', ' r-b88u0q')}${T(m.muted, '@placeholder')}${T(m.text, 'Building small tools for the timeline. Theme engines, mostly.')}<div class="css-175oi2r r-18u37iz" style="gap: 12px;">${T(m.text, '120', ' r-b88u0q')}${T(m.muted, 'Following')}${T(m.text, '4,500', ' r-b88u0q')}${T(m.muted, 'Followers')}</div></div></div>`,
};
function modal(m, title, body, action = '') {
// x.com structure (read live): an outer zero-height dialog, a group, the mask, and the sheet
// (`role=dialog aria-modal aria-labelledby=modal-header`) whose child carries the layer color.
return `<div class="css-175oi2r" style="position: fixed; inset: 0; z-index: 10; pointer-events: none;"><div role="dialog" class="css-175oi2r" style="position: absolute; left: 0; right: 0; top: 0; height: 0;"><div role="group" class="css-175oi2r" style="position: fixed; inset: 0; align-items: center; justify-content: center; pointer-events: auto;">
<div data-testid="mask" class="css-175oi2r" style="position: absolute; inset: 0; background-color: ${m.mask};"></div>
<div aria-modal="true" aria-labelledby="modal-header" role="dialog" class="css-175oi2r r-card16" style="width: min(600px, 100vw); max-height: 90vh; overflow: hidden;"><div class="css-175oi2r r-cl2sl0" style="overflow: auto;">
<div class="css-175oi2r r-18u37iz" style="align-items: center; padding: 8px 16px; min-height: 53px; gap: 16px;"><span class="css-1jxf684 r-chirp r-adyw6z" style="color: ${m.text};">✕</span><h2 id="modal-header" class="css-1jxf684 r-chirp r-adyw6z r-b88u0q" style="margin: 0; flex: 1; color: ${m.text};">${title}</h2>${action ? `<button class="css-175oi2r r-sdzlij" style="background-color: ${m.fill}; padding: 6px 16px; border: 0;"><span class="css-1jxf684 r-chirp r-b88u0q" style="color: ${m.onFill};">${action}</span></button>` : ''}</div>
${body}
</div></div></div></div></div>`;
}
/** Cells of the matrix: a route shell, optionally with a layer open over it. */
export const CELLS = {
home: ['home'], 'home-composer': ['home-composer'], profile: ['profile'], status: ['status'], notifications: ['notifications'], chat: ['chat'],
'creator-studio': ['creator-studio'], articles: ['articles'], settings: ['settings'], explore: ['explore'], bookmarks: ['bookmarks'],
'compose-modal': ['home', 'compose'], 'edit-profile-modal': ['profile', 'edit-profile'], 'more-menu': ['profile', 'more-menu'], 'hover-card': ['notifications', 'hover-card'],
'signed-out-landing': ['signed-out-landing'], 'signed-out-login': ['signed-out-login'],
'chat-group': ['chat-group'], 'chat-direct': ['chat-direct'], 'chat-menu': ['chat-direct-menu'], 'chat-embed': ['chat-embed'],
// Theme v3 cells: each one a live failure on 2026-10-02.
monetization: ['monetization'], 'sidebar-empty': ['sidebar-empty'], 'sidebar-nested': ['sidebar-nested'],
'hover-label': ['status', 'hover-label'], 'account-menu': ['home', 'account-menu'], 'hovercard-bottom': ['notifications', 'hovercard-bottom'],
};
/** Cells with widths of their own: the chat is checked at split-pane widths (one panel, one panel with the nav, both panels). */
export const CELL_WIDTHS = { 'chat-group': [360, 600, 1000, 1400], 'chat-direct': [360, 600, 1000, 1400], 'chat-menu': [600, 1400], 'chat-embed': [360, 1000, 1400] };
/** Routes with no `[data-testid="primaryColumn"]`: X's signed-out experience never shows X's own
* app shell, so these get their own top-level document in `fixtureHtml` instead of `nav()` + a row. */
export const SIGNED_OUT_ROUTES = new Set(['signed-out-landing', 'signed-out-login']);
/**
* X's logged-out flow (read live, 2026-09-28, no sign-in): a separate Tailwind app
* (`.jetfuel-style-root`, `.jf-element`) that paints literal black/white/gray, not X's palette
* variables — the shapes `signedOutCss` in `theme.ts` has to make legible. Colors below are the
* exact computed values captured from `x.com/` and `x.com/i/jf/onboarding/web?mode=login` logged
* out; independent of X's Default/Dim/Lights out mode (X renders this flow by the OS/browser color
* scheme, not by a signed-in preference), so both fixtures use the same literals in every X mode.
*/
function signedOutLanding() {
return `<div class="jetfuel-style-root jf-element" style="min-height: 100vh; display: flex; flex-direction: column;">
<div style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 48px; padding: 32px;">
<svg viewBox="0 0 24 24" width="180" height="180" aria-hidden="true"><path fill="currentColor" d="M3 3h18L13 12l8 9H3l8-9z"/></svg>
<div style="display: flex; flex-direction: column; gap: 16px; width: 320px;">
<p class="jf-element" style="font: 700 31px/36px TwitterChirp, sans-serif; margin: 0;">Happening now.</p>
<p class="jf-element" style="font: 700 23px/28px TwitterChirp, sans-serif; margin: 0;">Join today.</p>
<button class="jf-element" style="display: flex; align-items: center; justify-content: center; gap: 10px; border-radius: 9999px; border: 1px solid rgb(207, 217, 222); background: #ffffff; padding: 10px 16px;"><span style="color: rgb(0, 0, 0);">Continue with Google</span></button>
<button class="jf-element" style="display: flex; align-items: center; justify-content: center; gap: 10px; border-radius: 9999px; border: 1px solid rgb(207, 217, 222); background: #ffffff; padding: 10px 16px;"><span style="color: rgb(248, 250, 252);">Continue with Apple</span></button>
<div style="display: flex; align-items: center; gap: 10px;"><hr style="flex: 1; border-color: rgb(90, 94, 99);"><span class="jf-element" style="color: rgb(113, 113, 122);">or</span><hr style="flex: 1; border-color: rgb(90, 94, 99);"></div>
<button class="jf-element" style="border-radius: 9999px; border: 0; background: rgb(29, 155, 240); color: rgb(255, 255, 255); padding: 12px; font-weight: 700;">Create account</button>
<p class="jf-element" style="color: rgb(128, 128, 128); font-size: 13px; margin: 0;">By signing up, you agree to the <a href="#" style="color: rgb(255, 255, 255);">Terms of Service</a> and <a href="#" style="color: rgb(255, 255, 255);">Privacy Policy</a>, including <a href="#" style="color: rgb(255, 255, 255);">Cookie Use</a>.</p>
<button class="jf-element" style="border-radius: 9999px; border: 1px solid rgb(207, 217, 222); background: transparent; color: rgb(29, 155, 240); padding: 12px; font-weight: 700;">Sign in</button>
</div>
<div class="jf-element" style="display: flex; flex-direction: column; align-items: center; gap: 8px; border-radius: 16px; padding: 16px;">
<svg viewBox="0 0 100 100" width="120" height="120" aria-hidden="true"><rect width="100" height="100" fill="currentColor"/></svg>
<span style="color: rgb(115, 115, 115); font-size: 12px;">Scan to get the app</span>
</div>
</div>
<footer style="display: flex; flex-wrap: wrap; gap: 4px; padding: 16px 32px; font-size: 13px;">
${['About', 'Get App', 'Grok', 'Help', 'Terms', 'Privacy', 'Cookies', 'Careers', 'Ads & Business', 'Developers', 'News', 'Accessibility', '© 2026 X Corp.'].map(t => `<span style="display: flex; align-items: center;"><a href="#" style="color: rgb(113, 117, 122); text-decoration: none;">${t}</a><span style="color: rgb(90, 94, 99); padding: 0 4px;">·</span></span>`).join('')}
</footer>
</div>`;
}
function signedOutLogin() {
// The live Jetfuel login uses a child pill for phone/Apple and a separate visible Google face.
// A flat <button><span> misses the color collision in those nested surfaces.
const socialButton = (label) => `<button class="jf-element" style="display:block; border:0; background:transparent; padding:0; width:100%;"><div class="jf-element flex" style="display:flex; align-items:center; justify-content:center; gap:10px; border-radius:9999px; border:1px solid rgb(207,217,222); background:#ffffff; padding:10px 16px;"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="7" fill="currentColor"/></svg><div class="jf-element text-text"><p class="jf-element" style="color:#000000; margin:0;">Continue with ${label}</p></div></div></button>`;
return `<div class="jetfuel-style-root jf-element" style="min-height: 100vh; display: flex; align-items: center; justify-content: center;">
<div role="dialog" aria-modal="true" class="bg-modal jf-element" style="background: rgb(20, 20, 20); border-radius: 16px; width: min(600px, 92vw); padding: 32px; display: flex; flex-direction: column; gap: 16px;">
<svg viewBox="0 0 24 24" width="32" height="32" aria-hidden="true"><path fill="currentColor" d="M3 3h18L13 12l8 9H3l8-9z"/></svg>
<p class="jf-element" style="color: rgb(255, 255, 255); font: 700 23px/28px TwitterChirp, sans-serif; margin: 0;">See what's happening</p>
<p class="jf-element" style="color: rgb(115, 115, 115); margin: 0;">Select an option below:</p>
${socialButton('phone')}
<div class="jf-element jf-gsi-shell"><div class="jf-element jf-gsi-face" style="display:flex; align-items:center; justify-content:center; border-radius:9999px; border:1px solid rgb(207,217,222); background:#ffffff; padding:10px 16px;"><span class="jf-element" style="color:#000000;"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="7" fill="currentColor"/></svg>Continue with Google</span></div><div class="jf-gsi-hit" style="display:none;"></div></div>
${socialButton('Apple')}
<div style="display: flex; align-items: center; gap: 10px;"><hr style="flex: 1; border-color: rgb(51, 54, 57);"><span class="jf-element" style="color: rgb(113, 113, 122);">or</span><hr style="flex: 1; border-color: rgb(51, 54, 57);"></div>
<label class="jf-element jf-float-label" style="border: 1px solid rgb(51, 54, 57); border-radius: 4px; padding: 10px 12px; display: block;"><span style="color: rgb(230, 233, 234); font-size: 12px;">Email or username</span><input style="display: block; width: 100%; border: 0; background: transparent; color: rgb(255, 255, 255); outline: 0;"></label>
<button class="jf-element" style="border-radius: 9999px; border: 0; background: rgb(51, 54, 57); color: rgb(120, 113, 108); padding: 12px; font-weight: 700;">Continue</button>
<p class="jf-element" style="color: rgb(128, 128, 128); font-size: 13px; margin: 0;">By continuing, you agree to our <a href="#" style="color: rgb(255, 255, 255);">Terms of Service</a>, <a href="#" style="color: rgb(255, 255, 255);">Privacy Policy</a> and <a href="#" style="color: rgb(255, 255, 255);">Cookie Use</a>.</p>
</div>
</div>`;
}
const SIGNED_OUT = { 'signed-out-landing': signedOutLanding, 'signed-out-login': signedOutLogin };
/** A complete x.com-like document for one cell in one X mode. */
export function fixtureHtml(cell, modeName) {
const m = MODES[modeName];
const [route, layer] = CELLS[cell];
if (SIGNED_OUT_ROUTES.has(route)) {
// No nav, no primaryColumn, no #layers portal: X never shows its own app shell signed out.
return `<!doctype html><html lang="en" data-theme="${m.dataTheme}" class="${m.dataTheme}"><head><meta charset="utf-8"><meta name="theme-color" content="${m.theme}"><title>${cell} / X fixture</title>
<style id="x-css">${xCss(m)}</style></head>
<body style="background-color: ${m.page};"><div id="react-root">${SIGNED_OUT[route]()}</div><div id="layers"></div></body></html>`;
}
return `<!doctype html><html lang="en" data-theme="${m.dataTheme}" class="${m.dataTheme}"><head><meta charset="utf-8"><meta name="theme-color" content="${m.theme}"><title>${cell} / X fixture</title>
<style id="x-css">${xCss(m)}</style></head>
<body style="background-color: ${m.page};"><div id="react-root"><div class="css-175oi2r r-13qz1uu" style="min-height: 100vh;"><div class="css-175oi2r r-18u37iz fx-app" style="justify-content: center; min-height: 100vh;">
${nav(m)}${ROUTES[route](m)}
</div></div></div><div id="layers">${layer ? LAYERS[layer](m) : ''}</div>
<script>
// react-native-web swaps atomic classes at X's breakpoints; the fixture does the same.
(function () {
function layout() {
// x.com: nav 275px + row 1050 from 1330px, row 990 from 1265px, then an 88px icon nav with a
// 990 / 920 row, the sidebar hidden below 1010px, and the bottom tab bar below 700px.
var w = window.innerWidth, wide = w >= 1265;
var header = document.querySelector('header[role="banner"]'), navInner = document.querySelector('.fx-nav');
var side = document.querySelector('.fx-sidebar');
header.style.display = w < 700 ? 'none' : '';
navInner.classList.toggle('r-nav275', wide); navInner.classList.toggle('r-nav88', !wide);
// Read live 2026-10-02: below 1265px X renders each rail item without its label element (the icon is
// the pill's only child), not with a hidden label.
window.__fxLabels = window.__fxLabels || [...document.querySelectorAll('.fx-label-box')].map(function (box) { return [box, box.parentElement]; });
window.__fxLabels.forEach(function (pair) { if (wide && !pair[0].isConnected) pair[1].appendChild(pair[0]); if (!wide && pair[0].isConnected) pair[0].remove(); });
var post = document.querySelector('.fx-post span'); if (post) post.textContent = wide ? 'Post' : '+';
document.querySelectorAll('.fx-row').forEach(function (row) {
['r-113js5t', 'r-row990', 'r-row920', 'r-row600', 'r-13qz1uu'].forEach(function (c) { row.classList.remove(c); });
row.classList.add(w >= 1330 ? 'r-113js5t' : w >= 1080 ? 'r-row990' : w >= 1010 ? 'r-row920' : w >= 700 ? 'r-row600' : 'r-13qz1uu');
});
if (side) {
side.style.display = w >= 1010 ? '' : 'none';
side.classList.toggle('r-1hycxz', w >= 1080); side.classList.toggle('r-side290', w < 1080);
side.classList.toggle('r-10f7w94', w >= 1330);
}
}
layout(); window.addEventListener('resize', layout);
// Layers placed the way X places them: from the anchor's rect after the theme moved it (account menu), or
// at the bottom edge (hover card). Re-run on resize and while a theme settles.
function place() {
document.querySelectorAll('[data-fx-place]').forEach(function (layer) {
var h = layer.getBoundingClientRect().height;
if (layer.dataset.fxPlace === 'bottom') { layer.style.top = (window.innerHeight - 140) + 'px'; return; }
var a = document.querySelector('[data-testid="' + layer.dataset.fxAnchor + '"]'); if (!a) return;
var r = a.getBoundingClientRect();
layer.style.left = r.left + 'px'; layer.style.top = Math.max(0, r.top - h - 8) + 'px';
});
}
place(); window.addEventListener('resize', place); setInterval(place, 100);
})();
${EMBED_SCRIPT}
${CHAT_SCRIPT}</script></body></html>`;
}
export const MODE_NAMES = Object.keys(MODES);