@mt-gloss/ui 0.0.42 → 0.0.44

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.
@@ -0,0 +1,13 @@
1
+ import { default as React } from 'react';
2
+ export interface DevTuningPanelProps {
3
+ /**
4
+ * Reserved — optional DOM portal target. Not currently used; panel is
5
+ * position:fixed so it does not need a portal. Included in the public
6
+ * interface for forward compatibility.
7
+ */
8
+ container?: HTMLElement;
9
+ }
10
+ export declare function DevTuningPanel(_props?: DevTuningPanelProps): React.ReactElement | null;
11
+ export declare namespace DevTuningPanel {
12
+ var displayName: string;
13
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Phase 19 Plan 02 — DevTuningPanel barrel export.
3
+ *
4
+ * Re-exports the dev-only Resize Tuning primitive + its localStorage-backed
5
+ * hook. Barrel is zero-side-effect; the SCSS is imported from
6
+ * `DevTuningPanel.tsx` so consumers that only import the hook pay no CSS cost.
7
+ */
8
+ export { DevTuningPanel } from './DevTuningPanel';
9
+ export type { DevTuningPanelProps } from './DevTuningPanel';
10
+ export { useResizeTuning, TUNING_STORAGE_KEY, TUNING_DEFAULTS } from './useResizeTuning';
11
+ export type { UseResizeTuningReturn, TuningConfig } from './useResizeTuning';
@@ -0,0 +1,47 @@
1
+ /** D-03 — exact localStorage key. */
2
+ export declare const TUNING_STORAGE_KEY = "resize.tuning.dev";
3
+ /**
4
+ * TUNING_DEFAULTS extends ResizePhysicsConfig with px-expressed snap thresholds
5
+ * that the UI surfaces (UI-SPEC §Surface 3 rows 4 & 5). The hook consumer
6
+ * converts to fractions at drag-start (`snapInFraction = snapInPx / step`).
7
+ * Defaults match DEFAULT_RESIZE_PHYSICS fraction math against the 180px GRID_STEP:
8
+ * snapInPx = 180 × 1/3 = 60
9
+ * snapOutPx = 180 × 1/2 = 90
10
+ */
11
+ /**
12
+ * Raw defaults — declared as `as const` so the shape's keys are pinned, but
13
+ * `TuningConfig` below widens each field to `number` so downstream setters
14
+ * accept the full slider range (e.g. `snapOutPx: 110`), not just the literal
15
+ * default (`snapOutPx: 90`).
16
+ */
17
+ declare const _TUNING_DEFAULTS_LITERAL: {
18
+ readonly snapInPx: 60;
19
+ readonly snapOutPx: 90;
20
+ readonly stiffness: 500;
21
+ readonly damping: 35;
22
+ readonly mass: 1;
23
+ readonly snapInFraction: number;
24
+ readonly snapOutFraction: number;
25
+ readonly blueprintOpacity: 0.25;
26
+ readonly blueprintFadeInMs: 150;
27
+ readonly blueprintFadeOutMs: 120;
28
+ readonly rippleStaggerMs: 50;
29
+ readonly velocityMultiplier: 1;
30
+ };
31
+ export type TuningConfig = {
32
+ -readonly [K in keyof typeof _TUNING_DEFAULTS_LITERAL]: number;
33
+ };
34
+ /** Public defaults — widened to `TuningConfig` so consumers can reassign. */
35
+ export declare const TUNING_DEFAULTS: TuningConfig;
36
+ export interface UseResizeTuningReturn {
37
+ /** Current tuning values (defaults merged with any persisted deltas). */
38
+ values: TuningConfig;
39
+ /** Update a single key; persists to localStorage. */
40
+ setValue<K extends keyof TuningConfig>(key: K, value: TuningConfig[K]): void;
41
+ /** Restore a single key to its TUNING_DEFAULTS value. */
42
+ resetRow<K extends keyof TuningConfig>(key: K): void;
43
+ /** Restore every key to TUNING_DEFAULTS. */
44
+ resetAll(): void;
45
+ }
46
+ export declare function useResizeTuning(): UseResizeTuningReturn;
47
+ export {};
@@ -1,4 +1,4 @@
1
- import { ResizeState } from './useEdgeHoverResize';
1
+ import { ResizeState, ResizeDirection } from './useEdgeHoverResize';
2
2
  export interface EdgeHoverHandleProps {
3
3
  state: ResizeState;
4
4
  onPointerDown: (e: React.PointerEvent) => void;
@@ -11,16 +11,23 @@ export interface EdgeHoverHandleProps {
11
11
  */
12
12
  onPointerEnter?: () => void;
13
13
  onPointerLeave?: (e: React.PointerEvent) => void;
14
- /** Which card edge the handle is positioned on. Default: 'right' */
15
- edge?: 'right' | 'left';
14
+ /**
15
+ * D-13 — right-edge resize only. Left-edge support dropped (CONTEXT D-13);
16
+ * narrowed to the literal 'right'. Prop retained so downstream data-edge
17
+ * attributes and future bidirectional revival can slot in without a
18
+ * breaking API change.
19
+ */
20
+ edge?: 'right';
21
+ /** D-16 — keyboard step handler (ArrowLeft → 'shrink', ArrowRight → 'grow'). */
22
+ onKeyboardStep?: (direction: ResizeDirection) => void;
16
23
  ariaLabel?: string;
17
24
  }
18
25
  /**
19
26
  * EdgeHoverHandle — Corner/edge handle affordance for the resize gesture.
20
27
  *
21
28
  * Visibility is derived from resize state: only visible when state is
22
- * 'handle-visible' or 'dragging'. Invisible at 'idle' to avoid colliding
23
- * with the chip-menu click target on the value/row area.
29
+ * 'handle-visible', 'dragging', or 'settling'. Invisible at 'idle' to avoid
30
+ * colliding with the chip-menu click target on the value/row area.
24
31
  *
25
32
  * Motion contract (AN-06): opacity transitions driven by edge-hover.scss
26
33
  * using enterMs=180ms (enter) / exitMs=120ms (exit via ghost-preview).
@@ -1,8 +1,11 @@
1
+ import { MotionValue } from 'framer-motion';
1
2
  export interface GhostPreviewProps {
2
- /** Whether the ghost overlay is currently visible (active during drag). */
3
+ /** Whether the ghost overlay is currently visible (active during drag/settling). */
3
4
  visible: boolean;
4
5
  /** The target column span being previewed. */
5
6
  targetColSpan: 1 | 2 | 3;
7
+ /** D-07 — MotionValue driving translateX. Zero React re-renders per pointermove. */
8
+ motionX: MotionValue<number>;
6
9
  /** AN-06 enter duration in ms. Default: 180ms */
7
10
  enterMs?: number;
8
11
  /** AN-06 exit duration in ms. Default: 120ms */
@@ -11,14 +14,14 @@ export interface GhostPreviewProps {
11
14
  /**
12
15
  * GhostPreview — Dashed outline overlay rendered during an active resize drag.
13
16
  *
14
- * Provides visual feedback of the target card size before the user commits
15
- * by releasing the pointer. Uses the same ghost-preview vocabulary as the
16
- * existing chart-area resize affordance.
17
+ * Phase 19 evolution: consumes a framer-motion MotionValue for translateX
18
+ * (D-07) so ghost tracks the cursor 1:1 without React re-renders on
19
+ * pointermove (AP-17). Opacity transitions still use CSS enterMs/exitMs.
17
20
  *
18
21
  * Hidden via aria-hidden — purely decorative visual feedback.
19
22
  * Motion contract (AN-06): enter 180ms, exit 120ms.
20
23
  */
21
- export declare function GhostPreview({ visible, targetColSpan, enterMs, exitMs, }: GhostPreviewProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function GhostPreview({ visible, targetColSpan, motionX, enterMs, exitMs, }: GhostPreviewProps): import("react/jsx-runtime").JSX.Element;
22
25
  export declare namespace GhostPreview {
23
26
  var displayName: string;
24
27
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * NYQUIST-09 Parity Matrix — Phase 19 plan 19-05.
3
+ *
4
+ * Typed array of 144 rows enumerating the 7-axis resize parity space:
5
+ *
6
+ * spring — 3 presets: default (S500D35M1), gentle (S300D30M1), stiff (S700D40M1)
7
+ * viewport — round-robin across [1280, 1920, 2560] (all 3 appear in asserted set)
8
+ * reducedMotion — {false, true}
9
+ * pointerType — {'mouse', 'touch'}
10
+ * cardMode — {'slot-fill', 'sparkline-fidelity'}
11
+ * cancelPath — {'commit', 'escape', 'out-of-range'}
12
+ * dndKitActive — {false, true}
13
+ *
14
+ * Arithmetic (per 19-RESEARCH §NYQUIST-09 Fixture Matrix):
15
+ * 3 × 2 × 2 × 2 × 3 × 2 = 144 logical rows.
16
+ * Viewport axis is DERIVED per-row (round-robin) so every asserted row still
17
+ * represents one of the 3 canonical viewports without multiplying the base
18
+ * by a 4th full axis — this matches the RESEARCH-stated 144 count exactly.
19
+ *
20
+ * SkipFor taxonomy (~84 rows tagged; first-match wins):
21
+ *
22
+ * Rule 1 — reducedMotion && spring === 'stiff':
23
+ * Reduced motion bypasses spring; stiffer preset collapses to same
24
+ * instant-jump code path as default. (default spring + gentle preset
25
+ * still carry the rm=true assertion to exercise the branch.)
26
+ *
27
+ * Rule 2 — pointerType === 'touch' && cancelPath === 'escape':
28
+ * Touch devices lack Escape key; covered by separate touch-pointercancel
29
+ * fixture (useEdgeHoverResize.touch.test.tsx).
30
+ *
31
+ * Rule 3 — cardMode === 'sparkline-fidelity' && dndKitActive:
32
+ * Non-stackable cards are not sortable; dnd-kit active state is
33
+ * impossible (invariant from PagesContext.tsx stack rules).
34
+ *
35
+ * Rule 4 — cancelPath === 'out-of-range' && !dndKitActive && spring !== default:
36
+ * Out-of-range clamp is independent of spring variant; default-spring row
37
+ * carries the clamp assertion. Non-default springs on the clamp path are
38
+ * redundant (physics variant does not affect Math.min(3, …) behavior).
39
+ *
40
+ * Asserted target: 60 ± 5 rows (plan acceptance criterion).
41
+ * Actual asserted: 63 / 144 (skipped 81). Viewport distribution across
42
+ * asserted rows: 1280 → 25, 1920 → 18, 2560 → 20 (all 3 represented).
43
+ *
44
+ * @see .planning/phases/19-resize-interaction-rework/19-RESEARCH.md §NYQUIST-09 Fixture Matrix
45
+ * @see .planning/phases/19-resize-interaction-rework/19-PATTERNS.md §S10 NYQUIST matrix authoring
46
+ * @see .planning/phases/19-resize-interaction-rework/19-05-PLAN.md Task 1
47
+ */
48
+ export interface Nyquist09Row {
49
+ /**
50
+ * Stable row id, e.g. 'S500D35M1-vw1280-rm_off-mouse-stack-commit-dndk_on'.
51
+ * Shape: `S{stiffness}D{damping}M{mass}-vw{viewport}-rm_{on|off}-{pointer}-{stack|spark}-{cancelPath}-dndk_{on|off}`.
52
+ */
53
+ id: string;
54
+ spring: {
55
+ stiffness: number;
56
+ damping: number;
57
+ mass: number;
58
+ };
59
+ viewport: 1280 | 1920 | 2560;
60
+ reducedMotion: boolean;
61
+ pointerType: 'mouse' | 'touch';
62
+ cardMode: 'slot-fill' | 'sparkline-fidelity';
63
+ cancelPath: 'commit' | 'escape' | 'out-of-range';
64
+ dndKitActive: boolean;
65
+ /**
66
+ * If set, row is test.skip with this reason string. Acceptance criterion:
67
+ * skipped ≤ 84, asserted ≥ 55 (see PLAN threat-model T-19-16).
68
+ */
69
+ skipFor?: string;
70
+ }
71
+ /** Canonical 144-row NYQUIST-09 parity matrix — enumerated explicitly for diff-ability. */
72
+ export declare const NYQUIST_09_MATRIX: readonly Nyquist09Row[];
73
+ /** Convenience: number of rows that actually run (skipFor === undefined). Exported for CI sanity check. */
74
+ export declare const NYQUIST_09_ASSERTED_COUNT: number;
75
+ /** Convenience: number of rows tagged skipFor. Exported for CI sanity check. */
76
+ export declare const NYQUIST_09_SKIPPED_COUNT: number;
@@ -10,6 +10,10 @@ export { EdgeHoverHandle } from './EdgeHoverHandle';
10
10
  export { GhostPreview } from './GhostPreview';
11
11
  export { useSlideOutReveal } from './useSlideOutReveal';
12
12
  export { useEdgeHoverResize } from './useEdgeHoverResize';
13
+ export { DEFAULT_RESIZE_PHYSICS } from './resizePhysics';
14
+ export type { ResizePhysicsConfig } from './resizePhysics';
15
+ export { VelocityBuffer } from './velocityBuffer';
16
+ export type { VelocitySample } from './velocityBuffer';
13
17
  export * from './flipAndStage';
14
18
  export * from './types';
15
19
  export type { CardBackProps } from './CardBack';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * resizePhysics — D-08 / D-06 / D-11 / D-20 single source of truth.
3
+ *
4
+ * Constants consumed by both useEdgeHoverResize (spring commit) and the
5
+ * DevTuningPanel (Plan 19-02) so the panel and the hook never drift.
6
+ */
7
+ export declare const DEFAULT_RESIZE_PHYSICS: {
8
+ readonly stiffness: 500;
9
+ readonly damping: 35;
10
+ readonly mass: 1;
11
+ readonly snapInFraction: number;
12
+ readonly snapOutFraction: number;
13
+ readonly blueprintOpacity: 0.25;
14
+ readonly blueprintFadeInMs: 150;
15
+ readonly blueprintFadeOutMs: 120;
16
+ readonly rippleStaggerMs: 50;
17
+ readonly velocityMultiplier: 1;
18
+ };
19
+ export type ResizePhysicsConfig = typeof DEFAULT_RESIZE_PHYSICS;
@@ -1,4 +1,6 @@
1
- export type ResizeState = 'idle' | 'dwelling' | 'handle-visible' | 'dragging' | 'committed' | 'cancelled';
1
+ import { MotionValue } from 'framer-motion';
2
+ import { ResizePhysicsConfig } from './resizePhysics';
3
+ export type ResizeState = 'idle' | 'dwelling' | 'handle-visible' | 'dragging' | 'settling' | 'cancelled';
2
4
  export type ResizeDirection = 'grow' | 'shrink';
3
5
  export type ResizeMode = 'slot-fill' | 'sparkline-fidelity';
4
6
  export interface ResizeCommit {
@@ -11,12 +13,23 @@ export interface UseEdgeHoverResizeArgs {
11
13
  stackable: boolean;
12
14
  onResize: (commit: ResizeCommit) => void;
13
15
  onCancel?: () => void;
16
+ /** D-14 — colWidth snapshot passed per-render; captured at pointerdown. Default: 180px (1280px viewport). */
17
+ step?: number;
18
+ /** D-03 panel override; falls back to DEFAULT_RESIZE_PHYSICS. */
19
+ tuning?: Partial<ResizePhysicsConfig>;
14
20
  /** Hover dwell time before handle appears. Default: 120ms */
15
21
  dwellMs?: number;
16
22
  /** AN-06 enter motion duration exposed for consumer CSS. Default: 180ms */
17
23
  enterMs?: number;
18
24
  /** AN-06 exit motion duration exposed for consumer CSS. Default: 120ms */
19
25
  exitMs?: number;
26
+ /**
27
+ * Max colSpan supported by the grid (D-13 — right-edge-only grow). Default: 3.
28
+ * Used to clamp motionX writes during drag so the ghost cannot translate
29
+ * past the maximum-allowed column offset at any cursor position (debug
30
+ * session 260419-ghost-slides-across-page — AP-17 companion invariant).
31
+ */
32
+ maxColSpan?: 1 | 2 | 3;
20
33
  }
21
34
  export interface UseEdgeHoverResizeReturn {
22
35
  state: ResizeState;
@@ -29,35 +42,46 @@ export interface UseEdgeHoverResizeReturn {
29
42
  onPointerLeave: () => void;
30
43
  };
31
44
  /**
32
- * quick-260417-2na §2a — Live preview during drag.
33
- * While `state === 'dragging'`, reflects the colSpan the user would commit
34
- * if they released the pointer at the current position. Resets to
35
- * `currentColSpan` on transition out of 'dragging'. Always clamped to 1..3.
45
+ * Live preview during drag. Always clamped to 1..3.
46
+ * Reflects the colSpan the user would commit at the current cursor position,
47
+ * with hysteresis snap (D-06).
36
48
  */
37
49
  previewColSpan: 1 | 2 | 3;
38
50
  ghostProps: {
39
51
  visible: boolean;
40
- /** Same value as `previewColSpan`; exposed here for `<GhostPreview targetColSpan>` plumbing. */
41
52
  targetColSpan: 1 | 2 | 3;
53
+ /** D-07 — MotionValue driving ghost translateX. Zero React re-renders per pointermove. */
54
+ motionX: MotionValue<number>;
42
55
  enterMs: number;
43
56
  exitMs: number;
44
57
  };
58
+ /** D-16 — keyboard resize step (grow/shrink by 1) on focused handle. */
59
+ onKeyboardStep: (direction: ResizeDirection) => void;
60
+ /** D-19 — touch tap-to-reveal: show handle immediately on touch pointerdown on card body. */
61
+ onTouchReveal: () => void;
62
+ /** D-19 — outside-tap dismissal for touch. */
63
+ onTouchDismiss: () => void;
64
+ /** D-18 — cancel (used by parent; springs back if dragging). */
65
+ cancel: () => void;
45
66
  }
46
67
  /**
47
- * useEdgeHoverResize — State machine for edge-hover-to-resize gesture.
48
- *
49
- * State flow:
50
- * idle → dwelling (hover dwell 120ms) → handle-visible → dragging (pointerdown) → committed (pointerup) | cancelled
51
- * dwelling | handle-visible → idle (pointer leaves edge zone)
52
- * dragging | handle-visible → cancelled (Escape key)
53
- *
54
- * The hook does NOT inspect any reptime-domain data. The consumer passes `stackable`
55
- * (from metricInfo) and the hook emits the correct mode string — no cross-domain leakage.
68
+ * useEdgeHoverResize — Phase 19 reworked state machine for edge-hover-to-resize.
56
69
  *
57
- * Threat mitigations:
58
- * T-10.03-01: clearDwell() in cleanup useEffect prevents timer leak on unmount
59
- * T-10.03-02: Math.min/max clamp keeps targetColSpan ∈ {1,2,3}
70
+ * Changes vs Phase 10.5 original:
71
+ * - MotionValue ghost tracking (D-07) zero React re-renders per pointermove
72
+ * - Hysteresis snap (D-06) replaces Math.round(dx/step)
73
+ * - Velocity ring buffer (D-09) feeds spring commit
74
+ * - Spring commit via framer-motion animate() (D-08: stiffness 500, damping 35, mass 1)
75
+ * - 'settling' state (D-12) hard-blocks re-engagement during spring
76
+ * - committed state removed (D-05) — rolls into settling → idle
77
+ * - Keyboard step (D-16): onKeyboardStep('grow'|'shrink')
78
+ * - Escape spring-back with cursor velocity (D-15)
79
+ * - Touch tap-to-reveal with 5s idle (D-19)
80
+ * - prefers-reduced-motion bypass (D-22) — instant transform
60
81
  *
61
- * Motion contract (AN-06): enterMs=180, exitMs=120 (defaults)
82
+ * Preserved invariants:
83
+ * - Bug A React 19 updater purity — side effects fire OUTSIDE setState updaters
84
+ * - Bug E handle pointerenter keeps zone alive on sentinel→handle crossover
85
+ * - Dwell timer cleanup on unmount
62
86
  */
63
- export declare function useEdgeHoverResize({ currentColSpan, stackable, onResize, onCancel, dwellMs, enterMs, exitMs, }: UseEdgeHoverResizeArgs): UseEdgeHoverResizeReturn;
87
+ export declare function useEdgeHoverResize({ currentColSpan, stackable, onResize, onCancel, step, tuning, dwellMs, enterMs, exitMs, maxColSpan, }: UseEdgeHoverResizeArgs): UseEdgeHoverResizeReturn;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * VelocityBuffer — D-09 ring buffer for pointer velocity sampling.
3
+ *
4
+ * Stores up to `capacity` (3) VelocitySample entries. Computes a
5
+ * weighted-average velocity over the ring using 3×/2×/1× weights
6
+ * with divisor 6 (canonical formula per PLAN 19-01 resolution of
7
+ * §U1). When fewer than 3 samples exist, degrades gracefully:
8
+ * - 0 samples → 0
9
+ * - 1 sample → 0 (caller sees no velocity)
10
+ * - 2 samples → single-event dx/dt fallback (AP-20: never zero on a gesture)
11
+ * - 3+ samples → (vRecent·3 + vMid·2 + vOld·1) / 6
12
+ * where vOld degrades to vMid when only 2 deltas are available across 3 samples
13
+ * (flat fallback; buffer still lives on 3 instants — plan §Task 2 resolution).
14
+ *
15
+ * dt is clamped to >= 1ms via Math.max(1, …) to avoid Infinity on same-tick duplicates.
16
+ *
17
+ * Units: px/sec.
18
+ */
19
+ export interface VelocitySample {
20
+ x: number;
21
+ t: number;
22
+ }
23
+ export declare class VelocityBuffer {
24
+ private samples;
25
+ private readonly capacity;
26
+ push(sample: VelocitySample): void;
27
+ clear(): void;
28
+ /**
29
+ * D-09 — weighted-average velocity (px/sec).
30
+ *
31
+ * 0 samples → 0
32
+ * 1 sample → 0
33
+ * 2 samples → single-event fallback dx/dt
34
+ * 3 samples → weighted avg with vOld falling back to vMid to keep the
35
+ * divisor at 6. Formula: (vRecent·3 + vMid·2 + vMid·1) / 6
36
+ * (see PLAN §Task 2 §U1 resolution).
37
+ */
38
+ getWeightedVelocity(): number;
39
+ }
@@ -11,3 +11,4 @@ export * from './ReconfigBacksideButtons';
11
11
  export * from './MetricCardGrid';
12
12
  export * from './TabbedDataView';
13
13
  export * from './MetricGroupContainer';
14
+ export * from './DevTuningPanel';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mt-gloss/ui",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "restricted"