Files
phantomptrandClaude Opus 5.5 11827039d3 Android: rounded split buttons, Home tiles that fit a phone, a Payloads search box
- The build rewrote start/end corners and positions with :lang(ar,fa,…) language
  lists, which Chromium before 141 (Android's WebView) drops: split buttons were
  square and start/end-placed controls sat wrong. Each list is now split into
  one language per check (postcss/langLists.mjs, run on the built CSS).
- Home's action tiles stack the icon above the label on a phone; the labels broke
  mid-word.
- Payloads' search box takes its own row on a phone instead of shrinking to nothing.
- After Resend helper, pairing keeps checking for a minute, not 16 s.
- Reverts the Android blur change: the doubled title was the emulator's software
  rendering, gone with its GPU on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ub1mez9eaVtTF8pMEFXvA9
2026-10-10 17:05:48 -07:00

55 lines
2.4 KiB
JavaScript

// Split `:lang(a,b,c)` language lists in the built stylesheet.
//
// The build targets old WebKit (safari13), so the minifier rewrites logical properties
// (`rounded-s-full`, `start-3`, `border-s`…) into left/right pairs picked by language:
//
// .rounded-s-full:not(:lang(ae,ar,…,yi)) { border-top-left-radius: … }
// .rounded-s-full:lang(ae,ar,…,yi) { border-top-right-radius: … }
//
// A language LIST inside :lang() is newer than the rest of that output: Chromium before
// 141 (Android System WebView on most phones, older WebView2) drops both rules, which
// left every split button square-cornered and every start/end-positioned control
// misplaced on Android. One language per :lang() works everywhere:
//
// :not(:lang(ae,ar,fa)) -> :not(:lang(ar)):not(:lang(fa))
// .x:lang(ae,ar,fa) -> .x:lang(ar), .x:lang(fa) (only the right-to-left
// languages the app ships)
//
// Run on the final CSS (vite.config.ts, generateBundle), after the minifier wrote it.
import postcss from "postcss";
/** Right-to-left languages the app has a translation for (client/src/i18n/locales). */
export const SHIPPED_RTL = ["ar", "fa"];
const NOT_LIST = /:not\(:lang\(([a-z-]+(?:,[a-z-]+)+)\)\)/gi;
const LIST = /:lang\(([a-z-]+(?:,[a-z-]+)+)\)/i;
function splitSelector(sel) {
// Only the languages the app can be set to need excluding; the rest never occur, and
// each extra :not() would add to the rule's specificity.
const negated = sel.replace(NOT_LIST, (_, list) =>
list
.split(",")
.filter((l) => SHIPPED_RTL.includes(l))
.map((l) => `:not(:lang(${l}))`)
.join(""),
);
const m = LIST.exec(negated);
if (!m) return [negated];
const langs = m[1].split(",").filter((l) => SHIPPED_RTL.includes(l));
// Expand one list at a time; a selector with two lists is expanded again.
return langs.flatMap((l) => splitSelector(negated.replace(m[0], `:lang(${l})`)));
}
export function splitLangLists(css) {
if (!css.includes(":lang(") || !/:lang\([a-z-]+,/i.test(css)) return css;
const root = postcss.parse(css);
root.walkRules((rule) => {
if (!/:lang\([a-z-]+,/i.test(rule.selector)) return;
const selectors = rule.selectors.flatMap(splitSelector);
if (selectors.length === 0) rule.remove();
else rule.selectors = selectors;
});
return root.toResult({ map: false }).css.replace(/,\s*\n\s*/g, ",");
}