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.
@@ -4,6 +4,7 @@ var core = require('@mantine/core');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var react = require('react');
6
6
  var zustand = require('zustand');
7
+ var zod = require('zod');
7
8
  var reactFontawesome = require('@fortawesome/react-fontawesome');
8
9
  var framerMotion = require('framer-motion');
9
10
  var lucideReact = require('lucide-react');
@@ -1289,6 +1290,32 @@ function colorWithAlpha(color, alpha17) {
1289
1290
 
1290
1291
  // src/utils/misc.ts
1291
1292
  var isEnvBrowser = () => !window.invokeNative;
1293
+ var DEFAULT_PALETTE = [
1294
+ "#f0f4ff",
1295
+ "#d9e3ff",
1296
+ "#bfcfff",
1297
+ "#a6bbff",
1298
+ "#8ca7ff",
1299
+ "#7393ff",
1300
+ "#5a7fff",
1301
+ "#406bff",
1302
+ "#2547ff",
1303
+ "#0b33ff"
1304
+ ];
1305
+ zod.z.object({
1306
+ useOverride: zod.z.boolean(),
1307
+ primaryColor: zod.z.string(),
1308
+ primaryShade: zod.z.number(),
1309
+ customTheme: zod.z.array(zod.z.string())
1310
+ });
1311
+ var defaultThemeOverride = {
1312
+ useOverride: false,
1313
+ primaryColor: "dirk",
1314
+ primaryShade: 5,
1315
+ customTheme: DEFAULT_PALETTE
1316
+ };
1317
+
1318
+ // src/utils/useSettings.ts
1292
1319
  var useSettings = zustand.create(() => ({
1293
1320
  currency: "$",
1294
1321
  game: "fivem",
@@ -1296,18 +1323,7 @@ var useSettings = zustand.create(() => ({
1296
1323
  primaryShade: 9,
1297
1324
  itemImgPath: "",
1298
1325
  resourceVersion: "dev",
1299
- customTheme: [
1300
- "#f0f4ff",
1301
- "#d9e3ff",
1302
- "#bfcfff",
1303
- "#a6bbff",
1304
- "#8ca7ff",
1305
- "#7393ff",
1306
- "#5a7fff",
1307
- "#406bff",
1308
- "#2547ff",
1309
- "#0b33ff"
1310
- ]
1326
+ customTheme: DEFAULT_PALETTE
1311
1327
  }));
1312
1328
 
1313
1329
  // src/utils/fetchNui.ts
@@ -3161,11 +3177,11 @@ function ConfirmModal({
3161
3177
  placeholder: confirmText,
3162
3178
  value: typed,
3163
3179
  onChange: (e) => setTyped(e.currentTarget.value),
3164
- styles: (t3) => ({
3180
+ styles: (t4) => ({
3165
3181
  input: {
3166
- backgroundColor: core.alpha(t3.colors.dark[7], 0.5),
3182
+ backgroundColor: core.alpha(t4.colors.dark[7], 0.5),
3167
3183
  border: `0.1vh solid ${core.alpha(
3168
- typed === confirmText ? "#ef4444" : t3.colors.dark[5],
3184
+ typed === confirmText ? "#ef4444" : t4.colors.dark[5],
3169
3185
  0.5
3170
3186
  )}`,
3171
3187
  color: "rgba(255,255,255,0.85)",
@@ -3810,7 +3826,7 @@ function useAdminState(key, initial) {
3810
3826
  react.useEffect(() => {
3811
3827
  const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
3812
3828
  listeners.set(key, set);
3813
- const handler = () => setTick((t3) => t3 + 1);
3829
+ const handler = () => setTick((t4) => t4 + 1);
3814
3830
  set.add(handler);
3815
3831
  return () => {
3816
3832
  set.delete(handler);
@@ -5498,24 +5514,6 @@ var MANTINE_COLOR_OPTIONS = [
5498
5514
  "yellow",
5499
5515
  "orange"
5500
5516
  ].map((value) => ({ value, label: value }));
5501
- var DEFAULT_PALETTE = [
5502
- "#f0f4ff",
5503
- "#d9e3ff",
5504
- "#bfcfff",
5505
- "#a6bbff",
5506
- "#8ca7ff",
5507
- "#7393ff",
5508
- "#5a7fff",
5509
- "#406bff",
5510
- "#2547ff",
5511
- "#0b33ff"
5512
- ];
5513
- var DEFAULT_VALUE = {
5514
- useOverride: false,
5515
- primaryColor: "dirk",
5516
- primaryShade: 5,
5517
- customTheme: DEFAULT_PALETTE
5518
- };
5519
5517
  function GroupLabel({ label }) {
5520
5518
  return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", mt: "xxs", children: [
5521
5519
  /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.07em", c: "rgba(255,255,255,0.2)", children: label }),
@@ -5530,10 +5528,10 @@ function ThemeOverrideSection({
5530
5528
  const color = mantineTheme.colors[mantineTheme.primaryColor][5];
5531
5529
  const raw = useFormField(schemaKey);
5532
5530
  const value = {
5533
- useOverride: raw?.useOverride ?? DEFAULT_VALUE.useOverride,
5534
- primaryColor: raw?.primaryColor ?? DEFAULT_VALUE.primaryColor,
5535
- primaryShade: raw?.primaryShade ?? DEFAULT_VALUE.primaryShade,
5536
- customTheme: Array.isArray(raw?.customTheme) && raw.customTheme.length === 10 ? raw.customTheme : DEFAULT_VALUE.customTheme
5531
+ useOverride: raw?.useOverride ?? defaultThemeOverride.useOverride,
5532
+ primaryColor: raw?.primaryColor ?? defaultThemeOverride.primaryColor,
5533
+ primaryShade: raw?.primaryShade ?? defaultThemeOverride.primaryShade,
5534
+ customTheme: Array.isArray(raw?.customTheme) && raw.customTheme.length === 10 ? raw.customTheme : defaultThemeOverride.customTheme
5537
5535
  };
5538
5536
  const { setValue } = useFormActions();
5539
5537
  const set = (key, val) => setValue(schemaKey, { ...value, [key]: val });
@@ -5740,6 +5738,403 @@ function SwatchTile({
5740
5738
  ) })
5741
5739
  ] });
5742
5740
  }
5741
+ function usePlayers(opts = {}) {
5742
+ const {
5743
+ includeOffline = false,
5744
+ search = "",
5745
+ limit = 50,
5746
+ staleTimeMs,
5747
+ refetchIntervalMs
5748
+ } = opts;
5749
+ const query = reactQuery.useQuery({
5750
+ queryKey: includeOffline ? ["dirk:players", "search", search.trim().toLowerCase(), limit] : ["dirk:players", "online"],
5751
+ queryFn: async () => {
5752
+ const toolId = includeOffline ? "searchPlayers" : "getOnlinePlayers";
5753
+ const payload = includeOffline ? { id: toolId, value: { search: search.trim(), limit } } : { id: toolId };
5754
+ const result = await fetchNui(
5755
+ "ADMIN_TOOL_QUERY",
5756
+ payload,
5757
+ // Browser-dev fallback. Returns a couple of mock players so the
5758
+ // dev shell isn't blank.
5759
+ includeOffline ? [
5760
+ { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true },
5761
+ { id: null, citizenId: "DEF67890", name: "", charName: "Jane Offline", online: false }
5762
+ ] : [
5763
+ { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true }
5764
+ ]
5765
+ );
5766
+ return Array.isArray(result) ? result : [];
5767
+ },
5768
+ staleTime: staleTimeMs ?? (includeOffline ? 3e4 : 5e3),
5769
+ gcTime: 5 * 6e4,
5770
+ refetchInterval: refetchIntervalMs ?? false,
5771
+ refetchOnWindowFocus: false,
5772
+ placeholderData: includeOffline ? reactQuery.keepPreviousData : void 0
5773
+ });
5774
+ return {
5775
+ players: query.data ?? [],
5776
+ isLoading: query.isLoading,
5777
+ isFetching: query.isFetching,
5778
+ error: query.error ?? null,
5779
+ refresh: () => query.refetch()
5780
+ };
5781
+ }
5782
+ var DEBOUNCE_MS = 300;
5783
+ var ONLINE_COLOR = "#3FA83F";
5784
+ var OFFLINE_COLOR = "#E54141";
5785
+ function PlayerSelect({
5786
+ value,
5787
+ onChange,
5788
+ includeOffline = false,
5789
+ limit = 50,
5790
+ searchable: searchableProp,
5791
+ placeholder,
5792
+ nothingFoundMessage: nothingFoundMessageProp,
5793
+ loadingLabel = "Loading\u2026",
5794
+ onlineLabel = "Online",
5795
+ offlineLabel = "Offline",
5796
+ refreshLabel = "Refresh player list",
5797
+ ...rest
5798
+ }) {
5799
+ const theme = core.useMantineTheme();
5800
+ const color = theme.colors[theme.primaryColor][5];
5801
+ const [searchInput, setSearchInput] = react.useState("");
5802
+ const [debouncedSearch, setDebouncedSearch] = react.useState("");
5803
+ const { players, isFetching, refresh } = usePlayers({
5804
+ includeOffline,
5805
+ search: includeOffline ? debouncedSearch : void 0,
5806
+ limit
5807
+ });
5808
+ const sortedPlayers = react.useMemo(
5809
+ () => [...players].sort((a, b) => {
5810
+ if (a.online !== b.online) return a.online ? -1 : 1;
5811
+ return a.charName.localeCompare(b.charName);
5812
+ }),
5813
+ [players]
5814
+ );
5815
+ const playerByCitizen = react.useMemo(() => {
5816
+ const m = /* @__PURE__ */ new Map();
5817
+ for (const p of sortedPlayers) m.set(p.citizenId, p);
5818
+ return m;
5819
+ }, [sortedPlayers]);
5820
+ const data = react.useMemo(() => {
5821
+ const items = sortedPlayers.map((p) => ({
5822
+ value: p.citizenId,
5823
+ label: formatLabel(p)
5824
+ }));
5825
+ if (value && !items.some((i) => i.value === value)) {
5826
+ items.unshift({ value, label: value });
5827
+ }
5828
+ return items;
5829
+ }, [sortedPlayers, value]);
5830
+ const selectedPlayer = value ? playerByCitizen.get(value) ?? null : null;
5831
+ const selectedLabel = selectedPlayer ? formatLabel(selectedPlayer) : null;
5832
+ react.useEffect(() => {
5833
+ if (!includeOffline) return;
5834
+ if (selectedLabel && searchInput === selectedLabel) return;
5835
+ const t4 = setTimeout(() => setDebouncedSearch(searchInput), DEBOUNCE_MS);
5836
+ return () => clearTimeout(t4);
5837
+ }, [searchInput, includeOffline, selectedLabel]);
5838
+ const renderOption = ({ option }) => {
5839
+ const p = playerByCitizen.get(option.value);
5840
+ if (!p) return option.label;
5841
+ return /* @__PURE__ */ jsxRuntime.jsxs(core.Group, { justify: "space-between", wrap: "nowrap", gap: "xs", style: { width: "100%" }, children: [
5842
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { size: "xs", truncate: true, style: { flex: 1 }, children: formatLabel(p) }),
5843
+ /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { online: p.online, onlineLabel, offlineLabel })
5844
+ ] });
5845
+ };
5846
+ return /* @__PURE__ */ jsxRuntime.jsx(
5847
+ core.Select,
5848
+ {
5849
+ ...rest,
5850
+ data,
5851
+ value: value ?? null,
5852
+ onChange: (v) => {
5853
+ if (!v) return onChange(null);
5854
+ const player = playerByCitizen.get(v) ?? null;
5855
+ onChange(player);
5856
+ },
5857
+ searchable: searchableProp ?? true,
5858
+ searchValue: includeOffline ? searchInput : void 0,
5859
+ onSearchChange: includeOffline ? setSearchInput : void 0,
5860
+ placeholder,
5861
+ nothingFoundMessage: isFetching ? loadingLabel : nothingFoundMessageProp ?? "No players found",
5862
+ maxDropdownHeight: 300,
5863
+ renderOption,
5864
+ leftSection: selectedPlayer ? /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { online: selectedPlayer.online, onlineLabel, offlineLabel }) : void 0,
5865
+ rightSectionWidth: "3.5vh",
5866
+ rightSectionPointerEvents: "all",
5867
+ rightSection: /* @__PURE__ */ jsxRuntime.jsx(
5868
+ core.ActionIcon,
5869
+ {
5870
+ variant: "subtle",
5871
+ size: "sm",
5872
+ onClick: (e) => {
5873
+ e.stopPropagation();
5874
+ refresh();
5875
+ },
5876
+ "aria-label": refreshLabel,
5877
+ title: refreshLabel,
5878
+ style: { marginRight: "0.6vh" },
5879
+ children: isFetching ? /* @__PURE__ */ jsxRuntime.jsx(core.Loader, { size: "1.2vh", color }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: "1.4vh", color })
5880
+ }
5881
+ )
5882
+ }
5883
+ );
5884
+ }
5885
+ function StatusDot({ online, onlineLabel, offlineLabel }) {
5886
+ return /* @__PURE__ */ jsxRuntime.jsx(
5887
+ "span",
5888
+ {
5889
+ title: online ? onlineLabel : offlineLabel,
5890
+ style: {
5891
+ display: "inline-block",
5892
+ width: "0.9vh",
5893
+ height: "0.9vh",
5894
+ borderRadius: "50%",
5895
+ backgroundColor: online ? ONLINE_COLOR : OFFLINE_COLOR,
5896
+ boxShadow: `0 0 0.4vh ${online ? ONLINE_COLOR : OFFLINE_COLOR}`,
5897
+ flexShrink: 0
5898
+ }
5899
+ }
5900
+ );
5901
+ }
5902
+ function formatLabel(p) {
5903
+ const parts = [p.charName || p.citizenId];
5904
+ if (p.online) {
5905
+ if (p.name) parts.push(p.name);
5906
+ if (p.id != null) parts.push(`#${p.id}`);
5907
+ }
5908
+ return parts.join(" \xB7 ");
5909
+ }
5910
+ var t = (key, fallback) => {
5911
+ const v = locale(key);
5912
+ return v === key ? fallback : v;
5913
+ };
5914
+ var DEFAULT_VALUE = {
5915
+ groups: [],
5916
+ identifiers: []
5917
+ };
5918
+ function SectionLabel({ icon: Icon, label }) {
5919
+ return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", mt: "xs", children: [
5920
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: "1.4vh", color: "rgba(255,255,255,0.3)" }),
5921
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.07em", c: "rgba(255,255,255,0.2)", children: label }),
5922
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1, height: "0.05vh", background: "rgba(255,255,255,0.06)" } })
5923
+ ] });
5924
+ }
5925
+ function EmptyHint({ text }) {
5926
+ return /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.25)", ta: "center", py: "xs", children: text });
5927
+ }
5928
+ function AccessOverrideSection({
5929
+ schemaKey = "access",
5930
+ title,
5931
+ description,
5932
+ groupType,
5933
+ includeOffline = true
5934
+ }) {
5935
+ const mantineTheme = core.useMantineTheme();
5936
+ const color = mantineTheme.colors[mantineTheme.primaryColor][5];
5937
+ const raw = useFormField(schemaKey);
5938
+ const value = {
5939
+ groups: Array.isArray(raw?.groups) ? raw.groups : DEFAULT_VALUE.groups,
5940
+ identifiers: Array.isArray(raw?.identifiers) ? raw.identifiers : DEFAULT_VALUE.identifiers
5941
+ };
5942
+ const { setValue } = useFormActions();
5943
+ const set = (key, val) => setValue(schemaKey, { ...value, [key]: val });
5944
+ const [addingGroup, setAddingGroup] = react.useState(false);
5945
+ const [addingPlayer, setAddingPlayer] = react.useState(false);
5946
+ const commitGroup = (name) => {
5947
+ if (!name) return;
5948
+ set("groups", [...value.groups, name]);
5949
+ setAddingGroup(false);
5950
+ };
5951
+ const setGroup = (index, name) => {
5952
+ const next = [...value.groups];
5953
+ next[index] = name;
5954
+ set("groups", next.filter((g) => g !== ""));
5955
+ };
5956
+ const removeGroup = (index) => set("groups", value.groups.filter((_, i) => i !== index));
5957
+ const commitIdentifier = (id) => {
5958
+ if (!id) return;
5959
+ set("identifiers", [...value.identifiers, id]);
5960
+ setAddingPlayer(false);
5961
+ };
5962
+ const setIdentifier = (index, id) => {
5963
+ const next = [...value.identifiers];
5964
+ next[index] = id;
5965
+ set("identifiers", next.filter((x) => x !== ""));
5966
+ };
5967
+ const removeIdentifier = (index) => set("identifiers", value.identifiers.filter((_, i) => i !== index));
5968
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5969
+ core.Flex,
5970
+ {
5971
+ direction: "column",
5972
+ gap: "xs",
5973
+ p: "sm",
5974
+ style: { flex: 1, minHeight: 0, overflowY: "auto" },
5975
+ children: [
5976
+ /* @__PURE__ */ jsxRuntime.jsx(
5977
+ AdminPageTitle,
5978
+ {
5979
+ icon: lucideReact.ShieldCheck,
5980
+ title: title || t("Access", "Access"),
5981
+ color
5982
+ }
5983
+ ),
5984
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.4)", children: description || t(
5985
+ "AccessOverrideDesc",
5986
+ "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."
5987
+ ) }),
5988
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { icon: lucideReact.Users, label: t("AccessGroups", "Groups") }),
5989
+ /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { direction: "column", gap: "xxs", children: [
5990
+ value.groups.length === 0 && !addingGroup && /* @__PURE__ */ jsxRuntime.jsx(EmptyHint, { text: t("AccessNoGroups", "No groups added.") }),
5991
+ value.groups.map((name, i) => /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
5992
+ /* @__PURE__ */ jsxRuntime.jsx(
5993
+ GroupName,
5994
+ {
5995
+ value: name,
5996
+ onChange: (v) => setGroup(i, v),
5997
+ type: groupType,
5998
+ placeholder: t("AccessPickGroup", "Pick a group\u2026"),
5999
+ style: { flex: 1 }
6000
+ }
6001
+ ),
6002
+ /* @__PURE__ */ jsxRuntime.jsx(
6003
+ core.ActionIcon,
6004
+ {
6005
+ variant: "subtle",
6006
+ color: "red",
6007
+ size: "md",
6008
+ onClick: () => removeGroup(i),
6009
+ title: t("Remove", "Remove"),
6010
+ "aria-label": t("Remove", "Remove"),
6011
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6012
+ }
6013
+ )
6014
+ ] }, i)),
6015
+ addingGroup && /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6016
+ /* @__PURE__ */ jsxRuntime.jsx(
6017
+ GroupName,
6018
+ {
6019
+ value: "",
6020
+ onChange: (v) => commitGroup(v),
6021
+ type: groupType,
6022
+ placeholder: t("AccessPickGroup", "Pick a group\u2026"),
6023
+ style: { flex: 1 }
6024
+ }
6025
+ ),
6026
+ /* @__PURE__ */ jsxRuntime.jsx(
6027
+ core.ActionIcon,
6028
+ {
6029
+ variant: "subtle",
6030
+ color: "red",
6031
+ size: "md",
6032
+ onClick: () => setAddingGroup(false),
6033
+ title: t("Remove", "Remove"),
6034
+ "aria-label": t("Remove", "Remove"),
6035
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6036
+ }
6037
+ )
6038
+ ] })
6039
+ ] }),
6040
+ !addingGroup && /* @__PURE__ */ jsxRuntime.jsx(AddRowButton, { label: t("AccessAddGroup", "Add group"), onClick: () => setAddingGroup(true) }),
6041
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { icon: lucideReact.UserRound, label: t("AccessPlayers", "Players") }),
6042
+ /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { direction: "column", gap: "xxs", children: [
6043
+ value.identifiers.length === 0 && !addingPlayer && /* @__PURE__ */ jsxRuntime.jsx(EmptyHint, { text: t("AccessNoPlayers", "No players added.") }),
6044
+ value.identifiers.map((id, i) => /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6045
+ /* @__PURE__ */ jsxRuntime.jsx(
6046
+ PlayerSelect,
6047
+ {
6048
+ value: id || null,
6049
+ onChange: (p) => setIdentifier(i, p?.citizenId ?? ""),
6050
+ includeOffline,
6051
+ placeholder: t("AccessPickPlayer", "Pick a player\u2026"),
6052
+ style: { flex: 1 },
6053
+ size: "xs"
6054
+ }
6055
+ ),
6056
+ /* @__PURE__ */ jsxRuntime.jsx(
6057
+ core.ActionIcon,
6058
+ {
6059
+ variant: "subtle",
6060
+ color: "red",
6061
+ size: "md",
6062
+ onClick: () => removeIdentifier(i),
6063
+ title: t("Remove", "Remove"),
6064
+ "aria-label": t("Remove", "Remove"),
6065
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6066
+ }
6067
+ )
6068
+ ] }, i)),
6069
+ addingPlayer && /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6070
+ /* @__PURE__ */ jsxRuntime.jsx(
6071
+ PlayerSelect,
6072
+ {
6073
+ value: null,
6074
+ onChange: (p) => commitIdentifier(p?.citizenId ?? ""),
6075
+ includeOffline,
6076
+ placeholder: t("AccessPickPlayer", "Pick a player\u2026"),
6077
+ style: { flex: 1 },
6078
+ size: "xs"
6079
+ }
6080
+ ),
6081
+ /* @__PURE__ */ jsxRuntime.jsx(
6082
+ core.ActionIcon,
6083
+ {
6084
+ variant: "subtle",
6085
+ color: "red",
6086
+ size: "md",
6087
+ onClick: () => setAddingPlayer(false),
6088
+ title: t("Remove", "Remove"),
6089
+ "aria-label": t("Remove", "Remove"),
6090
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6091
+ }
6092
+ )
6093
+ ] })
6094
+ ] }),
6095
+ !addingPlayer && /* @__PURE__ */ jsxRuntime.jsx(AddRowButton, { label: t("AccessAddPlayer", "Add player"), onClick: () => setAddingPlayer(true) })
6096
+ ]
6097
+ }
6098
+ );
6099
+ }
6100
+ function AddRowButton({ label, onClick }) {
6101
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6102
+ core.Flex,
6103
+ {
6104
+ align: "center",
6105
+ justify: "center",
6106
+ gap: "xs",
6107
+ onClick,
6108
+ role: "button",
6109
+ tabIndex: 0,
6110
+ onKeyDown: (e) => {
6111
+ if (e.key === "Enter" || e.key === " ") {
6112
+ e.preventDefault();
6113
+ onClick();
6114
+ }
6115
+ },
6116
+ py: "xs",
6117
+ style: {
6118
+ cursor: "pointer",
6119
+ border: "0.1vh dashed rgba(255,255,255,0.12)",
6120
+ borderRadius: "0.4vh",
6121
+ transition: "background 0.15s, border-color 0.15s"
6122
+ },
6123
+ onMouseEnter: (e) => {
6124
+ e.currentTarget.style.background = "rgba(255,255,255,0.03)";
6125
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.2)";
6126
+ },
6127
+ onMouseLeave: (e) => {
6128
+ e.currentTarget.style.background = "transparent";
6129
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)";
6130
+ },
6131
+ children: [
6132
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { size: "1.4vh", color: "rgba(255,255,255,0.4)" }),
6133
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "rgba(255,255,255,0.4)", children: label })
6134
+ ]
6135
+ }
6136
+ );
6137
+ }
5743
6138
 
5744
6139
  // src/utils/gtaAnimPostFx.ts
5745
6140
  var GTA_ANIM_POST_FX_GROUP_ORDER = [
@@ -6329,7 +6724,7 @@ function ScenarioSelect({
6329
6724
  }
6330
6725
  );
6331
6726
  }
6332
- var t = (key, fallback) => {
6727
+ var t2 = (key, fallback) => {
6333
6728
  const v = locale(key);
6334
6729
  return v === key ? fallback : v;
6335
6730
  };
@@ -6397,7 +6792,7 @@ function DiscordRoleSelect(props) {
6397
6792
  return map;
6398
6793
  }, [roles]);
6399
6794
  if (errorCode === "NotConfigured") {
6400
- const notConfiguredPlaceholder = t(
6795
+ const notConfiguredPlaceholder = t2(
6401
6796
  "DiscordNotConfigured",
6402
6797
  "Not configured \u2014 set in /dirk_lib"
6403
6798
  );
@@ -6465,9 +6860,9 @@ function DiscordRoleSelect(props) {
6465
6860
  e.stopPropagation();
6466
6861
  refresh();
6467
6862
  },
6468
- title: t("Refresh", "Refresh"),
6863
+ title: t2("Refresh", "Refresh"),
6469
6864
  loading,
6470
- "aria-label": t("Refresh", "Refresh"),
6865
+ "aria-label": t2("Refresh", "Refresh"),
6471
6866
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: "1.2vh" })
6472
6867
  }
6473
6868
  );
@@ -6477,7 +6872,7 @@ function DiscordRoleSelect(props) {
6477
6872
  {
6478
6873
  label,
6479
6874
  size,
6480
- placeholder: placeholder || t("PickRoles", "Pick roles..."),
6875
+ placeholder: placeholder || t2("PickRoles", "Pick roles..."),
6481
6876
  data,
6482
6877
  value: props.value || [],
6483
6878
  onChange: (ids) => props.onChange(ids),
@@ -6489,7 +6884,7 @@ function DiscordRoleSelect(props) {
6489
6884
  rightSectionPointerEvents: "all",
6490
6885
  style,
6491
6886
  styles,
6492
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6887
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6493
6888
  }
6494
6889
  );
6495
6890
  }
@@ -6498,7 +6893,7 @@ function DiscordRoleSelect(props) {
6498
6893
  {
6499
6894
  label,
6500
6895
  size,
6501
- placeholder: placeholder || t("PickRole", "Pick a role..."),
6896
+ placeholder: placeholder || t2("PickRole", "Pick a role..."),
6502
6897
  data,
6503
6898
  value: props.value || null,
6504
6899
  onChange: (id) => props.onChange(id),
@@ -6510,11 +6905,11 @@ function DiscordRoleSelect(props) {
6510
6905
  rightSectionPointerEvents: "all",
6511
6906
  style,
6512
6907
  styles,
6513
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6908
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6514
6909
  }
6515
6910
  );
6516
6911
  }
6517
- var t2 = (key, fallback) => {
6912
+ var t3 = (key, fallback) => {
6518
6913
  const v = locale(key);
6519
6914
  return v === key ? fallback : v;
6520
6915
  };
@@ -6535,7 +6930,7 @@ function FrameworkHint({ framework }) {
6535
6930
  const theme = core.useMantineTheme();
6536
6931
  if (!framework) return null;
6537
6932
  const hintKey = `AccountFrameworkHint_${framework}`;
6538
- const text = t2(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6933
+ const text = t3(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6539
6934
  return /* @__PURE__ */ jsxRuntime.jsxs(
6540
6935
  core.Flex,
6541
6936
  {
@@ -6619,9 +7014,9 @@ function AccountSelect(props) {
6619
7014
  e.stopPropagation();
6620
7015
  refresh();
6621
7016
  },
6622
- title: t2("Refresh", "Refresh"),
7017
+ title: t3("Refresh", "Refresh"),
6623
7018
  loading,
6624
- "aria-label": t2("Refresh", "Refresh"),
7019
+ "aria-label": t3("Refresh", "Refresh"),
6625
7020
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: "1.2vh" })
6626
7021
  }
6627
7022
  );
@@ -6635,14 +7030,14 @@ function AccountSelect(props) {
6635
7030
  rightSection: refreshButton,
6636
7031
  rightSectionPointerEvents: "all",
6637
7032
  style,
6638
- nothingFoundMessage: t2("NoAccounts", "No accounts available")
7033
+ nothingFoundMessage: t3("NoAccounts", "No accounts available")
6639
7034
  };
6640
7035
  return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { direction: "column", gap: "0.3vh", style, children: [
6641
7036
  props.multi ? /* @__PURE__ */ jsxRuntime.jsx(
6642
7037
  core.MultiSelect,
6643
7038
  {
6644
7039
  ...sharedSelectProps,
6645
- placeholder: placeholder || t2("PickAccounts", "Pick accounts (or type custom)..."),
7040
+ placeholder: placeholder || t3("PickAccounts", "Pick accounts (or type custom)..."),
6646
7041
  value: props.value || [],
6647
7042
  onChange: (names) => props.onChange(names),
6648
7043
  renderOption,
@@ -6654,7 +7049,7 @@ function AccountSelect(props) {
6654
7049
  core.Select,
6655
7050
  {
6656
7051
  ...sharedSelectProps,
6657
- placeholder: placeholder || t2("PickAccount", "Pick an account..."),
7052
+ placeholder: placeholder || t3("PickAccount", "Pick an account..."),
6658
7053
  value: props.value || null,
6659
7054
  onChange: (name) => props.onChange(name),
6660
7055
  renderOption,
@@ -6946,175 +7341,6 @@ function WorldPositionGotoButton2({
6946
7341
  }
6947
7342
  ) });
6948
7343
  }
6949
- function usePlayers(opts = {}) {
6950
- const {
6951
- includeOffline = false,
6952
- search = "",
6953
- limit = 50,
6954
- staleTimeMs,
6955
- refetchIntervalMs
6956
- } = opts;
6957
- const query = reactQuery.useQuery({
6958
- queryKey: includeOffline ? ["dirk:players", "search", search.trim().toLowerCase(), limit] : ["dirk:players", "online"],
6959
- queryFn: async () => {
6960
- const toolId = includeOffline ? "searchPlayers" : "getOnlinePlayers";
6961
- const payload = includeOffline ? { id: toolId, value: { search: search.trim(), limit } } : { id: toolId };
6962
- const result = await fetchNui(
6963
- "ADMIN_TOOL_QUERY",
6964
- payload,
6965
- // Browser-dev fallback. Returns a couple of mock players so the
6966
- // dev shell isn't blank.
6967
- includeOffline ? [
6968
- { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true },
6969
- { id: null, citizenId: "DEF67890", name: "", charName: "Jane Offline", online: false }
6970
- ] : [
6971
- { id: 1, citizenId: "ABC12345", name: "Dev User", charName: "John Doe", online: true }
6972
- ]
6973
- );
6974
- return Array.isArray(result) ? result : [];
6975
- },
6976
- staleTime: staleTimeMs ?? (includeOffline ? 3e4 : 5e3),
6977
- gcTime: 5 * 6e4,
6978
- refetchInterval: refetchIntervalMs ?? false,
6979
- refetchOnWindowFocus: false,
6980
- placeholderData: includeOffline ? reactQuery.keepPreviousData : void 0
6981
- });
6982
- return {
6983
- players: query.data ?? [],
6984
- isLoading: query.isLoading,
6985
- isFetching: query.isFetching,
6986
- error: query.error ?? null,
6987
- refresh: () => query.refetch()
6988
- };
6989
- }
6990
- var DEBOUNCE_MS = 300;
6991
- var ONLINE_COLOR = "#3FA83F";
6992
- var OFFLINE_COLOR = "#E54141";
6993
- function PlayerSelect({
6994
- value,
6995
- onChange,
6996
- includeOffline = false,
6997
- limit = 50,
6998
- searchable: searchableProp,
6999
- placeholder,
7000
- nothingFoundMessage: nothingFoundMessageProp,
7001
- loadingLabel = "Loading\u2026",
7002
- onlineLabel = "Online",
7003
- offlineLabel = "Offline",
7004
- refreshLabel = "Refresh player list",
7005
- ...rest
7006
- }) {
7007
- const theme = core.useMantineTheme();
7008
- const color = theme.colors[theme.primaryColor][5];
7009
- const [searchInput, setSearchInput] = react.useState("");
7010
- const [debouncedSearch, setDebouncedSearch] = react.useState("");
7011
- const { players, isFetching, refresh } = usePlayers({
7012
- includeOffline,
7013
- search: includeOffline ? debouncedSearch : void 0,
7014
- limit
7015
- });
7016
- const sortedPlayers = react.useMemo(
7017
- () => [...players].sort((a, b) => {
7018
- if (a.online !== b.online) return a.online ? -1 : 1;
7019
- return a.charName.localeCompare(b.charName);
7020
- }),
7021
- [players]
7022
- );
7023
- const playerByCitizen = react.useMemo(() => {
7024
- const m = /* @__PURE__ */ new Map();
7025
- for (const p of sortedPlayers) m.set(p.citizenId, p);
7026
- return m;
7027
- }, [sortedPlayers]);
7028
- const data = react.useMemo(() => {
7029
- const items = sortedPlayers.map((p) => ({
7030
- value: p.citizenId,
7031
- label: formatLabel(p)
7032
- }));
7033
- if (value && !items.some((i) => i.value === value)) {
7034
- items.unshift({ value, label: value });
7035
- }
7036
- return items;
7037
- }, [sortedPlayers, value]);
7038
- const selectedPlayer = value ? playerByCitizen.get(value) ?? null : null;
7039
- const selectedLabel = selectedPlayer ? formatLabel(selectedPlayer) : null;
7040
- react.useEffect(() => {
7041
- if (!includeOffline) return;
7042
- if (selectedLabel && searchInput === selectedLabel) return;
7043
- const t3 = setTimeout(() => setDebouncedSearch(searchInput), DEBOUNCE_MS);
7044
- return () => clearTimeout(t3);
7045
- }, [searchInput, includeOffline, selectedLabel]);
7046
- const renderOption = ({ option }) => {
7047
- const p = playerByCitizen.get(option.value);
7048
- if (!p) return option.label;
7049
- return /* @__PURE__ */ jsxRuntime.jsxs(core.Group, { justify: "space-between", wrap: "nowrap", gap: "xs", style: { width: "100%" }, children: [
7050
- /* @__PURE__ */ jsxRuntime.jsx(core.Text, { size: "xs", truncate: true, style: { flex: 1 }, children: formatLabel(p) }),
7051
- /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { online: p.online, onlineLabel, offlineLabel })
7052
- ] });
7053
- };
7054
- return /* @__PURE__ */ jsxRuntime.jsx(
7055
- core.Select,
7056
- {
7057
- ...rest,
7058
- data,
7059
- value: value ?? null,
7060
- onChange: (v) => {
7061
- if (!v) return onChange(null);
7062
- const player = playerByCitizen.get(v) ?? null;
7063
- onChange(player);
7064
- },
7065
- searchable: searchableProp ?? true,
7066
- searchValue: includeOffline ? searchInput : void 0,
7067
- onSearchChange: includeOffline ? setSearchInput : void 0,
7068
- placeholder,
7069
- nothingFoundMessage: isFetching ? loadingLabel : nothingFoundMessageProp ?? "No players found",
7070
- maxDropdownHeight: 300,
7071
- renderOption,
7072
- leftSection: selectedPlayer ? /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { online: selectedPlayer.online, onlineLabel, offlineLabel }) : void 0,
7073
- rightSectionWidth: "3.5vh",
7074
- rightSectionPointerEvents: "all",
7075
- rightSection: /* @__PURE__ */ jsxRuntime.jsx(
7076
- core.ActionIcon,
7077
- {
7078
- variant: "subtle",
7079
- size: "sm",
7080
- onClick: (e) => {
7081
- e.stopPropagation();
7082
- refresh();
7083
- },
7084
- "aria-label": refreshLabel,
7085
- title: refreshLabel,
7086
- style: { marginRight: "0.6vh" },
7087
- children: isFetching ? /* @__PURE__ */ jsxRuntime.jsx(core.Loader, { size: "1.2vh", color }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: "1.4vh", color })
7088
- }
7089
- )
7090
- }
7091
- );
7092
- }
7093
- function StatusDot({ online, onlineLabel, offlineLabel }) {
7094
- return /* @__PURE__ */ jsxRuntime.jsx(
7095
- "span",
7096
- {
7097
- title: online ? onlineLabel : offlineLabel,
7098
- style: {
7099
- display: "inline-block",
7100
- width: "0.9vh",
7101
- height: "0.9vh",
7102
- borderRadius: "50%",
7103
- backgroundColor: online ? ONLINE_COLOR : OFFLINE_COLOR,
7104
- boxShadow: `0 0 0.4vh ${online ? ONLINE_COLOR : OFFLINE_COLOR}`,
7105
- flexShrink: 0
7106
- }
7107
- }
7108
- );
7109
- }
7110
- function formatLabel(p) {
7111
- const parts = [p.charName || p.citizenId];
7112
- if (p.online) {
7113
- if (p.name) parts.push(p.name);
7114
- if (p.id != null) parts.push(`#${p.id}`);
7115
- }
7116
- return parts.join(" \xB7 ");
7117
- }
7118
7344
  function usePickDoor() {
7119
7345
  const begin = useAdminToolStore((s) => s.begin);
7120
7346
  return async () => {
@@ -7391,6 +7617,7 @@ function BlipMarker({
7391
7617
  );
7392
7618
  }
7393
7619
 
7620
+ exports.AccessOverrideSection = AccessOverrideSection;
7394
7621
  exports.AccountSelect = AccountSelect;
7395
7622
  exports.AdminPageTitle = AdminPageTitle;
7396
7623
  exports.AnimPostFxSelect = AnimPostFxSelect;