@necrolab/dashboard 0.5.12 → 0.5.14

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 (54) hide show
  1. package/.playwright-mcp/verification-accounts-desktop.png +0 -0
  2. package/.playwright-mcp/verification-tasks-desktop.png +0 -0
  3. package/.playwright-mcp/verification-tasks-mobile.png +0 -0
  4. package/README.md +21 -0
  5. package/docs/plans/2026-02-08-tailwind-consolidation-results.md +476 -0
  6. package/docs/plans/2026-02-08-tailwind-consolidation.md +2416 -0
  7. package/package.json +1 -1
  8. package/src/App.vue +2 -163
  9. package/src/assets/css/components/buttons.scss +43 -95
  10. package/src/assets/css/components/forms.scss +10 -28
  11. package/src/assets/css/components/search-groups.scss +80 -0
  12. package/src/assets/css/components/tables.scss +0 -8
  13. package/src/assets/css/main.scss +2 -43
  14. package/src/components/Editors/Account/Account.vue +14 -220
  15. package/src/components/Editors/Account/AccountCreator.vue +0 -4
  16. package/src/components/Editors/Account/AccountView.vue +0 -1
  17. package/src/components/Editors/Account/CreateAccount.vue +36 -107
  18. package/src/components/Editors/Profile/CreateProfile.vue +46 -135
  19. package/src/components/Editors/Profile/Profile.vue +10 -213
  20. package/src/components/Editors/Profile/ProfileView.vue +0 -1
  21. package/src/components/Filter/Filter.vue +14 -17
  22. package/src/components/Filter/FilterPreview.vue +0 -6
  23. package/src/components/Table/Row.vue +1 -1
  24. package/src/components/Table/Table.vue +2 -16
  25. package/src/components/Tasks/CreateTaskAXS.vue +45 -104
  26. package/src/components/Tasks/CreateTaskTM.vue +58 -96
  27. package/src/components/Tasks/Task.vue +22 -209
  28. package/src/components/Tasks/TaskView.vue +5 -4
  29. package/src/components/Tasks/ViewTask.vue +201 -214
  30. package/src/components/icons/Copy.vue +6 -0
  31. package/src/components/icons/index.js +3 -1
  32. package/src/components/ui/ActionButtonGroup.vue +70 -0
  33. package/src/components/ui/FormField.vue +74 -0
  34. package/src/components/ui/InfoRow.vue +100 -0
  35. package/src/components/ui/Modal.vue +6 -47
  36. package/src/components/ui/Navbar.vue +15 -43
  37. package/src/components/ui/ReconnectIndicator.vue +4 -4
  38. package/src/components/ui/SectionCard.vue +24 -0
  39. package/src/components/ui/Splash.vue +1 -6
  40. package/src/components/ui/StatusBadge.vue +37 -0
  41. package/src/components/ui/controls/CountryChooser.vue +14 -58
  42. package/src/components/ui/controls/atomic/Dropdown.vue +16 -24
  43. package/src/components/ui/controls/atomic/MultiDropdown.vue +7 -1
  44. package/src/components/ui/controls/atomic/Switch.vue +13 -29
  45. package/src/composables/useCopyToClipboard.js +25 -0
  46. package/src/composables/useRowSelection.js +48 -0
  47. package/src/views/Accounts.vue +0 -81
  48. package/src/views/Console.vue +4 -21
  49. package/src/views/Editor.vue +48 -138
  50. package/src/views/FilterBuilder.vue +0 -23
  51. package/src/views/Login.vue +14 -63
  52. package/src/views/Profiles.vue +0 -82
  53. package/src/views/Tasks.vue +3 -24
  54. package/tailwind.config.js +47 -5
@@ -1,190 +1,214 @@
1
1
  <template>
2
2
  <Modal>
3
3
  <template #header>
4
- <span class="flex">View Task <EyeIcon class="ml-4 w-4" /></span>
4
+ <span class="flex">
5
+ View Task
6
+ <EyeIcon class="ml-4 w-4" />
7
+ </span>
5
8
  </template>
6
9
 
10
+ <!-- Task data updates in real-time via socket.
11
+ If task is deleted from table, modal retains last known data -->
12
+
7
13
  <!-- Task Overview Card -->
8
- <div class="section-card mb-4 mt-4">
9
- <h3 class="section-title">Task Overview</h3>
14
+ <SectionCard title="Task Overview" class="mb-4 mt-4">
10
15
  <div class="grid grid-cols-1 gap-3">
11
- <div class="info-row" @click="copy(task.taskId)">
12
- <span class="info-icon flex items-center justify-center text-base font-bold">#</span>
13
- <span class="info-label">Task ID</span>
14
- <span class="info-value copyable">{{ task.taskId }}</span>
15
- </div>
16
- <div class="info-row" @click="copy(task.eventId)">
17
- <StadiumIcon class="info-icon" />
18
- <span class="info-label">Event ID</span>
19
- <span class="info-value copyable">{{ task.eventId }}</span>
20
- </div>
21
- <div class="info-row" v-if="task.eventName">
22
- <StadiumWhiteIcon class="info-icon" />
23
- <span class="info-label">Event Name</span>
24
- <span class="info-value">{{ task.eventName }}</span>
25
- </div>
26
- <div class="info-row" v-if="task.venueName || task.eventCity">
27
- <StadiumWhiteIcon class="info-icon" />
28
- <span class="info-label">Venue</span>
29
- <span class="info-value">{{ [task.venueName, task.eventCity].filter(Boolean).join(', ') }}</span>
30
- </div>
31
- <div class="info-row" v-if="task.eventLocalDate">
32
- <StadiumWhiteIcon class="info-icon" />
33
- <span class="info-label">Date</span>
34
- <span class="info-value">{{ formatDate(task.eventLocalDate) }}</span>
35
- </div>
36
- <div class="info-row">
37
- <div class="status-indicator info-icon" :class="colorToClass(task.active || task.status.toLowerCase() === 'idle' ? task.statusColor : 'red')"></div>
38
- <span class="info-label">Status</span>
39
- <span class="info-value uppercase font-semibold whitespace-normal break-words text-right">{{ task.status }}</span>
40
- </div>
41
- <div class="info-row" v-if="task._timeLeftString && task._timeLeftString !== 'No Cartholds'">
42
- <TimerIcon class="info-icon" />
43
- <span class="info-label">Cart Expiration</span>
44
- <span class="info-value font-semibold text-right" :class="{ 'text-red-400': task._timeLeftString === '00:00' }">
45
- {{ task._timeLeftString !== '00:00' ? task._timeLeftString : 'Expired' }}
46
- </span>
47
- </div>
48
- <div class="info-row items-start" v-if="task.reservedTicketsList">
49
- <TicketIcon class="info-icon flex-shrink-0" />
50
- <span class="info-label flex-shrink-0">Reserved Tickets</span>
51
- <div class="info-value text-right flex-1">
52
- <div v-for="(line, index) in task.reservedTicketsList.split('\n')" :key="index" class="text-xs leading-tight">
53
- <span v-if="line.trim()" :class="{ 'text-green-400 font-bold': isTotalPrice(line, index, task.reservedTicketsList.split('\n')) }">
54
- {{ line.trim() }}
55
- </span>
56
- </div>
16
+ <InfoRow
17
+ icon-text="#"
18
+ label="Task ID"
19
+ :value="taskSnapshot.taskId"
20
+ copyable
21
+ :copy-value="taskSnapshot.taskId"
22
+ copy-message="Copied task ID" />
23
+ <InfoRow
24
+ :icon="StadiumIcon"
25
+ label="Event ID"
26
+ :value="taskSnapshot.eventId"
27
+ copyable
28
+ :copy-value="taskSnapshot.eventId"
29
+ copy-message="Copied event ID" />
30
+ <InfoRow
31
+ v-if="taskSnapshot.eventName"
32
+ :icon="StadiumWhiteIcon"
33
+ label="Event Name"
34
+ :value="taskSnapshot.eventName" />
35
+ <InfoRow
36
+ v-if="taskSnapshot.venueName || taskSnapshot.eventCity"
37
+ :icon="StadiumWhiteIcon"
38
+ label="Venue"
39
+ :value="[taskSnapshot.venueName, taskSnapshot.eventCity].filter(Boolean).join(', ')" />
40
+ <InfoRow
41
+ v-if="taskSnapshot.eventLocalDate"
42
+ :icon="StadiumWhiteIcon"
43
+ label="Date"
44
+ :value="formatDate(taskSnapshot.eventLocalDate)" />
45
+ <InfoRow :icon="StatusIcon" label="Status" value-class="uppercase font-semibold flex items-center gap-2">
46
+ <div class="status-indicator flex-shrink-0" :class="colorToClass(taskSnapshot.active || taskSnapshot.status.toLowerCase() === 'idle' ? taskSnapshot.statusColor : 'red')"></div>
47
+ <span>{{ taskSnapshot.status }}</span>
48
+ </InfoRow>
49
+ <InfoRow
50
+ v-if="taskSnapshot._timeLeftString && taskSnapshot._timeLeftString !== 'No Cartholds'"
51
+ :icon="TimerIcon"
52
+ label="Cart Expiration"
53
+ :value-class="`font-semibold text-right ${taskSnapshot._timeLeftString === '00:00' ? 'text-red-400' : ''}`">
54
+ {{ taskSnapshot._timeLeftString !== "00:00" ? taskSnapshot._timeLeftString : "Expired" }}
55
+ </InfoRow>
56
+ <InfoRow
57
+ v-if="taskSnapshot.reservedTicketsList"
58
+ :icon="TicketIcon"
59
+ label="Reserved Tickets"
60
+ align-start
61
+ value-class="text-right flex-1">
62
+ <div
63
+ v-for="(line, index) in taskSnapshot.reservedTicketsList.split('\n')"
64
+ :key="index"
65
+ class="text-xs leading-tight">
66
+ <span
67
+ v-if="line.trim()"
68
+ :class="{
69
+ 'font-bold text-green-400': isTotalPrice(
70
+ line,
71
+ index,
72
+ taskSnapshot.reservedTicketsList.split('\n')
73
+ )
74
+ }">
75
+ {{ line.trim() }}
76
+ </span>
57
77
  </div>
58
- </div>
78
+ </InfoRow>
59
79
  </div>
60
- </div>
80
+ </SectionCard>
61
81
 
62
82
  <!-- Account Details Card -->
63
- <div class="section-card mb-4">
64
- <h3 class="section-title">Account Details</h3>
83
+ <SectionCard title="Account Details" class="mb-4">
65
84
  <div class="grid grid-cols-1 gap-3">
66
- <div class="info-row" v-if="task.email" @click="copy(task.email)">
67
- <MailIcon class="info-icon" />
68
- <span class="info-label">Email</span>
69
- <span class="info-value copyable">{{ task.email }}</span>
70
- </div>
71
- <div class="info-row" v-if="task.password" @click="copy(task.password)">
72
- <KeyIcon class="info-icon" />
73
- <span class="info-label">Password</span>
74
- <span class="info-value copyable">{{ showPassword ? task.password : '••••••••' }}</span>
75
- <button @click.stop="showPassword = !showPassword" class="ml-auto">
76
- <EyeToggle :visible="showPassword" />
77
- </button>
78
- </div>
79
- <div class="info-row" v-if="!task.email && !task.password">
80
- <MailIcon class="info-icon" />
81
- <span class="info-label">Account</span>
82
- <span class="info-value text-light-500">No account chosen</span>
83
- </div>
84
- <div class="info-row" v-if="task.profileName">
85
- <ProfileIcon class="info-icon" />
86
- <span class="info-label">Profile</span>
87
- <span class="info-value">{{ task.profileName }}</span>
88
- </div>
85
+ <InfoRow
86
+ v-if="taskSnapshot.email"
87
+ :icon="MailIcon"
88
+ label="Email"
89
+ :value="taskSnapshot.email"
90
+ copyable
91
+ :copy-value="taskSnapshot.email"
92
+ copy-message="Copied email" />
93
+ <InfoRow
94
+ v-if="taskSnapshot.password"
95
+ :icon="KeyIcon"
96
+ label="Password">
97
+ {{ showPassword ? taskSnapshot.password : "••••••••" }}
98
+ <template #action>
99
+ <button @click.stop="showPassword = !showPassword" class="flex items-center justify-center w-8 h-8 rounded transition-all hover:bg-dark-600">
100
+ <EyeToggle :visible="showPassword" />
101
+ </button>
102
+ <button @click.stop="copy(taskSnapshot.password, 'Copied password')" class="copy-button flex items-center justify-center w-8 h-8 rounded transition-all hover:bg-dark-600 text-light-500 hover:text-light-300">
103
+ <CopyIcon class="w-4 h-4" />
104
+ </button>
105
+ </template>
106
+ </InfoRow>
107
+ <InfoRow
108
+ v-if="!taskSnapshot.email && !taskSnapshot.password"
109
+ :icon="MailIcon"
110
+ label="Account"
111
+ value="No account chosen"
112
+ value-class="text-light-500" />
113
+ <InfoRow
114
+ v-if="taskSnapshot.profileName"
115
+ :icon="ProfileIcon"
116
+ label="Profile"
117
+ :value="taskSnapshot.profileName" />
89
118
  </div>
90
- </div>
119
+ </SectionCard>
91
120
 
92
121
  <!-- Task Configuration -->
93
- <div class="section-card mb-4">
94
- <h3 class="section-title">Task Configuration</h3>
95
- <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
96
- <div class="info-row">
97
- <BagIcon class="info-icon" />
98
- <span class="info-label">Ticket Quantity</span>
99
- <span class="info-value">{{ task.quantity }}</span>
100
- </div>
101
- <div class="info-row">
102
- <ScannerIcon class="info-icon" />
103
- <span class="info-label">Account Tag</span>
104
- <span class="info-value">{{ task.accountTag || 'None' }}</span>
105
- </div>
106
- <div class="info-row">
107
- <SellIcon class="info-icon" />
108
- <span class="info-label">Profile Tags</span>
109
- <span class="info-value">{{ task.profileTags?.join(', ') || 'None' }}</span>
110
- </div>
111
- <div class="info-row" v-if="task.presaleCode" @click="copy(task.presaleCode)">
112
- <PencilIcon class="info-icon" />
113
- <span class="info-label">Presale Code</span>
114
- <span class="info-value copyable">{{ task.presaleCode }}</span>
115
- </div>
116
- <div class="info-row" v-if="task.startOffset">
117
- <ShieldIcon class="info-icon" />
118
- <span class="info-label">Start Offset</span>
119
- <span class="info-value">{{ task.startOffset }} min</span>
120
- </div>
122
+ <SectionCard title="Task Configuration" class="mb-4">
123
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
124
+ <InfoRow :icon="BagIcon" label="Ticket Quantity" :value="String(taskSnapshot.quantity)" />
125
+ <InfoRow :icon="ScannerIcon" label="Account Tag" :value="taskSnapshot.accountTag || 'None'" />
126
+ <InfoRow
127
+ :icon="SellIcon"
128
+ label="Profile Tags"
129
+ :value="taskSnapshot.profileTags?.join(', ') || 'None'" />
130
+ <InfoRow
131
+ v-if="taskSnapshot.presaleCode"
132
+ :icon="PencilIcon"
133
+ label="Presale Code"
134
+ :value="taskSnapshot.presaleCode"
135
+ copyable
136
+ :copy-value="taskSnapshot.presaleCode"
137
+ copy-message="Copied presale code" />
138
+ <InfoRow
139
+ v-if="taskSnapshot.startOffset"
140
+ :icon="ShieldIcon"
141
+ label="Start Offset"
142
+ :value="`${taskSnapshot.startOffset} min`" />
121
143
  </div>
122
- </div>
144
+ </SectionCard>
123
145
 
124
146
  <!-- Task Settings Toggles -->
125
- <div class="section-card mb-4">
126
- <h3 class="section-title mb-3">Task Settings</h3>
127
- <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
147
+ <SectionCard title="Task Settings" class="mb-4">
148
+ <div class="grid grid-cols-2 gap-4 md:grid-cols-3">
128
149
  <div class="toggle-item">
129
150
  <div class="toggle-label">
130
- <TimerIcon class="w-4 h-4" />
151
+ <TimerIcon class="h-4 w-4" />
131
152
  <span>Smart Timer</span>
132
153
  </div>
133
154
  <Switch v-model="toggles.smartTimer" disabled />
134
155
  </div>
135
156
  <div class="toggle-item">
136
157
  <div class="toggle-label">
137
- <GroupIcon class="w-4 h-4" />
158
+ <GroupIcon class="h-4 w-4" />
138
159
  <span>Login Later</span>
139
160
  </div>
140
161
  <Switch v-model="toggles.loginAfterCart" disabled />
141
162
  </div>
142
163
  <div class="toggle-item">
143
164
  <div class="toggle-label">
144
- <HandIcon class="w-4 h-4" />
165
+ <HandIcon class="h-4 w-4" />
145
166
  <span>Manual</span>
146
167
  </div>
147
168
  <Switch v-model="toggles.manual" disabled />
148
169
  </div>
149
170
  <div class="toggle-item">
150
171
  <div class="toggle-label">
151
- <SavingsIcon class="w-4 h-4" />
172
+ <SavingsIcon class="h-4 w-4" />
152
173
  <span>Do Not Pay</span>
153
174
  </div>
154
175
  <Switch v-model="toggles.doNotPay" disabled />
155
176
  </div>
156
177
  <div class="toggle-item">
157
178
  <div class="toggle-label">
158
- <LoyaltyIcon class="w-4 h-4" />
179
+ <LoyaltyIcon class="h-4 w-4" />
159
180
  <span>Presale Mode</span>
160
181
  </div>
161
182
  <Switch v-model="toggles.presaleMode" disabled />
162
183
  </div>
163
184
  <div class="toggle-item">
164
185
  <div class="toggle-label">
165
- <SkiIcon class="w-4 h-4" />
186
+ <SkiIcon class="h-4 w-4" />
166
187
  <span>Quick Queue</span>
167
188
  </div>
168
189
  <Switch v-model="toggles.quickQueue" disabled />
169
190
  </div>
170
191
  <div class="toggle-item">
171
192
  <div class="toggle-label">
172
- <SandclockIcon class="w-4 h-4" />
193
+ <SandclockIcon class="h-4 w-4" />
173
194
  <span>Aged Account</span>
174
195
  </div>
175
196
  <Switch v-model="toggles.agedAccount" disabled />
176
197
  </div>
177
198
  </div>
178
- </div>
199
+ </SectionCard>
179
200
  </Modal>
180
201
  </template>
181
202
 
182
203
  <script setup>
183
- import { ref, computed, watch } from 'vue';
184
- import Modal from '@/components/ui/Modal.vue';
185
- import Switch from '@/components/ui/controls/atomic/Switch.vue';
186
- import EyeToggle from '@/components/ui/controls/EyeToggle.vue';
187
- import { useUIStore } from '@/stores/ui';
204
+ import { ref, computed, watch } from "vue";
205
+ import Modal from "@/components/ui/Modal.vue";
206
+ import Switch from "@/components/ui/controls/atomic/Switch.vue";
207
+ import EyeToggle from "@/components/ui/controls/EyeToggle.vue";
208
+ import InfoRow from "@/components/ui/InfoRow.vue";
209
+ import SectionCard from "@/components/ui/SectionCard.vue";
210
+ import { useUIStore } from "@/stores/ui";
211
+ import { useCopyToClipboard } from "@/composables/useCopyToClipboard";
188
212
  import {
189
213
  EyeIcon,
190
214
  StadiumIcon,
@@ -204,10 +228,13 @@ import {
204
228
  SavingsIcon,
205
229
  LoyaltyIcon,
206
230
  SkiIcon,
207
- SandclockIcon
208
- } from '@/components/icons';
231
+ SandclockIcon,
232
+ StatusIcon,
233
+ CopyIcon
234
+ } from "@/components/icons";
209
235
 
210
236
  const ui = useUIStore();
237
+ const { copy } = useCopyToClipboard();
211
238
  const showPassword = ref(false);
212
239
 
213
240
  const props = defineProps({
@@ -217,35 +244,50 @@ const props = defineProps({
217
244
  }
218
245
  });
219
246
 
247
+ // Keep a local snapshot of task data that updates in real-time
248
+ const taskSnapshot = ref({ ...props.task });
249
+
250
+ // Watch for changes in the live task data from the store
251
+ const liveTask = computed(() => {
252
+ // Try to find the live task in the store
253
+ return ui.tasks?.[props.task.taskId];
254
+ });
255
+
256
+ watch(
257
+ liveTask,
258
+ (newTask) => {
259
+ if (newTask) {
260
+ // Task still exists - update snapshot with live data
261
+ taskSnapshot.value = { ...newTask };
262
+ }
263
+ // If newTask is undefined/null, task was deleted - keep last snapshot
264
+ },
265
+ { deep: true, immediate: true }
266
+ );
220
267
 
221
268
  const toggles = computed(() => ({
222
- smartTimer: props.task.smartTimer,
223
- loginAfterCart: props.task.loginAfterCart,
224
- manual: props.task.manual,
225
- doNotPay: props.task.doNotPay,
226
- presaleMode: props.task.presaleMode,
227
- quickQueue: props.task.quickQueue,
228
- agedAccount: props.task.agedAccount
269
+ smartTimer: taskSnapshot.value.smartTimer,
270
+ loginAfterCart: taskSnapshot.value.loginAfterCart,
271
+ manual: taskSnapshot.value.manual,
272
+ doNotPay: taskSnapshot.value.doNotPay,
273
+ presaleMode: taskSnapshot.value.presaleMode,
274
+ quickQueue: taskSnapshot.value.quickQueue,
275
+ agedAccount: taskSnapshot.value.agedAccount
229
276
  }));
230
277
 
231
- const copy = (text) => {
232
- navigator.clipboard.writeText(text);
233
- ui.showSuccess(`Copied: ${text}`);
234
- };
235
-
236
278
  const formatDate = (dateString) => {
237
- if (!dateString) return '';
279
+ if (!dateString) return "";
238
280
  try {
239
281
  const date = new Date(dateString);
240
282
  const options = {
241
- month: 'short',
242
- day: 'numeric',
243
- year: 'numeric',
244
- hour: 'numeric',
245
- minute: '2-digit',
283
+ month: "short",
284
+ day: "numeric",
285
+ year: "numeric",
286
+ hour: "numeric",
287
+ minute: "2-digit",
246
288
  hour12: true
247
289
  };
248
- return date.toLocaleString('en-US', options).replace(',', '');
290
+ return date.toLocaleString("en-US", options).replace(",", "");
249
291
  } catch {
250
292
  return dateString;
251
293
  }
@@ -258,103 +300,48 @@ const isTotalPrice = (line, index, lines) => {
258
300
  if (!isLastLine) return false;
259
301
  // Match currency symbols ($, €, £, etc.) OR currency codes (USD, EUR, AUD, etc.)
260
302
  const totalPricePattern = /^([$€£¥₹₽¢]|[A-Z]{3})\s*[\d,]+\.?\d*$/;
261
- return totalPricePattern.test(trimmed) && !trimmed.includes('(');
303
+ return totalPricePattern.test(trimmed) && !trimmed.includes("(");
262
304
  };
263
305
 
264
306
  const colorToClass = (color) => {
265
307
  const colorMap = {
266
- red: 'bg-red-400',
267
- green: 'bg-green-400',
268
- yellow: 'bg-yellow-400',
269
- blue: 'bg-blue-400'
308
+ red: "bg-red-400",
309
+ green: "bg-green-400",
310
+ yellow: "bg-yellow-400",
311
+ blue: "bg-blue-400"
270
312
  };
271
- return colorMap[color?.toLowerCase()] || 'bg-gray-400';
313
+ return colorMap[color?.toLowerCase()] || "bg-gray-400";
272
314
  };
273
315
  </script>
274
316
 
275
317
  <style lang="scss" scoped>
276
- .section-card {
277
- @apply rounded-lg border p-4;
278
- background: oklch(0.2046 0 0);
279
- border-color: oklch(0.2809 0 0);
280
- }
281
-
282
- .section-title {
283
- @apply text-sm font-semibold mb-3;
284
- color: oklch(0.90 0 0);
285
- }
286
-
287
- .info-row {
288
- @apply flex items-center gap-3 px-3 py-2 rounded-lg;
289
- background: oklch(0.2603 0 0);
290
- border: 1px solid oklch(0.2809 0 0);
291
- min-height: 40px;
292
-
293
- &.copyable {
294
- cursor: pointer;
295
-
296
- &:hover {
297
- border-color: oklch(0.72 0.15 145);
298
- background: oklch(0.72 0.15 145 / 0.08);
299
- }
300
- }
301
- }
302
-
303
- .info-icon {
304
- @apply w-4 h-4 flex-shrink-0;
305
- color: oklch(0.90 0 0) !important;
306
-
307
- svg {
308
- color: oklch(0.90 0 0) !important;
309
- width: 16px !important;
310
- height: 16px !important;
311
- }
312
- }
313
-
314
- .info-label {
315
- @apply text-xs font-medium flex-shrink-0;
316
- color: oklch(0.65 0 0);
317
- min-width: 100px;
318
- }
319
-
320
- .info-value {
321
- @apply text-sm flex-1;
322
- color: oklch(0.90 0 0);
323
- overflow-wrap: break-word;
324
- word-break: break-word;
325
- }
326
-
327
- .copyable {
328
- cursor: pointer;
329
- }
330
-
331
318
  .toggle-item {
332
319
  @apply flex flex-col items-center gap-2;
333
320
  }
334
321
 
335
322
  .toggle-label {
336
323
  @apply flex items-center gap-2 text-xs;
337
- color: oklch(0.90 0 0);
324
+ color: oklch(0.9 0 0);
338
325
 
339
326
  svg {
340
- color: oklch(0.90 0 0) !important;
327
+ color: oklch(0.9 0 0) !important;
341
328
  width: 16px !important;
342
329
  height: 16px !important;
343
330
  }
344
331
  }
345
332
 
346
333
  .status-indicator {
347
- @apply w-2 h-2 rounded-full flex-shrink-0;
334
+ @apply h-2 w-2 flex-shrink-0 rounded-full;
348
335
  }
349
336
 
350
- @media (max-width: 768px) {
351
- .info-label {
352
- min-width: 80px;
353
- font-size: 11px;
354
- }
337
+ .info-icon {
338
+ @apply h-4 w-4 flex-shrink-0;
339
+ color: oklch(0.9 0 0) !important;
355
340
 
356
- .info-value {
357
- font-size: 13px;
341
+ svg {
342
+ color: oklch(0.9 0 0) !important;
343
+ width: 16px !important;
344
+ height: 16px !important;
358
345
  }
359
346
  }
360
347
  </style>
@@ -0,0 +1,6 @@
1
+ <template>
2
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
4
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
5
+ </svg>
6
+ </template>
@@ -55,6 +55,7 @@ import CloseXIcon from "./CloseX.vue";
55
55
  import CheckIcon from "./Check.vue";
56
56
  import SpinnerIcon from "./Spinner.vue";
57
57
  import EyeIcon from "./Eye.vue";
58
+ import CopyIcon from "./Copy.vue";
58
59
 
59
60
  export {
60
61
  EditIcon,
@@ -109,5 +110,6 @@ export {
109
110
  CloseXIcon,
110
111
  CheckIcon,
111
112
  SpinnerIcon,
112
- EyeIcon
113
+ EyeIcon,
114
+ CopyIcon
113
115
  };
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <ul class="mx-auto flex items-center justify-center rounded overflow-visible flex-wrap
3
+ bg-dark-400 border-2 border-dark-600 p-[3px] gap-0.5
4
+ md:p-0.5 md:gap-px md:rounded-md
5
+ lg:p-[3px] lg:gap-0.5 lg:rounded-lg lg:flex-nowrap
6
+ max-md:max-w-[44px] max-md:min-h-7 max-md:h-auto max-md:justify-start max-md:mx-0">
7
+ <slot />
8
+ </ul>
9
+ </template>
10
+
11
+ <script setup>
12
+ // Simple wrapper component for action button groups
13
+ </script>
14
+
15
+ <style lang="scss" scoped>
16
+ ul {
17
+ :deep(button) {
18
+ @apply relative flex items-center justify-center rounded border-0 outline-0 transition-all duration-150;
19
+ @apply bg-transparent text-light-300;
20
+ @apply w-7 h-7 rounded-md;
21
+
22
+ &:hover {
23
+ @apply bg-accent-green/15 text-white;
24
+ }
25
+
26
+ &:active {
27
+ @apply bg-accent-green/25;
28
+ }
29
+
30
+ // Tablet sizing
31
+ @media (min-width: 768px) and (max-width: 1023px) {
32
+ @apply w-[26px] h-[26px] rounded;
33
+ }
34
+
35
+ // Desktop sizing
36
+ @media (min-width: 1024px) {
37
+ @apply w-7 h-7 rounded-md;
38
+ }
39
+
40
+ // Mobile sizing
41
+ @media (max-width: 640px) {
42
+ @apply w-[18px] h-[18px] rounded-xs min-w-[18px] m-px;
43
+ }
44
+ }
45
+
46
+ :deep(svg),
47
+ :deep(img) {
48
+ @apply w-4 h-4 relative z-[1];
49
+
50
+ // Tablet sizing
51
+ @media (min-width: 768px) and (max-width: 1023px) {
52
+ @apply w-3.5 h-3.5;
53
+ }
54
+
55
+ // Desktop sizing
56
+ @media (min-width: 1024px) {
57
+ @apply w-4 h-4;
58
+ }
59
+
60
+ // Mobile sizing
61
+ @media (max-width: 640px) {
62
+ @apply w-[12px] h-[12px];
63
+ }
64
+ }
65
+
66
+ :deep(li) {
67
+ @apply flex m-0 p-0;
68
+ }
69
+ }
70
+ </style>