Compare commits
3 Commits
v0.2.0
...
7a137abb66
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a137abb66 | |||
| 023b2ffdc5 | |||
| c9dafd76d1 |
@@ -25,17 +25,21 @@ jobs:
|
|||||||
- name: Run build
|
- name: Run build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Package dist as zip
|
- name: Prepare release
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
bash scripts/prepare_release.sh
|
||||||
zip -r ../vision-start-build.zip .
|
mv dist vision-start/
|
||||||
|
mv assets vision-start/
|
||||||
|
mv manifest.json vision-start/
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Create zip archive
|
||||||
|
run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: vision-start-build
|
name: release-zip
|
||||||
path: vision-start-build.zip
|
path: vision-start-${{ gitea.ref_name }}.zip
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
build_vision_start:
|
build_vision_start:
|
||||||
name: Build Vision Start Image
|
name: Build Vision Start Image
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
bash scripts/prepare_release.sh
|
bash scripts/prepare_release.sh
|
||||||
mv dist vision-start/
|
mv dist vision-start/
|
||||||
|
mv assets vision-start/
|
||||||
mv manifest.json vision-start/
|
mv manifest.json vision-start/
|
||||||
- name: Create zip archive
|
- name: Create zip archive
|
||||||
run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
|
run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Vision Start
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center; font-size: 1.5rem;">
|
<div style="display: flex; justify-content: center; font-size: 1.5rem;">
|
||||||
A light liquid-glass, modern and customizable startpage built with React.
|
A light, modern and customizable startpage built with React.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style="display: block; text-align: center; font-size: 1.2rem;">Try it here: <a href="http://vision-start.ivanch.me">http://vision-start.ivanch.me</a></span>
|
<span style="display: block; text-align: center; font-size: 1.2rem;">Try it here: <a href="http://vision-start.ivanch.me">http://vision-start.ivanch.me</a></span>
|
||||||
|
|||||||
BIN
assets/icons/vision-128.png
Normal file
BIN
assets/icons/vision-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/icons/vision-16.png
Normal file
BIN
assets/icons/vision-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 314 B |
BIN
assets/icons/vision-48.png
Normal file
BIN
assets/icons/vision-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 657 B |
@@ -13,15 +13,18 @@ interface WallpaperProps {
|
|||||||
wallpaperVersion: number;
|
wallpaperVersion: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIN_WALLPAPER_FREQUENCY_MS = 60 * 60 * 1000;
|
||||||
|
const MAX_WALLPAPER_FREQUENCY_MS = 48 * 60 * 60 * 1000;
|
||||||
|
const DEFAULT_WALLPAPER_FREQUENCY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const parseFrequencyToMs = (freq: string): number => {
|
const parseFrequencyToMs = (freq: string): number => {
|
||||||
if (!freq) return 24 * 60 * 60 * 1000; // default 1 day
|
if (!freq) return DEFAULT_WALLPAPER_FREQUENCY_MS;
|
||||||
const match = freq.match(/(\d+)(h|d)/);
|
const match = freq.match(/^(\d+)(h|d)$/);
|
||||||
if (!match) return 24 * 60 * 60 * 1000;
|
if (!match) return DEFAULT_WALLPAPER_FREQUENCY_MS;
|
||||||
const value = parseInt(match[1], 10);
|
const value = parseInt(match[1], 10);
|
||||||
const unit = match[2];
|
const unit = match[2];
|
||||||
if (unit === 'h') return value * 60 * 60 * 1000;
|
const frequencyMs = unit === 'd' ? value * 24 * 60 * 60 * 1000 : value * 60 * 60 * 1000;
|
||||||
if (unit === 'd') return value * 24 * 60 * 60 * 1000;
|
return Math.min(MAX_WALLPAPER_FREQUENCY_MS, Math.max(MIN_WALLPAPER_FREQUENCY_MS, frequencyMs));
|
||||||
return 24 * 60 * 60 * 1000;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const wallpaperUrlCache = new Map<string, string | undefined>();
|
const wallpaperUrlCache = new Map<string, string | undefined>();
|
||||||
|
|||||||
@@ -21,6 +21,24 @@ const getRangeStyle = (value: number, min: number, max: number): RangeStyle => {
|
|||||||
return { '--range-progress': `${progress}%` };
|
return { '--range-progress': `${progress}%` };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MIN_WALLPAPER_FREQUENCY_HOURS = 1;
|
||||||
|
const MAX_WALLPAPER_FREQUENCY_HOURS = 48;
|
||||||
|
const DEFAULT_WALLPAPER_FREQUENCY_HOURS = 24;
|
||||||
|
|
||||||
|
const clampWallpaperFrequencyHours = (hours: number): number =>
|
||||||
|
Math.min(MAX_WALLPAPER_FREQUENCY_HOURS, Math.max(MIN_WALLPAPER_FREQUENCY_HOURS, Math.round(hours)));
|
||||||
|
|
||||||
|
const getWallpaperFrequencyHours = (frequency: string): number => {
|
||||||
|
const match = frequency.match(/^(\d+)(h|d)$/);
|
||||||
|
if (!match) return DEFAULT_WALLPAPER_FREQUENCY_HOURS;
|
||||||
|
const value = Number(match[1]);
|
||||||
|
const hours = match[2] === 'd' ? value * 24 : value;
|
||||||
|
return clampWallpaperFrequencyHours(hours);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatWallpaperFrequency = (hours: number): string =>
|
||||||
|
`${hours} ${hours === 1 ? 'hour' : 'hours'}`;
|
||||||
|
|
||||||
const ThemeTab: React.FC<ThemeTabProps> = ({
|
const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -35,6 +53,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
|||||||
const [newWallpaperName, setNewWallpaperName] = useState('');
|
const [newWallpaperName, setNewWallpaperName] = useState('');
|
||||||
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
|
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const wallpaperFrequencyHours = getWallpaperFrequencyHours(config.wallpaperFrequency);
|
||||||
|
|
||||||
const handleAddWallpaper = async () => {
|
const handleAddWallpaper = async () => {
|
||||||
if (newWallpaperUrl.trim() === '') return;
|
if (newWallpaperUrl.trim() === '') return;
|
||||||
@@ -75,19 +94,25 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
|||||||
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
|
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
|
<label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
|
||||||
<Dropdown
|
<div className="flex items-center gap-4">
|
||||||
name="wallpaperFrequency"
|
<input
|
||||||
value={config.wallpaperFrequency}
|
type="range"
|
||||||
onChange={(e) => onChange({ wallpaperFrequency: e.target.value as string })}
|
min={MIN_WALLPAPER_FREQUENCY_HOURS}
|
||||||
options={[
|
max={MAX_WALLPAPER_FREQUENCY_HOURS}
|
||||||
{ value: '1h', label: '1 hour' },
|
step="1"
|
||||||
{ value: '3h', label: '3 hours' },
|
value={wallpaperFrequencyHours}
|
||||||
{ value: '6h', label: '6 hours' },
|
onChange={(e) => onChange({ wallpaperFrequency: `${Number(e.target.value)}h` })}
|
||||||
{ value: '12h', label: '12 hours' },
|
className="liquid-range"
|
||||||
{ value: '1d', label: '1 day' },
|
style={getRangeStyle(
|
||||||
{ value: '2d', label: '2 days' },
|
wallpaperFrequencyHours,
|
||||||
]}
|
MIN_WALLPAPER_FREQUENCY_HOURS,
|
||||||
/>
|
MAX_WALLPAPER_FREQUENCY_HOURS,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="w-20 text-right text-sm text-slate-200">
|
||||||
|
{formatWallpaperFrequency(wallpaperFrequencyHours)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
|||||||
@@ -43,13 +43,37 @@ const safeParse = (value: string | null): unknown => {
|
|||||||
const toStorageString = (value: unknown): string =>
|
const toStorageString = (value: unknown): string =>
|
||||||
typeof value === 'string' ? value : JSON.stringify(value);
|
typeof value === 'string' ? value : JSON.stringify(value);
|
||||||
|
|
||||||
|
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
||||||
|
typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||||
|
|
||||||
|
const deepMerge = <T>(base: T, stored: unknown): T => {
|
||||||
|
if (isPlainObject(base)) {
|
||||||
|
const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
||||||
|
const storedObj = isPlainObject(stored) ? stored : {};
|
||||||
|
for (const key of Object.keys(result)) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(storedObj, key)) {
|
||||||
|
result[key] = deepMerge(result[key], storedObj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result as T;
|
||||||
|
}
|
||||||
|
if (Array.isArray(base)) {
|
||||||
|
return (Array.isArray(stored) ? stored : base) as T;
|
||||||
|
}
|
||||||
|
if (stored === null) return base;
|
||||||
|
return typeof stored === typeof base ? (stored as T) : base;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeConfig = (stored: unknown): Config =>
|
||||||
|
deepMerge(DEFAULT_CONFIG, stored);
|
||||||
|
|
||||||
export const ConfigurationService = {
|
export const ConfigurationService = {
|
||||||
loadConfig(): Config {
|
loadConfig(): Config {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('config');
|
const stored = localStorage.getItem('config');
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored);
|
||||||
return { ...DEFAULT_CONFIG, ...parsed };
|
return normalizeConfig(parsed);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing config from localStorage', error);
|
console.error('Error parsing config from localStorage', error);
|
||||||
@@ -159,12 +183,12 @@ export const ConfigurationService = {
|
|||||||
throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`);
|
throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const importedConfig = (localStorageData as Record<string, unknown>).config as Config;
|
const importedConfig = (localStorageData as Record<string, unknown>).config;
|
||||||
const importedUserWallpapers = (localStorageData as Record<string, unknown>)
|
const importedUserWallpapers = (localStorageData as Record<string, unknown>)
|
||||||
.userWallpapers as Wallpaper[];
|
.userWallpapers;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: importedConfig || { ...DEFAULT_CONFIG },
|
config: normalizeConfig(importedConfig),
|
||||||
userWallpapers: Array.isArray(importedUserWallpapers) ? importedUserWallpapers : [],
|
userWallpapers: Array.isArray(importedUserWallpapers) ? importedUserWallpapers : [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Vision Startpage",
|
"name": "Vision Startpage",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"description": "A beautiful and customizable startpage for your browser.",
|
"description": "A light, modern and customizable startpage.",
|
||||||
"chrome_url_overrides": {
|
"chrome_url_overrides": {
|
||||||
"newtab": "index.html"
|
"newtab": "index.html"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "assets/icons/vision-16.png",
|
||||||
|
"48": "assets/icons/vision-48.png",
|
||||||
|
"128": "assets/icons/vision-128.png"
|
||||||
|
},
|
||||||
"content_security_policy": {
|
"content_security_policy": {
|
||||||
"extension_pages": "script-src 'self'; object-src 'self';"
|
"extension_pages": "script-src 'self'; object-src 'self';"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
project_name: Vision Start
|
project_name: Vision Start
|
||||||
date: 2026-07-03
|
date: 2026-07-08
|
||||||
type: general_overview
|
type: general_overview
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ The startpage is composed of widgets and a configuration panel:
|
|||||||
- **Clock** — Optional header clock with selectable size, font, and 12h/24h format.
|
- **Clock** — Optional header clock with selectable size, font, and 12h/24h format.
|
||||||
- **Title** — Optional big header title (text + size configurable).
|
- **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.
|
- **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 a cadence (`1h`–`2d`).
|
- **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`).
|
- 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.
|
- 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.
|
- **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.
|
||||||
@@ -109,7 +109,7 @@ vision-start/
|
|||||||
│ │
|
│ │
|
||||||
│ ├── configuration/
|
│ ├── configuration/
|
||||||
│ │ ├── GeneralTab.tsx # Title, sizes, alignment, tile size
|
│ │ ├── GeneralTab.tsx # Title, sizes, alignment, tile size
|
||||||
│ │ ├── ThemeTab.tsx # Background selection, wallpaper mgmt, blur/brightness/opacity
|
│ │ ├── ThemeTab.tsx # Background selection, wallpaper cadence, wallpaper mgmt, blur/brightness/opacity
|
||||||
│ │ ├── ClockTab.tsx # Clock enable/size/font/format
|
│ │ ├── ClockTab.tsx # Clock enable/size/font/format
|
||||||
│ │ └── ServerWidgetTab.tsx # Server widget enable/ping/servers (drag-to-reorder)
|
│ │ └── ServerWidgetTab.tsx # Server widget enable/ping/servers (drag-to-reorder)
|
||||||
│ │
|
│ │
|
||||||
@@ -158,9 +158,9 @@ The shape of all persisted data lives in `types.ts`:
|
|||||||
- **`Server`** — `id`, `name`, `address`
|
- **`Server`** — `id`, `name`, `address`
|
||||||
- **`Category`** — `id`, `name`, `websites: Website[]`
|
- **`Category`** — `id`, `name`, `websites: Website[]`
|
||||||
- **`Wallpaper`** — `name`, optional `url` or `base64`
|
- **`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}`
|
- **`Config`** — 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.
|
`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):
|
Storage layout (browser-side):
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
|||||||
- **`EditModal.tsx` has been removed.** It was a legacy drag-and-drop editor that imported non-existent `lucide-react` and `./IconPicker`; it was never wired into `App.tsx`. Use `WebsiteEditModal.tsx` / `CategoryEditModal.tsx` instead.
|
- **`EditModal.tsx` has been removed.** It was a legacy drag-and-drop editor that imported non-existent `lucide-react` and `./IconPicker`; it was never wired into `App.tsx`. Use `WebsiteEditModal.tsx` / `CategoryEditModal.tsx` instead.
|
||||||
- **`@hello-pangea/dnd`** is used only in `ServerWidgetTab.tsx` (server reorder), which itself is imported by the lazy-loaded `ConfigurationModal`, so it lives in a separate chunk and is absent from the initial page load. `WebsiteTile` moves tiles within their own category via simple left/right buttons (no cross-category movement), not drag-and-drop.
|
- **`@hello-pangea/dnd`** is used only in `ServerWidgetTab.tsx` (server reorder), which itself is imported by the lazy-loaded `ConfigurationModal`, so it lives in a separate chunk and is absent from the initial page load. `WebsiteTile` moves tiles within their own category via simple left/right buttons (no cross-category movement), 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.
|
- **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.
|
- **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 frequency is clamped to 1–48 hours, while older saved values like `1d`/`2d` still resolve to their hour equivalents. 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.
|
- **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.
|
- **`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.).
|
- **`tailwind.config.js` safelists** a set of `w-[Npx]/h-[Npx]` classes because `WebsiteTile` generates tailwind classes dynamically from `tileSize` (`w-[42px]`, etc.).
|
||||||
@@ -253,4 +253,4 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_Last updated: 2026-07-03. Generated as a general project overview; not a coding-style guide._
|
_Last updated: 2026-07-08. Generated as a general project overview; not a coding-style guide._
|
||||||
|
|||||||
Reference in New Issue
Block a user