diff --git a/web/src/components/buttons/SettingsButton.svelte b/web/src/components/buttons/SettingsButton.svelte index 871c2fa0..0105d8e8 100644 --- a/web/src/components/buttons/SettingsButton.svelte +++ b/web/src/components/buttons/SettingsButton.svelte @@ -6,6 +6,8 @@ Value extends CobaltSettings[Context][Id] " > + import { hapticSwitch } from "$lib/haptics"; + import settings, { updateSetting } from "$lib/state/settings"; import type { CobaltSettings } from "$lib/types/settings"; @@ -22,12 +24,14 @@ class="button" class:active={isActive} aria-pressed={isActive} - on:click={() => + on:click={() => { + hapticSwitch(); updateSetting({ [settingContext]: { [settingId]: settingValue, }, - })} + }); + }} > diff --git a/web/src/components/buttons/SettingsToggle.svelte b/web/src/components/buttons/SettingsToggle.svelte index f5dd6579..14edbb66 100644 --- a/web/src/components/buttons/SettingsToggle.svelte +++ b/web/src/components/buttons/SettingsToggle.svelte @@ -5,6 +5,7 @@ Id extends keyof CobaltSettings[Context] " > + import { hapticSwitch } from "$lib/haptics"; import settings, { updateSetting } from "$lib/state/settings"; import type { CobaltSettings } from "$lib/types/settings"; @@ -34,14 +35,15 @@ class="button toggle-container" role="switch" aria-checked={isEnabled} - disabled={disabled} - on:click={() => + {disabled} + on:click={() => { + hapticSwitch(); updateSetting({ [settingContext]: { [settingId]: !isEnabled, - } - }) - } + }, + }); + }} >

{title}

@@ -82,10 +84,14 @@ border-radius: var(--border-radius); overflow: scroll; - transition: background 0.1s, box-shadow 0.1s; + transition: + background 0.1s, + box-shadow 0.1s; } .toggle-container:active { - box-shadow: var(--button-box-shadow), 0 0 0 1.5px var(--button-stroke) inset; + box-shadow: + var(--button-box-shadow), + 0 0 0 1.5px var(--button-stroke) inset; } diff --git a/web/src/lib/device.ts b/web/src/lib/device.ts index e0864963..42f19aa5 100644 --- a/web/src/lib/device.ts +++ b/web/src/lib/device.ts @@ -11,6 +11,7 @@ const device = { iPhone: false, iPad: false, iOS: false, + modernIOS: false, android: false, mobile: false, }, @@ -35,6 +36,9 @@ if (browser) { const iPhone = ua.includes("iphone os"); const iPad = !iPhone && ua.includes("mac os") && navigator.maxTouchPoints > 0; + const iosVersion = Number(ua.match(/iphone os (\d+)_/)?.[1]); + const modernIOS = iPhone && iosVersion >= 18; + const iOS = iPhone || iPad; const android = ua.includes("android") || ua.includes("diordna"); @@ -45,11 +49,13 @@ if (browser) { }; device.is = { + mobile: iOS || android, + android, + iPhone, iPad, iOS, - android, - mobile: iOS || android, + modernIOS, }; device.browser = { diff --git a/web/src/lib/haptics.ts b/web/src/lib/haptics.ts new file mode 100644 index 00000000..22937832 --- /dev/null +++ b/web/src/lib/haptics.ts @@ -0,0 +1,26 @@ +import { device } from "$lib/device"; + +// not sure if vibrations feel the same on android, +// so they're enabled only on ios 18+ for now +const shouldVibrate = device.is.modernIOS; + +export const hapticSwitch = () => { + if (!shouldVibrate) return; + + try { + const label = document.createElement("label"); + label.ariaHidden = "true"; + label.style.display = "none"; + + const input = document.createElement("input"); + input.type = "checkbox"; + input.setAttribute("switch", ""); + label.appendChild(input); + + document.head.appendChild(label); + label.click(); + document.head.removeChild(label); + } catch { + // ignore + } +}