2 Commits

Author SHA1 Message Date
ddf2f2a416 fixing wallpaper
All checks were successful
Build and Release to Staging / Build Vision Start (push) Successful in 31s
Build and Release to Staging / Build Vision Start Image (push) Successful in 2m54s
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 1m10s
2026-07-02 19:22:19 -03:00
c397e77c01 adjusting agents docs 2026-07-02 19:22:09 -03:00
8 changed files with 475 additions and 133 deletions

113
AGENTS.md Normal file
View File

@@ -0,0 +1,113 @@
# AGENTS.md
Guidance for AI agents (and humans) working on Vision Start. Read before making changes.
---
## 0. First rule: keep `project-context.md` alive
- **Always read `project-context.md` first.** It is the canonical map of the project: structure, features, data model, flow, build/release. Treat it as required reading before any non-trivial change.
- **Always keep it updated.** Whenever you add, remove, rename, or materially change files, modules, features, storage keys, config fields, build/release steps, or workflows, update the relevant section of `project-context.md` in the same change set. If your work touches the structure tree, the data model, the storage layout, or the quick-lookup table, those sections must reflect the new state.
- Keep it a general overview, not a coding-style guide — that distinction lives here in `AGENTS.md`.
---
## 1. Styling guidance
The design language is **glassmorphism**: translucent black surfaces, backdrop blur, thin light borders, subtle shadows, cyan accents, and iOS-like easing.
### Surface recipe (use consistently)
- **Cards / modals / panels:** `bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl shadow-lg`
- **Inputs:** `bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400`
- **Floating buttons (corner):** `bg-black/25 backdrop-blur-md border border-white/10 rounded-xl ... hover:bg-white/25 active:scale-90`
- **Full-screen modal overlay:** `bg-black/90 backdrop-blur-sm` (modals) or `bg-black/60 backdrop-blur-sm` (drawers)
- **Slide-in drawer:** `bg-black/50 backdrop-blur-xl border-l border-white/10`
### Color tokens
- Accent / focus / selection: `cyan-400` (focus ring), `cyan-500` (active/selected background)
- Confirm/Save: `green-500``hover:bg-green-400`
- Cancel/secondary: `gray-600``hover:bg-gray-500`
- Destructive: `red-500``hover:bg-red-400`
- Tertiary (export/import etc.): `slate-700``hover:bg-slate-600`
- Status: online `bg-green-500`, offline `bg-red-500`, pending `bg-gray-500`
- Text: primary `text-white`, secondary `text-slate-300`/`text-slate-400`, muted `text-white/50` (in-app hover states)
### Motion
- Use the custom easing tokens defined in `index.css`: `ease-ios` (default) and `ease-spring` (toggle knobs, drawer slides).
- Standard durations: `duration-150` (buttons), `duration-200` (tiles, toggles, icons), `duration-250`/`duration-300` (modals, drawers).
- Micro-interactions are encouraged: `hover:scale-[1.04]`, `active:scale-90` / `active:scale-95` / `active:scale-[0.96]`, `hover:bg-white/25`.
### Tailwind specifics
- Tailwind **v4** via `@tailwindcss/vite` and `@tailwindcss/postcss`. Custom easing is in `index.css` under `@theme {}` — add new theme tokens there, not in `tailwind.config.js`.
- `tailwind.config.js` has a **safelist** of `w-[Npx]/h-[Npx]` classes because tile/icon sizes are constructed dynamically. If you add new pixel-size classes that are generated at runtime (not literally present in source), add them to the safelist or they will be purged.
- Prefer static classes; only build dynamic class strings when unavoidable and then safelist them.
### Components with established conventions to reuse
- `Dropdown` (`components/Dropdown.tsx`) for selects — single or multi. Has a glassy look and custom arrow. **Always use it** instead of `<select>`/`<input type=...>` for option picking.
- `ToggleSwitch` (`components/ToggleSwitch.tsx`) for boolean toggles — **always use it** instead of checkboxes.
- Modal pattern: backdrop + centered glass card (`bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl`), close on overlay click via `handleOverlayClick` (`if (e.target === e.currentTarget) onClose()`).
- New editors/settings go in `components/configuration/` as a `*Tab.tsx` and are wired into `ConfigurationModal.tsx`.
- New options' size presets follow the existing scale: `tiny | small | medium | large` (see `Header.tsx`, `WebsiteTile.tsx`).
---
## 2. New feature guidance
A "feature" in this project typically means: a new widget, a new settings option, or a new bit of persisted state. When adding one, follow this checklist.
### A. Types & defaults (in order)
1. **`types.ts`** — add new fields to the relevant interface (usually `Config`, sometimes a nested one like `clock`/`serverWidget`). Create a nested object when a feature has 2+ sub-options (mirror the `clock`/`serverWidget` pattern).
2. **`components/services/ConfigurationService.ts` `DEFAULT_CONFIG`** — add the new field with a sane default so existing users (with a previously-saved `Config`) get it via the `{ ...DEFAULT_CONFIG, ...parsed }` merge on load.
3. Any new standalone collections (not inside `Config`) get their own storage key/helper in `ConfigurationService` (see `userWallpapers`, `wallpaperState` for the pattern).
### B. Settings UI (if the feature is user-configurable)
1. Create `components/configuration/<Feature>Tab.tsx` and add its entry to the `tabs` array in `components/ConfigurationModal.tsx`.
2. Receive `config` and an `onChange(updates: Partial<Config>)` callback; emit partial updates — `App.tsx`'s `handleConfigChange` merges shallowly, so update nested objects as whole units (e.g. `{ clock: { ...config.clock, ...updates } }`).
3. Use `Dropdown` / `ToggleSwitch` / range inputs per the styling section above.
### C. Rendering & wiring
4. Render the feature in `App.tsx` (or a dedicated component imported by `App.tsx`), gated by its `config.<feature>.enabled` flag when it can be turned off.
5. Mark up new modal/edit surfaces to match existing modals (backdrop click closes, glass card styling).
### D. Persistence — configs must be properly saved
6. `App.tsx` already persists `config` via `ConfigurationService.saveConfig(config)` in a `useEffect`. **Any state added to `Config` is persisted automatically** — do not introduce parallel ad-hoc `localStorage` writes for config fields.
7. Standalone collections (not in `Config`) need explicit save calls — mirror `userWallpapers`: load on mount, save on every mutation, never keep stale copies.
8. Never write to `chrome.storage.local` directly outside `components/utils/StorageLocalManager.ts`. Always check `checkChromeStorageLocalAvailable()` first and gracefully degrade (the app must still run as a plain web page). Throw informative errors when the storage is required but missing.
### E. Export / import must keep working
9. If you add a new `localStorage` key (other than config fields, which are exported/imported automatically), add it to `REQUIRED_LOCAL_STORAGE_KEYS` in `ConfigurationService.ts` so it is included in exports and restored on imports.
10. After adding fields to `Config`, double check the import path: `importConfig` writes each restored key to `localStorage` and returns `{ config, userWallpapers }`. New config sub-objects flow through automatically because they're inside `config`.
### F. Defaults & migrations
11. Default values must be chosen so a fresh install and an upgrade from an older `Config` both behave sensibly (the load path merges onto `DEFAULT_CONFIG`).
12. If a new field changes behavior in a way that existing users' stored data should be preserved differently, handle the migration inside `loadConfig` (and consider bumping the export `version` field in `exportConfig` if the shape changes non-additively).
### G. Style of new tiles/widgets
13. A new widget should follow the existing widget shape: optional/gated by `config.<feature>.enabled`, positioned with a glass container, and use the surface/duration/easing tokens above.
14. New "editable" tiles/content reuse the `isEditing` mode and the edit/move/delete affordances pattern from `WebsiteTile.tsx` and `CategoryGroup.tsx`.
---
## 3. General engineering rules
- **Language:** TypeScript strict mode (`tsconfig.json` enforces `strict`, `noUnusedLocals`, `noUnusedParameters`, `noFallthroughCasesInSwitch`, `noUncheckedSideEffectImports`). Do not loosen these.
- **Runtime note:** Preact is the actual runtime (`@preact/preset-vite`), but types and imports use `react`/`@types/react`. Do not introduce React-only APIs without verifying Preact compatibility.
- **Path alias:** `@/*` maps to the project root (see `tsconfig.json` `paths`). Source files currently use relative imports (`./`, `../`) — match the surrounding file.
- **No comments** in committed code unless explicitly requested.
- **Build vs. dev:** Prefer verifying with `npm run build` rather than `npm run dev`. The only npm scripts are `dev`, `build`, `preview`.
- **No lint/typecheck script is configured.** Verify TS correctness manually (e.g. `npx tsc --noEmit`), and confirm `npm run build` succeeds before considering a task complete.
- **Don't touch `public/icon-metadata.json`** — it's gitignored and fetched at release-build time by `scripts/prepare_release.sh`. Don't commit a local copy.
- **Don't reintroduce dead code:** `components/EditModal.tsx` imports `lucide-react` and `./IconPicker`, neither of which is a dependency. It is not wired into the app. Use `WebsiteEditModal.tsx` / `CategoryEditModal.tsx` instead, and add the icon picker inline (as in `WebsiteEditModal.tsx`) rather than reviving `IconPicker`.
- **External asset URLs:** any new icons/css/JS pulled from CDNs (e.g. `cdn.jsdelivr.net`) must work offline-or-not without crashing — always provide a fallback (see how `iconService.ts` falls back to Google's S2 favicon service).
- **Manifest & extension constraints:** the extension uses Manifest V3 with `script-src 'self'` CSP and only the `storage` permission. Don't introduce inline scripts/eval, and avoid requesting new permissions unless essential — added permissions can disable updates on installed extensions.
- **Commits:** never commit unless explicitly asked. Don't touch secrets, don't edit `.gitignore`/`.env.local`/`.claude/`, don't run `git config`.
---
## 4. When in doubt
- Read `project-context.md`.
- Mirror the nearest existing equivalent (a similar widget, modal, or config tab) before inventing a new pattern.
- Favor the established glass tokens (`bg-black/25`, `backdrop-blur-md`, `border-white/10`, `cyan-400/500`) over ad-hoc colors.
- Surface failing assumptions (CORS, missing chrome.storage, larger-than-4MB wallpapers) with clear errors, matching the style of `StorageLocalManager.ts` and `iconService.ts`.
- Update `project-context.md` after your change.

24
App.tsx
View File

@@ -31,6 +31,7 @@ const App: React.FC = () => {
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false);
const [config, setConfig] = useState<Config>(() => ConfigurationService.loadConfig());
const [wallpaperVersion, setWallpaperVersion] = useState(0);
useEffect(() => {
ConfigurationService.saveConfig(config);
@@ -53,6 +54,27 @@ const App: React.FC = () => {
setConfig(prev => ({ ...prev, ...newConfig }));
};
const handleNextWallpaper = () => {
const names = config.currentWallpapers;
if (names.length === 0) return;
try {
const state = JSON.parse(localStorage.getItem('wallpaperState') || '{}');
const current = typeof state.currentIndex === 'number' ? state.currentIndex : 0;
const safeCurrent = current < 0 || current >= names.length ? 0 : current;
const nextIndex = (safeCurrent + 1) % names.length;
localStorage.setItem(
'wallpaperState',
JSON.stringify({
lastWallpaperChange: new Date().toISOString(),
currentIndex: nextIndex,
}),
);
} catch (error) {
console.error('Error advancing wallpaper state', error);
}
setWallpaperVersion(v => v + 1);
};
const handleSaveWebsite = (website: Partial<Website>) => {
if (editingWebsite) {
const idToUpdate = website.id ?? editingWebsite.id;
@@ -185,6 +207,7 @@ const App: React.FC = () => {
brightness={config.wallpaperBrightness}
opacity={config.wallpaperOpacity}
wallpaperFrequency={config.wallpaperFrequency}
wallpaperVersion={wallpaperVersion}
/>
<EditButton isEditing={isEditing} onClick={() => setIsEditing(!isEditing)} />
<ConfigurationButton onClick={() => setIsConfigModalOpen(true)} />
@@ -258,6 +281,7 @@ const App: React.FC = () => {
onClose={() => setIsConfigModalOpen(false)}
onSave={handleSaveConfig}
onWallpaperChange={handleWallpaperChange}
onNextWallpaper={handleNextWallpaper}
/>
)}
</main>

View File

@@ -1,39 +0,0 @@
# Vision Startpage Project
## Overview
This project is a highly customizable and stylish startpage built with React. The goal is to create a visually appealing and functional dashboard that serves as a user's entry point to the web.
## Key Features & Design Principles
* **Technology Stack:** The project is built using React and TypeScript.
* **Aesthetics:** The user interface should have a modern, "glassy" or "frosted glass" look (neumorphism/glassmorphism). This involves using transparency, blur effects, and subtle shadows to create a sense of depth.
* **Typography:** Specific font families and types will be used to maintain a consistent and elegant design.
* **Modals:** All modals in the application should follow a specific and consistent design language, contributing to the overall user experience.
* **Production Quality Code:** All code must be written to production standards, with a strong emphasis on readability, maintainability, and performance.
* **Creative & Beautiful Code:** Code should not only be functional but also well-structured, elegant, and creative.
* **Dropdown Component:** A reusable dropdown component (`components/Dropdown.tsx`) has been created for consistent styling and functionality across the application. It features a dark, glassy look with a custom arrow icon.
**Usage Example:**
```typescript jsx
import Dropdown from './components/Dropdown';
// ... inside a React component
<Dropdown
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]}
value={selectedValue}
onChange={handleSelectChange}
name="myDropdown"
/>
```
## Development Guidelines
* Follow the existing code style and conventions.
* Ensure all new components and features align with the established design principles.
* Write clean, commented, and reusable code.
* DO NOT run `npm run dev`, and instead, run `npm run build`.

View File

@@ -13,6 +13,7 @@ interface ConfigurationModalProps {
onSave: (config: Config) => void;
currentConfig: Config;
onWallpaperChange: (newConfig: Partial<Config>) => void;
onNextWallpaper: () => void;
}
const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
@@ -20,6 +21,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
onSave,
currentConfig,
onWallpaperChange,
onNextWallpaper,
}) => {
const [config, setConfig] = useState<Config>(currentConfig);
const [activeTab, setActiveTab] = useState('general');
@@ -175,6 +177,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
onAddWallpaper={handleAddWallpaper}
onAddWallpaperFile={handleAddWallpaperFile}
onDeleteWallpaper={handleDeleteWallpaper}
onNextWallpaper={onNextWallpaper}
/>
)}
{activeTab === 'clock' && (

View File

@@ -10,16 +10,20 @@ interface WallpaperProps {
brightness: number;
opacity: number;
wallpaperFrequency: string;
wallpaperVersion: number;
}
const getWallpaperUrlByName = async (name: string): Promise<string | undefined> => {
if (!name) return undefined;
const foundInBase = baseWallpapers.find((w: WallpaperType) => w.name === name);
if (foundInBase) {
return foundInBase.url || foundInBase.base64;
}
const userWallpapers: WallpaperType[] = JSON.parse(localStorage.getItem('userWallpapers') || '[]');
const foundInUser = userWallpapers.find((w: WallpaperType) => w.name === name);
try {
const storedUserWallpapers: WallpaperType[] =
JSON.parse(localStorage.getItem('userWallpapers') || '[]');
const foundInUser = storedUserWallpapers.find((w: WallpaperType) => w.name === name);
if (foundInUser) {
try {
const wallpaperData = await getWallpaperFromChromeStorageLocal(name);
@@ -32,13 +36,15 @@ const getWallpaperUrlByName = async (name: string): Promise<string | undefined>
return undefined;
}
}
} catch (error) {
console.error('Error reading userWallpapers from localStorage', error);
}
return undefined;
};
const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness, opacity, wallpaperFrequency }) => {
const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness, opacity, wallpaperFrequency, wallpaperVersion }) => {
const [imageUrl, setImageUrl] = useState<string | undefined>(undefined);
const [currentWallpaperIndex, setCurrentWallpaperIndex] = useState<number>(0);
// Helper to parse wallpaperFrequency string to ms
const parseFrequencyToMs = (freq: string): number => {
@@ -54,36 +60,61 @@ const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness,
useEffect(() => {
const updateWallpaper = async () => {
if (wallpaperNames.length === 0) return;
// Read wallpaperState from localStorage
if (wallpaperNames.length === 0) {
setImageUrl(undefined);
localStorage.setItem(
'wallpaperState',
JSON.stringify({ lastWallpaperChange: new Date().toISOString(), currentIndex: 0 }),
);
return;
}
const wallpaperState = JSON.parse(localStorage.getItem('wallpaperState') || '{}');
const lastChange = wallpaperState.lastWallpaperChange ? new Date(wallpaperState.lastWallpaperChange).getTime() : 0;
const lastChange = wallpaperState.lastWallpaperChange
? new Date(wallpaperState.lastWallpaperChange).getTime()
: 0;
const now = Date.now();
const freqMs = parseFrequencyToMs(wallpaperFrequency);
let currentIndex = typeof wallpaperState.currentIndex === 'number' ? wallpaperState.currentIndex : 0;
// If enough time has passed, pick a new wallpaper
if (now - lastChange >= freqMs) {
currentIndex = (currentIndex + 1) % wallpaperNames.length;
localStorage.setItem('wallpaperState', JSON.stringify({
lastWallpaperChange: new Date().toISOString(),
currentIndex
}));
} else {
// Keep currentIndex in sync with localStorage if not updating
localStorage.setItem('wallpaperState', JSON.stringify({
lastWallpaperChange: wallpaperState.lastWallpaperChange || new Date().toISOString(),
currentIndex
}));
let storedIndex =
typeof wallpaperState.currentIndex === 'number' ? wallpaperState.currentIndex : 0;
if (storedIndex < 0 || storedIndex >= wallpaperNames.length) storedIndex = 0;
const shouldRotate = now - lastChange >= freqMs;
let resolvedIndex = shouldRotate
? (storedIndex + 1) % wallpaperNames.length
: storedIndex;
const tried = new Set<number>();
let resolvedUrl: string | undefined;
for (let i = 0; i < wallpaperNames.length; i++) {
if (tried.has(resolvedIndex)) break;
tried.add(resolvedIndex);
const url = await getWallpaperUrlByName(wallpaperNames[resolvedIndex]);
if (url) {
resolvedUrl = url;
break;
}
setCurrentWallpaperIndex(currentIndex);
const wallpaperName = wallpaperNames[currentIndex];
const url = await getWallpaperUrlByName(wallpaperName);
setImageUrl(url);
resolvedIndex = (resolvedIndex + 1) % wallpaperNames.length;
}
const nextLastChange = shouldRotate
? new Date().toISOString()
: wallpaperState.lastWallpaperChange || new Date().toISOString();
localStorage.setItem(
'wallpaperState',
JSON.stringify({
lastWallpaperChange: nextLastChange,
currentIndex: resolvedIndex,
}),
);
setImageUrl(resolvedUrl);
};
updateWallpaper();
// No timer, just run on render/dependency change
}, [wallpaperNames, wallpaperFrequency]);
}, [wallpaperNames, wallpaperFrequency, wallpaperVersion]);
if (!imageUrl) return null;

View File

@@ -11,6 +11,7 @@ interface ThemeTabProps {
onAddWallpaper: (name: string, url: string) => Promise<void>;
onAddWallpaperFile: (file: File) => Promise<void>;
onDeleteWallpaper: (wallpaper: Wallpaper) => Promise<void>;
onNextWallpaper: () => void;
}
const ThemeTab: React.FC<ThemeTabProps> = ({
@@ -22,6 +23,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
onAddWallpaper,
onAddWallpaperFile,
onDeleteWallpaper,
onNextWallpaper,
}) => {
const [newWallpaperName, setNewWallpaperName] = useState('');
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
@@ -177,7 +179,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
/>
<button
onClick={handleAddWallpaper}
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
className="bg-cyan-500 hover:bg-cyan-400 active:scale-95 text-white font-bold py-2 px-4 rounded-lg transition-all duration-150 ease-ios"
>
Add
</button>
@@ -221,6 +223,24 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
</div>
</>
)}
<div className="flex justify-center pt-2">
<button
onClick={onNextWallpaper}
disabled={config.currentWallpapers.length === 0}
className="flex items-center gap-2 bg-black/25 backdrop-blur-md border border-white/10 hover:bg-white/25 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-semibold py-2 px-4 rounded-xl transition-all duration-150 ease-ios"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zM4.5 7.5a.5.5 0 0 1 .5-.5h5.379L8.646 5.354a.5.5 0 1 1 .708-.708l2.5 2.5a.5.5 0 0 1 0 .708l-2.5 2.5a.5.5 0 0 1-.708-.708L10.379 8H5a.5.5 0 0 1-.5-.5z" />
</svg>
Next Wallpaper
</button>
</div>
</div>
);
};

View File

@@ -1,57 +0,0 @@
# Notes Widget Implementation Plan
## Overview
This document outlines the implementation plan for a Notes / Scratchpad Widget feature that:
1. Provides a text area that saves content to local storage automatically
2. Includes bold and italic formatting buttons
3. Has a toggle icon to show/hide the widget
4. Has a glassy/frosty design with strong blur
## Implementation Steps
### 1. Update Config Interface
In `types.ts`, add a new `notesWidget` configuration object:
```typescript
notesWidget: {
enabled: boolean;
}
```
### 2. Update ConfigurationModal Component
In `components/ConfigurationModal.tsx`:
- Add `notesWidget` to the config state initialization (lines 18-44)
- Add a new "Notes" tab to the tab navigation (lines 259-284)
- Add a toggle switch for notesWidget.enabled in the Notes tab (lines 552-560)
- Add the Notes tab content with the toggle switch
### 3. Create NotesWidget Component
Create a new file `components/NotesWidget.tsx`:
- Text area for notes input
- Bold and italic formatting buttons
- Toggle icon for showing/hiding the widget
- Glassy/frosty design with strong blur
- Auto-save to localStorage using `userNotes` key
### 4. Update App Component
In `App.tsx`:
- Add notesWidget to the defaultConfig (lines 14-35)
- Add the NotesWidget component to the render output when enabled in config (lines 250-251)
- Add logic to save/load notes from localStorage
## Technical Details
### Configuration Structure
The configuration will be stored in the same way as other widgets:
```typescript
config: {
notesWidget: {
enabled: boolean;
}
}
```
### Local Storage Key
All notes text will be saved to localStorage with the key `userNotes`.
### UI Design
- Widget will be positioned on the left side of the screen
- Vertically centered using flexbox
- Glassy/frosty design using backdrop-blur and background transparency
- Strong blur effect (backdrop-blur-2xl or similar)
- Toggle icon will be positioned on the left edge of the widget
- When hidden, icon shows a "show" indicator
- When open, icon shows a "hide" indicator
### Formatting Buttons
- Bold button (B)
- Italic button (I)
- These will use HTML formatting (bold/italic tags) or rich text approach

247
project-context.md Normal file
View File

@@ -0,0 +1,247 @@
---
project_name: Vision Start
date: 2026-07-02
type: 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-vite` so Preact is the actual React runtime).
- Persistent state lives in `localStorage` and, 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) |
| 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/`) |
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 across categories 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. Supports rotating through multiple wallpapers at a cadence (`1h``2d`).
- 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.local` when available, falling back to storing the URL directly on CORS failure.
- **Icon library & auto-fetch** — Website icons can be picked from the [Dashboard Icons](https://dashboardicons.com/) library (metadata pre-downloaded to `public/icon-metadata.json`) or auto-fetched from the target site's `apple-touch-icon`/`icon` link 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 `localStorage` keys) and **Import** (restores from JSON and reloads the page).
- **Edit mode** — Toggle via the top-left pencil button; reveals per-tile move/edit buttons, per-category edit buttons, and "add" buttons.
- **Glassmorphism design language** — Translucent surfaces, `backdrop-blur`, subtle borders, and custom iOS-like easing tokens (`ease-ios`, `ease-spring`) defined in `index.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 + custom easing tokens
├── 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)
├── icon.png # Extension icon source
├── 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
│ ├── EditModal.tsx # Legacy drag-and-drop editor (imports IconPicker & lucide-react; not wired into App.tsx)
│ ├── 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 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 screenshots (dark page, editing, configuration)
├── scripts/
│ ├── 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
│ └── 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/
├── .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`, `categoryId`
- **`Server`** — `id`, `name`, `address`
- **`Category`** — `id`, `name`, `websites: Website[]`
- **`Wallpaper`** — `name`, optional `url` or `base64`
- **`Config`** — Everything else: title, wallpaper list + 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.
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)
1. `index.html` loads `index.tsx`, which mounts `<App/>` into `#root`.
2. `App.tsx` initializes state from `localStorage` (`categories`) and `ConfigurationService.loadConfig()` (`config`), falling back to defaults.
3. `useEffect` hooks persist `config` and `categories` back to `localStorage` whenever they change.
4. 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>`.
5. Edit / configuration interactions update central `App` state; the existing `useEffect`s persist it.
---
## 7. Build, Release & Deployment
### Local development / preview
```bash
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)
- **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`.
- **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-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 by `WebsiteEditModal` for the icon picker; gitignored).
---
## 8. Notable Behaviors & Quirks
- **`EditModal.tsx` is not wired up.** It imports `./IconPicker` and `lucide-react`, neither of which exist in `package.json` or this tree. It appears to be a legacy/abandoned editor superseded by `WebsiteEditModal.tsx` + `CategoryEditModal.tsx`. Treat it as dead code unless reintended.
- **`EditModal` references `lucide-react` and `IconPicker`** which are **not** in `package.json`; do not rely on them.
- **`@hello-pangea/dnd`** is used only inside `EditModal.tsx` (legacy) and `ServerWidgetTab.tsx` (server reorder). `WebsiteTile` moves tile via simple left/right buttons, not drag-and-drop.
- **Chrome storage is optional.** `StorageLocalManager` checks availability once (`checkChromeStorageLocalAvailable`) and caches it. When unavailable (e.g., running as a plain web page), wallpaper upload/delete flows are gated off and `addWallpaperToChromeStorageLocal` throws.
- **Wallpaper rotation** is time-based, evaluated on render/mount rather than via a timer. It reads `wallpaperState` from `localStorage`, advances the index if the frequency window has elapsed, and writes it back. The renderer clamps `currentIndex` to 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, `wallpaperState` is reset and the background is hidden. A manual "Next Wallpaper" button in the Theme tab advances `currentIndex` (with wraparound) and bumps a `wallpaperVersion` nonce in `App.tsx` that retriggers the renderer.
- **Icon picker** in `WebsiteEditModal` loads `/icon-metadata.json` at runtime and expands each icon's `colors` into duplicate-name entries so color variants are searchable.
- **`tsconfig.json` does not emit JS** (`noEmit: true`, bundler resolution); Vite handles all transpilation.
- **`tailwind.config.js` safelists** a set of `w-[Npx]/h-[Npx]` classes because `WebsiteTile` generates tailwind classes dynamically from `tileSize` (`w-[42px]`, etc.).
- **Project guidance note** in `PROJECT.md`: do not use `npm run dev` for real verification — use `npm run build`.
- **`.claude/` and `.env.local`** are 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`) |
| Release packaging | `scripts/prepare_release.sh`, `scripts/check_virustotal.sh`, `.gitea/workflows/release.yaml` |
| Docker build | `Dockerfile` |
---
_Last updated: 2026-07-02. Generated as a general project overview; not a coding-style guide._