@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,93 @@
1
+ export type { FileInfo, FileMetadata } from "./NuiFileInput.types";
2
+ import type { SvelteNuiClient } from "@fivem-nui/svelte";
3
+ import type { FileInfo } from "./NuiFileInput.types";
4
+ interface Props {
5
+ /** Optional HTML id attribute. Auto-generated via $props.id() if omitted. */
6
+ id?: string;
7
+ /** Form input name attribute. */
8
+ name?: string;
9
+ /** Visual label text displayed above the control. */
10
+ label?: string;
11
+ /** Secondary description text displayed below label. */
12
+ description?: string;
13
+ /**
14
+ * Accepted file types, e.g. "image/*", ".pdf", "image/png,image/jpeg".
15
+ * Validates both MIME types and file extensions with fallback for empty MIME types.
16
+ * Evaluated during user interactions (file picker, drag & drop).
17
+ * Defaults to empty string (all file types accepted).
18
+ */
19
+ accept?: string;
20
+ /**
21
+ * Whether multiple file selection is enabled. Defaults to false.
22
+ * If dynamically switched at runtime, values are automatically normalized.
23
+ */
24
+ multiple?: boolean;
25
+ /**
26
+ * Maximum allowed file size in bytes per single file. Must be > 0.
27
+ * Evaluated during user interactions (file picker, drag & drop).
28
+ */
29
+ maxSize?: number;
30
+ /**
31
+ * Maximum allowed total number of files when multiple=true. Must be > 0.
32
+ * Evaluated during user interactions (file picker, drag & drop) and reactively derived
33
+ * as an external limit error if the parent supplies or configures a limit lower than
34
+ * the current file count (without destructively truncating the parent's external data).
35
+ */
36
+ maxFiles?: number;
37
+ /**
38
+ * Selected raw DOM File object or array of File objects.
39
+ * Serves as the single canonical source of truth for selected files.
40
+ * When multiple=false: File | null.
41
+ * When multiple=true: File[].
42
+ * Supports two-way binding.
43
+ */
44
+ value?: File | File[] | null;
45
+ /**
46
+ * Read-only metadata array of all currently selected files.
47
+ * Represents the active UI metadata, including temporary ObjectURLs when showPreview=true.
48
+ * Supports two-way binding.
49
+ */
50
+ files?: FileInfo[];
51
+ /** Whether the file input is disabled. */
52
+ disabled?: boolean;
53
+ /** Placeholder prompt text displayed in the dropzone when no files are chosen. */
54
+ placeholder?: string;
55
+ /** Whether to render preview list and generate image ObjectURLs. Defaults to true. */
56
+ showPreview?: boolean;
57
+ /** Accessible label for screen readers when visual label is omitted. */
58
+ ariaLabel?: string;
59
+ /** External validation error message displayed below the input. */
60
+ error?: string;
61
+ /**
62
+ * Optional NUI event name sent on file addition/removal.
63
+ * Transmits clean FileMetadata ({ fileInfos: FileMetadata[], count }).
64
+ */
65
+ eventName?: string;
66
+ /**
67
+ * Callback fired when the user adds or removes files through UI interactions
68
+ * (native file picker dialog, drag-and-drop, or remove button).
69
+ * Does not fire on external programmatic changes to the `value` prop to prevent circular update loops.
70
+ */
71
+ onChange?: (files: FileInfo[], rawFiles: File | File[] | null) => void;
72
+ /** Callback fired when validation fails (e.g. exceeds maxSize or maxFiles). */
73
+ onError?: (errorMessage: string) => void;
74
+ /** SvelteNuiClient instance for event transport. */
75
+ client?: SvelteNuiClient;
76
+ }
77
+ /**
78
+ * NuiFileInput
79
+ * Accessible file input component conforming to W3C standards with drag-and-drop,
80
+ * per-file size constraints, total file count limits, reactive preview generation,
81
+ * memory leak-free ObjectURL management, and FiveM NUI event synchronization.
82
+ *
83
+ * Important note for FiveM NUI:
84
+ * Due to CEF browser sandbox and JSON serialization boundaries, the NUI event transport
85
+ * sends clean file metadata (FileMetadata: id, name, size, type), NOT temporary browser
86
+ * ObjectURLs ('blob:...') nor binary file contents. If the C# FiveM resource requires raw
87
+ * file content, serialize it via FileReader (ArrayBuffer/base64) before transmission.
88
+ * Client-side 'accept' and size validation serve as immediate UX guards; server/resource
89
+ * handlers should always independently re-validate transmitted file data.
90
+ */
91
+ declare const NuiFileInput: import("svelte").Component<Props, {}, "value" | "files">;
92
+ type NuiFileInput = ReturnType<typeof NuiFileInput>;
93
+ export default NuiFileInput;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * UI representation of a selected file, including optional browser preview URL.
3
+ */
4
+ export interface FileInfo {
5
+ /** Unique stable identifier for the file entry. */
6
+ id: string;
7
+ /** File name including extension. */
8
+ name: string;
9
+ /** File size in bytes. */
10
+ size: number;
11
+ /** MIME type of the file. */
12
+ type: string;
13
+ /** Browser-only temporary ObjectURL for image preview (if preview enabled). */
14
+ url?: string;
15
+ }
16
+ /**
17
+ * Serialized metadata payload transmitted over FiveM NUI (omits ephemeral browser URLs).
18
+ */
19
+ export interface FileMetadata {
20
+ /** Unique stable identifier for the file entry. */
21
+ id: string;
22
+ /** File name including extension. */
23
+ name: string;
24
+ /** File size in bytes. */
25
+ size: number;
26
+ /** MIME type of the file. */
27
+ type: string;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,391 @@
1
+ <!--
2
+ @component NuiForm
3
+ Accessible form container for FiveM NUI applications with client-side validation rules,
4
+ asynchronous submission locking, CEF-safe confirmation dialog support, resilient error
5
+ handling, and dual event synchronization.
6
+ -->
7
+ <script lang="ts">
8
+ import { onDestroy } from "svelte";
9
+ import { nui, isBrowser } from "@fivem-nui/svelte";
10
+ import type { SvelteNuiClient } from "@fivem-nui/svelte";
11
+ // Universal form validation engine
12
+ import { validateForm } from "../../composables/useFormValidation";
13
+ import type { ValidationRules } from "../../composables/useFormValidation";
14
+
15
+ interface Props {
16
+ /** Main event sent on submit with {@link payload} (or {@link values}). */
17
+ eventName?: string;
18
+ /** Secondary or pre-submit event sent just before {@link eventName} with {@link onSubmitPayload} (or {@link values}). */
19
+ onSubmit?: string;
20
+ /** Event sent when form submission succeeds. */
21
+ onSubmitSuccess?: string;
22
+ /** Event sent when form submission fails. */
23
+ onSubmitError?: string;
24
+ /** Specific payload sent with {@link onSubmit} instead of values. Defaults to {@link values}. */
25
+ onSubmitPayload?: unknown;
26
+ /** Specific payload sent with {@link eventName} instead of values. Defaults to {@link values}. */
27
+ payload?: unknown;
28
+ /** SvelteNuiClient instance for event transport. */
29
+ client?: SvelteNuiClient;
30
+ /**
31
+ * Whether network transport is currently in progress. Supports two-way binding.
32
+ *
33
+ * The component manages this flag only during its own transport phase.
34
+ * It will not reset `loading` to `false` unless it was the one that set it to `true`.
35
+ * If a parent binds `loading` for independent operations, those will not be clobbered.
36
+ */
37
+ loading?: boolean;
38
+ /** Whether the form is disabled. Supports two-way binding. */
39
+ disabled?: boolean;
40
+ /**
41
+ * Whether to automatically reset validation error display status on success.
42
+ * Note: This only resets the submission visibility flag (`submitted`),
43
+ * it does not reset field `values` or native input DOM values.
44
+ * Defaults to false.
45
+ */
46
+ autoReset?: boolean;
47
+ /** Whether confirmation is required before submitting. Defaults to false. */
48
+ confirm?: boolean;
49
+ /** Confirmation message prompt text. Defaults to 'Are you sure you want to submit?'. */
50
+ confirmMessage?: string;
51
+ /** Optional async confirmation handler (e.g. opens custom NuiDialog) safe for FiveM CEF. */
52
+ confirmCallback?: (message: string) => boolean | Promise<boolean>;
53
+ /** Field values validated against rules before the submission event is sent. */
54
+ values?: Record<string, unknown>;
55
+ /** Validation rules mapped by field name. */
56
+ rules?: Record<string, ValidationRules>;
57
+ /**
58
+ * Called locally after a successful submit.
59
+ * Receives the primary event payload (`payload ?? values ?? {}`).
60
+ */
61
+ onSubmitted?: (payload: unknown) => void | Promise<void>;
62
+ /**
63
+ * Called locally when submission is rejected due to validation failure,
64
+ * validation rule exception, CEF confirmation unavailability,
65
+ * confirmation callback exception, or backend transport error.
66
+ * Supports synchronous or asynchronous handlers.
67
+ */
68
+ onRejected?: (error: unknown) => void | Promise<void>;
69
+ children?: import("svelte").Snippet;
70
+ }
71
+
72
+ let {
73
+ eventName = "",
74
+ onSubmit = "",
75
+ onSubmitSuccess = "",
76
+ onSubmitError = "",
77
+ onSubmitPayload = undefined,
78
+ payload = undefined,
79
+ client = nui,
80
+ loading = $bindable(false),
81
+ disabled = $bindable(false),
82
+ autoReset = false,
83
+ confirm = false,
84
+ confirmMessage = "Are you sure you want to submit?",
85
+ confirmCallback,
86
+ values = {},
87
+ rules = {},
88
+ onSubmitted,
89
+ onRejected,
90
+ children,
91
+ }: Props = $props();
92
+
93
+ let submitted = $state(false);
94
+ let isSubmitting = false;
95
+ let isDestroyed = false;
96
+
97
+ onDestroy(() => {
98
+ isDestroyed = true;
99
+ });
100
+
101
+ /**
102
+ * Tracks whether the component itself activated `loading`.
103
+ * Prevents `finally` from clobbering a parent-controlled `loading` binding.
104
+ */
105
+ let ownLoading = false;
106
+
107
+ // Real-time validation errors derived from current values and rules
108
+ const errors = $derived(
109
+ Object.keys(rules).length
110
+ ? validateForm(values, rules)
111
+ : ({} as Record<string, string>),
112
+ );
113
+ const isValid = $derived(Object.keys(errors).length === 0);
114
+
115
+ // Live active errors displayed to the user once submission has been attempted
116
+ const activeErrors = $derived(
117
+ submitted ? errors : ({} as Record<string, string>),
118
+ );
119
+ const hasErrors = $derived(submitted && Object.keys(errors).length > 0);
120
+
121
+ /**
122
+ * Resilient event dispatcher that safely captures synchronous exceptions
123
+ * and unhandled Promise rejections from custom or asynchronous NUI clients.
124
+ */
125
+ function safeSend(event: string, data: unknown) {
126
+ if (!event) return;
127
+ try {
128
+ const res = client.send(event, data) as unknown;
129
+ if (res && typeof (res as Promise<unknown>).catch === "function") {
130
+ (res as Promise<unknown>).catch((err: unknown) => {
131
+ console.error(
132
+ `[fivem-nui-components] Async error sending form event '${event}':`,
133
+ err,
134
+ );
135
+ });
136
+ }
137
+ } catch (err) {
138
+ console.error(
139
+ `[fivem-nui-components] Failed to send form event '${event}':`,
140
+ err,
141
+ );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Safely invokes the onRejected callback, protecting against unhandled rejections
147
+ * or exceptions thrown by consumer application code.
148
+ */
149
+ function safeReject(reason: unknown) {
150
+ try {
151
+ const res = onRejected?.(reason);
152
+ if (res && typeof (res as Promise<void>).catch === "function") {
153
+ (res as Promise<void>).catch((err: unknown) => {
154
+ console.error(
155
+ "[fivem-nui-components] NuiForm: Unhandled rejection in onRejected callback:",
156
+ err,
157
+ );
158
+ });
159
+ }
160
+ } catch (err) {
161
+ console.error(
162
+ "[fivem-nui-components] NuiForm: Exception in onRejected callback:",
163
+ err,
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Safely invokes the onSubmitted callback, ensuring consumer application errors
170
+ * are isolated and never misclassified as transport failures.
171
+ */
172
+ function safeSubmitted(body: unknown) {
173
+ try {
174
+ const res = onSubmitted?.(body);
175
+ if (res && typeof (res as Promise<void>).catch === "function") {
176
+ (res as Promise<void>).catch((err: unknown) => {
177
+ console.error(
178
+ "[fivem-nui-components] NuiForm: Unhandled rejection in onSubmitted callback:",
179
+ err,
180
+ );
181
+ });
182
+ }
183
+ } catch (callbackError) {
184
+ console.error(
185
+ "[fivem-nui-components] NuiForm: Exception in onSubmitted callback:",
186
+ callbackError,
187
+ );
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Resets the submission and error display state of the form (`submitted = false`).
193
+ * Does not mutate or clear field `values`.
194
+ */
195
+ export function reset() {
196
+ submitted = false;
197
+ }
198
+
199
+ async function handleSubmit(event: SubmitEvent) {
200
+ event.preventDefault();
201
+ // 1. Immediate lock against concurrent / double submissions
202
+ if (disabled || loading || isSubmitting) return;
203
+
204
+ submitted = true;
205
+
206
+ // 2. Validate fields — wrapped as an error boundary so a broken custom
207
+ // rule cannot crash handleSubmit with an unhandled exception.
208
+ let validationFailed = false;
209
+ try {
210
+ validationFailed = !isValid;
211
+ } catch (validationError) {
212
+ console.error(
213
+ "[fivem-nui-components] NuiForm: Exception during validation:",
214
+ validationError,
215
+ );
216
+ safeReject(validationError);
217
+ return;
218
+ }
219
+
220
+ if (validationFailed) {
221
+ safeReject(errors);
222
+ return;
223
+ }
224
+
225
+ // Lock submission lifecycle immediately
226
+ isSubmitting = true;
227
+
228
+ try {
229
+ // 3. CEF-safe confirmation handling
230
+ if (confirm) {
231
+ let confirmed = false;
232
+ try {
233
+ if (typeof confirmCallback === "function") {
234
+ confirmed = await confirmCallback(confirmMessage);
235
+ } else if (
236
+ isBrowser() &&
237
+ typeof window !== "undefined" &&
238
+ typeof window.confirm === "function"
239
+ ) {
240
+ confirmed = window.confirm(confirmMessage);
241
+ } else {
242
+ const cefError = new Error(
243
+ "window.confirm() is unavailable in FiveM CEF and confirmCallback was not provided.",
244
+ );
245
+ console.warn(
246
+ "[fivem-nui-components] NuiForm:",
247
+ cefError.message,
248
+ "Aborting submission.",
249
+ );
250
+ confirmed = false;
251
+ safeReject(cefError);
252
+ return;
253
+ }
254
+ } catch (confirmError) {
255
+ console.error(
256
+ "[fivem-nui-components] NuiForm: Exception in confirmCallback:",
257
+ confirmError,
258
+ );
259
+ confirmed = false;
260
+ safeReject(confirmError);
261
+ return;
262
+ }
263
+
264
+ // Re-check state after async confirmation:
265
+ // - component unmounted while dialog was open
266
+ // - user cancelled
267
+ // - parent set disabled = true while dialog was open
268
+ // - parent set loading = true for an independent operation
269
+ if (isDestroyed || !confirmed || disabled || loading) {
270
+ return;
271
+ }
272
+ }
273
+
274
+ // 4. Mark loading as true only during actual backend transport.
275
+ // Track ownership so finally does not clobber a parent-controlled binding.
276
+ loading = true;
277
+ ownLoading = true;
278
+
279
+ // Decouple onSubmitPayload vs payload, falling back to values.
280
+ const submitBody = onSubmitPayload ?? values ?? {};
281
+ const eventBody = payload ?? values ?? {};
282
+
283
+ // 5. Isolated backend transport execution.
284
+ // Events are sent sequentially, NOT atomically — if onSubmit succeeds
285
+ // but eventName fails, the first operation's side effects persist.
286
+ // For atomic semantics, handle both steps in a single C# callback.
287
+ try {
288
+ if (onSubmit) await client.sendAndWait(onSubmit, submitBody);
289
+ if (isDestroyed) return;
290
+ if (eventName) await client.sendAndWait(eventName, eventBody);
291
+ if (isDestroyed) return;
292
+ } catch (transportError) {
293
+ if (isDestroyed) return;
294
+ const message =
295
+ transportError instanceof Error
296
+ ? transportError.message
297
+ : String(transportError);
298
+ if (onSubmitError) safeSend(onSubmitError, { error: message });
299
+ safeReject(transportError);
300
+ return;
301
+ }
302
+
303
+ // 6. Transport succeeded: emit success event safely if still mounted
304
+ if (!isDestroyed && onSubmitSuccess) safeSend(onSubmitSuccess, { success: true });
305
+
306
+ if (!isDestroyed && autoReset) {
307
+ submitted = false;
308
+ }
309
+
310
+ // 7. Invoke local onSubmitted callback with eventBody (main event payload)
311
+ if (!isDestroyed) safeSubmitted(eventBody);
312
+ } finally {
313
+ if (!isDestroyed) {
314
+ if (ownLoading) {
315
+ loading = false;
316
+ ownLoading = false;
317
+ }
318
+ isSubmitting = false;
319
+ }
320
+ }
321
+ }
322
+ </script>
323
+
324
+ <form
325
+ class="nui-form"
326
+ class:is-invalid={hasErrors}
327
+ class:is-disabled={disabled}
328
+ class:is-loading={loading}
329
+ onsubmit={handleSubmit}
330
+ >
331
+ <div class="nui-form-content">
332
+ {@render children?.()}
333
+ </div>
334
+
335
+ {#if hasErrors}
336
+ <ul class="nui-form-errors" role="alert" aria-live="polite">
337
+ {#each Object.entries(activeErrors) as [field, message] (field)}
338
+ <li class="nui-form-error-item" data-field={field}>
339
+ <span class="nui-form-error-field">{field}:</span>
340
+ {message}
341
+ </li>
342
+ {/each}
343
+ </ul>
344
+ {/if}
345
+ </form>
346
+
347
+ <style>
348
+ .nui-form {
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 0.75rem;
352
+ width: 100%;
353
+ font-family: inherit;
354
+ box-sizing: border-box;
355
+ }
356
+
357
+ .nui-form-content {
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 0.75rem;
361
+ }
362
+
363
+ .nui-form.is-disabled {
364
+ opacity: 0.5;
365
+ pointer-events: none;
366
+ }
367
+
368
+ .nui-form.is-loading {
369
+ cursor: wait;
370
+ }
371
+
372
+ .nui-form-errors {
373
+ margin: 0;
374
+ padding-left: 1.25rem;
375
+ color: var(--nui-danger, #ef4444);
376
+ font-size: 0.8125rem;
377
+ line-height: 1.4;
378
+ display: flex;
379
+ flex-direction: column;
380
+ gap: 0.25rem;
381
+ }
382
+
383
+ .nui-form-error-item {
384
+ word-break: break-word;
385
+ }
386
+
387
+ .nui-form-error-field {
388
+ font-weight: 600;
389
+ text-transform: capitalize;
390
+ }
391
+ </style>
@@ -0,0 +1,69 @@
1
+ import type { SvelteNuiClient } from "@fivem-nui/svelte";
2
+ import type { ValidationRules } from "../../composables/useFormValidation";
3
+ interface Props {
4
+ /** Main event sent on submit with {@link payload} (or {@link values}). */
5
+ eventName?: string;
6
+ /** Secondary or pre-submit event sent just before {@link eventName} with {@link onSubmitPayload} (or {@link values}). */
7
+ onSubmit?: string;
8
+ /** Event sent when form submission succeeds. */
9
+ onSubmitSuccess?: string;
10
+ /** Event sent when form submission fails. */
11
+ onSubmitError?: string;
12
+ /** Specific payload sent with {@link onSubmit} instead of values. Defaults to {@link values}. */
13
+ onSubmitPayload?: unknown;
14
+ /** Specific payload sent with {@link eventName} instead of values. Defaults to {@link values}. */
15
+ payload?: unknown;
16
+ /** SvelteNuiClient instance for event transport. */
17
+ client?: SvelteNuiClient;
18
+ /**
19
+ * Whether network transport is currently in progress. Supports two-way binding.
20
+ *
21
+ * The component manages this flag only during its own transport phase.
22
+ * It will not reset `loading` to `false` unless it was the one that set it to `true`.
23
+ * If a parent binds `loading` for independent operations, those will not be clobbered.
24
+ */
25
+ loading?: boolean;
26
+ /** Whether the form is disabled. Supports two-way binding. */
27
+ disabled?: boolean;
28
+ /**
29
+ * Whether to automatically reset validation error display status on success.
30
+ * Note: This only resets the submission visibility flag (`submitted`),
31
+ * it does not reset field `values` or native input DOM values.
32
+ * Defaults to false.
33
+ */
34
+ autoReset?: boolean;
35
+ /** Whether confirmation is required before submitting. Defaults to false. */
36
+ confirm?: boolean;
37
+ /** Confirmation message prompt text. Defaults to 'Are you sure you want to submit?'. */
38
+ confirmMessage?: string;
39
+ /** Optional async confirmation handler (e.g. opens custom NuiDialog) safe for FiveM CEF. */
40
+ confirmCallback?: (message: string) => boolean | Promise<boolean>;
41
+ /** Field values validated against rules before the submission event is sent. */
42
+ values?: Record<string, unknown>;
43
+ /** Validation rules mapped by field name. */
44
+ rules?: Record<string, ValidationRules>;
45
+ /**
46
+ * Called locally after a successful submit.
47
+ * Receives the primary event payload (`payload ?? values ?? {}`).
48
+ */
49
+ onSubmitted?: (payload: unknown) => void | Promise<void>;
50
+ /**
51
+ * Called locally when submission is rejected due to validation failure,
52
+ * validation rule exception, CEF confirmation unavailability,
53
+ * confirmation callback exception, or backend transport error.
54
+ * Supports synchronous or asynchronous handlers.
55
+ */
56
+ onRejected?: (error: unknown) => void | Promise<void>;
57
+ children?: import("svelte").Snippet;
58
+ }
59
+ /**
60
+ * NuiForm
61
+ * Accessible form container for FiveM NUI applications with client-side validation rules,
62
+ * asynchronous submission locking, CEF-safe confirmation dialog support, resilient error
63
+ * handling, and dual event synchronization.
64
+ */
65
+ declare const NuiForm: import("svelte").Component<Props, {
66
+ reset: () => void;
67
+ }, "loading" | "disabled">;
68
+ type NuiForm = ReturnType<typeof NuiForm>;
69
+ export default NuiForm;