dirk-cfx-react 1.1.58 → 1.1.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
- import { Flex, Text, Image, TextInput, Select, Box, useMantineTheme, alpha, Progress, RingProgress, Portal, Button, Loader, Group, JsonInput } from '@mantine/core';
1
+ import { Flex, Text, Image, TextInput, Select, Box, useMantineTheme, Tooltip, alpha, Progress, RingProgress, Portal, Button, Loader, ActionIcon, Stack, Group, JsonInput } from '@mantine/core';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { createContext, useContext, useRef, useState, useEffect, useMemo } from 'react';
4
4
  import { create, useStore, createStore } from 'zustand';
5
5
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6
6
  import { motion, AnimatePresence, useMotionValue } from 'framer-motion';
7
+ import { Info, X, AlertTriangle, Trash2, Check, FlaskConical, ChevronUp, ChevronDown, ArrowLeft, Undo2, Redo2, Save, History, XCircle, Code2, RotateCcw, Search, Filter, User } from 'lucide-react';
7
8
  import clickSoundUrl from '../click_sound-PNCRRTM4.mp3';
8
9
  import hoverSoundUrl from '../hover_sound-NBUA222C.mp3';
9
- import { X, AlertTriangle, Trash2, Check, Undo2, Redo2, Save, History, XCircle, Code2, RotateCcw, Search, Filter, User, ChevronDown } from 'lucide-react';
10
10
  import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query';
11
11
 
12
12
  // src/components/BlipSelect.tsx
@@ -1267,13 +1267,24 @@ async function fetchNui(eventName, data, mockData) {
1267
1267
  }
1268
1268
  const overrideResourceName = useSettings.getState().overideResourceName;
1269
1269
  const resourceName = window.GetParentResourceName ? window.GetParentResourceName() : overrideResourceName ? overrideResourceName : "dirk-cfx-react";
1270
- const resp = await fetch(`https://${resourceName}/${eventName}`, options);
1271
- return await resp.json();
1270
+ try {
1271
+ const resp = await fetch(`https://${resourceName}/${eventName}`, options);
1272
+ return await resp.json();
1273
+ } catch {
1274
+ return mockData ?? {};
1275
+ }
1272
1276
  }
1273
1277
  async function registerInitialFetch(eventName, data, mockData) {
1274
1278
  const fetcher = () => fetchNui(eventName, data, mockData);
1275
1279
  return fetcher();
1276
1280
  }
1281
+ var reportedMissing = /* @__PURE__ */ new Set();
1282
+ function reportMissingLocale(key) {
1283
+ if (!key || reportedMissing.has(key)) return;
1284
+ reportedMissing.add(key);
1285
+ fetchNui("REPORT_MISSING_LOCALE", { key }).catch(() => {
1286
+ });
1287
+ }
1277
1288
  var localeStore = create((set, get) => {
1278
1289
  return {
1279
1290
  locales: {
@@ -1281,6 +1292,7 @@ var localeStore = create((set, get) => {
1281
1292
  },
1282
1293
  locale: (key, ...args) => {
1283
1294
  const exists = get().locales[key];
1295
+ if (!exists) reportMissingLocale(key);
1284
1296
  let translation = exists || key;
1285
1297
  if (args.length) {
1286
1298
  translation = translation.replace(/%s/g, () => String(args.shift() || ""));
@@ -1292,7 +1304,16 @@ var localeStore = create((set, get) => {
1292
1304
  var locale = localeStore.getState().locale;
1293
1305
  registerInitialFetch("GET_LOCALES", void 0).then((data) => {
1294
1306
  localeStore.setState({ locales: data });
1307
+ }).catch(() => {
1295
1308
  });
1309
+ if (typeof window !== "undefined") {
1310
+ window.addEventListener("message", (event) => {
1311
+ const msg = event.data;
1312
+ if (!msg || msg.action !== "UPDATE_DIRK_LIB_LOCALES") return;
1313
+ if (!msg.data || typeof msg.data !== "object") return;
1314
+ localeStore.setState({ locales: msg.data });
1315
+ });
1316
+ }
1296
1317
  var useItems = create(() => ({}));
1297
1318
  var useItemsList = (excludeItemNames = []) => {
1298
1319
  const excludeSet = new Set(excludeItemNames);
@@ -1305,6 +1326,7 @@ registerInitialFetch("FETCH_ALL_ITEMS", null, {
1305
1326
  }).then((fetchedItems) => {
1306
1327
  if (!fetchedItems) return;
1307
1328
  useItems.setState(fetchedItems);
1329
+ }).catch(() => {
1308
1330
  });
1309
1331
 
1310
1332
  // src/utils/inputMapper.ts
@@ -1931,8 +1953,8 @@ function InputContainer(props) {
1931
1953
  (props.title || props.description) && /* @__PURE__ */ jsxs(
1932
1954
  Flex,
1933
1955
  {
1934
- direction: "column",
1935
- gap: "xxs",
1956
+ align: "center",
1957
+ flex: 1,
1936
1958
  p: props.p == "0" ? "sm" : 0,
1937
1959
  children: [
1938
1960
  props.title && /* @__PURE__ */ jsx(
@@ -1949,12 +1971,26 @@ function InputContainer(props) {
1949
1971
  }
1950
1972
  ),
1951
1973
  props.description && /* @__PURE__ */ jsx(
1952
- Text,
1974
+ Tooltip,
1953
1975
  {
1954
- size: "xs",
1955
- c: "rgba(255, 255, 255, 0.8)",
1956
- fw: 400,
1957
- children: props.description
1976
+ label: props.description,
1977
+ position: "top-end",
1978
+ withArrow: true,
1979
+ multiline: true,
1980
+ maw: "22vh",
1981
+ styles: {
1982
+ tooltip: {
1983
+ background: alpha(theme.colors.dark[7], 0.95),
1984
+ border: `0.1vh solid rgba(255,255,255,0.1)`,
1985
+ color: "rgba(255,255,255,0.75)",
1986
+ fontFamily: "Akrobat Bold",
1987
+ fontSize: "1.3vh",
1988
+ lineHeight: 1.3,
1989
+ padding: "0.6vh 0.8vh",
1990
+ letterSpacing: "0.03em"
1991
+ }
1992
+ },
1993
+ children: /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", style: { marginLeft: "auto", cursor: "help" }, children: /* @__PURE__ */ jsx(Info, { size: "1.6vh", color: alpha(theme.colors[theme.primaryColor][5], 0.45) }) })
1958
1994
  }
1959
1995
  )
1960
1996
  ]
@@ -1971,13 +2007,7 @@ function InputContainer(props) {
1971
2007
  children: props.error
1972
2008
  }
1973
2009
  ),
1974
- /* @__PURE__ */ jsx(
1975
- Flex,
1976
- {
1977
- ml: "auto",
1978
- children: props.rightSection
1979
- }
1980
- )
2010
+ props.rightSection && /* @__PURE__ */ jsx(Flex, { children: props.rightSection })
1981
2011
  ]
1982
2012
  }
1983
2013
  ),
@@ -2668,7 +2698,7 @@ function Modal({
2668
2698
  children: description
2669
2699
  }
2670
2700
  ) }),
2671
- children
2701
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }, children })
2672
2702
  ]
2673
2703
  }
2674
2704
  )
@@ -3291,10 +3321,10 @@ function useFormActions() {
3291
3321
  }
3292
3322
  return store.getState();
3293
3323
  }
3294
- function getScriptSettingsInstance() {
3295
- throw new Error("[dirk-cfx-react] createScriptSettings must be called before using SettingsPanel");
3324
+ function getScriptConfigInstance() {
3325
+ throw new Error("[dirk-cfx-react] createScriptConfig must be called before using ConfigPanel");
3296
3326
  }
3297
- var settingsPanelQueryClient = new QueryClient({
3327
+ var configPanelQueryClient = new QueryClient({
3298
3328
  defaultOptions: { queries: { staleTime: 3e4, gcTime: 5 * 6e4 } }
3299
3329
  });
3300
3330
  function NavItemButton({
@@ -3338,7 +3368,7 @@ function NavItemButton({
3338
3368
  }
3339
3369
  );
3340
3370
  }
3341
- function SettingsJsonModal({
3371
+ function ConfigJsonModal({
3342
3372
  onClose,
3343
3373
  schema
3344
3374
  }) {
@@ -3375,7 +3405,7 @@ function SettingsJsonModal({
3375
3405
  setError(e.message);
3376
3406
  }
3377
3407
  };
3378
- return /* @__PURE__ */ jsxs(Modal, { title: "Settings JSON", icon: Code2, iconColor: color, onClose, width: "60vh", maxHeight: "80vh", zIndex: 200, children: [
3408
+ return /* @__PURE__ */ jsxs(Modal, { title: "Config JSON", icon: Code2, iconColor: color, onClose, width: "60vh", maxHeight: "80vh", zIndex: 200, children: [
3379
3409
  /* @__PURE__ */ jsxs(Box, { flex: 1, p: "0.8vh", style: { overflowY: "auto" }, children: [
3380
3410
  /* @__PURE__ */ jsx(
3381
3411
  JsonInput,
@@ -3517,10 +3547,10 @@ function HistoryTableHeader() {
3517
3547
  /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", c: "rgba(255,255,255,0.45)", children: "Version" })
3518
3548
  ] });
3519
3549
  }
3520
- function SettingsHistoryModal({
3550
+ function ConfigHistoryModal({
3521
3551
  onClose
3522
3552
  }) {
3523
- const { getHistory } = getScriptSettingsInstance();
3553
+ const { getHistory } = getScriptConfigInstance();
3524
3554
  const theme = useMantineTheme();
3525
3555
  const color = theme.colors[theme.primaryColor][5];
3526
3556
  const [queryInput, setQueryInput] = useState("");
@@ -3532,7 +3562,7 @@ function SettingsHistoryModal({
3532
3562
  const [expandedKey, setExpandedKey] = useState(null);
3533
3563
  const filters = useMemo(() => ({ query, path, admin }), [query, path, admin]);
3534
3564
  const historyQuery = useInfiniteQuery({
3535
- queryKey: ["scriptSettingsHistory", filters],
3565
+ queryKey: ["scriptConfigHistory", filters],
3536
3566
  initialPageParam: 0,
3537
3567
  queryFn: async ({ pageParam }) => {
3538
3568
  const response = await getHistory({
@@ -3543,7 +3573,7 @@ function SettingsHistoryModal({
3543
3573
  admin: filters.admin || void 0
3544
3574
  });
3545
3575
  if (!response?.success || !response.data) {
3546
- throw new Error(response?._error || "Failed to load settings history");
3576
+ throw new Error(response?._error || "Failed to load config history");
3547
3577
  }
3548
3578
  return response.data;
3549
3579
  },
@@ -3557,7 +3587,7 @@ function SettingsHistoryModal({
3557
3587
  historyQuery.fetchNextPage();
3558
3588
  }
3559
3589
  };
3560
- return /* @__PURE__ */ jsxs(Modal, { title: "Settings History", icon: History, iconColor: color, onClose, width: "88vh", maxHeight: "82vh", zIndex: 260, children: [
3590
+ return /* @__PURE__ */ jsxs(Modal, { title: "Config History", icon: History, iconColor: color, onClose, width: "88vh", maxHeight: "82vh", zIndex: 260, children: [
3561
3591
  /* @__PURE__ */ jsxs(Flex, { direction: "column", style: { flex: 1, minHeight: 0 }, children: [
3562
3592
  /* @__PURE__ */ jsxs(Flex, { gap: "xs", p: "sm", style: { borderBottom: `0.1vh solid ${alpha(theme.colors.dark[7], 0.8)}` }, children: [
3563
3593
  /* @__PURE__ */ jsx(TextInput, { leftSection: /* @__PURE__ */ jsx(Search, { size: "1.4vh" }), placeholder: "Search path/admin/value", value: queryInput, onChange: (e) => setQueryInput(e.currentTarget.value), size: "xs", style: { flex: 1 } }),
@@ -3606,7 +3636,7 @@ function SettingsHistoryModal({
3606
3636
  ) })
3607
3637
  ] });
3608
3638
  }
3609
- function SettingsPanelInner({
3639
+ function ConfigPanelInner({
3610
3640
  navItems,
3611
3641
  title,
3612
3642
  subtitle,
@@ -3615,28 +3645,37 @@ function SettingsPanelInner({
3615
3645
  onClose,
3616
3646
  schema,
3617
3647
  resetConfirmText,
3618
- defaultSettings,
3648
+ defaultConfig,
3619
3649
  width,
3620
3650
  height
3621
3651
  }) {
3622
- const { updateSettings, getHistory } = getScriptSettingsInstance();
3652
+ const { updateConfig, resetConfig, getHistory } = getScriptConfigInstance();
3623
3653
  const form = useForm();
3624
3654
  const theme = useMantineTheme();
3625
3655
  const color = theme.colors[theme.primaryColor][5];
3626
3656
  const version = useSettings((s) => s.resourceVersion);
3627
3657
  const [activeTab, setActiveTab] = useState(navItems[0]?.id ?? "");
3658
+ const firstMountRef = useRef(true);
3628
3659
  const [jsonOpen, setJsonOpen] = useState(false);
3629
3660
  const [historyOpen, setHistoryOpen] = useState(false);
3630
3661
  const [resetOpen, setResetOpen] = useState(false);
3631
- const [closeConfirmOpen, setCloseConfirmOpen] = useState(false);
3662
+ const [pendingAction, setPendingAction] = useState(null);
3632
3663
  const changedCount = form.changedCount ?? 0;
3633
3664
  const isDirty = changedCount > 0;
3665
+ const goBack = () => fetchNui("CONFIG_PANEL_BACK");
3666
+ const handleBack = () => {
3667
+ if (isDirty) {
3668
+ setPendingAction("back");
3669
+ return;
3670
+ }
3671
+ goBack();
3672
+ };
3634
3673
  useEffect(() => {
3635
3674
  function handleKeyDown(e) {
3636
3675
  if (e.key !== "Escape") return;
3637
3676
  if (isDirty) {
3638
3677
  e.preventDefault();
3639
- setCloseConfirmOpen(true);
3678
+ setPendingAction("close");
3640
3679
  return;
3641
3680
  }
3642
3681
  onClose();
@@ -3645,34 +3684,40 @@ function SettingsPanelInner({
3645
3684
  return () => window.removeEventListener("keydown", handleKeyDown);
3646
3685
  }, [isDirty, onClose]);
3647
3686
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3648
- /* @__PURE__ */ jsx(AnimatePresence, { children: jsonOpen && /* @__PURE__ */ jsx(SettingsJsonModal, { onClose: () => setJsonOpen(false), schema }) }),
3649
- /* @__PURE__ */ jsx(AnimatePresence, { children: historyOpen && /* @__PURE__ */ jsx(SettingsHistoryModal, { onClose: () => setHistoryOpen(false) }) }),
3687
+ /* @__PURE__ */ jsx(AnimatePresence, { children: jsonOpen && /* @__PURE__ */ jsx(ConfigJsonModal, { onClose: () => setJsonOpen(false), schema }) }),
3688
+ /* @__PURE__ */ jsx(AnimatePresence, { children: historyOpen && /* @__PURE__ */ jsx(ConfigHistoryModal, { onClose: () => setHistoryOpen(false) }) }),
3650
3689
  /* @__PURE__ */ jsx(AnimatePresence, { children: resetOpen && /* @__PURE__ */ jsx(
3651
3690
  ConfirmModal,
3652
3691
  {
3653
3692
  title: "Reset to Defaults",
3654
- description: "This will permanently reset ALL settings back to their defaults. Every setting you have configured will be overwritten. This cannot be undone.",
3655
- confirmLabel: "Reset Settings",
3693
+ description: "This will permanently reset ALL config back to the defaults. Every value you have configured will be overwritten. This cannot be undone.",
3694
+ confirmLabel: "Reset Config",
3656
3695
  confirmText: resetConfirmText,
3657
- onConfirm: () => {
3658
- updateSettings(defaultSettings).then(() => form.reinitialize(cloneSettings(defaultSettings)));
3696
+ onConfirm: async () => {
3659
3697
  setResetOpen(false);
3698
+ const result = await resetConfig();
3699
+ if (result?.success) {
3700
+ const { store } = getScriptConfigInstance();
3701
+ form.reinitialize(cloneConfig(store.getState()));
3702
+ }
3660
3703
  },
3661
3704
  onClose: () => setResetOpen(false),
3662
3705
  zIndex: 300
3663
3706
  }
3664
3707
  ) }),
3665
- /* @__PURE__ */ jsx(AnimatePresence, { children: closeConfirmOpen && /* @__PURE__ */ jsx(
3708
+ /* @__PURE__ */ jsx(AnimatePresence, { children: pendingAction !== null && /* @__PURE__ */ jsx(
3666
3709
  ConfirmModal,
3667
3710
  {
3668
3711
  title: "Discard Unsaved Changes?",
3669
- description: "You have unsaved changes. Closing now will discard them.",
3670
- confirmLabel: "Close Without Saving",
3712
+ description: pendingAction === "back" ? "You have unsaved changes. Going back now will discard them." : "You have unsaved changes. Closing now will discard them.",
3713
+ confirmLabel: pendingAction === "back" ? "Go Back Without Saving" : "Close Without Saving",
3671
3714
  onConfirm: () => {
3672
- setCloseConfirmOpen(false);
3673
- onClose();
3715
+ const action = pendingAction;
3716
+ setPendingAction(null);
3717
+ if (action === "back") goBack();
3718
+ else onClose();
3674
3719
  },
3675
- onClose: () => setCloseConfirmOpen(false),
3720
+ onClose: () => setPendingAction(null),
3676
3721
  zIndex: 300
3677
3722
  }
3678
3723
  ) }),
@@ -3699,9 +3744,33 @@ function SettingsPanelInner({
3699
3744
  exit: { scale: 0.3, opacity: 0, transform: "translate(-50%, -50%)" },
3700
3745
  children: [
3701
3746
  /* @__PURE__ */ jsxs(Flex, { direction: "column", style: { width: "18vh", flexShrink: 0, borderRight: `0.1vh solid ${alpha(theme.colors.dark[6], 0.8)}`, background: alpha(theme.colors.dark[8], 0.6), overflow: "hidden" }, children: [
3702
- /* @__PURE__ */ jsxs(Flex, { align: "baseline", gap: "0.3vh", px: "sm", py: "sm", style: { borderBottom: `0.1vh solid ${alpha(theme.colors.dark[6], 0.5)}`, flexShrink: 0 }, children: [
3703
- /* @__PURE__ */ jsx(Text, { size: "lg", ff: "Akrobat Bold", tt: "uppercase", children: title }),
3704
- subtitle && /* @__PURE__ */ jsx(Text, { tt: "uppercase", fw: 600, c: color, children: subtitle })
3747
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "0.6vh", px: "sm", py: "sm", style: { borderBottom: `0.1vh solid ${alpha(theme.colors.dark[6], 0.5)}`, flexShrink: 0 }, children: [
3748
+ /* @__PURE__ */ jsx(
3749
+ motion.button,
3750
+ {
3751
+ title: "Back to script list",
3752
+ onClick: handleBack,
3753
+ whileHover: { background: alpha(color, 0.16), borderColor: alpha(color, 0.45) },
3754
+ whileTap: { scale: 0.95 },
3755
+ style: {
3756
+ aspectRatio: "1 / 1",
3757
+ height: "2.4vh",
3758
+ background: alpha(color, 0.08),
3759
+ border: `0.1vh solid ${alpha(color, 0.3)}`,
3760
+ borderRadius: theme.radius.xs,
3761
+ cursor: "pointer",
3762
+ display: "flex",
3763
+ alignItems: "center",
3764
+ justifyContent: "center",
3765
+ flexShrink: 0
3766
+ },
3767
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: "1.4vh", color })
3768
+ }
3769
+ ),
3770
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", style: { minWidth: 0, lineHeight: 1 }, children: [
3771
+ /* @__PURE__ */ jsx(Text, { size: "lg", ff: "Akrobat Bold", tt: "uppercase", lts: "0.04em", truncate: true, children: title }),
3772
+ subtitle && /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", size: "xxs", tt: "uppercase", lts: "0.08em", c: color, truncate: true, children: subtitle })
3773
+ ] })
3705
3774
  ] }),
3706
3775
  /* @__PURE__ */ jsxs(Flex, { gap: "xxs", px: "xs", py: "xs", style: { borderBottom: `0.1vh solid ${alpha(theme.colors.dark[6], 0.4)}`, flexShrink: 0 }, children: [
3707
3776
  /* @__PURE__ */ jsx(
@@ -3804,7 +3873,7 @@ function SettingsPanelInner({
3804
3873
  /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsx(
3805
3874
  motion.div,
3806
3875
  {
3807
- initial: { opacity: 0, y: 4 },
3876
+ initial: firstMountRef.current ? (firstMountRef.current = false, false) : { opacity: 0, y: 4 },
3808
3877
  animate: { opacity: 1, y: 0 },
3809
3878
  exit: { opacity: 0, y: -4 },
3810
3879
  transition: { duration: 0.15 },
@@ -3818,15 +3887,15 @@ function SettingsPanelInner({
3818
3887
  )
3819
3888
  ] });
3820
3889
  }
3821
- function cloneSettings(value) {
3890
+ function cloneConfig(value) {
3822
3891
  return JSON.parse(JSON.stringify(value));
3823
3892
  }
3824
3893
  function ServerOnlyFetcher() {
3825
- const { fetchSettings } = getScriptSettingsInstance();
3894
+ const { fetchConfig } = getScriptConfigInstance();
3826
3895
  const { reinitialize } = useFormActions();
3827
3896
  useEffect(() => {
3828
3897
  let cancelled = false;
3829
- fetchSettings().then((full) => {
3898
+ fetchConfig().then((full) => {
3830
3899
  if (!cancelled && full) reinitialize(full);
3831
3900
  }).catch(() => {
3832
3901
  });
@@ -3837,28 +3906,28 @@ function ServerOnlyFetcher() {
3837
3906
  return null;
3838
3907
  }
3839
3908
  var defaultOnClose = () => fetchNui("CLOSE_ADMIN_SECTION");
3840
- function SettingsPanel(props) {
3909
+ function ConfigPanel(props) {
3841
3910
  const { open, onClose = defaultOnClose } = props;
3842
- const { store, updateSettings, fetchSettings } = getScriptSettingsInstance();
3911
+ const { store, updateConfig } = getScriptConfigInstance();
3843
3912
  const [isSaving, setIsSaving] = useState(false);
3844
3913
  if (!open) return null;
3845
- return /* @__PURE__ */ jsx(QueryClientProvider, { client: settingsPanelQueryClient, children: /* @__PURE__ */ jsxs(
3914
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: configPanelQueryClient, children: /* @__PURE__ */ jsxs(
3846
3915
  FormProvider,
3847
3916
  {
3848
- initialValues: cloneSettings(store.getState()),
3917
+ initialValues: cloneConfig(store.getState()),
3849
3918
  onSubmit: async (form) => {
3850
3919
  if (isSaving) return;
3851
3920
  setIsSaving(true);
3852
3921
  try {
3853
- const result = await updateSettings(form.values);
3922
+ const result = await updateConfig(form.values);
3854
3923
  if (result?.success) {
3855
- form.reinitialize(cloneSettings(form.values));
3856
- settingsPanelQueryClient.invalidateQueries({ queryKey: ["scriptSettingsHistory"] });
3924
+ form.reinitialize(cloneConfig(form.values));
3925
+ configPanelQueryClient.invalidateQueries({ queryKey: ["scriptConfigHistory"] });
3857
3926
  return;
3858
3927
  }
3859
- form.reinitialize(cloneSettings(store.getState()));
3928
+ form.reinitialize(cloneConfig(store.getState()));
3860
3929
  if (result?._error) {
3861
- console.warn(`[SettingsPanel] settings save failed: ${result._error}`);
3930
+ console.warn(`[ConfigPanel] config save failed: ${result._error}`);
3862
3931
  }
3863
3932
  } finally {
3864
3933
  setIsSaving(false);
@@ -3867,7 +3936,7 @@ function SettingsPanel(props) {
3867
3936
  children: [
3868
3937
  /* @__PURE__ */ jsx(ServerOnlyFetcher, {}),
3869
3938
  /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsx(
3870
- SettingsPanelInner,
3939
+ ConfigPanelInner,
3871
3940
  {
3872
3941
  ...props,
3873
3942
  onClose,
@@ -4075,7 +4144,161 @@ function AdminPageTitle(props) {
4075
4144
  /* @__PURE__ */ jsx(Text, { ff: "Akrobat Bold", tt: "uppercase", lts: "0.1em", size: "sm", c: "rgba(255,255,255,0.6)", children: locale(props.title) })
4076
4145
  ] });
4077
4146
  }
4147
+ var loadPersistedState = (storageKey) => {
4148
+ try {
4149
+ const raw = localStorage.getItem(storageKey);
4150
+ return raw ? JSON.parse(raw) : {};
4151
+ } catch {
4152
+ return {};
4153
+ }
4154
+ };
4155
+ var savePersistedState = (storageKey, state) => {
4156
+ try {
4157
+ localStorage.setItem(storageKey, JSON.stringify(state));
4158
+ } catch {
4159
+ }
4160
+ };
4161
+ function TestBed({
4162
+ items,
4163
+ storageKey = "testbed:open-state",
4164
+ disablePersistence = false,
4165
+ title = "TestBed"
4166
+ }) {
4167
+ const [open, setOpen] = useState(false);
4168
+ const itemsRef = useRef(items);
4169
+ itemsRef.current = items;
4170
+ useEffect(() => {
4171
+ if (!isEnvBrowser() || disablePersistence) return;
4172
+ const persisted = loadPersistedState(storageKey);
4173
+ itemsRef.current.forEach((item) => {
4174
+ const persistedValue = persisted[item.key];
4175
+ if (typeof persistedValue === "boolean" && persistedValue !== item.active) {
4176
+ item.onToggle(persistedValue);
4177
+ }
4178
+ });
4179
+ }, []);
4180
+ if (!isEnvBrowser()) return null;
4181
+ const toggle = (item) => {
4182
+ const next = !item.active;
4183
+ item.onToggle(next);
4184
+ if (!disablePersistence) {
4185
+ const persisted = loadPersistedState(storageKey);
4186
+ persisted[item.key] = next;
4187
+ savePersistedState(storageKey, persisted);
4188
+ }
4189
+ };
4190
+ return /* @__PURE__ */ jsxs(
4191
+ "div",
4192
+ {
4193
+ style: {
4194
+ position: "fixed",
4195
+ top: "1vh",
4196
+ left: "1vh",
4197
+ zIndex: 2147483647,
4198
+ pointerEvents: "auto",
4199
+ fontSize: "1.4vh"
4200
+ },
4201
+ children: [
4202
+ /* @__PURE__ */ jsxs(
4203
+ Flex,
4204
+ {
4205
+ align: "center",
4206
+ gap: "xs",
4207
+ px: "sm",
4208
+ py: "xs",
4209
+ onClick: () => setOpen((v) => !v),
4210
+ style: {
4211
+ cursor: "pointer",
4212
+ background: "rgba(0,0,0,0.55)",
4213
+ backdropFilter: "blur(0.6vh)",
4214
+ WebkitBackdropFilter: "blur(0.6vh)",
4215
+ border: "0.1vh solid rgba(255,255,255,0.1)",
4216
+ borderRadius: "var(--mantine-radius-sm)",
4217
+ userSelect: "none",
4218
+ minWidth: "16vh"
4219
+ },
4220
+ children: [
4221
+ /* @__PURE__ */ jsx(FlaskConical, { size: 14, color: "rgba(255,255,255,0.7)" }),
4222
+ /* @__PURE__ */ jsx(
4223
+ Text,
4224
+ {
4225
+ size: "xs",
4226
+ ff: "Akrobat Bold",
4227
+ tt: "uppercase",
4228
+ lts: "0.08em",
4229
+ c: "rgba(255,255,255,0.85)",
4230
+ style: { flex: 1 },
4231
+ children: title
4232
+ }
4233
+ ),
4234
+ /* @__PURE__ */ jsx(ActionIcon, { size: "xs", variant: "transparent", c: "rgba(255,255,255,0.6)", children: open ? /* @__PURE__ */ jsx(ChevronUp, { size: 14 }) : /* @__PURE__ */ jsx(ChevronDown, { size: 14 }) })
4235
+ ]
4236
+ }
4237
+ ),
4238
+ open && /* @__PURE__ */ jsx(
4239
+ Stack,
4240
+ {
4241
+ gap: 4,
4242
+ mt: "xxs",
4243
+ p: "xs",
4244
+ style: {
4245
+ background: "rgba(0,0,0,0.55)",
4246
+ backdropFilter: "blur(0.6vh)",
4247
+ WebkitBackdropFilter: "blur(0.6vh)",
4248
+ border: "0.1vh solid rgba(255,255,255,0.1)",
4249
+ borderRadius: "var(--mantine-radius-sm)",
4250
+ minWidth: "16vh",
4251
+ maxHeight: "80vh",
4252
+ overflowY: "auto"
4253
+ },
4254
+ children: items.map((item) => /* @__PURE__ */ jsxs(
4255
+ Flex,
4256
+ {
4257
+ align: "center",
4258
+ justify: "space-between",
4259
+ gap: "xs",
4260
+ px: "xs",
4261
+ py: "xxs",
4262
+ onClick: () => toggle(item),
4263
+ style: {
4264
+ cursor: "pointer",
4265
+ borderRadius: "var(--mantine-radius-xs)",
4266
+ background: item.active ? "rgba(245,158,11,0.15)" : "rgba(255,255,255,0.03)",
4267
+ border: `0.1vh solid ${item.active ? "rgba(245,158,11,0.35)" : "rgba(255,255,255,0.05)"}`,
4268
+ userSelect: "none"
4269
+ },
4270
+ children: [
4271
+ /* @__PURE__ */ jsx(
4272
+ Text,
4273
+ {
4274
+ size: "xs",
4275
+ ff: "Akrobat Bold",
4276
+ c: item.active ? "#f59e0b" : "rgba(255,255,255,0.75)",
4277
+ children: item.label
4278
+ }
4279
+ ),
4280
+ /* @__PURE__ */ jsx(
4281
+ Text,
4282
+ {
4283
+ size: "xxs",
4284
+ ff: "Akrobat Bold",
4285
+ tt: "uppercase",
4286
+ lts: "0.06em",
4287
+ c: item.active ? "#f59e0b" : "rgba(255,255,255,0.35)",
4288
+ children: item.active ? "On" : "Off"
4289
+ }
4290
+ )
4291
+ ]
4292
+ },
4293
+ item.key
4294
+ ))
4295
+ }
4296
+ )
4297
+ ]
4298
+ }
4299
+ );
4300
+ }
4078
4301
 
4079
- export { AdminPageTitle, AsyncSaveButton, BlipColorSelect, BlipIconSelect, BorderedIcon, ConfirmModal, Counter, FiveMKeyBindInput, FloatingParticles, InfoBox, InputContainer, LevelBanner, LevelPanel, Modal, ModalContext, ModalProvider, MotionFlex, MotionIcon, MotionImage, MotionText, NavBar, NavigationContext, NavigationProvider, PromptModal, SegmentedControl, SegmentedProgress, SelectItem, SettingsPanel, Title, useModal, useModalActions, useNavigation, useNavigationStore };
4302
+ export { AdminPageTitle, AsyncSaveButton, BlipColorSelect, BlipIconSelect, BorderedIcon, ConfigPanel, ConfirmModal, Counter, FiveMKeyBindInput, FloatingParticles, InfoBox, InputContainer, LevelBanner, LevelPanel, Modal, ModalContext, ModalProvider, MotionFlex, MotionIcon, MotionImage, MotionText, NavBar, NavigationContext, NavigationProvider, PromptModal, SegmentedControl, SegmentedProgress, SelectItem, TestBed, Title, useModal, useModalActions, useNavigation, useNavigationStore };
4080
4303
  //# sourceMappingURL=index.js.map
4081
4304
  //# sourceMappingURL=index.js.map