@necrolab/dashboard 0.5.5 → 0.5.7

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.
@@ -64,6 +64,30 @@
64
64
  </div>
65
65
  </div>
66
66
  </div>
67
+
68
+ <!-- Readonly fields when editing -->
69
+ <div v-if="ui.currentlyEditing?.email" class="mt-6 grid grid-cols-12 gap-3 pt-4 border-t border-dark-600">
70
+ <div v-if="ui.currentlyEditing.tags && ui.currentlyEditing.tags.length > 0" class="col-span-6">
71
+ <label class="label-override mb-2">Tags</label>
72
+ <div class="flex gap-2 flex-wrap">
73
+ <TagLabel v-for="tag in ui.currentlyEditing.tags" :key="tag" :text="tag" />
74
+ </div>
75
+ </div>
76
+ <div class="col-span-6">
77
+ <label class="label-override mb-2">Status</label>
78
+ <div class="flex items-center gap-3 h-10">
79
+ <div v-if="ui.currentlyEditing.enabled" class="enabled-badge-large">
80
+ <CheckmarkIcon />
81
+ </div>
82
+ <div v-else class="disabled-badge-large">
83
+ <CloseXIcon />
84
+ </div>
85
+ <span class="text-sm font-medium" :class="ui.currentlyEditing.enabled ? 'text-green-400' : 'text-red-400'">
86
+ {{ ui.currentlyEditing.enabled ? 'Enabled' : 'Disabled' }}
87
+ </span>
88
+ </div>
89
+ </div>
90
+ </div>
67
91
  </div>
68
92
 
69
93
  <button
@@ -75,6 +99,53 @@
75
99
  </Modal>
76
100
  </template>
77
101
  <style lang="scss" scoped>
102
+ .enabled-badge-large {
103
+ @apply flex items-center justify-center rounded-full;
104
+ background: oklch(0.72 0.15 145 / 0.12);
105
+ border: 1.5px solid oklch(0.72 0.15 145);
106
+ width: 26px;
107
+ height: 26px;
108
+ padding: 0;
109
+
110
+ svg {
111
+ color: oklch(0.72 0.15 145) !important;
112
+ width: 14px !important;
113
+ height: auto !important;
114
+ max-height: 14px !important;
115
+ display: block;
116
+ margin: 0;
117
+ }
118
+
119
+ svg path {
120
+ stroke: oklch(0.72 0.15 145) !important;
121
+ fill: oklch(0.72 0.15 145) !important;
122
+ }
123
+ }
124
+
125
+ .disabled-badge-large {
126
+ @apply flex items-center justify-center rounded-full;
127
+ background: oklch(0.60 0.20 25 / 0.12);
128
+ border: 1.5px solid oklch(0.60 0.20 25);
129
+ width: 26px;
130
+ height: 26px;
131
+ padding: 0;
132
+ color: oklch(0.60 0.20 25) !important;
133
+
134
+ svg {
135
+ color: oklch(0.60 0.20 25) !important;
136
+ width: 14px !important;
137
+ height: 14px !important;
138
+ display: block;
139
+ margin: 0;
140
+ fill: oklch(0.60 0.20 25) !important;
141
+ }
142
+
143
+ svg path {
144
+ stroke: oklch(0.60 0.20 25) !important;
145
+ fill: oklch(0.60 0.20 25) !important;
146
+ }
147
+ }
148
+
78
149
  .input-wrapper {
79
150
  label {
80
151
  @apply flex;
@@ -105,10 +176,13 @@ import {
105
176
  TimerIcon,
106
177
  SandclockIcon,
107
178
  TagIcon,
108
- ScannerIcon
179
+ ScannerIcon,
180
+ CheckmarkIcon,
181
+ CloseXIcon
109
182
  } from "@/components/icons";
110
183
  import { useUIStore } from "@/stores/ui";
111
184
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
185
+ import TagLabel from "@/components/Editors/TagLabel.vue";
112
186
 
113
187
  import { ref } from "vue";
114
188
 
@@ -44,7 +44,7 @@
44
44
  <div class="input-wrapper col-span-2">
45
45
  <label class="label-override mb-2">
46
46
  Country
47
- <SandclockIcon />
47
+ <StadiumIcon />
48
48
  </label>
49
49
  <ProfileCountryChooser
50
50
  class="h-8"
@@ -187,6 +187,30 @@
187
187
  </div>
188
188
  </div>
189
189
  </div>
190
+
191
+ <!-- Readonly fields when editing -->
192
+ <div v-if="ui.currentlyEditing?.profileName" class="mt-6 grid grid-cols-12 gap-3 pt-4 border-t border-dark-600">
193
+ <div v-if="ui.currentlyEditing.tags && ui.currentlyEditing.tags.length > 0" class="col-span-6">
194
+ <label class="label-override mb-2">Tags</label>
195
+ <div class="flex gap-2 flex-wrap">
196
+ <TagLabel v-for="tag in ui.currentlyEditing.tags" :key="tag" :text="tag" />
197
+ </div>
198
+ </div>
199
+ <div class="col-span-6">
200
+ <label class="label-override mb-2">Status</label>
201
+ <div class="flex items-center gap-3 h-10">
202
+ <div v-if="ui.currentlyEditing.enabled" class="enabled-badge-large">
203
+ <CheckmarkIcon />
204
+ </div>
205
+ <div v-else class="disabled-badge-large">
206
+ <CloseXIcon />
207
+ </div>
208
+ <span class="text-sm font-medium" :class="ui.currentlyEditing.enabled ? 'text-green-400' : 'text-red-400'">
209
+ {{ ui.currentlyEditing.enabled ? 'Enabled' : 'Disabled' }}
210
+ </span>
211
+ </div>
212
+ </div>
213
+ </div>
190
214
  </div>
191
215
 
192
216
  <button
@@ -198,6 +222,53 @@
198
222
  </Modal>
199
223
  </template>
200
224
  <style lang="scss" scoped>
225
+ .enabled-badge-large {
226
+ @apply flex items-center justify-center rounded-full;
227
+ background: oklch(0.72 0.15 145 / 0.12);
228
+ border: 1.5px solid oklch(0.72 0.15 145);
229
+ width: 26px;
230
+ height: 26px;
231
+ padding: 0;
232
+
233
+ svg {
234
+ color: oklch(0.72 0.15 145) !important;
235
+ width: 14px !important;
236
+ height: auto !important;
237
+ max-height: 14px !important;
238
+ display: block;
239
+ margin: 0;
240
+ }
241
+
242
+ svg path {
243
+ stroke: oklch(0.72 0.15 145) !important;
244
+ fill: oklch(0.72 0.15 145) !important;
245
+ }
246
+ }
247
+
248
+ .disabled-badge-large {
249
+ @apply flex items-center justify-center rounded-full;
250
+ background: oklch(0.60 0.20 25 / 0.12);
251
+ border: 1.5px solid oklch(0.60 0.20 25);
252
+ width: 26px;
253
+ height: 26px;
254
+ padding: 0;
255
+ color: oklch(0.60 0.20 25) !important;
256
+
257
+ svg {
258
+ color: oklch(0.60 0.20 25) !important;
259
+ width: 14px !important;
260
+ height: 14px !important;
261
+ display: block;
262
+ margin: 0;
263
+ fill: oklch(0.60 0.20 25) !important;
264
+ }
265
+
266
+ svg path {
267
+ stroke: oklch(0.60 0.20 25) !important;
268
+ fill: oklch(0.60 0.20 25) !important;
269
+ }
270
+ }
271
+
201
272
  .input-wrapper {
202
273
  label {
203
274
  @apply flex;
@@ -231,9 +302,12 @@ import {
231
302
  SandclockIcon,
232
303
  TimerIcon,
233
304
  TagIcon,
234
- WildcardIcon
305
+ WildcardIcon,
306
+ CheckmarkIcon,
307
+ CloseXIcon
235
308
  } from "@/components/icons";
236
309
  import { EditIcon } from "@/components/icons";
310
+ import TagLabel from "@/components/Editors/TagLabel.vue";
237
311
  import ProfileCountryChooser from "@/components/Editors/Profile/ProfileCountryChooser.vue";
238
312
  import { useUIStore } from "@/stores/ui";
239
313
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
@@ -33,12 +33,12 @@
33
33
  <h4 class="text-white">{{ expDate() }}</h4>
34
34
  </div>
35
35
  <div class="col-span-1">
36
- <h4 v-if="props.profile.enabled" class="flex justify-center text-green-400">
37
- <img class="green h-3 w-3" src="/img/controls/enable.svg" />
38
- </h4>
39
- <h4 v-else class="flex justify-center text-red-400">
40
- <img class="h-3 w-3 fill-red-400" src="/img/close.svg" />
41
- </h4>
36
+ <div v-if="props.profile.enabled" class="enabled-badge">
37
+ <CheckmarkIcon class="w-3.5 h-3.5" />
38
+ </div>
39
+ <div v-else class="disabled-badge">
40
+ <CloseXIcon class="w-3.5 h-3.5" />
41
+ </div>
42
42
  </div>
43
43
 
44
44
  <div class="col-span-1 hidden lg:block">
@@ -72,34 +72,80 @@
72
72
  </Row>
73
73
  </template>
74
74
  <style lang="scss" scoped>
75
- .green svg path {
76
- fill: rgb(68 183 68);
75
+ .enabled-badge {
76
+ @apply flex items-center justify-center mx-auto rounded-full;
77
+ background: oklch(0.72 0.15 145 / 0.12);
78
+ border: 1.5px solid oklch(0.72 0.15 145);
79
+ width: 24px;
80
+ height: 24px;
81
+ padding: 0;
82
+
83
+ svg {
84
+ color: oklch(0.72 0.15 145) !important;
85
+ width: 12px !important;
86
+ height: auto !important;
87
+ max-height: 12px !important;
88
+ display: block;
89
+ margin: 0;
90
+ }
91
+
92
+ svg path {
93
+ stroke: oklch(0.72 0.15 145) !important;
94
+ fill: oklch(0.72 0.15 145) !important;
95
+ }
96
+ }
97
+
98
+ .disabled-badge {
99
+ @apply flex items-center justify-center mx-auto rounded-full;
100
+ background: oklch(0.60 0.20 25 / 0.12);
101
+ border: 1.5px solid oklch(0.60 0.20 25);
102
+ width: 24px;
103
+ height: 24px;
104
+ padding: 0;
105
+ color: oklch(0.60 0.20 25) !important;
106
+
107
+ svg {
108
+ color: oklch(0.60 0.20 25) !important;
109
+ width: 12px !important;
110
+ height: 12px !important;
111
+ display: block;
112
+ margin: 0;
113
+ fill: oklch(0.60 0.20 25) !important;
114
+ }
115
+
116
+ svg path {
117
+ stroke: oklch(0.60 0.20 25) !important;
118
+ fill: oklch(0.60 0.20 25) !important;
119
+ }
77
120
  }
78
121
 
79
122
  h4 {
80
123
  @apply text-center;
81
124
  }
82
125
  .profile-buttons {
83
- @apply mx-auto flex items-center justify-center rounded border border-dark-650 bg-dark-500;
126
+ @apply mx-auto flex items-center justify-center rounded;
127
+ background: oklch(0.2046 0 0);
128
+ border: 2px solid oklch(0.2809 0 0);
84
129
  padding: 3px;
85
130
  gap: 2px;
131
+ flex-shrink: 0;
132
+ overflow: visible;
86
133
 
87
134
  button {
88
135
  @apply relative flex items-center justify-center rounded border-0 outline-0 transition-all duration-150;
89
136
  background: transparent;
90
137
  width: 28px;
91
138
  height: 28px;
92
- color: oklch(0.82 0 0);
139
+ color: oklch(0.90 0 0);
140
+ border-radius: 6px;
93
141
 
94
142
  &:hover {
95
- background: rgba(255, 255, 255, 0.1);
96
- color: #ffffff;
97
- transform: scale(1.05);
143
+ background: oklch(0.72 0.15 145 / 0.15);
144
+ color: oklch(1 0 0);
98
145
  }
99
146
 
100
147
  &:active {
101
- background: rgba(255, 255, 255, 0.2);
102
- transform: scale(0.95);
148
+ background: oklch(0.72 0.15 145 / 0.25);
103
149
  }
104
150
  }
105
151
 
@@ -116,19 +162,17 @@ h4 {
116
162
  }
117
163
  }
118
164
 
119
- // Tablet optimization
120
- @media (max-width: 1024px) {
121
- h4 {
122
- font-size: 10px !important;
123
- }
124
-
165
+ // Tablet sizing - medium buttons
166
+ @media (min-width: 768px) and (max-width: 1023px) {
125
167
  .profile-buttons {
126
- padding: 3px;
127
- gap: 2px;
168
+ padding: 2px;
169
+ gap: 1px;
170
+ border-radius: 6px;
128
171
 
129
172
  button {
130
173
  width: 26px;
131
174
  height: 26px;
175
+ border-radius: 5px;
132
176
  }
133
177
 
134
178
  svg,
@@ -137,59 +181,74 @@ h4 {
137
181
  height: 14px;
138
182
  }
139
183
  }
140
-
141
- .profile-id {
142
- font-size: 6px !important;
143
- margin-right: -12px;
144
- margin-top: 20px;
145
- }
146
184
  }
147
185
 
148
- // Mobile optimization
149
- @media (max-width: 768px) {
186
+ // Desktop sizing - large buttons
187
+ @media (min-width: 1024px) {
150
188
  .profile-buttons {
151
- padding: 2px;
152
- gap: 1px;
189
+ padding: 3px;
190
+ gap: 2px;
191
+ border-radius: 8px;
153
192
 
154
193
  button {
155
- width: 22px;
156
- height: 22px;
194
+ width: 28px;
195
+ height: 28px;
196
+ border-radius: 6px;
157
197
  }
158
198
 
159
199
  svg,
160
200
  img {
161
- width: 12px;
162
- height: 12px;
201
+ width: 16px;
202
+ height: 16px;
163
203
  }
164
204
  }
165
205
  }
166
206
 
167
- // iPhone vertical (portrait) specific
168
- @media (max-width: 480px) and (orientation: portrait) {
207
+ // Mobile specific styling
208
+ @media (max-width: 640px) {
169
209
  .profile-buttons {
170
- padding: 2px;
210
+ padding: 1px;
171
211
  gap: 1px;
212
+ border-radius: 4px;
213
+ border: 2px solid oklch(0.2809 0 0) !important;
214
+ max-width: 100%;
215
+ min-height: 28px;
216
+ height: auto;
217
+ flex-wrap: wrap;
218
+ justify-content: center;
219
+ align-items: center;
220
+ background: oklch(0.2046 0 0);
172
221
 
173
222
  button {
174
- width: 18px;
175
- height: 18px;
223
+ width: 20px;
224
+ height: 20px;
225
+ border-radius: 3px;
226
+ min-width: 20px;
227
+ border: none !important;
228
+ flex-shrink: 0;
229
+ margin: 0.5px;
230
+ background: transparent;
176
231
 
177
232
  &:hover {
178
- transform: scale(1.1);
233
+ background: oklch(0.72 0.15 145 / 0.15);
234
+ }
235
+
236
+ &:active {
237
+ background: oklch(0.72 0.15 145 / 0.25);
179
238
  }
180
239
  }
181
240
 
182
241
  svg,
183
242
  img {
184
- width: 10px;
185
- height: 10px;
243
+ width: 12px;
244
+ height: 12px;
186
245
  }
187
246
  }
188
247
  }
189
248
  </style>
190
249
  <script setup>
191
250
  import { Row } from "@/components/Table";
192
- import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon } from "@/components/icons";
251
+ import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon, CheckmarkIcon, CloseXIcon } from "@/components/icons";
193
252
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
194
253
  import { useUIStore } from "@/stores/ui";
195
254
  import { validateCard } from "@/stores/utils";
@@ -33,22 +33,15 @@
33
33
  <h4 class="hidden md:flex">Actions</h4>
34
34
  </div>
35
35
  </Header>
36
- <div v-if="toRender.length != 0">
37
- <RecycleScroller
38
- :items="toRender"
39
- :item-size="64"
40
- key-field="index"
41
- class="scroller vue-recycle-scroller ready direction-vertical hidden-scrollbars stop-pan flex flex-col divide-y divide-dark-650 overflow-y-auto overflow-x-hidden"
42
- :style="{ maxHeight: dynamicTableHeight }">
43
- <template #default="props">
44
- <div class="profile" :key="`profile-${props.item.id || props.item.index}`">
45
- <Profile
46
- @click="i[props.item.index]++"
47
- :class="props.item.index % 2 == 1 ? 'table-row-even' : 'table-row-odd'"
48
- :profile="props.item" />
49
- </div>
50
- </template>
51
- </RecycleScroller>
36
+ <div
37
+ v-if="toRender.length != 0"
38
+ class="hidden-scrollbars stop-pan flex flex-col divide-y divide-dark-650 overflow-y-auto overflow-x-hidden"
39
+ :style="{ maxHeight: dynamicTableHeight }">
40
+ <div v-for="(profile, i) in toRender" :key="profile.id || profile.index" class="profile-row-container">
41
+ <Profile
42
+ :class="i % 2 == 1 ? 'table-row-even' : 'table-row-odd'"
43
+ :profile="profile" />
44
+ </div>
52
45
  </div>
53
46
  <div v-else class="empty-state flex flex-col items-center justify-center bg-dark-400 py-8 text-center">
54
47
  <ProfileIcon class="mb-3 h-12 w-12 text-dark-400 opacity-50" />
@@ -58,9 +51,16 @@
58
51
  </Table>
59
52
  </template>
60
53
  <style lang="scss" scoped>
61
- .profile {
62
- height: 64px;
54
+ .profile-row-container {
55
+ min-height: 64px;
56
+ flex-shrink: 0;
57
+ transition: background-color 0.15s ease;
58
+
59
+ &:hover {
60
+ @apply bg-dark-550 !important;
61
+ }
63
62
  }
63
+
64
64
  h4 {
65
65
  @apply text-white;
66
66
  }
@@ -6,63 +6,53 @@
6
6
 
7
7
  <style lang="scss" scoped>
8
8
  .tag-pill {
9
- @apply inline-flex items-center justify-center rounded-md transition-all duration-200;
10
- background: linear-gradient(145deg, oklch(0.26 0 0), oklch(0.22 0 0));
11
- border: 1px solid oklch(0.31 0 0);
12
- padding: 0.1875rem 0.5rem;
9
+ @apply inline-flex items-center justify-center rounded-full;
10
+ background: oklch(0.72 0.15 145 / 0.12);
11
+ border: 1.5px solid oklch(0.72 0.15 145 / 0.25);
12
+ padding: 0.25rem 0.625rem;
13
13
  min-width: 2rem;
14
- max-width: 4.5rem;
15
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05);
16
-
17
- &:hover {
18
- background: linear-gradient(145deg, oklch(0.28 0 0), oklch(0.26 0 0));
19
- border-color: oklch(0.33 0 0);
20
- transform: translateY(-0.5px);
21
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.08);
22
- }
14
+ max-width: 5rem;
23
15
  }
24
16
 
25
17
  .tag-text {
26
- @apply text-white font-semibold truncate;
18
+ @apply truncate font-semibold;
19
+ color: oklch(0.72 0.15 145);
27
20
  font-size: 0.6875rem;
28
- line-height: 1.1;
29
- letter-spacing: 0.025em;
30
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
21
+ line-height: 1.2;
22
+ letter-spacing: 0.015em;
31
23
  }
32
24
 
33
- // Ultra responsive design
25
+ // Responsive design
34
26
  @media (max-width: 1024px) {
35
27
  .tag-pill {
36
- padding: 0.1rem 0.3rem;
37
- max-width: 3.5rem;
28
+ padding: 0.2rem 0.5rem;
29
+ max-width: 4rem;
38
30
  }
39
31
 
40
32
  .tag-text {
41
- font-size: 0.575rem;
33
+ font-size: 0.625rem;
42
34
  }
43
35
  }
44
36
 
45
37
  @media (max-width: 768px) {
46
38
  .tag-pill {
47
- padding: 0.075rem 0.25rem;
48
- max-width: 3rem;
49
- min-width: 1.25rem;
39
+ padding: 0.1875rem 0.4rem;
40
+ max-width: 3.5rem;
50
41
  }
51
42
 
52
43
  .tag-text {
53
- font-size: 0.55rem;
44
+ font-size: 0.6rem;
54
45
  }
55
46
  }
56
47
 
57
48
  @media (max-width: 480px) {
58
49
  .tag-pill {
59
- padding: 0.05rem 0.2rem;
60
- max-width: 2.5rem;
61
- min-width: 1rem;
50
+ padding: 0.15rem 0.35rem;
51
+ max-width: 3rem;
62
52
  }
63
53
 
64
54
  .tag-text {
65
- font-size: 0.5rem;
55
+ font-size: 0.575rem;
66
56
  }
67
57
  }
68
58
  </style>
@@ -10,7 +10,25 @@
10
10
  class="ml-2 mr-4 flex-shrink-0"
11
11
  :toggled="props.task.selected"
12
12
  @valueUpdate="ui.toggleTaskSelected(props.task.taskId)" />
13
+ <div
14
+ v-if="props.preferEventName && props.task.eventName"
15
+ class="event-details hidden cursor-pointer lg:flex flex-col gap-0.5"
16
+ @click="copy(props.task.eventId)"
17
+ :title="`Event ID: ${props.task.eventId}`">
18
+ <div class="event-name text-white text-xs font-semibold leading-tight">
19
+ {{ props.task.eventName }}
20
+ </div>
21
+ <div v-if="props.task.eventVenue" class="event-venue flex items-center gap-1 text-[10px] text-light-400">
22
+ <StadiumIcon class="w-2.5 h-2.5 flex-shrink-0" />
23
+ <span class="truncate">{{ props.task.eventVenue }}</span>
24
+ </div>
25
+ <div v-if="props.task.eventLocalDate" class="event-date flex items-center gap-1 text-[10px] text-light-400">
26
+ <TimerIcon class="w-2.5 h-2.5 flex-shrink-0" />
27
+ <span>{{ formatEventDate(props.task.eventLocalDate) }}</span>
28
+ </div>
29
+ </div>
13
30
  <h4
31
+ v-else
14
32
  class="task-event-id mx-auto hidden cursor-pointer text-white hover:text-light-300 lg:block"
15
33
  @click="copy(props.task.eventId)">
16
34
  {{ props.task.eventId }}
@@ -90,7 +108,6 @@
90
108
  </h4>
91
109
  </div>
92
110
 
93
- <!-- Context menu -->
94
111
  <transition name="fade">
95
112
  <div
96
113
  v-if="ui.openContextMenu === task.taskId"
@@ -376,7 +393,7 @@ h4 {
376
393
  /// <reference path="@/types/index.js" />
377
394
 
378
395
  import { Row } from "@/components/Table";
379
- import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon, EyeIcon } from "@/components/icons";
396
+ import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon, EyeIcon, StadiumIcon, TimerIcon } from "@/components/icons";
380
397
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
381
398
  import { useUIStore } from "@/stores/ui";
382
399
  import TaskLabel from "@/components/Tasks/TaskLabel.vue";
@@ -387,9 +404,29 @@ const ui = useUIStore();
387
404
 
388
405
  /** @type {{ task: Task }} */
389
406
  const props = defineProps({
390
- task: { type: Object }
407
+ task: { type: Object },
408
+ preferEventName: { type: Boolean, default: false }
391
409
  });
392
410
 
411
+ // Format event date for display
412
+ const formatEventDate = (dateString) => {
413
+ if (!dateString) return '';
414
+ try {
415
+ const date = new Date(dateString);
416
+ const options = {
417
+ month: 'short',
418
+ day: 'numeric',
419
+ year: 'numeric',
420
+ hour: 'numeric',
421
+ minute: '2-digit',
422
+ hour12: true
423
+ };
424
+ return date.toLocaleString('en-US', options).replace(',', '');
425
+ } catch {
426
+ return dateString;
427
+ }
428
+ };
429
+
393
430
  // Context menu positioning
394
431
  const contextMenuPosition = ref({});
395
432
  const contextMenuRef = ref(null);