@adea-ai/ui 0.122.3 → 0.123.1
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/components/layout/split-layout/resize-controller-core-retry.d.ts +13 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.js +63 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.js.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-core.d.ts +13 -0
- package/dist/components/layout/split-layout/resize-controller-core.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-core.js +63 -0
- package/dist/components/layout/split-layout/resize-controller-core.js.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts +3 -0
- package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-retry-2.js +2 -0
- package/dist/components/layout/split-layout/resize-controller-retry.d.ts +2 -0
- package/dist/components/layout/split-layout/resize-controller-retry.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-retry.js +2 -0
- package/dist/components/layout/split-layout/resize-controller.d.ts +2 -0
- package/dist/components/layout/split-layout/resize-controller.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller.js +2 -0
- package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -1
- package/dist/components/layout/split-layout/split-layout.js +171 -60
- package/dist/components/layout/split-layout/split-layout.js.map +1 -1
- package/dist/lib/themes.d.ts +8 -7
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +53 -55
- package/dist/lib/themes.js.map +1 -1
- package/dist/r/appearance-editor.json +1 -1
- package/dist/r/lib.json +1 -1
- package/dist/r/registry.json +29 -3
- package/dist/r/split-layout.json +26 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
- package/dist/r/src/components/layout/split-layout/resize-controller.tsx +1 -0
- package/dist/r/src/components/layout/split-layout/split-layout.tsx +197 -49
- package/dist/r/src/lib/themes.ts +56 -73
- package/dist/r/theming.json +1 -1
- package/package.json +2 -2
- package/registry.json +29 -3
- package/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
- package/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
- package/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
- package/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
- package/src/components/layout/split-layout/resize-controller.tsx +1 -0
- package/src/components/layout/split-layout/split-layout.tsx +197 -49
- package/src/lib/themes.ts +56 -73
package/dist/r/registry.json
CHANGED
|
@@ -1811,6 +1811,7 @@
|
|
|
1811
1811
|
],
|
|
1812
1812
|
"registryDependencies": [
|
|
1813
1813
|
"@adea-ai/ui/action-button",
|
|
1814
|
+
"@adea-ai/ui/button",
|
|
1814
1815
|
"lib"
|
|
1815
1816
|
],
|
|
1816
1817
|
"files": [
|
|
@@ -1834,6 +1835,31 @@
|
|
|
1834
1835
|
"type": "registry:ui",
|
|
1835
1836
|
"target": "components/layout/split-layout/model.ts"
|
|
1836
1837
|
},
|
|
1838
|
+
{
|
|
1839
|
+
"path": "src/components/layout/split-layout/resize-controller-core-retry.tsx",
|
|
1840
|
+
"type": "registry:ui",
|
|
1841
|
+
"target": "components/layout/split-layout/resize-controller-core-retry.tsx"
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
"path": "src/components/layout/split-layout/resize-controller-core.tsx",
|
|
1845
|
+
"type": "registry:ui",
|
|
1846
|
+
"target": "components/layout/split-layout/resize-controller-core.tsx"
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
"path": "src/components/layout/split-layout/resize-controller-retry-2.tsx",
|
|
1850
|
+
"type": "registry:ui",
|
|
1851
|
+
"target": "components/layout/split-layout/resize-controller-retry-2.tsx"
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
"path": "src/components/layout/split-layout/resize-controller-retry.tsx",
|
|
1855
|
+
"type": "registry:ui",
|
|
1856
|
+
"target": "components/layout/split-layout/resize-controller-retry.tsx"
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
"path": "src/components/layout/split-layout/resize-controller.tsx",
|
|
1860
|
+
"type": "registry:ui",
|
|
1861
|
+
"target": "components/layout/split-layout/resize-controller.tsx"
|
|
1862
|
+
},
|
|
1837
1863
|
{
|
|
1838
1864
|
"path": "src/components/layout/split-layout/split-layout.tsx",
|
|
1839
1865
|
"type": "registry:ui",
|
|
@@ -1998,7 +2024,7 @@
|
|
|
1998
2024
|
"title": "Appearance Editor",
|
|
1999
2025
|
"description": "",
|
|
2000
2026
|
"dependencies": [
|
|
2001
|
-
"@adea-ai/themes@^0.
|
|
2027
|
+
"@adea-ai/themes@^0.10.4",
|
|
2002
2028
|
"@kobalte/core@^0.13.14",
|
|
2003
2029
|
"lucide-solid@^1.51.0"
|
|
2004
2030
|
],
|
|
@@ -2477,7 +2503,7 @@
|
|
|
2477
2503
|
"title": "Theming",
|
|
2478
2504
|
"description": "",
|
|
2479
2505
|
"dependencies": [
|
|
2480
|
-
"@adea-ai/themes@^0.
|
|
2506
|
+
"@adea-ai/themes@^0.10.4",
|
|
2481
2507
|
"@kobalte/core@^0.13.14",
|
|
2482
2508
|
"lucide-solid@^1.51.0"
|
|
2483
2509
|
],
|
|
@@ -2592,7 +2618,7 @@
|
|
|
2592
2618
|
"title": "Class-name and token helpers",
|
|
2593
2619
|
"description": "The private helpers the components import: the class-name merge, the variant recipes, the token manifest and the theme bridge. Installed automatically as a peer of any component that needs it.",
|
|
2594
2620
|
"dependencies": [
|
|
2595
|
-
"@adea-ai/themes@^0.
|
|
2621
|
+
"@adea-ai/themes@^0.10.4",
|
|
2596
2622
|
"class-variance-authority@^0.7.1",
|
|
2597
2623
|
"cn@^0.4.0"
|
|
2598
2624
|
],
|
package/dist/r/split-layout.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
],
|
|
10
10
|
"registryDependencies": [
|
|
11
11
|
"@adea-ai/ui/action-button",
|
|
12
|
+
"@adea-ai/ui/button",
|
|
12
13
|
"lib"
|
|
13
14
|
],
|
|
14
15
|
"files": [
|
|
@@ -32,6 +33,31 @@
|
|
|
32
33
|
"type": "registry:ui",
|
|
33
34
|
"target": "components/layout/split-layout/model.ts"
|
|
34
35
|
},
|
|
36
|
+
{
|
|
37
|
+
"path": "src/components/layout/split-layout/resize-controller-core-retry.tsx",
|
|
38
|
+
"type": "registry:ui",
|
|
39
|
+
"target": "components/layout/split-layout/resize-controller-core-retry.tsx"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"path": "src/components/layout/split-layout/resize-controller-core.tsx",
|
|
43
|
+
"type": "registry:ui",
|
|
44
|
+
"target": "components/layout/split-layout/resize-controller-core.tsx"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"path": "src/components/layout/split-layout/resize-controller-retry-2.tsx",
|
|
48
|
+
"type": "registry:ui",
|
|
49
|
+
"target": "components/layout/split-layout/resize-controller-retry-2.tsx"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"path": "src/components/layout/split-layout/resize-controller-retry.tsx",
|
|
53
|
+
"type": "registry:ui",
|
|
54
|
+
"target": "components/layout/split-layout/resize-controller-retry.tsx"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"path": "src/components/layout/split-layout/resize-controller.tsx",
|
|
58
|
+
"type": "registry:ui",
|
|
59
|
+
"target": "components/layout/split-layout/resize-controller.tsx"
|
|
60
|
+
},
|
|
35
61
|
{
|
|
36
62
|
"path": "src/components/layout/split-layout/split-layout.tsx",
|
|
37
63
|
"type": "registry:ui",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Resizable from '@corvu/resizable'
|
|
2
|
+
import { type Accessor, type JSX } from 'solid-js'
|
|
3
|
+
import {
|
|
4
|
+
listLeaves,
|
|
5
|
+
MIN_SPLIT_RATIO,
|
|
6
|
+
MAX_SPLIT_RATIO,
|
|
7
|
+
type SplitLayoutBranch,
|
|
8
|
+
type SplitLayoutLeaf,
|
|
9
|
+
} from './model'
|
|
10
|
+
|
|
11
|
+
export type SplitLayoutResizeControllerProps<L extends SplitLayoutLeaf> = {
|
|
12
|
+
branch: Accessor<SplitLayoutBranch<L>>
|
|
13
|
+
rect: Accessor<JSX.CSSProperties>
|
|
14
|
+
domId: (id: string) => string
|
|
15
|
+
root: () => HTMLDivElement | undefined
|
|
16
|
+
labelForSeparator?: (branch: SplitLayoutBranch<L>) => string
|
|
17
|
+
onResize: (splitId: string, ratio: number) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Corvu stays behind the first real split while the pane owners remain mounted. */
|
|
21
|
+
export default function SplitLayoutResizeController<L extends SplitLayoutLeaf>(
|
|
22
|
+
props: SplitLayoutResizeControllerProps<L>
|
|
23
|
+
) {
|
|
24
|
+
const id = () => props.branch().id
|
|
25
|
+
return (
|
|
26
|
+
<Resizable
|
|
27
|
+
orientation={props.branch().direction === 'row' ? 'horizontal' : 'vertical'}
|
|
28
|
+
sizes={[props.branch().ratio, 1 - props.branch().ratio]}
|
|
29
|
+
keyboardDelta={0.05}
|
|
30
|
+
onSizesChange={(sizes) => {
|
|
31
|
+
// Corvu also reports panel registration/unregistration while
|
|
32
|
+
// mounting or disposing a controller. Those incomplete or
|
|
33
|
+
// hidden-host sizes are not user resize intent.
|
|
34
|
+
const root = props.root()
|
|
35
|
+
if (!root?.isConnected || root.getClientRects().length === 0) return
|
|
36
|
+
if (
|
|
37
|
+
sizes.length !== 2 ||
|
|
38
|
+
sizes.some((size) => !Number.isFinite(size)) ||
|
|
39
|
+
Math.abs(sizes[0]! + sizes[1]! - 1) > 0.000001
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
if (sizes[0] !== undefined && Math.abs(sizes[0] - props.branch().ratio) > 0.000001)
|
|
43
|
+
props.onResize(id(), sizes[0])
|
|
44
|
+
}}
|
|
45
|
+
style={props.rect()}
|
|
46
|
+
class="pointer-events-none absolute flex min-h-0 min-w-0 data-[orientation=vertical]:flex-col"
|
|
47
|
+
>
|
|
48
|
+
<Resizable.Panel minSize={MIN_SPLIT_RATIO} maxSize={MAX_SPLIT_RATIO} aria-hidden="true" />
|
|
49
|
+
<Resizable.Handle
|
|
50
|
+
aria-label={
|
|
51
|
+
props.labelForSeparator?.(props.branch()) ??
|
|
52
|
+
(props.branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
53
|
+
}
|
|
54
|
+
aria-orientation={props.branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
55
|
+
aria-controls={listLeaves(props.branch())
|
|
56
|
+
.map((leaf) => props.domId(leaf.id))
|
|
57
|
+
.join(' ')}
|
|
58
|
+
aria-valuemin={10}
|
|
59
|
+
aria-valuemax={90}
|
|
60
|
+
aria-valuenow={Math.round(props.branch().ratio * 100)}
|
|
61
|
+
class="pointer-events-auto relative w-px shrink-0 bg-border after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 focus-visible:bg-primary focus-visible:outline-none hover:bg-primary data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:-inset-y-1 data-[orientation=vertical]:after:h-3 data-[orientation=vertical]:after:w-full"
|
|
62
|
+
/>
|
|
63
|
+
<Resizable.Panel minSize={MIN_SPLIT_RATIO} maxSize={MAX_SPLIT_RATIO} aria-hidden="true" />
|
|
64
|
+
</Resizable>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Resizable from '@corvu/resizable'
|
|
2
|
+
import { type Accessor, type JSX } from 'solid-js'
|
|
3
|
+
import {
|
|
4
|
+
listLeaves,
|
|
5
|
+
MIN_SPLIT_RATIO,
|
|
6
|
+
MAX_SPLIT_RATIO,
|
|
7
|
+
type SplitLayoutBranch,
|
|
8
|
+
type SplitLayoutLeaf,
|
|
9
|
+
} from './model'
|
|
10
|
+
|
|
11
|
+
export type SplitLayoutResizeControllerProps<L extends SplitLayoutLeaf> = {
|
|
12
|
+
branch: Accessor<SplitLayoutBranch<L>>
|
|
13
|
+
rect: Accessor<JSX.CSSProperties>
|
|
14
|
+
domId: (id: string) => string
|
|
15
|
+
root: () => HTMLDivElement | undefined
|
|
16
|
+
labelForSeparator?: (branch: SplitLayoutBranch<L>) => string
|
|
17
|
+
onResize: (splitId: string, ratio: number) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Corvu stays behind the first real split while the pane owners remain mounted. */
|
|
21
|
+
export default function SplitLayoutResizeController<L extends SplitLayoutLeaf>(
|
|
22
|
+
props: SplitLayoutResizeControllerProps<L>
|
|
23
|
+
) {
|
|
24
|
+
const id = () => props.branch().id
|
|
25
|
+
return (
|
|
26
|
+
<Resizable
|
|
27
|
+
orientation={props.branch().direction === 'row' ? 'horizontal' : 'vertical'}
|
|
28
|
+
sizes={[props.branch().ratio, 1 - props.branch().ratio]}
|
|
29
|
+
keyboardDelta={0.05}
|
|
30
|
+
onSizesChange={(sizes) => {
|
|
31
|
+
// Corvu also reports panel registration/unregistration while
|
|
32
|
+
// mounting or disposing a controller. Those incomplete or
|
|
33
|
+
// hidden-host sizes are not user resize intent.
|
|
34
|
+
const root = props.root()
|
|
35
|
+
if (!root?.isConnected || root.getClientRects().length === 0) return
|
|
36
|
+
if (
|
|
37
|
+
sizes.length !== 2 ||
|
|
38
|
+
sizes.some((size) => !Number.isFinite(size)) ||
|
|
39
|
+
Math.abs(sizes[0]! + sizes[1]! - 1) > 0.000001
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
if (sizes[0] !== undefined && Math.abs(sizes[0] - props.branch().ratio) > 0.000001)
|
|
43
|
+
props.onResize(id(), sizes[0])
|
|
44
|
+
}}
|
|
45
|
+
style={props.rect()}
|
|
46
|
+
class="pointer-events-none absolute flex min-h-0 min-w-0 data-[orientation=vertical]:flex-col"
|
|
47
|
+
>
|
|
48
|
+
<Resizable.Panel minSize={MIN_SPLIT_RATIO} maxSize={MAX_SPLIT_RATIO} aria-hidden="true" />
|
|
49
|
+
<Resizable.Handle
|
|
50
|
+
aria-label={
|
|
51
|
+
props.labelForSeparator?.(props.branch()) ??
|
|
52
|
+
(props.branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
53
|
+
}
|
|
54
|
+
aria-orientation={props.branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
55
|
+
aria-controls={listLeaves(props.branch())
|
|
56
|
+
.map((leaf) => props.domId(leaf.id))
|
|
57
|
+
.join(' ')}
|
|
58
|
+
aria-valuemin={10}
|
|
59
|
+
aria-valuemax={90}
|
|
60
|
+
aria-valuenow={Math.round(props.branch().ratio * 100)}
|
|
61
|
+
class="pointer-events-auto relative w-px shrink-0 bg-border after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 focus-visible:bg-primary focus-visible:outline-none hover:bg-primary data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:-inset-y-1 data-[orientation=vertical]:after:h-3 data-[orientation=vertical]:after:w-full"
|
|
62
|
+
/>
|
|
63
|
+
<Resizable.Panel minSize={MIN_SPLIT_RATIO} maxSize={MAX_SPLIT_RATIO} aria-hidden="true" />
|
|
64
|
+
</Resizable>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './resize-controller-core'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './resize-controller-core'
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import Resizable from '@corvu/resizable'
|
|
2
1
|
import {
|
|
3
2
|
For,
|
|
4
|
-
|
|
3
|
+
createEffect,
|
|
5
4
|
createComputed,
|
|
5
|
+
createMemo,
|
|
6
6
|
createUniqueId,
|
|
7
7
|
createSignal,
|
|
8
|
+
onMount,
|
|
8
9
|
onCleanup,
|
|
9
10
|
on,
|
|
11
|
+
Show,
|
|
10
12
|
type Accessor,
|
|
11
13
|
type JSX,
|
|
12
14
|
} from 'solid-js'
|
|
15
|
+
import { Dynamic } from 'solid-js/web'
|
|
13
16
|
import { X, GripVertical } from 'lucide-solid'
|
|
14
17
|
import { ActionButton } from '../../composites/action-button'
|
|
18
|
+
import { Button } from '../../ui/button'
|
|
15
19
|
import { cn } from '../../../lib/utils'
|
|
16
20
|
import { computeLayoutFrames, type LayoutRect } from './geometry'
|
|
17
21
|
import { paneDropIntent, type PaneDropIntent } from './drop'
|
|
@@ -21,12 +25,16 @@ const DRAG_PREVIEW_MAX_WIDTH = 384
|
|
|
21
25
|
const DRAG_PREVIEW_MAX_HEIGHT = 256
|
|
22
26
|
import {
|
|
23
27
|
listLeaves,
|
|
24
|
-
MIN_SPLIT_RATIO,
|
|
25
|
-
MAX_SPLIT_RATIO,
|
|
26
28
|
type SplitLayoutBranch,
|
|
27
29
|
type SplitLayoutLeaf,
|
|
28
30
|
type SplitLayoutState,
|
|
29
31
|
} from './model'
|
|
32
|
+
import type { SplitLayoutResizeControllerProps } from './resize-controller-core'
|
|
33
|
+
|
|
34
|
+
type ResizeController<L extends SplitLayoutLeaf> = (
|
|
35
|
+
props: SplitLayoutResizeControllerProps<L>
|
|
36
|
+
) => JSX.Element
|
|
37
|
+
type ResizeControllerState<L extends SplitLayoutLeaf> = ResizeController<L> | false | 'error'
|
|
30
38
|
|
|
31
39
|
export type SplitLayoutProps<L extends SplitLayoutLeaf> = {
|
|
32
40
|
state: SplitLayoutState<L>
|
|
@@ -59,6 +67,16 @@ function rectStyle(rect: LayoutRect): JSX.CSSProperties {
|
|
|
59
67
|
height: `${rect.height * 100}%`,
|
|
60
68
|
}
|
|
61
69
|
}
|
|
70
|
+
function separatorPlaceholderStyle(branch: SplitLayoutBranch, rect: LayoutRect): JSX.CSSProperties {
|
|
71
|
+
const horizontal = branch.direction === 'row'
|
|
72
|
+
return {
|
|
73
|
+
left: `${(rect.x + (horizontal ? rect.width * branch.ratio : 0)) * 100}%`,
|
|
74
|
+
top: `${(rect.y + (horizontal ? 0 : rect.height * branch.ratio)) * 100}%`,
|
|
75
|
+
width: horizontal ? '1px' : `${rect.width * 100}%`,
|
|
76
|
+
height: horizontal ? `${rect.height * 100}%` : '1px',
|
|
77
|
+
transform: horizontal ? 'translateX(-50%)' : 'translateY(-50%)',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
function dragPreviewPlaceholder(source: HTMLElement, message: string) {
|
|
63
81
|
const placeholder = document.createElement('div')
|
|
64
82
|
placeholder.className = cn(
|
|
@@ -171,14 +189,40 @@ function cloneDragPreviewNode(source: Node): Node | undefined {
|
|
|
171
189
|
export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L>) {
|
|
172
190
|
let root: HTMLDivElement | undefined
|
|
173
191
|
let pendingFrame: number | undefined
|
|
192
|
+
let disposed = false
|
|
193
|
+
let resizeLoadGeneration = 0
|
|
194
|
+
let retryWithAlternateImport = false
|
|
195
|
+
let alternateImportAttempts = 0
|
|
196
|
+
let focusHandleAfterRetry = false
|
|
197
|
+
let focusRecoveryButtonAfterError = false
|
|
198
|
+
let focusRestoreEpoch = 0
|
|
199
|
+
let focusEpoch = 0
|
|
200
|
+
let pendingFocusFrame: number | undefined
|
|
174
201
|
const refs = new Map<string, HTMLElement>()
|
|
175
202
|
const domIds = new Map<string, string>()
|
|
176
203
|
const prefix = createUniqueId()
|
|
204
|
+
const resizeReloadWarningId = `${prefix}-resize-reload-warning`
|
|
177
205
|
let nextDomId = 0
|
|
178
206
|
let nextDragId = 0
|
|
179
207
|
let dragPreview: HTMLElement | undefined
|
|
180
208
|
const [drag, setDrag] = createSignal<{ id: string; token: string }>()
|
|
181
209
|
const [drop, setDrop] = createSignal<{ id: string; intent: PaneDropIntent }>()
|
|
210
|
+
const [resizeController, setResizeController] = createSignal<ResizeControllerState<L>>()
|
|
211
|
+
const cancelPendingFocusRestore = () => {
|
|
212
|
+
if (pendingFocusFrame !== undefined) cancelAnimationFrame(pendingFocusFrame)
|
|
213
|
+
pendingFocusFrame = undefined
|
|
214
|
+
}
|
|
215
|
+
const trackFocusChange = (event: FocusEvent) => {
|
|
216
|
+
if (event.target === document.body) return
|
|
217
|
+
focusEpoch += 1
|
|
218
|
+
focusHandleAfterRetry = false
|
|
219
|
+
focusRecoveryButtonAfterError = false
|
|
220
|
+
cancelPendingFocusRestore()
|
|
221
|
+
}
|
|
222
|
+
onMount(() => {
|
|
223
|
+
document.addEventListener('focusin', trackFocusChange)
|
|
224
|
+
onCleanup(() => document.removeEventListener('focusin', trackFocusChange))
|
|
225
|
+
})
|
|
182
226
|
const clearDrag = () => {
|
|
183
227
|
setDrag(undefined)
|
|
184
228
|
setDrop(undefined)
|
|
@@ -221,6 +265,79 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
|
|
|
221
265
|
frame.node.kind === 'split' ? [frame.node] : []
|
|
222
266
|
)
|
|
223
267
|
)
|
|
268
|
+
createEffect(() => {
|
|
269
|
+
if (branches().length === 0) {
|
|
270
|
+
resizeLoadGeneration += 1
|
|
271
|
+
focusHandleAfterRetry = false
|
|
272
|
+
focusRecoveryButtonAfterError = false
|
|
273
|
+
cancelPendingFocusRestore()
|
|
274
|
+
// Keep a loaded controller so a later split does not re-import a facade
|
|
275
|
+
// that may have failed before recovery. Clear only transient UI state.
|
|
276
|
+
if (resizeController() === false || resizeController() === 'error')
|
|
277
|
+
setResizeController(undefined)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
if (resizeController() !== undefined) return
|
|
281
|
+
const retry = retryWithAlternateImport
|
|
282
|
+
const generation = ++resizeLoadGeneration
|
|
283
|
+
setResizeController(false)
|
|
284
|
+
const controllerImport = retry
|
|
285
|
+
? alternateImportAttempts++ === 0
|
|
286
|
+
? import('./resize-controller-retry')
|
|
287
|
+
: import('./resize-controller-retry-2')
|
|
288
|
+
: import('./resize-controller')
|
|
289
|
+
void controllerImport.then(
|
|
290
|
+
({ default: Controller }) => {
|
|
291
|
+
if (disposed || generation !== resizeLoadGeneration || branches().length === 0) return
|
|
292
|
+
const shouldFocusHandle = focusHandleAfterRetry && focusEpoch === focusRestoreEpoch
|
|
293
|
+
focusHandleAfterRetry = false
|
|
294
|
+
focusRecoveryButtonAfterError = false
|
|
295
|
+
retryWithAlternateImport = false
|
|
296
|
+
alternateImportAttempts = 0
|
|
297
|
+
setResizeController(() => Controller)
|
|
298
|
+
if (shouldFocusHandle)
|
|
299
|
+
pendingFocusFrame = requestAnimationFrame(() => {
|
|
300
|
+
pendingFocusFrame = undefined
|
|
301
|
+
if (
|
|
302
|
+
disposed ||
|
|
303
|
+
generation !== resizeLoadGeneration ||
|
|
304
|
+
branches().length === 0 ||
|
|
305
|
+
focusEpoch !== focusRestoreEpoch
|
|
306
|
+
)
|
|
307
|
+
return
|
|
308
|
+
const currentFocus = root?.ownerDocument.activeElement
|
|
309
|
+
if (currentFocus && currentFocus !== root?.ownerDocument.body && currentFocus !== root)
|
|
310
|
+
return
|
|
311
|
+
root?.querySelector<HTMLElement>('[data-corvu-resizable-handle]')?.focus({
|
|
312
|
+
preventScroll: true,
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
},
|
|
316
|
+
() => {
|
|
317
|
+
// A rejected dynamic import can remain cached under its specifier.
|
|
318
|
+
// The next split must start from the alternate module entry as well.
|
|
319
|
+
retryWithAlternateImport = true
|
|
320
|
+
if (!disposed && generation === resizeLoadGeneration && branches().length > 0) {
|
|
321
|
+
setResizeController('error')
|
|
322
|
+
const shouldFocusRetry = focusRecoveryButtonAfterError && focusEpoch === focusRestoreEpoch
|
|
323
|
+
if (shouldFocusRetry)
|
|
324
|
+
pendingFocusFrame = requestAnimationFrame(() => {
|
|
325
|
+
pendingFocusFrame = undefined
|
|
326
|
+
if (
|
|
327
|
+
disposed ||
|
|
328
|
+
generation !== resizeLoadGeneration ||
|
|
329
|
+
branches().length === 0 ||
|
|
330
|
+
focusEpoch !== focusRestoreEpoch
|
|
331
|
+
)
|
|
332
|
+
return
|
|
333
|
+
root?.querySelector<HTMLButtonElement>('[data-resize-retry]')?.focus({
|
|
334
|
+
preventScroll: true,
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
})
|
|
224
341
|
createComputed(() => {
|
|
225
342
|
const current = drag()
|
|
226
343
|
if (current && (!props.onMove || !leafMap().has(current.id))) clearDrag()
|
|
@@ -288,6 +405,10 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
|
|
|
288
405
|
)
|
|
289
406
|
)
|
|
290
407
|
onCleanup(() => {
|
|
408
|
+
disposed = true
|
|
409
|
+
focusHandleAfterRetry = false
|
|
410
|
+
focusRecoveryButtonAfterError = false
|
|
411
|
+
cancelPendingFocusRestore()
|
|
291
412
|
if (pendingFrame !== undefined) cancelAnimationFrame(pendingFrame)
|
|
292
413
|
clearDrag()
|
|
293
414
|
refs.clear()
|
|
@@ -452,62 +573,89 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
|
|
|
452
573
|
}}
|
|
453
574
|
</For>
|
|
454
575
|
<span class="sr-only" role="status">
|
|
455
|
-
{dropLabel()
|
|
576
|
+
{dropLabel() ||
|
|
577
|
+
(resizeController() === 'error'
|
|
578
|
+
? alternateImportAttempts >= 2
|
|
579
|
+
? 'Resize controls did not load after repeated attempts. Reloading may discard unsaved pane content.'
|
|
580
|
+
: 'Resize controls did not load.'
|
|
581
|
+
: '')}
|
|
456
582
|
</span>
|
|
457
583
|
<For each={branches().map((branch) => branch.id)}>
|
|
458
584
|
{(id) => {
|
|
459
585
|
const initial = branchMap().get(id)!
|
|
460
586
|
const branch: Accessor<SplitLayoutBranch<L>> = () => branchMap().get(id) ?? initial
|
|
587
|
+
const rect = () => frames().get(id)?.rect ?? { x: 0, y: 0, width: 0, height: 0 }
|
|
461
588
|
return (
|
|
462
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
589
|
+
<Show
|
|
590
|
+
when={typeof resizeController() === 'function'}
|
|
591
|
+
fallback={
|
|
592
|
+
<div
|
|
593
|
+
role="separator"
|
|
594
|
+
aria-label={
|
|
595
|
+
props.labelForSeparator?.(branch()) ??
|
|
596
|
+
(branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
597
|
+
}
|
|
598
|
+
aria-orientation={branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
599
|
+
aria-controls={listLeaves(branch())
|
|
600
|
+
.map((leaf) => domId(leaf.id))
|
|
601
|
+
.join(' ')}
|
|
602
|
+
aria-valuemin={10}
|
|
603
|
+
aria-valuemax={90}
|
|
604
|
+
aria-valuenow={Math.round(branch().ratio * 100)}
|
|
605
|
+
aria-disabled="true"
|
|
606
|
+
tabIndex={-1}
|
|
607
|
+
data-direction={branch().direction}
|
|
608
|
+
style={separatorPlaceholderStyle(branch(), rect())}
|
|
609
|
+
class="pointer-events-none absolute bg-border"
|
|
610
|
+
/>
|
|
611
|
+
}
|
|
482
612
|
>
|
|
483
|
-
<
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
(branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
492
|
-
}
|
|
493
|
-
aria-orientation={branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
494
|
-
aria-controls={listLeaves(branch())
|
|
495
|
-
.map((leaf) => domId(leaf.id))
|
|
496
|
-
.join(' ')}
|
|
497
|
-
aria-valuemin={10}
|
|
498
|
-
aria-valuemax={90}
|
|
499
|
-
aria-valuenow={Math.round(branch().ratio * 100)}
|
|
500
|
-
class="pointer-events-auto relative w-px shrink-0 bg-border after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 focus-visible:bg-primary focus-visible:outline-none hover:bg-primary data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:-inset-y-1 data-[orientation=vertical]:after:h-3 data-[orientation=vertical]:after:w-full"
|
|
501
|
-
/>
|
|
502
|
-
<Resizable.Panel
|
|
503
|
-
minSize={MIN_SPLIT_RATIO}
|
|
504
|
-
maxSize={MAX_SPLIT_RATIO}
|
|
505
|
-
aria-hidden="true"
|
|
613
|
+
<Dynamic
|
|
614
|
+
component={resizeController() as ResizeController<L>}
|
|
615
|
+
branch={branch}
|
|
616
|
+
rect={() => rectStyle(rect())}
|
|
617
|
+
domId={domId}
|
|
618
|
+
root={() => root}
|
|
619
|
+
labelForSeparator={props.labelForSeparator}
|
|
620
|
+
onResize={props.onResize}
|
|
506
621
|
/>
|
|
507
|
-
</
|
|
622
|
+
</Show>
|
|
508
623
|
)
|
|
509
624
|
}}
|
|
510
625
|
</For>
|
|
626
|
+
{resizeController() === 'error' ? (
|
|
627
|
+
<div class="absolute top-2 right-2 z-(--z-menu) flex max-w-sm flex-col items-end gap-2">
|
|
628
|
+
{alternateImportAttempts >= 2 ? (
|
|
629
|
+
<p
|
|
630
|
+
id={resizeReloadWarningId}
|
|
631
|
+
role="alert"
|
|
632
|
+
class="rounded-md border bg-background p-2 text-sm shadow-md"
|
|
633
|
+
>
|
|
634
|
+
Reloading may discard unsaved pane content.
|
|
635
|
+
</p>
|
|
636
|
+
) : null}
|
|
637
|
+
<Button
|
|
638
|
+
variant="outline"
|
|
639
|
+
size="sm"
|
|
640
|
+
data-resize-retry
|
|
641
|
+
aria-describedby={alternateImportAttempts >= 2 ? resizeReloadWarningId : undefined}
|
|
642
|
+
onClick={(event) => {
|
|
643
|
+
if (alternateImportAttempts >= 2) {
|
|
644
|
+
window.location.reload()
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
const ownsFocus = document.activeElement === event.currentTarget
|
|
648
|
+
focusHandleAfterRetry = ownsFocus
|
|
649
|
+
focusRecoveryButtonAfterError = ownsFocus
|
|
650
|
+
focusRestoreEpoch = focusEpoch
|
|
651
|
+
retryWithAlternateImport = true
|
|
652
|
+
setResizeController(undefined)
|
|
653
|
+
}}
|
|
654
|
+
>
|
|
655
|
+
{alternateImportAttempts >= 2 ? 'Reload page to retry resizing' : 'Retry pane resizing'}
|
|
656
|
+
</Button>
|
|
657
|
+
</div>
|
|
658
|
+
) : null}
|
|
511
659
|
</div>
|
|
512
660
|
)
|
|
513
661
|
}
|