20 KiB
project_name, date, type
| project_name | date | type |
|---|---|---|
| Vision Start | 2026-07-08 | general_overview |
Vision Start — Project Context
A general, non-normative overview of the Vision Start project: what it is, how it's structured, what features it offers, and which files do what. This document is intended as a map for humans and AI agents to orient themselves in the codebase. It deliberately avoids coding-style prescriptive rules.
1. What Is This Project?
Vision Start is a glassmorphism-styled, highly customizable browser startpage (new-tab page).
- Distributed as a Chrome/Chromium extension (Manifest V3) that overrides the new tab with
index.html. - Also runnable as a standalone web app and shipped as a Docker image served via nginx.
- Built with React + TypeScript, bundled with Vite, and styled with Tailwind CSS v4 (using the
@preact/preset-viteso Preact is the actual React runtime). - Persistent state lives in
localStorageand, when available,chrome.storage.local.
Live instances / artifacts:
- Public demo:
http://vision-start.ivanch.me - Source:
https://gitea.com/ivan/vision-start.git - Container registry:
git.ivanch.me/ivanch/vision-start - Releases:
https://git.ivanch.me/ivanch/vision-start/releases/latest
2. Technology Stack
| Layer | Tech |
|---|---|
| Language | TypeScript (~5.7), target ES2020, strict mode |
| UI runtime | Preact 10 (via @preact/preset-vite); types from @types/react 19 |
| Bundler / dev server | Vite 6 |
| Styling | Tailwind CSS v4 (@tailwindcss/vite plugin + @tailwindcss/postcss + autoprefixer) |
| Drag & drop | @hello-pangea/dnd 18 |
| Build output | Plain static files in dist/ (relative base: './', single CSS bundle; modals code-split into separate JS chunks via React.lazy) |
| Container | Node 22 Alpine build stage → nginx Alpine serving dist/ |
| Extension packaging | Manifest V3 (manifest.json) consuming dist/ + manifest.json zipped as vision-start-<tag>.zip |
| CI/CD | Gitea Actions workflows (.gitea/workflows/) |
| Release screenshots | Playwright 1.61.1 + Chromium capture the deployed production page at a fixed 1280×800 viewport |
Entry points: index.html → index.tsx → App.tsx.
3. Feature Overview
The startpage is composed of widgets and a configuration panel:
- Website Tiles — Bookmarks organized into categories. Each tile shows an icon + name and opens the configured URL. Tiles can be added, edited, deleted, and moved left/right within their own category (reordering) while in edit mode.
- Categories — Groupings of website tiles (e.g. "Search"). Add/edit/delete/name.
- Clock — Optional header clock with selectable size, font, and 12h/24h format.
- Title — Optional big header title (text + size configurable).
- Server Status Widget — Bottom-center glass pill that periodically "pings" configured server addresses and shows online/offline indicators. Ping uses an image-load trick (
components/utils/jsping.js) with a 5s timeout, at a configurable frequency. - Wallpaper background — Fullscreen background image with adjustable blur, brightness, and opacity, rendered behind a soft readability layer for the liquid-glass UI. Supports rotating through multiple wallpapers at an hourly cadence selected by slider (
1h–48h).- Built-in wallpapers: Abstract, Abstract Red, Beach, Dark, Mountain, Waves (
components/utils/baseWallpapers.ts). - User wallpapers: upload from a file (≤4MB, ≤4.5MB base64) or add by URL; stored in
chrome.storage.localwhen available, falling back to storing the URL directly on CORS failure.
- Built-in wallpapers: Abstract, Abstract Red, Beach, Dark, Mountain, Waves (
- Icon library & auto-fetch — Website icons can be picked from the Dashboard Icons library (metadata pre-downloaded to
public/icon-metadata.json) or auto-fetched from the target site'sapple-touch-icon/iconlink tags, with a fallback to Google's S2 favicon service. - Configuration panel — Slide-in right-side modal with four tabs: General, Theme, Clock, Server Widget. Includes Export (downloads a JSON bundle of selected
localStoragekeys) and Import (restores from JSON and reloads the page). - Edit mode — Toggle via the top-left pencil button; reveals per-tile glass action toolbars, per-category edit buttons, and ghost glass "add" tiles.
- Liquid glass design language — Soft translucent surfaces, restrained edge highlights, moderate backdrop blur, soft shadows, cyan focus states, and iOS-like easing tokens (
ease-ios,ease-spring,ease-liquid) defined inindex.css.
Performance notes:
- Modals (
ConfigurationModal,WebsiteEditModal,CategoryEditModal) are code-split viaReact.lazy+Suspenseand only loaded when opened.ConfigurationModalis the heaviest chunk (it pulls in@hello-pangea/dndviaServerWidgetTab); the rest of@hello-pangea/dndis isolated from the initial load. WebsiteTileandCategoryGroupare wrapped inReact.memo;App.tsxhandlers areuseCallback-stabilized and pure alignment helpers are hoisted to module scope, so opening a modal / toggling edit no longer re-renders every tile.Clockupdates on the minute boundary (onesetTimeout→setInterval(60_000)) instead of every second.jspingcancels its 5s timeout on image resolve/error and nulls theImagehandlers, preventing leaks across ping cycles.ServerWidgetbatches pending-status updates into onesetStateand depends on a stable servers signature (ids+addresses) so unrelated config edits don't restart pings.- Icon metadata (
/icon-metadata.json) is module-level cached and hydrated into each icon-picker instance, fetched lazily on first focus of the icon field withcache: 'force-cache', filter debounced ~150ms, and color variants are expanded lazily during filtering rather than upfront. Wallpapercaches resolved wallpaper URLs in a module-levelMap; its image transition and readability overlay classes live inindex.css.
Planned / To-do (tracked in README.md):
- Dynamic Weather widget, Search Bar widget, draggable/resizable grid system, Notes/Scratchpad widget, theming (light/dark, accent colors, wallpaper-derived accents, minimal feel toggle), and a general "refactor everything" note.
4. Project Structure
vision-start/
├── App.tsx # Root React component; central state + handlers
├── index.tsx # React root mount (ReactDOM.createRoot)
├── index.html # HTML shell, links index.css, mounts #root
├── index.css # Tailwind v4 import + liquid glass utilities, wallpaper overlays, and easing tokens
├── vite-env.d.ts # Type declaration for CSS imports used by TypeScript verification
├── types.ts # Core domain types (Config, Category, Website, Server, Wallpaper)
├── constants.tsx # DEFAULT_CATEGORIES seed data
├── manifest.json # Chrome MV3 manifest (newtab override, storage permission)
│
├── components/
│ ├── Clock.tsx # Header clock widget
│ ├── Wallpaper.tsx # Background image renderer + rotation logic
│ ├── WebsiteTile.tsx # Individual bookmark tile + loading/edit controls
│ ├── WebsiteEditModal.tsx # Add/edit a website (icon picker inside)
│ ├── CategoryEditModal.tsx # Add/edit a category
│ ├── ConfigurationModal.tsx # Tabbed settings drawer with Export/Import
│ ├── ServerWidget.tsx # Bottom server status pill
│ ├── Dropdown.tsx # Reusable glassy dropdown (single/multi select)
│ ├── ToggleSwitch.tsx # Reusable toggle switch
│ │
│ ├── layout/
│ │ ├── Header.tsx # Renders Clock + Title
│ │ ├── CategoryGroup.tsx # Renders a category's title + its tiles + edit controls
│ │ ├── EditButton.tsx # Top-left pencil toggle
│ │ └── ConfigurationButton.tsx # Top-right gear button
│ │
│ ├── configuration/
│ │ ├── GeneralTab.tsx # Title, sizes, alignment, tile size
│ │ ├── ThemeTab.tsx # Background selection, wallpaper cadence, wallpaper mgmt, blur/brightness/opacity
│ │ ├── ClockTab.tsx # Clock enable/size/font/format
│ │ └── ServerWidgetTab.tsx # Server widget enable/ping/servers (drag-to-reorder)
│ │
│ ├── services/
│ │ └── ConfigurationService.ts # DEFAULT_CONFIG, load/save config & wallpapers, add/delete wallpaper, export/import config, reset wallpaper state
│ │
│ └── utils/
│ ├── baseWallpapers.ts # Built-in wallpaper catalog (imgur/wallpapershome URLs)
│ ├── iconService.ts # getWebsiteIcon: fetch HTML, parse apple-touch-icon/icon, fallback to Google favicons
│ ├── jsping.js # Image-load based "ping" with 5s timeout (used by ServerWidget)
│ └── StorageLocalManager.ts # chrome.storage.local wrappers + availability check; wallpaper fetch/base64/URL storage
│
├── public/
│ ├── favicon.ico
│ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build)
│
├── screenshots/ # README/release screenshots (home, editing, configuration; regenerated at 1280×800)
├── scripts/
│ ├── capture_screenshots.mjs # Loads demoData, captures home/edit/configuration with Playwright, and can target a deployed URL
│ ├── demoData.json # Base64-encoded localStorage fixture used for release screenshots
│ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons
│ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict
│
├── .gitea/workflows/
│ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging
│ ├── pull-request.yaml # On PR updates: build/zip, capture screenshots, and publish an inline visual preview
│ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod
│
├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf)
├── nginx.conf # nginx gzip config (JSON/JS/CSS/SVG/XML), mounted into container
├── .dockerignore
├── .gitignore # Ignores node_modules, dist, .claude/, public/icon-metadata.json, etc.
├── postcss.config.cjs # @tailwindcss/postcss + autoprefixer
├── tailwind.config.js # Content globs + safelist of dynamic w-/h- sizes
├── tsconfig.json # Strict TS, bundler resolution, `@/*` path alias to project root
├── vite.config.ts # preact + tailwindcss plugins; single-bundle output; base './'
├── package.json # Scripts: dev, build, preview
├── README.md # Public-facing readme + feature list + roadmap
└── .env.local # Local env (not tracked in context here)
5. Data Model & State
The shape of all persisted data lives in types.ts:
Website—id,name,url,icon,categoryIdServer—id,name,addressCategory—id,name,websites: Website[]Wallpaper—name, optionalurlorbase64Config— Everything else: title, wallpaper list + hourly frequency/blur/brightness/opacity, titleSize, vertical & horizontal alignment, tileSize,clock {enabled, size, font, format},serverWidget {enabled, pingFrequency, servers}
ConfigurationService (components/services/ConfigurationService.ts) is the source of truth for default config and persistence helpers. Config loaded from localStorage (and imported from export files) is run through normalizeConfig(), a schema-driven deep merge against DEFAULT_CONFIG: nested blocks (clock, serverWidget) inherit new sub-fields added to defaults, stored values with the wrong type fall back to defaults, and keys absent from the default schema are pruned. The merge is self-healing — App.tsx's persist-useEffect writes the normalized shape back to localStorage on first run.
Storage layout (browser-side):
| Key | Where | Contents |
|---|---|---|
config |
localStorage |
The full Config JSON |
categories |
localStorage |
Category[] JSON |
userWallpapers |
localStorage |
Wallpaper[] index (names) |
wallpaperState |
localStorage |
{ lastWallpaperChange, currentIndex } for rotation |
<wallpaperName> |
chrome.storage.local (when available) |
base64 (or URL on CORS failure) image data |
Export/import bundles keys: config, categories, userWallpapers, wallpaperState.
6. Application Flow (high level)
index.htmlloadsindex.tsx, which mounts<App/>into#root.App.tsxinitializes state fromlocalStorage(categories) andConfigurationService.loadConfig()(config), falling back to defaults.useEffecthooks persistconfigandcategoriesback tolocalStoragewhenever they change.- The screen renders:
<Wallpaper>behind everything (fetches URL/base64 from base catalog or chrome.storage.local; rotates per frequency).<EditButton>(top-left) and<ConfigurationButton>(top-right).<Header>(clock + title).- One
<CategoryGroup>per category (renders its<WebsiteTile>s and, in edit mode, add/edit/move controls). - Optional
<ServerWidget>if enabled. - Conditionally one of:
<WebsiteEditModal>,<CategoryEditModal>,<ConfigurationModal>.
- Edit / configuration interactions update central
Appstate; the existinguseEffects persist it.
7. Build, Release & Deployment
Local development / preview
npm install
npm run dev # Vite dev server (note: PROJECT.md says prefer `npm run build` for real testing)
npm run build # Production build → dist/
npm run preview # Serve built dist/
Chrome extension install (manual)
Build, then combine dist/ + manifest.json into a folder and "Load unpacked" from chrome://extensions. The release workflow automates this zip.
Docker
Dockerfile builds in Node 22 Alpine (npm ci → runs scripts/prepare_release.sh → npm run build) and serves /app/dist + manifest.json via nginx:alpine on port 80.
CI/CD (Gitea Actions)
release.yamlvalidates eachvX.Y.Ztag and stamps the version intomanifest.json("version": "0.0.0"→ the tag) in each build checkout before producing the extension archive and production image.pull-request.yaml— Triggers on pull request open, reopen, and synchronization. It builds and uploads a PR extension archive containingdist/, unpacks that archive in a separate Playwright job to generate the three demo screenshots, and uploads both artifacts. For same-repository PRs, it maintains one Gitea PR comment with inline image attachments; fork PRs retain artifacts but skip the comment because their workflow token is read-only.- After
deploy_vision_startsucceeds,release.yamluses Playwright Chromium against the deployed production page and seeds each browser context fromscripts/demoData.json. It regenerateshome.png,editing.png, andconfiguration.pngat exactly 1280×800, uploads them as artifacts, and attaches them as individual Gitea release assets. main.yaml— Triggers on push tomain(andworkflow_dispatch). Builds, pushes astagingmulti-arch (amd64/arm64) image togit.ivanch.me/ivanch/vision-start:staging, then SSH-deploys on the staging host viadocker compose up -d --force-recreate.release.yaml— Triggers onv*tags. Builds, zipsdist/+manifest.jsonasvision-start-<tag>.zip, runsscripts/check_virustotal.shagainst it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes alatestmulti-arch image, and SSH-deploys to production.
Required CI secrets (referenced by the workflows): REGISTRY_PASSWORD, HOST, USERNAME, KEY, PORT, STAGING_DIR, PROD_DIR, VIRUSTOTAL_APIKEY.
External assets fetched at build time by scripts/prepare_release.sh:
https://raw.githubusercontent.com/homarr-labs/dashboard-icons/.../metadata.json→public/icon-metadata.json(used byWebsiteEditModalfor the icon picker; gitignored).
8. Notable Behaviors & Quirks
EditModal.tsxhas been removed. It was a legacy drag-and-drop editor that imported non-existentlucide-reactand./IconPicker; it was never wired intoApp.tsx. UseWebsiteEditModal.tsx/CategoryEditModal.tsxinstead.@hello-pangea/dndis used only inServerWidgetTab.tsx(server reorder), which itself is imported by the lazy-loadedConfigurationModal, so it lives in a separate chunk and is absent from the initial page load.WebsiteTilemoves tiles within their own category via simple left/right buttons (no cross-category movement), not drag-and-drop.- Chrome storage is optional.
StorageLocalManagerchecks availability once (checkChromeStorageLocalAvailable) and caches it. When unavailable (e.g., running as a plain web page), wallpaper upload/delete flows are gated off andaddWallpaperToChromeStorageLocalthrows. - Wallpaper rotation is time-based, evaluated on render/mount rather than via a timer. It reads
wallpaperStatefromlocalStorage, advances the index if the frequency window has elapsed, and writes it back; the frequency is clamped to 1–48 hours, while older saved values like1d/2dstill resolve to their hour equivalents. The renderer clampscurrentIndexto the valid range of the current selection and walks the list forward to find a wallpaper whose data actually resolves (so deleting the currently-displayed wallpaper, or shrinking the selection, never leaves the background blank); if no wallpaper resolves, the background layer is hidden. When the selection becomes empty,wallpaperStateis reset and the background is hidden. A manual "Next Wallpaper" button in the Theme tab advancescurrentIndex(with wraparound) and bumps awallpaperVersionnonce inApp.tsxthat retriggers the renderer. - Icon picker in
WebsiteEditModalloads/icon-metadata.jsonat runtime and expands each icon'scolorsinto duplicate-name entries so color variants are searchable. tsconfig.jsondoes not emit JS (noEmit: true, bundler resolution); Vite handles all transpilation.tailwind.config.jssafelists a set ofw-[Npx]/h-[Npx]classes becauseWebsiteTilegenerates tailwind classes dynamically fromtileSize(w-[42px], etc.).- Project guidance note in
PROJECT.md: do not usenpm run devfor real verification — usenpm run build. .claude/and.env.localare local-only / gitignored; not part of shipped artifacts.
9. Where Things Live (Quick Lookup)
| You want to find… | Look in… |
|---|---|
| The default config | components/services/ConfigurationService.ts (DEFAULT_CONFIG) |
| The default seed bookmarks | constants.tsx (DEFAULT_CATEGORIES) |
| Built-in wallpaper list | components/utils/baseWallpapers.ts |
| Type definitions | types.ts |
| Main app wiring (state, handlers, layout) | App.tsx |
| Settings UI | components/ConfigurationModal.tsx + components/configuration/*Tab.tsx |
| Wallpaper rendering/rotation | components/Wallpaper.tsx |
| Server status logic | components/ServerWidget.tsx + components/utils/jsping.js |
| Icon fetch / picker / metadata | components/utils/iconService.ts, components/WebsiteEditModal.tsx, public/icon-metadata.json |
| chrome.storage.local access | components/utils/StorageLocalManager.ts |
| Export/import config | components/services/ConfigurationService.ts (exportConfig, importConfig) |
| Build/release/PR pipelines | scripts/prepare_release.sh, scripts/capture_screenshots.mjs, scripts/check_virustotal.sh, .gitea/workflows/pull-request.yaml, .gitea/workflows/release.yaml |
| Docker build | Dockerfile |
Last updated: 2026-07-10. Generated as a general project overview; not a coding-style guide.