@adecore/ui 0.19.0 → 0.20.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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/dist/ColumnResizeHandle.d.ts +3 -2
  3. package/dist/ColumnResizeHandle.js +2 -2
  4. package/dist/SlidingColumn.d.ts +2 -1
  5. package/dist/SlidingColumn.js +11 -4
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.js +4 -0
  8. package/dist/locales/en.json +10 -0
  9. package/dist/locales/nl.json +10 -0
  10. package/dist/theme.css +181 -0
  11. package/dist/useColumnResize.js +12 -17
  12. package/dist/workspace/SplitView.d.ts +49 -0
  13. package/dist/workspace/SplitView.js +421 -0
  14. package/dist/workspace/TabStrip.d.ts +42 -0
  15. package/dist/workspace/TabStrip.js +168 -0
  16. package/dist/workspace/Workspace.d.ts +15 -0
  17. package/dist/workspace/Workspace.js +27 -0
  18. package/dist/workspace/context.d.ts +6 -0
  19. package/dist/workspace/context.js +5 -0
  20. package/dist/workspace/geometry.d.ts +39 -0
  21. package/dist/workspace/geometry.js +153 -0
  22. package/dist/workspace/model.d.ts +83 -0
  23. package/dist/workspace/model.js +360 -0
  24. package/dist/workspace/preview.d.ts +35 -0
  25. package/dist/workspace/preview.js +72 -0
  26. package/package.json +1 -1
  27. package/src/ColumnResizeHandle.tsx +4 -3
  28. package/src/SlidingColumn.tsx +23 -6
  29. package/src/index.ts +19 -0
  30. package/src/locales/en.json +10 -0
  31. package/src/locales/nl.json +10 -0
  32. package/src/theme.css +181 -0
  33. package/src/useColumnResize.ts +12 -18
  34. package/src/workspace/SplitView.tsx +656 -0
  35. package/src/workspace/TabStrip.tsx +296 -0
  36. package/src/workspace/Workspace.tsx +72 -0
  37. package/src/workspace/context.ts +8 -0
  38. package/src/workspace/geometry.ts +190 -0
  39. package/src/workspace/model.ts +440 -0
  40. package/src/workspace/preview.ts +116 -0
package/README.md CHANGED
@@ -81,3 +81,7 @@ Nothing else is public. No module does work on import, so a bundler keeps only w
81
81
  ## License
82
82
 
83
83
  FSL-1.1-MIT, see [LICENSE](./LICENSE).
84
+
85
+ ## Workspaces
86
+
87
+ `Workspace`, `SplitView` and `TabStrip` provide standard or roomy layouts, nested resizable panels and open document tabs. Views stay mounted when they move between panels. Applications own document state, close approval and persistence. See the [workspace guide](https://adecore.dev/ui/layout/workspace) for the controlled layout model and demos.
@@ -1,9 +1,10 @@
1
- import type { PointerEvent as ReactPointerEvent, Ref } from 'react';
1
+ import type { CSSProperties, PointerEvent as ReactPointerEvent, Ref } from 'react';
2
2
  import type { ColumnEdge } from './useColumnResize.ts';
3
3
  export interface ColumnResizeHandleProps {
4
4
  from: ColumnEdge;
5
5
  onPointerDown(event: ReactPointerEvent<HTMLElement>): void;
6
6
  className?: string;
7
+ style?: CSSProperties;
7
8
  ref?: Ref<HTMLDivElement>;
8
9
  }
9
- export declare function ColumnResizeHandle({ from, onPointerDown, className, ref }: ColumnResizeHandleProps): import("react").JSX.Element;
10
+ export declare function ColumnResizeHandle({ from, onPointerDown, className, style, ref }: ColumnResizeHandleProps): import("react").JSX.Element;
@@ -7,6 +7,6 @@ const PLACEMENT = {
7
7
  bottom: 'inset-x-0 top-0 h-2 cursor-row-resize'
8
8
  };
9
9
  /* An invisible strip over the free edge of a column, so the column needs `relative` and draws its own border. */
10
- export function ColumnResizeHandle({ from, onPointerDown, className, ref }) {
11
- return _jsx("div", { ref: ref, className: clsx('absolute z-10', PLACEMENT[from], className), onPointerDown: onPointerDown });
10
+ export function ColumnResizeHandle({ from, onPointerDown, className, style, ref }) {
11
+ return _jsx("div", { ref: ref, className: clsx('absolute z-10', PLACEMENT[from], className), style: style, onPointerDown: onPointerDown });
12
12
  }
@@ -2,6 +2,7 @@ import { type KeyboardEvent as ReactKeyboardEvent, type ReactNode, type Ref, typ
2
2
  export interface SlidingColumnProps {
3
3
  open: boolean;
4
4
  width: number;
5
+ gap?: number;
5
6
  bounds: {
6
7
  min: number;
7
8
  max(): number;
@@ -16,4 +17,4 @@ export interface SlidingColumnProps {
16
17
  ref?: Ref<HTMLElement>;
17
18
  children?: ReactNode;
18
19
  }
19
- export declare function SlidingColumn({ open, width, bounds, onWidthChange, instant, body, className, ref, children }: SlidingColumnProps): import("react").JSX.Element;
20
+ export declare function SlidingColumn({ open, width, gap, bounds, onWidthChange, instant, body, className, ref, children }: SlidingColumnProps): import("react").JSX.Element;
@@ -12,7 +12,7 @@ const TRANSITION_MS = 200;
12
12
  * moves, and they stay mounted until the slide is over, so a close plays out. The left edge is the
13
13
  * drag handle.
14
14
  */
15
- export function SlidingColumn({ open, width, bounds, onWidthChange, instant = false, body, className, ref, children }) {
15
+ export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, instant = false, body, className, ref, children }) {
16
16
  /* Closed and done animating. Until then the contents stay mounted, so a close plays out. */
17
17
  const [settled, setSettled] = useState(!open);
18
18
  /* A width that lands without a transition fires no `transitionend`, so the motion it would have
@@ -22,7 +22,14 @@ export function SlidingColumn({ open, width, bounds, onWidthChange, instant = fa
22
22
  }
23
23
  const present = open || !settled;
24
24
  const column = useRef(null);
25
- const { startResize } = useColumnResize(column, { ...bounds, size: width, from: 'right', onSize: onWidthChange });
25
+ const spacing = Math.max(0, Math.round(gap));
26
+ const { startResize } = useColumnResize(column, {
27
+ min: bounds.min + spacing,
28
+ max: () => bounds.max() + spacing,
29
+ size: width + spacing,
30
+ from: 'right',
31
+ onSize: (next) => onWidthChange(next - spacing)
32
+ });
26
33
  useEffect(() => {
27
34
  if (open || settled) {
28
35
  return;
@@ -33,9 +40,9 @@ export function SlidingColumn({ open, width, bounds, onWidthChange, instant = fa
33
40
  window.clearTimeout(timer);
34
41
  };
35
42
  }, [open, settled]);
36
- return (_jsx("aside", { ref: mergeRefs(column, ref), inert: !open, "data-instant": instant ? '' : undefined, className: clsx('sliding-column flex h-full shrink-0 justify-end overflow-hidden', className), style: { width: open ? width : 0 }, onTransitionEnd: (event) => {
43
+ return (_jsx("aside", { ref: mergeRefs(column, ref), inert: !open, "data-instant": instant ? '' : undefined, "data-gap": spacing > 0 ? '' : undefined, className: clsx('sliding-column flex h-full shrink-0 justify-end overflow-hidden', className), style: { width: open ? width + spacing : 0 }, onTransitionEnd: (event) => {
37
44
  if (event.propertyName === 'width' && event.target === event.currentTarget) {
38
45
  setSettled(!open);
39
46
  }
40
- }, children: present && (_jsxs("div", { ref: body?.ref, tabIndex: body ? -1 : undefined, className: "relative flex h-full shrink-0 flex-col border-l border-border bg-surface outline-none", style: { width }, onKeyDown: body?.onKeyDown, children: [open && _jsx(ColumnResizeHandle, { from: "right", onPointerDown: startResize }), children] })) }));
47
+ }, children: present && (_jsxs("div", { ref: body?.ref, tabIndex: body ? -1 : undefined, className: clsx('relative flex h-full shrink-0 flex-col bg-surface outline-none', spacing === 0 && 'border-l border-border'), style: { width, marginLeft: spacing }, onKeyDown: body?.onKeyDown, children: [open && (_jsx(ColumnResizeHandle, { from: "right", className: "sliding-column-resize", style: spacing > 0 ? { left: -spacing, width: spacing } : undefined, onPointerDown: startResize })), children] })) }));
41
48
  }
package/dist/index.d.ts CHANGED
@@ -83,3 +83,9 @@ export type { FileTreeRootProps } from './file-tree/parts.tsx';
83
83
  export type { FileTree as FileTreeModel, FileTreeVisibleRow, FileTreeDragAndDropConfig, FileTreeDropContext, FileTreeDropResult } from '@pierre/trees';
84
84
  export type { FoldKeyOf, SortRow } from './file-tree/helpers.ts';
85
85
  export type { TreeRootProps, TreeRowProps, TreeChevronProps, TreeChevronSlotProps, TreeLabelProps, TreeDecorationProps, TreeControlProps, TreeCheckboxProps } from './tree/parts.tsx';
86
+ export { Workspace, type WorkspaceProps } from './workspace/Workspace.tsx';
87
+ export type { WorkspaceLayout } from './workspace/context.ts';
88
+ export { DocumentTab, TabStrip, type DocumentTabProps, type TabStripProps, type TabStripItem } from './workspace/TabStrip.tsx';
89
+ export { SplitView, type SplitViewProps, type SplitViewInfo, type SplitViewBounds, type SplitDropTarget, type SplitDrag } from './workspace/SplitView.tsx';
90
+ export { createSplitLayout, normalizeSplitLayout, updateSplitLayout, splitPanes, type SplitAxis, type SplitSide, type SplitPane, type SplitBranch, type SplitNode, type SplitLayout, type SplitCommand } from './workspace/model.ts';
91
+ export type { SplitRect, SplitEdges, SplitMinimum } from './workspace/geometry.ts';
package/dist/index.js CHANGED
@@ -77,3 +77,7 @@ export { useMeasuredWidth } from './useMeasuredWidth.js';
77
77
  export { useNow, useTickingText } from './useNow.js';
78
78
  export { ZOOM_PRESETS } from './zoom.js';
79
79
  export { useFileTree } from './file-tree/use-file-tree.js';
80
+ export { Workspace } from './workspace/Workspace.js';
81
+ export { DocumentTab, TabStrip } from './workspace/TabStrip.js';
82
+ export { SplitView } from './workspace/SplitView.js';
83
+ export { createSplitLayout, normalizeSplitLayout, updateSplitLayout, splitPanes } from './workspace/model.js';
@@ -64,5 +64,15 @@
64
64
  "label": "Files",
65
65
  "expand": "Expand",
66
66
  "collapse": "Collapse"
67
+ },
68
+ "workspace": {
69
+ "tabs": "Open views",
70
+ "unsaved": "Unsaved changes",
71
+ "closeTab": "Close {{name}}",
72
+ "resize": "Resize panels",
73
+ "pane": "Panel",
74
+ "movePane": "Move panel",
75
+ "maximize": "Maximize panel",
76
+ "restore": "Restore panels"
67
77
  }
68
78
  }
@@ -64,5 +64,15 @@
64
64
  "label": "Bestanden",
65
65
  "expand": "Uitvouwen",
66
66
  "collapse": "Invouwen"
67
+ },
68
+ "workspace": {
69
+ "tabs": "Open views",
70
+ "unsaved": "Niet-opgeslagen wijzigingen",
71
+ "closeTab": "Sluit {{name}}",
72
+ "resize": "Panelen vergroten of verkleinen",
73
+ "pane": "Paneel",
74
+ "movePane": "Paneel verplaatsen",
75
+ "maximize": "Paneel maximaliseren",
76
+ "restore": "Panelen herstellen"
67
77
  }
68
78
  }
package/dist/theme.css CHANGED
@@ -26,6 +26,8 @@
26
26
  own clips its background to the padding box (see `@layer base`), or the border would blend
27
27
  with that surface and never with the ground behind it. */
28
28
  --border: rgb(0 0 0 / 0.11);
29
+ /* Opaque, so a gap has the same color on every surface. */
30
+ --separator: oklch(0.922 0.003 286.347);
29
31
  --border-strong: rgb(0 0 0 / 0.2);
30
32
  /* A hairline that stays out of the way: on a sunken field a full `--border` draws a box. */
31
33
  --border-soft: rgb(0 0 0 / 0.06);
@@ -94,6 +96,7 @@
94
96
  --surface-active: #28282e;
95
97
  /* White at a low alpha, which is what gives a dark surface its lifted edge. */
96
98
  --border: rgb(255 255 255 / 0.07);
99
+ --separator: oklch(0.236 0.006 285.956);
97
100
  --border-strong: rgb(255 255 255 / 0.13);
98
101
  --border-soft: rgb(255 255 255 / 0.03);
99
102
  --text: #ececf1;
@@ -205,6 +208,7 @@
205
208
  --color-surface-hover: var(--surface-hover);
206
209
  --color-surface-active: var(--surface-active);
207
210
  --color-border: var(--border);
211
+ --color-separator: var(--separator);
208
212
  --color-border-strong: var(--border-strong);
209
213
  --color-border-soft: var(--border-soft);
210
214
  --color-text: var(--text);
@@ -567,6 +571,17 @@
567
571
 
568
572
  /* A `SlidingColumn` opening and closing. A drag or a width restored on load lands at once. */
569
573
  .sliding-column { transition: width 200ms cubic-bezier(0, 0, 0.2, 1); }
574
+ .sliding-column[data-gap] { background: var(--separator); }
575
+ .sliding-column-resize { touch-action: none; }
576
+ .sliding-column[data-gap] .sliding-column-resize::after {
577
+ content: '';
578
+ position: absolute;
579
+ inset-block: 0;
580
+ left: calc(50% - 1px);
581
+ width: 2px;
582
+ }
583
+ .sliding-column[data-gap] .sliding-column-resize:hover::after,
584
+ .sliding-column[data-gap][data-resizing] .sliding-column-resize::after { background: var(--accent); }
570
585
  .sliding-column[data-resizing],
571
586
  .sliding-column[data-instant] { transition: none; }
572
587
  }
@@ -706,3 +721,169 @@
706
721
  }
707
722
  }
708
723
  }
724
+
725
+ @layer components {
726
+ .ade-workspace {
727
+ display: flex;
728
+ width: 100%;
729
+ height: 100%;
730
+ min-width: 0;
731
+ min-height: 0;
732
+ overflow: hidden;
733
+ background: var(--separator);
734
+ --border: var(--separator);
735
+ }
736
+ .ade-workspace-center {
737
+ display: flex;
738
+ flex: 1;
739
+ min-width: 0;
740
+ min-height: 0;
741
+ flex-direction: column;
742
+ }
743
+ .ade-workspace-sidebar, .ade-workspace-side-panel {
744
+ flex: none;
745
+ min-width: 0;
746
+ min-height: 0;
747
+ overflow: hidden;
748
+ background: var(--surface);
749
+ }
750
+ .ade-workspace-sidebar { border-right: 1px solid var(--separator); }
751
+ .ade-workspace-side-panel { border-left: 1px solid var(--separator); }
752
+ .ade-workspace-toolbar {
753
+ flex: none;
754
+ overflow: hidden;
755
+ background: var(--surface);
756
+ border-bottom: 1px solid var(--separator);
757
+ }
758
+ .ade-workspace-content {
759
+ position: relative;
760
+ flex: 1;
761
+ min-width: 0;
762
+ min-height: 0;
763
+ overflow: hidden;
764
+ background: var(--surface);
765
+ }
766
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-center { gap: 8px; }
767
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sidebar:not([data-auto]) { margin-right: 8px; }
768
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-side-panel:not([data-auto]) { margin-left: 8px; }
769
+ .ade-workspace-sidebar[data-auto], .ade-workspace-side-panel[data-auto] { display: contents; }
770
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sidebar,
771
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-side-panel,
772
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-center > .ade-workspace-toolbar { border: 0; }
773
+ .ade-workspace[data-layout="roomy"][data-sidebar] > .ade-workspace-center > .ade-workspace-toolbar { border-bottom-left-radius: 8px; }
774
+ .ade-workspace[data-layout="roomy"][data-side-panel] > .ade-workspace-center > .ade-workspace-toolbar { border-bottom-right-radius: 8px; }
775
+ .ade-split-view {
776
+ position: relative;
777
+ isolation: isolate;
778
+ width: 100%;
779
+ height: 100%;
780
+ min-width: 0;
781
+ min-height: 0;
782
+ overflow: hidden;
783
+ background: var(--separator);
784
+ --border: var(--separator);
785
+ }
786
+ .ade-split-pane, .ade-split-content {
787
+ position: absolute;
788
+ min-width: 0;
789
+ min-height: 0;
790
+ overflow: hidden;
791
+ background: var(--surface);
792
+ }
793
+ .ade-split-pane[hidden], .ade-split-content[hidden] { display: none; }
794
+ .ade-split-header {
795
+ display: flex;
796
+ align-items: center;
797
+ min-width: 0;
798
+ border-bottom: 1px solid var(--separator);
799
+ }
800
+ .ade-split-content { outline-offset: -2px; }
801
+ .ade-split-view[data-dragging] > .ade-split-content { pointer-events: none; }
802
+ .ade-split-divider {
803
+ position: absolute;
804
+ z-index: 3;
805
+ touch-action: none;
806
+ outline-offset: 2px;
807
+ }
808
+ /* The 2px line also marks keyboard focus. */
809
+ .ade-split-divider:focus-visible { outline: none; }
810
+ .ade-split-divider[aria-orientation="vertical"] { cursor: col-resize; }
811
+ .ade-split-divider[aria-orientation="horizontal"] { cursor: row-resize; }
812
+ .ade-split-divider > span {
813
+ position: absolute;
814
+ inset: 0;
815
+ }
816
+ .ade-split-divider[aria-orientation="vertical"] > span { left: -3px; right: -3px; }
817
+ .ade-split-divider[aria-orientation="horizontal"] > span { top: -3px; bottom: -3px; }
818
+ .ade-split-view[data-layout="roomy"] > .ade-split-divider > span { inset: 0; }
819
+ .ade-split-divider::after { content: ""; position: absolute; pointer-events: none; }
820
+ .ade-split-divider[aria-orientation="vertical"]::after { inset-block: 0; left: -1px; width: 2px; }
821
+ .ade-split-divider[aria-orientation="horizontal"]::after { inset-inline: 0; top: -1px; height: 2px; }
822
+ .ade-split-view[data-layout="roomy"] > .ade-split-divider[aria-orientation="vertical"]::after { left: 3px; }
823
+ .ade-split-view[data-layout="roomy"] > .ade-split-divider[aria-orientation="horizontal"]::after { top: 3px; }
824
+ .ade-workspace[data-layout="roomy"][data-toolbar][data-sidebar] > .ade-workspace-center > .ade-workspace-content { border-top-left-radius: 8px; }
825
+ .ade-workspace[data-layout="roomy"][data-toolbar][data-side-panel] > .ade-workspace-center > .ade-workspace-content { border-top-right-radius: 8px; }
826
+ .ade-split-divider:is(:hover, :focus-visible, [data-resizing])::after { background: var(--accent); }
827
+ .ade-split-preview {
828
+ position: absolute;
829
+ inset: 0;
830
+ z-index: 4;
831
+ pointer-events: none;
832
+ overflow: visible;
833
+ }
834
+ .ade-split-preview path {
835
+ fill: color-mix(in srgb, var(--accent) 8%, transparent);
836
+ stroke: var(--accent);
837
+ stroke-width: 2px;
838
+ transition: d 150ms ease-out;
839
+ }
840
+ .ade-tab-strip {
841
+ position: relative;
842
+ display: flex;
843
+ flex: 1;
844
+ height: 100%;
845
+ min-width: 0;
846
+ align-items: stretch;
847
+ overflow-x: auto;
848
+ scrollbar-width: none;
849
+ }
850
+ .ade-tab-strip::-webkit-scrollbar { display: none; }
851
+ .ade-document-tab {
852
+ position: relative;
853
+ display: inline-flex;
854
+ flex: none;
855
+ align-items: center;
856
+ gap: 4px;
857
+ padding: 0 8px 0 12px;
858
+ color: var(--text-muted);
859
+ }
860
+ .ade-document-tab:hover { color: var(--text); background: var(--surface-raised); }
861
+ .ade-document-tab[data-active] { color: var(--text); }
862
+ .ade-document-tab[data-active]::after {
863
+ content: "";
864
+ position: absolute;
865
+ inset-inline: 0;
866
+ bottom: 0;
867
+ height: 2px;
868
+ background: var(--text-muted);
869
+ }
870
+ .ade-tab-strip[data-focused] .ade-document-tab[data-active]::after { background: var(--accent); }
871
+ .ade-document-tab-open {
872
+ display: inline-flex;
873
+ height: 100%;
874
+ min-width: 0;
875
+ max-width: 160px;
876
+ align-items: center;
877
+ gap: 6px;
878
+ font-size: var(--text-sm);
879
+ color: inherit;
880
+ outline-offset: -2px;
881
+ white-space: nowrap;
882
+ }
883
+ .ade-document-tab-close { opacity: 0; }
884
+ .ade-document-tab:is(:hover, :focus-within, [data-active]) .ade-document-tab-close { opacity: 1; }
885
+ .ade-document-tab[data-detail] .ade-document-tab-open { max-width: 200px; }
886
+ .ade-tab-unsaved { width: 6px; height: 6px; flex: none; border-radius: 999px; background: var(--text-muted); }
887
+ .ade-tab-insertion { position: absolute; top: 4px; bottom: 4px; width: 2px; pointer-events: none; background: var(--accent); }
888
+ @media (prefers-reduced-motion: reduce) { .ade-split-preview path { transition: none; } }
889
+ }
@@ -4,19 +4,6 @@ export const clampColumnSize = (options, size) => {
4
4
  };
5
5
  /* Where the pointer sits on the axis this edge belongs to. */
6
6
  const along = (from, event) => (from === 'left' || from === 'right' ? event.clientX : event.clientY);
7
- /* The edge the column is pinned to, in page coordinates, or the window's own when there is no box yet. */
8
- const anchorOf = (from, rect) => {
9
- switch (from) {
10
- case 'left':
11
- return rect?.left ?? 0;
12
- case 'right':
13
- return rect?.right ?? window.innerWidth;
14
- case 'top':
15
- return rect?.top ?? 0;
16
- case 'bottom':
17
- return rect?.bottom ?? window.innerHeight;
18
- }
19
- };
20
7
  /*
21
8
  * One resizable column or row: a handle that drags the size its owner keeps. The handle takes the
22
9
  * pointer capture, so the drag survives leaving the few pixels it is wide, and `[data-resizing]` on
@@ -27,23 +14,31 @@ export const useColumnResize = (ref, options) => {
27
14
  event.preventDefault();
28
15
  const handle = event.currentTarget;
29
16
  const column = ref.current;
30
- const anchor = anchorOf(options.from, column?.getBoundingClientRect());
17
+ const rect = column?.getBoundingClientRect();
18
+ const size = (options.from === 'left' || options.from === 'right' ? rect?.width : rect?.height) ?? options.size;
19
+ const start = along(options.from, event);
31
20
  /* The far edge grows as the pointer comes towards it; the near edge grows as it goes away. */
32
21
  const growsTowardsAnchor = options.from === 'right' || options.from === 'bottom';
33
22
  column?.setAttribute('data-resizing', 'true');
34
23
  const onMove = (move) => {
35
- const reach = along(options.from, move);
36
- options.onSize(clampColumnSize(options, growsTowardsAnchor ? anchor - reach : reach - anchor));
24
+ const delta = along(options.from, move) - start;
25
+ options.onSize(clampColumnSize(options, size + (growsTowardsAnchor ? -delta : delta)));
37
26
  };
38
27
  const onUp = () => {
39
28
  handle.removeEventListener('pointermove', onMove);
40
29
  handle.removeEventListener('pointerup', onUp);
41
- handle.releasePointerCapture(event.pointerId);
30
+ handle.removeEventListener('pointercancel', onUp);
31
+ handle.removeEventListener('lostpointercapture', onUp);
32
+ if (handle.hasPointerCapture(event.pointerId)) {
33
+ handle.releasePointerCapture(event.pointerId);
34
+ }
42
35
  column?.removeAttribute('data-resizing');
43
36
  };
44
37
  handle.setPointerCapture(event.pointerId);
45
38
  handle.addEventListener('pointermove', onMove);
46
39
  handle.addEventListener('pointerup', onUp);
40
+ handle.addEventListener('pointercancel', onUp);
41
+ handle.addEventListener('lostpointercapture', onUp);
47
42
  };
48
43
  return { startResize };
49
44
  };
@@ -0,0 +1,49 @@
1
+ import { type DragEvent, type ReactNode } from 'react';
2
+ import { useRender } from '@base-ui-components/react/use-render';
3
+ import { type WorkspaceLayout } from './context.ts';
4
+ import { type SplitEdges, type SplitMinimum, type SplitRect } from './geometry.ts';
5
+ import { type SplitCommand, type SplitLayout, type SplitPane } from './model.ts';
6
+ import { type TabStripItem, type TabStripProps } from './TabStrip.tsx';
7
+ export interface SplitViewInfo {
8
+ paneId: string;
9
+ active: boolean;
10
+ }
11
+ export interface SplitViewBounds extends SplitViewInfo {
12
+ viewId: string;
13
+ /** Viewport coordinates, suitable for a native surface kept outside the layout's DOM. */
14
+ rect: SplitRect;
15
+ borderRadius: string;
16
+ }
17
+ export interface SplitDropTarget {
18
+ paneId: string;
19
+ /** A body drop names its zone; an omitted side denotes the tab strip. */
20
+ side?: 'left' | 'right' | 'top' | 'bottom' | 'center';
21
+ /** A gap in the original tab order, before the source is lifted out. */
22
+ index?: number;
23
+ }
24
+ export interface SplitDrag {
25
+ sourceId: string;
26
+ viewId?: string;
27
+ }
28
+ export interface SplitViewProps extends Omit<useRender.ComponentProps<'div'>, 'children'> {
29
+ value: SplitLayout;
30
+ onValueChange(value: SplitLayout, command: SplitCommand): void;
31
+ renderView(id: string, info: SplitViewInfo): ReactNode;
32
+ getTab?(id: string): Omit<TabStripItem, 'id' | 'tabId' | 'panelId'>;
33
+ renderPaneHeader?(pane: SplitPane): ReactNode;
34
+ onClose?(id: string): void;
35
+ onTabDoubleClick?: TabStripProps['onDoubleClick'];
36
+ onTabContextMenu?: TabStripProps['onContextMenu'];
37
+ onViewDragStart?(id: string, event: DragEvent<HTMLButtonElement>): void;
38
+ canDrop?(drag: SplitDrag, target: SplitDropTarget): boolean;
39
+ canDropExternal?(event: DragEvent<HTMLDivElement>, target: SplitDropTarget): boolean;
40
+ onExternalDrop?(target: SplitDropTarget, event: DragEvent<HTMLDivElement>): void;
41
+ onViewBoundsChange?(bounds: readonly SplitViewBounds[]): void;
42
+ minimumResizeShare?: number;
43
+ minimumSize?: SplitMinimum | ((pane: SplitPane) => SplitMinimum);
44
+ layout?: WorkspaceLayout;
45
+ flushEdges?: SplitEdges;
46
+ showTabs?: boolean;
47
+ headerHeight?: number;
48
+ }
49
+ export declare function SplitView({ value, onValueChange, renderView, getTab, renderPaneHeader, onClose, onTabDoubleClick, onTabContextMenu, onViewDragStart, canDrop, canDropExternal, onExternalDrop, onViewBoundsChange, minimumSize, minimumResizeShare, layout: ownLayout, flushEdges, showTabs, headerHeight, render, ref, className, style, ...props }: SplitViewProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;