dirk-cfx-react 1.1.90 → 1.1.92

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');
@@ -1264,11 +1265,11 @@ var colorNames = {
1264
1265
  Yellow: { r: 255, g: 255, b: 0 },
1265
1266
  YellowGreen: { r: 154, g: 205, b: 50 }
1266
1267
  };
1267
- function colorWithAlpha(color, alpha17) {
1268
+ function colorWithAlpha(color, alpha18) {
1268
1269
  const lowerCasedColor = color.toLowerCase();
1269
1270
  if (colorNames[lowerCasedColor]) {
1270
1271
  const rgb = colorNames[lowerCasedColor];
1271
- return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha17})`;
1272
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha18})`;
1272
1273
  }
1273
1274
  if (/^#([A-Fa-f0-9]{6})$/.test(color)) {
1274
1275
  const hex = color.slice(1);
@@ -1276,12 +1277,12 @@ function colorWithAlpha(color, alpha17) {
1276
1277
  const r = bigint >> 16 & 255;
1277
1278
  const g = bigint >> 8 & 255;
1278
1279
  const b = bigint & 255;
1279
- return `rgba(${r}, ${g}, ${b}, ${alpha17})`;
1280
+ return `rgba(${r}, ${g}, ${b}, ${alpha18})`;
1280
1281
  }
1281
1282
  if (/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/.test(color)) {
1282
1283
  const result = color.match(/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/);
1283
1284
  if (result) {
1284
- return `rgba(${result[1]}, ${result[2]}, ${result[3]}, ${alpha17})`;
1285
+ return `rgba(${result[1]}, ${result[2]}, ${result[3]}, ${alpha18})`;
1285
1286
  }
1286
1287
  }
1287
1288
  return color;
@@ -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);
@@ -4464,7 +4480,7 @@ function ConfigPanel(props) {
4464
4480
  if (isSaving) return;
4465
4481
  setIsSaving(true);
4466
4482
  try {
4467
- const result = await updateConfig(form.values);
4483
+ const result = await updateConfig(form.values, form.changedFields);
4468
4484
  if (result?.success) {
4469
4485
  form.reinitialize(cloneConfig(form.values));
4470
4486
  dirkQueryClient.invalidateQueries({ queryKey: ["scriptConfigHistory"] });
@@ -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,489 @@ 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 COMMON_ACE_GROUPS = ["group.admin", "group.mod", "group.superadmin"];
5911
+ var t = (key, fallback) => {
5912
+ const v = locale(key);
5913
+ return v === key ? fallback : v;
5914
+ };
5915
+ var DEFAULT_VALUE = {
5916
+ groups: [],
5917
+ identifiers: []
5918
+ };
5919
+ function SectionLabel({ icon: Icon, label }) {
5920
+ return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", mt: "xs", children: [
5921
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: "1.4vh", color: "rgba(255,255,255,0.3)" }),
5922
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.07em", c: "rgba(255,255,255,0.2)", children: label }),
5923
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1, height: "0.05vh", background: "rgba(255,255,255,0.06)" } })
5924
+ ] });
5925
+ }
5926
+ function EmptyHint({ text }) {
5927
+ return /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.25)", ta: "center", py: "xs", children: text });
5928
+ }
5929
+ function AccessOverrideSection({
5930
+ schemaKey = "access",
5931
+ title,
5932
+ description,
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
+ const g = name.trim();
5948
+ if (!g) return;
5949
+ if (!value.groups.includes(g)) set("groups", [...value.groups, g]);
5950
+ setAddingGroup(false);
5951
+ };
5952
+ const setGroup = (index, name) => {
5953
+ const next = [...value.groups];
5954
+ next[index] = name;
5955
+ set("groups", next);
5956
+ };
5957
+ const quickAddGroup = (name) => {
5958
+ if (value.groups.includes(name)) return;
5959
+ set("groups", [...value.groups, name]);
5960
+ };
5961
+ const removeGroup = (index) => set("groups", value.groups.filter((_, i) => i !== index));
5962
+ const commitIdentifier = (id) => {
5963
+ if (!id) return;
5964
+ set("identifiers", [...value.identifiers, id]);
5965
+ setAddingPlayer(false);
5966
+ };
5967
+ const setIdentifier = (index, id) => {
5968
+ const next = [...value.identifiers];
5969
+ next[index] = id;
5970
+ set("identifiers", next.filter((x) => x !== ""));
5971
+ };
5972
+ const removeIdentifier = (index) => set("identifiers", value.identifiers.filter((_, i) => i !== index));
5973
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5974
+ core.Flex,
5975
+ {
5976
+ direction: "column",
5977
+ gap: "xs",
5978
+ p: "sm",
5979
+ style: { flex: 1, minHeight: 0, overflowY: "auto" },
5980
+ children: [
5981
+ /* @__PURE__ */ jsxRuntime.jsx(
5982
+ AdminPageTitle,
5983
+ {
5984
+ icon: lucideReact.ShieldCheck,
5985
+ title: title || t("Access", "Access"),
5986
+ color
5987
+ }
5988
+ ),
5989
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.4)", children: description || t(
5990
+ "AccessOverrideDesc",
5991
+ "Grant access to this resource's admin panel to specific ACE groups and players, in addition to your global dirk_lib admins. Enforcement is server-side."
5992
+ ) }),
5993
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { icon: lucideReact.Users, label: t("AccessGroups", "Admin groups (ACE)") }),
5994
+ /* @__PURE__ */ jsxRuntime.jsx(core.Flex, { gap: "xs", wrap: "wrap", children: COMMON_ACE_GROUPS.map((g) => {
5995
+ const added = value.groups.includes(g);
5996
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5997
+ core.Flex,
5998
+ {
5999
+ align: "center",
6000
+ gap: "xxs",
6001
+ onClick: added ? void 0 : () => quickAddGroup(g),
6002
+ role: "button",
6003
+ tabIndex: added ? -1 : 0,
6004
+ "aria-disabled": added,
6005
+ onKeyDown: (e) => {
6006
+ if (added) return;
6007
+ if (e.key === "Enter" || e.key === " ") {
6008
+ e.preventDefault();
6009
+ quickAddGroup(g);
6010
+ }
6011
+ },
6012
+ px: "xs",
6013
+ py: "xxs",
6014
+ style: {
6015
+ cursor: added ? "default" : "pointer",
6016
+ opacity: added ? 0.4 : 1,
6017
+ background: core.alpha(color, 0.12),
6018
+ border: `0.1vh solid ${core.alpha(color, 0.4)}`,
6019
+ borderRadius: "0.4vh",
6020
+ transition: "background 0.15s, border-color 0.15s"
6021
+ },
6022
+ onMouseEnter: (e) => {
6023
+ if (added) return;
6024
+ e.currentTarget.style.background = core.alpha(color, 0.22);
6025
+ },
6026
+ onMouseLeave: (e) => {
6027
+ if (added) return;
6028
+ e.currentTarget.style.background = core.alpha(color, 0.12);
6029
+ },
6030
+ children: [
6031
+ !added && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { size: "1.2vh", color }),
6032
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", lts: "0.04em", c: color, children: g })
6033
+ ]
6034
+ },
6035
+ g
6036
+ );
6037
+ }) }),
6038
+ /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { direction: "column", gap: "xxs", children: [
6039
+ value.groups.length === 0 && !addingGroup && /* @__PURE__ */ jsxRuntime.jsx(
6040
+ EmptyHint,
6041
+ {
6042
+ text: t("AccessNoGroups", "No ACE groups added. e.g. group.admin")
6043
+ }
6044
+ ),
6045
+ value.groups.map((name, i) => /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6046
+ /* @__PURE__ */ jsxRuntime.jsx(
6047
+ core.TextInput,
6048
+ {
6049
+ value: name,
6050
+ onChange: (e) => setGroup(i, e.currentTarget.value),
6051
+ placeholder: t("AccessGroupPlaceholder", "ACE group or permission, e.g. group.admin"),
6052
+ size: "xs",
6053
+ style: { flex: 1 }
6054
+ }
6055
+ ),
6056
+ /* @__PURE__ */ jsxRuntime.jsx(
6057
+ core.ActionIcon,
6058
+ {
6059
+ variant: "subtle",
6060
+ color: "red",
6061
+ size: "md",
6062
+ onClick: () => removeGroup(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
+ addingGroup && /* @__PURE__ */ jsxRuntime.jsx(
6070
+ AceGroupAddRow,
6071
+ {
6072
+ placeholder: t("AccessGroupPlaceholder", "ACE group or permission, e.g. group.admin"),
6073
+ onCommit: commitGroup,
6074
+ onCancel: () => setAddingGroup(false),
6075
+ removeLabel: t("Remove", "Remove")
6076
+ }
6077
+ )
6078
+ ] }),
6079
+ !addingGroup && /* @__PURE__ */ jsxRuntime.jsx(AddRowButton, { label: t("AccessAddGroup", "Add group"), onClick: () => setAddingGroup(true) }),
6080
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { icon: lucideReact.UserRound, label: t("AccessPlayers", "Players") }),
6081
+ /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { direction: "column", gap: "xxs", children: [
6082
+ value.identifiers.length === 0 && !addingPlayer && /* @__PURE__ */ jsxRuntime.jsx(EmptyHint, { text: t("AccessNoPlayers", "No players added.") }),
6083
+ value.identifiers.map((id, i) => /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6084
+ /* @__PURE__ */ jsxRuntime.jsx(
6085
+ PlayerSelect,
6086
+ {
6087
+ value: id || null,
6088
+ onChange: (p) => setIdentifier(i, p?.citizenId ?? ""),
6089
+ includeOffline,
6090
+ placeholder: t("AccessPickPlayer", "Pick a player\u2026"),
6091
+ style: { flex: 1 },
6092
+ size: "xs"
6093
+ }
6094
+ ),
6095
+ /* @__PURE__ */ jsxRuntime.jsx(
6096
+ core.ActionIcon,
6097
+ {
6098
+ variant: "subtle",
6099
+ color: "red",
6100
+ size: "md",
6101
+ onClick: () => removeIdentifier(i),
6102
+ title: t("Remove", "Remove"),
6103
+ "aria-label": t("Remove", "Remove"),
6104
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6105
+ }
6106
+ )
6107
+ ] }, i)),
6108
+ addingPlayer && /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6109
+ /* @__PURE__ */ jsxRuntime.jsx(
6110
+ PlayerSelect,
6111
+ {
6112
+ value: null,
6113
+ onChange: (p) => commitIdentifier(p?.citizenId ?? ""),
6114
+ includeOffline,
6115
+ placeholder: t("AccessPickPlayer", "Pick a player\u2026"),
6116
+ style: { flex: 1 },
6117
+ size: "xs"
6118
+ }
6119
+ ),
6120
+ /* @__PURE__ */ jsxRuntime.jsx(
6121
+ core.ActionIcon,
6122
+ {
6123
+ variant: "subtle",
6124
+ color: "red",
6125
+ size: "md",
6126
+ onClick: () => setAddingPlayer(false),
6127
+ title: t("Remove", "Remove"),
6128
+ "aria-label": t("Remove", "Remove"),
6129
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6130
+ }
6131
+ )
6132
+ ] })
6133
+ ] }),
6134
+ !addingPlayer && /* @__PURE__ */ jsxRuntime.jsx(AddRowButton, { label: t("AccessAddPlayer", "Add player"), onClick: () => setAddingPlayer(true) })
6135
+ ]
6136
+ }
6137
+ );
6138
+ }
6139
+ function AddRowButton({ label, onClick }) {
6140
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6141
+ core.Flex,
6142
+ {
6143
+ align: "center",
6144
+ justify: "center",
6145
+ gap: "xs",
6146
+ onClick,
6147
+ role: "button",
6148
+ tabIndex: 0,
6149
+ onKeyDown: (e) => {
6150
+ if (e.key === "Enter" || e.key === " ") {
6151
+ e.preventDefault();
6152
+ onClick();
6153
+ }
6154
+ },
6155
+ py: "xs",
6156
+ style: {
6157
+ cursor: "pointer",
6158
+ border: "0.1vh dashed rgba(255,255,255,0.12)",
6159
+ borderRadius: "0.4vh",
6160
+ transition: "background 0.15s, border-color 0.15s"
6161
+ },
6162
+ onMouseEnter: (e) => {
6163
+ e.currentTarget.style.background = "rgba(255,255,255,0.03)";
6164
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.2)";
6165
+ },
6166
+ onMouseLeave: (e) => {
6167
+ e.currentTarget.style.background = "transparent";
6168
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)";
6169
+ },
6170
+ children: [
6171
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { size: "1.4vh", color: "rgba(255,255,255,0.4)" }),
6172
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.06em", c: "rgba(255,255,255,0.4)", children: label })
6173
+ ]
6174
+ }
6175
+ );
6176
+ }
6177
+ function AceGroupAddRow({
6178
+ placeholder,
6179
+ onCommit,
6180
+ onCancel,
6181
+ removeLabel
6182
+ }) {
6183
+ const [draft, setDraft] = react.useState("");
6184
+ const commit = () => {
6185
+ if (draft.trim()) onCommit(draft);
6186
+ else onCancel();
6187
+ };
6188
+ return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { align: "center", gap: "xs", children: [
6189
+ /* @__PURE__ */ jsxRuntime.jsx(
6190
+ core.TextInput,
6191
+ {
6192
+ value: draft,
6193
+ onChange: (e) => setDraft(e.currentTarget.value),
6194
+ onKeyDown: (e) => {
6195
+ if (e.key === "Enter") {
6196
+ e.preventDefault();
6197
+ commit();
6198
+ } else if (e.key === "Escape") {
6199
+ e.preventDefault();
6200
+ onCancel();
6201
+ }
6202
+ },
6203
+ onBlur: commit,
6204
+ placeholder,
6205
+ size: "xs",
6206
+ autoFocus: true,
6207
+ style: { flex: 1 }
6208
+ }
6209
+ ),
6210
+ /* @__PURE__ */ jsxRuntime.jsx(
6211
+ core.ActionIcon,
6212
+ {
6213
+ variant: "subtle",
6214
+ color: "red",
6215
+ size: "md",
6216
+ onClick: onCancel,
6217
+ title: removeLabel,
6218
+ "aria-label": removeLabel,
6219
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: "1.4vh" })
6220
+ }
6221
+ )
6222
+ ] });
6223
+ }
5743
6224
 
5744
6225
  // src/utils/gtaAnimPostFx.ts
5745
6226
  var GTA_ANIM_POST_FX_GROUP_ORDER = [
@@ -6329,7 +6810,7 @@ function ScenarioSelect({
6329
6810
  }
6330
6811
  );
6331
6812
  }
6332
- var t = (key, fallback) => {
6813
+ var t2 = (key, fallback) => {
6333
6814
  const v = locale(key);
6334
6815
  return v === key ? fallback : v;
6335
6816
  };
@@ -6397,7 +6878,7 @@ function DiscordRoleSelect(props) {
6397
6878
  return map;
6398
6879
  }, [roles]);
6399
6880
  if (errorCode === "NotConfigured") {
6400
- const notConfiguredPlaceholder = t(
6881
+ const notConfiguredPlaceholder = t2(
6401
6882
  "DiscordNotConfigured",
6402
6883
  "Not configured \u2014 set in /dirk_lib"
6403
6884
  );
@@ -6465,9 +6946,9 @@ function DiscordRoleSelect(props) {
6465
6946
  e.stopPropagation();
6466
6947
  refresh();
6467
6948
  },
6468
- title: t("Refresh", "Refresh"),
6949
+ title: t2("Refresh", "Refresh"),
6469
6950
  loading,
6470
- "aria-label": t("Refresh", "Refresh"),
6951
+ "aria-label": t2("Refresh", "Refresh"),
6471
6952
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: "1.2vh" })
6472
6953
  }
6473
6954
  );
@@ -6477,7 +6958,7 @@ function DiscordRoleSelect(props) {
6477
6958
  {
6478
6959
  label,
6479
6960
  size,
6480
- placeholder: placeholder || t("PickRoles", "Pick roles..."),
6961
+ placeholder: placeholder || t2("PickRoles", "Pick roles..."),
6481
6962
  data,
6482
6963
  value: props.value || [],
6483
6964
  onChange: (ids) => props.onChange(ids),
@@ -6489,7 +6970,7 @@ function DiscordRoleSelect(props) {
6489
6970
  rightSectionPointerEvents: "all",
6490
6971
  style,
6491
6972
  styles,
6492
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6973
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6493
6974
  }
6494
6975
  );
6495
6976
  }
@@ -6498,7 +6979,7 @@ function DiscordRoleSelect(props) {
6498
6979
  {
6499
6980
  label,
6500
6981
  size,
6501
- placeholder: placeholder || t("PickRole", "Pick a role..."),
6982
+ placeholder: placeholder || t2("PickRole", "Pick a role..."),
6502
6983
  data,
6503
6984
  value: props.value || null,
6504
6985
  onChange: (id) => props.onChange(id),
@@ -6510,11 +6991,11 @@ function DiscordRoleSelect(props) {
6510
6991
  rightSectionPointerEvents: "all",
6511
6992
  style,
6512
6993
  styles,
6513
- nothingFoundMessage: errorCode ? `${t("Error", "Error")} (${errorCode})` : t("NoRoles", "No roles found")
6994
+ nothingFoundMessage: errorCode ? `${t2("Error", "Error")} (${errorCode})` : t2("NoRoles", "No roles found")
6514
6995
  }
6515
6996
  );
6516
6997
  }
6517
- var t2 = (key, fallback) => {
6998
+ var t3 = (key, fallback) => {
6518
6999
  const v = locale(key);
6519
7000
  return v === key ? fallback : v;
6520
7001
  };
@@ -6535,7 +7016,7 @@ function FrameworkHint({ framework }) {
6535
7016
  const theme = core.useMantineTheme();
6536
7017
  if (!framework) return null;
6537
7018
  const hintKey = `AccountFrameworkHint_${framework}`;
6538
- const text = t2(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
7019
+ const text = t3(hintKey, FRAMEWORK_HINTS[framework] || FRAMEWORK_HINTS.unknown);
6539
7020
  return /* @__PURE__ */ jsxRuntime.jsxs(
6540
7021
  core.Flex,
6541
7022
  {
@@ -6619,9 +7100,9 @@ function AccountSelect(props) {
6619
7100
  e.stopPropagation();
6620
7101
  refresh();
6621
7102
  },
6622
- title: t2("Refresh", "Refresh"),
7103
+ title: t3("Refresh", "Refresh"),
6623
7104
  loading,
6624
- "aria-label": t2("Refresh", "Refresh"),
7105
+ "aria-label": t3("Refresh", "Refresh"),
6625
7106
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: "1.2vh" })
6626
7107
  }
6627
7108
  );
@@ -6635,14 +7116,14 @@ function AccountSelect(props) {
6635
7116
  rightSection: refreshButton,
6636
7117
  rightSectionPointerEvents: "all",
6637
7118
  style,
6638
- nothingFoundMessage: t2("NoAccounts", "No accounts available")
7119
+ nothingFoundMessage: t3("NoAccounts", "No accounts available")
6639
7120
  };
6640
7121
  return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { direction: "column", gap: "0.3vh", style, children: [
6641
7122
  props.multi ? /* @__PURE__ */ jsxRuntime.jsx(
6642
7123
  core.MultiSelect,
6643
7124
  {
6644
7125
  ...sharedSelectProps,
6645
- placeholder: placeholder || t2("PickAccounts", "Pick accounts (or type custom)..."),
7126
+ placeholder: placeholder || t3("PickAccounts", "Pick accounts (or type custom)..."),
6646
7127
  value: props.value || [],
6647
7128
  onChange: (names) => props.onChange(names),
6648
7129
  renderOption,
@@ -6654,7 +7135,7 @@ function AccountSelect(props) {
6654
7135
  core.Select,
6655
7136
  {
6656
7137
  ...sharedSelectProps,
6657
- placeholder: placeholder || t2("PickAccount", "Pick an account..."),
7138
+ placeholder: placeholder || t3("PickAccount", "Pick an account..."),
6658
7139
  value: props.value || null,
6659
7140
  onChange: (name) => props.onChange(name),
6660
7141
  renderOption,
@@ -6946,175 +7427,6 @@ function WorldPositionGotoButton2({
6946
7427
  }
6947
7428
  ) });
6948
7429
  }
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
7430
  function usePickDoor() {
7119
7431
  const begin = useAdminToolStore((s) => s.begin);
7120
7432
  return async () => {
@@ -7391,6 +7703,7 @@ function BlipMarker({
7391
7703
  );
7392
7704
  }
7393
7705
 
7706
+ exports.AccessOverrideSection = AccessOverrideSection;
7394
7707
  exports.AccountSelect = AccountSelect;
7395
7708
  exports.AdminPageTitle = AdminPageTitle;
7396
7709
  exports.AnimPostFxSelect = AnimPostFxSelect;