dirk-cfx-react 1.1.87 → 1.1.90

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.
@@ -1,15 +1,30 @@
1
1
  import { Flex, Text, Image, TextInput, Select, Box, useMantineTheme, Tooltip, alpha, Progress, RingProgress, Portal, Button, NumberInput, MultiSelect, Loader, Switch, ActionIcon, Stack, ColorInput, Popover, Group, JsonInput } from '@mantine/core';
2
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
- import { createContext, useContext, useRef, useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { createContext, memo, useRef, useEffect, useContext, useState, useCallback, useMemo } from 'react';
4
4
  import { create, useStore, createStore } from 'zustand';
5
5
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6
6
  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, ArrowLeft, Undo2, Redo2, Save, History, XCircle, Code2, Search, Filter, User } from 'lucide-react';
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
8
  import clickSoundUrl from '../click_sound-PNCRRTM4.mp3';
9
9
  import hoverSoundUrl from '../hover_sound-NBUA222C.mp3';
10
10
  import { notifications } from '@mantine/notifications';
11
- import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query';
11
+ import { QueryClient, useQuery, keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
12
+ import '@mantine/core/styles.css';
13
+ import '@mantine/notifications/styles.css';
14
+ import './styles/fonts.css';
15
+ import './styles/notify.css';
16
+ import './styles/scrollBar.css';
17
+ import './styles/tornEdge.css';
18
+ import { library } from '@fortawesome/fontawesome-svg-core';
19
+ import { fab } from '@fortawesome/free-brands-svg-icons';
20
+ import { far } from '@fortawesome/free-regular-svg-icons';
21
+ import { fas } from '@fortawesome/free-solid-svg-icons';
12
22
  import { generateColors } from '@mantine/colors-generator';
23
+ import { createPortal } from 'react-dom';
24
+ import { CRS, tileLayer, latLngBounds, latLng } from 'leaflet';
25
+ import 'leaflet/dist/leaflet.css';
26
+ import { MapContainer, useMap } from 'react-leaflet';
27
+ import { Marker } from '@adamscybot/react-leaflet-component-marker';
13
28
 
14
29
  // src/components/BlipSelect.tsx
15
30
  var BLIP_ENTRIES = [
@@ -953,6 +968,18 @@ var BLIP_COLOR_DATA = BLIP_COLORS.map(([id, label]) => ({
953
968
  }));
954
969
  var blipEntryMap = new Map(BLIP_ENTRIES.map(([id, name, ext]) => [String(id), { id, name, ext }]));
955
970
  var blipColorMap = new Map(BLIP_COLORS.map(([id, label, hex]) => [String(id), { id, label, hex }]));
971
+ function getBlipEntry(spriteId) {
972
+ if (spriteId == null) return void 0;
973
+ return blipEntryMap.get(String(spriteId));
974
+ }
975
+ function getBlipColor(colorId) {
976
+ if (colorId == null) return void 0;
977
+ return blipColorMap.get(String(colorId));
978
+ }
979
+ function blipUrlForSprite(spriteId) {
980
+ const entry = getBlipEntry(spriteId);
981
+ return entry ? blipUrl(entry.name, entry.ext) : null;
982
+ }
956
983
  var renderBlipOption = ({ option }) => {
957
984
  const entry = blipEntryMap.get(option.value);
958
985
  if (!entry) return option.label;
@@ -1230,11 +1257,11 @@ var colorNames = {
1230
1257
  Yellow: { r: 255, g: 255, b: 0 },
1231
1258
  YellowGreen: { r: 154, g: 205, b: 50 }
1232
1259
  };
1233
- function colorWithAlpha(color, alpha12) {
1260
+ function colorWithAlpha(color, alpha17) {
1234
1261
  const lowerCasedColor = color.toLowerCase();
1235
1262
  if (colorNames[lowerCasedColor]) {
1236
1263
  const rgb = colorNames[lowerCasedColor];
1237
- return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha12})`;
1264
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha17})`;
1238
1265
  }
1239
1266
  if (/^#([A-Fa-f0-9]{6})$/.test(color)) {
1240
1267
  const hex = color.slice(1);
@@ -1242,12 +1269,12 @@ function colorWithAlpha(color, alpha12) {
1242
1269
  const r = bigint >> 16 & 255;
1243
1270
  const g = bigint >> 8 & 255;
1244
1271
  const b = bigint & 255;
1245
- return `rgba(${r}, ${g}, ${b}, ${alpha12})`;
1272
+ return `rgba(${r}, ${g}, ${b}, ${alpha17})`;
1246
1273
  }
1247
1274
  if (/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/.test(color)) {
1248
1275
  const result = color.match(/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/);
1249
1276
  if (result) {
1250
- return `rgba(${result[1]}, ${result[2]}, ${result[3]}, ${alpha12})`;
1277
+ return `rgba(${result[1]}, ${result[2]}, ${result[3]}, ${alpha17})`;
1251
1278
  }
1252
1279
  }
1253
1280
  return color;
@@ -1316,11 +1343,35 @@ function reportMissingLocale(key) {
1316
1343
  fetchNui("REPORT_MISSING_LOCALE", { key }).catch(() => {
1317
1344
  });
1318
1345
  }
1346
+ var PACKAGE_DEFAULTS = {
1347
+ "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.",
1348
+ "cfgpanel_discard": "Discard",
1349
+ "cfgpanel_manual_edit": "Manual Edit",
1350
+ "cfgpanel_reset_defaults": "Reset Defaults",
1351
+ "cfgpanel_version": "Version",
1352
+ "cfgpanel_back_title": "Back to script list",
1353
+ "cfgpanel_undo": "Undo",
1354
+ "cfgpanel_redo": "Redo",
1355
+ "cfgpanel_save": "Save",
1356
+ "cfgpanel_saving": "Saving...",
1357
+ "cfgpanel_history": "History",
1358
+ "cfgpanel_reset_title": "Reset to Defaults",
1359
+ "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.",
1360
+ "cfgpanel_reset_confirm": "Reset Config",
1361
+ "cfgpanel_discard_title": "Discard Unsaved Changes?",
1362
+ "cfgpanel_discard_desc_back": "You have unsaved changes. Going back now will discard them.",
1363
+ "cfgpanel_discard_desc_close": "You have unsaved changes. Closing now will discard them.",
1364
+ "cfgpanel_discard_confirm_back": "Go Back Without Saving",
1365
+ "cfgpanel_discard_confirm_close": "Close Without Saving",
1366
+ "cfgpanel_json_title": "Config JSON",
1367
+ "cfgpanel_cancel": "Cancel",
1368
+ "cfgpanel_apply": "Apply",
1369
+ "cfgpanel_history_title": "Config History",
1370
+ "cfgpanel_close": "Close"
1371
+ };
1319
1372
  var localeStore = create((set, get) => {
1320
1373
  return {
1321
- locales: {
1322
- "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."
1323
- },
1374
+ locales: { ...PACKAGE_DEFAULTS },
1324
1375
  locale: (key, ...args) => {
1325
1376
  const exists = get().locales[key];
1326
1377
  if (!exists) reportMissingLocale(key);
@@ -1334,7 +1385,7 @@ var localeStore = create((set, get) => {
1334
1385
  });
1335
1386
  var locale = localeStore.getState().locale;
1336
1387
  registerInitialFetch("GET_LOCALES", void 0).then((data) => {
1337
- localeStore.setState({ locales: data });
1388
+ localeStore.setState({ locales: { ...PACKAGE_DEFAULTS, ...data } });
1338
1389
  }).catch(() => {
1339
1390
  });
1340
1391
  if (typeof window !== "undefined") {
@@ -1342,9 +1393,22 @@ if (typeof window !== "undefined") {
1342
1393
  const msg = event.data;
1343
1394
  if (!msg || msg.action !== "UPDATE_DIRK_LIB_LOCALES") return;
1344
1395
  if (!msg.data || typeof msg.data !== "object") return;
1345
- localeStore.setState({ locales: msg.data });
1396
+ if (Object.keys(msg.data).length === 0) return;
1397
+ localeStore.setState({ locales: { ...PACKAGE_DEFAULTS, ...msg.data } });
1346
1398
  });
1347
1399
  }
1400
+
1401
+ // src/utils/map.ts
1402
+ var mapCenter = [-119.43, 58.84];
1403
+ var latPr100 = 1.421;
1404
+ function gameToMap(x, y) {
1405
+ return [
1406
+ mapCenter[0] + latPr100 / 100 * y,
1407
+ // lng
1408
+ mapCenter[1] + latPr100 / 100 * x
1409
+ // lat
1410
+ ];
1411
+ }
1348
1412
  var useItems = create(() => ({}));
1349
1413
  var useItemsList = (excludeItemNames = []) => {
1350
1414
  const excludeSet = new Set(excludeItemNames);
@@ -1364,15 +1428,21 @@ var useFrameworkGroups = create(() => ({
1364
1428
  gangs: [],
1365
1429
  loaded: false
1366
1430
  }));
1367
- registerInitialFetch("GET_FRAMEWORK_GROUPS", void 0).then((data) => {
1368
- useFrameworkGroups.setState({
1369
- jobs: Array.isArray(data?.jobs) ? data.jobs : [],
1370
- gangs: Array.isArray(data?.gangs) ? data.gangs : [],
1371
- loaded: true
1431
+ var frameworkGroupsRequested = false;
1432
+ function ensureFrameworkGroups() {
1433
+ if (frameworkGroupsRequested) return;
1434
+ frameworkGroupsRequested = true;
1435
+ fetchNui("GET_FRAMEWORK_GROUPS", void 0).then((data) => {
1436
+ useFrameworkGroups.setState({
1437
+ jobs: Array.isArray(data?.jobs) ? data.jobs : [],
1438
+ gangs: Array.isArray(data?.gangs) ? data.gangs : [],
1439
+ loaded: true
1440
+ });
1441
+ }).catch(() => {
1442
+ frameworkGroupsRequested = false;
1443
+ useFrameworkGroups.setState({ loaded: true });
1372
1444
  });
1373
- }).catch(() => {
1374
- useFrameworkGroups.setState({ loaded: true });
1375
- });
1445
+ }
1376
1446
 
1377
1447
  // src/utils/inputMapper.ts
1378
1448
  var INPUT_MAPPER_PRIMARY_OPTIONS = [
@@ -2381,7 +2451,7 @@ function NavigationProvider({ children, defaultPage }) {
2381
2451
  }
2382
2452
  function NavBar(props) {
2383
2453
  const pageId = useNavigation((state) => state.pageId);
2384
- const store = useNavigationStore();
2454
+ const store2 = useNavigationStore();
2385
2455
  return /* @__PURE__ */ jsx(
2386
2456
  SegmentedControl,
2387
2457
  {
@@ -2390,7 +2460,7 @@ function NavBar(props) {
2390
2460
  value: pageId,
2391
2461
  items: props.items,
2392
2462
  onChange: (value) => {
2393
- store.setState({ pageId: value });
2463
+ store2.setState({ pageId: value });
2394
2464
  }
2395
2465
  }
2396
2466
  );
@@ -3697,34 +3767,82 @@ function FormProvider({
3697
3767
  return /* @__PURE__ */ jsx(FormContext.Provider, { value: storeRef.current, children });
3698
3768
  }
3699
3769
  function useForm() {
3700
- const store = useContext(FormContext);
3701
- if (!store) {
3770
+ const store2 = useContext(FormContext);
3771
+ if (!store2) {
3702
3772
  throw new Error("useForm must be used inside <FormProvider>");
3703
3773
  }
3704
- const state = useStore(store);
3774
+ const state = useStore(store2);
3705
3775
  const changedFields = useMemo(() => {
3706
3776
  return collectChangedPaths(state.values, state.initialValues);
3707
3777
  }, [state.values, state.initialValues]);
3708
3778
  return { ...state, changedFields, changedCount: changedFields.length };
3709
3779
  }
3710
3780
  function useFormField(path) {
3711
- const store = useContext(FormContext);
3712
- if (!store) {
3781
+ const store2 = useContext(FormContext);
3782
+ if (!store2) {
3713
3783
  throw new Error("useFormField must be used inside <FormProvider>");
3714
3784
  }
3715
- return useStore(store, (s) => getNested(s.values, path));
3785
+ return useStore(store2, (s) => getNested(s.values, path));
3716
3786
  }
3717
3787
  function useFormActions() {
3718
- const store = useContext(FormContext);
3719
- if (!store) {
3788
+ const store2 = useContext(FormContext);
3789
+ if (!store2) {
3720
3790
  throw new Error("useFormActions must be used inside <FormProvider>");
3721
3791
  }
3722
- return store.getState();
3792
+ return store2.getState();
3793
+ }
3794
+ var store = /* @__PURE__ */ new Map();
3795
+ var listeners = /* @__PURE__ */ new Map();
3796
+ function notify(key) {
3797
+ const ls = listeners.get(key);
3798
+ if (!ls) return;
3799
+ ls.forEach((fn) => fn());
3800
+ }
3801
+ function useAdminState(key, initial) {
3802
+ const [, setTick] = useState(0);
3803
+ useEffect(() => {
3804
+ const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
3805
+ listeners.set(key, set);
3806
+ const handler = () => setTick((t3) => t3 + 1);
3807
+ set.add(handler);
3808
+ return () => {
3809
+ set.delete(handler);
3810
+ if (set.size === 0) listeners.delete(key);
3811
+ };
3812
+ }, [key]);
3813
+ const value = store.has(key) ? store.get(key) : initial;
3814
+ const setValue = (v) => {
3815
+ const next = typeof v === "function" ? v(value) : v;
3816
+ store.set(key, next);
3817
+ notify(key);
3818
+ };
3819
+ return [value, setValue];
3723
3820
  }
3724
3821
  function getScriptConfigInstance() {
3725
3822
  throw new Error("[dirk-cfx-react] createScriptConfig must be called before using ConfigPanel");
3726
3823
  }
3727
- var configPanelQueryClient = new QueryClient({
3824
+ var useAdminToolStore = create((set, get) => ({
3825
+ active: null,
3826
+ begin: (spec) => new Promise((resolve) => {
3827
+ const prev = get().active;
3828
+ if (prev) prev.resolve(null);
3829
+ set({ active: { ...spec, resolve } });
3830
+ }),
3831
+ resolveActive: (value) => {
3832
+ const cur = get().active;
3833
+ if (!cur) return;
3834
+ cur.resolve(value);
3835
+ set({ active: null });
3836
+ },
3837
+ cancelActive: () => {
3838
+ const cur = get().active;
3839
+ if (!cur) return;
3840
+ cur.resolve(null);
3841
+ set({ active: null });
3842
+ }
3843
+ }));
3844
+ library.add(fas, far, fab);
3845
+ var dirkQueryClient = new QueryClient({
3728
3846
  defaultOptions: { queries: { staleTime: 3e4, gcTime: 5 * 6e4 } }
3729
3847
  });
3730
3848
  function NavItemButton({
@@ -3805,7 +3923,7 @@ function ConfigJsonModal({
3805
3923
  setError(e.message);
3806
3924
  }
3807
3925
  };
3808
- return /* @__PURE__ */ jsxs(Modal, { title: "Config JSON", icon: Code2, iconColor: color, onClose, width: "60vh", maxHeight: "80vh", zIndex: 200, children: [
3926
+ return /* @__PURE__ */ jsxs(Modal, { title: locale("cfgpanel_json_title"), icon: Code2, iconColor: color, onClose, width: "60vh", maxHeight: "80vh", zIndex: 200, children: [
3809
3927
  /* @__PURE__ */ jsxs(Box, { flex: 1, p: "0.8vh", style: { overflowY: "auto" }, children: [
3810
3928
  /* @__PURE__ */ jsx(
3811
3929
  JsonInput,
@@ -3833,7 +3951,7 @@ function ConfigJsonModal({
3833
3951
  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 },
3834
3952
  children: [
3835
3953
  /* @__PURE__ */ jsx(X, { color: "rgba(255,255,255,0.4)" }),
3836
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: "Cancel" })
3954
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: locale("cfgpanel_cancel") })
3837
3955
  ]
3838
3956
  }
3839
3957
  ),
@@ -3846,7 +3964,7 @@ function ConfigJsonModal({
3846
3964
  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 },
3847
3965
  children: [
3848
3966
  /* @__PURE__ */ jsx(Check, { color }),
3849
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: color, children: "Apply" })
3967
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: color, children: locale("cfgpanel_apply") })
3850
3968
  ]
3851
3969
  }
3852
3970
  )
@@ -3987,7 +4105,7 @@ function ConfigHistoryModal({
3987
4105
  historyQuery.fetchNextPage();
3988
4106
  }
3989
4107
  };
3990
- return /* @__PURE__ */ jsxs(Modal, { title: "Config History", icon: History, iconColor: color, onClose, width: "88vh", maxHeight: "82vh", zIndex: 260, children: [
4108
+ return /* @__PURE__ */ jsxs(Modal, { title: locale("cfgpanel_history_title"), icon: History, iconColor: color, onClose, width: "88vh", maxHeight: "82vh", zIndex: 260, children: [
3991
4109
  /* @__PURE__ */ jsxs(Flex, { direction: "column", style: { flex: 1, minHeight: 0 }, children: [
3992
4110
  /* @__PURE__ */ jsxs(Flex, { gap: "xs", p: "sm", style: { borderBottom: `0.1vh solid ${alpha(theme.colors.dark[7], 0.8)}` }, children: [
3993
4111
  /* @__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 } }),
@@ -4030,7 +4148,7 @@ function ConfigHistoryModal({
4030
4148
  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 },
4031
4149
  children: [
4032
4150
  /* @__PURE__ */ jsx(X, { color: "rgba(255,255,255,0.4)", size: "1.5vh" }),
4033
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: "Close" })
4151
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xs", tt: "uppercase", lts: "0.05em", c: "rgba(255,255,255,0.4)", children: locale("cfgpanel_close") })
4034
4152
  ]
4035
4153
  }
4036
4154
  ) })
@@ -4055,7 +4173,7 @@ function ConfigPanelInner({
4055
4173
  const theme = useMantineTheme();
4056
4174
  const color = theme.colors[theme.primaryColor][5];
4057
4175
  const version = useSettings((s) => s.resourceVersion);
4058
- const [activeTab, setActiveTab] = useState(navItems[0]?.id ?? "");
4176
+ const [activeTab, setActiveTab] = useAdminState("__dirkConfigPanel:activeTab", navItems[0]?.id ?? "");
4059
4177
  const firstMountRef = useRef(true);
4060
4178
  const [jsonOpen, setJsonOpen] = useState(false);
4061
4179
  const [historyOpen, setHistoryOpen] = useState(false);
@@ -4090,16 +4208,16 @@ function ConfigPanelInner({
4090
4208
  /* @__PURE__ */ jsx(AnimatePresence, { children: resetOpen && /* @__PURE__ */ jsx(
4091
4209
  ConfirmModal,
4092
4210
  {
4093
- title: "Reset to Defaults",
4094
- description: "This will permanently reset ALL config back to the defaults. Every value you have configured will be overwritten. This cannot be undone.",
4095
- confirmLabel: "Reset Config",
4211
+ title: locale("cfgpanel_reset_title"),
4212
+ description: locale("cfgpanel_reset_desc"),
4213
+ confirmLabel: locale("cfgpanel_reset_confirm"),
4096
4214
  confirmText: resetConfirmText,
4097
4215
  onConfirm: async () => {
4098
4216
  setResetOpen(false);
4099
4217
  const result = await resetConfig();
4100
4218
  if (result?.success) {
4101
- const { store } = getScriptConfigInstance();
4102
- form.reinitialize(cloneConfig(store.getState()));
4219
+ const { store: store2 } = getScriptConfigInstance();
4220
+ form.reinitialize(cloneConfig(store2.getState()));
4103
4221
  notifications.show({
4104
4222
  color: "green",
4105
4223
  title: locale("ConfigResetSuccessTitle"),
@@ -4124,9 +4242,9 @@ function ConfigPanelInner({
4124
4242
  /* @__PURE__ */ jsx(AnimatePresence, { children: pendingAction !== null && /* @__PURE__ */ jsx(
4125
4243
  ConfirmModal,
4126
4244
  {
4127
- title: "Discard Unsaved Changes?",
4128
- description: pendingAction === "back" ? "You have unsaved changes. Going back now will discard them." : "You have unsaved changes. Closing now will discard them.",
4129
- confirmLabel: pendingAction === "back" ? "Go Back Without Saving" : "Close Without Saving",
4245
+ title: locale("cfgpanel_discard_title"),
4246
+ description: pendingAction === "back" ? locale("cfgpanel_discard_desc_back") : locale("cfgpanel_discard_desc_close"),
4247
+ confirmLabel: pendingAction === "back" ? locale("cfgpanel_discard_confirm_back") : locale("cfgpanel_discard_confirm_close"),
4130
4248
  onConfirm: () => {
4131
4249
  const action = pendingAction;
4132
4250
  setPendingAction(null);
@@ -4164,7 +4282,7 @@ function ConfigPanelInner({
4164
4282
  /* @__PURE__ */ jsx(
4165
4283
  motion.button,
4166
4284
  {
4167
- title: "Back to script list",
4285
+ title: locale("cfgpanel_back_title"),
4168
4286
  onClick: handleBack,
4169
4287
  whileHover: { background: alpha(color, 0.16), borderColor: alpha(color, 0.45) },
4170
4288
  whileTap: { scale: 0.95 },
@@ -4192,7 +4310,7 @@ function ConfigPanelInner({
4192
4310
  /* @__PURE__ */ jsx(
4193
4311
  motion.button,
4194
4312
  {
4195
- title: "Undo",
4313
+ title: locale("cfgpanel_undo"),
4196
4314
  onClick: () => form.canBack && form.back(),
4197
4315
  disabled: !form.canBack,
4198
4316
  whileHover: form.canBack ? { background: "rgba(255,255,255,0.07)" } : void 0,
@@ -4204,7 +4322,7 @@ function ConfigPanelInner({
4204
4322
  /* @__PURE__ */ jsx(
4205
4323
  motion.button,
4206
4324
  {
4207
- title: "Redo",
4325
+ title: locale("cfgpanel_redo"),
4208
4326
  onClick: () => form.canForward && form.forward(),
4209
4327
  disabled: !form.canForward,
4210
4328
  whileHover: form.canForward ? { background: "rgba(255,255,255,0.07)" } : void 0,
@@ -4216,7 +4334,7 @@ function ConfigPanelInner({
4216
4334
  /* @__PURE__ */ jsx(
4217
4335
  motion.button,
4218
4336
  {
4219
- title: isSaving ? "Saving..." : isDirty ? `Save (${changedCount})` : "Save",
4337
+ title: isSaving ? locale("cfgpanel_saving") : isDirty ? `${locale("cfgpanel_save")} (${changedCount})` : locale("cfgpanel_save"),
4220
4338
  onClick: () => isDirty && !isSaving && form.submit(),
4221
4339
  disabled: !isDirty || isSaving,
4222
4340
  whileHover: isDirty && !isSaving ? { background: alpha(color, 0.25), borderColor: alpha(color, 0.5) } : void 0,
@@ -4228,7 +4346,7 @@ function ConfigPanelInner({
4228
4346
  /* @__PURE__ */ jsx(
4229
4347
  motion.button,
4230
4348
  {
4231
- title: "History",
4349
+ title: locale("cfgpanel_history"),
4232
4350
  onClick: () => setHistoryOpen(true),
4233
4351
  whileHover: { background: alpha("#22d3ee", 0.16), borderColor: alpha("#22d3ee", 0.5) },
4234
4352
  whileTap: { scale: 0.97 },
@@ -4249,7 +4367,7 @@ function ConfigPanelInner({
4249
4367
  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" },
4250
4368
  children: [
4251
4369
  /* @__PURE__ */ jsx(XCircle, { color: isDirty ? "#f97316" : "rgba(255,255,255,0.35)", size: "1.6vh" }),
4252
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: isDirty ? "#f97316" : "rgba(255,255,255,0.35)", children: "Discard" })
4370
+ /* @__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") })
4253
4371
  ]
4254
4372
  }
4255
4373
  ),
@@ -4263,7 +4381,7 @@ function ConfigPanelInner({
4263
4381
  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" },
4264
4382
  children: [
4265
4383
  /* @__PURE__ */ jsx(Code2, { color, size: "1.6vh" }),
4266
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: "Manual Edit" })
4384
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: locale("cfgpanel_manual_edit") })
4267
4385
  ]
4268
4386
  }
4269
4387
  ),
@@ -4276,12 +4394,13 @@ function ConfigPanelInner({
4276
4394
  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" },
4277
4395
  children: [
4278
4396
  /* @__PURE__ */ jsx(RotateCcw, { color: "#ef4444", size: "1.6vh" }),
4279
- /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "#ef4444", children: "Reset Defaults" })
4397
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "#ef4444", children: locale("cfgpanel_reset_defaults") })
4280
4398
  ]
4281
4399
  }
4282
4400
  ),
4283
4401
  version && /* @__PURE__ */ jsxs(Text, { size: "0.95vh", c: "dimmed", ta: "center", style: { letterSpacing: "0.04em", opacity: 0.8 }, children: [
4284
- "Version ",
4402
+ locale("cfgpanel_version"),
4403
+ " ",
4285
4404
  version
4286
4405
  ] })
4287
4406
  ] })
@@ -4327,13 +4446,13 @@ function ServerOnlyFetcher() {
4327
4446
  var defaultOnClose = () => fetchNui("CLOSE_ADMIN_SECTION");
4328
4447
  function ConfigPanel(props) {
4329
4448
  const { open, onClose = defaultOnClose } = props;
4330
- const { store, updateConfig } = getScriptConfigInstance();
4449
+ const { store: store2, updateConfig } = getScriptConfigInstance();
4331
4450
  const [isSaving, setIsSaving] = useState(false);
4332
4451
  if (!open) return null;
4333
- return /* @__PURE__ */ jsx(QueryClientProvider, { client: configPanelQueryClient, children: /* @__PURE__ */ jsxs(
4452
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(
4334
4453
  FormProvider,
4335
4454
  {
4336
- initialValues: cloneConfig(store.getState()),
4455
+ initialValues: cloneConfig(store2.getState()),
4337
4456
  onSubmit: async (form) => {
4338
4457
  if (isSaving) return;
4339
4458
  setIsSaving(true);
@@ -4341,7 +4460,7 @@ function ConfigPanel(props) {
4341
4460
  const result = await updateConfig(form.values);
4342
4461
  if (result?.success) {
4343
4462
  form.reinitialize(cloneConfig(form.values));
4344
- configPanelQueryClient.invalidateQueries({ queryKey: ["scriptConfigHistory"] });
4463
+ dirkQueryClient.invalidateQueries({ queryKey: ["scriptConfigHistory"] });
4345
4464
  useMissingItemsAudit.getState().refresh();
4346
4465
  notifications.show({
4347
4466
  color: "green",
@@ -4351,7 +4470,7 @@ function ConfigPanel(props) {
4351
4470
  });
4352
4471
  return;
4353
4472
  }
4354
- form.reinitialize(cloneConfig(store.getState()));
4473
+ form.reinitialize(cloneConfig(store2.getState()));
4355
4474
  const err = result?._error || "Unknown";
4356
4475
  console.warn(`[ConfigPanel] config save failed: ${err}`);
4357
4476
  const titleKey = err === "NoPermission" ? "ConfigSaveNoPermissionTitle" : err === "VersionConflict" ? "ConfigSaveVersionConflictTitle" : err === "NotReady" ? "ConfigSaveNotReadyTitle" : "ConfigSaveFailedTitle";
@@ -4848,6 +4967,9 @@ function GroupName(props) {
4848
4967
  const ctx = useContext(GroupSelectContext);
4849
4968
  const jobs = useFrameworkGroups((s) => s.jobs);
4850
4969
  const gangs = useFrameworkGroups((s) => s.gangs);
4970
+ useEffect(() => {
4971
+ ensureFrameworkGroups();
4972
+ }, []);
4851
4973
  const inCompound = ctx !== null;
4852
4974
  const currentValue = inCompound ? ctx.value.name : props.value;
4853
4975
  const filterType = inCompound ? ctx.type : props.type;
@@ -4887,6 +5009,9 @@ function GroupRank(props) {
4887
5009
  }
4888
5010
  const jobs = useFrameworkGroups((s) => s.jobs);
4889
5011
  const gangs = useFrameworkGroups((s) => s.gangs);
5012
+ useEffect(() => {
5013
+ ensureFrameworkGroups();
5014
+ }, []);
4890
5015
  const all = [...jobs, ...gangs];
4891
5016
  const selectedGroup = all.find((g) => g.name === ctx.value.name) ?? null;
4892
5017
  const grades = selectedGroup?.grades ?? [];
@@ -6532,7 +6657,733 @@ function AccountSelect(props) {
6532
6657
  !hideFrameworkHint && /* @__PURE__ */ jsx(FrameworkHint, { framework })
6533
6658
  ] });
6534
6659
  }
6660
+ var BODY_HIDE_STYLE_ID = "dirk-instruction-panel-style";
6661
+ var BODY_HIDE_ATTR = "data-dirk-instruction-active";
6662
+ var OVERLAY_ATTR = "data-dirk-instruction-overlay";
6663
+ function ensureBodyHideStyle() {
6664
+ if (document.getElementById(BODY_HIDE_STYLE_ID)) return;
6665
+ const el = document.createElement("style");
6666
+ el.id = BODY_HIDE_STYLE_ID;
6667
+ el.textContent = `
6668
+ body[${BODY_HIDE_ATTR}] > *:not([${OVERLAY_ATTR}]) {
6669
+ visibility: hidden !important;
6670
+ opacity: 0 !important;
6671
+ pointer-events: none !important;
6672
+ }
6673
+ `;
6674
+ document.head.appendChild(el);
6675
+ }
6676
+ function InstructionPanel({
6677
+ visible,
6678
+ title,
6679
+ hint,
6680
+ keys,
6681
+ icon: Icon = MapPin,
6682
+ hideRestOfAdmin = true
6683
+ }) {
6684
+ const theme = useMantineTheme();
6685
+ const pc = theme.colors[theme.primaryColor];
6686
+ useEffect(() => {
6687
+ if (!visible || !hideRestOfAdmin) return;
6688
+ ensureBodyHideStyle();
6689
+ document.body.setAttribute(BODY_HIDE_ATTR, "");
6690
+ return () => {
6691
+ document.body.removeAttribute(BODY_HIDE_ATTR);
6692
+ };
6693
+ }, [visible, hideRestOfAdmin]);
6694
+ if (!visible) return null;
6695
+ return createPortal(
6696
+ /* @__PURE__ */ jsx(
6697
+ "div",
6698
+ {
6699
+ ...{ [OVERLAY_ATTR]: "" },
6700
+ style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex: 1e4 },
6701
+ children: /* @__PURE__ */ jsx(AnimatePresence, { children: /* @__PURE__ */ jsx(
6702
+ motion.div,
6703
+ {
6704
+ initial: { opacity: 0, y: 12, scale: 0.92 },
6705
+ animate: { opacity: 1, y: 0, scale: 1 },
6706
+ exit: { opacity: 0, y: 12, scale: 0.92 },
6707
+ transition: { duration: 0.25, ease: "easeInOut" },
6708
+ style: { position: "absolute", bottom: "3vh", right: "3vh", userSelect: "none" },
6709
+ children: /* @__PURE__ */ jsxs(
6710
+ Flex,
6711
+ {
6712
+ direction: "column",
6713
+ gap: "0.8vh",
6714
+ style: {
6715
+ background: alpha(theme.colors.dark[9], 0.55),
6716
+ border: "0.1vh solid rgba(255,255,255,0.07)",
6717
+ borderRadius: theme.radius.sm,
6718
+ boxShadow: "0 0.74vh 2.96vh rgba(0,0,0,0.5)",
6719
+ padding: "1.4vh 1.6vh",
6720
+ minWidth: "22vh",
6721
+ maxWidth: "28vh"
6722
+ },
6723
+ children: [
6724
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "0.6vh", children: [
6725
+ /* @__PURE__ */ jsx(Icon, { size: "1.6vh", color: pc[6], strokeWidth: 2.5 }),
6726
+ /* @__PURE__ */ jsx(
6727
+ Text,
6728
+ {
6729
+ style: {
6730
+ fontFamily: "Akrobat Bold, sans-serif",
6731
+ fontSize: "1.7vh",
6732
+ fontWeight: 700,
6733
+ letterSpacing: "0.14em",
6734
+ textTransform: "uppercase",
6735
+ color: pc[6],
6736
+ textShadow: `0 0 0.8vh ${alpha(pc[7], 0.5)}, 0 0 1.6vh ${alpha(pc[9], 0.3)}`
6737
+ },
6738
+ children: title
6739
+ }
6740
+ )
6741
+ ] }),
6742
+ (hint || keys && keys.length > 0) && /* @__PURE__ */ jsx("div", { style: { height: "0.1vh", background: "rgba(255,255,255,0.06)", margin: "0.1vh 0" } }),
6743
+ hint && /* @__PURE__ */ jsx(
6744
+ Text,
6745
+ {
6746
+ style: {
6747
+ fontFamily: "Akrobat Bold, sans-serif",
6748
+ fontSize: "1.05vh",
6749
+ color: "rgba(255,255,255,0.45)",
6750
+ letterSpacing: "0.06em",
6751
+ textTransform: "uppercase",
6752
+ lineHeight: 1.4
6753
+ },
6754
+ children: hint
6755
+ }
6756
+ ),
6757
+ keys && keys.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
6758
+ hint && /* @__PURE__ */ jsx("div", { style: { height: "0.1vh", background: "rgba(255,255,255,0.06)", margin: "0.1vh 0" } }),
6759
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "0.5vh", children: keys.map((k, i) => /* @__PURE__ */ jsx(InstructionKeyRow, { keyLabel: k.key, action: k.action }, i)) })
6760
+ ] })
6761
+ ]
6762
+ }
6763
+ )
6764
+ },
6765
+ "instruction-card"
6766
+ ) })
6767
+ }
6768
+ ),
6769
+ document.body
6770
+ );
6771
+ }
6772
+ function renderKeyContent(keyLabel) {
6773
+ const normalized = keyLabel.trim().toUpperCase();
6774
+ if (normalized === "LMB") return /* @__PURE__ */ jsx(MouseIcon, { side: "left" });
6775
+ if (normalized === "RMB") return /* @__PURE__ */ jsx(MouseIcon, { side: "right" });
6776
+ if (normalized === "BACKSPACE" || keyLabel === "\u232B") return /* @__PURE__ */ jsx(BackspaceIcon, {});
6777
+ return /* @__PURE__ */ jsx(
6778
+ Text,
6779
+ {
6780
+ style: {
6781
+ fontFamily: "Akrobat Bold, sans-serif",
6782
+ fontSize: "1.2vh",
6783
+ color: "rgba(255,255,255,0.85)",
6784
+ lineHeight: 1,
6785
+ padding: "0 0.3vh"
6786
+ },
6787
+ children: keyLabel
6788
+ }
6789
+ );
6790
+ }
6791
+ function InstructionKeyRow({ keyLabel, action }) {
6792
+ const normalized = keyLabel.trim().toUpperCase();
6793
+ const isIconKey = normalized === "LMB" || normalized === "RMB" || normalized === "BACKSPACE" || keyLabel === "\u232B";
6794
+ const minWidth = isIconKey ? "2.6vh" : "2.4vh";
6795
+ return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "0.6vh", children: [
6796
+ /* @__PURE__ */ jsx(
6797
+ "div",
6798
+ {
6799
+ style: {
6800
+ minWidth,
6801
+ height: "2.4vh",
6802
+ padding: "0 0.4vh",
6803
+ borderRadius: "0.3vh",
6804
+ border: "0.15vh solid rgba(255,255,255,0.35)",
6805
+ background: "rgba(255,255,255,0.06)",
6806
+ display: "flex",
6807
+ alignItems: "center",
6808
+ justifyContent: "center",
6809
+ opacity: 0.85,
6810
+ filter: "drop-shadow(0 0 0.3vh rgba(0,0,0,0.5))",
6811
+ flexShrink: 0,
6812
+ boxSizing: "border-box"
6813
+ },
6814
+ children: renderKeyContent(keyLabel)
6815
+ }
6816
+ ),
6817
+ /* @__PURE__ */ jsx(
6818
+ Text,
6819
+ {
6820
+ style: {
6821
+ fontFamily: "Akrobat Bold, sans-serif",
6822
+ fontSize: "1.05vh",
6823
+ color: "rgba(255,255,255,0.45)",
6824
+ letterSpacing: "0.06em",
6825
+ textTransform: "uppercase"
6826
+ },
6827
+ children: action
6828
+ }
6829
+ )
6830
+ ] });
6831
+ }
6832
+ function MouseIcon({ side }) {
6833
+ const stroke = "rgba(255,255,255,0.85)";
6834
+ const fillActive = "rgba(255,255,255,0.85)";
6835
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 22", width: "1.4vh", height: "1.9vh", fill: "none", stroke, strokeWidth: "1.4", strokeLinejoin: "round", children: [
6836
+ /* @__PURE__ */ jsx("rect", { x: "1", y: "1", width: "14", height: "20", rx: "6" }),
6837
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "1", x2: "8", y2: "9" }),
6838
+ /* @__PURE__ */ jsx(
6839
+ "path",
6840
+ {
6841
+ d: side === "left" ? "M 7.4 1.6 L 2 1.6 A 5 5 0 0 0 1 6 L 1 9 L 7.4 9 Z" : "M 8.6 1.6 L 14 1.6 A 5 5 0 0 1 15 6 L 15 9 L 8.6 9 Z",
6842
+ fill: fillActive,
6843
+ stroke: "none"
6844
+ }
6845
+ )
6846
+ ] });
6847
+ }
6848
+ function BackspaceIcon() {
6849
+ const stroke = "rgba(255,255,255,0.85)";
6850
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 22 16", width: "1.7vh", height: "1.3vh", fill: "none", stroke, strokeWidth: "1.4", strokeLinejoin: "round", strokeLinecap: "round", children: [
6851
+ /* @__PURE__ */ jsx("path", { d: "M 21 2 L 8 2 L 2 8 L 8 14 L 21 14 Z" }),
6852
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "6", x2: "16", y2: "11" }),
6853
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "6", x2: "11", y2: "11" })
6854
+ ] });
6855
+ }
6856
+ function WorldPositionPicker({ value, onChange, compact, setOnly, gotoOnly }) {
6857
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6858
+ !gotoOnly && /* @__PURE__ */ jsx(WorldPositionSetButton2, { value, onChange, compact }),
6859
+ !setOnly && /* @__PURE__ */ jsx(WorldPositionGotoButton2, { value, compact })
6860
+ ] });
6861
+ }
6862
+ function WorldPositionSetButton2({
6863
+ value,
6864
+ onChange,
6865
+ compact
6866
+ }) {
6867
+ const theme = useMantineTheme();
6868
+ const color = theme.colors[theme.primaryColor][5];
6869
+ const begin = useAdminToolStore((s) => s.begin);
6870
+ const onClick = async () => {
6871
+ const instructions = {
6872
+ title: locale("PickPositionTitle") || "Pick Position",
6873
+ hint: locale("PickPositionHint") || "Walk to where you want this set",
6874
+ keys: [
6875
+ { key: "E", action: locale("Set") || "Set" },
6876
+ { key: "\u232B", action: locale("Cancel") || "Cancel" }
6877
+ ]
6878
+ };
6879
+ const pendingResult = begin({ id: "capturePosition", ...instructions });
6880
+ fetchNui("ADMIN_TOOL_BEGIN", { id: "capturePosition", instructions }).catch(() => {
6881
+ useAdminToolStore.getState().cancelActive();
6882
+ });
6883
+ const result = await pendingResult;
6884
+ if (result) onChange(result);
6885
+ };
6886
+ return /* @__PURE__ */ jsx(Tooltip, { label: locale("SetWorldTooltip"), position: "top", withArrow: true, withinPortal: true, zIndex: 2e3, children: /* @__PURE__ */ jsxs(
6887
+ motion.button,
6888
+ {
6889
+ onClick,
6890
+ whileHover: { background: alpha(color, 0.18) },
6891
+ whileTap: { scale: 0.95 },
6892
+ style: {
6893
+ background: alpha(color, 0.1),
6894
+ border: `0.1vh solid ${alpha(color, 0.35)}`,
6895
+ borderRadius: theme.radius.xs,
6896
+ padding: compact ? "0.25vh 0.6vh" : "0.5vh 0.8vh",
6897
+ cursor: "pointer",
6898
+ display: "flex",
6899
+ alignItems: "center",
6900
+ gap: compact ? "0.3vh" : "0.4vh"
6901
+ },
6902
+ children: [
6903
+ /* @__PURE__ */ jsx(Crosshair, { size: compact ? "1.1vh" : "1.3vh", color }),
6904
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: locale("Set") })
6905
+ ]
6906
+ }
6907
+ ) });
6908
+ }
6909
+ function WorldPositionGotoButton2({
6910
+ value,
6911
+ compact
6912
+ }) {
6913
+ const theme = useMantineTheme();
6914
+ const color = theme.colors[theme.primaryColor][5];
6915
+ const onClick = () => {
6916
+ fetchNui("ADMIN_TOOL_INVOKE", { id: "gotoCoord", value }).catch(() => {
6917
+ });
6918
+ };
6919
+ return /* @__PURE__ */ jsx(Tooltip, { label: locale("GotoWorldTooltip"), position: "top", withArrow: true, withinPortal: true, zIndex: 2e3, children: /* @__PURE__ */ jsxs(
6920
+ motion.button,
6921
+ {
6922
+ onClick,
6923
+ whileHover: { background: alpha(color, 0.18) },
6924
+ whileTap: { scale: 0.95 },
6925
+ style: {
6926
+ background: alpha(color, 0.1),
6927
+ border: `0.1vh solid ${alpha(color, 0.35)}`,
6928
+ borderRadius: theme.radius.xs,
6929
+ padding: compact ? "0.25vh 0.6vh" : "0.5vh 0.8vh",
6930
+ cursor: "pointer",
6931
+ display: "flex",
6932
+ alignItems: "center",
6933
+ gap: compact ? "0.3vh" : "0.4vh"
6934
+ },
6935
+ children: [
6936
+ /* @__PURE__ */ jsx(MapPin, { size: compact ? "1.1vh" : "1.3vh", color }),
6937
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: locale("Goto") })
6938
+ ]
6939
+ }
6940
+ ) });
6941
+ }
6942
+ function usePlayers(opts = {}) {
6943
+ const {
6944
+ includeOffline = false,
6945
+ search = "",
6946
+ limit = 50,
6947
+ staleTimeMs,
6948
+ refetchIntervalMs
6949
+ } = opts;
6950
+ const query = useQuery({
6951
+ queryKey: includeOffline ? ["dirk:players", "search", search.trim().toLowerCase(), limit] : ["dirk:players", "online"],
6952
+ queryFn: async () => {
6953
+ const toolId = includeOffline ? "searchPlayers" : "getOnlinePlayers";
6954
+ const payload = includeOffline ? { id: toolId, value: { search: search.trim(), limit } } : { id: toolId };
6955
+ const result = await fetchNui(
6956
+ "ADMIN_TOOL_QUERY",
6957
+ payload,
6958
+ // Browser-dev fallback. Returns a couple of mock players so the
6959
+ // dev shell isn't blank.
6960
+ includeOffline ? [
6961
+ { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true },
6962
+ { id: null, citizenId: "DEF67890", name: "", charName: "Jane Offline", online: false }
6963
+ ] : [
6964
+ { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true }
6965
+ ]
6966
+ );
6967
+ return Array.isArray(result) ? result : [];
6968
+ },
6969
+ staleTime: staleTimeMs ?? (includeOffline ? 3e4 : 5e3),
6970
+ gcTime: 5 * 6e4,
6971
+ refetchInterval: refetchIntervalMs ?? false,
6972
+ refetchOnWindowFocus: false,
6973
+ placeholderData: includeOffline ? keepPreviousData : void 0
6974
+ });
6975
+ return {
6976
+ players: query.data ?? [],
6977
+ isLoading: query.isLoading,
6978
+ isFetching: query.isFetching,
6979
+ error: query.error ?? null,
6980
+ refresh: () => query.refetch()
6981
+ };
6982
+ }
6983
+ var DEBOUNCE_MS = 300;
6984
+ var ONLINE_COLOR = "#3FA83F";
6985
+ var OFFLINE_COLOR = "#E54141";
6986
+ function PlayerSelect({
6987
+ value,
6988
+ onChange,
6989
+ includeOffline = false,
6990
+ limit = 50,
6991
+ searchable: searchableProp,
6992
+ placeholder,
6993
+ nothingFoundMessage: nothingFoundMessageProp,
6994
+ loadingLabel = "Loading\u2026",
6995
+ onlineLabel = "Online",
6996
+ offlineLabel = "Offline",
6997
+ refreshLabel = "Refresh player list",
6998
+ ...rest
6999
+ }) {
7000
+ const theme = useMantineTheme();
7001
+ const color = theme.colors[theme.primaryColor][5];
7002
+ const [searchInput, setSearchInput] = useState("");
7003
+ const [debouncedSearch, setDebouncedSearch] = useState("");
7004
+ const { players, isFetching, refresh } = usePlayers({
7005
+ includeOffline,
7006
+ search: includeOffline ? debouncedSearch : void 0,
7007
+ limit
7008
+ });
7009
+ const sortedPlayers = useMemo(
7010
+ () => [...players].sort((a, b) => {
7011
+ if (a.online !== b.online) return a.online ? -1 : 1;
7012
+ return a.charName.localeCompare(b.charName);
7013
+ }),
7014
+ [players]
7015
+ );
7016
+ const playerByCitizen = useMemo(() => {
7017
+ const m = /* @__PURE__ */ new Map();
7018
+ for (const p of sortedPlayers) m.set(p.citizenId, p);
7019
+ return m;
7020
+ }, [sortedPlayers]);
7021
+ const data = useMemo(() => {
7022
+ const items = sortedPlayers.map((p) => ({
7023
+ value: p.citizenId,
7024
+ label: formatLabel(p)
7025
+ }));
7026
+ if (value && !items.some((i) => i.value === value)) {
7027
+ items.unshift({ value, label: value });
7028
+ }
7029
+ return items;
7030
+ }, [sortedPlayers, value]);
7031
+ const selectedPlayer = value ? playerByCitizen.get(value) ?? null : null;
7032
+ const selectedLabel = selectedPlayer ? formatLabel(selectedPlayer) : null;
7033
+ useEffect(() => {
7034
+ if (!includeOffline) return;
7035
+ if (selectedLabel && searchInput === selectedLabel) return;
7036
+ const t3 = setTimeout(() => setDebouncedSearch(searchInput), DEBOUNCE_MS);
7037
+ return () => clearTimeout(t3);
7038
+ }, [searchInput, includeOffline, selectedLabel]);
7039
+ const renderOption = ({ option }) => {
7040
+ const p = playerByCitizen.get(option.value);
7041
+ if (!p) return option.label;
7042
+ return /* @__PURE__ */ jsxs(Group, { justify: "space-between", wrap: "nowrap", gap: "xs", style: { width: "100%" }, children: [
7043
+ /* @__PURE__ */ jsx(Text, { size: "xs", truncate: true, style: { flex: 1 }, children: formatLabel(p) }),
7044
+ /* @__PURE__ */ jsx(StatusDot, { online: p.online, onlineLabel, offlineLabel })
7045
+ ] });
7046
+ };
7047
+ return /* @__PURE__ */ jsx(
7048
+ Select,
7049
+ {
7050
+ ...rest,
7051
+ data,
7052
+ value: value ?? null,
7053
+ onChange: (v) => {
7054
+ if (!v) return onChange(null);
7055
+ const player = playerByCitizen.get(v) ?? null;
7056
+ onChange(player);
7057
+ },
7058
+ searchable: searchableProp ?? true,
7059
+ searchValue: includeOffline ? searchInput : void 0,
7060
+ onSearchChange: includeOffline ? setSearchInput : void 0,
7061
+ placeholder,
7062
+ nothingFoundMessage: isFetching ? loadingLabel : nothingFoundMessageProp ?? "No players found",
7063
+ maxDropdownHeight: 300,
7064
+ renderOption,
7065
+ leftSection: selectedPlayer ? /* @__PURE__ */ jsx(StatusDot, { online: selectedPlayer.online, onlineLabel, offlineLabel }) : void 0,
7066
+ rightSectionWidth: "3.5vh",
7067
+ rightSectionPointerEvents: "all",
7068
+ rightSection: /* @__PURE__ */ jsx(
7069
+ ActionIcon,
7070
+ {
7071
+ variant: "subtle",
7072
+ size: "sm",
7073
+ onClick: (e) => {
7074
+ e.stopPropagation();
7075
+ refresh();
7076
+ },
7077
+ "aria-label": refreshLabel,
7078
+ title: refreshLabel,
7079
+ style: { marginRight: "0.6vh" },
7080
+ children: isFetching ? /* @__PURE__ */ jsx(Loader, { size: "1.2vh", color }) : /* @__PURE__ */ jsx(RefreshCw, { size: "1.4vh", color })
7081
+ }
7082
+ )
7083
+ }
7084
+ );
7085
+ }
7086
+ function StatusDot({ online, onlineLabel, offlineLabel }) {
7087
+ return /* @__PURE__ */ jsx(
7088
+ "span",
7089
+ {
7090
+ title: online ? onlineLabel : offlineLabel,
7091
+ style: {
7092
+ display: "inline-block",
7093
+ width: "0.9vh",
7094
+ height: "0.9vh",
7095
+ borderRadius: "50%",
7096
+ backgroundColor: online ? ONLINE_COLOR : OFFLINE_COLOR,
7097
+ boxShadow: `0 0 0.4vh ${online ? ONLINE_COLOR : OFFLINE_COLOR}`,
7098
+ flexShrink: 0
7099
+ }
7100
+ }
7101
+ );
7102
+ }
7103
+ function formatLabel(p) {
7104
+ const parts = [p.charName || p.citizenId];
7105
+ if (p.online) {
7106
+ if (p.name) parts.push(p.name);
7107
+ if (p.id != null) parts.push(`#${p.id}`);
7108
+ }
7109
+ return parts.join(" \xB7 ");
7110
+ }
7111
+ function usePickDoor() {
7112
+ const begin = useAdminToolStore((s) => s.begin);
7113
+ return async () => {
7114
+ const pending = begin({
7115
+ id: "pickDoor",
7116
+ title: locale("PickDoorTitle"),
7117
+ hint: locale("PickDoorHint"),
7118
+ keys: [
7119
+ { key: "LMB", action: locale("Toggle") },
7120
+ { key: "E", action: locale("Confirm") },
7121
+ { key: "BACKSPACE", action: locale("Cancel") }
7122
+ ]
7123
+ });
7124
+ fetchNui("ADMIN_TOOL_BEGIN", { id: "pickDoor" }).catch(() => {
7125
+ useAdminToolStore.getState().cancelActive();
7126
+ });
7127
+ return await pending;
7128
+ };
7129
+ }
7130
+ function DoorPickerButton({
7131
+ onPick,
7132
+ compact,
7133
+ label,
7134
+ tooltip
7135
+ }) {
7136
+ const theme = useMantineTheme();
7137
+ const color = theme.colors[theme.primaryColor][5];
7138
+ const pickDoor = usePickDoor();
7139
+ const onClick = async () => {
7140
+ const picked = await pickDoor();
7141
+ if (picked && picked.doors.length > 0) onPick(picked);
7142
+ };
7143
+ return /* @__PURE__ */ jsx(
7144
+ Tooltip,
7145
+ {
7146
+ label: tooltip ?? locale("PickDoorTooltip"),
7147
+ position: "top",
7148
+ withArrow: true,
7149
+ withinPortal: true,
7150
+ zIndex: 2e3,
7151
+ children: /* @__PURE__ */ jsxs(
7152
+ motion.button,
7153
+ {
7154
+ onClick,
7155
+ whileHover: { background: alpha(color, 0.18) },
7156
+ whileTap: { scale: 0.95 },
7157
+ style: {
7158
+ background: alpha(color, 0.1),
7159
+ border: `0.1vh solid ${alpha(color, 0.35)}`,
7160
+ borderRadius: theme.radius.xs,
7161
+ padding: compact ? "0.25vh 0.6vh" : "0.5vh 0.8vh",
7162
+ cursor: "pointer",
7163
+ display: "flex",
7164
+ alignItems: "center",
7165
+ gap: compact ? "0.3vh" : "0.4vh"
7166
+ },
7167
+ children: [
7168
+ /* @__PURE__ */ jsx(DoorOpen, { size: compact ? "1.1vh" : "1.3vh", color }),
7169
+ /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: color, children: label ?? locale("PickDoor") })
7170
+ ]
7171
+ }
7172
+ )
7173
+ }
7174
+ );
7175
+ }
7176
+ var MapImpl = memo(({
7177
+ children,
7178
+ initialZoom = 2,
7179
+ initialCenter = gameToMap(0, 0),
7180
+ mapStyle = "game"
7181
+ }) => {
7182
+ const mapRef = useRef(null);
7183
+ return /* @__PURE__ */ jsxs(
7184
+ MapContainer,
7185
+ {
7186
+ maxBoundsViscosity: 1,
7187
+ preferCanvas: true,
7188
+ zoom: Math.round(initialZoom),
7189
+ zoomSnap: 1,
7190
+ zoomDelta: 1,
7191
+ zoomControl: false,
7192
+ crs: CRS.Simple,
7193
+ style: {
7194
+ height: "100%",
7195
+ width: "100%",
7196
+ overflow: "hidden",
7197
+ outline: "none !important",
7198
+ border: "none !important",
7199
+ boxShadow: "none !important",
7200
+ backgroundColor: "#384950"
7201
+ },
7202
+ center: initialCenter,
7203
+ attributionControl: false,
7204
+ doubleClickZoom: false,
7205
+ inertia: false,
7206
+ zoomAnimation: false,
7207
+ ref: mapRef,
7208
+ maxBounds: [[-250, -250], [250, 250]],
7209
+ children: [
7210
+ /* @__PURE__ */ jsx(MapLayer, { mapLayer: mapStyle }),
7211
+ children
7212
+ ]
7213
+ }
7214
+ );
7215
+ });
7216
+ MapImpl.displayName = "DirkMap";
7217
+ var Map2 = MapImpl;
7218
+ var MapLayer = ({ mapLayer }) => {
7219
+ const map = useMap();
7220
+ const layerRef = useRef(null);
7221
+ useEffect(() => {
7222
+ if (layerRef.current) {
7223
+ map.removeLayer(layerRef.current);
7224
+ }
7225
+ const layer = tileLayer(
7226
+ `https://s.rsg.sc/sc/images/games/GTAV/map/${mapLayer}/{z}/{x}/{y}.jpg`,
7227
+ {
7228
+ maxZoom: 6,
7229
+ minZoom: 4,
7230
+ bounds: latLngBounds(latLng(0, 128), latLng(-192, 0)),
7231
+ tileSize: 256,
7232
+ updateWhenZooming: false,
7233
+ keepBuffer: 2,
7234
+ opacity: 0.75
7235
+ }
7236
+ );
7237
+ layer.addTo(map);
7238
+ layerRef.current = layer;
7239
+ return () => {
7240
+ if (layerRef.current) {
7241
+ map.removeLayer(layerRef.current);
7242
+ }
7243
+ };
7244
+ }, [mapLayer, map]);
7245
+ return null;
7246
+ };
7247
+ function ZoomControls() {
7248
+ const theme = useMantineTheme();
7249
+ const map = useMap();
7250
+ const buttons = [
7251
+ { Icon: Plus, fn: () => map.zoomIn() },
7252
+ { Icon: Minus, fn: () => map.zoomOut() }
7253
+ ];
7254
+ return /* @__PURE__ */ jsx(
7255
+ motion.div,
7256
+ {
7257
+ style: {
7258
+ position: "absolute",
7259
+ right: "2vh",
7260
+ top: "2vh",
7261
+ display: "flex",
7262
+ flexDirection: "column",
7263
+ zIndex: 999999,
7264
+ boxShadow: `0 0 1vh ${alpha(theme.colors.dark[9], 0.85)}`,
7265
+ background: alpha(theme.colors.dark[9], 0.85),
7266
+ borderRadius: theme.radius.xs
7267
+ },
7268
+ initial: { opacity: 0, y: -20 },
7269
+ animate: { opacity: 1, y: 0 },
7270
+ exit: { opacity: 0, y: -20 },
7271
+ children: buttons.map(({ Icon, fn }, i) => /* @__PURE__ */ jsx(
7272
+ motion.div,
7273
+ {
7274
+ whileHover: { scale: 1.1, filter: "brightness(1.5)" },
7275
+ onClick: fn,
7276
+ style: {
7277
+ padding: theme.spacing.xs,
7278
+ cursor: "pointer",
7279
+ display: "flex"
7280
+ },
7281
+ children: /* @__PURE__ */ jsx(Icon, { size: 34, color: theme.colors.gray[5] })
7282
+ },
7283
+ i
7284
+ ))
7285
+ }
7286
+ );
7287
+ }
7288
+ var DEFAULT_SPRITE = 162;
7289
+ var DEFAULT_COLOR = 5;
7290
+ function BlipMarker({
7291
+ position,
7292
+ sprite,
7293
+ color,
7294
+ scale = 1,
7295
+ onClick,
7296
+ selected,
7297
+ disabled,
7298
+ fallbackSprite = DEFAULT_SPRITE,
7299
+ fallbackColor = DEFAULT_COLOR
7300
+ }) {
7301
+ const [hovered, setHovered] = useState(false);
7302
+ const mapCoords = useMemo(() => gameToMap(position.x, position.y), [position.x, position.y]);
7303
+ const effectiveSprite = sprite ?? fallbackSprite;
7304
+ const effectiveColor = color ?? fallbackColor;
7305
+ const url = blipUrlForSprite(effectiveSprite);
7306
+ const colorHex = getBlipColor(effectiveColor)?.hex ?? "#ffffff";
7307
+ const handleClick = (e) => {
7308
+ e.originalEvent?.stopPropagation?.();
7309
+ if (disabled) return;
7310
+ onClick?.();
7311
+ };
7312
+ const baseSize = 1.8 * scale;
7313
+ const size = `${baseSize}vh`;
7314
+ const ringSize = `${baseSize * 1.6}vh`;
7315
+ return /* @__PURE__ */ jsx(
7316
+ Marker,
7317
+ {
7318
+ position: mapCoords,
7319
+ eventHandlers: onClick ? { click: handleClick } : void 0,
7320
+ icon: /* @__PURE__ */ jsxs(
7321
+ motion.div,
7322
+ {
7323
+ onHoverStart: () => setHovered(true),
7324
+ onHoverEnd: () => setHovered(false),
7325
+ style: {
7326
+ position: "relative",
7327
+ display: "flex",
7328
+ alignItems: "center",
7329
+ justifyContent: "center",
7330
+ cursor: disabled ? "not-allowed" : onClick ? "pointer" : "default",
7331
+ opacity: disabled ? 0.35 : 1,
7332
+ width: size,
7333
+ height: size
7334
+ },
7335
+ animate: { scale: hovered && !disabled ? 1.2 : selected ? 1.15 : 1 },
7336
+ transition: { duration: 0.15, ease: "easeOut" },
7337
+ children: [
7338
+ (selected || hovered) && !disabled && /* @__PURE__ */ jsx(
7339
+ motion.div,
7340
+ {
7341
+ style: {
7342
+ position: "absolute",
7343
+ width: ringSize,
7344
+ height: ringSize,
7345
+ borderRadius: "50%",
7346
+ border: `0.18vh solid ${alpha(colorHex, 0.85)}`,
7347
+ boxShadow: `0 0 1vh ${alpha(colorHex, 0.55)}`
7348
+ },
7349
+ initial: { opacity: 0, scale: 0.8 },
7350
+ animate: { opacity: 1, scale: 1 }
7351
+ }
7352
+ ),
7353
+ url && /* @__PURE__ */ jsx(
7354
+ "div",
7355
+ {
7356
+ style: {
7357
+ width: size,
7358
+ height: size,
7359
+ backgroundColor: colorHex,
7360
+ WebkitMaskImage: `url(${url})`,
7361
+ maskImage: `url(${url})`,
7362
+ WebkitMaskRepeat: "no-repeat",
7363
+ maskRepeat: "no-repeat",
7364
+ WebkitMaskPosition: "center",
7365
+ maskPosition: "center",
7366
+ WebkitMaskSize: "contain",
7367
+ maskSize: "contain",
7368
+ filter: [
7369
+ "drop-shadow(0.12vh 0 0 #000)",
7370
+ "drop-shadow(-0.12vh 0 0 #000)",
7371
+ "drop-shadow(0 0.12vh 0 #000)",
7372
+ "drop-shadow(0 -0.12vh 0 #000)",
7373
+ `drop-shadow(0 0 0.4vh ${alpha("#000", 0.7)})`
7374
+ ].join(" "),
7375
+ pointerEvents: "none",
7376
+ userSelect: "none"
7377
+ }
7378
+ }
7379
+ )
7380
+ ]
7381
+ }
7382
+ )
7383
+ }
7384
+ );
7385
+ }
6535
7386
 
6536
- export { AccountSelect, AdminPageTitle, AnimPostFxSelect, AsyncSaveButton, BlipColorSelect, BlipDisplaySelect, BlipIconSelect, BorderedIcon, ConfigPanel, ConfirmModal, ControlMultiSelect, ControlSelect, Counter, DiscordRoleSelect, FiveMKeyBindInput, FloatingParticles, GroupName, GroupRank, GroupSelect, InfoBox, InputContainer, LevelBanner, LevelPanel, MissingItemsBanner, Modal, ModalContext, ModalProvider, MotionFlex, MotionIcon, MotionImage, MotionText, NavBar, NavigationContext, NavigationProvider, PositionPicker, PromptModal, ScenarioSelect, SegmentedControl, SegmentedProgress, SelectItem, SwitchPanel, TestBed, ThemeOverrideSection, Title, Vector4DeleteButton, Vector4Display, WorldPositionGotoButton, WorldPositionSetButton, useMissingItemsAudit, useModal, useModalActions, useNavigation, useNavigationStore };
7387
+ 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 };
6537
7388
  //# sourceMappingURL=index.js.map
6538
7389
  //# sourceMappingURL=index.js.map