From 925d2eec3eae02d8050d591d35daaea32b6aeb92 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 14 Jan 2026 16:06:02 +0300 Subject: [PATCH 1/7] chore(workflow): delete comments --- .gitea/workflows/workflow.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index 7be85ba..91b4709 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -17,38 +17,21 @@ jobs: uses: actions/setup-node@v4 with: node-version: '25' - # We handle caching manually below to ensure - # corepack-managed yarn is used correctly. - name: Enable Corepack run: | corepack enable corepack prepare yarn@stable --activate - # - name: Get yarn cache directory path - # id: yarn-cache-dir-path - # run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - # - name: Persistent Yarn Cache - # uses: actions/cache@v4 - # id: yarn-cache - # with: - # path: ${{ github.workspace }}/.yarn/cache - # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - # restore-keys: ${{ runner.os }}-yarn- - # - name: Persistent Yarn Cache uses: actions/cache@v4 id: yarn-cache with: - # In Yarn Berry, the cache is local to the project path: .yarn/cache - # Ensure a clean key without hidden newline characters key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: ${{ runner.os }}-yarn- - name: Install dependencies - # --immutable ensures the lockfile isn't changed (replaces --frozen-lockfile) run: yarn install --immutable - name: Build Svelte App -- 2.49.1 From 429a9a087775dd76f1e8e537d3ebf063fa7c853b Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 15 Jan 2026 13:33:59 +0300 Subject: [PATCH 2/7] feature(VirtualList): remove tanstack virtual list solution, add self written one --- package.json | 3 +- src/entities/Font/ui/FontList/FontList.svelte | 42 +-- .../createVirtualizer.svelte.ts | 243 +++++++++++------- src/shared/ui/VirtualList/VirtualList.svelte | 76 +----- yarn.lock | 19 -- 5 files changed, 175 insertions(+), 208 deletions(-) diff --git a/package.json b/package.json index e450e0b..99594e4 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "vitest-browser-svelte": "^2.0.1" }, "dependencies": { - "@tanstack/svelte-query": "^6.0.14", - "@tanstack/svelte-virtual": "^3.13.17" + "@tanstack/svelte-query": "^6.0.14" } } diff --git a/src/entities/Font/ui/FontList/FontList.svelte b/src/entities/Font/ui/FontList/FontList.svelte index c1f2ac4..528c0fb 100644 --- a/src/entities/Font/ui/FontList/FontList.svelte +++ b/src/entities/Font/ui/FontList/FontList.svelte @@ -1,6 +1,5 @@ -{#each fontshareStore.fonts as font (font.id)} - - - {font.name} - - {font.category} โ€ข {font.provider} - - - -{/each} + + {#snippet children({ item: font })} + + + {font.name} + + {font.category} โ€ข {font.provider} + + + + {/snippet} + diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts index 14004c7..18ac8f9 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts @@ -1,15 +1,159 @@ -import { - createVirtualizer as coreCreateVirtualizer, - observeElementRect, -} from '@tanstack/svelte-virtual'; -import type { VirtualItem as CoreVirtualItem } from '@tanstack/virtual-core'; -import { get } from 'svelte/store'; +import { untrack } from 'svelte'; + +export function createVirtualizer(optionsGetter: () => VirtualizerOptions) { + // Reactive State + let scrollOffset = $state(0); + let containerHeight = $state(0); + let measuredSizes = $state>({}); + + // Non-reactive ref for DOM manipulation (avoiding unnecessary state tracking) + let elementRef: HTMLElement | null = null; + + // Reactive Options + const options = $derived(optionsGetter()); + + // Optimized Memoization (The Cache Layer) + // Only recalculates when item count or measured sizes change. + const offsets = $derived.by(() => { + const count = options.count; + const result = new Array(count); + let accumulated = 0; + + for (let i = 0; i < count; i++) { + result[i] = accumulated; + accumulated += measuredSizes[i] ?? options.estimateSize(i); + } + return result; + }); + + const totalSize = $derived( + options.count > 0 + ? offsets[options.count - 1] + + (measuredSizes[options.count - 1] ?? options.estimateSize(options.count - 1)) + : 0, + ); + + // Visible Range Calculation + // Svelte tracks dependencies automatically here. + const items = $derived.by((): VirtualItem[] => { + const count = options.count; + if (count === 0 || containerHeight === 0) return []; + + const overscan = options.overscan ?? 5; + const viewportStart = scrollOffset; + const viewportEnd = scrollOffset + containerHeight; + + // Find Start (Linear Scan) + let startIdx = 0; + while (startIdx < count && offsets[startIdx + 1] < viewportStart) { + startIdx++; + } + + // Find End + let endIdx = startIdx; + while (endIdx < count && offsets[endIdx] < viewportEnd) { + endIdx++; + } + + const start = Math.max(0, startIdx - overscan); + const end = Math.min(count, endIdx + overscan); + + const result: VirtualItem[] = []; + for (let i = start; i < end; i++) { + const size = measuredSizes[i] ?? options.estimateSize(i); + result.push({ + index: i, + start: offsets[i], + size, + end: offsets[i] + size, + key: options.getItemKey?.(i) ?? i, + }); + } + return result; + }); + + // Svelte Actions (The DOM Interface) + function container(node: HTMLElement) { + elementRef = node; + containerHeight = node.offsetHeight; + + const handleScroll = () => { + scrollOffset = node.scrollTop; + }; + + const resizeObserver = new ResizeObserver(([entry]) => { + if (entry) containerHeight = entry.contentRect.height; + }); + + node.addEventListener('scroll', handleScroll, { passive: true }); + resizeObserver.observe(node); + + return { + destroy() { + node.removeEventListener('scroll', handleScroll); + resizeObserver.disconnect(); + elementRef = null; + }, + }; + } + + function measureElement(node: HTMLElement) { + // Use a ResizeObserver on individual items for dynamic height support + const resizeObserver = new ResizeObserver(([entry]) => { + if (entry) { + const index = parseInt(node.dataset.index || '', 10); + const height = entry.borderBoxSize[0]?.blockSize ?? node.offsetHeight; + + // Only update if height actually changed to prevent loops + if (!isNaN(index) && measuredSizes[index] !== height) { + measuredSizes[index] = height; + } + } + }); + + resizeObserver.observe(node); + return { + destroy: () => resizeObserver.disconnect(), + }; + } + + // Programmatic Scroll + function scrollToIndex(index: number, align: 'start' | 'center' | 'end' | 'auto' = 'auto') { + if (!elementRef || index < 0 || index >= options.count) return; + + const itemStart = offsets[index]; + const itemSize = measuredSizes[index] ?? options.estimateSize(index); + let target = itemStart; + + if (align === 'center') target = itemStart - containerHeight / 2 + itemSize / 2; + if (align === 'end') target = itemStart - containerHeight + itemSize; + + elementRef.scrollTo({ top: target, behavior: 'smooth' }); + } + + return { + get items() { + return items; + }, + get totalSize() { + return totalSize; + }, + container, + measureElement, + scrollToIndex, + }; +} export interface VirtualItem { + /** Index of the item in the data array */ index: number; + /** Offset from the top of the list */ start: number; + /** Height of the item */ size: number; + /** End position (start + size) */ end: number; + /** Unique key for the item (for Svelte's {#each} keying) */ key: string | number; } @@ -26,91 +170,4 @@ export interface VirtualizerOptions { scrollMargin?: number; } -/** - * Creates a reactive virtualizer using Svelte 5 runes and TanStack's core library. - * - * @example - * ```ts - * const virtualizer = createVirtualizer(() => ({ - * count: items.length, - * estimateSize: () => 80, - * overscan: 5, - * })); - * - * // In template: - * //
- * // {#each virtualizer.items as item} - * //
- * // {items[item.index]} - * //
- * // {/each} - * //
- * ``` - */ -export function createVirtualizer( - optionsGetter: () => VirtualizerOptions, -) { - let element = $state(null); - - const internalStore = coreCreateVirtualizer({ - get count() { - return optionsGetter().count; - }, - get estimateSize() { - return optionsGetter().estimateSize; - }, - get overscan() { - return optionsGetter().overscan ?? 5; - }, - get scrollMargin() { - return optionsGetter().scrollMargin; - }, - get getItemKey() { - return optionsGetter().getItemKey ?? (i => i); - }, - getScrollElement: () => element, - observeElementRect: observeElementRect, - }); - - const state = $derived(get(internalStore)); - - const virtualItems = $derived( - state.getVirtualItems().map((item: CoreVirtualItem): VirtualItem => ({ - index: item.index, - start: item.start, - size: item.size, - end: item.end, - key: typeof item.key === 'bigint' ? Number(item.key) : item.key, - })), - ); - - return { - get items() { - return virtualItems; - }, - - get totalSize() { - return state.getTotalSize(); - }, - - get scrollOffset() { - return state.scrollOffset ?? 0; - }, - - get scrollElement() { - return element; - }, - set scrollElement(el) { - element = el; - }, - - scrollToIndex: (idx: number, opt?: { align?: 'start' | 'center' | 'end' | 'auto' }) => - state.scrollToIndex(idx, opt), - - scrollToOffset: (off: number) => state.scrollToOffset(off), - - measureElement: (el: HTMLElement) => state.measureElement(el), - }; -} - export type Virtualizer = ReturnType; diff --git a/src/shared/ui/VirtualList/VirtualList.svelte b/src/shared/ui/VirtualList/VirtualList.svelte index 6e11fae..f6471f7 100644 --- a/src/shared/ui/VirtualList/VirtualList.svelte +++ b/src/shared/ui/VirtualList/VirtualList.svelte @@ -55,53 +55,15 @@ interface Props { let { items, itemHeight = 80, overscan = 5, class: className, children }: Props = $props(); -let activeIndex = $state(0); -const itemRefs = new Map(); - -const virtual = createVirtualizer(() => ({ +const virtualizer = createVirtualizer(() => ({ count: items.length, estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight, overscan, })); - -function registerItem(node: HTMLElement, index: number) { - itemRefs.set(index, node); - return { - destroy() { - itemRefs.delete(index); - }, - }; -} - -async function focusItem(index: number) { - activeIndex = index; - virtual.scrollToIndex(index, { align: 'auto' }); - await tick(); - itemRefs.get(index)?.focus(); -} - -async function handleKeydown(event: KeyboardEvent) { - let nextIndex = activeIndex; - if (event.key === 'ArrowDown') nextIndex++; - else if (event.key === 'ArrowUp') nextIndex--; - else if (event.key === 'Home') nextIndex = 0; - else if (event.key === 'End') nextIndex = items.length - 1; - else return; - - if (nextIndex >= 0 && nextIndex < items.length) { - event.preventDefault(); - await focusItem(nextIndex); - } -} -
e.target === virtual.scrollElement && focusItem(activeIndex))} > - -
- {#each virtual.items as row (row.key)} - -
(activeIndex = row.index)} - class="absolute top-0 left-0 w-full outline-none focus:bg-accent focus:text-accent-foreground" - style:height="{row.size}px" - style:transform="translateY({row.start}px)" - > - {@render children({ item: items[row.index], index: row.index })} -
- {/each} -
+ {#each virtualizer.items as item (item.key)} +
+ {@render children({ item: items[item.index], index: item.index })} +
+ {/each}
diff --git a/yarn.lock b/yarn.lock index 23b5b4b..e1f0e72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1310,24 +1310,6 @@ __metadata: languageName: node linkType: hard -"@tanstack/svelte-virtual@npm:^3.13.17": - version: 3.13.17 - resolution: "@tanstack/svelte-virtual@npm:3.13.17" - dependencies: - "@tanstack/virtual-core": "npm:3.13.17" - peerDependencies: - svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 - checksum: 10c0/8139a94d8b913c1a3aef0e7cda4cfd8451c3e46455a5bd5bae1df26ab7583bfde785ab93cacefba4f0f45f2e2cd13f43fa8cf672c45cb31d52b3232ffb37e69e - languageName: node - linkType: hard - -"@tanstack/virtual-core@npm:3.13.17": - version: 3.13.17 - resolution: "@tanstack/virtual-core@npm:3.13.17" - checksum: 10c0/a021795b88856eff8518137ecb85b72f875399bc234ad10bea440ecb6ab48e5e72a74c9a712649a7765f0c37bc41b88263f5104d18df8256b3d50f6a97b32c48 - languageName: node - linkType: hard - "@testing-library/dom@npm:9.x.x || 10.x.x": version: 10.4.1 resolution: "@testing-library/dom@npm:10.4.1" @@ -2466,7 +2448,6 @@ __metadata: "@sveltejs/vite-plugin-svelte": "npm:^6.2.1" "@tailwindcss/vite": "npm:^4.1.18" "@tanstack/svelte-query": "npm:^6.0.14" - "@tanstack/svelte-virtual": "npm:^3.13.17" "@testing-library/jest-dom": "npm:^6.9.1" "@testing-library/svelte": "npm:^5.3.1" "@tsconfig/svelte": "npm:^5.0.6" -- 2.49.1 From 462abdd2bc1783f34decc14d86263efeaa48a9d8 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 15 Jan 2026 20:05:37 +0300 Subject: [PATCH 3/7] chore: add README --- README.md | 124 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 75842c4..00c3c68 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,120 @@ -# sv +# GlyphDiff -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). +A modern, high-performance font exploration tool for browsing and comparing fonts from Google Fonts and Fontshare. -## Creating a project +## โœจ Features -If you're seeing this, you've probably already done this step. Congrats! +- **Multi-Provider Support**: Access fonts from Google Fonts and Fontshare in one place +- **Fast Virtual Scrolling**: Handles thousands of fonts smoothly with custom virtualization +- **Advanced Filtering**: Filter by category, provider, and character subsets +- **Responsive Design**: Beautiful UI built with shadcn components and Tailwind CSS +- **Type-Safe**: Full TypeScript coverage with strict mode enabled -```sh -# create a new project in the current directory -npx sv create +## ๐Ÿ› ๏ธ Tech Stack -# create a new project in my-app -npx sv create my-app +- **Frontend**: Svelte 5 with runes (reactive primitives) +- **Styling**: Tailwind CSS v4 + shadcn-svelte components +- **Data Fetching**: TanStack Query for caching and state management +- **Architecture**: Feature-Sliced Design (FSD) methodology +- **Testing**: Playwright (E2E), Vitest (unit), Storybook (components) +- **Quality**: Oxlint (linting), Dprint (formatting), Lefthook (git hooks) + +## ๐Ÿ“ Architecture + +``` +src/ +โ”œโ”€โ”€ app/ # App shell, layout, providers +โ”œโ”€โ”€ widgets/ # Composed UI blocks +โ”œโ”€โ”€ features/ # Business features (filters, search) +โ”œโ”€โ”€ entities/ # Domain entities (Font models, stores) +โ”œโ”€โ”€ shared/ # Reusable utilities, UI components, helpers +โ””โ”€โ”€ routes/ # Page-level components ``` -## Developing +## ๐Ÿš€ Quick Start -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: +```bash +# Install dependencies +yarn install -```sh -npm run dev +# Start development server +yarn dev -# or start the server and open the app in a new browser tab -npm run dev -- --open +# Open in browser +yarn dev -- --open ``` -## Building +## ๐Ÿ“ฆ Available Scripts -To create a production version of your app: +| Command | Description | +| ---------------- | -------------------------- | +| `yarn dev` | Start development server | +| `yarn build` | Build for production | +| `yarn preview` | Preview production build | +| `yarn check` | Run Svelte type checking | +| `yarn lint` | Run oxlint | +| `yarn format` | Format with dprint | +| `yarn test` | Run all tests (E2E + unit) | +| `yarn test:e2e` | Run Playwright E2E tests | +| `yarn test:unit` | Run Vitest unit tests | +| `yarn storybook` | Start Storybook dev server | -```sh -npm run build +## ๐Ÿงช Development + +### Type Checking + +```bash +yarn check # Single run +yarn check:watch # Watch mode ``` -You can preview the production build with `npm run preview`. +### Testing -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. +```bash +yarn test:unit # Unit tests +yarn test:unit:watch # Watch mode +yarn test:unit:ui # Vitest UI +yarn test:e2e # E2E tests with Playwright +yarn test:e2e --ui # Interactive test runner +``` + +### Code Quality + +```bash +yarn lint # Lint code +yarn format # Format code +yarn format:check # Check formatting +``` + +## ๐ŸŽฏ Key Components + +- **VirtualList**: Custom high-performance list virtualization using Svelte 5 runes +- **FontList**: Displays fonts with loading, empty, and error states +- **FilterControls**: Multi-filter system for category, provider, and subsets +- **TypographyControl**: Dynamic typography adjustment controls + +## ๐Ÿ“ Code Style + +- **Path Aliases**: Use `$app/`, `$shared/`, `$features/`, `$entities/`, `$widgets/`, `$routes/` +- **Components**: PascalCase (e.g., `CheckboxFilter.svelte`) +- **Formatting**: 100 char line width, 4-space indent, single quotes +- **Imports**: Auto-sorted by dprint, separated by blank line +- **Type Safety**: Strict TypeScript, JSDoc comments for public APIs + +## ๐Ÿ—๏ธ Building for Production + +```bash +yarn build +yarn preview +``` + +## ๐Ÿ“š Learn More + +- [Svelte 5 Documentation](https://svelte-5-preview.vercel.app/docs) +- [Feature-Sliced Design](https://feature-sliced.design) +- [Tailwind CSS v4](https://tailwindcss.com/blog/tailwindcss-v4-alpha) +- [shadcn-svelte](https://www.shadcn-svelte.com) + +## ๐Ÿ“„ License + +MIT -- 2.49.1 From 6129ad61f49184b74aa72823f2b188b9f60c12be Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 15 Jan 2026 20:05:55 +0300 Subject: [PATCH 4/7] fix: minor changes --- .gitea/README.md | 562 ----------------------------------------- src/routes/Page.svelte | 2 +- 2 files changed, 1 insertion(+), 563 deletions(-) delete mode 100644 .gitea/README.md diff --git a/.gitea/README.md b/.gitea/README.md deleted file mode 100644 index 8471c1f..0000000 --- a/.gitea/README.md +++ /dev/null @@ -1,562 +0,0 @@ -# Gitea Actions CI/CD Setup - -This document describes the CI/CD pipeline configuration for the GlyphDiff project using Gitea Actions (GitHub Actions compatible). - -## Table of Contents - -- [Overview](#overview) -- [Workflow Files](#workflow-files) -- [Workflow Triggers](#workflow-triggers) -- [Setup Instructions](#setup-instructions) -- [Self-Hosted Runner Setup](#self-hosted-runner-setup) -- [Caching Strategy](#caching-strategy) -- [Environment Variables](#environment-variables) -- [Troubleshooting](#troubleshooting) - -## Overview - -The CI/CD pipeline consists of four main workflows: - -1. **Lint** - Code quality checks (oxlint, dprint formatting) -2. **Test** - Type checking and E2E tests (Playwright) -3. **Build** - Production build verification -4. **Deploy** - Deployment automation (optional/template) - -All workflows are designed to run on both push and pull request events, with appropriate branch filtering and concurrency controls. - -## Workflow Files - -### `.gitea/workflows/lint.yml` - -**Purpose**: Run code quality checks to ensure code style and formatting standards. - -**Checks performed**: - -- `oxlint` - Fast JavaScript/TypeScript linter -- `dprint check` - Code formatting verification - -**Triggers**: - -- Push to `main`, `develop`, `feature/*` branches -- Pull requests to `main` or `develop` -- Manual workflow dispatch - -**Cache**: Node modules and Yarn cache - -**Concurrency**: Cancels in-progress runs for the same branch when a new commit is pushed. - ---- - -### `.gitea/workflows/test.yml` - -**Purpose**: Run type checking and end-to-end tests. - -**Jobs**: - -#### 1. `type-check` job - -- `tsc --noEmit` - TypeScript type checking -- `svelte-check --threshold warning` - Svelte component type checking - -#### 2. `e2e-tests` job - -- Installs Playwright browsers with system dependencies -- Runs E2E tests using Playwright -- Uploads test report artifacts (retained for 7 days) -- Uploads screenshots on test failure for debugging - -**Triggers**: Same as lint workflow - -**Cache**: Node modules and Yarn cache - -**Artifacts**: - -- `playwright-report` - Test execution report -- `playwright-screenshots` - Screenshots from failed tests - ---- - -### `.gitea/workflows/build.yml` - -**Purpose**: Verify that the production build completes successfully. - -**Steps**: - -1. Checkout repository -2. Setup Node.js v20 with Yarn caching -3. Install dependencies with `--frozen-lockfile` -4. Run `svelte-kit sync` to prepare SvelteKit -5. Build the project with `NODE_ENV=production` -6. Upload build artifacts (`.svelte-kit/output`, `.svelte-kit/build`) -7. Run the preview server and verify it responds (health check) - -**Triggers**: - -- Push to `main` or `develop` branches -- Pull requests to `main` or `develop` -- Manual workflow dispatch - -**Cache**: Node modules and Yarn cache - -**Artifacts**: - -- `build-artifacts` - Compiled SvelteKit output (retained for 7 days) - ---- - -### `.gitea/workflows/deploy.yml` - -**Purpose**: Automated deployment pipeline (template configuration). - -**Current state**: Placeholder configuration. Uncomment and customize one of the deployment examples. - -**Pre-deployment checks**: - -- Must pass linting workflow -- Must pass testing workflow -- Must pass build workflow - -**Deployment examples included**: - -1. **Docker container registry** - Build and push Docker image -2. **SSH deployment** - Deploy to server via SSH -3. **Vercel** - Deploy to Vercel platform - -**Triggers**: - -- Push to `main` branch -- Manual workflow dispatch with environment selection (staging/production) - -**Secrets required** (configure in Gitea): - -- For Docker: `REGISTRY_URL`, `REGISTRY_USERNAME`, `REGISTRY_PASSWORD` -- For SSH: `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_SSH_KEY` -- For Vercel: `VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID` - -## Workflow Triggers - -### Branch-Specific Behavior - -| Workflow | Push Triggers | PR Triggers | Runs on Merge | -| -------- | ------------------------------ | -------------------- | ------------- | -| Lint | `main`, `develop`, `feature/*` | To `main`, `develop` | Yes | -| Test | `main`, `develop`, `feature/*` | To `main`, `develop` | Yes | -| Build | `main`, `develop` | To `main`, `develop` | Yes | -| Deploy | `main` only | None | Yes | - -### Concurrency Strategy - -All workflows use concurrency groups based on the workflow name and branch reference: - -```yaml -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true # or false for deploy workflow -``` - -This ensures: - -- For lint/test/build: New commits cancel in-progress runs (saves resources) -- For deploy: Prevents concurrent deployments (ensures safety) - -## Setup Instructions - -### Step 1: Verify Gitea Actions is Enabled - -1. Navigate to your Gitea instance -2. Go to **Site Administration** โ†’ **Actions** -3. Ensure Actions is enabled -4. Configure default runner settings if needed - -### Step 2: Configure Repository Settings - -1. Go to your repository in Gitea -2. Click **Settings** โ†’ **Actions** -3. Enable Actions for the repository if not already enabled -4. Set appropriate permissions for read/write access - -### Step 3: Push Workflows to Repository - -The workflow files are already in `.gitea/workflows/`. Commit and push them: - -```bash -git add .gitea/workflows/ -git commit -m "Add Gitea Actions CI/CD workflows" -git push origin main -``` - -### Step 4: Verify Workflows Run - -1. Navigate to **Actions** tab in your repository -2. You should see the workflows trigger on the next push -3. Click into a workflow run to view logs and status - -### Step 5: Configure Secrets (Optional - for deployment) - -1. Go to repository **Settings** โ†’ **Secrets** โ†’ **Actions** -2. Click **Add New Secret** -3. Add secrets required for your deployment method - -Example secrets for SSH deployment: - -``` -DEPLOY_HOST=your-server.com -DEPLOY_USER=deploy -DEPLOY_SSH_KEY=-----BEGIN OPENSSH PRIVATE KEY----- -... ------END OPENSSH PRIVATE KEY----- -``` - -## Self-Hosted Runner Setup - -### Option 1: Using Gitea's Built-in Act Runner (Recommended) - -Gitea provides `act_runner` (compatible with GitHub Actions runner). - -#### Install act_runner - -On Linux (Debian/Ubuntu): - -```bash -wget -O /usr/local/bin/act_runner https://gitea.com/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64 -chmod +x /usr/local/bin/act_runner -``` - -Verify installation: - -```bash -act_runner --version -``` - -#### Register the Runner - -1. In Gitea, navigate to repository **Settings** โ†’ **Actions** โ†’ **Runners** -2. Click **New Runner** -3. Copy the registration token -4. Run the registration command: - -```bash -act_runner register \ - --instance https://your-gitea-instance.com \ - --token YOUR_REGISTRATION_TOKEN \ - --name "linux-runner-1" \ - --labels ubuntu-latest,linux,docker \ - --no-interactive -``` - -#### Start the Runner as a Service - -Create a systemd service file at `/etc/systemd/system/gitea-runner.service`: - -```ini -[Unit] -Description=Gitea Actions Runner -After=network.target - -[Service] -Type=simple -User=git -WorkingDirectory=/var/lib/gitea-runner -ExecStart=/usr/local/bin/act_runner daemon -Restart=always -RestartSec=5s - -[Install] -WantedBy=multi-user.target -``` - -Enable and start the service: - -```bash -sudo systemctl daemon-reload -sudo systemctl enable gitea-runner -sudo systemctl start gitea-runner -``` - -#### Check Runner Status - -```bash -sudo systemctl status gitea-runner -``` - -Verify in Gitea: The runner should appear as **Online** with the `ubuntu-latest` label. - -### Option 2: Using Self-Hosted Runners with Docker - -If you prefer Docker-based execution: - -#### Install Docker - -```bash -curl -fsSL https://get.docker.com -o get-docker.sh -sudo sh get-docker.sh -sudo usermod -aG docker $USER -``` - -#### Configure Runner to Use Docker - -Ensure the runner has access to the Docker socket: - -```bash -sudo usermod -aG docker act_runner_user -``` - -The workflows will now run containers inside the runner's Docker environment. - -### Option 3: Using External Runners (GitHub Actions Runner Compatible) - -If you want to use standard GitHub Actions runners: - -```bash -# Download and configure GitHub Actions runner -mkdir actions-runner && cd actions-runner -curl -o actions-runner-linux-x64-2.311.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz -tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz - -# Configure to point to Gitea instance -./config.sh --url https://your-gitea-instance.com --token YOUR_TOKEN -``` - -## Caching Strategy - -### Node.js and Yarn Cache - -All workflows use `actions/setup-node@v4` with built-in caching: - -```yaml -- name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'yarn' -``` - -This caches: - -- `node_modules` directory -- Yarn cache directory (`~/.yarn/cache`) -- Reduces installation time from minutes to seconds on subsequent runs - -### Playwright Cache - -Playwright browsers are installed fresh each time. To cache Playwright (optional optimization): - -```yaml -- name: Cache Playwright binaries - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-playwright- -``` - -## Environment Variables - -### Default Environment Variables - -The workflows use the following environment variables: - -```bash -NODE_ENV=production # For build workflow -NODE_VERSION=20 # Node.js version used across all workflows -``` - -### Custom Environment Variables - -To add custom environment variables: - -1. Go to repository **Settings** โ†’ **Variables** โ†’ **Actions** -2. Click **Add New Variable** -3. Add variable name and value -4. Set scope (environment, repository, or organization) - -Example for feature flags: - -``` -ENABLE_ANALYTICS=false -API_URL=https://api.example.com -``` - -Access in workflow: - -```yaml -env: - API_URL: ${{ vars.API_URL }} - ENABLE_ANALYTICS: ${{ vars.ENABLE_ANALYTICS }} -``` - -## Troubleshooting - -### Workflows Not Running - -**Symptoms**: Workflows don't appear or don't trigger - -**Solutions**: - -1. Verify Actions is enabled in Gitea site administration -2. Check repository Settings โ†’ Actions is enabled -3. Verify workflow files are in `.gitea/workflows/` directory -4. Check workflow YAML syntax (no indentation errors) - -### Runner Offline - -**Symptoms**: Runner shows as **Offline** or **Idle** - -**Solutions**: - -1. Check runner service status: `sudo systemctl status gitea-runner` -2. Review runner logs: `journalctl -u gitea-runner -f` -3. Verify network connectivity to Gitea instance -4. Restart runner: `sudo systemctl restart gitea-runner` - -### Linting Fails with Formatting Errors - -**Symptoms**: `dprint check` fails on CI but passes locally - -**Solutions**: - -1. Ensure dprint configuration (`dprint.json`) is committed -2. Run `yarn dprint fmt` locally before committing -3. Consider adding auto-fix workflow (see below) - -### Playwright Tests Timeout - -**Symptoms**: E2E tests fail with timeout errors - -**Solutions**: - -1. Check `playwright.config.ts` timeout settings -2. Ensure preview server starts before tests run (built into config) -3. Increase timeout in workflow: - ```yaml - - name: Run Playwright tests - run: yarn test:e2e - env: - PLAYWRIGHT_TIMEOUT: 60000 - ``` - -### Build Fails with Out of Memory - -**Symptoms**: Build fails with memory allocation errors - -**Solutions**: - -1. Increase Node.js memory limit: - ```yaml - - name: Build project - run: yarn build - env: - NODE_OPTIONS: --max-old-space-size=4096 - ``` -2. Ensure runner has sufficient RAM (minimum 2GB recommended) - -### Permission Denied on Runner - -**Symptoms**: Runner can't access repository or secrets - -**Solutions**: - -1. Verify runner has read access to repository -2. Check secret names match exactly in workflow -3. Ensure runner user has file system permissions - -### Yarn Install Fails with Lockfile Conflict - -**Symptoms**: `yarn install --frozen-lockfile` fails - -**Solutions**: - -1. Ensure `yarn.lock` is up-to-date locally -2. Run `yarn install` and commit updated `yarn.lock` -3. Do not use `--frozen-lockfile` if using different platforms (arm64 vs amd64) - -### Slow Workflow Execution - -**Symptoms**: Workflows take too long to complete - -**Solutions**: - -1. Verify caching is working (check logs for "Cache restored") -2. Use `--frozen-lockfile` for faster dependency resolution -3. Consider matrix strategy for parallel execution (not currently used) -4. Optimize Playwright tests (reduce test count, increase timeouts only if needed) - -## Best Practices - -### 1. Keep Dependencies Updated - -Regularly update action versions: - -```yaml -- uses: actions/checkout@v4 # Update from v3 to v4 when available -- uses: actions/setup-node@v4 -``` - -### 2. Use Frozen Lockfile - -Always use `--frozen-lockfile` in CI to ensure reproducible builds: - -```bash -yarn install --frozen-lockfile -``` - -### 3. Monitor Workflow Status - -Set up notifications for workflow failures: - -- Email notifications in Gitea user settings -- Integrate with Slack/Mattermost for team alerts -- Use status badges in README - -### 4. Test Locally Before Pushing - -Run the same checks locally: - -```bash -yarn lint # oxlint -yarn dprint check # Formatting check -yarn tsc --noEmit # Type check -yarn test:e2e # E2E tests -yarn build # Build -``` - -### 5. Leverage Git Hooks - -The project uses lefthook for pre-commit/pre-push checks. This catches issues before they reach CI: - -```bash -# Pre-commit: Format code, lint staged files -# Pre-push: Full type check, format check, full lint -``` - -## Additional Resources - -- [Gitea Actions Documentation](https://docs.gitea.com/usage/actions/overview) -- [Gitea act_runner Documentation](https://docs.gitea.com/usage/actions/act-runner) -- [GitHub Actions Documentation](https://docs.github.com/en/actions) -- [SvelteKit Deployment Guide](https://kit.svelte.dev/docs/adapters) -- [Playwright CI/CD Guide](https://playwright.dev/docs/ci) - -## Status Badges - -Add status badges to your README.md: - -```markdown -![Lint](https://your-gitea-instance.com/username/glyphdiff/actions/badges/workflow/lint.yml/badge.svg) -![Test](https://your-gitea-instance.com/username/glyphdiff/actions/badges/workflow/test.yml/badge.svg) -![Build](https://your-gitea-instance.com/username/glyphdiff/actions/badges/workflow/build.yml/badge.svg) -``` - -## Next Steps - -1. **Customize deployment**: Modify `deploy.yml` with your deployment strategy -2. **Add notifications**: Set up workflow failure notifications -3. **Optimize caching**: Add Playwright cache if needed -4. **Add badges**: Include status badges in README -5. **Schedule tasks**: Add periodic tests or dependency updates (optional) - ---- - -**Last Updated**: December 30, 2025 -**Version**: 1.0.0 diff --git a/src/routes/Page.svelte b/src/routes/Page.svelte index a89438d..dae8224 100644 --- a/src/routes/Page.svelte +++ b/src/routes/Page.svelte @@ -24,4 +24,4 @@ import { - + -- 2.49.1 From f97904f165007bafd300fe84c84631690ef16093 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 15 Jan 2026 20:06:51 +0300 Subject: [PATCH 5/7] fix: minor changes --- .../lib/helpers/createVirtualizer/createVirtualizer.svelte.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts index 18ac8f9..d94ee53 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts @@ -1,5 +1,3 @@ -import { untrack } from 'svelte'; - export function createVirtualizer(optionsGetter: () => VirtualizerOptions) { // Reactive State let scrollOffset = $state(0); -- 2.49.1 From 824581551f74587681a41839d420d981c9f1db36 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 15 Jan 2026 20:07:58 +0300 Subject: [PATCH 6/7] fix(createVirtualizer): change the way array is created --- .../lib/helpers/createVirtualizer/createVirtualizer.svelte.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts index d94ee53..417e986 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts @@ -14,7 +14,7 @@ export function createVirtualizer(optionsGetter: () => VirtualizerOptions) { // Only recalculates when item count or measured sizes change. const offsets = $derived.by(() => { const count = options.count; - const result = new Array(count); + const result = Array.from({ length: count }); let accumulated = 0; for (let i = 0; i < count; i++) { -- 2.49.1 From 56e6e450e8cdc0165e42565ac63bd0a9df173aed Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 15 Jan 2026 20:10:44 +0300 Subject: [PATCH 7/7] fix(createVirtualizer): add correct type to offset array --- .../lib/helpers/createVirtualizer/createVirtualizer.svelte.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts index 417e986..5d0fcec 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts @@ -14,7 +14,7 @@ export function createVirtualizer(optionsGetter: () => VirtualizerOptions) { // Only recalculates when item count or measured sizes change. const offsets = $derived.by(() => { const count = options.count; - const result = Array.from({ length: count }); + const result = Array.from({ length: count }); let accumulated = 0; for (let i = 0; i < count; i++) { -- 2.49.1