@wordrhyme/plugin 0.1.0-alpha.10

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 (57) hide show
  1. package/dist/admin/index.d.ts +19 -0
  2. package/dist/admin/index.js +42 -0
  3. package/dist/admin/index.js.map +1 -0
  4. package/dist/artifact.d.ts +17 -0
  5. package/dist/artifact.js +97 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/chunk-3IM3FPTJ.js +1470 -0
  8. package/dist/chunk-3IM3FPTJ.js.map +1 -0
  9. package/dist/chunk-6GDCFR67.js +218 -0
  10. package/dist/chunk-6GDCFR67.js.map +1 -0
  11. package/dist/chunk-7EM22QYB.js +333 -0
  12. package/dist/chunk-7EM22QYB.js.map +1 -0
  13. package/dist/chunk-BI7E5CVM.js +26 -0
  14. package/dist/chunk-BI7E5CVM.js.map +1 -0
  15. package/dist/chunk-BVOTSKM2.js +254 -0
  16. package/dist/chunk-BVOTSKM2.js.map +1 -0
  17. package/dist/chunk-DY44Q4CK.js +188 -0
  18. package/dist/chunk-DY44Q4CK.js.map +1 -0
  19. package/dist/chunk-MZOLSLJ7.js +65 -0
  20. package/dist/chunk-MZOLSLJ7.js.map +1 -0
  21. package/dist/chunk-O4AYK3YP.js +156 -0
  22. package/dist/chunk-O4AYK3YP.js.map +1 -0
  23. package/dist/chunk-UGMYO6AU.js +37 -0
  24. package/dist/chunk-UGMYO6AU.js.map +1 -0
  25. package/dist/client-poND5ovI.d.ts +679 -0
  26. package/dist/client.d.ts +9 -0
  27. package/dist/client.js +68 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/dev-utils.d.ts +105 -0
  30. package/dist/dev-utils.js +35 -0
  31. package/dist/dev-utils.js.map +1 -0
  32. package/dist/entity-extensions-CnhKoT4k.d.ts +56 -0
  33. package/dist/globalization.d.ts +85 -0
  34. package/dist/globalization.js +53 -0
  35. package/dist/globalization.js.map +1 -0
  36. package/dist/index.d.ts +724 -0
  37. package/dist/index.js +931 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/locale.d.ts +15 -0
  40. package/dist/locale.js +13 -0
  41. package/dist/locale.js.map +1 -0
  42. package/dist/manifest-CTFX-h0w.d.ts +2053 -0
  43. package/dist/react.d.ts +227 -0
  44. package/dist/react.js +226 -0
  45. package/dist/react.js.map +1 -0
  46. package/dist/release-BDJeO54k.d.ts +230 -0
  47. package/dist/server.d.ts +115 -0
  48. package/dist/server.js +194 -0
  49. package/dist/server.js.map +1 -0
  50. package/dist/time.d.ts +34 -0
  51. package/dist/time.js +31 -0
  52. package/dist/time.js.map +1 -0
  53. package/dist/trpc.d.ts +44 -0
  54. package/dist/trpc.js +11 -0
  55. package/dist/trpc.js.map +1 -0
  56. package/dist/types-BJo3_91V.d.ts +1997 -0
  57. package/package.json +92 -0
@@ -0,0 +1,227 @@
1
+ import React from 'react';
2
+ import { W as WebSlotRemoteExtension, P as PluginMediaInfo } from './types-BJo3_91V.js';
3
+ import { CurrencyInfo, FormatPriceOptions } from './globalization.js';
4
+ import './release-BDJeO54k.js';
5
+ import 'zod';
6
+ import './time.js';
7
+ import 'decimal.js';
8
+ import './locale.js';
9
+
10
+ /**
11
+ * React utilities for plugin admin UIs.
12
+ *
13
+ * At runtime, this module is shared via Module Federation from the admin host.
14
+ * Plugins should configure MF shared with `import: false` for this package.
15
+ */
16
+
17
+ /**
18
+ * Called by the Admin Host at startup to inject the host's tRPC React client.
19
+ * Plugin code should NOT call this directly.
20
+ * @internal
21
+ */
22
+ declare function __setTrpc(trpc: any): void;
23
+ /**
24
+ * Hook for plugin components to access their tRPC namespace with React Query hooks.
25
+ * Must be called within a component rendered inside the Admin Host.
26
+ *
27
+ * @param pluginId - Short plugin ID (e.g., 'storage-s3') or full ID (e.g., 'com.wordrhyme.storage-s3')
28
+ * @returns tRPC proxy with useQuery/useMutation hooks
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * import { usePluginTrpc } from '@wordrhyme/plugin/react';
33
+ *
34
+ * function MyPluginPage() {
35
+ * const api = usePluginTrpc('my-plugin');
36
+ * const { data, isLoading } = api.listItems.useQuery();
37
+ * const createItem = api.createItem.useMutation();
38
+ * }
39
+ * ```
40
+ */
41
+ declare function usePluginTrpc(pluginId: string): any;
42
+ /**
43
+ * Hook for plugin components to access the Admin Host's root tRPC React client.
44
+ * Useful for shared host capabilities such as currency, media, auth, etc.
45
+ */
46
+ declare function useHostTrpc(): any;
47
+ interface PluginI18nValue {
48
+ pluginId?: string | undefined;
49
+ namespace?: string | undefined;
50
+ locale: string;
51
+ timezone: string;
52
+ isLoading: boolean;
53
+ availableLocales: readonly string[];
54
+ languages: readonly PluginLanguageOption[];
55
+ t: (key: string, paramsOrDefault?: Record<string, string | number> | string) => string;
56
+ /** Host-bound Core common translator. Optional for compatibility with older Hosts. */
57
+ tCommon?: (key: string, paramsOrDefault?: Record<string, string | number> | string) => string;
58
+ addNamespace: (namespace: string) => void;
59
+ }
60
+ interface PluginLanguageOption {
61
+ locale: string;
62
+ name: string;
63
+ nativeName: string;
64
+ direction: 'ltr' | 'rtl';
65
+ isDefault: boolean;
66
+ sortOrder: number;
67
+ }
68
+ declare const PluginI18nContext: React.Context<PluginI18nValue>;
69
+ declare function usePluginTranslation(pluginId: string, customNamespace?: string): {
70
+ t: (key: string, paramsOrDefault?: Record<string, string | number> | string) => string;
71
+ tCommon: (key: string, paramsOrDefault?: Record<string, string | number> | string) => string;
72
+ locale: string;
73
+ timezone: string;
74
+ isLoading: boolean;
75
+ availableLocales: readonly string[];
76
+ languages: readonly PluginLanguageOption[];
77
+ namespace: string;
78
+ };
79
+ /** Currency capabilities exposed by the Admin Host to plugin UIs. */
80
+ interface PluginCurrencyValue {
81
+ currency: CurrencyInfo;
82
+ currencies: CurrencyInfo[];
83
+ baseCurrency: CurrencyInfo;
84
+ isLoading: boolean;
85
+ isReady: boolean;
86
+ changeCurrency: (currencyCode: string) => void;
87
+ formatPrice: (cents: number, fromCurrency?: string | undefined, options?: FormatPriceOptions | undefined) => string;
88
+ }
89
+ declare const PluginCurrencyContext: React.Context<PluginCurrencyValue>;
90
+ declare function usePluginCurrency(): PluginCurrencyValue;
91
+ interface PluginSlotProps {
92
+ /** Slot name, e.g. 'shop.product.detail.actions' */
93
+ name: string;
94
+ /** React props passed to extensions rendered in this slot */
95
+ props?: Record<string, unknown>;
96
+ /** @deprecated Use props. Kept for legacy plugin slot call sites. */
97
+ context?: Record<string, unknown>;
98
+ /** Layout mode */
99
+ layout?: 'inline' | 'stack' | 'tabs' | 'grid';
100
+ /** Additional CSS class */
101
+ className?: string;
102
+ /** Fallback when no extensions registered */
103
+ fallback?: React.ReactNode;
104
+ }
105
+ interface PluginSlotRegistry {
106
+ /** Returns whether at least one extension is registered for the slot. */
107
+ has: (name: string) => boolean;
108
+ /** Subscribes to extension registry changes. */
109
+ subscribe: (onStoreChange: () => void) => () => void;
110
+ }
111
+ /**
112
+ * Called by the Admin Host at startup to inject the PluginSlot component.
113
+ * Plugin code should NOT call this directly.
114
+ * @internal
115
+ */
116
+ declare function __setPluginSlot(component: React.ComponentType<PluginSlotProps>): void;
117
+ /**
118
+ * Called by the Admin Host at startup to expose reactive slot availability.
119
+ * Plugin code should NOT call this directly.
120
+ * @internal
121
+ */
122
+ declare function __setPluginSlotRegistry(registry: PluginSlotRegistry | null): void;
123
+ /**
124
+ * Returns whether the given slot currently has at least one registered extension.
125
+ * The value updates when plugins are registered, reloaded, disabled, or removed.
126
+ */
127
+ declare function usePluginSlot(name: string): boolean;
128
+ /**
129
+ * Renders all plugin extensions registered for the given slot.
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * import { PluginSlot } from '@wordrhyme/plugin/react';
134
+ *
135
+ * function ProductDetailPage({ productId }) {
136
+ * return (
137
+ * <div>
138
+ * <h1>Product Detail</h1>
139
+ * <PluginSlot
140
+ * name="shop.product.detail.actions"
141
+ * layout="inline"
142
+ * props={{ productId }}
143
+ * />
144
+ * </div>
145
+ * );
146
+ * }
147
+ * ```
148
+ */
149
+ declare function PluginSlot(props: PluginSlotProps): React.ReactElement | null;
150
+ type WebPluginSlotLayout = 'bare' | 'inline' | 'stack' | 'grid';
151
+ interface WebPluginSlotRenderOptions {
152
+ name: string;
153
+ extensions: WebSlotRemoteExtension[];
154
+ props?: Record<string, unknown> | undefined;
155
+ layout?: WebPluginSlotLayout | undefined;
156
+ }
157
+ interface PublicWebPluginRuntime {
158
+ scan: (root?: ParentNode) => void;
159
+ renderSlot: (mount: HTMLElement, options: WebPluginSlotRenderOptions) => void;
160
+ unmountSlot: (mount: HTMLElement) => void;
161
+ }
162
+ declare const PUBLIC_PLUGIN_RUNTIME_READY_EVENT = "wr:public-plugin-runtime-ready";
163
+ declare global {
164
+ interface Window {
165
+ __WR_PUBLIC_PLUGIN_RUNTIME__?: PublicWebPluginRuntime;
166
+ }
167
+ }
168
+ interface WebPluginSlotProps extends WebPluginSlotRenderOptions {
169
+ className?: string | undefined;
170
+ }
171
+ /**
172
+ * Declares a public-web plugin slot inside an owning React tree.
173
+ *
174
+ * The owner keeps control of live props and React lifecycle. The Web Host owns
175
+ * remote loading and renders extensions into the empty mount element through
176
+ * its shared Module Federation React runtime.
177
+ */
178
+ declare function WebPluginSlot({ name, extensions, props, layout, className, }: WebPluginSlotProps): React.ReactElement | null;
179
+ /**
180
+ * Minimal ability interface for plugin permission checks.
181
+ * Matches CASL MongoAbility.can() signature.
182
+ */
183
+ interface PluginAbility {
184
+ can(action: string, subject: string): boolean;
185
+ }
186
+ /**
187
+ * React Context for ability — shared between host and plugins.
188
+ * Host injects via AbilityBridge, plugins consume via usePluginAbility.
189
+ */
190
+ declare const PluginAbilityContext: React.Context<PluginAbility>;
191
+ /**
192
+ * Hook for plugin components to check permissions.
193
+ *
194
+ * @example
195
+ * ```tsx
196
+ * import { usePluginAbility } from '@wordrhyme/plugin/react';
197
+ *
198
+ * function MyPluginPage() {
199
+ * const ability = usePluginAbility();
200
+ * const canCreate = ability.can('create', 'PluginResource');
201
+ * const canDelete = ability.can('delete', 'PluginResource');
202
+ * }
203
+ * ```
204
+ */
205
+ declare function usePluginAbility(): PluginAbility;
206
+
207
+ interface MediaPickerOptions {
208
+ presentation?: 'dialog' | 'drawer';
209
+ mode?: 'single' | 'multi';
210
+ category?: 'all' | 'image' | 'document' | 'video' | 'audio' | 'archive' | 'other';
211
+ /** Allow callers to select an external image by entering its URL in the picker. */
212
+ allowExternalUrl?: boolean;
213
+ /** Prefer importing URL images into Media, or only allow a transient external reference. */
214
+ externalUrlMode?: 'import-preferred' | 'reference-only';
215
+ /** Publish selected images when needed and include an absolute public URL in each result. */
216
+ requirePublicUrl?: boolean;
217
+ onSelect: (media: PluginMediaInfo[]) => void;
218
+ }
219
+ /**
220
+ * Hook to open the global Media Picker dialog (rendered by Admin Host).
221
+ * Returns an imperative API `openDialog(options)`.
222
+ */
223
+ declare function useMediaPicker(): {
224
+ openDialog: (options: MediaPickerOptions) => void;
225
+ };
226
+
227
+ export { type MediaPickerOptions, PUBLIC_PLUGIN_RUNTIME_READY_EVENT, type PluginAbility, PluginAbilityContext, PluginCurrencyContext, type PluginCurrencyValue, PluginI18nContext, type PluginI18nValue, type PluginLanguageOption, PluginSlot, type PluginSlotProps, type PluginSlotRegistry, type PublicWebPluginRuntime, WebPluginSlot, type WebPluginSlotLayout, type WebPluginSlotProps, type WebPluginSlotRenderOptions, __setPluginSlot, __setPluginSlotRegistry, __setTrpc, useHostTrpc, useMediaPicker, usePluginAbility, usePluginCurrency, usePluginSlot, usePluginTranslation, usePluginTrpc };
package/dist/react.js ADDED
@@ -0,0 +1,226 @@
1
+ import {
2
+ DEFAULT_CURRENCY
3
+ } from "./chunk-BVOTSKM2.js";
4
+ import "./chunk-BI7E5CVM.js";
5
+
6
+ // src/react.ts
7
+ import React from "react";
8
+ var _trpc = null;
9
+ var _hostTrpcFacade = null;
10
+ function __setTrpc(trpc) {
11
+ _trpc = trpc;
12
+ _hostTrpcFacade = null;
13
+ }
14
+ function usePluginTrpc(pluginId) {
15
+ if (!_trpc) {
16
+ throw new Error(
17
+ "[plugin/react] tRPC not initialized. Ensure plugin is loaded within Admin Host context."
18
+ );
19
+ }
20
+ const normalizedId = pluginId.replace(/^com\.wordrhyme\./, "").replace(/\./g, "-");
21
+ return _trpc.pluginApis[normalizedId];
22
+ }
23
+ function useHostTrpc() {
24
+ if (!_trpc) {
25
+ throw new Error(
26
+ "[plugin/react] tRPC not initialized. Ensure plugin is loaded within Admin Host context."
27
+ );
28
+ }
29
+ if (!_hostTrpcFacade) {
30
+ _hostTrpcFacade = new Proxy(_trpc, {
31
+ get(target, property, receiver) {
32
+ if (property === "i18n") {
33
+ throw new Error(
34
+ "[plugin/react] Host i18n APIs are not exposed to plugins. Use the Host-bound usePluginTranslation() bridge."
35
+ );
36
+ }
37
+ return Reflect.get(target, property, receiver);
38
+ },
39
+ has(target, property) {
40
+ return property === "i18n" ? false : Reflect.has(target, property);
41
+ },
42
+ ownKeys(target) {
43
+ return Reflect.ownKeys(target).filter((property) => property !== "i18n");
44
+ }
45
+ });
46
+ }
47
+ return _hostTrpcFacade;
48
+ }
49
+ var fallbackPluginTranslate = (key, paramsOrDefault) => typeof paramsOrDefault === "string" ? paramsOrDefault : key;
50
+ var defaultI18n = {
51
+ locale: "en-US",
52
+ timezone: "UTC",
53
+ isLoading: false,
54
+ availableLocales: [],
55
+ languages: [],
56
+ t: fallbackPluginTranslate,
57
+ tCommon: fallbackPluginTranslate,
58
+ addNamespace: () => void 0
59
+ };
60
+ var PluginI18nContext = React.createContext(defaultI18n);
61
+ function normalizePluginNamespace(pluginId) {
62
+ return `plugin.${pluginId}`;
63
+ }
64
+ function usePluginTranslation(pluginId, customNamespace) {
65
+ const i18n = React.useContext(PluginI18nContext);
66
+ if (!i18n.pluginId || !i18n.namespace) {
67
+ throw new Error("[plugin/react] Plugin i18n boundary is missing");
68
+ }
69
+ if (pluginId !== i18n.pluginId) {
70
+ throw new Error(
71
+ `[plugin/react] Plugin i18n identity mismatch: expected ${i18n.pluginId}, received ${pluginId}`
72
+ );
73
+ }
74
+ const namespace = normalizePluginNamespace(pluginId);
75
+ if (customNamespace && customNamespace !== namespace) {
76
+ throw new Error(
77
+ `[plugin/react] Plugin namespace mismatch: expected ${namespace}, received ${customNamespace}`
78
+ );
79
+ }
80
+ if (i18n.namespace !== namespace) {
81
+ throw new Error(
82
+ `[plugin/react] Host-bound namespace mismatch: expected ${namespace}, received ${i18n.namespace}`
83
+ );
84
+ }
85
+ const t = React.useCallback(
86
+ (key, paramsOrDefault) => {
87
+ const prefix = `${namespace}.`;
88
+ const localKey = key.startsWith(prefix) ? key.slice(prefix.length) : key;
89
+ return i18n.t(localKey, paramsOrDefault);
90
+ },
91
+ [i18n]
92
+ );
93
+ const tCommon = i18n.tCommon ?? fallbackPluginTranslate;
94
+ return React.useMemo(
95
+ () => ({
96
+ t,
97
+ tCommon,
98
+ locale: i18n.locale,
99
+ timezone: i18n.timezone,
100
+ isLoading: i18n.isLoading,
101
+ availableLocales: i18n.availableLocales,
102
+ languages: i18n.languages,
103
+ namespace
104
+ }),
105
+ [
106
+ t,
107
+ tCommon,
108
+ i18n.locale,
109
+ i18n.timezone,
110
+ i18n.isLoading,
111
+ i18n.availableLocales,
112
+ i18n.languages,
113
+ namespace
114
+ ]
115
+ );
116
+ }
117
+ var defaultPluginCurrency = {
118
+ currency: DEFAULT_CURRENCY,
119
+ currencies: [],
120
+ baseCurrency: DEFAULT_CURRENCY,
121
+ isLoading: false,
122
+ isReady: false,
123
+ changeCurrency: () => void 0,
124
+ formatPrice: (cents, fromCurrency) => `${fromCurrency ?? DEFAULT_CURRENCY.code} ${(cents / 100).toFixed(2)}`
125
+ };
126
+ var PluginCurrencyContext = React.createContext(defaultPluginCurrency);
127
+ function usePluginCurrency() {
128
+ return React.useContext(PluginCurrencyContext);
129
+ }
130
+ var _PluginSlotImpl = null;
131
+ var _pluginSlotRegistry = null;
132
+ var emptyPluginSlotSubscribe = () => () => void 0;
133
+ function __setPluginSlot(component) {
134
+ _PluginSlotImpl = component;
135
+ }
136
+ function __setPluginSlotRegistry(registry) {
137
+ _pluginSlotRegistry = registry;
138
+ }
139
+ function usePluginSlot(name) {
140
+ const getSnapshot = () => _pluginSlotRegistry?.has(name) ?? false;
141
+ return React.useSyncExternalStore(
142
+ _pluginSlotRegistry?.subscribe ?? emptyPluginSlotSubscribe,
143
+ getSnapshot,
144
+ getSnapshot
145
+ );
146
+ }
147
+ function PluginSlot(props) {
148
+ if (!_PluginSlotImpl) {
149
+ return null;
150
+ }
151
+ return React.createElement(_PluginSlotImpl, props);
152
+ }
153
+ var PUBLIC_PLUGIN_RUNTIME_READY_EVENT = "wr:public-plugin-runtime-ready";
154
+ function WebPluginSlot({
155
+ name,
156
+ extensions,
157
+ props,
158
+ layout = "bare",
159
+ className
160
+ }) {
161
+ const mountRef = React.useRef(null);
162
+ const latestRef = React.useRef({ name, extensions, props, layout });
163
+ latestRef.current = { name, extensions, props, layout };
164
+ const renderCurrent = React.useCallback(() => {
165
+ const mount = mountRef.current;
166
+ const runtime = typeof window === "undefined" ? void 0 : window.__WR_PUBLIC_PLUGIN_RUNTIME__;
167
+ if (!mount || !runtime) return;
168
+ runtime.renderSlot(mount, latestRef.current);
169
+ }, []);
170
+ React.useEffect(() => {
171
+ if (typeof window === "undefined") return void 0;
172
+ const mount = mountRef.current;
173
+ window.addEventListener(PUBLIC_PLUGIN_RUNTIME_READY_EVENT, renderCurrent);
174
+ return () => {
175
+ window.removeEventListener(PUBLIC_PLUGIN_RUNTIME_READY_EVENT, renderCurrent);
176
+ if (mount) window.__WR_PUBLIC_PLUGIN_RUNTIME__?.unmountSlot(mount);
177
+ };
178
+ }, [renderCurrent]);
179
+ React.useEffect(() => {
180
+ renderCurrent();
181
+ });
182
+ if (extensions.length === 0) return null;
183
+ return React.createElement("div", {
184
+ ref: mountRef,
185
+ className,
186
+ "data-wr-plugin-react-slot": name
187
+ });
188
+ }
189
+ var defaultAbility = { can: () => true };
190
+ var PluginAbilityContext = React.createContext(defaultAbility);
191
+ function usePluginAbility() {
192
+ return React.useContext(PluginAbilityContext);
193
+ }
194
+ function useMediaPicker() {
195
+ return {
196
+ openDialog: (options) => {
197
+ if (typeof window !== "undefined") {
198
+ const picker = window.__WORDRHYME_MEDIA_PICKER__ || window.__OMNIDS_MEDIA_PICKER__;
199
+ if (picker) {
200
+ picker.open(options);
201
+ return;
202
+ }
203
+ }
204
+ console.warn("[plugin/react] Media Picker is not mounted by the Admin Host.");
205
+ }
206
+ };
207
+ }
208
+ export {
209
+ PUBLIC_PLUGIN_RUNTIME_READY_EVENT,
210
+ PluginAbilityContext,
211
+ PluginCurrencyContext,
212
+ PluginI18nContext,
213
+ PluginSlot,
214
+ WebPluginSlot,
215
+ __setPluginSlot,
216
+ __setPluginSlotRegistry,
217
+ __setTrpc,
218
+ useHostTrpc,
219
+ useMediaPicker,
220
+ usePluginAbility,
221
+ usePluginCurrency,
222
+ usePluginSlot,
223
+ usePluginTranslation,
224
+ usePluginTrpc
225
+ };
226
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n\n/**\n * React utilities for plugin admin UIs.\n *\n * At runtime, this module is shared via Module Federation from the admin host.\n * Plugins should configure MF shared with `import: false` for this package.\n */\nimport React from 'react';\nimport type { WebSlotRemoteExtension } from './types';\nimport {\n DEFAULT_CURRENCY,\n type CurrencyInfo,\n type FormatPriceOptions,\n} from './globalization';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _trpc: any = null;\nlet _hostTrpcFacade: any = null;\n\n/**\n * Called by the Admin Host at startup to inject the host's tRPC React client.\n * Plugin code should NOT call this directly.\n * @internal\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function __setTrpc(trpc: any) {\n _trpc = trpc;\n _hostTrpcFacade = null;\n}\n\n/**\n * Hook for plugin components to access their tRPC namespace with React Query hooks.\n * Must be called within a component rendered inside the Admin Host.\n *\n * @param pluginId - Short plugin ID (e.g., 'storage-s3') or full ID (e.g., 'com.wordrhyme.storage-s3')\n * @returns tRPC proxy with useQuery/useMutation hooks\n *\n * @example\n * ```tsx\n * import { usePluginTrpc } from '@wordrhyme/plugin/react';\n *\n * function MyPluginPage() {\n * const api = usePluginTrpc('my-plugin');\n * const { data, isLoading } = api.listItems.useQuery();\n * const createItem = api.createItem.useMutation();\n * }\n * ```\n */\nexport function usePluginTrpc(pluginId: string) {\n if (!_trpc) {\n throw new Error(\n '[plugin/react] tRPC not initialized. ' +\n 'Ensure plugin is loaded within Admin Host context.',\n );\n }\n const normalizedId = pluginId\n .replace(/^com\\.wordrhyme\\./, '')\n .replace(/\\./g, '-');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (_trpc as any).pluginApis[normalizedId];\n}\n\n/**\n * Hook for plugin components to access the Admin Host's root tRPC React client.\n * Useful for shared host capabilities such as currency, media, auth, etc.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useHostTrpc(): any {\n if (!_trpc) {\n throw new Error(\n '[plugin/react] tRPC not initialized. ' +\n 'Ensure plugin is loaded within Admin Host context.',\n );\n }\n if (!_hostTrpcFacade) {\n _hostTrpcFacade = new Proxy(_trpc, {\n get(target, property, receiver) {\n if (property === 'i18n') {\n throw new Error(\n '[plugin/react] Host i18n APIs are not exposed to plugins. ' +\n 'Use the Host-bound usePluginTranslation() bridge.',\n );\n }\n return Reflect.get(target, property, receiver);\n },\n has(target, property) {\n return property === 'i18n' ? false : Reflect.has(target, property);\n },\n ownKeys(target) {\n return Reflect.ownKeys(target).filter((property) => property !== 'i18n');\n },\n });\n }\n return _hostTrpcFacade;\n}\n\n// ============================================================\n// I18n — host translation bridge\n// ============================================================\n\nexport interface PluginI18nValue {\n pluginId?: string | undefined;\n namespace?: string | undefined;\n locale: string;\n timezone: string;\n isLoading: boolean;\n availableLocales: readonly string[];\n languages: readonly PluginLanguageOption[];\n t: (key: string, paramsOrDefault?: Record<string, string | number> | string) => string;\n /** Host-bound Core common translator. Optional for compatibility with older Hosts. */\n tCommon?: (key: string, paramsOrDefault?: Record<string, string | number> | string) => string;\n addNamespace: (namespace: string) => void;\n}\n\nexport interface PluginLanguageOption {\n locale: string;\n name: string;\n nativeName: string;\n direction: 'ltr' | 'rtl';\n isDefault: boolean;\n sortOrder: number;\n}\n\nconst fallbackPluginTranslate = (\n key: string,\n paramsOrDefault?: Record<string, string | number> | string,\n) => (typeof paramsOrDefault === 'string' ? paramsOrDefault : key);\n\nconst defaultI18n: PluginI18nValue = {\n locale: 'en-US',\n timezone: 'UTC',\n isLoading: false,\n availableLocales: [],\n languages: [],\n t: fallbackPluginTranslate,\n tCommon: fallbackPluginTranslate,\n addNamespace: () => undefined,\n};\n\nexport const PluginI18nContext = React.createContext<PluginI18nValue>(defaultI18n);\n\nfunction normalizePluginNamespace(pluginId: string): string {\n return `plugin.${pluginId}`;\n}\n\nexport function usePluginTranslation(pluginId: string, customNamespace?: string) {\n const i18n = React.useContext(PluginI18nContext);\n if (!i18n.pluginId || !i18n.namespace) {\n throw new Error('[plugin/react] Plugin i18n boundary is missing');\n }\n if (pluginId !== i18n.pluginId) {\n throw new Error(\n `[plugin/react] Plugin i18n identity mismatch: expected ${i18n.pluginId}, received ${pluginId}`,\n );\n }\n const namespace = normalizePluginNamespace(pluginId);\n if (customNamespace && customNamespace !== namespace) {\n throw new Error(\n `[plugin/react] Plugin namespace mismatch: expected ${namespace}, received ${customNamespace}`,\n );\n }\n if (i18n.namespace !== namespace) {\n throw new Error(\n `[plugin/react] Host-bound namespace mismatch: expected ${namespace}, received ${i18n.namespace}`,\n );\n }\n\n const t = React.useCallback(\n (key: string, paramsOrDefault?: Record<string, string | number> | string): string => {\n const prefix = `${namespace}.`;\n const localKey = key.startsWith(prefix) ? key.slice(prefix.length) : key;\n return i18n.t(localKey, paramsOrDefault);\n },\n [i18n],\n );\n const tCommon = i18n.tCommon ?? fallbackPluginTranslate;\n\n return React.useMemo(\n () => ({\n t,\n tCommon,\n locale: i18n.locale,\n timezone: i18n.timezone,\n isLoading: i18n.isLoading,\n availableLocales: i18n.availableLocales,\n languages: i18n.languages,\n namespace,\n }),\n [\n t,\n tCommon,\n i18n.locale,\n i18n.timezone,\n i18n.isLoading,\n i18n.availableLocales,\n i18n.languages,\n namespace,\n ],\n );\n}\n\n// ============================================================\n// Currency — host currency bridge\n// ============================================================\n\n/** Currency capabilities exposed by the Admin Host to plugin UIs. */\nexport interface PluginCurrencyValue {\n currency: CurrencyInfo;\n currencies: CurrencyInfo[];\n baseCurrency: CurrencyInfo;\n isLoading: boolean;\n isReady: boolean;\n changeCurrency: (currencyCode: string) => void;\n formatPrice: (\n cents: number,\n fromCurrency?: string | undefined,\n options?: FormatPriceOptions | undefined,\n ) => string;\n}\n\nconst defaultPluginCurrency: PluginCurrencyValue = {\n currency: DEFAULT_CURRENCY,\n currencies: [],\n baseCurrency: DEFAULT_CURRENCY,\n isLoading: false,\n isReady: false,\n changeCurrency: () => undefined,\n formatPrice: (cents, fromCurrency) => `${fromCurrency ?? DEFAULT_CURRENCY.code} ${(cents / 100).toFixed(2)}`,\n};\n\nexport const PluginCurrencyContext = React.createContext<PluginCurrencyValue>(defaultPluginCurrency);\n\nexport function usePluginCurrency(): PluginCurrencyValue {\n return React.useContext(PluginCurrencyContext);\n}\n\n// ============================================================\n// PluginSlot — runtime-injected from Admin Host\n// ============================================================\n\nexport interface PluginSlotProps {\n /** Slot name, e.g. 'shop.product.detail.actions' */\n name: string;\n /** React props passed to extensions rendered in this slot */\n props?: Record<string, unknown>;\n /** @deprecated Use props. Kept for legacy plugin slot call sites. */\n context?: Record<string, unknown>;\n /** Layout mode */\n layout?: 'inline' | 'stack' | 'tabs' | 'grid';\n /** Additional CSS class */\n className?: string;\n /** Fallback when no extensions registered */\n fallback?: React.ReactNode;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _PluginSlotImpl: React.ComponentType<PluginSlotProps> | null = null;\n\nexport interface PluginSlotRegistry {\n /** Returns whether at least one extension is registered for the slot. */\n has: (name: string) => boolean;\n /** Subscribes to extension registry changes. */\n subscribe: (onStoreChange: () => void) => () => void;\n}\n\nlet _pluginSlotRegistry: PluginSlotRegistry | null = null;\nconst emptyPluginSlotSubscribe = () => () => undefined;\n\n/**\n * Called by the Admin Host at startup to inject the PluginSlot component.\n * Plugin code should NOT call this directly.\n * @internal\n */\nexport function __setPluginSlot(component: React.ComponentType<PluginSlotProps>) {\n _PluginSlotImpl = component;\n}\n\n/**\n * Called by the Admin Host at startup to expose reactive slot availability.\n * Plugin code should NOT call this directly.\n * @internal\n */\nexport function __setPluginSlotRegistry(registry: PluginSlotRegistry | null) {\n _pluginSlotRegistry = registry;\n}\n\n/**\n * Returns whether the given slot currently has at least one registered extension.\n * The value updates when plugins are registered, reloaded, disabled, or removed.\n */\nexport function usePluginSlot(name: string): boolean {\n const getSnapshot = () => _pluginSlotRegistry?.has(name) ?? false;\n\n return React.useSyncExternalStore(\n _pluginSlotRegistry?.subscribe ?? emptyPluginSlotSubscribe,\n getSnapshot,\n getSnapshot,\n );\n}\n\n/**\n * Renders all plugin extensions registered for the given slot.\n *\n * @example\n * ```tsx\n * import { PluginSlot } from '@wordrhyme/plugin/react';\n *\n * function ProductDetailPage({ productId }) {\n * return (\n * <div>\n * <h1>Product Detail</h1>\n * <PluginSlot\n * name=\"shop.product.detail.actions\"\n * layout=\"inline\"\n * props={{ productId }}\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function PluginSlot(props: PluginSlotProps): React.ReactElement | null {\n if (!_PluginSlotImpl) {\n // Silently render nothing if host hasn't injected yet\n return null;\n }\n return React.createElement(_PluginSlotImpl, props);\n}\n\n// ============================================================\n// WebPluginSlot — declarative public-web React slot bridge\n// ============================================================\n\nexport type WebPluginSlotLayout = 'bare' | 'inline' | 'stack' | 'grid';\n\nexport interface WebPluginSlotRenderOptions {\n name: string;\n extensions: WebSlotRemoteExtension[];\n props?: Record<string, unknown> | undefined;\n layout?: WebPluginSlotLayout | undefined;\n}\n\nexport interface PublicWebPluginRuntime {\n scan: (root?: ParentNode) => void;\n renderSlot: (mount: HTMLElement, options: WebPluginSlotRenderOptions) => void;\n unmountSlot: (mount: HTMLElement) => void;\n}\n\nexport const PUBLIC_PLUGIN_RUNTIME_READY_EVENT = 'wr:public-plugin-runtime-ready';\n\ndeclare global {\n interface Window {\n __WR_PUBLIC_PLUGIN_RUNTIME__?: PublicWebPluginRuntime;\n }\n}\n\nexport interface WebPluginSlotProps extends WebPluginSlotRenderOptions {\n className?: string | undefined;\n}\n\n/**\n * Declares a public-web plugin slot inside an owning React tree.\n *\n * The owner keeps control of live props and React lifecycle. The Web Host owns\n * remote loading and renders extensions into the empty mount element through\n * its shared Module Federation React runtime.\n */\nexport function WebPluginSlot({\n name,\n extensions,\n props,\n layout = 'bare',\n className,\n}: WebPluginSlotProps): React.ReactElement | null {\n const mountRef = React.useRef<HTMLDivElement>(null);\n const latestRef = React.useRef<WebPluginSlotRenderOptions>({ name, extensions, props, layout });\n latestRef.current = { name, extensions, props, layout };\n\n const renderCurrent = React.useCallback(() => {\n const mount = mountRef.current;\n const runtime = typeof window === 'undefined' ? undefined : window.__WR_PUBLIC_PLUGIN_RUNTIME__;\n if (!mount || !runtime) return;\n runtime.renderSlot(mount, latestRef.current);\n }, []);\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n const mount = mountRef.current;\n window.addEventListener(PUBLIC_PLUGIN_RUNTIME_READY_EVENT, renderCurrent);\n return () => {\n window.removeEventListener(PUBLIC_PLUGIN_RUNTIME_READY_EVENT, renderCurrent);\n if (mount) window.__WR_PUBLIC_PLUGIN_RUNTIME__?.unmountSlot(mount);\n };\n }, [renderCurrent]);\n\n React.useEffect(() => {\n renderCurrent();\n });\n\n if (extensions.length === 0) return null;\n\n return React.createElement('div', {\n ref: mountRef,\n className,\n 'data-wr-plugin-react-slot': name,\n });\n}\n\n// Ability — runtime-injected from Admin Host (CASL permission)\n// ============================================================\n\n/**\n * Minimal ability interface for plugin permission checks.\n * Matches CASL MongoAbility.can() signature.\n */\nexport interface PluginAbility {\n can(action: string, subject: string): boolean;\n}\n\n/** Default allow-all ability (when host hasn't injected) */\nconst defaultAbility: PluginAbility = { can: () => true };\n\n/**\n * React Context for ability — shared between host and plugins.\n * Host injects via AbilityBridge, plugins consume via usePluginAbility.\n */\nexport const PluginAbilityContext = React.createContext<PluginAbility>(defaultAbility);\n\n/**\n * Hook for plugin components to check permissions.\n *\n * @example\n * ```tsx\n * import { usePluginAbility } from '@wordrhyme/plugin/react';\n *\n * function MyPluginPage() {\n * const ability = usePluginAbility();\n * const canCreate = ability.can('create', 'PluginResource');\n * const canDelete = ability.can('delete', 'PluginResource');\n * }\n * ```\n */\nexport function usePluginAbility(): PluginAbility {\n return React.useContext(PluginAbilityContext);\n}\n\n// ============================================================\n// Media Picker — accesses global imperative API\n// ============================================================\n\nimport type { PluginMediaInfo } from './types';\n\n// Define the options for the global MediaPicker\nexport interface MediaPickerOptions {\n presentation?: 'dialog' | 'drawer'; // Where to open the picker\n mode?: 'single' | 'multi';\n category?: 'all' | 'image' | 'document' | 'video' | 'audio' | 'archive' | 'other';\n /** Allow callers to select an external image by entering its URL in the picker. */\n allowExternalUrl?: boolean;\n /** Prefer importing URL images into Media, or only allow a transient external reference. */\n externalUrlMode?: 'import-preferred' | 'reference-only';\n /** Publish selected images when needed and include an absolute public URL in each result. */\n requirePublicUrl?: boolean;\n onSelect: (media: PluginMediaInfo[]) => void;\n}\n\n/**\n * Hook to open the global Media Picker dialog (rendered by Admin Host).\n * Returns an imperative API `openDialog(options)`.\n */\nexport function useMediaPicker() {\n return {\n openDialog: (options: MediaPickerOptions) => {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n if (typeof window !== 'undefined') {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n const picker = window.__WORDRHYME_MEDIA_PICKER__ || window.__OMNIDS_MEDIA_PICKER__;\n if (picker) {\n picker.open(options);\n return;\n }\n }\n console.warn('[plugin/react] Media Picker is not mounted by the Admin Host.');\n }\n };\n}\n"],"mappings":"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","names":[]}