Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extensions/lingoflash/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# LingoFlash Browser Extension v1.6.2
# LingoFlash Browser Extension v1.6.3

A Manifest V3 WebExtension for Chrome-compatible browsers and Safari.

Expand Down Expand Up @@ -47,7 +47,7 @@ npm run extension:build
Output:

- `artifacts/browser-extension/lingoflash/`
- `artifacts/browser-extension/lingoflash-extension-v1.6.2.zip`
- `artifacts/browser-extension/lingoflash-extension-v1.6.3.zip`

## Chrome installation

Expand Down
2 changes: 1 addition & 1 deletion extensions/lingoflash/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "LingoFlash — Translate & Save",
"short_name": "LingoFlash",
"version": "1.6.2",
"version": "1.6.3",
"description": "Translate selected text inline, or create and save one LingoFlash flashcard with one AI request.",
"permissions": [
"activeTab",
Expand Down
3 changes: 1 addition & 2 deletions extensions/lingoflash/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
* { box-sizing: border-box; }
body {
margin: 0;
width: 380px;
max-width: 100vw;
min-width: 380px;
background: radial-gradient(circle at 15% 0%, #163852 0%, transparent 45%), radial-gradient(circle at 85% 100%, #0e2b3d 0%, transparent 45%), #07111f;
}
button, textarea, select { font: inherit; }
Expand Down
2 changes: 1 addition & 1 deletion extensions/lingoflash/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<header class="brand">
<img src="icons/icon-48.png" width="42" height="42" alt="">
<div>
<p class="eyebrow">LINGOFLASH <span aria-label="Extension version">v1.6.2</span></p>
<p class="eyebrow">LINGOFLASH <span aria-label="Extension version">v1.6.3</span></p>
<h1>Dịch ngay tại trang</h1>
</div>
</header>
Expand Down
2 changes: 1 addition & 1 deletion extensions/lingoflash/tests/background.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const createWorkerContext = async ({
onStartup: events.startup,
onMessage: events.messages,
getManifest() {
return { version: '1.6.2' };
return { version: '1.6.3' };
},
sendMessage: message => {
calls.push({ type: 'runtime.sendMessage', message });
Expand Down
2 changes: 1 addition & 1 deletion extensions/lingoflash/tests/manifest.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const manifest = JSON.parse(
);

test('keeps Chrome manifest description within the published limit', () => {
assert.equal(manifest.version, '1.6.2');
assert.equal(manifest.version, '1.6.3');
assert.equal(manifest.background?.service_worker, 'background.js');
assert.equal(manifest.options_ui?.page, 'options.html');
assert.deepEqual(manifest.host_permissions, [
Expand Down
5 changes: 3 additions & 2 deletions extensions/lingoflash/tests/popup.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,9 @@ test('hides Chrome shortcut settings on browsers that cannot open them', async (
assert.equal(popup.elements.get('open-shortcuts').hidden, true);
});

test('popup styles reflow at narrow widths and keep helper text WCAG-readable', () => {
assert.doesNotMatch(popupCssSource, /min-width:\s*380px/);
test('popup keeps its designed width and reflows fallback layouts', () => {
assert.match(popupCssSource, /body\s*\{[^}]*min-width:\s*380px/);
assert.doesNotMatch(popupCssSource, /max-width:\s*100vw/);
assert.match(popupCssSource, /max-width:\s*340px[\s\S]*\.config-grid[\s\S]*grid-template-columns:\s*1fr/);
assert.match(popupCssSource, /max-width:\s*340px[\s\S]*\.actions[\s\S]*grid-template-columns:\s*1fr/);
assert.match(popupCssSource, /textarea::placeholder\s*\{\s*color:\s*#94a3b8/);
Expand Down
4 changes: 2 additions & 2 deletions extensions/lingoflash/tests/release-check.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ const runCheckWithMutation = async (file, mutate, expectedMessage = `${file} ver
};

test('release guard rejects popup and README version drift', async () => {
await runCheckWithMutation('popup.html', source => source.replace('v1.6.2', 'v1.6.3'));
await runCheckWithMutation('README.md', source => source.replace('v1.6.2', 'v1.6.3'));
await runCheckWithMutation('popup.html', source => source.replace('v1.6.3', 'v1.6.4'));
await runCheckWithMutation('README.md', source => source.replace('v1.6.3', 'v1.6.4'));
});

test('release guard rejects an extension that drops the v3 nonce contract', async () => {
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-browser-extension.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ for (const permission of manifest.permissions ?? []) if (!allowedPermissions.has
for (const permission of allowedPermissions) if (!(manifest.permissions ?? []).includes(permission)) fail(`missing permission: ${permission}`);
const command = manifest.commands?.['translate-selection'];
if (!command?.suggested_key?.default || !command.suggested_key.mac) fail('keyboard shortcut suggestions must cover default and macOS.');
if (manifest.version !== '1.6.2') fail('manifest must publish the polished v1.6 extension as v1.6.2.');
if (manifest.version !== '1.6.3') fail('manifest must publish the polished v1.6 extension as v1.6.3.');
if (JSON.stringify([...(manifest.optional_host_permissions ?? [])].sort()) !== JSON.stringify(optionalHostPermissions)) {
fail('optional_host_permissions must contain only the explicit http(s) site opt-in patterns.');
}
Expand Down
24 changes: 24 additions & 0 deletions src/components/Flashcard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ describe('Flashcard mobile controls', () => {
expect(html).toContain('data-translation-reveal="true"');
});

it('renders markdown emphasis in the memory hook', () => {
const html = renderToStaticMarkup(
<Flashcard
data={{
id: 'mnemonic-format',
word: 'brush up on',
translation: 'ôn tập',
explanation: 'Review something again.',
phonetic: '/brʌʃ ʌp ɒn/',
emoji: '📚',
category: 'Study',
audioUrl: null,
imageUrl: null,
mnemonic: '“Bờ rát úp” — **bờ** tường **rát** nên phải **úp** mặt vào sách để **ôn tập**.',
}}
initialSide="back"
/>,
);

expect(html).toContain('<strong');
expect(html).toContain('>bờ</strong>');
expect(html).not.toContain('**bờ**');
});

it('uses the same centered minimal action for returning to English', () => {
const html = renderToStaticMarkup(
<Flashcard
Expand Down
5 changes: 2 additions & 3 deletions src/components/flashcard/CardMnemonicSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useState } from 'react';
import type { CardData } from '../../types/card';
import { translateText } from '../../lib/gemini';
import { triggerHaptic } from '../../lib/haptics';
import { RichVietnameseExplanation } from './RichVietnameseExplanation';

interface CardMnemonicSectionProps {
card: CardData;
Expand Down Expand Up @@ -81,9 +82,7 @@ Rules:
{card.mnemonic ? (
<div className="flex items-start gap-2.5">
<Lightbulb size={16} className="mt-0.5 shrink-0 animate-pulse text-amber-600 drop-shadow-xs dark:text-amber-400" />
<p className="text-xs font-semibold leading-relaxed text-amber-950 dark:text-amber-100/95">
{card.mnemonic}
</p>
<RichVietnameseExplanation value={card.mnemonic} />
</div>
) : (
<div className="flex flex-col items-center justify-center py-1">
Expand Down
10 changes: 10 additions & 0 deletions src/components/shell/AppFooter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import { AppFooter } from './AppFooter';

const indexCssSource = readFileSync(fileURLToPath(new URL('../../index.css', import.meta.url)), 'utf8');

describe('AppFooter', () => {
it('renders online library status without announcing its decorative dot', () => {
const html = renderToStaticMarkup(
Expand All @@ -13,11 +17,17 @@ describe('AppFooter', () => {
);

expect(html).toContain('LIBRARY: 12 WORDS');
expect(html).toContain('app-footer-bar');
expect(html).toContain('STATUS:');
expect(html).toContain('Online');
expect(html).toContain('aria-hidden="true"');
});

it('gives the library status strip a raised surface and a visible edge', () => {
expect(indexCssSource).toMatch(/\.app-footer-bar\s*\{[^}]*border-top:\s*1px\s+solid\s+var\(--sf-border\)/s);
expect(indexCssSource).toMatch(/\.app-footer-bar\s*\{[^}]*background:\s*var\(--sf-surface-raised\)/s);
});

it('preserves the cache fallback labels', () => {
const offlineHtml = renderToStaticMarkup(
<AppFooter
Expand Down
2 changes: 1 addition & 1 deletion src/components/shell/AppFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function AppFooter({ viewMode, libraryCountLabel, syncStatus }: AppFooter
: 'bg-amber-500';

return (
<footer className={`${viewMode === 'study' ? 'hidden' : 'hidden lg:flex'} h-9 relative px-8 items-center justify-between text-[11px] font-bold text-[var(--sf-text-muted)] flex-shrink-0 z-10 transition-colors`}>
<footer className={`app-footer-bar ${viewMode === 'study' ? 'hidden' : 'hidden lg:flex'} h-9 relative px-8 items-center justify-between text-[11px] font-bold text-[var(--sf-text-muted)] flex-shrink-0 z-10 transition-colors`}>
<div className="flex gap-6 uppercase tracking-wider">
<span>LIBRARY: {libraryCountLabel}</span>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/components/shell/FloatingMobileNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe('FloatingMobileNav', () => {
expect(html).toContain('Library');
expect(html).toContain('Progress');
expect(html).toContain('data-shell-layer="mobile"');
expect(html).toContain('premium-surface');
expect(html).toContain('lg:hidden');
expect(html).not.toContain('md:hidden');
expect(html).toContain('data-shell-active="true"');
expect(html).not.toContain('aria-label="Open Practice Mode"');
});
Expand Down
2 changes: 1 addition & 1 deletion src/components/shell/FloatingMobileNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function FloatingMobileNav({
return (
<nav
data-shell-layer="mobile"
className="premium-surface fixed bottom-4 inset-x-4 z-40 mx-auto flex max-w-md items-center justify-around rounded-full p-1.5 md:hidden"
className="premium-surface fixed bottom-4 inset-x-4 z-40 mx-auto flex max-w-md items-center justify-around rounded-full p-1.5 lg:hidden"
aria-label="Mobile navigation bar"
>
{/* Home / Landing Tab */}
Expand Down
6 changes: 6 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,12 @@
box-shadow: inset 0 1px 0 var(--sf-edge-highlight), 0 26px 64px -34px var(--sf-shadow);
}

.app-footer-bar {
border-top: 1px solid var(--sf-border);
background: var(--sf-surface-raised);
box-shadow: inset 0 1px 0 var(--sf-edge-highlight);
}

.flashcard-content-dock {
border: 1px solid rgb(226 232 240 / 0.8);
background: rgb(248 250 252 / 0.95);
Expand Down
Loading