wip: visual re-work

This commit is contained in:
2026-07-03 14:56:43 -03:00
parent 1580187689
commit 1dfbe74280
32 changed files with 636 additions and 188 deletions

View File

@@ -14,28 +14,30 @@ Guidance for AI agents (and humans) working on Vision Start. Read before making
## 1. Styling guidance
The design language is **glassmorphism**: translucent black surfaces, backdrop blur, thin light borders, subtle shadows, cyan accents, and iOS-like easing.
The design language is **light liquid glass**: translucent bright surfaces, refractive edge highlights, backdrop blur, soft 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`
- **Tiles / floating controls / light surfaces:** `liquid-surface` plus `liquid-control` / `liquid-tile` / `liquid-focus` as appropriate.
- **Cards / modals / panels:** `liquid-panel liquid-modal-card rounded-3xl` for centered modals; `liquid-drawer` for the settings drawer.
- **Inputs:** `liquid-input p-3`; ranges use `liquid-range`.
- **Buttons:** `liquid-button` plus one of `liquid-button-primary`, `liquid-button-success`, `liquid-button-secondary`, or `liquid-button-danger`.
- **Add/edit surfaces:** add tiles use `liquid-ghost-tile`; tile edit controls use `liquid-edit-toolbar` and `liquid-edit-action`.
- **Full-screen modal overlay:** `liquid-modal-backdrop`.
### 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`
- Accent / focus / selection: cyan via `liquid-focus`, `cyan-400`, `cyan-200`, and `liquid-button-primary`
- Confirm/Save: `liquid-button-success`
- Cancel/secondary: `liquid-button-secondary`
- Destructive: `liquid-button-danger` or red text on `liquid-edit-action`
- Tertiary (export/import etc.): `liquid-button-secondary`
- Status: online `bg-green-400 text-green-400`, offline `bg-red-400 text-red-400`, pending `bg-slate-400 text-slate-400`
- 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`.
- Use the custom easing tokens defined in `index.css`: `ease-ios` (default), `ease-spring` (toggle knobs, drawer slides), and `ease-liquid` (tile lift and wallpaper transitions).
- Standard durations: `duration-150` (buttons), `duration-200` (tiles, toggles, icons), and `duration-300` (drawers/modals).
- Prefer the shared liquid classes for hover/press motion. Add custom transforms only when the shared classes do not cover the interaction.
- Keep `prefers-reduced-motion` behavior intact when adding animations.
### 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`.
@@ -45,7 +47,7 @@ The design language is **glassmorphism**: translucent black surfaces, backdrop b
### 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()`).
- Modal pattern: `liquid-modal-backdrop` + centered `liquid-panel liquid-modal-card rounded-3xl`, 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`).
@@ -83,7 +85,7 @@ A "feature" in this project typically means: a new widget, a new settings option
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.
13. A new widget should follow the existing widget shape: optional/gated by `config.<feature>.enabled`, positioned with a liquid 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`.
---
@@ -108,6 +110,6 @@ A "feature" in this project typically means: a new widget, a new settings option
- 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.
- Favor the established liquid utilities (`liquid-surface`, `liquid-panel`, `liquid-input`, `liquid-button`, `liquid-focus`) over ad-hoc glass class strings.
- 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.

16
App.tsx
View File

@@ -200,7 +200,7 @@ const App: React.FC = () => {
return (
<main
className={`min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} p-4`}
className={`vision-shell min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} px-4 py-8 sm:px-6 sm:py-10`}
>
<Wallpaper
wallpaperNames={config.currentWallpapers}
@@ -215,7 +215,7 @@ const App: React.FC = () => {
<Header config={config} />
<div className="flex flex-col gap-8 w-full mt-16">
<div className="relative z-10 flex flex-col gap-7 sm:gap-8 w-full mt-12 sm:mt-14">
{categories.map((category) => (
<CategoryGroup
key={category.id}
@@ -238,12 +238,14 @@ const App: React.FC = () => {
setEditingCategory(null);
setIsCategoryModalOpen(true);
}}
className="text-white/50 hover:text-white active:scale-90 transition-all duration-150 ease-ios"
className="liquid-surface liquid-control liquid-ghost-tile liquid-focus min-h-16 px-6 text-sm font-bold"
aria-label="Add category"
>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
Add category
</button>
</div>
)}
@@ -296,4 +298,4 @@ const App: React.FC = () => {
);
}
export default App;
export default App;

View File

@@ -3,16 +3,17 @@ Vision Start
</div>
<div style="display: flex; justify-content: center; font-size: 1.5rem;">
A glassmorphism-looking like, modern and customizable startpage built with React.
A light liquid-glass, modern and customizable startpage built with React.
</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>
## Screenshots
![Vision Start with the Dark background](screenshots/dark-page.png)
![Editing page with Abstract Red background](screenshots/editing-abstract-red.png)
![Configuration Tab opened](screenshots/configuration-abstract-red.png)
![Vision Start liquid glass home](screenshots/liquid-home.png)
![Vision Start liquid glass editing](screenshots/liquid-editing.png)
![Vision Start liquid glass configuration](screenshots/liquid-configuration.png)
![Vision Start liquid glass mobile](screenshots/liquid-mobile.png)
## Installing
@@ -30,7 +31,7 @@ Vision Start is not yet available on Chrome Web Store, but it can be installed m
* **Customizable Website Tiles:** Add, edit, and organize your favorite websites for quick access.
* **Elegant Clock:** A clock because all startpages have one.
* **Server Status Widgets:** Monitor the status of services directly from the startpage.
* **Glassmorphism UI:** A modern and stylish interface with a frosted glass effect.
* **Liquid Glass UI:** A modern, light interface with frosted surfaces, soft highlights, and gentle motion.
* **Icon Library:** It uses the [Dashboard Icon library](https://dashboardicons.com/) for a better look and feel. It also supports auto-fetch for some websites.
* **Settings:** A settings page to configure the startpage, with export/import functionality.

View File

@@ -19,31 +19,31 @@ const CategoryEditModal: React.FC<CategoryEditModalProps> = ({ category, edit, o
};
return (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50" onClick={handleOverlayClick}>
<div className="bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl p-8 w-full max-w-lg text-white">
<h2 className="text-3xl font-bold mb-6">{edit ? 'Edit Category' : 'Add Category'}</h2>
<div className="liquid-modal-backdrop fixed inset-0 flex items-center justify-center z-50 p-4" onClick={handleOverlayClick}>
<div className="liquid-panel liquid-modal-card rounded-3xl p-6 sm:p-8 w-full max-w-lg text-white">
<h2 className="liquid-title-text text-3xl font-extrabold mb-6">{edit ? 'Edit Category' : 'Add Category'}</h2>
<div className="flex flex-col gap-4">
<input
type="text"
placeholder="Category Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-3"
/>
</div>
<div className="flex justify-between items-center mt-8">
<div>
{edit && (
<button onClick={onDelete} className="bg-red-500 hover:bg-red-400 text-white font-bold py-2 px-6 rounded-lg">
<button onClick={onDelete} className="liquid-button liquid-button-danger liquid-focus py-2.5 px-5">
Delete
</button>
)}
</div>
<div className="flex justify-end gap-4">
<button onClick={() => onSave(name)} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
<div className="flex justify-end gap-3">
<button onClick={() => onSave(name)} className="liquid-button liquid-button-success liquid-focus py-2.5 px-5">
Save
</button>
<button onClick={onClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
<button onClick={onClose} className="liquid-button liquid-button-secondary liquid-focus py-2.5 px-5">
Close
</button>
</div>

View File

@@ -56,9 +56,8 @@ const Clock: React.FC<ClockProps> = ({ config, getClockSizeClass }) => {
return (
<div
className={`text-white font-bold ${getClockSizeClass(config.clock.size)}`}
className={`liquid-clock-text whitespace-nowrap text-white font-bold ${getClockSizeClass(config.clock.size)}`}
style={{
textShadow: '0 2px 4px rgba(0,0,0,0.5)',
fontFamily: config.clock.font,
}}
>

View File

@@ -133,7 +133,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
return (
<div className="fixed inset-0 z-50" role="dialog" aria-modal="true">
<div
className={`fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-250 ease-ios ${
className={`liquid-modal-backdrop fixed inset-0 transition-opacity duration-200 ease-ios ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
onClick={handleClose}
@@ -141,21 +141,21 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
<div
ref={menuRef}
className={`fixed top-0 right-0 h-full w-full max-w-lg bg-black/50 backdrop-blur-xl border-l border-white/10 text-white flex flex-col transition-transform duration-300 ease-spring transform ${
className={`liquid-drawer fixed top-0 right-0 h-full w-full max-w-xl text-white flex flex-col transition-transform duration-300 ease-spring transform ${
isVisible ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className="p-8 flex-grow overflow-y-auto">
<h2 className="text-3xl font-bold mb-6">Configuration</h2>
<div className="p-6 sm:p-8 flex-grow overflow-y-auto">
<h2 className="liquid-title-text text-3xl font-extrabold mb-6">Configuration</h2>
<div className="flex border-b border-white/10 mb-6">
<div className="liquid-surface grid grid-cols-2 sm:grid-cols-4 gap-1 rounded-2xl p-1 mb-7">
{tabs.map((tab) => (
<button
key={tab.id}
className={`px-4 py-2 text-lg font-semibold ${
className={`liquid-focus rounded-xl px-3 py-2 text-sm font-bold transition-all duration-200 ease-ios ${
activeTab === tab.id
? 'text-cyan-400 border-b-2 border-cyan-400'
: 'text-slate-400'
? 'bg-cyan-400/25 text-cyan-100 shadow-[inset_0_1px_0_rgba(255,255,255,0.28)]'
: 'text-slate-300 hover:bg-white/10 hover:text-white'
}`}
onClick={() => setActiveTab(tab.id)}
>
@@ -188,18 +188,18 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
)}
</div>
<div className="p-8 border-t border-white/10">
<div className="flex items-center justify-between gap-4">
<div className="p-6 sm:p-8 border-t border-white/10">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex gap-2">
<button
onClick={() => ConfigurationService.exportConfig()}
className="bg-slate-700 hover:bg-slate-600 active:scale-95 text-white text-sm font-semibold py-1.5 px-3 rounded-lg transition-all duration-150 ease-ios"
className="liquid-button liquid-button-secondary liquid-focus text-sm py-2 px-4"
>
Export
</button>
<button
onClick={() => importInputRef.current?.click()}
className="bg-slate-700 hover:bg-slate-600 active:scale-95 text-white text-sm font-semibold py-1.5 px-3 rounded-lg transition-all duration-150 ease-ios"
className="liquid-button liquid-button-secondary liquid-focus text-sm py-2 px-4"
>
Import
</button>
@@ -211,19 +211,19 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
onChange={handleImportConfig}
/>
</div>
<div className="flex justify-end gap-4">
<div className="flex justify-end gap-3">
<button
onClick={() => {
isSaving.current = true;
onSave(config);
}}
className="bg-green-500 hover:bg-green-400 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios"
className="liquid-button liquid-button-success liquid-focus py-2.5 px-5"
>
Save & Close
</button>
<button
onClick={handleClose}
className="bg-gray-600 hover:bg-gray-500 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios"
className="liquid-button liquid-button-secondary liquid-focus py-2.5 px-5"
>
Cancel
</button>

View File

@@ -71,7 +71,7 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
<div className="relative" ref={dropdownRef}>
<button
type="button"
className="bg-black/5 backdrop-blur-md border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-cyan-400 w-40 h-10 flex justify-between items-center transition-all duration-200 hover:bg-white/5"
className="liquid-surface liquid-focus rounded-xl px-3 text-white text-sm w-44 h-11 flex justify-between items-center transition-all duration-200 ease-ios hover:border-white/30"
onClick={() => setIsOpen(!isOpen)}
aria-haspopup="listbox"
aria-expanded={isOpen}
@@ -95,7 +95,7 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
{isOpen && (
<ul
className="absolute z-10 mt-1 w-full bg-black/70 backdrop-blur-xl border border-white/20 rounded-lg shadow-2xl overflow-hidden animate-in slide-in-from-top-2 fade-in duration-150"
className="liquid-panel liquid-dropdown-list absolute z-10 mt-2 w-full rounded-xl overflow-hidden"
role="listbox"
>
{options.map((option) => (
@@ -105,8 +105,8 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
className={`h-10 px-3 text-white cursor-pointer transition-all duration-150 ease-ios flex items-center
${
isSelected(option.value)
? 'bg-cyan-500/20 text-cyan-300'
: 'hover:bg-white/20 hover:text-white hover:shadow-lg'
? 'bg-cyan-400/20 text-cyan-200'
: 'hover:bg-white/20 hover:text-white'
}`}
role="option"
aria-selected={isSelected(option.value)}
@@ -117,7 +117,6 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
</ul>
)}
{/* Hidden input to mimic native select behavior for forms */}
{name && !multiple && <input type="hidden" name={name} value={value as string} />}
</div>
);

View File

@@ -15,11 +15,11 @@ interface ServerWidgetProps {
const getStatusColor = (status: string) => {
switch (status) {
case 'online':
return 'bg-green-500';
return 'bg-green-400 text-green-400';
case 'offline':
return 'bg-red-500';
return 'bg-red-400 text-red-400';
default:
return 'bg-gray-500';
return 'bg-slate-400 text-slate-400';
}
};
@@ -71,13 +71,11 @@ const ServerWidget: React.FC<ServerWidgetProps> = ({ config }) => {
}
return (
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 w-auto max-w-full">
<div className="flex items-center gap-4 bg-black/25 backdrop-blur-md border border-white/20 px-4 py-2 shadow-lg"
style={{ borderBottomLeftRadius: '0', borderBottomRightRadius: '0', borderTopLeftRadius: '16px', borderTopRightRadius: '15px', borderBottomWidth: '0' }}
>
<div className="fixed bottom-3 left-1/2 z-20 w-auto max-w-[calc(100%-1.5rem)] -translate-x-1/2">
<div className="liquid-surface flex items-center gap-4 rounded-full px-4 py-2">
{config.serverWidget.servers.map((server) => (
<div key={server.id} className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${getStatusColor(serverStatus[server.id])}`}></div>
<div className={`liquid-status-dot w-2.5 h-2.5 rounded-full ${getStatusColor(serverStatus[server.id])}`}></div>
<span className="text-slate-100 text-sm font-medium">
{server.name}
</span>
@@ -88,4 +86,4 @@ const ServerWidget: React.FC<ServerWidgetProps> = ({ config }) => {
);
};
export default ServerWidget;
export default ServerWidget;

View File

@@ -11,14 +11,20 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({ checked, onChange }) => {
};
return (
<div
className={`w-14 h-8 flex items-center rounded-full p-1 cursor-pointer transition-colors duration-200 ease-ios ${checked ? 'bg-cyan-500' : 'bg-gray-600'}`}
<button
type="button"
className={`liquid-focus relative w-14 h-8 flex items-center rounded-full p-1 cursor-pointer border transition-all duration-200 ease-ios ${
checked
? 'bg-cyan-400/70 border-cyan-200/60 shadow-[0_0_24px_rgba(34,211,238,0.22)]'
: 'bg-white/10 border-white/20'
}`}
onClick={handleToggle}
aria-pressed={checked}
>
<div
className={`bg-white w-6 h-6 rounded-full shadow-md transform transition-transform duration-200 ease-spring ${checked ? 'translate-x-6' : 'translate-x-0'}`}
className={`bg-white w-6 h-6 rounded-full shadow-lg transform transition-transform duration-200 ease-spring ${checked ? 'translate-x-6' : 'translate-x-0'}`}
/>
</div>
</button>
);
};

View File

@@ -128,17 +128,18 @@ const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness,
if (!imageUrl) return null;
return (
<div
className="fixed inset-0 -z-10 w-full h-full wallpaper-transition"
style={{
backgroundImage: `url(${imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: `blur(${blur}px) brightness(${brightness / 100})`,
opacity: opacity / 100,
}}
aria-label="Wallpaper background"
/>
<>
<div
className="wallpaper-layer wallpaper-transition"
style={{
backgroundImage: `url(${imageUrl})`,
filter: `blur(${blur}px) brightness(${brightness / 100})`,
opacity: opacity / 100,
}}
aria-label="Wallpaper background"
/>
<div className="wallpaper-luminance" aria-hidden="true" />
</>
);
};

View File

@@ -103,15 +103,15 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
};
return (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50" onClick={handleOverlayClick}>
<div className="bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl p-8 w-full max-w-lg text-white">
<h2 className="text-3xl font-bold mb-6">{edit ? 'Edit Website' : 'Add Website'}</h2>
<div className="liquid-modal-backdrop fixed inset-0 flex items-center justify-center z-50 p-4" onClick={handleOverlayClick}>
<div className="liquid-panel liquid-modal-card rounded-3xl p-6 sm:p-8 w-full max-w-lg text-white">
<h2 className="liquid-title-text text-3xl font-extrabold mb-6">{edit ? 'Edit Website' : 'Add Website'}</h2>
<div className="flex flex-col gap-4">
<div className="flex justify-center mb-4">
{icon ? (
<img src={icon} alt="Website Icon" className="h-24 w-24 object-contain" />
) : (
<div className="h-24 w-24 bg-white/10 rounded-lg flex items-center justify-center">
<div className="liquid-surface h-24 w-24 rounded-2xl flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="text-white/50">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
@@ -125,16 +125,16 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-3"
/>
<input
type="text"
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-3"
/>
<div className="flex items-center gap-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="relative w-full">
<input
type="text"
@@ -145,10 +145,10 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
setIconQuery(e.target.value);
}}
onFocus={ensureIconMetadata}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 w-full"
className="liquid-input p-3"
/>
{filteredIcons.length > 0 && (
<div className="absolute z-10 w-full bg-gray-800 rounded-lg mt-1 max-h-60 overflow-y-auto">
<div className="liquid-panel liquid-dropdown-list absolute z-20 w-full rounded-xl mt-2 max-h-60 overflow-y-auto">
{filteredIcons.map(iconData => (
<div
key={iconData.name}
@@ -157,7 +157,7 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
setIcon(iconUrl);
setFilteredIcons([]);
}}
className="cursor-pointer flex items-center p-2 hover:bg-gray-700"
className="cursor-pointer flex items-center p-2 transition-colors duration-150 ease-ios hover:bg-white/20"
>
<img
src={`https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/${iconData.base}/${iconData.name}.${iconData.base}`}
@@ -170,7 +170,7 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
</div>
)}
</div>
<button onClick={fetchIcon} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-3 px-4 rounded-lg">
<button onClick={fetchIcon} className="liquid-button liquid-button-secondary liquid-focus py-3 px-4">
Fetch
</button>
</div>
@@ -178,16 +178,16 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
<div className="flex justify-between items-center mt-8">
<div>
{edit && (
<button onClick={onDelete} className="bg-red-500 hover:bg-red-400 text-white font-bold py-2 px-6 rounded-lg">
<button onClick={onDelete} className="liquid-button liquid-button-danger liquid-focus py-2.5 px-5">
Delete
</button>
)}
</div>
<div className="flex justify-end gap-4">
<button onClick={handleSave} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
<div className="flex justify-end gap-3">
<button onClick={handleSave} className="liquid-button liquid-button-success liquid-focus py-2.5 px-5">
Save
</button>
<button onClick={onClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
<button onClick={onClose} className="liquid-button liquid-button-secondary liquid-focus py-2.5 px-5">
Close
</button>
</div>
@@ -197,4 +197,4 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
);
};
export default WebsiteEditModal;
export default WebsiteEditModal;

View File

@@ -59,53 +59,48 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
return;
}
setIsLoading(true);
// Simulate loading time (dev purpose)
// e.preventDefault();
// setTimeout(() => {
// setIsLoading(false);
// }, 3500); // Small delay to show spinner before navigation
};
const iconSizeClass = `w-[${getIconPixelSize(tileSize)}px] h-[${getIconPixelSize(tileSize)}px]`;
const iconSizeLoadingClass = `w-[${getIconLoadingPixelSize(tileSize)}px] h-[${getIconLoadingPixelSize(tileSize)}px]`;
return (
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-200 ease-ios`}>
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-200 ease-ios ${isEditing ? 'mb-4' : ''}`}>
<a
href={isEditing ? undefined : website.url}
target="_self"
rel="noopener noreferrer"
onClick={handleClick}
className="group flex flex-col items-center justify-center p-4 bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl w-full h-full transform transition-all duration-200 ease-ios hover:scale-[1.04] active:scale-[0.96] hover:bg-white/25 shadow-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 focus:ring-opacity-75"
className={`liquid-surface liquid-tile liquid-focus group flex flex-col items-center justify-center w-full h-full p-4 ${isEditing ? 'pb-6' : ''}`}
aria-label={isEditing ? `${website.name} edit controls` : `Open ${website.name}`}
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center mb-6">
<div className="absolute inset-0 z-10 flex items-center justify-center mb-6">
<svg className="animate-spin h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
)}
<div className={`flex items-center transition-all duration-200 ease-ios ${isLoading ? 'mt-18' : 'flex-col'} ${isLoading ? 'gap-2' : ''}`}>
<div className={`transition-all duration-200 ease-ios ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
<img src={website.icon} alt={`${website.name} icon`} className={`object-contain w-full h-full`} />
<div className={`relative z-10 flex items-center transition-all duration-200 ease-ios ${isLoading ? 'translate-y-5 gap-2' : 'flex-col gap-3'}`}>
<div className={`transition-all duration-200 ease-ios drop-shadow-[0_10px_20px_rgba(0,0,0,0.28)] ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
<img src={website.icon} alt={`${website.name} icon`} className="object-contain w-full h-full" />
</div>
<span className={`text-slate-100 font-medium text-base tracking-wide text-center transition-all duration-200 ease-ios ${isLoading ? 'text-sm' : ''}`}>
<span className={`max-w-full px-1 text-slate-50 font-semibold text-base text-center leading-tight transition-all duration-200 ease-ios [text-shadow:0_2px_12px_rgba(2,6,23,0.44)] ${isLoading ? 'text-sm' : ''}`}>
{website.name}
</span>
</div>
</a>
{isEditing && (
<div className="absolute bottom-2 left-0 right-0 flex justify-center gap-2">
<button onClick={() => onMove(website, 'left')} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z" />
<div className="liquid-surface liquid-edit-toolbar">
<button onClick={() => onMove(website, 'left')} className="liquid-edit-action liquid-focus" aria-label={`Move ${website.name} left`}><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path fillRule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z" />
</svg></button>
<button onClick={() => onEdit(website)} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
<button onClick={() => onEdit(website)} className="liquid-edit-action liquid-focus" aria-label={`Edit ${website.name}`}><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg></button>
<button onClick={() => onMove(website, 'right')} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z" />
<button onClick={() => onMove(website, 'right')} className="liquid-edit-action liquid-focus" aria-label={`Move ${website.name} right`}><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path fillRule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z" />
</svg></button>
</div>
)}

View File

@@ -15,14 +15,14 @@ const ClockTab: React.FC<ClockTabProps> = ({ config, onChange }) => {
return (
<div className="flex flex-col gap-6">
<div className="flex items-center 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">Enable Clock</label>
<ToggleSwitch
checked={config.clock.enabled}
onChange={(checked) => updateClock({ enabled: checked })}
/>
</div>
<div className="flex items-center 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">Clock Size</label>
<Dropdown
name="clock.size"
@@ -36,7 +36,7 @@ const ClockTab: React.FC<ClockTabProps> = ({ config, onChange }) => {
]}
/>
</div>
<div className="flex items-center 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">Clock Font</label>
<Dropdown
name="clock.font"
@@ -50,7 +50,7 @@ const ClockTab: React.FC<ClockTabProps> = ({ config, onChange }) => {
]}
/>
</div>
<div className="flex items-center 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">Time Format</label>
<Dropdown
name="clock.format"

View File

@@ -16,10 +16,10 @@ const GeneralTab: React.FC<GeneralTabProps> = ({ config, onChange }) => {
type="text"
value={config.title}
onChange={(e) => onChange({ title: e.target.value })}
className="bg-white/10 p-3 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-3"
/>
</div>
<div className="flex items-center 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">Title Size</label>
<Dropdown
name="titleSize"
@@ -33,7 +33,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({ config, onChange }) => {
]}
/>
</div>
<div className="flex items-center 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">Vertical Alignment</label>
<Dropdown
name="alignment"
@@ -46,7 +46,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({ config, onChange }) => {
]}
/>
</div>
<div className="flex items-center 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">Tile Size</label>
<Dropdown
name="tileSize"
@@ -59,7 +59,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({ config, onChange }) => {
]}
/>
</div>
<div className="flex items-center 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">Horizontal Alignment</label>
<Dropdown
name="horizontalAlignment"

View File

@@ -44,7 +44,7 @@ const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) =
return (
<div className="flex flex-col gap-6">
<div className="flex items-center 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">Enable Server Widget</label>
<ToggleSwitch
checked={config.serverWidget.enabled}
@@ -53,7 +53,7 @@ const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) =
</div>
{config.serverWidget.enabled && (
<>
<div className="flex items-center 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">Ping Frequency</label>
<div className="flex items-center gap-4">
<input
@@ -62,9 +62,9 @@ const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) =
max="60"
value={config.serverWidget.pingFrequency}
onChange={(e) => updateServerWidget({ pingFrequency: Number(e.target.value) })}
className="w-48"
className="liquid-range"
/>
<span>{config.serverWidget.pingFrequency}s</span>
<span className="w-12 text-right text-sm text-slate-200">{config.serverWidget.pingFrequency}s</span>
</div>
</div>
<div>
@@ -84,7 +84,7 @@ const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) =
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
className="liquid-surface flex items-center justify-between rounded-xl p-3"
>
<div>
<p className="font-semibold">{server.name}</p>
@@ -92,7 +92,8 @@ const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) =
</div>
<button
onClick={() => handleRemoveServer(server.id)}
className="text-red-500 hover:text-red-400"
className="liquid-edit-action liquid-focus text-red-300 hover:text-red-100"
aria-label={`Remove ${server.name}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -118,24 +119,24 @@ const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) =
)}
</Droppable>
</DragDropContext>
<div className="flex gap-2 mt-2">
<div className="flex flex-col gap-2 mt-3 sm:flex-row">
<input
type="text"
placeholder="Server Name"
value={newServerName}
onChange={(e) => setNewServerName(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-2.5"
/>
<input
type="text"
placeholder="HTTP Address"
value={newServerAddress}
onChange={(e) => setNewServerAddress(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-2.5"
/>
<button
onClick={handleAddServer}
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
className="liquid-button liquid-button-primary liquid-focus py-2.5 px-4"
>
Add
</button>

View File

@@ -55,7 +55,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
return (
<div className="flex flex-col gap-6">
<div className="flex items-center 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">Background</label>
<Dropdown
name="currentWallpapers"
@@ -66,7 +66,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
/>
</div>
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
<div className="flex items-center 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>
<Dropdown
name="wallpaperFrequency"
@@ -83,7 +83,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
/>
</div>
)}
<div className="flex items-center 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">Wallpaper Blur</label>
<div className="flex items-center gap-4">
<input
@@ -92,12 +92,12 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
max="50"
value={config.wallpaperBlur}
onChange={(e) => onChange({ wallpaperBlur: Number(e.target.value) })}
className="w-48"
className="liquid-range"
/>
<span>{config.wallpaperBlur}px</span>
<span className="w-12 text-right text-sm text-slate-200">{config.wallpaperBlur}px</span>
</div>
</div>
<div className="flex items-center 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">Wallpaper Brightness</label>
<div className="flex items-center gap-4">
<input
@@ -106,12 +106,12 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
max="200"
value={config.wallpaperBrightness}
onChange={(e) => onChange({ wallpaperBrightness: Number(e.target.value) })}
className="w-48"
className="liquid-range"
/>
<span>{config.wallpaperBrightness}%</span>
<span className="w-12 text-right text-sm text-slate-200">{config.wallpaperBrightness}%</span>
</div>
</div>
<div className="flex items-center 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">Wallpaper Opacity</label>
<div className="flex items-center gap-4">
<input
@@ -120,9 +120,9 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
max="100"
value={config.wallpaperOpacity}
onChange={(e) => onChange({ wallpaperOpacity: Number(e.target.value) })}
className="w-48"
className="liquid-range"
/>
<span>{config.wallpaperOpacity}%</span>
<span className="w-12 text-right text-sm text-slate-200">{config.wallpaperOpacity}%</span>
</div>
</div>
{chromeStorageAvailable && (
@@ -133,12 +133,13 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
{userWallpapers.map((wallpaper) => (
<div
key={wallpaper.name}
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
className="liquid-surface flex items-center justify-between rounded-xl p-2.5"
>
<span className="truncate">{wallpaper.name}</span>
<button
onClick={() => onDeleteWallpaper(wallpaper)}
className="text-red-500 hover:text-red-400"
className="liquid-edit-action liquid-focus text-red-300 hover:text-red-100"
aria-label={`Delete ${wallpaper.name}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -167,19 +168,19 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
placeholder="Wallpaper Name (optional for URLs)"
value={newWallpaperName}
onChange={(e) => setNewWallpaperName(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-2.5"
/>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<input
type="text"
placeholder="Image URL"
value={newWallpaperUrl}
onChange={(e) => setNewWallpaperUrl(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
className="liquid-input p-2.5"
/>
<button
onClick={handleAddWallpaper}
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"
className="liquid-button liquid-button-primary liquid-focus py-2.5 px-4"
>
Add
</button>
@@ -187,7 +188,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
<div className="flex items-center justify-center w-full">
<label
htmlFor="file-upload"
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-white/20 hover:bg-white/10"
className="liquid-surface liquid-ghost-tile flex flex-col items-center justify-center w-full h-32 cursor-pointer transition-all duration-200 ease-ios"
>
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<svg
@@ -227,7 +228,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
<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"
className="liquid-surface liquid-control liquid-focus disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-semibold py-2 px-4 rounded-2xl"
>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -15,6 +15,19 @@ interface CategoryGroupProps {
tileSize?: string;
}
const getAddTileSizeClass = (size: string | undefined) => {
switch (size) {
case 'small':
return 'w-28 h-28';
case 'medium':
return 'w-32 h-32';
case 'large':
return 'w-36 h-36';
default:
return 'w-32 h-32';
}
};
const CategoryGroup: React.FC<CategoryGroupProps> = ({
category,
isEditing,
@@ -29,23 +42,24 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
}) => {
return (
<div key={category.id} className="w-full">
<div className={`flex ${getHorizontalAlignmentClass(horizontalAlignment)} items-center mb-4 w-full ${horizontalAlignment !== 'middle' ? 'px-8' : ''}`}>
<h2 className={`text-2xl font-bold text-white ${horizontalAlignment === 'left' ? 'text-left' : horizontalAlignment === 'right' ? 'text-right' : 'text-center'} ${horizontalAlignment !== 'middle' ? 'w-full' : ''}`}>{category.name}</h2>
<div className={`flex ${getHorizontalAlignmentClass(horizontalAlignment)} items-center mb-3 w-full ${horizontalAlignment !== 'middle' ? 'px-3 sm:px-8' : ''}`}>
<h2 className={`liquid-category-title text-2xl font-extrabold text-white ${horizontalAlignment === 'left' ? 'text-left' : horizontalAlignment === 'right' ? 'text-right' : 'text-center'} ${horizontalAlignment !== 'middle' ? 'w-full' : ''}`}>{category.name}</h2>
{isEditing && (
<button
onClick={() => {
setEditingCategory(category);
setIsCategoryModalOpen(true);
}}
className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
className={`liquid-surface liquid-edit-action liquid-focus ml-2 shrink-0 transition-all duration-300 ease-spring transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
aria-label={`Edit ${category.name} category`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
</button>
)}
</div>
<div className={`flex flex-wrap ${getHorizontalAlignmentClass(horizontalAlignment)} gap-6`}>
<div className={`flex flex-wrap ${getHorizontalAlignmentClass(horizontalAlignment)} gap-5 sm:gap-6 px-1 sm:px-0`}>
{category.websites.map((website) => (
<WebsiteTile
key={website.id}
@@ -59,12 +73,14 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
{isEditing && (
<button
onClick={() => setAddingWebsite(category)}
className={`text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
className={`liquid-surface liquid-control liquid-ghost-tile liquid-focus flex-col ${getAddTileSizeClass(tileSize)} transition-all duration-300 ease-spring transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
aria-label={`Add website to ${category.name}`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
<span className="text-sm font-bold">Add</span>
</button>
)}
</div>

View File

@@ -6,12 +6,13 @@ interface ConfigurationButtonProps {
const ConfigurationButton: React.FC<ConfigurationButtonProps> = ({ onClick }) => {
return (
<div className="absolute top-4 right-4">
<div className="absolute top-4 right-4 z-20">
<button
onClick={onClick}
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 active:scale-90 transition-all duration-200 ease-ios"
className="liquid-surface liquid-control liquid-focus rounded-2xl p-3"
aria-label="Open configuration"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>
<path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09c.7 0 1.31-.4 1.51-1a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06c.51.51 1.31.61 1.82.33.51-.28 1-.81 1-1.51V3a2 2 0 1 1 4 0v.09c0 .7.49 1.23 1 1.51.51.28 1.31.18 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82c.2.6.81 1 1.51 1H21a2 2 0 1 1 0 4h-.09c-.7 0-1.31.4-1.51 1z"/>
</svg>

View File

@@ -7,13 +7,13 @@ interface EditButtonProps {
const EditButton: React.FC<EditButtonProps> = ({ isEditing, onClick }) => {
return (
<div className="absolute top-4 left-4">
<div className="absolute top-4 left-4 z-20">
<button
onClick={onClick}
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 active:scale-90 transition-all duration-200 ease-ios"
style={{ fontSize: '12px' }}
className={`liquid-surface liquid-control liquid-focus rounded-2xl px-3.5 py-3 text-xs font-bold ${isEditing ? 'pr-4' : ''}`}
aria-label={isEditing ? 'Finish editing' : 'Edit page'}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
{isEditing ? 'Done' : ''}

View File

@@ -56,16 +56,15 @@ const Header: React.FC<HeaderProps> = ({ config }) => {
return (
<>
{config.clock.enabled && (
<div className="absolute top-5 left-1/2 -translate-x-1/2 z-10 flex justify-center w-auto p-2">
<div className="absolute top-5 left-1/2 -translate-x-1/2 z-10 flex justify-center w-auto px-3 py-2">
<Clock config={config} getClockSizeClass={getClockSizeClass} />
</div>
)}
<div className={`flex flex-col ${config.alignment === 'bottom' ? 'mt-auto' : ''} items-center`}>
<div className={`relative z-10 flex flex-col ${config.alignment === 'bottom' ? 'mt-auto' : ''} items-center`}>
{config.title && (
<div className="text-center">
<h1
className={`${getTitleSizeClass(config.titleSize)} font-extrabold text-white tracking-tighter mb-3 mt-4`}
style={{ textShadow: '0 2px 4px rgba(0,0,0,0.5)' }}
className={`liquid-title-text ${getTitleSizeClass(config.titleSize)} font-extrabold text-white mb-2 mt-3`}
>
{config.title}
</h1>

View File

@@ -9,11 +9,11 @@ type RequiredLocalStorageKey = typeof REQUIRED_LOCAL_STORAGE_KEYS[number];
export const DEFAULT_CONFIG: Config = {
title: 'Vision Start',
currentWallpapers: ['Abstract'],
currentWallpapers: ['Beach'],
wallpaperFrequency: '1d',
wallpaperBlur: 0,
wallpaperBrightness: 100,
wallpaperOpacity: 100,
wallpaperBrightness: 108,
wallpaperOpacity: 96,
titleSize: 'medium',
alignment: 'middle',
horizontalAlignment: 'middle',

427
index.css
View File

@@ -3,8 +3,433 @@
@theme {
--ease-ios: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-liquid: cubic-bezier(0.2, 0.8, 0.2, 1);
}
html {
background: #0f1720;
}
body {
min-width: 320px;
margin: 0;
background: #0f1720;
color: white;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#root {
min-height: 100vh;
}
.vision-shell {
position: relative;
isolation: isolate;
overflow-x: hidden;
background:
radial-gradient(circle at 18% 12%, rgba(255, 255, 255, 0.16), transparent 28rem),
radial-gradient(circle at 82% 20%, rgba(34, 211, 238, 0.13), transparent 26rem),
linear-gradient(135deg, rgba(241, 245, 249, 0.1), rgba(15, 23, 42, 0.18) 52%, rgba(8, 13, 20, 0.34));
}
.vision-shell::before {
content: "";
position: fixed;
inset: 0;
z-index: -15;
pointer-events: none;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.04) 38%, rgba(2, 6, 23, 0.34) 100%),
radial-gradient(ellipse at center, transparent 0%, rgba(2, 6, 23, 0.2) 78%, rgba(2, 6, 23, 0.42) 100%);
}
.wallpaper-transition {
transition: filter 0.3s ease, opacity 0.3s ease;
transition: filter 0.7s var(--ease-liquid), opacity 0.7s var(--ease-liquid), transform 0.7s var(--ease-liquid);
}
.wallpaper-layer {
position: fixed;
inset: 0;
z-index: -30;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
transform: scale(1.015);
will-change: filter, opacity, transform;
}
.wallpaper-luminance {
position: fixed;
inset: 0;
z-index: -25;
pointer-events: none;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.04) 34%, rgba(5, 12, 20, 0.38)),
radial-gradient(circle at 50% 38%, rgba(255, 255, 255, 0.16), transparent 28rem),
radial-gradient(circle at 50% 100%, rgba(8, 13, 24, 0.42), transparent 36rem);
}
.liquid-surface {
position: relative;
overflow: hidden;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.07) 46%, rgba(15, 23, 42, 0.2)),
rgba(8, 13, 23, 0.22);
border: 1px solid rgba(255, 255, 255, 0.24);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.34),
inset 0 -18px 30px rgba(2, 6, 23, 0.18),
0 18px 42px rgba(2, 6, 23, 0.24);
backdrop-filter: blur(22px) saturate(150%);
-webkit-backdrop-filter: blur(22px) saturate(150%);
}
.liquid-surface::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(120deg, rgba(255, 255, 255, 0.36), transparent 34%),
radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.22), transparent 42%);
opacity: 0.7;
}
.liquid-panel {
position: relative;
overflow: hidden;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.08) 48%, rgba(5, 12, 22, 0.36)),
rgba(8, 13, 23, 0.38);
border: 1px solid rgba(255, 255, 255, 0.22);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.28),
0 28px 80px rgba(2, 6, 23, 0.34);
backdrop-filter: blur(30px) saturate(155%);
-webkit-backdrop-filter: blur(30px) saturate(155%);
}
.liquid-control {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-width: 2.75rem;
min-height: 2.75rem;
color: white;
transition:
transform 180ms var(--ease-ios),
background 180ms var(--ease-ios),
border-color 180ms var(--ease-ios),
box-shadow 180ms var(--ease-ios),
color 180ms var(--ease-ios);
}
.liquid-control:hover {
transform: translateY(-1px) scale(1.025);
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)),
rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.36);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.44),
0 18px 40px rgba(2, 6, 23, 0.28);
}
.liquid-control:active {
transform: scale(0.94);
}
.liquid-focus {
outline: none;
}
.liquid-focus:focus-visible {
outline: 2px solid rgba(34, 211, 238, 0.9);
outline-offset: 3px;
}
.liquid-input {
width: 100%;
color: white;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 0.875rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition:
background 180ms var(--ease-ios),
border-color 180ms var(--ease-ios),
box-shadow 180ms var(--ease-ios);
}
.liquid-input::placeholder {
color: rgba(226, 232, 240, 0.58);
}
.liquid-input:hover {
background: rgba(255, 255, 255, 0.14);
border-color: rgba(255, 255, 255, 0.24);
}
.liquid-input:focus {
outline: none;
border-color: rgba(34, 211, 238, 0.76);
box-shadow:
0 0 0 3px rgba(34, 211, 238, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
.liquid-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 0.875rem;
font-weight: 700;
color: white;
transition:
transform 150ms var(--ease-ios),
filter 150ms var(--ease-ios),
background 150ms var(--ease-ios),
box-shadow 150ms var(--ease-ios);
}
.liquid-button:hover {
transform: translateY(-1px);
filter: brightness(1.08);
}
.liquid-button:active {
transform: scale(0.96);
}
.liquid-button-primary {
background: linear-gradient(135deg, #06b6d4, #22d3ee);
box-shadow: 0 14px 28px rgba(8, 145, 178, 0.22);
}
.liquid-button-success {
background: linear-gradient(135deg, #10b981, #22c55e);
box-shadow: 0 14px 28px rgba(34, 197, 94, 0.2);
}
.liquid-button-secondary {
background: rgba(100, 116, 139, 0.74);
}
.liquid-button-danger {
background: linear-gradient(135deg, #ef4444, #fb7185);
}
.liquid-range {
width: 12rem;
height: 1.25rem;
appearance: none;
background: transparent;
accent-color: #22d3ee;
}
.liquid-range:focus {
outline: none;
}
.liquid-range::-webkit-slider-runnable-track {
height: 0.38rem;
border-radius: 999px;
background: linear-gradient(90deg, rgba(34, 211, 238, 0.9), rgba(255, 255, 255, 0.7));
box-shadow: inset 0 1px 2px rgba(2, 6, 23, 0.24);
}
.liquid-range::-webkit-slider-thumb {
width: 1rem;
height: 1rem;
margin-top: -0.31rem;
appearance: none;
border-radius: 999px;
background: white;
border: 2px solid rgba(34, 211, 238, 0.9);
box-shadow: 0 6px 16px rgba(2, 6, 23, 0.3);
}
.liquid-range::-moz-range-track {
height: 0.38rem;
border-radius: 999px;
background: linear-gradient(90deg, rgba(34, 211, 238, 0.9), rgba(255, 255, 255, 0.7));
}
.liquid-range::-moz-range-thumb {
width: 1rem;
height: 1rem;
border-radius: 999px;
background: white;
border: 2px solid rgba(34, 211, 238, 0.9);
}
.liquid-tile {
border-radius: 1.35rem;
transition:
transform 220ms var(--ease-liquid),
background 220ms var(--ease-ios),
border-color 220ms var(--ease-ios),
box-shadow 220ms var(--ease-liquid);
}
.liquid-tile:hover {
transform: translateY(-4px) scale(1.035);
border-color: rgba(255, 255, 255, 0.42);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.42),
0 24px 46px rgba(2, 6, 23, 0.28),
0 0 34px rgba(34, 211, 238, 0.12);
}
.liquid-tile:active {
transform: scale(0.965);
}
.liquid-ghost-tile {
border-radius: 1.35rem;
color: rgba(255, 255, 255, 0.72);
border-style: dashed;
border-color: rgba(255, 255, 255, 0.32);
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04)),
rgba(255, 255, 255, 0.04);
}
.liquid-ghost-tile:hover {
color: white;
border-color: rgba(34, 211, 238, 0.62);
background:
linear-gradient(145deg, rgba(34, 211, 238, 0.18), rgba(255, 255, 255, 0.08)),
rgba(255, 255, 255, 0.08);
}
.liquid-edit-toolbar {
position: absolute;
left: 50%;
bottom: -0.9rem;
z-index: 5;
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.25rem;
border-radius: 999px;
transform: translateX(-50%);
}
.liquid-edit-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
border-radius: 999px;
color: rgba(255, 255, 255, 0.72);
transition:
color 150ms var(--ease-ios),
background 150ms var(--ease-ios),
transform 150ms var(--ease-ios);
}
.liquid-edit-action:hover {
color: white;
background: rgba(255, 255, 255, 0.16);
transform: scale(1.08);
}
.liquid-edit-action:active {
transform: scale(0.92);
}
.liquid-category-title {
text-shadow: 0 2px 16px rgba(2, 6, 23, 0.45);
letter-spacing: 0;
}
.liquid-clock-text,
.liquid-title-text {
text-shadow:
0 2px 18px rgba(2, 6, 23, 0.45),
0 0 28px rgba(255, 255, 255, 0.12);
letter-spacing: 0;
}
.liquid-drawer {
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05) 38%, rgba(7, 12, 22, 0.48)),
rgba(7, 12, 22, 0.54);
border-left: 1px solid rgba(255, 255, 255, 0.18);
box-shadow:
inset 1px 0 0 rgba(255, 255, 255, 0.14),
-32px 0 80px rgba(2, 6, 23, 0.32);
backdrop-filter: blur(34px) saturate(160%);
-webkit-backdrop-filter: blur(34px) saturate(160%);
}
.liquid-modal-backdrop {
background:
radial-gradient(circle at 50% 30%, rgba(255, 255, 255, 0.12), transparent 32rem),
rgba(3, 7, 18, 0.72);
backdrop-filter: blur(10px) saturate(130%);
-webkit-backdrop-filter: blur(10px) saturate(130%);
}
.liquid-modal-card {
overflow: visible;
animation: liquid-pop 240ms var(--ease-spring);
}
.liquid-dropdown-list {
overflow-x: hidden;
overflow-y: auto;
animation: liquid-drop 150ms var(--ease-ios);
transform-origin: top;
}
.liquid-status-dot {
box-shadow:
0 0 0 3px rgba(255, 255, 255, 0.1),
0 0 18px currentColor;
}
@keyframes liquid-pop {
from {
opacity: 0;
transform: translateY(0.75rem) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes liquid-drop {
from {
opacity: 0;
transform: translateY(-0.35rem) scaleY(0.97);
}
to {
opacity: 1;
transform: translateY(0) scaleY(1);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
.liquid-control:hover,
.liquid-tile:hover,
.liquid-button:hover,
.liquid-edit-action:hover {
transform: none;
}
}

View File

@@ -8,8 +8,8 @@
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-black">
<body class="bg-slate-950">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
---
project_name: Vision Start
date: 2026-07-02
date: 2026-07-03
type: general_overview
---
@@ -54,13 +54,13 @@ The startpage is composed of widgets and a configuration panel:
- **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`).
- **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`).
- 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`.
- **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** — Light translucent surfaces, refractive edge highlights, backdrop blur, soft shadows, cyan focus states, and iOS-like easing tokens (`ease-ios`, `ease-spring`, `ease-liquid`) defined in `index.css`.
Performance notes:
- Modals (`ConfigurationModal`, `WebsiteEditModal`, `CategoryEditModal`) are code-split via `React.lazy` + `Suspense` and only loaded when opened. `ConfigurationModal` is the heaviest chunk (it pulls in `@hello-pangea/dnd` via `ServerWidgetTab`); the rest of `@hello-pangea/dnd` is isolated from the initial load.
@@ -69,7 +69,7 @@ Performance notes:
- `jsping` cancels its 5s timeout on image resolve/error and nulls the `Image` handlers, preventing leaks across ping cycles.
- `ServerWidget` batches pending-status updates into one `setState` and 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, fetched lazily on first focus of the icon field with `cache: 'force-cache'`, filter debounced ~150ms, and color variants are expanded lazily during filtering rather than upfront.
- `Wallpaper` caches resolved wallpaper URLs in a module-level `Map` and its CSS transition lives in the static `.wallpaper-transition` class in `index.css`.
- `Wallpaper` caches resolved wallpaper URLs in a module-level `Map`; its image transition and readability overlay classes live 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.
@@ -83,7 +83,8 @@ 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
├── 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)
@@ -252,4 +253,4 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
---
_Last updated: 2026-07-02. Generated as a general project overview; not a coding-style guide._
_Last updated: 2026-07-03. Generated as a general project overview; not a coding-style guide._

Binary file not shown.

Before

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
screenshots/liquid-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

1
vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '*.css';