@hoardodile/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +70 -0
- package/dist/components/alert.d.ts +13 -0
- package/dist/components/alert.js +97 -0
- package/dist/components/alert.js.map +1 -0
- package/dist/components/app-dialog.d.ts +62 -0
- package/dist/components/app-dialog.js +656 -0
- package/dist/components/app-dialog.js.map +1 -0
- package/dist/components/badge.d.ts +11 -0
- package/dist/components/badge.js +70 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/breadcrumb.js +617 -0
- package/dist/components/breadcrumb.js.map +1 -0
- package/dist/components/button.d.ts +17 -0
- package/dist/components/button.js +78 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/caption-bar.d.ts +44 -0
- package/dist/components/caption-bar.js +827 -0
- package/dist/components/caption-bar.js.map +1 -0
- package/dist/components/chart-card.d.ts +15 -0
- package/dist/components/chart-card.js +27 -0
- package/dist/components/chart-card.js.map +1 -0
- package/dist/components/checkbox.d.ts +6 -0
- package/dist/components/checkbox.js +76 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip-button.d.ts +28 -0
- package/dist/components/chip-button.js +87 -0
- package/dist/components/chip-button.js.map +1 -0
- package/dist/components/close-confirm-dialog.d.ts +19 -0
- package/dist/components/close-confirm-dialog.js +743 -0
- package/dist/components/close-confirm-dialog.js.map +1 -0
- package/dist/components/color-picker.d.ts +32 -0
- package/dist/components/color-picker.js +667 -0
- package/dist/components/color-picker.js.map +1 -0
- package/dist/components/combobox.d.ts +32 -0
- package/dist/components/combobox.js +1211 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/confirm-by-typing-dialog.d.ts +48 -0
- package/dist/components/confirm-by-typing-dialog.js +759 -0
- package/dist/components/confirm-by-typing-dialog.js.map +1 -0
- package/dist/components/confirm-dialog.d.ts +34 -0
- package/dist/components/confirm-dialog.js +710 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/count-badge.d.ts +13 -0
- package/dist/components/count-badge.js +39 -0
- package/dist/components/count-badge.js.map +1 -0
- package/dist/components/dialog.d.ts +40 -0
- package/dist/components/dialog.js +571 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +45 -0
- package/dist/components/dropdown-menu.js +1003 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/dropdown-select.d.ts +29 -0
- package/dist/components/dropdown-select.js +1036 -0
- package/dist/components/dropdown-select.js.map +1 -0
- package/dist/components/empty.d.ts +15 -0
- package/dist/components/empty.js +119 -0
- package/dist/components/empty.js.map +1 -0
- package/dist/components/filter-rail.d.ts +48 -0
- package/dist/components/filter-rail.js +231 -0
- package/dist/components/filter-rail.js.map +1 -0
- package/dist/components/fixed-action-bar.d.ts +24 -0
- package/dist/components/fixed-action-bar.js +48 -0
- package/dist/components/fixed-action-bar.js.map +1 -0
- package/dist/components/font-picker.d.ts +45 -0
- package/dist/components/font-picker.js +759 -0
- package/dist/components/font-picker.js.map +1 -0
- package/dist/components/form.d.ts +17 -0
- package/dist/components/form.js +150 -0
- package/dist/components/form.js.map +1 -0
- package/dist/components/group-label.d.ts +12 -0
- package/dist/components/group-label.js +10 -0
- package/dist/components/group-label.js.map +1 -0
- package/dist/components/icon-tile.d.ts +16 -0
- package/dist/components/icon-tile.js +67 -0
- package/dist/components/icon-tile.js.map +1 -0
- package/dist/components/icon-toggle.d.ts +24 -0
- package/dist/components/icon-toggle.js +81 -0
- package/dist/components/icon-toggle.js.map +1 -0
- package/dist/components/icon.d.ts +2 -0
- package/dist/components/icon.js +49 -0
- package/dist/components/icon.js.map +1 -0
- package/dist/components/image-crop-panel.d.ts +80 -0
- package/dist/components/image-crop-panel.js +1104 -0
- package/dist/components/image-crop-panel.js.map +1 -0
- package/dist/components/image-cropper.d.ts +63 -0
- package/dist/components/image-cropper.js +166 -0
- package/dist/components/image-cropper.js.map +1 -0
- package/dist/components/input-group.d.ts +22 -0
- package/dist/components/input-group.js +270 -0
- package/dist/components/input-group.js.map +1 -0
- package/dist/components/input.d.ts +16 -0
- package/dist/components/input.js +60 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.js +40 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/list-empty-row.d.ts +16 -0
- package/dist/components/list-empty-row.js +40 -0
- package/dist/components/list-empty-row.js.map +1 -0
- package/dist/components/management-empty.d.ts +14 -0
- package/dist/components/management-empty.js +40 -0
- package/dist/components/management-empty.js.map +1 -0
- package/dist/components/management-skeleton.d.ts +11 -0
- package/dist/components/management-skeleton.js +72 -0
- package/dist/components/management-skeleton.js.map +1 -0
- package/dist/components/meta-chip.d.ts +17 -0
- package/dist/components/meta-chip.js +49 -0
- package/dist/components/meta-chip.js.map +1 -0
- package/dist/components/mobile-drawer.d.ts +29 -0
- package/dist/components/mobile-drawer.js +331 -0
- package/dist/components/mobile-drawer.js.map +1 -0
- package/dist/components/page-header.d.ts +20 -0
- package/dist/components/page-header.js +44 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/page-scaffold.d.ts +34 -0
- package/dist/components/page-scaffold.js +157 -0
- package/dist/components/page-scaffold.js.map +1 -0
- package/dist/components/pagination-bar.d.ts +22 -0
- package/dist/components/pagination-bar.js +703 -0
- package/dist/components/pagination-bar.js.map +1 -0
- package/dist/components/pagination.d.ts +22 -0
- package/dist/components/pagination.js +680 -0
- package/dist/components/pagination.js.map +1 -0
- package/dist/components/panel-toolbar.d.ts +37 -0
- package/dist/components/panel-toolbar.js +781 -0
- package/dist/components/panel-toolbar.js.map +1 -0
- package/dist/components/pill-tabs.d.ts +33 -0
- package/dist/components/pill-tabs.js +66 -0
- package/dist/components/pill-tabs.js.map +1 -0
- package/dist/components/plugin-download-consent.d.ts +52 -0
- package/dist/components/plugin-download-consent.js +780 -0
- package/dist/components/plugin-download-consent.js.map +1 -0
- package/dist/components/popover.d.ts +26 -0
- package/dist/components/popover.js +343 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/progress.d.ts +10 -0
- package/dist/components/progress.js +103 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/query-state-view.d.ts +36 -0
- package/dist/components/query-state-view.js +17 -0
- package/dist/components/query-state-view.js.map +1 -0
- package/dist/components/radio-group.d.ts +8 -0
- package/dist/components/radio-group.js +60 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/scroll-area.d.ts +7 -0
- package/dist/components/scroll-area.js +80 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/search-field.d.ts +49 -0
- package/dist/components/search-field.js +586 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/secondary-stat.d.ts +13 -0
- package/dist/components/secondary-stat.js +56 -0
- package/dist/components/secondary-stat.js.map +1 -0
- package/dist/components/section-header.d.ts +23 -0
- package/dist/components/section-header.js +76 -0
- package/dist/components/section-header.js.map +1 -0
- package/dist/components/section-label.d.ts +20 -0
- package/dist/components/section-label.js +46 -0
- package/dist/components/section-label.js.map +1 -0
- package/dist/components/section-tabs.d.ts +47 -0
- package/dist/components/section-tabs.js +145 -0
- package/dist/components/section-tabs.js.map +1 -0
- package/dist/components/separator.d.ts +14 -0
- package/dist/components/separator.js +49 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +22 -0
- package/dist/components/sheet.js +469 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/sidebar.d.ts +69 -0
- package/dist/components/sidebar.js +1645 -0
- package/dist/components/sidebar.js.map +1 -0
- package/dist/components/skeleton.d.ts +8 -0
- package/dist/components/skeleton.js +41 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/slider.d.ts +6 -0
- package/dist/components/slider.js +75 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sortable-chip-list.d.ts +26 -0
- package/dist/components/sortable-chip-list.js +59 -0
- package/dist/components/sortable-chip-list.js.map +1 -0
- package/dist/components/spinner.d.ts +5 -0
- package/dist/components/spinner.js +65 -0
- package/dist/components/spinner.js.map +1 -0
- package/dist/components/surface.d.ts +13 -0
- package/dist/components/surface.js +51 -0
- package/dist/components/surface.js.map +1 -0
- package/dist/components/switch.d.ts +8 -0
- package/dist/components/switch.js +53 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +129 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/textarea.d.ts +15 -0
- package/dist/components/textarea.js +58 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/toast.d.ts +19 -0
- package/dist/components/toast.js +805 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle.d.ts +12 -0
- package/dist/components/toggle.js +63 -0
- package/dist/components/toggle.js.map +1 -0
- package/dist/components/tooltip.d.ts +9 -0
- package/dist/components/tooltip.js +82 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/hooks/use-dialog-footer-actions.d.ts +18 -0
- package/dist/hooks/use-dialog-footer-actions.js +11 -0
- package/dist/hooks/use-dialog-footer-actions.js.map +1 -0
- package/dist/hooks/use-form-field.d.ts +33 -0
- package/dist/hooks/use-form-field.js +36 -0
- package/dist/hooks/use-form-field.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +34 -0
- package/dist/hooks/use-mobile.js +29 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-pinch-pan.d.ts +56 -0
- package/dist/hooks/use-pinch-pan.js +223 -0
- package/dist/hooks/use-pinch-pan.js.map +1 -0
- package/dist/hooks/useMobileBackToClose.d.ts +61 -0
- package/dist/hooks/useMobileBackToClose.js +234 -0
- package/dist/hooks/useMobileBackToClose.js.map +1 -0
- package/dist/icon-BOgL5bpL.d.ts +85 -0
- package/dist/icons/actions.d.ts +4 -0
- package/dist/icons/actions.js +520 -0
- package/dist/icons/actions.js.map +1 -0
- package/dist/icons/icon-style.d.ts +2 -0
- package/dist/icons/icon-style.js +51 -0
- package/dist/icons/icon-style.js.map +1 -0
- package/dist/icons/marks.d.ts +15 -0
- package/dist/icons/marks.js +48 -0
- package/dist/icons/marks.js.map +1 -0
- package/dist/icons/registry.d.ts +177 -0
- package/dist/icons/registry.js +1180 -0
- package/dist/icons/registry.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +653 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/merge-config.d.ts +15 -0
- package/dist/lib/merge-config.js +20 -0
- package/dist/lib/merge-config.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.js +26 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +26 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles/theme.css +480 -0
- package/dist/styles/theme.css.map +1 -0
- package/dist/styles/theme.d.ts +2 -0
- package/dist/viewport.d.ts +64 -0
- package/dist/viewport.js +13 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +122 -0
- package/src/components/alert.tsx +76 -0
- package/src/components/app-dialog.tsx +187 -0
- package/src/components/badge.tsx +52 -0
- package/src/components/breadcrumb.tsx +129 -0
- package/src/components/button.tsx +70 -0
- package/src/components/caption-bar.test.tsx +151 -0
- package/src/components/caption-bar.tsx +392 -0
- package/src/components/chart-card.tsx +31 -0
- package/src/components/checkbox.tsx +28 -0
- package/src/components/chip-button.tsx +69 -0
- package/src/components/close-confirm-dialog.tsx +99 -0
- package/src/components/color-picker.tsx +186 -0
- package/src/components/combobox.tsx +335 -0
- package/src/components/confirm-by-typing-dialog.tsx +130 -0
- package/src/components/confirm-dialog.test.tsx +39 -0
- package/src/components/confirm-dialog.tsx +98 -0
- package/src/components/count-badge.tsx +23 -0
- package/src/components/dialog.tsx +315 -0
- package/src/components/dropdown-menu.tsx +311 -0
- package/src/components/dropdown-select.tsx +211 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/filter-rail.tsx +139 -0
- package/src/components/fixed-action-bar.tsx +46 -0
- package/src/components/font-picker.tsx +257 -0
- package/src/components/form.tsx +127 -0
- package/src/components/group-label.tsx +13 -0
- package/src/components/icon-tile.tsx +35 -0
- package/src/components/icon-toggle.tsx +57 -0
- package/src/components/icon.tsx +66 -0
- package/src/components/image-crop-panel.test.tsx +167 -0
- package/src/components/image-crop-panel.tsx +465 -0
- package/src/components/image-cropper.test.tsx +144 -0
- package/src/components/image-cropper.tsx +265 -0
- package/src/components/input-group.tsx +156 -0
- package/src/components/input.tsx +45 -0
- package/src/components/label.tsx +20 -0
- package/src/components/list-empty-row.tsx +27 -0
- package/src/components/management-empty.tsx +25 -0
- package/src/components/management-skeleton.tsx +23 -0
- package/src/components/meta-chip.tsx +40 -0
- package/src/components/mobile-drawer.test.tsx +52 -0
- package/src/components/mobile-drawer.tsx +98 -0
- package/src/components/page-header.tsx +47 -0
- package/src/components/page-scaffold.test.tsx +24 -0
- package/src/components/page-scaffold.tsx +131 -0
- package/src/components/pagination-bar.test.tsx +87 -0
- package/src/components/pagination-bar.tsx +139 -0
- package/src/components/pagination.tsx +139 -0
- package/src/components/panel-toolbar.tsx +99 -0
- package/src/components/pill-tabs.test.tsx +84 -0
- package/src/components/pill-tabs.tsx +77 -0
- package/src/components/plugin-download-consent.test.tsx +90 -0
- package/src/components/plugin-download-consent.tsx +183 -0
- package/src/components/popover.tsx +133 -0
- package/src/components/progress.tsx +88 -0
- package/src/components/query-state-view.tsx +47 -0
- package/src/components/radio-group.tsx +36 -0
- package/src/components/scroll-area.tsx +52 -0
- package/src/components/search-field.test.tsx +86 -0
- package/src/components/search-field.tsx +119 -0
- package/src/components/secondary-stat.tsx +24 -0
- package/src/components/section-header.tsx +54 -0
- package/src/components/section-label.tsx +40 -0
- package/src/components/section-tabs.test.tsx +154 -0
- package/src/components/section-tabs.tsx +190 -0
- package/src/components/separator.tsx +38 -0
- package/src/components/sheet.tsx +170 -0
- package/src/components/sidebar.tsx +725 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/slider.tsx +52 -0
- package/src/components/sortable-chip-list.tsx +56 -0
- package/src/components/spinner.tsx +35 -0
- package/src/components/surface.tsx +37 -0
- package/src/components/switch.tsx +30 -0
- package/src/components/table.tsx +116 -0
- package/src/components/textarea.tsx +41 -0
- package/src/components/toast.tsx +239 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/tooltip.tsx +64 -0
- package/src/hooks/use-dialog-footer-actions.ts +23 -0
- package/src/hooks/use-form-field.ts +62 -0
- package/src/hooks/use-mobile.ts +54 -0
- package/src/hooks/use-pinch-pan.test.tsx +42 -0
- package/src/hooks/use-pinch-pan.ts +340 -0
- package/src/hooks/useMobileBackToClose.test.tsx +140 -0
- package/src/hooks/useMobileBackToClose.ts +508 -0
- package/src/icons/actions.ts +10 -0
- package/src/icons/icon-style.test.tsx +69 -0
- package/src/icons/icon-style.ts +104 -0
- package/src/icons/marks.tsx +72 -0
- package/src/icons/registry.test.ts +65 -0
- package/src/icons/registry.ts +1163 -0
- package/src/index.ts +22 -0
- package/src/lib/merge-config.test.ts +109 -0
- package/src/lib/merge-config.ts +22 -0
- package/src/lib/pagination.test.ts +55 -0
- package/src/lib/pagination.ts +42 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/scan-contract.test.ts +74 -0
- package/src/styles/theme.css +798 -0
- package/src/test/i18n.tsx +22 -0
- package/src/test/setup.ts +104 -0
- package/src/viewport.ts +69 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
4
|
+
|
|
5
|
+
function TooltipProvider({
|
|
6
|
+
delay = 0,
|
|
7
|
+
...props
|
|
8
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<TooltipPrimitive.Provider
|
|
11
|
+
data-slot="tooltip-provider"
|
|
12
|
+
delay={delay}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
19
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
23
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TooltipContent({
|
|
27
|
+
className,
|
|
28
|
+
side = "top",
|
|
29
|
+
sideOffset = 4,
|
|
30
|
+
align = "center",
|
|
31
|
+
alignOffset = 0,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: TooltipPrimitive.Popup.Props &
|
|
35
|
+
Pick<
|
|
36
|
+
TooltipPrimitive.Positioner.Props,
|
|
37
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
38
|
+
>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Positioner
|
|
42
|
+
align={align}
|
|
43
|
+
alignOffset={alignOffset}
|
|
44
|
+
side={side}
|
|
45
|
+
sideOffset={sideOffset}
|
|
46
|
+
className="isolate z-50"
|
|
47
|
+
>
|
|
48
|
+
<TooltipPrimitive.Popup
|
|
49
|
+
data-slot="tooltip-content"
|
|
50
|
+
className={cn(
|
|
51
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
58
|
+
</TooltipPrimitive.Popup>
|
|
59
|
+
</TooltipPrimitive.Positioner>
|
|
60
|
+
</TooltipPrimitive.Portal>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Footer action slot plumbing for `AppDialog` — the context shared
|
|
5
|
+
* between the dialog shell (`@hoardodile/ui/components/app-dialog`) and
|
|
6
|
+
* body panels that contribute their primary action through
|
|
7
|
+
* `DialogFooterActions`, so action buttons land in the footer instead of
|
|
8
|
+
* the body.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type DialogFooterActionsValue = {
|
|
12
|
+
readonly setFooterActions: (node: ReactNode | null) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DialogFooterActionsContext =
|
|
16
|
+
createContext<DialogFooterActionsValue | null>(null)
|
|
17
|
+
|
|
18
|
+
/** The nearest dialog's footer action slot, or null outside a dialog. */
|
|
19
|
+
function useDialogFooterActions(): DialogFooterActionsValue | null {
|
|
20
|
+
return useContext(DialogFooterActionsContext)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { DialogFooterActionsContext, useDialogFooterActions }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import {
|
|
3
|
+
type FieldPath,
|
|
4
|
+
type FieldValues,
|
|
5
|
+
useFormContext,
|
|
6
|
+
useFormState,
|
|
7
|
+
} from "react-hook-form"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Field-state plumbing shared by the `Form*` components in
|
|
11
|
+
* `@hoardodile/ui/components/form`: connects a `<FormField>` (the
|
|
12
|
+
* react-hook-form `Controller` wrapper) to its `<FormItem>` via two
|
|
13
|
+
* private contexts and exposes the resolved field state — id parts and
|
|
14
|
+
* error — to the label/control/description/message render pieces.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type FormFieldContextValue<
|
|
18
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
19
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
20
|
+
> = {
|
|
21
|
+
name: TName
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FormFieldContext = React.createContext<FormFieldContextValue | undefined>(
|
|
25
|
+
undefined,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
type FormItemContextValue = {
|
|
29
|
+
id: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const FormItemContext = React.createContext<FormItemContextValue | undefined>(
|
|
33
|
+
undefined,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
function useFormField() {
|
|
37
|
+
const fieldContext = React.useContext(FormFieldContext)
|
|
38
|
+
const itemContext = React.useContext(FormItemContext)
|
|
39
|
+
const { getFieldState } = useFormContext()
|
|
40
|
+
const formState = useFormState({ name: fieldContext?.name })
|
|
41
|
+
|
|
42
|
+
if (!fieldContext) {
|
|
43
|
+
throw new Error("useFormField should be used within <FormField>")
|
|
44
|
+
}
|
|
45
|
+
if (!itemContext) {
|
|
46
|
+
throw new Error("useFormField should be used within <FormItem>")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fieldState = getFieldState(fieldContext.name, formState)
|
|
50
|
+
const { id } = itemContext
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id,
|
|
54
|
+
name: fieldContext.name,
|
|
55
|
+
formItemId: `${id}-form-item`,
|
|
56
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
57
|
+
formMessageId: `${id}-form-item-message`,
|
|
58
|
+
...fieldState,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { FormFieldContext, FormItemContext, useFormField }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MOBILE_QUERY,
|
|
3
|
+
PANEL_QUERY,
|
|
4
|
+
SIDEBAR_QUERY,
|
|
5
|
+
} from "@hoardodile/ui/viewport"
|
|
6
|
+
import { useMedia } from "react-use"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One-shot check for viewports below the `md` breakpoint (< 768 px).
|
|
10
|
+
*
|
|
11
|
+
* Prefer {@link useBelowMd} in React components so they re-render when the
|
|
12
|
+
* viewport crosses the breakpoint. Use this helper only in event handlers,
|
|
13
|
+
* utility functions, or effects where a reactive hook is not appropriate.
|
|
14
|
+
*/
|
|
15
|
+
export function isBelowMd(): boolean {
|
|
16
|
+
if (typeof window === "undefined") return false
|
|
17
|
+
return window.matchMedia(MOBILE_QUERY).matches
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useBelowMd(): boolean {
|
|
21
|
+
return useMedia(MOBILE_QUERY, false)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reactive check for viewports below the app-shell sidebar breakpoint
|
|
26
|
+
* (`max-sidebar`), where the fixed sidebar is replaced by the hamburger
|
|
27
|
+
* drawer. Keep in sync with `SIDEBAR_BREAKPOINT_PX` and the
|
|
28
|
+
* `sidebar:` / `max-sidebar:` CSS variants. The shell alone decides its own chrome;
|
|
29
|
+
* compact-UI consumers keep using {@link useBelowMd}.
|
|
30
|
+
*/
|
|
31
|
+
export function useBelowSidebar(): boolean {
|
|
32
|
+
return useMedia(SIDEBAR_QUERY, false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reactive check for viewports below the filter-rail breakpoint
|
|
37
|
+
* (< 1440 px), where the rail becomes a drawer instead of a fixed right
|
|
38
|
+
* column. Keep in sync with `PANEL_BREAKPOINT_PX` and the
|
|
39
|
+
* `panel:` / `max-panel:` CSS variants.
|
|
40
|
+
*/
|
|
41
|
+
export function useBelowPanel(): boolean {
|
|
42
|
+
return useMedia(PANEL_QUERY, false)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compatibility alias for official shadcn-generated components (e.g.
|
|
47
|
+
* `sidebar.tsx`), which import `useIsMobile` from this module. Mobile
|
|
48
|
+
* detection rules are owned by THIS file for the whole repo (apps and
|
|
49
|
+
* plugins alike) — never let the registry version of `use-mobile.ts`
|
|
50
|
+
* replace it; restore after every `shadcn add`.
|
|
51
|
+
*/
|
|
52
|
+
export function useIsMobile(): boolean {
|
|
53
|
+
return useBelowMd()
|
|
54
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { describe, expect, test } from "vitest"
|
|
3
|
+
import {
|
|
4
|
+
clampViewportScale,
|
|
5
|
+
isTapGesture,
|
|
6
|
+
panViewport,
|
|
7
|
+
type ViewportTransform,
|
|
8
|
+
zoomViewportAt,
|
|
9
|
+
} from "./use-pinch-pan"
|
|
10
|
+
|
|
11
|
+
const RANGE = { minScale: 0.5, maxScale: 4 }
|
|
12
|
+
|
|
13
|
+
describe("viewport transform math", () => {
|
|
14
|
+
test("clamps zoom into the viewer range", () => {
|
|
15
|
+
expect(clampViewportScale(8, RANGE)).toBe(4)
|
|
16
|
+
expect(clampViewportScale(0.1, RANGE)).toBe(0.5)
|
|
17
|
+
expect(clampViewportScale(1.5, RANGE)).toBe(1.5)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("pans by screen-space pixels", () => {
|
|
21
|
+
const start: ViewportTransform = { x: 10, y: -4, scale: 2 }
|
|
22
|
+
expect(panViewport(start, { x: 5, y: 7 })).toEqual({
|
|
23
|
+
x: 15,
|
|
24
|
+
y: 3,
|
|
25
|
+
scale: 2,
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("zooms while keeping the anchor point fixed", () => {
|
|
30
|
+
const start: ViewportTransform = { x: 10, y: 20, scale: 1 }
|
|
31
|
+
expect(zoomViewportAt(start, { x: 40, y: -10 }, 2, RANGE)).toEqual({
|
|
32
|
+
x: -30,
|
|
33
|
+
y: 30,
|
|
34
|
+
scale: 2,
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("a move within the threshold is a tap", () => {
|
|
39
|
+
expect(isTapGesture({ x: 0, y: 0 }, { x: 5, y: 5 }, 8)).toBe(true)
|
|
40
|
+
expect(isTapGesture({ x: 0, y: 0 }, { x: 8, y: 0 }, 8)).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useRef, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared pinch/pan viewport gesture for media viewers. One finger pans,
|
|
5
|
+
* two fingers zoom around their midpoint, a quick tap reports a click,
|
|
6
|
+
* and a second quick tap reports a double tap. The transform is
|
|
7
|
+
* `translate(x, y) scale(scale)` with the target's center as origin.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ViewportTransform = {
|
|
11
|
+
readonly x: number
|
|
12
|
+
readonly y: number
|
|
13
|
+
readonly scale: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ViewportPoint = {
|
|
17
|
+
readonly x: number
|
|
18
|
+
readonly y: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PinchPanOptions = {
|
|
22
|
+
readonly target: RefObject<HTMLElement | null>
|
|
23
|
+
readonly initial?: ViewportTransform
|
|
24
|
+
readonly minScale?: number
|
|
25
|
+
readonly maxScale?: number
|
|
26
|
+
/** Max travel for a tap before it becomes a drag, in CSS pixels. */
|
|
27
|
+
readonly tapThreshold?: number
|
|
28
|
+
/** When this value changes, the viewport returns to `initial`. */
|
|
29
|
+
readonly resetKey?: string | number
|
|
30
|
+
readonly onChange?: (next: ViewportTransform) => void
|
|
31
|
+
readonly onTap?: (point: ViewportPoint) => void
|
|
32
|
+
readonly onDoubleTap?: (point: ViewportPoint) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_TRANSFORM: ViewportTransform = { x: 0, y: 0, scale: 1 }
|
|
36
|
+
|
|
37
|
+
type ActivePointer = {
|
|
38
|
+
readonly id: number
|
|
39
|
+
readonly x: number
|
|
40
|
+
readonly y: number
|
|
41
|
+
readonly startX: number
|
|
42
|
+
readonly startY: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type GestureSession = {
|
|
46
|
+
readonly pointers: ReadonlyMap<number, ActivePointer>
|
|
47
|
+
readonly start: ViewportTransform
|
|
48
|
+
readonly startDistance: number
|
|
49
|
+
readonly startMidpoint: ViewportPoint
|
|
50
|
+
moved: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Clamp a scale into the viewer's zoom range. Pure so tests can hold the
|
|
55
|
+
* same arithmetic the DOM hook uses.
|
|
56
|
+
*/
|
|
57
|
+
export function clampViewportScale(
|
|
58
|
+
scale: number,
|
|
59
|
+
options: { readonly minScale: number; readonly maxScale: number },
|
|
60
|
+
): number {
|
|
61
|
+
return Math.max(options.minScale, Math.min(options.maxScale, scale))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Pan a transform by screen-space pixels. */
|
|
65
|
+
export function panViewport(
|
|
66
|
+
transform: ViewportTransform,
|
|
67
|
+
delta: ViewportPoint,
|
|
68
|
+
): ViewportTransform {
|
|
69
|
+
return { ...transform, x: transform.x + delta.x, y: transform.y + delta.y }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Zoom `transform` to `nextScale` while keeping the content under
|
|
74
|
+
* `anchor` (relative to the transform origin) visually fixed.
|
|
75
|
+
*/
|
|
76
|
+
export function zoomViewportAt(
|
|
77
|
+
transform: ViewportTransform,
|
|
78
|
+
anchor: ViewportPoint,
|
|
79
|
+
nextScale: number,
|
|
80
|
+
options: { readonly minScale: number; readonly maxScale: number },
|
|
81
|
+
): ViewportTransform {
|
|
82
|
+
const scale = clampViewportScale(nextScale, options)
|
|
83
|
+
const scaleDelta = scale - transform.scale
|
|
84
|
+
return {
|
|
85
|
+
scale,
|
|
86
|
+
x: transform.x - anchor.x * scaleDelta,
|
|
87
|
+
y: transform.y - anchor.y * scaleDelta,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when a press counts as a tap rather than a drag. */
|
|
92
|
+
export function isTapGesture(
|
|
93
|
+
start: ViewportPoint,
|
|
94
|
+
end: ViewportPoint,
|
|
95
|
+
tapThreshold: number,
|
|
96
|
+
): boolean {
|
|
97
|
+
return Math.hypot(end.x - start.x, end.y - start.y) < tapThreshold
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function usePinchPan(options: PinchPanOptions): {
|
|
101
|
+
readonly transform: ViewportTransform
|
|
102
|
+
readonly reset: () => void
|
|
103
|
+
} {
|
|
104
|
+
const {
|
|
105
|
+
target,
|
|
106
|
+
initial = DEFAULT_TRANSFORM,
|
|
107
|
+
minScale = 0.5,
|
|
108
|
+
maxScale = 4,
|
|
109
|
+
tapThreshold = 8,
|
|
110
|
+
resetKey,
|
|
111
|
+
onChange,
|
|
112
|
+
onTap,
|
|
113
|
+
onDoubleTap,
|
|
114
|
+
} = options
|
|
115
|
+
const [transform, setTransform] = useState<ViewportTransform>(initial)
|
|
116
|
+
const initialRef = useRef(initial)
|
|
117
|
+
initialRef.current = initial
|
|
118
|
+
const transformRef = useRef(transform)
|
|
119
|
+
transformRef.current = transform
|
|
120
|
+
const sessionRef = useRef<GestureSession | null>(null)
|
|
121
|
+
const lastTapRef = useRef<{
|
|
122
|
+
readonly point: ViewportPoint
|
|
123
|
+
readonly time: number
|
|
124
|
+
} | null>(null)
|
|
125
|
+
const boundsRef = useRef({ minScale, maxScale, tapThreshold })
|
|
126
|
+
boundsRef.current = { minScale, maxScale, tapThreshold }
|
|
127
|
+
|
|
128
|
+
function update(next: ViewportTransform) {
|
|
129
|
+
setTransform(next)
|
|
130
|
+
onChange?.(next)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reset() {
|
|
134
|
+
update({ ...DEFAULT_TRANSFORM })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (resetKey === undefined) return
|
|
139
|
+
update({ ...initialRef.current })
|
|
140
|
+
}, [resetKey])
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const currentTarget = target.current
|
|
144
|
+
if (currentTarget === null) return
|
|
145
|
+
const element: HTMLElement = currentTarget
|
|
146
|
+
|
|
147
|
+
function startPointer(event: PointerEvent): void {
|
|
148
|
+
const current = transformRef.current
|
|
149
|
+
const session = sessionRef.current
|
|
150
|
+
if (session === null) {
|
|
151
|
+
sessionRef.current = {
|
|
152
|
+
pointers: new Map([[event.pointerId, activePointer(event)]]),
|
|
153
|
+
start: current,
|
|
154
|
+
startDistance: 0,
|
|
155
|
+
startMidpoint: eventPoint(event),
|
|
156
|
+
moved: false,
|
|
157
|
+
}
|
|
158
|
+
element.setPointerCapture(event.pointerId)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
const pointers = new Map(session.pointers)
|
|
162
|
+
pointers.set(event.pointerId, activePointer(event))
|
|
163
|
+
const entries = [...pointers.values()]
|
|
164
|
+
if (entries.length >= 2) {
|
|
165
|
+
const first = entries[0]
|
|
166
|
+
const second = entries[1]
|
|
167
|
+
if (first !== undefined && second !== undefined) {
|
|
168
|
+
sessionRef.current = {
|
|
169
|
+
pointers,
|
|
170
|
+
start: current,
|
|
171
|
+
startDistance: distance(first, second),
|
|
172
|
+
startMidpoint: midpoint(first, second),
|
|
173
|
+
moved: session.moved,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
element.setPointerCapture(event.pointerId)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function movePointer(event: PointerEvent): void {
|
|
181
|
+
const session = sessionRef.current
|
|
182
|
+
if (session === null) return
|
|
183
|
+
const pointers = new Map(session.pointers)
|
|
184
|
+
pointers.set(
|
|
185
|
+
event.pointerId,
|
|
186
|
+
activePointer(event, session.pointers.get(event.pointerId)),
|
|
187
|
+
)
|
|
188
|
+
const entries = [...pointers.values()]
|
|
189
|
+
if (entries.length === 0) return
|
|
190
|
+
|
|
191
|
+
if (entries.length === 1) {
|
|
192
|
+
const pointer = entries[0]
|
|
193
|
+
if (pointer === undefined) return
|
|
194
|
+
const delta = {
|
|
195
|
+
x: pointer.x - pointer.startX,
|
|
196
|
+
y: pointer.y - pointer.startY,
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
!isTapGesture(
|
|
200
|
+
session.startMidpoint,
|
|
201
|
+
pointer,
|
|
202
|
+
boundsRef.current.tapThreshold,
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
session.moved = true
|
|
206
|
+
}
|
|
207
|
+
sessionRef.current = {
|
|
208
|
+
...session,
|
|
209
|
+
pointers,
|
|
210
|
+
moved:
|
|
211
|
+
session.moved ||
|
|
212
|
+
movedBeyondTap(pointer, boundsRef.current.tapThreshold),
|
|
213
|
+
}
|
|
214
|
+
update(panViewport(session.start, delta))
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const first = entries[0]
|
|
219
|
+
const second = entries[1]
|
|
220
|
+
if (first === undefined || second === undefined) return
|
|
221
|
+
const nextDistance = Math.max(1, distance(first, second))
|
|
222
|
+
const ratio = nextDistance / Math.max(1, session.startDistance)
|
|
223
|
+
const nextMidpoint = midpoint(first, second)
|
|
224
|
+
const centerMidpoint = relativeMidpoint(element, nextMidpoint)
|
|
225
|
+
const next = zoomViewportAt(
|
|
226
|
+
session.start,
|
|
227
|
+
centerMidpoint,
|
|
228
|
+
session.start.scale * ratio,
|
|
229
|
+
{
|
|
230
|
+
minScale: boundsRef.current.minScale,
|
|
231
|
+
maxScale: boundsRef.current.maxScale,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
sessionRef.current = { ...session, pointers, moved: true }
|
|
235
|
+
update(next)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function endPointer(event: PointerEvent): void {
|
|
239
|
+
const session = sessionRef.current
|
|
240
|
+
if (session === null) return
|
|
241
|
+
const pointers = new Map(session.pointers)
|
|
242
|
+
pointers.delete(event.pointerId)
|
|
243
|
+
if (pointers.size > 0) {
|
|
244
|
+
sessionRef.current = { ...session, pointers }
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
sessionRef.current = null
|
|
248
|
+
if (!session.moved && session.pointers.size === 1) {
|
|
249
|
+
const point = eventPoint(event)
|
|
250
|
+
handleTap(point)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function cancelPointer(event: PointerEvent): void {
|
|
255
|
+
const session = sessionRef.current
|
|
256
|
+
if (session === null) return
|
|
257
|
+
const pointers = new Map(session.pointers)
|
|
258
|
+
pointers.delete(event.pointerId)
|
|
259
|
+
sessionRef.current = pointers.size > 0 ? { ...session, pointers } : null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleTap(point: ViewportPoint): void {
|
|
263
|
+
onTap?.(point)
|
|
264
|
+
const now = performance.now()
|
|
265
|
+
const last = lastTapRef.current
|
|
266
|
+
lastTapRef.current = { point, time: now }
|
|
267
|
+
if (
|
|
268
|
+
last !== null &&
|
|
269
|
+
now - last.time < 300 &&
|
|
270
|
+
Math.hypot(point.x - last.point.x, point.y - last.point.y) < 30
|
|
271
|
+
) {
|
|
272
|
+
lastTapRef.current = null
|
|
273
|
+
onDoubleTap?.(point)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
element.addEventListener("pointerdown", startPointer)
|
|
278
|
+
element.addEventListener("pointermove", movePointer)
|
|
279
|
+
element.addEventListener("pointerup", endPointer)
|
|
280
|
+
element.addEventListener("pointercancel", cancelPointer)
|
|
281
|
+
return () => {
|
|
282
|
+
element.removeEventListener("pointerdown", startPointer)
|
|
283
|
+
element.removeEventListener("pointermove", movePointer)
|
|
284
|
+
element.removeEventListener("pointerup", endPointer)
|
|
285
|
+
element.removeEventListener("pointercancel", cancelPointer)
|
|
286
|
+
}
|
|
287
|
+
}, [target, onTap, onDoubleTap, onChange])
|
|
288
|
+
|
|
289
|
+
return { transform, reset }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function activePointer(
|
|
293
|
+
event: PointerEvent,
|
|
294
|
+
start?: ActivePointer | ViewportPoint,
|
|
295
|
+
): ActivePointer {
|
|
296
|
+
const origin = start ?? eventPoint(event)
|
|
297
|
+
return {
|
|
298
|
+
id: event.pointerId,
|
|
299
|
+
x: event.clientX,
|
|
300
|
+
y: event.clientY,
|
|
301
|
+
startX: origin.x,
|
|
302
|
+
startY: origin.y,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function eventPoint(event: {
|
|
307
|
+
readonly clientX: number
|
|
308
|
+
readonly clientY: number
|
|
309
|
+
}): ViewportPoint {
|
|
310
|
+
return { x: event.clientX, y: event.clientY }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function distance(a: ViewportPoint, b: ViewportPoint): number {
|
|
314
|
+
return Math.hypot(a.x - b.x, a.y - b.y)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function midpoint(a: ViewportPoint, b: ViewportPoint): ViewportPoint {
|
|
318
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function movedBeyondTap(pointer: ActivePointer, tapThreshold: number): boolean {
|
|
322
|
+
return (
|
|
323
|
+
isTapGesture(
|
|
324
|
+
{ x: pointer.startX, y: pointer.startY },
|
|
325
|
+
{ x: pointer.x, y: pointer.y },
|
|
326
|
+
tapThreshold,
|
|
327
|
+
) === false
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function relativeMidpoint(
|
|
332
|
+
element: HTMLElement,
|
|
333
|
+
point: ViewportPoint,
|
|
334
|
+
): ViewportPoint {
|
|
335
|
+
const rect = element.getBoundingClientRect()
|
|
336
|
+
return {
|
|
337
|
+
x: point.x - (rect.left + rect.width / 2),
|
|
338
|
+
y: point.y - (rect.top + rect.height / 2),
|
|
339
|
+
}
|
|
340
|
+
}
|