@fivem-nui/svelte 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +284 -2
  2. package/dist/actions/clickOutside/clickOutside.d.ts +24 -0
  3. package/dist/actions/clickOutside/clickOutside.d.ts.map +1 -0
  4. package/dist/actions/clickOutside/clickOutside.js +62 -0
  5. package/dist/actions/clickOutside/clickOutside.js.map +1 -0
  6. package/dist/actions/clickOutside/handler.d.ts +11 -0
  7. package/dist/actions/clickOutside/handler.d.ts.map +1 -0
  8. package/dist/actions/clickOutside/handler.js +45 -0
  9. package/dist/actions/clickOutside/handler.js.map +1 -0
  10. package/dist/actions/clickOutside/index.d.ts +4 -0
  11. package/dist/actions/clickOutside/index.d.ts.map +1 -0
  12. package/dist/actions/clickOutside/index.js +4 -0
  13. package/dist/actions/clickOutside/index.js.map +1 -0
  14. package/dist/actions/clickOutside/types.d.ts +36 -0
  15. package/dist/actions/clickOutside/types.d.ts.map +1 -0
  16. package/dist/actions/clickOutside/types.js +6 -0
  17. package/dist/actions/clickOutside/types.js.map +1 -0
  18. package/dist/actions/clickOutside.d.ts +63 -0
  19. package/dist/actions/clickOutside.d.ts.map +1 -0
  20. package/dist/actions/clickOutside.js +106 -0
  21. package/dist/actions/clickOutside.js.map +1 -0
  22. package/dist/actions/escape/escape.d.ts +26 -0
  23. package/dist/actions/escape/escape.d.ts.map +1 -0
  24. package/dist/actions/escape/escape.js +74 -0
  25. package/dist/actions/escape/escape.js.map +1 -0
  26. package/dist/actions/escape/handler.d.ts +11 -0
  27. package/dist/actions/escape/handler.d.ts.map +1 -0
  28. package/dist/actions/escape/handler.js +50 -0
  29. package/dist/actions/escape/handler.js.map +1 -0
  30. package/dist/actions/escape/index.d.ts +4 -0
  31. package/dist/actions/escape/index.d.ts.map +1 -0
  32. package/dist/actions/escape/index.js +4 -0
  33. package/dist/actions/escape/index.js.map +1 -0
  34. package/dist/actions/escape/types.d.ts +70 -0
  35. package/dist/actions/escape/types.d.ts.map +1 -0
  36. package/dist/actions/escape/types.js +12 -0
  37. package/dist/actions/escape/types.js.map +1 -0
  38. package/dist/actions/escape.d.ts +99 -0
  39. package/dist/actions/escape.d.ts.map +1 -0
  40. package/dist/actions/escape.js +128 -0
  41. package/dist/actions/escape.js.map +1 -0
  42. package/dist/actions/focusTrap/focusTrap.d.ts +19 -0
  43. package/dist/actions/focusTrap/focusTrap.d.ts.map +1 -0
  44. package/dist/actions/focusTrap/focusTrap.js +198 -0
  45. package/dist/actions/focusTrap/focusTrap.js.map +1 -0
  46. package/dist/actions/focusTrap/focusables.d.ts +39 -0
  47. package/dist/actions/focusTrap/focusables.d.ts.map +1 -0
  48. package/dist/actions/focusTrap/focusables.js +142 -0
  49. package/dist/actions/focusTrap/focusables.js.map +1 -0
  50. package/dist/actions/focusTrap/index.d.ts +5 -0
  51. package/dist/actions/focusTrap/index.d.ts.map +1 -0
  52. package/dist/actions/focusTrap/index.js +5 -0
  53. package/dist/actions/focusTrap/index.js.map +1 -0
  54. package/dist/actions/focusTrap/trapStack.d.ts +33 -0
  55. package/dist/actions/focusTrap/trapStack.d.ts.map +1 -0
  56. package/dist/actions/focusTrap/trapStack.js +84 -0
  57. package/dist/actions/focusTrap/trapStack.js.map +1 -0
  58. package/dist/actions/focusTrap/types.d.ts +50 -0
  59. package/dist/actions/focusTrap/types.d.ts.map +1 -0
  60. package/dist/actions/focusTrap/types.js +10 -0
  61. package/dist/actions/focusTrap/types.js.map +1 -0
  62. package/dist/actions/focusTrap.d.ts +24 -0
  63. package/dist/actions/focusTrap.d.ts.map +1 -0
  64. package/dist/actions/focusTrap.js +201 -0
  65. package/dist/actions/focusTrap.js.map +1 -0
  66. package/dist/actions/focusTrap.types.d.ts +50 -0
  67. package/dist/actions/focusTrap.types.d.ts.map +1 -0
  68. package/dist/actions/focusTrap.types.js +10 -0
  69. package/dist/actions/focusTrap.types.js.map +1 -0
  70. package/dist/actions/focusables.d.ts +39 -0
  71. package/dist/actions/focusables.d.ts.map +1 -0
  72. package/dist/actions/focusables.js +142 -0
  73. package/dist/actions/focusables.js.map +1 -0
  74. package/dist/actions/index.d.ts +5 -0
  75. package/dist/actions/index.d.ts.map +1 -0
  76. package/dist/actions/index.js +5 -0
  77. package/dist/actions/index.js.map +1 -0
  78. package/dist/actions/layer/index.d.ts +4 -0
  79. package/dist/actions/layer/index.d.ts.map +1 -0
  80. package/dist/actions/layer/index.js +4 -0
  81. package/dist/actions/layer/index.js.map +1 -0
  82. package/dist/actions/layer/layer.d.ts +28 -0
  83. package/dist/actions/layer/layer.d.ts.map +1 -0
  84. package/dist/actions/layer/layer.js +118 -0
  85. package/dist/actions/layer/layer.js.map +1 -0
  86. package/dist/actions/layer/layerStack.d.ts +10 -0
  87. package/dist/actions/layer/layerStack.d.ts.map +1 -0
  88. package/dist/actions/layer/layerStack.js +153 -0
  89. package/dist/actions/layer/layerStack.js.map +1 -0
  90. package/dist/actions/layer/types.d.ts +105 -0
  91. package/dist/actions/layer/types.d.ts.map +1 -0
  92. package/dist/actions/layer/types.js +14 -0
  93. package/dist/actions/layer/types.js.map +1 -0
  94. package/dist/actions/layer.d.ts +133 -0
  95. package/dist/actions/layer.d.ts.map +1 -0
  96. package/dist/actions/layer.js +269 -0
  97. package/dist/actions/layer.js.map +1 -0
  98. package/dist/actions/trapStack.d.ts +33 -0
  99. package/dist/actions/trapStack.d.ts.map +1 -0
  100. package/dist/actions/trapStack.js +84 -0
  101. package/dist/actions/trapStack.js.map +1 -0
  102. package/dist/client.d.ts +179 -0
  103. package/dist/client.d.ts.map +1 -0
  104. package/dist/client.js +176 -0
  105. package/dist/client.js.map +1 -0
  106. package/dist/context/index.d.ts +68 -0
  107. package/dist/context/index.d.ts.map +1 -0
  108. package/dist/context/index.js +112 -0
  109. package/dist/context/index.js.map +1 -0
  110. package/dist/index.d.ts +22 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +30 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/mock/index.d.ts +27 -0
  115. package/dist/mock/index.d.ts.map +1 -0
  116. package/dist/mock/index.js +137 -0
  117. package/dist/mock/index.js.map +1 -0
  118. package/dist/mock/interceptor.d.ts +19 -0
  119. package/dist/mock/interceptor.d.ts.map +1 -0
  120. package/dist/mock/interceptor.js +128 -0
  121. package/dist/mock/interceptor.js.map +1 -0
  122. package/dist/mock/types.d.ts +100 -0
  123. package/dist/mock/types.d.ts.map +1 -0
  124. package/dist/mock/types.js +27 -0
  125. package/dist/mock/types.js.map +1 -0
  126. package/dist/mock/url.d.ts +14 -0
  127. package/dist/mock/url.d.ts.map +1 -0
  128. package/dist/mock/url.js +38 -0
  129. package/dist/mock/url.js.map +1 -0
  130. package/dist/runes/clickOutside.svelte.d.ts +29 -0
  131. package/dist/runes/clickOutside.svelte.d.ts.map +1 -0
  132. package/dist/runes/clickOutside.svelte.js +42 -0
  133. package/dist/runes/clickOutside.svelte.js.map +1 -0
  134. package/dist/runes/escape.svelte.d.ts +34 -0
  135. package/dist/runes/escape.svelte.d.ts.map +1 -0
  136. package/dist/runes/escape.svelte.js +44 -0
  137. package/dist/runes/escape.svelte.js.map +1 -0
  138. package/dist/runes/focusTrap.svelte.d.ts +21 -0
  139. package/dist/runes/focusTrap.svelte.d.ts.map +1 -0
  140. package/dist/runes/focusTrap.svelte.js +37 -0
  141. package/dist/runes/focusTrap.svelte.js.map +1 -0
  142. package/dist/runes/index.svelte.d.ts +8 -0
  143. package/dist/runes/index.svelte.d.ts.map +1 -0
  144. package/dist/runes/index.svelte.js +8 -0
  145. package/dist/runes/index.svelte.js.map +1 -0
  146. package/dist/runes/layer.svelte.d.ts +36 -0
  147. package/dist/runes/layer.svelte.d.ts.map +1 -0
  148. package/dist/runes/layer.svelte.js +48 -0
  149. package/dist/runes/layer.svelte.js.map +1 -0
  150. package/dist/runes/query.svelte.d.ts +104 -0
  151. package/dist/runes/query.svelte.d.ts.map +1 -0
  152. package/dist/runes/query.svelte.js +179 -0
  153. package/dist/runes/query.svelte.js.map +1 -0
  154. package/dist/runes/state.svelte.d.ts +72 -0
  155. package/dist/runes/state.svelte.d.ts.map +1 -0
  156. package/dist/runes/state.svelte.js +98 -0
  157. package/dist/runes/state.svelte.js.map +1 -0
  158. package/dist/runes/window.svelte.d.ts +120 -0
  159. package/dist/runes/window.svelte.d.ts.map +1 -0
  160. package/dist/runes/window.svelte.js +196 -0
  161. package/dist/runes/window.svelte.js.map +1 -0
  162. package/dist/types/index.d.ts +2 -0
  163. package/dist/types/index.d.ts.map +1 -0
  164. package/dist/types/index.js +3 -0
  165. package/dist/types/index.js.map +1 -0
  166. package/dist/types/nui.d.ts +34 -0
  167. package/dist/types/nui.d.ts.map +1 -0
  168. package/dist/types/nui.js +17 -0
  169. package/dist/types/nui.js.map +1 -0
  170. package/dist/utils/env.d.ts +21 -0
  171. package/dist/utils/env.d.ts.map +1 -0
  172. package/dist/utils/env.js +36 -0
  173. package/dist/utils/env.js.map +1 -0
  174. package/dist/utils/error.d.ts +11 -0
  175. package/dist/utils/error.d.ts.map +1 -0
  176. package/dist/utils/error.js +30 -0
  177. package/dist/utils/error.js.map +1 -0
  178. package/dist/utils/index.d.ts +3 -0
  179. package/dist/utils/index.d.ts.map +1 -0
  180. package/dist/utils/index.js +3 -0
  181. package/dist/utils/index.js.map +1 -0
  182. package/dist/utils/keyboard.d.ts +127 -0
  183. package/dist/utils/keyboard.d.ts.map +1 -0
  184. package/dist/utils/keyboard.js +152 -0
  185. package/dist/utils/keyboard.js.map +1 -0
  186. package/package.json +76 -4
  187. package/src/actions/clickOutside/clickOutside.ts +78 -0
  188. package/src/actions/clickOutside/handler.ts +49 -0
  189. package/src/actions/clickOutside/index.ts +11 -0
  190. package/src/actions/clickOutside/types.ts +54 -0
  191. package/src/actions/escape/escape.ts +90 -0
  192. package/src/actions/escape/handler.ts +52 -0
  193. package/src/actions/escape/index.ts +11 -0
  194. package/src/actions/escape/types.ts +88 -0
  195. package/src/actions/focusTrap/focusTrap.ts +228 -0
  196. package/src/actions/focusTrap/focusables.ts +155 -0
  197. package/src/actions/focusTrap/index.ts +22 -0
  198. package/src/actions/focusTrap/trapStack.ts +109 -0
  199. package/src/actions/focusTrap/types.ts +64 -0
  200. package/src/actions/index.ts +35 -0
  201. package/src/actions/layer/index.ts +13 -0
  202. package/src/actions/layer/layer.ts +140 -0
  203. package/src/actions/layer/layerStack.ts +169 -0
  204. package/src/actions/layer/types.ts +132 -0
  205. package/src/client.ts +323 -0
  206. package/src/context/index.ts +127 -0
  207. package/src/index.ts +53 -0
  208. package/src/mock/index.ts +176 -0
  209. package/src/mock/interceptor.ts +177 -0
  210. package/src/mock/types.ts +125 -0
  211. package/src/mock/url.ts +37 -0
  212. package/src/runes/clickOutside.svelte.ts +53 -0
  213. package/src/runes/escape.svelte.ts +71 -0
  214. package/src/runes/focusTrap.svelte.ts +56 -0
  215. package/src/runes/index.svelte.ts +38 -0
  216. package/src/runes/layer.svelte.ts +74 -0
  217. package/src/runes/query.svelte.ts +313 -0
  218. package/src/runes/state.svelte.ts +175 -0
  219. package/src/runes/window.svelte.ts +308 -0
  220. package/src/utils/env.ts +36 -0
  221. package/src/utils/index.ts +2 -0
  222. package/src/utils/keyboard.ts +244 -0
@@ -0,0 +1,177 @@
1
+ import { isNuiUrl, parseNuiUrl } from './url';
2
+
3
+ export interface InterceptorContext {
4
+ targetResource: string;
5
+ latencyMs: number;
6
+ log: boolean;
7
+ activeRequests: Record<string, (payload: any) => Promise<any> | any>;
8
+ activeCallbacks: Record<string, (payload: any) => void>;
9
+ activeSendHandlers: Record<string, (payload: any) => void>;
10
+ originalFetch: typeof window.fetch;
11
+ }
12
+
13
+ /**
14
+ * Extracts the body payload from a `fetch` invocation, supporting both `init.body`
15
+ * and body text inside a `Request` instance.
16
+ */
17
+ export async function extractRequestBody(
18
+ input: RequestInfo | URL,
19
+ init?: RequestInit
20
+ ): Promise<any> {
21
+ let bodyText = typeof init?.body === 'string' ? init.body : null;
22
+ if (!bodyText && typeof Request !== 'undefined' && input instanceof Request) {
23
+ try {
24
+ bodyText = await input.clone().text();
25
+ } catch {
26
+ // body already consumed or unreadable
27
+ }
28
+ }
29
+
30
+ if (bodyText) {
31
+ try {
32
+ return JSON.parse(bodyText);
33
+ } catch {
34
+ return bodyText;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Creates the `window.fetch` interceptor function that simulates FiveM CEF transport.
42
+ */
43
+ export function createFetchInterceptor(ctx: InterceptorContext): typeof window.fetch {
44
+ const {
45
+ targetResource,
46
+ latencyMs,
47
+ log,
48
+ activeRequests,
49
+ activeCallbacks,
50
+ activeSendHandlers,
51
+ originalFetch
52
+ } = ctx;
53
+
54
+ return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
55
+ const url =
56
+ typeof input === 'string'
57
+ ? input
58
+ : input instanceof URL
59
+ ? input.toString()
60
+ : input.url;
61
+
62
+ if (!isNuiUrl(url, targetResource)) {
63
+ return originalFetch(input, init);
64
+ }
65
+
66
+ const { resourceName, callbackName } = parseNuiUrl(url);
67
+ const bodyObj = await extractRequestBody(input, init);
68
+
69
+ const eventName = bodyObj?.event;
70
+ const correlationId = bodyObj?.id;
71
+ const payload = bodyObj?.payload ?? bodyObj?.data;
72
+ const messageType = bodyObj?.type;
73
+
74
+ if (log) {
75
+ console.debug(
76
+ `[NUI Mock] -> POST https://${resourceName}/${callbackName} type='${messageType ?? 'event'}' event='${eventName ?? callbackName}' id=${correlationId ?? '-'}`,
77
+ payload
78
+ );
79
+ }
80
+
81
+ // Simulate consistent transport latency across all NUI operations
82
+ if (latencyMs > 0) {
83
+ await new Promise((resolve) => setTimeout(resolve, latencyMs));
84
+ }
85
+
86
+ let handled = false;
87
+
88
+ // Check callback notifications
89
+ if (activeCallbacks[callbackName]) {
90
+ handled = true;
91
+ try {
92
+ activeCallbacks[callbackName](payload);
93
+ } catch (e) {
94
+ console.error(`[NUI Mock] Error in callback handler '${callbackName}':`, e);
95
+ }
96
+ }
97
+
98
+ // Check send handlers
99
+ if (eventName && activeSendHandlers[eventName]) {
100
+ handled = true;
101
+ try {
102
+ activeSendHandlers[eventName](payload);
103
+ } catch (e) {
104
+ console.error(`[NUI Mock] Error in send handler '${eventName}':`, e);
105
+ }
106
+ }
107
+
108
+ // Handle correlated request/response
109
+ if (eventName && activeRequests[eventName]) {
110
+ handled = true;
111
+ try {
112
+ const result = await activeRequests[eventName](payload);
113
+
114
+ // In FiveM CEF, HTTP POST to https://<resource>/<callback> is strictly an outgoing transport ACK;
115
+ // the native C# backend delivers the correlated response envelope asynchronously via
116
+ // SendNuiMessage -> window.postMessage.
117
+ if (correlationId) {
118
+ window.postMessage(
119
+ {
120
+ type: 'response',
121
+ id: correlationId,
122
+ success: true,
123
+ payload: result,
124
+ data: result
125
+ },
126
+ '*'
127
+ );
128
+ }
129
+
130
+ return new Response(JSON.stringify({ ok: true }), {
131
+ status: 200,
132
+ headers: { 'Content-Type': 'application/json' }
133
+ });
134
+ } catch (err: unknown) {
135
+ const errorEnvelope = {
136
+ code: (err as any)?.code ?? 'MOCK_ERROR',
137
+ message: err instanceof Error ? err.message : String(err)
138
+ };
139
+
140
+ if (correlationId) {
141
+ window.postMessage(
142
+ {
143
+ type: 'response',
144
+ id: correlationId,
145
+ success: false,
146
+ error: errorEnvelope
147
+ },
148
+ '*'
149
+ );
150
+ }
151
+
152
+ return new Response(
153
+ JSON.stringify({
154
+ success: false,
155
+ error: errorEnvelope
156
+ }),
157
+ {
158
+ status: 500,
159
+ headers: { 'Content-Type': 'application/json' }
160
+ }
161
+ );
162
+ }
163
+ }
164
+
165
+ if (!handled && log) {
166
+ const targetEvent = eventName ?? callbackName;
167
+ console.warn(
168
+ `[NUI Mock] Unhandled NUI message: no mock handler registered for '${targetEvent}'.`
169
+ );
170
+ }
171
+
172
+ return new Response(JSON.stringify({ ok: true }), {
173
+ status: 200,
174
+ headers: { 'Content-Type': 'application/json' }
175
+ });
176
+ };
177
+ }
@@ -0,0 +1,125 @@
1
+ import type { NuiWindowAction } from '@fivem-nui/core';
2
+
3
+ /**
4
+ * Message types for synthetic NUI postMessage envelopes in the mock environment.
5
+ */
6
+ export enum NuiMessageType {
7
+ /**
8
+ * Standard event message.
9
+ */
10
+ Event = 'event',
11
+
12
+ /**
13
+ * RPC request message.
14
+ */
15
+ Request = 'request',
16
+
17
+ /**
18
+ * RPC response message.
19
+ */
20
+ Response = 'response'
21
+ }
22
+
23
+ /**
24
+ * Configuration options for {@link setupNuiMock}.
25
+ */
26
+ export interface NuiMockOptions {
27
+ /**
28
+ * Expected FiveM resource name to intercept.
29
+ * If omitted, falls back to `getResourceName('')` or `'nui'`.
30
+ */
31
+ resourceName?: string;
32
+
33
+ /**
34
+ * Mock handlers for requests sent via `nui.request(eventName, payload)`.
35
+ * Return the response payload or a Promise resolving to it.
36
+ */
37
+ requests?: Record<string, (payload: any) => Promise<any> | any>;
38
+
39
+ /**
40
+ * Mock callbacks registered with `nui.notify(callbackName, payload)`.
41
+ */
42
+ callbacks?: Record<string, (payload: any) => void>;
43
+
44
+ /**
45
+ * Listeners invoked when UI sends fire-and-forget messages (`nui.send`).
46
+ */
47
+ onSend?: Record<string, (payload: any) => void>;
48
+
49
+ /**
50
+ * Delay in milliseconds to simulate network latency in the browser.
51
+ * @defaultValue 50
52
+ */
53
+ latencyMs?: number;
54
+
55
+ /**
56
+ * Whether to log mock events and requests in the browser console.
57
+ * @defaultValue true
58
+ */
59
+ log?: boolean;
60
+ }
61
+
62
+ /**
63
+ * Controller interface returned by {@link setupNuiMock} for controlling the mock runtime.
64
+ */
65
+ export interface NuiMockController {
66
+ /**
67
+ * Simulates an event dispatched from C#/server to NUI.
68
+ *
69
+ * @param eventName - Name of the simulated event.
70
+ * @param payload - Optional data payload.
71
+ * @param delayMs - Optional delay in milliseconds before dispatching.
72
+ */
73
+ emitEvent(eventName: string, payload?: unknown, delayMs?: number): void;
74
+
75
+ /**
76
+ * Simulates a window lifecycle action (`'open'`, `'close'`, `'focus'`, `'blur'`).
77
+ *
78
+ * @param action - The window lifecycle action.
79
+ * @param windowName - Optional target window identifier.
80
+ * @param delayMs - Optional delay in milliseconds before dispatching.
81
+ */
82
+ emitWindow(action: NuiWindowAction, windowName?: string, delayMs?: number): void;
83
+
84
+ /**
85
+ * Dynamically adds or updates a request mock handler.
86
+ *
87
+ * @param eventName - RPC request event name to intercept.
88
+ * @param handler - Handler function returning the mock response.
89
+ * @returns Unsubscribe function to remove the mock handler.
90
+ */
91
+ mockRequest<TRequest = unknown, TResponse = unknown>(
92
+ eventName: string,
93
+ handler: (payload: TRequest) => Promise<TResponse> | TResponse
94
+ ): () => void;
95
+
96
+ /**
97
+ * Removes a mock request handler.
98
+ *
99
+ * @param eventName - Name of the request to remove.
100
+ */
101
+ removeRequest(eventName: string): void;
102
+
103
+ /**
104
+ * Dynamically adds a listener for UI-sent messages (`nui.send`).
105
+ *
106
+ * @param eventName - Event name to listen for.
107
+ * @param handler - Callback invoked with the sent payload.
108
+ * @returns Unsubscribe function to remove the listener.
109
+ */
110
+ onSend<T = unknown>(eventName: string, handler: (payload: T) => void): () => void;
111
+
112
+ /**
113
+ * Restores original window.fetch and cleans up mock handlers.
114
+ */
115
+ destroy(): void;
116
+ }
117
+
118
+ export const NOOP_CONTROLLER: NuiMockController = {
119
+ emitEvent: () => {},
120
+ emitWindow: () => {},
121
+ mockRequest: () => () => {},
122
+ removeRequest: () => {},
123
+ onSend: () => () => {},
124
+ destroy: () => {}
125
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Checks whether a given URL matches a targeted FiveM NUI endpoint:
3
+ * `https://<resourceName>/<callback>` or `https://cfx-nui-<resourceName>/<callback>`.
4
+ * Rejects localhost, IP addresses, ports, and external domains.
5
+ */
6
+ export function isNuiUrl(url: string, targetResource: string): boolean {
7
+ try {
8
+ const parsed = new URL(url);
9
+ if (parsed.protocol !== 'https:' || parsed.port) {
10
+ return false;
11
+ }
12
+ const host = parsed.hostname.toLowerCase();
13
+ if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.includes('.')) {
14
+ return false;
15
+ }
16
+
17
+ const expected = targetResource.toLowerCase();
18
+ return host === expected || host === `cfx-nui-${expected}`;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Extracts and safely decodes the resource name and callback endpoint from a NUI URL.
26
+ */
27
+ export function parseNuiUrl(url: string): { resourceName: string; callbackName: string } {
28
+ const urlParts = url.replace('https://', '').split('/');
29
+ const resourceName = urlParts[0];
30
+ let callbackName = urlParts[1] ?? 'nui';
31
+ try {
32
+ callbackName = decodeURIComponent(callbackName);
33
+ } catch {
34
+ // malformed URI sequence, keep raw callbackName
35
+ }
36
+ return { resourceName, callbackName };
37
+ }
@@ -0,0 +1,53 @@
1
+ import { untrack } from 'svelte';
2
+ import {
3
+ createClickOutsideHandler,
4
+ type ClickOutsideTarget,
5
+ type NuiClickOutsideParameter
6
+ } from '../actions/clickOutside';
7
+
8
+ export type { ClickOutsideTarget, NuiClickOutsideParameter };
9
+
10
+ /**
11
+ * Detects clicks outside a target element and invokes a callback.
12
+ * Automatically cleans up listeners on unmount.
13
+ *
14
+ * @param target - Getter returning the target DOM element.
15
+ * @param getOptions - Getter returning options (onClickOutside, ignore, enabled).
16
+ *
17
+ * @example
18
+ * ```svelte
19
+ * <script>
20
+ * let panelEl = $state<HTMLElement>();
21
+ * let triggerEl = $state<HTMLElement>();
22
+ * let isOpen = $state(false);
23
+ *
24
+ * useClickOutside(
25
+ * () => panelEl,
26
+ * () => ({
27
+ * onClickOutside: () => { isOpen = false; },
28
+ * ignore: [triggerEl],
29
+ * enabled: isOpen
30
+ * })
31
+ * );
32
+ * </script>
33
+ * ```
34
+ */
35
+ export function useClickOutside(
36
+ target: () => Element | null | undefined,
37
+ getOptions: () => NuiClickOutsideParameter
38
+ ): void {
39
+ const handler = createClickOutsideHandler(target, getOptions);
40
+
41
+ $effect(() => {
42
+ if (!target()) return;
43
+
44
+ const capture = untrack(() => getOptions().capture ?? true);
45
+ const ac = new AbortController();
46
+ document.addEventListener('pointerdown', handler, {
47
+ capture,
48
+ signal: ac.signal
49
+ });
50
+
51
+ return () => ac.abort();
52
+ });
53
+ }
@@ -0,0 +1,71 @@
1
+ import { untrack } from 'svelte';
2
+ import {
3
+ createEscapeHandler,
4
+ toEscapeOptions,
5
+ type NuiEscapeParameter,
6
+ NuiEscapeTarget
7
+ } from '../actions/escape';
8
+
9
+ export { NuiEscapeTarget };
10
+
11
+ export type NuiEscapeComposableParameter = Omit<NuiEscapeParameter, 'target'> & {
12
+ /**
13
+ * Where to attach the global listener:
14
+ * - `'document'` / `NuiEscapeTarget.Document`: listens globally on the document (default)
15
+ * - `'window'` / `NuiEscapeTarget.Window`: listens globally on the window
16
+ *
17
+ * Note: To scope Escape to a specific DOM element with focus, use the `nuiEscapeAction` action instead.
18
+ * @defaultValue NuiEscapeTarget.Document
19
+ */
20
+ target?: NuiEscapeTarget.Window | NuiEscapeTarget.Document;
21
+ };
22
+
23
+ export type NuiEscapeComposableInput =
24
+ | ((event: KeyboardEvent) => void)
25
+ | NuiEscapeComposableParameter;
26
+
27
+ /**
28
+ * Listens for the Escape key and invokes a callback.
29
+ * Automatically cleans up listeners on unmount.
30
+ *
31
+ * @param getOptions - Getter returning options or an onEscape callback.
32
+ *
33
+ * @example
34
+ * ```svelte
35
+ * <script>
36
+ * let isOpen = $state(false);
37
+ *
38
+ * useEscape(() => ({
39
+ * onEscape: () => { isOpen = false; },
40
+ * enabled: isOpen
41
+ * }));
42
+ * </script>
43
+ * ```
44
+ */
45
+ export function useEscape(
46
+ getOptions: () => NuiEscapeComposableInput
47
+ ): void {
48
+ const normalized = () => toEscapeOptions<NuiEscapeComposableParameter>(getOptions());
49
+ const handler = createEscapeHandler(normalized);
50
+
51
+ $effect(() => {
52
+ // Intentionally untracked: attach the DOM listener once upon mount,
53
+ // preserving event listener order. Reactive state (`enabled`, `onEscape`, etc.)
54
+ // is read lazily on each keydown event. Cleanup is called on component unmount.
55
+ const { target, capture } = untrack(() => {
56
+ const o = normalized();
57
+ return {
58
+ target: o?.target === NuiEscapeTarget.Window ? window : document,
59
+ capture: o?.capture ?? false
60
+ };
61
+ });
62
+
63
+ const ac = new AbortController();
64
+ target.addEventListener('keydown', handler as EventListener, {
65
+ capture,
66
+ signal: ac.signal
67
+ });
68
+
69
+ return () => ac.abort();
70
+ });
71
+ }
@@ -0,0 +1,56 @@
1
+ import { untrack } from 'svelte';
2
+ import {
3
+ nuiFocusTrap,
4
+ type NuiFocusTrapInput,
5
+ type NuiFocusTrapOptions,
6
+ type FocusTrapAllowOutsideTarget,
7
+ type FocusTrapAllowOutside
8
+ } from '../actions/focusTrap';
9
+
10
+ export type {
11
+ NuiFocusTrapInput,
12
+ NuiFocusTrapOptions,
13
+ FocusTrapAllowOutsideTarget,
14
+ FocusTrapAllowOutside
15
+ };
16
+
17
+ /**
18
+ * Traps Tab key navigation inside a container element.
19
+ * Automatically activates and tears down as the target element mounts/unmounts.
20
+ *
21
+ * @param target - Getter returning the container element.
22
+ * @param getOptions - Optional getter returning options or boolean enabled flag.
23
+ *
24
+ * @example
25
+ * ```svelte
26
+ * <script>
27
+ * let modalEl = $state<HTMLElement>();
28
+ * let isOpen = $state(false);
29
+ *
30
+ * useFocusTrap(() => modalEl, () => isOpen);
31
+ * </script>
32
+ * ```
33
+ */
34
+ export function useFocusTrap(
35
+ target: () => HTMLElement | null | undefined,
36
+ getOptions?: () => NuiFocusTrapInput
37
+ ): void {
38
+ let action: ReturnType<typeof nuiFocusTrap> | undefined;
39
+
40
+ $effect(() => {
41
+ const node = target();
42
+ if (!node) return;
43
+
44
+ action = nuiFocusTrap(node, untrack(() => getOptions?.() ?? true));
45
+
46
+ return () => {
47
+ action?.destroy?.();
48
+ action = undefined;
49
+ };
50
+ });
51
+
52
+ $effect(() => {
53
+ const opts = getOptions?.() ?? true;
54
+ action?.update?.(opts);
55
+ });
56
+ }
@@ -0,0 +1,38 @@
1
+ export {
2
+ useNuiState,
3
+ NuiStateController,
4
+ type NuiState,
5
+ type NuiStateOptions
6
+ } from './state.svelte';
7
+
8
+ export {
9
+ useNuiQuery,
10
+ NuiQueryController,
11
+ type NuiQuery,
12
+ type NuiQueryOptions,
13
+ NuiQueryStatus
14
+ } from './query.svelte';
15
+
16
+ export {
17
+ useNuiWindow,
18
+ NuiWindowController,
19
+ type NuiWindow,
20
+ type NuiWindowOptions,
21
+ NuiWindowActionType,
22
+ NuiWindowNotification
23
+ } from './window.svelte';
24
+
25
+ export { useClickOutside } from './clickOutside.svelte';
26
+
27
+ export {
28
+ useEscape,
29
+ type NuiEscapeComposableParameter,
30
+ type NuiEscapeComposableInput
31
+ } from './escape.svelte';
32
+
33
+ export { useFocusTrap } from './focusTrap.svelte';
34
+
35
+ export {
36
+ useLayer,
37
+ type NuiLayerComposableInput
38
+ } from './layer.svelte';
@@ -0,0 +1,74 @@
1
+ import { untrack } from 'svelte';
2
+ import {
3
+ nuiLayer,
4
+ NuiLayerDismissReason,
5
+ type NuiLayerInput,
6
+ type NuiLayerOptions,
7
+ type LayerInstance
8
+ } from '../actions/layer';
9
+
10
+ export {
11
+ NuiLayerDismissReason
12
+ };
13
+
14
+ export type {
15
+ NuiLayerInput,
16
+ NuiLayerOptions,
17
+ LayerInstance
18
+ };
19
+
20
+ /**
21
+ * Reactive options getter for `useLayer`.
22
+ */
23
+ export type NuiLayerComposableInput = () => NuiLayerOptions;
24
+
25
+ /**
26
+ * Manages an overlay layer (modal, dialog, drawer) in FiveM NUI.
27
+ * Coordinates Escape key, outside click, and focus trapping through a central layer stack.
28
+ *
29
+ * @param target - Getter returning the container element.
30
+ * @param getOptions - Optional getter returning layer options.
31
+ *
32
+ * @example
33
+ * ```svelte
34
+ * <script>
35
+ * let modalEl = $state<HTMLElement>();
36
+ * let isOpen = $state(false);
37
+ *
38
+ * useLayer(
39
+ * () => modalEl,
40
+ * () => ({ onDismiss: () => { isOpen = false; }, enabled: isOpen })
41
+ * );
42
+ * </script>
43
+ *
44
+ * {#if isOpen}
45
+ * <div bind:this={modalEl} class="modal">
46
+ * <h2>Modal</h2>
47
+ * <button onclick={() => { isOpen = false; }}>Close</button>
48
+ * </div>
49
+ * {/if}
50
+ * ```
51
+ */
52
+ export function useLayer(
53
+ target: () => HTMLElement | null | undefined,
54
+ getOptions?: () => NuiLayerOptions
55
+ ): void {
56
+ let action: ReturnType<typeof nuiLayer> | undefined;
57
+
58
+ $effect(() => {
59
+ const node = target();
60
+ if (!node) return;
61
+
62
+ action = nuiLayer(node, untrack(() => getOptions?.()));
63
+
64
+ return () => {
65
+ action?.destroy?.();
66
+ action = undefined;
67
+ };
68
+ });
69
+
70
+ $effect(() => {
71
+ const opts = getOptions?.();
72
+ action?.update?.(opts);
73
+ });
74
+ }