diff --git a/.changeset/reclaim-natural-gesture-names-3363.md b/.changeset/reclaim-natural-gesture-names-3363.md new file mode 100644 index 0000000000..2b9da77a1a --- /dev/null +++ b/.changeset/reclaim-natural-gesture-names-3363.md @@ -0,0 +1,58 @@ +--- +"@object-ui/types": minor +"@object-ui/mobile": minor +--- + +Reclaim the natural names `GestureType` and `GestureConfig` (objectui#3363). + +`@objectstack/spec` 17.0.0-rc.3 deleted the whole `ui/touch` module +(objectstack#4988, PR objectstack#5321), vacating three names objectui had +renamed **away from** in objectstack#4115 purely to avoid a collision. Two of +those workarounds have now outlived their reason and are undone. + +## Breaking, in FROM → TO form + +- `TouchGestureType` → **`GestureType`** — objectui's direction-fused recogniser + vocabulary (`tap`, `swipe-left`, `swipe-up`, …). +- `TouchGestureConfig` → **`GestureConfig`** — the flat gesture→`action` handler + binding. + +Both are exported from `@object-ui/types` and re-exported by `@object-ui/mobile`. +Nothing about either shape changed: same members, same optionality. Consumers +import the new name; there is no other edit. + +**The old names are gone, not deprecated.** This follows the precedent set by the +objectstack#4115 rename batch that introduced them, whose own migration note reads: +"an alias would preserve exactly the ambiguity being removed". A deprecated alias +would be worse here than in the general case, because the ambiguity these renames +exist to prevent is between two same-named types — leaving `TouchGestureType` +alive next to `GestureType` restores the two-spellings-one-concept problem while +claiming to retire it. + +The retired spec vocabulary that used to hold these names still lives in +`@object-ui/types`' `mobile` module under its deliberate `Spec…` prefix +(`SpecGestureType`, `SpecGestureConfig`, `SwipeGestureConfig`, …), and that prefix +is untouched — it is now the only thing distinguishing the two contracts, so +`useSpecGesture` still maps one onto the other exactly as before. + +## `PWAOfflineConfig` is deliberately NOT reclaimed + +The spec vacated `OfflineConfig` in the same retirement, but the spec was never +its only claimant: that rename was a **cross-package arbitration between two +objectui packages**, and `@object-ui/react` won it. `useOffline`'s config is the +offline data/sync model key for key, so it holds the bare `OfflineConfig`, while +this package's service-worker route cache stays `PWAOfflineConfig` +(objectui#3156 / objectui#3159). + +Before objectui#3560 that name reached `@object-ui/react` from the spec, so the +spec-side tripwire covered it by accident. Since the retirement it is declared +locally in `packages/react/src/hooks/useOffline.ts`, which means the spec's +vacancy no longer says anything about whether the name is free — it is not. +Reclaiming it would put two different `OfflineConfig` shapes on the public +surface of two packages that are routinely imported together, which is the exact +ambiguity objectstack#4115 renamed it away from. + +`page-nav-misc-spec-parity.test.ts` now pins that reason directly instead of +leaving it as prose: it asserts `@object-ui/react` still declares +`OfflineConfig`, and its failure message tells the next reader that the reclaim +has become available if it ever stops. diff --git a/packages/mobile/src/index.ts b/packages/mobile/src/index.ts index fd31012a9b..732293a1e1 100644 --- a/packages/mobile/src/index.ts +++ b/packages/mobile/src/index.ts @@ -63,8 +63,8 @@ export type { FetchCacheStrategy, PWAOfflineConfig, OfflineRoute, - TouchGestureType, - TouchGestureConfig, + GestureType, + GestureConfig, GestureContext, MobileComponentConfig, SpecGestureConfig, diff --git a/packages/mobile/src/useGesture.ts b/packages/mobile/src/useGesture.ts index ff8b5027ee..d9dda9fa4a 100644 --- a/packages/mobile/src/useGesture.ts +++ b/packages/mobile/src/useGesture.ts @@ -7,11 +7,11 @@ */ import { useEffect, useRef, useCallback } from 'react'; -import type { TouchGestureType, GestureContext } from '@object-ui/types'; +import type { GestureType, GestureContext } from '@object-ui/types'; export interface UseGestureOptions { /** Gesture type to detect */ - type: TouchGestureType; + type: GestureType; /** Callback when gesture is detected */ onGesture: (context: GestureContext) => void; /** Minimum distance for swipe detection (pixels) */ diff --git a/packages/mobile/src/useSpecGesture.ts b/packages/mobile/src/useSpecGesture.ts index 264e9d9673..7d5e94e2c6 100644 --- a/packages/mobile/src/useSpecGesture.ts +++ b/packages/mobile/src/useSpecGesture.ts @@ -7,7 +7,7 @@ */ import { useGesture } from './useGesture'; -import type { TouchGestureType, SpecGestureConfig } from '@object-ui/types'; +import type { GestureType, SpecGestureConfig } from '@object-ui/types'; export interface UseSpecGestureOptions { /** Spec gesture configuration */ @@ -28,7 +28,7 @@ export interface UseSpecGestureOptions { onGesture?: (context: { type: string; direction?: string; scale?: number; rotation?: number }) => void; } -const SWIPE_DIRECTION_MAP: Record = { +const SWIPE_DIRECTION_MAP: Record = { left: 'swipe-left', right: 'swipe-right', up: 'swipe-up', @@ -36,8 +36,11 @@ const SWIPE_DIRECTION_MAP: Record = { }; /** - * Spec `GestureTypeSchema` (`ui/touch.zod.ts`) → the recognizer type - * `useGesture` implements. The spec's `drag` and `pan` are one recognizer + * `SPEC_GESTURE_TYPES` (the retired `ui/touch` vocabulary, owned by + * `@object-ui/types` since objectstack#4988) → the recognizer + * {@link GestureType} `useGesture` implements. Note the two sides are + * different vocabularies, which is why this map exists at all: the retired + * spec's `drag` and `pan` are one recognizer * (any-direction move past the threshold); `swipe` resolves per configured * direction, so it maps through {@link SWIPE_DIRECTION_MAP} instead. * Exported for the spec-parity test. @@ -47,7 +50,7 @@ const SWIPE_DIRECTION_MAP: Record = { * `pan` / `drag` / `rotate` / `double_tap` (types with no sub-object) all * fell through to the `'tap'` initializer and fired on a tap. */ -export const SPEC_GESTURE_TYPE_MAP: Record = { +export const SPEC_GESTURE_TYPE_MAP: Record = { swipe: 'swipe-left', // per-direction; resolved via SWIPE_DIRECTION_MAP pinch: 'pinch', long_press: 'long-press', @@ -58,8 +61,9 @@ export const SPEC_GESTURE_TYPE_MAP: Record = { }; /** - * Spec-aware gesture hook that maps an @objectstack/spec GestureConfig - * to the existing useGesture hook. + * Spec-aware gesture hook that maps a {@link SpecGestureConfig} — the retired + * `@objectstack/spec` `ui/touch` shape, not this package's own + * `GestureConfig` — onto the existing useGesture hook. * * @example * ```tsx @@ -90,7 +94,7 @@ export function useSpecGesture( ? 'pinch' : undefined; - let gestureType: TouchGestureType = 'tap'; + let gestureType: GestureType = 'tap'; let threshold: number | undefined; let longPressDuration: number | undefined; let onGesture: (ctx: { direction?: string; scale?: number; rotation?: number }) => void = () => {}; diff --git a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts index a5cbacccb7..6509dd657d 100644 --- a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts +++ b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts @@ -27,6 +27,12 @@ * spec's export set, and every OLD name asserted still present — a rename * whose reason has evaporated should give the natural name back. * + * Two of those nine have since been given back: `GestureType` and + * `GestureConfig` were reclaimed in objectui#3363 after objectstack#4988 + * deleted `ui/touch`. A third, `OfflineConfig`, was vacated by the spec too but + * deliberately NOT reclaimed — `@object-ui/react` owns that name in-repo, and + * that reason is pinned below rather than left as prose. + * * Type-level assertions here are real gates: `tsconfig.test.json` compiles this * file, unlike the package build (see its header for why that distinction was * itself a bug once). @@ -36,6 +42,7 @@ import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; import ts from 'typescript'; import type { z } from 'zod'; import { NavigationAreaSchema as SpecNavigationAreaSchema } from '@objectstack/spec/ui'; @@ -564,39 +571,74 @@ describe('renamed local dialects do not collide with a spec export (objectui#307 }); /** - * THE TRIPWIRE FIRED, exactly as designed. `@objectstack/spec` 17.0.0-rc.3 - * deleted the whole `ui/touch` and `ui/offline` modules (objectstack#4988, - * PR objectstack#5321), so `GestureType`, `GestureConfig` and `OfflineConfig` - * moved from the first list to this one: the spec no longer owns them, and - * the three local dialects are free to take their natural names back. + * THE TRIPWIRE FIRED, exactly as designed, and objectui#3363 acted on it. + * `@objectstack/spec` 17.0.0-rc.3 deleted the whole `ui/touch` and + * `ui/offline` modules (objectstack#4988, PR objectstack#5321), so + * `GestureType`, `GestureConfig` and `OfflineConfig` moved from the first + * list to this one: the spec stopped owning all three. + * + * Two of the three were then RECLAIMED — `TouchGestureType` → `GestureType` + * and `TouchGestureConfig` → `GestureConfig` now carry the natural names in + * `@object-ui/types`' `mobile` module, so a workaround does not outlive its + * reason (objectui#3169). These rows keep asserting the same thing they did + * before the reclaim, and that is the point: they are now what makes the + * reclaimed names SAFE, not merely available. * - * The rename itself is objectui#3363's unlock item and is deliberately NOT - * done here — this is a dependency bump, and `TouchGestureType` → - * `GestureType` is a public-surface rename across `@object-ui/types`, - * `@object-ui/mobile` and every consumer, which deserves its own PR and its - * own changeset. What this block does is record that the reason for the - * workaround has expired, so the next reader finds the unlock rather than a - * deleted assertion. + * The third did not move — see the `OfflineConfig` block below. */ it.each([ ['GestureType', 'TouchGestureType'], ['GestureConfig', 'TouchGestureConfig'], - ['OfflineConfig', 'PWAOfflineConfig'], ])( - 'the spec has VACATED `%s` — `%s` may reclaim it (objectui#3363)', - (vacated) => { + 'the spec still does not own `%s`, reclaimed from `%s` (objectui#3363)', + (reclaimed) => { expect( names, - `spec owns '${vacated}' again — the local dialect rename is load-bearing once more, ` + - `move this row back to the list above and close objectui#3363's unlock item.`, - ).not.toContain(vacated); + `spec owns '${reclaimed}' again, and @object-ui/types now exports that ` + + `exact name — this is a live collision, not a latent one. Re-triage ` + + `(objectstack#4115): derive from the spec, or rename the local dialect back.`, + ).not.toContain(reclaimed); }, ); + /** + * `OfflineConfig` is the one the spec vacated that objectui did NOT reclaim, + * and the reason is worth pinning rather than remembering: **the spec was + * never the only claimant**. That rename was a cross-package arbitration + * between two objectui packages, and `@object-ui/react` won it — its + * `useOffline` config IS the offline data/sync model, key for key, so it + * holds the bare name while this package's service-worker route cache stays + * `PWAOfflineConfig` (objectui#3156 / objectui#3159). + * + * Before objectui#3560 that name reached `@object-ui/react` from the spec, so + * the spec-side assertion above covered it by accident. Since the retirement + * it is declared locally, which is exactly why it needs its own pin: the + * spec's vacancy no longer says anything about whether the name is free. If + * `@object-ui/react` ever drops or renames `OfflineConfig`, this goes red and + * the reclaim genuinely IS available — that is the unlock signal, and it now + * points at the right repository instead of at the spec. + */ + it('`@object-ui/react` still owns `OfflineConfig`, so `PWAOfflineConfig` keeps its prefix (objectui#3363)', () => { + // A source read, not an import: `@object-ui/types` has zero deps and must + // not take one on `@object-ui/react`. Same instrument the sibling + // `spec-ui-schema-reexports.test.ts` uses to inspect source it cannot load. + const useOfflinePath = resolve( + dirname(fileURLToPath(import.meta.url)), + '../../../react/src/hooks/useOffline.ts', + ); + const src = readFileSync(useOfflinePath, 'utf8'); + expect( + /^export interface OfflineConfig\b/m.test(src), + `${useOfflinePath} no longer declares 'export interface OfflineConfig'. If the ` + + `name was dropped or moved, the cross-package reason for 'PWAOfflineConfig' is ` + + `gone — the spec vacated 'OfflineConfig' back in objectstack#4988, so the ` + + `natural name is now free and @object-ui/types' mobile module may reclaim it ` + + `(objectui#3363 reclaimed 'GestureType'/'GestureConfig' the same way).`, + ).toBe(true); + }); + it.each([ ['UploadedFileMetadata', 'file-field VALUE payload, not the storage file record'], - ['TouchGestureType', 'direction-fused recognizer vocabulary (`swipe-left`, …)'], - ['TouchGestureConfig', 'gesture→action binding, not the spec per-gesture tuning'], ['PWAOfflineConfig', 'service-worker route caching, not the offline data model'], ['PageNodeRegion', 'region of the objectui page NODE, holding renderer nodes'], ['PageNodeRegionSchema', 'zod twin of PageNodeRegion'], diff --git a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts index 2407779830..3412519ffd 100644 --- a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts +++ b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts @@ -52,7 +52,11 @@ const DROPPED_SCHEMA_EXPORTS = [ // Notifications 'NotificationSchema', 'NotificationConfigSchema', - 'NotificationActionSchema', + // `NotificationActionSchema` removed (objectui#3362 residue, closed out on + // objectui#3363): objectstack#5015 / PR objectstack#5300 RETIRED + // `NotificationAction` from the spec outright. A deny-list entry for a name + // the spec no longer publishes asserts nothing about this package's decision + // — nothing could re-export it — so the row passed as a tautology. 'NotificationPositionSchema', 'NotificationSeveritySchema', 'NotificationTypeSchema', @@ -93,7 +97,9 @@ const DROPPED_SCHEMA_EXPORTS = [ 'WidgetColorVariantSchema', // Sharing & Embedding 'SharingConfigSchema', - 'EmbedConfigSchema', + // `EmbedConfigSchema` removed for the same reason as + // `NotificationActionSchema` above — `EmbedConfig` was retired by + // objectstack#5015 / PR objectstack#5300. // View Configuration 'AddRecordConfigSchema', 'AppearanceConfigSchema', diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2c732f7c2f..6b77dbef79 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -562,8 +562,12 @@ export type { FetchCacheStrategy, PWAOfflineConfig, OfflineRoute, - TouchGestureType, - TouchGestureConfig, + // `GestureType` / `GestureConfig` reclaimed their natural names in + // objectui#3363 once `@objectstack/spec` deleted `ui/touch` + // (objectstack#4988). `PWAOfflineConfig` above deliberately did NOT — see + // its note in `./mobile`; `@object-ui/react`'s `useOffline` owns that name. + GestureType, + GestureConfig, GestureContext, MobileComponentConfig, // The retired `@objectstack/spec/ui` touch vocabulary, now owned here — diff --git a/packages/types/src/mobile.ts b/packages/types/src/mobile.ts index 0c512afe8c..ff21f0dec0 100644 --- a/packages/types/src/mobile.ts +++ b/packages/types/src/mobile.ts @@ -148,7 +148,21 @@ export type FetchCacheStrategy = 'cache-first' | 'network-first' | 'stale-while- * `useOffline` config, which keeps the spec's name (and its ledger entry, * objectui#3159) precisely because it IS that concept. * - * Tripwire: `__tests__/page-nav-misc-spec-parity.test.ts`. + * **The prefix is NOT reclaimable, unlike its two gesture siblings** + * (objectui#3363). `@objectstack/spec` did vacate `OfflineConfig` when + * `ui/offline` was deleted (objectstack#4988, PR objectstack#5321) — but the + * spec was never the only claimant. This rename was a CROSS-PACKAGE + * arbitration between two objectui packages, and `@object-ui/react` won it: + * `useOffline`'s config is the offline DATA model key for key, so it holds + * `OfflineConfig`. Since objectui#3560 that name is declared locally in + * `packages/react/src/hooks/useOffline.ts` rather than re-exported from the + * spec, so the retirement did not free it — it only changed who owns it. + * Taking it back here would put two different `OfflineConfig` shapes on the + * public surface of two packages that are routinely imported together, which + * is the exact ambiguity objectstack#4115 renamed this away from. + * + * Tripwire: `__tests__/page-nav-misc-spec-parity.test.ts` (both the spec side + * and the `@object-ui/react` owner, so neither reason can expire unnoticed). */ export interface PWAOfflineConfig { /** Enable offline support */ @@ -188,35 +202,45 @@ export interface OfflineRoute { /** * Touch gesture types — objectui's **direction-fused** gesture vocabulary. * - * Renamed off the spec's `GestureType` name (objectstack#4115): the spec models - * a gesture and its direction separately (`swipe | pinch | long_press | - * double_tap | drag | rotate | pan`, with direction inside - * `GestureConfig.swipe.direction`), while objectui folds direction into the - * name (`swipe-left`, `swipe-up`, …). The two unions therefore agree on only - * three members, and neither is a subset of the other — objectui has `tap`, the - * spec has `drag`. + * Held the prefixed name `TouchGestureType` from objectstack#4115 until + * objectui#3363: `@objectstack/spec` owned `GestureType`, and the two unions + * agree on only three members (the spec modelled gesture and direction + * separately — `swipe | pinch | long_press | double_tap | drag | rotate | pan`, + * with direction inside its `GestureConfig.swipe.direction` — while objectui + * folds direction into the name: `swipe-left`, `swipe-up`, …). Neither was a + * subset of the other; objectui has `tap`, the spec had `drag`. * - * Tripwire: `__tests__/page-nav-misc-spec-parity.test.ts`. + * `@objectstack/spec` 17.0.0-rc.3 deleted the whole `ui/touch` module + * (objectstack#4988, PR objectstack#5321), vacating the name, so the natural + * name is reclaimed here rather than letting the workaround outlive its reason + * (objectui#3169). The retired spec vocabulary still lives in this file, under + * the deliberately prefixed {@link SpecGestureType} — that prefix is what now + * carries the distinction the `Touch` prefix used to. + * + * Tripwire: `__tests__/page-nav-misc-spec-parity.test.ts` — it fails if the + * spec ever claims `GestureType` back. */ -export type TouchGestureType ='tap' | 'double-tap' | 'long-press' | 'swipe-left' | 'swipe-right' | 'swipe-up' | 'swipe-down' | 'pinch' | 'rotate' | 'pan'; +export type GestureType ='tap' | 'double-tap' | 'long-press' | 'swipe-left' | 'swipe-right' | 'swipe-up' | 'swipe-down' | 'pinch' | 'rotate' | 'pan'; /** - * Gesture handler configuration — binds one {@link TouchGestureType} to an + * Gesture handler configuration — binds one {@link GestureType} to an * action name. * - * Renamed off the spec's `GestureConfig` name (objectstack#4115) for the same - * reason as its `type` field: the spec's `GestureConfig` is a per-gesture - * TUNING record (`{ type, label, enabled, swipe: { direction, threshold, - * velocity }, pinch: { minScale, maxScale }, longPress: { duration, - * moveTolerance } }`) with no notion of what the gesture DOES. This one is a - * handler binding: flat, and its whole point is `action`, which the spec's has - * no room for. + * Held the prefixed name `TouchGestureConfig` from objectstack#4115 until + * objectui#3363, for the same reason as its `type` field: the spec's + * `GestureConfig` was a per-gesture TUNING record (`{ type, label, enabled, + * swipe: { direction, threshold, velocity }, pinch: { minScale, maxScale }, + * longPress: { duration, moveTolerance } }`) with no notion of what the gesture + * DOES. This one is a handler binding: flat, and its whole point is `action`, + * which the spec's had no room for. That shape did not go away — it is + * {@link SpecGestureConfig} below, now owned by this package — but the spec no + * longer exports the bare name, so the dialect takes it back. * * Tripwire: `__tests__/page-nav-misc-spec-parity.test.ts`. */ -export interface TouchGestureConfig { +export interface GestureConfig { /** Gesture type */ - type: TouchGestureType; + type: GestureType; /** Action to execute */ action: string; /** Minimum distance for swipe gestures (pixels) */ @@ -232,7 +256,7 @@ export interface TouchGestureConfig { /** Touch gesture context */ export interface GestureContext { /** Gesture type that was detected */ - type: TouchGestureType; + type: GestureType; /** Start position */ startPosition: { x: number; y: number }; /** End position */ @@ -258,7 +282,7 @@ export interface MobileComponentConfig { /** Mobile-specific overrides */ mobileOverrides?: MobileOverrides; /** Touch gesture handlers */ - gestures?: TouchGestureConfig[]; + gestures?: GestureConfig[]; /** Pull-to-refresh configuration */ pullToRefresh?: { enabled: boolean; @@ -290,12 +314,13 @@ export interface MobileComponentConfig { // only implementations of these semantics in the repo, so this package is now // their owner. Nothing about either hook's behaviour changes. // -// The `Spec…` prefix on {@link SpecGestureConfig} is kept deliberately. It -// still distinguishes this shape from the sibling {@link TouchGestureConfig} -// dialect above — a DIFFERENT contract with different members (`swipe-left` vs -// `swipe` + a direction array). objectui#3363 records reclaiming the natural -// names as an unlock now that the spec has vacated them; that is a rename with -// its own blast radius and stays on that card, not on a dependency bump. +// The `Spec…` prefix on {@link SpecGestureConfig} is kept deliberately, and +// objectui#3363 has now made it the ONLY thing carrying the distinction: the +// sibling dialect above shed its own `Touch` prefix and is plain +// {@link GestureConfig} / {@link GestureType}. The two are still a DIFFERENT +// contract with different members (`swipe-left` vs `swipe` + a direction +// array), so both prefixed names below stay exactly as they are — dropping +// `Spec…` too would collapse the pair the rename just made legible. /** * Gesture kinds the retired `ui/touch` vocabulary recognised.