@arcote.tech/platform 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +39 -0
- package/src/arc.ts +308 -0
- package/src/hooks/use-title.ts +11 -0
- package/src/i18n.tsx +76 -0
- package/src/index.ts +121 -0
- package/src/layout/blank-layout.tsx +15 -0
- package/src/layout/layout-provider.tsx +39 -0
- package/src/layout/overflow-nav.tsx +164 -0
- package/src/layout/page-router.tsx +130 -0
- package/src/layout/page-sub-nav-shell.tsx +23 -0
- package/src/layout/slot-renderer.tsx +70 -0
- package/src/layout/use-slot-fragments.ts +20 -0
- package/src/locale.tsx +64 -0
- package/src/module-loader.ts +102 -0
- package/src/platform-app.tsx +208 -0
- package/src/platform-context.tsx +45 -0
- package/src/registry.ts +238 -0
- package/src/router.tsx +23 -0
- package/src/theme.tsx +72 -0
- package/src/types.ts +165 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ArcContextAny } from "@arcote.tech/arc";
|
|
2
|
+
import type { DSVariantOverrides } from "@arcote.tech/arc-ds";
|
|
3
|
+
import { DesignSystemProvider } from "@arcote.tech/arc-ds";
|
|
4
|
+
import { reactModel } from "@arcote.tech/arc-react";
|
|
5
|
+
import { Layout } from "@arcote.tech/arc-ds";
|
|
6
|
+
import { ArcLayoutProvider } from "./layout/layout-provider";
|
|
7
|
+
import { PageRouter } from "./layout/page-router";
|
|
8
|
+
import { I18nProvider } from "./i18n";
|
|
9
|
+
import {
|
|
10
|
+
getContext,
|
|
11
|
+
getVariantOverrides,
|
|
12
|
+
getWrapperFragments,
|
|
13
|
+
setDefaultLayout,
|
|
14
|
+
} from "./registry";
|
|
15
|
+
import { ThemeProvider } from "./theme";
|
|
16
|
+
import { Tooltip as RadixTooltip } from "radix-ui";
|
|
17
|
+
import { useEffect, useMemo, useState } from "react";
|
|
18
|
+
import { useModuleLoader } from "./module-loader";
|
|
19
|
+
import { PlatformModelProvider } from "./platform-context";
|
|
20
|
+
|
|
21
|
+
const TooltipProvider = RadixTooltip.Provider;
|
|
22
|
+
|
|
23
|
+
export interface PlatformAppProps {
|
|
24
|
+
apiUrl?: string;
|
|
25
|
+
wsUrl?: string;
|
|
26
|
+
variants?: DSVariantOverrides;
|
|
27
|
+
/** Pre-loaded context — skip module loading if provided. */
|
|
28
|
+
context?: ArcContextAny;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function LoadingScreen() {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
display: "flex",
|
|
36
|
+
alignItems: "center",
|
|
37
|
+
justifyContent: "center",
|
|
38
|
+
height: "100vh",
|
|
39
|
+
fontFamily: "system-ui, sans-serif",
|
|
40
|
+
color: "#888",
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
Loading...
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ErrorScreen({ error }: { error: Error }) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
display: "flex",
|
|
53
|
+
flexDirection: "column",
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
height: "100vh",
|
|
57
|
+
fontFamily: "system-ui, sans-serif",
|
|
58
|
+
color: "#c00",
|
|
59
|
+
gap: "1rem",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<div>Failed to load application</div>
|
|
63
|
+
<pre style={{ fontSize: "0.85rem", opacity: 0.7 }}>{error.message}</pre>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface I18nConfig {
|
|
69
|
+
locales: Record<string, string>;
|
|
70
|
+
sourceLocale: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function useI18nConfig(apiUrl: string): I18nConfig | null {
|
|
74
|
+
const [config, setConfig] = useState<I18nConfig | null>(null);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
fetch(`${apiUrl}/api/translations`)
|
|
78
|
+
.then((r) => r.json())
|
|
79
|
+
.then((data) => {
|
|
80
|
+
if (data?.locales && Array.isArray(data.locales) && data.locales.length > 0) {
|
|
81
|
+
// Convert ["pl-PL", "en-US"] → { "pl-PL": "pl-PL", "en-US": "en-US" }
|
|
82
|
+
const localesMap: Record<string, string> = {};
|
|
83
|
+
for (const l of data.locales) localesMap[l] = l;
|
|
84
|
+
setConfig({ locales: localesMap, sourceLocale: data.sourceLocale });
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.catch(() => {});
|
|
88
|
+
}, [apiUrl]);
|
|
89
|
+
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function PlatformAppInner({
|
|
94
|
+
context,
|
|
95
|
+
wsUrl,
|
|
96
|
+
apiUrl,
|
|
97
|
+
variants,
|
|
98
|
+
}: {
|
|
99
|
+
context: ArcContextAny;
|
|
100
|
+
wsUrl: string;
|
|
101
|
+
apiUrl: string;
|
|
102
|
+
variants?: DSVariantOverrides;
|
|
103
|
+
}) {
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setDefaultLayout(Layout);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const i18nConfig = useI18nConfig(apiUrl);
|
|
109
|
+
|
|
110
|
+
const model = useMemo(
|
|
111
|
+
() => reactModel(context, { remoteUrl: wsUrl }),
|
|
112
|
+
[context, wsUrl],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
ModelProvider,
|
|
117
|
+
useModel,
|
|
118
|
+
createScope,
|
|
119
|
+
resetModel,
|
|
120
|
+
} = model;
|
|
121
|
+
|
|
122
|
+
const wrappers = getWrapperFragments();
|
|
123
|
+
let content = <PageRouter />;
|
|
124
|
+
for (let i = wrappers.length - 1; i >= 0; i--) {
|
|
125
|
+
const Wrapper = wrappers[i].component;
|
|
126
|
+
content = <Wrapper>{content}</Wrapper>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const loadMessages = useMemo(
|
|
130
|
+
() => async (locale: string) => {
|
|
131
|
+
const res = await fetch(`${apiUrl}/locales/${locale}.json`);
|
|
132
|
+
return res.ok ? res.json() : {};
|
|
133
|
+
},
|
|
134
|
+
[apiUrl],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
let inner = (
|
|
138
|
+
<ThemeProvider>
|
|
139
|
+
<DesignSystemProvider
|
|
140
|
+
variants={{ ...getVariantOverrides(), ...variants }}
|
|
141
|
+
>
|
|
142
|
+
<TooltipProvider>
|
|
143
|
+
<ArcLayoutProvider>{content}</ArcLayoutProvider>
|
|
144
|
+
</TooltipProvider>
|
|
145
|
+
</DesignSystemProvider>
|
|
146
|
+
</ThemeProvider>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Wrap with I18nProvider if translations are configured
|
|
150
|
+
if (i18nConfig) {
|
|
151
|
+
inner = (
|
|
152
|
+
<I18nProvider
|
|
153
|
+
defaultLocale={i18nConfig.sourceLocale}
|
|
154
|
+
locales={i18nConfig.locales}
|
|
155
|
+
loadMessages={loadMessages}
|
|
156
|
+
>
|
|
157
|
+
{inner}
|
|
158
|
+
</I18nProvider>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<ModelProvider>
|
|
164
|
+
<PlatformModelProvider
|
|
165
|
+
hooks={{ useModel, createScope, resetModel }}
|
|
166
|
+
>
|
|
167
|
+
{inner}
|
|
168
|
+
</PlatformModelProvider>
|
|
169
|
+
</ModelProvider>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function PlatformApp({
|
|
174
|
+
apiUrl = "",
|
|
175
|
+
wsUrl,
|
|
176
|
+
variants,
|
|
177
|
+
context: preloadedContext,
|
|
178
|
+
}: PlatformAppProps) {
|
|
179
|
+
// If context is pre-loaded (modules imported statically in main.tsx),
|
|
180
|
+
// skip dynamic module loading entirely.
|
|
181
|
+
const loader = useModuleLoader(apiUrl, { skip: !!preloadedContext });
|
|
182
|
+
const context = preloadedContext ?? loader.context ?? getContext();
|
|
183
|
+
|
|
184
|
+
const resolvedWsUrl = wsUrl ?? `${location.protocol}//${location.host}`;
|
|
185
|
+
|
|
186
|
+
if (!preloadedContext && loader.state === "loading") return <LoadingScreen />;
|
|
187
|
+
if (!preloadedContext && loader.state === "error")
|
|
188
|
+
return <ErrorScreen error={loader.error!} />;
|
|
189
|
+
if (!context)
|
|
190
|
+
return (
|
|
191
|
+
<ErrorScreen
|
|
192
|
+
error={
|
|
193
|
+
new Error(
|
|
194
|
+
"No context registered. Make sure your context package calls module() to register.",
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<PlatformAppInner
|
|
202
|
+
context={context}
|
|
203
|
+
wsUrl={resolvedWsUrl}
|
|
204
|
+
apiUrl={apiUrl}
|
|
205
|
+
variants={variants}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ArcContextAny } from "@arcote.tech/arc";
|
|
2
|
+
import type { ScopeAPI } from "@arcote.tech/arc-react";
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface PlatformModelHooks {
|
|
6
|
+
useModel: any;
|
|
7
|
+
createScope: (name: string) => ScopeAPI;
|
|
8
|
+
resetModel: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PlatformModelContext = createContext<PlatformModelHooks | null>(null);
|
|
12
|
+
|
|
13
|
+
export function PlatformModelProvider({
|
|
14
|
+
hooks,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
hooks: PlatformModelHooks;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<PlatformModelContext.Provider value={hooks}>
|
|
22
|
+
{children}
|
|
23
|
+
</PlatformModelContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function usePlatformModel(): PlatformModelHooks {
|
|
28
|
+
const ctx = useContext(PlatformModelContext);
|
|
29
|
+
if (!ctx)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"usePlatformModel must be used within PlatformModelProvider",
|
|
32
|
+
);
|
|
33
|
+
return ctx;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function usePlatformScope<C extends ArcContextAny>(
|
|
37
|
+
_ctx: C,
|
|
38
|
+
name: string,
|
|
39
|
+
): ScopeAPI<C> {
|
|
40
|
+
return usePlatformModel().createScope(name) as unknown as ScopeAPI<C>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function usePlatformResetModel() {
|
|
44
|
+
return usePlatformModel().resetModel;
|
|
45
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ArcFragment,
|
|
3
|
+
ArcLayoutComponent,
|
|
4
|
+
ArcModule,
|
|
5
|
+
ContextElementFragment,
|
|
6
|
+
PageFragment,
|
|
7
|
+
SlotFragment,
|
|
8
|
+
SlotId,
|
|
9
|
+
WrapperFragment,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Registry — globalny rejestr modułów i ich fragmentów
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const modules: Map<string, ArcModule> = new Map();
|
|
17
|
+
const listeners: Set<() => void> = new Set();
|
|
18
|
+
|
|
19
|
+
function notify(): void {
|
|
20
|
+
for (const listener of listeners) listener();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function subscribe(listener: () => void): () => void {
|
|
24
|
+
listeners.add(listener);
|
|
25
|
+
return () => listeners.delete(listener);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Register module. Overwrites if already registered (needed for reload). */
|
|
29
|
+
export function registerModule(mod: ArcModule): void {
|
|
30
|
+
modules.set(mod.id, mod);
|
|
31
|
+
notify();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function unregisterModule(moduleId: string): void {
|
|
35
|
+
modules.delete(moduleId);
|
|
36
|
+
notify();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getModule(moduleId: string): ArcModule | undefined {
|
|
40
|
+
return modules.get(moduleId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getAllModules(): ArcModule[] {
|
|
44
|
+
return [...modules.values()];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Context store — dynamiczny context rejestrowany przez pakiety
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
let currentContext: any = null;
|
|
52
|
+
const contextListeners: Set<() => void> = new Set();
|
|
53
|
+
|
|
54
|
+
function notifyContext(): void {
|
|
55
|
+
for (const listener of contextListeners) listener();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Set the active Arc context (called by arc() or module().build()).
|
|
59
|
+
* If a context is already set, merges new elements into it
|
|
60
|
+
* while preserving the ArcContext instance (with .get() method). */
|
|
61
|
+
export function setContext(ctx: any): void {
|
|
62
|
+
if (currentContext && ctx && currentContext !== ctx) {
|
|
63
|
+
// Merge: append new elements that aren't already present
|
|
64
|
+
const existingNames = new Set(
|
|
65
|
+
currentContext.elements.map((e: any) => e.name ?? e.id ?? e),
|
|
66
|
+
);
|
|
67
|
+
const newElements = ctx.elements.filter(
|
|
68
|
+
(e: any) => !existingNames.has(e.name ?? e.id ?? e),
|
|
69
|
+
);
|
|
70
|
+
if (newElements.length > 0) {
|
|
71
|
+
// Rebuild using the context constructor to preserve .get() and other methods
|
|
72
|
+
const allElements = [...currentContext.elements, ...newElements];
|
|
73
|
+
const Ctor = currentContext.constructor;
|
|
74
|
+
if (typeof Ctor === "function" && Ctor !== Object) {
|
|
75
|
+
currentContext = new Ctor(allElements);
|
|
76
|
+
} else {
|
|
77
|
+
// Fallback: create context-like object with get() method
|
|
78
|
+
const elementMap = new Map(
|
|
79
|
+
allElements.map((e: any) => [e.name, e]),
|
|
80
|
+
);
|
|
81
|
+
currentContext = {
|
|
82
|
+
elements: allElements,
|
|
83
|
+
elementMap,
|
|
84
|
+
get(name: string) {
|
|
85
|
+
return elementMap.get(name);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
currentContext = ctx;
|
|
92
|
+
}
|
|
93
|
+
notifyContext();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Get the active Arc context. */
|
|
97
|
+
export function getContext<T = any>(): T {
|
|
98
|
+
return currentContext as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Subscribe to context changes. */
|
|
102
|
+
export function subscribeContext(listener: () => void): () => void {
|
|
103
|
+
contextListeners.add(listener);
|
|
104
|
+
return () => contextListeners.delete(listener);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Default layout — ustawiany przez shell, nadpisywalny per-page
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
let defaultLayout: ArcLayoutComponent | null = null;
|
|
112
|
+
|
|
113
|
+
/** Set the default layout component (used when page doesn't specify its own). */
|
|
114
|
+
export function setDefaultLayout(layout: ArcLayoutComponent): void {
|
|
115
|
+
defaultLayout = layout;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Get the default layout component. */
|
|
119
|
+
export function getDefaultLayout(): ArcLayoutComponent | null {
|
|
120
|
+
return defaultLayout;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Variant overrides — moduły rejestrują nadpisania wariantów DS
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
let variantOverrides: Record<
|
|
128
|
+
string,
|
|
129
|
+
Record<string, Record<string, string>>
|
|
130
|
+
> = {};
|
|
131
|
+
|
|
132
|
+
/** Register variant overrides (merges with existing). */
|
|
133
|
+
export function setVariantOverrides(
|
|
134
|
+
overrides: Record<string, Record<string, Record<string, string>>>,
|
|
135
|
+
): void {
|
|
136
|
+
for (const [component, variants] of Object.entries(overrides)) {
|
|
137
|
+
variantOverrides[component] = {
|
|
138
|
+
...variantOverrides[component],
|
|
139
|
+
...variants,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
notify();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Get all registered variant overrides. */
|
|
146
|
+
export function getVariantOverrides(): Record<
|
|
147
|
+
string,
|
|
148
|
+
Record<string, Record<string, string>>
|
|
149
|
+
> {
|
|
150
|
+
return variantOverrides;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Queries
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
export function getAllFragments(): ArcFragment[] {
|
|
158
|
+
return getAllModules().flatMap((m) => m.fragments);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getSlotFragments(slotId: SlotId | string): SlotFragment[] {
|
|
162
|
+
const fragments = getAllFragments().filter(
|
|
163
|
+
(f): f is SlotFragment => f.is("slot") && (f as SlotFragment).slotId === slotId,
|
|
164
|
+
);
|
|
165
|
+
return sortByOrdering(fragments);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getPageFragments(): PageFragment[] {
|
|
169
|
+
return getAllFragments().filter((f): f is PageFragment => f.is("page"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getContextElementFragments(): ContextElementFragment[] {
|
|
173
|
+
return getAllFragments().filter(
|
|
174
|
+
(f): f is ContextElementFragment => f.is("context-element"),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getPageByPath(path: string): PageFragment | undefined {
|
|
179
|
+
return getPageFragments().find((p) => p.path === path);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getWrapperFragments(): WrapperFragment[] {
|
|
183
|
+
const fragments = getAllFragments().filter(
|
|
184
|
+
(f): f is WrapperFragment => f.is("wrapper"),
|
|
185
|
+
);
|
|
186
|
+
return sortByOrdering(fragments);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Clear all modules (but keep context). Used for reload. */
|
|
190
|
+
export function clearModules(): void {
|
|
191
|
+
modules.clear();
|
|
192
|
+
notify();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Clear everything — modules + context + layout. */
|
|
196
|
+
export function clearRegistry(): void {
|
|
197
|
+
modules.clear();
|
|
198
|
+
currentContext = null;
|
|
199
|
+
defaultLayout = null;
|
|
200
|
+
variantOverrides = {};
|
|
201
|
+
notify();
|
|
202
|
+
notifyContext();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Ordering sort
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
interface HasOrdering {
|
|
210
|
+
id: string;
|
|
211
|
+
ordering: { order?: number; after?: string; before?: string };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sortByOrdering<T extends HasOrdering>(fragments: T[]): T[] {
|
|
215
|
+
const byId = new Map(fragments.map((f) => [f.id, f]));
|
|
216
|
+
|
|
217
|
+
return [...fragments].sort((a, b) => {
|
|
218
|
+
const orderA = a.ordering.order ?? 0;
|
|
219
|
+
const orderB = b.ordering.order ?? 0;
|
|
220
|
+
|
|
221
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
222
|
+
|
|
223
|
+
if (a.ordering.after && byId.has(a.ordering.after)) {
|
|
224
|
+
if (a.ordering.after === b.id) return 1;
|
|
225
|
+
}
|
|
226
|
+
if (a.ordering.before && byId.has(a.ordering.before)) {
|
|
227
|
+
if (a.ordering.before === b.id) return -1;
|
|
228
|
+
}
|
|
229
|
+
if (b.ordering.after && byId.has(b.ordering.after)) {
|
|
230
|
+
if (b.ordering.after === a.id) return -1;
|
|
231
|
+
}
|
|
232
|
+
if (b.ordering.before && byId.has(b.ordering.before)) {
|
|
233
|
+
if (b.ordering.before === a.id) return 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return 0;
|
|
237
|
+
});
|
|
238
|
+
}
|
package/src/router.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router — re-exports core router from design-system.
|
|
3
|
+
* Platform adds useModuleNavigate (typed per-module navigation).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ArcRouterProvider,
|
|
8
|
+
useArcNavigate,
|
|
9
|
+
useArcRoute,
|
|
10
|
+
useArcParams,
|
|
11
|
+
matchRoutePath,
|
|
12
|
+
hasRouteParams,
|
|
13
|
+
} from "@arcote.tech/arc-ds";
|
|
14
|
+
|
|
15
|
+
import { useArcNavigate } from "@arcote.tech/arc-ds";
|
|
16
|
+
import type { ArcModule, PublicPaths } from "./types";
|
|
17
|
+
|
|
18
|
+
export function useModuleNavigate<T extends ArcModule>(
|
|
19
|
+
_module: T,
|
|
20
|
+
): (path: PublicPaths<T>) => void {
|
|
21
|
+
const navigate = useArcNavigate();
|
|
22
|
+
return navigate as (path: PublicPaths<T>) => void;
|
|
23
|
+
}
|
package/src/theme.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
type Theme = "light" | "dark" | "system";
|
|
11
|
+
|
|
12
|
+
interface ThemeState {
|
|
13
|
+
theme: Theme;
|
|
14
|
+
resolved: "light" | "dark";
|
|
15
|
+
setTheme: (theme: Theme) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ThemeContext = createContext<ThemeState | null>(null);
|
|
19
|
+
|
|
20
|
+
function getSystemTheme(): "light" | "dark" {
|
|
21
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
22
|
+
? "dark"
|
|
23
|
+
: "light";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolve(theme: Theme): "light" | "dark" {
|
|
27
|
+
return theme === "system" ? getSystemTheme() : theme;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
31
|
+
const [theme, setThemeRaw] = useState<Theme>(() => {
|
|
32
|
+
return (localStorage.getItem("arc-theme") as Theme) ?? "system";
|
|
33
|
+
});
|
|
34
|
+
const [resolved, setResolved] = useState<"light" | "dark">(() =>
|
|
35
|
+
resolve(theme),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const setTheme = useCallback((t: Theme) => {
|
|
39
|
+
localStorage.setItem("arc-theme", t);
|
|
40
|
+
setThemeRaw(t);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const r = resolve(theme);
|
|
45
|
+
setResolved(r);
|
|
46
|
+
document.documentElement.classList.toggle("dark", r === "dark");
|
|
47
|
+
}, [theme]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (theme !== "system") return;
|
|
51
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
52
|
+
const handler = () => {
|
|
53
|
+
const r = getSystemTheme();
|
|
54
|
+
setResolved(r);
|
|
55
|
+
document.documentElement.classList.toggle("dark", r === "dark");
|
|
56
|
+
};
|
|
57
|
+
mq.addEventListener("change", handler);
|
|
58
|
+
return () => mq.removeEventListener("change", handler);
|
|
59
|
+
}, [theme]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ThemeContext.Provider value={{ theme, resolved, setTheme }}>
|
|
63
|
+
{children}
|
|
64
|
+
</ThemeContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useTheme(): ThemeState {
|
|
69
|
+
const ctx = useContext(ThemeContext);
|
|
70
|
+
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
|
|
71
|
+
return ctx;
|
|
72
|
+
}
|