@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,384 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NuiChart
|
|
3
|
+
Canvas chart component (bar, line, pie, doughnut) for FiveM NUI.
|
|
4
|
+
-->
|
|
5
|
+
<script module lang="ts">
|
|
6
|
+
export type {
|
|
7
|
+
ChartData,
|
|
8
|
+
ChartDataset,
|
|
9
|
+
ChartOptions,
|
|
10
|
+
ChartType,
|
|
11
|
+
} from "./types";
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { onMount } from "svelte";
|
|
16
|
+
import { nui } from "@fivem-nui/svelte";
|
|
17
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
18
|
+
import type {
|
|
19
|
+
ChartData,
|
|
20
|
+
ChartDataset,
|
|
21
|
+
ChartOptions,
|
|
22
|
+
ChartType,
|
|
23
|
+
} from "./types";
|
|
24
|
+
import { getColor } from "./color";
|
|
25
|
+
import { validateChartData } from "./validator";
|
|
26
|
+
import { renderChart } from "./registry";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Props for {@link NuiChart} component.
|
|
30
|
+
*/
|
|
31
|
+
interface Props {
|
|
32
|
+
/**
|
|
33
|
+
* Chart type determining the rendering strategy.
|
|
34
|
+
* Supported built-in types: `'bar'`, `'line'`, `'pie'`, `'doughnut'` or custom types.
|
|
35
|
+
*/
|
|
36
|
+
type: ChartType;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Source chart data passed from parent.
|
|
40
|
+
*/
|
|
41
|
+
data: ChartData;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Container width in pixels or CSS units.
|
|
45
|
+
* @defaultValue '100%'
|
|
46
|
+
*/
|
|
47
|
+
width?: number | string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Canvas height in pixels or CSS units.
|
|
51
|
+
* @defaultValue '300px'
|
|
52
|
+
*/
|
|
53
|
+
height?: number | string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional configuration for grid, Y-scale, legend position, and accessibility.
|
|
57
|
+
* @defaultValue {}
|
|
58
|
+
*/
|
|
59
|
+
options?: ChartOptions;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional NUI event name (from game / C#).
|
|
63
|
+
* When provided, automatically listens for chart updates from NUI.
|
|
64
|
+
*/
|
|
65
|
+
eventName?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SvelteNuiClient instance to listen for game events.
|
|
69
|
+
* @defaultValue singleton `nui` from `@fivem-nui/svelte`
|
|
70
|
+
*/
|
|
71
|
+
client?: SvelteNuiClient;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let {
|
|
75
|
+
type,
|
|
76
|
+
data,
|
|
77
|
+
width = "100%",
|
|
78
|
+
height = "300px",
|
|
79
|
+
options = {},
|
|
80
|
+
eventName,
|
|
81
|
+
client = nui,
|
|
82
|
+
}: Props = $props();
|
|
83
|
+
|
|
84
|
+
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
85
|
+
let containerEl = $state<HTMLDivElement | null>(null);
|
|
86
|
+
let rafId: number | null = null;
|
|
87
|
+
|
|
88
|
+
// Data sources: separation of parent props, NUI events, and local visibility state
|
|
89
|
+
let remoteData = $state<ChartData | null>(null);
|
|
90
|
+
let hiddenIndices = $state<Record<number, boolean>>({});
|
|
91
|
+
let lastPropsData: ChartData | null = null;
|
|
92
|
+
|
|
93
|
+
// New parent props reset remote overrides and local hidden states
|
|
94
|
+
$effect(() => {
|
|
95
|
+
if (data !== lastPropsData) {
|
|
96
|
+
lastPropsData = data;
|
|
97
|
+
remoteData = null;
|
|
98
|
+
hiddenIndices = {};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Listen for NUI updates with runtime validation
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (!eventName || !client) return;
|
|
105
|
+
const unsub = client.on(eventName, (payload: unknown) => {
|
|
106
|
+
const validation = validateChartData(payload);
|
|
107
|
+
if (validation.valid && validation.data) {
|
|
108
|
+
remoteData = validation.data;
|
|
109
|
+
hiddenIndices = {};
|
|
110
|
+
} else {
|
|
111
|
+
console.warn(
|
|
112
|
+
`[NuiChart] Rejected invalid payload from event '${eventName}':`,
|
|
113
|
+
validation.error,
|
|
114
|
+
payload,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return () => unsub();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Effective data combining remote overrides and local visibility toggles
|
|
122
|
+
const activeRawData = $derived(remoteData ?? data);
|
|
123
|
+
const currentData = $derived<ChartData>({
|
|
124
|
+
labels: activeRawData?.labels ?? [],
|
|
125
|
+
datasets: (activeRawData?.datasets ?? []).map((ds, idx) => ({
|
|
126
|
+
...ds,
|
|
127
|
+
hidden: hiddenIndices[idx] ?? ds.hidden ?? false,
|
|
128
|
+
})),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Common pie dataset used in renderer and legend
|
|
132
|
+
const activePieDataset = $derived(
|
|
133
|
+
currentData.datasets.find((d) => !d.hidden) ?? currentData.datasets[0],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Schedules a render on the next animation frame.
|
|
138
|
+
*/
|
|
139
|
+
function scheduleRender() {
|
|
140
|
+
if (typeof window === "undefined") return;
|
|
141
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
142
|
+
rafId = requestAnimationFrame(() => {
|
|
143
|
+
rafId = null;
|
|
144
|
+
if (!canvasEl) return;
|
|
145
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
146
|
+
const w = rect.width || (typeof width === "number" ? width : 400);
|
|
147
|
+
const h = rect.height || (typeof height === "number" ? height : 300);
|
|
148
|
+
renderChart(canvasEl, w, h, type, currentData, options);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
$effect(() => {
|
|
153
|
+
void type;
|
|
154
|
+
void currentData;
|
|
155
|
+
void options;
|
|
156
|
+
void width;
|
|
157
|
+
void height;
|
|
158
|
+
scheduleRender();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
onMount(() => {
|
|
162
|
+
scheduleRender();
|
|
163
|
+
|
|
164
|
+
if (containerEl && typeof ResizeObserver !== "undefined") {
|
|
165
|
+
const ro = new ResizeObserver((entries) => {
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
|
|
168
|
+
scheduleRender();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
ro.observe(containerEl);
|
|
173
|
+
return () => {
|
|
174
|
+
ro.disconnect();
|
|
175
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return () => {
|
|
180
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const legendPos = $derived(
|
|
185
|
+
options.legend?.position ?? options.plugins?.legend?.position ?? "bottom",
|
|
186
|
+
);
|
|
187
|
+
const showLegend = $derived(legendPos !== "none");
|
|
188
|
+
|
|
189
|
+
/** Accessible description for screen readers. */
|
|
190
|
+
const resolvedAriaLabel = $derived.by(() => {
|
|
191
|
+
if (options.ariaLabel) return options.ariaLabel;
|
|
192
|
+
const labels = currentData.labels || [];
|
|
193
|
+
const labelsSummary =
|
|
194
|
+
labels.length > 5
|
|
195
|
+
? `${labels.slice(0, 5).join(", ")} and ${labels.length - 5} more`
|
|
196
|
+
: labels.join(", ");
|
|
197
|
+
return `Chart of type ${type}.${labels.length ? ` Categories: ${labelsSummary}` : ""}`;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Toggles dataset visibility locally without mutating source data.
|
|
202
|
+
*/
|
|
203
|
+
function toggleDataset(index: number) {
|
|
204
|
+
const isCurrentlyHidden = currentData.datasets[index]?.hidden ?? false;
|
|
205
|
+
hiddenIndices[index] = !isCurrentlyHidden;
|
|
206
|
+
}
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<div
|
|
210
|
+
class="nui-chart-container"
|
|
211
|
+
bind:this={containerEl}
|
|
212
|
+
style="width: {typeof width === 'number' ? `${width}px` : width};"
|
|
213
|
+
>
|
|
214
|
+
{#if showLegend && legendPos === "top"}
|
|
215
|
+
<div class="nui-chart__legend nui-chart__legend--top">
|
|
216
|
+
{#if type === "pie" || type === "doughnut"}
|
|
217
|
+
{#if activePieDataset && !activePieDataset.hidden}
|
|
218
|
+
{#each currentData.labels || [] as label, idx}
|
|
219
|
+
{#if (activePieDataset.data[idx] ?? 0) > 0}
|
|
220
|
+
<div class="nui-chart__legend-item">
|
|
221
|
+
<span
|
|
222
|
+
class="nui-chart__legend-color"
|
|
223
|
+
style="background-color: {getColor(
|
|
224
|
+
activePieDataset.backgroundColor,
|
|
225
|
+
idx,
|
|
226
|
+
)}"
|
|
227
|
+
></span>
|
|
228
|
+
<span class="nui-chart__legend-label">{label}</span>
|
|
229
|
+
</div>
|
|
230
|
+
{/if}
|
|
231
|
+
{/each}
|
|
232
|
+
{/if}
|
|
233
|
+
{:else}
|
|
234
|
+
{#each currentData.datasets as ds, idx}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
class="nui-chart__legend-btn"
|
|
238
|
+
class:nui-chart__legend-btn--hidden={ds.hidden}
|
|
239
|
+
aria-pressed={!ds.hidden}
|
|
240
|
+
aria-label="{ds.hidden ? 'Show' : 'Hide'} series: {ds.label}"
|
|
241
|
+
onclick={() => toggleDataset(idx)}
|
|
242
|
+
>
|
|
243
|
+
<span
|
|
244
|
+
class="nui-chart__legend-color"
|
|
245
|
+
style="background-color: {getColor(
|
|
246
|
+
ds.borderColor || ds.backgroundColor,
|
|
247
|
+
idx,
|
|
248
|
+
)}"
|
|
249
|
+
></span>
|
|
250
|
+
<span class="nui-chart__legend-label">{ds.label}</span>
|
|
251
|
+
</button>
|
|
252
|
+
{/each}
|
|
253
|
+
{/if}
|
|
254
|
+
</div>
|
|
255
|
+
{/if}
|
|
256
|
+
|
|
257
|
+
<canvas
|
|
258
|
+
bind:this={canvasEl}
|
|
259
|
+
class="nui-chart"
|
|
260
|
+
role="img"
|
|
261
|
+
aria-label={resolvedAriaLabel}
|
|
262
|
+
style="width: 100%; height: {typeof height === 'number'
|
|
263
|
+
? `${height}px`
|
|
264
|
+
: height};"
|
|
265
|
+
>
|
|
266
|
+
<p>{resolvedAriaLabel}</p>
|
|
267
|
+
</canvas>
|
|
268
|
+
|
|
269
|
+
{#if showLegend && legendPos !== "top"}
|
|
270
|
+
<div class="nui-chart__legend nui-chart__legend--bottom">
|
|
271
|
+
{#if type === "pie" || type === "doughnut"}
|
|
272
|
+
{#if activePieDataset && !activePieDataset.hidden}
|
|
273
|
+
{#each currentData.labels || [] as label, idx}
|
|
274
|
+
{#if (activePieDataset.data[idx] ?? 0) > 0}
|
|
275
|
+
<div class="nui-chart__legend-item">
|
|
276
|
+
<span
|
|
277
|
+
class="nui-chart__legend-color"
|
|
278
|
+
style="background-color: {getColor(
|
|
279
|
+
activePieDataset.backgroundColor,
|
|
280
|
+
idx,
|
|
281
|
+
)}"
|
|
282
|
+
></span>
|
|
283
|
+
<span class="nui-chart__legend-label">{label}</span>
|
|
284
|
+
</div>
|
|
285
|
+
{/if}
|
|
286
|
+
{/each}
|
|
287
|
+
{/if}
|
|
288
|
+
{:else}
|
|
289
|
+
{#each currentData.datasets as ds, idx}
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
class="nui-chart__legend-btn"
|
|
293
|
+
class:nui-chart__legend-btn--hidden={ds.hidden}
|
|
294
|
+
aria-pressed={!ds.hidden}
|
|
295
|
+
aria-label="{ds.hidden ? 'Show' : 'Hide'} series: {ds.label}"
|
|
296
|
+
onclick={() => toggleDataset(idx)}
|
|
297
|
+
>
|
|
298
|
+
<span
|
|
299
|
+
class="nui-chart__legend-color"
|
|
300
|
+
style="background-color: {getColor(
|
|
301
|
+
ds.borderColor || ds.backgroundColor,
|
|
302
|
+
idx,
|
|
303
|
+
)}"
|
|
304
|
+
></span>
|
|
305
|
+
<span class="nui-chart__legend-label">{ds.label}</span>
|
|
306
|
+
</button>
|
|
307
|
+
{/each}
|
|
308
|
+
{/if}
|
|
309
|
+
</div>
|
|
310
|
+
{/if}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<style>
|
|
314
|
+
.nui-chart-container {
|
|
315
|
+
position: relative;
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-direction: column;
|
|
318
|
+
gap: 0.75rem;
|
|
319
|
+
font-family: inherit;
|
|
320
|
+
box-sizing: border-box;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.nui-chart {
|
|
324
|
+
display: block;
|
|
325
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
326
|
+
box-sizing: border-box;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.nui-chart__legend {
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-wrap: wrap;
|
|
332
|
+
align-items: center;
|
|
333
|
+
justify-content: center;
|
|
334
|
+
gap: 1rem;
|
|
335
|
+
padding: 0.25rem 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.nui-chart__legend-item,
|
|
339
|
+
.nui-chart__legend-btn {
|
|
340
|
+
display: inline-flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
gap: 0.375rem;
|
|
343
|
+
font-size: 0.75rem;
|
|
344
|
+
color: var(--nui-muted, #a1a1aa);
|
|
345
|
+
line-height: 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.nui-chart__legend-btn {
|
|
349
|
+
background: transparent;
|
|
350
|
+
border: none;
|
|
351
|
+
padding: 0.125rem 0.25rem;
|
|
352
|
+
border-radius: 0.25rem;
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
font: inherit;
|
|
355
|
+
transition:
|
|
356
|
+
opacity 0.15s ease,
|
|
357
|
+
text-decoration 0.15s ease;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.nui-chart__legend-btn:hover {
|
|
361
|
+
opacity: 0.8;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.nui-chart__legend-btn:focus-visible {
|
|
365
|
+
outline: 2px solid var(--nui-primary, #6366f1);
|
|
366
|
+
outline-offset: 2px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.nui-chart__legend-btn--hidden {
|
|
370
|
+
opacity: 0.4;
|
|
371
|
+
text-decoration: line-through;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.nui-chart__legend-color {
|
|
375
|
+
width: 0.75rem;
|
|
376
|
+
height: 0.75rem;
|
|
377
|
+
border-radius: 0.125rem;
|
|
378
|
+
flex-shrink: 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.nui-chart__legend-label {
|
|
382
|
+
line-height: 1;
|
|
383
|
+
}
|
|
384
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type { ChartData, ChartDataset, ChartOptions, ChartType, } from "./types";
|
|
2
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
3
|
+
import type { ChartData, ChartOptions, ChartType } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Props for {@link NuiChart} component.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
/**
|
|
9
|
+
* Chart type determining the rendering strategy.
|
|
10
|
+
* Supported built-in types: `'bar'`, `'line'`, `'pie'`, `'doughnut'` or custom types.
|
|
11
|
+
*/
|
|
12
|
+
type: ChartType;
|
|
13
|
+
/**
|
|
14
|
+
* Source chart data passed from parent.
|
|
15
|
+
*/
|
|
16
|
+
data: ChartData;
|
|
17
|
+
/**
|
|
18
|
+
* Container width in pixels or CSS units.
|
|
19
|
+
* @defaultValue '100%'
|
|
20
|
+
*/
|
|
21
|
+
width?: number | string;
|
|
22
|
+
/**
|
|
23
|
+
* Canvas height in pixels or CSS units.
|
|
24
|
+
* @defaultValue '300px'
|
|
25
|
+
*/
|
|
26
|
+
height?: number | string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional configuration for grid, Y-scale, legend position, and accessibility.
|
|
29
|
+
* @defaultValue {}
|
|
30
|
+
*/
|
|
31
|
+
options?: ChartOptions;
|
|
32
|
+
/**
|
|
33
|
+
* Optional NUI event name (from game / C#).
|
|
34
|
+
* When provided, automatically listens for chart updates from NUI.
|
|
35
|
+
*/
|
|
36
|
+
eventName?: string;
|
|
37
|
+
/**
|
|
38
|
+
* SvelteNuiClient instance to listen for game events.
|
|
39
|
+
* @defaultValue singleton `nui` from `@fivem-nui/svelte`
|
|
40
|
+
*/
|
|
41
|
+
client?: SvelteNuiClient;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* NuiChart
|
|
45
|
+
* Canvas chart component (bar, line, pie, doughnut) for FiveM NUI.
|
|
46
|
+
*/
|
|
47
|
+
declare const NuiChart: import("svelte").Component<Props, {}, "">;
|
|
48
|
+
type NuiChart = ReturnType<typeof NuiChart>;
|
|
49
|
+
export default NuiChart;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_COLORS: string[];
|
|
2
|
+
/**
|
|
3
|
+
* Returns valid color string from dataset property or fallback palette.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getColor(item: string | string[] | undefined, index: number): string;
|
|
6
|
+
/**
|
|
7
|
+
* Converts color (hex, rgb, hsl, named) to rgba(r, g, b, alpha).
|
|
8
|
+
*/
|
|
9
|
+
export declare function colorToRgba(color: string, alpha: number): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const DEFAULT_COLORS = [
|
|
2
|
+
'#6366f1', // indigo
|
|
3
|
+
'#06b6d4', // cyan
|
|
4
|
+
'#10b981', // emerald
|
|
5
|
+
'#f59e0b', // amber
|
|
6
|
+
'#ef4444', // red
|
|
7
|
+
'#8b5cf6', // purple
|
|
8
|
+
'#ec4899', // pink
|
|
9
|
+
];
|
|
10
|
+
let helperCtx = null;
|
|
11
|
+
function getHelperCtx() {
|
|
12
|
+
if (typeof document === 'undefined')
|
|
13
|
+
return null;
|
|
14
|
+
if (!helperCtx) {
|
|
15
|
+
const canvas = document.createElement('canvas');
|
|
16
|
+
canvas.width = 1;
|
|
17
|
+
canvas.height = 1;
|
|
18
|
+
helperCtx = canvas.getContext('2d', { willReadFrequently: true });
|
|
19
|
+
}
|
|
20
|
+
return helperCtx;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns valid color string from dataset property or fallback palette.
|
|
24
|
+
*/
|
|
25
|
+
export function getColor(item, index) {
|
|
26
|
+
const safeIdx = Number.isFinite(index) ? Math.floor(Math.abs(index)) : 0;
|
|
27
|
+
if (Array.isArray(item) && item.length > 0) {
|
|
28
|
+
const color = item[safeIdx % item.length];
|
|
29
|
+
if (typeof color === 'string' && color.trim()) {
|
|
30
|
+
return color.trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (typeof item === 'string' && item.trim()) {
|
|
34
|
+
return item.trim();
|
|
35
|
+
}
|
|
36
|
+
return DEFAULT_COLORS[safeIdx % DEFAULT_COLORS.length];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Converts color (hex, rgb, hsl, named) to rgba(r, g, b, alpha).
|
|
40
|
+
*/
|
|
41
|
+
export function colorToRgba(color, alpha) {
|
|
42
|
+
const a = Math.max(0, Math.min(1, Number.isFinite(alpha) ? alpha : 1));
|
|
43
|
+
if (!color || typeof color !== 'string')
|
|
44
|
+
return `rgba(99, 102, 241, ${a})`;
|
|
45
|
+
const trimmed = color.trim();
|
|
46
|
+
// Fast path for hex formats: #RGB or #RRGGBB
|
|
47
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(trimmed)) {
|
|
48
|
+
let hex = trimmed.slice(1);
|
|
49
|
+
if (hex.length === 3) {
|
|
50
|
+
hex = hex.split('').map((c) => c + c).join('');
|
|
51
|
+
}
|
|
52
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
53
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
54
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
55
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
56
|
+
}
|
|
57
|
+
// Fast path for rgb(...) or rgba(...)
|
|
58
|
+
const rgbMatch = trimmed.match(/^rgba?\(\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*[, ]\s*(\d{1,3})/i);
|
|
59
|
+
if (rgbMatch) {
|
|
60
|
+
return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${a})`;
|
|
61
|
+
}
|
|
62
|
+
// Canvas parser for named colors, hsl, etc.
|
|
63
|
+
const ctx = getHelperCtx();
|
|
64
|
+
if (ctx) {
|
|
65
|
+
try {
|
|
66
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
67
|
+
ctx.fillStyle = '#000000';
|
|
68
|
+
ctx.fillStyle = trimmed;
|
|
69
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
70
|
+
const pixel = ctx.getImageData(0, 0, 1, 1).data;
|
|
71
|
+
return `rgba(${pixel[0]}, ${pixel[1]}, ${pixel[2]}, ${a})`;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Fallback on environment error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return `rgba(99, 102, 241, ${a})`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as NuiChart } from './NuiChart.svelte';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './color';
|
|
4
|
+
export * from './validator';
|
|
5
|
+
export * from './scale';
|
|
6
|
+
export * from './registry';
|
|
7
|
+
export * from './renderers/base';
|
|
8
|
+
export * from './renderers/bar';
|
|
9
|
+
export * from './renderers/line';
|
|
10
|
+
export * from './renderers/pie';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as NuiChart } from './NuiChart.svelte';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './color';
|
|
4
|
+
export * from './validator';
|
|
5
|
+
export * from './scale';
|
|
6
|
+
export * from './registry';
|
|
7
|
+
export * from './renderers/base';
|
|
8
|
+
export * from './renderers/bar';
|
|
9
|
+
export * from './renderers/line';
|
|
10
|
+
export * from './renderers/pie';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChartData, ChartOptions, ChartType } from './types';
|
|
2
|
+
import type { ChartRenderer } from './renderers/base';
|
|
3
|
+
/**
|
|
4
|
+
* Registers or overrides a chart renderer.
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerChartRenderer(type: string, renderer: ChartRenderer): void;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the registered renderer for given chart type.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getChartRenderer(type: ChartType | string): ChartRenderer | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Renders chart on the given canvas element with hidpi scaling.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderChart(canvasEl: HTMLCanvasElement, w: number, h: number, type: ChartType | string, currentData: ChartData | undefined, options?: ChartOptions): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { drawMessage } from './renderers/base';
|
|
2
|
+
import { BarChartRenderer } from './renderers/bar';
|
|
3
|
+
import { LineChartRenderer } from './renderers/line';
|
|
4
|
+
import { PieChartRenderer } from './renderers/pie';
|
|
5
|
+
const renderers = new Map([
|
|
6
|
+
['bar', new BarChartRenderer()],
|
|
7
|
+
['line', new LineChartRenderer()],
|
|
8
|
+
['pie', new PieChartRenderer(false)],
|
|
9
|
+
['doughnut', new PieChartRenderer(true)],
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Registers or overrides a chart renderer.
|
|
13
|
+
*/
|
|
14
|
+
export function registerChartRenderer(type, renderer) {
|
|
15
|
+
renderers.set(type, renderer);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns the registered renderer for given chart type.
|
|
19
|
+
*/
|
|
20
|
+
export function getChartRenderer(type) {
|
|
21
|
+
return renderers.get(type);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Renders chart on the given canvas element with hidpi scaling.
|
|
25
|
+
*/
|
|
26
|
+
export function renderChart(canvasEl, w, h, type, currentData, options = {}) {
|
|
27
|
+
const ctx = canvasEl.getContext('2d');
|
|
28
|
+
if (!ctx)
|
|
29
|
+
return;
|
|
30
|
+
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
|
|
31
|
+
canvasEl.width = Math.floor(w * dpr);
|
|
32
|
+
canvasEl.height = Math.floor(h * dpr);
|
|
33
|
+
ctx.resetTransform?.();
|
|
34
|
+
ctx.scale(dpr, dpr);
|
|
35
|
+
ctx.clearRect(0, 0, w, h);
|
|
36
|
+
if (!currentData || !currentData.datasets || currentData.datasets.length === 0) {
|
|
37
|
+
drawMessage(ctx, w, h, 'No data to display');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const renderer = getChartRenderer(type);
|
|
41
|
+
if (!renderer) {
|
|
42
|
+
drawMessage(ctx, w, h, `Unsupported chart type: ${type}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const context = {
|
|
46
|
+
canvas: canvasEl,
|
|
47
|
+
ctx,
|
|
48
|
+
width: w,
|
|
49
|
+
height: h,
|
|
50
|
+
dpr,
|
|
51
|
+
};
|
|
52
|
+
renderer.render(context, currentData, options);
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChartData, ChartOptions } from '../types';
|
|
2
|
+
import type { ChartRenderer, RenderContext } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* Renderer for bar and column charts.
|
|
5
|
+
*/
|
|
6
|
+
export declare class BarChartRenderer implements ChartRenderer {
|
|
7
|
+
render(context: RenderContext, data: ChartData, options: ChartOptions): void;
|
|
8
|
+
}
|