wayBetter for X

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

View file

@ -0,0 +1,149 @@
// X's parts for theme engine v2: which of X's elements each generic component is (framework
// themeComponents.ts), where decoration goes, and how X draws each layout variant. Every selector
// is anchored on a test id, a role or an attribute; a generic tag only ends a selector as a text leaf
// under such an anchor. `seen` notes the day a selector was read on live x.com (Lights out, the two
// test accounts); the fixtures in scripts/theme-matrix carry the same markup.
import type { PartMap, RoleColors } from '../../../../waybetter/packages/runtime/src/themeComponents.ts';
import { cssString, type DecorTargets } from '../../../../waybetter/packages/runtime/src/themeDecor.ts';
import type { VariantSkins } from '../../../../waybetter/packages/runtime/src/themeVariants.ts';
const POST = 'article[data-testid="tweet"]';
const COLUMN = '[data-testid="primaryColumn"]';
/** The main column on every route but chat (a drawn bar above X's chat app would push its composer out of the window). */
/** X's chat app: in the page, or the host of its embed (an open shadow root the page's :has() cannot look into, 2026-10-01). */
const CHAT_APP = ':is([data-testid="dm-container"],[data-testid="xchatEmbedRoute"])';
const FRAMED = `[data-testid="primaryColumn"]:not(:has(${CHAT_APP}))`;
const RAIL = 'header[role="banner"]';
const NAV = `${RAIL} nav[role="navigation"]`;
const ACTIONS = ['reply', 'retweet', 'unretweet', 'like', 'unlike', 'bookmark', 'removeBookmark'].map(id => `[data-testid="${id}"]`).join(',');
const AVATAR = '[data-testid^="UserAvatar-Container-"]';
/** Read live 2026-09-30 unless marked otherwise. */
export const X_PARTS: PartMap = {
nav: { sel: NAV },
navItem: { sel: `${NAV} > :is(a,button) > div`, text: `${NAV} > :is(a,button) > div :is(span,svg)` },
topBar: { sel: `${COLUMN} > div > div:first-child` },
tabs: { sel: `${COLUMN} [role="tablist"]` },
tab: { sel: `${COLUMN} [role="tablist"] [role="tab"]`, text: `${COLUMN} [role="tablist"] [role="tab"] span` },
post: { sel: POST },
avatar: { sel: `${POST} ${AVATAR},${POST} ${AVATAR} :is(div,a,img)`, skip: ['size'] },
displayName: { sel: `${POST} [data-testid="User-Name"] > div:first-child`, text: `${POST} [data-testid="User-Name"] > div:first-child :is(span,a)` },
handle: { sel: `${POST} [data-testid="User-Name"] > div:nth-child(2)`, text: `${POST} [data-testid="User-Name"] > div:nth-child(2) :is(span,a)` },
postText: { sel: `${POST} [data-testid="tweetText"]`, text: `${POST} [data-testid="tweetText"],${POST} [data-testid="tweetText"] :is(span,a)` },
meta: { sel: `${POST} a[href*="/status/"] > time` },
actionBar: { sel: `${POST} [role="group"]` },
action: { sel: `${POST} [role="group"] :is(${ACTIONS})` },
count: { sel: `${POST} [role="group"] [data-testid="app-text-transition-container"]`, text: `${POST} [role="group"] [data-testid="app-text-transition-container"] span` },
buttonPrimary: { sel: '[data-testid="tweetButtonInline"],[data-testid="tweetButton"],[data-testid="SideNav_NewTweet_Button"]', text: ':is([data-testid="tweetButtonInline"],[data-testid="tweetButton"],[data-testid="SideNav_NewTweet_Button"]) :is(span,div[dir])' },
buttonSecondary: { sel: '[data-testid$="-follow"],[data-testid$="-unfollow"]', text: ':is([data-testid$="-follow"],[data-testid$="-unfollow"]) span' },
// Live 2026-10-01: the painted box is three levels above the input's label (a div, no <label>).
search: { sel: 'form[role="search"] div:has(> div > div > [data-testid="SearchBox_Search_Input_label"])', text: 'form[role="search"] input' },
composer: { sel: '[data-testid="tweetTextarea_0"]', text: '[data-testid="tweetTextarea_0"] :is(span,div)' },
composerTool: { sel: '[data-testid="toolBar"] nav [role="button"]', text: '[data-testid="toolBar"] nav [role="button"] svg' },
sidebarCard: { sel: '[data-testid="sidebarColumn"] div:has(> :is(aside[role="complementary"],section[role="region"]))' },
menu: { sel: '#layers [role="menu"]' },
hoverCard: { sel: '#layers [data-testid="HoverCard"]' },
};
/** Where X's decoration goes. */
export function xDecorTargets(): DecorTargets<XSkinContext> {
return {
divider: `${COLUMN} [data-testid="cellInnerDiv"] > div`,
link: `${POST} [data-testid="tweetText"] a`,
focusable: 'a,button,[role="button"],[role="tab"],[role="link"],input,textarea,[tabindex="0"]',
moving: `${POST},${NAV} > :is(a,button) > div,[role="tab"],[data-testid$="-follow"],[data-testid="tweetButtonInline"]`,
pressable: `${NAV} > :is(a,button),[data-testid="tweetButtonInline"],[data-testid="tweetButton"],[data-testid$="-follow"],${POST} [role="group"] [role="button"]`,
chrome: {
// A window title bar on the main column, and on each sidebar card with X's own label for it.
titlebar: (title, { colors }) => [
`${FRAMED}::before{content:${cssString(title || 'Home')};display:block;order:-1;box-sizing:border-box;height:24px;padding:4px 8px;font:700 12px/16px "Pixelated MS Sans Serif","MS Sans Serif","Tahoma",sans-serif;letter-spacing:.2px;color:#ffffff;background:linear-gradient(90deg, ${colors.inverse}, ${colors.accent});white-space:nowrap;overflow:hidden}`,
`[data-testid="sidebarColumn"] :is(aside[role="complementary"],section[role="region"])[aria-label]::before{content:attr(aria-label);display:block;margin:-12px -16px 10px;padding:3px 8px;font:700 12px/16px "Pixelated MS Sans Serif","MS Sans Serif","Tahoma",sans-serif;color:#ffffff;background:linear-gradient(90deg, ${colors.inverse}, ${colors.accent})}`,
].join('\n'),
// A newspaper masthead: the theme's title between two rules at the top of the column.
masthead: (title, { colors, fonts }) => `${FRAMED}::before{content:${cssString(title || 'The Timeline')};display:block;order:-1;padding:18px 16px 12px;text-align:center;font:700 34px/1.1 ${fonts.heading};letter-spacing:-.01em;color:${colors.text};border-bottom:4px double ${colors.text};margin:0 16px}`,
// A terminal prompt line above the column.
prompt: (title, { colors, fonts }) => `${FRAMED}::before{content:${cssString(`${title || 'guest@x'}:~$ tail -f timeline`)};display:block;order:-1;padding:10px 16px;font:14px/20px ${fonts.mono};color:${colors.accent};border-bottom:1px dashed ${colors.border}}`,
},
};
}
export type XSkinContext = {
colors: RoleColors;
fonts: { heading: string; mono: string; post: string; interface: string };
/** The words of worded actions (`variants.actions = "labels"`). */
labels: { reply: string; repost: string; like: string; views: string; bookmark: string; share: string };
/** Brackets around a worded action (Terminal: `[reply 12]`). */
brackets: boolean;
};
/** Only above X's narrow breakpoint, never on chat's own row, never while the pane's rail is parked. */
const WIDE = '@media (min-width:700px)';
const NOT_CHAT = `html:not(:has(${CHAT_APP}))`;
/** How X looks in each layout variant. */
export const X_SKINS: VariantSkins<XSkinContext> = {
nav: {
// The rail becomes a bar across the top: logo, items and the Post button in a row.
top: ({ colors }) => bar('top', colors),
bottom: ({ colors }) => bar('bottom', colors),
icons: () => `${WIDE}{${NOT_CHAT} ${NAV} > :is(a,button) span{display:none !important}}`,
},
post: {
compact: () => `${POST}{padding-top:6px !important;padding-bottom:6px !important}\n${POST} [data-testid="tweetText"]{margin-top:0 !important}`,
cards: ({ colors }) => `${COLUMN} [data-testid="cellInnerDiv"] ${POST}{margin:6px 12px;border:1px solid ${colors.border} !important;border-radius:14px;background-color:${colors.surface} !important}\n${COLUMN} [data-testid="cellInnerDiv"] > div{border-bottom-width:0 !important}`,
bubbles: ({ colors }) => `${COLUMN} [data-testid="cellInnerDiv"] ${POST} [data-testid="tweetText"]{background-color:${colors.surface};padding:10px 14px;border-radius:18px;margin-top:4px}\n${COLUMN} [data-testid="cellInnerDiv"] > div{border-bottom-width:0 !important}`,
// One column set like a newspaper: small-caps bylines, a serif body, hairlines, and a drop cap on a post's own page.
broadsheet: ({ colors, fonts }) => [
`${POST} [data-testid="User-Name"] > div:first-child :is(span,a){font-variant-caps:all-small-caps !important;letter-spacing:.06em !important}`,
`${POST} [data-testid="tweetText"],${POST} [data-testid="tweetText"] span{font-family:${fonts.post} !important}`,
`${COLUMN} [data-testid="cellInnerDiv"] > div{border-bottom:1px solid ${colors.text} !important}`,
`html[data-bt-route="status"] ${POST}[tabindex="-1"] [data-testid="tweetText"] > span:first-child::first-letter{float:left;font-size:3.2em;line-height:.9;padding:4px 6px 0 0;font-family:${fonts.post}}`,
].join('\n'),
},
actions: {
// Words instead of pictures: the icon box is hidden, the word stands next to X's own count.
labels: ({ labels, brackets, colors }) => {
const word = (testids: string[], text: string) => {
const buttons = testids.map(id => `${POST} [role="group"] [data-testid="${id}"]`).join(',');
return [
`:is(${buttons}) svg{display:none !important}`,
`:is(${buttons}) > div > div:first-child::before{content:${cssString(`${brackets ? '[' : ''}${text}`)};font-size:13px;font-weight:600;white-space:nowrap}`,
...(brackets ? [`:is(${buttons}) > div::after{content:"]";font-size:13px;font-weight:600;margin-left:1px}`] : []),
].join('\n');
};
return [
word(['reply'], labels.reply), word(['retweet', 'unretweet'], labels.repost), word(['like', 'unlike'], labels.like),
word(['bookmark', 'removeBookmark'], labels.bookmark),
`${POST} [role="group"] :is([data-testid="unlike"],[data-testid="unretweet"],[data-testid="removeBookmark"]) > div{color:${colors.accent} !important}`,
].join('\n');
},
// Counts show on hover only.
quiet: () => `${POST}:not(:hover) [role="group"] [data-testid="app-text-transition-container"]{opacity:0 !important}`,
},
header: {
solid: ({ colors }) => `${COLUMN} > div > div:first-child{background-color:${colors.bg} !important;backdrop-filter:none !important}`,
bar: ({ colors }) => `${COLUMN} > div > div:first-child{box-shadow:0 1px 0 ${colors.border},0 2px 8px rgba(0, 0, 0, 0.12) !important}`,
},
sidebar: {
hidden: () => `${WIDE}{${NOT_CHAT} [data-testid="sidebarColumn"]{display:none !important}}`,
},
};
/** The rail as a bar across the top or the bottom of the window. */
function bar(edge: 'top' | 'bottom', colors: RoleColors): string {
const inner = `${RAIL} > div > div`;
return `${WIDE}{
${NOT_CHAT} ${RAIL}{position:fixed !important;${edge}:0;left:0;right:0;z-index:30;height:56px;width:auto !important;flex-grow:0 !important;align-items:stretch !important;background-color:${colors.surface};border-${edge === 'top' ? 'bottom' : 'top'}:1px solid ${colors.border};box-shadow:0 ${edge === 'top' ? '1px' : '-1px'} 6px rgba(0, 0, 0, 0.12)}
${NOT_CHAT} ${RAIL} > div{width:100% !important;height:56px !important}
${NOT_CHAT} ${inner}{position:static !important;width:100% !important;height:56px !important;max-width:1280px;margin:0 auto !important}
${NOT_CHAT} ${inner} > div{flex-direction:row !important;align-items:center !important;justify-content:space-between !important;height:56px !important;width:100% !important;padding:0 16px !important;overflow:visible !important}
${NOT_CHAT} ${inner} > div > div{flex-direction:row !important;align-items:center !important;height:56px !important;gap:4px}
${NOT_CHAT} ${NAV}{flex-direction:row !important;align-items:center !important;flex-wrap:nowrap !important;gap:2px}
${NOT_CHAT} ${NAV} > :is(a,button) > div{padding:8px 10px !important}
${NOT_CHAT} ${RAIL} [data-testid="SideNav_NewTweet_Button"]{margin:0 0 0 8px !important;width:auto !important;min-height:40px !important;padding:0 16px !important}
${NOT_CHAT} ${RAIL} [data-testid="SideNav_AccountSwitcher_Button"]{margin:0 !important}
${NOT_CHAT} main[role="main"]{padding-${edge}:56px !important;align-items:center !important}
${NOT_CHAT} ${COLUMN} > div > div:first-child{${edge === 'top' ? 'top:56px !important' : ''}}
}`;
}