@igstack/app-catalog-frontend-core 6.6.0 → 6.7.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/esm/App.d.ts +29 -3
- package/dist/esm/App.js +4 -2
- package/dist/esm/App.js.map +1 -1
- package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/dismiss/DismissStack.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/extensions/slots.test.d.ts +0 -0
- package/dist/esm/__tests__/publicEntryResolves.test.d.ts +1 -0
- package/dist/esm/appPropsFactory.d.ts +19 -1
- package/dist/esm/appPropsFactory.js +4 -3
- package/dist/esm/appPropsFactory.js.map +1 -1
- package/dist/esm/index.d.ts +15 -2
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal.d.ts +7 -1
- package/dist/esm/internal.js +0 -2
- package/dist/esm/internal.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +17 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +7 -16
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +20 -11
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +39 -17
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +0 -14
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js +5 -18
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +12 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +3 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +17 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +212 -79
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +0 -14
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.d.ts +1 -2
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +31 -23
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +5 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +5 -3
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.d.ts +12 -0
- package/dist/esm/modules/auth/AuthContext.js +5 -0
- package/dist/esm/modules/auth/AuthContext.js.map +1 -1
- package/dist/esm/modules/auth/index.d.ts +1 -1
- package/dist/esm/modules/dismiss/DismissStack.d.ts +102 -0
- package/dist/esm/modules/dismiss/DismissStack.js +82 -0
- package/dist/esm/modules/dismiss/DismissStack.js.map +1 -0
- package/dist/esm/modules/extensions/ExtensionsContext.d.ts +17 -0
- package/dist/esm/modules/extensions/ExtensionsContext.js +15 -0
- package/dist/esm/modules/extensions/ExtensionsContext.js.map +1 -0
- package/dist/esm/modules/extensions/factories.d.ts +18 -0
- package/dist/esm/modules/extensions/factories.js +12 -0
- package/dist/esm/modules/extensions/factories.js.map +1 -0
- package/dist/esm/modules/extensions/features/resourceDetail.d.ts +92 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js +14 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -0
- package/dist/esm/modules/extensions/index.d.ts +79 -0
- package/dist/esm/modules/extensions/index.js +26 -0
- package/dist/esm/modules/extensions/index.js.map +1 -0
- package/dist/esm/modules/extensions/makeSlot.d.ts +46 -0
- package/dist/esm/modules/extensions/makeSlot.js +62 -0
- package/dist/esm/modules/extensions/makeSlot.js.map +1 -0
- package/dist/esm/modules/extensions/types.d.ts +46 -0
- package/dist/esm/modules/gallery/Gallery.js +6 -14
- package/dist/esm/modules/gallery/Gallery.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +34 -34
- package/dist/esm/routeTree.gen.js +14 -14
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/router.d.ts +23 -0
- package/dist/esm/router.js +5 -0
- package/dist/esm/router.js.map +1 -0
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
- package/dist/esm/routes/_layout/app.$slug_.sub.$subSlug.d.ts +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.d.ts +2 -2
- package/dist/esm/routes/_layout/index.d.ts +1 -1
- package/dist/esm/routes/_layout/login.d.ts +1 -1
- package/dist/esm/routes/_layout/mcp.d.ts +1 -1
- package/dist/esm/routes/_layout/service-desks.d.ts +1 -1
- package/dist/esm/routes/_layout.d.ts +1 -1
- package/dist/esm/routes/_layout.js +13 -2
- package/dist/esm/routes/_layout.js.map +1 -1
- package/dist/esm/routes/auth.callback.d.ts +1 -1
- package/dist/esm/ui/components/error/DefaultErrorComponent.js +9 -2
- package/dist/esm/ui/components/error/DefaultErrorComponent.js.map +1 -1
- package/dist/esm/ui/components/header/Header.js +0 -1
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/ui/dialog.d.ts +1 -1
- package/dist/esm/ui/dialog.js +9 -0
- package/dist/esm/ui/dialog.js.map +1 -1
- package/dist/esm/ui/table.d.ts +25 -1
- package/dist/esm/ui/table.js +20 -3
- package/dist/esm/ui/table.js.map +1 -1
- package/dist/esm/util/createAcRouter.d.ts +1 -1
- package/package.json +19 -11
- package/src/App.tsx +34 -3
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +4 -1
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.focus.test.tsx +1 -1
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +115 -0
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.tsx +120 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -5
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +9 -9
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +31 -5
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -3
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +48 -6
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +1 -7
- package/src/__tests__/modules/dismiss/DismissStack.test.tsx +330 -0
- package/src/__tests__/modules/extensions/slots.test.tsx +252 -0
- package/src/__tests__/publicEntryResolves.test.ts +63 -0
- package/src/appPropsFactory.ts +23 -3
- package/src/index.tsx +32 -2
- package/src/internal.ts +7 -1
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +21 -0
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +52 -12
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +59 -18
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +12 -20
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +19 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +396 -111
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +46 -26
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +20 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +17 -3
- package/src/modules/auth/AuthContext.tsx +15 -0
- package/src/modules/auth/index.ts +1 -0
- package/src/modules/dismiss/DismissStack.tsx +224 -0
- package/src/modules/extensions/ExtensionsContext.tsx +36 -0
- package/src/modules/extensions/factories.ts +19 -0
- package/src/modules/extensions/features/resourceDetail.ts +74 -0
- package/src/modules/extensions/index.ts +121 -0
- package/src/modules/extensions/makeSlot.tsx +123 -0
- package/src/modules/extensions/types.ts +51 -0
- package/src/modules/gallery/Gallery.tsx +13 -17
- package/src/routeTree.gen.ts +31 -31
- package/src/router.ts +23 -0
- package/src/routes/_layout.tsx +51 -1
- package/src/ui/components/error/DefaultErrorComponent.tsx +22 -4
- package/src/ui/dialog.tsx +26 -0
- package/src/ui/table.tsx +46 -3
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Escape key, one answer: dismiss the layer on top.
|
|
3
|
+
*
|
|
4
|
+
* Before this existed, Escape was handled in six independent places, and each
|
|
5
|
+
* one had to know about the others to avoid double-firing — the detail card
|
|
6
|
+
* bound Escape only `enabled: !isGalleryOpen`, the gallery installed its own
|
|
7
|
+
* capture-phase listener specifically to beat Radix to the event, and the
|
|
8
|
+
* screenshot dialog cancelled Radix's own Escape by reading the gallery's
|
|
9
|
+
* fullscreen state through a ref. Every new overlay had to learn that dance,
|
|
10
|
+
* and getting it wrong stayed invisible until someone pressed Escape in the
|
|
11
|
+
* wrong order. It was wrong: a dialog opened over the card had no say, so
|
|
12
|
+
* Escape closed the card out from under it and navigated away.
|
|
13
|
+
*
|
|
14
|
+
* A layer now says only "I am dismissable, and this element is me". The stack
|
|
15
|
+
* decides who Escape belongs to, and no layer needs to know another exists.
|
|
16
|
+
*
|
|
17
|
+
* "On top" is DOCUMENT ORDER, the last enabled layer in the document. Two
|
|
18
|
+
* cheaper keys were tried first and both broke:
|
|
19
|
+
*
|
|
20
|
+
* - Registration order. React runs a child's effects before its parent's, so
|
|
21
|
+
* an overlay nested inside another registers FIRST and the container looks
|
|
22
|
+
* newer: Escape in the screenshot viewer closed the card under it.
|
|
23
|
+
* - Activation time. On a cold deep link the dialog's route resolves before
|
|
24
|
+
* the catalog arrives, so the card it sits over mounts LATER and looks
|
|
25
|
+
* newer: Escape closed the card first, and the card took focus back off the
|
|
26
|
+
* dialog's first field.
|
|
27
|
+
*
|
|
28
|
+
* Document order has neither problem because it is what "on top" means on
|
|
29
|
+
* screen: a portal appends to the end of `<body>`, a child route renders after
|
|
30
|
+
* its parent's page, and a nested layer is a descendant — and a descendant
|
|
31
|
+
* follows its ancestor in document order. None of it depends on when anything
|
|
32
|
+
* mounted.
|
|
33
|
+
*
|
|
34
|
+
* `useDismissable` works with or without a provider above it, on purpose. A
|
|
35
|
+
* hook that silently did nothing outside a provider would be the same class of
|
|
36
|
+
* bug this module exists to delete — every render site, including tests and
|
|
37
|
+
* stories, would have to remember a wrapper, and forgetting it would look like
|
|
38
|
+
* working code with dead Escape handling. So an app-wide stack is the default
|
|
39
|
+
* and `DismissStackProvider` is only for scoping.
|
|
40
|
+
*/
|
|
41
|
+
interface DismissEntry {
|
|
42
|
+
/** The layer's root element; its position in the document is its rank. */
|
|
43
|
+
layer: {
|
|
44
|
+
readonly current: HTMLElement | null;
|
|
45
|
+
};
|
|
46
|
+
/** Called when Escape belongs to this layer. */
|
|
47
|
+
onDismiss: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* A layer can stay registered and opt out — a collapsed panel, a closed
|
|
50
|
+
* popover. Skipped when choosing the top rather than unregistering.
|
|
51
|
+
*/
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface DismissStackApi {
|
|
55
|
+
/** Adds an entry and returns its unregister function. */
|
|
56
|
+
register: (entry: DismissEntry) => () => void;
|
|
57
|
+
/** Dismisses the top layer; reports whether there was one. */
|
|
58
|
+
dismissTop: () => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The top layer's element, or null when nothing is open. For a layer that
|
|
61
|
+
* takes focus on mount: it should only do so while it is on top, or it
|
|
62
|
+
* steals the caret from whatever was opened over it.
|
|
63
|
+
*/
|
|
64
|
+
topElement: () => HTMLElement | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Scopes a stack to a subtree.
|
|
68
|
+
*
|
|
69
|
+
* Only needed when overlays inside must not compete with overlays outside —
|
|
70
|
+
* two independently mounted apps on one page, for instance. The app does not
|
|
71
|
+
* need it for ordinary nesting; the default stack already handles that.
|
|
72
|
+
*/
|
|
73
|
+
export declare function DismissStackProvider({ children, }: {
|
|
74
|
+
children: React.ReactNode;
|
|
75
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
76
|
+
/**
|
|
77
|
+
* Declare this layer dismissable by Escape.
|
|
78
|
+
*
|
|
79
|
+
* ```tsx
|
|
80
|
+
* const ref = useRef<HTMLDivElement>(null)
|
|
81
|
+
* useDismissable(ref, onClose) // always dismissable
|
|
82
|
+
* useDismissable(ref, close, { enabled: isOpen }) // only while open
|
|
83
|
+
* return <div ref={ref} role="dialog">…</div>
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* `ref` is the layer's root element, and it is required rather than optional
|
|
87
|
+
* because it IS the ordering: the last enabled layer in the document owns
|
|
88
|
+
* Escape. Call the hook once per layer, unconditionally; nothing else
|
|
89
|
+
* coordinates with any other layer, and adding one above an existing layer
|
|
90
|
+
* needs no change to it.
|
|
91
|
+
*/
|
|
92
|
+
export declare function useDismissable(layer: {
|
|
93
|
+
readonly current: HTMLElement | null;
|
|
94
|
+
}, onDismiss: () => void, { enabled }?: {
|
|
95
|
+
enabled?: boolean;
|
|
96
|
+
}): void;
|
|
97
|
+
/**
|
|
98
|
+
* The stack itself, for a component that owns its own key handling or focus
|
|
99
|
+
* and wants stack ordering instead of inventing its own.
|
|
100
|
+
*/
|
|
101
|
+
export declare function useDismissStack(): DismissStackApi;
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import "react/jsx-runtime";
|
|
2
|
+
import { use, createContext, useRef, useEffect } from "react";
|
|
3
|
+
function isAfter(a, b) {
|
|
4
|
+
return (b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
|
|
5
|
+
}
|
|
6
|
+
function createStack() {
|
|
7
|
+
const entries = [];
|
|
8
|
+
const top = () => {
|
|
9
|
+
let best;
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const el = entry.layer.current;
|
|
12
|
+
if (!entry.enabled || !(el == null ? void 0 : el.isConnected)) continue;
|
|
13
|
+
const bestEl = best == null ? void 0 : best.layer.current;
|
|
14
|
+
if (!bestEl || isAfter(el, bestEl)) best = entry;
|
|
15
|
+
}
|
|
16
|
+
return best;
|
|
17
|
+
};
|
|
18
|
+
const dismissTop = () => {
|
|
19
|
+
const entry = top();
|
|
20
|
+
if (!entry) return false;
|
|
21
|
+
entry.onDismiss();
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
const topElement = () => {
|
|
25
|
+
var _a;
|
|
26
|
+
return ((_a = top()) == null ? void 0 : _a.layer.current) ?? null;
|
|
27
|
+
};
|
|
28
|
+
const onKeyDown = (event) => {
|
|
29
|
+
if (event.key !== "Escape") return;
|
|
30
|
+
if (event.defaultPrevented) return;
|
|
31
|
+
if (!dismissTop()) return;
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
};
|
|
35
|
+
const register = (entry) => {
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
document.addEventListener("keydown", onKeyDown);
|
|
38
|
+
}
|
|
39
|
+
entries.push(entry);
|
|
40
|
+
return () => {
|
|
41
|
+
const i = entries.indexOf(entry);
|
|
42
|
+
if (i !== -1) entries.splice(i, 1);
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (document.activeElement && document.activeElement !== document.body)
|
|
50
|
+
return;
|
|
51
|
+
(_a = topElement()) == null ? void 0 : _a.focus();
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
return { register, dismissTop, topElement };
|
|
56
|
+
}
|
|
57
|
+
let appStack = null;
|
|
58
|
+
function getAppStack() {
|
|
59
|
+
appStack ?? (appStack = createStack());
|
|
60
|
+
return appStack;
|
|
61
|
+
}
|
|
62
|
+
const DismissStackContext = createContext(null);
|
|
63
|
+
function useDismissable(layer, onDismiss, { enabled = true } = {}) {
|
|
64
|
+
const scoped = use(DismissStackContext);
|
|
65
|
+
const stack = scoped ?? getAppStack();
|
|
66
|
+
const entryRef = useRef({ layer, onDismiss, enabled });
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
entryRef.current.layer = layer;
|
|
69
|
+
entryRef.current.onDismiss = onDismiss;
|
|
70
|
+
entryRef.current.enabled = enabled;
|
|
71
|
+
}, [layer, onDismiss, enabled]);
|
|
72
|
+
useEffect(() => stack.register(entryRef.current), [stack]);
|
|
73
|
+
}
|
|
74
|
+
function useDismissStack() {
|
|
75
|
+
const scoped = use(DismissStackContext);
|
|
76
|
+
return scoped ?? getAppStack();
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
useDismissStack,
|
|
80
|
+
useDismissable
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=DismissStack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DismissStack.js","sources":["../../../../src/modules/dismiss/DismissStack.tsx"],"sourcesContent":["import { createContext, use, useEffect, useMemo, useRef } from 'react'\n\n/**\n * One Escape key, one answer: dismiss the layer on top.\n *\n * Before this existed, Escape was handled in six independent places, and each\n * one had to know about the others to avoid double-firing — the detail card\n * bound Escape only `enabled: !isGalleryOpen`, the gallery installed its own\n * capture-phase listener specifically to beat Radix to the event, and the\n * screenshot dialog cancelled Radix's own Escape by reading the gallery's\n * fullscreen state through a ref. Every new overlay had to learn that dance,\n * and getting it wrong stayed invisible until someone pressed Escape in the\n * wrong order. It was wrong: a dialog opened over the card had no say, so\n * Escape closed the card out from under it and navigated away.\n *\n * A layer now says only \"I am dismissable, and this element is me\". The stack\n * decides who Escape belongs to, and no layer needs to know another exists.\n *\n * \"On top\" is DOCUMENT ORDER, the last enabled layer in the document. Two\n * cheaper keys were tried first and both broke:\n *\n * - Registration order. React runs a child's effects before its parent's, so\n * an overlay nested inside another registers FIRST and the container looks\n * newer: Escape in the screenshot viewer closed the card under it.\n * - Activation time. On a cold deep link the dialog's route resolves before\n * the catalog arrives, so the card it sits over mounts LATER and looks\n * newer: Escape closed the card first, and the card took focus back off the\n * dialog's first field.\n *\n * Document order has neither problem because it is what \"on top\" means on\n * screen: a portal appends to the end of `<body>`, a child route renders after\n * its parent's page, and a nested layer is a descendant — and a descendant\n * follows its ancestor in document order. None of it depends on when anything\n * mounted.\n *\n * `useDismissable` works with or without a provider above it, on purpose. A\n * hook that silently did nothing outside a provider would be the same class of\n * bug this module exists to delete — every render site, including tests and\n * stories, would have to remember a wrapper, and forgetting it would look like\n * working code with dead Escape handling. So an app-wide stack is the default\n * and `DismissStackProvider` is only for scoping.\n */\n\ninterface DismissEntry {\n /** The layer's root element; its position in the document is its rank. */\n layer: { readonly current: HTMLElement | null }\n /** Called when Escape belongs to this layer. */\n onDismiss: () => void\n /**\n * A layer can stay registered and opt out — a collapsed panel, a closed\n * popover. Skipped when choosing the top rather than unregistering.\n */\n enabled: boolean\n}\n\nexport interface DismissStackApi {\n /** Adds an entry and returns its unregister function. */\n register: (entry: DismissEntry) => () => void\n /** Dismisses the top layer; reports whether there was one. */\n dismissTop: () => boolean\n /**\n * The top layer's element, or null when nothing is open. For a layer that\n * takes focus on mount: it should only do so while it is on top, or it\n * steals the caret from whatever was opened over it.\n */\n topElement: () => HTMLElement | null\n}\n\nfunction isAfter(a: HTMLElement, b: HTMLElement): boolean {\n // A descendant reports FOLLOWING as well as CONTAINED_BY, so this one test\n // covers both \"rendered later\" and \"nested inside\".\n return (b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0\n}\n\nfunction createStack(): DismissStackApi {\n const entries: DismissEntry[] = []\n\n const top = (): DismissEntry | undefined => {\n let best: DismissEntry | undefined\n for (const entry of entries) {\n const el = entry.layer.current\n // Not in the document yet, or already gone: not something on screen.\n if (!entry.enabled || !el?.isConnected) continue\n const bestEl = best?.layer.current\n if (!bestEl || isAfter(el, bestEl)) best = entry\n }\n return best\n }\n\n const dismissTop = () => {\n const entry = top()\n if (!entry) return false\n entry.onDismiss()\n return true\n }\n\n const topElement = () => top()?.layer.current ?? null\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return\n // Something closer to the event already dealt with it — a text field\n // cancelling an edit, say. Honoured rather than overridden: the element the\n // user is typing in is more specific than any layer containing it.\n //\n // This check is the reason the listener below is on the BUBBLE phase. In\n // capture it would run before every component handler, so `defaultPrevented`\n // would always be false and a field could never keep Escape for itself.\n if (event.defaultPrevented) return\n if (!dismissTop()) return\n // Claim it, so a handler further out does not also act.\n event.preventDefault()\n event.stopPropagation()\n }\n\n const register = (entry: DismissEntry) => {\n if (entries.length === 0) {\n // Bubble, on the document: every component handler and React's own\n // delegated handlers run first, so the innermost thing that wants Escape\n // gets it. Radix is the one exception — it listens with `capture: true`,\n // so it runs BEFORE this. `DialogContent` bridges that by asking the\n // stack from inside Radix's own escape callback.\n document.addEventListener('keydown', onKeyDown)\n }\n entries.push(entry)\n return () => {\n const i = entries.indexOf(entry)\n if (i !== -1) entries.splice(i, 1)\n if (entries.length === 0) {\n document.removeEventListener('keydown', onKeyDown)\n return\n }\n // When a layer closes, the caret it held goes with it and lands on\n // <body>, where the next Tab walks whatever is behind every overlay and\n // Escape still works only because this listener is on the document.\n // Hand it to the layer that is now on top. Only from <body>: if\n // something already restored focus — Radix returns it to the trigger —\n // that choice is more specific than this one and stands.\n queueMicrotask(() => {\n if (document.activeElement && document.activeElement !== document.body)\n return\n topElement()?.focus()\n })\n }\n }\n\n return { register, dismissTop, topElement }\n}\n\n/**\n * The app-wide stack, used whenever no provider scopes one.\n *\n * Module-level so two sibling overlays coordinate even if neither sits under a\n * provider — which is the whole point. Created lazily so importing this module\n * never touches `document`, keeping it safe in a non-DOM environment.\n */\nlet appStack: DismissStackApi | null = null\nfunction getAppStack(): DismissStackApi {\n appStack ??= createStack()\n return appStack\n}\n\nconst DismissStackContext = createContext<DismissStackApi | null>(null)\n\n/**\n * Scopes a stack to a subtree.\n *\n * Only needed when overlays inside must not compete with overlays outside —\n * two independently mounted apps on one page, for instance. The app does not\n * need it for ordinary nesting; the default stack already handles that.\n */\nexport function DismissStackProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n const api = useMemo(() => createStack(), [])\n return <DismissStackContext value={api}>{children}</DismissStackContext>\n}\n\n/**\n * Declare this layer dismissable by Escape.\n *\n * ```tsx\n * const ref = useRef<HTMLDivElement>(null)\n * useDismissable(ref, onClose) // always dismissable\n * useDismissable(ref, close, { enabled: isOpen }) // only while open\n * return <div ref={ref} role=\"dialog\">…</div>\n * ```\n *\n * `ref` is the layer's root element, and it is required rather than optional\n * because it IS the ordering: the last enabled layer in the document owns\n * Escape. Call the hook once per layer, unconditionally; nothing else\n * coordinates with any other layer, and adding one above an existing layer\n * needs no change to it.\n */\nexport function useDismissable(\n layer: { readonly current: HTMLElement | null },\n onDismiss: () => void,\n { enabled = true }: { enabled?: boolean } = {},\n): void {\n const scoped = use(DismissStackContext)\n const stack = scoped ?? getAppStack()\n\n // A stable entry whose fields are refreshed in place. Handing `register` a\n // new object whenever the callback identity changed would churn the stack on\n // every render for no benefit.\n const entryRef = useRef<DismissEntry>({ layer, onDismiss, enabled })\n useEffect(() => {\n entryRef.current.layer = layer\n entryRef.current.onDismiss = onDismiss\n entryRef.current.enabled = enabled\n }, [layer, onDismiss, enabled])\n\n useEffect(() => stack.register(entryRef.current), [stack])\n}\n\n/**\n * The stack itself, for a component that owns its own key handling or focus\n * and wants stack ordering instead of inventing its own.\n */\nexport function useDismissStack(): DismissStackApi {\n const scoped = use(DismissStackContext)\n return scoped ?? getAppStack()\n}\n"],"names":[],"mappings":";;AAoEA,SAAS,QAAQ,GAAgB,GAAyB;AAGxD,UAAQ,EAAE,wBAAwB,CAAC,IAAI,KAAK,iCAAiC;AAC/E;AAEA,SAAS,cAA+B;AACtC,QAAM,UAA0B,CAAA;AAEhC,QAAM,MAAM,MAAgC;AAC1C,QAAI;AACJ,eAAW,SAAS,SAAS;AAC3B,YAAM,KAAK,MAAM,MAAM;AAEvB,UAAI,CAAC,MAAM,WAAW,EAAC,yBAAI,aAAa;AACxC,YAAM,SAAS,6BAAM,MAAM;AAC3B,UAAI,CAAC,UAAU,QAAQ,IAAI,MAAM,EAAG,QAAO;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,MAAM;AACvB,UAAM,QAAQ,IAAA;AACd,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,UAAA;AACN,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,MAAA;;AAAM,sBAAA,MAAA,mBAAO,MAAM,YAAW;AAAA;AAEjD,QAAM,YAAY,CAAC,UAAyB;AAC1C,QAAI,MAAM,QAAQ,SAAU;AAQ5B,QAAI,MAAM,iBAAkB;AAC5B,QAAI,CAAC,aAAc;AAEnB,UAAM,eAAA;AACN,UAAM,gBAAA;AAAA,EACR;AAEA,QAAM,WAAW,CAAC,UAAwB;AACxC,QAAI,QAAQ,WAAW,GAAG;AAMxB,eAAS,iBAAiB,WAAW,SAAS;AAAA,IAChD;AACA,YAAQ,KAAK,KAAK;AAClB,WAAO,MAAM;AACX,YAAM,IAAI,QAAQ,QAAQ,KAAK;AAC/B,UAAI,MAAM,GAAI,SAAQ,OAAO,GAAG,CAAC;AACjC,UAAI,QAAQ,WAAW,GAAG;AACxB,iBAAS,oBAAoB,WAAW,SAAS;AACjD;AAAA,MACF;AAOA,qBAAe,MAAM;;AACnB,YAAI,SAAS,iBAAiB,SAAS,kBAAkB,SAAS;AAChE;AACF,yBAAA,MAAA,mBAAc;AAAA,MAChB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,YAAY,WAAA;AACjC;AASA,IAAI,WAAmC;AACvC,SAAS,cAA+B;AACtC,0BAAa,YAAA;AACb,SAAO;AACT;AAEA,MAAM,sBAAsB,cAAsC,IAAI;AAkC/D,SAAS,eACd,OACA,WACA,EAAE,UAAU,KAAA,IAAgC,IACtC;AACN,QAAM,SAAS,IAAI,mBAAmB;AACtC,QAAM,QAAQ,UAAU,YAAA;AAKxB,QAAM,WAAW,OAAqB,EAAE,OAAO,WAAW,SAAS;AACnE,YAAU,MAAM;AACd,aAAS,QAAQ,QAAQ;AACzB,aAAS,QAAQ,YAAY;AAC7B,aAAS,QAAQ,UAAU;AAAA,EAC7B,GAAG,CAAC,OAAO,WAAW,OAAO,CAAC;AAE9B,YAAU,MAAM,MAAM,SAAS,SAAS,OAAO,GAAG,CAAC,KAAK,CAAC;AAC3D;AAMO,SAAS,kBAAmC;AACjD,QAAM,SAAS,IAAI,mBAAmB;AACtC,SAAO,UAAU,YAAA;AACnB;"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { PluginRegistration } from './types.js';
|
|
3
|
+
interface ExtensionsContextProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
value?: readonly PluginRegistration[];
|
|
6
|
+
}
|
|
7
|
+
export declare function ExtensionsContext({ children, value }: ExtensionsContextProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
/**
|
|
9
|
+
* Registered plugins, or an empty list.
|
|
10
|
+
*
|
|
11
|
+
* Empty is the open-source default and has to stay a first-class state: every
|
|
12
|
+
* slot renders nothing and every wrapper passes its children through, so the UI
|
|
13
|
+
* is identical to one with no slots at all. Slots are additive — a slot must
|
|
14
|
+
* never be the only route to something the core itself needs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useExtensions(): readonly PluginRegistration[];
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, use } from "react";
|
|
3
|
+
const ExtensionsInternalContext = createContext(void 0);
|
|
4
|
+
function ExtensionsContext({ children, value }) {
|
|
5
|
+
return /* @__PURE__ */ jsx(ExtensionsInternalContext, { value, children });
|
|
6
|
+
}
|
|
7
|
+
function useExtensions() {
|
|
8
|
+
return use(ExtensionsInternalContext) ?? EMPTY;
|
|
9
|
+
}
|
|
10
|
+
const EMPTY = [];
|
|
11
|
+
export {
|
|
12
|
+
ExtensionsContext,
|
|
13
|
+
useExtensions
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=ExtensionsContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExtensionsContext.js","sources":["../../../../src/modules/extensions/ExtensionsContext.tsx"],"sourcesContent":["import { createContext, use } from 'react'\nimport type { ReactNode } from 'react'\nimport type { PluginRegistration } from './types'\n\nconst ExtensionsInternalContext = createContext<\n readonly PluginRegistration[] | undefined\n>(undefined)\n\ninterface ExtensionsContextProps {\n children: ReactNode\n value?: readonly PluginRegistration[]\n}\n\nexport function ExtensionsContext({ children, value }: ExtensionsContextProps) {\n return (\n <ExtensionsInternalContext value={value}>\n {children}\n </ExtensionsInternalContext>\n )\n}\n\n/**\n * Registered plugins, or an empty list.\n *\n * Empty is the open-source default and has to stay a first-class state: every\n * slot renders nothing and every wrapper passes its children through, so the UI\n * is identical to one with no slots at all. Slots are additive — a slot must\n * never be the only route to something the core itself needs.\n */\nexport function useExtensions(): readonly PluginRegistration[] {\n return use(ExtensionsInternalContext) ?? EMPTY\n}\n\n// Module-level constant: a fresh `[]` per call would be a new identity every\n// render and defeat memoisation in anything downstream of it.\nconst EMPTY: readonly PluginRegistration[] = []\n"],"names":[],"mappings":";;AAIA,MAAM,4BAA4B,cAEhC,MAAS;AAOJ,SAAS,kBAAkB,EAAE,UAAU,SAAiC;AAC7E,SACE,oBAAC,2BAAA,EAA0B,OACxB,SAAA,CACH;AAEJ;AAUO,SAAS,gBAA+C;AAC7D,SAAO,IAAI,yBAAyB,KAAK;AAC3C;AAIA,MAAM,QAAuC,CAAA;"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binds one feature's slot spec, so each slot names itself exactly once and the
|
|
3
|
+
* name is checked against that feature's own spec.
|
|
4
|
+
*
|
|
5
|
+
* const mkSlot = slotFactory<AppCatalogSlots>()
|
|
6
|
+
* export const ResourceDetailAccessActions = mkSlot('resourceDetailAccessActions')
|
|
7
|
+
* // ^ typo here is a compile error
|
|
8
|
+
*/
|
|
9
|
+
export declare function slotFactory<TSpec>(): <TName extends keyof TSpec & string>(name: TName) => {
|
|
10
|
+
(props: TSpec[TName]): import('react').ReactNode;
|
|
11
|
+
displayName: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function wrapperFactory<TSpec>(): <TName extends keyof TSpec & string>(name: TName) => {
|
|
14
|
+
(props: TSpec[TName] & {
|
|
15
|
+
children: import('react').ReactNode;
|
|
16
|
+
}): import('react').ReactNode;
|
|
17
|
+
displayName: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { makeWrapper, makeSlot } from "./makeSlot.js";
|
|
2
|
+
function slotFactory() {
|
|
3
|
+
return (name) => makeSlot(name);
|
|
4
|
+
}
|
|
5
|
+
function wrapperFactory() {
|
|
6
|
+
return (name) => makeWrapper(name);
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
slotFactory,
|
|
10
|
+
wrapperFactory
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=factories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factories.js","sources":["../../../../src/modules/extensions/factories.ts"],"sourcesContent":["import { makeSlot, makeWrapper } from './makeSlot'\n\n/**\n * Binds one feature's slot spec, so each slot names itself exactly once and the\n * name is checked against that feature's own spec.\n *\n * const mkSlot = slotFactory<AppCatalogSlots>()\n * export const ResourceDetailAccessActions = mkSlot('resourceDetailAccessActions')\n * // ^ typo here is a compile error\n */\nexport function slotFactory<TSpec>() {\n return <TName extends keyof TSpec & string>(name: TName) =>\n makeSlot<TSpec[TName]>(name)\n}\n\nexport function wrapperFactory<TSpec>() {\n return <TName extends keyof TSpec & string>(name: TName) =>\n makeWrapper<TSpec[TName]>(name)\n}\n"],"names":[],"mappings":";AAUO,SAAS,cAAqB;AACnC,SAAO,CAAqC,SAC1C,SAAuB,IAAI;AAC/B;AAEO,SAAS,iBAAwB;AACtC,SAAO,CAAqC,SAC1C,YAA0B,IAAI;AAClC;"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
|
+
import { PluginUser } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Leaf slots on the resource detail card.
|
|
5
|
+
*
|
|
6
|
+
* Payloads are additive-only. Adding a field is invisible to existing plugins;
|
|
7
|
+
* renaming or removing one breaks them at compile time on their next core bump,
|
|
8
|
+
* which is the intended moment for that to surface.
|
|
9
|
+
*/
|
|
10
|
+
export interface ResourceDetailSlots {
|
|
11
|
+
/**
|
|
12
|
+
* Actions on the detail card's header band.
|
|
13
|
+
*
|
|
14
|
+
* The band and not a tab panel: tab panels mount only while active and
|
|
15
|
+
* Overview is first, so a slot inside a tab is invisible until a second click.
|
|
16
|
+
*/
|
|
17
|
+
resourceDetailAccessActions: {
|
|
18
|
+
/** The resource on screen — a top-level app OR one of its sub-resources. */
|
|
19
|
+
resource: Resource;
|
|
20
|
+
/** Present only on a sub-resource view. Its absence is how a plugin tells the two apart. */
|
|
21
|
+
parent?: Resource;
|
|
22
|
+
/** Children of `parent ?? resource`: the family's sub-resources. */
|
|
23
|
+
subResources: Resource[];
|
|
24
|
+
user: PluginUser | null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* One cell per row of the sub-resources table, in a column of its own.
|
|
28
|
+
*
|
|
29
|
+
* Called once per rendered row, which is the opposite shape to
|
|
30
|
+
* `resourceDetailAccessActions` — that one is called once and handed the whole
|
|
31
|
+
* list. Use this when the action belongs to a single child.
|
|
32
|
+
*
|
|
33
|
+
* Two things to know before filling it. The table renders EVERY row with no
|
|
34
|
+
* virtualisation, so a contribution here is mounted as many times as the
|
|
35
|
+
* family has children (hundreds, in a large catalog) — keep the cell cheap and
|
|
36
|
+
* defer anything expensive until it is interacted with. And the column only
|
|
37
|
+
* appears when a plugin actually fills it, so a build with no plugin renders
|
|
38
|
+
* the table exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
resourceSubResourceRowActions: {
|
|
41
|
+
/** The child this row is showing. */
|
|
42
|
+
resource: Resource;
|
|
43
|
+
/** The entry the row hangs off. Always present here, unlike the header slot. */
|
|
44
|
+
parent: Resource;
|
|
45
|
+
user: PluginUser | null;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Wrappers on the resource detail card.
|
|
50
|
+
*
|
|
51
|
+
* `resourceDetailProvider` is mounted inside the panel's `key={shownSlug}`
|
|
52
|
+
* subtree, so anything a plugin provides here is scoped to the resource on
|
|
53
|
+
* screen and is discarded when the card navigates. That is what lets two slots
|
|
54
|
+
* share per-resource state without the plugin writing any cleanup.
|
|
55
|
+
*/
|
|
56
|
+
export interface ResourceDetailWrappers {
|
|
57
|
+
resourceDetailProvider: {
|
|
58
|
+
resource: Resource;
|
|
59
|
+
parent?: Resource;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export declare const ResourceDetailAccessActions: {
|
|
63
|
+
(props: {
|
|
64
|
+
/** The resource on screen — a top-level app OR one of its sub-resources. */
|
|
65
|
+
resource: Resource;
|
|
66
|
+
/** Present only on a sub-resource view. Its absence is how a plugin tells the two apart. */
|
|
67
|
+
parent?: Resource;
|
|
68
|
+
/** Children of `parent ?? resource`: the family's sub-resources. */
|
|
69
|
+
subResources: Resource[];
|
|
70
|
+
user: PluginUser | null;
|
|
71
|
+
}): import('react').ReactNode;
|
|
72
|
+
displayName: string;
|
|
73
|
+
};
|
|
74
|
+
export declare const ResourceSubResourceRowActions: {
|
|
75
|
+
(props: {
|
|
76
|
+
/** The child this row is showing. */
|
|
77
|
+
resource: Resource;
|
|
78
|
+
/** The entry the row hangs off. Always present here, unlike the header slot. */
|
|
79
|
+
parent: Resource;
|
|
80
|
+
user: PluginUser | null;
|
|
81
|
+
}): import('react').ReactNode;
|
|
82
|
+
displayName: string;
|
|
83
|
+
};
|
|
84
|
+
export declare const ResourceDetailProvider: {
|
|
85
|
+
(props: {
|
|
86
|
+
resource: Resource;
|
|
87
|
+
parent?: Resource;
|
|
88
|
+
} & {
|
|
89
|
+
children: import('react').ReactNode;
|
|
90
|
+
}): import('react').ReactNode;
|
|
91
|
+
displayName: string;
|
|
92
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { wrapperFactory, slotFactory } from "../factories.js";
|
|
2
|
+
const mkSlot = slotFactory();
|
|
3
|
+
const mkWrapper = wrapperFactory();
|
|
4
|
+
const ResourceDetailAccessActions = mkSlot("resourceDetailAccessActions");
|
|
5
|
+
const ResourceSubResourceRowActions = mkSlot(
|
|
6
|
+
"resourceSubResourceRowActions"
|
|
7
|
+
);
|
|
8
|
+
const ResourceDetailProvider = mkWrapper("resourceDetailProvider");
|
|
9
|
+
export {
|
|
10
|
+
ResourceDetailAccessActions,
|
|
11
|
+
ResourceDetailProvider,
|
|
12
|
+
ResourceSubResourceRowActions
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=resourceDetail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resourceDetail.js","sources":["../../../../../src/modules/extensions/features/resourceDetail.ts"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { slotFactory, wrapperFactory } from '../factories'\nimport type { PluginUser } from '../types'\n\n/**\n * Leaf slots on the resource detail card.\n *\n * Payloads are additive-only. Adding a field is invisible to existing plugins;\n * renaming or removing one breaks them at compile time on their next core bump,\n * which is the intended moment for that to surface.\n */\nexport interface ResourceDetailSlots {\n /**\n * Actions on the detail card's header band.\n *\n * The band and not a tab panel: tab panels mount only while active and\n * Overview is first, so a slot inside a tab is invisible until a second click.\n */\n resourceDetailAccessActions: {\n /** The resource on screen — a top-level app OR one of its sub-resources. */\n resource: Resource\n /** Present only on a sub-resource view. Its absence is how a plugin tells the two apart. */\n parent?: Resource\n /** Children of `parent ?? resource`: the family's sub-resources. */\n subResources: Resource[]\n user: PluginUser | null\n }\n\n /**\n * One cell per row of the sub-resources table, in a column of its own.\n *\n * Called once per rendered row, which is the opposite shape to\n * `resourceDetailAccessActions` — that one is called once and handed the whole\n * list. Use this when the action belongs to a single child.\n *\n * Two things to know before filling it. The table renders EVERY row with no\n * virtualisation, so a contribution here is mounted as many times as the\n * family has children (hundreds, in a large catalog) — keep the cell cheap and\n * defer anything expensive until it is interacted with. And the column only\n * appears when a plugin actually fills it, so a build with no plugin renders\n * the table exactly as before.\n */\n resourceSubResourceRowActions: {\n /** The child this row is showing. */\n resource: Resource\n /** The entry the row hangs off. Always present here, unlike the header slot. */\n parent: Resource\n user: PluginUser | null\n }\n}\n\n/**\n * Wrappers on the resource detail card.\n *\n * `resourceDetailProvider` is mounted inside the panel's `key={shownSlug}`\n * subtree, so anything a plugin provides here is scoped to the resource on\n * screen and is discarded when the card navigates. That is what lets two slots\n * share per-resource state without the plugin writing any cleanup.\n */\nexport interface ResourceDetailWrappers {\n resourceDetailProvider: {\n resource: Resource\n parent?: Resource\n }\n}\n\nconst mkSlot = slotFactory<ResourceDetailSlots>()\nconst mkWrapper = wrapperFactory<ResourceDetailWrappers>()\n\nexport const ResourceDetailAccessActions = mkSlot('resourceDetailAccessActions')\nexport const ResourceSubResourceRowActions = mkSlot(\n 'resourceSubResourceRowActions',\n)\nexport const ResourceDetailProvider = mkWrapper('resourceDetailProvider')\n"],"names":[],"mappings":";AAkEA,MAAM,SAAS,YAAA;AACf,MAAM,YAAY,eAAA;AAEX,MAAM,8BAA8B,OAAO,6BAA6B;AACxE,MAAM,gCAAgC;AAAA,EAC3C;AACF;AACO,MAAM,yBAAyB,UAAU,wBAAwB;"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ResourceDetailSlots, ResourceDetailWrappers } from './features/resourceDetail.js';
|
|
3
|
+
import { PluginUser } from './types.js';
|
|
4
|
+
export { ExtensionsContext, useExtensions } from './ExtensionsContext.js';
|
|
5
|
+
export { slotFactory, wrapperFactory } from './factories.js';
|
|
6
|
+
export { makeSlot, makeWrapper } from './makeSlot.js';
|
|
7
|
+
export type { PluginUser, PluginRegistration } from './types.js';
|
|
8
|
+
export type { AssertDisjoint } from './types.js';
|
|
9
|
+
export { ResourceDetailAccessActions, ResourceSubResourceRowActions, ResourceDetailProvider, } from './features/resourceDetail.js';
|
|
10
|
+
export type { ResourceDetailSlots, ResourceDetailWrappers, } from './features/resourceDetail.js';
|
|
11
|
+
/**
|
|
12
|
+
* Every leaf slot in the core, composed from the per-feature specs.
|
|
13
|
+
*
|
|
14
|
+
* Adding a feature means a new `features/<name>.ts` plus one member here and one
|
|
15
|
+
* line in the guard below. No existing feature file is touched.
|
|
16
|
+
*/
|
|
17
|
+
export type SlotSpec = ResourceDetailSlots;
|
|
18
|
+
/** Every wrapper slot in the core. */
|
|
19
|
+
export type WrapperSpec = ResourceDetailWrappers;
|
|
20
|
+
/**
|
|
21
|
+
* A plugin's leaf-slot handlers. Each receives exactly its slot's payload.
|
|
22
|
+
*
|
|
23
|
+
* Registering an unknown slot, or reading a field the slot does not pass, is a
|
|
24
|
+
* compile error — which is the whole point of composing `SlotSpec` rather than
|
|
25
|
+
* typing handlers as `(props: unknown)`.
|
|
26
|
+
*/
|
|
27
|
+
export type PluginSlots = {
|
|
28
|
+
[N in keyof SlotSpec]?: (props: SlotSpec[N]) => ReactNode;
|
|
29
|
+
};
|
|
30
|
+
/** A plugin's wrapper handlers. `children` is injected by the core. */
|
|
31
|
+
export type PluginWrappers = {
|
|
32
|
+
[N in keyof WrapperSpec]?: (props: WrapperSpec[N] & {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}) => ReactNode;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* One deployment-specific feature's contribution to the core's UI.
|
|
38
|
+
*
|
|
39
|
+
* Passed to `<App extensions={[...]} />` as an array, not a keyed map: a map
|
|
40
|
+
* holds one value per key, so a second plugin at the same slot would silently
|
|
41
|
+
* replace the first and two wrappers around one subtree would be impossible.
|
|
42
|
+
*
|
|
43
|
+
* The array must be a module-level constant. The core iterates it in render
|
|
44
|
+
* order, so rebuilding it per render would change hook call order inside the
|
|
45
|
+
* plugins and violate the rules of hooks.
|
|
46
|
+
*/
|
|
47
|
+
export interface AcPlugin {
|
|
48
|
+
/** Identity, used in error reports and test assertions. */
|
|
49
|
+
name: string;
|
|
50
|
+
slots?: PluginSlots;
|
|
51
|
+
wrappers?: PluginWrappers;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether any registered plugin fills this slot.
|
|
55
|
+
*
|
|
56
|
+
* For the cases where rendering the slot is not enough and the core has to
|
|
57
|
+
* change its own layout around it — a table column, say, which would otherwise
|
|
58
|
+
* be an empty column with a header and no cells in every build that registers
|
|
59
|
+
* nothing. Rendering an empty slot costs nothing; reserving space for one is
|
|
60
|
+
* what this is for.
|
|
61
|
+
*
|
|
62
|
+
* Use it ONLY for layout. A slot must never be the only route to something the
|
|
63
|
+
* core itself needs, so branching behaviour on a plugin's presence is a design
|
|
64
|
+
* smell even where branching layout is not.
|
|
65
|
+
*/
|
|
66
|
+
export declare function useSlotFilled(slot: keyof SlotSpec): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The signed-in user, reduced to what slots are given.
|
|
69
|
+
*
|
|
70
|
+
* One place, so two slots cannot disagree about the shape — and deliberately a
|
|
71
|
+
* projection rather than the real user object: the core's `User` is free to grow
|
|
72
|
+
* fields, and handing a plugin the whole thing would make every one of them part
|
|
73
|
+
* of the published slot contract.
|
|
74
|
+
*
|
|
75
|
+
* Returns `null` with no `AuthProvider` above it. The detail card renders inside
|
|
76
|
+
* the catalog panel, which some suites mount outside the provider, and throwing
|
|
77
|
+
* there would make a plugin's presence break unrelated tests.
|
|
78
|
+
*/
|
|
79
|
+
export declare function usePluginUser(): PluginUser | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useOptionalUser } from "../auth/AuthContext.js";
|
|
3
|
+
import { useExtensions } from "./ExtensionsContext.js";
|
|
4
|
+
import { ExtensionsContext } from "./ExtensionsContext.js";
|
|
5
|
+
function useSlotFilled(slot) {
|
|
6
|
+
return useExtensions().some((plugin) => {
|
|
7
|
+
var _a;
|
|
8
|
+
return Boolean((_a = plugin.slots) == null ? void 0 : _a[slot]);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function usePluginUser() {
|
|
12
|
+
const user = useOptionalUser();
|
|
13
|
+
return useMemo(
|
|
14
|
+
// `?? false`, because admin is a capability: absent must read as "not an
|
|
15
|
+
// admin", never as undefined leaking into a plugin's own check.
|
|
16
|
+
() => user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,
|
|
17
|
+
[user]
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
ExtensionsContext,
|
|
22
|
+
useExtensions,
|
|
23
|
+
usePluginUser,
|
|
24
|
+
useSlotFilled
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/modules/extensions/index.ts"],"sourcesContent":["import { useMemo } from 'react'\nimport type { ReactNode } from 'react'\n// Relative, not `~`: this file is on the published entry's graph and therefore\n// in every consumer's program, and `~` is the CORE's alias — a consumer only\n// resolves it if it has mapped it, which the example app has not.\nimport { useOptionalUser } from '../auth/AuthContext'\nimport type {\n ResourceDetailSlots,\n ResourceDetailWrappers,\n} from './features/resourceDetail'\nimport type { AssertDisjoint, PluginUser } from './types'\n\nimport { useExtensions } from './ExtensionsContext'\n\nexport { ExtensionsContext, useExtensions } from './ExtensionsContext'\nexport { slotFactory, wrapperFactory } from './factories'\nexport { makeSlot, makeWrapper } from './makeSlot'\nexport type { PluginUser, PluginRegistration } from './types'\nexport type { AssertDisjoint } from './types'\nexport {\n ResourceDetailAccessActions,\n ResourceSubResourceRowActions,\n ResourceDetailProvider,\n} from './features/resourceDetail'\nexport type {\n ResourceDetailSlots,\n ResourceDetailWrappers,\n} from './features/resourceDetail'\n\n/**\n * Every leaf slot in the core, composed from the per-feature specs.\n *\n * Adding a feature means a new `features/<name>.ts` plus one member here and one\n * line in the guard below. No existing feature file is touched.\n */\nexport type SlotSpec = ResourceDetailSlots\n\n/** Every wrapper slot in the core. */\nexport type WrapperSpec = ResourceDetailWrappers\n\n// Guard: with a second feature this becomes\n// `AssertDisjoint<ResourceDetailSlots, ServiceDeskSlots>` and fails with the\n// duplicated id spelled out in the error. With one feature it is trivially true,\n// and is kept so the next feature inherits the check rather than inventing it.\nconst _slotIdsAreDisjoint: AssertDisjoint<SlotSpec, Record<never, never>> = true\nvoid _slotIdsAreDisjoint\n\n/**\n * A plugin's leaf-slot handlers. Each receives exactly its slot's payload.\n *\n * Registering an unknown slot, or reading a field the slot does not pass, is a\n * compile error — which is the whole point of composing `SlotSpec` rather than\n * typing handlers as `(props: unknown)`.\n */\nexport type PluginSlots = {\n [N in keyof SlotSpec]?: (props: SlotSpec[N]) => ReactNode\n}\n\n/** A plugin's wrapper handlers. `children` is injected by the core. */\nexport type PluginWrappers = {\n [N in keyof WrapperSpec]?: (\n props: WrapperSpec[N] & { children: ReactNode },\n ) => ReactNode\n}\n\n/**\n * One deployment-specific feature's contribution to the core's UI.\n *\n * Passed to `<App extensions={[...]} />` as an array, not a keyed map: a map\n * holds one value per key, so a second plugin at the same slot would silently\n * replace the first and two wrappers around one subtree would be impossible.\n *\n * The array must be a module-level constant. The core iterates it in render\n * order, so rebuilding it per render would change hook call order inside the\n * plugins and violate the rules of hooks.\n */\nexport interface AcPlugin {\n /** Identity, used in error reports and test assertions. */\n name: string\n slots?: PluginSlots\n wrappers?: PluginWrappers\n}\n\n/**\n * Whether any registered plugin fills this slot.\n *\n * For the cases where rendering the slot is not enough and the core has to\n * change its own layout around it — a table column, say, which would otherwise\n * be an empty column with a header and no cells in every build that registers\n * nothing. Rendering an empty slot costs nothing; reserving space for one is\n * what this is for.\n *\n * Use it ONLY for layout. A slot must never be the only route to something the\n * core itself needs, so branching behaviour on a plugin's presence is a design\n * smell even where branching layout is not.\n */\nexport function useSlotFilled(slot: keyof SlotSpec): boolean {\n return useExtensions().some((plugin) => Boolean(plugin.slots?.[slot]))\n}\n\n/**\n * The signed-in user, reduced to what slots are given.\n *\n * One place, so two slots cannot disagree about the shape — and deliberately a\n * projection rather than the real user object: the core's `User` is free to grow\n * fields, and handing a plugin the whole thing would make every one of them part\n * of the published slot contract.\n *\n * Returns `null` with no `AuthProvider` above it. The detail card renders inside\n * the catalog panel, which some suites mount outside the provider, and throwing\n * there would make a plugin's presence break unrelated tests.\n */\nexport function usePluginUser(): PluginUser | null {\n const user = useOptionalUser()\n return useMemo(\n // `?? false`, because admin is a capability: absent must read as \"not an\n // admin\", never as undefined leaking into a plugin's own check.\n () => (user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null),\n [user],\n )\n}\n"],"names":[],"mappings":";;;;AAgGO,SAAS,cAAc,MAA+B;AAC3D,SAAO,cAAA,EAAgB,KAAK,CAAC,WAAA;;AAAW,oBAAQ,YAAO,UAAP,mBAAe,KAAK;AAAA,GAAC;AACvE;AAcO,SAAS,gBAAmC;AACjD,QAAM,OAAO,gBAAA;AACb,SAAO;AAAA;AAAA;AAAA,IAGL,MAAO,OAAO,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,MAAA,IAAU;AAAA,IACtE,CAAC,IAAI;AAAA,EAAA;AAET;"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the component the core renders at one leaf slot.
|
|
4
|
+
*
|
|
5
|
+
* Every registered plugin contributing to this slot renders, in array order, so
|
|
6
|
+
* two plugins can occupy the same point. Each is isolated: one plugin throwing
|
|
7
|
+
* removes only its own node.
|
|
8
|
+
*
|
|
9
|
+
* Each contribution also gets its own `Suspense`. Without it a plugin that
|
|
10
|
+
* suspends — any `useSuspenseQuery`, any lazy import — bubbles to the nearest
|
|
11
|
+
* boundary above, which for the detail card is `AppCatalogPage`'s
|
|
12
|
+
* `<Suspense fallback={null}>`; the whole card would then vanish until the
|
|
13
|
+
* plugin's data arrived.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeSlot<TProps>(slotName: string): {
|
|
16
|
+
(props: TProps): ReactNode;
|
|
17
|
+
displayName: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Builds the component the core renders *around* a subtree.
|
|
21
|
+
*
|
|
22
|
+
* Folded right-to-left so the FIRST plugin in the array ends up outermost, which
|
|
23
|
+
* makes nesting order readable from the array itself.
|
|
24
|
+
*
|
|
25
|
+
* Note both fallbacks differ from a leaf slot's: they are the accumulated
|
|
26
|
+
* children, never `null`. A wrapper only decorates — usually by providing
|
|
27
|
+
* context — so neither a broken one nor a suspending one may blank the core
|
|
28
|
+
* subtree it was wrapping.
|
|
29
|
+
*
|
|
30
|
+
* The `Suspense` matters as much as the boundary. Without it a wrapper that
|
|
31
|
+
* suspends bubbles to the nearest boundary above, which for the detail card is
|
|
32
|
+
* `AppCatalogPage`'s `<Suspense fallback={null}>` — so one plugin fetching
|
|
33
|
+
* something would make the entire card disappear until it resolved. Falling
|
|
34
|
+
* back to `acc` instead renders the core subtree immediately, just without the
|
|
35
|
+
* decoration, and it arrives when the plugin is ready.
|
|
36
|
+
*
|
|
37
|
+
* A consequence worth stating: while suspended, descendants see no context from
|
|
38
|
+
* this wrapper. A wrapper whose children cannot cope with that should not
|
|
39
|
+
* suspend — it should render children and fetch inside its own subtree.
|
|
40
|
+
*/
|
|
41
|
+
export declare function makeWrapper<TProps>(wrapperName: string): {
|
|
42
|
+
(props: TProps & {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}): ReactNode;
|
|
45
|
+
displayName: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
4
|
+
import { useExtensions } from "./ExtensionsContext.js";
|
|
5
|
+
function reportSlotError(plugin, slot, error) {
|
|
6
|
+
console.error(`[plugin:${plugin}] slot "${slot}" failed to render`, error);
|
|
7
|
+
}
|
|
8
|
+
const call = (fn, props) => fn(props);
|
|
9
|
+
function Contribution({
|
|
10
|
+
render,
|
|
11
|
+
props
|
|
12
|
+
}) {
|
|
13
|
+
return call(render, props);
|
|
14
|
+
}
|
|
15
|
+
function makeSlot(slotName) {
|
|
16
|
+
function Slot(props) {
|
|
17
|
+
const plugins = useExtensions();
|
|
18
|
+
return plugins.map((plugin) => {
|
|
19
|
+
var _a;
|
|
20
|
+
const render = (_a = plugin.slots) == null ? void 0 : _a[slotName];
|
|
21
|
+
if (!render) return null;
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
ErrorBoundary,
|
|
24
|
+
{
|
|
25
|
+
fallbackRender: () => null,
|
|
26
|
+
onError: (error) => reportSlotError(plugin.name, slotName, error),
|
|
27
|
+
children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Contribution, { render, props }) })
|
|
28
|
+
},
|
|
29
|
+
plugin.name
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
Slot.displayName = `Slot(${slotName})`;
|
|
34
|
+
return Slot;
|
|
35
|
+
}
|
|
36
|
+
function makeWrapper(wrapperName) {
|
|
37
|
+
function Wrapper(props) {
|
|
38
|
+
const plugins = useExtensions();
|
|
39
|
+
const { children, ...rest } = props;
|
|
40
|
+
return plugins.reduceRight((acc, plugin) => {
|
|
41
|
+
var _a;
|
|
42
|
+
const render = (_a = plugin.wrappers) == null ? void 0 : _a[wrapperName];
|
|
43
|
+
if (!render) return acc;
|
|
44
|
+
return /* @__PURE__ */ jsx(
|
|
45
|
+
ErrorBoundary,
|
|
46
|
+
{
|
|
47
|
+
fallbackRender: () => acc,
|
|
48
|
+
onError: (error) => reportSlotError(plugin.name, wrapperName, error),
|
|
49
|
+
children: /* @__PURE__ */ jsx(Suspense, { fallback: acc, children: /* @__PURE__ */ jsx(Contribution, { render, props: { ...rest, children: acc } }) })
|
|
50
|
+
},
|
|
51
|
+
plugin.name
|
|
52
|
+
);
|
|
53
|
+
}, children);
|
|
54
|
+
}
|
|
55
|
+
Wrapper.displayName = `SlotWrapper(${wrapperName})`;
|
|
56
|
+
return Wrapper;
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
makeSlot,
|
|
60
|
+
makeWrapper
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=makeSlot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"makeSlot.js","sources":["../../../../src/modules/extensions/makeSlot.tsx"],"sourcesContent":["import { Suspense } from 'react'\nimport { ErrorBoundary } from 'react-error-boundary'\nimport type { ReactNode } from 'react'\nimport { useExtensions } from './ExtensionsContext'\nimport type { OpaqueRenderer } from './types'\n\n/**\n * Reported when a plugin's render throws.\n *\n * Deliberately not swallowed. With a `null` fallback and no report, a crashing\n * plugin is indistinguishable from an unregistered one — the button is simply\n * absent, in production and in tests alike. Routing it through `console.error`\n * is what makes `getGlobalError()` in the test kit able to assert it.\n */\nfunction reportSlotError(plugin: string, slot: string, error: unknown): void {\n console.error(`[plugin:${plugin}] slot \"${slot}\" failed to render`, error)\n}\n\nconst call = (fn: OpaqueRenderer, props: unknown): ReactNode =>\n (fn as (p: unknown) => ReactNode)(props)\n\n/**\n * Invokes a plugin's handler from inside its own component.\n *\n * Not an inline `{call(render, props)}`: that expression is evaluated while the\n * surrounding `Slot` renders, which is BEFORE React mounts the ErrorBoundary\n * around it. A boundary only catches errors thrown by its descendants, so an\n * inline call escaped the boundary entirely and took the host down with it.\n * Putting the invocation in a child component is what moves the throw inside.\n */\nfunction Contribution({\n render,\n props,\n}: {\n render: OpaqueRenderer\n props: unknown\n}): ReactNode {\n return call(render, props)\n}\n\n/**\n * Builds the component the core renders at one leaf slot.\n *\n * Every registered plugin contributing to this slot renders, in array order, so\n * two plugins can occupy the same point. Each is isolated: one plugin throwing\n * removes only its own node.\n *\n * Each contribution also gets its own `Suspense`. Without it a plugin that\n * suspends — any `useSuspenseQuery`, any lazy import — bubbles to the nearest\n * boundary above, which for the detail card is `AppCatalogPage`'s\n * `<Suspense fallback={null}>`; the whole card would then vanish until the\n * plugin's data arrived.\n */\nexport function makeSlot<TProps>(slotName: string) {\n function Slot(props: TProps): ReactNode {\n const plugins = useExtensions()\n\n return plugins.map((plugin) => {\n const render = plugin.slots?.[slotName]\n if (!render) return null\n return (\n <ErrorBoundary\n key={plugin.name}\n fallbackRender={() => null}\n onError={(error) => reportSlotError(plugin.name, slotName, error)}\n >\n <Suspense fallback={null}>\n <Contribution render={render} props={props} />\n </Suspense>\n </ErrorBoundary>\n )\n })\n }\n Slot.displayName = `Slot(${slotName})`\n return Slot\n}\n\n/**\n * Builds the component the core renders *around* a subtree.\n *\n * Folded right-to-left so the FIRST plugin in the array ends up outermost, which\n * makes nesting order readable from the array itself.\n *\n * Note both fallbacks differ from a leaf slot's: they are the accumulated\n * children, never `null`. A wrapper only decorates — usually by providing\n * context — so neither a broken one nor a suspending one may blank the core\n * subtree it was wrapping.\n *\n * The `Suspense` matters as much as the boundary. Without it a wrapper that\n * suspends bubbles to the nearest boundary above, which for the detail card is\n * `AppCatalogPage`'s `<Suspense fallback={null}>` — so one plugin fetching\n * something would make the entire card disappear until it resolved. Falling\n * back to `acc` instead renders the core subtree immediately, just without the\n * decoration, and it arrives when the plugin is ready.\n *\n * A consequence worth stating: while suspended, descendants see no context from\n * this wrapper. A wrapper whose children cannot cope with that should not\n * suspend — it should render children and fetch inside its own subtree.\n */\nexport function makeWrapper<TProps>(wrapperName: string) {\n function Wrapper(props: TProps & { children: ReactNode }): ReactNode {\n const plugins = useExtensions()\n const { children, ...rest } = props\n\n return plugins.reduceRight<ReactNode>((acc, plugin) => {\n const render = plugin.wrappers?.[wrapperName]\n if (!render) return acc\n return (\n <ErrorBoundary\n key={plugin.name}\n fallbackRender={() => acc}\n onError={(error) => reportSlotError(plugin.name, wrapperName, error)}\n >\n <Suspense fallback={acc}>\n <Contribution render={render} props={{ ...rest, children: acc }} />\n </Suspense>\n </ErrorBoundary>\n )\n }, children)\n }\n Wrapper.displayName = `SlotWrapper(${wrapperName})`\n return Wrapper\n}\n"],"names":[],"mappings":";;;;AAcA,SAAS,gBAAgB,QAAgB,MAAc,OAAsB;AAC3E,UAAQ,MAAM,WAAW,MAAM,WAAW,IAAI,sBAAsB,KAAK;AAC3E;AAEA,MAAM,OAAO,CAAC,IAAoB,UAC/B,GAAiC,KAAK;AAWzC,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AACF,GAGc;AACZ,SAAO,KAAK,QAAQ,KAAK;AAC3B;AAeO,SAAS,SAAiB,UAAkB;AACjD,WAAS,KAAK,OAA0B;AACtC,UAAM,UAAU,cAAA;AAEhB,WAAO,QAAQ,IAAI,CAAC,WAAW;;AAC7B,YAAM,UAAS,YAAO,UAAP,mBAAe;AAC9B,UAAI,CAAC,OAAQ,QAAO;AACpB,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,gBAAgB,MAAM;AAAA,UACtB,SAAS,CAAC,UAAU,gBAAgB,OAAO,MAAM,UAAU,KAAK;AAAA,UAEhE,UAAA,oBAAC,YAAS,UAAU,MAClB,8BAAC,cAAA,EAAa,QAAgB,OAAc,EAAA,CAC9C;AAAA,QAAA;AAAA,QANK,OAAO;AAAA,MAAA;AAAA,IASlB,CAAC;AAAA,EACH;AACA,OAAK,cAAc,QAAQ,QAAQ;AACnC,SAAO;AACT;AAwBO,SAAS,YAAoB,aAAqB;AACvD,WAAS,QAAQ,OAAoD;AACnE,UAAM,UAAU,cAAA;AAChB,UAAM,EAAE,UAAU,GAAG,KAAA,IAAS;AAE9B,WAAO,QAAQ,YAAuB,CAAC,KAAK,WAAW;;AACrD,YAAM,UAAS,YAAO,aAAP,mBAAkB;AACjC,UAAI,CAAC,OAAQ,QAAO;AACpB,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,gBAAgB,MAAM;AAAA,UACtB,SAAS,CAAC,UAAU,gBAAgB,OAAO,MAAM,aAAa,KAAK;AAAA,UAEnE,UAAA,oBAAC,UAAA,EAAS,UAAU,KAClB,8BAAC,cAAA,EAAa,QAAgB,OAAO,EAAE,GAAG,MAAM,UAAU,IAAA,GAAO,EAAA,CACnE;AAAA,QAAA;AAAA,QANK,OAAO;AAAA,MAAA;AAAA,IASlB,GAAG,QAAQ;AAAA,EACb;AACA,UAAQ,cAAc,eAAe,WAAW;AAChD,SAAO;AACT;"}
|