@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,341 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NuiList
|
|
3
|
+
Interactive list component supporting actions, icons, descriptions, and toggle switches.
|
|
4
|
+
|
|
5
|
+
Controlled Component:
|
|
6
|
+
Switch items follow a controlled state pattern where `switchValue` reflects the authoritative state
|
|
7
|
+
from backend/parent data. Toggling emits `switchEvent` and `onSwitchToggle` to request a state change.
|
|
8
|
+
-->
|
|
9
|
+
<script module lang="ts">
|
|
10
|
+
/**
|
|
11
|
+
* Represents an item in the {@link NuiList} component.
|
|
12
|
+
*/
|
|
13
|
+
export interface NuiListItem {
|
|
14
|
+
/** Unique item identifier. Highly recommended for dynamic or reorderable lists. */
|
|
15
|
+
id?: string;
|
|
16
|
+
/** Primary display label. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Associated value (should be JSON-serializable if transmitted via NUI). */
|
|
19
|
+
value?: unknown;
|
|
20
|
+
/** Icon string, emoji, or icon character. */
|
|
21
|
+
icon?: string;
|
|
22
|
+
/** Secondary description text. */
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Whether to render a toggle switch instead of an action button. */
|
|
25
|
+
switch?: boolean;
|
|
26
|
+
/** Current state of the toggle switch. */
|
|
27
|
+
switchValue?: boolean;
|
|
28
|
+
/** NUI event sent to game client when switch state is toggled. */
|
|
29
|
+
switchEvent?: string;
|
|
30
|
+
/** Item-specific NUI event sent on item click. */
|
|
31
|
+
eventName?: string;
|
|
32
|
+
/** Whether the item is disabled. */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script lang="ts">
|
|
38
|
+
import { nui } from '@fivem-nui/svelte';
|
|
39
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
40
|
+
|
|
41
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
/** Array of list items to display. */
|
|
45
|
+
items?: NuiListItem[];
|
|
46
|
+
/** SvelteNuiClient instance for event transport. */
|
|
47
|
+
client?: SvelteNuiClient;
|
|
48
|
+
/** Vertical density sizing. */
|
|
49
|
+
itemSize?: 'sm' | 'md' | 'lg';
|
|
50
|
+
/** Global NUI event name sent with `{ id, value }` when any row is activated. */
|
|
51
|
+
selectEvent?: string;
|
|
52
|
+
/** Local click callback executed when an item is activated (supports sync or async). */
|
|
53
|
+
onItemClick?: (item: NuiListItem) => MaybePromise<void>;
|
|
54
|
+
/** Local change callback executed when a switch item is toggled (supports sync or async). */
|
|
55
|
+
onSwitchToggle?: (item: NuiListItem, value: boolean) => MaybePromise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let {
|
|
59
|
+
items = [],
|
|
60
|
+
client = nui,
|
|
61
|
+
itemSize = 'md',
|
|
62
|
+
selectEvent = '',
|
|
63
|
+
onItemClick,
|
|
64
|
+
onSwitchToggle
|
|
65
|
+
}: Props = $props();
|
|
66
|
+
|
|
67
|
+
function safeCall(fn: (() => unknown) | undefined, errorPrefix: string): void {
|
|
68
|
+
if (!fn) return;
|
|
69
|
+
try {
|
|
70
|
+
const result = fn();
|
|
71
|
+
if (result && typeof (result as Promise<unknown>).catch === 'function') {
|
|
72
|
+
(result as Promise<unknown>).catch((err) => {
|
|
73
|
+
console.error(errorPrefix, err);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(errorPrefix, err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleItemClick(item: NuiListItem) {
|
|
82
|
+
if (item.disabled) return;
|
|
83
|
+
if (item.eventName) {
|
|
84
|
+
try {
|
|
85
|
+
client.send(item.eventName, { value: item.value, id: item.id });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(`[fivem-nui-components] Failed to send event '${item.eventName}':`, err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (selectEvent) {
|
|
91
|
+
try {
|
|
92
|
+
client.send(selectEvent, { id: item.id, value: item.value });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[fivem-nui-components] Failed to send selectEvent '${selectEvent}':`, err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
safeCall(
|
|
98
|
+
() => onItemClick?.(item),
|
|
99
|
+
'[fivem-nui-components] onItemClick callback error:'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleSwitch(item: NuiListItem) {
|
|
104
|
+
if (item.disabled) return;
|
|
105
|
+
const nextVal = !(item.switchValue ?? false);
|
|
106
|
+
if (item.switchEvent) {
|
|
107
|
+
try {
|
|
108
|
+
client.send(item.switchEvent, { id: item.id, value: nextVal });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`[fivem-nui-components] Failed to send switchEvent '${item.switchEvent}':`, err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
safeCall(
|
|
114
|
+
() => onSwitchToggle?.(item, nextVal),
|
|
115
|
+
'[fivem-nui-components] onSwitchToggle callback error:'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<ul class="nui-list nui-list--{itemSize}">
|
|
121
|
+
{#each items as item, index (item.id ?? `${item.label}-${index}`)}
|
|
122
|
+
<li class="nui-list-item" class:nui-list-item--switch={item.switch}>
|
|
123
|
+
{#if item.switch}
|
|
124
|
+
<label class="nui-list-item-label" class:disabled={item.disabled}>
|
|
125
|
+
{#if item.icon}
|
|
126
|
+
<span class="nui-list-item-icon" aria-hidden="true">{item.icon}</span>
|
|
127
|
+
{/if}
|
|
128
|
+
<span class="nui-list-item-text">
|
|
129
|
+
<span class="nui-list-item-title">{item.label}</span>
|
|
130
|
+
{#if item.description}
|
|
131
|
+
<span class="nui-list-item-description">{item.description}</span>
|
|
132
|
+
{/if}
|
|
133
|
+
</span>
|
|
134
|
+
<span class="nui-switch-wrapper">
|
|
135
|
+
<input
|
|
136
|
+
type="checkbox"
|
|
137
|
+
class="nui-switch__input"
|
|
138
|
+
checked={item.switchValue ?? false}
|
|
139
|
+
disabled={item.disabled}
|
|
140
|
+
onchange={() => handleSwitch(item)}
|
|
141
|
+
/>
|
|
142
|
+
<span class="nui-switch" class:checked={item.switchValue ?? false} aria-hidden="true">
|
|
143
|
+
<span class="nui-switch__thumb"></span>
|
|
144
|
+
</span>
|
|
145
|
+
</span>
|
|
146
|
+
</label>
|
|
147
|
+
{:else}
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="nui-list-item-btn"
|
|
151
|
+
disabled={item.disabled}
|
|
152
|
+
onclick={() => handleItemClick(item)}
|
|
153
|
+
>
|
|
154
|
+
{#if item.icon}
|
|
155
|
+
<span class="nui-list-item-icon" aria-hidden="true">{item.icon}</span>
|
|
156
|
+
{/if}
|
|
157
|
+
<span class="nui-list-item-text">
|
|
158
|
+
<span class="nui-list-item-title">{item.label}</span>
|
|
159
|
+
{#if item.description}
|
|
160
|
+
<span class="nui-list-item-description">{item.description}</span>
|
|
161
|
+
{/if}
|
|
162
|
+
</span>
|
|
163
|
+
</button>
|
|
164
|
+
{/if}
|
|
165
|
+
</li>
|
|
166
|
+
{/each}
|
|
167
|
+
</ul>
|
|
168
|
+
|
|
169
|
+
<style>
|
|
170
|
+
.nui-list {
|
|
171
|
+
list-style: none;
|
|
172
|
+
padding: 0;
|
|
173
|
+
margin: 0;
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
font-family: inherit;
|
|
177
|
+
width: 100%;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.nui-list-item {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
border-bottom: 1px solid var(--nui-border, #27272a);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.nui-list-item:last-child {
|
|
187
|
+
border-bottom: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.nui-list-item-btn,
|
|
191
|
+
.nui-list-item-label {
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.nui-list--sm .nui-list-item-btn,
|
|
196
|
+
.nui-list--sm .nui-list-item-label {
|
|
197
|
+
padding: 0.4rem 0.625rem;
|
|
198
|
+
}
|
|
199
|
+
.nui-list--md .nui-list-item-btn,
|
|
200
|
+
.nui-list--md .nui-list-item-label {
|
|
201
|
+
padding: 0.6rem 0.875rem;
|
|
202
|
+
}
|
|
203
|
+
.nui-list--lg .nui-list-item-btn,
|
|
204
|
+
.nui-list--lg .nui-list-item-label {
|
|
205
|
+
padding: 0.85rem 1.125rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.nui-list-item-btn {
|
|
209
|
+
appearance: none;
|
|
210
|
+
-webkit-appearance: none;
|
|
211
|
+
border: none;
|
|
212
|
+
background: transparent;
|
|
213
|
+
padding: 0;
|
|
214
|
+
margin: 0;
|
|
215
|
+
font: inherit;
|
|
216
|
+
color: inherit;
|
|
217
|
+
text-align: left;
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 0.75rem;
|
|
221
|
+
width: 100%;
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
224
|
+
transition: background 0.15s ease;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.nui-list-item-btn:hover:not(:disabled) {
|
|
228
|
+
background: var(--nui-hover, rgb(255 255 255 / 0.04));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.nui-list-item-btn:disabled {
|
|
232
|
+
opacity: 0.45;
|
|
233
|
+
cursor: not-allowed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.nui-list-item-btn:focus-visible {
|
|
237
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
238
|
+
outline-offset: 2px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.nui-list-item-label {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
gap: 0.75rem;
|
|
246
|
+
width: 100%;
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
cursor: pointer;
|
|
249
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
250
|
+
transition: background 0.15s ease;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.nui-list-item-label:hover:not(.disabled) {
|
|
254
|
+
background: var(--nui-hover, rgb(255 255 255 / 0.04));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.nui-list-item-label.disabled {
|
|
258
|
+
opacity: 0.45;
|
|
259
|
+
cursor: not-allowed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.nui-list-item-icon {
|
|
263
|
+
font-size: 1.15rem;
|
|
264
|
+
line-height: 1;
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.nui-list-item-text {
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-direction: column;
|
|
271
|
+
line-height: 1.3;
|
|
272
|
+
flex: 1;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.nui-list-item-title {
|
|
277
|
+
font-weight: 500;
|
|
278
|
+
font-size: 0.875rem;
|
|
279
|
+
color: var(--nui-fg, #fafafa);
|
|
280
|
+
overflow-wrap: anywhere;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.nui-list-item-description {
|
|
284
|
+
font-size: 0.75rem;
|
|
285
|
+
opacity: 0.65;
|
|
286
|
+
overflow-wrap: anywhere;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.nui-switch-wrapper {
|
|
290
|
+
position: relative;
|
|
291
|
+
display: inline-flex;
|
|
292
|
+
align-items: center;
|
|
293
|
+
flex-shrink: 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.nui-switch__input {
|
|
297
|
+
position: absolute;
|
|
298
|
+
opacity: 0;
|
|
299
|
+
width: 0;
|
|
300
|
+
height: 0;
|
|
301
|
+
margin: 0;
|
|
302
|
+
pointer-events: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.nui-switch {
|
|
306
|
+
flex: none;
|
|
307
|
+
width: 2.25rem;
|
|
308
|
+
height: 1.25rem;
|
|
309
|
+
padding: 0.125rem;
|
|
310
|
+
border-radius: 999px;
|
|
311
|
+
background: var(--nui-surface-alt, #3f3f46);
|
|
312
|
+
border: 1px solid var(--nui-border, #52525b);
|
|
313
|
+
transition: background 0.2s ease, border-color 0.2s ease;
|
|
314
|
+
box-sizing: border-box;
|
|
315
|
+
display: inline-flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.nui-switch.checked {
|
|
320
|
+
background: var(--nui-primary, #3b82f6);
|
|
321
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.nui-switch__input:focus-visible + .nui-switch {
|
|
325
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
326
|
+
outline-offset: 3px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.nui-switch__thumb {
|
|
330
|
+
display: block;
|
|
331
|
+
width: 0.95rem;
|
|
332
|
+
height: 0.95rem;
|
|
333
|
+
border-radius: 50%;
|
|
334
|
+
background: #fff;
|
|
335
|
+
transition: transform 0.2s ease;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.nui-switch.checked .nui-switch__thumb {
|
|
339
|
+
transform: translateX(1rem);
|
|
340
|
+
}
|
|
341
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an item in the {@link NuiList} component.
|
|
3
|
+
*/
|
|
4
|
+
export interface NuiListItem {
|
|
5
|
+
/** Unique item identifier. Highly recommended for dynamic or reorderable lists. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Primary display label. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Associated value (should be JSON-serializable if transmitted via NUI). */
|
|
10
|
+
value?: unknown;
|
|
11
|
+
/** Icon string, emoji, or icon character. */
|
|
12
|
+
icon?: string;
|
|
13
|
+
/** Secondary description text. */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Whether to render a toggle switch instead of an action button. */
|
|
16
|
+
switch?: boolean;
|
|
17
|
+
/** Current state of the toggle switch. */
|
|
18
|
+
switchValue?: boolean;
|
|
19
|
+
/** NUI event sent to game client when switch state is toggled. */
|
|
20
|
+
switchEvent?: string;
|
|
21
|
+
/** Item-specific NUI event sent on item click. */
|
|
22
|
+
eventName?: string;
|
|
23
|
+
/** Whether the item is disabled. */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
27
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Array of list items to display. */
|
|
30
|
+
items?: NuiListItem[];
|
|
31
|
+
/** SvelteNuiClient instance for event transport. */
|
|
32
|
+
client?: SvelteNuiClient;
|
|
33
|
+
/** Vertical density sizing. */
|
|
34
|
+
itemSize?: 'sm' | 'md' | 'lg';
|
|
35
|
+
/** Global NUI event name sent with `{ id, value }` when any row is activated. */
|
|
36
|
+
selectEvent?: string;
|
|
37
|
+
/** Local click callback executed when an item is activated (supports sync or async). */
|
|
38
|
+
onItemClick?: (item: NuiListItem) => MaybePromise<void>;
|
|
39
|
+
/** Local change callback executed when a switch item is toggled (supports sync or async). */
|
|
40
|
+
onSwitchToggle?: (item: NuiListItem, value: boolean) => MaybePromise<void>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* NuiList
|
|
44
|
+
* Interactive list component supporting actions, icons, descriptions, and toggle switches.
|
|
45
|
+
*
|
|
46
|
+
* Controlled Component:
|
|
47
|
+
* Switch items follow a controlled state pattern where `switchValue` reflects the authoritative state
|
|
48
|
+
* from backend/parent data. Toggling emits `switchEvent` and `onSwitchToggle` to request a state change.
|
|
49
|
+
*/
|
|
50
|
+
declare const NuiList: import("svelte").Component<Props, {}, "">;
|
|
51
|
+
type NuiList = ReturnType<typeof NuiList>;
|
|
52
|
+
export default NuiList;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NuiProgressBar
|
|
3
|
+
Accessible progress bar component with real-time NUI sync, custom colors, and percentage indicators.
|
|
4
|
+
|
|
5
|
+
Data Precedence & Synchronization:
|
|
6
|
+
- If `eventName` is specified, the component listens for incoming progress payloads (number or `{ value, max }`).
|
|
7
|
+
- Incoming NUI updates take precedence over initial `value`/`max` props.
|
|
8
|
+
- If the parent component changes `value`, `max`, or `eventName` props, remote overrides are reset.
|
|
9
|
+
|
|
10
|
+
Completion Contract:
|
|
11
|
+
- Completion is detected when `safeValue >= safeMax` (not by rounded percent).
|
|
12
|
+
- Fires `onComplete` and emits `completeEvent` once when reaching 100%.
|
|
13
|
+
- If progress drops below 100% and reaches 100% again, the completion handlers are re-triggered.
|
|
14
|
+
|
|
15
|
+
Label Behavior:
|
|
16
|
+
- `labelPosition === 'top'` renders the label above the track on the left, with percentage on the right.
|
|
17
|
+
- `labelPosition === 'inside'` renders the text label inside the progress fill, while percentage (if enabled) remains above the track.
|
|
18
|
+
-->
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { nui } from '@fivem-nui/svelte';
|
|
21
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
22
|
+
|
|
23
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Current progress value. */
|
|
27
|
+
value?: number;
|
|
28
|
+
/** Maximum progress value. Defaults to 100 if <= 0, NaN, or non-finite. */
|
|
29
|
+
max?: number;
|
|
30
|
+
/** Text label describing the progress (e.g. "Downloading resources"). */
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Custom CSS color override for the fill bar. */
|
|
33
|
+
color?: string;
|
|
34
|
+
/** Whether to display the percentage number. */
|
|
35
|
+
showPercentage?: boolean;
|
|
36
|
+
/** Position of the text label ('top' above track, 'inside' within fill bar). */
|
|
37
|
+
labelPosition?: 'top' | 'inside';
|
|
38
|
+
/** Sizing density. */
|
|
39
|
+
size?: 'sm' | 'md' | 'lg';
|
|
40
|
+
/** Color palette variant. */
|
|
41
|
+
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
|
42
|
+
/** Optional NUI event name to listen for progress updates from the game backend. */
|
|
43
|
+
eventName?: string;
|
|
44
|
+
/** Optional NUI event name sent to the game backend once progress reaches 100%. */
|
|
45
|
+
completeEvent?: string;
|
|
46
|
+
/** Callback executed when progress reaches 100% (safe for sync or async). */
|
|
47
|
+
onComplete?: () => MaybePromise<void>;
|
|
48
|
+
/** Accessible label for screen readers (defaults to label or "Progress"). */
|
|
49
|
+
ariaLabel?: string;
|
|
50
|
+
/** SvelteNuiClient instance for event transport. */
|
|
51
|
+
client?: SvelteNuiClient;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
value = 0,
|
|
56
|
+
max = 100,
|
|
57
|
+
label,
|
|
58
|
+
color,
|
|
59
|
+
showPercentage = false,
|
|
60
|
+
labelPosition = 'top',
|
|
61
|
+
size = 'md',
|
|
62
|
+
variant = 'primary',
|
|
63
|
+
eventName,
|
|
64
|
+
completeEvent,
|
|
65
|
+
onComplete,
|
|
66
|
+
ariaLabel,
|
|
67
|
+
client = nui
|
|
68
|
+
}: Props = $props();
|
|
69
|
+
|
|
70
|
+
let remoteValue = $state<number | null>(null);
|
|
71
|
+
let remoteMax = $state<number | null>(null);
|
|
72
|
+
|
|
73
|
+
let lastPropsValue = value;
|
|
74
|
+
let lastPropsMax = max;
|
|
75
|
+
let lastEventName = eventName;
|
|
76
|
+
|
|
77
|
+
// Reset remote overrides when parent updates props or switches eventName
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (value !== lastPropsValue || max !== lastPropsMax || eventName !== lastEventName) {
|
|
80
|
+
lastPropsValue = value;
|
|
81
|
+
lastPropsMax = max;
|
|
82
|
+
lastEventName = eventName;
|
|
83
|
+
remoteValue = null;
|
|
84
|
+
remoteMax = null;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Listen for real-time progress updates from NUI game client
|
|
89
|
+
$effect(() => {
|
|
90
|
+
if (!eventName || !client) return;
|
|
91
|
+
const unsub = client.on(eventName, (payload: unknown) => {
|
|
92
|
+
if (typeof payload === 'number' && Number.isFinite(payload)) {
|
|
93
|
+
remoteValue = payload;
|
|
94
|
+
} else if (payload && typeof payload === 'object') {
|
|
95
|
+
const obj = payload as Record<string, unknown>;
|
|
96
|
+
if (typeof obj.value === 'number' && Number.isFinite(obj.value)) {
|
|
97
|
+
remoteValue = obj.value;
|
|
98
|
+
}
|
|
99
|
+
if (typeof obj.max === 'number' && Number.isFinite(obj.max)) {
|
|
100
|
+
remoteMax = obj.max;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return () => unsub();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const activeValue = $derived(remoteValue ?? value);
|
|
108
|
+
const activeMax = $derived(remoteMax ?? max);
|
|
109
|
+
|
|
110
|
+
const safeMax = $derived(
|
|
111
|
+
typeof activeMax === 'number' && Number.isFinite(activeMax) && activeMax > 0
|
|
112
|
+
? activeMax
|
|
113
|
+
: 100
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const safeValue = $derived(
|
|
117
|
+
typeof activeValue === 'number' && Number.isFinite(activeValue)
|
|
118
|
+
? Math.max(0, Math.min(safeMax, activeValue))
|
|
119
|
+
: 0
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const clampedPercent = $derived(
|
|
123
|
+
safeMax > 0 ? Math.min(100, Math.max(0, Math.round((safeValue / safeMax) * 100))) : 0
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const isComplete = $derived(safeValue >= safeMax);
|
|
127
|
+
|
|
128
|
+
let hasCompleted = $state(false);
|
|
129
|
+
|
|
130
|
+
function safeCall(fn: (() => unknown) | undefined, errorPrefix: string): void {
|
|
131
|
+
if (!fn) return;
|
|
132
|
+
try {
|
|
133
|
+
const result = fn();
|
|
134
|
+
if (result && typeof (result as Promise<unknown>).catch === 'function') {
|
|
135
|
+
(result as Promise<unknown>).catch((err) => {
|
|
136
|
+
console.error(errorPrefix, err);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(errorPrefix, err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$effect(() => {
|
|
145
|
+
if (isComplete) {
|
|
146
|
+
if (!hasCompleted) {
|
|
147
|
+
hasCompleted = true;
|
|
148
|
+
if (completeEvent) {
|
|
149
|
+
try {
|
|
150
|
+
client.send(completeEvent, { value: safeValue, max: safeMax });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(`[fivem-nui-components] Failed to send completeEvent '${completeEvent}':`, err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
safeCall(() => onComplete?.(), '[fivem-nui-components] onComplete callback error:');
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
hasCompleted = false;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const resolvedAriaLabel = $derived(ariaLabel || label || 'Progress');
|
|
163
|
+
const resolvedAriaValueText = $derived(`${clampedPercent}%`);
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<div class="nui-progress-bar nui-progress-bar--{size}">
|
|
167
|
+
{#if (label && labelPosition === 'top') || showPercentage}
|
|
168
|
+
<div class="nui-progress-bar__label">
|
|
169
|
+
{#if label && labelPosition === 'top'}
|
|
170
|
+
<span>{label}</span>
|
|
171
|
+
{/if}
|
|
172
|
+
{#if showPercentage}
|
|
173
|
+
<span class="nui-progress-bar__percent">{clampedPercent}%</span>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
|
|
178
|
+
<div
|
|
179
|
+
class="nui-progress-bar__track"
|
|
180
|
+
role="progressbar"
|
|
181
|
+
aria-label={resolvedAriaLabel}
|
|
182
|
+
aria-valuenow={safeValue}
|
|
183
|
+
aria-valuemin={0}
|
|
184
|
+
aria-valuemax={safeMax}
|
|
185
|
+
aria-valuetext={resolvedAriaValueText}
|
|
186
|
+
>
|
|
187
|
+
<div
|
|
188
|
+
class="nui-progress-bar__fill nui-progress-bar__fill--{variant}"
|
|
189
|
+
style:width="{clampedPercent}%"
|
|
190
|
+
style:background={color || undefined}
|
|
191
|
+
aria-hidden="true"
|
|
192
|
+
>
|
|
193
|
+
{#if label && labelPosition === 'inside'}
|
|
194
|
+
<span class="nui-progress-bar__inner-label">
|
|
195
|
+
{label}
|
|
196
|
+
</span>
|
|
197
|
+
{/if}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
.nui-progress-bar {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
gap: 0.35rem;
|
|
207
|
+
font-family: inherit;
|
|
208
|
+
width: 100%;
|
|
209
|
+
box-sizing: border-box;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.nui-progress-bar__label {
|
|
213
|
+
display: flex;
|
|
214
|
+
justify-content: space-between;
|
|
215
|
+
align-items: center;
|
|
216
|
+
font-size: 0.8125rem;
|
|
217
|
+
color: var(--nui-fg, #fafafa);
|
|
218
|
+
opacity: 0.85;
|
|
219
|
+
line-height: 1.2;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.nui-progress-bar__percent {
|
|
223
|
+
font-weight: 600;
|
|
224
|
+
margin-left: auto;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.nui-progress-bar__track {
|
|
228
|
+
width: 100%;
|
|
229
|
+
background: var(--nui-surface-alt, #27272a);
|
|
230
|
+
border: 1px solid var(--nui-border, #3f3f46);
|
|
231
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
position: relative;
|
|
234
|
+
box-sizing: border-box;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.nui-progress-bar--sm .nui-progress-bar__track { height: 0.75rem; }
|
|
238
|
+
.nui-progress-bar--md .nui-progress-bar__track { height: 1.25rem; }
|
|
239
|
+
.nui-progress-bar--lg .nui-progress-bar__track { height: 1.75rem; }
|
|
240
|
+
|
|
241
|
+
.nui-progress-bar__fill {
|
|
242
|
+
height: 100%;
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
transition: width 0.3s ease;
|
|
247
|
+
border-radius: inherit;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.nui-progress-bar__fill--primary { background: var(--nui-primary, #3b82f6); }
|
|
251
|
+
.nui-progress-bar__fill--secondary { background: var(--nui-secondary, #8b8b99); }
|
|
252
|
+
.nui-progress-bar__fill--success { background: var(--nui-success, #22c55e); }
|
|
253
|
+
.nui-progress-bar__fill--warning { background: var(--nui-warning, #f59e0b); }
|
|
254
|
+
.nui-progress-bar__fill--danger { background: var(--nui-danger, #ef4444); }
|
|
255
|
+
|
|
256
|
+
.nui-progress-bar__inner-label {
|
|
257
|
+
color: #fff;
|
|
258
|
+
font-size: 0.75rem;
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
padding: 0 0.5rem;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
2
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Current progress value. */
|
|
5
|
+
value?: number;
|
|
6
|
+
/** Maximum progress value. Defaults to 100 if <= 0, NaN, or non-finite. */
|
|
7
|
+
max?: number;
|
|
8
|
+
/** Text label describing the progress (e.g. "Downloading resources"). */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Custom CSS color override for the fill bar. */
|
|
11
|
+
color?: string;
|
|
12
|
+
/** Whether to display the percentage number. */
|
|
13
|
+
showPercentage?: boolean;
|
|
14
|
+
/** Position of the text label ('top' above track, 'inside' within fill bar). */
|
|
15
|
+
labelPosition?: 'top' | 'inside';
|
|
16
|
+
/** Sizing density. */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
/** Color palette variant. */
|
|
19
|
+
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
|
20
|
+
/** Optional NUI event name to listen for progress updates from the game backend. */
|
|
21
|
+
eventName?: string;
|
|
22
|
+
/** Optional NUI event name sent to the game backend once progress reaches 100%. */
|
|
23
|
+
completeEvent?: string;
|
|
24
|
+
/** Callback executed when progress reaches 100% (safe for sync or async). */
|
|
25
|
+
onComplete?: () => MaybePromise<void>;
|
|
26
|
+
/** Accessible label for screen readers (defaults to label or "Progress"). */
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
/** SvelteNuiClient instance for event transport. */
|
|
29
|
+
client?: SvelteNuiClient;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* NuiProgressBar
|
|
33
|
+
* Accessible progress bar component with real-time NUI sync, custom colors, and percentage indicators.
|
|
34
|
+
*
|
|
35
|
+
* Data Precedence & Synchronization:
|
|
36
|
+
* - If `eventName` is specified, the component listens for incoming progress payloads (number or `{ value, max }`).
|
|
37
|
+
* - Incoming NUI updates take precedence over initial `value`/`max` props.
|
|
38
|
+
* - If the parent component changes `value`, `max`, or `eventName` props, remote overrides are reset.
|
|
39
|
+
*
|
|
40
|
+
* Completion Contract:
|
|
41
|
+
* - Completion is detected when `safeValue >= safeMax` (not by rounded percent).
|
|
42
|
+
* - Fires `onComplete` and emits `completeEvent` once when reaching 100%.
|
|
43
|
+
* - If progress drops below 100% and reaches 100% again, the completion handlers are re-triggered.
|
|
44
|
+
*
|
|
45
|
+
* Label Behavior:
|
|
46
|
+
* - `labelPosition === 'top'` renders the label above the track on the left, with percentage on the right.
|
|
47
|
+
* - `labelPosition === 'inside'` renders the text label inside the progress fill, while percentage (if enabled) remains above the track.
|
|
48
|
+
*/
|
|
49
|
+
declare const NuiProgressBar: import("svelte").Component<Props, {}, "">;
|
|
50
|
+
type NuiProgressBar = ReturnType<typeof NuiProgressBar>;
|
|
51
|
+
export default NuiProgressBar;
|