Improve drag/swipe handling and Features page UX

- SwipeTabs: Delay pointer capture until drag starts for better tap detection
- Features: Only allow drag via handle dots, not entire card
- Features: Smart hasOrderChanges - hide buttons when order returns to initial
- Features: Don't apply dragging style until movement exceeds threshold
- ThemeSettings: Add role="button" for accessibility on all option cards
- Sidebar: Light theme active menu item styling improvements
- Layout: Tab bar translucency and blur effects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-23 03:19:40 +01:00
parent 75cb687500
commit 9e3556322f
6 changed files with 130 additions and 50 deletions

View File

@@ -88,6 +88,17 @@ export default function Features() {
return positions;
}, [moduleStates]);
const hasChangesFromInitial = useCallback((nextOrder: string[], nextPositions: Record<string, 'top' | 'bottom'>) => {
const initialOrder = initialOrderRef.current;
const initialPositions = initialPositionsRef.current;
if (nextOrder.length !== initialOrder.length) return true;
if (nextOrder.some((id, index) => id !== initialOrder[index])) return true;
for (const id of nextOrder) {
if ((nextPositions[id] || 'top') !== (initialPositions[id] || 'top')) return true;
}
return false;
}, []);
const beginOrderEdit = useCallback(() => {
if (!isUserEditing.current) {
const snapshotOrder = localOrder.length ? [...localOrder] : buildFullOrder(moduleOrder);
@@ -294,9 +305,15 @@ export default function Features() {
localPositionsRef.current = nextPositions;
setLocalPositions(nextPositions);
setLocalOrder(nextOrder);
setHasOrderChanges(true);
// Only show buttons if different from initial saved state
const hasChanges = hasChangesFromInitial(nextOrder, nextPositions);
setHasOrderChanges(hasChanges);
// Reset editing state when order returns to initial
if (!hasChanges) {
isUserEditing.current = false;
}
}
}, [buildFullOrder, buildPositionsFromStates, getInsertIndex, getSectionForY, moduleOrder]);
}, [buildFullOrder, buildPositionsFromStates, getInsertIndex, getSectionForY, hasChangesFromInitial, moduleOrder]);
const handleOrderPointerMove = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
const dragState = dragPointerRef.current;
@@ -363,7 +380,7 @@ export default function Features() {
const handleOrderPointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>, moduleId: string) => {
if (event.pointerType === 'mouse' && event.button !== 0) return;
beginOrderEdit();
setDraggedItem(moduleId);
// Don't set draggedItem here - only set it when drag actually starts (in handleOrderPointerMove)
dragPointerRef.current.pointerId = event.pointerId;
dragPointerRef.current.touchId = null;
dragPointerRef.current.activeId = moduleId;
@@ -432,7 +449,7 @@ export default function Features() {
const handleOrderTouchStart = useCallback((event: React.TouchEvent<HTMLDivElement>, moduleId: string) => {
if (event.changedTouches.length === 0) return;
beginOrderEdit();
setDraggedItem(moduleId);
// Don't set draggedItem here - only set it when drag actually starts (in handleGlobalTouchMove)
const touch = event.changedTouches[0];
dragPointerRef.current.pointerId = null;
dragPointerRef.current.touchId = touch.identifier;
@@ -565,7 +582,6 @@ export default function Features() {
</div>
<div
className="order-cards"
data-swipe-ignore="true"
ref={topOrderRef}
>
{topOrderModules.map((moduleId) => {
@@ -576,11 +592,6 @@ export default function Features() {
key={moduleId}
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
data-module-id={moduleId}
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
>
<div className="order-card-preview">
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
@@ -588,7 +599,15 @@ export default function Features() {
<div className="order-card-info">
<span className="order-card-name">{moduleName}</span>
</div>
<div className="order-card-handle">
<div
className="order-card-handle"
data-swipe-ignore="true"
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
>
<span className="material-symbols-outlined">drag_indicator</span>
</div>
</div>
@@ -606,7 +625,6 @@ export default function Features() {
</div>
<div
className="order-cards"
data-swipe-ignore="true"
ref={bottomOrderRef}
>
{bottomOrderModules.map((moduleId) => {
@@ -617,11 +635,6 @@ export default function Features() {
key={moduleId}
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
data-module-id={moduleId}
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
>
<div className="order-card-preview">
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
@@ -629,7 +642,15 @@ export default function Features() {
<div className="order-card-info">
<span className="order-card-name">{moduleName}</span>
</div>
<div className="order-card-handle">
<div
className="order-card-handle"
data-swipe-ignore="true"
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
>
<span className="material-symbols-outlined">drag_indicator</span>
</div>
</div>

View File

@@ -291,8 +291,16 @@ export default function ThemeSettings() {
{colors.map((color) => (
<div
key={color.id}
role="button"
tabIndex={0}
className={`color-card ${accentColor === color.id ? 'active' : ''}`}
onClick={() => handleAccentColorChange(color.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAccentColorChange(color.id);
}
}}
>
<div className="color-swatch-large" style={{ backgroundColor: color.value }}>
{accentColor === color.id && (
@@ -318,8 +326,16 @@ export default function ThemeSettings() {
return (
<div
key={palette.id}
role="button"
tabIndex={0}
className={`palette-card ${colorPalette === palette.id ? 'active' : ''}`}
onClick={() => handleColorPaletteChange(palette.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleColorPaletteChange(palette.id);
}
}}
>
<div className="palette-preview">
<div className="palette-swatch-row">
@@ -363,8 +379,16 @@ export default function ThemeSettings() {
{radii.map((radius) => (
<div
key={radius.id}
role="button"
tabIndex={0}
className={`option-card ${borderRadius === radius.id ? 'active' : ''}`}
onClick={() => handleBorderRadiusChange(radius.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleBorderRadiusChange(radius.id);
}
}}
>
<div className="option-preview">
<div
@@ -389,8 +413,16 @@ export default function ThemeSettings() {
{sidebarStyles.map((style) => (
<div
key={style.id}
role="button"
tabIndex={0}
className={`option-card ${sidebarStyle === style.id ? 'active' : ''}`}
onClick={() => handleSidebarStyleChange(style.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSidebarStyleChange(style.id);
}
}}
>
<div className="option-preview">
<div className={`sidebar-preview sidebar-preview-${style.id}`}>
@@ -415,8 +447,16 @@ export default function ThemeSettings() {
{sidebarModes.map((mode) => (
<div
key={mode.id}
role="button"
tabIndex={0}
className={`option-card ${sidebarMode === mode.id ? 'active' : ''}`}
onClick={() => setSidebarMode(mode.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSidebarMode(mode.id);
}
}}
>
<div className="option-preview">
<div className={`sidebar-mode-preview sidebar-mode-${mode.id}`}>
@@ -441,8 +481,16 @@ export default function ThemeSettings() {
{densities.map((d) => (
<div
key={d.id}
role="button"
tabIndex={0}
className={`option-card ${density === d.id ? 'active' : ''}`}
onClick={() => handleDensityChange(d.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleDensityChange(d.id);
}
}}
>
<div className="option-preview">
<div className={`density-preview density-preview-${d.id}`}>
@@ -468,8 +516,16 @@ export default function ThemeSettings() {
{fonts.map((f) => (
<div
key={f.id}
role="button"
tabIndex={0}
className={`option-card ${fontFamily === f.id ? 'active' : ''}`}
onClick={() => handleFontFamilyChange(f.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleFontFamilyChange(f.id);
}
}}
>
<div className="option-preview">
<div className="font-preview" style={{ fontFamily: f.fontStyle }}>