diff --git a/assets/icons/brand/android-monochrome.png b/assets/icons/brand/android-monochrome.png index fe88567..d551201 100644 Binary files a/assets/icons/brand/android-monochrome.png and b/assets/icons/brand/android-monochrome.png differ diff --git a/assets/icons/brand/build-android-res.mjs b/assets/icons/brand/build-android-res.mjs index b237389..5136b56 100644 --- a/assets/icons/brand/build-android-res.mjs +++ b/assets/icons/brand/build-android-res.mjs @@ -19,6 +19,11 @@ const uri = p => 'data:image/png;base64,' + readFileSync(p).toString('base64'); const BG = uri(join(ROOT, 'ui', 'assets', 'icon-background.png')); const FG = uri(join(ROOT, 'ui', 'assets', 'icon-foreground.png')); const MONO = uri(join(DIR, 'android-monochrome.png')); +// The full-bleed master (square tile + master mark) — the legacy SQUARE launcher (API < 26) +// uses it so old Android shows a large mark rather than the safe-zone foreground shrunk into +// the middle. The round legacy icon keeps the safe-zone composite (a round mask would clip +// the master's corner ✻). +const MASTER = uri(join(ROOT, 'ui', 'assets', 'icon.png')); // density: [adaptive-layer px (108 dp), legacy launcher px (48 dp)] const D = { ldpi: [81, 36], mdpi: [108, 48], hdpi: [162, 72], xhdpi: [216, 96], xxhdpi: [324, 144], xxxhdpi: [432, 192] }; @@ -38,8 +43,9 @@ for (const [d, [fg, lg]] of Object.entries(D)) { writeFileSync(join(dir, 'ic_launcher_background.png'), await shot(im(BG, fg), fg, false)); writeFileSync(join(dir, 'ic_launcher_foreground.png'), await shot(im(FG, fg), fg, true)); writeFileSync(join(dir, 'ic_launcher_monochrome.png'), await shot(im(MONO, fg), fg, true)); - const comp = `
${im(BG, lg)}
${im(FG, lg)}
`; - writeFileSync(join(dir, 'ic_launcher.png'), await shot(comp, lg, false)); + // Square launcher: the full-bleed master (large mark). Round launcher: the safe-zone + // composite, circle-clipped (the master's corner ✻ would be clipped by the round mask). + writeFileSync(join(dir, 'ic_launcher.png'), await shot(im(MASTER, lg), lg, false)); const round = `
${im(BG, lg)}
${im(FG, lg)}
`; writeFileSync(join(dir, 'ic_launcher_round.png'), await shot(round, lg, true)); console.log('mipmap-' + d, 'ok'); diff --git a/assets/icons/brand/build-icon.mjs b/assets/icons/brand/build-icon.mjs index 7d5b1ea..bf54c03 100644 --- a/assets/icons/brand/build-icon.mjs +++ b/assets/icons/brand/build-icon.mjs @@ -33,8 +33,12 @@ const SHADE = 0.15; // black, this alpha bottom-left -> transp // The mark, placed for the standalone master (bottom-right biased, full-bleed): const MASTER = { lh: 0.6055, lc: [0.4814, 0.4922], sd: 0.1729, sc: [0.7881, 0.7881] }; // The mark, placed for the Android foreground layer (centred-ish inside the 61% safe -// zone, nudged right 8% / down 3% for optical balance under the round mask; smaller ✻): -const FOREGROUND = { lh: 0.5391, lc: [0.5234, 0.4951], sd: 0.1318, sc: [0.7627, 0.7266] }; +// zone, nudged right 8% / down 3% for optical balance under the round mask; smaller ✻). +// Every value is the earlier placement scaled 0.75 about the icon centre (0.5, 0.5): the +// whole mark is 25% smaller, its «Э»↔✻ arrangement unchanged, so the ✻ that used to graze +// the round mask now sits well inside the safe zone. Round-mask launchers (adaptive + the +// legacy round icon) get more breathing room; the full-bleed master is untouched. +const FOREGROUND = { lh: 0.4043, lc: [0.5176, 0.4963], sd: 0.0989, sc: [0.6970, 0.6700] }; const PALETTE = { light: { wood: '#e6b053', ink: '#5a3a12', grain: '#d8a54e' }, diff --git a/assets/icons/brand/icon-dark-android-foreground.png b/assets/icons/brand/icon-dark-android-foreground.png index a4a22d6..02de1ff 100644 Binary files a/assets/icons/brand/icon-dark-android-foreground.png and b/assets/icons/brand/icon-dark-android-foreground.png differ diff --git a/assets/icons/brand/icon-light-android-foreground.png b/assets/icons/brand/icon-light-android-foreground.png index 12b4a8e..123fd76 100644 Binary files a/assets/icons/brand/icon-light-android-foreground.png and b/assets/icons/brand/icon-light-android-foreground.png differ diff --git a/docs/ICONS.md b/docs/ICONS.md index 12ff0d0..17a787d 100644 --- a/docs/ICONS.md +++ b/docs/ICONS.md @@ -54,7 +54,10 @@ already fill the 108 dp canvas (foreground inside the 61 % safe zone, background full-bleed), so they must be placed with **no inset**, and we add the **monochrome** themed layer, neither of which `capacitor-assets` does. Per density mdpi→xxxhdpi (+ldpi): -- `mipmap-*/ic_launcher.png` / `ic_launcher_round.png` — legacy square / round (composite) +- `mipmap-*/ic_launcher.png` — legacy square (< API 26): the **full-bleed master** (large + mark), so old square launchers do not shrink it into the safe zone +- `mipmap-*/ic_launcher_round.png` — legacy round (< API 26): the safe-zone **composite**, + circle-clipped (a round mask would clip the master's corner ✻) - `mipmap-*/ic_launcher_foreground.png` + `ic_launcher_background.png` — adaptive layers - `mipmap-*/ic_launcher_monochrome.png` — themed layer - `mipmap-anydpi-v26/ic_launcher.xml` + `ic_launcher_round.xml` — **no ``**, with diff --git a/docs/ICON_BRANDBOOK.md b/docs/ICON_BRANDBOOK.md index 0a0e713..4bb0a04 100644 --- a/docs/ICON_BRANDBOOK.md +++ b/docs/ICON_BRANDBOOK.md @@ -123,12 +123,13 @@ So Android is **not** the full-bleed master — it is built as two layers: and square** (no rounded corners; the OS supplies the shape). It fills the whole 108 dp layer, so the sacrificial outer ring is just more wood. - **Foreground** — **only the «Э» + ✻**, transparent, **re-placed** to sit safely inside - the mask: - - «Э»: box height **53.9 %**, box center **52.3 % / 49.5 %**. - - ✻: outer diameter **13.2 %** (smaller than the master's), center **76.3 % / 72.7 %**, + the mask. The whole mark is the master's optical placement (nudged right 8 % / down 3 %) + scaled **0.75 about the icon centre** — **25 % smaller**, its «Э» ↔ ✻ arrangement + unchanged — so the ✻ that used to graze the round mask now sits well inside the **61 % + safe zone** with margin to spare: + - «Э»: box height **40.4 %**, box center **51.8 % / 49.6 %**. + - ✻: outer diameter **9.9 %** (smaller than the master's), center **69.7 % / 67.0 %**, tucked closer to the letter. - - The mark is centred, then nudged **right 8 % / down 3 %** so it reads optically - centred under the round mask, and the whole group fits within the **61 % safe zone**. Everything else (palette, grain, gradients, star construction) is identical to the master. diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 48fdd77..8fb8bd2 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 9024f34..63228c3 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png index f83e989..ddaafc8 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 8909874..16c1180 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png index a4ff54b..c546019 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png index eb08478..4fe946d 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png index 1d171d7..5f3ae18 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png index 1be6faa..168bd44 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index ee098ab..9a2a994 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index e2a7460..a648a5a 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png index 497be0f..1adbd1a 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 05d4e1c..1ed63a7 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 8e448c3..94b3c96 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 69a8967..7257423 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png index b0e40ea..5b6f910 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index b517260..97bb510 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 88ece49..2218429 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 15a62cf..032d0f3 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png index 1eae863..57d6eb4 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 38ba881..9c168d0 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index b705035..6dffad5 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index 717737b..7c13b89 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png index c22f4e4..426fadd 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 6133338..abac6c3 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/ui/assets/icon-foreground.png b/ui/assets/icon-foreground.png index 12b4a8e..123fd76 100644 Binary files a/ui/assets/icon-foreground.png and b/ui/assets/icon-foreground.png differ diff --git a/ui/e2e/native.spec.ts b/ui/e2e/native.spec.ts index d3d8666..4fc1f5b 100644 --- a/ui/e2e/native.spec.ts +++ b/ui/e2e/native.spec.ts @@ -43,20 +43,22 @@ test.describe('native offline-first', () => { await page.goto('/'); await expectOfflineGuestLobby(page); - // Play a local English vs_ai game with a pinned bag seed (deals the rack NEWYMAO). The dictionary - // comes from the APK's bundled tier (./dict/scrabble_en@dev.dawg served from dist-e2e), so no - // network is needed — this is the device-local guest playing with zero connectivity. + // Play a local Erudit vs_ai game with a pinned bag seed (deals the rack ОЖЬЯНАО). Erudit is the + // only variant the profileless offline guest is offered (the new-account default), so the single + // `.variant` click both selects it and asserts nothing else is on offer. The dictionary comes from + // the APK's bundled tier (./dict/ru_erudit@dev.dawg served from dist-e2e), so no network is needed + // — this is the device-local guest playing with zero connectivity. await page.evaluate(() => (window as unknown as { __mock: { setLocalSeed(s: string): void } }).__mock.setLocalSeed('1')); await page.locator('button.tab').nth(0).click(); - await page.locator('.variant', { hasText: 'Scrabble' }).click(); + await page.locator('.variant').click(); await page.locator('button.invite').click(); await expect(page.locator('[data-cell]').first()).toBeVisible(); - // The human plays WAY horizontally across the centre (7,5)-(7,7); the local robot replies with a - // real move (which needs the bundled dictionary), so the board gains tiles beyond WAY's three. - await placeTile(page, 'W', 7, 5); - await placeTile(page, 'A', 7, 6); - await placeTile(page, 'Y', 7, 7); + // The human plays НОЖ horizontally across the centre (7,5)-(7,7); the local robot replies with a + // real move (which needs the bundled dictionary), so the board gains tiles beyond НОЖ's three. + await placeTile(page, 'Н', 7, 5); + await placeTile(page, 'О', 7, 6); + await placeTile(page, 'Ж', 7, 7); await expect(page.locator('[data-cell].pending')).toHaveCount(3); await page.locator('.make').click(); await expect(async () => { @@ -95,14 +97,15 @@ test.describe('native offline-first', () => { await expectOfflineGuestLobby(page); // New game -> the offline mode selector offers "with friends" (pass-and-play) to the local guest. - // A minimal create: set the mandatory host (master) PIN, decline a seat, English, two open seats. + // A minimal create: set the mandatory host (master) PIN, decline a seat, the sole offered variant + // (Erudit — the profileless guest's default), two open seats. await page.locator('button.tab').nth(0).click(); await page.getByRole('button', { name: /With friends|друзьями/i }).click(); await page.locator('.hostpin .plink').click(); await typePin(page, '9999'); await typePin(page, '9999'); await page.getByRole('button', { name: /^(No|Нет)$/ }).click(); - await page.locator('.variant', { hasText: 'Scrabble' }).click(); + await page.locator('.variant').click(); await page.locator('.pname').nth(0).fill('Ann'); await page.locator('.pname').nth(1).fill('Bob'); await page.locator('button.invite').click(); diff --git a/ui/public/icon-maskable-512-dark.png b/ui/public/icon-maskable-512-dark.png index 9b6d045..24389fa 100644 Binary files a/ui/public/icon-maskable-512-dark.png and b/ui/public/icon-maskable-512-dark.png differ diff --git a/ui/public/icon-maskable-512.png b/ui/public/icon-maskable-512.png index ee6e488..a6aad67 100644 Binary files a/ui/public/icon-maskable-512.png and b/ui/public/icon-maskable-512.png differ diff --git a/ui/src/lib/variants.test.ts b/ui/src/lib/variants.test.ts index e8a208d..9558a62 100644 --- a/ui/src/lib/variants.test.ts +++ b/ui/src/lib/variants.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { ALL_VARIANTS, + DEFAULT_VARIANTS, availableVariants, supportsMultipleWordsToggle, multipleWordsForRequest, @@ -16,9 +17,11 @@ describe('ALL_VARIANTS', () => { }); describe('availableVariants', () => { - it('is ungated (all variants) for an empty or absent preference set', () => { - expect(availableVariants([])).toEqual(ALL_VARIANTS); - expect(availableVariants(undefined)).toEqual(ALL_VARIANTS); + it('falls back to the Erudit-only default for an empty or absent preference set', () => { + const fallback = ALL_VARIANTS.filter((v) => DEFAULT_VARIANTS.includes(v.id)); + expect(fallback.map((v) => v.id)).toEqual(['erudit_ru']); + expect(availableVariants([])).toEqual(fallback); + expect(availableVariants(undefined)).toEqual(fallback); }); it('offers only the preferred variants', () => { diff --git a/ui/src/lib/variants.ts b/ui/src/lib/variants.ts index 5981007..b520082 100644 --- a/ui/src/lib/variants.ts +++ b/ui/src/lib/variants.ts @@ -62,13 +62,23 @@ export function usesStarBlank(v: Variant): boolean { return v === 'erudit_ru'; } -// availableVariants gates ALL_VARIANTS by the player's variant preferences (the set -// they enabled in Settings). An empty or absent set is ungated (returns every variant) -// — a safety fallback; a real profile always carries at least one preference. +// DEFAULT_VARIANTS is the variant set a player sees before any preference is stored — a fresh +// or offline native client whose profile has not been synced yet (the device-local guest boots +// with no profile at all). It mirrors the backend's new-account default (the account service +// seeds Erudit only), so the local guest and a later synced account agree on what is enabled. +// Every dictionary is still bundled; the other variants are simply off until the player turns +// them on in Settings. +export const DEFAULT_VARIANTS: Variant[] = ['erudit_ru']; + +// availableVariants gates ALL_VARIANTS by the player's variant preferences (the set they +// enabled in Settings). An empty or absent set falls back to DEFAULT_VARIANTS rather than every +// variant: a real profile always carries at least one preference, and a profileless client (a +// fresh offline native launch) must match the server's Erudit-only default instead of exposing +// the English game before the player opts in. export function availableVariants(preferences: Variant[] | undefined): VariantOption[] { const prefs = preferences ?? []; - if (prefs.length === 0) return ALL_VARIANTS; - return ALL_VARIANTS.filter((v) => prefs.includes(v.id)); + const effective = prefs.length === 0 ? DEFAULT_VARIANTS : prefs; + return ALL_VARIANTS.filter((v) => effective.includes(v.id)); } // supportsMultipleWordsToggle reports whether the New Game "multiple words per turn" toggle