@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,308 @@
1
+ import { onDestroy } from 'svelte';
2
+ import type { NuiWindowAction } from '@fivem-nui/core';
3
+ import { globalErrorHandler } from '@fivem-nui/error';
4
+ import type { SvelteNuiClient } from '../client';
5
+ import { useNuiClient } from '../context';
6
+
7
+ /**
8
+ * Window lifecycle actions dispatched from C# backend to NUI UI.
9
+ */
10
+ export enum NuiWindowActionType {
11
+ Open = 'open',
12
+ Close = 'close',
13
+ Focus = 'focus',
14
+ Unfocus = 'unfocus'
15
+ }
16
+
17
+ /**
18
+ * Window lifecycle notifications dispatched from UI to C# backend.
19
+ */
20
+ export enum NuiWindowNotification {
21
+ Ready = 'ready',
22
+ Close = 'close'
23
+ }
24
+
25
+ export interface NuiWindowOptions {
26
+ /**
27
+ * Explicit client instance override.
28
+ * Defaults to the client provided via context (`useNuiClient()`) or global client.
29
+ */
30
+ client?: SvelteNuiClient;
31
+
32
+ /**
33
+ * Initial visibility without focus or backend notification.
34
+ * @defaultValue false
35
+ */
36
+ initialVisible?: boolean;
37
+
38
+ /**
39
+ * Automatically closes the window when Escape is pressed.
40
+ * @defaultValue true
41
+ */
42
+ closeOnEscape?: boolean;
43
+
44
+ /**
45
+ * Callback invoked when window visibility changes from hidden to visible.
46
+ */
47
+ onOpen?: () => void;
48
+
49
+ /**
50
+ * Callback invoked when window visibility changes from visible to hidden.
51
+ */
52
+ onClose?: () => void;
53
+
54
+ /**
55
+ * Callback invoked when window focus state changes.
56
+ */
57
+ onFocusChange?: (focused: boolean) => void;
58
+
59
+ /**
60
+ * Controls automatic cleanup through Svelte's component lifecycle.
61
+ *
62
+ * - `true`: Strictly requires component initialization lifecycle; throws if called outside.
63
+ * - `false`: Disables automatic lifecycle cleanup; call `destroy()` manually.
64
+ * - `undefined`: Attempts automatic cleanup, falling back to manual cleanup outside components.
65
+ *
66
+ * @defaultValue undefined
67
+ */
68
+ autoDestroy?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Controller interface representing a reactive NUI window.
73
+ */
74
+ export interface NuiWindow {
75
+ /** Whether the window is currently visible. */
76
+ readonly visible: boolean;
77
+
78
+ /** Whether the window currently has input focus. */
79
+ readonly focused: boolean;
80
+
81
+ /** Opens the window, sets focused to true, and sends a 'ready' notification to C#. */
82
+ open(): void;
83
+
84
+ /** Closes the window, clears focus, and sends a 'close' notification to C#. */
85
+ close(): void;
86
+
87
+ /** Toggles window visibility between open and closed. */
88
+ toggle(): void;
89
+
90
+ /** Destroys the window controller, unsubscribing from C# window events and key listeners. */
91
+ destroy(): void;
92
+ }
93
+
94
+ function safeCallback<TArgs extends unknown[]>(
95
+ operation: string,
96
+ callback: ((...args: TArgs) => void) | undefined,
97
+ ...args: TArgs
98
+ ): void {
99
+ if (!callback) return;
100
+
101
+ try {
102
+ callback(...args);
103
+ } catch (error) {
104
+ void globalErrorHandler
105
+ .handle(
106
+ error instanceof Error ? error : new Error(String(error)),
107
+ { args },
108
+ operation
109
+ )
110
+ .catch(() => {});
111
+ console.error(error);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Controller managing reactive visibility, focus, and backend events for a named NUI window.
117
+ */
118
+ export class NuiWindowController implements NuiWindow {
119
+ visible = $state(false);
120
+ focused = $state(false);
121
+
122
+ private destroyed = false;
123
+ private listeningForEscape = false;
124
+ private readonly unsubscribe: () => void;
125
+
126
+ constructor(
127
+ private readonly windowName: string,
128
+ private readonly options: NuiWindowOptions = {},
129
+ private readonly client: SvelteNuiClient
130
+ ) {
131
+ this.visible = options.initialVisible ?? false;
132
+ this.unsubscribe = client.onWindow(this.handleWindowAction);
133
+
134
+ if (this.visible) {
135
+ this.syncEscapeListener(true);
136
+ }
137
+ }
138
+
139
+ private syncEscapeListener(shouldListen: boolean): void {
140
+ if (typeof window === 'undefined' || this.options.closeOnEscape === false) return;
141
+ if (this.listeningForEscape === shouldListen) return;
142
+
143
+ if (shouldListen) {
144
+ window.addEventListener('keydown', this.handleEscape);
145
+ } else {
146
+ window.removeEventListener('keydown', this.handleEscape);
147
+ }
148
+
149
+ this.listeningForEscape = shouldListen;
150
+ }
151
+
152
+ private handleEscape = (event: KeyboardEvent): void => {
153
+ if (event.key !== 'Escape' || !this.visible || this.destroyed) return;
154
+
155
+ event.preventDefault();
156
+ this.close();
157
+ };
158
+
159
+ private transition(
160
+ nextVisible?: boolean,
161
+ nextFocused?: boolean
162
+ ): { visibility: boolean; focus: boolean } {
163
+ const changes = {
164
+ visibility: nextVisible !== undefined && this.visible !== nextVisible,
165
+ focus: nextFocused !== undefined && this.focused !== nextFocused
166
+ };
167
+
168
+ if (nextVisible !== undefined && changes.visibility) {
169
+ this.visible = nextVisible;
170
+ this.syncEscapeListener(nextVisible);
171
+ }
172
+
173
+ if (nextFocused !== undefined && changes.focus) {
174
+ this.focused = nextFocused;
175
+ }
176
+
177
+ return changes;
178
+ }
179
+
180
+ private notifyAndDispatch(
181
+ changes: { visibility: boolean; focus: boolean },
182
+ notification?: NuiWindowNotification
183
+ ): void {
184
+ if (notification && (changes.visibility || changes.focus)) {
185
+ try {
186
+ this.client.notify(notification, { window: this.windowName });
187
+ } catch (error) {
188
+ void globalErrorHandler
189
+ .handle(
190
+ error instanceof Error ? error : new Error(String(error)),
191
+ { window: this.windowName, notification },
192
+ 'useNuiWindow:notify'
193
+ )
194
+ .catch(() => {});
195
+ console.error('[NuiWindow] Failed to dispatch notify to backend:', error);
196
+ }
197
+ }
198
+
199
+ if (changes.visibility) {
200
+ safeCallback('useNuiWindow:visibility', this.visible ? this.options.onOpen : this.options.onClose);
201
+ }
202
+
203
+ if (changes.focus) {
204
+ safeCallback('useNuiWindow:focus', this.options.onFocusChange, this.focused);
205
+ }
206
+ }
207
+
208
+ open = (): void => {
209
+ if (this.destroyed) return;
210
+
211
+ const changes = this.transition(true, true);
212
+ this.notifyAndDispatch(changes, NuiWindowNotification.Ready);
213
+ };
214
+
215
+ close = (): void => {
216
+ if (this.destroyed) return;
217
+
218
+ const changes = this.transition(false, false);
219
+ this.notifyAndDispatch(changes, NuiWindowNotification.Close);
220
+ };
221
+
222
+ toggle = (): void => {
223
+ if (this.destroyed) return;
224
+
225
+ if (this.visible) this.close();
226
+ else this.open();
227
+ };
228
+
229
+ private handleWindowAction = (
230
+ action: NuiWindowAction,
231
+ targetWindow: string | undefined
232
+ ): void => {
233
+ if (this.destroyed || (targetWindow && targetWindow !== this.windowName)) {
234
+ return;
235
+ }
236
+
237
+ switch (action) {
238
+ case NuiWindowActionType.Open:
239
+ this.notifyAndDispatch(this.transition(true, true));
240
+ break;
241
+
242
+ case NuiWindowActionType.Close:
243
+ this.notifyAndDispatch(this.transition(false, false));
244
+ break;
245
+
246
+ case NuiWindowActionType.Focus:
247
+ if (this.visible) {
248
+ this.notifyAndDispatch(this.transition(undefined, true));
249
+ }
250
+ break;
251
+
252
+ case NuiWindowActionType.Unfocus:
253
+ this.notifyAndDispatch(this.transition(undefined, false));
254
+ break;
255
+ }
256
+ };
257
+
258
+ destroy = (): void => {
259
+ if (this.destroyed) return;
260
+
261
+ this.destroyed = true;
262
+ this.syncEscapeListener(false);
263
+ this.unsubscribe();
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Reactive controller for a named NUI window.
269
+ * Synchronizes visibility and focus states with the FiveM C# backend.
270
+ *
271
+ * @param windowName - Window identifier matching backend events.
272
+ * @param options - Optional configuration (callbacks, Escape handling).
273
+ *
274
+ * @example
275
+ * ```svelte
276
+ * <script>
277
+ * import { useNuiWindow } from '@fivem-nui/svelte';
278
+ *
279
+ * const win = useNuiWindow('inventory', { closeOnEscape: true });
280
+ * </script>
281
+ *
282
+ * {#if win.visible}
283
+ * <div class="panel">
284
+ * <button onclick={() => win.close()}>Close</button>
285
+ * </div>
286
+ * {/if}
287
+ * ```
288
+ */
289
+ export function useNuiWindow(
290
+ windowName: string,
291
+ options: NuiWindowOptions = {}
292
+ ): NuiWindow {
293
+ const client = options.client ?? useNuiClient();
294
+ const controller = new NuiWindowController(windowName, options, client);
295
+
296
+ if (options.autoDestroy !== false) {
297
+ try {
298
+ onDestroy(controller.destroy);
299
+ } catch (error) {
300
+ if (options.autoDestroy === true) {
301
+ controller.destroy();
302
+ throw error;
303
+ }
304
+ }
305
+ }
306
+
307
+ return controller;
308
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Detects whether the current page is running inside FiveM's Chromium Embedded Framework (CEF).
3
+ * In FiveM CEF, `window.GetParentResourceName` is injected as a native global function.
4
+ *
5
+ * @returns `true` if executing within FiveM CEF, `false` otherwise.
6
+ */
7
+ export function isNui(): boolean {
8
+ return typeof (globalThis as { GetParentResourceName?: unknown }).GetParentResourceName === 'function';
9
+ }
10
+
11
+ /**
12
+ * Returns true when running in a standard browser (Vite dev server, standalone preview), outside FiveM.
13
+ *
14
+ * @returns `true` if running in a standard browser outside FiveM, `false` if inside CEF.
15
+ */
16
+ export function isBrowser(): boolean {
17
+ return !isNui();
18
+ }
19
+
20
+ /**
21
+ * Safely resolves the parent resource name if running in FiveM CEF, or returns fallback in browser.
22
+ *
23
+ * @param fallback - Resource name to return if not running in FiveM CEF or if resolution fails.
24
+ * @returns The FiveM resource name or the provided fallback.
25
+ */
26
+ export function getResourceName(fallback = 'unknown_resource'): string {
27
+ const g = globalThis as { GetParentResourceName?: () => string };
28
+ if (typeof g.GetParentResourceName === 'function') {
29
+ try {
30
+ return g.GetParentResourceName();
31
+ } catch {
32
+ return fallback;
33
+ }
34
+ }
35
+ return fallback;
36
+ }
@@ -0,0 +1,2 @@
1
+ export * from './env';
2
+ export * from './keyboard';
@@ -0,0 +1,244 @@
1
+ import { onDestroy } from 'svelte';
2
+
3
+ /**
4
+ * Keyboard modifier keys.
5
+ */
6
+ export enum NuiKeyboardModifier {
7
+ Ctrl = 'ctrl',
8
+ Alt = 'alt',
9
+ Shift = 'shift',
10
+ Meta = 'meta'
11
+ }
12
+
13
+ /**
14
+ * Standard special key names commonly used in FiveM NUI shortcuts.
15
+ */
16
+ export enum NuiSpecialKey {
17
+ Escape = 'Escape',
18
+ Enter = 'Enter',
19
+ Tab = 'Tab',
20
+ Space = ' ',
21
+ Backspace = 'Backspace',
22
+ ArrowUp = 'ArrowUp',
23
+ ArrowDown = 'ArrowDown',
24
+ ArrowLeft = 'ArrowLeft',
25
+ ArrowRight = 'ArrowRight'
26
+ }
27
+
28
+ /**
29
+ * Configuration options for keyboard event handlers and shortcuts.
30
+ */
31
+ export interface KeyOptions {
32
+ /**
33
+ * Listen during capture phase.
34
+ * @defaultValue false
35
+ */
36
+ capture?: boolean;
37
+
38
+ /**
39
+ * Automatically call `event.preventDefault()`.
40
+ * @defaultValue false
41
+ */
42
+ preventDefault?: boolean;
43
+
44
+ /**
45
+ * Automatically call `event.stopPropagation()`.
46
+ * @defaultValue false
47
+ */
48
+ stopPropagation?: boolean;
49
+
50
+ /**
51
+ * Whether the key shortcut is enabled.
52
+ * Accepts a static boolean or a reactive getter `() => boolean`.
53
+ * @defaultValue true
54
+ */
55
+ enabled?: boolean | (() => boolean);
56
+
57
+ /**
58
+ * If true, suppresses shortcut execution when the active element is an input,
59
+ * textarea, select, or contenteditable.
60
+ * Crucial in FiveM NUI panels to prevent game hotkeys firing while typing messages/searches.
61
+ * @defaultValue false
62
+ */
63
+ ignoreInputs?: boolean;
64
+
65
+ /**
66
+ * Ignore repeating keydown events triggered while holding a key down.
67
+ * @defaultValue false
68
+ */
69
+ ignoreRepeat?: boolean;
70
+
71
+ /**
72
+ * Require Ctrl key to be pressed (or Cmd on Mac).
73
+ */
74
+ ctrl?: boolean;
75
+
76
+ /**
77
+ * Require Alt key to be pressed.
78
+ */
79
+ alt?: boolean;
80
+
81
+ /**
82
+ * Require Shift key to be pressed.
83
+ */
84
+ shift?: boolean;
85
+
86
+ /**
87
+ * Require Meta (Windows / Cmd) key to be pressed.
88
+ */
89
+ meta?: boolean;
90
+
91
+ /**
92
+ * Component lifecycle registration strategy:
93
+ * - `true`: Strictly requires component initialization lifecycle; registers `onDestroy`.
94
+ * If called outside a component, unsubscribes immediately to prevent memory leaks and throws.
95
+ * - `false`: Disables automatic lifecycle cleanup. The caller must invoke the returned cleanup function manually.
96
+ * - `undefined` (default): Attempts to register `onDestroy` within a component lifecycle;
97
+ * falls back gracefully to manual cleanup if called outside a component.
98
+ * @defaultValue undefined
99
+ */
100
+ autoDestroy?: boolean;
101
+ }
102
+
103
+ function isTextInputActive(): boolean {
104
+ if (typeof document === 'undefined') return false;
105
+ const el = document.activeElement;
106
+ if (!el) return false;
107
+
108
+ const tag = el.tagName.toLowerCase();
109
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
110
+ return true;
111
+ }
112
+ return el.hasAttribute('contenteditable') && el.getAttribute('contenteditable') !== 'false';
113
+ }
114
+
115
+ /**
116
+ * Registers a keyboard shortcut with automatic lifecycle management.
117
+ *
118
+ * Designed for FiveM NUI with support for:
119
+ * - `ignoreInputs`: prevents hotkeys from firing while typing in input fields.
120
+ * - Modifier keys (Ctrl, Alt, Shift, Meta).
121
+ * - Automatic cleanup via `onDestroy` and `AbortController`.
122
+ *
123
+ * @param keys - Key or array of keys (case-insensitive) to listen for.
124
+ * @param handler - Callback function invoked when matching keydown occurs.
125
+ * @param options - Optional shortcut configuration including modifiers and lifecycle strategy.
126
+ * @returns Cleanup function to remove event listeners.
127
+ *
128
+ * @example
129
+ * ```svelte
130
+ * <script>
131
+ * import { useNuiKey } from '@fivem-nui/svelte';
132
+ * useNuiKey('e', () => interact(), { ignoreInputs: true });
133
+ * </script>
134
+ * ```
135
+ */
136
+ export function useNuiKey(
137
+ keys: string | string[],
138
+ handler: (event: KeyboardEvent) => void,
139
+ options: KeyOptions = {}
140
+ ): () => void {
141
+ const targetKeys = (Array.isArray(keys) ? keys : [keys]).map((k) => k.toLowerCase());
142
+ const {
143
+ capture = false,
144
+ preventDefault = false,
145
+ stopPropagation = false,
146
+ enabled,
147
+ ignoreInputs = false,
148
+ ignoreRepeat = false,
149
+ ctrl,
150
+ alt,
151
+ shift,
152
+ meta
153
+ } = options;
154
+
155
+ let controller: AbortController | null = null;
156
+
157
+ function isAllowed(): boolean {
158
+ if (enabled === undefined) return true;
159
+ return typeof enabled === 'function' ? enabled() : enabled;
160
+ }
161
+
162
+ function onKeyDown(event: KeyboardEvent) {
163
+ if (!isAllowed()) return;
164
+ if (ignoreRepeat && event.repeat) return;
165
+ if (ignoreInputs && isTextInputActive()) return;
166
+
167
+ if (ctrl !== undefined && event.ctrlKey !== ctrl) return;
168
+ if (alt !== undefined && event.altKey !== alt) return;
169
+ if (shift !== undefined && event.shiftKey !== shift) return;
170
+ if (meta !== undefined && event.metaKey !== meta) return;
171
+
172
+ if (targetKeys.includes(event.key.toLowerCase())) {
173
+ if (preventDefault) {
174
+ event.preventDefault();
175
+ }
176
+ if (stopPropagation) {
177
+ event.stopPropagation();
178
+ }
179
+ try {
180
+ handler(event);
181
+ } catch (err) {
182
+ console.error('[NuiKey] Error in key handler callback:', err);
183
+ }
184
+ }
185
+ }
186
+
187
+ if (typeof window !== 'undefined') {
188
+ controller = new AbortController();
189
+ window.addEventListener('keydown', onKeyDown, {
190
+ capture,
191
+ signal: controller.signal
192
+ });
193
+ }
194
+
195
+ let isCleanedUp = false;
196
+ function cleanup() {
197
+ if (isCleanedUp) return;
198
+ isCleanedUp = true;
199
+ if (controller) {
200
+ controller.abort();
201
+ controller = null;
202
+ }
203
+ }
204
+
205
+ if (options.autoDestroy !== false) {
206
+ try {
207
+ onDestroy(cleanup);
208
+ } catch (lifecycleError) {
209
+ if (options.autoDestroy === true) {
210
+ cleanup();
211
+ throw lifecycleError;
212
+ }
213
+ // Best-effort fallback when autoDestroy is undefined and called outside component
214
+ }
215
+ }
216
+
217
+ return cleanup;
218
+ }
219
+
220
+ /**
221
+ * Listens for the `Escape` key to close or dismiss NUI panels.
222
+ * In FiveM, ESC is the universal standard for closing NUI windows.
223
+ *
224
+ * @param onEscape - Callback function invoked when the Escape key is pressed.
225
+ * @param options - Optional configuration overriding default key options (`preventDefault` defaults to `true`).
226
+ * @returns Cleanup function to remove the Escape listener.
227
+ *
228
+ * @example
229
+ * ```svelte
230
+ * <script>
231
+ * import { useNuiEscape, nui } from '@fivem-nui/svelte';
232
+ * useNuiEscape(() => nui.notify('close'));
233
+ * </script>
234
+ * ```
235
+ */
236
+ export function useNuiEscape(
237
+ onEscape: (event: KeyboardEvent) => void,
238
+ options: KeyOptions = {}
239
+ ): () => void {
240
+ return useNuiKey(NuiSpecialKey.Escape, onEscape, {
241
+ preventDefault: true,
242
+ ...options
243
+ });
244
+ }