@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
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@fivem-nui/svelte-components",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "UI components for FiveM NUI built on Svelte 5",
5
+ "type": "module",
6
+ "svelte": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "svelte": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./src": "./src/index.ts"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "scripts": {
21
+ "build": "svelte-package -i src -o dist",
22
+ "check": "tsc --noEmit --skipLibCheck --moduleResolution bundler --allowJs --esModuleInterop --strict",
23
+ "test": "npx --prefix ../bridge vitest run --config ../bridge/vitest.config.ts"
24
+ },
25
+ "keywords": [
26
+ "fivem",
27
+ "nui",
28
+ "components",
29
+ "svelte"
30
+ ],
31
+ "author": "Rainbe",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/rian-be/FiveM.Nui.git",
36
+ "directory": "packages/svelte/components"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/rian-be/FiveM.Nui/issues"
40
+ },
41
+ "homepage": "https://github.com/rian-be/FiveM.Nui/tree/main/packages/svelte/components#readme",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "engines": {
46
+ "node": ">=22.12.0"
47
+ },
48
+ "peerDependencies": {
49
+ "@fivem-nui/core": "^1.0.0",
50
+ "@fivem-nui/svelte": "^1.0.0",
51
+ "svelte": "^5.20.0"
52
+ },
53
+ "dependencies": {
54
+ "esm-env": "^1.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@sveltejs/package": "^2.0.0",
58
+ "svelte": "^5.20.0",
59
+ "typescript": "^5.7.3"
60
+ }
6
61
  }
@@ -0,0 +1,292 @@
1
+ <script lang="ts">
2
+ import { nui } from '@fivem-nui/svelte';
3
+ import type { SvelteNuiClient } from '@fivem-nui/svelte';
4
+
5
+ interface Props {
6
+ /** Event name sent to the game when the button is clicked. */
7
+ eventName?: string;
8
+ /** Payload sent along with {@link eventName}. */
9
+ payload?: unknown;
10
+ client?: SvelteNuiClient;
11
+ loading?: boolean;
12
+ disabled?: boolean;
13
+ type?: 'button' | 'submit' | 'reset';
14
+ variant?: 'primary' | 'secondary' | 'danger' | 'link';
15
+ size?: 'xs' | 'sm' | 'md' | 'lg';
16
+ /** Event sent right after the main NUI operation succeeds. */
17
+ onSuccess?: string;
18
+ /**
19
+ * Event sent when the local handler or main NUI operation fails.
20
+ * Note: A failure in sending onSuccess will not trigger this.
21
+ */
22
+ onError?: string;
23
+ onSuccessPayload?: unknown;
24
+ onErrorPayload?: unknown;
25
+ /** Local callback invoked when the operation fails. */
26
+ onFailure?: (error: unknown) => void;
27
+ /** Local callback invoked when the operation succeeds. */
28
+ onSuccessCallback?: (result?: unknown) => void;
29
+ /**
30
+ * Predicate deciding whether the button is currently enabled.
31
+ * Function must read reactive Svelte state to update automatically.
32
+ */
33
+ condition?: (() => boolean) | null;
34
+ /**
35
+ * Predicate or boolean that disables the button when true.
36
+ * Function must read reactive Svelte state to update automatically.
37
+ */
38
+ disabledWhen?: (() => boolean) | boolean | null;
39
+ /**
40
+ * When true, keeps loading state true upon successful completion.
41
+ * The parent component is responsible for resetting loading via bind:loading.
42
+ * On error, loading is always reset to false if autoReset is true.
43
+ */
44
+ keepLoadingOnSuccess?: boolean;
45
+ /**
46
+ * @deprecated Use {@link keepLoadingOnSuccess} instead.
47
+ */
48
+ autoUpdateOnSuccess?: boolean;
49
+ /**
50
+ * Reset loading state automatically after click.
51
+ * Set to false if parent exclusively manages loading state via bind:loading.
52
+ */
53
+ autoReset?: boolean;
54
+ /**
55
+ * Local click handler, invoked before the NUI event. Can be async.
56
+ * Return `false` or call `event.preventDefault()` to cancel the subsequent NUI event.
57
+ */
58
+ onclick?: (event: MouseEvent) => boolean | void | Promise<boolean | void>;
59
+ children?: import('svelte').Snippet;
60
+ }
61
+
62
+ let {
63
+ eventName = '',
64
+ payload = undefined,
65
+ client = nui,
66
+ loading = $bindable(false),
67
+ disabled = $bindable(false),
68
+ type = 'button',
69
+ variant = 'primary',
70
+ size = 'md',
71
+ onSuccess = '',
72
+ onError = '',
73
+ onSuccessPayload = {},
74
+ onErrorPayload = {},
75
+ onFailure,
76
+ onSuccessCallback,
77
+ condition = null,
78
+ disabledWhen = null,
79
+ keepLoadingOnSuccess = false,
80
+ autoUpdateOnSuccess = false,
81
+ autoReset = true,
82
+ onclick,
83
+ children
84
+ }: Props = $props();
85
+
86
+ const shouldKeepLoading = $derived(keepLoadingOnSuccess || autoUpdateOnSuccess);
87
+
88
+ const isEnabled = $derived.by(() => {
89
+ if (condition && typeof condition === 'function' && !condition()) return false;
90
+ if (disabledWhen) {
91
+ if (typeof disabledWhen === 'function') {
92
+ if (disabledWhen()) return false;
93
+ } else if (typeof disabledWhen === 'boolean') {
94
+ if (disabledWhen) return false;
95
+ }
96
+ }
97
+ return true;
98
+ });
99
+
100
+ async function dispatchClientEvent(name: string, data?: unknown): Promise<unknown> {
101
+ if ('sendAndWait' in client && typeof client.sendAndWait === 'function') {
102
+ return await client.sendAndWait(name, data);
103
+ }
104
+ return client.send(name, data);
105
+ }
106
+
107
+ async function handleClick(event: MouseEvent) {
108
+ if (!isEnabled || loading || disabled) return;
109
+
110
+ // If there is no NUI event and no custom onclick, let native action proceed
111
+ if (!eventName && !onclick) {
112
+ return;
113
+ }
114
+
115
+ loading = true;
116
+ let hasError = false;
117
+
118
+ try {
119
+ // 1. Run local onclick hook
120
+ if (onclick) {
121
+ const initialPrevented = event.defaultPrevented;
122
+ const result = await onclick(event);
123
+
124
+ // Cancel if onclick returned false or marked event as defaultPrevented
125
+ if (result === false || (!initialPrevented && event.defaultPrevented)) {
126
+ if (autoReset) {
127
+ loading = false;
128
+ }
129
+ return;
130
+ }
131
+ }
132
+
133
+ // 2. Prevent native form submission/reset if proceeding with NUI event
134
+ if (eventName && (type === 'submit' || type === 'reset')) {
135
+ event.preventDefault();
136
+ }
137
+
138
+ // 3. Send main NUI event
139
+ let operationResult: unknown = undefined;
140
+ if (eventName) {
141
+ operationResult = await dispatchClientEvent(eventName, payload);
142
+
143
+ // Handle clients or mocks returning an explicit failure envelope { success: false, error: ... }
144
+ if (
145
+ operationResult &&
146
+ typeof operationResult === 'object' &&
147
+ 'success' in operationResult &&
148
+ (operationResult as { success?: boolean }).success === false
149
+ ) {
150
+ const envelope = operationResult as { error?: { message?: string } | string };
151
+ const errMsg =
152
+ typeof envelope.error === 'object' && envelope.error !== null
153
+ ? envelope.error.message || 'NUI operation returned failure'
154
+ : typeof envelope.error === 'string'
155
+ ? envelope.error
156
+ : 'NUI operation returned failure';
157
+ throw new Error(errMsg);
158
+ }
159
+ }
160
+
161
+ // 4. Trigger local success callback
162
+ onSuccessCallback?.(operationResult);
163
+
164
+ // 5. Send success notification event (isolated so its failure does not trigger onError)
165
+ if (onSuccess) {
166
+ try {
167
+ await dispatchClientEvent(onSuccess, onSuccessPayload);
168
+ } catch (successSendError) {
169
+ console.warn(
170
+ `[NuiButton] Main operation succeeded, but failed to dispatch onSuccess event '${onSuccess}':`,
171
+ successSendError
172
+ );
173
+ }
174
+ }
175
+ } catch (error) {
176
+ hasError = true;
177
+ const message = error instanceof Error ? error.message : String(error);
178
+
179
+ // Trigger local failure callback
180
+ onFailure?.(error);
181
+
182
+ const basePayload =
183
+ typeof onErrorPayload === 'object' && onErrorPayload !== null && !Array.isArray(onErrorPayload)
184
+ ? onErrorPayload
185
+ : { payload: onErrorPayload };
186
+
187
+ if (onError) {
188
+ try {
189
+ await dispatchClientEvent(onError, { ...basePayload, error: message });
190
+ } catch (sendErr) {
191
+ console.error(`[NuiButton] Failed to dispatch onError event '${onError}':`, sendErr);
192
+ }
193
+ }
194
+
195
+ console.error(`[NuiButton] Error handling click for event '${eventName || 'local'}':`, error);
196
+ } finally {
197
+ if (autoReset) {
198
+ if (hasError || !shouldKeepLoading) {
199
+ loading = false;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ </script>
205
+
206
+ <button
207
+ {type}
208
+ class="nui-btn nui-btn--{variant} nui-btn--{size}"
209
+ disabled={!isEnabled || disabled || loading}
210
+ aria-busy={loading}
211
+ onclick={handleClick}
212
+ >
213
+ {#if loading}
214
+ <span class="nui-btn__spinner" aria-hidden="true"></span>
215
+ {/if}
216
+ {@render children?.()}
217
+ </button>
218
+
219
+ <style>
220
+ .nui-btn {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ gap: 0.5rem;
225
+ border: 1px solid transparent;
226
+ border-radius: var(--nui-radius, 0.375rem);
227
+ font-family: inherit;
228
+ font-weight: 600;
229
+ line-height: 1;
230
+ cursor: pointer;
231
+ transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
232
+ box-sizing: border-box;
233
+ }
234
+
235
+ .nui-btn:disabled {
236
+ opacity: 0.5;
237
+ cursor: not-allowed;
238
+ }
239
+
240
+ .nui-btn--xs { padding: 0.2rem 0.5rem; font-size: 0.75rem; }
241
+ .nui-btn--sm { padding: 0.25rem 0.625rem; font-size: 0.8125rem; }
242
+ .nui-btn--md { padding: 0.5rem 0.875rem; font-size: 0.9375rem; }
243
+ .nui-btn--lg { padding: 0.75rem 1.25rem; font-size: 1.0625rem; }
244
+
245
+ .nui-btn--primary {
246
+ background: var(--nui-primary, #3b82f6);
247
+ color: #fff;
248
+ }
249
+ .nui-btn--primary:hover:not(:disabled) {
250
+ background: var(--nui-primary-hover, #2563eb);
251
+ }
252
+
253
+ .nui-btn--secondary {
254
+ background: transparent;
255
+ border-color: var(--nui-border, #3f3f46);
256
+ color: var(--nui-fg, #fafafa);
257
+ }
258
+ .nui-btn--secondary:hover:not(:disabled) {
259
+ background: var(--nui-hover, rgb(255 255 255 / 0.06));
260
+ }
261
+
262
+ .nui-btn--danger {
263
+ background: var(--nui-danger, #ef4444);
264
+ color: #fff;
265
+ }
266
+ .nui-btn--danger:hover:not(:disabled) {
267
+ background: var(--nui-danger-hover, #dc2626);
268
+ }
269
+
270
+ .nui-btn--link {
271
+ background: transparent;
272
+ color: var(--nui-primary, #3b82f6);
273
+ padding-left: 0;
274
+ padding-right: 0;
275
+ }
276
+ .nui-btn--link:hover:not(:disabled) {
277
+ text-decoration: underline;
278
+ }
279
+
280
+ .nui-btn__spinner {
281
+ width: 0.85em;
282
+ height: 0.85em;
283
+ border: 2px solid currentColor;
284
+ border-right-color: transparent;
285
+ border-radius: 50%;
286
+ animation: nui-btn-spin 0.6s linear infinite;
287
+ }
288
+
289
+ @keyframes nui-btn-spin {
290
+ to { transform: rotate(360deg); }
291
+ }
292
+ </style>
@@ -0,0 +1,277 @@
1
+ <!--
2
+ @component NuiBadge
3
+ Status badge and notification counter component.
4
+ -->
5
+ <script lang="ts">
6
+ interface Props {
7
+ /** Text or snippet content rendered inside the badge. */
8
+ children?: import("svelte").Snippet;
9
+ /**
10
+ * Numerical counter value (e.g. notification count).
11
+ * Negative, non-numeric, or infinite values are normalized to 0.
12
+ * @defaultValue 0
13
+ */
14
+ count?: number;
15
+ /**
16
+ * Whether to display the counter when count is 0.
17
+ * When false (default), a count of 0 falls back to children or a dot indicator.
18
+ * @defaultValue false
19
+ */
20
+ showZero?: boolean;
21
+ /**
22
+ * Maximum displayed counter value (e.g. 99 displays as "99+").
23
+ * @defaultValue 99
24
+ */
25
+ maxCount?: number;
26
+ /**
27
+ * Color variant.
28
+ * @defaultValue 'primary'
29
+ */
30
+ variant?:
31
+ | "primary"
32
+ | "secondary"
33
+ | "success"
34
+ | "warning"
35
+ | "danger"
36
+ | "info";
37
+ /**
38
+ * Size of the badge.
39
+ * @defaultValue 'md'
40
+ */
41
+ size?: "xs" | "sm" | "md" | "lg";
42
+ /**
43
+ * Outline style with transparent background and colored border.
44
+ * @defaultValue false
45
+ */
46
+ outline?: boolean;
47
+ /**
48
+ * Pill-shaped rounded borders.
49
+ * @defaultValue true
50
+ */
51
+ rounded?: boolean;
52
+ /**
53
+ * Visibility of the badge.
54
+ * @defaultValue true
55
+ */
56
+ visible?: boolean;
57
+ /**
58
+ * Whether the badge is interactive (automatically true when onclick is provided).
59
+ * @defaultValue false
60
+ */
61
+ clickable?: boolean;
62
+ /**
63
+ * Whether the clickable badge is disabled.
64
+ * @defaultValue false
65
+ */
66
+ disabled?: boolean;
67
+ /**
68
+ * Accessible description for screen readers.
69
+ * Recommended for interactive badges or badges displaying only symbols/numbers.
70
+ */
71
+ ariaLabel?: string;
72
+ /**
73
+ * Optional role (defaults to "status" when ariaLabel is provided on a non-interactive <span>).
74
+ */
75
+ role?: string;
76
+ /** Click event handler. */
77
+ onclick?: (event: MouseEvent) => void;
78
+ }
79
+
80
+ let {
81
+ children,
82
+ count = 0,
83
+ showZero = false,
84
+ maxCount = 99,
85
+ variant = "primary",
86
+ size = "md",
87
+ outline = false,
88
+ rounded = true,
89
+ visible = true,
90
+ clickable = false,
91
+ disabled = false,
92
+ ariaLabel,
93
+ role,
94
+ onclick,
95
+ }: Props = $props();
96
+
97
+ const normalizedCount = $derived(
98
+ Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0,
99
+ );
100
+ const normalizedMaxCount = $derived(
101
+ Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 99,
102
+ );
103
+ const showCount = $derived(
104
+ normalizedCount > 0 || (showZero && count !== undefined),
105
+ );
106
+ const displayCount = $derived(
107
+ normalizedCount > normalizedMaxCount
108
+ ? `${normalizedMaxCount}+`
109
+ : normalizedCount,
110
+ );
111
+ const isClickable = $derived(clickable || typeof onclick === "function");
112
+ const resolvedRole = $derived(
113
+ role ?? (ariaLabel && !isClickable ? "status" : undefined),
114
+ );
115
+ </script>
116
+
117
+ {#if visible}
118
+ {#if isClickable}
119
+ <button
120
+ type="button"
121
+ class="nui-badge nui-badge--{variant} nui-badge--{size} nui-badge--clickable"
122
+ class:nui-badge--outline={outline}
123
+ class:nui-badge--rounded={rounded}
124
+ class:nui-badge--square={!rounded}
125
+ aria-label={ariaLabel}
126
+ {disabled}
127
+ {onclick}
128
+ >
129
+ {#if showCount}
130
+ {displayCount}
131
+ {:else if children}
132
+ {@render children()}
133
+ {:else}
134
+ <span aria-hidden="true">●</span>
135
+ {/if}
136
+ </button>
137
+ {:else}
138
+ <span
139
+ class="nui-badge nui-badge--{variant} nui-badge--{size}"
140
+ class:nui-badge--outline={outline}
141
+ class:nui-badge--rounded={rounded}
142
+ class:nui-badge--square={!rounded}
143
+ aria-label={ariaLabel}
144
+ role={resolvedRole}
145
+ >
146
+ {#if showCount}
147
+ {displayCount}
148
+ {:else if children}
149
+ {@render children()}
150
+ {:else}
151
+ <span aria-hidden="true">●</span>
152
+ {/if}
153
+ </span>
154
+ {/if}
155
+ {/if}
156
+
157
+ <style>
158
+ .nui-badge {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ font: inherit;
163
+ font-weight: 600;
164
+ line-height: 1;
165
+ text-align: center;
166
+ border: 1px solid transparent;
167
+ transition:
168
+ opacity 0.15s ease,
169
+ background 0.15s ease,
170
+ border-color 0.15s ease;
171
+ box-sizing: border-box;
172
+ vertical-align: middle;
173
+ }
174
+
175
+ button.nui-badge {
176
+ appearance: none;
177
+ -webkit-appearance: none;
178
+ color: inherit;
179
+ padding: 0;
180
+ cursor: pointer;
181
+ }
182
+
183
+ button.nui-badge:focus-visible {
184
+ outline: 2px solid currentColor;
185
+ outline-offset: 2px;
186
+ }
187
+
188
+ .nui-badge--clickable {
189
+ cursor: pointer;
190
+ }
191
+
192
+ .nui-badge--clickable:hover:not(:disabled) {
193
+ opacity: 0.85;
194
+ }
195
+
196
+ .nui-badge--clickable:disabled {
197
+ opacity: 0.5;
198
+ cursor: not-allowed;
199
+ }
200
+
201
+ .nui-badge--primary {
202
+ background: var(--nui-primary, #3b82f6);
203
+ color: #fff;
204
+ }
205
+ .nui-badge--secondary {
206
+ background: var(--nui-surface-alt, #3f3f46);
207
+ color: var(--nui-fg, #fafafa);
208
+ }
209
+ .nui-badge--success {
210
+ background: var(--nui-success, #22c55e);
211
+ color: #fff;
212
+ }
213
+ .nui-badge--warning {
214
+ background: var(--nui-warning, #f59e0b);
215
+ color: #18181b;
216
+ }
217
+ .nui-badge--danger {
218
+ background: var(--nui-danger, #ef4444);
219
+ color: #fff;
220
+ }
221
+ .nui-badge--info {
222
+ background: var(--nui-info, #06b6d4);
223
+ color: #fff;
224
+ }
225
+
226
+ .nui-badge.nui-badge--outline {
227
+ background: transparent;
228
+ }
229
+ .nui-badge.nui-badge--outline.nui-badge--primary {
230
+ border-color: var(--nui-primary, #3b82f6);
231
+ color: var(--nui-primary, #3b82f6);
232
+ }
233
+ .nui-badge.nui-badge--outline.nui-badge--secondary {
234
+ border-color: var(--nui-border, #3f3f46);
235
+ color: var(--nui-fg-secondary, #8b8b99);
236
+ }
237
+ .nui-badge.nui-badge--outline.nui-badge--success {
238
+ border-color: var(--nui-success, #22c55e);
239
+ color: var(--nui-success, #22c55e);
240
+ }
241
+ .nui-badge.nui-badge--outline.nui-badge--warning {
242
+ border-color: var(--nui-warning, #f59e0b);
243
+ color: var(--nui-warning, #f59e0b);
244
+ }
245
+ .nui-badge.nui-badge--outline.nui-badge--danger {
246
+ border-color: var(--nui-danger, #ef4444);
247
+ color: var(--nui-danger, #ef4444);
248
+ }
249
+ .nui-badge.nui-badge--outline.nui-badge--info {
250
+ border-color: var(--nui-info, #06b6d4);
251
+ color: var(--nui-info, #06b6d4);
252
+ }
253
+
254
+ .nui-badge--xs {
255
+ padding: 0.125rem 0.35rem;
256
+ font-size: 0.625rem;
257
+ }
258
+ .nui-badge--sm {
259
+ padding: 0.2rem 0.5rem;
260
+ font-size: 0.75rem;
261
+ }
262
+ .nui-badge--md {
263
+ padding: 0.3rem 0.65rem;
264
+ font-size: 0.8125rem;
265
+ }
266
+ .nui-badge--lg {
267
+ padding: 0.4rem 0.85rem;
268
+ font-size: 0.9375rem;
269
+ }
270
+
271
+ .nui-badge--rounded {
272
+ border-radius: 9999px;
273
+ }
274
+ .nui-badge--square {
275
+ border-radius: var(--nui-radius, 0.25rem);
276
+ }
277
+ </style>