@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,72 @@
1
+ export interface ModalAction {
2
+ /**
3
+ * Unique action identifier within the modal.
4
+ * Used as a keyed `#each` identifier and for `initialFocus` targeting.
5
+ */
6
+ id?: string;
7
+ label: string;
8
+ variant?: "primary" | "secondary" | "danger";
9
+ disabled?: boolean;
10
+ /** Automatically closes the modal after `onclick` succeeds (does not throw or return `false`). */
11
+ closeOnClick?: boolean;
12
+ /** Action callback. Returning `false` or throwing an error prevents `closeOnClick` from closing. */
13
+ onclick?: (event: MouseEvent) => unknown | Promise<unknown>;
14
+ }
15
+ export interface ModalLabels {
16
+ dialog?: string;
17
+ close?: string;
18
+ }
19
+ import type { Snippet } from "svelte";
20
+ import type { HTMLAttributes } from "svelte/elements";
21
+ /**
22
+ * Note on transitions: animations are tied to the internal `{#if open}` block.
23
+ * Control visibility via the `open` prop rather than wrapping `<NuiModal>` in an
24
+ * external `{#if show}` block so that exit transitions and focus cleanup run properly.
25
+ */
26
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "title" | "role" | "tabindex"> {
27
+ /** Controls whether the modal is open. */
28
+ open?: boolean;
29
+ /** Modal header title. */
30
+ title?: string;
31
+ /** Optional description text rendered inside the body and linked via `aria-describedby`. */
32
+ description?: string;
33
+ /** Modal body content. */
34
+ children?: Snippet;
35
+ /**
36
+ * Custom header snippet. Receives `titleId` so you can bind `id={titleId}`
37
+ * to your custom heading element for `aria-labelledby`.
38
+ */
39
+ header?: Snippet<[titleId: string]>;
40
+ /** Custom footer snippet (replaces the default `actions` bar). */
41
+ footer?: Snippet;
42
+ /** Footer action buttons. Each `action.id` must be unique if provided. */
43
+ actions?: ModalAction[];
44
+ /** Master switch for user dismissal. Individual flags (`closeOnEscape`, `closeOnBackdrop`, `showClose`) override this when defined. */
45
+ closable?: boolean;
46
+ /** Allow dismissing via the Escape key (defaults to `closable`). */
47
+ closeOnEscape?: boolean;
48
+ /** Allow dismissing by clicking the backdrop (defaults to `closable`). */
49
+ closeOnBackdrop?: boolean;
50
+ /** Render the header close ("X") button (defaults to `closable`). */
51
+ showClose?: boolean;
52
+ /** Modal width preset. */
53
+ size?: "sm" | "md" | "lg" | "xl" | "full";
54
+ /** Enable entry and exit transitions (respects `prefers-reduced-motion`). */
55
+ animated?: boolean;
56
+ /** Semantic ARIA role. */
57
+ role?: "dialog" | "alertdialog";
58
+ /** Action `id` or valid CSS selector to focus when the modal opens. */
59
+ initialFocus?: string;
60
+ /** Accessible labels for the dialog and close button. */
61
+ labels?: ModalLabels;
62
+ /**
63
+ * Called when the user dismisses the modal (via Escape, backdrop, close button, or an action with `closeOnClick`).
64
+ * Not called when `open` is set to `false` programmatically by the parent.
65
+ */
66
+ onClose?: () => void;
67
+ /** Called when an action callback throws or rejects. */
68
+ onActionError?: (error: unknown, action: ModalAction) => void;
69
+ }
70
+ declare const NuiModal: import("svelte").Component<Props, {}, "open">;
71
+ type NuiModal = ReturnType<typeof NuiModal>;
72
+ export default NuiModal;
@@ -0,0 +1,159 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+
4
+ interface Props {
5
+ /** Tekst podpowiedzi */
6
+ text: string;
7
+ /** Pozycja podpowiedzi */
8
+ position?: 'top' | 'bottom' | 'left' | 'right';
9
+ /** Czas opóźnienia wyświetlania w ms */
10
+ delay?: number;
11
+ /** Element docelowy podpowiedzi */
12
+ children?: import('svelte').Snippet;
13
+ }
14
+
15
+ let {
16
+ text,
17
+ position = 'top',
18
+ delay = 150,
19
+ children
20
+ }: Props = $props();
21
+
22
+ let visible = $state(false);
23
+ let showTimer: ReturnType<typeof setTimeout> | null = null;
24
+
25
+ function show() {
26
+ if (showTimer) clearTimeout(showTimer);
27
+ if (delay > 0) {
28
+ showTimer = setTimeout(() => {
29
+ visible = true;
30
+ }, delay);
31
+ } else {
32
+ visible = true;
33
+ }
34
+ }
35
+
36
+ function hide() {
37
+ if (showTimer) {
38
+ clearTimeout(showTimer);
39
+ showTimer = null;
40
+ }
41
+ visible = false;
42
+ }
43
+
44
+ const cleanup = () => {
45
+ if (showTimer) clearTimeout(showTimer);
46
+ };
47
+
48
+ try {
49
+ onDestroy(cleanup);
50
+ } catch {
51
+ // Outside component lifecycle
52
+ }
53
+ </script>
54
+
55
+ <span
56
+ class="nui-tooltip-wrapper"
57
+ role="presentation"
58
+ onmouseenter={show}
59
+ onmouseleave={hide}
60
+ onfocusin={show}
61
+ onfocusout={hide}
62
+ >
63
+ {@render children?.()}
64
+
65
+ {#if visible && text}
66
+ <div class="nui-tooltip nui-tooltip--{position}" role="tooltip">
67
+ {text}
68
+ </div>
69
+ {/if}
70
+ </span>
71
+
72
+ <style>
73
+ .nui-tooltip-wrapper {
74
+ position: relative;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ }
78
+
79
+ .nui-tooltip {
80
+ position: absolute;
81
+ padding: 0.375rem 0.625rem;
82
+ background: var(--nui-surface-alt, #18181b);
83
+ color: var(--nui-fg, #fafafa);
84
+ border: 1px solid var(--nui-border, #3f3f46);
85
+ border-radius: var(--nui-radius, 0.375rem);
86
+ font-size: 0.75rem;
87
+ line-height: 1.2;
88
+ box-shadow: 0 4px 12px rgb(0 0 0 / 0.35);
89
+ z-index: var(--nui-z-tooltip, 1100);
90
+ pointer-events: none;
91
+ white-space: nowrap;
92
+ animation: nui-tooltip-pop 0.15s ease-out;
93
+ }
94
+
95
+ .nui-tooltip--top {
96
+ bottom: calc(100% + 0.375rem);
97
+ left: 50%;
98
+ transform: translateX(-50%);
99
+ }
100
+
101
+ .nui-tooltip--bottom {
102
+ top: calc(100% + 0.375rem);
103
+ left: 50%;
104
+ transform: translateX(-50%);
105
+ }
106
+
107
+ .nui-tooltip--left {
108
+ right: calc(100% + 0.375rem);
109
+ top: 50%;
110
+ transform: translateY(-50%);
111
+ }
112
+
113
+ .nui-tooltip--right {
114
+ left: calc(100% + 0.375rem);
115
+ top: 50%;
116
+ transform: translateY(-50%);
117
+ }
118
+
119
+ @keyframes nui-tooltip-pop {
120
+ from {
121
+ opacity: 0;
122
+ transform: translate(-50%, 2px);
123
+ }
124
+ to {
125
+ opacity: 1;
126
+ transform: translate(-50%, 0);
127
+ }
128
+ }
129
+
130
+ .nui-tooltip--left {
131
+ animation-name: nui-tooltip-pop-left;
132
+ }
133
+
134
+ @keyframes nui-tooltip-pop-left {
135
+ from {
136
+ opacity: 0;
137
+ transform: translateY(-50%) translateX(2px);
138
+ }
139
+ to {
140
+ opacity: 1;
141
+ transform: translateY(-50%) translateX(0);
142
+ }
143
+ }
144
+
145
+ .nui-tooltip--right {
146
+ animation-name: nui-tooltip-pop-right;
147
+ }
148
+
149
+ @keyframes nui-tooltip-pop-right {
150
+ from {
151
+ opacity: 0;
152
+ transform: translateY(-50%) translateX(-2px);
153
+ }
154
+ to {
155
+ opacity: 1;
156
+ transform: translateY(-50%) translateX(0);
157
+ }
158
+ }
159
+ </style>
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ /** Tekst podpowiedzi */
3
+ text: string;
4
+ /** Pozycja podpowiedzi */
5
+ position?: 'top' | 'bottom' | 'left' | 'right';
6
+ /** Czas opóźnienia wyświetlania w ms */
7
+ delay?: number;
8
+ /** Element docelowy podpowiedzi */
9
+ children?: import('svelte').Snippet;
10
+ }
11
+ declare const NuiTooltip: import("svelte").Component<Props, {}, "">;
12
+ type NuiTooltip = ReturnType<typeof NuiTooltip>;
13
+ export default NuiTooltip;
@@ -0,0 +1,43 @@
1
+ import { type ClickOutsideTarget, type NuiClickOutsideParameter } from '@fivem-nui/svelte';
2
+ export interface ClickOutsideOptions {
3
+ /**
4
+ * Callback invoked when a click or touch occurs outside the target element.
5
+ */
6
+ callback: (event: MouseEvent | TouchEvent | PointerEvent) => void;
7
+ /**
8
+ * Reactive getter or boolean to control whether detection is active.
9
+ * @default true
10
+ */
11
+ enabled?: boolean | (() => boolean);
12
+ /**
13
+ * Elements, selectors, or getters to exclude from triggering outside click.
14
+ * Essential for toggle buttons and popover trigger elements.
15
+ */
16
+ ignore?: ClickOutsideTarget[];
17
+ /**
18
+ * Listen during capture phase.
19
+ * @default true
20
+ */
21
+ capture?: boolean;
22
+ }
23
+ export type ClickOutsideInput = (() => void) | ((event: MouseEvent | TouchEvent | PointerEvent) => void) | ClickOutsideOptions | NuiClickOutsideParameter;
24
+ export interface ClickOutsideActionReturn {
25
+ update: (param: ClickOutsideInput) => void;
26
+ destroy: () => void;
27
+ }
28
+ export type ClickOutsideAction = (node: HTMLElement, param: ClickOutsideInput) => ClickOutsideActionReturn;
29
+ /**
30
+ * Action for detecting clicks outside an element.
31
+ *
32
+ * Backed by `@fivem-nui/svelte` with `event.composedPath()` (eliminating the detached
33
+ * DOM node bug in dynamic lists) and `AbortController` lifecycle management.
34
+ *
35
+ * @example
36
+ * ```svelte
37
+ * <div use:clickOutsideAction={() => (open = false)}>...</div>
38
+ * <div use:clickOutsideAction={{ callback: close, ignore: [btnEl] }}>...</div>
39
+ * ```
40
+ */
41
+ export declare const clickOutsideAction: ClickOutsideAction;
42
+ export declare function useClickOutside(getElement: () => HTMLElement | null | undefined, callbackOrOptions: ClickOutsideInput | (() => ClickOutsideInput)): void;
43
+ export declare const useClickOutsideElement: typeof useClickOutside;
@@ -0,0 +1,73 @@
1
+ import { nuiClickOutside, useClickOutside as bridgeUseClickOutside } from '@fivem-nui/svelte';
2
+ function mapOptions(param) {
3
+ if (typeof param === 'function') {
4
+ return {
5
+ callback: param
6
+ };
7
+ }
8
+ const cb = 'callback' in param && param.callback
9
+ ? param.callback
10
+ : 'onClickOutside' in param && param.onClickOutside
11
+ ? param.onClickOutside
12
+ : undefined;
13
+ return {
14
+ callback: cb,
15
+ onClickOutside: cb,
16
+ enabled: param.enabled,
17
+ ignore: 'ignore' in param ? param.ignore : undefined,
18
+ capture: 'capture' in param ? param.capture : true
19
+ };
20
+ }
21
+ /**
22
+ * Action for detecting clicks outside an element.
23
+ *
24
+ * Backed by `@fivem-nui/svelte` with `event.composedPath()` (eliminating the detached
25
+ * DOM node bug in dynamic lists) and `AbortController` lifecycle management.
26
+ *
27
+ * @example
28
+ * ```svelte
29
+ * <div use:clickOutsideAction={() => (open = false)}>...</div>
30
+ * <div use:clickOutsideAction={{ callback: close, ignore: [btnEl] }}>...</div>
31
+ * ```
32
+ */
33
+ export const clickOutsideAction = (node, param) => {
34
+ const innerAction = nuiClickOutside(node, mapOptions(param));
35
+ return {
36
+ update(newParam) {
37
+ innerAction?.update?.(mapOptions(newParam));
38
+ },
39
+ destroy() {
40
+ innerAction?.destroy?.();
41
+ }
42
+ };
43
+ };
44
+ /**
45
+ * Functional composable to watch click outside for an element getter or reference.
46
+ * Automatically cleans up on component unmount via `$effect` in `@fivem-nui/svelte`.
47
+ */
48
+ function isOptionsGetter(fn) {
49
+ if (typeof fn !== 'function')
50
+ return false;
51
+ if (fn.length > 0)
52
+ return false;
53
+ const str = Function.prototype.toString.call(fn);
54
+ return /(return\s*\{|\(\s*\{)[\s\S]*\b(callback|onClickOutside|enabled|ignore|capture)\b/.test(str);
55
+ }
56
+ export function useClickOutside(getElement, callbackOrOptions) {
57
+ const getOptions = () => {
58
+ if (typeof callbackOrOptions !== 'function') {
59
+ return mapOptions(callbackOrOptions);
60
+ }
61
+ if (isOptionsGetter(callbackOrOptions)) {
62
+ const result = callbackOrOptions();
63
+ return mapOptions(result);
64
+ }
65
+ return {
66
+ callback: callbackOrOptions,
67
+ onClickOutside: callbackOrOptions,
68
+ capture: true
69
+ };
70
+ };
71
+ bridgeUseClickOutside(getElement, getOptions);
72
+ }
73
+ export const useClickOutsideElement = useClickOutside;
@@ -0,0 +1,23 @@
1
+ export interface ValidationRules {
2
+ /** Whether the field is required. Considered empty: null, undefined, '', whitespace-only, [], NaN */
3
+ required?: boolean;
4
+ /** Minimum length for strings/arrays, or minimum numeric value */
5
+ min?: number;
6
+ /** Maximum length for strings/arrays, or maximum numeric value */
7
+ max?: number;
8
+ /** Whether to validate the value as an email address */
9
+ email?: boolean;
10
+ /** Regular expression pattern for strings. Sticky (y) and global (g) flags are ignored */
11
+ pattern?: string | RegExp;
12
+ /** Synchronous custom validator. Returns true if valid, false for default error, or a custom error message string */
13
+ custom?: (value: unknown, values?: Record<string, unknown>) => boolean | string;
14
+ /** Field label used in validation messages instead of the field key */
15
+ label?: string;
16
+ /** Custom error message overrides for specific rules */
17
+ messages?: Partial<Record<'required' | 'min' | 'max' | 'email' | 'pattern' | 'custom', string>>;
18
+ }
19
+ export declare function validateField(name: string, value: unknown, rules: ValidationRules, values?: Record<string, unknown>): string | null;
20
+ /**
21
+ * Validates form values against the provided rules and returns a map of field errors.
22
+ */
23
+ export declare function validateForm(values: Record<string, unknown>, rules: Record<string, ValidationRules>): Record<string, string>;
@@ -0,0 +1,72 @@
1
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2
+ function isEmpty(value) {
3
+ if (value === null || value === undefined)
4
+ return true;
5
+ if (typeof value === 'string')
6
+ return value.trim() === '';
7
+ if (typeof value === 'number')
8
+ return Number.isNaN(value);
9
+ if (Array.isArray(value))
10
+ return value.length === 0;
11
+ return false;
12
+ }
13
+ function toRegExp(pattern) {
14
+ // New instance without g/y flags: ensures test() is stateless
15
+ const flags = pattern instanceof RegExp ? pattern.flags.replace(/[gy]/g, '') : '';
16
+ return new RegExp(pattern instanceof RegExp ? pattern.source : pattern, flags);
17
+ }
18
+ export function validateField(name, value, rules, values) {
19
+ const label = rules.label ?? name;
20
+ const msg = rules.messages ?? {};
21
+ if (isEmpty(value)) {
22
+ return rules.required ? (msg.required ?? `${label} is required`) : null;
23
+ }
24
+ if (rules.min !== undefined) {
25
+ if (typeof value === 'string' && value.length < rules.min)
26
+ return msg.min ?? `${label} must be at least ${rules.min} characters`;
27
+ if (Array.isArray(value) && value.length < rules.min)
28
+ return msg.min ?? `${label} must have at least ${rules.min} items`;
29
+ if (typeof value === 'number' && value < rules.min)
30
+ return msg.min ?? `${label} must be at least ${rules.min}`;
31
+ }
32
+ if (rules.max !== undefined) {
33
+ if (typeof value === 'string' && value.length > rules.max)
34
+ return msg.max ?? `${label} must be at most ${rules.max} characters`;
35
+ if (Array.isArray(value) && value.length > rules.max)
36
+ return msg.max ?? `${label} must have at most ${rules.max} items`;
37
+ if (typeof value === 'number' && value > rules.max)
38
+ return msg.max ?? `${label} must be at most ${rules.max}`;
39
+ }
40
+ if (rules.email && typeof value === 'string' && !EMAIL_RE.test(value.trim())) {
41
+ return msg.email ?? `${label} must be a valid email address`;
42
+ }
43
+ if (rules.pattern && typeof value === 'string') {
44
+ // Invalid pattern is a configuration error: new RegExp will throw SyntaxError
45
+ if (!toRegExp(rules.pattern).test(value)) {
46
+ return msg.pattern ?? `${label} has an invalid format`;
47
+ }
48
+ }
49
+ if (rules.custom) {
50
+ const result = rules.custom(value, values);
51
+ if (result && typeof result.then === 'function') {
52
+ throw new TypeError(`[validateField] "${name}": async custom validators are not supported`);
53
+ }
54
+ if (typeof result === 'string')
55
+ return result === '' ? null : result;
56
+ if (!result)
57
+ return msg.custom ?? `${label} is invalid`;
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Validates form values against the provided rules and returns a map of field errors.
63
+ */
64
+ export function validateForm(values, rules) {
65
+ const errors = {};
66
+ for (const [field, rule] of Object.entries(rules)) {
67
+ const error = validateField(field, values[field], rule, values);
68
+ if (error)
69
+ errors[field] = error;
70
+ }
71
+ return errors;
72
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Hooks tracking user inactivity and idle state in FiveM NUI.
3
+ * Note: Must be called within a component lifecycle context due to `$effect`.
4
+ */
5
+ export interface IdleState {
6
+ readonly isIdle: boolean;
7
+ readonly idleTime: number;
8
+ reset(force?: boolean): void;
9
+ }
10
+ /**
11
+ * Tracks user inactivity and idle state in NUI.
12
+ */
13
+ export declare function useIdle(timeoutMs?: number, updateIntervalMs?: number): IdleState;
14
+ /**
15
+ * One-shot activity observer: executes callback exactly once when the user becomes idle.
16
+ * Returns a cancel function to abort observation.
17
+ */
18
+ export declare function useIdleUntil(timeoutMs?: number, callback?: () => void): () => void;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Hooks tracking user inactivity and idle state in FiveM NUI.
3
+ * Note: Must be called within a component lifecycle context due to `$effect`.
4
+ */
5
+ const IDLE_EVENTS = [
6
+ 'pointermove',
7
+ 'pointerdown',
8
+ 'keydown',
9
+ 'scroll',
10
+ 'wheel',
11
+ ];
12
+ const THROTTLE_MS = 500;
13
+ /**
14
+ * Shared core helper managing event listeners with capture mode, throttle bypass, and timeout scheduling.
15
+ */
16
+ function createIdleManager(timeoutMs, onTimeout, onActivity, shouldBypassThrottle) {
17
+ let timer = null;
18
+ let controller = null;
19
+ let lastResetTime = 0;
20
+ function clearTimer() {
21
+ if (timer) {
22
+ clearTimeout(timer);
23
+ timer = null;
24
+ }
25
+ }
26
+ function schedule() {
27
+ clearTimer();
28
+ timer = setTimeout(onTimeout, timeoutMs);
29
+ }
30
+ function handleActivity(force = false) {
31
+ const now = Date.now();
32
+ const bypass = force || (shouldBypassThrottle?.() ?? false);
33
+ if (!bypass && now - lastResetTime < THROTTLE_MS)
34
+ return;
35
+ lastResetTime = now;
36
+ onActivity?.();
37
+ schedule();
38
+ }
39
+ const listener = () => handleActivity(false);
40
+ function init() {
41
+ controller = new AbortController();
42
+ for (const event of IDLE_EVENTS) {
43
+ window.addEventListener(event, listener, {
44
+ passive: true,
45
+ capture: true, // Crucial for catching scrolls inside inner NUI containers
46
+ signal: controller.signal,
47
+ });
48
+ }
49
+ schedule();
50
+ return () => {
51
+ clearTimer();
52
+ controller?.abort();
53
+ controller = null;
54
+ };
55
+ }
56
+ return {
57
+ init,
58
+ reset: (force = true) => handleActivity(force), // Manual reset defaults to forcing bypass
59
+ };
60
+ }
61
+ /**
62
+ * Tracks user inactivity and idle state in NUI.
63
+ */
64
+ export function useIdle(timeoutMs = 30_000, updateIntervalMs = 1_000) {
65
+ let isIdle = $state(false);
66
+ let idleTime = $state(0);
67
+ let lastActive = Date.now();
68
+ let interval = null;
69
+ function updateIdleTime() {
70
+ idleTime = Math.max(0, Date.now() - lastActive);
71
+ }
72
+ function startInterval() {
73
+ if (interval)
74
+ return;
75
+ interval = setInterval(updateIdleTime, updateIntervalMs);
76
+ }
77
+ function stopInterval() {
78
+ if (interval) {
79
+ clearInterval(interval);
80
+ interval = null;
81
+ }
82
+ }
83
+ const manager = createIdleManager(timeoutMs, () => {
84
+ isIdle = true;
85
+ // Synchronize idleTime immediately when entering idle state
86
+ idleTime = Math.max(0, Date.now() - lastActive);
87
+ }, () => {
88
+ lastActive = Date.now();
89
+ idleTime = 0;
90
+ if (isIdle) {
91
+ isIdle = false;
92
+ }
93
+ startInterval();
94
+ }, () => isIdle);
95
+ $effect(() => {
96
+ lastActive = Date.now();
97
+ idleTime = 0;
98
+ isIdle = false;
99
+ startInterval();
100
+ const cleanupManager = manager.init();
101
+ return () => {
102
+ cleanupManager();
103
+ stopInterval();
104
+ };
105
+ });
106
+ return {
107
+ get isIdle() {
108
+ return isIdle;
109
+ },
110
+ get idleTime() {
111
+ return idleTime;
112
+ },
113
+ reset: (force = true) => manager.reset(force),
114
+ };
115
+ }
116
+ /**
117
+ * One-shot activity observer: executes callback exactly once when the user becomes idle.
118
+ * Returns a cancel function to abort observation.
119
+ */
120
+ export function useIdleUntil(timeoutMs = 30_000, callback) {
121
+ let fired = false;
122
+ let cleanupManager = null;
123
+ const manager = createIdleManager(timeoutMs, () => {
124
+ if (!fired) {
125
+ fired = true;
126
+ callback?.();
127
+ cleanupManager?.();
128
+ cleanupManager = null;
129
+ }
130
+ });
131
+ $effect(() => {
132
+ fired = false; // Reset fired state on effect re-run/re-mount
133
+ cleanupManager = manager.init();
134
+ return () => {
135
+ cleanupManager?.();
136
+ cleanupManager = null;
137
+ };
138
+ });
139
+ const cancel = () => {
140
+ cleanupManager?.();
141
+ cleanupManager = null;
142
+ };
143
+ return cancel;
144
+ }