feat(haptics): add haptic feedback to nav items and back buttons (#196)

## Summary

- Installs `web-haptics` via bun
- Triggers `selection` haptic feedback on all navigation items (mobile
bottom bar and desktop sidebar)
- Triggers `light` haptic feedback on all back buttons
(`connect-account-inline.tsx`, `categorize.tsx`)
This commit is contained in:
Víctor Falcón 2026-03-03 21:03:58 +00:00 committed by GitHub
parent 09d81ac7e7
commit 3d742677b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 977 additions and 937 deletions

View File

@ -56,6 +56,7 @@
"uuidv7": "^1.1.0",
"vaul": "^1.1.2",
"vite": "^7.3.1",
"web-haptics": "^0.0.6",
"xlsx": "^0.18.5",
},
"devDependencies": {
@ -1758,6 +1759,8 @@
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"web-haptics": ["web-haptics@0.0.6", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "svelte": ">=4", "vue": ">=3" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw=="],
"web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],

1882
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -94,6 +94,7 @@
"uuidv7": "^1.1.0",
"vaul": "^1.1.2",
"vite": "^7.3.1",
"web-haptics": "^0.0.6",
"xlsx": "^0.18.5"
},
"optionalDependencies": {

View File

@ -20,6 +20,7 @@ import { SharedData } from '@/types';
import { Link, usePage } from '@inertiajs/react';
import { LucideIcon } from 'lucide-react';
import { useMemo } from 'react';
import { useWebHaptics } from 'web-haptics/react';
import AppLogo from './app-logo';
export function AppSidebar() {
@ -28,6 +29,7 @@ export function AppSidebar() {
() => getMainNavItems(page.props.features, page.props.locale),
[page.props.features, page.props.locale],
);
const { trigger } = useWebHaptics();
return (
<>
@ -40,6 +42,7 @@ export function AppSidebar() {
<Link
key={item.title}
href={item.href}
onClick={() => trigger('selection')}
className={cn([
'flex flex-1 flex-col items-center justify-center gap-1 rounded-full px-3 py-2 transition-all duration-200',
{

View File

@ -9,9 +9,11 @@ import { resolveUrl } from '@/lib/utils';
import { type NavItem } from '@/types';
import { __ } from '@/utils/i18n';
import { Link, usePage } from '@inertiajs/react';
import { useWebHaptics } from 'web-haptics/react';
export function NavMain({ items = [] }: { items: NavItem[] }) {
const page = usePage();
const { trigger } = useWebHaptics();
return (
<SidebarGroup className="px-2 py-0">
<SidebarGroupLabel>{__('Platform')}</SidebarGroupLabel>
@ -25,7 +27,11 @@ export function NavMain({ items = [] }: { items: NavItem[] }) {
)}
tooltip={{ children: __(item.title) }}
>
<Link href={item.href} prefetch>
<Link
href={item.href}
prefetch
onClick={() => trigger('selection')}
>
{item.icon && <item.icon />}
<span>{__(item.title)}</span>
</Link>

View File

@ -13,6 +13,7 @@ import type { EnableBankingInstitution } from '@/types/banking';
import { __ } from '@/utils/i18n';
import { ArrowLeft } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useWebHaptics } from 'web-haptics/react';
const COUNTRIES = [
{ code: 'ES', name: 'Spain' },
@ -73,6 +74,7 @@ interface ConnectAccountInlineProps {
export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) {
const [step, setStep] = useState<Step>('country');
const { trigger } = useWebHaptics();
const [country, setCountry] = useState<string>('');
const [institutions, setInstitutions] = useState<
EnableBankingInstitution[]
@ -272,7 +274,10 @@ export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) {
<Button
variant={'ghost'}
type="button"
onClick={handleBack}
onClick={() => {
trigger('light');
handleBack();
}}
className="w-full"
>
<ArrowLeft className="h-4 w-4" />
@ -329,7 +334,10 @@ export function ConnectAccountInline({ onBack }: ConnectAccountInlineProps) {
<Button
variant={'ghost'}
type="button"
onClick={handleBack}
onClick={() => {
trigger('light');
handleBack();
}}
className="w-full"
>
<ArrowLeft className="h-4 w-4" />

View File

@ -43,6 +43,7 @@ import {
SkipForward,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWebHaptics } from 'web-haptics/react';
const CATEGORY_USAGE_KEY = 'category-usage-order';
@ -107,6 +108,7 @@ export default function CategorizeTransactions({
automationRules: AutomationRule[];
}>().props;
const locale = useLocale();
const { trigger } = useWebHaptics();
const [uncategorizedTransactions, setUncategorizedTransactions] = useState<
DecryptedTransaction[]
@ -506,6 +508,7 @@ export default function CategorizeTransactions({
href={categorizeRoute
.url()
?.replace('/categorize', '')}
onClick={() => trigger('light')}
>
<Button size="lg" className="mt-4">
<ArrowLeft className="mr-2 h-4 w-4" />
@ -541,6 +544,7 @@ export default function CategorizeTransactions({
href={categorizeRoute
.url()
?.replace('/categorize', '')}
onClick={() => trigger('light')}
>
<Button>
<ArrowLeft className="mr-2 h-4 w-4" />
@ -561,6 +565,7 @@ export default function CategorizeTransactions({
<header className="flex items-center justify-between gap-6 px-4 py-3 dark:border-zinc-800">
<Link
href={categorizeRoute.url()?.replace('/categorize', '')}
onClick={() => trigger('light')}
className="flex w-fit flex-1 items-center gap-2 text-sm text-zinc-600 opacity-50 transition-all duration-200 hover:text-zinc-900 hover:opacity-100 dark:text-zinc-400 dark:hover:text-zinc-100"
>
<ArrowLeft className="h-4 w-4" />