@necrolab/dashboard 0.5.28 → 0.5.29

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.
Files changed (38) hide show
  1. package/backend/api.js +7 -5
  2. package/backend/batching.js +59 -2
  3. package/backend/index.js +1 -1
  4. package/index.html +10 -23
  5. package/package.json +1 -1
  6. package/src/assets/css/base/scroll.scss +1 -1
  7. package/src/assets/css/main.scss +14 -14
  8. package/src/components/Console/ConsoleToolbar.vue +8 -8
  9. package/src/components/Editors/Account/Account.vue +9 -5
  10. package/src/components/Editors/Account/AccountView.vue +37 -18
  11. package/src/components/Editors/Account/CreateAccount.vue +38 -4
  12. package/src/components/Editors/Profile/CreateProfile.vue +29 -4
  13. package/src/components/Editors/Profile/Profile.vue +11 -6
  14. package/src/components/Editors/Profile/ProfileCountryChooser.vue +2 -2
  15. package/src/components/Editors/Profile/ProfileView.vue +37 -18
  16. package/src/components/Tasks/CreateTaskAXS.vue +16 -2
  17. package/src/components/Tasks/CreateTaskTM.vue +28 -5
  18. package/src/components/Tasks/QuickSettings.vue +77 -10
  19. package/src/components/Tasks/Task.vue +20 -7
  20. package/src/components/Tasks/TaskView.vue +144 -58
  21. package/src/components/Tasks/ViewTask.vue +17 -3
  22. package/src/components/ui/Modal.vue +1 -1
  23. package/src/components/ui/ReadonlyFieldsSection.vue +3 -3
  24. package/src/components/ui/StatusBadge.vue +1 -1
  25. package/src/components/ui/TaskToggle.vue +2 -3
  26. package/src/components/ui/controls/CountryChooser.vue +2 -2
  27. package/src/components/ui/controls/atomic/Dropdown.vue +2 -2
  28. package/src/components/ui/controls/atomic/MultiDropdown.vue +1 -1
  29. package/src/composables/useDynamicTableHeight.js +4 -4
  30. package/src/composables/useRowSelection.js +0 -1
  31. package/src/composables/useZoomPrevention.js +16 -55
  32. package/src/stores/connection.js +453 -68
  33. package/src/stores/sampleData.js +34 -24
  34. package/src/stores/ui.js +89 -100
  35. package/src/views/Accounts.vue +2 -5
  36. package/src/views/Console.vue +13 -14
  37. package/src/views/Profiles.vue +2 -5
  38. package/vite.config.js +4 -2
package/backend/api.js CHANGED
@@ -20,8 +20,10 @@ import { users } from "./mock-data.js";
20
20
 
21
21
  const logger = createLogger("WEB UI");
22
22
 
23
- const maxWaitTime = 350; // ms
24
- const maxMessageSize = 100;
23
+ const TASK_BATCH_WAIT_TIME = 120; // ms
24
+ const TASK_BATCH_MAX_MESSAGES = 250;
25
+ const CONSOLE_BATCH_WAIT_TIME = 250; // ms
26
+ const CONSOLE_BATCH_MAX_MESSAGES = 120;
25
27
 
26
28
  const app = express();
27
29
  const auth = new authSystem();
@@ -97,8 +99,8 @@ const pushTaskWSUpdate = (batch) => {
97
99
  });
98
100
  };
99
101
 
100
- const taskBatcher = new Batcher(pushTaskWSUpdate, maxMessageSize, maxWaitTime);
101
- const logBatcher = new Batcher(pushConsoleWSUpdate, maxMessageSize, maxWaitTime);
102
+ const taskBatcher = new Batcher(pushTaskWSUpdate, TASK_BATCH_MAX_MESSAGES, TASK_BATCH_WAIT_TIME);
103
+ const logBatcher = new Batcher(pushConsoleWSUpdate, CONSOLE_BATCH_MAX_MESSAGES, CONSOLE_BATCH_WAIT_TIME);
102
104
 
103
105
  const pushWSUpdate = (u) => {
104
106
  if (u.type === "console") logBatcher.add(u);
@@ -270,7 +272,7 @@ app.post("/api/json-file", async (req, res) => {
270
272
 
271
273
  try {
272
274
  if (requestedFile.endsWith(".json")) JSON.parse(content);
273
- } catch (e) {
275
+ } catch {
274
276
  return res.status(400).send({
275
277
  error: "Content must be valid JSON"
276
278
  });
@@ -10,11 +10,68 @@ class Batcher {
10
10
  }
11
11
 
12
12
  add(msg) {
13
- if (msg.type == "task-update") this.inMessages = this.inMessages.filter((t) => t.id != msg.taskId);
14
- else if (msg.type == "console" && !msg.log) return;
13
+ if (!msg) return;
14
+
15
+ const isTaskUpdate =
16
+ msg.event === "task-update" || msg.event === "task-setstatus" || msg.event === "task-setinfo";
17
+
18
+ if (isTaskUpdate) {
19
+ const taskId = msg.id || msg.task?.taskId;
20
+ if (taskId) {
21
+ const index = this.inMessages.findIndex((queued) => {
22
+ const queuedIsTaskUpdate =
23
+ queued.event === "task-update" ||
24
+ queued.event === "task-setstatus" ||
25
+ queued.event === "task-setinfo";
26
+ const queuedTaskId = queued.id || queued.task?.taskId;
27
+ return queuedIsTaskUpdate && queuedTaskId === taskId && queued.event === msg.event;
28
+ });
29
+
30
+ if (index !== -1) {
31
+ this.inMessages[index] = this.mergeTaskMessages(this.inMessages[index], msg, taskId);
32
+ return this.sendIfReady();
33
+ }
34
+ }
35
+ } else if (msg.type === "console" && !msg.log) return;
15
36
 
16
37
  this.inMessages.push(msg);
38
+ this.sendIfReady();
39
+ }
40
+
41
+ mergeTaskMessages(existing, incoming, taskId) {
42
+ if (!existing) return { ...incoming, id: incoming.id || taskId };
43
+ if (incoming?.task?.removed) return { ...incoming, id: incoming.id || taskId };
44
+ if (existing?.task?.removed) return existing;
45
+
46
+ const merged = {
47
+ ...existing,
48
+ ...incoming,
49
+ id: incoming.id || existing.id || taskId
50
+ };
51
+
52
+ if (existing.task || incoming.task) {
53
+ merged.task = {
54
+ ...(existing.task || {}),
55
+ ...(incoming.task || {})
56
+ };
57
+ }
58
+
59
+ if (existing.update || incoming.update) {
60
+ merged.update = {
61
+ ...(existing.update || {}),
62
+ ...(incoming.update || {})
63
+ };
64
+ }
65
+
66
+ if (merged.task && merged.update) {
67
+ Object.assign(merged.task, merged.update);
68
+ delete merged.update;
69
+ }
70
+
71
+ return merged;
72
+ }
17
73
 
74
+ sendIfReady() {
18
75
  // Enough messages for new batch
19
76
  if (this.inMessages.length >= this.maxSize) {
20
77
  this.lastBatchSent = Date.now();
package/backend/index.js CHANGED
@@ -5,7 +5,7 @@ const Bot = {};
5
5
  Bot.ConsoleBuffer = [];
6
6
  Bot.Tasks = {};
7
7
  Bot.CurrentTaskId = 0;
8
- Bot.Settings = { DebugMode: true };
8
+ Bot.Settings = { DebugMode: false };
9
9
  Bot.Monitors = {};
10
10
  Bot.Users = users;
11
11
  Bot.Profiles = profiles;
package/index.html CHANGED
@@ -1,12 +1,11 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" href="/favicon.ico" />
6
6
  <meta
7
7
  name="viewport"
8
- content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
9
- />
8
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
10
9
  <!-- Lock iPhone to portrait orientation only -->
11
10
  <meta name="apple-mobile-web-app-capable" content="yes" />
12
11
  <meta name="mobile-web-app-capable" content="yes" />
@@ -24,7 +23,8 @@
24
23
  color-scheme: dark only;
25
24
  -webkit-color-scheme: dark;
26
25
  }
27
- html, body {
26
+ html,
27
+ body {
28
28
  color-scheme: dark only !important;
29
29
  }
30
30
  </style>
@@ -38,21 +38,11 @@
38
38
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
39
39
  <link
40
40
  href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
41
- rel="stylesheet"
42
- />
43
-
44
- <!-- Preload critical fonts -->
45
- <link
46
- rel="preload"
47
- href="https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2"
48
- as="font"
49
- type="font/woff2"
50
- crossorigin
51
- />
41
+ rel="stylesheet" />
52
42
 
53
- <!-- Preload critical assets -->
54
- <link rel="preload" as="image" href="/img/logo_trans.png" />
55
- <link rel="preload" as="image" href="/img/reconnect-logo.png" />
43
+ <!-- Prefetch likely-used assets -->
44
+ <link rel="prefetch" as="image" href="/img/logo_trans.png" />
45
+ <link rel="prefetch" as="image" href="/img/reconnect-logo.png" />
56
46
 
57
47
  <!-- Prefetch core navigation icons -->
58
48
  <link rel="prefetch" as="image" href="/img/close.svg" />
@@ -112,17 +102,14 @@
112
102
  <!-- Prefetch PWA resources -->
113
103
  <link rel="prefetch" as="script" href="/sw.js" />
114
104
 
115
- <!-- Preload critical PWA manifest -->
116
- <link rel="preload" as="fetch" href="/manifest.json" crossorigin />
117
- <link rel="manifest" href="/manifest.json?v=3" />
105
+ <link rel="manifest" href="/manifest.json?v=3" crossorigin />
118
106
  <meta name="theme-color" content="#1a1b1e" />
119
107
  <link rel="apple-touch-icon" href="/apple-touch-icon.png?v=3" />
120
108
 
121
109
  <!-- Prism.js for syntax highlighting -->
122
110
  <link
123
111
  rel="stylesheet"
124
- href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"
125
- />
112
+ href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
126
113
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
127
114
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
128
115
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@necrolab/dashboard",
3
- "version": "0.5.28",
3
+ "version": "0.5.29",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "rm -rf dist && vite build && npx workbox-cli generateSW workbox-config.cjs",
@@ -2,7 +2,7 @@
2
2
  SCROLL BEHAVIOR & TOUCH HANDLING
3
3
  ========================================================================== */
4
4
 
5
- @use "mixins" as *;
5
+ @use "./mixins" as *;
6
6
 
7
7
  html {
8
8
  overflow: hidden !important;
@@ -3,20 +3,20 @@
3
3
  Modular SCSS architecture with @use imports
4
4
  ========================================================================== */
5
5
 
6
- @use "base/reset";
7
- @use "base/scroll";
8
- @use "base/typography";
9
- @use "base/variables";
10
- @use "base/mixins" as *;
11
- @use "components/buttons";
12
- @use "components/forms";
13
- @use "components/toasts";
14
- @use "components/modals";
15
- @use "components/tables";
16
- @use "components/search-groups";
17
- @use "components/headers";
18
- @use "components/utilities";
19
- @use "components/accessibility";
6
+ @use "./base/reset";
7
+ @use "./base/scroll";
8
+ @use "./base/typography";
9
+ @use "./base/variables";
10
+ @use "./base/mixins" as *;
11
+ @use "./components/buttons";
12
+ @use "./components/forms";
13
+ @use "./components/toasts";
14
+ @use "./components/modals";
15
+ @use "./components/tables";
16
+ @use "./components/search-groups";
17
+ @use "./components/headers";
18
+ @use "./components/utilities";
19
+ @use "./components/accessibility";
20
20
 
21
21
  /* ==========================================================================
22
22
  BODY LAYOUT & BACKGROUND
@@ -28,8 +28,8 @@
28
28
  @mousedown="$emit('scroll', 'up')"
29
29
  @mouseup="$emit('scroll-stop')"
30
30
  @mouseleave="$emit('scroll-stop')"
31
- @touchstart="$emit('scroll', 'up')"
32
- @touchend="$emit('scroll-stop')"
31
+ @touchstart.passive="$emit('scroll', 'up')"
32
+ @touchend.passive="$emit('scroll-stop')"
33
33
  aria-label="Scroll up">
34
34
  <UpIcon class="pointer-events-none h-5 w-5" />
35
35
  </button>
@@ -38,8 +38,8 @@
38
38
  @mousedown="$emit('scroll', 'down')"
39
39
  @mouseup="$emit('scroll-stop')"
40
40
  @mouseleave="$emit('scroll-stop')"
41
- @touchstart="$emit('scroll', 'down')"
42
- @touchend="$emit('scroll-stop')">
41
+ @touchstart.passive="$emit('scroll', 'down')"
42
+ @touchend.passive="$emit('scroll-stop')">
43
43
  <DownIcon class="pointer-events-none h-5 w-5" />
44
44
  </button>
45
45
  </div>
@@ -64,8 +64,8 @@
64
64
  @mousedown="$emit('scroll', 'up')"
65
65
  @mouseup="$emit('scroll-stop')"
66
66
  @mouseleave="$emit('scroll-stop')"
67
- @touchstart="$emit('scroll', 'up')"
68
- @touchend="$emit('scroll-stop')">
67
+ @touchstart.passive="$emit('scroll', 'up')"
68
+ @touchend.passive="$emit('scroll-stop')">
69
69
  <UpIcon class="pointer-events-none h-5 w-5" />
70
70
  </button>
71
71
  <button
@@ -73,8 +73,8 @@
73
73
  @mousedown="$emit('scroll', 'down')"
74
74
  @mouseup="$emit('scroll-stop')"
75
75
  @mouseleave="$emit('scroll-stop')"
76
- @touchstart="$emit('scroll', 'down')"
77
- @touchend="$emit('scroll-stop')">
76
+ @touchstart.passive="$emit('scroll', 'down')"
77
+ @touchend.passive="$emit('scroll-stop')">
78
78
  <DownIcon class="pointer-events-none h-5 w-5" />
79
79
  </button>
80
80
  </div>
@@ -4,8 +4,8 @@
4
4
  @click="ui.setOpenContextMenu('')"
5
5
  @click.right.prevent="ui.setOpenContextMenu('')"
6
6
  @dblclick="handleDoubleClick"
7
- @touchstart="handleTouchStart"
8
- @touchend="handleTouchEnd">
7
+ @touchstart.passive="handleTouchStart"
8
+ @touchend.passive="handleTouchEnd">
9
9
  <div class="col-span-3 flex lg:col-span-2">
10
10
  <Checkbox
11
11
  class="ml-0 mr-4"
@@ -17,11 +17,11 @@
17
17
  </div>
18
18
  <div class="col-span-2 hidden md:block" @click="copy(props.account.password, 'Copied password')">
19
19
  <h4 class="text-center text-white">
20
- {{ props.account.privacy ? "•".repeat(props.account.password.length) : props.account.password }}
20
+ {{ props.privacy ? "•".repeat(props.account.password.length) : props.account.password }}
21
21
  </h4>
22
22
  </div>
23
23
  <div class="col-span-1 flex justify-center">
24
- <StatusBadge :enabled="props.account.enabled" size="small" />
24
+ <StatusBadge :enabled="Boolean(props.account.enabled)" size="small" />
25
25
  </div>
26
26
 
27
27
  <div class="col-span-1 hidden lg:block">
@@ -37,7 +37,7 @@
37
37
  <EditIcon />
38
38
  </button>
39
39
  </li>
40
- <li v-if="props.account.enabled">
40
+ <li v-if="Boolean(props.account.enabled)">
41
41
  <button @click="disable">
42
42
  <img class="icon-md" src="/img/controls/disable.svg" />
43
43
  </button>
@@ -69,6 +69,10 @@ const props = defineProps({
69
69
  account: {
70
70
  type: Object,
71
71
  required: true
72
+ },
73
+ privacy: {
74
+ type: Boolean,
75
+ default: true
72
76
  }
73
77
  });
74
78
 
@@ -29,19 +29,23 @@
29
29
  <h4 class="hidden text-white md:flex">Actions</h4>
30
30
  </div>
31
31
  </Header>
32
- <div
33
- v-if="toRender.length != 0"
34
- class="hidden-scrollbars flex flex-col divide-y divide-dark-650 overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll"
35
- :style="{ maxHeight: dynamicTableHeight }">
36
- <div
37
- v-for="(account, i) in toRender"
38
- :key="account.id || account.index"
39
- class="min-h-16 flex-shrink-0 hover:bg-dark-550">
40
- <Account
41
- :class="getRowClass(i)"
42
- :account="account" />
43
- </div>
44
- </div>
32
+ <RecycleScroller
33
+ v-if="props.accounts.length !== 0"
34
+ class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
35
+ :style="{ height: dynamicTableHeight, maxHeight: dynamicTableHeight }"
36
+ :items="virtualAccounts"
37
+ key-field="virtualKey"
38
+ :item-size="64"
39
+ @wheel.passive="handleVirtualWheel">
40
+ <template #default="{ item, index }">
41
+ <div class="min-h-16 flex-shrink-0 hover:bg-dark-550">
42
+ <Account
43
+ :class="getRowClass(index)"
44
+ :account="item.account"
45
+ :privacy="props.privacy" />
46
+ </div>
47
+ </template>
48
+ </RecycleScroller>
45
49
  <EmptyState v-else :icon="MailIcon" message="No accounts found" subtitle="Create accounts to get started" />
46
50
  </div>
47
51
  </template>
@@ -59,21 +63,36 @@ import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
59
63
  import EmptyState from "@/components/ui/EmptyState.vue";
60
64
  import { useUIStore } from "@/stores/ui";
61
65
  import { useDynamicTableHeight } from "@/composables/useDynamicTableHeight";
62
- import { computed } from "vue";
63
- import { useTableRender } from "@/composables/useTableRender";
64
66
  import { getRowClass } from "@/utils/tableHelpers";
67
+ import { RecycleScroller } from "vue-virtual-scroller";
68
+ import { computed } from "vue";
65
69
 
66
70
  const props = defineProps({
67
71
  accounts: {
68
- type: Object,
72
+ type: Array,
69
73
  required: true
74
+ },
75
+ privacy: {
76
+ type: Boolean,
77
+ default: true
70
78
  }
71
79
  });
72
80
  const ui = useUIStore();
73
81
 
74
- const { toRender } = useTableRender(computed(() => props.accounts));
75
-
76
82
  import { TABLE_LAYOUT } from "@/constants/tableLayout";
77
83
 
78
84
  const { dynamicTableHeight } = useDynamicTableHeight(TABLE_LAYOUT.ACCOUNTS);
85
+
86
+ const virtualAccounts = computed(() =>
87
+ props.accounts.map((account) => ({
88
+ account,
89
+ virtualKey: String(account.id ?? account.email ?? "account")
90
+ }))
91
+ );
92
+
93
+ const handleVirtualWheel = (event) => {
94
+ const target = event.currentTarget;
95
+ if (!target) return;
96
+ target.scrollTop += event.deltaY;
97
+ };
79
98
  </script>
@@ -41,11 +41,21 @@
41
41
  <FormField label="Password" :icon="KeyIcon" required :error="errors.includes('password')" z-index="0" class="col-span-12">
42
42
  <input
43
43
  placeholder="***********"
44
- type="password"
44
+ :type="showPassword ? 'text' : 'password'"
45
45
  v-model="account.password"
46
46
  required
47
47
  autocomplete="off"
48
48
  name="account_password_disableautocomplete" />
49
+ <button
50
+ type="button"
51
+ class="ml-2 flex size-7 items-center justify-center rounded transition-colors hover:bg-dark-550"
52
+ @click="showPassword = !showPassword"
53
+ :aria-label="showPassword ? 'Hide password' : 'Show password'">
54
+ <img
55
+ :src="showPassword ? eyeOpenIcon : eyeClosedIcon"
56
+ :alt="showPassword ? 'Hide password' : 'Show password'"
57
+ class="h-4 w-4" />
58
+ </button>
49
59
  </FormField>
50
60
  </div>
51
61
 
@@ -71,17 +81,41 @@ import {
71
81
  import { useUIStore } from "@/stores/ui";
72
82
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
73
83
  import ReadonlyFieldsSection from "@/components/ui/ReadonlyFieldsSection.vue";
74
- import { ref } from "vue";
84
+ import { ref, toRaw } from "vue";
75
85
  import { useFormValidation } from "@/composables/useFormValidation";
86
+ import eyeOpenIcon from "@/assets/img/eyes/open.svg";
87
+ import eyeClosedIcon from "@/assets/img/eyes/closed.svg";
76
88
 
77
89
  const ui = useUIStore();
78
- const account = ref({
90
+ const createDefaultAccount = () => ({
79
91
  email: "",
80
92
  password: "",
81
93
  tag: ui.profile.tags[0]
82
94
  });
95
+ const cloneValue = (value) => {
96
+ const rawValue = toRaw(value);
97
+ if (typeof structuredClone === "function") {
98
+ try {
99
+ return structuredClone(rawValue);
100
+ } catch {
101
+ return JSON.parse(JSON.stringify(rawValue));
102
+ }
103
+ }
104
+ return JSON.parse(JSON.stringify(rawValue));
105
+ };
106
+ const getInitialAccount = () => {
107
+ if (!ui.currentlyEditing?.email) return createDefaultAccount();
83
108
 
84
- if (ui.currentlyEditing?.email) account.value = ui.currentlyEditing;
109
+ const cloned = cloneValue(ui.currentlyEditing);
110
+ const fallbackTag = Array.isArray(cloned.tags) && cloned.tags.length ? cloned.tags[0] : ui.profile.tags[0];
111
+ return {
112
+ ...createDefaultAccount(),
113
+ ...cloned,
114
+ tag: cloned.tag || fallbackTag
115
+ };
116
+ };
117
+ const account = ref(getInitialAccount());
118
+ const showPassword = ref(false);
85
119
 
86
120
  const { errors, validateAccount } = useFormValidation();
87
121
 
@@ -160,7 +160,7 @@ import ReadonlyFieldsSection from "@/components/ui/ReadonlyFieldsSection.vue";
160
160
  import ProfileCountryChooser from "@/components/Editors/Profile/ProfileCountryChooser.vue";
161
161
  import { useUIStore } from "@/stores/ui";
162
162
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
163
- import { ref, watch, nextTick, onMounted } from "vue";
163
+ import { ref, watch, nextTick, onMounted, toRaw } from "vue";
164
164
  import { fakeId } from "@/libs/utils/dataGeneration";
165
165
  import { validateCard } from "@/libs/utils/validation";
166
166
  import { useFormValidation } from "@/composables/useFormValidation";
@@ -228,17 +228,42 @@ const usStates = [
228
228
  "WY"
229
229
  ];
230
230
  const cardNumberInput = ref(null);
231
- const formProfile = ref({
231
+ const createDefaultProfile = () => ({
232
232
  cvv: "",
233
233
  cardNumber: "",
234
234
  city: "",
235
235
  tag: ui.profile.tags[0],
236
236
  state: "",
237
237
  country: "US",
238
- zipCode: ""
238
+ zipCode: "",
239
+ expMonth: "",
240
+ expYear: "",
241
+ address: ""
239
242
  });
243
+ const cloneValue = (value) => {
244
+ const rawValue = toRaw(value);
245
+ if (typeof structuredClone === "function") {
246
+ try {
247
+ return structuredClone(rawValue);
248
+ } catch {
249
+ return JSON.parse(JSON.stringify(rawValue));
250
+ }
251
+ }
252
+ return JSON.parse(JSON.stringify(rawValue));
253
+ };
254
+ const getInitialProfile = () => {
255
+ if (!ui.currentlyEditing?.profileName) return createDefaultProfile();
240
256
 
241
- if (ui.currentlyEditing?.profileName) formProfile.value = ui.currentlyEditing;
257
+ const cloned = cloneValue(ui.currentlyEditing);
258
+ const fallbackTag = Array.isArray(cloned.tags) && cloned.tags.length ? cloned.tags[0] : ui.profile.tags[0];
259
+
260
+ return {
261
+ ...createDefaultProfile(),
262
+ ...cloned,
263
+ tag: cloned.tag || fallbackTag
264
+ };
265
+ };
266
+ const formProfile = ref(getInitialProfile());
242
267
 
243
268
  // Reactive display value for the formatted card number
244
269
  const displayCardNumber = ref("");
@@ -4,8 +4,8 @@
4
4
  @click="ui.setOpenContextMenu('')"
5
5
  @click.right.prevent="ui.setOpenContextMenu('')"
6
6
  @dblclick="handleDoubleClick"
7
- @touchstart="handleTouchStart"
8
- @touchend="handleTouchEnd">
7
+ @touchstart.passive="handleTouchStart"
8
+ @touchend.passive="handleTouchEnd">
9
9
  <div class="col-span-2 sm:col-span-2 md:col-span-3 lg:col-span-2 flex">
10
10
  <Checkbox
11
11
  class="ml-0 mr-4"
@@ -19,7 +19,7 @@
19
19
  <h4 class="flex items-center justify-center gap-2 text-center text-white">
20
20
  <span class="hidden sm:block">
21
21
  {{
22
- props.profile.privacy
22
+ props.privacy
23
23
  ? props.profile.cardNumber[0] +
24
24
  "•".repeat(props.profile.cardNumber.length - 5) +
25
25
  props.profile.cardNumber.slice(-4)
@@ -33,7 +33,7 @@
33
33
  <h4 class="text-center text-white">{{ expDate() }}</h4>
34
34
  </div>
35
35
  <div class="col-span-1 flex justify-center">
36
- <StatusBadge :enabled="props.profile.enabled" size="small" />
36
+ <StatusBadge :enabled="isEnabled" size="small" />
37
37
  </div>
38
38
 
39
39
  <div class="col-span-1 hidden lg:block">
@@ -49,7 +49,7 @@
49
49
  <EditIcon />
50
50
  </button>
51
51
  </li>
52
- <li v-if="props.profile.enabled">
52
+ <li v-if="isEnabled">
53
53
  <button @click="disable">
54
54
  <img class="icon-md" src="/img/controls/disable.svg" />
55
55
  </button>
@@ -85,6 +85,10 @@ const props = defineProps({
85
85
  profile: {
86
86
  type: Object,
87
87
  required: true
88
+ },
89
+ privacy: {
90
+ type: Boolean,
91
+ default: true
88
92
  }
89
93
  });
90
94
 
@@ -98,7 +102,8 @@ const getAccountType = () => {
98
102
  };
99
103
 
100
104
  const expDate = () =>
101
- props.profile.privacy ? "••/••" : `${props.profile.expMonth}/${props.profile.expYear?.replace("20", "")}`;
105
+ props.privacy ? "••/••" : `${props.profile.expMonth}/${props.profile.expYear?.replace("20", "")}`;
106
+ const isEnabled = computed(() => Boolean(props.profile.enabled));
102
107
  const { enable, disable } = useEnableDisable(computed(() => props.profile), ui.addProfile);
103
108
  const edit = () => {
104
109
  ui.currentlyEditing = props.profile;
@@ -12,8 +12,8 @@
12
12
  class="min-w-20 bg-dark-400 border border-dark-650 rounded-lg shadow-2xl z-50 p-2 [max-height:192px] overflow-y-auto custom-scrollbar-y"
13
13
  :style="menuStyle"
14
14
  @click.stop
15
- @wheel.stop
16
- @touchmove.stop>
15
+ @wheel.passive.stop
16
+ @touchmove.passive.stop>
17
17
  <div
18
18
  v-for="(country, i) in countries"
19
19
  v-bind:key="country"