@necrolab/dashboard 0.4.48 → 0.4.50

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 (42) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.prettierrc +12 -1
  3. package/exit +209 -0
  4. package/index.html +1 -1
  5. package/package.json +1 -1
  6. package/public/manifest.json +8 -3
  7. package/src/assets/css/_input.scss +104 -111
  8. package/src/assets/css/_utilities.scss +441 -0
  9. package/src/assets/css/main.scss +228 -154
  10. package/src/components/Auth/LoginForm.vue +49 -6
  11. package/src/components/Editors/Account/Account.vue +156 -146
  12. package/src/components/Editors/Account/AccountCreator.vue +1 -1
  13. package/src/components/Editors/Account/AccountView.vue +13 -13
  14. package/src/components/Editors/Account/CreateAccount.vue +25 -16
  15. package/src/components/Editors/Profile/CreateProfile.vue +1 -1
  16. package/src/components/Editors/Profile/Profile.vue +1 -1
  17. package/src/components/Editors/Profile/ProfileCountryChooser.vue +83 -19
  18. package/src/components/Editors/Profile/ProfileView.vue +11 -11
  19. package/src/components/Tasks/CreateTaskAXS.vue +3 -3
  20. package/src/components/Tasks/CreateTaskTM.vue +7 -35
  21. package/src/components/Tasks/QuickSettings.vue +112 -9
  22. package/src/components/Tasks/Stats.vue +29 -26
  23. package/src/components/Tasks/Task.vue +499 -365
  24. package/src/components/Tasks/TaskView.vue +187 -127
  25. package/src/components/icons/Sandclock.vue +2 -2
  26. package/src/components/icons/Stadium.vue +1 -1
  27. package/src/components/ui/Modal.vue +37 -35
  28. package/src/components/ui/controls/CountryChooser.vue +200 -62
  29. package/src/components/ui/controls/atomic/Dropdown.vue +177 -91
  30. package/src/components/ui/controls/atomic/MultiDropdown.vue +247 -168
  31. package/src/composables/useClickOutside.js +21 -0
  32. package/src/composables/useDropdownPosition.js +174 -0
  33. package/src/stores/sampleData.js +4 -2
  34. package/src/stores/ui.js +8 -6
  35. package/src/views/Accounts.vue +12 -19
  36. package/src/views/Console.vue +34 -47
  37. package/src/views/Editor.vue +1194 -730
  38. package/src/views/Login.vue +65 -119
  39. package/src/views/Profiles.vue +2 -2
  40. package/src/views/Tasks.vue +170 -137
  41. package/static/offline.html +192 -50
  42. package/tailwind.config.js +47 -21
@@ -1,140 +1,188 @@
1
1
  <template>
2
- <Table>
3
- <Header class="text-center grid-cols-10 gap-2 ipadlg:grid-cols-12">
4
- <div class="col-span-1 lg:col-span-2 flex items-center justify-start">
5
- <Checkbox
6
- class="ml-2 mr-4"
7
- :toggled="ui.mainCheckbox.tasks"
8
- @valueUpdate="ui.toggleMainCheckbox('tasks')"
9
- :isHeader="true" />
10
- <div class="mx-auto hidden lg:flex items-center" @click="ui.toggleSort('eventId')">
11
- <EventIcon class="ipadlg:mr-3" />
12
- <h4 class="hidden ipadlg:flex">Event</h4>
13
- <DownIcon v-if="ui.sortData.sortBy === 'eventId' && !ui.sortData.reversed" class="ml-1" />
14
- <UpIcon v-if="ui.sortData.sortBy === 'eventId' && ui.sortData.reversed" class="ml-1" />
15
- </div>
16
- </div>
17
- <div class="col-span-2 flex-center" v-once>
18
- <TicketIcon class="mr-0 ipadlg:mr-3" />
19
- <h4 class="hidden ipadlg:flex">Tickets</h4>
20
- </div>
21
- <div
22
- class="col-span-5 md:col-span-4 lg:col-span-5 ipadlg:col-span-6 flex-center"
23
- @click="ui.toggleSort('status')">
24
- <StatusIcon class="mr-0 ipadlg:mr-3" />
25
- <h4 class="hidden ipadlg:flex">Status</h4>
26
- <DownIcon v-if="ui.sortData.sortBy === 'status' && !ui.sortData.reversed" class="ml-1" />
27
- <UpIcon v-if="ui.sortData.sortBy === 'status' && ui.sortData.reversed" class="ml-1" />
28
- </div>
29
- <div class="col-span-2 ipadlg:col-span-3 flex-center" v-once>
30
- <ClickIcon class="mr-0 ipadlg:mr-3" />
31
- <h4 class="hidden ipadlg:flex">Actions</h4>
32
- </div>
33
- <div class="absolute right-5 hidden md:flex items-center top-3,5" @click="ui.toggleSort('taskId')">
34
- <h4 class="">ID</h4>
35
- <DownIcon v-if="ui.sortData.sortBy === 'taskId' && !ui.sortData.reversed" class="ml-1" />
36
- <UpIcon v-if="ui.sortData.sortBy === 'taskId' && ui.sortData.reversed" class="ml-1" />
37
- </div>
38
- </Header>
2
+ <div class="table-component">
3
+ <Header class="text-center grid-cols-10 gap-2 lg:grid-cols-12">
4
+ <div class="col-span-1 lg:col-span-2 flex items-center justify-start">
5
+ <Checkbox
6
+ class="ml-2 mr-4 flex-shrink-0"
7
+ :toggled="ui.mainCheckbox.tasks"
8
+ @valueUpdate="ui.toggleMainCheckbox('tasks')"
9
+ :isHeader="true"
10
+ />
39
11
  <div
40
- class="flex flex-col divide-y divide-dark-650 overflow-y-auto hidden-scrollbars overflow-x-hidden stop-pan"
41
- :style="{ maxHeight: dynamicTableHeight }">
42
- <div v-for="(task, i) in getTasksInOrder()" :key="task.taskId" class="task-row-container">
43
- <Task :task="task" :style="{ backgroundColor: i % 2 == 1 ? '#1a1b1e' : '#242529' }" />
44
- </div>
45
- <div
46
- v-if="getTasksInOrder().length === 0"
47
- class="flex flex-col items-center justify-center py-8 empty-state text-center">
48
- <div v-if="ui.queueStats.total === 0">
49
- <TasksIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50 mx-auto" />
50
- <p class="text-light-400 text-sm">No tasks yet</p>
51
- <p class="text-light-500 text-xs mt-1">Create tasks to get started</p>
52
- </div>
53
- <div v-else>
54
- <TasksIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50 mx-auto" />
55
- <p class="text-light-400 text-sm">
56
- {{ ui.queueStats.total }} hidden task{{ ui.queueStats.total === 1 ? "" : "s" }}
57
- </p>
58
- <p class="text-light-500 text-xs mt-1">Adjust filters to see tasks</p>
59
- </div>
60
- </div>
12
+ class="mx-auto hidden lg:flex items-center"
13
+ @click="ui.toggleSort('eventId')"
14
+ >
15
+ <EventIcon class="lg:mr-3" />
16
+ <h4 class="hidden lg:flex">Event</h4>
17
+ <DownIcon
18
+ v-if="ui.sortData.sortBy === 'eventId' && !ui.sortData.reversed"
19
+ class="ml-1"
20
+ />
21
+ <UpIcon
22
+ v-if="ui.sortData.sortBy === 'eventId' && ui.sortData.reversed"
23
+ class="ml-1"
24
+ />
61
25
  </div>
62
- </Table>
26
+ </div>
27
+ <div class="col-span-2 flex-center" v-once>
28
+ <TicketIcon class="mr-0 lg:mr-3" />
29
+ <h4 class="hidden lg:flex">Tickets</h4>
30
+ </div>
31
+ <div
32
+ class="col-span-5 md:col-span-4 lg:col-span-5 flex-center"
33
+ @click="ui.toggleSort('status')"
34
+ >
35
+ <StatusIcon class="mr-0 lg:mr-3" />
36
+ <h4 class="hidden lg:flex">Status</h4>
37
+ <DownIcon
38
+ v-if="ui.sortData.sortBy === 'status' && !ui.sortData.reversed"
39
+ class="ml-1"
40
+ />
41
+ <UpIcon
42
+ v-if="ui.sortData.sortBy === 'status' && ui.sortData.reversed"
43
+ class="ml-1"
44
+ />
45
+ </div>
46
+ <div class="col-span-2 lg:col-span-3 flex-center" v-once>
47
+ <ClickIcon class="mr-0 lg:mr-3" />
48
+ <h4 class="hidden lg:flex">Actions</h4>
49
+ </div>
50
+ <div
51
+ class="absolute right-5 hidden lg:flex items-center top-3.5"
52
+ @click="ui.toggleSort('taskId')"
53
+ >
54
+ <h4 class="text-center text-xs">ID</h4>
55
+ <DownIcon
56
+ v-if="ui.sortData.sortBy === 'taskId' && !ui.sortData.reversed"
57
+ class="ml-1"
58
+ />
59
+ <UpIcon
60
+ v-if="ui.sortData.sortBy === 'taskId' && ui.sortData.reversed"
61
+ class="ml-1"
62
+ />
63
+ </div>
64
+ </Header>
65
+ <div
66
+ class="flex flex-col divide-y divide-dark-650 overflow-y-auto hidden-scrollbars overflow-x-hidden stop-pan"
67
+ :style="{ maxHeight: dynamicTableHeight }"
68
+ >
69
+ <div
70
+ v-for="(task, i) in getTasksInOrder()"
71
+ :key="task.taskId"
72
+ class="task-row-container"
73
+ >
74
+ <Task :task="task" :class="i % 2 == 1 ? 'table-row-even' : 'table-row-odd'" />
75
+ </div>
76
+ <div
77
+ v-if="getTasksInOrder().length === 0"
78
+ class="flex flex-col items-center justify-center py-8 empty-state text-center"
79
+ >
80
+ <div
81
+ v-if="
82
+ !ui.queueStats.queued &&
83
+ !ui.queueStats.sleeping &&
84
+ ui.queueStats.nextQueuePasses.length === 0
85
+ "
86
+ >
87
+ <TasksIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50 mx-auto" />
88
+ <p class="text-light-400 text-sm">No tasks yet</p>
89
+ <p class="text-light-500 text-xs mt-1">Create tasks to get started</p>
90
+ </div>
91
+ <div v-else>
92
+ <TasksIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50 mx-auto" />
93
+ <p class="text-light-400 text-sm">No tasks match current filters</p>
94
+ <p class="text-light-500 text-xs mt-1">Adjust filters to see tasks</p>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
63
99
  </template>
64
100
  <style lang="scss" scoped>
65
101
  h4 {
66
- @apply text-white;
102
+ @apply text-white;
67
103
  }
68
104
 
69
105
  .stop-pan {
70
- touch-action: pan-y pan-up pan-down;
106
+ touch-action: pan-y pan-up pan-down;
71
107
  }
72
108
 
73
109
  .task-row-container {
74
- min-height: 69px;
75
- flex-shrink: 0;
76
- transition: background-color 0.15s ease;
110
+ min-height: 69px;
111
+ flex-shrink: 0;
112
+ transition: background-color 0.15s ease;
77
113
 
78
- &:hover {
79
- @apply bg-dark-550 !important;
80
- }
114
+ &:hover {
115
+ @apply bg-dark-550 !important;
116
+ }
81
117
 
82
- @media (max-width: 768px) {
83
- min-height: 58px;
84
- }
118
+ @media (max-width: 768px) {
119
+ min-height: 58px;
120
+ }
121
+
122
+ @media (max-width: 480px) and (orientation: portrait) {
123
+ min-height: 50px;
124
+ }
85
125
  }
86
126
 
87
127
  .empty-state {
88
- @apply bg-dark-400;
89
- color: #969696;
90
- font-size: 14px;
91
- font-weight: 500;
128
+ @apply bg-dark-400;
129
+ color: #969696;
130
+ font-size: 14px;
131
+ font-weight: 500;
92
132
  }
93
133
  </style>
94
134
  <script setup>
95
135
  import { computed, ref, onMounted, onUnmounted } from "vue";
96
136
  import { Table, Header } from "@/components/Table";
97
- import { EventIcon, TicketIcon, StatusIcon, ClickIcon, DownIcon, UpIcon, TasksIcon } from "@/components/icons";
137
+ import {
138
+ EventIcon,
139
+ TicketIcon,
140
+ StatusIcon,
141
+ ClickIcon,
142
+ DownIcon,
143
+ UpIcon,
144
+ TasksIcon,
145
+ } from "@/components/icons";
98
146
  import Task from "./Task.vue";
99
147
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
100
148
  import { useUIStore } from "@/stores/ui";
101
149
 
102
150
  const props = defineProps({
103
- tasks: { type: Object }
151
+ tasks: { type: Object },
104
152
  });
105
153
 
106
154
  const shouldTaskShow = (task) => {
107
- if (ui.taskFilter === "All") return true;
108
- else if (ui.taskFilter === "Checkout") return task.expirationTime || task.noCartholds;
109
- else return true;
155
+ if (ui.taskFilter === "All") return true;
156
+ else if (ui.taskFilter === "Checkout") return task.expirationTime || task.noCartholds;
157
+ else return true;
110
158
  };
111
159
 
112
160
  const ui = useUIStore();
113
161
 
114
162
  const siteIdEdgeCases = {
115
- LN_US: ["TM_US"],
116
- TM_CA: ["TM_US"],
117
- TM_IE: ["TM_UK"],
118
- TM_NZ: ["TM_AU"]
163
+ LN_US: ["TM_US"],
164
+ TM_CA: ["TM_US"],
165
+ TM_IE: ["TM_UK"],
166
+ TM_NZ: ["TM_AU"],
119
167
  };
120
168
 
121
169
  const getTasksInOrder = () => {
122
- let out = [];
123
- ui.taskIdOrder.forEach((id) => {
124
- if (props.tasks[id] && !props.tasks[id]?.hidden) {
125
- const task = props.tasks[id];
126
-
127
- if (
128
- task.siteId !== ui.currentCountry.siteId &&
129
- !siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)
130
- )
131
- return;
132
- if (ui.currentEvent && task.eventId !== ui.currentEvent) return;
133
- if (!shouldTaskShow(task)) return;
134
- out.push(task);
135
- }
136
- });
137
- return out;
170
+ let out = [];
171
+ ui.taskIdOrder.forEach((id) => {
172
+ if (props.tasks[id] && !props.tasks[id]?.hidden) {
173
+ const task = props.tasks[id];
174
+
175
+ if (
176
+ task.siteId !== ui.currentCountry.siteId &&
177
+ !siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)
178
+ )
179
+ return;
180
+ if (ui.currentEvent && task.eventId !== ui.currentEvent) return;
181
+ if (!shouldTaskShow(task)) return;
182
+ out.push(task);
183
+ }
184
+ });
185
+ return out;
138
186
  };
139
187
 
140
188
  // Dynamic height calculation to prevent page scrolling
@@ -142,42 +190,54 @@ const windowHeight = ref(window.innerHeight);
142
190
  const windowWidth = ref(window.innerWidth);
143
191
 
144
192
  const updateDimensions = () => {
145
- windowHeight.value = window.innerHeight;
146
- windowWidth.value = window.innerWidth;
193
+ windowHeight.value = window.innerHeight;
194
+ windowWidth.value = window.innerWidth;
147
195
  };
148
196
 
149
197
  onMounted(() => {
150
- window.addEventListener("resize", updateDimensions);
198
+ window.addEventListener("resize", updateDimensions);
151
199
  });
152
200
 
153
201
  onUnmounted(() => {
154
- window.removeEventListener("resize", updateDimensions);
202
+ window.removeEventListener("resize", updateDimensions);
155
203
  });
156
204
 
157
205
  const dynamicTableHeight = computed(() => {
158
- // Calculate available space for table
159
- const headerHeight = 60; // Header + navbar
160
- const titleHeight = 50; // Tasks title and mobile controls
161
- const statsHeight = 50; // Queue stats
162
- const controlsHeight = windowWidth.value >= 650 ? 60 : 0; // Desktop controls
163
- const filtersHeight = 50; // Event dropdown and filter toggle
164
- const utilitiesHeight = windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 150 : 200; // Reduce utilities height in mobile portrait
165
- const margins =
166
- windowWidth.value >= 1024 ? 40 : windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 15 : 20; // Reduce margins in mobile portrait
167
-
168
- const totalUsedSpace =
169
- headerHeight + titleHeight + statsHeight + controlsHeight + filtersHeight + utilitiesHeight + margins;
170
- const availableHeight = windowHeight.value - totalUsedSpace;
171
-
172
- // Calculate row height based on screen size
173
- const rowHeight = windowWidth.value <= 768 ? 58 : 69; // Mobile vs desktop row height
174
- const minRowsToShow = 2; // Always show at least 2 rows
175
- const minHeight = minRowsToShow * rowHeight;
176
-
177
- // Calculate how many complete rows can fit
178
- const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight) + 1;
179
- const exactHeight = maxCompleteRows * rowHeight;
180
-
181
- return exactHeight + "px";
206
+ // Calculate available space for table
207
+ const headerHeight = 60; // Header + navbar
208
+ const titleHeight = 50; // Tasks title and mobile controls
209
+ const statsHeight = 50; // Queue stats
210
+ const controlsHeight = windowWidth.value >= 650 ? 60 : 0; // Desktop controls
211
+ const filtersHeight = 50; // Event dropdown and filter toggle
212
+ const utilitiesHeight =
213
+ windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 150 : 200; // Reduce utilities height in mobile portrait
214
+ const margins =
215
+ windowWidth.value >= 1024
216
+ ? 40
217
+ : windowWidth.value <= 480 && windowHeight.value > windowWidth.value
218
+ ? 15
219
+ : 20; // Reduce margins in mobile portrait
220
+
221
+ const totalUsedSpace =
222
+ headerHeight +
223
+ titleHeight +
224
+ statsHeight +
225
+ controlsHeight +
226
+ filtersHeight +
227
+ utilitiesHeight +
228
+ margins;
229
+ const availableHeight = windowHeight.value - totalUsedSpace;
230
+
231
+ // Calculate row height based on screen size
232
+ const rowHeight = windowWidth.value <= 768 ? 58 : 69; // Mobile vs desktop row height
233
+ const minRowsToShow = 2; // Always show at least 2 rows
234
+ const minHeight = minRowsToShow * rowHeight;
235
+
236
+ // Calculate how many complete rows can fit
237
+ const maxCompleteRows =
238
+ Math.floor(Math.max(availableHeight, minHeight) / rowHeight) + 1;
239
+ const exactHeight = maxCompleteRows * rowHeight;
240
+
241
+ return exactHeight + "px";
182
242
  });
183
243
  </script>
@@ -13,14 +13,14 @@
13
13
  <g>
14
14
  <g>
15
15
  <path
16
- style="fill: #6e7084"
16
+ fill="white"
17
17
  d="M16.192,5.224V4.487h-8.77v0.737c0,0,1.334,3.713,3.838,5.428v1.785c0,0-2.761,2.686-3.838,5.775
18
18
  v0.842h8.77v-0.842c-1.399-3.41-3.837-5.775-3.837-5.775v-1.785C15.759,7.726,16.192,5.224,16.192,5.224z"
19
19
  />
20
20
  </g>
21
21
  <g>
22
22
  <path
23
- style="fill: #6e7084"
23
+ fill="white"
24
24
  d="M19.353,3.856V2.529h1.258V0H3.002v2.529h1.259v1.327c0,2.025,3.634,7.555,3.804,7.955
25
25
  c-0.167,0.397-3.804,5.929-3.804,7.946v1.325H3.002v2.53h17.609v-2.53h-1.258v-1.325c0-2.025-3.635-7.521-3.829-7.951
26
26
  C15.718,11.376,19.353,5.88,19.353,3.856z M18.096,19.757v1.325H5.519v-1.325c0-1.455,3.854-7.222,3.854-7.951
@@ -6,7 +6,7 @@
6
6
  <g mask="url(#mask0_16_241)">
7
7
  <path
8
8
  d="M2.37499 5.54165V2.37498L5.54166 3.95831L2.37499 5.54165ZM14.25 5.54165V2.37498L17.4167 3.95831L14.25 5.54165ZM8.70833 4.74998V1.58331L11.875 3.16665L8.70833 4.74998ZM8.70833 17.4166C7.70555 17.3903 6.77218 17.3079 5.9082 17.1696C5.0437 17.0308 4.29162 16.8559 3.65195 16.6448C3.01176 16.4337 2.50694 16.1896 2.13749 15.9125C1.76805 15.6354 1.58333 15.3451 1.58333 15.0416V7.91665C1.58333 7.58678 1.79127 7.27988 2.20716 6.99594C2.62252 6.71252 3.18645 6.46183 3.89895 6.24385C4.61145 6.02641 5.4493 5.85488 6.4125 5.72927C7.37569 5.60419 8.40486 5.54165 9.49999 5.54165C10.5951 5.54165 11.6243 5.60419 12.5875 5.72927C13.5507 5.85488 14.3885 6.02641 15.101 6.24385C15.8135 6.46183 16.3775 6.71252 16.7928 6.99594C17.2087 7.27988 17.4167 7.58678 17.4167 7.91665V15.0416C17.4167 15.3451 17.2319 15.6354 16.8625 15.9125C16.493 16.1896 15.9885 16.4337 15.3488 16.6448C14.7086 16.8559 13.9565 17.0308 13.0926 17.1696C12.2281 17.3079 11.2944 17.3903 10.2917 17.4166V14.25H8.70833V17.4166ZM9.49999 8.70831C10.7799 8.70831 11.885 8.63231 12.8155 8.48031C13.7454 8.32884 14.4875 8.15415 15.0417 7.95623C15.0417 7.89026 14.5403 7.73509 13.5375 7.49073C12.5347 7.2469 11.1889 7.12498 9.49999 7.12498C7.81111 7.12498 6.46527 7.2469 5.46249 7.49073C4.45972 7.73509 3.95833 7.89026 3.95833 7.95623C4.51249 8.15415 5.25481 8.32884 6.18529 8.48031C7.11523 8.63231 8.22013 8.70831 9.49999 8.70831ZM7.12499 15.7146V12.6666H11.875V15.7146C12.9305 15.609 13.7948 15.4538 14.4677 15.2491C15.1406 15.0448 15.5958 14.8635 15.8333 14.7052V9.34165C15.1076 9.63192 14.1972 9.86283 13.1021 10.0344C12.0069 10.2059 10.8062 10.2916 9.49999 10.2916C8.19374 10.2916 6.99305 10.2059 5.89791 10.0344C4.80277 9.86283 3.89236 9.63192 3.16666 9.34165V14.7052C3.40416 14.8635 3.85937 15.0448 4.53229 15.2491C5.2052 15.4538 6.06944 15.609 7.12499 15.7146Z"
9
- fill="currentColor"
9
+ fill="white"
10
10
  />
11
11
  </g>
12
12
  </svg>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="modal-mask pt-14 ipadlg:py-1 overflow-y-scroll scrollable" @touchmove.prevent>
2
+ <div class="modal-mask pt-14 md:py-1 overflow-y-scroll scrollable" @touchmove.prevent>
3
3
  <div class="component-modal" ref="target" @touchmove.stop>
4
4
  <div class="modal-header">
5
5
  <slot name="header" />
@@ -57,17 +57,19 @@ onClickOutside(target, (event) => {
57
57
  <style lang="scss" scoped>
58
58
  .modal-mask {
59
59
  @apply fixed top-0 left-0 w-screen h-screen flex duration-300 ease-in-out;
60
- z-index: 9998;
60
+ z-index: 25000;
61
61
  background-color: rgba(17, 17, 17, 0.85);
62
62
  backdrop-filter: blur(4px);
63
- align-items: center; // Keep centered on desktop
63
+ align-items: center;
64
64
  justify-content: center;
65
- padding: 2rem;
65
+ padding: 1rem;
66
66
  }
67
67
 
68
68
  .component-modal {
69
69
  width: 640px;
70
- // Remove max-height on desktop to show full modal
70
+ max-height: calc(100vh - 8rem); // Ensure bottom margin on desktop
71
+ margin-bottom: 3rem;
72
+ overflow-y: auto; // Make scrollable when content is too tall
71
73
  @apply bg-dark-300 px-5 py-5 rounded-lg flex flex-col;
72
74
 
73
75
  .modal-header {
@@ -80,6 +82,8 @@ onClickOutside(target, (event) => {
80
82
 
81
83
  .modal-body {
82
84
  @apply flex flex-col;
85
+ flex: 1;
86
+ min-height: 0;
83
87
  }
84
88
  }
85
89
 
@@ -88,54 +92,52 @@ onClickOutside(target, (event) => {
88
92
  align-items: flex-start; // Start from top on mobile
89
93
  justify-content: center;
90
94
  padding: 1rem;
91
- padding-top: 2rem;
95
+ padding-top: 3rem;
96
+ padding-bottom: 2rem; // Ensure bottom space
92
97
  }
93
98
 
94
99
  .component-modal {
95
100
  width: calc(100vw - 2rem);
96
- max-height: calc(100vh - 8rem); // Only limit height on mobile
97
- overflow-y: auto; // Only add scrolling on mobile
101
+ margin-bottom: 6rem; // Increased bottom margin for better button access
98
102
  }
99
103
 
100
104
  .modal-body {
101
- padding-bottom: 3rem; // Increased padding for create button accessibility
105
+ padding-bottom: 4rem; // Increased padding for create button accessibility
102
106
  overflow-y: auto; // Allow scrolling on mobile
103
107
  flex: 1;
104
108
  min-height: 0;
105
109
  }
106
110
  }
107
111
 
108
- // iPhone landscape mode specific adjustments
109
- @media only screen and (max-device-width: 430px) and (orientation: landscape) {
112
+ /* iPhone portrait mode - extra spacing for create button */
113
+ @media (max-width: 480px) and (orientation: portrait) {
110
114
  .modal-mask {
111
- top: 0;
112
- left: 0;
113
- right: 0;
114
- bottom: 0;
115
- width: 100vw;
116
- height: 100vh;
117
- padding: 0;
118
- padding-top: calc(env(safe-area-inset-top, 0px) + 4rem); // Navbar + safe area
119
- padding-bottom: env(safe-area-inset-bottom, 0.5rem);
120
- padding-left: env(safe-area-inset-left, 0.5rem);
121
- padding-right: env(safe-area-inset-right, 0.5rem);
122
- align-items: flex-start;
123
- justify-content: center;
115
+ padding-bottom: 3rem !important; // Extra bottom padding for iPhone
124
116
  }
125
-
117
+
126
118
  .component-modal {
127
- width: 100%;
128
- max-width: none;
129
- max-height: calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 5rem);
130
- min-height: auto;
131
- margin: 0;
119
+ margin-bottom: 8rem !important; // Even more bottom margin for iPhone portrait
120
+ max-height: calc(100vh - 12rem) !important; // Ensure modal doesn't get too tall
132
121
  }
133
-
122
+
134
123
  .modal-body {
135
- padding-bottom: 4rem; // Ensure create button is accessible
136
- max-height: calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 8rem);
137
- overflow-y: auto;
138
- -webkit-overflow-scrolling: touch;
124
+ padding-bottom: 5rem !important; // Extra padding for create button on iPhone
125
+ }
126
+ }
127
+
128
+ // iPhone landscape mode - proper modal centering with consistent spacing
129
+ @media (max-height: 500px) and (orientation: landscape) {
130
+ .modal-mask {
131
+ padding: 1rem 1rem 4rem 1rem !important;
132
+ align-items: center !important;
133
+ justify-content: center !important;
134
+ }
135
+
136
+ .component-modal {
137
+ max-height: calc(100vh - 10rem) !important;
138
+ overflow-y: auto !important;
139
+ margin: 0 !important;
140
+ margin-bottom: 30px;
139
141
  }
140
142
  }
141
143
  </style>