cursedbelt 5.58.0 → 5.59.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/dist/react/components/PaneSplit.d.ts +52 -0
- package/dist/react/components/PaneSplit.d.ts.map +1 -0
- package/dist/react/components/PaneSplit.js +103 -0
- package/dist/react/components/PaneSplit.js.map +1 -0
- package/dist/react/components/PointerDrag.d.ts +88 -0
- package/dist/react/components/PointerDrag.d.ts.map +1 -0
- package/dist/react/components/PointerDrag.js +156 -0
- package/dist/react/components/PointerDrag.js.map +1 -0
- package/dist/react/components/paneSplit.css +93 -0
- package/dist/react/components/pointerDrag.css +39 -0
- package/package.json +15 -1
- package/scripts/styleAreas.ts +3 -0
- package/src/componentCssReadsAreDeclared.spec.ts +62 -0
- package/src/react/components/PaneSplit.spec.tsx +60 -0
- package/src/react/components/PaneSplit.tsx +206 -0
- package/src/react/components/PointerDrag.spec.tsx +108 -0
- package/src/react/components/PointerDrag.tsx +261 -0
- package/src/react/components/paneSplit.css +93 -0
- package/src/react/components/pointerDrag.css +39 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PaneSplit` — an editor's pane tree, drawn VS Code's way: panes that meet edge to edge, square,
|
|
3
|
+
* with one hairline between them.
|
|
4
|
+
*
|
|
5
|
+
* Graduated from desk's `WorkspaceSplit` on the owner's word: *"Panels and panes should be separated
|
|
6
|
+
* by sharp corners and thin border lines just like they are in vscode"* — and *"since these changes
|
|
7
|
+
* apply to layouts and cards they should be changed in cb and should impact desk through cb"*.
|
|
8
|
+
*
|
|
9
|
+
* The model is the app's — a tree of leaves and splits, each split carrying the fractions of its
|
|
10
|
+
* children. This component is only geometry: flex tracks, one seam between each pair of children,
|
|
11
|
+
* and the arithmetic that turns a pointer into a pair of fractions. Unlike `PanelGrid` it owns no
|
|
12
|
+
* sizes and persists nothing, which is what an app that stores its layout as a setting needs.
|
|
13
|
+
*
|
|
14
|
+
* 🔴 **The seam is keyboard-operable.** Arrow keys move it by 2%, Home/End take it to the bounds.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 **The hit strip lives inside the seam's own track** — a real flex item, never a bar hung over
|
|
17
|
+
* the boundary. An invisible strip centred on the seam sits over the first pixels of the pane beside
|
|
18
|
+
* it and swallows clicks nobody aimed at it (desk's file `21`, where a 44px hit area ate a fill
|
|
19
|
+
* handle). So the track is a few pixels wide for the hand, painted the panes' own ground
|
|
20
|
+
* (`--cb-panesplit-ground`) with the hairline drawn down its middle: to the eye, two panes and one
|
|
21
|
+
* line, which is VS Code's picture; to the pointer, a target that touches no pane.
|
|
22
|
+
*
|
|
23
|
+
* Plain CSS (`paneSplit.css`), prefix `cb-panesplit`, so it draws in an app with no Tailwind pass.
|
|
24
|
+
*/
|
|
25
|
+
import type { JSX } from 'react';
|
|
26
|
+
import './paneSplit.css';
|
|
27
|
+
export type PaneSplitDirection = 'row' | 'column';
|
|
28
|
+
export type PaneSplitNode<P> = {
|
|
29
|
+
kind: 'leaf';
|
|
30
|
+
pane: P;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'split';
|
|
33
|
+
direction: PaneSplitDirection;
|
|
34
|
+
sizes: readonly number[];
|
|
35
|
+
children: readonly PaneSplitNode<P>[];
|
|
36
|
+
};
|
|
37
|
+
/** Where a node sits in the tree: the child index at each level, from the root. */
|
|
38
|
+
export type PaneSplitPath = readonly number[];
|
|
39
|
+
export interface PaneSplitProps<P> {
|
|
40
|
+
node: PaneSplitNode<P>;
|
|
41
|
+
/** A key unique across the whole tree — what keeps a pane's subtree when its siblings close. */
|
|
42
|
+
paneKey: (pane: P) => string;
|
|
43
|
+
renderPane: (pane: P) => JSX.Element;
|
|
44
|
+
/** A seam moved: the split at `path` now has these fractions. */
|
|
45
|
+
onResize: (path: PaneSplitPath, sizes: number[]) => void;
|
|
46
|
+
/** The smallest share of an axis a pane can be dragged down to. Default 0.12. */
|
|
47
|
+
minFraction?: number;
|
|
48
|
+
/** Where `node` sits — only for a tree drawn from below its root. */
|
|
49
|
+
path?: PaneSplitPath;
|
|
50
|
+
}
|
|
51
|
+
export declare function PaneSplit<P>(props: PaneSplitProps<P>): JSX.Element;
|
|
52
|
+
//# sourceMappingURL=PaneSplit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaneSplit.d.ts","sourceRoot":"","sources":["../../../src/react/components/PaneSplit.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAiB,GAAG,EAA0E,MAAM,OAAO,CAAC;AAExH,OAAO,iBAAiB,CAAC;AAEzB,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAElD,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,SAAS,aAAa,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,CAAC;AAEtH,mFAAmF;AACnF,MAAM,MAAM,aAAa,GAAG,SAAS,MAAM,EAAE,CAAC;AAE9C,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACvB,gGAAgG;IAChG,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC;IACrC,iEAAiE;IACjE,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACzD,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAaD,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,OAAO,CAIlE"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef } from 'react';
|
|
3
|
+
import './paneSplit.css';
|
|
4
|
+
const DEFAULT_MIN_FRACTION = 0.12;
|
|
5
|
+
/**
|
|
6
|
+
* A key for a child that follows its content rather than its position: closing a pane in the middle
|
|
7
|
+
* of a row shifts every later child's index, and `key={index}` would hand React the wrong subtree.
|
|
8
|
+
*/
|
|
9
|
+
function childKey(node, paneKey) {
|
|
10
|
+
if (node.kind === 'leaf')
|
|
11
|
+
return paneKey(node.pane);
|
|
12
|
+
return node.children.map((child) => childKey(child, paneKey)).join('|');
|
|
13
|
+
}
|
|
14
|
+
export function PaneSplit(props) {
|
|
15
|
+
const { node } = props;
|
|
16
|
+
if (node.kind === 'leaf')
|
|
17
|
+
return props.renderPane(node.pane);
|
|
18
|
+
return _jsx(SplitNode, { ...props, node: node, path: props.path ?? [] });
|
|
19
|
+
}
|
|
20
|
+
function SplitNode({ node, path, paneKey, renderPane, onResize, minFraction = DEFAULT_MIN_FRACTION, }) {
|
|
21
|
+
const frame = useRef(null);
|
|
22
|
+
const dragging = useRef(null);
|
|
23
|
+
/** Set the share of the pair either side of seam `index`; only those two panes move. */
|
|
24
|
+
const setShare = useCallback((index, share) => {
|
|
25
|
+
const pair = (node.sizes[index] ?? 0) + (node.sizes[index + 1] ?? 0);
|
|
26
|
+
if (pair <= 0)
|
|
27
|
+
return;
|
|
28
|
+
const bounded = Math.min(Math.max(share, minFraction), 1 - minFraction);
|
|
29
|
+
const sizes = [...node.sizes];
|
|
30
|
+
sizes[index] = pair * bounded;
|
|
31
|
+
sizes[index + 1] = pair * (1 - bounded);
|
|
32
|
+
onResize(path, sizes);
|
|
33
|
+
}, [node.sizes, minFraction, onResize, path]);
|
|
34
|
+
const moveSeam = (index, position) => {
|
|
35
|
+
const box = frame.current?.getBoundingClientRect();
|
|
36
|
+
if (!box)
|
|
37
|
+
return;
|
|
38
|
+
const total = node.direction === 'row' ? box.width : box.height;
|
|
39
|
+
if (total <= 0)
|
|
40
|
+
return;
|
|
41
|
+
const offset = (node.direction === 'row' ? position - box.left : position - box.top) / total;
|
|
42
|
+
const before = node.sizes.slice(0, index).reduce((sum, size) => sum + size, 0);
|
|
43
|
+
const pair = (node.sizes[index] ?? 0) + (node.sizes[index + 1] ?? 0);
|
|
44
|
+
if (pair <= 0)
|
|
45
|
+
return;
|
|
46
|
+
setShare(index, (offset - before) / pair);
|
|
47
|
+
};
|
|
48
|
+
const nudgeSeam = (index, delta) => {
|
|
49
|
+
const pair = (node.sizes[index] ?? 0) + (node.sizes[index + 1] ?? 0);
|
|
50
|
+
if (pair <= 0)
|
|
51
|
+
return;
|
|
52
|
+
setShare(index, (node.sizes[index] ?? 0) / pair + delta);
|
|
53
|
+
};
|
|
54
|
+
const onKeyDown = (event, index) => {
|
|
55
|
+
const back = node.direction === 'row' ? 'ArrowLeft' : 'ArrowUp';
|
|
56
|
+
const forward = node.direction === 'row' ? 'ArrowRight' : 'ArrowDown';
|
|
57
|
+
if (event.key === back)
|
|
58
|
+
nudgeSeam(index, -0.02);
|
|
59
|
+
else if (event.key === forward)
|
|
60
|
+
nudgeSeam(index, 0.02);
|
|
61
|
+
else if (event.key === 'Home')
|
|
62
|
+
nudgeSeam(index, -1);
|
|
63
|
+
else if (event.key === 'End')
|
|
64
|
+
nudgeSeam(index, 1);
|
|
65
|
+
else
|
|
66
|
+
return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
};
|
|
69
|
+
const onPointerDown = (event, index) => {
|
|
70
|
+
if (event.button !== 0)
|
|
71
|
+
return;
|
|
72
|
+
dragging.current = index;
|
|
73
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
};
|
|
76
|
+
const onPointerMove = (event) => {
|
|
77
|
+
if (dragging.current === null)
|
|
78
|
+
return;
|
|
79
|
+
moveSeam(dragging.current, node.direction === 'row' ? event.clientX : event.clientY);
|
|
80
|
+
};
|
|
81
|
+
const endDrag = (event) => {
|
|
82
|
+
if (dragging.current === null)
|
|
83
|
+
return;
|
|
84
|
+
dragging.current = null;
|
|
85
|
+
if (event.currentTarget.hasPointerCapture?.(event.pointerId))
|
|
86
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
87
|
+
};
|
|
88
|
+
const minPercent = Math.round(minFraction * 100);
|
|
89
|
+
return (_jsx("div", { className: "cb-panesplit", "data-direction": node.direction, ref: frame, children: node.children.map((child, index) => (_jsx(ChildAndSeam, { last: index === node.children.length - 1, size: node.sizes[index] ?? 1 / node.children.length, seam: {
|
|
90
|
+
direction: node.direction,
|
|
91
|
+
percent: Math.round((node.sizes[index] ?? 0) * 100),
|
|
92
|
+
min: minPercent,
|
|
93
|
+
max: 100 - minPercent,
|
|
94
|
+
onKeyDown: (event) => onKeyDown(event, index),
|
|
95
|
+
onPointerDown: (event) => onPointerDown(event, index),
|
|
96
|
+
onPointerMove,
|
|
97
|
+
endDrag,
|
|
98
|
+
}, children: _jsx(PaneSplit, { node: child, path: [...path, index], paneKey: paneKey, renderPane: renderPane, onResize: onResize, minFraction: minFraction }) }, childKey(child, paneKey)))) }));
|
|
99
|
+
}
|
|
100
|
+
function ChildAndSeam({ last, size, seam, children }) {
|
|
101
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "cb-panesplit__cell", style: { '--cb-panesplit-share': size }, children: children }), !last && (_jsx("div", { className: "cb-panesplit__seam", role: "separator", tabIndex: 0, "aria-orientation": seam.direction === 'row' ? 'vertical' : 'horizontal', "aria-label": seam.direction === 'row' ? 'Width of the pane on the left' : 'Height of the pane above', "aria-valuenow": seam.percent, "aria-valuemin": seam.min, "aria-valuemax": seam.max, onKeyDown: seam.onKeyDown, onPointerDown: seam.onPointerDown, onPointerMove: seam.onPointerMove, onPointerUp: seam.endDrag, onPointerCancel: seam.endDrag }))] }));
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=PaneSplit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaneSplit.js","sourceRoot":"","sources":["../../../src/react/components/PaneSplit.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `usePointerDrag` + `DragGhost` — a drag you can SEE, with a mouse or a finger.
|
|
3
|
+
*
|
|
4
|
+
* The owner, of desk's tab deck: *"When I drag a tab in desk app it should animate or show the tab
|
|
5
|
+
* moving with your mouse cursor (or on iphone with the dragging gesture) like vs code does."* — and
|
|
6
|
+
* *"since these changes apply to layouts and cards they should be changed in cb"*. So the gesture
|
|
7
|
+
* and its picture live here, and an app keeps only what is its own: where a drop lands.
|
|
8
|
+
*
|
|
9
|
+
* ## The gesture
|
|
10
|
+
*
|
|
11
|
+
* Pointer events, never HTML5 drag-and-drop, for `SheetTabStrip`'s reasons: the native API's image
|
|
12
|
+
* is a translucent screenshot the page cannot style, it cannot be driven by CDP in a check, and it
|
|
13
|
+
* fires nothing at all for touch.
|
|
14
|
+
*
|
|
15
|
+
* - **Mouse and pen**: a press becomes a drag past `slop` pixels of travel, so a click is still a
|
|
16
|
+
* click and a double-click still lands. 🔴 The pointer is captured AT THE PRESS: a quick flick
|
|
17
|
+
* leaves a 30px tab inside one frame, and an element that only hears moves while the pointer is
|
|
18
|
+
* over it never sees the travel that would make it a drag (measured in desk's browser check).
|
|
19
|
+
* Capture to the pressed element itself redirects nothing a click needs — down and up both land
|
|
20
|
+
* on it.
|
|
21
|
+
* - **Touch**: a finger has to REST for `holdMs` first. Anything that moves before then is a scroll
|
|
22
|
+
* and is left to the browser — a tab strip pans sideways, and a drag that starts on any touch takes
|
|
23
|
+
* the pan away from the gesture people use most. Once the hold fires, a non-passive `touchmove`
|
|
24
|
+
* listener (registered at the press, so the browser waits for it) cancels the scroll for the rest
|
|
25
|
+
* of the gesture. 250 ms and 5 px are `pointerDnd.ts`'s long-press, so a hold means the same thing
|
|
26
|
+
* on every draggable surface of the belt.
|
|
27
|
+
* - **Escape** abandons a drag wherever focus is; nothing is committed.
|
|
28
|
+
*
|
|
29
|
+
* ## The picture
|
|
30
|
+
*
|
|
31
|
+
* `DragGhost` draws the thing being dragged under the pointer, offset by where it was grabbed, at
|
|
32
|
+
* the size it had — VS Code's lifted tab. It is `position: fixed`, so a scrolling strip or a pane's
|
|
33
|
+
* `overflow: hidden` does not clip it, and it ignores the pointer, so `document.elementFromPoint`
|
|
34
|
+
* still answers with the drop target beneath it. Plain CSS (`pointerDrag.css`), so it draws in an
|
|
35
|
+
* app with no Tailwind pass.
|
|
36
|
+
*/
|
|
37
|
+
import type { PointerEvent as ReactPointerEvent, ReactNode } from 'react';
|
|
38
|
+
import './pointerDrag.css';
|
|
39
|
+
/** Pixels a mouse or pen travels before a press is a drag. */
|
|
40
|
+
export declare const POINTER_DRAG_SLOP_PX = 5;
|
|
41
|
+
/** How long a finger rests before a touch press is a drag — `pointerDnd.ts`'s long-press. */
|
|
42
|
+
export declare const POINTER_DRAG_HOLD_MS = 250;
|
|
43
|
+
export interface DragPoint {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
}
|
|
47
|
+
export interface PointerDragState<T> {
|
|
48
|
+
item: T;
|
|
49
|
+
/** Where the pointer is now, in client coordinates. */
|
|
50
|
+
at: DragPoint;
|
|
51
|
+
/** Where inside the source element it was grabbed — the ghost keeps that point under the pointer. */
|
|
52
|
+
grab: DragPoint;
|
|
53
|
+
/** The source element's size when the press began, so the ghost is the same shape. */
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
pointerType: string;
|
|
57
|
+
}
|
|
58
|
+
export interface UsePointerDragOptions<T> {
|
|
59
|
+
/** Every move once the press has become a drag — where a drop marker is worked out. */
|
|
60
|
+
onMove?: (state: PointerDragState<T>) => void;
|
|
61
|
+
/** Released while dragging. */
|
|
62
|
+
onDrop: (state: PointerDragState<T>) => void;
|
|
63
|
+
/** Released without ever becoming a drag. */
|
|
64
|
+
onTap?: (item: T) => void;
|
|
65
|
+
/** The drag was abandoned — Escape, or the browser took the pointer back. */
|
|
66
|
+
onCancel?: () => void;
|
|
67
|
+
slop?: number;
|
|
68
|
+
holdMs?: number;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface PointerDragBindings {
|
|
72
|
+
onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;
|
|
73
|
+
onPointerMove: (event: ReactPointerEvent<HTMLElement>) => void;
|
|
74
|
+
onPointerUp: (event: ReactPointerEvent<HTMLElement>) => void;
|
|
75
|
+
onPointerCancel: (event: ReactPointerEvent<HTMLElement>) => void;
|
|
76
|
+
}
|
|
77
|
+
export declare function usePointerDrag<T>(options: UsePointerDragOptions<T>): {
|
|
78
|
+
drag: PointerDragState<T> | null;
|
|
79
|
+
bind: (item: T) => PointerDragBindings;
|
|
80
|
+
};
|
|
81
|
+
export interface DragGhostProps {
|
|
82
|
+
drag: Pick<PointerDragState<unknown>, 'at' | 'grab' | 'width' | 'height'> | null;
|
|
83
|
+
children: ReactNode;
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
/** The dragged thing, lifted and following the pointer. Renders nothing while nothing is dragged. */
|
|
87
|
+
export declare function DragGhost({ drag, children, className }: DragGhostProps): import("react").JSX.Element | null;
|
|
88
|
+
//# sourceMappingURL=PointerDrag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PointerDrag.d.ts","sourceRoot":"","sources":["../../../src/react/components/PointerDrag.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAiB,YAAY,IAAI,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,mBAAmB,CAAC;AAE3B,8DAA8D;AAC9D,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,6FAA6F;AAC7F,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC;IACR,uDAAuD;IACvD,EAAE,EAAE,SAAS,CAAC;IACd,qGAAqG;IACrG,IAAI,EAAE,SAAS,CAAC;IAChB,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,uFAAuF;IACvF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC9C,+BAA+B;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC7C,6CAA6C;IAC7C,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAC/D,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAC/D,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAC7D,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CAClE;AA8BD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG;IACpE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,mBAAmB,CAAC;CACxC,CAoHA;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC;IACjF,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,sCAoBtE"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import './pointerDrag.css';
|
|
4
|
+
/** Pixels a mouse or pen travels before a press is a drag. */
|
|
5
|
+
export const POINTER_DRAG_SLOP_PX = 5;
|
|
6
|
+
/** How long a finger rests before a touch press is a drag — `pointerDnd.ts`'s long-press. */
|
|
7
|
+
export const POINTER_DRAG_HOLD_MS = 250;
|
|
8
|
+
function stateOf(current) {
|
|
9
|
+
return {
|
|
10
|
+
item: current.item,
|
|
11
|
+
at: current.at,
|
|
12
|
+
grab: current.grab,
|
|
13
|
+
width: current.width,
|
|
14
|
+
height: current.height,
|
|
15
|
+
pointerType: current.pointerType,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function usePointerDrag(options) {
|
|
19
|
+
const latest = useRef(options);
|
|
20
|
+
latest.current = options;
|
|
21
|
+
const press = useRef(null);
|
|
22
|
+
const [drag, setDrag] = useState(null);
|
|
23
|
+
/** The non-passive listener that holds the page still once a touch drag is live. */
|
|
24
|
+
const holdScroll = useCallback((event) => {
|
|
25
|
+
if (press.current?.live)
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
}, []);
|
|
28
|
+
const finish = useCallback(() => {
|
|
29
|
+
const current = press.current;
|
|
30
|
+
press.current = null;
|
|
31
|
+
if (current?.timer)
|
|
32
|
+
clearTimeout(current.timer);
|
|
33
|
+
if (current?.element.hasPointerCapture?.(current.pointerId))
|
|
34
|
+
current.element.releasePointerCapture(current.pointerId);
|
|
35
|
+
window.removeEventListener('touchmove', holdScroll);
|
|
36
|
+
setDrag(null);
|
|
37
|
+
}, [holdScroll]);
|
|
38
|
+
const goLive = useCallback((current) => {
|
|
39
|
+
current.live = true;
|
|
40
|
+
const state = stateOf(current);
|
|
41
|
+
setDrag(state);
|
|
42
|
+
latest.current.onMove?.(state);
|
|
43
|
+
}, []);
|
|
44
|
+
// Escape puts everything back, wherever focus is.
|
|
45
|
+
const live = drag !== null;
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!live)
|
|
48
|
+
return;
|
|
49
|
+
const onKey = (event) => {
|
|
50
|
+
if (event.key !== 'Escape')
|
|
51
|
+
return;
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
event.stopPropagation();
|
|
54
|
+
finish();
|
|
55
|
+
latest.current.onCancel?.();
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener('keydown', onKey, true);
|
|
58
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
59
|
+
}, [live, finish]);
|
|
60
|
+
// A drag in flight when the owner unmounts must not leave a timer or a listener behind.
|
|
61
|
+
useEffect(() => finish, [finish]);
|
|
62
|
+
const bind = (item) => ({
|
|
63
|
+
onPointerDown: (event) => {
|
|
64
|
+
if (latest.current.disabled || press.current)
|
|
65
|
+
return;
|
|
66
|
+
if (event.pointerType === 'mouse' && event.button !== 0)
|
|
67
|
+
return;
|
|
68
|
+
const element = event.currentTarget;
|
|
69
|
+
const box = element.getBoundingClientRect();
|
|
70
|
+
const at = { x: event.clientX, y: event.clientY };
|
|
71
|
+
const current = {
|
|
72
|
+
item,
|
|
73
|
+
pointerId: event.pointerId,
|
|
74
|
+
pointerType: event.pointerType,
|
|
75
|
+
start: at,
|
|
76
|
+
grab: { x: at.x - box.left, y: at.y - box.top },
|
|
77
|
+
width: box.width,
|
|
78
|
+
height: box.height,
|
|
79
|
+
element,
|
|
80
|
+
live: false,
|
|
81
|
+
abandoned: false,
|
|
82
|
+
timer: null,
|
|
83
|
+
at,
|
|
84
|
+
};
|
|
85
|
+
press.current = current;
|
|
86
|
+
element.setPointerCapture?.(event.pointerId);
|
|
87
|
+
if (event.pointerType === 'touch') {
|
|
88
|
+
window.addEventListener('touchmove', holdScroll, { passive: false });
|
|
89
|
+
current.timer = setTimeout(() => {
|
|
90
|
+
current.timer = null;
|
|
91
|
+
if (press.current === current && !current.abandoned)
|
|
92
|
+
goLive(current);
|
|
93
|
+
}, latest.current.holdMs ?? POINTER_DRAG_HOLD_MS);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onPointerMove: (event) => {
|
|
97
|
+
const current = press.current;
|
|
98
|
+
if (!current || current.pointerId !== event.pointerId)
|
|
99
|
+
return;
|
|
100
|
+
current.at = { x: event.clientX, y: event.clientY };
|
|
101
|
+
const travelled = Math.hypot(current.at.x - current.start.x, current.at.y - current.start.y);
|
|
102
|
+
const slop = latest.current.slop ?? POINTER_DRAG_SLOP_PX;
|
|
103
|
+
if (!current.live) {
|
|
104
|
+
if (travelled < slop)
|
|
105
|
+
return;
|
|
106
|
+
if (current.pointerType === 'touch') {
|
|
107
|
+
// Moved before the hold: a scroll. Let the browser have it.
|
|
108
|
+
current.abandoned = true;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
goLive(current);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const state = stateOf(current);
|
|
115
|
+
setDrag(state);
|
|
116
|
+
latest.current.onMove?.(state);
|
|
117
|
+
},
|
|
118
|
+
onPointerUp: (event) => {
|
|
119
|
+
const current = press.current;
|
|
120
|
+
if (!current || current.pointerId !== event.pointerId)
|
|
121
|
+
return;
|
|
122
|
+
current.at = { x: event.clientX, y: event.clientY };
|
|
123
|
+
const state = stateOf(current);
|
|
124
|
+
const { live: wasLive, abandoned } = current;
|
|
125
|
+
finish();
|
|
126
|
+
if (wasLive)
|
|
127
|
+
latest.current.onDrop(state);
|
|
128
|
+
else if (!abandoned)
|
|
129
|
+
latest.current.onTap?.(current.item);
|
|
130
|
+
},
|
|
131
|
+
onPointerCancel: (event) => {
|
|
132
|
+
const current = press.current;
|
|
133
|
+
if (!current || current.pointerId !== event.pointerId)
|
|
134
|
+
return;
|
|
135
|
+
const wasLive = current.live;
|
|
136
|
+
finish();
|
|
137
|
+
if (wasLive)
|
|
138
|
+
latest.current.onCancel?.();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
return { drag, bind };
|
|
142
|
+
}
|
|
143
|
+
/** The dragged thing, lifted and following the pointer. Renders nothing while nothing is dragged. */
|
|
144
|
+
export function DragGhost({ drag, children, className }) {
|
|
145
|
+
if (!drag)
|
|
146
|
+
return null;
|
|
147
|
+
return (_jsx("div", { "aria-hidden": true, className: className ? `cb-dragghost ${className}` : 'cb-dragghost', "data-testid": "drag-ghost",
|
|
148
|
+
// Custom properties only — `pointerDrag.css` turns them into the box, the offset and the band.
|
|
149
|
+
style: {
|
|
150
|
+
'--cb-dragghost-x': `${drag.at.x - drag.grab.x}px`,
|
|
151
|
+
'--cb-dragghost-y': `${drag.at.y - drag.grab.y}px`,
|
|
152
|
+
'--cb-dragghost-w': `${drag.width}px`,
|
|
153
|
+
'--cb-dragghost-h': `${drag.height}px`,
|
|
154
|
+
}, children: children }));
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=PointerDrag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PointerDrag.js","sourceRoot":"","sources":["../../../src/react/components/PointerDrag.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PaneSplit's look, as plain CSS — so it draws in an app with NO Tailwind pass (desk), the way
|
|
3
|
+
* `sheetTabStrip.css` travels. Prefix `cb-panesplit`. `@layer base` for that file's reason.
|
|
4
|
+
*
|
|
5
|
+
* VS Code's picture: panes meet edge to edge and one 1px line separates them. The seam's track is
|
|
6
|
+
* wider than the line for the hand (`--cb-panesplit-reach`, 5px), painted the panes' own ground so
|
|
7
|
+
* only the line reads; on hover or keyboard focus the line thickens to the accent, as VS Code's sash
|
|
8
|
+
* does.
|
|
9
|
+
*
|
|
10
|
+
* Knobs:
|
|
11
|
+
* --cb-panesplit-ground what the seam's track is painted — set it to the panes' background
|
|
12
|
+
* --cb-panesplit-line the hairline's colour (default `--border`)
|
|
13
|
+
* --cb-panesplit-reach the seam's track, the size of the hit area (default 5px)
|
|
14
|
+
* --cb-panesplit-active the line on hover and focus (default `--ring`)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
@layer base {
|
|
18
|
+
.cb-panesplit {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex: 1 1 auto;
|
|
21
|
+
min-width: 0;
|
|
22
|
+
min-height: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.cb-panesplit[data-direction='row'] {
|
|
26
|
+
flex-direction: row;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.cb-panesplit[data-direction='column'] {
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* `flex-basis: 0` with the share as the grow factor, never a percentage width: a percentage would
|
|
34
|
+
have to account for the seams' own pixels, so a row of four panes would drift wider than its
|
|
35
|
+
frame. The share arrives as `--cb-panesplit-share`. */
|
|
36
|
+
.cb-panesplit__cell {
|
|
37
|
+
flex: var(--cb-panesplit-share, 1) 1 0;
|
|
38
|
+
display: flex;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
min-height: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.cb-panesplit__seam {
|
|
44
|
+
flex: 0 0 auto;
|
|
45
|
+
position: relative;
|
|
46
|
+
background: var(--cb-panesplit-ground, transparent);
|
|
47
|
+
touch-action: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.cb-panesplit[data-direction='row'] > .cb-panesplit__seam {
|
|
51
|
+
width: var(--cb-panesplit-reach, 5px);
|
|
52
|
+
cursor: col-resize;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.cb-panesplit[data-direction='column'] > .cb-panesplit__seam {
|
|
56
|
+
height: var(--cb-panesplit-reach, 5px);
|
|
57
|
+
cursor: row-resize;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* The hairline, down the middle of the track. */
|
|
61
|
+
.cb-panesplit__seam::after {
|
|
62
|
+
content: '';
|
|
63
|
+
position: absolute;
|
|
64
|
+
background: var(--cb-panesplit-line, var(--border));
|
|
65
|
+
transition: background 120ms ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.cb-panesplit[data-direction='row'] > .cb-panesplit__seam::after {
|
|
69
|
+
inset: 0 auto 0 calc(50% - 0.5px);
|
|
70
|
+
width: 1px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.cb-panesplit[data-direction='column'] > .cb-panesplit__seam::after {
|
|
74
|
+
inset: calc(50% - 0.5px) 0 auto 0;
|
|
75
|
+
height: 1px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.cb-panesplit[data-direction='row'] > .cb-panesplit__seam:is(:hover, :focus-visible)::after {
|
|
79
|
+
left: calc(50% - 1.5px);
|
|
80
|
+
width: 3px;
|
|
81
|
+
background: var(--cb-panesplit-active, var(--ring));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cb-panesplit[data-direction='column'] > .cb-panesplit__seam:is(:hover, :focus-visible)::after {
|
|
85
|
+
top: calc(50% - 1.5px);
|
|
86
|
+
height: 3px;
|
|
87
|
+
background: var(--cb-panesplit-active, var(--ring));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.cb-panesplit__seam:focus-visible {
|
|
91
|
+
outline: none;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DragGhost's look, as plain CSS — so it draws in an app with NO Tailwind pass (desk), the way
|
|
3
|
+
* `sheetTabStrip.css` travels. Prefix `cb-dragghost`. `@layer base` for that file's reason: an app's
|
|
4
|
+
* own rule for the ghost's class still wins.
|
|
5
|
+
*
|
|
6
|
+
* The lifted tab: the surface it came from, a hairline and a soft shadow, slightly translucent so
|
|
7
|
+
* the drop marker underneath stays readable. Square corners — a tab is square in VS Code, and the
|
|
8
|
+
* ghost is the tab.
|
|
9
|
+
*
|
|
10
|
+
* It rides the popover band (06 §1), as useCrossContainerDrag's ghost does. 🔴 Every `var()` read here
|
|
11
|
+
* has a fallback or a theme token behind it: an app's built stylesheet must define whatever it reads
|
|
12
|
+
* bare (desk's `belt-tokens.test.ts`), and a name only an inline style sets is not defined there.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@layer base {
|
|
16
|
+
.cb-dragghost {
|
|
17
|
+
position: fixed;
|
|
18
|
+
left: 0;
|
|
19
|
+
top: 0;
|
|
20
|
+
width: var(--cb-dragghost-w, auto);
|
|
21
|
+
height: var(--cb-dragghost-h, auto);
|
|
22
|
+
transform: translate3d(var(--cb-dragghost-x, 0), var(--cb-dragghost-y, 0), 0);
|
|
23
|
+
z-index: var(--z-popover, 1100);
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 0.375rem;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
color: var(--foreground);
|
|
32
|
+
background: var(--popover, var(--background));
|
|
33
|
+
border: 1px solid var(--border);
|
|
34
|
+
box-shadow: var(--shadow-popover);
|
|
35
|
+
opacity: 0.92;
|
|
36
|
+
cursor: grabbing;
|
|
37
|
+
will-change: transform;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.59.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"./react/media-gallery",
|
|
31
31
|
"./react/media/player-strip",
|
|
32
32
|
"./react/media/video-player",
|
|
33
|
+
"./react/pane-split",
|
|
33
34
|
"./react/pdf-viewer",
|
|
35
|
+
"./react/pointer-drag",
|
|
34
36
|
"./react/rich-text",
|
|
35
37
|
"./react/search-input",
|
|
36
38
|
"./react/sheet-tab-strip",
|
|
@@ -546,6 +548,18 @@
|
|
|
546
548
|
"source": "./src/react/sharing/index.ts",
|
|
547
549
|
"import": "./dist/react/sharing/index.js"
|
|
548
550
|
},
|
|
551
|
+
"./react/pane-split": {
|
|
552
|
+
"types": "./dist/react/components/PaneSplit.d.ts",
|
|
553
|
+
"bun": "./src/react/components/PaneSplit.tsx",
|
|
554
|
+
"source": "./src/react/components/PaneSplit.tsx",
|
|
555
|
+
"import": "./dist/react/components/PaneSplit.js"
|
|
556
|
+
},
|
|
557
|
+
"./react/pointer-drag": {
|
|
558
|
+
"types": "./dist/react/components/PointerDrag.d.ts",
|
|
559
|
+
"bun": "./src/react/components/PointerDrag.tsx",
|
|
560
|
+
"source": "./src/react/components/PointerDrag.tsx",
|
|
561
|
+
"import": "./dist/react/components/PointerDrag.js"
|
|
562
|
+
},
|
|
549
563
|
"./react/sheet-tab-strip": {
|
|
550
564
|
"types": "./dist/react/components/SheetTabStrip.d.ts",
|
|
551
565
|
"bun": "./src/react/components/SheetTabStrip.tsx",
|
package/scripts/styleAreas.ts
CHANGED
|
@@ -389,6 +389,9 @@ export const AREAS: readonly Area[] = [
|
|
|
389
389
|
'components/SheetTabStrip.tsx',
|
|
390
390
|
'components/sheetTabStripLogic.ts',
|
|
391
391
|
'components/SplitPane.tsx',
|
|
392
|
+
// An app-owned pane tree and the visible tab drag (desk-split-pane-tab-dragging) — plain CSS both.
|
|
393
|
+
'components/PaneSplit.tsx',
|
|
394
|
+
'components/PointerDrag.tsx',
|
|
392
395
|
'components/WidgetHeader.tsx',
|
|
393
396
|
'components/DragHandle.tsx',
|
|
394
397
|
'components/DropIndicator.tsx',
|