@lobb-js/studio 0.47.0 → 0.48.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/actions.d.ts +2 -28
- package/dist/actions.js +4 -60
- package/dist/components/popup/popup.svelte +23 -4
- package/dist/components/popup/popup.svelte.d.ts +2 -0
- package/dist/extensions/extension.types.d.ts +2 -1
- package/dist/extensions/extensionUtils.js +2 -1
- package/dist/popup.d.ts +31 -0
- package/dist/popup.js +104 -0
- package/package.json +1 -1
- package/src/lib/actions.ts +5 -95
- package/src/lib/components/popup/popup.svelte +23 -4
- package/src/lib/extensions/extension.types.ts +2 -1
- package/src/lib/extensions/extensionUtils.ts +2 -1
- package/src/lib/popup.ts +147 -0
package/dist/actions.d.ts
CHANGED
|
@@ -11,35 +11,9 @@ export interface OpenDataTableDrawerProps {
|
|
|
11
11
|
position?: "side" | "bottom";
|
|
12
12
|
tabs?: CollectionTab[];
|
|
13
13
|
}
|
|
14
|
-
export interface OpenPopupProps {
|
|
15
|
-
component: any;
|
|
16
|
-
componentProps?: Record<string, any>;
|
|
17
|
-
title?: string;
|
|
18
|
-
size?: "default" | "sm";
|
|
19
|
-
hideHeader?: boolean;
|
|
20
|
-
replaceLast?: boolean;
|
|
21
|
-
}
|
|
22
|
-
export interface OpenDataTablePopupProps {
|
|
23
|
-
collectionName: string;
|
|
24
|
-
filter?: Record<string, any>;
|
|
25
|
-
sort?: Record<string, "asc" | "desc">;
|
|
26
|
-
limit?: number;
|
|
27
|
-
title?: string;
|
|
28
|
-
showHeader?: boolean;
|
|
29
|
-
showFooter?: boolean;
|
|
30
|
-
showFilter?: boolean;
|
|
31
|
-
tabs?: CollectionTab[];
|
|
32
|
-
view?: {
|
|
33
|
-
id: string;
|
|
34
|
-
[key: string]: any;
|
|
35
|
-
};
|
|
36
|
-
replaceLast?: boolean;
|
|
37
|
-
size?: "default" | "sm";
|
|
38
|
-
hideHeader?: boolean;
|
|
39
|
-
}
|
|
40
14
|
export declare function showDialog(title: string, description: string): Promise<boolean>;
|
|
41
15
|
export declare function openCreateDetailView(studioContext: StudioContext, props: CreateDetailViewProp): void;
|
|
42
16
|
export declare function openUpdateDetailView(studioContext: StudioContext, props: UpdateDetailViewProp): Promise<void>;
|
|
43
17
|
export declare function openDataTableDrawer(studioContext: StudioContext, props: OpenDataTableDrawerProps): void;
|
|
44
|
-
export
|
|
45
|
-
export
|
|
18
|
+
export { openPopup, openDataTablePopup, goBackPopup } from "./popup";
|
|
19
|
+
export type { OpenPopupProps, OpenDataTablePopupProps } from "./popup";
|
package/dist/actions.js
CHANGED
|
@@ -3,18 +3,8 @@ import ConfirmationDialog from "./components/confirmationDialog/confirmationDial
|
|
|
3
3
|
import CreateDetailView from "./components/detailView/create/createDetailView.svelte";
|
|
4
4
|
import UpdateDetailView from "./components/detailView/update/updateDetailView.svelte";
|
|
5
5
|
import DataTableDrawer from "./components/dataTableDrawer/dataTableDrawer.svelte";
|
|
6
|
-
import DataTable from "./components/dataTable/dataTable.svelte";
|
|
7
|
-
import Popup from "./components/popup/popup.svelte";
|
|
8
6
|
import { createStudioContextMap } from "./context";
|
|
9
7
|
import { getCollectionParamsFields } from "./components/dataTable/utils";
|
|
10
|
-
let activePopup = null;
|
|
11
|
-
function closeActivePopupIfRequested(replaceLast) {
|
|
12
|
-
if (!replaceLast || !activePopup)
|
|
13
|
-
return;
|
|
14
|
-
const toUnmount = activePopup;
|
|
15
|
-
activePopup = null;
|
|
16
|
-
unmount(toUnmount, { outro: true });
|
|
17
|
-
}
|
|
18
8
|
export function showDialog(title, description) {
|
|
19
9
|
return new Promise((resolve) => {
|
|
20
10
|
const targetElement = document.querySelector("main");
|
|
@@ -90,53 +80,7 @@ export function openDataTableDrawer(studioContext, props) {
|
|
|
90
80
|
},
|
|
91
81
|
});
|
|
92
82
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
closeActivePopupIfRequested(props.replaceLast);
|
|
98
|
-
const mounted = mount(Popup, {
|
|
99
|
-
target: targetElement,
|
|
100
|
-
context: createStudioContextMap(studioContext),
|
|
101
|
-
props: {
|
|
102
|
-
component: props.component,
|
|
103
|
-
componentProps: props.componentProps,
|
|
104
|
-
title: props.title,
|
|
105
|
-
size: props.size,
|
|
106
|
-
hideHeader: props.hideHeader,
|
|
107
|
-
onClose: async () => {
|
|
108
|
-
if (activePopup === mounted)
|
|
109
|
-
activePopup = null;
|
|
110
|
-
await unmount(mounted, { outro: true });
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
activePopup = mounted;
|
|
115
|
-
}
|
|
116
|
-
export function openDataTablePopup(studioContext, props) {
|
|
117
|
-
// DataTable reads `searchParams` once during its initial $state setup,
|
|
118
|
-
// so sort/limit need to be folded into the searchParams object before
|
|
119
|
-
// we mount. Built here in plain TS — no Svelte reactivity to escape.
|
|
120
|
-
const searchParams = {};
|
|
121
|
-
if (props.sort)
|
|
122
|
-
searchParams.sort = props.sort;
|
|
123
|
-
if (props.limit != null)
|
|
124
|
-
searchParams.limit = String(props.limit);
|
|
125
|
-
openPopup(studioContext, {
|
|
126
|
-
component: DataTable,
|
|
127
|
-
componentProps: {
|
|
128
|
-
collectionName: props.collectionName,
|
|
129
|
-
filter: props.filter,
|
|
130
|
-
searchParams,
|
|
131
|
-
showHeader: props.showHeader,
|
|
132
|
-
showFooter: props.showFooter,
|
|
133
|
-
showFilter: props.showFilter ?? false,
|
|
134
|
-
tabs: props.tabs,
|
|
135
|
-
view: props.view,
|
|
136
|
-
},
|
|
137
|
-
title: props.title ?? props.collectionName,
|
|
138
|
-
size: props.size,
|
|
139
|
-
hideHeader: props.hideHeader,
|
|
140
|
-
replaceLast: props.replaceLast,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
83
|
+
// Popup helpers (openPopup, openDataTablePopup, goBackPopup) live in
|
|
84
|
+
// `./popup` — they share enough state (active overlay, back-history)
|
|
85
|
+
// to warrant their own module.
|
|
86
|
+
export { openPopup, openDataTablePopup, goBackPopup } from "./popup";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `openPopup` mounts this; specialised popup helpers like
|
|
7
7
|
// `openDataTablePopup` are thin wrappers that supply the body
|
|
8
8
|
// component + props.
|
|
9
|
-
import { X } from "lucide-svelte";
|
|
9
|
+
import { ArrowLeft, X } from "lucide-svelte";
|
|
10
10
|
import { fade, scale } from "svelte/transition";
|
|
11
11
|
import { cubicOut } from "svelte/easing";
|
|
12
12
|
import Portal from "svelte-portal";
|
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
// Use when the body brings its own header.
|
|
29
29
|
hideHeader?: boolean;
|
|
30
30
|
onClose?: () => void;
|
|
31
|
+
// Set by the action layer when there's a popup-history entry to
|
|
32
|
+
// pop. The chrome renders a back arrow in the title bar; bodies
|
|
33
|
+
// that `hideHeader: true` get `canGoBack` + `onBack` forwarded
|
|
34
|
+
// so they can render their own back affordance.
|
|
35
|
+
canGoBack?: boolean;
|
|
36
|
+
onBack?: () => void;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
let {
|
|
@@ -37,6 +43,8 @@
|
|
|
37
43
|
size = "default",
|
|
38
44
|
hideHeader = false,
|
|
39
45
|
onClose,
|
|
46
|
+
canGoBack = false,
|
|
47
|
+
onBack,
|
|
40
48
|
}: Props = $props();
|
|
41
49
|
</script>
|
|
42
50
|
|
|
@@ -54,8 +62,19 @@
|
|
|
54
62
|
{size === 'sm' ? 'max-h-[calc(100vh-6rem)] max-w-2xl' : 'h-[85vh] max-w-7xl'}"
|
|
55
63
|
>
|
|
56
64
|
{#if !hideHeader}
|
|
57
|
-
<div class="flex h-12 shrink-0 items-center justify-between gap-
|
|
58
|
-
<div class="
|
|
65
|
+
<div class="flex h-12 shrink-0 items-center justify-between gap-2 border-b px-4">
|
|
66
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
67
|
+
{#if canGoBack}
|
|
68
|
+
<Button
|
|
69
|
+
variant="ghost"
|
|
70
|
+
size="icon"
|
|
71
|
+
onclick={() => onBack?.()}
|
|
72
|
+
class="h-8 w-8 rounded-full text-muted-foreground"
|
|
73
|
+
Icon={ArrowLeft}
|
|
74
|
+
/>
|
|
75
|
+
{/if}
|
|
76
|
+
<div class="truncate text-sm font-medium">{title ?? ""}</div>
|
|
77
|
+
</div>
|
|
59
78
|
<Button
|
|
60
79
|
variant="ghost"
|
|
61
80
|
size="icon"
|
|
@@ -73,7 +92,7 @@
|
|
|
73
92
|
percentages, which don't resolve when the popup uses
|
|
74
93
|
`max-h` (size="sm") instead of a fixed `h`. -->
|
|
75
94
|
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
76
|
-
<Body {...componentProps} {onClose} />
|
|
95
|
+
<Body {...componentProps} {onClose} {canGoBack} {onBack} />
|
|
77
96
|
</div>
|
|
78
97
|
</div>
|
|
79
98
|
</Portal>
|
|
@@ -23,7 +23,8 @@ import * as Icons from "lucide-svelte";
|
|
|
23
23
|
import { ContextMenu } from "bits-ui";
|
|
24
24
|
import * as Tooltip from "../components/ui/tooltip";
|
|
25
25
|
import * as Breadcrumb from "../components/ui/breadcrumb";
|
|
26
|
-
import { showDialog, type OpenDataTableDrawerProps
|
|
26
|
+
import { showDialog, type OpenDataTableDrawerProps } from "../actions";
|
|
27
|
+
import type { OpenDataTablePopupProps, OpenPopupProps } from "../popup";
|
|
27
28
|
import { toast } from "svelte-sonner";
|
|
28
29
|
import { Switch } from "../components/ui/switch";
|
|
29
30
|
export interface Components {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toast } from "svelte-sonner";
|
|
2
|
-
import { showDialog, openDataTableDrawer
|
|
2
|
+
import { showDialog, openDataTableDrawer } from "../actions";
|
|
3
|
+
import { openDataTablePopup, openPopup } from "../popup";
|
|
3
4
|
import { emitEvent } from "../eventSystem";
|
|
4
5
|
import { Button } from "../components/ui/button";
|
|
5
6
|
import { Input } from "../components/ui/input";
|
package/dist/popup.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type StudioContext } from "./context";
|
|
2
|
+
import type { CollectionTab } from "./store.types";
|
|
3
|
+
export interface OpenPopupProps {
|
|
4
|
+
component: any;
|
|
5
|
+
componentProps?: Record<string, any>;
|
|
6
|
+
title?: string;
|
|
7
|
+
size?: "default" | "sm";
|
|
8
|
+
hideHeader?: boolean;
|
|
9
|
+
replaceLast?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface OpenDataTablePopupProps {
|
|
12
|
+
collectionName: string;
|
|
13
|
+
filter?: Record<string, any>;
|
|
14
|
+
sort?: Record<string, "asc" | "desc">;
|
|
15
|
+
limit?: number;
|
|
16
|
+
title?: string;
|
|
17
|
+
showHeader?: boolean;
|
|
18
|
+
showFooter?: boolean;
|
|
19
|
+
showFilter?: boolean;
|
|
20
|
+
tabs?: CollectionTab[];
|
|
21
|
+
view?: {
|
|
22
|
+
id: string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
};
|
|
25
|
+
replaceLast?: boolean;
|
|
26
|
+
size?: "default" | "sm";
|
|
27
|
+
hideHeader?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function openPopup(studioContext: StudioContext, props: OpenPopupProps): void;
|
|
30
|
+
export declare function goBackPopup(studioContext: StudioContext): void;
|
|
31
|
+
export declare function openDataTablePopup(studioContext: StudioContext, props: OpenDataTablePopupProps): void;
|
package/dist/popup.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte";
|
|
2
|
+
import DataTable from "./components/dataTable/dataTable.svelte";
|
|
3
|
+
import Popup from "./components/popup/popup.svelte";
|
|
4
|
+
import { createStudioContextMap } from "./context";
|
|
5
|
+
let activePopup = null;
|
|
6
|
+
// Props that produced the currently-mounted popup. Tracked so we can
|
|
7
|
+
// push them onto `popupHistory` when this popup is swapped out by a
|
|
8
|
+
// `replaceLast: true` opener, enabling back navigation.
|
|
9
|
+
let activePopupProps = null;
|
|
10
|
+
// Stack of popup-props that were swapped out via `replaceLast: true`.
|
|
11
|
+
// The back arrow re-opens the top of the stack; closing via X or the
|
|
12
|
+
// backdrop wipes the whole stack (a deliberate full-reset, not back).
|
|
13
|
+
const popupHistory = [];
|
|
14
|
+
// Internal mount path used by both `openPopup` (user-initiated) and
|
|
15
|
+
// `goBackPopup` (history-driven). The `fromBack` flag suppresses
|
|
16
|
+
// pushing the outgoing popup onto the history stack — when the user
|
|
17
|
+
// hits Back, the popup they're leaving shouldn't reappear if they hit
|
|
18
|
+
// Back again.
|
|
19
|
+
function mountPopup(studioContext, props, opts = {}) {
|
|
20
|
+
const targetElement = document.querySelector("main");
|
|
21
|
+
if (!targetElement)
|
|
22
|
+
throw new Error("main html element doesn't exist");
|
|
23
|
+
if (props.replaceLast && activePopup) {
|
|
24
|
+
// Swap: archive the outgoing popup's props (unless this open is
|
|
25
|
+
// itself a back-navigation) and unmount it. Outro plays in
|
|
26
|
+
// parallel with the new popup's intro.
|
|
27
|
+
if (!opts.fromBack && activePopupProps) {
|
|
28
|
+
popupHistory.push(activePopupProps);
|
|
29
|
+
}
|
|
30
|
+
const toUnmount = activePopup;
|
|
31
|
+
activePopup = null;
|
|
32
|
+
activePopupProps = null;
|
|
33
|
+
unmount(toUnmount, { outro: true });
|
|
34
|
+
}
|
|
35
|
+
else if (!props.replaceLast) {
|
|
36
|
+
// A fresh, non-stacking popup starts a new context — drop any
|
|
37
|
+
// accumulated back history from the previous chain.
|
|
38
|
+
popupHistory.length = 0;
|
|
39
|
+
}
|
|
40
|
+
const canGoBack = popupHistory.length > 0;
|
|
41
|
+
const mounted = mount(Popup, {
|
|
42
|
+
target: targetElement,
|
|
43
|
+
context: createStudioContextMap(studioContext),
|
|
44
|
+
props: {
|
|
45
|
+
component: props.component,
|
|
46
|
+
componentProps: props.componentProps,
|
|
47
|
+
title: props.title,
|
|
48
|
+
size: props.size,
|
|
49
|
+
hideHeader: props.hideHeader,
|
|
50
|
+
canGoBack,
|
|
51
|
+
onBack: () => goBackPopup(studioContext),
|
|
52
|
+
onClose: async () => {
|
|
53
|
+
if (activePopup === mounted) {
|
|
54
|
+
activePopup = null;
|
|
55
|
+
activePopupProps = null;
|
|
56
|
+
// X / backdrop is a full reset — nuke the back
|
|
57
|
+
// stack so reopening anything starts fresh.
|
|
58
|
+
popupHistory.length = 0;
|
|
59
|
+
}
|
|
60
|
+
await unmount(mounted, { outro: true });
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
activePopup = mounted;
|
|
65
|
+
activePopupProps = props;
|
|
66
|
+
}
|
|
67
|
+
export function openPopup(studioContext, props) {
|
|
68
|
+
mountPopup(studioContext, props);
|
|
69
|
+
}
|
|
70
|
+
export function goBackPopup(studioContext) {
|
|
71
|
+
const prev = popupHistory.pop();
|
|
72
|
+
if (!prev)
|
|
73
|
+
return;
|
|
74
|
+
// Force `replaceLast: true` so the swap+outro path runs, and tell
|
|
75
|
+
// `mountPopup` not to push the current popup onto history.
|
|
76
|
+
mountPopup(studioContext, { ...prev, replaceLast: true }, { fromBack: true });
|
|
77
|
+
}
|
|
78
|
+
export function openDataTablePopup(studioContext, props) {
|
|
79
|
+
// DataTable reads `searchParams` once during its initial $state setup,
|
|
80
|
+
// so sort/limit need to be folded into the searchParams object before
|
|
81
|
+
// we mount. Built here in plain TS — no Svelte reactivity to escape.
|
|
82
|
+
const searchParams = {};
|
|
83
|
+
if (props.sort)
|
|
84
|
+
searchParams.sort = props.sort;
|
|
85
|
+
if (props.limit != null)
|
|
86
|
+
searchParams.limit = String(props.limit);
|
|
87
|
+
openPopup(studioContext, {
|
|
88
|
+
component: DataTable,
|
|
89
|
+
componentProps: {
|
|
90
|
+
collectionName: props.collectionName,
|
|
91
|
+
filter: props.filter,
|
|
92
|
+
searchParams,
|
|
93
|
+
showHeader: props.showHeader,
|
|
94
|
+
showFooter: props.showFooter,
|
|
95
|
+
showFilter: props.showFilter ?? false,
|
|
96
|
+
tabs: props.tabs,
|
|
97
|
+
view: props.view,
|
|
98
|
+
},
|
|
99
|
+
title: props.title ?? props.collectionName,
|
|
100
|
+
size: props.size,
|
|
101
|
+
hideHeader: props.hideHeader,
|
|
102
|
+
replaceLast: props.replaceLast,
|
|
103
|
+
});
|
|
104
|
+
}
|
package/package.json
CHANGED
package/src/lib/actions.ts
CHANGED
|
@@ -3,8 +3,6 @@ import ConfirmationDialog from "./components/confirmationDialog/confirmationDial
|
|
|
3
3
|
import CreateDetailView from "./components/detailView/create/createDetailView.svelte";
|
|
4
4
|
import UpdateDetailView from "./components/detailView/update/updateDetailView.svelte";
|
|
5
5
|
import DataTableDrawer from "./components/dataTableDrawer/dataTableDrawer.svelte";
|
|
6
|
-
import DataTable from "./components/dataTable/dataTable.svelte";
|
|
7
|
-
import Popup from "./components/popup/popup.svelte";
|
|
8
6
|
import type { CreateDetailViewProp } from "./components/detailView/create/createDetailView.svelte";
|
|
9
7
|
import type { UpdateDetailViewProp } from "./components/detailView/update/updateDetailView.svelte";
|
|
10
8
|
import type { StudioContext } from "./context";
|
|
@@ -22,49 +20,6 @@ export interface OpenDataTableDrawerProps {
|
|
|
22
20
|
tabs?: CollectionTab[];
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
export interface OpenPopupProps {
|
|
26
|
-
// Component rendered inside the popup. It receives `componentProps`
|
|
27
|
-
// spread onto it plus `onClose` so it can close the popup itself.
|
|
28
|
-
component: any;
|
|
29
|
-
componentProps?: Record<string, any>;
|
|
30
|
-
title?: string;
|
|
31
|
-
// `default` is the wide data-table popup (max-w-7xl, fixed 85vh).
|
|
32
|
-
// `sm` is the narrow detail-view variant (max-w-2xl, content-height).
|
|
33
|
-
size?: "default" | "sm";
|
|
34
|
-
// When true, the popup renders no chrome at all (no title bar, no
|
|
35
|
-
// close button). The body uses the `onClose` prop the popup passes
|
|
36
|
-
// it to render its own close affordance.
|
|
37
|
-
hideHeader?: boolean;
|
|
38
|
-
// Close the previously-opened popup before mounting this one —
|
|
39
|
-
// drill-down semantics. Only popups participate; drawers don't.
|
|
40
|
-
replaceLast?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface OpenDataTablePopupProps {
|
|
44
|
-
collectionName: string;
|
|
45
|
-
filter?: Record<string, any>;
|
|
46
|
-
sort?: Record<string, "asc" | "desc">;
|
|
47
|
-
limit?: number;
|
|
48
|
-
title?: string;
|
|
49
|
-
showHeader?: boolean;
|
|
50
|
-
showFooter?: boolean;
|
|
51
|
-
showFilter?: boolean;
|
|
52
|
-
tabs?: CollectionTab[];
|
|
53
|
-
view?: { id: string; [key: string]: any };
|
|
54
|
-
replaceLast?: boolean;
|
|
55
|
-
size?: "default" | "sm";
|
|
56
|
-
hideHeader?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let activePopup: any = null;
|
|
60
|
-
|
|
61
|
-
function closeActivePopupIfRequested(replaceLast: boolean | undefined) {
|
|
62
|
-
if (!replaceLast || !activePopup) return;
|
|
63
|
-
const toUnmount = activePopup;
|
|
64
|
-
activePopup = null;
|
|
65
|
-
unmount(toUnmount, { outro: true });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
23
|
export function showDialog(title: string, description: string): Promise<boolean> {
|
|
69
24
|
return new Promise((resolve) => {
|
|
70
25
|
const targetElement = document.querySelector("main");
|
|
@@ -145,53 +100,8 @@ export function openDataTableDrawer(studioContext: StudioContext, props: OpenDat
|
|
|
145
100
|
});
|
|
146
101
|
}
|
|
147
102
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const mounted = mount(Popup, {
|
|
155
|
-
target: targetElement,
|
|
156
|
-
context: createStudioContextMap(studioContext),
|
|
157
|
-
props: {
|
|
158
|
-
component: props.component,
|
|
159
|
-
componentProps: props.componentProps,
|
|
160
|
-
title: props.title,
|
|
161
|
-
size: props.size,
|
|
162
|
-
hideHeader: props.hideHeader,
|
|
163
|
-
onClose: async () => {
|
|
164
|
-
if (activePopup === mounted) activePopup = null;
|
|
165
|
-
await unmount(mounted, { outro: true });
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
activePopup = mounted;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function openDataTablePopup(studioContext: StudioContext, props: OpenDataTablePopupProps) {
|
|
173
|
-
// DataTable reads `searchParams` once during its initial $state setup,
|
|
174
|
-
// so sort/limit need to be folded into the searchParams object before
|
|
175
|
-
// we mount. Built here in plain TS — no Svelte reactivity to escape.
|
|
176
|
-
const searchParams: Record<string, any> = {};
|
|
177
|
-
if (props.sort) searchParams.sort = props.sort;
|
|
178
|
-
if (props.limit != null) searchParams.limit = String(props.limit);
|
|
179
|
-
|
|
180
|
-
openPopup(studioContext, {
|
|
181
|
-
component: DataTable,
|
|
182
|
-
componentProps: {
|
|
183
|
-
collectionName: props.collectionName,
|
|
184
|
-
filter: props.filter,
|
|
185
|
-
searchParams,
|
|
186
|
-
showHeader: props.showHeader,
|
|
187
|
-
showFooter: props.showFooter,
|
|
188
|
-
showFilter: props.showFilter ?? false,
|
|
189
|
-
tabs: props.tabs,
|
|
190
|
-
view: props.view,
|
|
191
|
-
},
|
|
192
|
-
title: props.title ?? props.collectionName,
|
|
193
|
-
size: props.size,
|
|
194
|
-
hideHeader: props.hideHeader,
|
|
195
|
-
replaceLast: props.replaceLast,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
103
|
+
// Popup helpers (openPopup, openDataTablePopup, goBackPopup) live in
|
|
104
|
+
// `./popup` — they share enough state (active overlay, back-history)
|
|
105
|
+
// to warrant their own module.
|
|
106
|
+
export { openPopup, openDataTablePopup, goBackPopup } from "./popup";
|
|
107
|
+
export type { OpenPopupProps, OpenDataTablePopupProps } from "./popup";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `openPopup` mounts this; specialised popup helpers like
|
|
7
7
|
// `openDataTablePopup` are thin wrappers that supply the body
|
|
8
8
|
// component + props.
|
|
9
|
-
import { X } from "lucide-svelte";
|
|
9
|
+
import { ArrowLeft, X } from "lucide-svelte";
|
|
10
10
|
import { fade, scale } from "svelte/transition";
|
|
11
11
|
import { cubicOut } from "svelte/easing";
|
|
12
12
|
import Portal from "svelte-portal";
|
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
// Use when the body brings its own header.
|
|
29
29
|
hideHeader?: boolean;
|
|
30
30
|
onClose?: () => void;
|
|
31
|
+
// Set by the action layer when there's a popup-history entry to
|
|
32
|
+
// pop. The chrome renders a back arrow in the title bar; bodies
|
|
33
|
+
// that `hideHeader: true` get `canGoBack` + `onBack` forwarded
|
|
34
|
+
// so they can render their own back affordance.
|
|
35
|
+
canGoBack?: boolean;
|
|
36
|
+
onBack?: () => void;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
let {
|
|
@@ -37,6 +43,8 @@
|
|
|
37
43
|
size = "default",
|
|
38
44
|
hideHeader = false,
|
|
39
45
|
onClose,
|
|
46
|
+
canGoBack = false,
|
|
47
|
+
onBack,
|
|
40
48
|
}: Props = $props();
|
|
41
49
|
</script>
|
|
42
50
|
|
|
@@ -54,8 +62,19 @@
|
|
|
54
62
|
{size === 'sm' ? 'max-h-[calc(100vh-6rem)] max-w-2xl' : 'h-[85vh] max-w-7xl'}"
|
|
55
63
|
>
|
|
56
64
|
{#if !hideHeader}
|
|
57
|
-
<div class="flex h-12 shrink-0 items-center justify-between gap-
|
|
58
|
-
<div class="
|
|
65
|
+
<div class="flex h-12 shrink-0 items-center justify-between gap-2 border-b px-4">
|
|
66
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
67
|
+
{#if canGoBack}
|
|
68
|
+
<Button
|
|
69
|
+
variant="ghost"
|
|
70
|
+
size="icon"
|
|
71
|
+
onclick={() => onBack?.()}
|
|
72
|
+
class="h-8 w-8 rounded-full text-muted-foreground"
|
|
73
|
+
Icon={ArrowLeft}
|
|
74
|
+
/>
|
|
75
|
+
{/if}
|
|
76
|
+
<div class="truncate text-sm font-medium">{title ?? ""}</div>
|
|
77
|
+
</div>
|
|
59
78
|
<Button
|
|
60
79
|
variant="ghost"
|
|
61
80
|
size="icon"
|
|
@@ -73,7 +92,7 @@
|
|
|
73
92
|
percentages, which don't resolve when the popup uses
|
|
74
93
|
`max-h` (size="sm") instead of a fixed `h`. -->
|
|
75
94
|
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
76
|
-
<Body {...componentProps} {onClose} />
|
|
95
|
+
<Body {...componentProps} {onClose} {canGoBack} {onBack} />
|
|
77
96
|
</div>
|
|
78
97
|
</div>
|
|
79
98
|
</Portal>
|
|
@@ -23,7 +23,8 @@ import * as Icons from "lucide-svelte"
|
|
|
23
23
|
import { ContextMenu } from "bits-ui";
|
|
24
24
|
import * as Tooltip from "../components/ui/tooltip";
|
|
25
25
|
import * as Breadcrumb from "../components/ui/breadcrumb";
|
|
26
|
-
import { showDialog, type OpenDataTableDrawerProps
|
|
26
|
+
import { showDialog, type OpenDataTableDrawerProps } from "../actions";
|
|
27
|
+
import type { OpenDataTablePopupProps, OpenPopupProps } from "../popup";
|
|
27
28
|
import { toast } from "svelte-sonner";
|
|
28
29
|
import type Drawer from "../components/drawer.svelte";
|
|
29
30
|
import { Switch } from "../components/ui/switch";
|
|
@@ -7,7 +7,8 @@ import type {
|
|
|
7
7
|
import type { LobbClient } from "@lobb-js/sdk";
|
|
8
8
|
import type { CTX } from "../store.types";
|
|
9
9
|
import { toast } from "svelte-sonner";
|
|
10
|
-
import { showDialog, openDataTableDrawer
|
|
10
|
+
import { showDialog, openDataTableDrawer } from "../actions";
|
|
11
|
+
import { openDataTablePopup, openPopup } from "../popup";
|
|
11
12
|
import { emitEvent } from "../eventSystem";
|
|
12
13
|
import { Button } from "../components/ui/button";
|
|
13
14
|
import { Input } from "../components/ui/input";
|
package/src/lib/popup.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte";
|
|
2
|
+
import DataTable from "./components/dataTable/dataTable.svelte";
|
|
3
|
+
import Popup from "./components/popup/popup.svelte";
|
|
4
|
+
import { createStudioContextMap, type StudioContext } from "./context";
|
|
5
|
+
import type { CollectionTab } from "./store.types";
|
|
6
|
+
|
|
7
|
+
export interface OpenPopupProps {
|
|
8
|
+
// Component rendered inside the popup. It receives `componentProps`
|
|
9
|
+
// spread onto it plus `onClose` so it can close the popup itself.
|
|
10
|
+
component: any;
|
|
11
|
+
componentProps?: Record<string, any>;
|
|
12
|
+
title?: string;
|
|
13
|
+
// `default` is the wide data-table popup (max-w-7xl, fixed 85vh).
|
|
14
|
+
// `sm` is the narrow detail-view variant (max-w-2xl, content-height).
|
|
15
|
+
size?: "default" | "sm";
|
|
16
|
+
// When true, the popup renders no chrome at all (no title bar, no
|
|
17
|
+
// close button). The body uses the `onClose` prop the popup passes
|
|
18
|
+
// it to render its own close affordance.
|
|
19
|
+
hideHeader?: boolean;
|
|
20
|
+
// Close the previously-opened popup before mounting this one —
|
|
21
|
+
// drill-down semantics. Only popups participate; drawers don't.
|
|
22
|
+
replaceLast?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OpenDataTablePopupProps {
|
|
26
|
+
collectionName: string;
|
|
27
|
+
filter?: Record<string, any>;
|
|
28
|
+
sort?: Record<string, "asc" | "desc">;
|
|
29
|
+
limit?: number;
|
|
30
|
+
title?: string;
|
|
31
|
+
showHeader?: boolean;
|
|
32
|
+
showFooter?: boolean;
|
|
33
|
+
showFilter?: boolean;
|
|
34
|
+
tabs?: CollectionTab[];
|
|
35
|
+
view?: { id: string; [key: string]: any };
|
|
36
|
+
replaceLast?: boolean;
|
|
37
|
+
size?: "default" | "sm";
|
|
38
|
+
hideHeader?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let activePopup: any = null;
|
|
42
|
+
// Props that produced the currently-mounted popup. Tracked so we can
|
|
43
|
+
// push them onto `popupHistory` when this popup is swapped out by a
|
|
44
|
+
// `replaceLast: true` opener, enabling back navigation.
|
|
45
|
+
let activePopupProps: OpenPopupProps | null = null;
|
|
46
|
+
// Stack of popup-props that were swapped out via `replaceLast: true`.
|
|
47
|
+
// The back arrow re-opens the top of the stack; closing via X or the
|
|
48
|
+
// backdrop wipes the whole stack (a deliberate full-reset, not back).
|
|
49
|
+
const popupHistory: OpenPopupProps[] = [];
|
|
50
|
+
|
|
51
|
+
// Internal mount path used by both `openPopup` (user-initiated) and
|
|
52
|
+
// `goBackPopup` (history-driven). The `fromBack` flag suppresses
|
|
53
|
+
// pushing the outgoing popup onto the history stack — when the user
|
|
54
|
+
// hits Back, the popup they're leaving shouldn't reappear if they hit
|
|
55
|
+
// Back again.
|
|
56
|
+
function mountPopup(
|
|
57
|
+
studioContext: StudioContext,
|
|
58
|
+
props: OpenPopupProps,
|
|
59
|
+
opts: { fromBack?: boolean } = {},
|
|
60
|
+
) {
|
|
61
|
+
const targetElement = document.querySelector("main");
|
|
62
|
+
if (!targetElement) throw new Error("main html element doesn't exist");
|
|
63
|
+
|
|
64
|
+
if (props.replaceLast && activePopup) {
|
|
65
|
+
// Swap: archive the outgoing popup's props (unless this open is
|
|
66
|
+
// itself a back-navigation) and unmount it. Outro plays in
|
|
67
|
+
// parallel with the new popup's intro.
|
|
68
|
+
if (!opts.fromBack && activePopupProps) {
|
|
69
|
+
popupHistory.push(activePopupProps);
|
|
70
|
+
}
|
|
71
|
+
const toUnmount = activePopup;
|
|
72
|
+
activePopup = null;
|
|
73
|
+
activePopupProps = null;
|
|
74
|
+
unmount(toUnmount, { outro: true });
|
|
75
|
+
} else if (!props.replaceLast) {
|
|
76
|
+
// A fresh, non-stacking popup starts a new context — drop any
|
|
77
|
+
// accumulated back history from the previous chain.
|
|
78
|
+
popupHistory.length = 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const canGoBack = popupHistory.length > 0;
|
|
82
|
+
|
|
83
|
+
const mounted = mount(Popup, {
|
|
84
|
+
target: targetElement,
|
|
85
|
+
context: createStudioContextMap(studioContext),
|
|
86
|
+
props: {
|
|
87
|
+
component: props.component,
|
|
88
|
+
componentProps: props.componentProps,
|
|
89
|
+
title: props.title,
|
|
90
|
+
size: props.size,
|
|
91
|
+
hideHeader: props.hideHeader,
|
|
92
|
+
canGoBack,
|
|
93
|
+
onBack: () => goBackPopup(studioContext),
|
|
94
|
+
onClose: async () => {
|
|
95
|
+
if (activePopup === mounted) {
|
|
96
|
+
activePopup = null;
|
|
97
|
+
activePopupProps = null;
|
|
98
|
+
// X / backdrop is a full reset — nuke the back
|
|
99
|
+
// stack so reopening anything starts fresh.
|
|
100
|
+
popupHistory.length = 0;
|
|
101
|
+
}
|
|
102
|
+
await unmount(mounted, { outro: true });
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
activePopup = mounted;
|
|
107
|
+
activePopupProps = props;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function openPopup(studioContext: StudioContext, props: OpenPopupProps) {
|
|
111
|
+
mountPopup(studioContext, props);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function goBackPopup(studioContext: StudioContext) {
|
|
115
|
+
const prev = popupHistory.pop();
|
|
116
|
+
if (!prev) return;
|
|
117
|
+
// Force `replaceLast: true` so the swap+outro path runs, and tell
|
|
118
|
+
// `mountPopup` not to push the current popup onto history.
|
|
119
|
+
mountPopup(studioContext, { ...prev, replaceLast: true }, { fromBack: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function openDataTablePopup(studioContext: StudioContext, props: OpenDataTablePopupProps) {
|
|
123
|
+
// DataTable reads `searchParams` once during its initial $state setup,
|
|
124
|
+
// so sort/limit need to be folded into the searchParams object before
|
|
125
|
+
// we mount. Built here in plain TS — no Svelte reactivity to escape.
|
|
126
|
+
const searchParams: Record<string, any> = {};
|
|
127
|
+
if (props.sort) searchParams.sort = props.sort;
|
|
128
|
+
if (props.limit != null) searchParams.limit = String(props.limit);
|
|
129
|
+
|
|
130
|
+
openPopup(studioContext, {
|
|
131
|
+
component: DataTable,
|
|
132
|
+
componentProps: {
|
|
133
|
+
collectionName: props.collectionName,
|
|
134
|
+
filter: props.filter,
|
|
135
|
+
searchParams,
|
|
136
|
+
showHeader: props.showHeader,
|
|
137
|
+
showFooter: props.showFooter,
|
|
138
|
+
showFilter: props.showFilter ?? false,
|
|
139
|
+
tabs: props.tabs,
|
|
140
|
+
view: props.view,
|
|
141
|
+
},
|
|
142
|
+
title: props.title ?? props.collectionName,
|
|
143
|
+
size: props.size,
|
|
144
|
+
hideHeader: props.hideHeader,
|
|
145
|
+
replaceLast: props.replaceLast,
|
|
146
|
+
});
|
|
147
|
+
}
|