@veap/plugins 0.0.0-stage → 0.1.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/dist/actions/index.d.ts +1 -0
  4. package/dist/actions/index.js +1 -0
  5. package/dist/actions/plugin.actions.d.ts +6 -0
  6. package/dist/actions/plugin.actions.js +60 -0
  7. package/dist/actions.d.ts +1 -0
  8. package/dist/actions.js +1 -0
  9. package/dist/cli/add.d.ts +4 -0
  10. package/dist/cli/add.js +191 -0
  11. package/dist/cli/eject.d.ts +2 -0
  12. package/dist/cli/eject.js +136 -0
  13. package/dist/cli/generate-plugin.d.ts +3 -0
  14. package/dist/cli/generate-plugin.js +48 -0
  15. package/dist/cli/index.d.ts +6 -0
  16. package/dist/cli/index.js +6 -0
  17. package/dist/cli/package-manager.d.ts +14 -0
  18. package/dist/cli/package-manager.js +62 -0
  19. package/dist/cli/stubs.d.ts +16 -0
  20. package/dist/cli/stubs.js +42 -0
  21. package/dist/cli/utils.d.ts +3 -0
  22. package/dist/cli/utils.js +164 -0
  23. package/dist/client.d.ts +6 -0
  24. package/dist/client.js +8 -0
  25. package/dist/common/context.d.ts +26 -0
  26. package/dist/common/context.js +26 -0
  27. package/dist/common/events.d.ts +19 -0
  28. package/dist/common/events.js +26 -0
  29. package/dist/common/index.d.ts +3 -0
  30. package/dist/common/index.js +3 -0
  31. package/dist/common/types.d.ts +195 -0
  32. package/dist/common/types.js +40 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +5 -0
  35. package/dist/migrations/0001_create_plugins_tables.d.ts +4 -0
  36. package/dist/migrations/0001_create_plugins_tables.js +29 -0
  37. package/dist/migrations/index.d.ts +4 -0
  38. package/dist/migrations/index.js +7 -0
  39. package/dist/models/SystemPlugin.d.ts +19 -0
  40. package/dist/models/SystemPlugin.js +14 -0
  41. package/dist/models/SystemUserWidget.d.ts +19 -0
  42. package/dist/models/SystemUserWidget.js +17 -0
  43. package/dist/models/index.d.ts +2 -0
  44. package/dist/models/index.js +2 -0
  45. package/dist/models.d.ts +1 -0
  46. package/dist/models.js +1 -0
  47. package/dist/navigation/breadcrumbs.d.ts +2 -0
  48. package/dist/navigation/breadcrumbs.js +84 -0
  49. package/dist/navigation/index.d.ts +2 -0
  50. package/dist/navigation/index.js +2 -0
  51. package/dist/navigation/navigation.service.d.ts +27 -0
  52. package/dist/navigation/navigation.service.js +183 -0
  53. package/dist/providers/index.d.ts +1 -0
  54. package/dist/providers/index.js +1 -0
  55. package/dist/providers/plugins.provider.d.ts +20 -0
  56. package/dist/providers/plugins.provider.js +142 -0
  57. package/dist/registry/contracts.d.ts +72 -0
  58. package/dist/registry/contracts.js +4 -0
  59. package/dist/registry/facade.d.ts +45 -0
  60. package/dist/registry/facade.js +49 -0
  61. package/dist/registry/index.d.ts +5 -0
  62. package/dist/registry/index.js +5 -0
  63. package/dist/registry/migration-runner.d.ts +8 -0
  64. package/dist/registry/migration-runner.js +12 -0
  65. package/dist/registry/plugin.repository.d.ts +19 -0
  66. package/dist/registry/plugin.repository.js +74 -0
  67. package/dist/registry/registry.service.d.ts +69 -0
  68. package/dist/registry/registry.service.js +535 -0
  69. package/dist/router/discovery.d.ts +27 -0
  70. package/dist/router/discovery.js +37 -0
  71. package/dist/router/handler.d.ts +6 -0
  72. package/dist/router/handler.js +40 -0
  73. package/dist/router/index.d.ts +13 -0
  74. package/dist/router/index.js +13 -0
  75. package/dist/router/matcher.d.ts +5 -0
  76. package/dist/router/matcher.js +32 -0
  77. package/dist/router/middlewares.d.ts +25 -0
  78. package/dist/router/middlewares.js +119 -0
  79. package/dist/router/react/error-boundary.d.ts +30 -0
  80. package/dist/router/react/error-boundary.js +68 -0
  81. package/dist/router/react/index.d.ts +4 -0
  82. package/dist/router/react/index.js +4 -0
  83. package/dist/router/react/renderer.d.ts +13 -0
  84. package/dist/router/react/renderer.js +159 -0
  85. package/dist/router/react/soft-navigation.d.ts +4 -0
  86. package/dist/router/react/soft-navigation.js +34 -0
  87. package/dist/router/react/with-router.d.ts +22 -0
  88. package/dist/router/react/with-router.js +19 -0
  89. package/dist/router/route-tree.d.ts +177 -0
  90. package/dist/router/route-tree.js +633 -0
  91. package/dist/router/router.service.d.ts +25 -0
  92. package/dist/router/router.service.js +127 -0
  93. package/dist/router/scanner.d.ts +4 -0
  94. package/dist/router/scanner.js +337 -0
  95. package/dist/router/utils.d.ts +10 -0
  96. package/dist/router/utils.js +48 -0
  97. package/dist/router.d.ts +6 -0
  98. package/dist/router.js +6 -0
  99. package/dist/server.d.ts +11 -0
  100. package/dist/server.js +17 -0
  101. package/dist/widgets/index.d.ts +8 -0
  102. package/dist/widgets/index.js +8 -0
  103. package/dist/widgets/path-prefix-context.d.ts +5 -0
  104. package/dist/widgets/path-prefix-context.js +9 -0
  105. package/dist/widgets/plugin-extension-point-client.d.ts +15 -0
  106. package/dist/widgets/plugin-extension-point-client.js +44 -0
  107. package/dist/widgets/plugin-extension-point.d.ts +14 -0
  108. package/dist/widgets/plugin-extension-point.js +51 -0
  109. package/dist/widgets/plugin-widget-area-client.d.ts +11 -0
  110. package/dist/widgets/plugin-widget-area-client.js +31 -0
  111. package/dist/widgets/plugin-widget-area.d.ts +10 -0
  112. package/dist/widgets/plugin-widget-area.js +19 -0
  113. package/dist/widgets/use-confirm-action.d.ts +19 -0
  114. package/dist/widgets/use-confirm-action.js +68 -0
  115. package/dist/widgets/widget-composer-client.d.ts +21 -0
  116. package/dist/widgets/widget-composer-client.js +281 -0
  117. package/dist/widgets/widget-composer.d.ts +8 -0
  118. package/dist/widgets/widget-composer.js +28 -0
  119. package/package.json +115 -4
@@ -0,0 +1,14 @@
1
+ import type React from "react";
2
+ export interface PluginExtensionPointProps {
3
+ target: string;
4
+ point: string;
5
+ mode?: "single" | "multiple";
6
+ className?: string;
7
+ props?: any;
8
+ fallback?: React.ReactNode;
9
+ children?: React.ReactNode;
10
+ as?: React.ElementType;
11
+ includeDisabled?: boolean;
12
+ }
13
+ export declare function PluginExtensionPoint({ target, point, mode, className, props, fallback, children, as: Container, includeDisabled, }: PluginExtensionPointProps): Promise<string | number | bigint | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined>;
14
+ export { PluginExtensionPoint as ExtensionPoint };
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getCurrentSession } from "@veap/auth";
3
+ import { isPluginsContextBound, pluginsContext } from "../common/context";
4
+ export async function PluginExtensionPoint({ target, point, mode = "multiple", className, props, fallback, children, as: Container, includeDisabled = false, }) {
5
+ const g = globalThis;
6
+ if (g.__VEAP_BOOTSTRAPPING_PROMISE__) {
7
+ try {
8
+ await g.__VEAP_BOOTSTRAPPING_PROMISE__;
9
+ }
10
+ catch { }
11
+ }
12
+ const fallbackContent = (children ?? fallback ?? null);
13
+ if (!isPluginsContextBound()) {
14
+ return fallbackContent;
15
+ }
16
+ let user = null;
17
+ try {
18
+ const sessionData = await getCurrentSession();
19
+ user = sessionData?.user ?? null;
20
+ }
21
+ catch {
22
+ user = null;
23
+ }
24
+ const userRoles = user?.roles || [];
25
+ const userPermissions = user?.permissions || [];
26
+ const extensions = await pluginsContext().registry.getExtensions(target, point, includeDisabled, {
27
+ roles: userRoles,
28
+ permissions: userPermissions,
29
+ });
30
+ if (extensions.length === 0)
31
+ return fallbackContent;
32
+ if (mode === "single") {
33
+ const ext = extensions[0];
34
+ const Component = ext?.component;
35
+ if (!Component)
36
+ return fallbackContent;
37
+ if (Container || className) {
38
+ const Wrapper = Container || "div";
39
+ return (_jsx(Wrapper, { className: className, children: _jsx(Component, { ...props }, ext.id) }));
40
+ }
41
+ return _jsx(Component, { ...props }, ext.id);
42
+ }
43
+ const Wrapper = Container || "div";
44
+ return (_jsx(Wrapper, { className: className, children: extensions.map((ext) => {
45
+ const Component = ext.component;
46
+ if (!Component)
47
+ return null;
48
+ return _jsx(Component, { ...props }, ext.id);
49
+ }) }));
50
+ }
51
+ export { PluginExtensionPoint as ExtensionPoint };
@@ -0,0 +1,11 @@
1
+ import type React from "react";
2
+ interface PluginWidgetAreaClientProps {
3
+ area: string;
4
+ className?: string;
5
+ props?: any;
6
+ fallback?: React.ReactNode;
7
+ as?: React.ElementType;
8
+ }
9
+ export declare function usePluginWidgets(area: string): any[];
10
+ export declare function PluginWidgetAreaClient({ area, className, props, fallback, as: Container, }: PluginWidgetAreaClientProps): any;
11
+ export { PluginWidgetAreaClient as WidgetAreaClient };
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { getPluginWidgetsAction } from "../actions/index";
5
+ import { onPluginsChanged } from "../common/events";
6
+ export function usePluginWidgets(area) {
7
+ const [widgets, setWidgets] = useState([]);
8
+ const fetchWidgets = useCallback(() => {
9
+ getPluginWidgetsAction(area).then(setWidgets);
10
+ }, [area]);
11
+ useEffect(() => {
12
+ fetchWidgets();
13
+ const unsubscribe = onPluginsChanged(() => {
14
+ fetchWidgets();
15
+ });
16
+ return unsubscribe;
17
+ }, [fetchWidgets]);
18
+ return widgets;
19
+ }
20
+ export function PluginWidgetAreaClient({ area, className, props, fallback, as: Container = "div", }) {
21
+ const widgets = usePluginWidgets(area);
22
+ if (widgets.length === 0)
23
+ return fallback || null;
24
+ return (_jsx(Container, { className: className, children: widgets.map((widget) => {
25
+ const Component = widget.component;
26
+ if (!Component)
27
+ return null;
28
+ return _jsx(Component, { ...props }, widget.id);
29
+ }) }));
30
+ }
31
+ export { PluginWidgetAreaClient as WidgetAreaClient };
@@ -0,0 +1,10 @@
1
+ import type React from "react";
2
+ interface PluginWidgetAreaProps {
3
+ area: string;
4
+ className?: string;
5
+ props?: any;
6
+ fallback?: React.ReactNode;
7
+ as?: React.ElementType;
8
+ }
9
+ export declare function PluginWidgetArea({ area, className, props, fallback, as: Container, }: PluginWidgetAreaProps): Promise<string | number | bigint | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined>;
10
+ export { PluginWidgetArea as WidgetArea };
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getCurrentSession } from "@veap/auth";
3
+ import { pluginsContext } from "../common/context";
4
+ export async function PluginWidgetArea({ area, className, props, fallback, as: Container = "div", }) {
5
+ const { user } = await getCurrentSession();
6
+ const userRoles = user?.roles || [];
7
+ const userPermissions = user?.permissions || [];
8
+ const widgets = await pluginsContext().registry.getWidgets(area, {
9
+ roles: userRoles,
10
+ permissions: userPermissions,
11
+ });
12
+ if (widgets.length === 0)
13
+ return fallback || null;
14
+ return (_jsx(Container, { className: className, children: widgets.map((widget) => {
15
+ const Component = widget.component;
16
+ return _jsx(Component, { ...props }, widget.id);
17
+ }) }));
18
+ }
19
+ export { PluginWidgetArea as WidgetArea };
@@ -0,0 +1,19 @@
1
+ export type ConfirmOptions = {
2
+ action?: string;
3
+ title?: string;
4
+ description?: string;
5
+ preferredMethod?: string;
6
+ rememberMinutes?: number;
7
+ metadata?: Record<string, any>;
8
+ };
9
+ export type ConfirmResult = {
10
+ confirmed: boolean;
11
+ method?: string;
12
+ };
13
+ /**
14
+ * Hook for confirming sensitive actions via password/passkey.
15
+ *
16
+ * If the action-confirm plugin is enabled, shows an action verification dialog.
17
+ * If it's disabled, falls back to the native browser confirm().
18
+ */
19
+ export declare function useConfirmAction(): (options: ConfirmOptions) => Promise<ConfirmResult>;
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { eventBus } from "@veap/kernel/events";
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ /**
5
+ * Hook for confirming sensitive actions via password/passkey.
6
+ *
7
+ * If the action-confirm plugin is enabled, shows an action verification dialog.
8
+ * If it's disabled, falls back to the native browser confirm().
9
+ */
10
+ export function useConfirmAction() {
11
+ const pendingRequests = useRef(new Map());
12
+ const subscriberId = useRef(`use-confirm-action-${Math.random().toString(36).slice(2, 9)}`).current;
13
+ useEffect(() => {
14
+ const responseHandler = (event) => {
15
+ const { requestId, confirmed, method } = event.payload;
16
+ const pending = pendingRequests.current.get(requestId);
17
+ if (pending) {
18
+ clearTimeout(pending.timeout);
19
+ pendingRequests.current.delete(requestId);
20
+ pending.resolve({ confirmed, method });
21
+ }
22
+ };
23
+ eventBus.subscribe("action:confirm:response", subscriberId, responseHandler);
24
+ return () => {
25
+ eventBus.unsubscribe("action:confirm:response", subscriberId);
26
+ for (const [, pending] of pendingRequests.current) {
27
+ clearTimeout(pending.timeout);
28
+ }
29
+ pendingRequests.current.clear();
30
+ };
31
+ }, [subscriberId]);
32
+ const confirmAction = useCallback((options) => {
33
+ return new Promise((resolve) => {
34
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
35
+ const ackSubId = `${subscriberId}-ack-${requestId}`;
36
+ const fallbackTimeout = setTimeout(() => {
37
+ eventBus.unsubscribe("action:confirm:ack", ackSubId);
38
+ const confirmed = window.confirm(options.description || options.title || "Confirm action?");
39
+ resolve({ confirmed });
40
+ }, 500);
41
+ const ackHandler = (event) => {
42
+ if (event.payload?.requestId !== requestId)
43
+ return;
44
+ eventBus.unsubscribe("action:confirm:ack", ackSubId);
45
+ clearTimeout(fallbackTimeout);
46
+ const responseTimeout = setTimeout(() => {
47
+ pendingRequests.current.delete(requestId);
48
+ resolve({ confirmed: false });
49
+ }, 5 * 60 * 1000);
50
+ pendingRequests.current.set(requestId, {
51
+ resolve,
52
+ timeout: responseTimeout,
53
+ });
54
+ };
55
+ eventBus.subscribe("action:confirm:ack", ackSubId, ackHandler);
56
+ eventBus.publish("action:confirm:request", {
57
+ requestId,
58
+ action: options.action ?? "confirm",
59
+ title: options.title ?? "Confirm action",
60
+ description: options.description,
61
+ preferredMethod: options.preferredMethod,
62
+ rememberMinutes: options.rememberMinutes,
63
+ metadata: options.metadata,
64
+ });
65
+ });
66
+ }, [subscriberId]);
67
+ return confirmAction;
68
+ }
@@ -0,0 +1,21 @@
1
+ import type React from "react";
2
+ interface WidgetState {
3
+ id: string;
4
+ enabled: boolean;
5
+ colSpan: number;
6
+ rowSpan?: number;
7
+ }
8
+ interface WidgetComposerClientProps {
9
+ slot: string;
10
+ defaultWidgets: {
11
+ id: string;
12
+ name: string;
13
+ component: React.ReactNode;
14
+ defaultColSpan?: number;
15
+ defaultRowSpan?: number;
16
+ }[];
17
+ userState: WidgetState[] | null;
18
+ columns?: number;
19
+ }
20
+ export declare function WidgetComposerClient({ slot, defaultWidgets, userState, columns, }: WidgetComposerClientProps): React.JSX.Element | null;
21
+ export {};
@@ -0,0 +1,281 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core";
4
+ import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable";
5
+ import { CSS } from "@dnd-kit/utilities";
6
+ import { Icon } from "@iconify/react";
7
+ import { useTranslation } from "@veap/intl";
8
+ import { Button, cn } from "@veap/ui";
9
+ import { Grip, Trash, UnfoldHorizontal, UnfoldVertical } from "lucide-react";
10
+ import { useEffect, useState } from "react";
11
+ import { toast } from "sonner";
12
+ import { saveUserWidgetsState } from "../actions/index";
13
+ const activeEditSections = new Set();
14
+ const sectionHandlers = new Map();
15
+ let listenerCount = 0;
16
+ function registerSection(slot, handler) {
17
+ sectionHandlers.set(slot, handler);
18
+ return () => {
19
+ sectionHandlers.delete(slot);
20
+ activeEditSections.delete(slot);
21
+ };
22
+ }
23
+ function setSectionEditMode(slot, edit) {
24
+ if (edit) {
25
+ activeEditSections.add(slot);
26
+ }
27
+ else {
28
+ activeEditSections.delete(slot);
29
+ }
30
+ sectionHandlers.get(slot)?.setEdit(edit);
31
+ }
32
+ function toggleAllSections() {
33
+ const anyActive = activeEditSections.size > 0;
34
+ if (anyActive) {
35
+ for (const [slot, handler] of sectionHandlers.entries()) {
36
+ handler.revert();
37
+ handler.setEdit(false);
38
+ }
39
+ activeEditSections.clear();
40
+ }
41
+ else {
42
+ for (const [slot, handler] of sectionHandlers.entries()) {
43
+ activeEditSections.add(slot);
44
+ handler.setEdit(true);
45
+ }
46
+ }
47
+ }
48
+ function closeAllSections() {
49
+ for (const handler of sectionHandlers.values()) {
50
+ handler.revert();
51
+ handler.setEdit(false);
52
+ }
53
+ activeEditSections.clear();
54
+ }
55
+ function handleGlobalKeyDown(e) {
56
+ const isKeyW = e.key?.toLowerCase() === "w" || e.code === "KeyW";
57
+ const isKeyE = e.key?.toLowerCase() === "e" || e.code === "KeyE";
58
+ // Alt+W, Alt+E, Cmd+W, Ctrl+W, Ctrl+Shift+W
59
+ const isAltShortcut = e.altKey && (isKeyW || isKeyE);
60
+ const isCmdOrCtrlW = (e.metaKey || e.ctrlKey) && isKeyW;
61
+ if (isAltShortcut || isCmdOrCtrlW) {
62
+ const target = e.target;
63
+ const isInputField = target &&
64
+ (target.tagName === "INPUT" ||
65
+ target.tagName === "TEXTAREA" ||
66
+ target.isContentEditable);
67
+ if (isInputField && !e.metaKey && !e.altKey) {
68
+ return;
69
+ }
70
+ e.preventDefault();
71
+ e.stopPropagation();
72
+ toggleAllSections();
73
+ }
74
+ else if (e.key === "Escape") {
75
+ if (activeEditSections.size > 0) {
76
+ e.preventDefault();
77
+ closeAllSections();
78
+ }
79
+ }
80
+ }
81
+ function handleCustomToggle() {
82
+ toggleAllSections();
83
+ }
84
+ function handleCustomClose() {
85
+ closeAllSections();
86
+ }
87
+ function attachGlobalListener() {
88
+ listenerCount++;
89
+ if (listenerCount === 1 && typeof window !== "undefined") {
90
+ window.addEventListener("keydown", handleGlobalKeyDown);
91
+ window.addEventListener("veap:toggle-widgets-edit", handleCustomToggle);
92
+ window.addEventListener("veap:close-widgets-edit", handleCustomClose);
93
+ }
94
+ }
95
+ function detachGlobalListener() {
96
+ listenerCount = Math.max(0, listenerCount - 1);
97
+ if (listenerCount === 0 && typeof window !== "undefined") {
98
+ window.removeEventListener("keydown", handleGlobalKeyDown);
99
+ window.removeEventListener("veap:toggle-widgets-edit", handleCustomToggle);
100
+ window.removeEventListener("veap:close-widgets-edit", handleCustomClose);
101
+ }
102
+ }
103
+ function computeInitialState(userState, defaultWidgets, columns) {
104
+ if (userState && userState.length > 0) {
105
+ const defaultMap = new Map(defaultWidgets.map((dw) => [dw.id, dw]));
106
+ const stateMap = new Map(userState.map((w) => [w.id, w]));
107
+ const newState = userState
108
+ .filter((w) => w.id.startsWith("spacer-") || defaultMap.has(w.id))
109
+ .map((w) => {
110
+ const def = defaultMap.get(w.id);
111
+ return {
112
+ ...w,
113
+ colSpan: w.colSpan ?? Math.min(def?.defaultColSpan ?? 1, columns),
114
+ rowSpan: w.rowSpan ?? def?.defaultRowSpan ?? 1,
115
+ };
116
+ });
117
+ for (const w of defaultWidgets) {
118
+ if (!stateMap.has(w.id)) {
119
+ newState.push({
120
+ id: w.id,
121
+ enabled: true,
122
+ colSpan: Math.min(w.defaultColSpan ?? 1, columns),
123
+ rowSpan: w.defaultRowSpan ?? 1,
124
+ });
125
+ }
126
+ }
127
+ return newState;
128
+ }
129
+ else {
130
+ return defaultWidgets.map((w) => ({
131
+ id: w.id,
132
+ enabled: true,
133
+ colSpan: Math.min(w.defaultColSpan ?? 1, columns),
134
+ rowSpan: w.defaultRowSpan ?? 1,
135
+ }));
136
+ }
137
+ }
138
+ function SortableWidget({ id, widget, isEditMode, colSpan, rowSpan = 1, onToggle, onResize, onResizeRow, maxColumns, }) {
139
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({
140
+ id,
141
+ });
142
+ const { t } = useTranslation();
143
+ const isSpacer = id.startsWith("spacer-");
144
+ const style = {
145
+ transform: CSS.Transform.toString(transform),
146
+ transition,
147
+ zIndex: isDragging ? 10 : 1,
148
+ opacity: isDragging ? 0.8 : 1,
149
+ gridColumn: `span ${Math.min(colSpan, maxColumns)} / span ${Math.min(colSpan, maxColumns)}`,
150
+ gridRow: `span ${rowSpan} / span ${rowSpan}`,
151
+ };
152
+ return (_jsxs("div", { ref: setNodeRef, style: style, className: `relative flex flex-col ${isEditMode ? (isSpacer ? "border-primary/50 bg-primary/5 min-h-[100px] rounded-xl border border-dashed" : "squircle -m-2 rounded-xl border border-dashed p-2") : ""}`, children: [isEditMode && (_jsxs("div", { className: "bg-background squircle absolute -top-3 right-3 z-20 flex items-center gap-1 rounded-lg border p-1 shadow-sm", children: [_jsx("button", { type: "button", className: "hover:bg-muted cursor-pointer rounded p-1", onClick: () => {
153
+ const nextSpan = colSpan >= maxColumns ? 1 : colSpan + 1;
154
+ onResize(id, nextSpan);
155
+ }, title: t("widgets.change-width"), children: _jsx(UnfoldHorizontal, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", className: "hover:bg-muted cursor-pointer rounded p-1", onClick: () => {
156
+ const nextRowSpan = rowSpan >= 4 ? 1 : rowSpan + 1;
157
+ onResizeRow(id, nextRowSpan);
158
+ }, title: t("widgets.change-row-span"), children: _jsx(UnfoldVertical, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", className: "hover:bg-muted text-destructive cursor-pointer rounded p-1", onClick: () => onToggle(id), title: isSpacer
159
+ ? t("widgets.remove-spacer")
160
+ : t("widgets.disable-widget"), children: _jsx(Trash, { className: "h-4 w-4" }) }), _jsx("div", { ...attributes, ...listeners, className: "hover:bg-muted cursor-grab rounded p-1 active:cursor-grabbing", title: t("widgets.drag-to-move"), children: _jsx(Grip, { className: "h-4 w-4" }) })] })), _jsx("div", { className: `h-full w-full flex-1 [&>*]:min-h-full [&>*]:w-full ${isEditMode ? "pointer-events-none opacity-50" : ""}`, children: widget?.component })] }));
161
+ }
162
+ export function WidgetComposerClient({ slot, defaultWidgets, userState, columns = 4, }) {
163
+ const [isEditMode, setIsEditMode] = useState(false);
164
+ const [widgetsState, setWidgetsState] = useState(() => computeInitialState(userState, defaultWidgets, columns));
165
+ const [savedWidgetsState, setSavedWidgetsState] = useState(() => computeInitialState(userState, defaultWidgets, columns));
166
+ const [isSaving, setIsSaving] = useState(false);
167
+ const { t } = useTranslation();
168
+ useEffect(() => {
169
+ const initial = computeInitialState(userState, defaultWidgets, columns);
170
+ setWidgetsState(initial);
171
+ setSavedWidgetsState(initial);
172
+ }, [userState, defaultWidgets, columns]);
173
+ useEffect(() => {
174
+ attachGlobalListener();
175
+ const unsubscribe = registerSection(slot, {
176
+ setEdit: setIsEditMode,
177
+ revert: () => {
178
+ setWidgetsState(savedWidgetsState);
179
+ },
180
+ });
181
+ return () => {
182
+ unsubscribe();
183
+ detachGlobalListener();
184
+ };
185
+ }, [slot, savedWidgetsState]);
186
+ const resetToDefaultLayout = () => {
187
+ setWidgetsState(defaultWidgets.map((w) => ({
188
+ id: w.id,
189
+ enabled: true,
190
+ colSpan: Math.min(w.defaultColSpan ?? 1, columns),
191
+ rowSpan: w.defaultRowSpan ?? 1,
192
+ })));
193
+ };
194
+ const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
195
+ coordinateGetter: sortableKeyboardCoordinates,
196
+ }));
197
+ const handleDragEnd = (event) => {
198
+ const { active, over } = event;
199
+ if (over && active.id !== over.id) {
200
+ setWidgetsState((items) => {
201
+ const oldIndex = items.findIndex((i) => i.id === active.id);
202
+ const newIndex = items.findIndex((i) => i.id === over.id);
203
+ return arrayMove(items, oldIndex, newIndex);
204
+ });
205
+ }
206
+ };
207
+ const toggleWidget = (id) => {
208
+ if (id.startsWith("spacer-")) {
209
+ setWidgetsState((items) => items.filter((i) => i.id !== id));
210
+ }
211
+ else {
212
+ setWidgetsState((items) => items.map((i) => (i.id === id ? { ...i, enabled: !i.enabled } : i)));
213
+ }
214
+ };
215
+ const resizeWidget = (id, newColSpan) => {
216
+ setWidgetsState((items) => items.map((i) => (i.id === id ? { ...i, colSpan: newColSpan } : i)));
217
+ };
218
+ const resizeRowWidget = (id, newRowSpan) => {
219
+ setWidgetsState((items) => items.map((i) => (i.id === id ? { ...i, rowSpan: newRowSpan } : i)));
220
+ };
221
+ const addSpacer = () => {
222
+ const newId = `spacer-${Date.now()}`;
223
+ setWidgetsState((items) => [
224
+ ...items,
225
+ { id: newId, enabled: true, colSpan: 1, rowSpan: 1 },
226
+ ]);
227
+ };
228
+ const handleCancel = () => {
229
+ setWidgetsState(savedWidgetsState);
230
+ setSectionEditMode(slot, false);
231
+ };
232
+ const handleSave = async () => {
233
+ setIsSaving(true);
234
+ try {
235
+ await saveUserWidgetsState(slot, widgetsState);
236
+ setSavedWidgetsState(widgetsState);
237
+ setSectionEditMode(slot, false);
238
+ toast.success(t("widgets.save-success"));
239
+ }
240
+ catch (_e) {
241
+ toast.error(t("widgets.save-error"));
242
+ }
243
+ finally {
244
+ setIsSaving(false);
245
+ }
246
+ };
247
+ const enabledWidgets = widgetsState.filter((w) => w.enabled);
248
+ const disabledWidgets = widgetsState.filter((w) => !w.enabled && !w.id.startsWith("spacer-"));
249
+ if (defaultWidgets.length === 0) {
250
+ return null;
251
+ }
252
+ return (_jsxs("div", { className: cn("flex flex-col gap-4 transition-all duration-200", isEditMode &&
253
+ "border-primary/40 bg-primary/[0.02] dark:bg-primary/[0.04] rounded-2xl border-2 border-dashed p-4 shadow-sm"), children: [isEditMode && (_jsxs("div", { className: "border-primary/20 flex flex-wrap items-center justify-between gap-3 border-b border-dashed pb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsxs("span", { className: "text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase", children: [_jsx(Icon, { icon: "solar:widget-add-linear", className: "text-primary h-4 w-4" }), slot.replace(/-/g, " ")] }) }), _jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: addSpacer, className: "squircle text-xs", children: [_jsx(Icon, { icon: "solar:programming-linear", className: "mr-1.5 h-3.5 w-3.5" }), t("widgets.add-spacer")] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: resetToDefaultLayout, className: "squircle text-xs", children: [_jsx(Icon, { icon: "solar:restart-bold", className: "mr-1.5 h-3.5 w-3.5" }), t("widgets.reset-layout")] }), _jsx(Button, { variant: "outline", size: "sm", onClick: handleCancel, className: "squircle text-xs", children: t("widgets.cancel") }), _jsx(Button, { size: "sm", onClick: handleSave, disabled: isSaving, className: "squircle text-xs", children: isSaving ? t("widgets.saving") : t("widgets.save-layout") })] })] })), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: _jsx("div", { className: "grid grid-cols-1 gap-6 xl:grid-flow-row-dense", style: {
254
+ gridTemplateColumns: typeof window !== "undefined" && window.innerWidth >= 640
255
+ ? `repeat(${columns}, minmax(0, 1fr))`
256
+ : undefined,
257
+ gridAutoRows: typeof window !== "undefined" && window.innerWidth >= 640
258
+ ? "minmax(120px, auto)"
259
+ : undefined,
260
+ gridAutoFlow: typeof window !== "undefined" && window.innerWidth >= 640
261
+ ? "row dense"
262
+ : undefined,
263
+ }, children: _jsx(SortableContext, { items: enabledWidgets.map((w) => w.id), strategy: rectSortingStrategy, children: enabledWidgets.map((ws) => {
264
+ const isSpacer = ws.id.startsWith("spacer-");
265
+ const widgetDef = isSpacer
266
+ ? {
267
+ id: ws.id,
268
+ name: "Spacer",
269
+ component: (_jsx("div", { className: "invisible h-full min-h-[100px] w-full" })),
270
+ }
271
+ : defaultWidgets.find((w) => w.id === ws.id);
272
+ if (!widgetDef)
273
+ return null;
274
+ return (_jsx(SortableWidget, { id: ws.id, widget: widgetDef, isEditMode: isEditMode, colSpan: ws.colSpan, rowSpan: ws.rowSpan, onToggle: toggleWidget, onResize: resizeWidget, onResizeRow: resizeRowWidget, maxColumns: columns }, ws.id));
275
+ }) }) }) }), isEditMode && disabledWidgets.length > 0 && (_jsxs("div", { className: "bg-muted/30 mt-4 rounded-xl border p-4", children: [_jsx("h4", { className: "mb-3 text-sm font-medium", children: t("widgets.available-widgets") }), _jsx("div", { className: "flex flex-wrap gap-2", children: disabledWidgets.map((ws) => {
276
+ const widgetDef = defaultWidgets.find((w) => w.id === ws.id);
277
+ if (!widgetDef)
278
+ return null;
279
+ return (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleWidget(ws.id), children: [_jsx(Icon, { icon: "solar:add-circle-linear", className: "mr-2 h-4 w-4" }), widgetDef.name] }, ws.id));
280
+ }) })] }))] }));
281
+ }
@@ -0,0 +1,8 @@
1
+ import type React from "react";
2
+ interface WidgetComposerProps {
3
+ slot: string;
4
+ columns?: number;
5
+ fallback?: React.ReactNode;
6
+ }
7
+ export declare function WidgetComposer({ slot, columns, fallback, }: WidgetComposerProps): Promise<string | number | bigint | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined>;
8
+ export {};
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getCurrentSession } from "@veap/auth";
3
+ import { getUserWidgetsState } from "../actions/index";
4
+ import { pluginsContext } from "../common/context";
5
+ import { WidgetComposerClient } from "./widget-composer-client";
6
+ export async function WidgetComposer({ slot, columns = 4, fallback, }) {
7
+ const { user } = await getCurrentSession();
8
+ const userRoles = user?.roles || [];
9
+ const userPermissions = user?.permissions || [];
10
+ const defaultWidgetsRaw = await pluginsContext().registry.getWidgets(slot, {
11
+ roles: userRoles,
12
+ permissions: userPermissions,
13
+ });
14
+ if (defaultWidgetsRaw.length === 0)
15
+ return fallback || null;
16
+ const userState = await getUserWidgetsState(slot);
17
+ const defaultWidgets = defaultWidgetsRaw.map((w) => {
18
+ const Component = w.component;
19
+ return {
20
+ id: w.id,
21
+ name: w.name,
22
+ component: _jsx(Component, {}),
23
+ defaultColSpan: w.defaultColSpan,
24
+ defaultRowSpan: w.defaultRowSpan,
25
+ };
26
+ });
27
+ return (_jsx(WidgetComposerClient, { slot: slot, defaultWidgets: defaultWidgets, userState: userState, columns: columns }));
28
+ }