From 85313c60464a888a7bf3f919844b72668c58a308 Mon Sep 17 00:00:00 2001 From: robonen Date: Mon, 3 Aug 2026 21:11:47 +0700 Subject: [PATCH] feat: update useSnapPoints to improve drawer snapping behavior and add new features --- core/stdlib/package.json | 2 +- .../behavioral/StateMachine/index.test-d.ts | 24 + .../behavioral/StateMachine/index.test.ts | 2 + .../patterns/behavioral/StateMachine/types.ts | 6 +- .../src/overlays/drawer/DrawerClose.vue | 25 + .../src/overlays/drawer/DrawerContent.vue | 32 +- .../src/overlays/drawer/DrawerHandle.vue | 122 ++- .../src/overlays/drawer/DrawerOverlay.vue | 3 +- .../src/overlays/drawer/DrawerRoot.vue | 81 +- .../src/overlays/drawer/DrawerRootNested.vue | 5 +- .../src/overlays/drawer/DrawerTrigger.vue | 25 + .../overlays/drawer/__test__/Drawer.test.ts | 570 +++++++++++++- ...t-drag-on-the-handle-ends-in-a-click-1.png | Bin 0 -> 3023 bytes .../src/overlays/drawer/__test__/a11y.test.ts | 74 ++ .../overlays/drawer/__test__/gesture.test.ts | 224 ++++++ .../overlays/drawer/__test__/snapping.test.ts | 192 +++++ .../src/overlays/drawer/constants.ts | 30 + vue/primitives/src/overlays/drawer/context.ts | 48 +- .../src/overlays/drawer/controls.ts | 707 ++++++++++++------ vue/primitives/src/overlays/drawer/gesture.ts | 178 +++++ vue/primitives/src/overlays/drawer/helpers.ts | 37 + vue/primitives/src/overlays/drawer/index.ts | 10 +- .../src/overlays/drawer/snapping.ts | 184 +++++ vue/primitives/src/overlays/drawer/style.ts | 30 +- vue/primitives/src/overlays/drawer/types.ts | 25 +- .../src/overlays/drawer/usePositionFixed.ts | 2 +- .../src/overlays/drawer/useScaleBackground.ts | 15 +- .../src/overlays/drawer/useSnapPoints.ts | 251 ++++--- vue/toolkit/package.json | 2 +- .../composables/sensors/onLongPress/index.ts | 6 + .../src/composables/state/activeApp/demo.vue | 71 ++ .../composables/state/activeApp/index.test.ts | 176 +++++ .../src/composables/state/activeApp/index.ts | 150 ++++ vue/toolkit/src/composables/state/index.ts | 2 + .../state/useStateMachine/demo.vue | 60 ++ .../state/useStateMachine/index.test.ts | 184 +++++ .../state/useStateMachine/index.ts | 122 +++ 37 files changed, 3216 insertions(+), 461 deletions(-) create mode 100644 vue/primitives/src/overlays/drawer/DrawerClose.vue create mode 100644 vue/primitives/src/overlays/drawer/DrawerTrigger.vue create mode 100644 vue/primitives/src/overlays/drawer/__test__/__screenshots__/Drawer.test.ts/Drawer---handle-does-not-cycle-when-a-short-drag-on-the-handle-ends-in-a-click-1.png create mode 100644 vue/primitives/src/overlays/drawer/__test__/a11y.test.ts create mode 100644 vue/primitives/src/overlays/drawer/__test__/gesture.test.ts create mode 100644 vue/primitives/src/overlays/drawer/__test__/snapping.test.ts create mode 100644 vue/primitives/src/overlays/drawer/gesture.ts create mode 100644 vue/primitives/src/overlays/drawer/snapping.ts create mode 100644 vue/toolkit/src/composables/state/activeApp/demo.vue create mode 100644 vue/toolkit/src/composables/state/activeApp/index.test.ts create mode 100644 vue/toolkit/src/composables/state/activeApp/index.ts create mode 100644 vue/toolkit/src/composables/state/useStateMachine/demo.vue create mode 100644 vue/toolkit/src/composables/state/useStateMachine/index.test.ts create mode 100644 vue/toolkit/src/composables/state/useStateMachine/index.ts diff --git a/core/stdlib/package.json b/core/stdlib/package.json index 79d0dc5..ae85cec 100644 --- a/core/stdlib/package.json +++ b/core/stdlib/package.json @@ -1,6 +1,6 @@ { "name": "@robonen/stdlib", - "version": "0.0.11", + "version": "0.0.12", "license": "Apache-2.0", "description": "A collection of tools, utilities, and helpers for TypeScript", "keywords": [ diff --git a/core/stdlib/src/patterns/behavioral/StateMachine/index.test-d.ts b/core/stdlib/src/patterns/behavioral/StateMachine/index.test-d.ts index 361aec2..0b3923d 100644 --- a/core/stdlib/src/patterns/behavioral/StateMachine/index.test-d.ts +++ b/core/stdlib/src/patterns/behavioral/StateMachine/index.test-d.ts @@ -21,4 +21,28 @@ describe('createMachine', () => { it('send returns the (typed) resulting state', () => { expectTypeOf(machine.send('START')).toEqualTypeOf<'idle' | 'running'>(); }); + + it('empty terminal nodes do not widen the event union to string', () => { + const terminal = createMachine({ + initial: 'idle', + states: { + idle: { on: { START: 'done' } }, + done: {}, + }, + }); + + expectTypeOf(terminal.send).parameter(0).toEqualTypeOf<'START'>(); + }); + + it('entry/exit-only nodes do not widen the event union either', () => { + const hooked = createMachine({ + initial: 'idle', + states: { + idle: { on: { START: 'done' } }, + done: { entry: () => {} }, + }, + }); + + expectTypeOf(hooked.send).parameter(0).toEqualTypeOf<'START'>(); + }); }); diff --git a/core/stdlib/src/patterns/behavioral/StateMachine/index.test.ts b/core/stdlib/src/patterns/behavioral/StateMachine/index.test.ts index 4fcdffd..66a80ac 100644 --- a/core/stdlib/src/patterns/behavioral/StateMachine/index.test.ts +++ b/core/stdlib/src/patterns/behavioral/StateMachine/index.test.ts @@ -418,6 +418,7 @@ describe('asyncStateMachine', () => { }, }); + // @ts-expect-error -- deliberately undeclared event: runtime must ignore it const result = await machine.send('STOP'); expect(result).toBe('idle'); @@ -597,6 +598,7 @@ describe('asyncStateMachine', () => { }, }); + // @ts-expect-error -- deliberately undeclared event: can() must report false expect(await machine.can('STOP')).toBe(false); }); diff --git a/core/stdlib/src/patterns/behavioral/StateMachine/types.ts b/core/stdlib/src/patterns/behavioral/StateMachine/types.ts index a4ef161..5f376ff 100644 --- a/core/stdlib/src/patterns/behavioral/StateMachine/types.ts +++ b/core/stdlib/src/patterns/behavioral/StateMachine/types.ts @@ -57,8 +57,12 @@ export type AsyncStateNodeConfig = StateNodeConfig = keyof T & string; +// `on` is matched as REQUIRED here on purpose: an empty terminal node (`{}`) +// satisfies an optional-`on` pattern with no inference candidate, so `infer E` +// would fall back to its constraint and collapse the whole union to `string`, +// silently accepting any event name in `send`/`can`. export type ExtractEvents = { - [K in keyof T]: T[K] extends { readonly on?: Readonly> } + [K in keyof T]: T[K] extends { readonly on: Readonly> } ? E : never; }[keyof T]; diff --git a/vue/primitives/src/overlays/drawer/DrawerClose.vue b/vue/primitives/src/overlays/drawer/DrawerClose.vue new file mode 100644 index 0000000..3696e89 --- /dev/null +++ b/vue/primitives/src/overlays/drawer/DrawerClose.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/vue/primitives/src/overlays/drawer/DrawerContent.vue b/vue/primitives/src/overlays/drawer/DrawerContent.vue index 193a0e9..afacdfb 100644 --- a/vue/primitives/src/overlays/drawer/DrawerContent.vue +++ b/vue/primitives/src/overlays/drawer/DrawerContent.vue @@ -14,6 +14,7 @@ export type DrawerContentEmits = DialogContentEmits;