@mkbabb/glass-ui 0.2.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/README.md +172 -0
- package/dist/glass-ui.css +1 -0
- package/dist/glass-ui.js +10019 -0
- package/dist/index.d.ts +6619 -0
- package/package.json +65 -0
- package/src/components/custom/aurora/Aurora.vue +34 -0
- package/src/components/custom/aurora/composables/color.ts +122 -0
- package/src/components/custom/aurora/composables/useAurora.ts +355 -0
- package/src/components/custom/aurora/index.ts +8 -0
- package/src/components/custom/confirm-dialog/ConfirmDialog.vue +88 -0
- package/src/components/custom/confirm-dialog/index.ts +1 -0
- package/src/components/custom/controls/DarkModeToggle.vue +96 -0
- package/src/components/custom/controls/index.ts +1 -0
- package/src/components/custom/dock/DockLayerGroup.vue +21 -0
- package/src/components/custom/dock/DockPopover.vue +263 -0
- package/src/components/custom/dock/GlassDock.vue +276 -0
- package/src/components/custom/dock/composables/index.ts +16 -0
- package/src/components/custom/dock/composables/isTeleportedTarget.ts +19 -0
- package/src/components/custom/dock/composables/useDockActionBar.ts +33 -0
- package/src/components/custom/dock/composables/useDockState.ts +301 -0
- package/src/components/custom/dock/composables/useDockTransition.ts +146 -0
- package/src/components/custom/dock/composables/useLayerTransition.ts +135 -0
- package/src/components/custom/dock/composables/usePopupMutex.ts +83 -0
- package/src/components/custom/dock/index.ts +9 -0
- package/src/components/custom/expandable-container/ExpandableContainer.vue +64 -0
- package/src/components/custom/expandable-container/index.ts +1 -0
- package/src/components/custom/glass-panel/GlassPanel.vue +98 -0
- package/src/components/custom/glass-panel/index.ts +2 -0
- package/src/components/custom/icon-tooltip/IconTooltip.vue +20 -0
- package/src/components/custom/icon-tooltip/index.ts +1 -0
- package/src/components/custom/index.ts +15 -0
- package/src/components/custom/infinite-scroll/InfiniteScroll.vue +55 -0
- package/src/components/custom/infinite-scroll/composables/index.ts +2 -0
- package/src/components/custom/infinite-scroll/composables/types.ts +23 -0
- package/src/components/custom/infinite-scroll/composables/useInfiniteScroll.ts +73 -0
- package/src/components/custom/infinite-scroll/index.ts +1 -0
- package/src/components/custom/labeled-field/LabeledInput.vue +29 -0
- package/src/components/custom/labeled-field/LabeledSelect.vue +59 -0
- package/src/components/custom/labeled-field/LabeledSlider.vue +32 -0
- package/src/components/custom/labeled-field/LabeledSwitch.vue +27 -0
- package/src/components/custom/labeled-field/index.ts +4 -0
- package/src/components/custom/metaballs/MetaballCanvas.vue +23 -0
- package/src/components/custom/metaballs/index.ts +4 -0
- package/src/components/custom/metaballs/shaders.ts +63 -0
- package/src/components/custom/metaballs/types.ts +29 -0
- package/src/components/custom/metaballs/useMetaballs.ts +252 -0
- package/src/components/custom/search/FuzzySearch.vue +589 -0
- package/src/components/custom/search/SearchBar.vue +44 -0
- package/src/components/custom/search/composables/fuzzySearchIndex.ts +224 -0
- package/src/components/custom/search/composables/index.ts +5 -0
- package/src/components/custom/search/composables/types.ts +34 -0
- package/src/components/custom/search/composables/useFuzzySearch.ts +115 -0
- package/src/components/custom/search/index.ts +7 -0
- package/src/components/custom/sidebar/ProgressiveSidebar.vue +256 -0
- package/src/components/custom/sidebar/composables/index.ts +6 -0
- package/src/components/custom/sidebar/composables/useScrollTracker.ts +242 -0
- package/src/components/custom/sidebar/composables/useSidebarFollow.ts +247 -0
- package/src/components/custom/sidebar/composables/useSidebarState.ts +72 -0
- package/src/components/custom/sidebar/composables/useTreeIndex.ts +152 -0
- package/src/components/custom/sidebar/index.ts +15 -0
- package/src/components/custom/sidebar/types.ts +50 -0
- package/src/components/custom/tabs/BouncyTabs.vue +39 -0
- package/src/components/custom/tabs/BouncyToggle.vue +352 -0
- package/src/components/custom/tabs/UnderlineTabs.vue +115 -0
- package/src/components/custom/tabs/index.ts +5 -0
- package/src/components/custom/timeline/GlassTimeline.vue +174 -0
- package/src/components/custom/timeline/index.ts +1 -0
- package/src/components/custom/typewriter/TypewriterText.vue +239 -0
- package/src/components/custom/typewriter/composables/index.ts +1 -0
- package/src/components/custom/typewriter/composables/useTypewriter.ts +413 -0
- package/src/components/custom/typewriter/index.ts +7 -0
- package/src/components/custom/typewriter/types.ts +159 -0
- package/src/components/custom/typewriter/utils/keyboard.ts +213 -0
- package/src/components/custom/typewriter/utils/pausePatterns.ts +55 -0
- package/src/components/custom/typewriter/utils/timing.ts +104 -0
- package/src/components/custom/typewriter/utils/typoStateMachine.ts +197 -0
- package/src/components/index.ts +2 -0
- package/src/components/ui/accordion/Accordion.vue +19 -0
- package/src/components/ui/accordion/AccordionContent.vue +24 -0
- package/src/components/ui/accordion/AccordionItem.vue +24 -0
- package/src/components/ui/accordion/AccordionTrigger.vue +39 -0
- package/src/components/ui/accordion/index.ts +4 -0
- package/src/components/ui/alert/Alert.vue +20 -0
- package/src/components/ui/alert/AlertDescription.vue +17 -0
- package/src/components/ui/alert/AlertTitle.vue +17 -0
- package/src/components/ui/alert/index.ts +23 -0
- package/src/components/ui/avatar/Avatar.vue +21 -0
- package/src/components/ui/avatar/AvatarFallback.vue +11 -0
- package/src/components/ui/avatar/AvatarImage.vue +9 -0
- package/src/components/ui/avatar/index.ts +24 -0
- package/src/components/ui/badge/Badge.vue +16 -0
- package/src/components/ui/badge/index.ts +25 -0
- package/src/components/ui/button/Button.vue +26 -0
- package/src/components/ui/button/index.ts +43 -0
- package/src/components/ui/card/Card.vue +28 -0
- package/src/components/ui/card/CardContent.vue +14 -0
- package/src/components/ui/card/CardDescription.vue +14 -0
- package/src/components/ui/card/CardFooter.vue +14 -0
- package/src/components/ui/card/CardHeader.vue +14 -0
- package/src/components/ui/card/CardTitle.vue +21 -0
- package/src/components/ui/card/index.ts +6 -0
- package/src/components/ui/carousel/Carousel.vue +53 -0
- package/src/components/ui/carousel/CarouselContent.vue +35 -0
- package/src/components/ui/carousel/CarouselItem.vue +24 -0
- package/src/components/ui/carousel/CarouselNext.vue +40 -0
- package/src/components/ui/carousel/CarouselPrevious.vue +40 -0
- package/src/components/ui/carousel/index.ts +10 -0
- package/src/components/ui/carousel/interface.ts +26 -0
- package/src/components/ui/carousel/useCarousel.ts +56 -0
- package/src/components/ui/checkbox/Checkbox.vue +33 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/collapsible/Collapsible.vue +15 -0
- package/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
- package/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
- package/src/components/ui/collapsible/index.ts +3 -0
- package/src/components/ui/combobox/Combobox.vue +17 -0
- package/src/components/ui/combobox/ComboboxAnchor.vue +23 -0
- package/src/components/ui/combobox/ComboboxEmpty.vue +21 -0
- package/src/components/ui/combobox/ComboboxGroup.vue +27 -0
- package/src/components/ui/combobox/ComboboxInput.vue +41 -0
- package/src/components/ui/combobox/ComboboxItem.vue +24 -0
- package/src/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
- package/src/components/ui/combobox/ComboboxList.vue +29 -0
- package/src/components/ui/combobox/ComboboxSeparator.vue +21 -0
- package/src/components/ui/combobox/ComboboxViewport.vue +23 -0
- package/src/components/ui/combobox/index.ts +12 -0
- package/src/components/ui/command/Command.vue +30 -0
- package/src/components/ui/command/CommandDialog.vue +21 -0
- package/src/components/ui/command/CommandEmpty.vue +20 -0
- package/src/components/ui/command/CommandGroup.vue +29 -0
- package/src/components/ui/command/CommandInput.vue +33 -0
- package/src/components/ui/command/CommandItem.vue +26 -0
- package/src/components/ui/command/CommandList.vue +27 -0
- package/src/components/ui/command/CommandSeparator.vue +23 -0
- package/src/components/ui/command/CommandShortcut.vue +14 -0
- package/src/components/ui/command/index.ts +9 -0
- package/src/components/ui/context-menu/ContextMenu.vue +15 -0
- package/src/components/ui/context-menu/ContextMenuCheckboxItem.vue +40 -0
- package/src/components/ui/context-menu/ContextMenuContent.vue +36 -0
- package/src/components/ui/context-menu/ContextMenuGroup.vue +11 -0
- package/src/components/ui/context-menu/ContextMenuItem.vue +34 -0
- package/src/components/ui/context-menu/ContextMenuLabel.vue +25 -0
- package/src/components/ui/context-menu/ContextMenuPortal.vue +11 -0
- package/src/components/ui/context-menu/ContextMenuRadioGroup.vue +19 -0
- package/src/components/ui/context-menu/ContextMenuRadioItem.vue +40 -0
- package/src/components/ui/context-menu/ContextMenuSeparator.vue +20 -0
- package/src/components/ui/context-menu/ContextMenuShortcut.vue +14 -0
- package/src/components/ui/context-menu/ContextMenuSub.vue +19 -0
- package/src/components/ui/context-menu/ContextMenuSubContent.vue +35 -0
- package/src/components/ui/context-menu/ContextMenuSubTrigger.vue +34 -0
- package/src/components/ui/context-menu/ContextMenuTrigger.vue +13 -0
- package/src/components/ui/context-menu/index.ts +14 -0
- package/src/components/ui/data-table/DataTable.vue +167 -0
- package/src/components/ui/data-table/DataTablePagination.vue +112 -0
- package/src/components/ui/data-table/index.ts +3 -0
- package/src/components/ui/data-table/types.ts +48 -0
- package/src/components/ui/dialog/Dialog.vue +14 -0
- package/src/components/ui/dialog/DialogClose.vue +11 -0
- package/src/components/ui/dialog/DialogContent.vue +61 -0
- package/src/components/ui/dialog/DialogDescription.vue +24 -0
- package/src/components/ui/dialog/DialogFooter.vue +19 -0
- package/src/components/ui/dialog/DialogHeader.vue +16 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +65 -0
- package/src/components/ui/dialog/DialogTitle.vue +29 -0
- package/src/components/ui/dialog/DialogTrigger.vue +11 -0
- package/src/components/ui/dialog/index.ts +9 -0
- package/src/components/ui/drawer/Drawer.vue +19 -0
- package/src/components/ui/drawer/DrawerContent.vue +28 -0
- package/src/components/ui/drawer/DrawerDescription.vue +20 -0
- package/src/components/ui/drawer/DrawerFooter.vue +14 -0
- package/src/components/ui/drawer/DrawerHeader.vue +14 -0
- package/src/components/ui/drawer/DrawerOverlay.vue +18 -0
- package/src/components/ui/drawer/DrawerTitle.vue +20 -0
- package/src/components/ui/drawer/index.ts +8 -0
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +44 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +36 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
- package/src/components/ui/dropdown-menu/index.ts +16 -0
- package/src/components/ui/hover-card/HoverCard.vue +14 -0
- package/src/components/ui/hover-card/HoverCardContent.vue +41 -0
- package/src/components/ui/hover-card/HoverCardTrigger.vue +11 -0
- package/src/components/ui/hover-card/index.ts +3 -0
- package/src/components/ui/index.ts +41 -0
- package/src/components/ui/input/Input.vue +24 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/label/Label.vue +27 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/multi-select/MultiSelect.vue +141 -0
- package/src/components/ui/multi-select/index.ts +7 -0
- package/src/components/ui/notification/Notification.vue +85 -0
- package/src/components/ui/notification/index.ts +1 -0
- package/src/components/ui/number-field/NumberField.vue +23 -0
- package/src/components/ui/number-field/NumberFieldContent.vue +14 -0
- package/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
- package/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
- package/src/components/ui/number-field/NumberFieldInput.vue +8 -0
- package/src/components/ui/number-field/index.ts +5 -0
- package/src/components/ui/popover/Popover.vue +15 -0
- package/src/components/ui/popover/PopoverContent.vue +61 -0
- package/src/components/ui/popover/PopoverTrigger.vue +11 -0
- package/src/components/ui/popover/index.ts +3 -0
- package/src/components/ui/progress/Progress.vue +39 -0
- package/src/components/ui/progress/index.ts +1 -0
- package/src/components/ui/radio-group/RadioGroup.vue +25 -0
- package/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
- package/src/components/ui/radio-group/index.ts +2 -0
- package/src/components/ui/scroll-area/ScrollArea.vue +29 -0
- package/src/components/ui/scroll-area/ScrollBar.vue +30 -0
- package/src/components/ui/scroll-area/index.ts +2 -0
- package/src/components/ui/scroll-pane/ScrollPane.vue +25 -0
- package/src/components/ui/scroll-pane/ScrollPaneHeader.vue +75 -0
- package/src/components/ui/scroll-pane/index.ts +2 -0
- package/src/components/ui/select/Select.vue +15 -0
- package/src/components/ui/select/SelectContent.vue +57 -0
- package/src/components/ui/select/SelectGroup.vue +19 -0
- package/src/components/ui/select/SelectItem.vue +47 -0
- package/src/components/ui/select/SelectItemText.vue +11 -0
- package/src/components/ui/select/SelectLabel.vue +13 -0
- package/src/components/ui/select/SelectScrollDownButton.vue +24 -0
- package/src/components/ui/select/SelectScrollUpButton.vue +24 -0
- package/src/components/ui/select/SelectSeparator.vue +17 -0
- package/src/components/ui/select/SelectTrigger.vue +45 -0
- package/src/components/ui/select/SelectValue.vue +11 -0
- package/src/components/ui/select/index.ts +11 -0
- package/src/components/ui/separator/Separator.vue +35 -0
- package/src/components/ui/separator/index.ts +1 -0
- package/src/components/ui/sheet/Sheet.vue +14 -0
- package/src/components/ui/sheet/SheetClose.vue +11 -0
- package/src/components/ui/sheet/SheetContent.vue +56 -0
- package/src/components/ui/sheet/SheetDescription.vue +22 -0
- package/src/components/ui/sheet/SheetFooter.vue +19 -0
- package/src/components/ui/sheet/SheetHeader.vue +16 -0
- package/src/components/ui/sheet/SheetTitle.vue +22 -0
- package/src/components/ui/sheet/SheetTrigger.vue +11 -0
- package/src/components/ui/sheet/index.ts +31 -0
- package/src/components/ui/skeleton/Skeleton.vue +14 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/slider/Slider.vue +66 -0
- package/src/components/ui/slider/index.ts +1 -0
- package/src/components/ui/switch/Switch.vue +37 -0
- package/src/components/ui/switch/index.ts +1 -0
- package/src/components/ui/table/Table.vue +16 -0
- package/src/components/ui/table/TableBody.vue +14 -0
- package/src/components/ui/table/TableCaption.vue +14 -0
- package/src/components/ui/table/TableCell.vue +14 -0
- package/src/components/ui/table/TableEmpty.vue +39 -0
- package/src/components/ui/table/TableFooter.vue +16 -0
- package/src/components/ui/table/TableHead.vue +21 -0
- package/src/components/ui/table/TableHeader.vue +14 -0
- package/src/components/ui/table/TableRow.vue +21 -0
- package/src/components/ui/table/index.ts +9 -0
- package/src/components/ui/tabs/Tabs.vue +15 -0
- package/src/components/ui/tabs/TabsContent.vue +22 -0
- package/src/components/ui/tabs/TabsIndicator.vue +22 -0
- package/src/components/ui/tabs/TabsList.vue +25 -0
- package/src/components/ui/tabs/TabsTrigger.vue +29 -0
- package/src/components/ui/tabs/index.ts +5 -0
- package/src/components/ui/tags-input/TagsInput.vue +22 -0
- package/src/components/ui/tags-input/TagsInputInput.vue +19 -0
- package/src/components/ui/tags-input/TagsInputItem.vue +22 -0
- package/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
- package/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
- package/src/components/ui/tags-input/index.ts +5 -0
- package/src/components/ui/textarea/Textarea.vue +24 -0
- package/src/components/ui/textarea/index.ts +1 -0
- package/src/components/ui/toast/Toast.vue +57 -0
- package/src/components/ui/toast/ToastAction.vue +30 -0
- package/src/components/ui/toast/ToastClose.vue +31 -0
- package/src/components/ui/toast/ToastDescription.vue +25 -0
- package/src/components/ui/toast/ToastTitle.vue +25 -0
- package/src/components/ui/toast/Toaster.vue +31 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toast/use-toast.ts +136 -0
- package/src/components/ui/toggle/Toggle.vue +35 -0
- package/src/components/ui/toggle/index.ts +27 -0
- package/src/components/ui/toggle-group/ToggleGroup.vue +34 -0
- package/src/components/ui/toggle-group/ToggleGroupItem.vue +35 -0
- package/src/components/ui/toggle-group/index.ts +2 -0
- package/src/components/ui/tooltip/Tooltip.vue +14 -0
- package/src/components/ui/tooltip/TooltipContent.vue +31 -0
- package/src/components/ui/tooltip/TooltipProvider.vue +11 -0
- package/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/composables/glass/index.ts +8 -0
- package/src/composables/glass/useGlassRenderer.ts +252 -0
- package/src/composables/glass/webgl/frostShader.ts +221 -0
- package/src/composables/glass/webgpu/glassShader.wgsl +173 -0
- package/src/composables/index.ts +32 -0
- package/src/composables/infinite-scroll/index.ts +2 -0
- package/src/composables/infinite-scroll/types.ts +25 -0
- package/src/composables/infinite-scroll/useInfiniteScroll.ts +101 -0
- package/src/composables/interaction/index.ts +5 -0
- package/src/composables/interaction/useHeightTransition.ts +82 -0
- package/src/composables/interaction/useHoverPopover.ts +64 -0
- package/src/composables/interaction/useHoverToggle.ts +103 -0
- package/src/composables/interaction/useLeaveTimer.ts +17 -0
- package/src/composables/interaction/useTouchGate.ts +207 -0
- package/src/composables/pagination/index.ts +2 -0
- package/src/composables/pagination/useOffsetPagination.ts +70 -0
- package/src/composables/prng.ts +32 -0
- package/src/composables/useCharSplit.ts +31 -0
- package/src/composables/useClipboard.ts +46 -0
- package/src/composables/useGlobalDark.ts +61 -0
- package/src/composables/useKeyboardShortcuts.ts +205 -0
- package/src/composables/useWatercolorBlob.ts +136 -0
- package/src/composables/virtual/index.ts +22 -0
- package/src/composables/virtual/useVirtualSectionWindow.ts +338 -0
- package/src/composables/virtual/useWindowedStore.ts +86 -0
- package/src/composables/virtual/virtualSectionLayout.ts +212 -0
- package/src/index.ts +9 -0
- package/src/styles/animations.css +233 -0
- package/src/styles/cards.css +66 -0
- package/src/styles/dock.css +221 -0
- package/src/styles/floating-panel.css +49 -0
- package/src/styles/glass.css +266 -0
- package/src/styles/index.css +26 -0
- package/src/styles/scroll-pane.css +10 -0
- package/src/styles/theme.css +138 -0
- package/src/styles/tokens.css +333 -0
- package/src/styles/transitions.css +226 -0
- package/src/styles/typography.css +277 -0
- package/src/styles/utilities.css +697 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// --- Key position types and QWERTY map ---
|
|
2
|
+
|
|
3
|
+
export interface KeyPosition {
|
|
4
|
+
row: number;
|
|
5
|
+
col: number;
|
|
6
|
+
finger: "pinky" | "ring" | "middle" | "index" | "thumb";
|
|
7
|
+
hand: "left" | "right";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const QWERTY_MAP: Record<string, KeyPosition> = {
|
|
11
|
+
// Row 0 - Numbers row
|
|
12
|
+
"1": { row: 0, col: 1, finger: "pinky", hand: "left" },
|
|
13
|
+
"2": { row: 0, col: 2, finger: "ring", hand: "left" },
|
|
14
|
+
"3": { row: 0, col: 3, finger: "middle", hand: "left" },
|
|
15
|
+
"4": { row: 0, col: 4, finger: "index", hand: "left" },
|
|
16
|
+
"5": { row: 0, col: 5, finger: "index", hand: "left" },
|
|
17
|
+
"6": { row: 0, col: 6, finger: "index", hand: "right" },
|
|
18
|
+
"7": { row: 0, col: 7, finger: "index", hand: "right" },
|
|
19
|
+
"8": { row: 0, col: 8, finger: "middle", hand: "right" },
|
|
20
|
+
"9": { row: 0, col: 9, finger: "ring", hand: "right" },
|
|
21
|
+
"0": { row: 0, col: 10, finger: "pinky", hand: "right" },
|
|
22
|
+
|
|
23
|
+
// Row 1 - QWERTY row
|
|
24
|
+
q: { row: 1, col: 1, finger: "pinky", hand: "left" },
|
|
25
|
+
w: { row: 1, col: 2, finger: "ring", hand: "left" },
|
|
26
|
+
e: { row: 1, col: 3, finger: "middle", hand: "left" },
|
|
27
|
+
r: { row: 1, col: 4, finger: "index", hand: "left" },
|
|
28
|
+
t: { row: 1, col: 5, finger: "index", hand: "left" },
|
|
29
|
+
y: { row: 1, col: 6, finger: "index", hand: "right" },
|
|
30
|
+
u: { row: 1, col: 7, finger: "index", hand: "right" },
|
|
31
|
+
i: { row: 1, col: 8, finger: "middle", hand: "right" },
|
|
32
|
+
o: { row: 1, col: 9, finger: "ring", hand: "right" },
|
|
33
|
+
p: { row: 1, col: 10, finger: "pinky", hand: "right" },
|
|
34
|
+
|
|
35
|
+
// Row 2 - ASDF row (home row)
|
|
36
|
+
a: { row: 2, col: 1, finger: "pinky", hand: "left" },
|
|
37
|
+
s: { row: 2, col: 2, finger: "ring", hand: "left" },
|
|
38
|
+
d: { row: 2, col: 3, finger: "middle", hand: "left" },
|
|
39
|
+
f: { row: 2, col: 4, finger: "index", hand: "left" },
|
|
40
|
+
g: { row: 2, col: 5, finger: "index", hand: "left" },
|
|
41
|
+
h: { row: 2, col: 6, finger: "index", hand: "right" },
|
|
42
|
+
j: { row: 2, col: 7, finger: "index", hand: "right" },
|
|
43
|
+
k: { row: 2, col: 8, finger: "middle", hand: "right" },
|
|
44
|
+
l: { row: 2, col: 9, finger: "ring", hand: "right" },
|
|
45
|
+
";": { row: 2, col: 10, finger: "pinky", hand: "right" },
|
|
46
|
+
|
|
47
|
+
// Row 3 - ZXCV row
|
|
48
|
+
z: { row: 3, col: 1, finger: "pinky", hand: "left" },
|
|
49
|
+
x: { row: 3, col: 2, finger: "ring", hand: "left" },
|
|
50
|
+
c: { row: 3, col: 3, finger: "middle", hand: "left" },
|
|
51
|
+
v: { row: 3, col: 4, finger: "index", hand: "left" },
|
|
52
|
+
b: { row: 3, col: 5, finger: "index", hand: "left" },
|
|
53
|
+
n: { row: 3, col: 6, finger: "index", hand: "right" },
|
|
54
|
+
m: { row: 3, col: 7, finger: "index", hand: "right" },
|
|
55
|
+
",": { row: 3, col: 8, finger: "middle", hand: "right" },
|
|
56
|
+
".": { row: 3, col: 9, finger: "ring", hand: "right" },
|
|
57
|
+
"/": { row: 3, col: 10, finger: "pinky", hand: "right" },
|
|
58
|
+
|
|
59
|
+
// Space bar
|
|
60
|
+
" ": { row: 4, col: 5, finger: "thumb", hand: "right" },
|
|
61
|
+
|
|
62
|
+
// Common punctuation
|
|
63
|
+
"'": { row: 2, col: 11, finger: "pinky", hand: "right" },
|
|
64
|
+
'"': { row: 2, col: 11, finger: "pinky", hand: "right" },
|
|
65
|
+
"?": { row: 3, col: 10, finger: "pinky", hand: "right" },
|
|
66
|
+
"!": { row: 0, col: 1, finger: "pinky", hand: "left" },
|
|
67
|
+
"-": { row: 0, col: 11, finger: "pinky", hand: "right" },
|
|
68
|
+
"=": { row: 0, col: 12, finger: "pinky", hand: "right" },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// --- Key delay calculation ---
|
|
72
|
+
|
|
73
|
+
export const calculateKeyDelay = (prevChar: string, nextChar: string): number => {
|
|
74
|
+
const prev = QWERTY_MAP[prevChar.toLowerCase()];
|
|
75
|
+
const next = QWERTY_MAP[nextChar.toLowerCase()];
|
|
76
|
+
|
|
77
|
+
if (!prev || !next) return 250;
|
|
78
|
+
|
|
79
|
+
// Fastest: alternating hands
|
|
80
|
+
if (prev.hand !== next.hand) {
|
|
81
|
+
return 150 + Math.random() * 50;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Same key pressed twice
|
|
85
|
+
if (prev.row === next.row && prev.col === next.col) {
|
|
86
|
+
return 350 + Math.random() * 100;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Adjacent fingers, same hand
|
|
90
|
+
const colDiff = Math.abs(prev.col - next.col);
|
|
91
|
+
if (colDiff === 1 && prev.row === next.row) {
|
|
92
|
+
return 180 + Math.random() * 40;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Home row combinations
|
|
96
|
+
if (prev.row === 2 && next.row === 2) {
|
|
97
|
+
return 200 + Math.random() * 50;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reaching from home row
|
|
101
|
+
if ((prev.row === 2 && next.row !== 2) || (prev.row !== 2 && next.row === 2)) {
|
|
102
|
+
return 280 + Math.random() * 70;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Bottom row with pinky/ring fingers
|
|
106
|
+
if (
|
|
107
|
+
(prev.row === 3 || next.row === 3) &&
|
|
108
|
+
(["pinky", "ring"].includes(prev.finger) || ["pinky", "ring"].includes(next.finger))
|
|
109
|
+
) {
|
|
110
|
+
return 320 + Math.random() * 80;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Large movements
|
|
114
|
+
const rowDiff = Math.abs(prev.row - next.row);
|
|
115
|
+
if (rowDiff > 1 || colDiff > 2) {
|
|
116
|
+
return 300 + Math.random() * 100;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Default
|
|
120
|
+
return 250 + Math.random() * 50;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// --- Adjacency map for typo generation ---
|
|
124
|
+
|
|
125
|
+
/** Physical row stagger offsets (in key-width units) */
|
|
126
|
+
const ROW_STAGGER: Record<number, number> = {
|
|
127
|
+
0: 0,
|
|
128
|
+
1: 0.25,
|
|
129
|
+
2: 0.5,
|
|
130
|
+
3: 0.75,
|
|
131
|
+
4: 0, // space bar row
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** Maximum distance for keys to be considered adjacent */
|
|
135
|
+
const MAX_DISTANCE = 1.2;
|
|
136
|
+
const MAX_ROW_DIFF = 1;
|
|
137
|
+
|
|
138
|
+
function physicalDistance(a: KeyPosition, b: KeyPosition): number {
|
|
139
|
+
const staggerA = ROW_STAGGER[a.row] ?? 0;
|
|
140
|
+
const staggerB = ROW_STAGGER[b.row] ?? 0;
|
|
141
|
+
const dx = a.col + staggerA - (b.col + staggerB);
|
|
142
|
+
const dy = a.row - b.row;
|
|
143
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Precomputed adjacency: for each key char, the list of adjacent key chars */
|
|
147
|
+
const ADJACENCY_MAP: Record<string, string[]> = {};
|
|
148
|
+
|
|
149
|
+
// Build adjacency map at module load
|
|
150
|
+
const alphaKeys = Object.entries(QWERTY_MAP).filter(([ch]) => /^[a-z]$/.test(ch));
|
|
151
|
+
for (const [charA, posA] of alphaKeys) {
|
|
152
|
+
const neighbors: string[] = [];
|
|
153
|
+
for (const [charB, posB] of alphaKeys) {
|
|
154
|
+
if (charA === charB) continue;
|
|
155
|
+
if (Math.abs(posA.row - posB.row) > MAX_ROW_DIFF) continue;
|
|
156
|
+
if (physicalDistance(posA, posB) <= MAX_DISTANCE) {
|
|
157
|
+
neighbors.push(charB);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
ADJACENCY_MAP[charA] = neighbors;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getAdjacentKeys(char: string): string[] {
|
|
164
|
+
return ADJACENCY_MAP[char.toLowerCase()] ?? [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Pick a plausible typo character for the intended character.
|
|
169
|
+
* Weighted: same-finger 2x, same-hand 1.5x, other 1x.
|
|
170
|
+
* Preserves case. Falls back to home row for unmapped chars.
|
|
171
|
+
*/
|
|
172
|
+
export function pickTypoChar(intendedChar: string): string {
|
|
173
|
+
const lower = intendedChar.toLowerCase();
|
|
174
|
+
const isUpper = intendedChar !== lower;
|
|
175
|
+
const intended = QWERTY_MAP[lower];
|
|
176
|
+
const adjacent = ADJACENCY_MAP[lower];
|
|
177
|
+
|
|
178
|
+
// Fallback: random home row key
|
|
179
|
+
if (!intended || !adjacent || adjacent.length === 0) {
|
|
180
|
+
const homeRow = "asdfghjkl";
|
|
181
|
+
const fallback = homeRow[Math.floor(Math.random() * homeRow.length)];
|
|
182
|
+
return isUpper ? fallback.toUpperCase() : fallback;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Build weighted pool
|
|
186
|
+
const pool: { char: string; weight: number }[] = [];
|
|
187
|
+
for (const adj of adjacent) {
|
|
188
|
+
const pos = QWERTY_MAP[adj];
|
|
189
|
+
if (!pos) continue;
|
|
190
|
+
|
|
191
|
+
let weight = 1;
|
|
192
|
+
if (pos.finger === intended.finger) {
|
|
193
|
+
weight = 2;
|
|
194
|
+
} else if (pos.hand === intended.hand) {
|
|
195
|
+
weight = 1.5;
|
|
196
|
+
}
|
|
197
|
+
pool.push({ char: adj, weight });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Weighted random selection
|
|
201
|
+
const totalWeight = pool.reduce((sum, p) => sum + p.weight, 0);
|
|
202
|
+
let roll = Math.random() * totalWeight;
|
|
203
|
+
for (const entry of pool) {
|
|
204
|
+
roll -= entry.weight;
|
|
205
|
+
if (roll <= 0) {
|
|
206
|
+
return isUpper ? entry.char.toUpperCase() : entry.char;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Should not reach here, but fallback
|
|
211
|
+
const last = pool[pool.length - 1].char;
|
|
212
|
+
return isUpper ? last.toUpperCase() : last;
|
|
213
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface PausePatterns {
|
|
2
|
+
sentenceEnd: { min: number; max: number };
|
|
3
|
+
commaBreak: { min: number; max: number };
|
|
4
|
+
wordBoundary: { min: number; max: number };
|
|
5
|
+
thinkingPause: { min: number; max: number };
|
|
6
|
+
colonPause: { min: number; max: number };
|
|
7
|
+
dashPause: { min: number; max: number };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const PAUSE_PATTERNS: PausePatterns = {
|
|
11
|
+
sentenceEnd: { min: 400, max: 800 },
|
|
12
|
+
commaBreak: { min: 200, max: 400 },
|
|
13
|
+
wordBoundary: { min: 50, max: 150 },
|
|
14
|
+
thinkingPause: { min: 800, max: 2000 },
|
|
15
|
+
colonPause: { min: 300, max: 500 },
|
|
16
|
+
dashPause: { min: 250, max: 450 },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const random = (min: number, max: number): number => {
|
|
20
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getPauseDelay = (char: string, nextChar: string, patterns: PausePatterns): number => {
|
|
24
|
+
// Sentence endings
|
|
25
|
+
if ([".", "!", "?"].includes(char) && nextChar === " ") {
|
|
26
|
+
return random(patterns.sentenceEnd.min, patterns.sentenceEnd.max);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Comma breaks
|
|
30
|
+
if (char === "," && nextChar === " ") {
|
|
31
|
+
return random(patterns.commaBreak.min, patterns.commaBreak.max);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Colon or semicolon pause
|
|
35
|
+
if ([":", ";"].includes(char) && nextChar === " ") {
|
|
36
|
+
return random(patterns.colonPause.min, patterns.colonPause.max);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Dash pause
|
|
40
|
+
if (char === "-" && nextChar === " ") {
|
|
41
|
+
return random(patterns.dashPause.min, patterns.dashPause.max);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Word boundaries (occasional micro-pauses)
|
|
45
|
+
if (char === " " && Math.random() < 0.3) {
|
|
46
|
+
return random(patterns.wordBoundary.min, patterns.wordBoundary.max);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Occasional thinking pauses (5% chance)
|
|
50
|
+
if (Math.random() < 0.05) {
|
|
51
|
+
return random(patterns.thinkingPause.min, patterns.thinkingPause.max);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return 0;
|
|
55
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { CancellationToken } from "../types";
|
|
2
|
+
|
|
3
|
+
// --- Cancellation ---
|
|
4
|
+
|
|
5
|
+
export function createCancellationToken(): CancellationToken {
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
let _cancelled = false;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
get cancelled() {
|
|
11
|
+
return _cancelled;
|
|
12
|
+
},
|
|
13
|
+
cancel() {
|
|
14
|
+
if (_cancelled) return;
|
|
15
|
+
_cancelled = true;
|
|
16
|
+
for (const fn of listeners) fn();
|
|
17
|
+
listeners.clear();
|
|
18
|
+
},
|
|
19
|
+
onCancel(fn: () => void) {
|
|
20
|
+
if (_cancelled) {
|
|
21
|
+
fn();
|
|
22
|
+
} else {
|
|
23
|
+
listeners.add(fn);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
offCancel(fn: () => void) {
|
|
27
|
+
listeners.delete(fn);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Cancellable sleep ---
|
|
33
|
+
|
|
34
|
+
/** Resolves `true` if elapsed normally, `false` if cancelled. */
|
|
35
|
+
export function sleep(ms: number, token: CancellationToken): Promise<boolean> {
|
|
36
|
+
if (token.cancelled) return Promise.resolve(false);
|
|
37
|
+
if (ms <= 0) return Promise.resolve(true);
|
|
38
|
+
|
|
39
|
+
return new Promise<boolean>((resolve) => {
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
token.offCancel(onCancel);
|
|
42
|
+
resolve(true);
|
|
43
|
+
}, ms);
|
|
44
|
+
|
|
45
|
+
function onCancel() {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve(false);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
token.onCancel(onCancel);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Stochastic delays ---
|
|
55
|
+
|
|
56
|
+
/** Uniform noise around base: `base * (1 + uniform(-variance, +variance))`, clamped to [min, max]. */
|
|
57
|
+
export function stochasticDelay(base: number, variance: number, min = 30, max = 600): number {
|
|
58
|
+
const noise = (Math.random() * 2 - 1) * variance;
|
|
59
|
+
return Math.max(min, Math.min(max, base * (1 + noise)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Accelerating backspace delay: gets faster with each consecutive backspace. */
|
|
63
|
+
export function backspaceDelay(
|
|
64
|
+
base: number,
|
|
65
|
+
variance: number,
|
|
66
|
+
index: number,
|
|
67
|
+
acceleration: number,
|
|
68
|
+
): number {
|
|
69
|
+
const factor = Math.max(0.3, 1 - acceleration * index);
|
|
70
|
+
const raw = base * factor;
|
|
71
|
+
return stochasticDelay(raw, variance, 20, 200);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- N-gram sizing ---
|
|
75
|
+
|
|
76
|
+
/** Pick n-gram size. Fixed number passthrough, or geometric distribution biased small. */
|
|
77
|
+
export function pickNgramSize(config: number | { min: number; max: number }): number {
|
|
78
|
+
if (typeof config === "number") return config;
|
|
79
|
+
const { min, max } = config;
|
|
80
|
+
if (min === max) return min;
|
|
81
|
+
|
|
82
|
+
// Geometric-ish: smaller sizes are more likely
|
|
83
|
+
// P(k) ~ (1/2)^(k - min)
|
|
84
|
+
let roll = Math.random();
|
|
85
|
+
for (let k = min; k < max; k++) {
|
|
86
|
+
// Each size has 50% chance of being selected vs continuing
|
|
87
|
+
if (roll < 0.5) return k;
|
|
88
|
+
roll = (roll - 0.5) * 2; // renormalize
|
|
89
|
+
}
|
|
90
|
+
return max;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Utilities ---
|
|
94
|
+
|
|
95
|
+
/** Uniform random integer in [min, max] (inclusive). */
|
|
96
|
+
export function randomInRange(min: number, max: number): number {
|
|
97
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Check if user prefers reduced motion. SSR-safe. */
|
|
101
|
+
export function prefersReducedMotion(): boolean {
|
|
102
|
+
if (typeof window === "undefined") return false;
|
|
103
|
+
return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
104
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { TypoContext, TypoAction } from "../types";
|
|
2
|
+
import { pickTypoChar } from "./keyboard";
|
|
3
|
+
import { randomInRange } from "./timing";
|
|
4
|
+
|
|
5
|
+
export interface TypoConfig {
|
|
6
|
+
maxCharsBeforeNotice: number;
|
|
7
|
+
continueAfterTypoProbability: number;
|
|
8
|
+
sequentialTypoDecay: number;
|
|
9
|
+
noticePauseMin: number;
|
|
10
|
+
noticePauseMax: number;
|
|
11
|
+
resumePauseMin: number;
|
|
12
|
+
resumePauseMax: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_TYPO_CONFIG: TypoConfig = {
|
|
16
|
+
maxCharsBeforeNotice: 4,
|
|
17
|
+
continueAfterTypoProbability: 0.6,
|
|
18
|
+
sequentialTypoDecay: 0.3,
|
|
19
|
+
noticePauseMin: 200,
|
|
20
|
+
noticePauseMax: 500,
|
|
21
|
+
resumePauseMin: 100,
|
|
22
|
+
resumePauseMax: 300,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createTypoContext(): TypoContext {
|
|
26
|
+
return {
|
|
27
|
+
state: "normal",
|
|
28
|
+
charsPastTypo: 0,
|
|
29
|
+
charsToDelete: 0,
|
|
30
|
+
sequentialTypoCount: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pure FSM transition.
|
|
36
|
+
*
|
|
37
|
+
* States: normal -> typo_injected -> typing_past -> noticed -> correcting -> resuming -> normal
|
|
38
|
+
*
|
|
39
|
+
* The caller provides the intended character(s) at the current position.
|
|
40
|
+
* Returns the action to perform and the new context.
|
|
41
|
+
*/
|
|
42
|
+
export function nextTypoAction(
|
|
43
|
+
ctx: TypoContext,
|
|
44
|
+
intendedChar: string,
|
|
45
|
+
config: TypoConfig,
|
|
46
|
+
errorRate: number,
|
|
47
|
+
): { action: TypoAction; ctx: TypoContext } {
|
|
48
|
+
switch (ctx.state) {
|
|
49
|
+
case "normal":
|
|
50
|
+
return handleNormal(ctx, intendedChar, errorRate);
|
|
51
|
+
|
|
52
|
+
case "typo_injected":
|
|
53
|
+
return handleTypoInjected(ctx, intendedChar, config);
|
|
54
|
+
|
|
55
|
+
case "typing_past":
|
|
56
|
+
return handleTypingPast(ctx, intendedChar, config);
|
|
57
|
+
|
|
58
|
+
case "noticed":
|
|
59
|
+
return handleNoticed(ctx, config);
|
|
60
|
+
|
|
61
|
+
case "correcting":
|
|
62
|
+
return handleCorrecting(ctx);
|
|
63
|
+
|
|
64
|
+
case "resuming":
|
|
65
|
+
return handleResuming(ctx, config);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleNormal(
|
|
70
|
+
ctx: TypoContext,
|
|
71
|
+
intendedChar: string,
|
|
72
|
+
errorRate: number,
|
|
73
|
+
): { action: TypoAction; ctx: TypoContext } {
|
|
74
|
+
// Sequential typos decay: each consecutive typo is less likely
|
|
75
|
+
const effectiveRate = errorRate * Math.pow(1 - 0.3, ctx.sequentialTypoCount);
|
|
76
|
+
|
|
77
|
+
if (Math.random() < effectiveRate && /[a-zA-Z]/.test(intendedChar)) {
|
|
78
|
+
const wrongChar = pickTypoChar(intendedChar);
|
|
79
|
+
return {
|
|
80
|
+
action: { type: "type_wrong", char: wrongChar, intended: intendedChar },
|
|
81
|
+
ctx: {
|
|
82
|
+
...ctx,
|
|
83
|
+
state: "typo_injected",
|
|
84
|
+
charsPastTypo: 0,
|
|
85
|
+
charsToDelete: 1, // the wrong char itself
|
|
86
|
+
sequentialTypoCount: ctx.sequentialTypoCount + 1,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
action: { type: "type_correct", char: intendedChar },
|
|
93
|
+
ctx: { ...ctx, sequentialTypoCount: 0 },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleTypoInjected(
|
|
98
|
+
ctx: TypoContext,
|
|
99
|
+
intendedChar: string,
|
|
100
|
+
config: TypoConfig,
|
|
101
|
+
): { action: TypoAction; ctx: TypoContext } {
|
|
102
|
+
// Decide: continue typing past, or notice immediately?
|
|
103
|
+
if (Math.random() < config.continueAfterTypoProbability) {
|
|
104
|
+
return {
|
|
105
|
+
action: { type: "type_past_correct", char: intendedChar },
|
|
106
|
+
ctx: {
|
|
107
|
+
...ctx,
|
|
108
|
+
state: "typing_past",
|
|
109
|
+
charsPastTypo: 1,
|
|
110
|
+
charsToDelete: ctx.charsToDelete + 1,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Notice immediately
|
|
116
|
+
return {
|
|
117
|
+
action: {
|
|
118
|
+
type: "notice",
|
|
119
|
+
pauseMs: randomInRange(config.noticePauseMin, config.noticePauseMax),
|
|
120
|
+
},
|
|
121
|
+
ctx: { ...ctx, state: "noticed" },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleTypingPast(
|
|
126
|
+
ctx: TypoContext,
|
|
127
|
+
intendedChar: string,
|
|
128
|
+
config: TypoConfig,
|
|
129
|
+
): { action: TypoAction; ctx: TypoContext } {
|
|
130
|
+
// Hard cap
|
|
131
|
+
if (ctx.charsPastTypo >= config.maxCharsBeforeNotice) {
|
|
132
|
+
return {
|
|
133
|
+
action: {
|
|
134
|
+
type: "notice",
|
|
135
|
+
pauseMs: randomInRange(config.noticePauseMin, config.noticePauseMax),
|
|
136
|
+
},
|
|
137
|
+
ctx: { ...ctx, state: "noticed" },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Roll to continue or notice
|
|
142
|
+
if (Math.random() < config.continueAfterTypoProbability) {
|
|
143
|
+
return {
|
|
144
|
+
action: { type: "type_past_correct", char: intendedChar },
|
|
145
|
+
ctx: {
|
|
146
|
+
...ctx,
|
|
147
|
+
charsPastTypo: ctx.charsPastTypo + 1,
|
|
148
|
+
charsToDelete: ctx.charsToDelete + 1,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
action: {
|
|
155
|
+
type: "notice",
|
|
156
|
+
pauseMs: randomInRange(config.noticePauseMin, config.noticePauseMax),
|
|
157
|
+
},
|
|
158
|
+
ctx: { ...ctx, state: "noticed" },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleNoticed(
|
|
163
|
+
ctx: TypoContext,
|
|
164
|
+
_config: TypoConfig,
|
|
165
|
+
): { action: TypoAction; ctx: TypoContext } {
|
|
166
|
+
return {
|
|
167
|
+
action: { type: "backspace", count: ctx.charsToDelete, frantic: true },
|
|
168
|
+
ctx: { ...ctx, state: "correcting" },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handleCorrecting(ctx: TypoContext): { action: TypoAction; ctx: TypoContext } {
|
|
173
|
+
// After backspace completes, transition to resuming
|
|
174
|
+
return {
|
|
175
|
+
action: { type: "resume", pauseMs: randomInRange(100, 300) },
|
|
176
|
+
ctx: {
|
|
177
|
+
...ctx,
|
|
178
|
+
state: "resuming",
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleResuming(
|
|
184
|
+
ctx: TypoContext,
|
|
185
|
+
_config: TypoConfig,
|
|
186
|
+
): { action: TypoAction; ctx: TypoContext } {
|
|
187
|
+
// Reset to normal
|
|
188
|
+
return {
|
|
189
|
+
action: { type: "type_correct", char: "" }, // caller will re-drive with actual char
|
|
190
|
+
ctx: {
|
|
191
|
+
...ctx,
|
|
192
|
+
state: "normal",
|
|
193
|
+
charsPastTypo: 0,
|
|
194
|
+
charsToDelete: 0,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
AccordionRoot,
|
|
4
|
+
type AccordionRootEmits,
|
|
5
|
+
type AccordionRootProps,
|
|
6
|
+
useForwardPropsEmits,
|
|
7
|
+
} from 'reka-ui'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<AccordionRootProps>()
|
|
10
|
+
const emits = defineEmits<AccordionRootEmits>()
|
|
11
|
+
|
|
12
|
+
const forwarded = useForwardPropsEmits(props, emits)
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<AccordionRoot v-bind="forwarded">
|
|
17
|
+
<slot />
|
|
18
|
+
</AccordionRoot>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type HTMLAttributes, computed } from 'vue'
|
|
3
|
+
import { AccordionContent, type AccordionContentProps } from 'reka-ui'
|
|
4
|
+
import { cn } from '../../../utils'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
|
7
|
+
|
|
8
|
+
const delegatedProps = computed(() => {
|
|
9
|
+
const { class: _, ...delegated } = props
|
|
10
|
+
|
|
11
|
+
return delegated
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<AccordionContent
|
|
17
|
+
v-bind="delegatedProps"
|
|
18
|
+
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
19
|
+
>
|
|
20
|
+
<div :class="cn('pb-4 pt-0', props.class)">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</AccordionContent>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type HTMLAttributes, computed } from 'vue'
|
|
3
|
+
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui'
|
|
4
|
+
import { cn } from '../../../utils'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
|
7
|
+
|
|
8
|
+
const delegatedProps = computed(() => {
|
|
9
|
+
const { class: _, ...delegated } = props
|
|
10
|
+
|
|
11
|
+
return delegated
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const forwardedProps = useForwardProps(delegatedProps)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<AccordionItem
|
|
19
|
+
v-bind="forwardedProps"
|
|
20
|
+
:class="cn('border-b', props.class)"
|
|
21
|
+
>
|
|
22
|
+
<slot />
|
|
23
|
+
</AccordionItem>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type HTMLAttributes, computed } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
AccordionHeader,
|
|
5
|
+
AccordionTrigger,
|
|
6
|
+
type AccordionTriggerProps,
|
|
7
|
+
} from 'reka-ui'
|
|
8
|
+
import { ChevronDown } from 'lucide-vue-next'
|
|
9
|
+
import { cn } from '../../../utils'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
|
|
12
|
+
|
|
13
|
+
const delegatedProps = computed(() => {
|
|
14
|
+
const { class: _, ...delegated } = props
|
|
15
|
+
|
|
16
|
+
return delegated
|
|
17
|
+
})
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<AccordionHeader class="flex">
|
|
22
|
+
<AccordionTrigger
|
|
23
|
+
v-bind="delegatedProps"
|
|
24
|
+
:class="
|
|
25
|
+
cn(
|
|
26
|
+
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
|
27
|
+
props.class,
|
|
28
|
+
)
|
|
29
|
+
"
|
|
30
|
+
>
|
|
31
|
+
<slot />
|
|
32
|
+
<slot name="icon">
|
|
33
|
+
<ChevronDown
|
|
34
|
+
class="h-4 w-4 shrink-0 transition-transform duration-200"
|
|
35
|
+
/>
|
|
36
|
+
</slot>
|
|
37
|
+
</AccordionTrigger>
|
|
38
|
+
</AccordionHeader>
|
|
39
|
+
</template>
|