migrate to Preact and add animations (#1)
All checks were successful
Build and Release to Staging / Build Vision Start (push) Successful in 8s
Build and Release to Staging / Build Vision Start Image (push) Successful in 1m1s
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 3s

- Replace React 19 with Preact via @preact/preset-vite (zero component changes needed — Vite aliases react → preact/compat at build time)
- Add custom iOS easing curves (ease-ios, ease-spring) via Tailwind @theme
- Update all transitions to use iOS-standard 200ms durations and spring/decel easing
- Add active:scale press feedback on tiles, buttons, and toggles
- Toggle knob now uses spring easing for a satisfying snap

Reviewed-on: #1
Co-authored-by: Jose Henrique <jose.henrique.ivan@gmail.com>
Co-committed-by: Jose Henrique <jose.henrique.ivan@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-03-21 03:32:01 +00:00
committed by ivanch
parent c2b3356022
commit c4dce04d42
11 changed files with 495 additions and 180 deletions

View File

@@ -81,7 +81,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
setIsVisible(false);
setTimeout(() => {
onClose();
}, 300);
}, 250);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | { target: { name: string; value: string | string[] } }) => {
@@ -241,7 +241,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
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-300 ease-in-out ${
className={`fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-250 ease-ios ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
onClick={handleClose}
@@ -249,7 +249,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
<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-in-out transform ${
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 ${
isVisible ? 'translate-x-0' : 'translate-x-full'
}`}
>
@@ -642,10 +642,10 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
</div>
<div className="p-8 border-t border-white/10">
<div className="flex justify-end gap-4">
<button onClick={() => { isSaving.current = true; onSave(config); }} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
<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">
Save & Close
</button>
<button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
<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">
Cancel
</button>
</div>

View File

@@ -79,7 +79,7 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
>
<span className="truncate">{selectedOptionLabel}</span>
<svg
className={`w-5 h-5 transition-transform duration-300 ease-in-out ${isOpen ? 'rotate-180' : 'rotate-0'}`}
className={`w-5 h-5 transition-transform duration-200 ease-ios ${isOpen ? 'rotate-180' : 'rotate-0'}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
@@ -95,14 +95,14 @@ 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-200"
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"
role="listbox"
>
{options.map((option) => (
<li
key={option.value}
onClick={() => handleOptionClick(option.value)}
className={`h-10 px-3 text-white cursor-pointer transition-all duration-150 ease-in-out flex items-center
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'

View File

@@ -11,12 +11,12 @@ 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-300 ${checked ? 'bg-cyan-500' : 'bg-gray-600'}`}
<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'}`}
onClick={handleToggle}
>
<div
className={`bg-white w-6 h-6 rounded-full shadow-md transform transition-transform duration-300 ${checked ? 'translate-x-6' : 'translate-x-0'}`}
<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'}`}
/>
</div>
);

View File

@@ -73,13 +73,13 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
const iconSizeLoadingClass = `w-[${getIconLoadingPixelSize(tileSize)}px] h-[${getIconLoadingPixelSize(tileSize)}px]`;
return (
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-300 ease-in-out`}>
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-200 ease-ios`}>
<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-300 ease-in-out hover:scale-105 hover:bg-white/25 shadow-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 focus:ring-opacity-75"
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"
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center mb-6">
@@ -89,11 +89,11 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
</svg>
</div>
)}
<div className={`flex items-center transition-all duration-300 ease-in ${isLoading ? 'mt-18' : 'flex-col'} ${isLoading ? 'gap-2' : ''}`}>
<div className={`transition-all duration-300 ease-in ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
<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>
<span className={`text-slate-100 font-medium text-base tracking-wide text-center transition-all duration-300 ease-in ${isLoading ? 'text-sm' : ''}`}>
<span className={`text-slate-100 font-medium text-base tracking-wide text-center transition-all duration-200 ease-ios ${isLoading ? 'text-sm' : ''}`}>
{website.name}
</span>
</div>

View File

@@ -9,7 +9,7 @@ const ConfigurationButton: React.FC<ConfigurationButtonProps> = ({ onClick }) =>
<div className="absolute top-4 right-4">
<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 transition-colors"
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"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>

View File

@@ -10,7 +10,7 @@ const EditButton: React.FC<EditButtonProps> = ({ isEditing, onClick }) => {
<div className="absolute top-4 left-4">
<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 transition-colors"
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' }}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">