+
+
{#if !$currentActor.initialized}
- {themeSaveError ? 'Appearance could not sync.' : 'Couldn’t sign out. Try again.'}
+ {themeSaveError ? 'Theme could not sync.' : 'Couldn’t sign out. Try again.'}
{#if themeSaveError}
diff --git a/src/lib/components/ThemeCycleButton.svelte b/src/lib/components/ThemeCycleButton.svelte
new file mode 100644
index 0000000..4dc8696
--- /dev/null
+++ b/src/lib/components/ThemeCycleButton.svelte
@@ -0,0 +1,65 @@
+
+
+
+ {#if preference === 'system'}
+
+ {:else if preference === 'light'}
+
+ {:else}
+
+ {/if}
+
diff --git a/src/lib/components/problemPagination.ts b/src/lib/components/problemPagination.ts
deleted file mode 100644
index 11e75ca..0000000
--- a/src/lib/components/problemPagination.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export function initialProblemVisibleCount(rowBatchSize: number | null): number {
- return rowBatchSize && rowBatchSize > 0 ? rowBatchSize : Number.POSITIVE_INFINITY;
-}
-
-export function nextProblemVisibleCount(
- currentCount: number,
- totalCount: number,
- rowBatchSize: number
-): number {
- return Math.min(currentCount + rowBatchSize, totalCount);
-}
-
-export function nextProblemBatchCount(
- currentCount: number,
- totalCount: number,
- rowBatchSize: number
-): number {
- return Math.min(rowBatchSize, Math.max(totalCount - currentCount, 0));
-}
diff --git a/src/lib/services/appearance.ts b/src/lib/services/appearance.ts
index 4f7d0fa..1f8e04d 100644
--- a/src/lib/services/appearance.ts
+++ b/src/lib/services/appearance.ts
@@ -6,6 +6,11 @@ export function normalizeThemePreference(value: unknown): ThemePreference {
return value === 'light' || value === 'dark' || value === 'system' ? value : 'system';
}
+export function nextThemePreference(preference: ThemePreference): ThemePreference {
+ const index = THEME_PREFERENCES.indexOf(preference);
+ return THEME_PREFERENCES[(index + 1) % THEME_PREFERENCES.length];
+}
+
export function resolveTheme(
preference: ThemePreference,
systemPrefersDark: boolean
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 2d1529b..736a2a7 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -9,5 +9,4 @@
pageTitle="Problems"
defaultSolvedFilterState="all"
initialProblems={data.problems}
- rowBatchSize={50}
/>
diff --git a/tests/performance-budget.test.ts b/tests/performance-budget.test.ts
index 6e71a8e..36f620d 100644
--- a/tests/performance-budget.test.ts
+++ b/tests/performance-budget.test.ts
@@ -32,12 +32,12 @@ test('asset collection includes only immutable JavaScript and CSS in stable orde
assert.equal(summarizeAssets(assets).assetCount, 2);
});
-test('homepage fixture models the deterministic initial 50-row batch', () => {
+test('homepage fixture models the complete deterministic problem list', () => {
const html = renderHomepageTableFixture();
- assert.equal((html.match(/data-problem-id=/g) ?? []).length, 50);
+ assert.equal((html.match(/data-problem-id=/g) ?? []).length, 280);
assert.match(html, /Fixture Problem 1 <quality>/);
- assert.match(html, /aria-controls="problem-table-body"/);
- assert.match(html, />50 of 280 problems shown);
+ assert.match(html, /Fixture Problem 280 <quality>/);
+ assert.doesNotMatch(html, /more problems|problems shown/);
assert.deepEqual(measureHomepageFixture(), measureHomepageFixture());
});
@@ -55,6 +55,6 @@ test('budget failures name only exceeded limits', () => {
{ totalGzipBytes: 100, largestJavaScriptGzipBytes: 50, largestCssGzipBytes: 21 },
{ htmlBytes: 300, gzipBytes: 29 }
),
- ['total immutable JS/CSS gzip: 101 > 100 bytes', 'initial homepage fixture gzip: 30 > 29 bytes']
+ ['total immutable JS/CSS gzip: 101 > 100 bytes', 'full homepage fixture gzip: 30 > 29 bytes']
);
});
diff --git a/tests/problem-pagination.test.ts b/tests/problem-pagination.test.ts
deleted file mode 100644
index eedd51c..0000000
--- a/tests/problem-pagination.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import assert from 'node:assert/strict';
-import { readFileSync } from 'node:fs';
-import { test } from 'node:test';
-import {
- initialProblemVisibleCount,
- nextProblemBatchCount,
- nextProblemVisibleCount
-} from '../src/lib/components/problemPagination.ts';
-
-test('problem pagination advances in bounded batches', () => {
- assert.equal(initialProblemVisibleCount(50), 50);
- assert.equal(initialProblemVisibleCount(null), Number.POSITIVE_INFINITY);
- assert.equal(nextProblemVisibleCount(50, 121, 50), 100);
- assert.equal(nextProblemVisibleCount(100, 121, 50), 121);
- assert.equal(nextProblemBatchCount(50, 121, 50), 50);
- assert.equal(nextProblemBatchCount(100, 121, 50), 21);
- assert.equal(nextProblemBatchCount(121, 121, 50), 0);
-});
-
-test('ProblemDisplay slices derived rows and resets only from filter and sort handlers', () => {
- const source = readFileSync('src/lib/components/ProblemDisplay.svelte', 'utf8');
- assert.match(source, /\$: fullRows = collection\.rows;/);
- assert.match(source, /\$: visibleRows = fullRows\.slice\(0, visibleRowCount\);/);
-
- for (const handler of [
- 'handleTopicSelect',
- 'handleDifficultySort',
- 'handleSolvedFilter',
- 'handleAuthorFilter',
- 'handleSourceFilter'
- ]) {
- assert.match(source, new RegExp(`function ${handler}\\([\\s\\S]*?resetVisibleRows\\(\\);`));
- }
-
- for (const handler of ['handleLike', 'handleToggleSolved']) {
- const body = source.match(new RegExp(`function ${handler}\\([\\s\\S]*?\\n}`))?.[0] ?? '';
- assert.doesNotMatch(body, /resetVisibleRows/);
- }
-});
-
-test('homepage opts into batches while profile pages retain the unlimited default', () => {
- const homepage = readFileSync('src/routes/+page.svelte', 'utf8');
- const profile = readFileSync('src/routes/user/[userId]/+page.svelte', 'utf8');
- assert.match(homepage, /rowBatchSize=\{50\}/);
- assert.doesNotMatch(profile, /rowBatchSize/);
-});
-
-test('show-more markup links a real button to the problem tbody and announces progress', () => {
- const display = readFileSync('src/lib/components/ProblemDisplay.svelte', 'utf8');
- const table = readFileSync('src/lib/components/ProblemTable.svelte', 'utf8');
- assert.match(table, /
/);
- assert.match(display, /type="button"/);
- assert.match(display, /aria-controls=\{problemTableBodyId\}/);
- assert.match(display, /`Show \$\{nextBatchCount\} more problems`/);
- assert.match(display, /role="status"/);
- assert.match(display, /aria-live="polite"/);
- assert.match(display, /aria-disabled=\{nextBatchCount === 0\}/);
-});
diff --git a/tests/theme-service.test.ts b/tests/theme-service.test.ts
index 477993d..eababa6 100644
--- a/tests/theme-service.test.ts
+++ b/tests/theme-service.test.ts
@@ -2,7 +2,11 @@ import assert from 'node:assert/strict';
import { test } from 'node:test';
import { get, writable } from 'svelte/store';
import { createThemeService } from '../src/lib/services/theme.ts';
-import type { ResolvedTheme, ThemePreference } from '../src/lib/services/appearance.ts';
+import {
+ nextThemePreference,
+ type ResolvedTheme,
+ type ThemePreference
+} from '../src/lib/services/appearance.ts';
import type { UserPreferences } from '../src/lib/services/user.ts';
function setup(
@@ -70,6 +74,12 @@ function setup(
};
}
+test('theme preferences cycle from System to Light to Dark', () => {
+ assert.equal(nextThemePreference('system'), 'light');
+ assert.equal(nextThemePreference('light'), 'dark');
+ assert.equal(nextThemePreference('dark'), 'system');
+});
+
test('initialization normalizes missing and invalid local preferences to System', () => {
for (const value of [null, 'paper']) {
const context = setup({ stored: value, systemDark: true });