A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

haptics

+729 -40
+2 -1
apps/docs/package.json
··· 56 56 "shiki": "^3.13.0", 57 57 "tailwindcss": "^4.1.16", 58 58 "tldraw": "^4.1.2", 59 - "vite-tsconfig-paths": "^5.1.4" 59 + "vite-tsconfig-paths": "^5.1.4", 60 + "web-haptics": "^0.0.6" 60 61 }, 61 62 "devDependencies": { 62 63 "@content-collections/core": "^0.11.1",
+8 -1
apps/docs/src/components/alert-dialog/index.tsx
··· 19 19 import type { StyleXComponentProps } from "../theme/types"; 20 20 21 21 import { Button } from "../button"; 22 + import { useHaptics } from "../haptics"; 22 23 import { IconButton } from "../icon-button"; 23 24 import { spacing } from "../theme/spacing.stylex"; 24 25 import { fontSize, typeramp } from "../theme/typography.stylex"; ··· 74 75 isOpen, 75 76 onOpenChange, 76 77 }: AlertDialogProps) => { 78 + const { trigger: triggerHaptic } = useHaptics(); 77 79 const dialogStyles = useDialogStyles({ size: "sm" }); 78 80 81 + const handleOpenChange = (open: boolean) => { 82 + triggerHaptic("impactLight"); 83 + onOpenChange?.(open); 84 + }; 85 + 79 86 return ( 80 87 <DialogTrigger 81 88 defaultOpen={defaultOpen} 82 89 isOpen={isOpen} 83 - onOpenChange={onOpenChange} 90 + onOpenChange={handleOpenChange} 84 91 > 85 92 {trigger} 86 93
+11
apps/docs/src/components/button/index.tsx
··· 7 7 8 8 import type { ButtonVariant, Size, StyleXComponentProps } from "../theme/types"; 9 9 10 + import { useHaptics } from "../haptics"; 10 11 import { ProgressCircle } from "../progress-circle"; 11 12 import { animationDuration } from "../theme/animations.stylex"; 12 13 import { spacing } from "../theme/spacing.stylex"; ··· 49 50 size, 50 51 isPending = false, 51 52 isDisabled, 53 + onPress, 52 54 ...props 53 55 }: ButtonProps) => { 56 + const { trigger } = useHaptics(); 54 57 const buttonStyles = useButtonStyles({ variant, size }); 55 58 const isHref = "href" in props; 56 59 60 + const handlePress = (e: Parameters<NonNullable<typeof onPress>>[0]) => { 61 + if (variant === "primary" && !isDisabled && !isPending) { 62 + trigger("impactMedium"); 63 + } 64 + onPress?.(e); 65 + }; 66 + 57 67 return ( 58 68 <AriaButton 59 69 {...props} 70 + onPress={handlePress} 60 71 {...stylex.props(buttonStyles, isHref && styles.link, style)} 61 72 data-size={size} 62 73 data-pending={isPending || undefined}
+8 -1
apps/docs/src/components/dialog/index.tsx
··· 14 14 15 15 import type { Size, StyleXComponentProps } from "../theme/types"; 16 16 17 + import { useHaptics } from "../haptics"; 17 18 import { IconButton } from "../icon-button"; 18 19 import { uiColor } from "../theme/color.stylex"; 19 20 import { spacing } from "../theme/spacing.stylex"; ··· 87 88 onOpenChange, 88 89 size, 89 90 }: DialogProps) => { 91 + const { trigger: triggerHaptic } = useHaptics(); 90 92 const dialogStyles = useDialogStyles({ size }); 91 93 94 + const handleOpenChange = (open: boolean) => { 95 + triggerHaptic("impactLight"); 96 + onOpenChange?.(open); 97 + }; 98 + 92 99 return ( 93 100 <DialogTrigger 94 101 defaultOpen={defaultOpen} 95 102 isOpen={isOpen} 96 - onOpenChange={onOpenChange} 103 + onOpenChange={handleOpenChange} 97 104 > 98 105 {trigger} 99 106
+8 -1
apps/docs/src/components/drawer/index.tsx
··· 14 14 15 15 import type { Size, StyleXComponentProps } from "../theme/types"; 16 16 17 + import { useHaptics } from "../haptics"; 17 18 import { IconButton } from "../icon-button"; 18 19 import { 19 20 animationDuration, ··· 173 174 direction = "right", 174 175 isNonModal = false, 175 176 }: DrawerProps) => { 177 + const { trigger: triggerHaptic } = useHaptics(); 176 178 const dialogStyles = useDialogStyles({ size }); 177 179 180 + const handleOpenChange = (open: boolean) => { 181 + triggerHaptic("impactLight"); 182 + onOpenChange?.(open); 183 + }; 184 + 178 185 return ( 179 186 <DialogTrigger 180 187 defaultOpen={defaultOpen} 181 188 isOpen={isOpen} 182 - onOpenChange={onOpenChange} 189 + onOpenChange={handleOpenChange} 183 190 > 184 191 {trigger} 185 192
+51
apps/docs/src/components/haptics/context.tsx
··· 1 + "use client"; 2 + 3 + import { createContext, useCallback } from "react"; 4 + import { useWebHaptics } from "web-haptics/react"; 5 + 6 + import type { HapticIntent } from "./haptics"; 7 + 8 + import { 9 + HAPTIC_PRESET_MAP, 10 + isHapticsEnabled, 11 + setHapticsEnabled, 12 + } from "./haptics"; 13 + 14 + export interface HapticsContextValue { 15 + enabled: boolean; 16 + setEnabled: (enabled: boolean) => void; 17 + trigger: (intent: HapticIntent) => void; 18 + } 19 + 20 + /* eslint-disable react/only-export-components -- HapticsContext is consumed by useHaptics */ 21 + export const HapticsContext = createContext<HapticsContextValue | null>(null); 22 + /* eslint-enable react/only-export-components */ 23 + 24 + /** 25 + * Provider for haptics enable/disable state. 26 + * Uses useWebHaptics under the hood; children use useHaptics() to trigger feedback. 27 + */ 28 + export function HapticsProvider({ children }: { children: React.ReactNode }) { 29 + const { trigger: webTrigger } = useWebHaptics(); 30 + 31 + const trigger = useCallback( 32 + (intent: HapticIntent) => { 33 + if (!isHapticsEnabled()) return; 34 + const preset = HAPTIC_PRESET_MAP[intent]; 35 + webTrigger(preset); 36 + }, 37 + [webTrigger], 38 + ); 39 + 40 + const value: HapticsContextValue = { 41 + enabled: isHapticsEnabled(), 42 + setEnabled: useCallback((enabled: boolean) => { 43 + setHapticsEnabled(enabled); 44 + }, []), 45 + trigger, 46 + }; 47 + 48 + return ( 49 + <HapticsContext.Provider value={value}>{children}</HapticsContext.Provider> 50 + ); 51 + }
+44
apps/docs/src/components/haptics/haptics.ts
··· 1 + /** 2 + * Semantic haptic intents aligned with Apple Human Interface Guidelines. 3 + * Maps to web-haptics preset names; no-ops gracefully when unsupported. 4 + */ 5 + 6 + export type HapticIntent = 7 + | "selection" 8 + | "impactLight" 9 + | "impactMedium" 10 + | "impactHeavy" 11 + | "success" 12 + | "warning" 13 + | "error"; 14 + 15 + /** Maps semantic intents to web-haptics preset names. */ 16 + export const HAPTIC_PRESET_MAP: Record< 17 + HapticIntent, 18 + "selection" | "light" | "medium" | "heavy" | "success" | "warning" | "error" 19 + > = { 20 + selection: "selection", 21 + impactLight: "light", 22 + impactMedium: "medium", 23 + impactHeavy: "heavy", 24 + success: "success", 25 + warning: "warning", 26 + error: "error", 27 + }; 28 + 29 + let hapticsEnabled = true; 30 + 31 + /** 32 + * Enable or disable haptic feedback globally. 33 + * @param enabled - Whether haptics should fire (default: true) 34 + */ 35 + export function setHapticsEnabled(enabled: boolean): void { 36 + hapticsEnabled = enabled; 37 + } 38 + 39 + /** 40 + * Check if haptics are currently enabled. 41 + */ 42 + export function isHapticsEnabled(): boolean { 43 + return hapticsEnabled; 44 + }
+7
apps/docs/src/components/haptics/index.ts
··· 1 + export { 2 + setHapticsEnabled, 3 + isHapticsEnabled, 4 + type HapticIntent, 5 + } from "./haptics"; 6 + export { HapticsContext, HapticsProvider } from "./context"; 7 + export { useHaptics } from "./useHaptics";
+9
apps/docs/src/components/haptics/index.tsx
··· 1 + /* eslint-disable react/only-export-components -- Barrel file for haptics utility */ 2 + export { 3 + setHapticsEnabled, 4 + isHapticsEnabled, 5 + type HapticIntent, 6 + } from "./haptics"; 7 + export { HapticsContext, HapticsProvider } from "./context"; 8 + export { useHaptics } from "./useHaptics"; 9 + /* eslint-enable react/only-export-components */
+44
apps/docs/src/components/haptics/useHaptics.ts
··· 1 + "use client"; 2 + 3 + import { useCallback, useContext } from "react"; 4 + import { useWebHaptics } from "web-haptics/react"; 5 + 6 + import type { HapticIntent } from "./haptics"; 7 + 8 + import { HapticsContext } from "./context"; 9 + import { 10 + HAPTIC_PRESET_MAP, 11 + isHapticsEnabled, 12 + setHapticsEnabled, 13 + } from "./haptics"; 14 + 15 + interface HapticsContextValue { 16 + enabled: boolean; 17 + setEnabled: (enabled: boolean) => void; 18 + trigger: (intent: HapticIntent) => void; 19 + } 20 + 21 + /** 22 + * Access haptics API via useWebHaptics. Returns context value when inside HapticsProvider. 23 + */ 24 + export function useHaptics(): HapticsContextValue { 25 + const context = useContext(HapticsContext); 26 + const { trigger: webTrigger } = useWebHaptics(); 27 + 28 + const fallbackTrigger = useCallback( 29 + (intent: HapticIntent) => { 30 + if (!isHapticsEnabled()) return; 31 + const preset = HAPTIC_PRESET_MAP[intent]; 32 + webTrigger(preset); 33 + }, 34 + [webTrigger], 35 + ); 36 + 37 + if (context) return context; 38 + 39 + return { 40 + enabled: isHapticsEnabled(), 41 + setEnabled: setHapticsEnabled, 42 + trigger: fallbackTrigger, 43 + }; 44 + }
+23 -1
apps/docs/src/components/listbox/index.tsx
··· 23 23 24 24 import { Checkbox } from "../checkbox"; 25 25 import { SizeContext } from "../context"; 26 + import { useHaptics } from "../haptics"; 26 27 import { Separator } from "../separator"; 27 28 import { ui } from "../theme/semantic-color.stylex"; 28 29 import { spacing } from "../theme/spacing.stylex"; ··· 76 77 style, 77 78 variant = "default", 78 79 isVirtualized = false, 80 + onSelectionChange, 81 + onAction, 79 82 ...props 80 83 }: ListBoxProps<T>) { 84 + const { trigger } = useHaptics(); 81 85 const size = sizeProp || use(SizeContext); 86 + 87 + const handleSelectionChange = ( 88 + keys: Parameters<NonNullable<typeof onSelectionChange>>[0], 89 + ) => { 90 + trigger("selection"); 91 + onSelectionChange?.(keys); 92 + }; 93 + 94 + const handleAction = (key: Parameters<NonNullable<typeof onAction>>[0]) => { 95 + trigger("selection"); 96 + onAction?.(key); 97 + }; 98 + 82 99 const listbox = ( 83 - <AriaListBox {...props} {...stylex.props(styles.listBox, style)} /> 100 + <AriaListBox 101 + {...props} 102 + onSelectionChange={handleSelectionChange} 103 + onAction={handleAction} 104 + {...stylex.props(styles.listBox, style)} 105 + /> 84 106 ); 85 107 86 108 return (
+19 -2
apps/docs/src/components/menu/index.tsx
··· 22 22 import type { Size, StyleXComponentProps } from "../theme/types"; 23 23 24 24 import { SizeContext } from "../context"; 25 + import { useHaptics } from "../haptics"; 25 26 import { ListBoxSeparator } from "../listbox"; 26 27 import { spacing } from "../theme/spacing.stylex"; 27 28 import { useListBoxItemStyles } from "../theme/useListBoxItemStyles"; ··· 78 79 placement, 79 80 header, 80 81 footer, 82 + onAction, 81 83 ...props 82 84 }: MenuProps<T>) { 85 + const { trigger: triggerHaptic } = useHaptics(); 83 86 const popoverStyles = usePopoverStyles(); 84 87 const size = sizeProp || use(SizeContext); 85 88 89 + const handleOpenChange = (open: boolean) => { 90 + triggerHaptic("impactLight"); 91 + onOpenChange?.(open); 92 + }; 93 + 94 + const handleAction = (key: Parameters<NonNullable<typeof onAction>>[0]) => { 95 + triggerHaptic("selection"); 96 + onAction?.(key); 97 + }; 98 + 86 99 return ( 87 100 <SizeContext value={size}> 88 101 <MenuTrigger 89 102 defaultOpen={defaultOpen} 90 103 isOpen={isOpen} 91 - onOpenChange={onOpenChange} 104 + onOpenChange={handleOpenChange} 92 105 > 93 106 {trigger} 94 107 <Popover ··· 105 118 <ListBoxSeparator /> 106 119 </> 107 120 )} 108 - <AriaMenu {...props} {...stylex.props(styles.menu)} /> 121 + <AriaMenu 122 + {...props} 123 + onAction={handleAction} 124 + {...stylex.props(styles.menu)} 125 + /> 109 126 {Boolean(footer) && ( 110 127 <> 111 128 <ListBoxSeparator />
-13
apps/docs/src/components/page/Page.tsx
··· 1 1 "use client"; 2 2 3 - import type { LinkProps as AriaLinkProps } from "react-aria-components"; 4 - 5 3 import * as stylex from "@stylexjs/stylex"; 6 4 import { ArrowLeft } from "lucide-react"; 7 5 import { useEffect, useRef, useState } from "react"; ··· 360 358 /> 361 359 ); 362 360 }; 363 - 364 - export interface PageBackLinkProps extends StyleXComponentProps< 365 - Omit<AriaLinkProps, "children" | "href"> 366 - > { 367 - /** The route path to navigate back to. When omitted with variant="small", uses router.back(). */ 368 - href?: string; 369 - /** Link content. Defaults to back arrow icon. */ 370 - children?: React.ReactNode; 371 - /** Enable view transition when navigating (for shared element transitions). Only applies when href is provided. */ 372 - viewTransition?: boolean; 373 - } 374 361 375 362 export interface PageTitleProps extends StyleXComponentProps< 376 363 React.ComponentProps<"h1">
+12 -1
apps/docs/src/components/popover/index.tsx
··· 15 15 16 16 import type { StyleXComponentProps } from "../theme/types"; 17 17 18 + import { useHaptics } from "../haptics"; 18 19 import { uiColor } from "../theme/color.stylex"; 19 20 import { spacing } from "../theme/spacing.stylex"; 20 21 import { usePopoverStyles } from "../theme/usePopoverStyles"; ··· 70 71 hasArrow, 71 72 ...popoverProps 72 73 }: PopoverProps) => { 74 + const { trigger: triggerHaptic } = useHaptics(); 73 75 const popoverStyles = usePopoverStyles(); 76 + 77 + const handleOpenChange = (open: boolean) => { 78 + triggerHaptic("impactLight"); 79 + onOpenChange?.(open); 80 + }; 74 81 75 82 return ( 76 83 <DialogTrigger 77 - {...({ isOpen, onOpenChange, defaultOpen } as DialogTriggerProps)} 84 + {...({ 85 + isOpen, 86 + onOpenChange: handleOpenChange, 87 + defaultOpen, 88 + } as DialogTriggerProps)} 78 89 > 79 90 {trigger} 80 91
+11
apps/docs/src/components/segmented-control/index.tsx
··· 16 16 import type { Size, StyleXComponentProps } from "../theme/types"; 17 17 18 18 import { SizeContext } from "../context"; 19 + import { useHaptics } from "../haptics"; 19 20 import { animationDuration } from "../theme/animations.stylex"; 20 21 import { uiColor } from "../theme/color.stylex"; 21 22 import { mediaQueries } from "../theme/media-queries.stylex"; ··· 119 120 children, 120 121 style, 121 122 size: sizeProp, 123 + onSelectionChange, 122 124 ...props 123 125 }: SegmentedControlProps) => { 126 + const { trigger } = useHaptics(); 124 127 const size = sizeProp ?? use(SizeContext); 125 128 129 + const handleSelectionChange = ( 130 + keys: Parameters<NonNullable<typeof onSelectionChange>>[0], 131 + ) => { 132 + trigger("selection"); 133 + onSelectionChange?.(keys); 134 + }; 135 + 126 136 return ( 127 137 <AriaToggleButtonGroup 128 138 disallowEmptySelection 129 139 selectionMode="single" 130 140 data-size={size} 141 + onSelectionChange={handleSelectionChange} 131 142 {...props} 132 143 {...stylex.props(styles.group, style)} 133 144 >
+14 -2
apps/docs/src/components/switch/index.tsx
··· 5 5 6 6 import type { StyleXComponentProps } from "../theme/types"; 7 7 8 + import { useHaptics } from "../haptics"; 8 9 import { animationDuration } from "../theme/animations.stylex"; 9 10 import { primaryColor, uiColor } from "../theme/color.stylex"; 10 11 import { mediaQueries } from "../theme/media-queries.stylex"; ··· 86 87 | SwitchWithAriaLabelProps 87 88 | SwitchWithAriaLabelledbyProps; 88 89 89 - export function Switch({ children, style, ...props }: SwitchProps) { 90 + export function Switch({ children, style, onChange, ...props }: SwitchProps) { 91 + const { trigger } = useHaptics(); 92 + 93 + const handleChange = (isSelected: boolean) => { 94 + trigger("selection"); 95 + onChange?.(isSelected); 96 + }; 97 + 90 98 return ( 91 - <AriaSwitch {...props} {...stylex.props(styles.wrapper, style)}> 99 + <AriaSwitch 100 + {...props} 101 + onChange={handleChange} 102 + {...stylex.props(styles.wrapper, style)} 103 + > 92 104 <div {...stylex.props(styles.indicator)}> 93 105 <div {...stylex.props(styles.thumb)} /> 94 106 </div>
+11
apps/docs/src/components/tabs/index.tsx
··· 20 20 import type { Size, StyleXComponentProps } from "../theme/types"; 21 21 22 22 import { SizeContext } from "../context"; 23 + import { useHaptics } from "../haptics"; 23 24 import { animationDuration } from "../theme/animations.stylex"; 24 25 import { primaryColor, uiColor } from "../theme/color.stylex"; 25 26 import { mediaQueries } from "../theme/media-queries.stylex"; ··· 200 201 style, 201 202 size: sizeProp, 202 203 orientation = "horizontal", 204 + onSelectionChange, 203 205 ...props 204 206 }: TabsProps) { 207 + const { trigger } = useHaptics(); 205 208 const size = sizeProp || use(SizeContext); 206 209 210 + const handleSelectionChange = ( 211 + key: Parameters<NonNullable<typeof onSelectionChange>>[0], 212 + ) => { 213 + trigger("selection"); 214 + onSelectionChange?.(key); 215 + }; 216 + 207 217 return ( 208 218 <SizeContext value={size}> 209 219 <AriaTabs 210 220 {...props} 221 + onSelectionChange={handleSelectionChange} 211 222 orientation={orientation} 212 223 data-size={size} 213 224 {...stylex.props(
+11
apps/docs/src/components/toast/Toast.tsx
··· 7 7 8 8 import * as stylex from "@stylexjs/stylex"; 9 9 import { X } from "lucide-react"; 10 + import { useEffect } from "react"; 10 11 import { 11 12 UNSTABLE_ToastRegion as AriaToastRegion, 12 13 Text, ··· 18 19 import type { ToastContentType } from "./queue"; 19 20 20 21 import { Button } from "../button"; 22 + import { useHaptics } from "../haptics"; 21 23 import { IconButton } from "../icon-button"; 22 24 import { 23 25 criticalColor, ··· 115 117 }); 116 118 117 119 function ToastItem({ toast }: { toast: QueuedToast<ToastContentType> }) { 120 + const { trigger } = useHaptics(); 118 121 const popoverStyles = usePopoverStyles(); 122 + 123 + useEffect(() => { 124 + if (toast.content.variant === "success") { 125 + trigger("success"); 126 + } else if (toast.content.variant === "critical") { 127 + trigger("error"); 128 + } 129 + }, [toast.key, toast.content.variant, trigger]); 119 130 120 131 return ( 121 132 <Toast
+3
apps/docs/src/components/toast/index.tsx
··· 1 + /* eslint-disable react/only-export-components -- Barrel re-exports ToastRegion and toasts */ 1 2 export { ToastRegion, type ToastRegionProps } from "./Toast"; 3 + export { toasts } from "./queue"; 4 + /* eslint-enable react/only-export-components */
+15 -1
apps/docs/src/components/toggle-button-group/index.tsx
··· 9 9 import type { StyleXComponentProps } from "../theme/types"; 10 10 11 11 import { ButtonGroupContext } from "../button/context"; 12 + import { useHaptics } from "../haptics"; 12 13 import { spacing } from "../theme/spacing.stylex"; 13 14 14 15 const styles = stylex.create({ ··· 77 78 orientation: orientationProp = "horizontal", 78 79 variant = "grouped", 79 80 itemsPerRow, 81 + onSelectionChange, 80 82 ...props 81 83 }: ToggleButtonGroupProps) => { 84 + const { trigger } = useHaptics(); 82 85 const groupOrientation = use(ButtonGroupContext); 83 86 const isInGroup = groupOrientation?.orientation !== undefined; 84 87 const orientation = groupOrientation?.orientation || orientationProp; 88 + 89 + const handleSelectionChange = ( 90 + keys: Parameters<NonNullable<typeof onSelectionChange>>[0], 91 + ) => { 92 + trigger("selection"); 93 + onSelectionChange?.(keys); 94 + }; 85 95 86 96 let stylesToApply = []; 87 97 ··· 109 119 110 120 return ( 111 121 <ButtonGroupContext value={contextValue}> 112 - <AriaToggleButtonGroup {...props} {...stylex.props(stylesToApply, style)}> 122 + <AriaToggleButtonGroup 123 + {...props} 124 + onSelectionChange={handleSelectionChange} 125 + {...stylex.props(stylesToApply, style)} 126 + > 113 127 {children} 114 128 </AriaToggleButtonGroup> 115 129 </ButtonGroupContext>
+1
packages/hip-ui/package.json
··· 50 50 "react-dom": "catalog:", 51 51 "react-stately": "catalog:", 52 52 "react-markdown": "^10.1.0", 53 + "web-haptics": "^0.0.6", 53 54 "rehype-sanitize": "^6.0.0", 54 55 "remark-gfm": "^4.0.0" 55 56 }
+2
packages/hip-ui/src/cli/install.tsx
··· 51 51 import { footerConfig } from "../components/footer/footer-config.js"; 52 52 import { formConfig } from "../components/form/form-config.js"; 53 53 import { gridConfig } from "../components/grid/grid-config.js"; 54 + import { hapticsConfig } from "../components/haptics/haptics-config.js"; 54 55 import { headerLayoutConfig } from "../components/header-layout/header-layout-config.js"; 55 56 import { hoverCardConfig } from "../components/hover-card/hover-card-config.js"; 56 57 import { iconButtonConfig } from "../components/icon-button/icon-button-config.js"; ··· 152 153 badgeConfig, 153 154 breadcrumbsConfig, 154 155 gridConfig, 156 + hapticsConfig, 155 157 switchConfig, 156 158 aspectRatioConfig, 157 159 autocompleteConfig,
+9 -1
packages/hip-ui/src/components/alert-dialog/alert-dialog-config.ts
··· 3 3 export const alertDialogConfig: ComponentConfig = { 4 4 name: "alert-dialog", 5 5 filepath: "./index.tsx", 6 - hipDependencies: ["../context.ts", "../theme/useDialogStyles.ts"], 6 + hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 11 + "../context.ts", 12 + "../theme/useDialogStyles.ts", 13 + ], 7 14 dependencies: { 8 15 "lucide-react": "^0.545.0", 16 + "web-haptics": "^0.0.6", 9 17 "@react-stately/utils": "^3.10.8", 10 18 "react-stately": "^3.42.0", 11 19 "@stylexjs/stylex": "^0.16.2",
+8 -1
packages/hip-ui/src/components/alert-dialog/index.tsx
··· 19 19 import type { StyleXComponentProps } from "../theme/types"; 20 20 21 21 import { Button } from "../button"; 22 + import { useHaptics } from "../haptics"; 22 23 import { IconButton } from "../icon-button"; 23 24 import { spacing } from "../theme/spacing.stylex"; 24 25 import { fontSize, typeramp } from "../theme/typography.stylex"; ··· 74 75 isOpen, 75 76 onOpenChange, 76 77 }: AlertDialogProps) => { 78 + const { trigger: triggerHaptic } = useHaptics(); 77 79 const dialogStyles = useDialogStyles({ size: "sm" }); 78 80 81 + const handleOpenChange = (open: boolean) => { 82 + triggerHaptic("impactLight"); 83 + onOpenChange?.(open); 84 + }; 85 + 79 86 return ( 80 87 <DialogTrigger 81 88 defaultOpen={defaultOpen} 82 89 isOpen={isOpen} 83 - onOpenChange={onOpenChange} 90 + onOpenChange={handleOpenChange} 84 91 > 85 92 {trigger} 86 93
+5
packages/hip-ui/src/components/button/button-config.ts
··· 4 4 name: "button", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../theme/color.stylex.tsx", 8 12 "../theme/spacing.stylex.tsx", 9 13 "../theme/radius.stylex.tsx", ··· 47 51 ], 48 52 dependencies: { 49 53 "react-aria-components": "^1.13.0", 54 + "web-haptics": "^0.0.6", 50 55 }, 51 56 };
+11
packages/hip-ui/src/components/button/index.tsx
··· 7 7 8 8 import type { ButtonVariant, Size, StyleXComponentProps } from "../theme/types"; 9 9 10 + import { useHaptics } from "../haptics"; 10 11 import { ProgressCircle } from "../progress-circle"; 11 12 import { animationDuration } from "../theme/animations.stylex"; 12 13 import { spacing } from "../theme/spacing.stylex"; ··· 49 50 size, 50 51 isPending = false, 51 52 isDisabled, 53 + onPress, 52 54 ...props 53 55 }: ButtonProps) => { 56 + const { trigger } = useHaptics(); 54 57 const buttonStyles = useButtonStyles({ variant, size }); 55 58 const isHref = "href" in props; 56 59 60 + const handlePress = (e: Parameters<NonNullable<typeof onPress>>[0]) => { 61 + if (variant === "primary" && !isDisabled && !isPending) { 62 + trigger("impactMedium"); 63 + } 64 + onPress?.(e); 65 + }; 66 + 57 67 return ( 58 68 <AriaButton 59 69 {...props} 70 + onPress={handlePress} 60 71 {...stylex.props(buttonStyles, isHref && styles.link, style)} 61 72 data-size={size} 62 73 data-pending={isPending || undefined}
+9 -1
packages/hip-ui/src/components/dialog/dialog-config.ts
··· 3 3 export const dialogConfig: ComponentConfig = { 4 4 name: "dialog", 5 5 filepath: "./index.tsx", 6 - hipDependencies: ["../context.ts", "../theme/useDialogStyles.ts"], 6 + hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 11 + "../context.ts", 12 + "../theme/useDialogStyles.ts", 13 + ], 7 14 dependencies: { 8 15 "lucide-react": "^0.545.0", 16 + "web-haptics": "^0.0.6", 9 17 "@react-stately/utils": "^3.10.8", 10 18 "react-stately": "^3.42.0", 11 19 },
+8 -1
packages/hip-ui/src/components/dialog/index.tsx
··· 14 14 15 15 import type { Size, StyleXComponentProps } from "../theme/types"; 16 16 17 + import { useHaptics } from "../haptics"; 17 18 import { IconButton } from "../icon-button"; 18 19 import { uiColor } from "../theme/color.stylex"; 19 20 import { spacing } from "../theme/spacing.stylex"; ··· 87 88 onOpenChange, 88 89 size, 89 90 }: DialogProps) => { 91 + const { trigger: triggerHaptic } = useHaptics(); 90 92 const dialogStyles = useDialogStyles({ size }); 91 93 94 + const handleOpenChange = (open: boolean) => { 95 + triggerHaptic("impactLight"); 96 + onOpenChange?.(open); 97 + }; 98 + 92 99 return ( 93 100 <DialogTrigger 94 101 defaultOpen={defaultOpen} 95 102 isOpen={isOpen} 96 - onOpenChange={onOpenChange} 103 + onOpenChange={handleOpenChange} 97 104 > 98 105 {trigger} 99 106
+5
packages/hip-ui/src/components/drawer/drawer-config.ts
··· 4 4 name: "drawer", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../context.ts", 8 12 "../theme/useDialogStyles.ts", 9 13 "../theme/spacing.stylex.tsx", ··· 15 19 dependencies: { 16 20 "react-aria-components": "^1.13.0", 17 21 "lucide-react": "^0.545.0", 22 + "web-haptics": "^0.0.6", 18 23 }, 19 24 };
+8 -1
packages/hip-ui/src/components/drawer/index.tsx
··· 14 14 15 15 import type { Size, StyleXComponentProps } from "../theme/types"; 16 16 17 + import { useHaptics } from "../haptics"; 17 18 import { IconButton } from "../icon-button"; 18 19 import { 19 20 animationDuration, ··· 173 174 direction = "right", 174 175 isNonModal = false, 175 176 }: DrawerProps) => { 177 + const { trigger: triggerHaptic } = useHaptics(); 176 178 const dialogStyles = useDialogStyles({ size }); 177 179 180 + const handleOpenChange = (open: boolean) => { 181 + triggerHaptic("impactLight"); 182 + onOpenChange?.(open); 183 + }; 184 + 178 185 return ( 179 186 <DialogTrigger 180 187 defaultOpen={defaultOpen} 181 188 isOpen={isOpen} 182 - onOpenChange={onOpenChange} 189 + onOpenChange={handleOpenChange} 183 190 > 184 191 {trigger} 185 192
+51
packages/hip-ui/src/components/haptics/context.tsx
··· 1 + "use client"; 2 + 3 + import { createContext, useCallback } from "react"; 4 + import { useWebHaptics } from "web-haptics/react"; 5 + 6 + import type { HapticIntent } from "./haptics"; 7 + 8 + import { 9 + HAPTIC_PRESET_MAP, 10 + isHapticsEnabled, 11 + setHapticsEnabled, 12 + } from "./haptics"; 13 + 14 + export interface HapticsContextValue { 15 + enabled: boolean; 16 + setEnabled: (enabled: boolean) => void; 17 + trigger: (intent: HapticIntent) => void; 18 + } 19 + 20 + /* eslint-disable react/only-export-components -- HapticsContext is consumed by useHaptics */ 21 + export const HapticsContext = createContext<HapticsContextValue | null>(null); 22 + /* eslint-enable react/only-export-components */ 23 + 24 + /** 25 + * Provider for haptics enable/disable state. 26 + * Uses useWebHaptics under the hood; children use useHaptics() to trigger feedback. 27 + */ 28 + export function HapticsProvider({ children }: { children: React.ReactNode }) { 29 + const { trigger: webTrigger } = useWebHaptics(); 30 + 31 + const trigger = useCallback( 32 + (intent: HapticIntent) => { 33 + if (!isHapticsEnabled()) return; 34 + const preset = HAPTIC_PRESET_MAP[intent]; 35 + webTrigger(preset); 36 + }, 37 + [webTrigger], 38 + ); 39 + 40 + const value: HapticsContextValue = { 41 + enabled: isHapticsEnabled(), 42 + setEnabled: useCallback((enabled: boolean) => { 43 + setHapticsEnabled(enabled); 44 + }, []), 45 + trigger, 46 + }; 47 + 48 + return ( 49 + <HapticsContext.Provider value={value}>{children}</HapticsContext.Provider> 50 + ); 51 + }
+10
packages/hip-ui/src/components/haptics/haptics-config.ts
··· 1 + import type { ComponentConfig } from "../../types"; 2 + 3 + export const hapticsConfig: ComponentConfig = { 4 + name: "haptics", 5 + filepath: "./index.ts", 6 + hipDependencies: ["./haptics.ts", "./context.tsx", "./useHaptics.ts"], 7 + dependencies: { 8 + "web-haptics": "^0.0.6", 9 + }, 10 + };
+44
packages/hip-ui/src/components/haptics/haptics.ts
··· 1 + /** 2 + * Semantic haptic intents aligned with Apple Human Interface Guidelines. 3 + * Maps to web-haptics preset names; no-ops gracefully when unsupported. 4 + */ 5 + 6 + export type HapticIntent = 7 + | "selection" 8 + | "impactLight" 9 + | "impactMedium" 10 + | "impactHeavy" 11 + | "success" 12 + | "warning" 13 + | "error"; 14 + 15 + /** Maps semantic intents to web-haptics preset names. */ 16 + export const HAPTIC_PRESET_MAP: Record< 17 + HapticIntent, 18 + "selection" | "light" | "medium" | "heavy" | "success" | "warning" | "error" 19 + > = { 20 + selection: "selection", 21 + impactLight: "light", 22 + impactMedium: "medium", 23 + impactHeavy: "heavy", 24 + success: "success", 25 + warning: "warning", 26 + error: "error", 27 + }; 28 + 29 + let hapticsEnabled = true; 30 + 31 + /** 32 + * Enable or disable haptic feedback globally. 33 + * @param enabled - Whether haptics should fire (default: true) 34 + */ 35 + export function setHapticsEnabled(enabled: boolean): void { 36 + hapticsEnabled = enabled; 37 + } 38 + 39 + /** 40 + * Check if haptics are currently enabled. 41 + */ 42 + export function isHapticsEnabled(): boolean { 43 + return hapticsEnabled; 44 + }
+7
packages/hip-ui/src/components/haptics/index.ts
··· 1 + export { 2 + setHapticsEnabled, 3 + isHapticsEnabled, 4 + type HapticIntent, 5 + } from "./haptics"; 6 + export { HapticsContext, HapticsProvider } from "./context"; 7 + export { useHaptics } from "./useHaptics";
+44
packages/hip-ui/src/components/haptics/useHaptics.ts
··· 1 + "use client"; 2 + 3 + import { useCallback, useContext } from "react"; 4 + import { useWebHaptics } from "web-haptics/react"; 5 + 6 + import type { HapticIntent } from "./haptics"; 7 + 8 + import { HapticsContext } from "./context"; 9 + import { 10 + HAPTIC_PRESET_MAP, 11 + isHapticsEnabled, 12 + setHapticsEnabled, 13 + } from "./haptics"; 14 + 15 + interface HapticsContextValue { 16 + enabled: boolean; 17 + setEnabled: (enabled: boolean) => void; 18 + trigger: (intent: HapticIntent) => void; 19 + } 20 + 21 + /** 22 + * Access haptics API via useWebHaptics. Returns context value when inside HapticsProvider. 23 + */ 24 + export function useHaptics(): HapticsContextValue { 25 + const context = useContext(HapticsContext); 26 + const { trigger: webTrigger } = useWebHaptics(); 27 + 28 + const fallbackTrigger = useCallback( 29 + (intent: HapticIntent) => { 30 + if (!isHapticsEnabled()) return; 31 + const preset = HAPTIC_PRESET_MAP[intent]; 32 + webTrigger(preset); 33 + }, 34 + [webTrigger], 35 + ); 36 + 37 + if (context) return context; 38 + 39 + return { 40 + enabled: isHapticsEnabled(), 41 + setEnabled: setHapticsEnabled, 42 + trigger: fallbackTrigger, 43 + }; 44 + }
+23 -1
packages/hip-ui/src/components/listbox/index.tsx
··· 23 23 24 24 import { Checkbox } from "../checkbox"; 25 25 import { SizeContext } from "../context"; 26 + import { useHaptics } from "../haptics"; 26 27 import { Separator } from "../separator"; 27 28 import { ui } from "../theme/semantic-color.stylex"; 28 29 import { spacing } from "../theme/spacing.stylex"; ··· 76 77 style, 77 78 variant = "default", 78 79 isVirtualized = false, 80 + onSelectionChange, 81 + onAction, 79 82 ...props 80 83 }: ListBoxProps<T>) { 84 + const { trigger } = useHaptics(); 81 85 const size = sizeProp || use(SizeContext); 86 + 87 + const handleSelectionChange = ( 88 + keys: Parameters<NonNullable<typeof onSelectionChange>>[0], 89 + ) => { 90 + trigger("selection"); 91 + onSelectionChange?.(keys); 92 + }; 93 + 94 + const handleAction = (key: Parameters<NonNullable<typeof onAction>>[0]) => { 95 + trigger("selection"); 96 + onAction?.(key); 97 + }; 98 + 82 99 const listbox = ( 83 - <AriaListBox {...props} {...stylex.props(styles.listBox, style)} /> 100 + <AriaListBox 101 + {...props} 102 + onSelectionChange={handleSelectionChange} 103 + onAction={handleAction} 104 + {...stylex.props(styles.listBox, style)} 105 + /> 84 106 ); 85 107 86 108 return (
+10 -1
packages/hip-ui/src/components/listbox/listbox-config.ts
··· 3 3 export const listboxConfig: ComponentConfig = { 4 4 name: "listbox", 5 5 filepath: "./index.tsx", 6 - hipDependencies: ["../context.ts"], 6 + hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 11 + "../context.ts", 12 + ], 13 + dependencies: { 14 + "web-haptics": "^0.0.6", 15 + }, 7 16 };
+19 -2
packages/hip-ui/src/components/menu/index.tsx
··· 22 22 import type { Size, StyleXComponentProps } from "../theme/types"; 23 23 24 24 import { SizeContext } from "../context"; 25 + import { useHaptics } from "../haptics"; 25 26 import { ListBoxSeparator } from "../listbox"; 26 27 import { spacing } from "../theme/spacing.stylex"; 27 28 import { useListBoxItemStyles } from "../theme/useListBoxItemStyles"; ··· 78 79 placement, 79 80 header, 80 81 footer, 82 + onAction, 81 83 ...props 82 84 }: MenuProps<T>) { 85 + const { trigger: triggerHaptic } = useHaptics(); 83 86 const popoverStyles = usePopoverStyles(); 84 87 const size = sizeProp || use(SizeContext); 85 88 89 + const handleOpenChange = (open: boolean) => { 90 + triggerHaptic("impactLight"); 91 + onOpenChange?.(open); 92 + }; 93 + 94 + const handleAction = (key: Parameters<NonNullable<typeof onAction>>[0]) => { 95 + triggerHaptic("selection"); 96 + onAction?.(key); 97 + }; 98 + 86 99 return ( 87 100 <SizeContext value={size}> 88 101 <MenuTrigger 89 102 defaultOpen={defaultOpen} 90 103 isOpen={isOpen} 91 - onOpenChange={onOpenChange} 104 + onOpenChange={handleOpenChange} 92 105 > 93 106 {trigger} 94 107 <Popover ··· 105 118 <ListBoxSeparator /> 106 119 </> 107 120 )} 108 - <AriaMenu {...props} {...stylex.props(styles.menu)} /> 121 + <AriaMenu 122 + {...props} 123 + onAction={handleAction} 124 + {...stylex.props(styles.menu)} 125 + /> 109 126 {Boolean(footer) && ( 110 127 <> 111 128 <ListBoxSeparator />
+7
packages/hip-ui/src/components/menu/menu-config.ts
··· 4 4 name: "menu", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../context.ts", 8 12 "../theme/usePopoverStyles.ts", 9 13 "../theme/useListBoxItemStyles.ts", 10 14 ], 15 + dependencies: { 16 + "web-haptics": "^0.0.6", 17 + }, 11 18 };
-2
packages/hip-ui/src/components/page/Page.tsx
··· 1 1 "use client"; 2 2 3 - import type { LinkProps as AriaLinkProps } from "react-aria-components"; 4 - 5 3 import * as stylex from "@stylexjs/stylex"; 6 4 import { ArrowLeft } from "lucide-react"; 7 5 import { useEffect, useRef, useState } from "react";
+12 -1
packages/hip-ui/src/components/popover/index.tsx
··· 15 15 16 16 import type { StyleXComponentProps } from "../theme/types"; 17 17 18 + import { useHaptics } from "../haptics"; 18 19 import { uiColor } from "../theme/color.stylex"; 19 20 import { spacing } from "../theme/spacing.stylex"; 20 21 import { usePopoverStyles } from "../theme/usePopoverStyles"; ··· 70 71 hasArrow, 71 72 ...popoverProps 72 73 }: PopoverProps) => { 74 + const { trigger: triggerHaptic } = useHaptics(); 73 75 const popoverStyles = usePopoverStyles(); 76 + 77 + const handleOpenChange = (open: boolean) => { 78 + triggerHaptic("impactLight"); 79 + onOpenChange?.(open); 80 + }; 74 81 75 82 return ( 76 83 <DialogTrigger 77 - {...({ isOpen, onOpenChange, defaultOpen } as DialogTriggerProps)} 84 + {...({ 85 + isOpen, 86 + onOpenChange: handleOpenChange, 87 + defaultOpen, 88 + } as DialogTriggerProps)} 78 89 > 79 90 {trigger} 80 91
+5
packages/hip-ui/src/components/popover/popover-config.ts
··· 4 4 name: "popover", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../theme/spacing.stylex.tsx", 8 12 "../theme/radius.stylex.tsx", 9 13 "../theme/semantic-color.stylex.tsx", ··· 12 16 ], 13 17 dependencies: { 14 18 "react-aria-components": "^1.13.0", 19 + "web-haptics": "^0.0.6", 15 20 }, 16 21 };
+11
packages/hip-ui/src/components/segmented-control/index.tsx
··· 16 16 import type { Size, StyleXComponentProps } from "../theme/types"; 17 17 18 18 import { SizeContext } from "../context"; 19 + import { useHaptics } from "../haptics"; 19 20 import { animationDuration } from "../theme/animations.stylex"; 20 21 import { uiColor } from "../theme/color.stylex"; 21 22 import { mediaQueries } from "../theme/media-queries.stylex"; ··· 119 120 children, 120 121 style, 121 122 size: sizeProp, 123 + onSelectionChange, 122 124 ...props 123 125 }: SegmentedControlProps) => { 126 + const { trigger } = useHaptics(); 124 127 const size = sizeProp ?? use(SizeContext); 125 128 129 + const handleSelectionChange = ( 130 + keys: Parameters<NonNullable<typeof onSelectionChange>>[0], 131 + ) => { 132 + trigger("selection"); 133 + onSelectionChange?.(keys); 134 + }; 135 + 126 136 return ( 127 137 <AriaToggleButtonGroup 128 138 disallowEmptySelection 129 139 selectionMode="single" 130 140 data-size={size} 141 + onSelectionChange={handleSelectionChange} 131 142 {...props} 132 143 {...stylex.props(styles.group, style)} 133 144 >
+8 -1
packages/hip-ui/src/components/segmented-control/segmented-control-config.ts
··· 3 3 export const segmentedControlConfig: ComponentConfig = { 4 4 name: "segmented-control", 5 5 filepath: "./index.tsx", 6 - hipDependencies: ["../theme/animations.stylex.tsx"], 6 + hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 11 + "../theme/animations.stylex.tsx", 12 + ], 7 13 dependencies: { 8 14 "react-aria-components": "^1.13.0", 15 + "web-haptics": "^0.0.6", 9 16 }, 10 17 };
+14 -2
packages/hip-ui/src/components/switch/index.tsx
··· 5 5 6 6 import type { StyleXComponentProps } from "../theme/types"; 7 7 8 + import { useHaptics } from "../haptics"; 8 9 import { animationDuration } from "../theme/animations.stylex"; 9 10 import { primaryColor, uiColor } from "../theme/color.stylex"; 10 11 import { mediaQueries } from "../theme/media-queries.stylex"; ··· 86 87 | SwitchWithAriaLabelProps 87 88 | SwitchWithAriaLabelledbyProps; 88 89 89 - export function Switch({ children, style, ...props }: SwitchProps) { 90 + export function Switch({ children, style, onChange, ...props }: SwitchProps) { 91 + const { trigger } = useHaptics(); 92 + 93 + const handleChange = (isSelected: boolean) => { 94 + trigger("selection"); 95 + onChange?.(isSelected); 96 + }; 97 + 90 98 return ( 91 - <AriaSwitch {...props} {...stylex.props(styles.wrapper, style)}> 99 + <AriaSwitch 100 + {...props} 101 + onChange={handleChange} 102 + {...stylex.props(styles.wrapper, style)} 103 + > 92 104 <div {...stylex.props(styles.indicator)}> 93 105 <div {...stylex.props(styles.thumb)} /> 94 106 </div>
+7
packages/hip-ui/src/components/switch/switch-config.ts
··· 4 4 name: "switch", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../theme/spacing.stylex.tsx", 8 12 "../theme/radius.stylex.tsx", 9 13 "../theme/semantic-color.stylex.tsx", 10 14 "../theme/typography.stylex.tsx", 11 15 "../theme/shadow.stylex.tsx", 12 16 ], 17 + dependencies: { 18 + "web-haptics": "^0.0.6", 19 + }, 13 20 };
+11
packages/hip-ui/src/components/tabs/index.tsx
··· 20 20 import type { Size, StyleXComponentProps } from "../theme/types"; 21 21 22 22 import { SizeContext } from "../context"; 23 + import { useHaptics } from "../haptics"; 23 24 import { animationDuration } from "../theme/animations.stylex"; 24 25 import { primaryColor, uiColor } from "../theme/color.stylex"; 25 26 import { mediaQueries } from "../theme/media-queries.stylex"; ··· 200 201 style, 201 202 size: sizeProp, 202 203 orientation = "horizontal", 204 + onSelectionChange, 203 205 ...props 204 206 }: TabsProps) { 207 + const { trigger } = useHaptics(); 205 208 const size = sizeProp || use(SizeContext); 206 209 210 + const handleSelectionChange = ( 211 + key: Parameters<NonNullable<typeof onSelectionChange>>[0], 212 + ) => { 213 + trigger("selection"); 214 + onSelectionChange?.(key); 215 + }; 216 + 207 217 return ( 208 218 <SizeContext value={size}> 209 219 <AriaTabs 210 220 {...props} 221 + onSelectionChange={handleSelectionChange} 211 222 orientation={orientation} 212 223 data-size={size} 213 224 {...stylex.props(
+5
packages/hip-ui/src/components/tabs/tabs-config.ts
··· 4 4 name: "tabs", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../theme/animations.stylex.tsx", 8 12 "../theme/semantic-color.stylex.tsx", 9 13 "../theme/spacing.stylex.tsx", ··· 14 18 ], 15 19 dependencies: { 16 20 "react-aria-components": "^1.13.0", 21 + "web-haptics": "^0.0.6", 17 22 }, 18 23 };
+11
packages/hip-ui/src/components/toast/Toast.tsx
··· 7 7 8 8 import * as stylex from "@stylexjs/stylex"; 9 9 import { X } from "lucide-react"; 10 + import { useEffect } from "react"; 10 11 import { 11 12 UNSTABLE_ToastRegion as AriaToastRegion, 12 13 Text, ··· 18 19 import type { ToastContentType } from "./queue"; 19 20 20 21 import { Button } from "../button"; 22 + import { useHaptics } from "../haptics"; 21 23 import { IconButton } from "../icon-button"; 22 24 import { 23 25 criticalColor, ··· 115 117 }); 116 118 117 119 function ToastItem({ toast }: { toast: QueuedToast<ToastContentType> }) { 120 + const { trigger } = useHaptics(); 118 121 const popoverStyles = usePopoverStyles(); 122 + 123 + useEffect(() => { 124 + if (toast.content.variant === "success") { 125 + trigger("success"); 126 + } else if (toast.content.variant === "critical") { 127 + trigger("error"); 128 + } 129 + }, [toast.key, toast.content.variant, trigger]); 119 130 120 131 return ( 121 132 <Toast
+5
packages/hip-ui/src/components/toast/toast-config.ts
··· 4 4 name: "toast", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../theme/spacing.stylex.tsx", 8 12 "../theme/radius.stylex.tsx", 9 13 "../theme/semantic-color.stylex.tsx", ··· 17 21 dependencies: { 18 22 "react-aria-components": "^1.13.0", 19 23 "lucide-react": "^0.545.0", 24 + "web-haptics": "^0.0.6", 20 25 }, 21 26 };
+15 -1
packages/hip-ui/src/components/toggle-button-group/index.tsx
··· 9 9 import type { StyleXComponentProps } from "../theme/types"; 10 10 11 11 import { ButtonGroupContext } from "../button/context"; 12 + import { useHaptics } from "../haptics"; 12 13 import { spacing } from "../theme/spacing.stylex"; 13 14 14 15 const styles = stylex.create({ ··· 77 78 orientation: orientationProp = "horizontal", 78 79 variant = "grouped", 79 80 itemsPerRow, 81 + onSelectionChange, 80 82 ...props 81 83 }: ToggleButtonGroupProps) => { 84 + const { trigger } = useHaptics(); 82 85 const groupOrientation = use(ButtonGroupContext); 83 86 const isInGroup = groupOrientation?.orientation !== undefined; 84 87 const orientation = groupOrientation?.orientation || orientationProp; 88 + 89 + const handleSelectionChange = ( 90 + keys: Parameters<NonNullable<typeof onSelectionChange>>[0], 91 + ) => { 92 + trigger("selection"); 93 + onSelectionChange?.(keys); 94 + }; 85 95 86 96 let stylesToApply = []; 87 97 ··· 109 119 110 120 return ( 111 121 <ButtonGroupContext value={contextValue}> 112 - <AriaToggleButtonGroup {...props} {...stylex.props(stylesToApply, style)}> 122 + <AriaToggleButtonGroup 123 + {...props} 124 + onSelectionChange={handleSelectionChange} 125 + {...stylex.props(stylesToApply, style)} 126 + > 113 127 {children} 114 128 </AriaToggleButtonGroup> 115 129 </ButtonGroupContext>
+5
packages/hip-ui/src/components/toggle-button-group/toggle-button-group-config.ts
··· 4 4 name: "toggle-button-group", 5 5 filepath: "./index.tsx", 6 6 hipDependencies: [ 7 + "../haptics/haptics.ts", 8 + "../haptics/context.tsx", 9 + "../haptics/useHaptics.ts", 10 + "../haptics/index.ts", 7 11 "../theme/spacing.stylex.tsx", 8 12 "../theme/radius.stylex.tsx", 9 13 "../theme/semantic-color.stylex.tsx", ··· 14 18 ], 15 19 dependencies: { 16 20 "react-aria-components": "^1.13.0", 21 + "web-haptics": "^0.0.6", 17 22 }, 18 23 };
+28
pnpm-lock.yaml
··· 251 251 vite-tsconfig-paths: 252 252 specifier: ^5.1.4 253 253 version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) 254 + web-haptics: 255 + specifier: ^0.0.6 256 + version: 0.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 254 257 devDependencies: 255 258 '@content-collections/core': 256 259 specifier: ^0.11.1 ··· 512 515 remark-gfm: 513 516 specifier: ^4.0.0 514 517 version: 4.0.1 518 + web-haptics: 519 + specifier: ^0.0.6 520 + version: 0.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 515 521 devDependencies: 516 522 '@repo/eslint-config': 517 523 specifier: workspace:* ··· 8069 8075 watchpack@2.4.4: 8070 8076 resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} 8071 8077 engines: {node: '>=10.13.0'} 8078 + 8079 + web-haptics@0.0.6: 8080 + resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==} 8081 + peerDependencies: 8082 + react: '>=18' 8083 + react-dom: '>=18' 8084 + svelte: '>=4' 8085 + vue: '>=3' 8086 + peerDependenciesMeta: 8087 + react: 8088 + optional: true 8089 + react-dom: 8090 + optional: true 8091 + svelte: 8092 + optional: true 8093 + vue: 8094 + optional: true 8072 8095 8073 8096 web-vitals@5.1.0: 8074 8097 resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} ··· 17582 17605 dependencies: 17583 17606 glob-to-regexp: 0.4.1 17584 17607 graceful-fs: 4.2.11 17608 + 17609 + web-haptics@0.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): 17610 + optionalDependencies: 17611 + react: 19.2.0 17612 + react-dom: 19.2.0(react@19.2.0) 17585 17613 17586 17614 web-vitals@5.1.0: {} 17587 17615