@object-ui/react 2.0.0 → 3.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +28 -0
- package/dist/SchemaRenderer.d.ts +20 -1
- package/dist/SchemaRenderer.js +76 -13
- package/dist/context/DndContext.d.ts +135 -0
- package/dist/context/DndContext.js +120 -0
- package/dist/context/NotificationContext.d.ts +113 -0
- package/dist/context/NotificationContext.js +130 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +17 -0
- package/dist/hooks/index.js +17 -0
- package/dist/hooks/useAnimation.d.ts +46 -0
- package/dist/hooks/useAnimation.js +59 -0
- package/dist/hooks/useClientNotifications.d.ts +53 -0
- package/dist/hooks/useClientNotifications.js +190 -0
- package/dist/hooks/useColumnSummary.d.ts +38 -0
- package/dist/hooks/useColumnSummary.js +54 -0
- package/dist/hooks/useCrudShortcuts.d.ts +39 -0
- package/dist/hooks/useCrudShortcuts.js +96 -0
- package/dist/hooks/useDensityMode.d.ts +48 -0
- package/dist/hooks/useDensityMode.js +60 -0
- package/dist/hooks/useETagCache.d.ts +75 -0
- package/dist/hooks/useETagCache.js +306 -0
- package/dist/hooks/useFocusManagement.d.ts +89 -0
- package/dist/hooks/useFocusManagement.js +188 -0
- package/dist/hooks/useFocusTrap.d.ts +41 -0
- package/dist/hooks/useFocusTrap.js +103 -0
- package/dist/hooks/useKeyboardShortcuts.d.ts +49 -0
- package/dist/hooks/useKeyboardShortcuts.js +86 -0
- package/dist/hooks/useOffline.d.ts +96 -0
- package/dist/hooks/useOffline.js +185 -0
- package/dist/hooks/usePageTransition.d.ts +60 -0
- package/dist/hooks/usePageTransition.js +111 -0
- package/dist/hooks/usePerformance.d.ts +87 -0
- package/dist/hooks/usePerformance.js +165 -0
- package/dist/hooks/usePerformanceBudget.d.ts +78 -0
- package/dist/hooks/usePerformanceBudget.js +209 -0
- package/dist/hooks/useReducedMotion.d.ts +28 -0
- package/dist/hooks/useReducedMotion.js +48 -0
- package/dist/hooks/useSchemaPersistence.d.ts +87 -0
- package/dist/hooks/useSchemaPersistence.js +173 -0
- package/dist/hooks/useViewSharing.d.ts +57 -0
- package/dist/hooks/useViewSharing.js +74 -0
- package/dist/hooks/useViewTransition.d.ts +54 -0
- package/dist/hooks/useViewTransition.js +83 -0
- package/package.json +7 -7
- package/src/SchemaRenderer.tsx +122 -13
- package/src/__tests__/SchemaRenderer.aria.test.tsx +118 -0
- package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +114 -0
- package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +132 -0
- package/src/__tests__/useOffline.test.ts +198 -0
- package/src/__tests__/usePageTransition.test.ts +214 -0
- package/src/__tests__/usePerformance.test.ts +137 -0
- package/src/__tests__/view-switching.test.tsx +456 -0
- package/src/context/DndContext.tsx +260 -0
- package/src/context/NotificationContext.tsx +280 -0
- package/src/context/__tests__/DndContext.test.tsx +150 -0
- package/src/context/__tests__/NotificationContext.test.tsx +176 -0
- package/src/context/index.ts +2 -0
- package/src/hooks/__tests__/useAnimation.test.ts +82 -0
- package/src/hooks/__tests__/useColumnSummary.test.ts +93 -0
- package/src/hooks/__tests__/useDensityMode.test.ts +81 -0
- package/src/hooks/__tests__/useETagCache.test.ts +153 -0
- package/src/hooks/__tests__/useFocusTrap.test.ts +47 -0
- package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +144 -0
- package/src/hooks/__tests__/usePerformanceBudget.test.ts +157 -0
- package/src/hooks/__tests__/useSchemaPersistence.test.ts +220 -0
- package/src/hooks/__tests__/useViewSharing.test.ts +121 -0
- package/src/hooks/__tests__/useViewTransition.test.ts +61 -0
- package/src/hooks/index.ts +17 -1
- package/src/hooks/useAnimation.ts +117 -0
- package/src/hooks/useClientNotifications.ts +251 -0
- package/src/hooks/useColumnSummary.ts +87 -0
- package/src/hooks/useCrudShortcuts.ts +126 -0
- package/src/hooks/useDensityMode.ts +102 -0
- package/src/hooks/useETagCache.ts +404 -0
- package/src/hooks/useFocusManagement.ts +248 -0
- package/src/hooks/useFocusTrap.ts +138 -0
- package/src/hooks/useKeyboardShortcuts.ts +119 -0
- package/src/hooks/useOffline.ts +301 -0
- package/src/hooks/usePageTransition.ts +187 -0
- package/src/hooks/usePerformance.ts +246 -0
- package/src/hooks/usePerformanceBudget.ts +313 -0
- package/src/hooks/useReducedMotion.ts +53 -0
- package/src/hooks/useSchemaPersistence.ts +222 -0
- package/src/hooks/useViewSharing.ts +140 -0
- package/src/hooks/useViewTransition.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @object-ui/react
|
|
2
2
|
|
|
3
|
+
## 3.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- adf2cc0: Patch release for bug fixes and improvements
|
|
8
|
+
- @object-ui/types@3.0.1
|
|
9
|
+
- @object-ui/core@3.0.1
|
|
10
|
+
- @object-ui/i18n@3.0.1
|
|
11
|
+
|
|
12
|
+
## 3.0.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 87979c3: Upgrade to @objectstack v3.0.0 and console bundle optimization
|
|
17
|
+
- Upgraded all @objectstack/\* packages from ^2.0.7 to ^3.0.0
|
|
18
|
+
- Breaking change migrations: Hub → Cloud namespace, definePlugin removed, PaginatedResult.value → .records, PaginatedResult.count → .total, client.meta.getObject() → client.meta.getItem()
|
|
19
|
+
- Console bundle optimization: split monolithic 3.7 MB chunk into 17 granular cacheable chunks (95% main entry reduction)
|
|
20
|
+
- Added gzip + brotli pre-compression via vite-plugin-compression2
|
|
21
|
+
- Lazy MSW loading for build:server (~150 KB gzip saved)
|
|
22
|
+
- Added bundle analysis with rollup-plugin-visualizer
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [87979c3]
|
|
27
|
+
- @object-ui/types@3.0.0
|
|
28
|
+
- @object-ui/core@3.0.0
|
|
29
|
+
- @object-ui/i18n@3.0.0
|
|
30
|
+
|
|
3
31
|
## 2.0.0
|
|
4
32
|
|
|
5
33
|
### Major Changes
|
package/dist/SchemaRenderer.d.ts
CHANGED
|
@@ -5,8 +5,27 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { Component } from 'react';
|
|
9
9
|
import { SchemaNode } from '@object-ui/core';
|
|
10
|
+
/**
|
|
11
|
+
* Per-component Error Boundary for SchemaRenderer.
|
|
12
|
+
* Catches render errors in individual components, preventing one broken
|
|
13
|
+
* component from crashing the entire page.
|
|
14
|
+
*/
|
|
15
|
+
interface SchemaErrorBoundaryState {
|
|
16
|
+
hasError: boolean;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
}
|
|
19
|
+
export declare class SchemaErrorBoundary extends Component<{
|
|
20
|
+
componentType?: string;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}, SchemaErrorBoundaryState> {
|
|
23
|
+
state: SchemaErrorBoundaryState;
|
|
24
|
+
static getDerivedStateFromError(error: Error): SchemaErrorBoundaryState;
|
|
25
|
+
handleRetry: () => void;
|
|
26
|
+
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
27
|
+
}
|
|
10
28
|
export declare const SchemaRenderer: React.ForwardRefExoticComponent<Omit<{
|
|
11
29
|
schema: SchemaNode;
|
|
12
30
|
} & Record<string, any>, "ref"> & React.RefAttributes<any>>;
|
|
31
|
+
export {};
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* ObjectUI
|
|
4
4
|
* Copyright (c) 2024-present ObjectStack Inc.
|
|
@@ -6,9 +6,63 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
* This source code is licensed under the MIT license found in the
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
|
-
import React, { forwardRef, useContext, useMemo } from 'react';
|
|
10
|
-
import { ComponentRegistry, ExpressionEvaluator } from '@object-ui/core';
|
|
9
|
+
import React, { forwardRef, useContext, useMemo, Component } from 'react';
|
|
10
|
+
import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, } from '@object-ui/core';
|
|
11
11
|
import { SchemaRendererContext } from './context/SchemaRendererContext';
|
|
12
|
+
import { resolveI18nLabel } from './utils/i18n';
|
|
13
|
+
/**
|
|
14
|
+
* Extract AriaPropsSchema properties from a schema node and convert
|
|
15
|
+
* them to standard HTML ARIA attributes.
|
|
16
|
+
*
|
|
17
|
+
* @objectstack/spec AriaPropsSchema defines:
|
|
18
|
+
* ariaLabel: string | I18nLabel (→ aria-label)
|
|
19
|
+
* ariaDescribedBy: string (→ aria-describedby)
|
|
20
|
+
* role: string (→ role)
|
|
21
|
+
*/
|
|
22
|
+
function resolveAriaProps(schema) {
|
|
23
|
+
const aria = {};
|
|
24
|
+
if (schema.ariaLabel) {
|
|
25
|
+
aria['aria-label'] = resolveI18nLabel(schema.ariaLabel);
|
|
26
|
+
}
|
|
27
|
+
if (schema.ariaDescribedBy) {
|
|
28
|
+
aria['aria-describedby'] = schema.ariaDescribedBy;
|
|
29
|
+
}
|
|
30
|
+
if (schema.role) {
|
|
31
|
+
aria['role'] = schema.role;
|
|
32
|
+
}
|
|
33
|
+
return aria;
|
|
34
|
+
}
|
|
35
|
+
export class SchemaErrorBoundary extends Component {
|
|
36
|
+
constructor() {
|
|
37
|
+
super(...arguments);
|
|
38
|
+
Object.defineProperty(this, "state", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: { hasError: false, error: null }
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "handleRetry", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: () => {
|
|
49
|
+
this.setState({ hasError: false, error: null });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
static getDerivedStateFromError(error) {
|
|
54
|
+
return { hasError: true, error };
|
|
55
|
+
}
|
|
56
|
+
render() {
|
|
57
|
+
if (this.state.hasError && this.state.error) {
|
|
58
|
+
const error = this.state.error;
|
|
59
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
60
|
+
const objuiError = isObjectUIError(error) ? error : null;
|
|
61
|
+
return (_jsxs("div", { className: "p-4 border border-orange-400 rounded bg-orange-50 text-orange-700 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Component", this.props.componentType ? ` "${this.props.componentType}"` : '', " failed to render"] }), _jsx("p", { className: "text-sm mt-1", children: error.message }), isDev && objuiError?.code && (_jsxs("p", { className: "text-xs mt-1 text-orange-500", children: ["Error code: ", objuiError.code, objuiError.details?.suggestion ? (_jsxs("span", { className: "block mt-0.5", children: ["\uD83D\uDCA1 ", String(objuiError.details.suggestion)] })) : null] })), _jsx("button", { onClick: this.handleRetry, className: "mt-2 text-sm underline hover:no-underline", children: "Retry" })] }));
|
|
62
|
+
}
|
|
63
|
+
return this.props.children;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
12
66
|
export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
13
67
|
const context = useContext(SchemaRendererContext);
|
|
14
68
|
const dataSource = context?.dataSource || {};
|
|
@@ -43,22 +97,31 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
43
97
|
// If schema is just a string, render it as text
|
|
44
98
|
if (typeof evaluatedSchema === 'string')
|
|
45
99
|
return _jsx(_Fragment, { children: evaluatedSchema });
|
|
100
|
+
debugLog('schema', 'Rendering schema node', { type: evaluatedSchema.type, id: evaluatedSchema.id });
|
|
46
101
|
const Component = ComponentRegistry.get(evaluatedSchema.type);
|
|
47
102
|
if (!Component) {
|
|
48
|
-
|
|
103
|
+
debugLog('schema', 'Component not found in registry', { type: evaluatedSchema.type });
|
|
104
|
+
const errorInfo = ERROR_CODES['OBJUI-001'];
|
|
105
|
+
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Unknown component type: ", _jsx("strong", { children: evaluatedSchema.type })] }), process.env.NODE_ENV !== 'production' && (_jsxs("p", { className: "text-xs mt-1", children: ["\uD83D\uDCA1 ", errorInfo.suggestion, " (OBJUI-001)"] })), _jsx("pre", { className: "text-xs mt-2 overflow-auto", children: JSON.stringify(evaluatedSchema, null, 2) })] }));
|
|
49
106
|
}
|
|
50
107
|
// Note: We don't forward the ref to the Component because components in the registry
|
|
51
108
|
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
52
109
|
// Extract schema metadata properties that should NOT be passed as React props
|
|
53
110
|
const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, ...componentProps } = evaluatedSchema;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
111
|
+
// Extract AriaPropsSchema properties for accessibility
|
|
112
|
+
const ariaProps = resolveAriaProps(evaluatedSchema);
|
|
113
|
+
debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
114
|
+
const rendered = (_jsx(SchemaErrorBoundary, { componentType: evaluatedSchema.type, children: React.createElement(Component, {
|
|
115
|
+
schema: evaluatedSchema,
|
|
116
|
+
...componentProps, // Spread non-metadata schema properties as props
|
|
117
|
+
...(evaluatedSchema.props || {}), // Override with explicit props if provided
|
|
118
|
+
...ariaProps, // Inject ARIA attributes from AriaPropsSchema
|
|
119
|
+
className: evaluatedSchema.className,
|
|
120
|
+
'data-obj-id': evaluatedSchema.id,
|
|
121
|
+
'data-obj-type': evaluatedSchema.type,
|
|
122
|
+
...props
|
|
123
|
+
}) }));
|
|
124
|
+
debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
125
|
+
return rendered;
|
|
63
126
|
});
|
|
64
127
|
SchemaRenderer.displayName = 'SchemaRenderer';
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
* @object-ui/react - Drag and Drop Context
|
|
10
|
+
*
|
|
11
|
+
* Provides a unified DnD configuration system to the component tree.
|
|
12
|
+
* Implements DndConfigSchema, DragItemSchema, DropZoneSchema,
|
|
13
|
+
* DragConstraintSchema, and DropEffectSchema from @objectstack/spec v2.0.7.
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
/** Drop effect types aligned with DropEffectSchema */
|
|
17
|
+
export type DropEffectType = 'move' | 'copy' | 'link' | 'none';
|
|
18
|
+
/** Drag constraint axis aligned with DragConstraintSchema */
|
|
19
|
+
export type DragAxis = 'x' | 'y' | 'both';
|
|
20
|
+
/** Drag item metadata */
|
|
21
|
+
export interface DragItemData {
|
|
22
|
+
/** Unique identifier for the dragged item */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Type of the dragged item (for drop zone filtering) */
|
|
25
|
+
type: string;
|
|
26
|
+
/** Arbitrary data associated with the drag */
|
|
27
|
+
data?: Record<string, unknown>;
|
|
28
|
+
/** Source container/zone identifier */
|
|
29
|
+
sourceId?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Drop zone configuration */
|
|
32
|
+
export interface DropZoneConfig {
|
|
33
|
+
/** Unique identifier for the drop zone */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Accepted drag item types */
|
|
36
|
+
acceptTypes: string[];
|
|
37
|
+
/** Drop effect to apply */
|
|
38
|
+
effect?: DropEffectType;
|
|
39
|
+
/** Whether the zone is currently disabled */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Maximum number of items this zone can hold */
|
|
42
|
+
maxItems?: number;
|
|
43
|
+
}
|
|
44
|
+
/** Drag constraints configuration */
|
|
45
|
+
export interface DragConstraintConfig {
|
|
46
|
+
/** Constrain movement to an axis */
|
|
47
|
+
axis?: DragAxis;
|
|
48
|
+
/** Minimum distance before drag starts (in pixels) */
|
|
49
|
+
activationDistance?: number;
|
|
50
|
+
/** Whether to show a drag handle */
|
|
51
|
+
showHandle?: boolean;
|
|
52
|
+
/** Container bounds (CSS selector) */
|
|
53
|
+
containerId?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Overall DnD configuration aligned with DndConfigSchema */
|
|
56
|
+
export interface DndSystemConfig {
|
|
57
|
+
/** Whether DnD is enabled */
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
/** Default drag constraints */
|
|
60
|
+
defaultConstraints?: DragConstraintConfig;
|
|
61
|
+
/** Whether to show drag overlay/preview */
|
|
62
|
+
showPreview?: boolean;
|
|
63
|
+
/** Auto-scroll when dragging near edges */
|
|
64
|
+
autoScroll?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** Drag event information */
|
|
67
|
+
export interface DragEvent {
|
|
68
|
+
/** The dragged item */
|
|
69
|
+
item: DragItemData;
|
|
70
|
+
/** Target drop zone (null if not over a zone) */
|
|
71
|
+
targetZoneId: string | null;
|
|
72
|
+
/** The applied drop effect */
|
|
73
|
+
effect: DropEffectType;
|
|
74
|
+
}
|
|
75
|
+
export interface DndProviderProps {
|
|
76
|
+
children: React.ReactNode;
|
|
77
|
+
/** System-wide DnD configuration */
|
|
78
|
+
config?: DndSystemConfig;
|
|
79
|
+
/** Called when a drag starts */
|
|
80
|
+
onDragStart?: (event: DragEvent) => void;
|
|
81
|
+
/** Called when an item is dropped */
|
|
82
|
+
onDrop?: (event: DragEvent) => void;
|
|
83
|
+
/** Called when a drag is cancelled */
|
|
84
|
+
onDragCancel?: (event: DragEvent) => void;
|
|
85
|
+
}
|
|
86
|
+
interface DndContextValue {
|
|
87
|
+
/** System-wide DnD configuration */
|
|
88
|
+
config: DndSystemConfig;
|
|
89
|
+
/** Whether DnD is enabled */
|
|
90
|
+
enabled: boolean;
|
|
91
|
+
/** Currently dragging item (null if not dragging) */
|
|
92
|
+
activeItem: DragItemData | null;
|
|
93
|
+
/** Registered drop zones */
|
|
94
|
+
dropZones: Map<string, DropZoneConfig>;
|
|
95
|
+
/** Start a drag operation */
|
|
96
|
+
startDrag: (item: DragItemData) => void;
|
|
97
|
+
/** End the current drag operation */
|
|
98
|
+
endDrag: (targetZoneId?: string) => void;
|
|
99
|
+
/** Cancel the current drag */
|
|
100
|
+
cancelDrag: () => void;
|
|
101
|
+
/** Register a drop zone */
|
|
102
|
+
registerDropZone: (zone: DropZoneConfig) => void;
|
|
103
|
+
/** Unregister a drop zone */
|
|
104
|
+
unregisterDropZone: (zoneId: string) => void;
|
|
105
|
+
/** Check if a drop zone accepts a drag item type */
|
|
106
|
+
canDrop: (zoneId: string, itemType: string) => boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* DndProvider — Provides a unified DnD system aligned with @objectstack/spec v2.0.7.
|
|
110
|
+
*
|
|
111
|
+
* This is a coordination layer; actual drag mechanics are delegated to
|
|
112
|
+
* libraries like @dnd-kit/core or react-grid-layout in individual plugins.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* <DndProvider
|
|
117
|
+
* config={{ enabled: true, showPreview: true, autoScroll: true }}
|
|
118
|
+
* onDrop={(event) => handleDrop(event.item, event.targetZoneId)}
|
|
119
|
+
* >
|
|
120
|
+
* <KanbanBoard />
|
|
121
|
+
* </DndProvider>
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export declare const DndProvider: React.FC<DndProviderProps>;
|
|
125
|
+
/**
|
|
126
|
+
* Hook to consume the DnD context.
|
|
127
|
+
*
|
|
128
|
+
* @throws Error if used outside a DndProvider
|
|
129
|
+
*/
|
|
130
|
+
export declare function useDnd(): DndContextValue;
|
|
131
|
+
/**
|
|
132
|
+
* Hook to check if a DndProvider is available.
|
|
133
|
+
*/
|
|
134
|
+
export declare function useHasDndProvider(): boolean;
|
|
135
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* @object-ui/react - Drag and Drop Context
|
|
11
|
+
*
|
|
12
|
+
* Provides a unified DnD configuration system to the component tree.
|
|
13
|
+
* Implements DndConfigSchema, DragItemSchema, DropZoneSchema,
|
|
14
|
+
* DragConstraintSchema, and DropEffectSchema from @objectstack/spec v2.0.7.
|
|
15
|
+
*/
|
|
16
|
+
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
17
|
+
const DndCtx = createContext(null);
|
|
18
|
+
/**
|
|
19
|
+
* DndProvider — Provides a unified DnD system aligned with @objectstack/spec v2.0.7.
|
|
20
|
+
*
|
|
21
|
+
* This is a coordination layer; actual drag mechanics are delegated to
|
|
22
|
+
* libraries like @dnd-kit/core or react-grid-layout in individual plugins.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <DndProvider
|
|
27
|
+
* config={{ enabled: true, showPreview: true, autoScroll: true }}
|
|
28
|
+
* onDrop={(event) => handleDrop(event.item, event.targetZoneId)}
|
|
29
|
+
* >
|
|
30
|
+
* <KanbanBoard />
|
|
31
|
+
* </DndProvider>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const DndProvider = ({ children, config: userConfig = {}, onDragStart, onDrop, onDragCancel, }) => {
|
|
35
|
+
const config = useMemo(() => ({
|
|
36
|
+
enabled: true,
|
|
37
|
+
showPreview: true,
|
|
38
|
+
autoScroll: true,
|
|
39
|
+
...userConfig,
|
|
40
|
+
}),
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
[JSON.stringify(userConfig)]);
|
|
43
|
+
const [activeItem, setActiveItem] = useState(null);
|
|
44
|
+
const [dropZones] = useState(() => new Map());
|
|
45
|
+
const startDrag = useCallback((item) => {
|
|
46
|
+
if (!config.enabled)
|
|
47
|
+
return;
|
|
48
|
+
setActiveItem(item);
|
|
49
|
+
onDragStart?.({ item, targetZoneId: null, effect: 'move' });
|
|
50
|
+
}, [config.enabled, onDragStart]);
|
|
51
|
+
const endDrag = useCallback((targetZoneId) => {
|
|
52
|
+
if (!activeItem)
|
|
53
|
+
return;
|
|
54
|
+
const zone = targetZoneId ? dropZones.get(targetZoneId) : undefined;
|
|
55
|
+
const effect = zone?.effect ?? 'move';
|
|
56
|
+
onDrop?.({ item: activeItem, targetZoneId: targetZoneId ?? null, effect });
|
|
57
|
+
setActiveItem(null);
|
|
58
|
+
}, [activeItem, dropZones, onDrop]);
|
|
59
|
+
const cancelDrag = useCallback(() => {
|
|
60
|
+
if (!activeItem)
|
|
61
|
+
return;
|
|
62
|
+
onDragCancel?.({ item: activeItem, targetZoneId: null, effect: 'none' });
|
|
63
|
+
setActiveItem(null);
|
|
64
|
+
}, [activeItem, onDragCancel]);
|
|
65
|
+
const registerDropZone = useCallback((zone) => {
|
|
66
|
+
dropZones.set(zone.id, zone);
|
|
67
|
+
}, [dropZones]);
|
|
68
|
+
const unregisterDropZone = useCallback((zoneId) => {
|
|
69
|
+
dropZones.delete(zoneId);
|
|
70
|
+
}, [dropZones]);
|
|
71
|
+
const canDrop = useCallback((zoneId, itemType) => {
|
|
72
|
+
const zone = dropZones.get(zoneId);
|
|
73
|
+
if (!zone || zone.disabled)
|
|
74
|
+
return false;
|
|
75
|
+
return zone.acceptTypes.includes(itemType);
|
|
76
|
+
}, [dropZones]);
|
|
77
|
+
const value = useMemo(() => ({
|
|
78
|
+
config,
|
|
79
|
+
enabled: config.enabled ?? true,
|
|
80
|
+
activeItem,
|
|
81
|
+
dropZones,
|
|
82
|
+
startDrag,
|
|
83
|
+
endDrag,
|
|
84
|
+
cancelDrag,
|
|
85
|
+
registerDropZone,
|
|
86
|
+
unregisterDropZone,
|
|
87
|
+
canDrop,
|
|
88
|
+
}), [
|
|
89
|
+
config,
|
|
90
|
+
activeItem,
|
|
91
|
+
dropZones,
|
|
92
|
+
startDrag,
|
|
93
|
+
endDrag,
|
|
94
|
+
cancelDrag,
|
|
95
|
+
registerDropZone,
|
|
96
|
+
unregisterDropZone,
|
|
97
|
+
canDrop,
|
|
98
|
+
]);
|
|
99
|
+
return _jsx(DndCtx.Provider, { value: value, children: children });
|
|
100
|
+
};
|
|
101
|
+
DndProvider.displayName = 'DndProvider';
|
|
102
|
+
/**
|
|
103
|
+
* Hook to consume the DnD context.
|
|
104
|
+
*
|
|
105
|
+
* @throws Error if used outside a DndProvider
|
|
106
|
+
*/
|
|
107
|
+
export function useDnd() {
|
|
108
|
+
const ctx = useContext(DndCtx);
|
|
109
|
+
if (!ctx) {
|
|
110
|
+
throw new Error('useDnd must be used within a <DndProvider>. ' +
|
|
111
|
+
'Wrap your app with <DndProvider> to use the drag and drop system.');
|
|
112
|
+
}
|
|
113
|
+
return ctx;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Hook to check if a DndProvider is available.
|
|
117
|
+
*/
|
|
118
|
+
export function useHasDndProvider() {
|
|
119
|
+
return useContext(DndCtx) !== null;
|
|
120
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
* @object-ui/react - Notification Context
|
|
10
|
+
*
|
|
11
|
+
* Provides a spec-driven notification system to the component tree.
|
|
12
|
+
* Implements NotificationSchema, NotificationConfigSchema, and
|
|
13
|
+
* NotificationActionSchema from @objectstack/spec v2.0.7.
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
/** Notification severity levels aligned with NotificationSeveritySchema */
|
|
17
|
+
export type NotificationSeverityLevel = 'info' | 'success' | 'warning' | 'error';
|
|
18
|
+
/** Notification display type aligned with NotificationTypeSchema */
|
|
19
|
+
export type NotificationDisplayType = 'toast' | 'banner' | 'snackbar' | 'modal';
|
|
20
|
+
/** Notification position aligned with NotificationPositionSchema */
|
|
21
|
+
export type NotificationPositionValue = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
22
|
+
/** Action button on a notification */
|
|
23
|
+
export interface NotificationActionButton {
|
|
24
|
+
label: string;
|
|
25
|
+
onClick: () => void;
|
|
26
|
+
variant?: 'default' | 'destructive' | 'outline';
|
|
27
|
+
}
|
|
28
|
+
/** A single notification item */
|
|
29
|
+
export interface NotificationItem {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
message?: string;
|
|
33
|
+
severity: NotificationSeverityLevel;
|
|
34
|
+
displayType?: NotificationDisplayType;
|
|
35
|
+
actions?: NotificationActionButton[];
|
|
36
|
+
/** Duration in ms (0 = persistent). Default: 5000 */
|
|
37
|
+
duration?: number;
|
|
38
|
+
/** Whether the notification has been read */
|
|
39
|
+
read?: boolean;
|
|
40
|
+
/** Timestamp */
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
/** Optional icon name (lucide icon) */
|
|
43
|
+
icon?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Configuration for the notification system */
|
|
46
|
+
export interface NotificationSystemConfig {
|
|
47
|
+
/** Default position for toast notifications */
|
|
48
|
+
position?: NotificationPositionValue;
|
|
49
|
+
/** Default duration in ms */
|
|
50
|
+
defaultDuration?: number;
|
|
51
|
+
/** Maximum number of visible notifications */
|
|
52
|
+
maxVisible?: number;
|
|
53
|
+
/** Whether to stack notifications */
|
|
54
|
+
stacking?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface NotificationProviderProps {
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
/** System configuration */
|
|
59
|
+
config?: NotificationSystemConfig;
|
|
60
|
+
/** External toast handler (e.g., Sonner) for rendering toasts */
|
|
61
|
+
onToast?: (notification: NotificationItem) => void;
|
|
62
|
+
}
|
|
63
|
+
interface NotificationContextValue {
|
|
64
|
+
/** All notifications (including history) */
|
|
65
|
+
notifications: NotificationItem[];
|
|
66
|
+
/** Unread notification count */
|
|
67
|
+
unreadCount: number;
|
|
68
|
+
/** Add a notification */
|
|
69
|
+
notify: (notification: Omit<NotificationItem, 'id' | 'createdAt' | 'read'>) => string;
|
|
70
|
+
/** Convenience: show info notification */
|
|
71
|
+
info: (title: string, message?: string) => string;
|
|
72
|
+
/** Convenience: show success notification */
|
|
73
|
+
success: (title: string, message?: string) => string;
|
|
74
|
+
/** Convenience: show warning notification */
|
|
75
|
+
warning: (title: string, message?: string) => string;
|
|
76
|
+
/** Convenience: show error notification */
|
|
77
|
+
error: (title: string, message?: string) => string;
|
|
78
|
+
/** Mark a notification as read */
|
|
79
|
+
markAsRead: (id: string) => void;
|
|
80
|
+
/** Mark all notifications as read */
|
|
81
|
+
markAllAsRead: () => void;
|
|
82
|
+
/** Dismiss a notification */
|
|
83
|
+
dismiss: (id: string) => void;
|
|
84
|
+
/** Clear all notifications */
|
|
85
|
+
clearAll: () => void;
|
|
86
|
+
/** System configuration */
|
|
87
|
+
config: NotificationSystemConfig;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* NotificationProvider — Provides a spec-driven notification system.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* <NotificationProvider
|
|
95
|
+
* config={{ position: 'top-right', defaultDuration: 5000, maxVisible: 5 }}
|
|
96
|
+
* onToast={(n) => toast[n.severity](n.title, { description: n.message })}
|
|
97
|
+
* >
|
|
98
|
+
* <App />
|
|
99
|
+
* </NotificationProvider>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare const NotificationProvider: React.FC<NotificationProviderProps>;
|
|
103
|
+
/**
|
|
104
|
+
* Hook to consume the NotificationProvider context.
|
|
105
|
+
*
|
|
106
|
+
* @throws Error if used outside a NotificationProvider
|
|
107
|
+
*/
|
|
108
|
+
export declare function useNotifications(): NotificationContextValue;
|
|
109
|
+
/**
|
|
110
|
+
* Hook to check if a NotificationProvider is available.
|
|
111
|
+
*/
|
|
112
|
+
export declare function useHasNotificationProvider(): boolean;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* @object-ui/react - Notification Context
|
|
11
|
+
*
|
|
12
|
+
* Provides a spec-driven notification system to the component tree.
|
|
13
|
+
* Implements NotificationSchema, NotificationConfigSchema, and
|
|
14
|
+
* NotificationActionSchema from @objectstack/spec v2.0.7.
|
|
15
|
+
*/
|
|
16
|
+
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
17
|
+
let notificationCounter = 0;
|
|
18
|
+
const NotificationCtx = createContext(null);
|
|
19
|
+
/**
|
|
20
|
+
* NotificationProvider — Provides a spec-driven notification system.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <NotificationProvider
|
|
25
|
+
* config={{ position: 'top-right', defaultDuration: 5000, maxVisible: 5 }}
|
|
26
|
+
* onToast={(n) => toast[n.severity](n.title, { description: n.message })}
|
|
27
|
+
* >
|
|
28
|
+
* <App />
|
|
29
|
+
* </NotificationProvider>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const NotificationProvider = ({ children, config: userConfig = {}, onToast, }) => {
|
|
33
|
+
const config = useMemo(() => ({
|
|
34
|
+
position: 'top-right',
|
|
35
|
+
defaultDuration: 5000,
|
|
36
|
+
maxVisible: 5,
|
|
37
|
+
stacking: true,
|
|
38
|
+
...userConfig,
|
|
39
|
+
}),
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
[JSON.stringify(userConfig)]);
|
|
42
|
+
const [notifications, setNotifications] = useState([]);
|
|
43
|
+
const notify = useCallback((input) => {
|
|
44
|
+
const id = `notification-${++notificationCounter}`;
|
|
45
|
+
const notification = {
|
|
46
|
+
...input,
|
|
47
|
+
id,
|
|
48
|
+
createdAt: new Date(),
|
|
49
|
+
read: false,
|
|
50
|
+
};
|
|
51
|
+
setNotifications((prev) => [notification, ...prev]);
|
|
52
|
+
// Delegate to external toast handler if provided
|
|
53
|
+
if (onToast) {
|
|
54
|
+
onToast(notification);
|
|
55
|
+
}
|
|
56
|
+
// Auto-dismiss non-persistent notifications
|
|
57
|
+
const duration = input.duration ?? config.defaultDuration ?? 5000;
|
|
58
|
+
if (duration > 0) {
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
|
61
|
+
}, duration);
|
|
62
|
+
}
|
|
63
|
+
return id;
|
|
64
|
+
}, [config.defaultDuration, onToast]);
|
|
65
|
+
const info = useCallback((title, message) => notify({ title, message, severity: 'info' }), [notify]);
|
|
66
|
+
const success = useCallback((title, message) => notify({ title, message, severity: 'success' }), [notify]);
|
|
67
|
+
const warning = useCallback((title, message) => notify({ title, message, severity: 'warning' }), [notify]);
|
|
68
|
+
const error = useCallback((title, message) => notify({ title, message, severity: 'error' }), [notify]);
|
|
69
|
+
const markAsRead = useCallback((id) => {
|
|
70
|
+
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)));
|
|
71
|
+
}, []);
|
|
72
|
+
const markAllAsRead = useCallback(() => {
|
|
73
|
+
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
|
74
|
+
}, []);
|
|
75
|
+
const dismiss = useCallback((id) => {
|
|
76
|
+
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
|
77
|
+
}, []);
|
|
78
|
+
const clearAll = useCallback(() => {
|
|
79
|
+
setNotifications([]);
|
|
80
|
+
}, []);
|
|
81
|
+
const unreadCount = useMemo(() => notifications.filter((n) => !n.read).length, [notifications]);
|
|
82
|
+
const value = useMemo(() => ({
|
|
83
|
+
notifications,
|
|
84
|
+
unreadCount,
|
|
85
|
+
notify,
|
|
86
|
+
info,
|
|
87
|
+
success,
|
|
88
|
+
warning,
|
|
89
|
+
error,
|
|
90
|
+
markAsRead,
|
|
91
|
+
markAllAsRead,
|
|
92
|
+
dismiss,
|
|
93
|
+
clearAll,
|
|
94
|
+
config,
|
|
95
|
+
}), [
|
|
96
|
+
notifications,
|
|
97
|
+
unreadCount,
|
|
98
|
+
notify,
|
|
99
|
+
info,
|
|
100
|
+
success,
|
|
101
|
+
warning,
|
|
102
|
+
error,
|
|
103
|
+
markAsRead,
|
|
104
|
+
markAllAsRead,
|
|
105
|
+
dismiss,
|
|
106
|
+
clearAll,
|
|
107
|
+
config,
|
|
108
|
+
]);
|
|
109
|
+
return (_jsx(NotificationCtx.Provider, { value: value, children: children }));
|
|
110
|
+
};
|
|
111
|
+
NotificationProvider.displayName = 'NotificationProvider';
|
|
112
|
+
/**
|
|
113
|
+
* Hook to consume the NotificationProvider context.
|
|
114
|
+
*
|
|
115
|
+
* @throws Error if used outside a NotificationProvider
|
|
116
|
+
*/
|
|
117
|
+
export function useNotifications() {
|
|
118
|
+
const ctx = useContext(NotificationCtx);
|
|
119
|
+
if (!ctx) {
|
|
120
|
+
throw new Error('useNotifications must be used within a <NotificationProvider>. ' +
|
|
121
|
+
'Wrap your app with <NotificationProvider> to use the notification system.');
|
|
122
|
+
}
|
|
123
|
+
return ctx;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Hook to check if a NotificationProvider is available.
|
|
127
|
+
*/
|
|
128
|
+
export function useHasNotificationProvider() {
|
|
129
|
+
return useContext(NotificationCtx) !== null;
|
|
130
|
+
}
|