@fivem-nui/svelte-components 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 (143) hide show
  1. package/README.md +101 -2
  2. package/dist/components/actions/NuiButton.svelte +292 -0
  3. package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
  4. package/dist/components/data/NuiBadge.svelte +277 -0
  5. package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
  6. package/dist/components/data/NuiCodeBlock.svelte +256 -0
  7. package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
  8. package/dist/components/data/NuiList.svelte +341 -0
  9. package/dist/components/data/NuiList.svelte.d.ts +52 -0
  10. package/dist/components/data/NuiProgressBar.svelte +264 -0
  11. package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
  12. package/dist/components/data/NuiRichText.svelte +260 -0
  13. package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
  14. package/dist/components/data/NuiTable.svelte +470 -0
  15. package/dist/components/data/NuiTable.svelte.d.ts +53 -0
  16. package/dist/components/data/chart/NuiChart.svelte +384 -0
  17. package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
  18. package/dist/components/data/chart/color.d.ts +9 -0
  19. package/dist/components/data/chart/color.js +78 -0
  20. package/dist/components/data/chart/index.d.ts +10 -0
  21. package/dist/components/data/chart/index.js +10 -0
  22. package/dist/components/data/chart/registry.d.ts +14 -0
  23. package/dist/components/data/chart/registry.js +53 -0
  24. package/dist/components/data/chart/renderers/bar.d.ts +8 -0
  25. package/dist/components/data/chart/renderers/bar.js +79 -0
  26. package/dist/components/data/chart/renderers/base.d.ts +18 -0
  27. package/dist/components/data/chart/renderers/base.js +10 -0
  28. package/dist/components/data/chart/renderers/line.d.ts +8 -0
  29. package/dist/components/data/chart/renderers/line.js +89 -0
  30. package/dist/components/data/chart/renderers/pie.d.ts +10 -0
  31. package/dist/components/data/chart/renderers/pie.js +55 -0
  32. package/dist/components/data/chart/scale.d.ts +22 -0
  33. package/dist/components/data/chart/scale.js +101 -0
  34. package/dist/components/data/chart/types.d.ts +84 -0
  35. package/dist/components/data/chart/types.js +1 -0
  36. package/dist/components/data/chart/validator.d.ts +10 -0
  37. package/dist/components/data/chart/validator.js +82 -0
  38. package/dist/components/forms/NuiCheckbox.svelte +218 -0
  39. package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
  40. package/dist/components/forms/NuiDatepicker.svelte +755 -0
  41. package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
  42. package/dist/components/forms/NuiFileInput.svelte +1047 -0
  43. package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
  44. package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
  45. package/dist/components/forms/NuiFileInput.types.js +1 -0
  46. package/dist/components/forms/NuiForm.svelte +391 -0
  47. package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
  48. package/dist/components/forms/NuiInput.svelte +296 -0
  49. package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
  50. package/dist/components/forms/NuiRadioGroup.svelte +238 -0
  51. package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
  52. package/dist/components/forms/NuiSelect.svelte +819 -0
  53. package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
  54. package/dist/components/layout/NuiCard.svelte +314 -0
  55. package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
  56. package/dist/components/layout/NuiCardGrid.svelte +98 -0
  57. package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
  58. package/dist/components/layout/NuiFlex.svelte +121 -0
  59. package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
  60. package/dist/components/layout/NuiGrid.svelte +66 -0
  61. package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
  62. package/dist/components/layout/NuiStack.svelte +138 -0
  63. package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
  64. package/dist/components/navigation/NuiAccordion.svelte +323 -0
  65. package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
  66. package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
  67. package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
  68. package/dist/components/navigation/NuiMenu.svelte +596 -0
  69. package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
  70. package/dist/components/navigation/NuiPagination.svelte +306 -0
  71. package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
  72. package/dist/components/navigation/NuiSidebar.svelte +186 -0
  73. package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
  74. package/dist/components/navigation/NuiTabs.svelte +249 -0
  75. package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
  76. package/dist/components/overlay/NuiDialog.svelte +118 -0
  77. package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
  78. package/dist/components/overlay/NuiModal.svelte +992 -0
  79. package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
  80. package/dist/components/overlay/NuiTooltip.svelte +159 -0
  81. package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
  82. package/dist/composables/useClickOutside.d.ts +43 -0
  83. package/dist/composables/useClickOutside.js +73 -0
  84. package/dist/composables/useFormValidation.d.ts +23 -0
  85. package/dist/composables/useFormValidation.js +72 -0
  86. package/dist/composables/useIdle.svelte.d.ts +18 -0
  87. package/dist/composables/useIdle.svelte.js +144 -0
  88. package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
  89. package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
  90. package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
  91. package/dist/composables/useLocalStorage.svelte.js +105 -0
  92. package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
  93. package/dist/composables/useMediaQuery.svelte.js +80 -0
  94. package/dist/index.d.ts +46 -0
  95. package/dist/index.js +42 -0
  96. package/package.json +58 -3
  97. package/src/components/actions/NuiButton.svelte +292 -0
  98. package/src/components/data/NuiBadge.svelte +277 -0
  99. package/src/components/data/NuiCodeBlock.svelte +256 -0
  100. package/src/components/data/NuiList.svelte +341 -0
  101. package/src/components/data/NuiProgressBar.svelte +264 -0
  102. package/src/components/data/NuiRichText.svelte +260 -0
  103. package/src/components/data/NuiTable.svelte +470 -0
  104. package/src/components/data/chart/NuiChart.svelte +384 -0
  105. package/src/components/data/chart/color.ts +83 -0
  106. package/src/components/data/chart/index.ts +10 -0
  107. package/src/components/data/chart/registry.ts +71 -0
  108. package/src/components/data/chart/renderers/bar.ts +90 -0
  109. package/src/components/data/chart/renderers/base.ts +32 -0
  110. package/src/components/data/chart/renderers/line.ts +99 -0
  111. package/src/components/data/chart/renderers/pie.ts +66 -0
  112. package/src/components/data/chart/scale.ts +139 -0
  113. package/src/components/data/chart/types.ts +98 -0
  114. package/src/components/data/chart/validator.ts +98 -0
  115. package/src/components/forms/NuiCheckbox.svelte +218 -0
  116. package/src/components/forms/NuiDatepicker.svelte +755 -0
  117. package/src/components/forms/NuiFileInput.svelte +1047 -0
  118. package/src/components/forms/NuiFileInput.types.ts +29 -0
  119. package/src/components/forms/NuiForm.svelte +391 -0
  120. package/src/components/forms/NuiInput.svelte +296 -0
  121. package/src/components/forms/NuiRadioGroup.svelte +238 -0
  122. package/src/components/forms/NuiSelect.svelte +819 -0
  123. package/src/components/layout/NuiCard.svelte +314 -0
  124. package/src/components/layout/NuiCardGrid.svelte +98 -0
  125. package/src/components/layout/NuiFlex.svelte +121 -0
  126. package/src/components/layout/NuiGrid.svelte +66 -0
  127. package/src/components/layout/NuiStack.svelte +138 -0
  128. package/src/components/navigation/NuiAccordion.svelte +323 -0
  129. package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
  130. package/src/components/navigation/NuiMenu.svelte +596 -0
  131. package/src/components/navigation/NuiPagination.svelte +306 -0
  132. package/src/components/navigation/NuiSidebar.svelte +186 -0
  133. package/src/components/navigation/NuiTabs.svelte +249 -0
  134. package/src/components/overlay/NuiDialog.svelte +118 -0
  135. package/src/components/overlay/NuiModal.svelte +992 -0
  136. package/src/components/overlay/NuiTooltip.svelte +159 -0
  137. package/src/composables/useClickOutside.ts +132 -0
  138. package/src/composables/useFormValidation.ts +105 -0
  139. package/src/composables/useIdle.svelte.ts +191 -0
  140. package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
  141. package/src/composables/useLocalStorage.svelte.ts +137 -0
  142. package/src/composables/useMediaQuery.svelte.ts +102 -0
  143. package/src/index.ts +66 -0
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Keyboard shortcut hooks for FiveM NUI.
3
+ */
4
+
5
+ export interface KeyboardShortcut {
6
+ /**
7
+ * The key to match case-insensitively (eg. 'e', 'Enter', 'Escape', ' ', 'F1').
8
+ * Ignored if `code` is provided.
9
+ */
10
+ key?: string;
11
+ /**
12
+ * @deprecated Use `key` instead. Alias kept for backwards compatibility.
13
+ */
14
+ event?: KeyboardEvent['key'];
15
+ /**
16
+ * Physical key code to match (e.g. 'KeyE', 'Digit1', 'Space', 'Enter').
17
+ * When provided, `key` is completely ignored in favor of `code`.
18
+ * Highly recommended for layout-independent shortcuts and combinations with Shift (e.g. Shift+1).
19
+ */
20
+ code?: string;
21
+ /**
22
+ * Require Ctrl key to be pressed in the FiveM Windows CEF environment.
23
+ */
24
+ ctrl?: boolean;
25
+ /**
26
+ * Require primary modifier key to be pressed.
27
+ * In FiveM (Windows CEF), this is equivalent to `ctrl`.
28
+ * Note: `ctrl` takes precedence over `mod` if both are defined.
29
+ */
30
+ mod?: boolean;
31
+ /**
32
+ * Require Alt key to be pressed.
33
+ */
34
+ alt?: boolean;
35
+ /**
36
+ * Require Shift key to be pressed.
37
+ */
38
+ shift?: boolean;
39
+ /**
40
+ * Require Meta (Windows key) to be pressed.
41
+ */
42
+ meta?: boolean;
43
+ /**
44
+ * When true, requires modifier keys (Ctrl, Alt, Shift, Meta) to strictly match
45
+ * the specified options. Unspecified modifiers must NOT be pressed.
46
+ * @default false
47
+ */
48
+ exact?: boolean;
49
+ /**
50
+ * Callback invoked when the shortcut matches.
51
+ */
52
+ callback: (event: KeyboardEvent) => void;
53
+ /**
54
+ * Whether the shortcut is currently enabled.
55
+ * Accepts a boolean or a reactive getter `() => boolean`.
56
+ * @default true
57
+ */
58
+ enabled?: boolean | (() => boolean);
59
+ /**
60
+ * Automatically call `event.preventDefault()`.
61
+ * @default true
62
+ */
63
+ preventDefault?: boolean;
64
+ /**
65
+ * Automatically call `event.stopPropagation()`.
66
+ * Stops bubbling up the DOM hierarchy.
67
+ * @default false
68
+ */
69
+ stopPropagation?: boolean;
70
+ /**
71
+ * Automatically call `event.stopImmediatePropagation()`.
72
+ * Prevents other listeners on the same element from being executed.
73
+ * @default false
74
+ */
75
+ stopImmediatePropagation?: boolean;
76
+ /**
77
+ * If true, suppresses shortcut execution when the active element is an input,
78
+ * textarea, select, contenteditable, or ARIA textbox/searchbox.
79
+ * @default false
80
+ */
81
+ ignoreInputs?: boolean;
82
+ /**
83
+ * Ignore repeating keydown events when holding a key down.
84
+ * @default false
85
+ */
86
+ ignoreRepeat?: boolean;
87
+ }
88
+
89
+ export interface KeyboardShortcutOptions {
90
+ /**
91
+ * Event target to attach the keydown listener to.
92
+ * Can be an EventTarget (e.g. window, document, HTMLElement) or getter function returning one (e.g. `() => myElement`).
93
+ * Note: The options object itself is evaluated once on init to make the target reactive, pass `target` as a getter function.
94
+ * Defaults to `window` (global NUI shortcuts).
95
+ */
96
+ target?: EventTarget | null | (() => EventTarget | null);
97
+ /**
98
+ * Listen during capture phase. Useful for global NUI handlers to intercept events early.
99
+ * @default false
100
+ */
101
+ capture?: boolean;
102
+ }
103
+
104
+ export type KeyboardShortcutsSource =
105
+ | KeyboardShortcut[]
106
+ | (() => KeyboardShortcut[]);
107
+
108
+ export type KeyboardShortcutSource =
109
+ | KeyboardShortcut
110
+ | (() => KeyboardShortcut);
111
+
112
+ /**
113
+ * Checks whether the focused element accepts text input, supporting Shadow DOM via event.composedPath().
114
+ */
115
+ function isTextInputFocused(event: KeyboardEvent): boolean {
116
+ if (typeof document === 'undefined') return false;
117
+
118
+ const path = event.composedPath();
119
+ const el = path[0] instanceof HTMLElement ? path[0] : document.activeElement;
120
+
121
+ if (!el || !(el instanceof HTMLElement)) return false;
122
+
123
+ const tag = el.tagName.toLowerCase();
124
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
125
+ return true;
126
+ }
127
+
128
+ if (el.isContentEditable) {
129
+ return true;
130
+ }
131
+
132
+ const role = el.getAttribute('role');
133
+ if (role === 'textbox' || role === 'searchbox') {
134
+ return true;
135
+ }
136
+
137
+ if (el.closest('[contenteditable]:not([contenteditable="false"])')) {
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ function isShortcutActive(shortcut: KeyboardShortcut, event: KeyboardEvent): boolean {
145
+ // 1. Cheap checks first (ignoreRepeat)
146
+ if (shortcut.ignoreRepeat && event.repeat) return false;
147
+
148
+ // 2. Key or Code match (code overrides key entirely)
149
+ if (shortcut.code) {
150
+ if (event.code?.toLowerCase() !== shortcut.code.toLowerCase()) return false;
151
+ } else {
152
+ const expectedKey = (shortcut.key ?? shortcut.event ?? '').toLowerCase();
153
+ if (!expectedKey) return false;
154
+ if (event.key?.toLowerCase() !== expectedKey) return false;
155
+ }
156
+
157
+ // 3. Modifier match
158
+ const expectedCtrl = shortcut.ctrl ?? shortcut.mod;
159
+
160
+ if (shortcut.exact) {
161
+ const needCtrl = Boolean(expectedCtrl);
162
+ const needAlt = Boolean(shortcut.alt);
163
+ const needShift = Boolean(shortcut.shift);
164
+ const needMeta = Boolean(shortcut.meta);
165
+
166
+ if (event.ctrlKey !== needCtrl) return false;
167
+ if (event.altKey !== needAlt) return false;
168
+ if (event.shiftKey !== needShift) return false;
169
+ if (event.metaKey !== needMeta) return false;
170
+ } else {
171
+ if (expectedCtrl !== undefined && Boolean(expectedCtrl) !== event.ctrlKey) return false;
172
+ if (shortcut.alt !== undefined && Boolean(shortcut.alt) !== event.altKey) return false;
173
+ if (shortcut.shift !== undefined && Boolean(shortcut.shift) !== event.shiftKey) return false;
174
+ if (shortcut.meta !== undefined && Boolean(shortcut.meta) !== event.metaKey) return false;
175
+ }
176
+
177
+ // 4. Expensive / side-effect checks last (enabled function, DOM queries for inputs)
178
+ if (shortcut.enabled !== undefined) {
179
+ const isAllowed = typeof shortcut.enabled === 'function' ? shortcut.enabled() : shortcut.enabled;
180
+ if (!isAllowed) return false;
181
+ }
182
+
183
+ if (shortcut.ignoreInputs && isTextInputFocused(event)) return false;
184
+
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Registers multiple keyboard shortcuts with reactive lifecycle management.
190
+ *
191
+ * Note:
192
+ * - Must be called within a component initialization context (or wrapped in `$effect.root`).
193
+ * - Array order matters: the first matching shortcut terminates evaluation via `break`.
194
+ */
195
+ export function useKeyboardShortcuts(
196
+ shortcuts: KeyboardShortcutsSource,
197
+ options?: KeyboardShortcutOptions
198
+ ): () => void {
199
+ let controller: AbortController | null = null;
200
+
201
+ function handler(event: KeyboardEvent) {
202
+ // Ignore IME composition events (e.g. active Asian text input)
203
+ if (event.isComposing) return;
204
+
205
+ const list = typeof shortcuts === 'function' ? shortcuts() : shortcuts;
206
+ if (!list || !Array.isArray(list)) return;
207
+
208
+ for (const shortcut of list) {
209
+ if (isShortcutActive(shortcut, event)) {
210
+ if (shortcut.preventDefault ?? true) {
211
+ event.preventDefault();
212
+ }
213
+ if (shortcut.stopPropagation) {
214
+ event.stopPropagation();
215
+ }
216
+ if (shortcut.stopImmediatePropagation) {
217
+ event.stopImmediatePropagation();
218
+ }
219
+ try {
220
+ shortcut.callback(event);
221
+ } catch (err) {
222
+ console.error(
223
+ '[nui-keyboard] Error in shortcut callback:',
224
+ err
225
+ );
226
+ }
227
+ break;
228
+ }
229
+ }
230
+ }
231
+
232
+ const cleanup = () => {
233
+ if (controller) {
234
+ controller.abort();
235
+ controller = null;
236
+ }
237
+ };
238
+
239
+ const setupListener = () => {
240
+ cleanup(); // Prevent memory leaks on re-binding
241
+
242
+ const targetFn = options?.target;
243
+ const target = typeof targetFn === 'function' ? targetFn() : targetFn;
244
+ const resolvedTarget = target ?? (typeof window !== 'undefined' ? window : null);
245
+
246
+ if (!resolvedTarget) return;
247
+
248
+ controller = new AbortController();
249
+ resolvedTarget.addEventListener('keydown', handler as EventListener, {
250
+ capture: options?.capture ?? false,
251
+ signal: controller.signal
252
+ });
253
+ };
254
+
255
+ // Lifecycle binding
256
+ $effect(() => {
257
+ setupListener();
258
+ return cleanup;
259
+ });
260
+
261
+ return cleanup;
262
+ }
263
+
264
+ /**
265
+ * Registers a single keyboard shortcut with reactive lifecycle management.
266
+ */
267
+ export function useKeyboardShortcut(
268
+ shortcut: KeyboardShortcutSource,
269
+ options?: KeyboardShortcutOptions
270
+ ): () => void {
271
+ return useKeyboardShortcuts(
272
+ () => {
273
+ const item = typeof shortcut === 'function' ? shortcut() : shortcut;
274
+ return item ? [item] : [];
275
+ },
276
+ options
277
+ );
278
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * LocalStorage synchronization hook for FiveM NUI.
3
+ */
4
+ export interface LocalStorageState<T> {
5
+ get current(): T;
6
+ set current(value: T);
7
+ set(value: T | ((prev: T) => T)): void;
8
+ }
9
+
10
+ const STORAGE_EVENT_PREFIX = 'nui-local-storage:';
11
+
12
+ /**
13
+ * LocalStorage synchronization hook.
14
+ *
15
+ * Automatically synchronizes state with `localStorage`, listens for cross-window
16
+ * `storage` events (including removals/clears), and dispatches custom events with
17
+ * source filtering for intra-window CEF (FiveM NUI) synchronization.
18
+ *
19
+ * Note:
20
+ * - Must be called within a component initialization context (or wrapped in `$effect.root`).
21
+ * - Mutating nested properties of objects directly (e.g. `state.current.prop = val`) does not trigger a write; use assignments or `.set()`.
22
+ * - If the key changes dynamically, the state switches to the new key, overwriting uncommitted state.
23
+ * - Write failures (e.g., storage quota exceeded) keep the memory state updated while leaving storage unchanged.
24
+ */
25
+ export function useLocalStorage<T>(
26
+ key: string | (() => string),
27
+ initialValue: T
28
+ ): LocalStorageState<T> {
29
+ // Helper to resolve reactive or static key
30
+ const resolveKey = () => (typeof key === 'function' ? key() : key);
31
+
32
+ function cloneInitial(): T {
33
+ if (initialValue === undefined) return initialValue as T;
34
+ try {
35
+ if (typeof structuredClone === 'function') {
36
+ return structuredClone(initialValue);
37
+ }
38
+ return JSON.parse(JSON.stringify(initialValue));
39
+ } catch {
40
+ return initialValue;
41
+ }
42
+ }
43
+
44
+ function readStorage(targetKey: string): T {
45
+ try {
46
+ const item = window.localStorage.getItem(targetKey);
47
+ if (item !== null) {
48
+ return JSON.parse(item);
49
+ }
50
+ } catch (error) {
51
+ console.warn(`[nui-local-storage] Error reading key "${targetKey}":`, error);
52
+ }
53
+ return cloneInitial();
54
+ }
55
+
56
+ // Synchronous initial read to prevent flash of default value on first render
57
+ let stored = $state<T>(readStorage(resolveKey()));
58
+ let loadedKey = resolveKey();
59
+ const instanceId = Symbol();
60
+
61
+ function setValue(value: T | ((prev: T) => T)) {
62
+ const currentKey = resolveKey();
63
+ const valueToStore = typeof value === 'function' ? (value as (prev: T) => T)(stored) : value;
64
+ stored = valueToStore;
65
+
66
+ try {
67
+ window.localStorage.setItem(currentKey, JSON.stringify(valueToStore));
68
+ } catch (error) {
69
+ console.warn(`[nui-local-storage] Error writing key "${currentKey}":`, error);
70
+ return; // Memory state is updated, but storage write failed
71
+ }
72
+
73
+ // Dispatch custom event for intra-window CEF synchronization (critical for FiveM NUI) only after successful write
74
+ try {
75
+ window.dispatchEvent(
76
+ new CustomEvent(`${STORAGE_EVENT_PREFIX}${currentKey}`, {
77
+ detail: { source: instanceId }
78
+ })
79
+ );
80
+ } catch (error) {
81
+ console.warn(`[nui-local-storage] Error dispatching sync event for key "${currentKey}":`, error);
82
+ }
83
+ }
84
+
85
+ const setupEffect = () => {
86
+ const currentKey = resolveKey();
87
+
88
+ // Sync state only if the key has actually changed (prevents double parsing on initial mount)
89
+ if (currentKey !== loadedKey) {
90
+ loadedKey = currentKey;
91
+ stored = readStorage(currentKey);
92
+ }
93
+
94
+ const controller = new AbortController();
95
+ const { signal } = controller;
96
+
97
+ // 1. Cross-window storage events (handles external tabs, clear(), removeItem())
98
+ window.addEventListener(
99
+ 'storage',
100
+ (event: StorageEvent) => {
101
+ if (event.key === currentKey || event.key === null) {
102
+ stored = readStorage(currentKey);
103
+ }
104
+ },
105
+ { signal }
106
+ );
107
+
108
+ // 2. Intra-window custom events (FiveM NUI CEF synchronization with fresh storage reads)
109
+ window.addEventListener(
110
+ `${STORAGE_EVENT_PREFIX}${currentKey}`,
111
+ (event: Event) => {
112
+ const customEvent = event as CustomEvent<{ source: symbol }>;
113
+ if (customEvent.detail.source !== instanceId) {
114
+ stored = readStorage(currentKey);
115
+ }
116
+ },
117
+ { signal }
118
+ );
119
+
120
+ return () => {
121
+ controller.abort();
122
+ };
123
+ };
124
+
125
+ // Reactive lifecycle management for dynamic keys
126
+ $effect(setupEffect);
127
+
128
+ return {
129
+ get current() {
130
+ return stored;
131
+ },
132
+ set current(val: T) {
133
+ setValue(val);
134
+ },
135
+ set: setValue
136
+ };
137
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Media query hooks for FiveM NUI.
3
+ */
4
+
5
+ export interface MediaQueryState {
6
+ readonly matches: boolean;
7
+ }
8
+
9
+ export type MediaQuerySource = string | (() => string);
10
+
11
+ /**
12
+ * Media query listener.
13
+ *
14
+ * Supports both static query strings and reactive getters (`() => string`).
15
+ * Automatically manages event listener lifecycle via `$effect`.
16
+ *
17
+ * Note: Must be called within a component initialization context (or wrapped in `$effect.root`).
18
+ */
19
+ export function useMediaQuery(querySource: MediaQuerySource): MediaQueryState {
20
+ const resolveQuery = () => (typeof querySource === 'function' ? querySource() : querySource);
21
+
22
+ let matches = $state<boolean>(
23
+ typeof window !== 'undefined' ? window.matchMedia(resolveQuery()).matches : false
24
+ );
25
+
26
+ $effect(() => {
27
+ const query = resolveQuery();
28
+ const mediaQuery = window.matchMedia(query);
29
+
30
+ // Sync matches state in case the query changed
31
+ matches = mediaQuery.matches;
32
+
33
+ const handler = (event: MediaQueryListEvent) => {
34
+ matches = event.matches;
35
+ };
36
+
37
+ mediaQuery.addEventListener('change', handler);
38
+
39
+ return () => {
40
+ mediaQuery.removeEventListener('change', handler);
41
+ };
42
+ });
43
+
44
+ return {
45
+ get matches() {
46
+ return matches;
47
+ }
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Tracks whether the viewport width is strictly less than the specified breakpoint.
53
+ * Uses modern CSS range syntax (fully supported in modern Chromium/FiveM CEF).
54
+ * Accepts any valid CSS length unit (e.g. '768px', '48rem').
55
+ * @default breakpoint '768px'
56
+ */
57
+ export function useIsMobile(breakpoint = '768px'): MediaQueryState {
58
+ return useMediaQuery(`(width < ${breakpoint})`);
59
+ }
60
+
61
+ /**
62
+ * Tracks whether the viewport width falls within the tablet range using modern CSS range syntax.
63
+ * Accepts any valid CSS length units.
64
+ * @default min '768px', max '1024px'
65
+ */
66
+ export function useIsTablet(min = '768px', max = '1024px'): MediaQueryState {
67
+ return useMediaQuery(`(${min} <= width < ${max})`);
68
+ }
69
+
70
+ /**
71
+ * Tracks whether the viewport width is greater than or equal to the specified breakpoint.
72
+ * Accepts any valid CSS length units.
73
+ * @default breakpoint '1024px'
74
+ */
75
+ export function useIsDesktop(breakpoint = '1024px'): MediaQueryState {
76
+ return useMediaQuery(`(width >= ${breakpoint})`);
77
+ }
78
+
79
+ /**
80
+ * Tracks dark mode preference.
81
+ * Note: In embedded CEF (FiveM NUI), OS-level color scheme detection may not reflect host system settings;
82
+ * consider overriding or syncing this via `useLocalStorage` if your UI supports manual theme switching.
83
+ */
84
+ export function useDarkMode(): MediaQueryState {
85
+ return useMediaQuery('(prefers-color-scheme: dark)');
86
+ }
87
+
88
+ /**
89
+ * Tracks reduced motion preference.
90
+ * Note: In embedded CEF (FiveM NUI), OS-level accessibility settings may not propagate from the host system.
91
+ */
92
+ export function useReducedMotion(): MediaQueryState {
93
+ return useMediaQuery('(prefers-reduced-motion: reduce)');
94
+ }
95
+
96
+ /**
97
+ * Tracks high contrast / forced colors mode.
98
+ * Note: In embedded CEF (FiveM NUI), system contrast settings may not propagate from the host system.
99
+ */
100
+ export function useHighContrast(): MediaQueryState {
101
+ return useMediaQuery('(forced-colors: active)');
102
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ // Actions
2
+ export { default as NuiButton } from './components/actions/NuiButton.svelte';
3
+
4
+ // Forms
5
+ export { default as NuiForm } from './components/forms/NuiForm.svelte';
6
+ export { default as NuiInput } from './components/forms/NuiInput.svelte';
7
+ export { default as NuiCheckbox } from './components/forms/NuiCheckbox.svelte';
8
+ export { default as NuiRadioGroup } from './components/forms/NuiRadioGroup.svelte';
9
+ export type { NuiRadioOption } from './components/forms/NuiRadioGroup.svelte';
10
+ export { default as NuiSelect } from './components/forms/NuiSelect.svelte';
11
+ export type { NuiSelectOption } from './components/forms/NuiSelect.svelte';
12
+ export { default as NuiDatepicker } from './components/forms/NuiDatepicker.svelte';
13
+ export { default as NuiFileInput } from './components/forms/NuiFileInput.svelte';
14
+ export type { FileInfo, FileMetadata } from './components/forms/NuiFileInput.types';
15
+
16
+ // Layout
17
+ export { default as NuiCard } from './components/layout/NuiCard.svelte';
18
+ export { default as NuiCardGrid } from './components/layout/NuiCardGrid.svelte';
19
+ export { default as NuiFlex } from './components/layout/NuiFlex.svelte';
20
+ export { default as NuiGrid } from './components/layout/NuiGrid.svelte';
21
+ export { default as NuiStack } from './components/layout/NuiStack.svelte';
22
+
23
+ // Navigation
24
+ export { default as NuiAccordion } from './components/navigation/NuiAccordion.svelte';
25
+ export type { AccordionItem } from './components/navigation/NuiAccordion.svelte';
26
+ export { default as NuiBreadcrumbs } from './components/navigation/NuiBreadcrumbs.svelte';
27
+ export { default as NuiMenu } from './components/navigation/NuiMenu.svelte';
28
+ export { default as NuiPagination } from './components/navigation/NuiPagination.svelte';
29
+ export { default as NuiSidebar } from './components/navigation/NuiSidebar.svelte';
30
+ export { default as NuiTabs } from './components/navigation/NuiTabs.svelte';
31
+ export type { Tab } from './components/navigation/NuiTabs.svelte';
32
+
33
+ // Overlay
34
+ export { default as NuiDialog } from './components/overlay/NuiDialog.svelte';
35
+ export type { NuiDialogAction } from './components/overlay/NuiDialog.svelte';
36
+ export { default as NuiModal } from './components/overlay/NuiModal.svelte';
37
+ export type { ModalAction } from './components/overlay/NuiModal.svelte';
38
+ export { default as NuiTooltip } from './components/overlay/NuiTooltip.svelte';
39
+
40
+ // Data display
41
+ export { default as NuiBadge } from './components/data/NuiBadge.svelte';
42
+ export { default as NuiChart } from './components/data/chart/NuiChart.svelte';
43
+ export type { ChartData, ChartDataset, ChartOptions, ChartType } from './components/data/chart/types';
44
+ export { default as NuiCodeBlock } from './components/data/NuiCodeBlock.svelte';
45
+ export { default as NuiList } from './components/data/NuiList.svelte';
46
+ export type { NuiListItem } from './components/data/NuiList.svelte';
47
+ export { default as NuiProgressBar } from './components/data/NuiProgressBar.svelte';
48
+ export { default as NuiRichText } from './components/data/NuiRichText.svelte';
49
+ export { default as NuiTable } from './components/data/NuiTable.svelte';
50
+ export type { Column as NuiTableColumn } from './components/data/NuiTable.svelte';
51
+
52
+ // Composables
53
+ export { validateForm, validateField } from './composables/useFormValidation';
54
+ export type { ValidationRules } from './composables/useFormValidation';
55
+ export {
56
+ useKeyboardShortcut,
57
+ useKeyboardShortcuts,
58
+ type KeyboardShortcut,
59
+ type KeyboardShortcutOptions,
60
+ type KeyboardShortcutsSource,
61
+ type KeyboardShortcutSource
62
+ } from './composables/useKeyboardShortcut.svelte';
63
+ export { useClickOutside, useClickOutsideElement, clickOutsideAction, type ClickOutsideOptions } from './composables/useClickOutside';
64
+ export { useLocalStorage, type LocalStorageState } from './composables/useLocalStorage.svelte';
65
+ export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop, useDarkMode, useReducedMotion, useHighContrast, type MediaQueryState } from './composables/useMediaQuery.svelte';
66
+ export { useIdle, useIdleUntil, type IdleState } from './composables/useIdle.svelte';