dirk-cfx-react 1.1.89 → 1.1.91

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.
@@ -2,9 +2,10 @@ import { Flex, Text, Image, TextInput, Select, Box, useMantineTheme, Tooltip, al
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
  import { createContext, memo, useRef, useEffect, useContext, useState, useCallback, useMemo } from 'react';
4
4
  import { create, useStore, createStore } from 'zustand';
5
+ import { z } from 'zod';
5
6
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6
7
  import { motion, AnimatePresence, useMotionValue } from 'framer-motion';
7
- import { Info, X, AlertTriangle, Trash2, RefreshCw, ChevronDown, Check, Copy, MapPin, Crosshair, EyeOff, Eye, RotateCcw, FlaskConical, ChevronUp, Palette, DoorOpen, Plus, Minus, ArrowLeft, Undo2, Redo2, Save, History, XCircle, Code2, Search, Filter, User } from 'lucide-react';
8
+ import { Info, X, AlertTriangle, Trash2, RefreshCw, ChevronDown, Check, Copy, MapPin, Crosshair, EyeOff, Eye, RotateCcw, FlaskConical, ChevronUp, Palette, ShieldCheck, Users, UserRound, Plus, DoorOpen, Minus, ArrowLeft, Undo2, Redo2, Save, History, XCircle, Code2, Search, Filter, User } from 'lucide-react';
8
9
  import clickSoundUrl from '../click_sound-PNCRRTM4.mp3';
9
10
  import hoverSoundUrl from '../hover_sound-NBUA222C.mp3';
10
11
  import { notifications } from '@mantine/notifications';
@@ -1282,6 +1283,32 @@ function colorWithAlpha(color, alpha17) {
1282
1283
 
1283
1284
  // src/utils/misc.ts
1284
1285
  var isEnvBrowser = () => !window.invokeNative;
1286
+ var DEFAULT_PALETTE = [
1287
+ "#f0f4ff",
1288
+ "#d9e3ff",
1289
+ "#bfcfff",
1290
+ "#a6bbff",
1291
+ "#8ca7ff",
1292
+ "#7393ff",
1293
+ "#5a7fff",
1294
+ "#406bff",
1295
+ "#2547ff",
1296
+ "#0b33ff"
1297
+ ];
1298
+ z.object({
1299
+ useOverride: z.boolean(),
1300
+ primaryColor: z.string(),
1301
+ primaryShade: z.number(),
1302
+ customTheme: z.array(z.string())
1303
+ });
1304
+ var defaultThemeOverride = {
1305
+ useOverride: false,
1306
+ primaryColor: "dirk",
1307
+ primaryShade: 5,
1308
+ customTheme: DEFAULT_PALETTE
1309
+ };
1310
+
1311
+ // src/utils/useSettings.ts
1285
1312
  var useSettings = create(() => ({
1286
1313
  currency: "$",
1287
1314
  game: "fivem",
@@ -1289,18 +1316,7 @@ var useSettings = create(() => ({
1289
1316
  primaryShade: 9,
1290
1317
  itemImgPath: "",
1291
1318
  resourceVersion: "dev",
1292
- customTheme: [
1293
- "#f0f4ff",
1294
- "#d9e3ff",
1295
- "#bfcfff",
1296
- "#a6bbff",
1297
- "#8ca7ff",
1298
- "#7393ff",
1299
- "#5a7fff",
1300
- "#406bff",
1301
- "#2547ff",
1302
- "#0b33ff"
1303
- ]
1319
+ customTheme: DEFAULT_PALETTE
1304
1320
  }));
1305
1321
 
1306
1322
  // src/utils/fetchNui.ts
@@ -1343,11 +1359,35 @@ function reportMissingLocale(key) {
1343
1359
  fetchNui("REPORT_MISSING_LOCALE", { key }).catch(() => {
1344
1360
  });
1345
1361
  }
1362
+ var PACKAGE_DEFAULTS = {
1363
+ "OccupantsDesc": "Here you can view and manage the occupants of your traphouse. These occupants can be used mainly for selling drugs to the NPCs surrounding your traphouse. However they have other uses to so be careful who you add as an occupant.",
1364
+ "cfgpanel_discard": "Discard",
1365
+ "cfgpanel_manual_edit": "Manual Edit",
1366
+ "cfgpanel_reset_defaults": "Reset Defaults",
1367
+ "cfgpanel_version": "Version",
1368
+ "cfgpanel_back_title": "Back to script list",
1369
+ "cfgpanel_undo": "Undo",
1370
+ "cfgpanel_redo": "Redo",
1371
+ "cfgpanel_save": "Save",
1372
+ "cfgpanel_saving": "Saving...",
1373
+ "cfgpanel_history": "History",
1374
+ "cfgpanel_reset_title": "Reset to Defaults",
1375
+ "cfgpanel_reset_desc": "This will permanently reset ALL config back to the defaults. Every value you have configured will be overwritten. This cannot be undone.",
1376
+ "cfgpanel_reset_confirm": "Reset Config",
1377
+ "cfgpanel_discard_title": "Discard Unsaved Changes?",
1378
+ "cfgpanel_discard_desc_back": "You have unsaved changes. Going back now will discard them.",
1379
+ "cfgpanel_discard_desc_close": "You have unsaved changes. Closing now will discard them.",
1380
+ "cfgpanel_discard_confirm_back": "Go Back Without Saving",
1381
+ "cfgpanel_discard_confirm_close": "Close Without Saving",
1382
+ "cfgpanel_json_title": "Config JSON",
1383
+ "cfgpanel_cancel": "Cancel",
1384
+ "cfgpanel_apply": "Apply",
1385
+ "cfgpanel_history_title": "Config History",
1386
+ "cfgpanel_close": "Close"
1387
+ };
1346
1388
  var localeStore = create((set, get) => {
1347
1389
  return {
1348
- locales: {
1349
- "OccupantsDesc": "Here you can view and manage the occupants of your traphouse. These occupants can be used mainly for selling drugs to the NPCs surrounding your traphouse. However they have other uses to so be careful who you add as an occupant."
1350
- },
1390
+ locales: { ...PACKAGE_DEFAULTS },
1351
1391
  locale: (key, ...args) => {
1352
1392
  const exists = get().locales[key];
1353
1393
  if (!exists) reportMissingLocale(key);
@@ -1361,7 +1401,7 @@ var localeStore = create((set, get) => {
1361
1401
  });
1362
1402
  var locale = localeStore.getState().locale;
1363
1403
  registerInitialFetch("GET_LOCALES", void 0).then((data) => {
1364
- localeStore.setState({ locales: data });
1404
+ localeStore.setState({ locales: { ...PACKAGE_DEFAULTS, ...data } });
1365
1405
  }).catch(() => {
1366
1406
  });
1367
1407
  if (typeof window !== "undefined") {
@@ -1370,7 +1410,7 @@ if (typeof window !== "undefined") {
1370
1410
  if (!msg || msg.action !== "UPDATE_DIRK_LIB_LOCALES") return;
1371
1411
  if (!msg.data || typeof msg.data !== "object") return;
1372
1412
  if (Object.keys(msg.data).length === 0) return;
1373
- localeStore.setState({ locales: msg.data });
1413
+ localeStore.setState({ locales: { ...PACKAGE_DEFAULTS, ...msg.data } });
1374
1414
  });
1375
1415
  }
1376
1416
 
@@ -3130,11 +3170,11 @@ function ConfirmModal({
3130
3170
  placeholder: confirmText,
3131
3171
  value: typed,
3132
3172
  onChange: (e) => setTyped(e.currentTarget.value),
3133
- styles: (t3) => ({
3173
+ styles: (t4) => ({
3134
3174
  input: {
3135
- backgroundColor: alpha(t3.colors.dark[7], 0.5),
3175
+ backgroundColor: alpha(t4.colors.dark[7], 0.5),
3136
3176
  border: `0.1vh solid ${alpha(
3137
- typed === confirmText ? "#ef4444" : t3.colors.dark[5],
3177
+ typed === confirmText ? "#ef4444" : t4.colors.dark[5],
3138
3178
  0.5
3139
3179
  )}`,
3140
3180
  color: "rgba(255,255,255,0.85)",
@@ -3779,7 +3819,7 @@ function useAdminState(key, initial) {
3779
3819
  useEffect(() => {
3780
3820
  const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
3781
3821
  listeners.set(key, set);
3782
- const handler = () => setTick((t3) => t3 + 1);
3822
+ const handler = () => setTick((t4) => t4 + 1);
3783
3823
  set.add(handler);
3784
3824
  return () => {
3785
3825
  set.delete(handler);
@@ -3899,7 +3939,7 @@ function ConfigJsonModal({
3899
3939
  setError(e.message);
3900
3940
  }
3901
3941
  };
3902
- return /* @__PURE__ */ jsxs(Modal, { title: "Config JSON", icon: Code2, iconColor: color, onClose, width: "60vh", maxHeight: "80vh", zIndex: 200, children: [
3942
+ return /* @__PURE__ */ jsxs(Modal, { title: locale("cfgpanel_json_title"), icon: Code2, iconColor: color, onClose, width: "60vh", maxHeight: "80vh", zIndex: 200, children: [
3903
3943
  /* @__PURE__ */ jsxs(Box, { flex: 1, p: "0.8vh", style: { overflowY: "auto" }, children: [
3904
3944
  /* @__PURE__ */ jsx(
3905
3945
  JsonInput,
@@ -3927,7 +3967,7 @@ function ConfigJsonModal({
3927
3967
  style: { background: "transparent", border: `0.1vh solid ${theme.colors.dark[6]}`, borderRadius: theme.radius.xs, padding: `${theme.spacing.xxs} ${theme.spacing.xs}`, cursor: "pointer", display: "flex", alignItems: "center", gap: theme.spacing.xxs },
3928
3968
  children: [
3929
3969
  /* @__PURE__ */ jsx(X, { color: "rgba(255,255,255,0.4)" }),
3930
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: "Cancel" })
3970
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: locale("cfgpanel_cancel") })
3931
3971
  ]
3932
3972
  }
3933
3973
  ),
@@ -3940,7 +3980,7 @@ function ConfigJsonModal({
3940
3980
  style: { background: alpha(color, 0.14), border: `0.1vh solid ${alpha(color, 0.4)}`, borderRadius: theme.radius.xs, padding: `${theme.spacing.xxs} ${theme.spacing.xs}`, cursor: "pointer", display: "flex", alignItems: "center", gap: theme.spacing.xxs },
3941
3981
  children: [
3942
3982
  /* @__PURE__ */ jsx(Check, { color }),
3943
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: color, children: "Apply" })
3983
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: color, children: locale("cfgpanel_apply") })
3944
3984
  ]
3945
3985
  }
3946
3986
  )
@@ -4081,7 +4121,7 @@ function ConfigHistoryModal({
4081
4121
  historyQuery.fetchNextPage();
4082
4122
  }
4083
4123
  };
4084
- return /* @__PURE__ */ jsxs(Modal, { title: "Config History", icon: History, iconColor: color, onClose, width: "88vh", maxHeight: "82vh", zIndex: 260, children: [
4124
+ return /* @__PURE__ */ jsxs(Modal, { title: locale("cfgpanel_history_title"), icon: History, iconColor: color, onClose, width: "88vh", maxHeight: "82vh", zIndex: 260, children: [
4085
4125
  /* @__PURE__ */ jsxs(Flex, { direction: "column", style: { flex: 1, minHeight: 0 }, children: [
4086
4126
  /* @__PURE__ */ jsxs(Flex, { gap: "xs", p: "sm", style: { borderBottom: `0.1vh solid ${alpha(theme.colors.dark[7], 0.8)}` }, children: [
4087
4127
  /* @__PURE__ */ jsx(TextInput, { leftSection: /* @__PURE__ */ jsx(Search, { size: "1.4vh" }), placeholder: "Search path/admin/value", value: queryInput, onChange: (e) => setQueryInput(e.currentTarget.value), size: "xs", style: { flex: 1 } }),
@@ -4124,7 +4164,7 @@ function ConfigHistoryModal({
4124
4164
  style: { background: "transparent", border: `0.1vh solid ${theme.colors.dark[6]}`, borderRadius: theme.radius.xs, padding: `${theme.spacing.xxs} ${theme.spacing.xs}`, cursor: "pointer", display: "flex", alignItems: "center", gap: theme.spacing.xxs },
4125
4165
  children: [
4126
4166
  /* @__PURE__ */ jsx(X, { color: "rgba(255,255,255,0.4)", size: "1.5vh" }),
4127
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: "Close" })
4167
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: locale("cfgpanel_close") })
4128
4168
  ]
4129
4169
  }
4130
4170
  ) })
@@ -4184,9 +4224,9 @@ function ConfigPanelInner({
4184
4224
  /* @__PURE__ */ jsx(AnimatePresence, { children: resetOpen && /* @__PURE__ */ jsx(
4185
4225
  ConfirmModal,
4186
4226
  {
4187
- title: "Reset to Defaults",
4188
- description: "This will permanently reset ALL config back to the defaults. Every value you have configured will be overwritten. This cannot be undone.",
4189
- confirmLabel: "Reset Config",
4227
+ title: locale("cfgpanel_reset_title"),
4228
+ description: locale("cfgpanel_reset_desc"),
4229
+ confirmLabel: locale("cfgpanel_reset_confirm"),
4190
4230
  confirmText: resetConfirmText,
4191
4231
  onConfirm: async () => {
4192
4232
  setResetOpen(false);
@@ -4218,9 +4258,9 @@ function ConfigPanelInner({
4218
4258
  /* @__PURE__ */ jsx(AnimatePresence, { children: pendingAction !== null && /* @__PURE__ */ jsx(
4219
4259
  ConfirmModal,
4220
4260
  {
4221
- title: "Discard Unsaved Changes?",
4222
- description: pendingAction === "back" ? "You have unsaved changes. Going back now will discard them." : "You have unsaved changes. Closing now will discard them.",
4223
- confirmLabel: pendingAction === "back" ? "Go Back Without Saving" : "Close Without Saving",
4261
+ title: locale("cfgpanel_discard_title"),
4262
+ description: pendingAction === "back" ? locale("cfgpanel_discard_desc_back") : locale("cfgpanel_discard_desc_close"),
4263
+ confirmLabel: pendingAction === "back" ? locale("cfgpanel_discard_confirm_back") : locale("cfgpanel_discard_confirm_close"),
4224
4264
  onConfirm: () => {
4225
4265
  const action = pendingAction;
4226
4266
  setPendingAction(null);
@@ -4258,7 +4298,7 @@ function ConfigPanelInner({
4258
4298
  /* @__PURE__ */ jsx(
4259
4299
  motion.button,
4260
4300
  {
4261
- title: "Back to script list",
4301
+ title: locale("cfgpanel_back_title"),
4262
4302
  onClick: handleBack,
4263
4303
  whileHover: { background: alpha(color, 0.16), borderColor: alpha(color, 0.45) },
4264
4304
  whileTap: { scale: 0.95 },
@@ -4286,7 +4326,7 @@ function ConfigPanelInner({
4286
4326
  /* @__PURE__ */ jsx(
4287
4327
  motion.button,
4288
4328
  {
4289
- title: "Undo",
4329
+ title: locale("cfgpanel_undo"),
4290
4330
  onClick: () => form.canBack && form.back(),
4291
4331
  disabled: !form.canBack,
4292
4332
  whileHover: form.canBack ? { background: "rgba(255,255,255,0.07)" } : void 0,
@@ -4298,7 +4338,7 @@ function ConfigPanelInner({
4298
4338
  /* @__PURE__ */ jsx(
4299
4339
  motion.button,
4300
4340
  {
4301
- title: "Redo",
4341
+ title: locale("cfgpanel_redo"),
4302
4342
  onClick: () => form.canForward && form.forward(),
4303
4343
  disabled: !form.canForward,
4304
4344
  whileHover: form.canForward ? { background: "rgba(255,255,255,0.07)" } : void 0,
@@ -4310,7 +4350,7 @@ function ConfigPanelInner({
4310
4350
  /* @__PURE__ */ jsx(
4311
4351
  motion.button,
4312
4352
  {
4313
- title: isSaving ? "Saving..." : isDirty ? `Save (${changedCount})` : "Save",
4353
+ title: isSaving ? locale("cfgpanel_saving") : isDirty ? `${locale("cfgpanel_save")} (${changedCount})` : locale("cfgpanel_save"),
4314
4354
  onClick: () => isDirty && !isSaving && form.submit(),
4315
4355
  disabled: !isDirty || isSaving,
4316
4356
  whileHover: isDirty && !isSaving ? { background: alpha(color, 0.25), borderColor: alpha(color, 0.5) } : void 0,
@@ -4322,7 +4362,7 @@ function ConfigPanelInner({
4322
4362
  /* @__PURE__ */ jsx(
4323
4363
  motion.button,
4324
4364
  {
4325
- title: "History",
4365
+ title: locale("cfgpanel_history"),
4326
4366
  onClick: () => setHistoryOpen(true),
4327
4367
  whileHover: { background: alpha("#22d3ee", 0.16), borderColor: alpha("#22d3ee", 0.5) },
4328
4368
  whileTap: { scale: 0.97 },
@@ -4343,7 +4383,7 @@ function ConfigPanelInner({
4343
4383
  style: { width: "100%", background: "transparent", border: `0.1vh solid ${isDirty ? alpha("#f97316", 0.35) : alpha(theme.colors.dark[5], 0.3)}`, borderRadius: theme.radius.xs, padding: "0.65vh 0.8vh", cursor: isDirty ? "pointer" : "default", display: "flex", alignItems: "center", gap: "0.55vh", opacity: isDirty ? 1 : 0.35, transition: "opacity 0.2s" },
4344
4384
  children: [
4345
4385
  /* @__PURE__ */ jsx(XCircle, { color: isDirty ? "#f97316" : "rgba(255,255,255,0.35)", size: "1.6vh" }),
4346
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: isDirty ? "#f97316" : "rgba(255,255,255,0.35)", children: "Discard" })
4386
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: isDirty ? "#f97316" : "rgba(255,255,255,0.35)", children: locale("cfgpanel_discard") })
4347
4387
  ]
4348
4388
  }
4349
4389
  ),
@@ -4357,7 +4397,7 @@ function ConfigPanelInner({
4357
4397
  style: { width: "100%", background: alpha(color, 0.07), border: `0.1vh solid ${alpha(color, 0.28)}`, borderRadius: theme.radius.xs, padding: "0.65vh 0.8vh", cursor: "pointer", display: "flex", alignItems: "center", gap: "0.55vh" },
4358
4398
  children: [
4359
4399
  /* @__PURE__ */ jsx(Code2, { color, size: "1.6vh" }),
4360
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: "Manual Edit" })
4400
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: locale("cfgpanel_manual_edit") })
4361
4401
  ]
4362
4402
  }
4363
4403
  ),
@@ -4370,12 +4410,13 @@ function ConfigPanelInner({
4370
4410
  style: { width: "100%", background: alpha("#ef4444", 0.07), border: `0.1vh solid ${alpha("#ef4444", 0.25)}`, borderRadius: theme.radius.xs, padding: "0.65vh 0.8vh", cursor: "pointer", display: "flex", alignItems: "center", gap: "0.55vh" },
4371
4411
  children: [
4372
4412
  /* @__PURE__ */ jsx(RotateCcw, { color: "#ef4444", size: "1.6vh" }),
4373
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "#ef4444", children: "Reset Defaults" })
4413
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "#ef4444", children: locale("cfgpanel_reset_defaults") })
4374
4414
  ]
4375
4415
  }
4376
4416
  ),
4377
4417
  version && /* @__PURE__ */ jsxs(Text, { size: "0.95vh", c: "dimmed", ta: "center", style: { letterSpacing: "0.04em", opacity: 0.8 }, children: [
4378
- "Version ",
4418
+ locale("cfgpanel_version"),
4419
+ " ",
4379
4420
  version
4380
4421
  ] })
4381
4422
  ] })
@@ -5466,24 +5507,6 @@ var MANTINE_COLOR_OPTIONS = [
5466
5507
  "yellow",
5467
5508
  "orange"
5468
5509
  ].map((value) => ({ value, label: value }));
5469
- var DEFAULT_PALETTE = [
5470
- "#f0f4ff",
5471
- "#d9e3ff",
5472
- "#bfcfff",
5473
- "#a6bbff",
5474
- "#8ca7ff",
5475
- "#7393ff",
5476
- "#5a7fff",
5477
- "#406bff",
5478
- "#2547ff",
5479
- "#0b33ff"
5480
- ];
5481
- var DEFAULT_VALUE = {
5482
- useOverride: false,
5483
- primaryColor: "dirk",
5484
- primaryShade: 5,
5485
- customTheme: DEFAULT_PALETTE
5486
- };
5487
5510
  function GroupLabel({ label }) {
5488
5511
  return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", mt: "xxs", children: [
5489
5512
  /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.07em", c: "rgba(255,255,255,0.2)", children: label }),
@@ -5498,10 +5521,10 @@ function ThemeOverrideSection({
5498
5521
  const color = mantineTheme.colors[mantineTheme.primaryColor][5];
5499
5522
  const raw = useFormField(schemaKey);
5500
5523
  const value = {
5501
- useOverride: raw?.useOverride ?? DEFAULT_VALUE.useOverride,
5502
- primaryColor: raw?.primaryColor ?? DEFAULT_VALUE.primaryColor,
5503
- primaryShade: raw?.primaryShade ?? DEFAULT_VALUE.primaryShade,
5504
- customTheme: Array.isArray(raw?.customTheme) && raw.customTheme.length === 10 ? raw.customTheme : DEFAULT_VALUE.customTheme
5524
+ useOverride: raw?.useOverride ?? defaultThemeOverride.useOverride,
5525
+ primaryColor: raw?.primaryColor ?? defaultThemeOverride.primaryColor,
5526
+ primaryShade: raw?.primaryShade ?? defaultThemeOverride.primaryShade,
5527
+ customTheme: Array.isArray(raw?.customTheme) && raw.customTheme.length === 10 ? raw.customTheme : defaultThemeOverride.customTheme
5505
5528
  };
5506
5529
  const { setValue } = useFormActions();
5507
5530
  const set = (key, val) => setValue(schemaKey, { ...value, [key]: val });
@@ -5708,6 +5731,403 @@ function SwatchTile({
5708
5731
  ) })
5709
5732
  ] });
5710
5733
  }
5734
+ function usePlayers(opts = {}) {
5735
+ const {
5736
+ includeOffline = false,
5737
+ search = "",
5738
+ limit = 50,
5739
+ staleTimeMs,
5740
+ refetchIntervalMs
5741
+ } = opts;
5742
+ const query = useQuery({
5743
+ queryKey: includeOffline ? ["dirk:players", "search", search.trim().toLowerCase(), limit] : ["dirk:players", "online"],
5744
+ queryFn: async () => {
5745
+ const toolId = includeOffline ? "searchPlayers" : "getOnlinePlayers";
5746
+ const payload = includeOffline ? { id: toolId, value: { search: search.trim(), limit } } : { id: toolId };
5747
+ const result = await fetchNui(
5748
+ "ADMIN_TOOL_QUERY",
5749
+ payload,
5750
+ // Browser-dev fallback. Returns a couple of mock players so the
5751
+ // dev shell isn't blank.
5752
+ includeOffline ? [
5753
+ { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true },
5754
+ { id: null, citizenId: "DEF67890", name: "", charName: "Jane Offline", online: false }
5755
+ ] : [
5756
+ { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true }
5757
+ ]
5758
+ );
5759
+ return Array.isArray(result) ? result : [];
5760
+ },
5761
+ staleTime: staleTimeMs ?? (includeOffline ? 3e4 : 5e3),
5762
+ gcTime: 5 * 6e4,
5763
+ refetchInterval: refetchIntervalMs ?? false,
5764
+ refetchOnWindowFocus: false,
5765
+ placeholderData: includeOffline ? keepPreviousData : void 0
5766
+ });
5767
+ return {
5768
+ players: query.data ?? [],
5769
+ isLoading: query.isLoading,
5770
+ isFetching: query.isFetching,
5771
+ error: query.error ?? null,
5772
+ refresh: () => query.refetch()
5773
+ };
5774
+ }
5775
+ var DEBOUNCE_MS = 300;
5776
+ var ONLINE_COLOR = "#3FA83F";
5777
+ var OFFLINE_COLOR = "#E54141";
5778
+ function PlayerSelect({
5779
+ value,
5780
+ onChange,
5781
+ includeOffline = false,
5782
+ limit = 50,
5783
+ searchable: searchableProp,
5784
+ placeholder,
5785
+ nothingFoundMessage: nothingFoundMessageProp,
5786
+ loadingLabel = "Loading\u2026",
5787
+ onlineLabel = "Online",
5788
+ offlineLabel = "Offline",
5789
+ refreshLabel = "Refresh player list",
5790
+ ...rest
5791
+ }) {
5792
+ const theme = useMantineTheme();
5793
+ const color = theme.colors[theme.primaryColor][5];
5794
+ const [searchInput, setSearchInput] = useState("");
5795
+ const [debouncedSearch, setDebouncedSearch] = useState("");
5796
+ const { players, isFetching, refresh } = usePlayers({
5797
+ includeOffline,
5798
+ search: includeOffline ? debouncedSearch : void 0,
5799
+ limit
5800
+ });
5801
+ const sortedPlayers = useMemo(
5802
+ () => [...players].sort((a, b) => {
5803
+ if (a.online !== b.online) return a.online ? -1 : 1;
5804
+ return a.charName.localeCompare(b.charName);
5805
+ }),
5806
+ [players]
5807
+ );
5808
+ const playerByCitizen = useMemo(() => {
5809
+ const m = /* @__PURE__ */ new Map();
5810
+ for (const p of sortedPlayers) m.set(p.citizenId, p);
5811
+ return m;
5812
+ }, [sortedPlayers]);
5813
+ const data = useMemo(() => {
5814
+ const items = sortedPlayers.map((p) => ({
5815
+ value: p.citizenId,
5816
+ label: formatLabel(p)
5817
+ }));
5818
+ if (value && !items.some((i) => i.value === value)) {
5819
+ items.unshift({ value, label: value });
5820
+ }
5821
+ return items;
5822
+ }, [sortedPlayers, value]);
5823
+ const selectedPlayer = value ? playerByCitizen.get(value) ?? null : null;
5824
+ const selectedLabel = selectedPlayer ? formatLabel(selectedPlayer) : null;
5825
+ useEffect(() => {
5826
+ if (!includeOffline) return;
5827
+ if (selectedLabel && searchInput === selectedLabel) return;
5828
+ const t4 = setTimeout(() => setDebouncedSearch(searchInput), DEBOUNCE_MS);
5829
+ return () => clearTimeout(t4);
5830
+ }, [searchInput, includeOffline, selectedLabel]);
5831
+ const renderOption = ({ option }) => {
5832
+ const p = playerByCitizen.get(option.value);
5833
+ if (!p) return option.label;
5834
+ return /* @__PURE__ */ jsxs(Group, { justify: "space-between", wrap: "nowrap", gap: "xs", style: { width: "100%" }, children: [
5835
+ /* @__PURE__ */ jsx(Text, { size: "xs", truncate: true, style: { flex: 1 }, children: formatLabel(p) }),
5836
+ /* @__PURE__ */ jsx(StatusDot, { online: p.online, onlineLabel, offlineLabel })
5837
+ ] });
5838
+ };
5839
+ return /* @__PURE__ */ jsx(
5840
+ Select,
5841
+ {
5842
+ ...rest,
5843
+ data,
5844
+ value: value ?? null,
5845
+ onChange: (v) => {
5846
+ if (!v) return onChange(null);
5847
+ const player = playerByCitizen.get(v) ?? null;
5848
+ onChange(player);
5849
+ },
5850
+ searchable: searchableProp ?? true,
5851
+ searchValue: includeOffline ? searchInput : void 0,
5852
+ onSearchChange: includeOffline ? setSearchInput : void 0,
5853
+ placeholder,
5854
+ nothingFoundMessage: isFetching ? loadingLabel : nothingFoundMessageProp ?? "No players found",
5855
+ maxDropdownHeight: 300,
5856
+ renderOption,
5857
+ leftSection: selectedPlayer ? /* @__PURE__ */ jsx(StatusDot, { online: selectedPlayer.online, onlineLabel, offlineLabel }) : void 0,
5858
+ rightSectionWidth: "3.5vh",
5859
+ rightSectionPointerEvents: "all",
5860
+ rightSection: /* @__PURE__ */ jsx(
5861
+ ActionIcon,
5862
+ {
5863
+ variant: "subtle",
5864
+ size: "sm",
5865
+ onClick: (e) => {
5866
+ e.stopPropagation();
5867
+ refresh();
5868
+ },
5869
+ "aria-label": refreshLabel,
5870
+ title: refreshLabel,
5871
+ style: { marginRight: "0.6vh" },
5872
+ children: isFetching ? /* @__PURE__ */ jsx(Loader, { size: "1.2vh", color }) : /* @__PURE__ */ jsx(RefreshCw, { size: "1.4vh", color })
5873
+ }
5874
+ )
5875
+ }
5876
+ );
5877
+ }
5878
+ function StatusDot({ online, onlineLabel, offlineLabel }) {
5879
+ return /* @__PURE__ */ jsx(
5880
+ "span",
5881
+ {
5882
+ title: online ? onlineLabel : offlineLabel,
5883
+ style: {
5884
+ display: "inline-block",
5885
+ width: "0.9vh",
5886
+ height: "0.9vh",
5887
+ borderRadius: "50%",
5888
+ backgroundColor: online ? ONLINE_COLOR : OFFLINE_COLOR,
5889
+ boxShadow: `0 0 0.4vh ${online ? ONLINE_COLOR : OFFLINE_COLOR}`,
5890
+ flexShrink: 0
5891
+ }
5892
+ }
5893
+ );
5894
+ }
5895
+ function formatLabel(p) {
5896
+ const parts = [p.charName || p.citizenId];
5897
+ if (p.online) {
5898
+ if (p.name) parts.push(p.name);
5899
+ if (p.id != null) parts.push(`#${p.id}`);
5900
+ }
5901
+ return parts.join(" \xB7 ");
5902
+ }
5903
+ var t = (key, fallback) => {
5904
+ const v = locale(key);
5905
+ return v === key ? fallback : v;
5906
+ };
5907
+ var DEFAULT_VALUE = {
5908
+ groups: [],
5909
+ identifiers: []
5910
+ };
5911
+ function SectionLabel({ icon: Icon, label }) {
5912
+ return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", mt: "xs", children: [
5913
+ /* @__PURE__ */ jsx(Icon, { size: "1.4vh", color: "rgba(255,255,255,0.3)" }),
5914
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.07em", c: "rgba(255,255,255,0.2)", children: label }),
5915
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, height: "0.05vh", background: "rgba(255,255,255,0.06)" } })
5916
+ ] });
5917
+ }
5918
+ function EmptyHint({ text }) {
5919
+ return /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.25)", ta: "center", py: "xs", children: text });
5920
+ }
5921
+ function AccessOverrideSection({
5922
+ schemaKey = "access",
5923
+ title,
5924
+ description,
5925
+ groupType,
5926
+ includeOffline = true
5927
+ }) {
5928
+ const mantineTheme = useMantineTheme();
5929
+ const color = mantineTheme.colors[mantineTheme.primaryColor][5];
5930
+ const raw = useFormField(schemaKey);
5931
+ const value = {
5932
+ groups: Array.isArray(raw?.groups) ? raw.groups : DEFAULT_VALUE.groups,
5933
+ identifiers: Array.isArray(raw?.identifiers) ? raw.identifiers : DEFAULT_VALUE.identifiers
5934
+ };
5935
+ const { setValue } = useFormActions();
5936
+ const set = (key, val) => setValue(schemaKey, { ...value, [key]: val });
5937
+ const [addingGroup, setAddingGroup] = useState(false);
5938
+ const [addingPlayer, setAddingPlayer] = useState(false);
5939
+ const commitGroup = (name) => {
5940
+ if (!name) return;
5941
+ set("groups", [...value.groups, name]);
5942
+ setAddingGroup(false);
5943
+ };
5944
+ const setGroup = (index, name) => {
5945
+ const next = [...value.groups];
5946
+ next[index] = name;
5947
+ set("groups", next.filter((g) => g !== ""));
5948
+ };
5949
+ const removeGroup = (index) => set("groups", value.groups.filter((_, i) => i !== index));
5950
+ const commitIdentifier = (id) => {
5951
+ if (!id) return;
5952
+ set("identifiers", [...value.identifiers, id]);
5953
+ setAddingPlayer(false);
5954
+ };
5955
+ const setIdentifier = (index, id) => {
5956
+ const next = [...value.identifiers];
5957
+ next[index] = id;
5958
+ set("identifiers", next.filter((x) => x !== ""));
5959
+ };
5960
+ const removeIdentifier = (index) => set("identifiers", value.identifiers.filter((_, i) => i !== index));
5961
+ return /* @__PURE__ */ jsxs(
5962
+ Flex,
5963
+ {
5964
+ direction: "column",
5965
+ gap: "xs",
5966
+ p: "sm",
5967
+ style: { flex: 1, minHeight: 0, overflowY: "auto" },
5968
+ children: [
5969
+ /* @__PURE__ */ jsx(
5970
+ AdminPageTitle,
5971
+ {
5972
+ icon: ShieldCheck,
5973
+ title: title || t("Access", "Access"),
5974
+ color
5975
+ }
5976
+ ),
5977
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.4)", children: description || t(
5978
+ "AccessOverrideDesc",
5979
+ "Grant access to this resource's admin panel to specific groups and players, in addition to your global dirk_lib admins. Enforcement is server-side."
5980
+ ) }),
5981
+ /* @__PURE__ */ jsx(SectionLabel, { icon: Users, label: t("AccessGroups", "Groups") }),
5982
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "xxs", children: [
5983
+ value.groups.length === 0 && !addingGroup && /* @__PURE__ */ jsx(EmptyHint, { text: t("AccessNoGroups", "No groups added.") }),
5984
+ value.groups.map((name, i) => /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", children: [
5985
+ /* @__PURE__ */ jsx(
5986
+ GroupName,
5987
+ {
5988
+ value: name,
5989
+ onChange: (v) => setGroup(i, v),
5990
+ type: groupType,
5991
+ placeholder: t("AccessPickGroup", "Pick a group\u2026"),
5992
+ style: { flex: 1 }
5993
+ }
5994
+ ),
5995
+ /* @__PURE__ */ jsx(
5996
+ ActionIcon,
5997
+ {
5998
+ variant: "subtle",
5999
+ color: "red",
6000
+ size: "md",
6001
+ onClick: () => removeGroup(i),
6002
+ title: t("Remove", "Remove"),
6003
+ "aria-label": t("Remove", "Remove"),
6004
+ children: /* @__PURE__ */ jsx(Trash2, { size: "1.4vh" })
6005
+ }
6006
+ )
6007
+ ] }, i)),
6008
+ addingGroup && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", children: [
6009
+ /* @__PURE__ */ jsx(
6010
+ GroupName,
6011
+ {
6012
+ value: "",
6013
+ onChange: (v) => commitGroup(v),
6014
+ type: groupType,
6015
+ placeholder: t("AccessPickGroup", "Pick a group\u2026"),
6016
+ style: { flex: 1 }
6017
+ }
6018
+ ),
6019
+ /* @__PURE__ */ jsx(
6020
+ ActionIcon,
6021
+ {
6022
+ variant: "subtle",
6023
+ color: "red",
6024
+ size: "md",
6025
+ onClick: () => setAddingGroup(false),
6026
+ title: t("Remove", "Remove"),
6027
+ "aria-label": t("Remove", "Remove"),
6028
+ children: /* @__PURE__ */ jsx(Trash2, { size: "1.4vh" })
6029
+ }
6030
+ )
6031
+ ] })
6032
+ ] }),
6033
+ !addingGroup && /* @__PURE__ */ jsx(AddRowButton, { label: t("AccessAddGroup", "Add group"), onClick: () => setAddingGroup(true) }),
6034
+ /* @__PURE__ */ jsx(SectionLabel, { icon: UserRound, label: t("AccessPlayers", "Players") }),
6035
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "xxs", children: [
6036
+ value.identifiers.length === 0 && !addingPlayer && /* @__PURE__ */ jsx(EmptyHint, { text: t("AccessNoPlayers", "No players added.") }),
6037
+ value.identifiers.map((id, i) => /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", children: [
6038
+ /* @__PURE__ */ jsx(
6039
+ PlayerSelect,
6040
+ {
6041
+ value: id || null,
6042
+ onChange: (p) => setIdentifier(i, p?.citizenId ?? ""),
6043
+ includeOffline,
6044
+ placeholder: t("AccessPickPlayer", "Pick a player\u2026"),
6045
+ style: { flex: 1 },
6046
+ size: "xs"
6047
+ }
6048
+ ),
6049
+ /* @__PURE__ */ jsx(
6050
+ ActionIcon,
6051
+ {
6052
+ variant: "subtle",
6053
+ color: "red",
6054
+ size: "md",
6055
+ onClick: () => removeIdentifier(i),
6056
+ title: t("Remove", "Remove"),
6057
+ "aria-label": t("Remove", "Remove"),
6058
+ children: /* @__PURE__ */ jsx(Trash2, { size: "1.4vh" })
6059
+ }
6060
+ )
6061
+ ] }, i)),
6062
+ addingPlayer && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", children: [
6063
+ /* @__PURE__ */ jsx(
6064
+ PlayerSelect,
6065
+ {
6066
+ value: null,
6067
+ onChange: (p) => commitIdentifier(p?.citizenId ?? ""),
6068
+ includeOffline,
6069
+ placeholder: t("AccessPickPlayer", "Pick a player\u2026"),
6070
+ style: { flex: 1 },
6071
+ size: "xs"
6072
+ }
6073
+ ),
6074
+ /* @__PURE__ */ jsx(
6075
+ ActionIcon,
6076
+ {
6077
+ variant: "subtle",
6078
+ color: "red",
6079
+ size: "md",
6080
+ onClick: () => setAddingPlayer(false),
6081
+ title: t("Remove", "Remove"),
6082
+ "aria-label": t("Remove", "Remove"),
6083
+ children: /* @__PURE__ */ jsx(Trash2, { size: "1.4vh" })
6084
+ }
6085
+ )
6086
+ ] })
6087
+ ] }),
6088
+ !addingPlayer && /* @__PURE__ */ jsx(AddRowButton, { label: t("AccessAddPlayer", "Add player"), onClick: () => setAddingPlayer(true) })
6089
+ ]
6090
+ }
6091
+ );
6092
+ }
6093
+ function AddRowButton({ label, onClick }) {
6094
+ return /* @__PURE__ */ jsxs(
6095
+ Flex,
6096
+ {
6097
+ align: "center",
6098
+ justify: "center",
6099
+ gap: "xs",
6100
+ onClick,
6101
+ role: "button",
6102
+ tabIndex: 0,
6103
+ onKeyDown: (e) => {
6104
+ if (e.key === "Enter" || e.key === " ") {
6105
+ e.preventDefault();
6106
+ onClick();
6107
+ }
6108
+ },
6109
+ py: "xs",
6110
+ style: {
6111
+ cursor: "pointer",
6112
+ border: "0.1vh dashed rgba(255,255,255,0.12)",
6113
+ borderRadius: "0.4vh",
6114
+ transition: "background 0.15s, border-color 0.15s"
6115
+ },
6116
+ onMouseEnter: (e) => {
6117
+ e.currentTarget.style.background = "rgba(255,255,255,0.03)";
6118
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.2)";
6119
+ },
6120
+ onMouseLeave: (e) => {
6121
+ e.currentTarget.style.background = "transparent";
6122
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)";
6123
+ },
6124
+ children: [
6125
+ /* @__PURE__ */ jsx(Plus, { size: "1.4vh", color: "rgba(255,255,255,0.4)" }),
6126
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "rgba(255,255,255,0.4)", children: label })
6127
+ ]
6128
+ }
6129
+ );
6130
+ }
5711
6131
 
5712
6132
  // src/utils/gtaAnimPostFx.ts
5713
6133
  var GTA_ANIM_POST_FX_GROUP_ORDER = [
@@ -6297,7 +6717,7 @@ function ScenarioSelect({
6297
6717
  }
6298
6718
  );
6299
6719
  }
6300
- var t = (key, fallback) => {
6720
+ var t2 = (key, fallback) => {
6301
6721
  const v = locale(key);
6302
6722
  return v === key ? fallback : v;
6303
6723
  };
@@ -6365,7 +6785,7 @@ function DiscordRoleSelect(props) {
6365
6785
  return map;
6366
6786
  }, [roles]);
6367
6787
  if (errorCode === "NotConfigured") {
6368
- const notConfiguredPlaceholder = t(
6788
+ const notConfiguredPlaceholder = t2(
6369
6789
  "DiscordNotConfigured",
6370
6790
  "Not configured \u2014 set in /dirk_lib"
6371
6791
  );
@@ -6433,9 +6853,9 @@ function DiscordRoleSelect(props) {
6433
6853
  e.stopPropagation();
6434
6854
  refresh();
6435
6855
  },
6436
- title: t("Refresh", "Refresh"),
6856
+ title: t2("Refresh", "Refresh"),
6437
6857
  loading,
6438
- "aria-label": t("Refresh", "Refresh"),
6858
+ "aria-label": t2("Refresh", "Refresh"),
6439
6859
  children: /* @__PURE__ */ jsx(RefreshCw, { size: "1.2vh" })
6440
6860
  }
6441
6861
  );
@@ -6445,7 +6865,7 @@ function DiscordRoleSelect(props) {
6445
6865
  {
6446
6866
  label,
6447
6867
  size,
6448
- placeholder: placeholder || t("PickRoles", "Pick roles..."),
6868
+ placeholder: placeholder || t2("PickRoles", "Pick roles..."),
6449
6869
  data,
6450
6870
  value: props.value || [],
6451
6871
  onChange: (ids) => props.onChange(ids),
@@ -6457,7 +6877,7 @@ function DiscordRoleSelect(props) {
6457
6877
  rightSectionPointerEvents: "all",
6458
6878
  style,
6459
6879
  styles,
6460
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6880
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6461
6881
  }
6462
6882
  );
6463
6883
  }
@@ -6466,7 +6886,7 @@ function DiscordRoleSelect(props) {
6466
6886
  {
6467
6887
  label,
6468
6888
  size,
6469
- placeholder: placeholder || t("PickRole", "Pick a role..."),
6889
+ placeholder: placeholder || t2("PickRole", "Pick a role..."),
6470
6890
  data,
6471
6891
  value: props.value || null,
6472
6892
  onChange: (id) => props.onChange(id),
@@ -6478,11 +6898,11 @@ function DiscordRoleSelect(props) {
6478
6898
  rightSectionPointerEvents: "all",
6479
6899
  style,
6480
6900
  styles,
6481
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6901
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6482
6902
  }
6483
6903
  );
6484
6904
  }
6485
- var t2 = (key, fallback) => {
6905
+ var t3 = (key, fallback) => {
6486
6906
  const v = locale(key);
6487
6907
  return v === key ? fallback : v;
6488
6908
  };
@@ -6503,7 +6923,7 @@ function FrameworkHint({ framework }) {
6503
6923
  const theme = useMantineTheme();
6504
6924
  if (!framework) return null;
6505
6925
  const hintKey = `AccountFrameworkHint_${framework}`;
6506
- const text = t2(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6926
+ const text = t3(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6507
6927
  return /* @__PURE__ */ jsxs(
6508
6928
  Flex,
6509
6929
  {
@@ -6587,9 +7007,9 @@ function AccountSelect(props) {
6587
7007
  e.stopPropagation();
6588
7008
  refresh();
6589
7009
  },
6590
- title: t2("Refresh", "Refresh"),
7010
+ title: t3("Refresh", "Refresh"),
6591
7011
  loading,
6592
- "aria-label": t2("Refresh", "Refresh"),
7012
+ "aria-label": t3("Refresh", "Refresh"),
6593
7013
  children: /* @__PURE__ */ jsx(RefreshCw, { size: "1.2vh" })
6594
7014
  }
6595
7015
  );
@@ -6603,14 +7023,14 @@ function AccountSelect(props) {
6603
7023
  rightSection: refreshButton,
6604
7024
  rightSectionPointerEvents: "all",
6605
7025
  style,
6606
- nothingFoundMessage: t2("NoAccounts", "No accounts available")
7026
+ nothingFoundMessage: t3("NoAccounts", "No accounts available")
6607
7027
  };
6608
7028
  return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "0.3vh", style, children: [
6609
7029
  props.multi ? /* @__PURE__ */ jsx(
6610
7030
  MultiSelect,
6611
7031
  {
6612
7032
  ...sharedSelectProps,
6613
- placeholder: placeholder || t2("PickAccounts", "Pick accounts (or type custom)..."),
7033
+ placeholder: placeholder || t3("PickAccounts", "Pick accounts (or type custom)..."),
6614
7034
  value: props.value || [],
6615
7035
  onChange: (names) => props.onChange(names),
6616
7036
  renderOption,
@@ -6622,7 +7042,7 @@ function AccountSelect(props) {
6622
7042
  Select,
6623
7043
  {
6624
7044
  ...sharedSelectProps,
6625
- placeholder: placeholder || t2("PickAccount", "Pick an account..."),
7045
+ placeholder: placeholder || t3("PickAccount", "Pick an account..."),
6626
7046
  value: props.value || null,
6627
7047
  onChange: (name) => props.onChange(name),
6628
7048
  renderOption,
@@ -6914,175 +7334,6 @@ function WorldPositionGotoButton2({
6914
7334
  }
6915
7335
  ) });
6916
7336
  }
6917
- function usePlayers(opts = {}) {
6918
- const {
6919
- includeOffline = false,
6920
- search = "",
6921
- limit = 50,
6922
- staleTimeMs,
6923
- refetchIntervalMs
6924
- } = opts;
6925
- const query = useQuery({
6926
- queryKey: includeOffline ? ["dirk:players", "search", search.trim().toLowerCase(), limit] : ["dirk:players", "online"],
6927
- queryFn: async () => {
6928
- const toolId = includeOffline ? "searchPlayers" : "getOnlinePlayers";
6929
- const payload = includeOffline ? { id: toolId, value: { search: search.trim(), limit } } : { id: toolId };
6930
- const result = await fetchNui(
6931
- "ADMIN_TOOL_QUERY",
6932
- payload,
6933
- // Browser-dev fallback. Returns a couple of mock players so the
6934
- // dev shell isn't blank.
6935
- includeOffline ? [
6936
- { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true },
6937
- { id: null, citizenId: "DEF67890", name: "", charName: "Jane Offline", online: false }
6938
- ] : [
6939
- { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true }
6940
- ]
6941
- );
6942
- return Array.isArray(result) ? result : [];
6943
- },
6944
- staleTime: staleTimeMs ?? (includeOffline ? 3e4 : 5e3),
6945
- gcTime: 5 * 6e4,
6946
- refetchInterval: refetchIntervalMs ?? false,
6947
- refetchOnWindowFocus: false,
6948
- placeholderData: includeOffline ? keepPreviousData : void 0
6949
- });
6950
- return {
6951
- players: query.data ?? [],
6952
- isLoading: query.isLoading,
6953
- isFetching: query.isFetching,
6954
- error: query.error ?? null,
6955
- refresh: () => query.refetch()
6956
- };
6957
- }
6958
- var DEBOUNCE_MS = 300;
6959
- var ONLINE_COLOR = "#3FA83F";
6960
- var OFFLINE_COLOR = "#E54141";
6961
- function PlayerSelect({
6962
- value,
6963
- onChange,
6964
- includeOffline = false,
6965
- limit = 50,
6966
- searchable: searchableProp,
6967
- placeholder,
6968
- nothingFoundMessage: nothingFoundMessageProp,
6969
- loadingLabel = "Loading\u2026",
6970
- onlineLabel = "Online",
6971
- offlineLabel = "Offline",
6972
- refreshLabel = "Refresh player list",
6973
- ...rest
6974
- }) {
6975
- const theme = useMantineTheme();
6976
- const color = theme.colors[theme.primaryColor][5];
6977
- const [searchInput, setSearchInput] = useState("");
6978
- const [debouncedSearch, setDebouncedSearch] = useState("");
6979
- const { players, isFetching, refresh } = usePlayers({
6980
- includeOffline,
6981
- search: includeOffline ? debouncedSearch : void 0,
6982
- limit
6983
- });
6984
- const sortedPlayers = useMemo(
6985
- () => [...players].sort((a, b) => {
6986
- if (a.online !== b.online) return a.online ? -1 : 1;
6987
- return a.charName.localeCompare(b.charName);
6988
- }),
6989
- [players]
6990
- );
6991
- const playerByCitizen = useMemo(() => {
6992
- const m = /* @__PURE__ */ new Map();
6993
- for (const p of sortedPlayers) m.set(p.citizenId, p);
6994
- return m;
6995
- }, [sortedPlayers]);
6996
- const data = useMemo(() => {
6997
- const items = sortedPlayers.map((p) => ({
6998
- value: p.citizenId,
6999
- label: formatLabel(p)
7000
- }));
7001
- if (value && !items.some((i) => i.value === value)) {
7002
- items.unshift({ value, label: value });
7003
- }
7004
- return items;
7005
- }, [sortedPlayers, value]);
7006
- const selectedPlayer = value ? playerByCitizen.get(value) ?? null : null;
7007
- const selectedLabel = selectedPlayer ? formatLabel(selectedPlayer) : null;
7008
- useEffect(() => {
7009
- if (!includeOffline) return;
7010
- if (selectedLabel && searchInput === selectedLabel) return;
7011
- const t3 = setTimeout(() => setDebouncedSearch(searchInput), DEBOUNCE_MS);
7012
- return () => clearTimeout(t3);
7013
- }, [searchInput, includeOffline, selectedLabel]);
7014
- const renderOption = ({ option }) => {
7015
- const p = playerByCitizen.get(option.value);
7016
- if (!p) return option.label;
7017
- return /* @__PURE__ */ jsxs(Group, { justify: "space-between", wrap: "nowrap", gap: "xs", style: { width: "100%" }, children: [
7018
- /* @__PURE__ */ jsx(Text, { size: "xs", truncate: true, style: { flex: 1 }, children: formatLabel(p) }),
7019
- /* @__PURE__ */ jsx(StatusDot, { online: p.online, onlineLabel, offlineLabel })
7020
- ] });
7021
- };
7022
- return /* @__PURE__ */ jsx(
7023
- Select,
7024
- {
7025
- ...rest,
7026
- data,
7027
- value: value ?? null,
7028
- onChange: (v) => {
7029
- if (!v) return onChange(null);
7030
- const player = playerByCitizen.get(v) ?? null;
7031
- onChange(player);
7032
- },
7033
- searchable: searchableProp ?? true,
7034
- searchValue: includeOffline ? searchInput : void 0,
7035
- onSearchChange: includeOffline ? setSearchInput : void 0,
7036
- placeholder,
7037
- nothingFoundMessage: isFetching ? loadingLabel : nothingFoundMessageProp ?? "No players found",
7038
- maxDropdownHeight: 300,
7039
- renderOption,
7040
- leftSection: selectedPlayer ? /* @__PURE__ */ jsx(StatusDot, { online: selectedPlayer.online, onlineLabel, offlineLabel }) : void 0,
7041
- rightSectionWidth: "3.5vh",
7042
- rightSectionPointerEvents: "all",
7043
- rightSection: /* @__PURE__ */ jsx(
7044
- ActionIcon,
7045
- {
7046
- variant: "subtle",
7047
- size: "sm",
7048
- onClick: (e) => {
7049
- e.stopPropagation();
7050
- refresh();
7051
- },
7052
- "aria-label": refreshLabel,
7053
- title: refreshLabel,
7054
- style: { marginRight: "0.6vh" },
7055
- children: isFetching ? /* @__PURE__ */ jsx(Loader, { size: "1.2vh", color }) : /* @__PURE__ */ jsx(RefreshCw, { size: "1.4vh", color })
7056
- }
7057
- )
7058
- }
7059
- );
7060
- }
7061
- function StatusDot({ online, onlineLabel, offlineLabel }) {
7062
- return /* @__PURE__ */ jsx(
7063
- "span",
7064
- {
7065
- title: online ? onlineLabel : offlineLabel,
7066
- style: {
7067
- display: "inline-block",
7068
- width: "0.9vh",
7069
- height: "0.9vh",
7070
- borderRadius: "50%",
7071
- backgroundColor: online ? ONLINE_COLOR : OFFLINE_COLOR,
7072
- boxShadow: `0 0 0.4vh ${online ? ONLINE_COLOR : OFFLINE_COLOR}`,
7073
- flexShrink: 0
7074
- }
7075
- }
7076
- );
7077
- }
7078
- function formatLabel(p) {
7079
- const parts = [p.charName || p.citizenId];
7080
- if (p.online) {
7081
- if (p.name) parts.push(p.name);
7082
- if (p.id != null) parts.push(`#${p.id}`);
7083
- }
7084
- return parts.join(" \xB7 ");
7085
- }
7086
7337
  function usePickDoor() {
7087
7338
  const begin = useAdminToolStore((s) => s.begin);
7088
7339
  return async () => {
@@ -7359,6 +7610,6 @@ function BlipMarker({
7359
7610
  );
7360
7611
  }
7361
7612
 
7362
- export { AccountSelect, AdminPageTitle, AnimPostFxSelect, AsyncSaveButton, BlipColorSelect, BlipDisplaySelect, BlipIconSelect, BlipMarker, BorderedIcon, ConfigPanel, ConfirmModal, ControlMultiSelect, ControlSelect, Counter, DiscordRoleSelect, DoorPickerButton, FiveMKeyBindInput, FloatingParticles, GroupName, GroupRank, GroupSelect, InfoBox, InputContainer, InstructionPanel, LevelBanner, LevelPanel, Map2 as Map, MapLayer, MissingItemsBanner, Modal, ModalContext, ModalProvider, MotionFlex, MotionIcon, MotionImage, MotionText, NavBar, NavigationContext, NavigationProvider, PlayerSelect, PositionPicker, PromptModal, ScenarioSelect, SegmentedControl, SegmentedProgress, SelectItem, SwitchPanel, TestBed, ThemeOverrideSection, Title, Vector4DeleteButton, Vector4Display, WorldPositionGotoButton, WorldPositionPicker, WorldPositionSetButton, ZoomControls, blipUrl, blipUrlForSprite, getBlipColor, getBlipEntry, useAdminToolStore, useMissingItemsAudit, useModal, useModalActions, useNavigation, useNavigationStore, usePickDoor };
7613
+ export { AccessOverrideSection, AccountSelect, AdminPageTitle, AnimPostFxSelect, AsyncSaveButton, BlipColorSelect, BlipDisplaySelect, BlipIconSelect, BlipMarker, BorderedIcon, ConfigPanel, ConfirmModal, ControlMultiSelect, ControlSelect, Counter, DiscordRoleSelect, DoorPickerButton, FiveMKeyBindInput, FloatingParticles, GroupName, GroupRank, GroupSelect, InfoBox, InputContainer, InstructionPanel, LevelBanner, LevelPanel, Map2 as Map, MapLayer, MissingItemsBanner, Modal, ModalContext, ModalProvider, MotionFlex, MotionIcon, MotionImage, MotionText, NavBar, NavigationContext, NavigationProvider, PlayerSelect, PositionPicker, PromptModal, ScenarioSelect, SegmentedControl, SegmentedProgress, SelectItem, SwitchPanel, TestBed, ThemeOverrideSection, Title, Vector4DeleteButton, Vector4Display, WorldPositionGotoButton, WorldPositionPicker, WorldPositionSetButton, ZoomControls, blipUrl, blipUrlForSprite, getBlipColor, getBlipEntry, useAdminToolStore, useMissingItemsAudit, useModal, useModalActions, useNavigation, useNavigationStore, usePickDoor };
7363
7614
  //# sourceMappingURL=index.js.map
7364
7615
  //# sourceMappingURL=index.js.map