@object-ui/react 3.3.0 → 3.3.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +21 -1
  3. package/dist/context/AppShellContext.d.ts +26 -0
  4. package/dist/context/AppShellContext.js +67 -0
  5. package/dist/context/index.d.ts +1 -7
  6. package/dist/context/index.js +1 -7
  7. package/package.json +32 -9
  8. package/.turbo/turbo-build.log +0 -4
  9. package/src/LazyPluginLoader.tsx +0 -174
  10. package/src/SchemaRenderer.tsx +0 -266
  11. package/src/__tests__/LazyPluginLoader.test.tsx +0 -176
  12. package/src/__tests__/SchemaRenderer.aria.test.tsx +0 -118
  13. package/src/__tests__/SchemaRenderer.debug.test.tsx +0 -69
  14. package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +0 -114
  15. package/src/__tests__/SchemaRenderer.expressions.test.tsx +0 -131
  16. package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +0 -132
  17. package/src/__tests__/ThemeProvider.test.tsx +0 -451
  18. package/src/__tests__/useNavigationOverlay.test.ts +0 -578
  19. package/src/__tests__/useOffline.test.ts +0 -198
  20. package/src/__tests__/usePageTransition.test.ts +0 -214
  21. package/src/__tests__/usePageVariables.test.tsx +0 -334
  22. package/src/__tests__/usePerformance.test.ts +0 -137
  23. package/src/__tests__/useViewData.test.tsx +0 -422
  24. package/src/__tests__/view-switching.test.tsx +0 -456
  25. package/src/components/form/FieldFactory.tsx +0 -516
  26. package/src/components/form/FormRenderer.test.tsx +0 -744
  27. package/src/components/form/FormRenderer.tsx +0 -258
  28. package/src/components/form/README.md +0 -352
  29. package/src/components/form/index.ts +0 -12
  30. package/src/context/ActionContext.tsx +0 -213
  31. package/src/context/DndContext.tsx +0 -260
  32. package/src/context/NotificationContext.tsx +0 -280
  33. package/src/context/SchemaRendererContext.tsx +0 -48
  34. package/src/context/ThemeContext.tsx +0 -325
  35. package/src/context/__tests__/ActionContext.test.tsx +0 -174
  36. package/src/context/__tests__/DndContext.test.tsx +0 -150
  37. package/src/context/__tests__/NotificationContext.test.tsx +0 -176
  38. package/src/context/__tests__/useDataScope.test.tsx +0 -91
  39. package/src/context/index.ts +0 -13
  40. package/src/hooks/__tests__/useActionEngine.test.ts +0 -170
  41. package/src/hooks/__tests__/useActionRunner.test.ts +0 -153
  42. package/src/hooks/__tests__/useAnimation.test.ts +0 -82
  43. package/src/hooks/__tests__/useColumnSummary.test.ts +0 -93
  44. package/src/hooks/__tests__/useCrudShortcuts.test.ts +0 -85
  45. package/src/hooks/__tests__/useDataRefresh.test.ts +0 -115
  46. package/src/hooks/__tests__/useDebugMode.test.ts +0 -83
  47. package/src/hooks/__tests__/useDensityMode.test.ts +0 -81
  48. package/src/hooks/__tests__/useDiscovery.test.tsx +0 -247
  49. package/src/hooks/__tests__/useDynamicApp.test.ts +0 -161
  50. package/src/hooks/__tests__/useETagCache.test.ts +0 -153
  51. package/src/hooks/__tests__/useExpression.test.ts +0 -86
  52. package/src/hooks/__tests__/useFocusTrap.test.ts +0 -294
  53. package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +0 -144
  54. package/src/hooks/__tests__/usePageTransition.test.ts +0 -158
  55. package/src/hooks/__tests__/usePerformanceBudget.test.ts +0 -157
  56. package/src/hooks/__tests__/useReducedMotion.test.ts +0 -104
  57. package/src/hooks/__tests__/useSchemaPersistence.test.ts +0 -220
  58. package/src/hooks/__tests__/useViewSharing.test.ts +0 -121
  59. package/src/hooks/__tests__/useViewTransition.test.ts +0 -61
  60. package/src/hooks/index.ts +0 -36
  61. package/src/hooks/useActionEngine.ts +0 -84
  62. package/src/hooks/useActionRunner.ts +0 -119
  63. package/src/hooks/useAnimation.ts +0 -117
  64. package/src/hooks/useClientNotifications.ts +0 -251
  65. package/src/hooks/useColumnSummary.ts +0 -87
  66. package/src/hooks/useCrudShortcuts.ts +0 -126
  67. package/src/hooks/useDataRefresh.ts +0 -58
  68. package/src/hooks/useDebugMode.ts +0 -95
  69. package/src/hooks/useDensityMode.ts +0 -102
  70. package/src/hooks/useDiscovery.ts +0 -168
  71. package/src/hooks/useDynamicApp.ts +0 -142
  72. package/src/hooks/useETagCache.ts +0 -404
  73. package/src/hooks/useExpression.ts +0 -56
  74. package/src/hooks/useFocusManagement.ts +0 -248
  75. package/src/hooks/useFocusTrap.ts +0 -138
  76. package/src/hooks/useGlobalUndo.ts +0 -98
  77. package/src/hooks/useKeyboardShortcuts.ts +0 -119
  78. package/src/hooks/useNavigationOverlay.ts +0 -190
  79. package/src/hooks/useOffline.ts +0 -301
  80. package/src/hooks/usePageTransition.ts +0 -187
  81. package/src/hooks/usePageVariables.tsx +0 -157
  82. package/src/hooks/usePerformance.ts +0 -246
  83. package/src/hooks/usePerformanceBudget.ts +0 -313
  84. package/src/hooks/useReducedMotion.ts +0 -53
  85. package/src/hooks/useSchemaPersistence.ts +0 -222
  86. package/src/hooks/useViewData.ts +0 -201
  87. package/src/hooks/useViewSharing.ts +0 -140
  88. package/src/hooks/useViewTransition.ts +0 -144
  89. package/src/index.ts +0 -41
  90. package/src/spec-bridge/SpecBridge.ts +0 -64
  91. package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +0 -855
  92. package/src/spec-bridge/__tests__/SpecBridge.test.ts +0 -550
  93. package/src/spec-bridge/bridges/dashboard.ts +0 -180
  94. package/src/spec-bridge/bridges/form-view.ts +0 -109
  95. package/src/spec-bridge/bridges/list-view.ts +0 -187
  96. package/src/spec-bridge/bridges/page.ts +0 -136
  97. package/src/spec-bridge/index.ts +0 -14
  98. package/src/spec-bridge/types.ts +0 -38
  99. package/src/utils/i18n.ts +0 -10
  100. package/tsconfig.json +0 -17
  101. package/tsconfig.tsbuildinfo +0 -1
  102. package/vitest.config.ts +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 3.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@3.3.1
8
+ - @object-ui/core@3.3.1
9
+ - @object-ui/i18n@3.3.1
10
+ - @object-ui/data-objectstack@3.3.1
11
+
3
12
  ## 3.3.0
4
13
 
5
14
  ### Patch Changes
package/README.md CHANGED
@@ -162,6 +162,26 @@ The `previewMode` object contains:
162
162
 
163
163
  See [full documentation](https://objectui.org/api/react) for detailed API reference.
164
164
 
165
+ <!-- release-metadata:v3.3.0 -->
166
+
167
+ ## Compatibility
168
+
169
+ - **React:** 18.x or 19.x
170
+ - **Node.js:** ≥ 18
171
+ - **TypeScript:** ≥ 5.0 (strict mode)
172
+ - **`@objectstack/spec`:** ^3.3.0
173
+ - **`@objectstack/client`:** ^3.3.0
174
+ - **Tailwind CSS:** ≥ 3.4 (for packages with UI)
175
+
176
+ ## Links
177
+
178
+ - 📚 [Documentation](https://www.objectui.org/docs/packages/react)
179
+ - 📦 [npm package](https://www.npmjs.com/package/@object-ui/react)
180
+ - 📝 [Changelog](./CHANGELOG.md)
181
+ - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
182
+ - 🤝 [Contributing Guide](https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md)
183
+ - 🗺️ [Roadmap](https://github.com/objectstack-ai/objectui/blob/main/ROADMAP.md)
184
+
165
185
  ## License
166
186
 
167
- MIT
187
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,26 @@
1
+ import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
2
+ export declare const AdapterCtx: import("react").Context<ObjectStackAdapter<unknown> | null>;
3
+ export declare function useAdapter(): ObjectStackAdapter | null;
4
+ export interface MetadataState {
5
+ apps: any[];
6
+ objects: any[];
7
+ dashboards: any[];
8
+ reports: any[];
9
+ pages: any[];
10
+ loading: boolean;
11
+ error: Error | null;
12
+ }
13
+ export interface MetadataContextValue extends MetadataState {
14
+ refresh: (type?: string) => Promise<void>;
15
+ invalidate: (type: string, name?: string) => void;
16
+ ensureType: (type: string) => Promise<any[]>;
17
+ getItem: (type: string, name: string) => Promise<any | null>;
18
+ getItemsByType: (type: string) => any[];
19
+ }
20
+ export declare const MetadataCtx: import("react").Context<MetadataContextValue | null>;
21
+ export declare function useMetadata(): MetadataContextValue;
22
+ export declare function useMetadataItem(type: string, name: string | undefined | null): {
23
+ item: any | null;
24
+ loading: boolean;
25
+ error: Error | null;
26
+ };
@@ -0,0 +1,67 @@
1
+ import { createContext, useContext, useEffect, useState } from 'react';
2
+ // ---------------------------------------------------------------------------
3
+ // AdapterContext
4
+ // ---------------------------------------------------------------------------
5
+ export const AdapterCtx = createContext(null);
6
+ export function useAdapter() {
7
+ return useContext(AdapterCtx);
8
+ }
9
+ export const MetadataCtx = createContext(null);
10
+ export function useMetadata() {
11
+ const ctx = useContext(MetadataCtx);
12
+ if (!ctx) {
13
+ // Graceful fallback: when a consumer is rendered outside a MetadataProvider
14
+ // (common in unit tests that only need to assert on rendering), return an
15
+ // empty no-op implementation rather than crash. Production code paths
16
+ // should always wrap in <MetadataProvider>.
17
+ return {
18
+ apps: [],
19
+ objects: [],
20
+ dashboards: [],
21
+ reports: [],
22
+ pages: [],
23
+ loading: false,
24
+ error: null,
25
+ refresh: async () => { },
26
+ invalidate: () => { },
27
+ ensureType: async () => [],
28
+ getItem: async () => null,
29
+ getItemsByType: () => [],
30
+ };
31
+ }
32
+ return ctx;
33
+ }
34
+ export function useMetadataItem(type, name) {
35
+ const { getItem } = useMetadata();
36
+ const [state, setState] = useState({
37
+ item: null,
38
+ loading: !!name,
39
+ error: null,
40
+ });
41
+ useEffect(() => {
42
+ if (!name) {
43
+ setState({ item: null, loading: false, error: null });
44
+ return;
45
+ }
46
+ let cancelled = false;
47
+ setState(s => ({ ...s, loading: true, error: null }));
48
+ getItem(type, name)
49
+ .then(item => {
50
+ if (!cancelled)
51
+ setState({ item, loading: false, error: null });
52
+ })
53
+ .catch(err => {
54
+ if (!cancelled) {
55
+ setState({
56
+ item: null,
57
+ loading: false,
58
+ error: err instanceof Error ? err : new Error(String(err)),
59
+ });
60
+ }
61
+ });
62
+ return () => {
63
+ cancelled = true;
64
+ };
65
+ }, [type, name, getItem]);
66
+ return state;
67
+ }
@@ -1,12 +1,6 @@
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
1
  export * from './SchemaRendererContext';
9
2
  export * from './ActionContext';
10
3
  export * from './ThemeContext';
11
4
  export * from './NotificationContext';
12
5
  export * from './DndContext';
6
+ export * from './AppShellContext';
@@ -1,12 +1,6 @@
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
1
  export * from './SchemaRendererContext';
9
2
  export * from './ActionContext';
10
3
  export * from './ThemeContext';
11
4
  export * from './NotificationContext';
12
5
  export * from './DndContext';
6
+ export * from './AppShellContext';
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "React bindings and SchemaRenderer component for Object UI",
7
- "homepage": "https://www.objectui.org",
7
+ "homepage": "https://www.objectui.org/docs/packages/react",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/objectstack-ai/objectui.git",
10
+ "url": "git+https://github.com/objectstack-ai/objectui.git",
11
11
  "directory": "packages/react"
12
12
  },
13
13
  "bugs": {
@@ -24,11 +24,12 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@objectstack/spec": "^4.0.3",
28
- "react-hook-form": "^7.72.1",
29
- "@object-ui/core": "3.3.0",
30
- "@object-ui/i18n": "3.3.0",
31
- "@object-ui/types": "3.3.0"
27
+ "@objectstack/spec": "^4.0.4",
28
+ "react-hook-form": "^7.75.0",
29
+ "@object-ui/core": "3.3.1",
30
+ "@object-ui/data-objectstack": "3.3.1",
31
+ "@object-ui/i18n": "3.3.1",
32
+ "@object-ui/types": "3.3.1"
32
33
  },
33
34
  "peerDependencies": {
34
35
  "react": "^18.0.0 || ^19.0.0",
@@ -37,8 +38,30 @@
37
38
  "devDependencies": {
38
39
  "@types/react": "19.2.14",
39
40
  "@types/react-dom": "19.2.3",
40
- "typescript": "^6.0.2"
41
+ "typescript": "^6.0.3"
41
42
  },
43
+ "keywords": [
44
+ "objectui",
45
+ "sdui",
46
+ "schema-driven-ui",
47
+ "react",
48
+ "tailwind",
49
+ "shadcn",
50
+ "objectstack",
51
+ "renderer",
52
+ "schema-renderer",
53
+ "hooks"
54
+ ],
55
+ "author": "ObjectStack Team <team@objectstack.ai>",
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "files": [
60
+ "dist",
61
+ "README.md",
62
+ "CHANGELOG.md",
63
+ "LICENSE"
64
+ ],
42
65
  "scripts": {
43
66
  "build": "tsc",
44
67
  "test": "vitest run",
@@ -1,4 +0,0 @@
1
-
2
- > @object-ui/react@3.3.0 build /home/runner/work/objectui/objectui/packages/react
3
- > tsc
4
-
@@ -1,174 +0,0 @@
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
- import React, { lazy, Suspense, Component } from 'react';
10
-
11
- export interface LazyPluginOptions {
12
- /**
13
- * Fallback component to show while loading
14
- */
15
- fallback?: React.ReactNode;
16
- /**
17
- * Number of retry attempts on load failure (default: 2)
18
- */
19
- retries?: number;
20
- /**
21
- * Delay in ms between retries (default: 1000)
22
- */
23
- retryDelay?: number;
24
- /**
25
- * Custom error fallback component
26
- */
27
- errorFallback?: React.ComponentType<{ error: Error; retry: () => void }>;
28
- }
29
-
30
- /**
31
- * Error boundary for lazy-loaded plugins
32
- */
33
- interface ErrorBoundaryState {
34
- hasError: boolean;
35
- error: Error | null;
36
- }
37
-
38
- class PluginErrorBoundary extends Component<
39
- { fallback?: React.ComponentType<{ error: Error; retry: () => void }>; children: React.ReactNode },
40
- ErrorBoundaryState
41
- > {
42
- state: ErrorBoundaryState = { hasError: false, error: null };
43
-
44
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
45
- return { hasError: true, error };
46
- }
47
-
48
- handleRetry = () => {
49
- this.setState({ hasError: false, error: null });
50
- };
51
-
52
- render() {
53
- if (this.state.hasError && this.state.error) {
54
- const FallbackComponent = this.props.fallback;
55
- if (FallbackComponent) {
56
- return <FallbackComponent error={this.state.error} retry={this.handleRetry} />;
57
- }
58
- return null;
59
- }
60
- return this.props.children;
61
- }
62
- }
63
-
64
- /**
65
- * Create a lazy-loaded import function with retry support
66
- */
67
- function createRetryImport<P extends object>(
68
- importFn: () => Promise<{ default: React.ComponentType<P> }>,
69
- retries: number,
70
- retryDelay: number,
71
- ): () => Promise<{ default: React.ComponentType<P> }> {
72
- return () => {
73
- let attempt = 0;
74
- const tryImport = (): Promise<{ default: React.ComponentType<P> }> =>
75
- importFn().catch((err) => {
76
- attempt++;
77
- if (attempt <= retries) {
78
- return new Promise((resolve) =>
79
- setTimeout(() => resolve(tryImport()), retryDelay),
80
- );
81
- }
82
- throw err;
83
- });
84
- return tryImport();
85
- };
86
- }
87
-
88
- /**
89
- * Create a lazy-loaded plugin component with Suspense wrapper
90
- *
91
- * @param importFn - Dynamic import function that returns a module with default export
92
- * @param options - Configuration options for the lazy plugin
93
- * @returns A component that lazy loads the plugin
94
- *
95
- * @example
96
- * ```tsx
97
- * // Basic usage
98
- * const ObjectGrid = createLazyPlugin(
99
- * () => import('@object-ui/plugin-grid')
100
- * );
101
- *
102
- * // With custom fallback
103
- * const ObjectGrid = createLazyPlugin(
104
- * () => import('@object-ui/plugin-grid'),
105
- * { fallback: <div>Loading grid...</div> }
106
- * );
107
- *
108
- * // With retry and error handling
109
- * const ObjectGrid = createLazyPlugin(
110
- * () => import('@object-ui/plugin-grid'),
111
- * {
112
- * retries: 3,
113
- * errorFallback: ({ error, retry }) => (
114
- * <div>
115
- * <p>Failed to load: {error.message}</p>
116
- * <button onClick={retry}>Retry</button>
117
- * </div>
118
- * ),
119
- * }
120
- * );
121
- * ```
122
- */
123
- export function createLazyPlugin<P extends object = any>(
124
- importFn: () => Promise<{ default: React.ComponentType<P> }>,
125
- options?: LazyPluginOptions
126
- ): React.ComponentType<P> {
127
- const { retries = 2, retryDelay = 1000, errorFallback } = options || {};
128
-
129
- const retryImport = retries > 0
130
- ? createRetryImport(importFn, retries, retryDelay)
131
- : importFn;
132
-
133
- const LazyComponent = lazy(retryImport);
134
-
135
- const PluginWrapper: React.FC<P> = (props) => {
136
- const content = (
137
- <Suspense fallback={options?.fallback || null}>
138
- <LazyComponent {...props} />
139
- </Suspense>
140
- );
141
-
142
- if (errorFallback) {
143
- return (
144
- <PluginErrorBoundary fallback={errorFallback}>
145
- {content}
146
- </PluginErrorBoundary>
147
- );
148
- }
149
-
150
- return content;
151
- };
152
-
153
- return PluginWrapper;
154
- }
155
-
156
- /**
157
- * Preload a plugin module without rendering it.
158
- * Useful for preloading plugins that will be needed soon.
159
- *
160
- * @param importFn - Dynamic import function
161
- * @returns Promise that resolves when the module is loaded
162
- *
163
- * @example
164
- * ```tsx
165
- * // Preload on hover
166
- * const loadGrid = () => import('@object-ui/plugin-grid');
167
- * <button onMouseEnter={() => preloadPlugin(loadGrid)}>Show Grid</button>
168
- * ```
169
- */
170
- export function preloadPlugin<T = unknown>(
171
- importFn: () => Promise<T>,
172
- ): Promise<T> {
173
- return importFn();
174
- }
@@ -1,266 +0,0 @@
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
- import React, { forwardRef, useContext, useMemo, Component } from 'react';
10
- import {
11
- SchemaNode,
12
- ComponentRegistry,
13
- ExpressionEvaluator,
14
- isObjectUIError,
15
- type ObjectUIError,
16
- ERROR_CODES,
17
- debugLog,
18
- debugTime,
19
- debugTimeEnd,
20
- DebugCollector,
21
- } from '@object-ui/core';
22
- import { SchemaRendererContext } from './context/SchemaRendererContext';
23
- import { resolveI18nLabel } from './utils/i18n';
24
-
25
- /**
26
- * Extract AriaPropsSchema properties from a schema node and convert
27
- * them to standard HTML ARIA attributes.
28
- *
29
- * @objectstack/spec AriaPropsSchema defines:
30
- * ariaLabel: string | I18nLabel (→ aria-label)
31
- * ariaDescribedBy: string (→ aria-describedby)
32
- * role: string (→ role)
33
- */
34
- function resolveAriaProps(schema: Record<string, any>): Record<string, string | undefined> {
35
- const aria: Record<string, string | undefined> = {};
36
- if (schema.ariaLabel) {
37
- aria['aria-label'] = resolveI18nLabel(schema.ariaLabel);
38
- }
39
- if (schema.ariaDescribedBy) {
40
- aria['aria-describedby'] = schema.ariaDescribedBy;
41
- }
42
- if (schema.role) {
43
- aria['role'] = schema.role;
44
- }
45
- return aria;
46
- }
47
-
48
- /**
49
- * Per-component Error Boundary for SchemaRenderer.
50
- * Catches render errors in individual components, preventing one broken
51
- * component from crashing the entire page.
52
- */
53
- interface SchemaErrorBoundaryState {
54
- hasError: boolean;
55
- error: Error | null;
56
- }
57
-
58
- export class SchemaErrorBoundary extends Component<
59
- { componentType?: string; children: React.ReactNode },
60
- SchemaErrorBoundaryState
61
- > {
62
- state: SchemaErrorBoundaryState = { hasError: false, error: null };
63
-
64
- static getDerivedStateFromError(error: Error): SchemaErrorBoundaryState {
65
- return { hasError: true, error };
66
- }
67
-
68
- handleRetry = () => {
69
- this.setState({ hasError: false, error: null });
70
- };
71
-
72
- render() {
73
- if (this.state.hasError && this.state.error) {
74
- const error = this.state.error;
75
- const isDev = (globalThis as any).process?.env?.NODE_ENV !== 'production';
76
- const objuiError = isObjectUIError(error) ? error as ObjectUIError : null;
77
-
78
- return (
79
- <div className="p-4 border border-orange-400 rounded bg-orange-50 text-orange-700 my-2" role="alert">
80
- <p className="font-medium">
81
- Component{this.props.componentType ? ` "${this.props.componentType}"` : ''} failed to render
82
- </p>
83
- <p className="text-sm mt-1">{error.message}</p>
84
- {isDev && objuiError?.code && (
85
- <p className="text-xs mt-1 text-orange-500">
86
- Error code: {objuiError.code}
87
- {objuiError.details?.suggestion ? (
88
- <span className="block mt-0.5">💡 {String(objuiError.details.suggestion)}</span>
89
- ) : null}
90
- </p>
91
- )}
92
- <button
93
- onClick={this.handleRetry}
94
- className="mt-2 text-sm underline hover:no-underline"
95
- >
96
- Retry
97
- </button>
98
- </div>
99
- );
100
- }
101
- return this.props.children;
102
- }
103
- }
104
-
105
- export const SchemaRenderer = forwardRef<any, { schema: SchemaNode } & Record<string, any>>(({ schema, ...props }, _ref) => {
106
- const context = useContext(SchemaRendererContext);
107
- const dataSource = context?.dataSource || {};
108
-
109
- // Evaluate schema expressions against the data source
110
- const evaluatedSchema = useMemo(() => {
111
- if (!schema || typeof schema === 'string') return schema;
112
-
113
- const evaluator = new ExpressionEvaluator({ data: dataSource });
114
- // Shallow copy
115
- const newSchema = { ...schema };
116
-
117
- // COMPAT: Hoist 'properties' up to schema level
118
- // This allows support for strict configs that wrap all props in 'properties'
119
- if (newSchema.properties) {
120
- Object.assign(newSchema, newSchema.properties);
121
- }
122
-
123
- // Evaluate 'content' (common in Text, Button)
124
- if (typeof newSchema.content === 'string') {
125
- newSchema.content = evaluator.evaluate(newSchema.content);
126
- }
127
-
128
- // Evaluate 'props'
129
- if (newSchema.props) {
130
- const newProps = { ...newSchema.props };
131
- for (const [key, val] of Object.entries(newProps)) {
132
- newProps[key] = evaluator.evaluate(val as any);
133
- }
134
- newSchema.props = newProps;
135
- }
136
-
137
- // Evaluate visibility: visible / visibleOn / hidden / hiddenOn
138
- const shouldHide = (() => {
139
- if (newSchema.visible !== undefined) {
140
- return !evaluator.evaluateCondition(newSchema.visible);
141
- }
142
- if (newSchema.visibleOn !== undefined) {
143
- return !evaluator.evaluateCondition(newSchema.visibleOn);
144
- }
145
- if (newSchema.hidden !== undefined) {
146
- return evaluator.evaluateCondition(newSchema.hidden);
147
- }
148
- if (newSchema.hiddenOn !== undefined) {
149
- return evaluator.evaluateCondition(newSchema.hiddenOn);
150
- }
151
- return false;
152
- })();
153
-
154
- if (shouldHide) {
155
- newSchema._hidden = true;
156
- }
157
-
158
- // Evaluate disabled: disabled / disabledOn
159
- const isDisabled = (() => {
160
- if (newSchema.disabled !== undefined) {
161
- return evaluator.evaluateCondition(newSchema.disabled);
162
- }
163
- if (newSchema.disabledOn !== undefined) {
164
- return evaluator.evaluateCondition(newSchema.disabledOn);
165
- }
166
- return false;
167
- })();
168
-
169
- if (isDisabled) {
170
- newSchema._disabled = true;
171
- }
172
-
173
- return newSchema;
174
- }, [schema, dataSource]);
175
-
176
- if (!evaluatedSchema) return null;
177
- // Handle visibility: if evaluated schema is hidden, render nothing
178
- if (evaluatedSchema?._hidden) return null;
179
- // If schema is just a string, render it as text
180
- if (typeof evaluatedSchema === 'string') return <>{evaluatedSchema}</>;
181
-
182
- debugLog('schema', 'Rendering schema node', { type: evaluatedSchema.type, id: evaluatedSchema.id });
183
-
184
- const Component = ComponentRegistry.get(evaluatedSchema.type);
185
-
186
- if (!Component) {
187
- debugLog('schema', 'Component not found in registry', { type: evaluatedSchema.type });
188
- const errorInfo = ERROR_CODES['OBJUI-001'];
189
- return (
190
- <div className="p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2" role="alert">
191
- <p className="font-medium">Unknown component type: <strong>{evaluatedSchema.type}</strong></p>
192
- {(globalThis as any).process?.env?.NODE_ENV !== 'production' && (
193
- <p className="text-xs mt-1">💡 {errorInfo.suggestion} (OBJUI-001)</p>
194
- )}
195
- <pre className="text-xs mt-2 overflow-auto">{JSON.stringify(evaluatedSchema, null, 2)}</pre>
196
- </div>
197
- );
198
- }
199
-
200
- // Note: We don't forward the ref to the Component because components in the registry
201
- // may not support refs. The SchemaRenderer itself can still receive refs for its own use.
202
-
203
- // Extract schema metadata properties that should NOT be passed as React props
204
- const {
205
- type: _type,
206
- children: _children,
207
- body: _body,
208
- schema: _schema,
209
- visible: _visible,
210
- visibleOn: _visibleOn,
211
- hidden: _hidden,
212
- hiddenOn: _hiddenOn,
213
- disabled: _disabled,
214
- disabledOn: _disabledOn,
215
- _hidden: __hidden, // stripped: internal visibility flag
216
- _disabled: __disabled, // stripped: internal disabled flag
217
- ...componentProps
218
- } = evaluatedSchema;
219
-
220
- // Extract AriaPropsSchema properties for accessibility
221
- const ariaProps = resolveAriaProps(evaluatedSchema);
222
-
223
- // Debug-mode enhancements: extra data attributes + perf tracking
224
- const isDebug = context?.debug || context?.debugFlags?.enabled;
225
- const debugAttrs: Record<string, string> = {};
226
- if (isDebug) {
227
- debugAttrs['data-debug-type'] = evaluatedSchema.type;
228
- if (evaluatedSchema.id) {
229
- debugAttrs['data-debug-id'] = evaluatedSchema.id;
230
- }
231
- }
232
-
233
- debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
234
- const renderStart = isDebug ? performance.now() : 0;
235
- const rendered = (
236
- <SchemaErrorBoundary componentType={evaluatedSchema.type}>
237
- {React.createElement(Component, {
238
- schema: evaluatedSchema,
239
- ...componentProps, // Spread non-metadata schema properties as props
240
- ...(evaluatedSchema.props || {}), // Override with explicit props if provided
241
- ...ariaProps, // Inject ARIA attributes from AriaPropsSchema
242
- ...debugAttrs, // Debug-mode data attributes
243
- disabled: __disabled || undefined,
244
- className: evaluatedSchema.className,
245
- 'data-obj-id': evaluatedSchema.id,
246
- 'data-obj-type': evaluatedSchema.type,
247
- ...props
248
- })}
249
- </SchemaErrorBoundary>
250
- );
251
- debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
252
-
253
- // Report render perf to DebugCollector when debug mode is active
254
- if (isDebug && renderStart) {
255
- const durationMs = performance.now() - renderStart;
256
- DebugCollector.getInstance().addPerf({
257
- type: evaluatedSchema.type,
258
- id: evaluatedSchema.id,
259
- durationMs,
260
- timestamp: Date.now(),
261
- });
262
- }
263
-
264
- return rendered;
265
- });
266
- SchemaRenderer.displayName = 'SchemaRenderer';