@arcote.tech/platform 0.4.7 → 0.4.9
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/package.json +4 -4
- package/src/arc.ts +2 -0
- package/src/index.ts +4 -1
- package/src/layout/page-router.tsx +13 -4
- package/src/layout/use-page-fragments.ts +20 -0
- package/src/module-loader.ts +128 -40
- package/src/platform-app.tsx +11 -2
- package/src/registry.ts +42 -2
- package/src/types.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/platform",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.9",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Przemysław Krasiński [arcote.tech]",
|
|
7
7
|
"description": "Arc Platform — module system, router, layout, theme, i18n, platform app shell",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"type-check": "tsc --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@arcote.tech/arc-ds": "^0.4.
|
|
22
|
-
"@arcote.tech/arc-react": "^0.4.
|
|
21
|
+
"@arcote.tech/arc-ds": "^0.4.9",
|
|
22
|
+
"@arcote.tech/arc-react": "^0.4.9"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@arcote.tech/arc": "^0.4.
|
|
25
|
+
"@arcote.tech/arc": "^0.4.9",
|
|
26
26
|
"@lingui/core": "^5.0.0",
|
|
27
27
|
"@lingui/react": "^5.0.0",
|
|
28
28
|
"framer-motion": "^12.0.0",
|
package/src/arc.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,9 +7,11 @@ export { arc } from "./arc";
|
|
|
7
7
|
export {
|
|
8
8
|
clearModules,
|
|
9
9
|
clearRegistry,
|
|
10
|
+
forceRegisterModule,
|
|
10
11
|
getAllFragments,
|
|
11
12
|
getAllModuleAccess,
|
|
12
13
|
getAllModules,
|
|
14
|
+
getAllRegisteredModules,
|
|
13
15
|
getContext,
|
|
14
16
|
getModuleAccess,
|
|
15
17
|
getContextElementFragments,
|
|
@@ -62,6 +64,7 @@ export { PageRouter } from "./layout/page-router";
|
|
|
62
64
|
export { PageSubNavShell } from "./layout/page-sub-nav-shell";
|
|
63
65
|
export { SlotRenderer } from "./layout/slot-renderer";
|
|
64
66
|
export type { SlotRendererProps } from "./layout/slot-renderer";
|
|
67
|
+
export { usePageFragments } from "./layout/use-page-fragments";
|
|
65
68
|
export { useSlotFragments } from "./layout/use-slot-fragments";
|
|
66
69
|
|
|
67
70
|
// Router
|
|
@@ -88,7 +91,7 @@ export type { LocaleProviderProps } from "./locale";
|
|
|
88
91
|
export { useTitle } from "./hooks/use-title";
|
|
89
92
|
|
|
90
93
|
// Platform
|
|
91
|
-
export { loadModules, reloadModules, useModuleLoader } from "./module-loader";
|
|
94
|
+
export { loadModules, reloadModules, syncModules, useModuleLoader } from "./module-loader";
|
|
92
95
|
export type { ModuleLoaderState, ModuleManifest } from "./module-loader";
|
|
93
96
|
export { PlatformApp } from "./platform-app";
|
|
94
97
|
export type { PlatformAppProps } from "./platform-app";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnimatePresence, motion } from "framer-motion";
|
|
2
2
|
import { Suspense, useEffect } from "react";
|
|
3
|
-
import { getDefaultLayout
|
|
3
|
+
import { getDefaultLayout } from "../registry";
|
|
4
|
+
import { usePageFragments } from "./use-page-fragments";
|
|
4
5
|
import { useArcNavigate, useArcRoute, matchRoutePath, hasRouteParams } from "../router";
|
|
5
6
|
import { useArcRouter } from "@arcote.tech/arc-ds";
|
|
6
7
|
import type { PageFragment } from "../types";
|
|
@@ -11,8 +12,7 @@ type MatchResult = {
|
|
|
11
12
|
params: Record<string, string>;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
function matchPage(path: string): MatchResult | undefined {
|
|
15
|
-
const pages = getPageFragments();
|
|
15
|
+
function matchPage(path: string, pages: PageFragment[]): MatchResult | undefined {
|
|
16
16
|
|
|
17
17
|
// 1. Exact match (non-parent pages only)
|
|
18
18
|
const exact = pages.find((p) => p.path === path && !p.children?.length);
|
|
@@ -64,7 +64,8 @@ export function PageRouter() {
|
|
|
64
64
|
const route = useArcRoute();
|
|
65
65
|
const navigate = useArcNavigate();
|
|
66
66
|
const router = useArcRouter();
|
|
67
|
-
const
|
|
67
|
+
const pages = usePageFragments();
|
|
68
|
+
const match = matchPage(route, pages);
|
|
68
69
|
const DefaultLayout = getDefaultLayout();
|
|
69
70
|
|
|
70
71
|
// Sync matched params to router context
|
|
@@ -80,6 +81,14 @@ export function PageRouter() {
|
|
|
80
81
|
}
|
|
81
82
|
}, [route, match?.params]);
|
|
82
83
|
|
|
84
|
+
// Redirect to first available page when current route is no longer registered
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!match && pages.length > 0 && route !== "/") {
|
|
87
|
+
const firstNav = pages.find((p) => p.icon && p.label && !p.path.includes("/:"));
|
|
88
|
+
navigate(firstNav?.path ?? "/");
|
|
89
|
+
}
|
|
90
|
+
}, [match, pages, route, navigate]);
|
|
91
|
+
|
|
83
92
|
if (!match) {
|
|
84
93
|
if (DefaultLayout) return <DefaultLayout>{null}</DefaultLayout>;
|
|
85
94
|
return null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { getPageFragments, subscribe } from "../registry";
|
|
3
|
+
import type { PageFragment } from "../types";
|
|
4
|
+
|
|
5
|
+
export function usePageFragments(): PageFragment[] {
|
|
6
|
+
const cacheRef = useRef<PageFragment[]>([]);
|
|
7
|
+
|
|
8
|
+
const getSnapshot = () => {
|
|
9
|
+
const next = getPageFragments();
|
|
10
|
+
const prev = cacheRef.current;
|
|
11
|
+
|
|
12
|
+
const changed =
|
|
13
|
+
next.length !== prev.length || next.some((f, i) => f.id !== prev[i]?.id);
|
|
14
|
+
|
|
15
|
+
if (changed) cacheRef.current = next;
|
|
16
|
+
return cacheRef.current;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
20
|
+
}
|
package/src/module-loader.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearModules, getContext } from "./registry";
|
|
1
|
+
import { clearModules, getAllRegisteredModules, getContext, setActiveModules } from "./registry";
|
|
2
2
|
import type { ModuleDescriptor } from "./types";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
@@ -14,34 +14,58 @@ function moduleUrl(baseUrl: string, mod: ModuleDescriptor, bust?: string): strin
|
|
|
14
14
|
return bust ? `${base}${base.includes("?") ? "&" : "?"}t=${bust}` : base;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** Read
|
|
18
|
-
function
|
|
19
|
-
if (typeof localStorage === "undefined") return
|
|
17
|
+
/** Read all persisted arc tokens from localStorage */
|
|
18
|
+
function getAllPersistedTokens(): Record<string, string> {
|
|
19
|
+
if (typeof localStorage === "undefined") return {};
|
|
20
|
+
const tokens: Record<string, string> = {};
|
|
20
21
|
for (let i = 0; i < localStorage.length; i++) {
|
|
21
22
|
const key = localStorage.key(i);
|
|
22
23
|
if (key?.startsWith("arc:token:")) {
|
|
23
|
-
|
|
24
|
+
const raw = localStorage.getItem(key);
|
|
25
|
+
if (raw) tokens[key.slice(10)] = raw;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
|
-
return
|
|
28
|
+
return tokens;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/** Build X-Arc-Tokens header value from token map */
|
|
32
|
+
function buildTokensHeader(tokens: Record<string, string>): string {
|
|
33
|
+
return Object.entries(tokens)
|
|
34
|
+
.map(([scope, jwt]) => `${scope}:${jwt}`)
|
|
35
|
+
.join(",");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildHeaders(tokens?: Record<string, string>): HeadersInit {
|
|
39
|
+
const resolved = tokens ?? getAllPersistedTokens();
|
|
37
40
|
const headers: HeadersInit = {};
|
|
38
|
-
|
|
41
|
+
const tokensHeader = buildTokensHeader(resolved);
|
|
42
|
+
if (tokensHeader) headers["X-Arc-Tokens"] = tokensHeader;
|
|
43
|
+
// Also send first token as Authorization for backwards compatibility
|
|
44
|
+
const firstToken = Object.values(resolved)[0];
|
|
45
|
+
if (firstToken) headers["Authorization"] = `Bearer ${firstToken}`;
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
39
48
|
|
|
49
|
+
/** Fetch manifest from server */
|
|
50
|
+
async function fetchManifest(
|
|
51
|
+
baseUrl: string,
|
|
52
|
+
tokens?: Record<string, string>,
|
|
53
|
+
): Promise<ModuleManifest> {
|
|
54
|
+
const headers = buildHeaders(tokens);
|
|
40
55
|
const res = await fetch(`${baseUrl}/api/modules`, { headers });
|
|
41
56
|
if (!res.ok)
|
|
42
57
|
throw new Error(`Failed to fetch module manifest: ${res.status}`);
|
|
58
|
+
return res.json();
|
|
59
|
+
}
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Load all modules from manifest. Each module auto-registers via module().build().
|
|
63
|
+
*/
|
|
64
|
+
export async function loadModules(
|
|
65
|
+
baseUrl: string,
|
|
66
|
+
tokens?: Record<string, string>,
|
|
67
|
+
): Promise<ModuleManifest> {
|
|
68
|
+
const manifest = await fetchManifest(baseUrl, tokens);
|
|
45
69
|
|
|
46
70
|
await Promise.all(
|
|
47
71
|
manifest.modules.map(
|
|
@@ -53,25 +77,49 @@ export async function loadModules(
|
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
/**
|
|
56
|
-
*
|
|
80
|
+
* Sync modules with server — import any NEW modules, then set active filter.
|
|
81
|
+
* Modules are never unregistered (ES module cache prevents re-execution).
|
|
82
|
+
* Instead, visibility is controlled via an active set.
|
|
57
83
|
*/
|
|
58
|
-
export async function
|
|
84
|
+
export async function syncModules(
|
|
59
85
|
baseUrl: string,
|
|
60
|
-
|
|
86
|
+
tokens?: Record<string, string>,
|
|
61
87
|
): Promise<ModuleManifest> {
|
|
62
|
-
|
|
88
|
+
const manifest = await fetchManifest(baseUrl, tokens);
|
|
63
89
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
const manifestNames = new Set(manifest.modules.map((m) => m.name));
|
|
91
|
+
// Check against ALL registered modules (not just active ones)
|
|
92
|
+
const registeredNames = new Set(getAllRegisteredModules().map((m) => m.name));
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
// Import only truly new modules (never imported before)
|
|
95
|
+
const newModules = manifest.modules.filter((m) => !registeredNames.has(m.name));
|
|
96
|
+
|
|
97
|
+
if (newModules.length > 0) {
|
|
98
|
+
await Promise.all(
|
|
99
|
+
newModules.map((mod) => import(/* @vite-ignore */ moduleUrl(baseUrl, mod))),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set active filter — only modules in the manifest are visible
|
|
104
|
+
setActiveModules(manifestNames);
|
|
105
|
+
|
|
106
|
+
return manifest;
|
|
107
|
+
}
|
|
71
108
|
|
|
72
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Full reload — clear all modules, re-import with cache bust.
|
|
111
|
+
* Used for dev-mode file changes (code actually changed on disk).
|
|
112
|
+
*/
|
|
113
|
+
export async function reloadModules(
|
|
114
|
+
baseUrl: string,
|
|
115
|
+
tokens?: Record<string, string>,
|
|
116
|
+
): Promise<ModuleManifest> {
|
|
117
|
+
const manifest = await fetchManifest(baseUrl, tokens);
|
|
73
118
|
const bust = String(Date.now());
|
|
74
119
|
|
|
120
|
+
// Clear all modules — fresh code will re-register them
|
|
121
|
+
clearModules();
|
|
122
|
+
|
|
75
123
|
await Promise.all(
|
|
76
124
|
manifest.modules.map(
|
|
77
125
|
(mod) => import(/* @vite-ignore */ moduleUrl(baseUrl, mod, bust)),
|
|
@@ -83,7 +131,7 @@ export async function reloadModules(
|
|
|
83
131
|
|
|
84
132
|
/**
|
|
85
133
|
* Hook: loads modules on mount, provides state + context.
|
|
86
|
-
*
|
|
134
|
+
* Syncs modules when tokens change (login/logout/workspace switch).
|
|
87
135
|
*/
|
|
88
136
|
export function useModuleLoader(
|
|
89
137
|
baseUrl: string,
|
|
@@ -94,51 +142,91 @@ export function useModuleLoader(
|
|
|
94
142
|
);
|
|
95
143
|
const [error, setError] = useState<Error | null>(null);
|
|
96
144
|
const loaded = useRef(false);
|
|
97
|
-
const
|
|
145
|
+
const [tokenVersion, setTokenVersion] = useState(0);
|
|
146
|
+
|
|
147
|
+
// Full reload — for dev-mode file changes (re-import all JS)
|
|
148
|
+
const fullReload = useCallback(async () => {
|
|
149
|
+
try {
|
|
150
|
+
await reloadModules(baseUrl);
|
|
151
|
+
setState("ready");
|
|
152
|
+
} catch (e) {
|
|
153
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
154
|
+
setState("error");
|
|
155
|
+
}
|
|
156
|
+
}, [baseUrl]);
|
|
98
157
|
|
|
99
|
-
|
|
158
|
+
// Sync — for token changes (only fetch manifest, import new, remove stale)
|
|
159
|
+
const sync = useCallback(async () => {
|
|
100
160
|
try {
|
|
101
|
-
await
|
|
161
|
+
await syncModules(baseUrl);
|
|
102
162
|
setState("ready");
|
|
103
163
|
} catch (e) {
|
|
104
164
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
105
165
|
setState("error");
|
|
106
166
|
}
|
|
107
|
-
}, [baseUrl
|
|
167
|
+
}, [baseUrl]);
|
|
108
168
|
|
|
109
169
|
// Initial load
|
|
110
170
|
useEffect(() => {
|
|
111
171
|
if (options.skip || loaded.current) return;
|
|
112
172
|
loaded.current = true;
|
|
113
173
|
|
|
114
|
-
loadModules(baseUrl
|
|
174
|
+
loadModules(baseUrl)
|
|
115
175
|
.then(() => setState("ready"))
|
|
116
176
|
.catch((e) => {
|
|
117
177
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
118
178
|
setState("error");
|
|
119
179
|
});
|
|
120
180
|
|
|
121
|
-
// Listen for SSE reload events from CLI
|
|
181
|
+
// Listen for SSE reload events from CLI (code changed on disk → full reload)
|
|
122
182
|
const evtSource = new EventSource(`${baseUrl}/api/reload-stream`);
|
|
123
183
|
evtSource.onmessage = (event) => {
|
|
124
184
|
if (event.data === "connected") return;
|
|
125
185
|
console.log("[arc] Modules changed, reloading...");
|
|
126
|
-
|
|
186
|
+
fullReload();
|
|
127
187
|
};
|
|
128
188
|
evtSource.onerror = () => {
|
|
129
189
|
// SSE disconnected — will auto-reconnect
|
|
130
190
|
};
|
|
131
191
|
|
|
132
192
|
return () => evtSource.close();
|
|
133
|
-
}, [baseUrl,
|
|
193
|
+
}, [baseUrl, fullReload, options.skip]);
|
|
194
|
+
|
|
195
|
+
// Listen for token changes → sync (not full reload)
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (typeof window === "undefined") return;
|
|
198
|
+
|
|
199
|
+
const onTokenChange = () => setTokenVersion((v) => v + 1);
|
|
200
|
+
window.addEventListener("arc:token-change", onTokenChange);
|
|
201
|
+
|
|
202
|
+
const onStorage = (e: StorageEvent) => {
|
|
203
|
+
if (e.key?.startsWith("arc:token:")) {
|
|
204
|
+
setTokenVersion((v) => v + 1);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener("storage", onStorage);
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
window.removeEventListener("arc:token-change", onTokenChange);
|
|
211
|
+
window.removeEventListener("storage", onStorage);
|
|
212
|
+
};
|
|
213
|
+
}, []);
|
|
134
214
|
|
|
135
|
-
//
|
|
215
|
+
// Token changed → sync modules (import new, remove stale, don't re-import existing)
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!loaded.current) return;
|
|
218
|
+
if (tokenVersion === 0) return;
|
|
219
|
+
sync();
|
|
220
|
+
}, [tokenVersion, sync]);
|
|
221
|
+
|
|
222
|
+
// Legacy: sync when explicit token prop changes
|
|
223
|
+
const prevToken = useRef(options.token);
|
|
136
224
|
useEffect(() => {
|
|
137
225
|
if (!loaded.current) return;
|
|
138
226
|
if (prevToken.current === options.token) return;
|
|
139
227
|
prevToken.current = options.token;
|
|
140
|
-
|
|
141
|
-
}, [options.token,
|
|
228
|
+
sync();
|
|
229
|
+
}, [options.token, sync]);
|
|
142
230
|
|
|
143
|
-
return { state, error, context: getContext(), reload };
|
|
231
|
+
return { state, error, context: getContext(), reload: fullReload };
|
|
144
232
|
}
|
package/src/platform-app.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
setDefaultLayout,
|
|
14
14
|
} from "./registry";
|
|
15
15
|
import { ThemeProvider } from "./theme";
|
|
16
|
-
import { useEffect, useMemo, useState } from "react";
|
|
16
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
17
17
|
import { useModuleLoader } from "./module-loader";
|
|
18
18
|
import { PlatformModelProvider } from "./platform-context";
|
|
19
19
|
|
|
@@ -176,7 +176,16 @@ export function PlatformApp({
|
|
|
176
176
|
// If context is pre-loaded (modules imported statically in main.tsx),
|
|
177
177
|
// skip dynamic module loading entirely.
|
|
178
178
|
const loader = useModuleLoader(apiUrl, { skip: !!preloadedContext });
|
|
179
|
-
|
|
179
|
+
|
|
180
|
+
// Stabilize context reference — once set, don't change it.
|
|
181
|
+
// New context elements from synced modules merge into the global registry
|
|
182
|
+
// but we must NOT recreate the model (would remount the entire tree).
|
|
183
|
+
const contextRef = useRef<ArcContextAny | null>(preloadedContext ?? null);
|
|
184
|
+
if (!contextRef.current) {
|
|
185
|
+
const resolved = loader.context ?? getContext();
|
|
186
|
+
if (resolved) contextRef.current = resolved;
|
|
187
|
+
}
|
|
188
|
+
const context = contextRef.current;
|
|
180
189
|
|
|
181
190
|
const resolvedWsUrl = wsUrl ?? `${location.protocol}//${location.host}`;
|
|
182
191
|
|
package/src/registry.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
|
|
17
17
|
const modules: Map<string, ArcModule> = new Map();
|
|
18
|
+
/** Set of module names currently active (visible). Managed by syncModules. */
|
|
19
|
+
let activeModuleNames: Set<string> | null = null; // null = all active (no filtering)
|
|
18
20
|
const listeners: Set<() => void> = new Set();
|
|
19
21
|
|
|
20
22
|
function notify(): void {
|
|
@@ -26,8 +28,32 @@ export function subscribe(listener: () => void): () => void {
|
|
|
26
28
|
return () => listeners.delete(listener);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Register module. Skips if a module with the same name already exists
|
|
33
|
+
* (prevents duplicates from bundled dependencies importing the same module).
|
|
34
|
+
* Use `forceRegisterModule` for dev reloads where code has actually changed.
|
|
35
|
+
*/
|
|
30
36
|
export function registerModule(mod: ArcModule): void {
|
|
37
|
+
for (const existing of modules.values()) {
|
|
38
|
+
if (existing.name === mod.name) {
|
|
39
|
+
console.log(`[arc:registry] SKIP ${mod.name} (already registered as ${existing.id})`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log(`[arc:registry] REGISTER ${mod.name} as ${mod.id} (fragments: ${mod.fragments.length})`);
|
|
44
|
+
modules.set(mod.id, mod);
|
|
45
|
+
notify();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Force-register module, replacing any existing module with the same name.
|
|
49
|
+
* Used for dev reloads where code has changed on disk. */
|
|
50
|
+
export function forceRegisterModule(mod: ArcModule): void {
|
|
51
|
+
for (const [id, existing] of modules) {
|
|
52
|
+
if (existing.name === mod.name && id !== mod.id) {
|
|
53
|
+
modules.delete(id);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
31
57
|
modules.set(mod.id, mod);
|
|
32
58
|
notify();
|
|
33
59
|
}
|
|
@@ -42,9 +68,22 @@ export function getModule(moduleId: string): ArcModule | undefined {
|
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
export function getAllModules(): ArcModule[] {
|
|
71
|
+
if (!activeModuleNames) return [...modules.values()];
|
|
72
|
+
return [...modules.values()].filter((m) => activeModuleNames!.has(m.name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Get ALL registered modules (ignoring active filter). */
|
|
76
|
+
export function getAllRegisteredModules(): ArcModule[] {
|
|
45
77
|
return [...modules.values()];
|
|
46
78
|
}
|
|
47
79
|
|
|
80
|
+
/** Set which modules are currently active (visible in queries).
|
|
81
|
+
* Pass null to show all. Triggers re-render of slot/page subscribers. */
|
|
82
|
+
export function setActiveModules(names: Set<string> | null): void {
|
|
83
|
+
activeModuleNames = names;
|
|
84
|
+
notify();
|
|
85
|
+
}
|
|
86
|
+
|
|
48
87
|
// ---------------------------------------------------------------------------
|
|
49
88
|
// Context store — dynamiczny context rejestrowany przez pakiety
|
|
50
89
|
// ---------------------------------------------------------------------------
|
|
@@ -167,7 +206,8 @@ export function getSlotFragments(slotId: SlotId | string): SlotFragment[] {
|
|
|
167
206
|
}
|
|
168
207
|
|
|
169
208
|
export function getPageFragments(): PageFragment[] {
|
|
170
|
-
|
|
209
|
+
const fragments = getAllFragments().filter((f): f is PageFragment => f.is("page"));
|
|
210
|
+
return sortByOrdering(fragments);
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
export function getContextElementFragments(): ContextElementFragment[] {
|
package/src/types.ts
CHANGED
|
@@ -57,6 +57,9 @@ export interface PageFragment<
|
|
|
57
57
|
readonly label?: string | ReactNode;
|
|
58
58
|
readonly tooltip?: string | ReactNode;
|
|
59
59
|
readonly children?: readonly PageFragment[];
|
|
60
|
+
readonly ordering: FragmentOrdering;
|
|
61
|
+
/** Show in main navigation. Defaults to auto-detect (has icon + label). */
|
|
62
|
+
readonly nav?: boolean;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export type PageShellProps = {
|
|
@@ -158,6 +161,12 @@ export interface PageOptions {
|
|
|
158
161
|
label?: string | ReactNode;
|
|
159
162
|
tooltip?: string | ReactNode;
|
|
160
163
|
children?: PageFragment[];
|
|
164
|
+
/** Navigation ordering. Lower = earlier. Default 0. */
|
|
165
|
+
order?: number;
|
|
166
|
+
after?: string;
|
|
167
|
+
before?: string;
|
|
168
|
+
/** Explicitly include/exclude from main navigation. Default: auto (has icon + label). */
|
|
169
|
+
nav?: boolean;
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
export interface WrapperOptions extends FragmentOrdering {
|