Add a one-time visual hint for wheel-scrolling the tile row
The existing peek animation (row nudges right and back on first load)
only demonstrated that the row moves, not how to move it - ambiguous
now that mouse-drag is gone. On non-touch devices it's paired with a
small pill ("Mit dem Mausrad blättern" / "Scroll to browse", mouse icon
flanked by chevrons) that fades in around the peek and out ~1.2s later,
floating centered over the row at a z-index above even the frontmost
coverflow card. Touch devices keep just the peek, since swiping is
already self-explanatory there. Plays once per page load, gated behind
the same reduced-motion check as the peek itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
eba15ef52b
commit
b1b66f1813
9 changed files with 124 additions and 95 deletions
|
|
@ -151,6 +151,7 @@ export default {
|
||||||
featuredBadge: 'Empfohlen',
|
featuredBadge: 'Empfohlen',
|
||||||
moreBullets: (n) => (n === 1 ? '+ 1 weitere Leistung' : `+ ${n} weitere Leistungen`),
|
moreBullets: (n) => (n === 1 ? '+ 1 weitere Leistung' : `+ ${n} weitere Leistungen`),
|
||||||
lessBullets: 'Weniger anzeigen',
|
lessBullets: 'Weniger anzeigen',
|
||||||
|
wheelHint: 'Mit dem Mausrad blättern',
|
||||||
},
|
},
|
||||||
leistungen: {
|
leistungen: {
|
||||||
metaTitle: 'Leistungen',
|
metaTitle: 'Leistungen',
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ export default {
|
||||||
featuredBadge: 'Recommended',
|
featuredBadge: 'Recommended',
|
||||||
moreBullets: (n) => (n === 1 ? '+ 1 more feature' : `+ ${n} more features`),
|
moreBullets: (n) => (n === 1 ? '+ 1 more feature' : `+ ${n} more features`),
|
||||||
lessBullets: 'Show less',
|
lessBullets: 'Show less',
|
||||||
|
wheelHint: 'Scroll to browse',
|
||||||
},
|
},
|
||||||
leistungen: {
|
leistungen: {
|
||||||
metaTitle: 'Services',
|
metaTitle: 'Services',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import usePageMeta from '../../hooks/usePageMeta.js';
|
||||||
import MaintenanceNotice from '../../components/MaintenanceNotice.jsx';
|
import MaintenanceNotice from '../../components/MaintenanceNotice.jsx';
|
||||||
import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.jsx';
|
import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.jsx';
|
||||||
import DynamicIcon from '../../components/DynamicIcon.jsx';
|
import DynamicIcon from '../../components/DynamicIcon.jsx';
|
||||||
|
import { ChevronLeft, ChevronRight, Mouse } from 'lucide-react';
|
||||||
import { useMaintenance } from '../../context/MaintenanceContext.jsx';
|
import { useMaintenance } from '../../context/MaintenanceContext.jsx';
|
||||||
import { useLanguage } from '../../context/LanguageContext.jsx';
|
import { useLanguage } from '../../context/LanguageContext.jsx';
|
||||||
import { useSiteSettings } from '../../context/SiteSettingsContext.jsx';
|
import { useSiteSettings } from '../../context/SiteSettingsContext.jsx';
|
||||||
|
|
@ -271,6 +272,7 @@ export default function ModelPage() {
|
||||||
const trackRef = useRef(null);
|
const trackRef = useRef(null);
|
||||||
const thumbRef = useRef(null);
|
const thumbRef = useRef(null);
|
||||||
const hintPlayedRef = useRef(false);
|
const hintPlayedRef = useRef(false);
|
||||||
|
const [wheelHintVisible, setWheelHintVisible] = useState(false);
|
||||||
const formatPrice = (value) =>
|
const formatPrice = (value) =>
|
||||||
new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value);
|
new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value);
|
||||||
|
|
||||||
|
|
@ -321,24 +323,30 @@ export default function ModelPage() {
|
||||||
// Scroll-Leiste; Touch-Geraete behalten ohnehin das native, fluessige
|
// Scroll-Leiste; Touch-Geraete behalten ohnehin das native, fluessige
|
||||||
// Wisch-Scrolling der scroll-snap-Kartenreihe.
|
// Wisch-Scrolling der scroll-snap-Kartenreihe.
|
||||||
//
|
//
|
||||||
// Stattdessen: einmaliger kurzer Wisch-Hinweis beim ersten Laden auf Touch-Geraeten
|
// Stattdessen: einmaliger kurzer Wisch-Hinweis beim ersten Laden (Kundenwunsch) -
|
||||||
// (Kundenwunsch) - die Reihe faehrt kurz ein Stueck nach rechts und wieder zurueck,
|
// die Reihe faehrt kurz ein Stueck nach rechts und wieder zurueck, damit sofort
|
||||||
// damit sofort klar ist, dass sich die Kacheln wischen lassen. Nur einmal pro
|
// klar ist, dass sich die Kacheln wischen lassen. Nur einmal pro Seitenaufruf,
|
||||||
// Seitenaufruf, nur auf echten Touch-Geraeten, nicht bei reduzierter Bewegung.
|
// nicht bei reduzierter Bewegung. Auf Touch-Geraeten reicht die Bewegung allein
|
||||||
|
// (Wischen ist selbsterklaerend); auf Geraeten mit Maus blendet zusaetzlich kurz
|
||||||
|
// ein Mausrad-Icon mit Pfeilen ein, weil die Ausweich-Bewegung allein offen liesse,
|
||||||
|
// WIE man scrollt (Ziehen mit der Maus gibt es seit Kundenfeedback nicht mehr).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = scrollerRef.current;
|
const el = scrollerRef.current;
|
||||||
if (layout === 'grid' || !el || hintPlayedRef.current) return undefined;
|
if (layout === 'grid' || !el || hintPlayedRef.current) return undefined;
|
||||||
const isTouch = window.matchMedia('(pointer: coarse)').matches;
|
const isTouch = window.matchMedia('(pointer: coarse)').matches;
|
||||||
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
if (!isTouch || reducedMotion) return undefined;
|
if (reducedMotion) return undefined;
|
||||||
hintPlayedRef.current = true;
|
hintPlayedRef.current = true;
|
||||||
const peek = Math.min(el.clientWidth * 0.4, 160);
|
const peek = Math.min(el.clientWidth * 0.4, 160);
|
||||||
const t1 = setTimeout(() => el.scrollTo({ left: peek, behavior: 'smooth' }), 500);
|
const timers = [
|
||||||
const t2 = setTimeout(() => el.scrollTo({ left: 0, behavior: 'smooth' }), 1250);
|
setTimeout(() => el.scrollTo({ left: peek, behavior: 'smooth' }), 500),
|
||||||
return () => {
|
setTimeout(() => el.scrollTo({ left: 0, behavior: 'smooth' }), 1250),
|
||||||
clearTimeout(t1);
|
];
|
||||||
clearTimeout(t2);
|
if (!isTouch) {
|
||||||
};
|
timers.push(setTimeout(() => setWheelHintVisible(true), 450));
|
||||||
|
timers.push(setTimeout(() => setWheelHintVisible(false), 1700));
|
||||||
|
}
|
||||||
|
return () => timers.forEach(clearTimeout);
|
||||||
}, [layout, data]);
|
}, [layout, data]);
|
||||||
|
|
||||||
// Mausrad ueber der Kartenreihe scrollt horizontal (Kundenfeedback: die Scroll-
|
// Mausrad ueber der Kartenreihe scrollt horizontal (Kundenfeedback: die Scroll-
|
||||||
|
|
@ -589,7 +597,7 @@ export default function ModelPage() {
|
||||||
// bleiben dunkel (Referenzdesign), wodurch sie auf heller Flaeche als eigenstaendige
|
// bleiben dunkel (Referenzdesign), wodurch sie auf heller Flaeche als eigenstaendige
|
||||||
// "Karten" wirken. Der Scrollbalken-Stil unten passt sich per dark: mit an.
|
// "Karten" wirken. Der Scrollbalken-Stil unten passt sich per dark: mit an.
|
||||||
<section className="py-10 sm:py-14" style={{ fontFamily: "'Barlow', sans-serif" }}>
|
<section className="py-10 sm:py-14" style={{ fontFamily: "'Barlow', sans-serif" }}>
|
||||||
<div className={layout === 'grid' ? 'mx-auto max-w-6xl px-4 sm:px-6' : 'mx-auto max-w-[1880px] px-4 sm:px-6'}>
|
<div className={layout === 'grid' ? 'mx-auto max-w-6xl px-4 sm:px-6' : 'relative mx-auto max-w-[1880px] px-4 sm:px-6'}>
|
||||||
<div
|
<div
|
||||||
ref={layout === 'grid' ? undefined : scrollerRef}
|
ref={layout === 'grid' ? undefined : scrollerRef}
|
||||||
className={
|
className={
|
||||||
|
|
@ -628,6 +636,25 @@ export default function ModelPage() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mausrad-Hinweis: einmaliger, kurz ein- und ausblendender Pill mittig ueber
|
||||||
|
der Reihe, zeitlich an die Ausweich-Bewegung oben gekoppelt (Hint-Effekt).
|
||||||
|
z-[200] haelt ihn ueber den Coverflow-Karten (die bis z-index 100 reichen). */}
|
||||||
|
{layout !== 'grid' && (
|
||||||
|
<div
|
||||||
|
className={`pointer-events-none absolute inset-x-0 top-1/2 z-[200] flex -translate-y-1/2 justify-center transition-opacity duration-300 ${
|
||||||
|
wheelHintVisible ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 rounded-full border border-white/10 bg-neutral-900/85 px-4 py-2 text-xs font-medium text-white shadow-lg backdrop-blur-sm">
|
||||||
|
<ChevronLeft className="h-3.5 w-3.5 text-brand-400" />
|
||||||
|
<Mouse className="h-4 w-4 text-brand-400" />
|
||||||
|
<ChevronRight className="h-3.5 w-3.5 text-brand-400" />
|
||||||
|
<span>{t('modelPage.wheelHint')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Eigene Scroll-Leiste: zentrierter Track, markengruener leuchtender Daumen.
|
{/* Eigene Scroll-Leiste: zentrierter Track, markengruener leuchtender Daumen.
|
||||||
Ziehbar + klickbar (Logik im Scrollbar-Effekt); blendet aus, wenn alle
|
Ziehbar + klickbar (Logik im Scrollbar-Effekt); blendet aus, wenn alle
|
||||||
Karten ohne Scrollen passen. touch-action none fuer sauberes Pointer-Ziehen. */}
|
Karten ohne Scrollen passen. touch-action none fuer sauberes Pointer-Ziehen. */}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
78
hifi/assets/index-DQAs_YfT.js
Normal file
78
hifi/assets/index-DQAs_YfT.js
Normal file
File diff suppressed because one or more lines are too long
1
hifi/assets/index-wBHRPmjw.css
Normal file
1
hifi/assets/index-wBHRPmjw.css
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -8766,4 +8766,4 @@ function IE(t,r){for(var _=0;_<r.length;_++){const a=r[_];if(typeof a!="string"&
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
*/const wE=[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]],bPe=o("zoom-out",wE),gPe=Object.freeze(Object.defineProperty({__proto__:null,__iconNode:wE,default:bPe},Symbol.toStringTag,{value:"Module"}));export{AR as C,OPe as D,EW as E,_ae as M,Lce as P,MPe as R,s4e as S,vge as U,wie as a,Lae as b,oS as c,PPe as i,p as r};
|
*/const wE=[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]],bPe=o("zoom-out",wE),gPe=Object.freeze(Object.defineProperty({__proto__:null,__iconNode:wE,default:bPe},Symbol.toStringTag,{value:"Module"}));export{h$ as C,OPe as D,EW as E,Wne as M,Lce as P,MPe as R,s4e as S,vge as U,p$ as a,_ae as b,wie as c,AR as d,Lae as e,oS as f,PPe as i,p as r};
|
||||||
|
|
@ -43,9 +43,9 @@
|
||||||
"sameAs": ["https://www.youtube.com/@hifiplanet2812"]
|
"sameAs": ["https://www.youtube.com/@hifiplanet2812"]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-D-bbfDHb.js"></script>
|
<script type="module" crossorigin src="/assets/index-DQAs_YfT.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/lucide-icons-CnpfS6R8.js">
|
<link rel="modulepreload" crossorigin href="/assets/lucide-icons-66ioQXWI.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-9666bnLb.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-wBHRPmjw.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
|
<body class="bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue