Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
9bc2347
Add Svelte 5 rewrite PoC (logs view) + migration proposal
Hazer Jun 19, 2026
1d5eb8a
Expand Svelte PoC: Home, Item detail, Login, Settings hub
Hazer Jun 19, 2026
fb2e33f
Phase 0: Foundation — diet CSS, full API/types, verbatim mock data, h…
Hazer Jun 19, 2026
db3f9aa
Phase 1: App shell + auth (sidebar rail, backdrop, login/welcome, guard)
Hazer Jun 19, 2026
dab2eef
Phase 2: Home (navbar, hero, MediaCard, Shelf, tabs, search)
Hazer Jun 19, 2026
745dc3c
Phase 3: Item detail (hero, breadcrumbs, people, extras, children, su…
Hazer Jun 19, 2026
19a14f7
Phase 4: Person detail (hero, credit grid + season/episode trays)
Hazer Jun 19, 2026
e5b70e2
Fix: AuthShell imported Koko.svg as a JS module, breaking the app und…
Hazer Jun 19, 2026
d43fcfc
Svelte port: fix ~90 fidelity drifts + add porting playbook
Hazer Jun 19, 2026
f392020
MediaCard: restore overflow:hidden on .media-card-art (mock fallback …
Hazer Jun 19, 2026
b2de4cc
MediaCard: fix hover 'card leaking past poster corners' (vanilla bug)
Hazer Jun 19, 2026
db5e0c7
MediaCard hover shadow + Rail Home-active bug
Hazer Jun 19, 2026
d00ef2f
MediaCard: fix metadata badge structure to match vanilla
Hazer Jun 19, 2026
d4e58cf
Responsive rail + fix rail-collapse condition to match vanilla
Hazer Jun 19, 2026
895a576
MediaCard: isUnmatched missed items with has_metadata absent
Hazer Jun 19, 2026
2b0ac7c
MediaCard/ItemHero: harden badge predicates, fix watched to use watch…
Hazer Jun 19, 2026
5cbb1d8
BROWSE_FILTER_PROPOSAL: decision recorded (Option A, no store)
Hazer Jun 19, 2026
a88a5ff
Phase 6.1-6.4: browse-detail routes (Option A, no store) + Categories…
Hazer Jun 19, 2026
b618476
Storybook: integrate @storybook/sveltekit 10.4.6 + foundation + first…
Hazer Jun 19, 2026
4c60a7c
Storybook: stories for all ported components (16 groups, 67 stories)
Hazer Jun 19, 2026
56062c9
Fix WithStores decorator: $props() must be in the instance script
Hazer Jun 19, 2026
c16f39d
Fix WithStores: children snippet is optional for args-driven stories
Hazer Jun 19, 2026
ea6cd06
BrowseDetail story: use asChild instead of args-driven (fixes anchor.…
Hazer Jun 19, 2026
2af4634
fix(storybook): replace Svelte-component global decorator with functi…
Hazer Jun 19, 2026
948a74f
fix(storybook): resolve all surfaced story render errors
Hazer Jun 19, 2026
9e5d534
feat(storybook): logo, dark docs, artworks, categorization, component…
Hazer Jun 19, 2026
2f57a03
fix(storybook): use theme API for dark docs; scope button spinner to …
Hazer Jun 19, 2026
ec84516
feat(storybook): global preset dropdown + BrowseDetail disclaimer + I…
Hazer Jun 19, 2026
c9365ed
refactor: rename Item* → Section* and BrowseDetail → BrowseListing
Hazer Jun 19, 2026
050f740
refactor: extract CardSurface shell + PersonCard/MediaExtraCard compo…
Hazer Jun 19, 2026
d0db6aa
refactor: split SectionSupport into SupportFileInfo + SupportMetadata
Hazer Jun 19, 2026
650d601
refactor: split SectionHero into HeroActions + FactList
Hazer Jun 19, 2026
45a8d75
refactor: split BrowseListing into BrowseListingHero + BrowseListingGrid
Hazer Jun 19, 2026
9064698
chore: update remaining comments to Section*/BrowseListing names
Hazer Jun 19, 2026
01b3f70
feat(storybook): add stories for all split components
Hazer Jun 19, 2026
47b3dc9
fix: clear ui.error on successful loads (parity with vanilla)
Hazer Jun 19, 2026
eb1b11e
feat: metadata-link panel (search + manual link + force refresh)
Hazer Jun 19, 2026
d18846e
feat: auto-refresh polling for metadata/scan state
Hazer Jun 19, 2026
6df69df
chore: bump Vite 7→8 + @sveltejs/vite-plugin-svelte 6→7
Hazer Jun 19, 2026
707f2aa
feat(storybook): add stories for pre-existing component gaps
Hazer Jun 20, 2026
f6120cd
feat: settings foundation — routes + SettingsShell (Step 1 of Phase 5)
Hazer Jun 20, 2026
9fb0922
feat: settings general + users with profile-image upload (Step 2 of P…
Hazer Jun 20, 2026
bae2e35
feat: settings libraries — cards, actions, add-library form (Step 3 o…
Hazer Jun 20, 2026
fe73d9d
feat: settings providers — cards, clear-cache, priority reordering (S…
Hazer Jun 20, 2026
8d48518
feat: settings scheduled tasks — runner + 3 task cards (Step 5)
Hazer Jun 20, 2026
21580be
feat: settings dashboard — metadata table + sorting + activities (Ste…
Hazer Jun 20, 2026
5a3b50f
feat: settings logs — filter form + entries table (Step 7)
Hazer Jun 20, 2026
2a9a749
feat: settings stories, missing CSS, settings preset (Step 8 — Phase …
Hazer Jun 20, 2026
c181359
fix: improve 6.5 (b/c/d) to full fidelity
Hazer Jun 20, 2026
6e8ce88
docs: Phase 7 partial verification — completeness + improvements report
Hazer Jun 20, 2026
017f36d
docs: player spike architecture reference + opportunities draft
Hazer Jun 21, 2026
8b36cb8
fix: add never-scanned library polling (minor gap from Phase 7)
Hazer Jun 21, 2026
1066f75
feat: player foundation — store, helpers, types (Phase A of player sp…
Hazer Jun 21, 2026
15f9784
feat: media player + controls + actions (Phase B of player spike)
Hazer Jun 21, 2026
778f73e
feat: YouTube overlays — trailer + theme song (Phase C of player spike)
Hazer Jun 21, 2026
2a511ad
feat: player integration — overlay + wiring + back-button (Phase D)
Hazer Jun 21, 2026
af0ef15
feat: player CSS + stories + verification (Phase E — player spike com…
Hazer Jun 21, 2026
82f40b8
fix: pin player duration from item metadata during transcoding
Hazer Jun 21, 2026
0f255bb
fix: dev server real-API support + warn on mock fallback
Hazer Jun 21, 2026
5dc69a3
fix: proxy HTTPS support + remove dev-mode mock fallback
Hazer Jun 21, 2026
cba33e0
fix: proxy bypass for SvelteKit page routes vs API POST endpoints
Hazer Jun 21, 2026
554cc16
fix: subpath proxy for dev server — /proxy/* → Rust server
Hazer Jun 21, 2026
d45da80
fix: store auth token after login/createUser (was discarded)
Hazer Jun 21, 2026
7088785
fix: player volume + progress seeking broken
Hazer Jun 21, 2026
6cfadd8
fix: slider edge dead-zones + seek snap-back + CSS/warning cleanup
Hazer Jun 21, 2026
d6491f0
fix: slider fill color + transcoded seek position offset
Hazer Jun 21, 2026
37d6e48
fix: input border on sliders + audio track default selection
Hazer Jun 21, 2026
b0a0488
fix: audio track active index + stale error overlay on track switch
Hazer Jun 21, 2026
2a24b50
fix: use session audio_stream_index for initial active track
Hazer Jun 21, 2026
7f56fbc
fix: eliminate all dev warnings (0 errors, 0 warnings)
Hazer Jun 21, 2026
467cf06
feat(server): X-Content-Duration header + ffmpeg -t for transcode str…
Hazer Jun 21, 2026
67914e1
docs: comprehensive project overview for LizardByte evaluation
Hazer Jun 21, 2026
84e5703
feat: gamepad + remote spatial navigation (from the ground up)
Hazer Jun 21, 2026
d0efa7e
fix: add inline:'nearest' to scrollIntoView for horizontal shelf scro…
Hazer Jun 21, 2026
359edc2
feat: gamepad rewrite — 8BitDo support, sensitivity, L/R tabs, help m…
Hazer Jun 22, 2026
db31209
fix: precise D-pad hat detection for 8BitDo Pro 3
Hazer Jun 22, 2026
2b34bba
fix: D-pad neutral value + stick sensitivity
Hazer Jun 22, 2026
ae4d4fd
feat: gamepad layout detection system — 8BitDo Pro 3 full mapping
Hazer Jun 22, 2026
a46f4af
fix: D-pad neutral value (3.286) was permanently triggering Up
Hazer Jun 22, 2026
54d8eff
fix: right-stick Y axis is 5, not 3 (axis 3 is L2 trigger)
Hazer Jun 22, 2026
da09ada
refactor: isolate gamepad into standalone package with unit tests
Hazer Jun 22, 2026
3aafeae
fix: rewrite gamepad handling — use isolated package, proper stick de…
Hazer Jun 22, 2026
38e0fb2
fix: spatial navigation — debounce, container preference, escape fall…
Hazer Jun 22, 2026
f3b6c66
feat: declarative navigation region system (predictable spatial nav)
Hazer Jun 22, 2026
cc343d4
fix: ensure shelf sections + cards are fully visible during navigation
Hazer Jun 22, 2026
7e815e5
fix: shelf scroll visibility + stay at bottom edge
Hazer Jun 22, 2026
7547a45
fix: L/R bumper tab switching — correct selectors + context-aware
Hazer Jun 22, 2026
180a830
feat: right stick = volume in player + updated controls help modal
Hazer Jun 22, 2026
be178bf
chore: lint fixes
Hazer Jun 22, 2026
a92ea05
fix: resolve all 92 SonarCloud issues + tighten lint + CSS scoping
Hazer Jun 22, 2026
77dc72a
refactor: Button owns all variants + 29→0 Sonar TS issues + checkVisi…
Hazer Jun 22, 2026
e613253
refactor: IconButton component + Rail decoupling + rail-button scoping
Hazer Jun 22, 2026
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
75 changes: 75 additions & 0 deletions .github/workflows/ci-client-web-svelte.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: CI-Client-Web-Svelte
permissions: {}

on:
pull_request:
paths:
- 'crates/client-web-svelte/**'
- '.github/workflows/ci-client-web-svelte.yml'
push:
branches:
- master
paths:
- 'crates/client-web-svelte/**'
- '.github/workflows/ci-client-web-svelte.yml'
workflow_dispatch:

concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true

jobs:
svelte:
name: Svelte Client (${{ matrix.run }})
permissions:
contents: read
strategy:
fail-fast: false
matrix:
run:
- check
- lint
- test
- build
runs-on: ubuntu-latest
defaults:
run:
working-directory: crates/client-web-svelte
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
cache: npm
cache-dependency-path: crates/client-web-svelte/package-lock.json

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Check (svelte-check)
if: matrix.run == 'check'
run: npm run check

- name: Lint (oxlint)
if: matrix.run == 'lint'
run: npm run lint:ci

- name: Test (vitest)
if: matrix.run == 'test'
run: npm run test

- name: Build
if: matrix.run == 'build'
run: npm run build

- name: Upload build artifact
if: matrix.run == 'build'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
if-no-files-found: error
name: client-web-svelte-dist
path: crates/client-web-svelte/dist
222 changes: 222 additions & 0 deletions PROPOSAL.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions crates/client-web-svelte/.env.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Activated by `npm run dev:mock` (vite dev --mode mock).
# Same toggle as the vanilla client's .env.mock — proves the workflow carries over.
VITE_USE_MOCK_API=true
13 changes: 13 additions & 0 deletions crates/client-web-svelte/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules/
/dist/
/.svelte-kit/
/build/
/package
.env
.env.*
!.env.mock
*.log
.DS_Store

*storybook.log
storybook-static
55 changes: 55 additions & 0 deletions crates/client-web-svelte/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"typescript",
"unicorn",
"oxc",
"import"
],
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn"
},
"rules": {
"no-console": "off",
"no-explicit-any": "off",
"no-underscore-dangle": "off",
"no-map-spread": "off",
"preserve-caught-error": "off",
"consistent-function-scoping": "off",
"require-module-specifiers": "off",
"no-shadow": "off",
"no-unassigned-import": "off",

"no-cond-assign": ["warn", "always"],
"no-empty-function": "warn",
"no-mutable-exports": "warn",
"no-nested-ternary": "warn",
"no-useless-assignment": "warn",
"prefer-at": "warn",
"prefer-math-min-max": "warn",
"prefer-modern-math-apis": "warn",
"prefer-optional-chain": "warn",
"prefer-regexp-exec": "warn",
"complexity": ["warn", { "max": 15 }]
},
"env": {
"builtin": true,
"browser": true
},
"overrides": [
{
"files": ["*.test.ts"],
"rules": {
"no-console": "off"
}
},
{
"files": ["*.stories.svelte", "src/lib/storybook/**"],
"rules": {
"no-empty-function": "off"
}
}
]
}
50 changes: 50 additions & 0 deletions crates/client-web-svelte/.storybook/decorators/withStores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Global Storybook decorator: seeds the store singletons + mock $app/state
// around every story based on `args.preset` / `args.route`.
//
// Why a plain function (not a Svelte component)?
// The Svelte renderer decorator contract (see `decorateStory` in
// @storybook/svelte) calls each decorator as `decorator(storyFn, context)`
// where `context.args` holds the story's args. A function decorator is the
// documented way to read story context for mocking (addon-svelte-csf README
// "Accessing Story context"). Returning `storyFn()` hands rendering back to
// the renderer's normal mount path.
//
// Registering a Svelte *component* as the decorator instead (the earlier
// `[WithStores]` / `() => WithStores` attempts) made the renderer invoke the
// component function with the wrong `(internal, props)` shape, corrupting
// Svelte 5's internal mount and producing `anchor.before is not a function`.
//
// Why untrack + synchronous seeding?
// The Svelte renderer invokes decorators from inside a reactive effect
// (decorateStory's reduce). Mutating store `$state` synchronously from there
// trips Svelte 5's `state_unsafe_mutation` guard ("Updating state inside a
// $derived/template expression is forbidden") *and* would register reactive
// dependencies we don't want.
//
// `untrack` runs the seeding outside the current reactive computation, so
// the mutation is permitted and no spurious dependencies are recorded. It
// stays synchronous, so the component mounted by `storyFn()` below sees the
// already-seeded stores on first render (no race).
//
// `applyPreset` always calls `resetStores()` first, so state never bleeds
// between stories — no `$effect` cleanup is required.
import { untrack } from 'svelte';
import type { Decorator } from '@storybook/svelte';
import { applyPreset, type Preset } from '$lib/storybook/presets';
import { setPage } from '$lib/storybook/mockAppState.svelte';
import { setMockArtworkResolver } from '$lib/api';
import { lookupArtwork } from '$lib/storybook/artworks';

// Register the local CC0 artwork resolver once so getArtworkUrl() in mock mode
// serves bundled images for fixture item ids (see artworks.ts registry).
// Unknown ids fall through to the mock:// placeholder (gradient fallback).
setMockArtworkResolver((itemId, kind) => lookupArtwork(itemId, kind));

export const withStores: Decorator = (storyFn, context) => {
const args = (context.args ?? {}) as { preset?: Preset; route?: string };
untrack(() => {
applyPreset(args.preset ?? 'empty');
if (args.route) setPage({ pathname: args.route });
});
return storyFn();
};
50 changes: 50 additions & 0 deletions crates/client-web-svelte/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { StorybookConfig } from '@storybook/sveltekit';

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@storybook/addon-svelte-csf',
'@chromatic-com/storybook',
'@storybook/addon-vitest',
'@storybook/addon-a11y',
'@storybook/addon-docs',
],
framework: {
name: '@storybook/sveltekit',
options: {},
},
docs: {
autodocs: 'tag',
},
// Dark theming is applied via the proper Storybook theme API, not CSS:
// - manager.ts sets `addons.setConfig({ theme: themes.dark })` for the
// sidebar + toolbar (Storybook 10 uses Emotion CSS-in-JS, so overriding
// CSS variables does nothing — the theme API generates the right styles).
// - preview.ts sets `parameters.docs.theme = themes.dark` for the docs page.
viteFinal: async (config) => {
// Stub the SvelteKit $app modules — Storybook isn't a router, so components
// that read $app/state (page) or $app/navigation (goto) get a mutable mock.
// See src/lib/storybook/mockAppState.svelte.ts + mockAppNavigation.ts.
config.resolve ??= {};
config.resolve.alias = {
...(config.resolve.alias as Record<string, string> | undefined),
'$app/state': new URL('../src/lib/storybook/mockAppState.svelte.ts', import.meta.url)
.pathname,
'$app/navigation': new URL('../src/lib/storybook/mockAppNavigation.ts', import.meta.url)
.pathname,
};

// Force mock API mode in Storybook. Stories rely on the mock dispatch layer
// (src/lib/mockApi.ts) + store presets — without this, components try to
// hit a real backend and the artwork resolver / fixture seeding never runs.
// `import.meta.env.VITE_USE_MOCK_API` is read as a Vite env var (see
// api.ts:606), so we define the full property access on import.meta.env.
config.define ??= {};
const define = config.define as Record<string, string>;
define['import.meta.env.VITE_USE_MOCK_API'] = JSON.stringify('true');

return config;
},
};

export default config;
17 changes: 17 additions & 0 deletions crates/client-web-svelte/.storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Storybook manager — runs in the Storybook UI shell (sidebar + toolbar), not
// in the story canvas. The Koko client is dark-only, so we force the Storybook
// UI to the built-in `dark` theme.
//
// Why the theme API and not CSS overrides?
// Storybook 10 renders its entire UI with Emotion CSS-in-JS (hashed class
// names like `css-1z0jwx`). There are no stable CSS variables or class names
// to override — a `<style>` block in managerHead/previewHead gets ignored by
// the Emotion-generated styles. `addons.setConfig({ theme })` is the
// supported way to theme the UI; it feeds the theme into Emotion so the
// correct dark styles are generated.
import { addons } from 'storybook/manager-api';
import { themes } from 'storybook/theming';

addons.setConfig({
theme: themes.dark,
});
61 changes: 61 additions & 0 deletions crates/client-web-svelte/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Preview } from '@storybook/sveltekit';
import { themes, ensure } from 'storybook/theming';
import '../src/app.css'; // design tokens + shared rules — components depend on these
import { withStores } from './decorators/withStores';
import { PRESETS } from '$lib/storybook/presets';

// The Koko client is dark-only. Force the docs page to the dark theme too —
// the manager's `addons.setConfig({ theme: themes.dark })` (see manager.ts)
// doesn't reliably propagate to docs pages (storybookjs/storybook#28664), so
// we set it explicitly here. `ensure` makes the theme resolve synchronously.
ensure(themes.dark);

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
// Force the docs page chrome to dark (sidebar is handled by manager.ts).
theme: themes.dark,
},
backgrounds: {
// The Koko client is dark-only (color-scheme: dark hardcoded in app.css).
// Force the Storybook canvas to match so stories render on the right bg.
default: 'dark',
values: [{ name: 'dark', value: '#0c111d' }],
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
},
// Global argTypes — apply to every story. `preset` drives the store-seeding
// decorator (withStores.ts); making it a select dropdown prevents typos and
// shows the available fixture bundles. `route` simulates the current path
// for components reading $app/state.
argTypes: {
preset: {
control: { type: 'select' },
options: [...PRESETS],
description:
'Store fixture bundle applied by the withStores decorator (see src/lib/storybook/presets.ts). Seeds catalog/item/libraries/auth/ui stores for the story.',
table: { category: 'Storybook' },
},
route: {
control: 'text',
description: 'Simulated $app/state pathname for components reading the current route.',
table: { category: 'Storybook' },
},
},
// Seed stores + mock $app/state around every story. Stories select a fixture
// preset via args.preset (default 'empty'); see decorators/withStores.ts.
decorators: [withStores],
};

export default preview;
Loading