@object-ui/react 0.5.0 → 2.0.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.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/LazyPluginLoader.d.ts +44 -0
  4. package/dist/LazyPluginLoader.js +92 -3
  5. package/dist/components/form/FieldFactory.js +44 -40
  6. package/dist/components/form/FormRenderer.js +45 -3
  7. package/dist/context/ActionContext.d.ts +76 -0
  8. package/dist/context/ActionContext.js +139 -0
  9. package/dist/context/ThemeContext.d.ts +94 -0
  10. package/dist/context/ThemeContext.js +215 -0
  11. package/dist/context/index.d.ts +2 -0
  12. package/dist/context/index.js +2 -0
  13. package/dist/hooks/index.d.ts +5 -0
  14. package/dist/hooks/index.js +5 -0
  15. package/dist/hooks/useActionRunner.d.ts +21 -7
  16. package/dist/hooks/useActionRunner.js +27 -7
  17. package/dist/hooks/useDiscovery.d.ts +78 -0
  18. package/dist/hooks/useDiscovery.js +96 -0
  19. package/dist/hooks/useDynamicApp.d.ts +70 -0
  20. package/dist/hooks/useDynamicApp.js +92 -0
  21. package/dist/hooks/useNavigationOverlay.d.ts +78 -0
  22. package/dist/hooks/useNavigationOverlay.js +112 -0
  23. package/dist/hooks/usePageVariables.d.ts +67 -0
  24. package/dist/hooks/usePageVariables.js +109 -0
  25. package/dist/hooks/useViewData.d.ts +73 -0
  26. package/dist/hooks/useViewData.js +135 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +2 -0
  29. package/dist/utils/i18n.d.ts +10 -0
  30. package/dist/utils/i18n.js +12 -0
  31. package/package.json +6 -4
  32. package/src/LazyPluginLoader.tsx +129 -7
  33. package/src/__tests__/LazyPluginLoader.test.tsx +97 -1
  34. package/src/__tests__/ThemeProvider.test.tsx +451 -0
  35. package/src/__tests__/useNavigationOverlay.test.ts +416 -0
  36. package/src/__tests__/usePageVariables.test.tsx +334 -0
  37. package/src/__tests__/useViewData.test.tsx +422 -0
  38. package/src/components/form/FieldFactory.tsx +51 -46
  39. package/src/components/form/FormRenderer.test.tsx +145 -0
  40. package/src/components/form/FormRenderer.tsx +52 -3
  41. package/src/context/ActionContext.tsx +213 -0
  42. package/src/context/ThemeContext.tsx +325 -0
  43. package/src/context/__tests__/ActionContext.test.tsx +174 -0
  44. package/src/context/index.ts +2 -0
  45. package/src/hooks/__tests__/useActionRunner.test.ts +153 -0
  46. package/src/hooks/__tests__/useDiscovery.test.tsx +149 -0
  47. package/src/hooks/__tests__/useDynamicApp.test.ts +161 -0
  48. package/src/hooks/__tests__/useExpression.test.ts +86 -0
  49. package/src/hooks/index.ts +5 -0
  50. package/src/hooks/useActionRunner.ts +49 -11
  51. package/src/hooks/useDiscovery.ts +141 -0
  52. package/src/hooks/useDynamicApp.ts +142 -0
  53. package/src/hooks/useNavigationOverlay.ts +180 -0
  54. package/src/hooks/usePageVariables.tsx +157 -0
  55. package/src/hooks/useViewData.ts +201 -0
  56. package/src/index.ts +21 -0
  57. package/src/utils/i18n.ts +10 -0
  58. package/tsconfig.json +1 -0
  59. package/tsconfig.tsbuildinfo +1 -1
  60. package/dist/__tests__/LazyPluginLoader.test.d.ts +0 -8
  61. package/dist/__tests__/LazyPluginLoader.test.js +0 -49
  62. package/dist/components/form/FormRenderer.test.d.ts +0 -8
  63. package/dist/components/form/FormRenderer.test.js +0 -534
  64. package/dist/index.test.d.ts +0 -8
  65. package/dist/index.test.js +0 -13
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useState, useEffect, useContext } from 'react';
9
+ import { SchemaRendererContext } from '../context/SchemaRendererContext';
10
+ /**
11
+ * Hook to access discovery information from the ObjectStack server.
12
+ *
13
+ * This hook retrieves server capabilities and service status, which can be used
14
+ * to conditionally enable/disable features based on server configuration.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * function App() {
19
+ * const { discovery, isLoading } = useDiscovery();
20
+ *
21
+ * if (isLoading) {
22
+ * return <LoadingScreen />;
23
+ * }
24
+ *
25
+ * // Check if auth is enabled on the server
26
+ * const authEnabled = discovery?.services?.auth?.enabled ?? true;
27
+ *
28
+ * return (
29
+ * <div>
30
+ * {authEnabled ? <AuthProvider>...</AuthProvider> : <App />}
31
+ * </div>
32
+ * );
33
+ * }
34
+ * ```
35
+ *
36
+ * @returns Discovery information and loading state
37
+ */
38
+ export function useDiscovery() {
39
+ const context = useContext(SchemaRendererContext);
40
+ const dataSource = context?.dataSource;
41
+ const [discovery, setDiscovery] = useState(null);
42
+ const [isLoading, setIsLoading] = useState(true);
43
+ const [error, setError] = useState(null);
44
+ useEffect(() => {
45
+ let cancelled = false;
46
+ async function fetchDiscovery() {
47
+ if (!dataSource) {
48
+ setIsLoading(false);
49
+ return;
50
+ }
51
+ try {
52
+ setIsLoading(true);
53
+ // Check if dataSource has getDiscovery method
54
+ if (typeof dataSource.getDiscovery === 'function') {
55
+ const discoveryData = await dataSource.getDiscovery();
56
+ if (!cancelled) {
57
+ setDiscovery(discoveryData);
58
+ setError(null);
59
+ }
60
+ }
61
+ else {
62
+ // DataSource doesn't support discovery
63
+ if (!cancelled) {
64
+ setDiscovery(null);
65
+ setError(null);
66
+ }
67
+ }
68
+ }
69
+ catch (err) {
70
+ if (!cancelled) {
71
+ setError(err instanceof Error ? err : new Error('Failed to fetch discovery'));
72
+ setDiscovery(null);
73
+ }
74
+ }
75
+ finally {
76
+ if (!cancelled) {
77
+ setIsLoading(false);
78
+ }
79
+ }
80
+ }
81
+ fetchDiscovery();
82
+ return () => {
83
+ cancelled = true;
84
+ };
85
+ }, [dataSource]);
86
+ return {
87
+ discovery,
88
+ isLoading,
89
+ error,
90
+ /**
91
+ * Check if authentication is enabled on the server.
92
+ * Defaults to true if discovery data is not available.
93
+ */
94
+ isAuthEnabled: discovery?.services?.auth?.enabled ?? true,
95
+ };
96
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Options for dynamic app configuration loading.
10
+ */
11
+ export interface DynamicAppOptions<TAppConfig = unknown> {
12
+ /** Application identifier to load */
13
+ appId: string;
14
+ /**
15
+ * Static (fallback) configuration used when the server does not
16
+ * provide an app definition or is unreachable.
17
+ */
18
+ staticConfig?: TAppConfig;
19
+ /**
20
+ * Data source adapter with `getApp` and `invalidateCache` methods.
21
+ * Typically an ObjectStackAdapter instance.
22
+ */
23
+ adapter?: {
24
+ getApp: (appId: string) => Promise<unknown | null>;
25
+ invalidateCache?: (key?: string) => void;
26
+ getObjectSchema?: (objectName: string) => Promise<unknown>;
27
+ };
28
+ /** Whether to attempt loading from the server (default: true) */
29
+ enabled?: boolean;
30
+ }
31
+ /**
32
+ * Result returned by useDynamicApp.
33
+ */
34
+ export interface DynamicAppResult<TAppConfig = unknown> {
35
+ /** The resolved app configuration (server or static fallback) */
36
+ config: TAppConfig | null;
37
+ /** Whether the configuration is currently loading */
38
+ isLoading: boolean;
39
+ /** Error from the most recent load attempt */
40
+ error: Error | null;
41
+ /** Whether the config was loaded from the server (vs static fallback) */
42
+ isServerConfig: boolean;
43
+ /** Re-fetch the app configuration from the server, invalidating cache */
44
+ refresh: () => Promise<void>;
45
+ }
46
+ /**
47
+ * React hook for dynamic app configuration loading.
48
+ *
49
+ * Fetches app configuration from the server via `adapter.getApp(appId)`,
50
+ * falling back to `staticConfig` when the server is unavailable.
51
+ * Supports cache-based hot-reload via the `refresh()` callback.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * import { useDynamicApp } from '@object-ui/react';
56
+ * import staticAppConfig from '../config/app.json';
57
+ *
58
+ * function App() {
59
+ * const { config, isLoading, refresh } = useDynamicApp({
60
+ * appId: 'crm',
61
+ * staticConfig: staticAppConfig,
62
+ * adapter: dataSource,
63
+ * });
64
+ *
65
+ * if (isLoading) return <LoadingScreen />;
66
+ * return <Console config={config} onRefresh={refresh} />;
67
+ * }
68
+ * ```
69
+ */
70
+ export declare function useDynamicApp<TAppConfig = unknown>(options: DynamicAppOptions<TAppConfig>): DynamicAppResult<TAppConfig>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useState, useEffect, useCallback, useRef } from 'react';
9
+ /**
10
+ * React hook for dynamic app configuration loading.
11
+ *
12
+ * Fetches app configuration from the server via `adapter.getApp(appId)`,
13
+ * falling back to `staticConfig` when the server is unavailable.
14
+ * Supports cache-based hot-reload via the `refresh()` callback.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import { useDynamicApp } from '@object-ui/react';
19
+ * import staticAppConfig from '../config/app.json';
20
+ *
21
+ * function App() {
22
+ * const { config, isLoading, refresh } = useDynamicApp({
23
+ * appId: 'crm',
24
+ * staticConfig: staticAppConfig,
25
+ * adapter: dataSource,
26
+ * });
27
+ *
28
+ * if (isLoading) return <LoadingScreen />;
29
+ * return <Console config={config} onRefresh={refresh} />;
30
+ * }
31
+ * ```
32
+ */
33
+ export function useDynamicApp(options) {
34
+ const { appId, staticConfig, adapter, enabled = true } = options;
35
+ const [config, setConfig] = useState(staticConfig ?? null);
36
+ const [isLoading, setIsLoading] = useState(enabled && !!adapter);
37
+ const [error, setError] = useState(null);
38
+ const [isServerConfig, setIsServerConfig] = useState(false);
39
+ // Ref to track unmount / stale requests
40
+ const mountedRef = useRef(true);
41
+ useEffect(() => {
42
+ mountedRef.current = true;
43
+ return () => { mountedRef.current = false; };
44
+ }, []);
45
+ const loadConfig = useCallback(async () => {
46
+ if (!adapter || !enabled) {
47
+ setConfig(staticConfig ?? null);
48
+ setIsServerConfig(false);
49
+ setIsLoading(false);
50
+ return;
51
+ }
52
+ setIsLoading(true);
53
+ setError(null);
54
+ try {
55
+ const serverConfig = await adapter.getApp(appId);
56
+ if (!mountedRef.current)
57
+ return;
58
+ if (serverConfig) {
59
+ setConfig(serverConfig);
60
+ setIsServerConfig(true);
61
+ }
62
+ else {
63
+ // Server returned null — fall back to static config
64
+ setConfig(staticConfig ?? null);
65
+ setIsServerConfig(false);
66
+ }
67
+ }
68
+ catch (err) {
69
+ if (!mountedRef.current)
70
+ return;
71
+ setError(err instanceof Error ? err : new Error(String(err)));
72
+ // Fall back to static config on error
73
+ setConfig(staticConfig ?? null);
74
+ setIsServerConfig(false);
75
+ }
76
+ finally {
77
+ if (mountedRef.current) {
78
+ setIsLoading(false);
79
+ }
80
+ }
81
+ }, [adapter, appId, staticConfig, enabled]);
82
+ // Load on mount and when appId changes
83
+ useEffect(() => {
84
+ loadConfig();
85
+ }, [loadConfig]);
86
+ const refresh = useCallback(async () => {
87
+ // Invalidate cache before re-fetching
88
+ adapter?.invalidateCache?.(`app:${appId}`);
89
+ await loadConfig();
90
+ }, [adapter, appId, loadConfig]);
91
+ return { config, isLoading, error, isServerConfig, refresh };
92
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Inline ViewNavigationConfig to avoid importing from @object-ui/types
10
+ * (which may not be a direct dependency of @object-ui/react).
11
+ * Mirrors the canonical definition in @object-ui/types/objectql.ts.
12
+ */
13
+ export interface NavigationConfig {
14
+ mode: 'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none';
15
+ view?: string;
16
+ preventNavigation?: boolean;
17
+ openNewTab?: boolean;
18
+ width?: string | number;
19
+ }
20
+ export type NavigationMode = NavigationConfig['mode'];
21
+ export interface UseNavigationOverlayOptions {
22
+ /** The navigation configuration from the schema */
23
+ navigation?: NavigationConfig;
24
+ /** Object name — used to build default URLs for page/new_window modes */
25
+ objectName?: string;
26
+ /** External onNavigate callback (e.g., from ActionProvider or parent) */
27
+ onNavigate?: (recordId: string | number, action?: string) => void;
28
+ /** External onRowClick callback — if set, takes full priority */
29
+ onRowClick?: (record: Record<string, unknown>) => void;
30
+ }
31
+ export interface NavigationOverlayState {
32
+ /** Whether the overlay (drawer/modal/split/popover) is open */
33
+ isOpen: boolean;
34
+ /** The record that triggered the navigation */
35
+ selectedRecord: Record<string, unknown> | null;
36
+ /** The resolved navigation mode */
37
+ mode: NavigationMode;
38
+ /** Close the overlay */
39
+ close: () => void;
40
+ /** Open the overlay with a specific record */
41
+ open: (record: Record<string, unknown>) => void;
42
+ /** Set the open state (for controlled Sheet/Dialog `onOpenChange`) */
43
+ setIsOpen: (open: boolean) => void;
44
+ /** The click handler to attach to rows/items */
45
+ handleClick: (record: Record<string, unknown>) => void;
46
+ /** The width from NavigationConfig (for drawer/modal/split sizing) */
47
+ width: string | number | undefined;
48
+ /** Whether navigation is an overlay mode (drawer/modal/split/popover) */
49
+ isOverlay: boolean;
50
+ }
51
+ /**
52
+ * Hook for NavigationConfig-driven navigation overlay.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const { handleClick, isOpen, selectedRecord, mode, close, width } =
57
+ * useNavigationOverlay({
58
+ * navigation: schema.navigation,
59
+ * objectName: schema.objectName,
60
+ * onNavigate: schema.onNavigate,
61
+ * onRowClick: props.onRowClick,
62
+ * });
63
+ *
64
+ * return (
65
+ * <>
66
+ * <DataTable onRowClick={handleClick} ... />
67
+ * {isOpen && mode === 'drawer' && (
68
+ * <Sheet open onOpenChange={() => close()}>
69
+ * <SheetContent style={{ maxWidth: width }}>
70
+ * <RecordDetail record={selectedRecord} />
71
+ * </SheetContent>
72
+ * </Sheet>
73
+ * )}
74
+ * </>
75
+ * );
76
+ * ```
77
+ */
78
+ export declare function useNavigationOverlay(options: UseNavigationOverlayOptions): NavigationOverlayState;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * useNavigationOverlay
10
+ *
11
+ * A reusable hook for handling NavigationConfig-driven row/item click behavior.
12
+ * Manages overlay state (drawer/modal/split/popover) and generates click handlers
13
+ * that respect the ViewNavigationConfig specification.
14
+ *
15
+ * Used by plugin-grid, plugin-list, plugin-detail and any component that needs
16
+ * NavigationConfig support.
17
+ */
18
+ import { useState, useCallback, useMemo } from 'react';
19
+ /**
20
+ * Hook for NavigationConfig-driven navigation overlay.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const { handleClick, isOpen, selectedRecord, mode, close, width } =
25
+ * useNavigationOverlay({
26
+ * navigation: schema.navigation,
27
+ * objectName: schema.objectName,
28
+ * onNavigate: schema.onNavigate,
29
+ * onRowClick: props.onRowClick,
30
+ * });
31
+ *
32
+ * return (
33
+ * <>
34
+ * <DataTable onRowClick={handleClick} ... />
35
+ * {isOpen && mode === 'drawer' && (
36
+ * <Sheet open onOpenChange={() => close()}>
37
+ * <SheetContent style={{ maxWidth: width }}>
38
+ * <RecordDetail record={selectedRecord} />
39
+ * </SheetContent>
40
+ * </Sheet>
41
+ * )}
42
+ * </>
43
+ * );
44
+ * ```
45
+ */
46
+ export function useNavigationOverlay(options) {
47
+ const { navigation, objectName, onNavigate, onRowClick } = options;
48
+ const [isOpen, setIsOpen] = useState(false);
49
+ const [selectedRecord, setSelectedRecord] = useState(null);
50
+ const mode = navigation?.mode ?? 'page';
51
+ const width = navigation?.width;
52
+ const isOverlay = mode === 'drawer' || mode === 'modal' || mode === 'split' || mode === 'popover';
53
+ const close = useCallback(() => {
54
+ setIsOpen(false);
55
+ setSelectedRecord(null);
56
+ }, []);
57
+ const open = useCallback((record) => {
58
+ setSelectedRecord(record);
59
+ setIsOpen(true);
60
+ }, []);
61
+ const handleClick = useCallback((record) => {
62
+ // External onRowClick takes full priority
63
+ if (onRowClick) {
64
+ onRowClick(record);
65
+ return;
66
+ }
67
+ // No navigation config — default to page navigation
68
+ if (!navigation) {
69
+ const recordId = record._id || record.id;
70
+ if (onNavigate && recordId != null) {
71
+ onNavigate(recordId, 'view');
72
+ }
73
+ return;
74
+ }
75
+ // 'none' or preventNavigation — do nothing
76
+ if (mode === 'none' || navigation.preventNavigation) {
77
+ return;
78
+ }
79
+ // new_window / openNewTab — open in new browser tab
80
+ if (mode === 'new_window' || navigation.openNewTab) {
81
+ const recordId = record._id || record.id;
82
+ const url = objectName ? `/${objectName}/${recordId}` : `/${recordId}`;
83
+ window.open(url, '_blank');
84
+ return;
85
+ }
86
+ // page — delegate to onNavigate callback
87
+ if (mode === 'page') {
88
+ const recordId = record._id || record.id;
89
+ if (onNavigate && recordId != null) {
90
+ onNavigate(recordId, 'view');
91
+ }
92
+ return;
93
+ }
94
+ // Overlay modes: drawer, modal, split, popover
95
+ if (isOverlay) {
96
+ setSelectedRecord(record);
97
+ setIsOpen(true);
98
+ return;
99
+ }
100
+ }, [onRowClick, navigation, mode, objectName, onNavigate, isOverlay]);
101
+ return useMemo(() => ({
102
+ isOpen,
103
+ selectedRecord,
104
+ mode,
105
+ close,
106
+ open,
107
+ setIsOpen,
108
+ handleClick,
109
+ width,
110
+ isOverlay,
111
+ }), [isOpen, selectedRecord, mode, close, open, handleClick, width, isOverlay]);
112
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React from 'react';
9
+ import type { PageVariable } from '@object-ui/types';
10
+ /**
11
+ * Page variables context value.
12
+ * Provides access to page-level state variables.
13
+ */
14
+ export interface PageVariablesContextValue {
15
+ /** Current variable values */
16
+ variables: Record<string, any>;
17
+ /** Set a single variable value */
18
+ setVariable: (name: string, value: any) => void;
19
+ /** Set multiple variable values at once */
20
+ setVariables: (updates: Record<string, any>) => void;
21
+ /** Reset all variables to their default values */
22
+ resetVariables: () => void;
23
+ }
24
+ /**
25
+ * Props for PageVariablesProvider
26
+ */
27
+ export interface PageVariablesProviderProps {
28
+ /** Variable definitions from PageSchema.variables */
29
+ definitions?: PageVariable[];
30
+ /** Child components */
31
+ children: React.ReactNode;
32
+ }
33
+ /**
34
+ * PageVariablesProvider — Provides page-level state variables to the component tree.
35
+ *
36
+ * Initializes variables from their definitions and provides read/write access
37
+ * to all child components via the usePageVariables hook.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <PageVariablesProvider definitions={[
42
+ * { name: 'selectedId', type: 'string', defaultValue: '' },
43
+ * { name: 'count', type: 'number', defaultValue: 0 },
44
+ * ]}>
45
+ * <MyComponents />
46
+ * </PageVariablesProvider>
47
+ * ```
48
+ */
49
+ export declare const PageVariablesProvider: React.FC<PageVariablesProviderProps>;
50
+ /**
51
+ * Hook to access page-level variables.
52
+ *
53
+ * Returns the current variable values and setter functions.
54
+ * Returns a no-op fallback if used outside a PageVariablesProvider.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * const { variables, setVariable } = usePageVariables();
59
+ * const userId = variables.selectedId;
60
+ * setVariable('selectedId', '123');
61
+ * ```
62
+ */
63
+ export declare function usePageVariables(): PageVariablesContextValue;
64
+ /**
65
+ * Hook to check if a PageVariablesProvider is available.
66
+ */
67
+ export declare function useHasPageVariables(): boolean;
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import { createContext, useContext, useState, useCallback, useMemo } from 'react';
10
+ const PageVariablesContext = createContext(null);
11
+ /**
12
+ * Initialize page variables from their definitions.
13
+ * Sets each variable to its defaultValue or type-appropriate default.
14
+ */
15
+ function initializeVariables(definitions) {
16
+ if (!definitions || definitions.length === 0)
17
+ return {};
18
+ const initial = {};
19
+ for (const def of definitions) {
20
+ if (def.defaultValue !== undefined) {
21
+ initial[def.name] = def.defaultValue;
22
+ }
23
+ else {
24
+ // Type-appropriate defaults
25
+ switch (def.type) {
26
+ case 'number':
27
+ initial[def.name] = 0;
28
+ break;
29
+ case 'boolean':
30
+ initial[def.name] = false;
31
+ break;
32
+ case 'object':
33
+ initial[def.name] = {};
34
+ break;
35
+ case 'array':
36
+ initial[def.name] = [];
37
+ break;
38
+ case 'string':
39
+ default:
40
+ initial[def.name] = '';
41
+ break;
42
+ }
43
+ }
44
+ }
45
+ return initial;
46
+ }
47
+ /**
48
+ * PageVariablesProvider — Provides page-level state variables to the component tree.
49
+ *
50
+ * Initializes variables from their definitions and provides read/write access
51
+ * to all child components via the usePageVariables hook.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * <PageVariablesProvider definitions={[
56
+ * { name: 'selectedId', type: 'string', defaultValue: '' },
57
+ * { name: 'count', type: 'number', defaultValue: 0 },
58
+ * ]}>
59
+ * <MyComponents />
60
+ * </PageVariablesProvider>
61
+ * ```
62
+ */
63
+ export const PageVariablesProvider = ({ definitions, children, }) => {
64
+ const [variables, setVariablesState] = useState(() => initializeVariables(definitions));
65
+ const setVariable = useCallback((name, value) => {
66
+ setVariablesState((prev) => ({ ...prev, [name]: value }));
67
+ }, []);
68
+ const setVariables = useCallback((updates) => {
69
+ setVariablesState((prev) => ({ ...prev, ...updates }));
70
+ }, []);
71
+ const resetVariables = useCallback(() => {
72
+ setVariablesState(initializeVariables(definitions));
73
+ }, [definitions]);
74
+ const value = useMemo(() => ({ variables, setVariable, setVariables, resetVariables }), [variables, setVariable, setVariables, resetVariables]);
75
+ return (_jsx(PageVariablesContext.Provider, { value: value, children: children }));
76
+ };
77
+ PageVariablesProvider.displayName = 'PageVariablesProvider';
78
+ /**
79
+ * Hook to access page-level variables.
80
+ *
81
+ * Returns the current variable values and setter functions.
82
+ * Returns a no-op fallback if used outside a PageVariablesProvider.
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * const { variables, setVariable } = usePageVariables();
87
+ * const userId = variables.selectedId;
88
+ * setVariable('selectedId', '123');
89
+ * ```
90
+ */
91
+ export function usePageVariables() {
92
+ const ctx = useContext(PageVariablesContext);
93
+ if (!ctx) {
94
+ // Graceful fallback — allows components to work outside a Page
95
+ return {
96
+ variables: {},
97
+ setVariable: () => { },
98
+ setVariables: () => { },
99
+ resetVariables: () => { },
100
+ };
101
+ }
102
+ return ctx;
103
+ }
104
+ /**
105
+ * Hook to check if a PageVariablesProvider is available.
106
+ */
107
+ export function useHasPageVariables() {
108
+ return useContext(PageVariablesContext) !== null;
109
+ }