@necrolab/dashboard 0.5.15 → 0.5.17

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 (137) hide show
  1. package/backend/api.js +2 -3
  2. package/eslint.config.js +46 -0
  3. package/index.html +2 -1
  4. package/package.json +5 -2
  5. package/src/App.vue +70 -566
  6. package/src/assets/css/base/mixins.scss +72 -0
  7. package/src/assets/css/base/reset.scss +0 -2
  8. package/src/assets/css/base/scroll.scss +43 -36
  9. package/src/assets/css/base/typography.scss +9 -10
  10. package/src/assets/css/base/variables.scss +43 -0
  11. package/src/assets/css/components/accessibility.scss +37 -0
  12. package/src/assets/css/components/buttons.scss +61 -74
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +13 -21
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -22
  17. package/src/assets/css/components/tables.scss +5 -7
  18. package/src/assets/css/components/toasts.scss +7 -7
  19. package/src/assets/css/components/utilities.scss +295 -0
  20. package/src/assets/css/main.scss +55 -139
  21. package/src/components/Auth/LoginForm.vue +7 -86
  22. package/src/components/Console/ConsoleToolbar.vue +123 -0
  23. package/src/components/Editors/Account/Account.vue +12 -12
  24. package/src/components/Editors/Account/AccountView.vue +38 -111
  25. package/src/components/Editors/Account/CreateAccount.vue +11 -61
  26. package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +28 -59
  27. package/src/components/Editors/AdminFileEditor.vue +179 -0
  28. package/src/components/Editors/Profile/CreateProfile.vue +77 -150
  29. package/src/components/Editors/Profile/Profile.vue +20 -21
  30. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  31. package/src/components/Editors/Profile/ProfileView.vue +41 -116
  32. package/src/components/Editors/ProxyFileEditor.vue +86 -0
  33. package/src/components/Editors/TagLabel.vue +16 -55
  34. package/src/components/Editors/TagToggle.vue +20 -8
  35. package/src/components/Filter/Filter.vue +66 -79
  36. package/src/components/Filter/FilterPreview.vue +153 -135
  37. package/src/components/Filter/PriceSortToggle.vue +36 -43
  38. package/src/components/Table/Header.vue +1 -1
  39. package/src/components/Table/Table.vue +45 -51
  40. package/src/components/Tasks/CheckStock.vue +7 -16
  41. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  42. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  43. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  44. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  45. package/src/components/Tasks/EventDetailRow.vue +21 -0
  46. package/src/components/Tasks/MassEdit.vue +6 -16
  47. package/src/components/Tasks/QuickSettings.vue +140 -216
  48. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  49. package/src/components/Tasks/Stats.vue +20 -39
  50. package/src/components/Tasks/Task.vue +64 -270
  51. package/src/components/Tasks/TaskLabel.vue +9 -3
  52. package/src/components/Tasks/TaskView.vue +45 -64
  53. package/src/components/Tasks/Utilities.vue +10 -44
  54. package/src/components/Tasks/ViewTask.vue +23 -107
  55. package/src/components/icons/Close.vue +2 -8
  56. package/src/components/icons/Gear.vue +8 -8
  57. package/src/components/icons/Hash.vue +5 -0
  58. package/src/components/icons/Key.vue +2 -8
  59. package/src/components/icons/Pencil.vue +2 -8
  60. package/src/components/icons/Profile.vue +2 -8
  61. package/src/components/icons/Sell.vue +2 -8
  62. package/src/components/icons/Spinner.vue +4 -7
  63. package/src/components/icons/Wildcard.vue +2 -8
  64. package/src/components/icons/index.js +3 -5
  65. package/src/components/ui/ActionButtonGroup.vue +113 -52
  66. package/src/components/ui/BalanceIndicator.vue +60 -0
  67. package/src/components/ui/EmptyState.vue +24 -0
  68. package/src/components/ui/EnableDisableToggle.vue +23 -0
  69. package/src/components/ui/FormField.vue +49 -49
  70. package/src/components/ui/IconLabel.vue +23 -0
  71. package/src/components/ui/InfoRow.vue +21 -54
  72. package/src/components/ui/Modal.vue +161 -54
  73. package/src/components/ui/Navbar.vue +63 -44
  74. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  75. package/src/components/ui/ReconnectIndicator.vue +111 -124
  76. package/src/components/ui/SectionCard.vue +6 -14
  77. package/src/components/ui/Splash.vue +2 -10
  78. package/src/components/ui/StatusBadge.vue +26 -28
  79. package/src/components/ui/TaskToggle.vue +54 -0
  80. package/src/components/ui/controls/CountryChooser.vue +29 -66
  81. package/src/components/ui/controls/EyeToggle.vue +1 -1
  82. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  83. package/src/components/ui/controls/atomic/Dropdown.vue +103 -139
  84. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -120
  85. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  86. package/src/composables/useCodeEditor.js +117 -0
  87. package/src/composables/useColorMapping.js +15 -0
  88. package/src/composables/useCopyToClipboard.js +1 -1
  89. package/src/composables/useDateFormatting.js +21 -0
  90. package/src/composables/useDeviceDetection.js +14 -0
  91. package/src/composables/useDropdownPosition.js +1 -4
  92. package/src/composables/useDynamicTableHeight.js +31 -0
  93. package/src/composables/useEnableDisable.js +6 -0
  94. package/src/composables/useFilterCSS.js +71 -0
  95. package/src/composables/useFormValidation.js +92 -0
  96. package/src/composables/useGetAllTags.js +9 -0
  97. package/src/composables/useIOSViewportHandling.js +76 -0
  98. package/src/composables/useNotchHandling.js +306 -0
  99. package/src/composables/useRowSelection.js +0 -3
  100. package/src/composables/useTableRender.js +23 -0
  101. package/src/composables/useTicketPricing.js +16 -0
  102. package/src/composables/useWindowDimensions.js +21 -0
  103. package/src/composables/useZoomPrevention.js +96 -0
  104. package/src/constants/tableLayout.js +14 -0
  105. package/src/libs/Filter.js +14 -20
  106. package/src/libs/panzoom.js +1 -5
  107. package/src/libs/utils/array.js +58 -0
  108. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  109. package/src/libs/utils/eventUrl.js +40 -0
  110. package/src/libs/utils/string.js +3 -0
  111. package/src/libs/utils/time.js +20 -0
  112. package/src/libs/utils/validation.js +64 -0
  113. package/src/main.js +0 -2
  114. package/src/stores/connection.js +1 -29
  115. package/src/stores/logger.js +6 -12
  116. package/src/stores/sampleData.js +1 -2
  117. package/src/stores/ui.js +80 -71
  118. package/src/utils/tableHelpers.js +1 -0
  119. package/src/views/Accounts.vue +19 -38
  120. package/src/views/Console.vue +74 -253
  121. package/src/views/Editor.vue +47 -1114
  122. package/src/views/FilterBuilder.vue +190 -461
  123. package/src/views/Login.vue +3 -28
  124. package/src/views/Profiles.vue +17 -32
  125. package/src/views/Tasks.vue +51 -38
  126. package/tailwind.config.js +82 -71
  127. package/workbox-config.cjs +47 -5
  128. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2438
  129. package/exit +0 -209
  130. package/run +0 -177
  131. package/src/assets/css/base/color-fallbacks.scss +0 -10
  132. package/src/assets/img/background.svg.backup +0 -11
  133. package/src/components/icons/SquareCheck.vue +0 -18
  134. package/src/components/icons/SquareUncheck.vue +0 -18
  135. package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
  136. package/switch-branch.sh +0 -41
  137. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
@@ -1,41 +1,26 @@
1
1
  <template>
2
2
  <div class="form-section">
3
3
  <!-- Username -->
4
- <div class="input-container mb-3">
5
- <div class="flex items-center gap-2">
4
+ <div class="form-field-labeled mb-3">
5
+ <div class="flex-gap-2 items-center">
6
6
  <ProfileIcon class="w-4 h-4" />
7
7
  <span class="text-light-300 font-medium text-sm">Username</span>
8
8
  </div>
9
9
  <div class="flex-1 flex items-center">
10
- <input
11
- type="text"
12
- class="login-input"
13
- v-model="user"
14
- autocapitalize="off"
15
- autocorrect="off"
16
- autocomplete="username"
17
- />
10
+ <input type="text" class="form-input-minimal" v-model="user" autocapitalize="off" autocorrect="off" autocomplete="username" aria-label="Username" />
18
11
  </div>
19
12
  </div>
20
13
  <!-- Password -->
21
- <div class="input-container mb-4">
22
- <div class="flex items-center gap-2">
14
+ <div class="form-field-labeled mb-4">
15
+ <div class="flex-gap-2 items-center">
23
16
  <KeyIcon class="w-4 h-4" />
24
17
  <span class="text-light-300 font-medium text-sm">Password</span>
25
18
  </div>
26
19
  <div class="flex-1 flex items-center">
27
- <input
28
- type="password"
29
- class="login-input"
30
- v-model="password"
31
- />
20
+ <input type="password" class="form-input-minimal" v-model="password" aria-label="Password" />
32
21
  </div>
33
22
  </div>
34
- <button
35
- class="login-btn mt-6 mx-auto"
36
- @click="login()"
37
- :disabled="buttonDisabled"
38
- >
23
+ <button class="login-btn w-48 mt-6 mx-auto" @click="login()" :disabled="buttonDisabled">
39
24
  <span v-if="!buttonDisabled">Login</span>
40
25
  <svg v-if="!buttonDisabled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
41
26
  <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M13.8 12H3"/>
@@ -68,67 +53,3 @@ async function login() {
68
53
  buttonDisabled.value = false;
69
54
  }
70
55
  </script>
71
-
72
- <style lang="scss" scoped>
73
- .login-btn {
74
- @apply flex items-center justify-center gap-2 rounded-lg transition-all duration-150;
75
- background: oklch(0.72 0.15 145);
76
- border: 2px solid oklch(0.72 0.15 145);
77
- color: oklch(1 0 0);
78
- height: 3rem;
79
- width: 12rem;
80
- font-size: 0.9375rem;
81
- font-weight: 600;
82
- letter-spacing: 0.05em;
83
- text-transform: uppercase;
84
-
85
- &:hover:not(:disabled) {
86
- background: oklch(0.68 0.15 145);
87
- border-color: oklch(0.68 0.15 145);
88
- transform: translateY(-1px);
89
- box-shadow: 0 4px 12px oklch(0.72 0.15 145 / 0.3);
90
- }
91
-
92
- &:active:not(:disabled) {
93
- transform: translateY(0);
94
- box-shadow: 0 2px 4px oklch(0.72 0.15 145 / 0.2);
95
- }
96
-
97
- &:disabled {
98
- opacity: 0.7;
99
- cursor: not-allowed;
100
- }
101
- }
102
-
103
- .input-container {
104
- @apply text-white bg-dark-500 px-3 rounded-lg border-2 border-dark-550 flex items-center justify-between h-11;
105
- overflow: visible;
106
- transition: border-color 0.15s ease;
107
-
108
- &:hover {
109
- border-color: oklch(0.30 0 0);
110
- }
111
-
112
- &:focus-within {
113
- border-color: oklch(0.72 0.15 145) !important;
114
- outline: 1px solid oklch(0.72 0.15 145);
115
- outline-offset: 0;
116
- }
117
- }
118
-
119
- .login-input {
120
- @apply w-full h-full text-sm text-white bg-transparent border-0 outline-none px-2 py-1;
121
-
122
- &:focus {
123
- @apply outline-none border-0 shadow-none bg-transparent;
124
- }
125
-
126
- &:hover:not(:focus) {
127
- background: transparent;
128
- }
129
-
130
- &::placeholder {
131
- color: oklch(0.50 0 0);
132
- }
133
- }
134
- </style>
@@ -0,0 +1,123 @@
1
+ <template>
2
+ <div>
3
+ <div class="mb-3 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
4
+ <div class="flex flex-col gap-3 md:flex-1 md:flex-row md:items-center">
5
+ <div class="w-full md:w-64">
6
+ <Dropdown
7
+ class="console-dropdown input-default w-full border-2 border-dark-550 bg-dark-500"
8
+ rightAmount="right-2"
9
+ default="All logs"
10
+ :allowDefault="true"
11
+ :value="currentTaskLog"
12
+ :onClick="(f) => $emit('update:currentTaskLog', f.split(' ')[0])"
13
+ :options="Object.entries(taskLogMapping).map(([k, v]) => `${k} (${v.length})`).sort((a, b) => a.localeCompare(b))" />
14
+ </div>
15
+ <div class="flex flex-1 items-center gap-2">
16
+ <div class="input-default flex flex-1 items-center md:max-w-64">
17
+ <input
18
+ :value="searchQuery"
19
+ @input="$emit('update:searchQuery', $event.target.value)"
20
+ type="text"
21
+ placeholder="Search logs..."
22
+ aria-label="Search logs"
23
+ class="transparent-input" />
24
+ <span v-if="searchQuery" class="ml-2 text-xs text-light-500">{{ filteredCount }}</span>
25
+ </div>
26
+ <button
27
+ class="console-scroll-btn md:hidden"
28
+ @mousedown="$emit('scroll', 'up')"
29
+ @mouseup="$emit('scroll-stop')"
30
+ @mouseleave="$emit('scroll-stop')"
31
+ @touchstart="$emit('scroll', 'up')"
32
+ @touchend="$emit('scroll-stop')"
33
+ aria-label="Scroll up">
34
+ <UpIcon class="pointer-events-none h-5 w-5" />
35
+ </button>
36
+ <button
37
+ class="console-scroll-btn md:hidden"
38
+ @mousedown="$emit('scroll', 'down')"
39
+ @mouseup="$emit('scroll-stop')"
40
+ @mouseleave="$emit('scroll-stop')"
41
+ @touchstart="$emit('scroll', 'down')"
42
+ @touchend="$emit('scroll-stop')">
43
+ <DownIcon class="pointer-events-none h-5 w-5" />
44
+ </button>
45
+ </div>
46
+ </div>
47
+ <div class="flex hidden items-center gap-3 md:flex">
48
+ <div class="hidden items-center gap-3 md:flex">
49
+ <button class="flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
50
+ <h3 class="text-sm text-white">Hide Monitors</h3>
51
+ <Switch class="scale-75" :model-value="filteredLogs" @update:model-value="$emit('update:filteredLogs', $event)" />
52
+ </button>
53
+ <button class="relative flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
54
+ <h3 class="text-sm text-white">Auto</h3>
55
+ <Switch class="scale-75" :model-value="autoscrollToggled" @update:model-value="$emit('update:autoscrollToggled', $event); $emit('autoscroll-toggle')" />
56
+ <div
57
+ v-if="userScrolledUp && autoscrollToggled"
58
+ class="absolute -right-1 -top-1 h-2 w-2 animate-pulse rounded-full bg-yellow-500"
59
+ title="Autoscroll paused - scroll to bottom to resume"></div>
60
+ </button>
61
+ </div>
62
+ <button
63
+ class="hidden size-10 items-center justify-center rounded border border-dark-650 bg-dark-400 shadow-sm transition-colors duration-150 hover:bg-dark-300 active:bg-dark-200 md:flex"
64
+ @mousedown="$emit('scroll', 'up')"
65
+ @mouseup="$emit('scroll-stop')"
66
+ @mouseleave="$emit('scroll-stop')"
67
+ @touchstart="$emit('scroll', 'up')"
68
+ @touchend="$emit('scroll-stop')">
69
+ <UpIcon class="pointer-events-none h-5 w-5" />
70
+ </button>
71
+ <button
72
+ class="hidden size-10 items-center justify-center rounded border border-dark-650 bg-dark-400 shadow-sm transition-colors duration-150 hover:bg-dark-300 active:bg-dark-200 md:flex"
73
+ @mousedown="$emit('scroll', 'down')"
74
+ @mouseup="$emit('scroll-stop')"
75
+ @mouseleave="$emit('scroll-stop')"
76
+ @touchstart="$emit('scroll', 'down')"
77
+ @touchend="$emit('scroll-stop')">
78
+ <DownIcon class="pointer-events-none h-5 w-5" />
79
+ </button>
80
+ </div>
81
+ </div>
82
+ <div class="mb-6 mt-4 flex justify-between md:hidden mobile-portrait:mb-16 mobile-portrait:mt-6">
83
+ <button class="flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
84
+ <h3 class="text-sm text-white">Hide Monitors</h3>
85
+ <Switch class="scale-75" :model-value="filteredLogs" @update:model-value="$emit('update:filteredLogs', $event)" />
86
+ </button>
87
+ <button class="relative flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
88
+ <h3 class="text-sm text-white">Auto</h3>
89
+ <Switch class="scale-75" :model-value="autoscrollToggled" @update:model-value="$emit('update:autoscrollToggled', $event); $emit('autoscroll-toggle')" />
90
+ <div
91
+ v-if="userScrolledUp && autoscrollToggled"
92
+ class="absolute -right-1 -top-1 h-2 w-2 animate-pulse rounded-full bg-yellow-500"
93
+ title="Autoscroll paused - scroll to bottom to resume"></div>
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </template>
98
+
99
+ <script setup>
100
+ import { DownIcon, UpIcon } from '@/components/icons';
101
+ import Switch from '@/components/ui/controls/atomic/Switch.vue';
102
+ import Dropdown from '@/components/ui/controls/atomic/Dropdown.vue';
103
+
104
+ defineProps({
105
+ currentTaskLog: String,
106
+ taskLogMapping: Object,
107
+ searchQuery: String,
108
+ filteredLogs: Boolean,
109
+ autoscrollToggled: Boolean,
110
+ userScrolledUp: Boolean,
111
+ filteredCount: String
112
+ });
113
+
114
+ defineEmits([
115
+ 'update:currentTaskLog',
116
+ 'update:searchQuery',
117
+ 'update:filteredLogs',
118
+ 'update:autoscrollToggled',
119
+ 'scroll',
120
+ 'scroll-stop',
121
+ 'autoscroll-toggle'
122
+ ]);
123
+ </script>
@@ -11,12 +11,12 @@
11
11
  class="ml-0 mr-4"
12
12
  :toggled="props.account.selected"
13
13
  @valueUpdate="ui.toggleAccountSelected(props.account.id)" />
14
- <h4 class="mx-auto text-white" @click="copy(props.account.email, 'Copied email')">
14
+ <h4 class="mx-auto text-center text-white" @click="copy(props.account.email, 'Copied email')">
15
15
  {{ props.account.email }}
16
16
  </h4>
17
17
  </div>
18
18
  <div class="col-span-2 hidden md:block" @click="copy(props.account.password, 'Copied password')">
19
- <h4 class="text-white">
19
+ <h4 class="text-center text-white">
20
20
  {{ props.account.privacy ? "•".repeat(props.account.password.length) : props.account.password }}
21
21
  </h4>
22
22
  </div>
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
 
27
27
  <div class="col-span-1 hidden lg:block">
28
- <h4 class="flex justify-center gap-1 text-white">
28
+ <h4 class="flex justify-center gap-1 text-center text-white">
29
29
  <TagLabel v-for="tag in props.account.tags" :key="tag" :text="tag" />
30
30
  </h4>
31
31
  </div>
@@ -39,26 +39,21 @@
39
39
  </li>
40
40
  <li v-if="props.account.enabled">
41
41
  <button @click="disable">
42
- <img class="h-4 w-4" src="/img/controls/disable.svg" />
42
+ <img class="icon-md" src="/img/controls/disable.svg" />
43
43
  </button>
44
44
  </li>
45
45
  <li v-else>
46
46
  <button @click="enable">
47
- <img class="h-4 w-4" src="/img/controls/enable.svg" />
47
+ <img class="icon-md" src="/img/controls/enable.svg" />
48
48
  </button>
49
49
  </li>
50
50
  </ActionButtonGroup>
51
51
  </div>
52
52
  </Row>
53
53
  </template>
54
- <style lang="scss" scoped>
55
- h4 {
56
- @apply text-center;
57
- }
58
- </style>
59
54
  <script setup>
60
55
  import { Row } from "@/components/Table";
61
- import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon } from "@/components/icons";
56
+ import { EditIcon } from "@/components/icons";
62
57
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
63
58
  import StatusBadge from "@/components/ui/StatusBadge.vue";
64
59
  import ActionButtonGroup from "@/components/ui/ActionButtonGroup.vue";
@@ -66,12 +61,17 @@ import { useUIStore } from "@/stores/ui";
66
61
  import TagLabel from "@/components/Editors/TagLabel.vue";
67
62
  import { useRowSelection } from "@/composables/useRowSelection";
68
63
  import { useCopyToClipboard } from "@/composables/useCopyToClipboard";
64
+ import { computed } from "vue";
65
+ import { useEnableDisable } from "@/composables/useEnableDisable";
69
66
 
70
67
  const ui = useUIStore();
71
68
  const { copy } = useCopyToClipboard();
72
69
 
73
70
  const props = defineProps({
74
- account: { type: Object }
71
+ account: {
72
+ type: Object,
73
+ required: true
74
+ }
75
75
  });
76
76
 
77
77
  const enable = async () => await ui.addAccount({ ...props.account, enabled: true });
@@ -1,152 +1,79 @@
1
1
  <template>
2
- <Table>
3
- <Header class="sticky top-0 z-10 grid-cols-5 bg-dark-400 text-center md:grid-cols-7">
4
- <div class="col-span-3 flex lg:col-span-2">
2
+ <div class="table-component relative box-border flex flex-col rounded-lg bg-dark-500 bg-clip-padding overflow-hidden shadow-sm">
3
+ <Header class="sticky top-0 z-10 grid-cols-5 text-center md:grid-cols-7">
4
+ <div class="col-span-3 flex items-center justify-start lg:col-span-2">
5
5
  <Checkbox
6
- class="mr-3"
6
+ class="ml-2 mr-4 flex-shrink-0"
7
7
  :toggled="ui.mainCheckbox.accounts"
8
8
  @valueUpdate="ui.toggleMainCheckbox('accounts')"
9
9
  :isHeader="true" />
10
- <div class="mx-auto flex items-center" @click="ui.toggleSort('eventId')">
11
- <MailIcon class="mr-0 h-4 w-4 md:mr-3" />
12
- <h4 class="hidden md:flex">Email</h4>
10
+ <div class="mx-auto flex cursor-pointer items-center" @click="ui.toggleSort('eventId')">
11
+ <MailIcon class="mr-0 icon-md md:mr-3" />
12
+ <h4 class="hidden text-white md:flex">Email</h4>
13
13
  </div>
14
14
  </div>
15
15
  <div class="col-span-2 hidden items-center justify-center md:flex" v-once>
16
- <KeyIcon class="mr-0 h-4 w-4 md:mr-3" />
17
- <h4 class="hidden md:flex">Password</h4>
16
+ <KeyIcon class="mr-0 icon-md md:mr-3" />
17
+ <h4 class="hidden text-white md:flex">Password</h4>
18
18
  </div>
19
- <div class="col-span-1 flex items-center justify-center" v-once>
20
- <CheckmarkIcon class="mr-0 h-4 w-4 md:mr-3" />
21
- <h4 class="hidden md:flex">Enabled</h4>
19
+ <div class="grid-cell-center" v-once>
20
+ <CheckmarkIcon class="mr-0 icon-md md:mr-3" />
21
+ <h4 class="hidden text-white md:flex">Enabled</h4>
22
22
  </div>
23
23
  <div class="col-span-1 hidden items-center justify-center lg:flex" v-once>
24
- <TicketIcon class="mr-0 h-4 w-4 md:mr-3" />
25
- <h4 class="hidden md:flex">Tags</h4>
24
+ <TicketIcon class="mr-0 icon-md md:mr-3" />
25
+ <h4 class="hidden text-white md:flex">Tags</h4>
26
26
  </div>
27
- <div class="col-span-1 flex items-center justify-center" v-once>
28
- <ClickIcon class="mr-0 h-4 w-4 md:mr-3" />
29
- <h4 class="hidden md:flex">Actions</h4>
27
+ <div class="grid-cell-center" v-once>
28
+ <ClickIcon class="mr-0 icon-md md:mr-3" />
29
+ <h4 class="hidden text-white md:flex">Actions</h4>
30
30
  </div>
31
31
  </Header>
32
32
  <div
33
33
  v-if="toRender.length != 0"
34
- class="hidden-scrollbars stop-pan flex flex-col divide-y divide-dark-650 overflow-y-auto overflow-x-hidden"
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
35
  :style="{ maxHeight: dynamicTableHeight }">
36
- <div v-for="(account, i) in toRender" :key="account.id || account.index" class="account-row-container">
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">
37
40
  <Account
38
- :class="i % 2 == 1 ? 'table-row-even' : 'table-row-odd'"
41
+ :class="getRowClass(i)"
39
42
  :account="account" />
40
43
  </div>
41
44
  </div>
42
- <div v-else class="empty-state flex flex-col items-center justify-center bg-dark-400 py-8 text-center">
43
- <MailIcon class="mb-3 h-12 w-12 text-dark-400 opacity-50" />
44
- <p class="text-sm text-light-400">No accounts found</p>
45
- <p class="mt-1 text-xs text-light-500">Create accounts to get started</p>
46
- </div>
47
- </Table>
45
+ <EmptyState v-else :icon="MailIcon" message="No accounts found" subtitle="Create accounts to get started" />
46
+ </div>
48
47
  </template>
49
- <style lang="scss" scoped>
50
- .account-row-container {
51
- min-height: 64px;
52
- flex-shrink: 0;
53
- transition: background-color 0.15s ease;
54
-
55
- &:hover {
56
- @apply bg-dark-550 !important;
57
- }
58
- }
59
-
60
- h4 {
61
- @apply text-white;
62
- }
63
-
64
- .stop-pan {
65
- touch-action: pan-y pan-up pan-down;
66
- }
67
-
68
- .empty-state {
69
- font-size: 14px;
70
- font-weight: 500;
71
- }
72
- </style>
73
48
  <script setup>
74
- import { Table, Header } from "@/components/Table";
49
+ import { Header } from "@/components/Table";
75
50
  import {
76
- EventIcon,
77
51
  TicketIcon,
78
- StatusIcon,
79
52
  ClickIcon,
80
- DownIcon,
81
53
  MailIcon,
82
54
  KeyIcon,
83
55
  CheckmarkIcon
84
56
  } from "@/components/icons";
85
57
  import Account from "./Account.vue";
86
58
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
59
+ import EmptyState from "@/components/ui/EmptyState.vue";
87
60
  import { useUIStore } from "@/stores/ui";
88
- import { computed, ref, onMounted, onUnmounted } from "vue";
61
+ import { useDynamicTableHeight } from "@/composables/useDynamicTableHeight";
62
+ import { computed } from "vue";
63
+ import { useTableRender } from "@/composables/useTableRender";
64
+ import { getRowClass } from "@/utils/tableHelpers";
89
65
 
90
66
  const props = defineProps({
91
- accounts: { type: Object }
67
+ accounts: {
68
+ type: Object,
69
+ required: true
70
+ }
92
71
  });
93
72
  const ui = useUIStore();
94
73
 
95
- const i = ref({});
96
- const toRender = computed(() => {
97
- let c = 0;
98
- const rendered = props.accounts.map((t) => ({ ...t, index: c++ }));
99
-
100
- // Initialize reactive refs for click tracking
101
- rendered.forEach((t) => {
102
- if (t.id && !(t.id in i.value)) {
103
- i.value[t.id] = 0;
104
- }
105
- if (!(t.index in i.value)) {
106
- i.value[t.index] = 0;
107
- }
108
- });
109
-
110
- return rendered;
111
- });
112
-
113
- // Dynamic height calculation for perfect item fitting
114
- const windowHeight = ref(window.innerHeight);
115
- const windowWidth = ref(window.innerWidth);
116
-
117
- const updateDimensions = () => {
118
- windowHeight.value = window.innerHeight;
119
- windowWidth.value = window.innerWidth;
120
- };
121
-
122
- onMounted(() => {
123
- window.addEventListener("resize", updateDimensions);
124
- });
125
-
126
- onUnmounted(() => {
127
- window.removeEventListener("resize", updateDimensions);
128
- });
129
-
130
- const dynamicTableHeight = computed(() => {
131
- // Calculate available space for accounts table with conservative buffer
132
- const headerHeight = 60; // Header + navbar
133
- const titleHeight = 50; // Accounts title and controls
134
- const searchHeight = 50; // Search and filter controls
135
- const margins = windowWidth.value >= 1024 ? 40 : 25;
136
- const bufferSpace = 50; // Conservative buffer to prevent partial items
137
-
138
- const totalUsedSpace = headerHeight + titleHeight + searchHeight + margins + bufferSpace;
139
- const availableHeight = windowHeight.value - totalUsedSpace;
74
+ const { toRender } = useTableRender(computed(() => props.accounts));
140
75
 
141
- // Account row height is always 64px
142
- const rowHeight = 64;
143
- const minRowsToShow = 2;
144
- const minHeight = minRowsToShow * rowHeight;
76
+ import { TABLE_LAYOUT } from "@/constants/tableLayout";
145
77
 
146
- // Calculate exact number of complete rows that fit with conservative approach
147
- const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
148
- const exactHeight = maxCompleteRows * rowHeight;
149
-
150
- return exactHeight + "px";
151
- });
78
+ const { dynamicTableHeight } = useDynamicTableHeight(TABLE_LAYOUT.ACCOUNTS);
152
79
  </script>
@@ -9,11 +9,7 @@
9
9
  <div>
10
10
  <div class="my-3 grid grid-cols-12 gap-3">
11
11
  <!-- Account tag -->
12
- <div class="input-wrapper col-span-4">
13
- <label class="label-override mb-2">
14
- Account Tag
15
- <TagIcon />
16
- </label>
12
+ <FormField label="Account Tag" :icon="TagIcon" z-index="0" class="col-span-12 md:col-span-4" noWrapper>
17
13
  <Dropdown
18
14
  :class="`input-default dropdown w-full p-4`"
19
15
  :default="ui.profile.tags[0]"
@@ -22,10 +18,10 @@
22
18
  :capitalize="true"
23
19
  :allowDefault="false"
24
20
  :chosen="account.tag" />
25
- </div>
21
+ </FormField>
26
22
 
27
23
  <!-- Email -->
28
- <FormField label="Email" :icon="MailIcon" required :error="errors.includes('email')" z-index="0" class="col-span-8">
24
+ <FormField label="Email" :icon="MailIcon" required :error="errors.includes('email')" z-index="0" class="col-span-12 md:col-span-8">
29
25
  <input
30
26
  placeholder="email@example.com"
31
27
  type="email"
@@ -54,47 +50,15 @@
54
50
  </div>
55
51
 
56
52
  <!-- Readonly fields when editing -->
57
- <div v-if="ui.currentlyEditing?.email" class="mt-6 grid grid-cols-12 gap-3 pt-4 border-t border-dark-600">
58
- <div v-if="ui.currentlyEditing.tags && ui.currentlyEditing.tags.length > 0" class="col-span-6">
59
- <label class="label-override mb-2">Tags</label>
60
- <div class="flex gap-2 flex-wrap">
61
- <TagLabel v-for="tag in ui.currentlyEditing.tags" :key="tag" :text="tag" />
62
- </div>
63
- </div>
64
- <div class="col-span-6">
65
- <label class="label-override mb-2">Status</label>
66
- <div class="flex items-center gap-3 h-10">
67
- <StatusBadge :enabled="ui.currentlyEditing.enabled" size="large" />
68
- <span class="text-sm font-medium" :class="ui.currentlyEditing.enabled ? 'text-green-400' : 'text-red-400'">
69
- {{ ui.currentlyEditing.enabled ? 'Enabled' : 'Disabled' }}
70
- </span>
71
- </div>
72
- </div>
73
- </div>
53
+ <ReadonlyFieldsSection v-if="ui.currentlyEditing?.email" :data="ui.currentlyEditing" />
74
54
  </div>
75
55
 
76
- <button
77
- class="button-default ml-auto mt-4 flex w-48 items-center justify-center gap-x-2 bg-dark-400 text-xs"
78
- @click="done()">
56
+ <button class="btn-modal ml-auto mt-4 w-48" @click="done()">
79
57
  Save
80
- <EditIcon />
58
+ <EditIcon class="ml-2" />
81
59
  </button>
82
60
  </Modal>
83
61
  </template>
84
- <style lang="scss" scoped>
85
- .label-override {
86
- @apply flex items-center;
87
- color: #e1e1e4 !important;
88
-
89
- svg {
90
- @apply ml-2;
91
-
92
- path {
93
- fill: #e1e1e4 !important;
94
- }
95
- }
96
- }
97
- </style>
98
62
  <script setup>
99
63
  import Modal from "@/components/ui/Modal.vue";
100
64
  import FormField from "@/components/ui/FormField.vue";
@@ -102,18 +66,13 @@ import {
102
66
  EditIcon,
103
67
  MailIcon,
104
68
  KeyIcon,
105
- ProfileIcon,
106
- TimerIcon,
107
- SandclockIcon,
108
- TagIcon,
109
- ScannerIcon
69
+ TagIcon
110
70
  } from "@/components/icons";
111
71
  import { useUIStore } from "@/stores/ui";
112
72
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
113
- import StatusBadge from "@/components/ui/StatusBadge.vue";
114
- import TagLabel from "@/components/Editors/TagLabel.vue";
115
-
73
+ import ReadonlyFieldsSection from "@/components/ui/ReadonlyFieldsSection.vue";
116
74
  import { ref } from "vue";
75
+ import { useFormValidation } from "@/composables/useFormValidation";
117
76
 
118
77
  const ui = useUIStore();
119
78
  const account = ref({
@@ -124,19 +83,10 @@ const account = ref({
124
83
 
125
84
  if (ui.currentlyEditing?.email) account.value = ui.currentlyEditing;
126
85
 
127
- const errors = ref([]);
128
-
129
- const validate = (p) => {
130
- errors.value = [];
131
-
132
- if (!p.email.includes("@")) errors.value.push("email");
133
- if (p.password.length < 5) errors.value.push("password");
134
-
135
- return errors.value.length === 0;
136
- };
86
+ const { errors, validateAccount } = useFormValidation();
137
87
 
138
88
  function done() {
139
- if (!validate(account.value)) return;
89
+ if (!validateAccount(account.value)) return;
140
90
  ui.toggleModal("");
141
91
  ui.addAccount(account.value);
142
92
  }