waybetter 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/bin/waybetter.mjs +2 -5
- package/cli/buildenv.mjs +1 -1
- package/cli/release.mjs +11 -18
- package/cli/releasecheck.mjs +15 -0
- package/cli/remote.mjs +4 -4
- package/cli/store.mjs +80 -28
- package/cli/strings.mjs +2 -0
- package/dist/runtime/stringPackHook.js +2 -2
- package/dist/runtime/stringPackHook.js.map +1 -1
- package/dist/runtime/stringPackTranslate.d.ts +3 -1
- package/dist/runtime/stringPackTranslate.js +6 -3
- package/dist/runtime/stringPackTranslate.js.map +1 -1
- package/dist/runtime/stringPacks.d.ts +53 -5
- package/dist/runtime/stringPacks.js +161 -25
- package/dist/runtime/stringPacks.js.map +1 -1
- package/dist/runtime/themeBar.d.ts +28 -0
- package/dist/runtime/themeBar.js +82 -0
- package/dist/runtime/themeBar.js.map +1 -0
- package/dist/runtime/themeComponents.js +16 -3
- package/dist/runtime/themeComponents.js.map +1 -1
- package/dist/runtime/themeDecor.js +3 -1
- package/dist/runtime/themeDecor.js.map +1 -1
- package/dist/runtime/themeDerive.d.ts +69 -0
- package/dist/runtime/themeDerive.js +117 -0
- package/dist/runtime/themeDerive.js.map +1 -0
- package/dist/runtime/themeDoctor.d.ts +44 -3
- package/dist/runtime/themeDoctor.js +86 -25
- package/dist/runtime/themeDoctor.js.map +1 -1
- package/dist/runtime/themeGuards.d.ts +60 -0
- package/dist/runtime/themeGuards.js +235 -0
- package/dist/runtime/themeGuards.js.map +1 -0
- package/dist/runtime/themeIcons.d.ts +1 -1
- package/dist/runtime/themeIcons.js +13 -5
- package/dist/runtime/themeIcons.js.map +1 -1
- package/dist/runtime/themeInk.d.ts +37 -0
- package/dist/runtime/themeInk.js +111 -0
- package/dist/runtime/themeInk.js.map +1 -0
- package/dist/runtime/themeInvariants.d.ts +39 -0
- package/dist/runtime/themeInvariants.js +273 -0
- package/dist/runtime/themeInvariants.js.map +1 -0
- package/dist/runtime/themeRuntime.d.ts +15 -1
- package/dist/runtime/themeRuntime.js +78 -5
- package/dist/runtime/themeRuntime.js.map +1 -1
- package/dist/runtime/themeSpec.d.ts +5 -1
- package/dist/runtime/themeSpec.js +9 -13
- package/dist/runtime/themeSpec.js.map +1 -1
- package/dist/shell/InstallDialog.js +14 -8
- package/dist/shell/InstallDialog.js.map +1 -1
- package/dist/shell/StoreView.d.ts +12 -0
- package/dist/shell/StoreView.js +9 -4
- package/dist/shell/StoreView.js.map +1 -1
- package/dist/shell/frame/AppMenu.d.ts +26 -3
- package/dist/shell/frame/AppMenu.js +106 -27
- package/dist/shell/frame/AppMenu.js.map +1 -1
- package/dist/shell/frame/FindBar.js +1 -1
- package/dist/shell/frame/FindBar.js.map +1 -1
- package/dist/shell/frame/Shell.d.ts +1 -0
- package/dist/shell/frame/Shell.js +10 -2
- package/dist/shell/frame/Shell.js.map +1 -1
- package/dist/shell/frame/Titlebar.d.ts +2 -1
- package/dist/shell/frame/Titlebar.js +5 -3
- package/dist/shell/frame/Titlebar.js.map +1 -1
- package/dist/shell/frame/WebPanel.js +4 -1
- package/dist/shell/frame/WebPanel.js.map +1 -1
- package/dist/shell/frame/accountSwitcher.css +10 -10
- package/dist/shell/frame/appMenu.css +19 -13
- package/dist/shell/frame/boot.js +2 -0
- package/dist/shell/frame/boot.js.map +1 -1
- package/dist/shell/frame/chromeTheme.d.ts +19 -1
- package/dist/shell/frame/chromeTheme.js +51 -14
- package/dist/shell/frame/chromeTheme.js.map +1 -1
- package/dist/shell/frame/deck.css +30 -30
- package/dist/shell/frame/edges.css +159 -0
- package/dist/shell/frame/index.d.ts +2 -2
- package/dist/shell/frame/index.js +1 -1
- package/dist/shell/frame/index.js.map +1 -1
- package/dist/shell/frame/mediaModel.d.ts +13 -0
- package/dist/shell/frame/mediaModel.js +14 -0
- package/dist/shell/frame/mediaModel.js.map +1 -1
- package/dist/shell/frame/menuMessages.en.json +43 -1
- package/dist/shell/frame/menuModel.d.ts +42 -0
- package/dist/shell/frame/menuModel.js +8 -0
- package/dist/shell/frame/menuModel.js.map +1 -1
- package/dist/shell/frame/nativeMenu.d.ts +5 -0
- package/dist/shell/frame/nativeMenu.js +178 -0
- package/dist/shell/frame/nativeMenu.js.map +1 -0
- package/dist/shell/frame/nativeMenuModel.d.ts +102 -0
- package/dist/shell/frame/nativeMenuModel.js +186 -0
- package/dist/shell/frame/nativeMenuModel.js.map +1 -0
- package/dist/shell/frame/paletteModel.js +3 -1
- package/dist/shell/frame/paletteModel.js.map +1 -1
- package/dist/shell/frame/paneTheme.css +1 -1
- package/dist/shell/frame/reportModel.d.ts +1 -1
- package/dist/shell/frame/reportModel.js +1 -1
- package/dist/shell/frame/reportModel.js.map +1 -1
- package/dist/shell/frame/shell.css +56 -51
- package/dist/shell/frame/state.d.ts +2 -0
- package/dist/shell/frame/state.js +3 -1
- package/dist/shell/frame/state.js.map +1 -1
- package/dist/shell/frame/themeStore.js +21 -6
- package/dist/shell/frame/themeStore.js.map +1 -1
- package/dist/shell/frame/workspaces.css +12 -12
- package/dist/shell/installController.d.ts +16 -1
- package/dist/shell/installController.js +31 -5
- package/dist/shell/installController.js.map +1 -1
- package/dist/shell/panel/Appearance.js +12 -3
- package/dist/shell/panel/Appearance.js.map +1 -1
- package/dist/shell/panel/Settings.d.ts +1 -1
- package/dist/shell/panel/Settings.js +4 -3
- package/dist/shell/panel/Settings.js.map +1 -1
- package/dist/shell/panel/StoreSection.d.ts +2 -1
- package/dist/shell/panel/StoreSection.js +31 -1
- package/dist/shell/panel/StoreSection.js.map +1 -1
- package/dist/shell/panel/StringPacks.d.ts +12 -3
- package/dist/shell/panel/StringPacks.js +33 -8
- package/dist/shell/panel/StringPacks.js.map +1 -1
- package/dist/shell/panel/backup.css +1 -1
- package/dist/shell/panel/builder.css +48 -40
- package/dist/shell/panel/make.css +8 -7
- package/dist/shell/panel/panel.css +401 -401
- package/dist/shell/panel/panelMessages.d.ts +41 -1
- package/dist/shell/panel/panelMessages.js +13 -1
- package/dist/shell/panel/panelMessages.js.map +1 -1
- package/dist/shell/panel/plugins.css +5 -5
- package/dist/shell/panel/strings.css +1 -1
- package/dist/shell/store.css +34 -14
- package/dist/shell/storeMessages.d.ts +24 -0
- package/dist/shell/storeMessages.js +24 -0
- package/dist/shell/storeMessages.js.map +1 -1
- package/dist/shell/storeModel.d.ts +31 -7
- package/dist/shell/storeModel.js +15 -2
- package/dist/shell/storeModel.js.map +1 -1
- package/dist/shell/stringPacksModel.js +7 -2
- package/dist/shell/stringPacksModel.js.map +1 -1
- package/icons.schema.json +9 -0
- package/locales/ar.json +109 -2
- package/locales/bg.json +109 -2
- package/locales/bn.json +109 -2
- package/locales/ca.json +109 -2
- package/locales/cs.json +109 -2
- package/locales/da.json +109 -2
- package/locales/de.json +109 -2
- package/locales/el.json +109 -2
- package/locales/en.json +108 -2
- package/locales/es.json +109 -2
- package/locales/eu.json +109 -2
- package/locales/fa.json +109 -2
- package/locales/fi.json +109 -2
- package/locales/fil.json +109 -2
- package/locales/fr.json +109 -2
- package/locales/ga.json +109 -2
- package/locales/gl.json +109 -2
- package/locales/gu.json +109 -2
- package/locales/ha.json +109 -2
- package/locales/he.json +109 -2
- package/locales/hi.json +109 -2
- package/locales/hr.json +109 -2
- package/locales/hu.json +110 -2
- package/locales/id.json +109 -2
- package/locales/ig.json +109 -2
- package/locales/it.json +109 -2
- package/locales/ja.json +109 -2
- package/locales/kn.json +109 -2
- package/locales/ko.json +109 -2
- package/locales/mr.json +109 -2
- package/locales/ms.json +109 -2
- package/locales/nb.json +109 -2
- package/locales/nl.json +109 -2
- package/locales/pl.json +109 -2
- package/locales/pt.json +109 -2
- package/locales/ro.json +109 -2
- package/locales/ru.json +109 -2
- package/locales/sk.json +109 -2
- package/locales/sr.json +109 -2
- package/locales/sv.json +109 -2
- package/locales/ta.json +109 -2
- package/locales/th.json +109 -2
- package/locales/tr.json +109 -2
- package/locales/uk.json +109 -2
- package/locales/ur.json +109 -2
- package/locales/vi.json +109 -2
- package/locales/yo.json +109 -2
- package/locales/zh-Hans.json +109 -2
- package/locales/zh-Hant.json +109 -2
- package/package.json +2 -3
- package/store-catalog.schema.json +18 -5
- package/theme.schema.json +9 -0
- package/windows/installer.nsi +22 -2
- package/cli/privacy.mjs +0 -768
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themeDerive.js","sourceRoot":"","sources":["../../../runtime/src/themeDerive.ts"],"names":[],"mappings":"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","sourcesContent":["// Theme engine v3, step 5 (.omc/plans/theme-engine-v3.md 3.2): every surface the site and the shell\n// paint, derived from a theme's tokens, with every ink solved for the fill it sits on (themeInk.ts).\n// Pure. Consumers ask \"what is the ink on the chrome\", never \"which palette role reads here\".\n//\n// The input is the v3 palette shape; v2 documents reach it through `surfacesInputFromV2` until step 13\n// makes v3 the stored format.\n\nimport { contrastRatio, relativeLuminance } from './themeApi.ts';\nimport { inkOn, inkOnEvery, mixOklab, TARGET } from './themeInk.ts';\n\nexport const SURFACE_NAMES = ['canvas', 'content', 'card', 'chrome', 'raised', 'panel', 'strong', 'accentFill', 'tooltip'] as const;\nexport type SurfaceName = typeof SURFACE_NAMES[number];\nexport type EdgeStyle = 'flat' | 'hairline' | 'outline' | 'bevel' | 'gloss';\n\nexport type DerivePalette = {\n scheme?: 'light' | 'dark';\n page?: string | null; bg: string; surface: string; elevated: string; face?: string | null;\n border: string; text: string; muted: string; accent: string;\n inverse?: string | null; inverse2?: string | null; onInverse?: string | null;\n hover?: string | null; selected?: string | null;\n};\nexport type DeriveInput = {\n palette: DerivePalette;\n edge?: { style?: EdgeStyle; strong?: boolean };\n access?: 'standard' | 'high';\n post?: { style?: 'flat' | 'cards' | 'bubbles' | 'compact' | 'broadsheet' };\n};\n\nexport type Surface = {\n /** The fill (`fill2`: the far end of a gradient fill; inks read on both). */\n fill: string; fill2?: string;\n ink: string; inkSoft: string; link: string; line: string;\n hover: string; selected: string; focus: string;\n};\nexport type SurfaceSet = Record<SurfaceName, Surface> & { scheme: 'light' | 'dark'; edge: EdgeStyle; notes: string[] };\n\nconst isDarkFill = (hex: string) => relativeLuminance(hex) < 0.18;\n\n/** The best ratio any ink can reach on every one of `fills` (black or white on the worst of them). */\nexport const reachable = (fills: string[]) => Math.max(...['#000000', '#ffffff'].map(pole => Math.min(...fills.map(fill => contrastRatio(pole, fill)))));\n\n/**\n * One surface. Inks are solved on the fill (both ends of a gradient); the hover and selected states are\n * then pulled back toward the fill until every ink still reads on them, so a hovered or selected row\n * never costs contrast. A target no ink can reach on this fill (7:1 on a mid tone) is capped at the\n * best there is and noted.\n */\nfunction surface(name: SurfaceName, fill: string, p: DerivePalette, input: DeriveInput, notes: string[], opts: { fill2?: string; inkPreferred?: string; hover?: string | null; selected?: string | null } = {}): Surface {\n const fills = opts.fill2 ? [fill, opts.fill2] : [fill];\n const best = reachable(fills);\n const cap = (target: number, what: string) => {\n if (best >= target) return target;\n notes.push(`${name}.${what}: ${target}:1 is out of reach on ${fills.join('/')}, ${best.toFixed(2)}:1 at most`);\n return best;\n };\n const textTarget = cap(input.access === 'high' ? TARGET.high : TARGET.text, 'ink');\n const softTarget = cap(TARGET.text, 'inkSoft');\n const solve = (preferred: string, target: number, what: string) => {\n const result = inkOnEvery(fills, preferred, target);\n if (result.adjusted) notes.push(`${name}.${what} ${preferred} -> ${result.ink} (${result.ratio.toFixed(2)}:1)`);\n return result.ink;\n };\n const ink = solve(opts.inkPreferred ?? p.text, textTarget, 'ink');\n const inkSoft = solve(p.muted, softTarget, 'inkSoft');\n const link = solve(p.accent, softTarget, 'link');\n const reads = (state: string) => contrastRatio(ink, state) >= textTarget - 1e-9 && contrastRatio(inkSoft, state) >= softTarget - 1e-9 && contrastRatio(link, state) >= softTarget - 1e-9;\n /** A state `t` of the way from the fill to `toward`, shrunk until the inks read on it. */\n const state = (toward: string, t: number, what: string) => {\n for (let k = t; k > 0.004; k /= 2) { const candidate = mixOklab(fill, toward, k); if (reads(candidate)) { if (k !== t) notes.push(`${name}.${what} step ${t} -> ${k.toFixed(3)}`); return candidate; } }\n return fill;\n };\n const dark = isDarkFill(fill);\n // States: the theme's own hover/selected when they keep every ink readable, else a neutral step toward the ink (never the accent hue for hover).\n const hover = opts.hover && reads(opts.hover) ? opts.hover : state(ink, dark ? 0.08 : 0.05, 'hover');\n const selected = opts.selected && reads(opts.selected) ? opts.selected : state(p.accent, 0.14, 'selected');\n const strongLine = input.edge?.strong || input.access === 'high';\n const lineTarget = Math.min(best, strongLine || input.edge?.style === 'outline' ? TARGET.nonText : 1.3);\n return {\n fill, ...(opts.fill2 ? { fill2: opts.fill2 } : {}),\n ink, inkSoft, link,\n line: strongLine ? ink : inkOnEvery(fills, p.border, lineTarget).ink,\n hover, selected, focus: inkOnEvery(fills, p.accent, Math.min(best, TARGET.nonText)).ink,\n };\n}\n\n/** Every surface of a theme. Notes list each ink the solver had to move. */\nexport function deriveSurfaces(input: DeriveInput): SurfaceSet {\n const p = input.palette;\n const notes: string[] = [];\n const scheme = p.scheme ?? (relativeLuminance(p.bg) < relativeLuminance(p.text) ? 'dark' : 'light');\n const edge = input.edge?.style ?? 'hairline';\n const face = p.face ?? (edge === 'bevel' ? mixOklab(p.surface, p.border, 0.2) : p.surface);\n const cards = input.post?.style === 'cards' || input.post?.style === 'bubbles';\n // The strong fill (title bars, inverted buttons): its gradient end moves until one ink reads on both ends.\n const inverse = p.inverse ?? p.text;\n let inverse2 = p.inverse2 ?? inverse;\n const strongPreferred = p.onInverse ?? p.bg;\n if (inverse2 !== inverse) {\n const both = inkOnEvery([inverse, inverse2], strongPreferred, TARGET.text);\n if (both.ratio < TARGET.text) {\n const ink = inkOn(inverse, strongPreferred, TARGET.text).ink;\n const moved = inkOn(ink, inverse2, TARGET.text).ink;\n notes.push(`strong.fill2 ${inverse2} -> ${moved} (one ink on both ends)`);\n inverse2 = moved;\n }\n }\n // The accent fill's label: white or the page colour, whichever reads better, then solved.\n const accentLabel = contrastRatio('#ffffff', p.accent) >= contrastRatio(p.bg, p.accent) ? '#ffffff' : p.bg;\n const tooltipFill = mixOklab(p.text, p.bg, 0.12);\n const set = {\n canvas: surface('canvas', p.page ?? p.bg, p, input, notes),\n content: surface('content', p.bg, p, input, notes, { hover: p.hover, selected: p.selected }),\n card: surface('card', cards ? p.surface : p.bg, p, input, notes, { hover: p.hover, selected: p.selected }),\n chrome: surface('chrome', face, p, input, notes, { hover: p.hover, selected: p.selected }),\n raised: surface('raised', p.elevated, p, input, notes),\n panel: surface('panel', p.bg, p, input, notes, { hover: p.hover, selected: p.selected }),\n strong: surface('strong', inverse, p, input, notes, { ...(inverse2 !== inverse ? { fill2: inverse2 } : {}), inkPreferred: strongPreferred }),\n accentFill: surface('accentFill', p.accent, p, input, notes, { inkPreferred: accentLabel }),\n tooltip: surface('tooltip', tooltipFill, p, input, notes, { inkPreferred: p.bg }),\n } satisfies Record<SurfaceName, Surface>;\n return { ...set, scheme, edge, notes };\n}\n\n/** The v2 palette and component hints as v3 derivation input (until the documents are v3, step 13). */\nexport function surfacesInputFromV2(colors: { bg: string; surface: string; elevated: string; border: string; text: string; muted: string; accent: string; page?: string | null; inverse?: string | null; onInverse?: string | null }, hints: { bevel?: boolean; high?: boolean; cards?: boolean } = {}): DeriveInput {\n return {\n palette: {\n page: colors.page ?? null, bg: colors.bg, surface: colors.surface, elevated: colors.elevated, border: colors.border,\n text: colors.text, muted: colors.muted, accent: colors.accent, inverse: colors.inverse ?? null, onInverse: colors.onInverse ?? null,\n // A bevelled v2 theme draws its chrome from `page` (Windows 98: the silver face) and its title bar from inverse to accent.\n face: hints.bevel ? colors.page ?? colors.elevated : null,\n inverse2: hints.bevel ? colors.accent : null,\n },\n edge: { style: hints.bevel ? 'bevel' : 'hairline' },\n access: hints.high ? 'high' : 'standard',\n post: { style: hints.cards ? 'cards' : 'flat' },\n };\n}\n"]}
|
|
@@ -2,10 +2,51 @@ import { type ThemeSpec } from './themeSpec.ts';
|
|
|
2
2
|
export type DoctorAdvice = {
|
|
3
3
|
/** `lighter`: a lighter mechanism would do; `performance`, `accessibility`, `contrast`: a cost. */
|
|
4
4
|
kind: 'lighter' | 'performance' | 'accessibility' | 'contrast';
|
|
5
|
-
/** Stable id, for tests
|
|
5
|
+
/** Stable id of the finding, for tests (`css-radius`, `contrast-text`). */
|
|
6
6
|
id: string;
|
|
7
|
-
/**
|
|
7
|
+
/** Its words in the catalog: `wb.doctor.<key>.found` and `wb.doctor.<key>.fix` (`css-radius`, `contrast`). */
|
|
8
|
+
key: string;
|
|
9
|
+
/** A token path of the theme (`colors.text`, `components.postText.fontSize`) or the CSS rule (`.card { border-radius }`). */
|
|
10
|
+
where: string;
|
|
11
|
+
/** Values for the `{name}` slots of the words. */
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
/** English: what was found. */
|
|
8
14
|
text: string;
|
|
15
|
+
/** English: how to fix it. */
|
|
16
|
+
fix: string;
|
|
9
17
|
};
|
|
10
|
-
/**
|
|
18
|
+
/** The doctor's words in English (the source of `wb.doctor.*` in the catalogs). */
|
|
19
|
+
export declare const DOCTOR_MESSAGES: {
|
|
20
|
+
readonly 'wb.doctor.css-radius.found': "This rule sets corner radii in CSS.";
|
|
21
|
+
readonly 'wb.doctor.css-radius.fix': "Set them with the radius of a part under components, or shape.radius for all of them: no CSS needed, and it keeps working when the site renames its classes.";
|
|
22
|
+
readonly 'wb.doctor.css-bevel.found': "This rule draws a bevelled edge with stacked inset shadows.";
|
|
23
|
+
readonly 'wb.doctor.css-bevel.fix': "Use borderStyle \"bevel-out\" or \"bevel-in\" on a part under components: it draws the bevel from the theme’s colors.";
|
|
24
|
+
readonly 'wb.doctor.css-hide.found': "This rule hides something with display: none.";
|
|
25
|
+
readonly 'wb.doctor.css-hide.fix': "Use visible on a part under components (avatars, counts, names) or a layout variant such as sidebar: hidden; the owner can then switch it back.";
|
|
26
|
+
readonly 'wb.doctor.css-color.found': "This rule uses a literal color.";
|
|
27
|
+
readonly 'wb.doctor.css-color.fix': "Name a role (text, muted, accent, surface…) in colors or in a component, so the contrast pass covers it.";
|
|
28
|
+
readonly 'wb.doctor.css-font.found': "This rule sets a font family.";
|
|
29
|
+
readonly 'wb.doctor.css-font.fix': "Use font.roles: it sets the post, heading, numbers and mono faces without CSS.";
|
|
30
|
+
readonly 'wb.doctor.css-has.found': "This rule uses :has(), which is matched again on every change of the page.";
|
|
31
|
+
readonly 'wb.doctor.css-has.fix': "Keep :has() off rules that repeat down a timeline, or anchor the rule on a part.";
|
|
32
|
+
readonly 'wb.doctor.css-universal.found': "This rule uses a universal selector (*), matched against every element.";
|
|
33
|
+
readonly 'wb.doctor.css-universal.fix': "Anchor the rule on a part instead.";
|
|
34
|
+
readonly 'wb.doctor.css-blur.found': "This rule blurs what is behind it (backdrop-filter), which is costly on parts that repeat.";
|
|
35
|
+
readonly 'wb.doctor.css-blur.fix': "Use the glass fill of bars and panels: it is the cheaper place for blur.";
|
|
36
|
+
readonly 'wb.doctor.css-outline-none.found': "This rule removes the focus outline, so keyboard focus is no longer visible.";
|
|
37
|
+
readonly 'wb.doctor.css-outline-none.fix': "Use decor.focusRing to draw a visible focus ring instead.";
|
|
38
|
+
readonly 'wb.doctor.css-motion.found': "This rule animates without checking for reduced motion.";
|
|
39
|
+
readonly 'wb.doctor.css-motion.fix': "Keep motion inside @media (prefers-reduced-motion: no-preference), or let decor.motion do it for you.";
|
|
40
|
+
readonly 'wb.doctor.css-size.found': "The CSS is more than 20,000 characters long.";
|
|
41
|
+
readonly 'wb.doctor.css-size.fix': "The site recalculates styles with every rule; tokens and component settings compile to fewer rules.";
|
|
42
|
+
readonly 'wb.doctor.contrast.found': "The {token} color was adjusted to stay readable ({ratio}:1 on the background).";
|
|
43
|
+
readonly 'wb.doctor.contrast.fix': "Pick a lighter or darker {token} color; until then the theme is shown with the adjusted one.";
|
|
44
|
+
readonly 'wb.doctor.glass-post.found': "Glass is set on posts, but it is not drawn: blur on every post of a timeline is too costly.";
|
|
45
|
+
readonly 'wb.doctor.glass-post.fix': "Use a solid or tinted fill for posts and keep glass for bars and panels.";
|
|
46
|
+
readonly 'wb.doctor.small-post-text.found': "Post text is {size}px, which is hard to read for many people.";
|
|
47
|
+
readonly 'wb.doctor.small-post-text.fix': "Use 14px or more for the post text size, or raise the text size of the whole theme.";
|
|
48
|
+
readonly 'wb.doctor.nav-moved.found': "The navigation is moved to the {position}, which changes where people look for it.";
|
|
49
|
+
readonly 'wb.doctor.nav-moved.fix': "It only applies on wide panes, and the owner can turn layout changes off. Keep the default position unless the layout needs it.";
|
|
50
|
+
};
|
|
51
|
+
/** Advice for a theme document: every finding, with where it is. Empty when there is nothing to say. */
|
|
11
52
|
export declare function themeDoctor(theme: ThemeSpec): DoctorAdvice[];
|
|
@@ -1,50 +1,111 @@
|
|
|
1
1
|
// The theme doctor: advice, never a block. A theme may do anything its trust level allows; the doctor
|
|
2
2
|
// points at the lighter mechanism for what the theme does (a token before a component setting, a
|
|
3
3
|
// component setting before CSS, CSS before a script) and at performance, accessibility and contrast
|
|
4
|
-
// costs. Used by the import screen, the Theme Builder's report and `waybetter store check`.
|
|
4
|
+
// costs. Used by the import screen, the Theme Builder's full report and `waybetter store check`.
|
|
5
|
+
// Pure. Every finding says where it is (a token path or the CSS rule), what was found and how to fix
|
|
6
|
+
// it; the words are catalog keys (`wb.doctor.<id>.found` / `.fix`, English below), so a panel shows
|
|
7
|
+
// them in the owner's language and the CLI prints the English.
|
|
5
8
|
import { contrastRatio } from "./themeApi.js";
|
|
6
9
|
import { ensureContrast } from "./themeSpec.js";
|
|
10
|
+
/** The doctor's words in English (the source of `wb.doctor.*` in the catalogs). */
|
|
11
|
+
export const DOCTOR_MESSAGES = {
|
|
12
|
+
'wb.doctor.css-radius.found': 'This rule sets corner radii in CSS.',
|
|
13
|
+
'wb.doctor.css-radius.fix': 'Set them with the radius of a part under components, or shape.radius for all of them: no CSS needed, and it keeps working when the site renames its classes.',
|
|
14
|
+
'wb.doctor.css-bevel.found': 'This rule draws a bevelled edge with stacked inset shadows.',
|
|
15
|
+
'wb.doctor.css-bevel.fix': 'Use borderStyle "bevel-out" or "bevel-in" on a part under components: it draws the bevel from the theme’s colors.',
|
|
16
|
+
'wb.doctor.css-hide.found': 'This rule hides something with display: none.',
|
|
17
|
+
'wb.doctor.css-hide.fix': 'Use visible on a part under components (avatars, counts, names) or a layout variant such as sidebar: hidden; the owner can then switch it back.',
|
|
18
|
+
'wb.doctor.css-color.found': 'This rule uses a literal color.',
|
|
19
|
+
'wb.doctor.css-color.fix': 'Name a role (text, muted, accent, surface…) in colors or in a component, so the contrast pass covers it.',
|
|
20
|
+
'wb.doctor.css-font.found': 'This rule sets a font family.',
|
|
21
|
+
'wb.doctor.css-font.fix': 'Use font.roles: it sets the post, heading, numbers and mono faces without CSS.',
|
|
22
|
+
'wb.doctor.css-has.found': 'This rule uses :has(), which is matched again on every change of the page.',
|
|
23
|
+
'wb.doctor.css-has.fix': 'Keep :has() off rules that repeat down a timeline, or anchor the rule on a part.',
|
|
24
|
+
'wb.doctor.css-universal.found': 'This rule uses a universal selector (*), matched against every element.',
|
|
25
|
+
'wb.doctor.css-universal.fix': 'Anchor the rule on a part instead.',
|
|
26
|
+
'wb.doctor.css-blur.found': 'This rule blurs what is behind it (backdrop-filter), which is costly on parts that repeat.',
|
|
27
|
+
'wb.doctor.css-blur.fix': 'Use the glass fill of bars and panels: it is the cheaper place for blur.',
|
|
28
|
+
'wb.doctor.css-outline-none.found': 'This rule removes the focus outline, so keyboard focus is no longer visible.',
|
|
29
|
+
'wb.doctor.css-outline-none.fix': 'Use decor.focusRing to draw a visible focus ring instead.',
|
|
30
|
+
'wb.doctor.css-motion.found': 'This rule animates without checking for reduced motion.',
|
|
31
|
+
'wb.doctor.css-motion.fix': 'Keep motion inside @media (prefers-reduced-motion: no-preference), or let decor.motion do it for you.',
|
|
32
|
+
'wb.doctor.css-size.found': 'The CSS is more than 20,000 characters long.',
|
|
33
|
+
'wb.doctor.css-size.fix': 'The site recalculates styles with every rule; tokens and component settings compile to fewer rules.',
|
|
34
|
+
'wb.doctor.contrast.found': 'The {token} color was adjusted to stay readable ({ratio}:1 on the background).',
|
|
35
|
+
'wb.doctor.contrast.fix': 'Pick a lighter or darker {token} color; until then the theme is shown with the adjusted one.',
|
|
36
|
+
'wb.doctor.glass-post.found': 'Glass is set on posts, but it is not drawn: blur on every post of a timeline is too costly.',
|
|
37
|
+
'wb.doctor.glass-post.fix': 'Use a solid or tinted fill for posts and keep glass for bars and panels.',
|
|
38
|
+
'wb.doctor.small-post-text.found': 'Post text is {size}px, which is hard to read for many people.',
|
|
39
|
+
'wb.doctor.small-post-text.fix': 'Use 14px or more for the post text size, or raise the text size of the whole theme.',
|
|
40
|
+
'wb.doctor.nav-moved.found': 'The navigation is moved to the {position}, which changes where people look for it.',
|
|
41
|
+
'wb.doctor.nav-moved.fix': 'It only applies on wide panes, and the owner can turn layout changes off. Keep the default position unless the layout needs it.',
|
|
42
|
+
};
|
|
43
|
+
const fill = (text, params) => text.replace(/\{(\w+)\}/g, (_, name) => params[name] ?? '');
|
|
7
44
|
/** What a theme's free-form CSS does that a v2 group does without CSS. */
|
|
8
45
|
const CSS_RULES = [
|
|
9
|
-
{ id: 'css-radius', kind: 'lighter', test: /border-radius\s*:/
|
|
10
|
-
{ id: 'css-bevel', kind: 'lighter', test: /box-shadow\s*:[^;{}]*inset[^;{}]*inset[^;{}]*inset/
|
|
11
|
-
{ id: 'css-hide', kind: 'lighter', test: /display\s*:\s*none/
|
|
12
|
-
{ id: 'css-color', kind: 'lighter', test: /(?:^|[;{\s])(?:color|background(?:-color)?)\s*:\s*#[0-9a-f]{3,8}/
|
|
13
|
-
{ id: 'css-font', kind: 'lighter', test: /font-family\s*:/
|
|
14
|
-
{ id: 'css-has', kind: 'performance', test: /:has\(/
|
|
15
|
-
{ id: 'css-universal', kind: 'performance', test: /(?:^|[\s,>+~])\*\s*(?:[,{
|
|
16
|
-
{ id: 'css-blur', kind: 'performance', test: /backdrop-filter\s*:/
|
|
17
|
-
{ id: 'css-outline-none', kind: 'accessibility', test: /outline\s*:\s*(?:none|0)\b/
|
|
18
|
-
{ id: 'css-motion', kind: 'accessibility', test: /(?:animation|transition)\s*:/
|
|
46
|
+
{ id: 'css-radius', kind: 'lighter', test: /border-radius\s*:/gi, property: 'border-radius' },
|
|
47
|
+
{ id: 'css-bevel', kind: 'lighter', test: /box-shadow\s*:[^;{}]*inset[^;{}]*inset[^;{}]*inset/gi, property: 'box-shadow' },
|
|
48
|
+
{ id: 'css-hide', kind: 'lighter', test: /display\s*:\s*none/gi, property: 'display' },
|
|
49
|
+
{ id: 'css-color', kind: 'lighter', test: /(?:^|[;{\s])(?:color|background(?:-color)?)\s*:\s*#[0-9a-f]{3,8}/gi, property: 'color' },
|
|
50
|
+
{ id: 'css-font', kind: 'lighter', test: /font-family\s*:/gi, property: 'font-family' },
|
|
51
|
+
{ id: 'css-has', kind: 'performance', test: /:has\(/gi, property: ':has()' },
|
|
52
|
+
{ id: 'css-universal', kind: 'performance', test: /(?:^|[\s,>+~])\*\s*(?:[,{>+~:[]|$)/gm, property: '*' },
|
|
53
|
+
{ id: 'css-blur', kind: 'performance', test: /backdrop-filter\s*:/gi, property: 'backdrop-filter' },
|
|
54
|
+
{ id: 'css-outline-none', kind: 'accessibility', test: /outline\s*:\s*(?:none|0)\b/gi, property: 'outline' },
|
|
55
|
+
{ id: 'css-motion', kind: 'accessibility', test: /(?:animation|transition)\s*:/gi, property: 'transition' },
|
|
19
56
|
];
|
|
20
|
-
|
|
57
|
+
const MAX_PER_RULE = 25;
|
|
58
|
+
/** The selector of the rule block a position of `css` is in (the text before its `{`), shortened. */
|
|
59
|
+
function selectorAt(css, index) {
|
|
60
|
+
const open = css.lastIndexOf('{', index);
|
|
61
|
+
if (open < 0)
|
|
62
|
+
return '';
|
|
63
|
+
const close = Math.max(css.lastIndexOf('}', open - 1), css.lastIndexOf(';', open - 1), css.lastIndexOf('{', open - 1));
|
|
64
|
+
const selector = css.slice(close + 1, open).replace(/\s+/g, ' ').trim();
|
|
65
|
+
return selector.length > 80 ? `${selector.slice(0, 79)}…` : selector;
|
|
66
|
+
}
|
|
67
|
+
function advice(id, kind, where, params = {}, key = id) {
|
|
68
|
+
const words = DOCTOR_MESSAGES;
|
|
69
|
+
return { kind, id, key, where, params, text: fill(words[`wb.doctor.${key}.found`], params), fix: fill(words[`wb.doctor.${key}.fix`], params) };
|
|
70
|
+
}
|
|
71
|
+
/** Advice for a theme document: every finding, with where it is. Empty when there is nothing to say. */
|
|
21
72
|
export function themeDoctor(theme) {
|
|
22
73
|
const out = [];
|
|
23
74
|
const css = theme.css ?? '';
|
|
24
75
|
if (css.trim()) {
|
|
25
|
-
for (const rule of CSS_RULES)
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
for (const rule of CSS_RULES) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const match of css.matchAll(rule.test)) {
|
|
79
|
+
const selector = selectorAt(css, match.index ?? 0) || 'css';
|
|
80
|
+
const where = rule.id === 'css-universal' || rule.id === 'css-has' ? selector : `${selector} { ${rule.property} }`;
|
|
81
|
+
if (seen.has(where))
|
|
82
|
+
continue;
|
|
83
|
+
seen.add(where);
|
|
84
|
+
out.push(advice(rule.id, rule.kind, where));
|
|
85
|
+
if (seen.size >= MAX_PER_RULE)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
28
89
|
if (css.length > 20_000)
|
|
29
|
-
out.push(
|
|
90
|
+
out.push(advice('css-size', 'performance', 'css'));
|
|
30
91
|
}
|
|
31
92
|
if (theme.colors) {
|
|
32
93
|
const fixed = ensureContrast(theme.colors);
|
|
33
94
|
for (const role of ['text', 'muted', 'accent']) {
|
|
34
|
-
if (fixed[role] !== theme.colors[role])
|
|
35
|
-
out.push({
|
|
95
|
+
if (fixed[role] !== theme.colors[role]) {
|
|
96
|
+
out.push(advice(`contrast-${role}`, 'contrast', `colors.${role}`, { token: role, ratio: contrastRatio(theme.colors[role], theme.colors.bg).toFixed(2) }, 'contrast'));
|
|
97
|
+
}
|
|
36
98
|
}
|
|
37
99
|
}
|
|
38
100
|
if (theme.components) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
out.push({ kind: 'accessibility', id: 'small-post-text', text: 'Post text under 14px is hard to read for many people.' });
|
|
101
|
+
if (theme.components.post?.fill === 'glass')
|
|
102
|
+
out.push(advice('glass-post', 'performance', 'components.post.fill'));
|
|
103
|
+
const size = theme.components.postText?.fontSize;
|
|
104
|
+
if (size !== undefined && size < 14)
|
|
105
|
+
out.push(advice('small-post-text', 'accessibility', 'components.postText.fontSize', { size: String(size) }));
|
|
45
106
|
}
|
|
46
107
|
if (theme.variants?.nav === 'top' || theme.variants?.nav === 'bottom') {
|
|
47
|
-
out.push(
|
|
108
|
+
out.push(advice('nav-moved', 'accessibility', 'variants.nav', { position: theme.variants.nav }));
|
|
48
109
|
}
|
|
49
110
|
return out;
|
|
50
111
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"themeDoctor.js","sourceRoot":"","sources":["../../../runtime/src/themeDoctor.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,iGAAiG;AACjG,oGAAoG;AACpG,kGAAkG;AAElG,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAkB,MAAM,gBAAgB,CAAC;AAYhE,0EAA0E;AAC1E,MAAM,SAAS,GAAW;IACxB,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,qIAAqI,EAAE;IAC9M,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,qDAAqD,EAAE,IAAI,EAAE,6GAA6G,EAAE;IACtN,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,oJAAoJ,EAAE;IAC5N,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,mEAAmE,EAAE,IAAI,EAAE,0HAA0H,EAAE;IACjP,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,+EAA+E,EAAE;IACpJ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,qGAAqG,EAAE;IACpK,EAAE,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,sCAAsC,EAAE,IAAI,EAAE,uFAAuF,EAAE;IACzL,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,uHAAuH,EAAE;IACpM,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,6BAA6B,EAAE,IAAI,EAAE,kGAAkG,EAAE;IAChM,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,+BAA+B,EAAE,IAAI,EAAE,2GAA2G,EAAE;CACtM,CAAC;AAEF,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,KAAgB;IAC1C,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC;IAC5B,IAAI,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QACf,KAAK,MAAM,IAAI,IAAI,SAAS;YAAE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACnH,IAAI,GAAG,CAAC,MAAM,GAAG,MAAM;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,yIAAyI,EAAE,CAAC,CAAC;IAC9N,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAU,EAAE,CAAC;YACxD,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,IAAI,yCAAyC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,wBAAwB,EAAE,CAAC,CAAC;QAClP,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QACpC,IAAI,KAAK,EAAE,IAAI,KAAK,OAAO;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,8EAA8E,EAAE,CAAC,CAAC;QACvK,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC;QACjD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,GAAG,EAAE;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,uDAAuD,EAAE,CAAC,CAAC;IACjK,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,EAAE,GAAG,KAAK,KAAK,IAAI,KAAK,CAAC,QAAQ,EAAE,GAAG,KAAK,QAAQ,EAAE,CAAC;QACtE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mIAAmI,EAAE,CAAC,CAAC;IAClM,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["// The theme doctor: advice, never a block. A theme may do anything its trust level allows; the doctor\n// points at the lighter mechanism for what the theme does (a token before a component setting, a\n// component setting before CSS, CSS before a script) and at performance, accessibility and contrast\n// costs. Used by the import screen, the Theme Builder's report and `waybetter store check`. Pure.\n\nimport { contrastRatio } from './themeApi.ts';\nimport { ensureContrast, type ThemeSpec } from './themeSpec.ts';\n\nexport type DoctorAdvice = {\n /** `lighter`: a lighter mechanism would do; `performance`, `accessibility`, `contrast`: a cost. */\n kind: 'lighter' | 'performance' | 'accessibility' | 'contrast';\n /** Stable id, for tests and translations (`wb.doctor.<id>`). */\n id: string;\n /** English text (the panel translates by id when it has the words). */\n text: string;\n};\n\ntype Rule = { id: string; kind: DoctorAdvice['kind']; test: RegExp; text: string };\n/** What a theme's free-form CSS does that a v2 group does without CSS. */\nconst CSS_RULES: Rule[] = [\n { id: 'css-radius', kind: 'lighter', test: /border-radius\\s*:/i, text: 'Corner radii: components.<part>.radius (or shape.radius) sets them without CSS and keeps working when the site renames its classes.' },\n { id: 'css-bevel', kind: 'lighter', test: /box-shadow\\s*:[^;{}]*inset[^;{}]*inset[^;{}]*inset/i, text: 'Bevelled edges: components.<part>.borderStyle \"bevel-out\" / \"bevel-in\" draws them from the theme\\'s colors.' },\n { id: 'css-hide', kind: 'lighter', test: /display\\s*:\\s*none/i, text: 'Hiding a part: components.<part>.visible (avatars, counts, names) or a layout variant (sidebar: hidden) does it, and the owner can switch it back.' },\n { id: 'css-color', kind: 'lighter', test: /(?:^|[;{\\s])(?:color|background(?:-color)?)\\s*:\\s*#[0-9a-f]{3,8}/i, text: 'Literal colors: name a role (text, muted, accent, surface…) in colors or in a component, so the contrast pass covers it.' },\n { id: 'css-font', kind: 'lighter', test: /font-family\\s*:/i, text: 'Fonts: font.roles sets the post, heading, numbers and mono faces without CSS.' },\n { id: 'css-has', kind: 'performance', test: /:has\\(/i, text: ':has() is matched again on every change of the page; keep it off rules that repeat down a timeline.' },\n { id: 'css-universal', kind: 'performance', test: /(?:^|[\\s,>+~])\\*\\s*(?:[,{>+~:\\[]|$)/m, text: 'A universal selector (*) is matched against every element; anchor the rule on a part.' },\n { id: 'css-blur', kind: 'performance', test: /backdrop-filter\\s*:/i, text: 'Blur (backdrop-filter) is costly on parts that repeat; the glass fill of bars and panels is the cheaper place for it.' },\n { id: 'css-outline-none', kind: 'accessibility', test: /outline\\s*:\\s*(?:none|0)\\b/i, text: 'Removing the outline hides where keyboard focus is; decor.focusRing draws a visible one instead.' },\n { id: 'css-motion', kind: 'accessibility', test: /(?:animation|transition)\\s*:/i, text: 'Motion should stay inside @media (prefers-reduced-motion: no-preference); decor.motion does that for you.' },\n];\n\n/** Advice for a theme document. Empty when there is nothing to say. */\nexport function themeDoctor(theme: ThemeSpec): DoctorAdvice[] {\n const out: DoctorAdvice[] = [];\n const css = theme.css ?? '';\n if (css.trim()) {\n for (const rule of CSS_RULES) if (rule.test.test(css)) out.push({ kind: rule.kind, id: rule.id, text: rule.text });\n if (css.length > 20_000) out.push({ kind: 'performance', id: 'css-size', text: 'More than 20,000 characters of CSS: the site recalculates styles with every rule; tokens and component settings compile to fewer rules.' });\n }\n if (theme.colors) {\n const fixed = ensureContrast(theme.colors);\n for (const role of ['text', 'muted', 'accent'] as const) {\n if (fixed[role] !== theme.colors[role]) out.push({ kind: 'contrast', id: `contrast-${role}`, text: `The ${role} color was adjusted to stay readable (${contrastRatio(theme.colors[role], theme.colors.bg).toFixed(2)}:1 on the background).` });\n }\n }\n if (theme.components) {\n const posts = theme.components.post;\n if (posts?.fill === 'glass') out.push({ kind: 'performance', id: 'glass-post', text: 'Glass on posts is not drawn: blur on every post of a timeline is too costly.' });\n const text = theme.components.postText?.fontSize;\n if (text !== undefined && text < 14) out.push({ kind: 'accessibility', id: 'small-post-text', text: 'Post text under 14px is hard to read for many people.' });\n }\n if (theme.variants?.nav === 'top' || theme.variants?.nav === 'bottom') {\n out.push({ kind: 'accessibility', id: 'nav-moved', text: 'Moving the navigation changes where people look for it; it only applies on wide panes, and the owner can turn layout changes off.' });\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"themeDoctor.js","sourceRoot":"","sources":["../../../runtime/src/themeDoctor.ts"],"names":[],"mappings":"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","sourcesContent":["// The theme doctor: advice, never a block. A theme may do anything its trust level allows; the doctor\n// points at the lighter mechanism for what the theme does (a token before a component setting, a\n// component setting before CSS, CSS before a script) and at performance, accessibility and contrast\n// costs. Used by the import screen, the Theme Builder's full report and `waybetter store check`.\n// Pure. Every finding says where it is (a token path or the CSS rule), what was found and how to fix\n// it; the words are catalog keys (`wb.doctor.<id>.found` / `.fix`, English below), so a panel shows\n// them in the owner's language and the CLI prints the English.\n\nimport { contrastRatio } from './themeApi.ts';\nimport { ensureContrast, type ThemeSpec } from './themeSpec.ts';\n\nexport type DoctorAdvice = {\n /** `lighter`: a lighter mechanism would do; `performance`, `accessibility`, `contrast`: a cost. */\n kind: 'lighter' | 'performance' | 'accessibility' | 'contrast';\n /** Stable id of the finding, for tests (`css-radius`, `contrast-text`). */\n id: string;\n /** Its words in the catalog: `wb.doctor.<key>.found` and `wb.doctor.<key>.fix` (`css-radius`, `contrast`). */\n key: string;\n /** A token path of the theme (`colors.text`, `components.postText.fontSize`) or the CSS rule (`.card { border-radius }`). */\n where: string;\n /** Values for the `{name}` slots of the words. */\n params: Record<string, string>;\n /** English: what was found. */\n text: string;\n /** English: how to fix it. */\n fix: string;\n};\n\n/** The doctor's words in English (the source of `wb.doctor.*` in the catalogs). */\nexport const DOCTOR_MESSAGES = {\n 'wb.doctor.css-radius.found': 'This rule sets corner radii in CSS.',\n 'wb.doctor.css-radius.fix': 'Set them with the radius of a part under components, or shape.radius for all of them: no CSS needed, and it keeps working when the site renames its classes.',\n 'wb.doctor.css-bevel.found': 'This rule draws a bevelled edge with stacked inset shadows.',\n 'wb.doctor.css-bevel.fix': 'Use borderStyle \"bevel-out\" or \"bevel-in\" on a part under components: it draws the bevel from the theme’s colors.',\n 'wb.doctor.css-hide.found': 'This rule hides something with display: none.',\n 'wb.doctor.css-hide.fix': 'Use visible on a part under components (avatars, counts, names) or a layout variant such as sidebar: hidden; the owner can then switch it back.',\n 'wb.doctor.css-color.found': 'This rule uses a literal color.',\n 'wb.doctor.css-color.fix': 'Name a role (text, muted, accent, surface…) in colors or in a component, so the contrast pass covers it.',\n 'wb.doctor.css-font.found': 'This rule sets a font family.',\n 'wb.doctor.css-font.fix': 'Use font.roles: it sets the post, heading, numbers and mono faces without CSS.',\n 'wb.doctor.css-has.found': 'This rule uses :has(), which is matched again on every change of the page.',\n 'wb.doctor.css-has.fix': 'Keep :has() off rules that repeat down a timeline, or anchor the rule on a part.',\n 'wb.doctor.css-universal.found': 'This rule uses a universal selector (*), matched against every element.',\n 'wb.doctor.css-universal.fix': 'Anchor the rule on a part instead.',\n 'wb.doctor.css-blur.found': 'This rule blurs what is behind it (backdrop-filter), which is costly on parts that repeat.',\n 'wb.doctor.css-blur.fix': 'Use the glass fill of bars and panels: it is the cheaper place for blur.',\n 'wb.doctor.css-outline-none.found': 'This rule removes the focus outline, so keyboard focus is no longer visible.',\n 'wb.doctor.css-outline-none.fix': 'Use decor.focusRing to draw a visible focus ring instead.',\n 'wb.doctor.css-motion.found': 'This rule animates without checking for reduced motion.',\n 'wb.doctor.css-motion.fix': 'Keep motion inside @media (prefers-reduced-motion: no-preference), or let decor.motion do it for you.',\n 'wb.doctor.css-size.found': 'The CSS is more than 20,000 characters long.',\n 'wb.doctor.css-size.fix': 'The site recalculates styles with every rule; tokens and component settings compile to fewer rules.',\n 'wb.doctor.contrast.found': 'The {token} color was adjusted to stay readable ({ratio}:1 on the background).',\n 'wb.doctor.contrast.fix': 'Pick a lighter or darker {token} color; until then the theme is shown with the adjusted one.',\n 'wb.doctor.glass-post.found': 'Glass is set on posts, but it is not drawn: blur on every post of a timeline is too costly.',\n 'wb.doctor.glass-post.fix': 'Use a solid or tinted fill for posts and keep glass for bars and panels.',\n 'wb.doctor.small-post-text.found': 'Post text is {size}px, which is hard to read for many people.',\n 'wb.doctor.small-post-text.fix': 'Use 14px or more for the post text size, or raise the text size of the whole theme.',\n 'wb.doctor.nav-moved.found': 'The navigation is moved to the {position}, which changes where people look for it.',\n 'wb.doctor.nav-moved.fix': 'It only applies on wide panes, and the owner can turn layout changes off. Keep the default position unless the layout needs it.',\n} as const;\n\nconst fill = (text: string, params: Record<string, string>) => text.replace(/\\{(\\w+)\\}/g, (_, name: string) => params[name] ?? '');\n\ntype Rule = { id: string; kind: DoctorAdvice['kind']; test: RegExp; property: string };\n/** What a theme's free-form CSS does that a v2 group does without CSS. */\nconst CSS_RULES: Rule[] = [\n { id: 'css-radius', kind: 'lighter', test: /border-radius\\s*:/gi, property: 'border-radius' },\n { id: 'css-bevel', kind: 'lighter', test: /box-shadow\\s*:[^;{}]*inset[^;{}]*inset[^;{}]*inset/gi, property: 'box-shadow' },\n { id: 'css-hide', kind: 'lighter', test: /display\\s*:\\s*none/gi, property: 'display' },\n { id: 'css-color', kind: 'lighter', test: /(?:^|[;{\\s])(?:color|background(?:-color)?)\\s*:\\s*#[0-9a-f]{3,8}/gi, property: 'color' },\n { id: 'css-font', kind: 'lighter', test: /font-family\\s*:/gi, property: 'font-family' },\n { id: 'css-has', kind: 'performance', test: /:has\\(/gi, property: ':has()' },\n { id: 'css-universal', kind: 'performance', test: /(?:^|[\\s,>+~])\\*\\s*(?:[,{>+~:[]|$)/gm, property: '*' },\n { id: 'css-blur', kind: 'performance', test: /backdrop-filter\\s*:/gi, property: 'backdrop-filter' },\n { id: 'css-outline-none', kind: 'accessibility', test: /outline\\s*:\\s*(?:none|0)\\b/gi, property: 'outline' },\n { id: 'css-motion', kind: 'accessibility', test: /(?:animation|transition)\\s*:/gi, property: 'transition' },\n];\n\nconst MAX_PER_RULE = 25;\n\n/** The selector of the rule block a position of `css` is in (the text before its `{`), shortened. */\nfunction selectorAt(css: string, index: number): string {\n const open = css.lastIndexOf('{', index);\n if (open < 0) return '';\n const close = Math.max(css.lastIndexOf('}', open - 1), css.lastIndexOf(';', open - 1), css.lastIndexOf('{', open - 1));\n const selector = css.slice(close + 1, open).replace(/\\s+/g, ' ').trim();\n return selector.length > 80 ? `${selector.slice(0, 79)}…` : selector;\n}\n\nfunction advice(id: string, kind: DoctorAdvice['kind'], where: string, params: Record<string, string> = {}, key = id): DoctorAdvice {\n const words = DOCTOR_MESSAGES as Record<string, string>;\n return { kind, id, key, where, params, text: fill(words[`wb.doctor.${key}.found`]!, params), fix: fill(words[`wb.doctor.${key}.fix`]!, params) };\n}\n\n/** Advice for a theme document: every finding, with where it is. Empty when there is nothing to say. */\nexport function themeDoctor(theme: ThemeSpec): DoctorAdvice[] {\n const out: DoctorAdvice[] = [];\n const css = theme.css ?? '';\n if (css.trim()) {\n for (const rule of CSS_RULES) {\n const seen = new Set<string>();\n for (const match of css.matchAll(rule.test)) {\n const selector = selectorAt(css, match.index ?? 0) || 'css';\n const where = rule.id === 'css-universal' || rule.id === 'css-has' ? selector : `${selector} { ${rule.property} }`;\n if (seen.has(where)) continue;\n seen.add(where);\n out.push(advice(rule.id, rule.kind, where));\n if (seen.size >= MAX_PER_RULE) break;\n }\n }\n if (css.length > 20_000) out.push(advice('css-size', 'performance', 'css'));\n }\n if (theme.colors) {\n const fixed = ensureContrast(theme.colors);\n for (const role of ['text', 'muted', 'accent'] as const) {\n if (fixed[role] !== theme.colors[role]) {\n out.push(advice(`contrast-${role}`, 'contrast', `colors.${role}`, { token: role, ratio: contrastRatio(theme.colors[role], theme.colors.bg).toFixed(2) }, 'contrast'));\n }\n }\n }\n if (theme.components) {\n if (theme.components.post?.fill === 'glass') out.push(advice('glass-post', 'performance', 'components.post.fill'));\n const size = theme.components.postText?.fontSize;\n if (size !== undefined && size < 14) out.push(advice('small-post-text', 'accessibility', 'components.postText.fontSize', { size: String(size) }));\n }\n if (theme.variants?.nav === 'top' || theme.variants?.nav === 'bottom') {\n out.push(advice('nav-moved', 'accessibility', 'variants.nav', { position: theme.variants.nav }));\n }\n return out;\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** The viewport box popovers must stay in. */
|
|
2
|
+
export type Box = {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
right: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
};
|
|
8
|
+
export type Rect = {
|
|
9
|
+
left: number;
|
|
10
|
+
top: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
/** Attribute on a shifted layer (a static rule hides its caret, which points at the old anchor). */
|
|
15
|
+
export declare const CLAMPED_ATTR = "data-wb-clamped";
|
|
16
|
+
export declare const CLAMP_X = "--wb-clamp-x";
|
|
17
|
+
export declare const CLAMP_Y = "--wb-clamp-y";
|
|
18
|
+
/**
|
|
19
|
+
* Pure: the shift that brings `rect` inside `box`. A rect larger than the box keeps its start edge
|
|
20
|
+
* inside (left/top) and reports `overflowY`, so the caller can cap its height and let it scroll.
|
|
21
|
+
*/
|
|
22
|
+
export declare function clampShift(rect: Rect, box: Box): {
|
|
23
|
+
dx: number;
|
|
24
|
+
dy: number;
|
|
25
|
+
overflowY: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** The box: the viewport less `margin` on every side and the bar insets top and bottom. */
|
|
28
|
+
export declare function viewportBox(width: number, height: number, insets: {
|
|
29
|
+
top: number;
|
|
30
|
+
bottom: number;
|
|
31
|
+
}, margin?: number): Box;
|
|
32
|
+
/** The static rules the guard relies on (part of the theme sheet). */
|
|
33
|
+
export declare function layerGuardCss(): string;
|
|
34
|
+
export type LayerGuardOptions = {
|
|
35
|
+
/** The container the site renders its portals into (X: `#layers`). */
|
|
36
|
+
root: string;
|
|
37
|
+
/** Inside a new layer, the element that is positioned and should be shifted (first match wins). */
|
|
38
|
+
targets: string[];
|
|
39
|
+
/** Bar insets in px (read from the theme's `--wb-bar-inset-top/bottom` by default). */
|
|
40
|
+
insets?: () => {
|
|
41
|
+
top: number;
|
|
42
|
+
bottom: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** Watches the layer root; returns a stop function. Off in safe mode (the host never installs it then). */
|
|
46
|
+
export declare function installLayerGuard(win: Window, options: LayerGuardOptions): () => void;
|
|
47
|
+
export declare const VOID_ATTR = "data-wb-void";
|
|
48
|
+
/** Elements that make a part non-empty when they render. */
|
|
49
|
+
export declare const VISIBLE_INSIDE = "a,button,h2,img,svg,input,video,[role=\"heading\"],[data-testid=\"tweetText\"]";
|
|
50
|
+
/** The static rule the marker relies on. */
|
|
51
|
+
export declare const frameMarkerCss: () => string;
|
|
52
|
+
export type FrameMarkerOptions = {
|
|
53
|
+
/** Containers to watch (X: the sidebar column). */
|
|
54
|
+
scopes: string[];
|
|
55
|
+
/** Framed parts inside them (one selector list). */
|
|
56
|
+
parts: string;
|
|
57
|
+
};
|
|
58
|
+
/** Pure: whether `part` renders nothing (every candidate inside has no client rect). */
|
|
59
|
+
export declare function isVoid(part: Element): boolean;
|
|
60
|
+
export declare function installFrameMarker(win: Window, options: FrameMarkerOptions): () => void;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Theme engine v3, step 8 (.omc/plans/theme-engine-v3.md 3.5, 4.2): the layer guard. A site positions
|
|
2
|
+
// its popovers (menus, hover cards, tooltips) from an anchor; a theme that moves the anchor (a bottom
|
|
3
|
+
// taskbar, a top bar) moves them past the window edge. Static CSS cannot see where a portal lands, so
|
|
4
|
+
// this guard measures each new layer root once and shifts it back inside the viewport minus the bar
|
|
5
|
+
// insets. Bounded: it watches only the layer container's children (no subtree attribute observation
|
|
6
|
+
// of the page), measures in an idle callback, at most a few layers per frame, never timeline cells.
|
|
7
|
+
/** Attribute on a shifted layer (a static rule hides its caret, which points at the old anchor). */
|
|
8
|
+
export const CLAMPED_ATTR = 'data-wb-clamped';
|
|
9
|
+
export const CLAMP_X = '--wb-clamp-x';
|
|
10
|
+
export const CLAMP_Y = '--wb-clamp-y';
|
|
11
|
+
/**
|
|
12
|
+
* Pure: the shift that brings `rect` inside `box`. A rect larger than the box keeps its start edge
|
|
13
|
+
* inside (left/top) and reports `overflowY`, so the caller can cap its height and let it scroll.
|
|
14
|
+
*/
|
|
15
|
+
export function clampShift(rect, box) {
|
|
16
|
+
const shift = (start, size, min, max) => {
|
|
17
|
+
if (size >= max - min)
|
|
18
|
+
return min - start;
|
|
19
|
+
if (start < min)
|
|
20
|
+
return min - start;
|
|
21
|
+
if (start + size > max)
|
|
22
|
+
return max - (start + size);
|
|
23
|
+
return 0;
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
dx: Math.round(shift(rect.left, rect.width, box.left, box.right)),
|
|
27
|
+
dy: Math.round(shift(rect.top, rect.height, box.top, box.bottom)),
|
|
28
|
+
overflowY: rect.height > box.bottom - box.top,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** The box: the viewport less `margin` on every side and the bar insets top and bottom. */
|
|
32
|
+
export function viewportBox(width, height, insets, margin = 8) {
|
|
33
|
+
return { left: margin, top: margin + insets.top, right: width - margin, bottom: height - margin - insets.bottom };
|
|
34
|
+
}
|
|
35
|
+
/** The static rules the guard relies on (part of the theme sheet). */
|
|
36
|
+
export function layerGuardCss() {
|
|
37
|
+
return `[${CLAMPED_ATTR}]{translate:var(${CLAMP_X},0px) var(${CLAMP_Y},0px) !important}\n[${CLAMPED_ATTR}="scroll"]{max-height:var(--wb-clamp-h) !important;overflow-y:auto !important}`;
|
|
38
|
+
}
|
|
39
|
+
const px = (value) => { const n = parseFloat(value); return Number.isFinite(n) ? n : 0; };
|
|
40
|
+
/** Watches the layer root; returns a stop function. Off in safe mode (the host never installs it then). */
|
|
41
|
+
export function installLayerGuard(win, options) {
|
|
42
|
+
const doc = win.document;
|
|
43
|
+
const insets = options.insets ?? (() => {
|
|
44
|
+
const style = win.getComputedStyle(doc.documentElement);
|
|
45
|
+
return { top: px(style.getPropertyValue('--wb-bar-inset-top')), bottom: px(style.getPropertyValue('--wb-bar-inset-bottom')) };
|
|
46
|
+
});
|
|
47
|
+
const selector = options.targets.join(',');
|
|
48
|
+
const pending = new Set();
|
|
49
|
+
let scheduled = false;
|
|
50
|
+
const measure = () => {
|
|
51
|
+
scheduled = false;
|
|
52
|
+
let budget = 4;
|
|
53
|
+
for (const layer of [...pending]) {
|
|
54
|
+
if (budget-- <= 0)
|
|
55
|
+
break;
|
|
56
|
+
pending.delete(layer);
|
|
57
|
+
if (!layer.isConnected)
|
|
58
|
+
continue;
|
|
59
|
+
for (const target of layer.matches(selector) ? [layer] : [...layer.querySelectorAll(selector)].slice(0, 2))
|
|
60
|
+
clamp(target);
|
|
61
|
+
}
|
|
62
|
+
if (pending.size)
|
|
63
|
+
schedule();
|
|
64
|
+
};
|
|
65
|
+
// A measured popover is watched for the site moving it again (its inline position): only its own
|
|
66
|
+
// style attribute, and only position changes count (our own clamp variables change it too).
|
|
67
|
+
const placed = new WeakMap();
|
|
68
|
+
const position = (el) => `${el.style.left}|${el.style.top}|${el.style.right}|${el.style.bottom}|${el.style.transform}`;
|
|
69
|
+
const Observer = win.MutationObserver;
|
|
70
|
+
const moved = new Observer((records) => {
|
|
71
|
+
for (const record of records) {
|
|
72
|
+
const el = record.target;
|
|
73
|
+
if (el.isConnected && placed.get(el) !== position(el)) {
|
|
74
|
+
pending.add(el);
|
|
75
|
+
schedule();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// A popover that grows (a theme's font, content loading in) is measured again too.
|
|
80
|
+
const Resize = win.ResizeObserver;
|
|
81
|
+
const sized = Resize ? new Resize(entries => { for (const entry of entries) {
|
|
82
|
+
const el = entry.target;
|
|
83
|
+
if (el.isConnected) {
|
|
84
|
+
pending.add(el);
|
|
85
|
+
schedule();
|
|
86
|
+
}
|
|
87
|
+
} }) : null;
|
|
88
|
+
const clamp = (el) => {
|
|
89
|
+
if (!placed.has(el)) {
|
|
90
|
+
moved.observe(el, { attributes: true, attributeFilter: ['style'] });
|
|
91
|
+
sized?.observe(el);
|
|
92
|
+
}
|
|
93
|
+
placed.set(el, position(el));
|
|
94
|
+
// Measure without our own shift.
|
|
95
|
+
el.style.removeProperty(CLAMP_X);
|
|
96
|
+
el.style.removeProperty(CLAMP_Y);
|
|
97
|
+
const r = el.getBoundingClientRect();
|
|
98
|
+
if (r.width < 1 || r.height < 1)
|
|
99
|
+
return;
|
|
100
|
+
const box = viewportBox(win.innerWidth, win.innerHeight, insets());
|
|
101
|
+
const { dx, dy, overflowY } = clampShift({ left: r.left, top: r.top, width: r.width, height: r.height }, box);
|
|
102
|
+
if (!dx && !dy && !overflowY) {
|
|
103
|
+
el.removeAttribute(CLAMPED_ATTR);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
el.style.setProperty(CLAMP_X, `${dx}px`);
|
|
107
|
+
el.style.setProperty(CLAMP_Y, `${dy}px`);
|
|
108
|
+
if (overflowY)
|
|
109
|
+
el.style.setProperty('--wb-clamp-h', `${Math.max(80, box.bottom - box.top)}px`);
|
|
110
|
+
el.setAttribute(CLAMPED_ATTR, overflowY ? 'scroll' : '');
|
|
111
|
+
};
|
|
112
|
+
const schedule = () => {
|
|
113
|
+
if (scheduled)
|
|
114
|
+
return;
|
|
115
|
+
scheduled = true;
|
|
116
|
+
const idle = win.requestIdleCallback;
|
|
117
|
+
if (idle)
|
|
118
|
+
idle.call(win, measure, { timeout: 120 });
|
|
119
|
+
else
|
|
120
|
+
win.setTimeout(measure, 16);
|
|
121
|
+
};
|
|
122
|
+
// Only a node that is or holds a popover is worth a measurement (typing in a dialog inserts nodes too).
|
|
123
|
+
const queue = (node) => {
|
|
124
|
+
if (!node || node.nodeType !== 1)
|
|
125
|
+
return;
|
|
126
|
+
const el = node;
|
|
127
|
+
if (!el.matches(selector) && !el.querySelector(selector))
|
|
128
|
+
return;
|
|
129
|
+
pending.add(el);
|
|
130
|
+
schedule();
|
|
131
|
+
// The site positions a portal after it mounts (a layout effect, a measured anchor): measure again
|
|
132
|
+
// once it has settled. Two late passes, no observer on the page's style attributes.
|
|
133
|
+
for (const delay of [120, 450])
|
|
134
|
+
win.setTimeout(() => { if (el.isConnected) {
|
|
135
|
+
pending.add(el);
|
|
136
|
+
schedule();
|
|
137
|
+
} }, delay);
|
|
138
|
+
};
|
|
139
|
+
let observer = null;
|
|
140
|
+
let layers = null;
|
|
141
|
+
const attach = () => {
|
|
142
|
+
const found = doc.querySelector(options.root);
|
|
143
|
+
if (found === layers)
|
|
144
|
+
return;
|
|
145
|
+
observer?.disconnect();
|
|
146
|
+
layers = found;
|
|
147
|
+
if (!found)
|
|
148
|
+
return;
|
|
149
|
+
// Children of the layer root and their children: a portal mounts its positioned box a level or two down.
|
|
150
|
+
const next = new Observer((records) => { for (const record of records)
|
|
151
|
+
for (const node of record.addedNodes)
|
|
152
|
+
queue(node.nodeType === 1 ? (node.closest(selector) ?? node) : null); });
|
|
153
|
+
next.observe(found, { childList: true, subtree: true });
|
|
154
|
+
observer = next;
|
|
155
|
+
for (const child of found.children)
|
|
156
|
+
queue(child);
|
|
157
|
+
};
|
|
158
|
+
attach();
|
|
159
|
+
const retry = win.setInterval(attach, 2000);
|
|
160
|
+
const onResize = () => { if (layers)
|
|
161
|
+
for (const child of layers.children)
|
|
162
|
+
queue(child); };
|
|
163
|
+
win.addEventListener('resize', onResize);
|
|
164
|
+
return () => { observer?.disconnect(); moved.disconnect(); sized?.disconnect(); win.clearInterval(retry); win.removeEventListener('resize', onResize); };
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------------------------
|
|
167
|
+
// Frame marker (theme engine v3 4.3, step 10): a framed part (a card, a panel) whose content is all
|
|
168
|
+
// hidden (a feature switch, a plugin, the site itself) collapses instead of leaving an empty frame.
|
|
169
|
+
// Static CSS cannot see `display:none` inside, so the marker sets `data-wb-void` and a static rule
|
|
170
|
+
// hides the part. Watched by a ResizeObserver per framed part (a hidden module changes the part's size)
|
|
171
|
+
// and a child-list observer on the scopes (new parts), never by a timer.
|
|
172
|
+
export const VOID_ATTR = 'data-wb-void';
|
|
173
|
+
/** Elements that make a part non-empty when they render. */
|
|
174
|
+
export const VISIBLE_INSIDE = 'a,button,h2,img,svg,input,video,[role="heading"],[data-testid="tweetText"]';
|
|
175
|
+
/** The static rule the marker relies on. */
|
|
176
|
+
export const frameMarkerCss = () => `:root:root [${VOID_ATTR}]{display:none !important}`;
|
|
177
|
+
/** Pure: whether `part` renders nothing (every candidate inside has no client rect). */
|
|
178
|
+
export function isVoid(part) {
|
|
179
|
+
return ![...part.querySelectorAll(VISIBLE_INSIDE)].some(el => el.getClientRects().length > 0);
|
|
180
|
+
}
|
|
181
|
+
export function installFrameMarker(win, options) {
|
|
182
|
+
const doc = win.document;
|
|
183
|
+
const Resize = win.ResizeObserver;
|
|
184
|
+
const Observer = win.MutationObserver;
|
|
185
|
+
const watched = new WeakSet();
|
|
186
|
+
let frame = 0;
|
|
187
|
+
const check = (part) => {
|
|
188
|
+
// A void part is display:none itself: lift the mark to see whether something came back.
|
|
189
|
+
const was = part.hasAttribute(VOID_ATTR);
|
|
190
|
+
if (was)
|
|
191
|
+
part.removeAttribute(VOID_ATTR);
|
|
192
|
+
if (isVoid(part))
|
|
193
|
+
part.setAttribute(VOID_ATTR, '');
|
|
194
|
+
};
|
|
195
|
+
const sweep = () => {
|
|
196
|
+
frame = 0;
|
|
197
|
+
for (const scope of options.scopes) {
|
|
198
|
+
const root = doc.querySelector(scope);
|
|
199
|
+
if (!root)
|
|
200
|
+
continue;
|
|
201
|
+
for (const part of root.querySelectorAll(options.parts)) {
|
|
202
|
+
if (!watched.has(part)) {
|
|
203
|
+
watched.add(part);
|
|
204
|
+
sized?.observe(part);
|
|
205
|
+
}
|
|
206
|
+
check(part);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const schedule = () => { if (!frame)
|
|
211
|
+
frame = win.requestAnimationFrame(sweep); };
|
|
212
|
+
const sized = Resize ? new Resize(schedule) : null;
|
|
213
|
+
// Inner modules change while the part keeps its size only when hidden content was already 0 tall:
|
|
214
|
+
// child-list changes under the scopes cover those, and new parts.
|
|
215
|
+
const added = new Observer(schedule);
|
|
216
|
+
const roots = new WeakSet();
|
|
217
|
+
const attach = () => {
|
|
218
|
+
for (const scope of options.scopes) {
|
|
219
|
+
const root = doc.querySelector(scope);
|
|
220
|
+
if (root && !roots.has(root)) {
|
|
221
|
+
roots.add(root);
|
|
222
|
+
added.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
|
|
223
|
+
schedule();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
attach();
|
|
228
|
+
// Scopes mount late and are replaced on navigation: looked up again every 2 s (one querySelector per
|
|
229
|
+
// scope; the per-part work is all observer-driven).
|
|
230
|
+
const retry = win.setInterval(attach, 2000);
|
|
231
|
+
return () => { sized?.disconnect(); added.disconnect(); win.clearInterval(retry); if (frame)
|
|
232
|
+
win.cancelAnimationFrame(frame); for (const el of doc.querySelectorAll(`[${VOID_ATTR}]`))
|
|
233
|
+
el.removeAttribute(VOID_ATTR); };
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=themeGuards.js.map
|