dirk-cfx-react 1.1.90 → 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
@@ -3154,11 +3170,11 @@ function ConfirmModal({
3154
3170
  placeholder: confirmText,
3155
3171
  value: typed,
3156
3172
  onChange: (e) => setTyped(e.currentTarget.value),
3157
- styles: (t3) => ({
3173
+ styles: (t4) => ({
3158
3174
  input: {
3159
- backgroundColor: alpha(t3.colors.dark[7], 0.5),
3175
+ backgroundColor: alpha(t4.colors.dark[7], 0.5),
3160
3176
  border: `0.1vh solid ${alpha(
3161
- typed === confirmText ? "#ef4444" : t3.colors.dark[5],
3177
+ typed === confirmText ? "#ef4444" : t4.colors.dark[5],
3162
3178
  0.5
3163
3179
  )}`,
3164
3180
  color: "rgba(255,255,255,0.85)",
@@ -3803,7 +3819,7 @@ function useAdminState(key, initial) {
3803
3819
  useEffect(() => {
3804
3820
  const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
3805
3821
  listeners.set(key, set);
3806
- const handler = () => setTick((t3) => t3 + 1);
3822
+ const handler = () => setTick((t4) => t4 + 1);
3807
3823
  set.add(handler);
3808
3824
  return () => {
3809
3825
  set.delete(handler);
@@ -5491,24 +5507,6 @@ var MANTINE_COLOR_OPTIONS = [
5491
5507
  "yellow",
5492
5508
  "orange"
5493
5509
  ].map((value) => ({ value, label: value }));
5494
- var DEFAULT_PALETTE = [
5495
- "#f0f4ff",
5496
- "#d9e3ff",
5497
- "#bfcfff",
5498
- "#a6bbff",
5499
- "#8ca7ff",
5500
- "#7393ff",
5501
- "#5a7fff",
5502
- "#406bff",
5503
- "#2547ff",
5504
- "#0b33ff"
5505
- ];
5506
- var DEFAULT_VALUE = {
5507
- useOverride: false,
5508
- primaryColor: "dirk",
5509
- primaryShade: 5,
5510
- customTheme: DEFAULT_PALETTE
5511
- };
5512
5510
  function GroupLabel({ label }) {
5513
5511
  return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "xs", mt: "xxs", children: [
5514
5512
  /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.07em", c: "rgba(255,255,255,0.2)", children: label }),
@@ -5523,10 +5521,10 @@ function ThemeOverrideSection({
5523
5521
  const color = mantineTheme.colors[mantineTheme.primaryColor][5];
5524
5522
  const raw = useFormField(schemaKey);
5525
5523
  const value = {
5526
- useOverride: raw?.useOverride ?? DEFAULT_VALUE.useOverride,
5527
- primaryColor: raw?.primaryColor ?? DEFAULT_VALUE.primaryColor,
5528
- primaryShade: raw?.primaryShade ?? DEFAULT_VALUE.primaryShade,
5529
- 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
5530
5528
  };
5531
5529
  const { setValue } = useFormActions();
5532
5530
  const set = (key, val) => setValue(schemaKey, { ...value, [key]: val });
@@ -5733,6 +5731,403 @@ function SwatchTile({
5733
5731
  ) })
5734
5732
  ] });
5735
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
+ }
5736
6131
 
5737
6132
  // src/utils/gtaAnimPostFx.ts
5738
6133
  var GTA_ANIM_POST_FX_GROUP_ORDER = [
@@ -6322,7 +6717,7 @@ function ScenarioSelect({
6322
6717
  }
6323
6718
  );
6324
6719
  }
6325
- var t = (key, fallback) => {
6720
+ var t2 = (key, fallback) => {
6326
6721
  const v = locale(key);
6327
6722
  return v === key ? fallback : v;
6328
6723
  };
@@ -6390,7 +6785,7 @@ function DiscordRoleSelect(props) {
6390
6785
  return map;
6391
6786
  }, [roles]);
6392
6787
  if (errorCode === "NotConfigured") {
6393
- const notConfiguredPlaceholder = t(
6788
+ const notConfiguredPlaceholder = t2(
6394
6789
  "DiscordNotConfigured",
6395
6790
  "Not configured \u2014 set in /dirk_lib"
6396
6791
  );
@@ -6458,9 +6853,9 @@ function DiscordRoleSelect(props) {
6458
6853
  e.stopPropagation();
6459
6854
  refresh();
6460
6855
  },
6461
- title: t("Refresh", "Refresh"),
6856
+ title: t2("Refresh", "Refresh"),
6462
6857
  loading,
6463
- "aria-label": t("Refresh", "Refresh"),
6858
+ "aria-label": t2("Refresh", "Refresh"),
6464
6859
  children: /* @__PURE__ */ jsx(RefreshCw, { size: "1.2vh" })
6465
6860
  }
6466
6861
  );
@@ -6470,7 +6865,7 @@ function DiscordRoleSelect(props) {
6470
6865
  {
6471
6866
  label,
6472
6867
  size,
6473
- placeholder: placeholder || t("PickRoles", "Pick roles..."),
6868
+ placeholder: placeholder || t2("PickRoles", "Pick roles..."),
6474
6869
  data,
6475
6870
  value: props.value || [],
6476
6871
  onChange: (ids) => props.onChange(ids),
@@ -6482,7 +6877,7 @@ function DiscordRoleSelect(props) {
6482
6877
  rightSectionPointerEvents: "all",
6483
6878
  style,
6484
6879
  styles,
6485
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6880
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6486
6881
  }
6487
6882
  );
6488
6883
  }
@@ -6491,7 +6886,7 @@ function DiscordRoleSelect(props) {
6491
6886
  {
6492
6887
  label,
6493
6888
  size,
6494
- placeholder: placeholder || t("PickRole", "Pick a role..."),
6889
+ placeholder: placeholder || t2("PickRole", "Pick a role..."),
6495
6890
  data,
6496
6891
  value: props.value || null,
6497
6892
  onChange: (id) => props.onChange(id),
@@ -6503,11 +6898,11 @@ function DiscordRoleSelect(props) {
6503
6898
  rightSectionPointerEvents: "all",
6504
6899
  style,
6505
6900
  styles,
6506
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6901
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6507
6902
  }
6508
6903
  );
6509
6904
  }
6510
- var t2 = (key, fallback) => {
6905
+ var t3 = (key, fallback) => {
6511
6906
  const v = locale(key);
6512
6907
  return v === key ? fallback : v;
6513
6908
  };
@@ -6528,7 +6923,7 @@ function FrameworkHint({ framework }) {
6528
6923
  const theme = useMantineTheme();
6529
6924
  if (!framework) return null;
6530
6925
  const hintKey = `AccountFrameworkHint_${framework}`;
6531
- const text = t2(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6926
+ const text = t3(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6532
6927
  return /* @__PURE__ */ jsxs(
6533
6928
  Flex,
6534
6929
  {
@@ -6612,9 +7007,9 @@ function AccountSelect(props) {
6612
7007
  e.stopPropagation();
6613
7008
  refresh();
6614
7009
  },
6615
- title: t2("Refresh", "Refresh"),
7010
+ title: t3("Refresh", "Refresh"),
6616
7011
  loading,
6617
- "aria-label": t2("Refresh", "Refresh"),
7012
+ "aria-label": t3("Refresh", "Refresh"),
6618
7013
  children: /* @__PURE__ */ jsx(RefreshCw, { size: "1.2vh" })
6619
7014
  }
6620
7015
  );
@@ -6628,14 +7023,14 @@ function AccountSelect(props) {
6628
7023
  rightSection: refreshButton,
6629
7024
  rightSectionPointerEvents: "all",
6630
7025
  style,
6631
- nothingFoundMessage: t2("NoAccounts", "No accounts available")
7026
+ nothingFoundMessage: t3("NoAccounts", "No accounts available")
6632
7027
  };
6633
7028
  return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "0.3vh", style, children: [
6634
7029
  props.multi ? /* @__PURE__ */ jsx(
6635
7030
  MultiSelect,
6636
7031
  {
6637
7032
  ...sharedSelectProps,
6638
- placeholder: placeholder || t2("PickAccounts", "Pick accounts (or type custom)..."),
7033
+ placeholder: placeholder || t3("PickAccounts", "Pick accounts (or type custom)..."),
6639
7034
  value: props.value || [],
6640
7035
  onChange: (names) => props.onChange(names),
6641
7036
  renderOption,
@@ -6647,7 +7042,7 @@ function AccountSelect(props) {
6647
7042
  Select,
6648
7043
  {
6649
7044
  ...sharedSelectProps,
6650
- placeholder: placeholder || t2("PickAccount", "Pick an account..."),
7045
+ placeholder: placeholder || t3("PickAccount", "Pick an account..."),
6651
7046
  value: props.value || null,
6652
7047
  onChange: (name) => props.onChange(name),
6653
7048
  renderOption,
@@ -6939,175 +7334,6 @@ function WorldPositionGotoButton2({
6939
7334
  }
6940
7335
  ) });
6941
7336
  }
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
7337
  function usePickDoor() {
7112
7338
  const begin = useAdminToolStore((s) => s.begin);
7113
7339
  return async () => {
@@ -7384,6 +7610,6 @@ function BlipMarker({
7384
7610
  );
7385
7611
  }
7386
7612
 
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 };
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 };
7388
7614
  //# sourceMappingURL=index.js.map
7389
7615
  //# sourceMappingURL=index.js.map