@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,66 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ /** Exact number of equal columns. When omitted, columns auto fit by `minColumnWidth` */
7
+ columns?: number;
8
+ /** Gap between items. Numbers are treated as px */
9
+ gap?: string | number;
10
+ /** Minimum column width in auto mode. Numbers are treated as px */
11
+ minColumnWidth?: string | number;
12
+ children?: Snippet;
13
+ }
14
+
15
+ let {
16
+ columns,
17
+ gap = "1rem",
18
+ minColumnWidth = "200px",
19
+ children,
20
+ class: className = "",
21
+ style,
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ const toCss = (v: string | number) => (typeof v === "number" ? `${v}px` : v);
26
+
27
+ const cols = $derived(
28
+ columns !== undefined && Number.isFinite(columns) && columns >= 1
29
+ ? Math.floor(columns)
30
+ : undefined,
31
+ );
32
+
33
+ const styleVars = $derived(
34
+ `--nui-grid-gap:${toCss(gap)};--nui-grid-min:${toCss(minColumnWidth)};` +
35
+ (cols ? `--nui-grid-columns:${cols};` : ""),
36
+ );
37
+ const styleExtra = $derived(typeof style === "string" ? style : "");
38
+ </script>
39
+
40
+ <div
41
+ {...rest}
42
+ class={["nui-grid", cols ? "nui-grid--fixed" : "nui-grid--auto", className]}
43
+ style={styleVars + styleExtra}
44
+ >
45
+ {@render children?.()}
46
+ </div>
47
+
48
+ <style>
49
+ .nui-grid {
50
+ display: grid;
51
+ gap: var(--nui-grid-gap, 1rem);
52
+ font-family: inherit;
53
+ box-sizing: border-box;
54
+ }
55
+
56
+ .nui-grid--fixed {
57
+ grid-template-columns: repeat(var(--nui-grid-columns), minmax(0, 1fr));
58
+ }
59
+
60
+ .nui-grid--auto {
61
+ grid-template-columns: repeat(
62
+ auto-fit,
63
+ minmax(min(100%, var(--nui-grid-min, 200px)), 1fr)
64
+ );
65
+ }
66
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ /** Exact number of equal columns. When omitted, columns auto fit by `minColumnWidth` */
5
+ columns?: number;
6
+ /** Gap between items. Numbers are treated as px */
7
+ gap?: string | number;
8
+ /** Minimum column width in auto mode. Numbers are treated as px */
9
+ minColumnWidth?: string | number;
10
+ children?: Snippet;
11
+ }
12
+ declare const NuiGrid: import("svelte").Component<Props, {}, "">;
13
+ type NuiGrid = ReturnType<typeof NuiGrid>;
14
+ export default NuiGrid;
@@ -0,0 +1,138 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ type Direction = "row" | "column" | "row-reverse" | "column-reverse";
6
+ type Align = "start" | "center" | "end" | "stretch" | "baseline";
7
+
8
+ interface Props extends HTMLAttributes<HTMLDivElement> {
9
+ /** Stack direction. Note: `*-reverse` changes visual order only, not DOM/tab order */
10
+ direction?: Direction;
11
+ /**
12
+ * Push items to opposite ends (`space-between`).
13
+ * Needs extra space along the main axis, eg. parent defined height in column mode.
14
+ * Note: for single child, items align at the start.
15
+ */
16
+ distribute?: boolean;
17
+ /** Cross axis alignment */
18
+ align?: Align;
19
+ /** Allow items to wrap onto multiple lines */
20
+ wrap?: boolean;
21
+ /** Gap between items. Numbers are treated as px. Default: `0.5rem` (tighter than NuiFlex's `1rem`) */
22
+ gap?: string | number;
23
+ /** Render as inline flex */
24
+ inline?: boolean;
25
+ children?: Snippet;
26
+ }
27
+
28
+ const VALID_DIRECTIONS = new Set<Direction>([
29
+ "row",
30
+ "column",
31
+ "row-reverse",
32
+ "column-reverse",
33
+ ]);
34
+ const VALID_ALIGNS = new Set<Align>([
35
+ "start",
36
+ "center",
37
+ "end",
38
+ "stretch",
39
+ "baseline",
40
+ ]);
41
+
42
+ let {
43
+ direction = "column",
44
+ distribute = false,
45
+ align = "stretch",
46
+ wrap = false,
47
+ gap = "0.5rem",
48
+ inline = false,
49
+ children,
50
+ class: className = "",
51
+ style,
52
+ ...rest
53
+ }: Props = $props();
54
+
55
+ const toCss = (v: string | number | undefined | null, fallback: string) => {
56
+ if (typeof v === "number") return `${v}px`;
57
+ if (typeof v === "string" && v.trim().length > 0) return v.trim();
58
+ return fallback;
59
+ };
60
+
61
+ const safeDirection = $derived(
62
+ direction && VALID_DIRECTIONS.has(direction) ? direction : "column",
63
+ );
64
+ const safeAlign = $derived(
65
+ align && VALID_ALIGNS.has(align) ? align : "stretch",
66
+ );
67
+
68
+ const styleVars = $derived(`--nui-stack-gap:${toCss(gap, "0.5rem")};`);
69
+ /**
70
+ * Concatenate extra inline style. Svelte 5 passes style strings here; non-string values are safely ignored.
71
+ */
72
+ const styleExtra = $derived(typeof style === "string" ? style : "");
73
+ </script>
74
+
75
+ <div
76
+ {...rest}
77
+ class={[
78
+ "nui-stack",
79
+ `nui-stack--${safeDirection}`,
80
+ `nui-stack--align-${safeAlign}`,
81
+ distribute && "nui-stack--distribute",
82
+ wrap && "nui-stack--wrap",
83
+ inline && "nui-stack--inline",
84
+ className,
85
+ ]}
86
+ style={styleVars + styleExtra}
87
+ >
88
+ {@render children?.()}
89
+ </div>
90
+
91
+ <style>
92
+ .nui-stack {
93
+ display: flex;
94
+ gap: var(--nui-stack-gap, 0.5rem);
95
+ font-family: inherit;
96
+ box-sizing: border-box;
97
+ }
98
+
99
+ .nui-stack--inline {
100
+ display: inline-flex;
101
+ }
102
+
103
+ .nui-stack--row {
104
+ flex-direction: row;
105
+ }
106
+ .nui-stack--column {
107
+ flex-direction: column;
108
+ }
109
+ .nui-stack--row-reverse {
110
+ flex-direction: row-reverse;
111
+ }
112
+ .nui-stack--column-reverse {
113
+ flex-direction: column-reverse;
114
+ }
115
+
116
+ .nui-stack--distribute {
117
+ justify-content: space-between;
118
+ }
119
+ .nui-stack--wrap {
120
+ flex-wrap: wrap;
121
+ }
122
+
123
+ .nui-stack--align-start {
124
+ align-items: flex-start;
125
+ }
126
+ .nui-stack--align-center {
127
+ align-items: center;
128
+ }
129
+ .nui-stack--align-end {
130
+ align-items: flex-end;
131
+ }
132
+ .nui-stack--align-stretch {
133
+ align-items: stretch;
134
+ }
135
+ .nui-stack--align-baseline {
136
+ align-items: baseline;
137
+ }
138
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ type Direction = "row" | "column" | "row-reverse" | "column-reverse";
4
+ type Align = "start" | "center" | "end" | "stretch" | "baseline";
5
+ interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ /** Stack direction. Note: `*-reverse` changes visual order only, not DOM/tab order */
7
+ direction?: Direction;
8
+ /**
9
+ * Push items to opposite ends (`space-between`).
10
+ * Needs extra space along the main axis, eg. parent defined height in column mode.
11
+ * Note: for single child, items align at the start.
12
+ */
13
+ distribute?: boolean;
14
+ /** Cross axis alignment */
15
+ align?: Align;
16
+ /** Allow items to wrap onto multiple lines */
17
+ wrap?: boolean;
18
+ /** Gap between items. Numbers are treated as px. Default: `0.5rem` (tighter than NuiFlex's `1rem`) */
19
+ gap?: string | number;
20
+ /** Render as inline flex */
21
+ inline?: boolean;
22
+ children?: Snippet;
23
+ }
24
+ declare const NuiStack: import("svelte").Component<Props, {}, "">;
25
+ type NuiStack = ReturnType<typeof NuiStack>;
26
+ export default NuiStack;
@@ -0,0 +1,323 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ export interface AccordionItem {
5
+ id: string;
6
+ title: string;
7
+ /** Plain text content. Rendered as text (never as HTML) */
8
+ content?: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export interface AccordionToggleEvent {
13
+ id: string;
14
+ isOpen: boolean;
15
+ openIds: string[];
16
+ /** Single mode only: the section that was open before and is now closed */
17
+ previousId?: string;
18
+ }
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ import { SvelteSet } from "svelte/reactivity";
23
+ import { nui } from "@fivem-nui/svelte";
24
+ import type { SvelteNuiClient } from "@fivem-nui/svelte";
25
+ import type { HTMLAttributes } from "svelte/elements";
26
+
27
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
28
+ items: AccordionItem[];
29
+ client?: SvelteNuiClient;
30
+ /** Called after every toggle */
31
+ onToggle?: (data: AccordionToggleEvent) => void;
32
+ /** Event name sent to FiveM on every toggle */
33
+ eventName?: string;
34
+ /** Allow several sections open at once */
35
+ multiple?: boolean;
36
+ /** Open all sections initially (ignored when `multiple` is false) */
37
+ defaultOpenAll?: boolean;
38
+ /** Initially open ids (trimmed to the first one when `multiple` is false) */
39
+ defaultOpenIds?: string[];
40
+ /** Currently open section IDs. Supports two-way binding via `bind:openIds`. */
41
+ openIds?: string[];
42
+ /** Custom panel content. Receives the item. Takes precedence over `item.content` */
43
+ panel?: Snippet<[AccordionItem]>;
44
+ }
45
+
46
+ const uid = $props.id();
47
+
48
+ let {
49
+ items = [],
50
+ client = nui,
51
+ onToggle,
52
+ eventName,
53
+ multiple = false,
54
+ defaultOpenAll = false,
55
+ defaultOpenIds = [],
56
+ openIds: openIdsProp = $bindable(),
57
+ panel,
58
+ class: className = "",
59
+ ...rest
60
+ }: Props = $props();
61
+
62
+ const internalOpenIds = new SvelteSet<string>();
63
+ let hasInitializedDefaults = false;
64
+
65
+ function toSafeId(id: string): string {
66
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
67
+ }
68
+
69
+ $effect.pre(() => {
70
+ if (openIdsProp !== undefined || hasInitializedDefaults || items.length === 0) return;
71
+ hasInitializedDefaults = true;
72
+
73
+ const initial =
74
+ defaultOpenAll && multiple
75
+ ? items.map((i) => i.id)
76
+ : defaultOpenIds;
77
+
78
+ const known = new Set(items.map((i) => i.id));
79
+ const validInitial = initial.filter((id) => known.has(id));
80
+ const finalInitial = !multiple ? validInitial.slice(0, 1) : validInitial;
81
+
82
+ for (const id of finalInitial) internalOpenIds.add(id);
83
+ });
84
+
85
+ // Sync external openIdsProp changes into internalOpenIds, and update openIdsProp if pruned
86
+ $effect(() => {
87
+ if (openIdsProp === undefined) return;
88
+ const known = new Set(items.map((i) => i.id));
89
+ const target = multiple ? openIdsProp : openIdsProp.slice(0, 1);
90
+ const targetValid = target.filter((id) => known.has(id));
91
+
92
+ // If openIdsProp contained invalid items or exceeded single-mode limit, sync back to parent
93
+ if (
94
+ targetValid.length !== openIdsProp.length ||
95
+ !targetValid.every((id, idx) => id === openIdsProp![idx])
96
+ ) {
97
+ openIdsProp = [...targetValid];
98
+ }
99
+
100
+ const currentList = [...internalOpenIds];
101
+ const isSame =
102
+ currentList.length === targetValid.length &&
103
+ currentList.every((id) => targetValid.includes(id));
104
+ if (!isSame) {
105
+ internalOpenIds.clear();
106
+ for (const id of targetValid) internalOpenIds.add(id);
107
+ }
108
+ });
109
+
110
+ // For uncontrolled mode: drop ids that no longer exist in `items`
111
+ $effect(() => {
112
+ if (openIdsProp !== undefined) return;
113
+ const known = new Set(items.map((i) => i.id));
114
+ for (const id of [...internalOpenIds]) {
115
+ if (!known.has(id)) {
116
+ internalOpenIds.delete(id);
117
+ }
118
+ }
119
+ });
120
+
121
+ function toggle(item: AccordionItem) {
122
+ if (item.disabled) return;
123
+
124
+ const { id } = item;
125
+ const wasOpen = internalOpenIds.has(id);
126
+ let previousId: string | undefined;
127
+
128
+ if (multiple) {
129
+ if (wasOpen) internalOpenIds.delete(id);
130
+ else internalOpenIds.add(id);
131
+ } else {
132
+ const other = [...internalOpenIds].find((x) => x !== id);
133
+ previousId = other;
134
+ internalOpenIds.clear();
135
+ if (!wasOpen) internalOpenIds.add(id);
136
+ }
137
+
138
+ if (openIdsProp !== undefined) {
139
+ openIdsProp = [...internalOpenIds];
140
+ }
141
+
142
+ const payload: AccordionToggleEvent = {
143
+ id,
144
+ isOpen: !wasOpen,
145
+ openIds: [...internalOpenIds],
146
+ previousId,
147
+ };
148
+
149
+ onToggle?.(payload);
150
+
151
+ if (eventName) {
152
+ try {
153
+ const res: unknown = client?.send(eventName, { ...payload, count: payload.openIds.length });
154
+ if (res && typeof (res as { catch?: unknown }).catch === "function") {
155
+ (res as Promise<unknown>).catch((err: unknown) => {
156
+ console.warn(
157
+ "[fivem-nui-components] NuiAccordion: failed to send event",
158
+ err,
159
+ );
160
+ });
161
+ }
162
+ } catch (err) {
163
+ console.warn(
164
+ "[fivem-nui-components] NuiAccordion: failed to send event",
165
+ err,
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ function onKeydown(event: KeyboardEvent) {
172
+ const keys = ["ArrowDown", "ArrowUp", "Home", "End"];
173
+ if (!keys.includes(event.key)) return;
174
+
175
+ const root = event.currentTarget as HTMLElement;
176
+ const headers = [
177
+ ...root.querySelectorAll<HTMLButtonElement>(
178
+ ".nui-accordion__header:not(:disabled)",
179
+ ),
180
+ ];
181
+ const targetBtn = (event.target as HTMLElement)?.closest<HTMLButtonElement>(
182
+ ".nui-accordion__header",
183
+ );
184
+ const current = targetBtn ? headers.indexOf(targetBtn) : -1;
185
+ if (current === -1) return;
186
+
187
+ event.preventDefault();
188
+ let next = current;
189
+ if (event.key === "ArrowDown") next = (current + 1) % headers.length;
190
+ else if (event.key === "ArrowUp")
191
+ next = (current - 1 + headers.length) % headers.length;
192
+ else if (event.key === "Home") next = 0;
193
+ else if (event.key === "End") next = headers.length - 1;
194
+ headers[next]?.focus();
195
+ }
196
+ </script>
197
+
198
+ <div {...rest} class={["nui-accordion", className]} onkeydown={onKeydown}>
199
+ {#each items as item (item.id)}
200
+ {@const isOpen = internalOpenIds.has(item.id)}
201
+ {@const safeId = toSafeId(item.id)}
202
+ {@const headerId = `nui-acc-${uid}-${safeId}-header`}
203
+ {@const panelId = `nui-acc-${uid}-${safeId}-panel`}
204
+ <div class="nui-accordion__item">
205
+ <button
206
+ type="button"
207
+ id={headerId}
208
+ class="nui-accordion__header"
209
+ class:nui-accordion__header--open={isOpen}
210
+ disabled={item.disabled}
211
+ aria-expanded={isOpen}
212
+ aria-controls={panelId}
213
+ onclick={() => toggle(item)}
214
+ onkeydown={(e) => {
215
+ if (e.key === "Enter" || e.key === " ") {
216
+ e.preventDefault();
217
+ toggle(item);
218
+ }
219
+ }}
220
+ >
221
+ <span class="nui-accordion__title">{item.title}</span>
222
+ <span class="nui-accordion__icon" aria-hidden="true">▾</span>
223
+ </button>
224
+
225
+ <div
226
+ id={panelId}
227
+ class="nui-accordion__panel"
228
+ role="region"
229
+ aria-labelledby={headerId}
230
+ hidden={!isOpen}
231
+ >
232
+ <div class="nui-accordion__content">
233
+ {#if panel}
234
+ {@render panel(item)}
235
+ {:else if item.content !== undefined}
236
+ {item.content}
237
+ {/if}
238
+ </div>
239
+ </div>
240
+ </div>
241
+ {/each}
242
+ </div>
243
+
244
+ <style>
245
+ .nui-accordion {
246
+ display: flex;
247
+ flex-direction: column;
248
+ font-family: inherit;
249
+ box-sizing: border-box;
250
+ }
251
+
252
+ .nui-accordion__item {
253
+ border-bottom: 1px solid var(--nui-border, #27272a);
254
+ }
255
+
256
+ .nui-accordion__header {
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: space-between;
260
+ width: 100%;
261
+ padding: 0.875rem 1rem;
262
+ background: var(--nui-surface-alt, #101012);
263
+ border: none;
264
+ border-radius: 0;
265
+ color: var(--nui-fg, #fafafa);
266
+ font: inherit;
267
+ font-weight: 500;
268
+ cursor: pointer;
269
+ transition: background 0.15s ease;
270
+ }
271
+
272
+ .nui-accordion__header:hover:not(:disabled) {
273
+ background: var(--nui-hover, rgb(255 255 255 / 0.06));
274
+ }
275
+
276
+ .nui-accordion__header:focus-visible {
277
+ outline: 2px solid var(--nui-primary, #3b82f6);
278
+ outline-offset: -2px;
279
+ }
280
+
281
+ .nui-accordion__header:disabled {
282
+ opacity: 0.4;
283
+ cursor: not-allowed;
284
+ }
285
+
286
+ .nui-accordion__header--open {
287
+ background: var(--nui-surface, #18181b);
288
+ }
289
+
290
+ .nui-accordion__title {
291
+ flex: 1;
292
+ text-align: left;
293
+ font-size: 0.875rem;
294
+ }
295
+
296
+ .nui-accordion__icon {
297
+ font-size: 0.85rem;
298
+ opacity: 0.6;
299
+ transition: transform 0.2s ease;
300
+ }
301
+
302
+ .nui-accordion__header--open .nui-accordion__icon {
303
+ transform: rotate(180deg);
304
+ }
305
+
306
+ .nui-accordion__panel {
307
+ background: var(--nui-surface, #18181b);
308
+ color: var(--nui-fg, #fafafa);
309
+ }
310
+
311
+ .nui-accordion__content {
312
+ padding: 1rem;
313
+ font-size: 0.85rem;
314
+ line-height: 1.5;
315
+ }
316
+
317
+ @media (prefers-reduced-motion: reduce) {
318
+ .nui-accordion__header,
319
+ .nui-accordion__icon {
320
+ transition: none;
321
+ }
322
+ }
323
+ </style>
@@ -0,0 +1,38 @@
1
+ import type { Snippet } from "svelte";
2
+ export interface AccordionItem {
3
+ id: string;
4
+ title: string;
5
+ /** Plain text content. Rendered as text (never as HTML) */
6
+ content?: string;
7
+ disabled?: boolean;
8
+ }
9
+ export interface AccordionToggleEvent {
10
+ id: string;
11
+ isOpen: boolean;
12
+ openIds: string[];
13
+ /** Single mode only: the section that was open before and is now closed */
14
+ previousId?: string;
15
+ }
16
+ import type { SvelteNuiClient } from "@fivem-nui/svelte";
17
+ import type { HTMLAttributes } from "svelte/elements";
18
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
19
+ items: AccordionItem[];
20
+ client?: SvelteNuiClient;
21
+ /** Called after every toggle */
22
+ onToggle?: (data: AccordionToggleEvent) => void;
23
+ /** Event name sent to FiveM on every toggle */
24
+ eventName?: string;
25
+ /** Allow several sections open at once */
26
+ multiple?: boolean;
27
+ /** Open all sections initially (ignored when `multiple` is false) */
28
+ defaultOpenAll?: boolean;
29
+ /** Initially open ids (trimmed to the first one when `multiple` is false) */
30
+ defaultOpenIds?: string[];
31
+ /** Currently open section IDs. Supports two-way binding via `bind:openIds`. */
32
+ openIds?: string[];
33
+ /** Custom panel content. Receives the item. Takes precedence over `item.content` */
34
+ panel?: Snippet<[AccordionItem]>;
35
+ }
36
+ declare const NuiAccordion: import("svelte").Component<Props, {}, "openIds">;
37
+ type NuiAccordion = ReturnType<typeof NuiAccordion>;
38
+ export default NuiAccordion;