@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,5 +1,5 @@
1
1
  <template>
2
- <div
2
+ <div
3
3
  class="filter-card group text-white text-sm transition-all duration-200 hover:bg-dark-400 relative"
4
4
  :class="{
5
5
  'expanded-filter': filterBuilder.expandedFilter === filter.id,
@@ -9,7 +9,7 @@
9
9
  >
10
10
  <div class="grid grid-cols-12 items-center py-3 px-2 sm:px-4 gap-2 sm:gap-3">
11
11
  <div class="col-span-9 sm:col-span-10">
12
- <div class="flex items-center gap-2 sm:gap-3 cursor-pointer flex-1" @click="handleFilterClick(filter.id)">
12
+ <div class="flex-gap-2 items-center sm:gap-3 cursor-pointer flex-1" @click="handleFilterClick(filter.id)">
13
13
  <div class="filter-type-badge flex-shrink-0">
14
14
  <component :is="getFilterIcon()" class="w-3 h-3 sm:w-4 sm:h-4" />
15
15
  </div>
@@ -19,7 +19,6 @@
19
19
  </div>
20
20
  </div>
21
21
  <div class="expanded-content mt-4 mx-2 sm:mx-0" v-if="filterBuilder.expandedFilter === filter.id">
22
- <!-- NORMAL -->
23
22
  <div v-if="filterType === 2" class="space-y-2">
24
23
  <div class="info-row">
25
24
  <span class="label">Section:</span>
@@ -34,7 +33,6 @@
34
33
  <span class="value">{{ filter?.event || filter?.eventId || "-" }}</span>
35
34
  </div>
36
35
  </div>
37
- <!-- NORMAL_FIRSTROW -->
38
36
  <div v-if="filterType === 3" class="space-y-2">
39
37
  <div class="info-row">
40
38
  <span class="label">Section:</span>
@@ -49,7 +47,6 @@
49
47
  <span class="value">{{ filter?.event || "-" }}</span>
50
48
  </div>
51
49
  </div>
52
- <!-- CATCH_ALL_GA -->
53
50
  <div v-if="filterType === 1">
54
51
  <button
55
52
  @click.stop="update({ buyAny: true, floor: false, generalAdmission: false })"
@@ -59,7 +56,6 @@
59
56
  Convert to wildcard
60
57
  </button>
61
58
  </div>
62
- <!-- CATCH_ALL -->
63
59
  <div v-if="filterType === 0">
64
60
  <button
65
61
  @click.stop="update({ floor: true, buyAny: false, generalAdmission: false })"
@@ -69,7 +65,6 @@
69
65
  Convert to Floor Wildcard
70
66
  </button>
71
67
  </div>
72
- <!-- CATCH_ALL_FLOOR -->
73
68
  <div v-if="filterType === 5">
74
69
  <button
75
70
  @click.stop="update({ generalAdmission: true, floor: false, buyAny: false })"
@@ -82,11 +77,11 @@
82
77
  </div>
83
78
  <div v-if="filterBuilder.expandedFilter === filter.id" class="controls-section mt-4 mx-2 sm:mx-0">
84
79
  <div class="flex flex-col sm:flex-row flex-wrap gap-3 sm:items-center">
85
- <div class="flex items-center gap-2 control-group">
80
+ <div class="flex-gap-2 items-center control-group">
86
81
  <Checkbox :toggled="filter.exclude || false" @valueUpdate="handleExcludeClick" />
87
82
  <label class="text-sm font-medium">Excluded</label>
88
83
  </div>
89
- <div class="flex items-center gap-2 control-group">
84
+ <div class="flex-gap-2 items-center control-group">
90
85
  <label class="text-xs whitespace-nowrap text-light-400">Min:</label>
91
86
  <input
92
87
  type="number"
@@ -104,7 +99,7 @@
104
99
  class="filter-input w-16 sm:w-20"
105
100
  />
106
101
  </div>
107
- <div class="flex items-center gap-2 control-group">
102
+ <div class="flex-gap-2 items-center control-group">
108
103
  <label class="text-xs whitespace-nowrap text-light-400">Max:</label>
109
104
  <input
110
105
  type="number"
@@ -131,16 +126,15 @@
131
126
  </div>
132
127
  </div>
133
128
  </div>
134
- <div class="col-span-3 sm:col-span-2 flex justify-end items-center gap-1 sm:gap-2">
135
- <div class="drag-handle handle filter-action-btn drag-btn cursor-grab active:cursor-grabbing" title="Drag to reorder">
136
- <MenuIcon class="w-3 h-3 sm:w-4 sm:h-4" />
129
+ <div class="col-span-3 sm:col-span-2 flex justify-end items-center gap-2">
130
+ <div class="drag-handle handle drag-btn cursor-grab active:cursor-grabbing" title="Drag to reorder">
131
+ <MenuIcon class="icon-md" />
137
132
  </div>
138
- <button
139
- @click="filterBuilder.deleteFilterById(filter.id)"
140
- class="delete-btn filter-action-btn"
141
- title="Delete filter"
142
- >
143
- <TrashIcon class="w-3 h-3 sm:w-4 sm:h-4" />
133
+ <button
134
+ @click="filterBuilder.deleteFilterById(filter.id)"
135
+ class="delete-btn"
136
+ title="Delete filter">
137
+ <TrashIcon class="icon-md" />
144
138
  </button>
145
139
  </div>
146
140
  </div>
@@ -155,16 +149,27 @@ import { ref } from "vue";
155
149
  import { useUIStore } from "@/stores/ui";
156
150
  const ui = useUIStore();
157
151
 
158
- // eslint-disable-next-line no-unused-vars
159
- import { UpIcon, DownIcon, ReloadIcon, TrashIcon, MenuIcon, WildcardIcon, BoxIcon, GroupIcon, FilterIcon, StadiumIcon } from "@/components/icons";
152
+ import { TrashIcon, MenuIcon, WildcardIcon, BoxIcon, GroupIcon, FilterIcon, StadiumIcon } from "@/components/icons";
160
153
 
161
154
  let isAllRows = ref(false);
162
155
 
163
156
  const props = defineProps({
164
- filter: Object,
165
- index: Number,
166
- expandedFilter: String,
167
- filterBuilder: Object
157
+ filter: {
158
+ type: Object,
159
+ required: true
160
+ },
161
+ index: {
162
+ type: Number,
163
+ required: true
164
+ },
165
+ expandedFilter: {
166
+ type: String,
167
+ default: null
168
+ },
169
+ filterBuilder: {
170
+ type: Object,
171
+ required: true
172
+ }
168
173
  });
169
174
 
170
175
  const handleFilterClick = (id) => {
@@ -282,17 +287,20 @@ props.filterBuilder.onUpdate(() => {
282
287
  else {
283
288
  filterTitle.value = getFilterTitle();
284
289
  }
285
- // else if (newData.id === filter.value.id) update(newData);
286
290
  });
287
291
  </script>
288
292
 
289
293
  <style scoped>
290
294
  .filter-card {
291
- @apply bg-dark-500 border-dark-550 relative;
292
- @apply border-dark-625/30;
293
- border-width: 1px;
294
- margin-bottom: 8px;
295
- transition: all 0.15s ease-out;
295
+ @apply bg-dark-500 border border-dark-625/30 relative mb-2 transition-all duration-200;
296
+ }
297
+
298
+ .filter-card:hover {
299
+ transform: scale(1.03);
300
+ }
301
+
302
+ .filter-card:active {
303
+ transform: scale(0.98);
296
304
  }
297
305
 
298
306
  .filter-card:hover:not(.expanded-filter) {
@@ -332,59 +340,39 @@ props.filterBuilder.onUpdate(() => {
332
340
  }
333
341
 
334
342
  .filter-input {
335
- @apply border border-dark-550 rounded px-2 py-1.5 text-sm text-white focus:outline-none transition-colors;
336
- @apply bg-dark-450/90;
337
- }
338
-
339
- .filter-input:focus {
340
- border-color: oklch(0.28 0 0);
341
- @apply bg-dark-475/90;
343
+ @apply border border-dark-550 rounded px-2 py-1.5 text-sm text-white bg-dark-450/90 focus:bg-dark-475/90 focus:outline-none transition-all duration-200 placeholder:text-light-400;
342
344
  }
343
345
 
344
- .filter-input::placeholder {
345
- @apply text-light-400;
346
+ /* Filter action buttons - drag and delete */
347
+ .drag-btn,
348
+ .delete-btn {
349
+ @apply flex items-center justify-center rounded-full transition-all duration-200;
350
+ @apply w-8 h-8 flex-shrink-0 p-0;
351
+ @apply border border-dark-500 bg-dark-450;
346
352
  }
347
353
 
348
- .filter-action-btn {
349
- @apply flex items-center justify-center rounded-full transition-all duration-200 border-2 border-transparent;
350
- width: 28px;
351
- height: 28px;
352
- @apply text-light-400;
353
- background-color: rgba(35, 36, 41, 0.8);
354
- backdrop-filter: blur(4px);
355
- }
354
+ .drag-btn {
355
+ @apply text-light-500;
356
356
 
357
- @media (min-width: 640px) {
358
- .filter-action-btn {
359
- width: 32px;
360
- height: 32px;
357
+ &:hover {
358
+ @apply bg-dark-300 border-dark-550 text-yellow-400 shadow-md;
361
359
  }
362
360
  }
363
361
 
364
- .filter-action-btn:hover {
365
- color: white;
366
- transform: scale(1.15);
367
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
368
- }
369
-
370
- .drag-btn:hover {
371
- @apply bg-dark-675/80;
372
- border-color: oklch(0.28 0 0);
373
- }
362
+ .delete-btn {
363
+ @apply bg-error-500/30 border-error-400/50 text-error-300;
374
364
 
375
- .delete-btn:hover {
376
- @apply bg-error-500/80 border-error-400 text-error-400;
365
+ &:hover {
366
+ @apply bg-error-400/80 border-error-500 text-white shadow-md;
367
+ }
377
368
  }
378
369
 
379
370
  .drag-handle.sortable-chosen {
380
- border: 1px solid oklch(0.28 0 0);
381
- @apply bg-dark-675/20;
371
+ @apply border border-dark-550 bg-dark-675/20;
382
372
  }
383
373
 
384
374
  .filter-card.sortable-ghost {
385
- @apply opacity-50;
386
- border: 1px solid oklch(0.28 0 0);
387
- @apply bg-dark-675/10;
375
+ @apply opacity-50 border border-dark-550 bg-dark-675/10;
388
376
  }
389
377
 
390
378
  .filter-card.sortable-drag {
@@ -392,27 +380,26 @@ props.filterBuilder.onUpdate(() => {
392
380
  }
393
381
 
394
382
  .expanded-filter {
395
- border-left: 4px solid oklch(0.28 0 0);
396
- background-color: rgba(26, 27, 30, 0.95);
383
+ @apply border-l-4 border-l-dark-550 bg-dark-300/95;
397
384
  }
398
385
 
399
386
  .expanded-content {
400
- background-color: rgba(26, 27, 30, 0.95);
401
- border-radius: 6px;
402
- padding: 12px;
387
+ @apply rounded-md p-3 bg-dark-300/95;
403
388
  }
404
389
 
405
390
  .controls-section {
406
- background-color: rgba(26, 27, 30, 0.98);
407
- border-radius: 6px;
408
- padding: 12px;
391
+ @apply rounded-md p-3 bg-dark-350/[0.98];
409
392
  }
410
393
 
411
394
  .excluded-filter {
412
- @apply border-l-4 border-l-error-300;
395
+ @apply border-l-4 border-l-error-400 bg-error-500/30;
396
+ }
397
+
398
+ .excluded-filter .delete-btn {
399
+ @apply bg-error-400/50 border-error-500 text-error-300;
413
400
  }
414
401
 
415
402
  .normal-filter {
416
- border-left: 4px solid transparent;
403
+ @apply border-l-4 border-l-transparent;
417
404
  }
418
405
  </style>
@@ -6,9 +6,9 @@
6
6
  </template>
7
7
 
8
8
  <div class="my-3">
9
- <div class="editor-container">
10
- <div class="editor-wrapper">
11
- <pre ref="codeDisplay" class="language-json code-highlight"></pre>
9
+ <div class="relative min-h-75 max-h-125 rounded-lg overflow-hidden shadow-card bg-dark-350">
10
+ <div class="relative w-full h-full min-h-75 max-h-125">
11
+ <pre ref="codeDisplay" class="code-highlight language-json"></pre>
12
12
  <textarea
13
13
  ref="codeEditor"
14
14
  v-model="text"
@@ -19,173 +19,91 @@
19
19
  @keydown.tab.prevent="handleTab"></textarea>
20
20
  </div>
21
21
  </div>
22
- <p class="text-red-400 text-bold mt-2">{{ errorMessage }}</p>
22
+ <p class="text-red-400 font-bold mt-2">{{ errorMessage }}</p>
23
23
  </div>
24
24
 
25
25
  <div class="ml-auto flex gap-3 mt-3">
26
- <button class="modal-btn save-btn" @click="save()">Apply</button>
27
- <button class="modal-btn" @click="done()">Close</button>
26
+ <button class="btn-modal px-6" @click="save()">Apply</button>
27
+ <button class="btn-modal px-6" @click="done()">Close</button>
28
28
  </div>
29
29
  </Modal>
30
30
  </template>
31
- <style lang="scss" scoped>
32
- .modal-btn {
33
- @apply rounded transition-all duration-150 flex items-center justify-center;
34
- background: oklch(0.2046 0 0);
35
- border: 2px solid oklch(0.2809 0 0);
36
- color: oklch(0.90 0 0);
37
- height: 2.5rem;
38
- width: 6.5rem;
39
- font-size: 0.75rem;
40
- font-weight: 500;
41
-
42
- &:hover {
43
- border-color: oklch(0.72 0.15 145);
44
- }
45
-
46
- &:active, &:focus {
47
- border-color: oklch(0.72 0.15 145);
48
- outline: 1px solid oklch(0.72 0.15 145);
49
- outline-offset: 0;
50
- }
51
-
52
- &.save-btn {
53
- background-color: oklch(0.72 0.15 145 / 0.15);
54
- border-color: oklch(0.72 0.15 145 / 0.5);
55
-
56
- &:hover {
57
- background-color: oklch(0.72 0.15 145 / 0.25);
58
- border-color: oklch(0.72 0.15 145);
59
- }
60
- }
61
- }
62
-
63
- /* Prism.js syntax highlighting styles */
64
- .editor-container {
65
- position: relative;
66
- min-height: 300px;
67
- max-height: 500px;
68
- border-radius: 8px;
69
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
70
- overflow: hidden;
71
- background-color: oklch(0.19 0 0);
72
- }
73
-
74
- .editor-wrapper {
75
- position: relative;
76
- width: 100%;
77
- height: 100%;
78
- min-height: 300px;
79
- max-height: 500px;
80
- }
81
-
82
- .code-editor {
83
- width: 100%;
84
- height: 100%;
85
- min-height: 300px;
86
- max-height: 500px;
87
- background-color: transparent;
88
- /* Make text completely transparent */
89
- color: rgba(0, 0, 0, 0);
90
- caret-color: #e2e8f0;
91
- font-family: "JetBrains Mono", "Fira Code", "Menlo", "Monaco", "Courier New", monospace;
92
- padding: 12px;
93
- border: none;
94
- resize: none;
95
- font-size: 14px;
96
- line-height: 1.6;
97
- tab-size: 4;
98
- outline: none;
99
- border: 1px solid oklch(0.26 0 0);
100
- border-radius: 8px;
101
- z-index: 10;
102
- position: absolute;
103
- top: 0;
104
- left: 0;
105
- right: 0;
106
- bottom: 0;
107
- white-space: pre;
108
- overflow: auto;
109
- }
110
-
111
- .code-highlight {
112
- width: 100%;
113
- height: 100%;
114
- min-height: 300px;
115
- max-height: 500px;
116
- overflow: auto;
117
- white-space: pre;
118
- font-family: "JetBrains Mono", "Fira Code", "Menlo", "Monaco", "Courier New", monospace;
119
- font-size: 14px;
120
- line-height: 1.6;
121
- background-color: transparent !important;
122
- pointer-events: none;
123
- z-index: 5;
124
- position: absolute;
125
- top: 0;
126
- left: 0;
127
- right: 0;
128
- bottom: 0;
129
- padding: 12px;
130
- margin: 0;
131
- }
132
- </style>
133
31
  <script setup>
134
32
  import Modal from "@/components/ui/Modal.vue";
135
33
  import { FilterIcon } from "@/components/icons";
136
34
  import { useUIStore } from "@/stores/ui";
137
35
  import { ref, computed, onMounted, nextTick } from "vue";
36
+ import { DEBUG } from "@/utils/debug";
138
37
 
139
38
  const props = defineProps({
140
- filter: Object
39
+ filter: {
40
+ type: Object,
41
+ required: true
42
+ }
141
43
  });
142
44
 
143
45
  const ui = useUIStore();
144
46
  const text = ref(JSON.stringify(props.filter.out(), null, 4));
145
47
  const codeEditor = ref(null);
146
48
  const codeDisplay = ref(null);
49
+ let highlightTimer = null;
147
50
 
148
51
  // Function to highlight code using Prism
149
52
  const highlightCode = () => {
150
53
  if (!codeDisplay.value || !codeEditor.value) return;
151
54
 
152
- // Ensure Prism is available
153
- if (typeof Prism === "undefined") {
154
- console.error("Prism is not loaded");
155
- return;
156
- }
55
+ // Debounce to avoid excessive highlighting
56
+ if (highlightTimer) clearTimeout(highlightTimer);
157
57
 
158
- // Use requestAnimationFrame for smoother updates
159
- requestAnimationFrame(() => {
160
- try {
161
- // Update the pre element with highlighted HTML
162
- const highlighted = Prism.highlight(text.value || "", Prism.languages.json, "json");
163
- codeDisplay.value.innerHTML = highlighted;
164
- codeDisplay.value.className = "language-json code-highlight";
165
- } catch (e) {
166
- console.error("Highlight error:", e);
167
- // Fallback to plain text if highlighting fails
168
- codeDisplay.value.textContent = text.value || "";
169
- }
58
+ highlightTimer = setTimeout(() => {
59
+ requestAnimationFrame(() => {
60
+ try {
61
+ // Check if Prism is available
62
+ if (typeof window.Prism === "undefined" || !window.Prism.languages?.json) {
63
+ // Fallback to plain text
64
+ if (codeDisplay.value) {
65
+ codeDisplay.value.textContent = text.value || "";
66
+ codeDisplay.value.className = "language-json";
67
+ }
68
+ return;
69
+ }
170
70
 
171
- // Ensure scroll positions are synced after highlighting
172
- syncScroll();
173
- });
71
+ // Highlight the code (Prism.highlight returns sanitized HTML)
72
+ const highlighted = window.Prism.highlight(
73
+ text.value || "",
74
+ window.Prism.languages.json,
75
+ "json"
76
+ );
77
+
78
+ if (codeDisplay.value) {
79
+ codeDisplay.value.innerHTML = highlighted;
80
+ codeDisplay.value.className = "language-json code-highlight";
81
+ }
82
+ } catch (e) {
83
+ if (DEBUG) ui.logger.Error("Highlight error:", e);
84
+ // Fallback to plain text
85
+ if (codeDisplay.value) {
86
+ codeDisplay.value.textContent = text.value || "";
87
+ }
88
+ } finally {
89
+ // Always sync scroll
90
+ syncScroll();
91
+ }
92
+ });
93
+ }, 50);
174
94
  };
175
95
 
176
96
  // Function to sync scrolling between textarea and highlighted code
177
97
  const syncScroll = () => {
178
98
  if (!codeDisplay.value || !codeEditor.value) return;
179
99
 
180
- // Synchronize scrolling between the textarea and the highlighted code
181
- requestAnimationFrame(() => {
182
- codeDisplay.value.scrollTop = codeEditor.value.scrollTop;
183
- codeDisplay.value.scrollLeft = codeEditor.value.scrollLeft;
184
- });
100
+ // Direct synchronization without RAF for immediate response
101
+ codeDisplay.value.scrollTop = codeEditor.value.scrollTop;
102
+ codeDisplay.value.scrollLeft = codeEditor.value.scrollLeft;
185
103
  };
186
104
 
187
105
  // Function to handle tab key press in the editor
188
- const handleTab = (e) => {
106
+ const handleTab = () => {
189
107
  const textarea = codeEditor.value;
190
108
  const start = textarea.selectionStart;
191
109
  const end = textarea.selectionEnd;
@@ -225,10 +143,12 @@ function done() {
225
143
 
226
144
  function save() {
227
145
  const out = JSON.parse(text.value);
146
+ /* eslint-disable vue/no-mutating-props -- intentional sync from JSON to filter */
228
147
  props.filter.reset();
229
148
  props.filter.globalFilter = out.globalFilter;
230
149
  props.filter.filters = out.filters;
231
150
  props.filter.updateCss();
151
+ /* eslint-enable vue/no-mutating-props */
232
152
  done();
233
153
  }
234
154
 
@@ -241,3 +161,101 @@ const errorMessage = computed(() => {
241
161
  }
242
162
  });
243
163
  </script>
164
+
165
+ <style scoped>
166
+ /* Shared base styles for perfect alignment */
167
+ .code-editor,
168
+ .code-highlight {
169
+ position: absolute;
170
+ inset: 0;
171
+ width: 100%;
172
+ height: 100%;
173
+ min-height: 300px;
174
+ max-height: 500px;
175
+ padding: 12px;
176
+ margin: 0;
177
+ overflow: auto;
178
+ white-space: pre;
179
+ font-family: "JetBrains Mono", "Fira Code", "Menlo", "Monaco", "Courier New", monospace;
180
+ font-size: 14px;
181
+ line-height: 1.6;
182
+ tab-size: 4;
183
+ -moz-tab-size: 4;
184
+ word-wrap: normal;
185
+ word-break: normal;
186
+ border: 0;
187
+ outline: none;
188
+ background: transparent;
189
+ box-sizing: border-box;
190
+ }
191
+
192
+ /* Code editor layer - interactive */
193
+ .code-editor {
194
+ z-index: 2;
195
+ color: transparent !important;
196
+ caret-color: #ffffff;
197
+ resize: none;
198
+ }
199
+
200
+ .code-editor::selection {
201
+ background-color: oklch(0.60 0.18 250 / 0.6);
202
+ color: transparent;
203
+ }
204
+
205
+ .code-editor::-moz-selection {
206
+ background-color: oklch(0.60 0.18 250 / 0.6);
207
+ color: transparent;
208
+ }
209
+
210
+ /* Highlight layer - visual only */
211
+ .code-highlight {
212
+ z-index: 1;
213
+ pointer-events: none;
214
+ user-select: none;
215
+ -webkit-user-select: none;
216
+ -moz-user-select: none;
217
+ }
218
+
219
+ :deep(.token.property),
220
+ :deep(.token.tag),
221
+ :deep(.token.boolean),
222
+ :deep(.token.number),
223
+ :deep(.token.constant),
224
+ :deep(.token.symbol) {
225
+ color: #b5cea8;
226
+ }
227
+
228
+ :deep(.token.selector),
229
+ :deep(.token.attr-name),
230
+ :deep(.token.string),
231
+ :deep(.token.char),
232
+ :deep(.token.builtin) {
233
+ color: #ce9178;
234
+ }
235
+
236
+ :deep(.token.punctuation) {
237
+ color: #d4d4d4;
238
+ }
239
+
240
+ :deep(.token.operator),
241
+ :deep(.token.entity),
242
+ :deep(.token.url) {
243
+ color: #d4d4d4;
244
+ }
245
+
246
+ :deep(.token.atrule),
247
+ :deep(.token.attr-value),
248
+ :deep(.token.keyword) {
249
+ color: #c586c0;
250
+ }
251
+
252
+ :deep(.token.function),
253
+ :deep(.token.class-name) {
254
+ color: #dcdcaa;
255
+ }
256
+
257
+ :deep(.token.comment) {
258
+ color: #6a9955;
259
+ font-style: italic;
260
+ }
261
+ </style>