234 lines
28 KiB
JavaScript
234 lines
28 KiB
JavaScript
// X-shaped composer pages for the Emoji Picker check (scripts/emoji-picker-verify.mjs): the home
|
|
// composer at the top of the timeline, the inline reply box under a post on a post page, and the
|
|
// modal composer of /compose/post (an [aria-modal] dialog in X's #layers, full screen below
|
|
// 500 px), served with X's real Content-Security-Policy (X_CSP_DIRECTIVES).
|
|
// They reproduce the parts of x.com that decide where a popup goes and how a toolbar
|
|
// button must look: the left nav (275 px) and right column (350 px) at desktop widths, the 600 px
|
|
// primary column, X's composer toolbar (a role=tablist of 34 px round buttons with 20 px accent
|
|
// icons, the first one hanging 8 px left of the text column, X's "Add emoji" button fourth-last),
|
|
// the Post/Reply pill on the right, a Draft.js editor. The icons are simple line drawings of our
|
|
// own, not X's artwork. Passing here is not a live X test.
|
|
|
|
/**
|
|
* X's Content-Security-Policy for a signed-in x.com page, as read live from the app on 2026-09-29
|
|
* (the response header of https://x.com/home; report-uri shortened). The fixture pages are served
|
|
* with it, so anything our runtime or a plugin does that X's page would refuse (an inline <script>
|
|
* or eval, a font from a data:/blob: URL, a worker from a disallowed source, a style X's
|
|
* style-src would block) fails here as it would on x.com. `nonce` is the fixture's own script nonce.
|
|
*/
|
|
export const X_CSP_DIRECTIVES = [
|
|
"connect-src 'self' blob: https://fonts.googleapis.com/css https://mapsresources-pa.googleapis.com https://maps.googleapis.com https://translate.googleapis.com https://www.gstatic.com/maps/ https://*.pscp.tv https://*.twimg.com https://*.video.pscp.tv https://*.watch.x.com https://aa.twitter.com https://aa.x.com https://accounts.google.com/gsi/ https://ads-api.twitter.com https://ads-api.x.com https://api-stream.twitter.com https://api-stream.x.com https://api.twitter.com https://api.x.ai https://api.x.com https://api.x.com https://caps.twitter.com https://caps.x.com https://grok.x.com https://jf.twitter.com https://jf.x.com https://jf-t.x.com https://pay.twitter.com https://pay.x.com https://sentry.io https://*.sentry.io https://ton-staging.atla.twitter.com https://ton-staging.atla.x.com https://ton-staging.pdxa.twitter.com https://ton-staging.pdxa.x.com https://ton.twitter.com https://ton.local.twitter.com https://ton.x.com https://twitter.com https://upload.twitter.com https://upload.x.com https://upload-a.x.com https://upload-b.x.com https://www.google-analytics.com https://x.com https://grok-api.gcp.mouseion.dev https://assets.mouseion.dev https://grok.com https://assets.grok.com https://imagine-public.x.ai/ wss://grok.com wss://grok-api.gcp.mouseion.dev wss://*.cursorvm.com https://storage.googleapis.com/xai-enterprise-api-mc-assets-prod/ https://*.adtrafficquality.google https://*.googlesyndication.com https://*.doubleclick.net https://adservice.google.com https://www.googleadservices.com https://pagead2.googlesyndication.com https://www.google.com https://google.com https://via.intercom.io https://api.intercom.io https://api.au.intercom.io https://api.eu.intercom.io https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io https://api-ping.intercom.io https://nexus-websocket-a.intercom.io wss://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-europe-websocket.intercom.io wss://nexus-europe-websocket.intercom.io https://nexus-australia-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://uploads.intercomcdn.com https://uploads.intercomcdn.eu https://uploads.au.intercomcdn.com https://uploads.eu.intercomcdn.com https://uploads.intercomusercontent.com https://livekit.grok.com wss://livekit.grok.com https://production.plaid.com/ https://sandbox.plaid.com/ https://ingestion.dv.socure.io https://network.dv.socure.io/ https://analytics.dv.socure.io/ https://payments-dev.x.com/customer/wasm/forward-with-v1.wasm https://payments-staging.x.com/customer/wasm/forward-with-v1.wasm https://payments-prod.x.com/customer/wasm/forward-with-v1.wasm https://money-dev.x.com/customer/wasm/forward-with-v1.wasm https://money-staging.x.com/customer/wasm/forward-with-v1.wasm https://money.x.com/customer/wasm/forward-with-v1.wasm https://payments-dev.x.com/customer/api/device-signal https://payments-staging.x.com/customer/api/device-signal https://payments-prod.x.com/customer/api/device-signal https://money-dev.x.com/customer/api/device-signal https://money-staging.x.com/customer/api/device-signal https://money.x.com/customer/api/device-signal https://api.stripe.com https://api.braintreegateway.com https://api.sandbox.braintreegateway.com https://client-analytics.braintreegateway.com https://*.braintree-api.com https://www.paypal.com https://www.sandbox.paypal.com https://www.paypalobjects.com https://cognito-identity.us-east-1.amazonaws.com https://streaming-rekognition.us-east-1.amazonaws.com wss://streaming-rekognition.us-east-1.amazonaws.com https://cdn.liveness.rekognition.amazonaws.com https://m.castle.io https://edge.prelude.dev https://checkoutshopper-live.adyen.com wss://*.pscp.tv https://vmap.grabyo.com https://dwo3ckksxlb0v.cloudfront.net https://media.riffsy.com https://*.giphy.com https://media.tenor.com https://c.tenor.com wss://chat-ws.x.com https://d1muhwhmpsz4u8.cloudfront.net/ https://d2bchqfeno8n2m.cloudfront.net/ https://d2shtph9y6bxk.cloudfront.net/ https://xchat-hsm-staging.x.com/ https://realm-a.x.com https://realm-b.x.com https://realm-west1.x.com https://realm-east1.x.com https://hsm-staging.x.com https://ads-twitter.com https://analytics.twitter.com https://analytics.x.com https://call.x.com",
|
|
"default-src 'self'",
|
|
"form-action 'self' https://twitter.com https://*.twitter.com https://x.com https://*.x.com https://localhost.twitter.com:3443 https://localhost.x.com:3443 https://accounts.x.ai https://grok.com https://*.paypal.com https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io",
|
|
"font-src 'self' https://*.twimg.com https://js.intercomcdn.com https://fonts.intercomcdn.com",
|
|
"frame-src 'self' https://accounts.google.com/ https://accounts.google.com/gsi/ https://cards-frame.twitter.com https://cdn.plaid.com/ https://client-api.arkoselabs.com/ https://content.googleapis.com/ https://iframe.arkoselabs.com/ https://mobile.twitter.com https://mobile.x.com https://pay.twitter.com https://pay.x.com https://google.com https://www.google.com https://intercom-sheets.com https://www.intercom-reporting.com https://www.youtube.com https://player.vimeo.com https://fast.wistia.net https://console.googletagservices.com https://*.doubleclick.net https://*.adtrafficquality.google https://*.safeframe.googlesyndication.com https://www.googleadservices.com https://googleadservices.com https://adservice.google.com https://*.googlesyndication.com https://td.doubleclick.net https://payments-dev.x.com/ https://payments-staging.x.com/ https://payments-prod.x.com/ https://sdn.payments-dev.x.com/ https://sdn.payments-staging.x.com/ https://sdn.payments-prod.x.com/ https://money-dev.x.com/ https://money-staging.x.com/ https://money.x.com/ https://sdn.money-dev.x.com/ https://sdn.money-staging.x.com/ https://sdn.money.x.com/ https://p2pcreditcardiframesandbox.crbcos.com https://p2pcreditcardiframe.crbcos.com https://verify-sandbox.plaid.com/ https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://cdn.getpinwheel.com/ https://artifacts.grokusercontent.com https://twitter.com https://x.com https://studio.x.com https://studio-dev.x.com https://recaptcha.net/recaptcha/",
|
|
"img-src 'self' blob: data: https://assets.braintreegateway.com https://checkout.paypal.com https://www.paypalobjects.com https://www.google.com/maps/place/ https://imgs.search.brave.com https://*.cdn.twitter.com https://*.cdn.x.com https://ton.twitter.com https://ton.x.com https://*.twimg.com https://analytics.twitter.com https://analytics.x.com https://cm.g.doubleclick.net https://www.google-analytics.com https://maps.googleapis.com https://www.periscope.tv https://www.pscp.tv https://ads-twitter.com https://ads-api.twitter.com https://ads-api.x.com https://api.x.com https://developer.x.com blob: data: https://js.intercomcdn.com https://static.intercomassets.com https://downloads.intercomcdn.com https://downloads.intercomcdn.eu https://downloads.au.intercomcdn.com https://uploads.intercomusercontent.com https://gifs.intercomcdn.com https://video-messages.intercomcdn.com https://messenger-apps.intercom.io https://messenger-apps.eu.intercom.io https://messenger-apps.au.intercom.io https://*.intercom-attachments-1.com https://*.intercom-attachments.eu https://*.au.intercom-attachments.com https://*.intercom-attachments-2.com https://*.intercom-attachments-3.com https://*.intercom-attachments-4.com https://*.intercom-attachments-5.com https://*.intercom-attachments-6.com https://*.intercom-attachments-7.com https://*.intercom-attachments-8.com https://*.intercom-attachments-9.com https://static.intercomassets.eu https://static.au.intercomassets.com https://media.riffsy.com https://*.giphy.com https://media.tenor.com https://c.tenor.com https://*.pscp.tv https://*.periscope.tv https://prod-periscope-profile.s3-us-west-2.amazonaws.com https://platform-lookaside.fbsbx.com https://scontent.xx.fbcdn.net https://scontent-sea1-1.xx.fbcdn.net https://*.googleusercontent.com https://t.co/1/i/adsct https://*.googleusercontent.com https://*.gstatic.com https://*.googlesyndication.com https://*.adtrafficquality.google https://www.google.com/ads/measurement/ https://*.google.com/ads/measurement/ https://googleads.g.doubleclick.net https://google.com https://www.google.com https://plaid-merchant-logos.plaid.com https://plaid-counterparty-logos.plaid.com https://client-logos.plaid.com https://assets.mouseion.dev https://assets.grok.com https://computer-hub.grok.com https://*.grok.me https://serpapi.com",
|
|
"manifest-src 'self'",
|
|
"media-src 'self' data: blob: https://twitter.com https://x.com https://*.twimg.com https://*.vine.co https://*.pscp.tv https://*.video.pscp.tv https://*.watch.x.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://downloads.intercomcdn.eu https://downloads.au.intercomcdn.com https://dwo3ckksxlb0v.cloudfront.net",
|
|
"object-src 'none'",
|
|
"script-src 'self' 'unsafe-inline' https://*.twimg.com https://recaptcha.net/recaptcha/ http://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js https://accounts.google.com/gsi/client https://apis.google.com/js/api.js https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js https://client-api.arkoselabs.com/ https://static.ads-twitter.com https://twitter.com https://www.google-analytics.com https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js https://x.com https://sdn.payments-dev.x.com/assets/loader.min.js https://sdn.payments-staging.x.com/assets/loader.min.js https://sdn.payments-prod.x.com/assets/loader.min.js https://sdn.money-dev.x.com/assets/loader.min.js https://sdn.money-staging.x.com/assets/loader.min.js https://sdn.money.x.com/assets/loader.min.js https://sdk.dv.socure.io/latest/device-risk-sdk.js https://cdn.plaid.com/link/v2/stable/link-initialize.js https://payments-dev.x.com/customer/wasm/xxp-forward-with-sdk.js https://payments-staging.x.com/customer/wasm/xxp-forward-with-sdk.js https://payments-prod.x.com/customer/wasm/xxp-forward-with-sdk.js https://money-dev.x.com/customer/wasm/xxp-forward-with-sdk.js https://money-staging.x.com/customer/wasm/xxp-forward-with-sdk.js https://money.x.com/customer/wasm/xxp-forward-with-sdk.js https://js.stripe.com https://*.js.stripe.com https://js.braintreegateway.com https://assets.braintreegateway.com https://www.paypalobjects.com https://*.paypal.com https://cdn.getpinwheel.com/pinwheel-v3.2.1.js https://securepubads.g.doubleclick.net https://www.googletagservices.com https://*.googletagservices.com https://pagead2.googlesyndication.com https://adservice.google.com https://www.googleadservices.com https://ads.google.com https://tpc.googlesyndication.com https://*.tpc.googlesyndication.com https://googleads.g.doubleclick.net https://app.intercom.io https://widget.intercom.io https://js.intercomcdn.com 'wasm-unsafe-eval' 'nonce-${nonce}'",
|
|
"style-src 'self' 'unsafe-inline' https://accounts.google.com/gsi/style https://*.twimg.com",
|
|
"child-src 'self' blob:",
|
|
"worker-src 'self' blob: https://*.twimg.com",
|
|
"report-uri https://x.com/i/csp_report"
|
|
];
|
|
export const X_SCRIPT_NONCE = 'd2F5QmV0dGVyLWZpeHR1cmU=';
|
|
export const xCsp = (nonce = X_SCRIPT_NONCE) => X_CSP_DIRECTIVES.join('; ').replace('${nonce}', nonce);
|
|
|
|
export const PALETTES = {
|
|
light: { bg: '#ffffff', text: 'rgb(15, 20, 25)', muted: 'rgb(83, 100, 113)', border: 'rgb(239, 243, 244)', accent: 'rgb(29, 155, 240)', hover: 'rgba(29, 155, 240, 0.1)', post: 'rgb(15, 20, 25)', onPost: '#ffffff', mask: 'rgba(0, 0, 0, 0.4)' },
|
|
dark: { bg: '#000000', text: 'rgb(231, 233, 234)', muted: 'rgb(113, 118, 123)', border: 'rgb(47, 51, 54)', accent: 'rgb(29, 155, 240)', hover: 'rgba(29, 155, 240, 0.1)', post: 'rgb(239, 243, 244)', onPost: 'rgb(15, 20, 25)', mask: 'rgba(91, 112, 131, 0.4)' },
|
|
};
|
|
|
|
const I = {
|
|
media: '<rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.8"/><path d="m4 17.5 5-4.5 3.5 3 3-2.5 4.5 4"/>',
|
|
gif: '<rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><path d="M10.2 10.2a2 2 0 1 0 0 3.6V12H9M12.5 10v4M15 14v-4h2.2M15 12h1.8"/>',
|
|
grok: '<path d="M5 19 19 5M8.5 5.5a6.8 6.8 0 0 1 10 8.9M15.5 18.5a6.8 6.8 0 0 1-10-8.9"/>',
|
|
poll: '<path d="M4.5 6h4M4.5 12h9M4.5 18h6.5"/><path d="M12 6h7.5M17 12h2.5M14.5 18h5"/>',
|
|
emoji: '<circle cx="12" cy="12" r="8.5"/><path d="M8.6 14.2a4.3 4.3 0 0 0 6.8 0"/><path d="M9.3 9.6v.8M14.7 9.6v.8"/>',
|
|
schedule: '<rect x="3.5" y="5" width="17" height="15" rx="2.5"/><path d="M3.5 9.5h17M8 3v4M16 3v4M12 12.5v3l2 1.2"/>',
|
|
location: '<path d="M12 21s-6.5-5.7-6.5-11a6.5 6.5 0 0 1 13 0c0 5.3-6.5 11-6.5 11Z"/><circle cx="12" cy="10" r="2.3"/>',
|
|
globe: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.4 2.6 3.5 5.4 3.5 8.5s-1.1 5.9-3.5 8.5c-2.4-2.6-3.5-5.4-3.5-8.5S9.6 6.1 12 3.5Z"/>',
|
|
back: '<path d="M20 12H5M11 5l-7 7 7 7"/>',
|
|
close: '<path d="M6 6l12 12M18 6 6 18"/>',
|
|
reply: '<path d="M4.5 11.5a7 7 0 0 1 7-7h1a7 7 0 0 1 0 14H9l-4 3v-4.3a7 7 0 0 1-.5-5.7Z"/>',
|
|
repost: '<path d="M7 17V8.5a2 2 0 0 1 2-2h8M14 3.5l3 3-3 3M17 7v8.5a2 2 0 0 1-2 2H7M10 20.5l-3-3 3-3"/>',
|
|
like: '<path d="M12 19.5s-7.5-4.4-7.5-9.7A4.3 4.3 0 0 1 12 7.2a4.3 4.3 0 0 1 7.5 2.6c0 5.3-7.5 9.7-7.5 9.7Z"/>',
|
|
views: '<path d="M5 20V12M10 20V5M15 20v-9M20 20v-5"/>',
|
|
home: '<path d="M4 10.5 12 4l8 6.5V20h-5.5v-6h-5v6H4Z"/>',
|
|
search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 4.5 4.5"/>',
|
|
bell: '<path d="M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 2h-15ZM10 20.5h4"/>',
|
|
mail: '<rect x="3.5" y="5.5" width="17" height="13" rx="2"/><path d="m4 7 8 6 8-6"/>',
|
|
person: '<circle cx="12" cy="8" r="3.8"/><path d="M4.5 20.5a7.5 7.5 0 0 1 15 0"/>',
|
|
logo: '<path d="M4 4l16 16M20 4 4 20"/>',
|
|
};
|
|
const svg = (d, size = 20, extra = '') => `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" ${extra}>${d}</svg>`;
|
|
|
|
function toolbar(buttonLabel) {
|
|
const buttons = [
|
|
['Add photos or video', I.media, ''], ['Add a GIF', I.gif, 'gifSearchButton'], ['Enhance your post with Grok', I.grok, ''],
|
|
['Add poll', I.poll, 'createPollButton'], ['Add emoji', I.emoji, ''], ['Schedule post', I.schedule, 'scheduleOption'], ['Tag location', I.location, 'geoButton'],
|
|
];
|
|
return `<div class="tb-row">
|
|
<div data-testid="toolBar" class="tb"><nav role="navigation"><div role="tablist" data-testid="ScrollSnap-List" class="tb-list">
|
|
${buttons.map(([label, icon, id]) => `<div role="presentation" class="tb-item"><button type="button" role="button" aria-label="${label}"${id ? ` data-testid="${id}"` : ''} class="tb-btn"${label === 'Tag location' ? ' disabled' : ''}><div class="tb-ink">${svg(icon)}</div></button></div>`).join('')}
|
|
</div></nav></div>
|
|
<button type="button" class="post-btn" data-testid="tweetButtonInline" disabled>${buttonLabel}</button>
|
|
</div>`;
|
|
}
|
|
|
|
function editor(id, placeholder, minHeight) {
|
|
return `<div class="DraftEditor-root" style="min-height:${minHeight}px"><div class="public-DraftEditorPlaceholder-root">${placeholder}</div><div class="public-DraftEditor-content" contenteditable="true" role="textbox" aria-multiline="true" data-testid="tweetTextarea_${id}" aria-label="${placeholder}" spellcheck="true"><div data-contents="true"><div data-block="true"><div class="public-DraftStyleDefault-block"><span><br data-text="true"></span></div></div></div></div></div>`;
|
|
}
|
|
|
|
function post(name, handle, text, extra = '') {
|
|
return `<article data-testid="tweet" class="post"><div class="avatar a2"></div><div class="col">
|
|
<div class="who"><b>${name}</b> <span class="muted">@${handle} · 2h</span></div>
|
|
<div class="text">${text}</div>${extra}
|
|
<div role="group" class="actions">${[I.reply, I.repost, I.like, I.views].map((d, i) => `<span>${svg(d, 18)}<em>${[12, 48, 310, '12K'][i]}</em></span>`).join('')}</div>
|
|
</div></article>`;
|
|
}
|
|
|
|
const FEED = [
|
|
['Design Notes', 'designnotes', 'The best interfaces feel like they were always there. Nobody notices the picker; they notice the emoji they wanted was one click away.'],
|
|
['Ada', 'ada_builds', 'Shipping a small thing today. Feels good to finish something.'],
|
|
['Weather Bot', 'wxbot', 'Clear skies tonight with a light breeze from the west. Low of 14°C.'],
|
|
['Kai', 'kai', 'Coffee first, then the inbox. Never the other way round.'],
|
|
['Mira', 'mira_codes', 'Hot take: keyboard shortcuts are an accessibility feature, not a power-user feature.'],
|
|
['Design Notes', 'designnotes', 'Spacing on a 4 px grid is not a rule, it is a rhythm. You hear it when it is off.'],
|
|
];
|
|
|
|
const CSS = p => `
|
|
:root { --x-bg: ${p.bg}; --x-text: ${p.text}; --x-muted: ${p.muted}; --x-border: ${p.border}; --x-accent: ${p.accent}; }
|
|
html { font-size: 15px; }
|
|
html, body { margin: 0; }
|
|
body { font-family: TwitterChirp, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 20px; overflow-y: scroll; }
|
|
button { font: inherit; }
|
|
.muted { color: var(--x-muted); }
|
|
#react-root { display: flex; justify-content: center; min-height: 100vh; }
|
|
.nav { position: sticky; top: 0; align-self: flex-start; height: 100vh; width: 275px; box-sizing: border-box; padding: 0 12px 0 8px; display: flex; flex-direction: column; gap: 4px; }
|
|
.nav a { display: flex; align-items: center; gap: 20px; height: 50px; padding: 0 12px; border-radius: 9999px; font-size: 20px; text-decoration: none; color: inherit; }
|
|
.nav a svg { width: 26px; height: 26px; }
|
|
.nav .logo { width: 50px; justify-content: center; padding: 0; }
|
|
.nav .logo svg { width: 28px; height: 28px; stroke-width: 2.4; }
|
|
.nav .cta { margin-top: 16px; height: 52px; border-radius: 9999px; background: ${p.post}; color: ${p.onPost}; font-weight: 700; font-size: 17px; display: grid; place-items: center; width: 234px; }
|
|
main[role="main"] { display: flex; }
|
|
[data-testid="primaryColumn"] { width: 600px; max-width: 100vw; box-sizing: border-box; min-height: 100vh; border-left: 1px solid var(--x-border); border-right: 1px solid var(--x-border); }
|
|
[data-testid="sidebarColumn"] { width: 350px; margin: 0 0 0 30px; padding-top: 6px; }
|
|
.side-search { height: 42px; border-radius: 9999px; border: 1px solid var(--x-border); display: flex; align-items: center; gap: 12px; padding: 0 16px; color: var(--x-muted); }
|
|
.side-card { margin-top: 16px; border: 1px solid var(--x-border); border-radius: 16px; padding: 12px 16px; }
|
|
.side-card h2 { margin: 0 0 12px; font-size: 20px; line-height: 24px; font-weight: 800; }
|
|
.side-card p { margin: 0 0 14px; color: var(--x-muted); font-size: 14px; }
|
|
.topbar { position: sticky; top: 0; z-index: 3; background: color-mix(in srgb, var(--x-bg) 85%, transparent); backdrop-filter: blur(12px); border-bottom: 1px solid var(--x-border); }
|
|
.tabs { display: flex; height: 53px; }
|
|
.tabs div { flex: 1; display: grid; place-items: center; font-weight: 500; color: var(--x-muted); position: relative; }
|
|
.tabs div.on { color: var(--x-text); font-weight: 700; }
|
|
.tabs div.on::after { content: ''; position: absolute; bottom: 0; width: 56px; height: 4px; border-radius: 9999px; background: var(--x-accent); }
|
|
.head { display: flex; align-items: center; gap: 32px; height: 53px; padding: 0 16px; font-size: 20px; font-weight: 700; }
|
|
.composer { display: flex; gap: 8px; padding: 12px 16px 8px; border-bottom: 1px solid var(--x-border); }
|
|
.avatar { flex: none; width: 40px; height: 40px; border-radius: 9999px; background: linear-gradient(135deg, #7856ff, #1d9bf0); }
|
|
.avatar.a2 { background: linear-gradient(135deg, #f91880, #ffd400); }
|
|
.col { flex: 1; min-width: 0; padding-top: 4px; }
|
|
.DraftEditor-root { position: relative; font-size: 20px; line-height: 24px; padding: 8px 0 4px; }
|
|
.public-DraftEditorPlaceholder-root { position: absolute; top: 8px; left: 0; color: var(--x-muted); pointer-events: none; }
|
|
.DraftEditor-root.has-text .public-DraftEditorPlaceholder-root { display: none; }
|
|
.public-DraftEditor-content { outline: none; white-space: pre-wrap; overflow-wrap: break-word; min-height: 24px; }
|
|
.reply-who { font-size: 15px; color: var(--x-muted); margin: 0 0 4px; }
|
|
.reply-who b { color: var(--wb-accent, var(--x-accent)); font-weight: 400; }
|
|
.audience { display: flex; align-items: center; gap: 4px; height: 36px; margin-bottom: 4px; padding-bottom: 8px; border-bottom: 1px solid var(--x-border); color: var(--wb-accent, var(--x-accent)); font-weight: 700; font-size: 14px; }
|
|
.audience svg { width: 16px; height: 16px; }
|
|
.tb-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; min-height: 48px; }
|
|
.tb { display: flex; flex-direction: row; margin-left: -8px; min-width: 0; }
|
|
.tb-list { display: flex; flex-direction: row; align-items: center; }
|
|
.tb-item { display: flex; }
|
|
.tb-btn { all: unset; box-sizing: border-box; width: 34px; height: 34px; display: grid; place-items: center; border-radius: 9999px; cursor: pointer; transition: background-color .2s; }
|
|
.tb-btn:hover { background: ${p.hover}; }
|
|
.tb-btn[disabled] { opacity: .5; cursor: default; }
|
|
.tb-ink { display: grid; place-items: center; color: var(--wb-accent, var(--x-accent)); }
|
|
.tb-ink svg { width: 20px; height: 20px; }
|
|
.post-btn { flex: none; height: 36px; min-width: 36px; padding: 0 16px; border: 0; border-radius: 9999px; background: ${p.post}; color: ${p.onPost}; font-weight: 700; font-size: 15px; }
|
|
.post-btn[disabled] { opacity: .5; }
|
|
.post { display: flex; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--x-border); }
|
|
.who { font-size: 15px; } .text { margin-top: 2px; }
|
|
.actions { display: flex; justify-content: space-between; max-width: 425px; margin-top: 12px; color: var(--x-muted); font-size: 13px; }
|
|
.actions span { display: flex; align-items: center; gap: 4px; } .actions em { font-style: normal; }
|
|
.focal { padding: 12px 16px 0; border-bottom: 1px solid var(--x-border); }
|
|
.focal .row { display: flex; gap: 8px; align-items: center; }
|
|
.focal .text { font-size: 17px; line-height: 24px; margin: 12px 0; }
|
|
.focal .meta { color: var(--x-muted); padding: 0 0 16px; border-bottom: 1px solid var(--x-border); }
|
|
.focal .actions { max-width: none; margin: 0; height: 48px; align-items: center; }
|
|
#layers { position: fixed; inset: 0; z-index: 10; }
|
|
.mask { position: absolute; inset: 0; background: ${p.mask}; }
|
|
[role="dialog"][aria-modal="true"] { position: absolute; left: 50%; top: 5vh; transform: translateX(-50%); width: 600px; max-width: calc(100vw - 32px); background: var(--wb-bg, var(--x-bg)); border-radius: 16px; overflow: hidden; }
|
|
.dlg-head { display: flex; align-items: center; justify-content: space-between; height: 53px; padding: 0 16px 0 8px; }
|
|
.dlg-head button { all: unset; width: 34px; height: 34px; display: grid; place-items: center; border-radius: 9999px; cursor: pointer; }
|
|
.dlg-head .drafts { color: var(--wb-accent, var(--x-accent)); font-weight: 700; font-size: 14px; }
|
|
[role="dialog"] .composer { border-bottom: 0; padding-top: 0; }
|
|
@media (max-width: 1265px) { .nav { width: 88px; } .nav a span, .nav .cta { display: none; } }
|
|
@media (max-width: 1000px) { [data-testid="sidebarColumn"] { display: none; } }
|
|
@media (max-width: 500px) {
|
|
.nav { display: none; }
|
|
[data-testid="primaryColumn"] { border: 0; width: 100vw; }
|
|
[role="dialog"][aria-modal="true"] { top: 0; left: 0; transform: none; width: 100vw; max-width: none; height: 100vh; border-radius: 0; }
|
|
}
|
|
`;
|
|
|
|
const NAV = `<header role="banner" class="nav">
|
|
<a class="logo" href="/home">${svg(I.logo)}</a>
|
|
${[['Home', I.home], ['Explore', I.search], ['Notifications', I.bell], ['Messages', I.mail], ['Profile', I.person]].map(([t, d]) => `<a href="#">${svg(d)}<span>${t}</span></a>`).join('')}
|
|
<div class="cta">Post</div>
|
|
</header>`;
|
|
const SIDEBAR = `<div data-testid="sidebarColumn"><div class="side-search">${svg(I.search, 18)}Search</div>
|
|
<div class="side-card"><h2>What’s happening</h2>${'<p>Trending in Technology<br><b style="color:var(--x-text)">#DesignSystems</b></p>'.repeat(3)}</div></div>`;
|
|
|
|
const homeColumn = `<div class="topbar"><div class="tabs"><div class="on">For you</div><div>Following</div></div></div>
|
|
<div class="composer" data-composer="home"><div class="avatar"></div><div class="col">
|
|
${editor('0', 'What’s happening?', 52)}
|
|
<div class="audience">${svg(I.globe)}Everyone can reply</div>
|
|
${toolbar('Post')}
|
|
</div></div>
|
|
${FEED.map(f => post(...f)).join('')}${FEED.map(f => post(...f)).join('')}`;
|
|
|
|
const replyColumn = `<div class="topbar"><div class="head">${svg(I.back)}Post</div></div>
|
|
<div class="focal"><div class="row"><div class="avatar a2"></div><div><b>Design Notes</b><div class="muted">@designnotes</div></div></div>
|
|
<div class="text">What makes an emoji picker feel native? Instant open, a grid you can scan, search that finds “joy” when you type “laugh”, and it never covers what you are writing.</div>
|
|
<div class="meta">10:24 AM · Sep 29, 2026 · <b style="color:var(--x-text)">48.2K</b> Views</div>
|
|
<div role="group" class="actions">${[I.reply, I.repost, I.like, I.views].map(d => `<span>${svg(d, 22)}</span>`).join('')}</div>
|
|
</div>
|
|
<div class="composer" data-composer="reply"><div class="avatar"></div><div class="col">
|
|
<div class="reply-who">Replying to <b>@designnotes</b></div>
|
|
${editor('1', 'Post your reply', 24)}
|
|
${toolbar('Reply')}
|
|
</div></div>
|
|
${FEED.slice(1).map(f => post(...f)).join('')}`;
|
|
|
|
const modalLayer = `<div id="layers"><div class="mask"></div>
|
|
<div role="dialog" aria-modal="true" aria-labelledby="modal-header">
|
|
<div class="dlg-head"><button type="button" aria-label="Close">${svg(I.close)}</button><span class="drafts">Drafts</span></div>
|
|
<div class="composer" data-composer="modal"><div class="avatar"></div><div class="col">
|
|
${editor('0', 'What’s happening?', 96)}
|
|
<div class="audience">${svg(I.globe)}Everyone can reply</div>
|
|
${toolbar('Post')}
|
|
</div></div>
|
|
</div></div>`;
|
|
|
|
/** The fixture page for `kind` ('home' | 'reply' | 'modal') in X's `mode` ('light' | 'dark'). */
|
|
export function composerPage(kind, mode) {
|
|
const p = PALETTES[mode];
|
|
const column = kind === 'reply' ? replyColumn : homeColumn;
|
|
return `<!doctype html><html><head><meta charset="utf-8"><title>X fixture</title><style>${CSS(p)}</style></head>
|
|
<body style="background-color: ${p.bg}; color: ${p.text}">
|
|
<div id="react-root">${NAV}<main role="main"><div data-testid="primaryColumn">${column}</div>${SIDEBAR}</main></div>
|
|
${kind === 'modal' ? modalLayer : ''}
|
|
<script nonce="${X_SCRIPT_NONCE}">
|
|
window.__xKeys = []; document.addEventListener('keydown', e => window.__xKeys.push(e.key));
|
|
window.__xEmojiClicks = 0;
|
|
document.addEventListener('click', e => { if (e.target.closest && e.target.closest('[aria-label="Add emoji"]')) window.__xEmojiClicks += 1; });
|
|
document.addEventListener('input', e => { const root = e.target.closest && e.target.closest('.DraftEditor-root'); if (root) root.classList.toggle('has-text', !!e.target.textContent.trim()); }, true);
|
|
const sync = () => { for (const ed of document.querySelectorAll('.public-DraftEditor-content')) ed.closest('.DraftEditor-root').classList.toggle('has-text', !!ed.textContent.trim()); };
|
|
new MutationObserver(sync).observe(document.body, { subtree: true, childList: true, characterData: true });
|
|
</script>
|
|
</body></html>`;
|
|
}
|
|
|
|
export const PATHS = { home: '/home', reply: '/designnotes/status/1840000000000000001', modal: '/compose/post' };
|