HapticButton
A button with the phone's own feel, falling back to vibrate and then to nothing, with nine named patterns.
Demo
Pressed 0 times; the last was felt: nothing yet.
Get it
It needs theme.qwe, as every component does, and button.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/button.qwe$ curl -O https://quillolang.org/components/ui/haptic_button.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/haptic_button.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/haptic_button.qwe
let haptic_button_demo_css = css`.ui-hapbtn-demo { display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: 10px; }`component HapticButtonDemoLive() { style global haptic_button_demo_css state presses = 0 state last = "nothing yet" state quiet = true let note = (how) => { last = how } <div.demo-stack> <div.ui-hapbtn-demo> {for n in ["selection", "light", "medium", "heavy", "soft", "rigid"] => <HapticButton key={n} pattern={n} variant="soft" full={true} say={false} on_click={() => { presses += 1 }} on_feel={note}>{upper(slice(n, 0, 1)) + slice(n, 1, len(n))}</HapticButton>} </div> <div.ui-hapbtn-demo> <HapticButton pattern="success" full={true} on_click={() => { presses += 1 }} on_feel={note}>Success</HapticButton> <HapticButton pattern="warning" variant="outline" full={true} on_click={() => { presses += 1 }} on_feel={note}>Warning</HapticButton> <HapticButton pattern="error" tone="danger" full={true} on_click={() => { presses += 1 }} on_feel={note}>Error</HapticButton> </div> <div.demo-row> <HapticButton pattern="thud" variant="outline" say={true} on_click={() => { presses += 1 }} on_feel={note}>A name it does not know</HapticButton> <HapticButton muted={quiet} variant="ghost" say={true} on_click={() => { presses += 1 }} on_feel={note}>Muted</HapticButton> <HapticButton disabled={true} on_click={() => { presses += 1 }}>Disabled</HapticButton> <Button size="lg" variant="ghost" id="haptic-button-demo-quiet" on_click={() => { quiet = !quiet }}>{quiet ? "Unmute" : "Mute"}</Button> </div> <p.demo-note id="haptic-button-demo-note">Pressed {presses} times; the last was felt: {last}.</p> </div>}view HapticButtonDemo() = <HapticButtonDemoLive/>Source
ui/haptic_button.qwe
// HapticButton: a button that is felt as well as pressed. In the Quillo app it uses the// phone's own haptics (quilloNative); in a browser with `navigator.vibrate`, Android's, it// plays a short pattern of the same name; elsewhere, iOS Safari among them, it is an// ordinary button and nothing is missing but the feel. Each pattern has a name, the same// nine everywhere, and the button says which way it was felt, for a test or a settings page.//// <HapticButton pattern="success" on_click={() => { save() }}>Save</HapticButton>// <HapticButton pattern="error" tone="danger" say={true}>Delete</HapticButton>//// pattern: "light", "medium" (the default, and what a name it does not know means),// "heavy", "rigid", "soft", "selection", "success", "warning" or "error".// muted: no feel at all, for a person who turned it off in the app's settings (false).// say: shows under the button how the last press was felt (false).// on_click(): inside one of your components, what a press does. It is called whether or// not anything could be felt. on_feel(how): called after it with "app", "vibrate", "none"// or "muted". The wrapper has data-pattern and data-felt, the same words, for a test.// variant, tone, full, disabled, loading, type, name, value, label, id: as for Button. There is no small size:// a thing to feel is a thing for a thumb.import ui/theme.qwe use ui_clsimport ui/button.qwe use Buttonlet haptic_button_css = css`.ui-hapbtn { display: inline-grid; gap: 4px; justify-items: stretch; font: 13px/1.4 var(--ui-font); }.ui-hapbtn-full { display: grid; }.ui-hapbtn-say { color: var(--ui-muted); text-align: center; }.ui-hapbtn .ui-btn { touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: transform .08s; }.ui-hapbtn .ui-btn:active { transform: scale(.97); }@media (prefers-reduced-motion: reduce) { .ui-hapbtn .ui-btn:active { transform: none; } }`// The nine names, in the order a settings page would list them.let haptic_button_names = ["selection", "light", "medium", "heavy", "soft", "rigid", "success", "warning", "error"]// A name the app knows; anything else is medium, as the apps themselves take it.fn haptic_button_kind(name) = haptic_button_names | contains(name) ? name : "medium"// The same names as vibrate patterns, in milliseconds on, off, on. A motor takes// about 10 ms to be felt at all. The three that report an outcome have two or more// beats, as the system's do, so that they can be told apart with the phone in a hand.fn haptic_button_pattern(name) = match haptic_button_kind(name) { "selection" => [8] "light" => [12] "heavy" => [40] "soft" => [30] "rigid" => [16] "success" => [15, 60, 30] "warning" => [30, 80, 30] "error" => [50, 60, 50, 60, 50] _ => [20]}// What a press's route was, in words.fn haptic_button_words(how, kind) = match how { "app" => `Felt ${kind} through the app` "vibrate" => `Vibrated ${kind}: ${join(haptic_button_pattern(kind) | map((n) => str(n)), ", ")} ms` "none" => "This device has no way to feel it" "muted" => "Turned off" _ => ""}component HapticButton(pattern: str = "medium", muted: bool = false, say: bool = false, variant: str = "solid", tone: str = "accent", full: bool = false, disabled: bool = false, loading: bool = false, type: str = "button", name: str? = null, value: str? = null, label: str? = null, id: str? = null, on_click = null, on_feel = null, class: str? = null, children) { style global haptic_button_css state felt = null let kind = haptic_button_kind(pattern) // The app first. Its call failing is no reason to do without: vibrate may still be there. let feel = () => { let r = muted ? null : js("quilloNative.haptic", kind) if muted { "muted" } else if !is_err(r) { "app" } else { // false is a refusal: no recent tap, or vibration turned off in the system. js("navigator.vibrate", haptic_button_pattern(kind)) == true ? "vibrate" : "none" } } <span class={ui_cls(["ui-hapbtn", full && "ui-hapbtn-full", class])} data-pattern={kind} data-felt={felt}> <Button size="lg" variant={variant} tone={tone} full={full} disabled={disabled} loading={loading} type={type} name={name} value={value} label={label} id={id} on_click={() => { on_click && on_click() felt = feel() on_feel && on_feel(felt) }}>{children}</Button> {say && <span.ui-hapbtn-say role="status">{haptic_button_words(felt, kind)}</span>} </span>}Related
WaterTrackerTap a glass to add what was drunk, with undo, a goal and a day that starts empty at the phone's own midnight.2 views · 7 downloadsWorkoutTimerIntervals of work and rest with a large countdown, felt as well as seen, the screen kept on, and time that stays right when the screen sleeps.3 views · 6 downloadsDockA floating bar of icon buttons fixed to the bottom or a side of the page, with badges and the current one marked.6 views · 7 downloadsFabA round floating action button fixed to a corner of the screen, with an icon and an optional label.5 views · 7 downloadsSpeedDialA floating action button that opens a column of smaller labelled actions, with the keyboard and a click elsewhere handled.5 views · 8 downloadsActionSheetA list of actions rising from the bottom, with a destructive style and Cancel: swipe down or the phone's back gesture closes it; on a tablet, a card by what opened it.3 views · 9 downloads
Thread
Thread 0 posts
On HapticButton: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.