@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.
- package/README.md +101 -2
- package/dist/components/actions/NuiButton.svelte +292 -0
- package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
- package/dist/components/data/NuiBadge.svelte +277 -0
- package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
- package/dist/components/data/NuiCodeBlock.svelte +256 -0
- package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
- package/dist/components/data/NuiList.svelte +341 -0
- package/dist/components/data/NuiList.svelte.d.ts +52 -0
- package/dist/components/data/NuiProgressBar.svelte +264 -0
- package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
- package/dist/components/data/NuiRichText.svelte +260 -0
- package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
- package/dist/components/data/NuiTable.svelte +470 -0
- package/dist/components/data/NuiTable.svelte.d.ts +53 -0
- package/dist/components/data/chart/NuiChart.svelte +384 -0
- package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
- package/dist/components/data/chart/color.d.ts +9 -0
- package/dist/components/data/chart/color.js +78 -0
- package/dist/components/data/chart/index.d.ts +10 -0
- package/dist/components/data/chart/index.js +10 -0
- package/dist/components/data/chart/registry.d.ts +14 -0
- package/dist/components/data/chart/registry.js +53 -0
- package/dist/components/data/chart/renderers/bar.d.ts +8 -0
- package/dist/components/data/chart/renderers/bar.js +79 -0
- package/dist/components/data/chart/renderers/base.d.ts +18 -0
- package/dist/components/data/chart/renderers/base.js +10 -0
- package/dist/components/data/chart/renderers/line.d.ts +8 -0
- package/dist/components/data/chart/renderers/line.js +89 -0
- package/dist/components/data/chart/renderers/pie.d.ts +10 -0
- package/dist/components/data/chart/renderers/pie.js +55 -0
- package/dist/components/data/chart/scale.d.ts +22 -0
- package/dist/components/data/chart/scale.js +101 -0
- package/dist/components/data/chart/types.d.ts +84 -0
- package/dist/components/data/chart/types.js +1 -0
- package/dist/components/data/chart/validator.d.ts +10 -0
- package/dist/components/data/chart/validator.js +82 -0
- package/dist/components/forms/NuiCheckbox.svelte +218 -0
- package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
- package/dist/components/forms/NuiDatepicker.svelte +755 -0
- package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
- package/dist/components/forms/NuiFileInput.svelte +1047 -0
- package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
- package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
- package/dist/components/forms/NuiFileInput.types.js +1 -0
- package/dist/components/forms/NuiForm.svelte +391 -0
- package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
- package/dist/components/forms/NuiInput.svelte +296 -0
- package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
- package/dist/components/forms/NuiRadioGroup.svelte +238 -0
- package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
- package/dist/components/forms/NuiSelect.svelte +819 -0
- package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
- package/dist/components/layout/NuiCard.svelte +314 -0
- package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
- package/dist/components/layout/NuiCardGrid.svelte +98 -0
- package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
- package/dist/components/layout/NuiFlex.svelte +121 -0
- package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
- package/dist/components/layout/NuiGrid.svelte +66 -0
- package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
- package/dist/components/layout/NuiStack.svelte +138 -0
- package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiAccordion.svelte +323 -0
- package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
- package/dist/components/navigation/NuiMenu.svelte +596 -0
- package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
- package/dist/components/navigation/NuiPagination.svelte +306 -0
- package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
- package/dist/components/navigation/NuiSidebar.svelte +186 -0
- package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiTabs.svelte +249 -0
- package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
- package/dist/components/overlay/NuiDialog.svelte +118 -0
- package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
- package/dist/components/overlay/NuiModal.svelte +992 -0
- package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
- package/dist/components/overlay/NuiTooltip.svelte +159 -0
- package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
- package/dist/composables/useClickOutside.d.ts +43 -0
- package/dist/composables/useClickOutside.js +73 -0
- package/dist/composables/useFormValidation.d.ts +23 -0
- package/dist/composables/useFormValidation.js +72 -0
- package/dist/composables/useIdle.svelte.d.ts +18 -0
- package/dist/composables/useIdle.svelte.js +144 -0
- package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
- package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
- package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
- package/dist/composables/useLocalStorage.svelte.js +105 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
- package/dist/composables/useMediaQuery.svelte.js +80 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42 -0
- package/package.json +58 -3
- package/src/components/actions/NuiButton.svelte +292 -0
- package/src/components/data/NuiBadge.svelte +277 -0
- package/src/components/data/NuiCodeBlock.svelte +256 -0
- package/src/components/data/NuiList.svelte +341 -0
- package/src/components/data/NuiProgressBar.svelte +264 -0
- package/src/components/data/NuiRichText.svelte +260 -0
- package/src/components/data/NuiTable.svelte +470 -0
- package/src/components/data/chart/NuiChart.svelte +384 -0
- package/src/components/data/chart/color.ts +83 -0
- package/src/components/data/chart/index.ts +10 -0
- package/src/components/data/chart/registry.ts +71 -0
- package/src/components/data/chart/renderers/bar.ts +90 -0
- package/src/components/data/chart/renderers/base.ts +32 -0
- package/src/components/data/chart/renderers/line.ts +99 -0
- package/src/components/data/chart/renderers/pie.ts +66 -0
- package/src/components/data/chart/scale.ts +139 -0
- package/src/components/data/chart/types.ts +98 -0
- package/src/components/data/chart/validator.ts +98 -0
- package/src/components/forms/NuiCheckbox.svelte +218 -0
- package/src/components/forms/NuiDatepicker.svelte +755 -0
- package/src/components/forms/NuiFileInput.svelte +1047 -0
- package/src/components/forms/NuiFileInput.types.ts +29 -0
- package/src/components/forms/NuiForm.svelte +391 -0
- package/src/components/forms/NuiInput.svelte +296 -0
- package/src/components/forms/NuiRadioGroup.svelte +238 -0
- package/src/components/forms/NuiSelect.svelte +819 -0
- package/src/components/layout/NuiCard.svelte +314 -0
- package/src/components/layout/NuiCardGrid.svelte +98 -0
- package/src/components/layout/NuiFlex.svelte +121 -0
- package/src/components/layout/NuiGrid.svelte +66 -0
- package/src/components/layout/NuiStack.svelte +138 -0
- package/src/components/navigation/NuiAccordion.svelte +323 -0
- package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/src/components/navigation/NuiMenu.svelte +596 -0
- package/src/components/navigation/NuiPagination.svelte +306 -0
- package/src/components/navigation/NuiSidebar.svelte +186 -0
- package/src/components/navigation/NuiTabs.svelte +249 -0
- package/src/components/overlay/NuiDialog.svelte +118 -0
- package/src/components/overlay/NuiModal.svelte +992 -0
- package/src/components/overlay/NuiTooltip.svelte +159 -0
- package/src/composables/useClickOutside.ts +132 -0
- package/src/composables/useFormValidation.ts +105 -0
- package/src/composables/useIdle.svelte.ts +191 -0
- package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
- package/src/composables/useLocalStorage.svelte.ts +137 -0
- package/src/composables/useMediaQuery.svelte.ts +102 -0
- package/src/index.ts +66 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { nui } from '@fivem-nui/svelte';
|
|
4
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
5
|
+
|
|
6
|
+
type DelayUnit = 'ms' | 's' | 'm';
|
|
7
|
+
|
|
8
|
+
/** Maximum allowed debounce delay (60 seconds). */
|
|
9
|
+
const MAX_DELAY = 60_000;
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
eventName?: string;
|
|
13
|
+
client?: SvelteNuiClient;
|
|
14
|
+
value?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
type?: 'text' | 'number' | 'password' | 'email' | 'search' | 'textarea';
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
readonly?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Native HTML `min` attribute — effective for `number`, `range`, `date`, `time`, etc.
|
|
22
|
+
* Passed through as-is; no type enforcement beyond what the browser validates.
|
|
23
|
+
*/
|
|
24
|
+
min?: number | string;
|
|
25
|
+
/**
|
|
26
|
+
* Native HTML `max` attribute — effective for `number`, `range`, `date`, `time`, etc.
|
|
27
|
+
* Passed through as-is; no type enforcement beyond what the browser validates.
|
|
28
|
+
*/
|
|
29
|
+
max?: number | string;
|
|
30
|
+
/** Native HTML `step` attribute — effective for `number`, `range`, `date`, `time`, etc. */
|
|
31
|
+
step?: number | string;
|
|
32
|
+
/**
|
|
33
|
+
* Native HTML `minlength` attribute — constrains text length, not numeric value.
|
|
34
|
+
* Effective for `text`, `search`, `email`, `password`, `tel`, `url`, and `textarea`.
|
|
35
|
+
*/
|
|
36
|
+
minlength?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Native HTML `maxlength` attribute — constrains text length, not numeric value.
|
|
39
|
+
* Effective for `text`, `search`, `email`, `password`, `tel`, `url`, and `textarea`.
|
|
40
|
+
*/
|
|
41
|
+
maxlength?: number;
|
|
42
|
+
/** Native HTML `pattern` attribute — only effective for input types that support it (e.g. `text`, `search`). */
|
|
43
|
+
pattern?: string;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
autofocus?: boolean;
|
|
46
|
+
autocomplete?: HTMLInputElement['autocomplete'];
|
|
47
|
+
name?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Keep the field in sync with `eventName` events coming from the game.
|
|
50
|
+
*
|
|
51
|
+
* When enabled, incoming values are treated as authoritative state from the game
|
|
52
|
+
* and will overwrite the current field value, including text being actively edited.
|
|
53
|
+
* Any pending debounced `onValueChange` is cancelled to prevent stale payloads.
|
|
54
|
+
*/
|
|
55
|
+
twoWay?: boolean;
|
|
56
|
+
/** Event sent when the value settles (immediately, or after {@link debounce}). */
|
|
57
|
+
onValueChange?: string;
|
|
58
|
+
onInput?: string;
|
|
59
|
+
onBlur?: string;
|
|
60
|
+
onFocus?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Debounce delay for {@link onValueChange}, e.g. `'300ms'`, `'2s'`, `'1m'`.
|
|
63
|
+
* Clamped to a maximum of 60 seconds. Invalid formats result in no delay.
|
|
64
|
+
*/
|
|
65
|
+
debounce?: string;
|
|
66
|
+
/** Transform applied to every incoming value when {@link twoWay} is set. */
|
|
67
|
+
valueProcessor?: ((value: unknown) => string) | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
eventName = '',
|
|
72
|
+
client = nui,
|
|
73
|
+
value = $bindable(''),
|
|
74
|
+
label = '',
|
|
75
|
+
placeholder = '',
|
|
76
|
+
type = 'text',
|
|
77
|
+
disabled = false,
|
|
78
|
+
readonly = false,
|
|
79
|
+
min = '',
|
|
80
|
+
max = '',
|
|
81
|
+
step = '',
|
|
82
|
+
minlength = undefined,
|
|
83
|
+
maxlength = undefined,
|
|
84
|
+
pattern = '',
|
|
85
|
+
required = false,
|
|
86
|
+
autofocus = false,
|
|
87
|
+
autocomplete = 'off',
|
|
88
|
+
name = '',
|
|
89
|
+
twoWay = false,
|
|
90
|
+
onValueChange = '',
|
|
91
|
+
onInput = '',
|
|
92
|
+
onBlur = '',
|
|
93
|
+
onFocus = '',
|
|
94
|
+
debounce = '',
|
|
95
|
+
valueProcessor = null
|
|
96
|
+
}: Props = $props();
|
|
97
|
+
|
|
98
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parses a human-readable delay specification into milliseconds.
|
|
102
|
+
* Returns 0 for invalid formats. Result is clamped to {@link MAX_DELAY}.
|
|
103
|
+
*/
|
|
104
|
+
function parseDelay(spec: string): number {
|
|
105
|
+
const match = /^(\d+)(ms|s|m)$/.exec(spec.trim());
|
|
106
|
+
if (!match) return 0;
|
|
107
|
+
const amount = Number.parseInt(match[1], 10);
|
|
108
|
+
if (!Number.isSafeInteger(amount)) return 0;
|
|
109
|
+
const unit = match[2] as DelayUnit;
|
|
110
|
+
const delay = unit === 'ms' ? amount : unit === 's' ? amount * 1000 : amount * 60_000;
|
|
111
|
+
return Math.min(delay, MAX_DELAY);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Safely sends an event via the NUI client, catching synchronous exceptions
|
|
116
|
+
* and attaching a rejection handler to any returned Promise.
|
|
117
|
+
*/
|
|
118
|
+
function safeSend(event: string, data: unknown) {
|
|
119
|
+
if (!event) return;
|
|
120
|
+
try {
|
|
121
|
+
const res = client.send(event, data) as unknown;
|
|
122
|
+
if (res && typeof (res as Promise<unknown>).catch === 'function') {
|
|
123
|
+
(res as Promise<unknown>).catch((err: unknown) => {
|
|
124
|
+
console.error(
|
|
125
|
+
`[fivem-nui-components] NuiInput: Async error sending event '${event}':`,
|
|
126
|
+
err,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(
|
|
132
|
+
`[fivem-nui-components] NuiInput: Failed to send event '${event}':`,
|
|
133
|
+
err,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Cancels any pending debounce timer. */
|
|
139
|
+
function cancelDebounce() {
|
|
140
|
+
if (timer !== null) {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
timer = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Immediately flushes the current value through {@link onValueChange},
|
|
148
|
+
* cancelling any pending debounce timer first.
|
|
149
|
+
*/
|
|
150
|
+
function flushValueChange() {
|
|
151
|
+
cancelDebounce();
|
|
152
|
+
if (onValueChange) safeSend(onValueChange, { value });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Two-way sync: incoming game values cancel pending debounce and override local state
|
|
156
|
+
$effect(() => {
|
|
157
|
+
if (!twoWay || !eventName) return;
|
|
158
|
+
return client.on(eventName, (data) => {
|
|
159
|
+
if (data === null || data === undefined) return;
|
|
160
|
+
|
|
161
|
+
let processed: string;
|
|
162
|
+
try {
|
|
163
|
+
processed = valueProcessor ? valueProcessor(data) : String(data);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(
|
|
166
|
+
'[fivem-nui-components] NuiInput: Exception in valueProcessor:',
|
|
167
|
+
err,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Cancel any pending debounce to prevent stale local value from being sent
|
|
173
|
+
cancelDebounce();
|
|
174
|
+
value = processed;
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
function handleInput(event: Event) {
|
|
179
|
+
const next = (event.currentTarget as HTMLInputElement | HTMLTextAreaElement).value;
|
|
180
|
+
value = next;
|
|
181
|
+
|
|
182
|
+
if (onInput) safeSend(onInput, { value: next });
|
|
183
|
+
if (!onValueChange) return;
|
|
184
|
+
|
|
185
|
+
cancelDebounce();
|
|
186
|
+
const delay = parseDelay(debounce);
|
|
187
|
+
if (delay > 0) {
|
|
188
|
+
timer = setTimeout(() => {
|
|
189
|
+
timer = null;
|
|
190
|
+
safeSend(onValueChange, { value: next });
|
|
191
|
+
}, delay);
|
|
192
|
+
} else {
|
|
193
|
+
safeSend(onValueChange, { value: next });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleBlur() {
|
|
198
|
+
// Flush any pending debounced value change with the current (latest) value
|
|
199
|
+
if (timer !== null) {
|
|
200
|
+
flushValueChange();
|
|
201
|
+
}
|
|
202
|
+
if (onBlur) safeSend(onBlur, { value });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function handleFocus() {
|
|
206
|
+
if (onFocus) safeSend(onFocus, { value });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
onDestroy(() => {
|
|
210
|
+
cancelDebounce();
|
|
211
|
+
});
|
|
212
|
+
</script>
|
|
213
|
+
|
|
214
|
+
<label class="nui-field">
|
|
215
|
+
{#if label}<span class="nui-field__label">{label}</span>{/if}
|
|
216
|
+
|
|
217
|
+
{#if type === 'textarea'}
|
|
218
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
219
|
+
<textarea
|
|
220
|
+
class="nui-field__control"
|
|
221
|
+
{placeholder}
|
|
222
|
+
{disabled}
|
|
223
|
+
{readonly}
|
|
224
|
+
{required}
|
|
225
|
+
{minlength}
|
|
226
|
+
{maxlength}
|
|
227
|
+
{autofocus}
|
|
228
|
+
{autocomplete}
|
|
229
|
+
{name}
|
|
230
|
+
bind:value
|
|
231
|
+
oninput={handleInput}
|
|
232
|
+
onblur={handleBlur}
|
|
233
|
+
onfocus={handleFocus}
|
|
234
|
+
></textarea>
|
|
235
|
+
{:else}
|
|
236
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
237
|
+
<input
|
|
238
|
+
class="nui-field__control"
|
|
239
|
+
{type}
|
|
240
|
+
{placeholder}
|
|
241
|
+
{disabled}
|
|
242
|
+
{readonly}
|
|
243
|
+
{required}
|
|
244
|
+
{min}
|
|
245
|
+
{max}
|
|
246
|
+
{step}
|
|
247
|
+
{minlength}
|
|
248
|
+
{maxlength}
|
|
249
|
+
{pattern}
|
|
250
|
+
{autofocus}
|
|
251
|
+
{autocomplete}
|
|
252
|
+
{name}
|
|
253
|
+
bind:value
|
|
254
|
+
oninput={handleInput}
|
|
255
|
+
onblur={handleBlur}
|
|
256
|
+
onfocus={handleFocus}
|
|
257
|
+
/>
|
|
258
|
+
{/if}
|
|
259
|
+
</label>
|
|
260
|
+
|
|
261
|
+
<style>
|
|
262
|
+
.nui-field {
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-direction: column;
|
|
265
|
+
gap: 0.3rem;
|
|
266
|
+
width: 100%;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.nui-field__label {
|
|
270
|
+
font-size: 0.8125rem;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
opacity: 0.75;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.nui-field__control {
|
|
276
|
+
width: 100%;
|
|
277
|
+
box-sizing: border-box;
|
|
278
|
+
font-family: inherit;
|
|
279
|
+
font-size: 0.9375rem;
|
|
280
|
+
padding: 0.5rem 0.75rem;
|
|
281
|
+
border: 1px solid var(--nui-border, #3f3f46);
|
|
282
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
283
|
+
background: var(--nui-surface-alt, #101012);
|
|
284
|
+
color: var(--nui-fg, #fafafa);
|
|
285
|
+
transition: border-color 0.15s ease;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.nui-field__control:focus {
|
|
289
|
+
outline: none;
|
|
290
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.nui-field__control:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
294
|
+
|
|
295
|
+
textarea.nui-field__control { min-height: 5rem; resize: vertical; }
|
|
296
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
eventName?: string;
|
|
4
|
+
client?: SvelteNuiClient;
|
|
5
|
+
value?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
type?: 'text' | 'number' | 'password' | 'email' | 'search' | 'textarea';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Native HTML `min` attribute — effective for `number`, `range`, `date`, `time`, etc.
|
|
13
|
+
* Passed through as-is; no type enforcement beyond what the browser validates.
|
|
14
|
+
*/
|
|
15
|
+
min?: number | string;
|
|
16
|
+
/**
|
|
17
|
+
* Native HTML `max` attribute — effective for `number`, `range`, `date`, `time`, etc.
|
|
18
|
+
* Passed through as-is; no type enforcement beyond what the browser validates.
|
|
19
|
+
*/
|
|
20
|
+
max?: number | string;
|
|
21
|
+
/** Native HTML `step` attribute — effective for `number`, `range`, `date`, `time`, etc. */
|
|
22
|
+
step?: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Native HTML `minlength` attribute — constrains text length, not numeric value.
|
|
25
|
+
* Effective for `text`, `search`, `email`, `password`, `tel`, `url`, and `textarea`.
|
|
26
|
+
*/
|
|
27
|
+
minlength?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Native HTML `maxlength` attribute — constrains text length, not numeric value.
|
|
30
|
+
* Effective for `text`, `search`, `email`, `password`, `tel`, `url`, and `textarea`.
|
|
31
|
+
*/
|
|
32
|
+
maxlength?: number;
|
|
33
|
+
/** Native HTML `pattern` attribute — only effective for input types that support it (e.g. `text`, `search`). */
|
|
34
|
+
pattern?: string;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
autofocus?: boolean;
|
|
37
|
+
autocomplete?: HTMLInputElement['autocomplete'];
|
|
38
|
+
name?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Keep the field in sync with `eventName` events coming from the game.
|
|
41
|
+
*
|
|
42
|
+
* When enabled, incoming values are treated as authoritative state from the game
|
|
43
|
+
* and will overwrite the current field value, including text being actively edited.
|
|
44
|
+
* Any pending debounced `onValueChange` is cancelled to prevent stale payloads.
|
|
45
|
+
*/
|
|
46
|
+
twoWay?: boolean;
|
|
47
|
+
/** Event sent when the value settles (immediately, or after {@link debounce}). */
|
|
48
|
+
onValueChange?: string;
|
|
49
|
+
onInput?: string;
|
|
50
|
+
onBlur?: string;
|
|
51
|
+
onFocus?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Debounce delay for {@link onValueChange}, e.g. `'300ms'`, `'2s'`, `'1m'`.
|
|
54
|
+
* Clamped to a maximum of 60 seconds. Invalid formats result in no delay.
|
|
55
|
+
*/
|
|
56
|
+
debounce?: string;
|
|
57
|
+
/** Transform applied to every incoming value when {@link twoWay} is set. */
|
|
58
|
+
valueProcessor?: ((value: unknown) => string) | null;
|
|
59
|
+
}
|
|
60
|
+
declare const NuiInput: import("svelte").Component<Props, {}, "value">;
|
|
61
|
+
type NuiInput = ReturnType<typeof NuiInput>;
|
|
62
|
+
export default NuiInput;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export interface NuiRadioOption {
|
|
3
|
+
/**
|
|
4
|
+
* Unique value identifying this option.
|
|
5
|
+
* Must be unique within the options array to prevent key conflicts in Svelte's
|
|
6
|
+
* `{#each}` block and ambiguous selection behavior.
|
|
7
|
+
*/
|
|
8
|
+
value: string;
|
|
9
|
+
/** text label for the radio option. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Optional secondary description text displayed below the label. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** Whether this individual option is disabled. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import { nui } from "@fivem-nui/svelte";
|
|
20
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** NUI event name emitted when an option is selected. */
|
|
24
|
+
eventName?: string;
|
|
25
|
+
/** SvelteNuiClient instance used to dispatch NUI events. */
|
|
26
|
+
client?: SvelteNuiClient;
|
|
27
|
+
/** Currently selected value. Supports two-way binding via `bind:value`. */
|
|
28
|
+
value?: string;
|
|
29
|
+
/** Text rendered inside the `<legend>` element. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Available radio options.
|
|
33
|
+
*
|
|
34
|
+
* Each option's `value` must be unique within the array —
|
|
35
|
+
* duplicate values will cause key conflicts in Svelte's `{#each}` block and
|
|
36
|
+
* ambiguous selection behavior.
|
|
37
|
+
*/
|
|
38
|
+
options?: NuiRadioOption[];
|
|
39
|
+
/** Whether the entire radio group is disabled. */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* HTML `name` attribute for the radio group.
|
|
43
|
+
*
|
|
44
|
+
* If omitted or an empty string, a unique name is generated per component instance
|
|
45
|
+
* via `$props.id()` to prevent cross-group interference when multiple NuiRadioGroup
|
|
46
|
+
* components coexist on the same page. If a non-empty name is explicitly provided,
|
|
47
|
+
* it takes precedence.
|
|
48
|
+
*/
|
|
49
|
+
name?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const autoId = $props.id();
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
eventName = "",
|
|
56
|
+
client = nui,
|
|
57
|
+
value = $bindable(""),
|
|
58
|
+
label = "",
|
|
59
|
+
options = [],
|
|
60
|
+
disabled = false,
|
|
61
|
+
name,
|
|
62
|
+
}: Props = $props();
|
|
63
|
+
|
|
64
|
+
const groupName = $derived(name || `nui-radio-${autoId}`);
|
|
65
|
+
|
|
66
|
+
// Warn about duplicate option values to prevent key conflicts in {#each} and ambiguous selection
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const seen = new Set<string>();
|
|
69
|
+
for (const opt of options) {
|
|
70
|
+
if (seen.has(opt.value)) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[fivem-nui-components] NuiRadioGroup: Duplicate option value '${opt.value}' detected. Option values must be unique within the group.`,
|
|
73
|
+
);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
seen.add(opt.value);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Safely sends an event via the NUI client, catching synchronous exceptions.
|
|
82
|
+
*
|
|
83
|
+
* Note: Selection is optimistic the bound `value` is updated immediately and
|
|
84
|
+
* does not revert if network transport fails.
|
|
85
|
+
*/
|
|
86
|
+
function safeSend(event: string, data: unknown) {
|
|
87
|
+
if (!event) return;
|
|
88
|
+
try {
|
|
89
|
+
client.send(event, data);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(
|
|
92
|
+
`[fivem-nui-components] NuiRadioGroup: Failed to send event '${event}':`,
|
|
93
|
+
err,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function select(option: NuiRadioOption) {
|
|
99
|
+
if (disabled || option.disabled) return;
|
|
100
|
+
value = option.value;
|
|
101
|
+
safeSend(eventName, { value: option.value, label: option.label });
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<fieldset class="nui-radiogroup" {disabled}>
|
|
106
|
+
{#if label}<legend class="nui-radiogroup__label">{label}</legend>{/if}
|
|
107
|
+
|
|
108
|
+
{#each options as option, i (`${option.value}-${i}`)}
|
|
109
|
+
<label
|
|
110
|
+
class="nui-radio-option"
|
|
111
|
+
class:is-disabled={option.disabled || disabled}
|
|
112
|
+
class:is-checked={value === option.value}
|
|
113
|
+
>
|
|
114
|
+
<input
|
|
115
|
+
class="nui-radio-option__input"
|
|
116
|
+
type="radio"
|
|
117
|
+
name={groupName}
|
|
118
|
+
value={option.value}
|
|
119
|
+
checked={value === option.value}
|
|
120
|
+
disabled={option.disabled || disabled}
|
|
121
|
+
onchange={() => select(option)}
|
|
122
|
+
/>
|
|
123
|
+
<span class="nui-radio-track" aria-hidden="true"
|
|
124
|
+
><span class="nui-radio-dot"></span></span
|
|
125
|
+
>
|
|
126
|
+
<span class="nui-radio-text">
|
|
127
|
+
<span class="nui-radio-label">{option.label}</span>
|
|
128
|
+
{#if option.description}
|
|
129
|
+
<span class="nui-radio-description">{option.description}</span>
|
|
130
|
+
{/if}
|
|
131
|
+
</span>
|
|
132
|
+
</label>
|
|
133
|
+
{/each}
|
|
134
|
+
</fieldset>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
.nui-radiogroup {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 0.375rem;
|
|
141
|
+
margin: 0;
|
|
142
|
+
padding: 0;
|
|
143
|
+
border: 0;
|
|
144
|
+
font-family: inherit;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.nui-radiogroup__label {
|
|
148
|
+
padding: 0 0 0.25rem;
|
|
149
|
+
font-size: 0.8125rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
opacity: 0.75;
|
|
152
|
+
color: var(--nui-fg, #fafafa);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.nui-radio-option {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
gap: 0.625rem;
|
|
159
|
+
padding: 0.4rem 0.5rem;
|
|
160
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
color: var(--nui-fg, #fafafa);
|
|
163
|
+
transition: background 0.15s ease;
|
|
164
|
+
user-select: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.nui-radio-option:hover:not(.is-disabled) {
|
|
168
|
+
background: var(--nui-hover, rgb(255 255 255 / 0.05));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.nui-radio-option.is-disabled {
|
|
172
|
+
opacity: 0.45;
|
|
173
|
+
cursor: not-allowed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Visually hidden using standard technique — accessible to screen readers & keyboard */
|
|
177
|
+
.nui-radio-option__input {
|
|
178
|
+
position: absolute;
|
|
179
|
+
width: 1px;
|
|
180
|
+
height: 1px;
|
|
181
|
+
padding: 0;
|
|
182
|
+
margin: -1px;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
clip: rect(0, 0, 0, 0);
|
|
185
|
+
clip-path: inset(50%);
|
|
186
|
+
border: 0;
|
|
187
|
+
white-space: nowrap;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Visible focus indicator on the custom radio track when navigating via keyboard */
|
|
191
|
+
.nui-radio-option__input:focus-visible + .nui-radio-track {
|
|
192
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
193
|
+
outline-offset: 3px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.nui-radio-track {
|
|
197
|
+
position: relative;
|
|
198
|
+
flex: none;
|
|
199
|
+
width: 1.1rem;
|
|
200
|
+
height: 1.1rem;
|
|
201
|
+
border: 2px solid var(--nui-border, #52525b);
|
|
202
|
+
border-radius: 50%;
|
|
203
|
+
transition: border-color 0.15s ease;
|
|
204
|
+
box-sizing: border-box;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.nui-radio-option.is-checked .nui-radio-track {
|
|
208
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.nui-radio-dot {
|
|
212
|
+
position: absolute;
|
|
213
|
+
inset: 2px;
|
|
214
|
+
border-radius: 50%;
|
|
215
|
+
background: var(--nui-primary, #3b82f6);
|
|
216
|
+
transform: scale(0);
|
|
217
|
+
transition: transform 0.15s ease;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.nui-radio-option.is-checked .nui-radio-dot {
|
|
221
|
+
transform: scale(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.nui-radio-text {
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
line-height: 1.3;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.nui-radio-label {
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.nui-radio-description {
|
|
235
|
+
font-size: 0.75rem;
|
|
236
|
+
opacity: 0.65;
|
|
237
|
+
}
|
|
238
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface NuiRadioOption {
|
|
2
|
+
/**
|
|
3
|
+
* Unique value identifying this option.
|
|
4
|
+
* Must be unique within the options array to prevent key conflicts in Svelte's
|
|
5
|
+
* `{#each}` block and ambiguous selection behavior.
|
|
6
|
+
*/
|
|
7
|
+
value: string;
|
|
8
|
+
/** text label for the radio option. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Optional secondary description text displayed below the label. */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Whether this individual option is disabled. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
16
|
+
interface Props {
|
|
17
|
+
/** NUI event name emitted when an option is selected. */
|
|
18
|
+
eventName?: string;
|
|
19
|
+
/** SvelteNuiClient instance used to dispatch NUI events. */
|
|
20
|
+
client?: SvelteNuiClient;
|
|
21
|
+
/** Currently selected value. Supports two-way binding via `bind:value`. */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** Text rendered inside the `<legend>` element. */
|
|
24
|
+
label?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Available radio options.
|
|
27
|
+
*
|
|
28
|
+
* Each option's `value` must be unique within the array —
|
|
29
|
+
* duplicate values will cause key conflicts in Svelte's `{#each}` block and
|
|
30
|
+
* ambiguous selection behavior.
|
|
31
|
+
*/
|
|
32
|
+
options?: NuiRadioOption[];
|
|
33
|
+
/** Whether the entire radio group is disabled. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* HTML `name` attribute for the radio group.
|
|
37
|
+
*
|
|
38
|
+
* If omitted or an empty string, a unique name is generated per component instance
|
|
39
|
+
* via `$props.id()` to prevent cross-group interference when multiple NuiRadioGroup
|
|
40
|
+
* components coexist on the same page. If a non-empty name is explicitly provided,
|
|
41
|
+
* it takes precedence.
|
|
42
|
+
*/
|
|
43
|
+
name?: string;
|
|
44
|
+
}
|
|
45
|
+
declare const NuiRadioGroup: import("svelte").Component<Props, {}, "value">;
|
|
46
|
+
type NuiRadioGroup = ReturnType<typeof NuiRadioGroup>;
|
|
47
|
+
export default NuiRadioGroup;
|