@innertia-solutions/innertia-nuxt 0.1.1

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 (108) hide show
  1. package/.github/workflows/auto-publish.yml +64 -0
  2. package/.github/workflows/release.yml +59 -0
  3. package/README.md +60 -0
  4. package/app.config.ts +70 -0
  5. package/components/Admin/Base.vue +144 -0
  6. package/components/Admin/Header.vue +32 -0
  7. package/components/Admin/Page.vue +65 -0
  8. package/components/Admin/PageHeader.vue +31 -0
  9. package/components/App/Button.vue +59 -0
  10. package/components/App/DevEnvironmentBar.vue +43 -0
  11. package/components/App/Dropdown.vue +286 -0
  12. package/components/App/EmptyState.vue +433 -0
  13. package/components/App/LoadingState.vue +40 -0
  14. package/components/App/PageLoadingSpinner.vue +118 -0
  15. package/components/App/PreviewDock.vue +64 -0
  16. package/components/App/SwitchColorTheme.vue +51 -0
  17. package/components/App/Tag.vue +193 -0
  18. package/components/DataTable.vue +713 -0
  19. package/components/Forms/DatePicker.vue +255 -0
  20. package/components/Forms/Input.vue +75 -0
  21. package/components/Forms/Select.vue +100 -0
  22. package/components/Forms/SelectServer.vue +726 -0
  23. package/components/Layout/Admin.vue +32 -0
  24. package/components/Layout/Auth.vue +29 -0
  25. package/components/Layout/SidebarWithAppColumn.vue +388 -0
  26. package/components/Layout/TopBar.vue +113 -0
  27. package/components/MobileBlocker.vue +85 -0
  28. package/components/MobileLoginPicker.vue +83 -0
  29. package/components/Modal/Base.vue +29 -0
  30. package/components/Modal/DeleteConfirm.vue +48 -0
  31. package/components/Modal.vue +103 -0
  32. package/components/Nav/Tabs.vue +55 -0
  33. package/components/PermissionsTree.vue +272 -0
  34. package/components/Table/Database.vue +183 -0
  35. package/components/Table/DownloadDropdown.vue +111 -0
  36. package/components/Table/Enterprise.vue +540 -0
  37. package/components/Table/FilterDropdown.vue +226 -0
  38. package/components/Table/Grid.vue +62 -0
  39. package/components/Table/Kanban.vue +188 -0
  40. package/components/Table/List.vue +128 -0
  41. package/components/Table/PreviewTimeline.vue +118 -0
  42. package/components/Table/Standard.vue +1217 -0
  43. package/components/Table/index.vue +974 -0
  44. package/components/TableExportable.vue +172 -0
  45. package/components/TableFilter.vue +93 -0
  46. package/components/Toast/Alert.vue +113 -0
  47. package/components/Toast/Container.vue +34 -0
  48. package/components/Toast/Notification.vue +45 -0
  49. package/components/Toast/Process.vue +88 -0
  50. package/composables/useApi.js +95 -0
  51. package/composables/useApp.ts +46 -0
  52. package/composables/useAuth.js +82 -0
  53. package/composables/useContext.js +44 -0
  54. package/composables/useDate.js +241 -0
  55. package/composables/useDevice.js +21 -0
  56. package/composables/useDockedPreviews.js +56 -0
  57. package/composables/useDownload.js +87 -0
  58. package/composables/useEntity.js +82 -0
  59. package/composables/useForm.js +119 -0
  60. package/composables/useInnertiaMode.ts +25 -0
  61. package/composables/useMobileGuard.ts +81 -0
  62. package/composables/useNotifications.js +22 -0
  63. package/composables/usePermissions.js +23 -0
  64. package/composables/useRealtime.js +123 -0
  65. package/composables/useRequestInterceptors.js +27 -0
  66. package/composables/useRoles.js +53 -0
  67. package/composables/useRutFormatter.js +39 -0
  68. package/composables/useTable.ts +94 -0
  69. package/composables/useTablePreferences.ts +33 -0
  70. package/composables/useTenant.js +27 -0
  71. package/composables/useTimeAgo.js +37 -0
  72. package/composables/useToast.js +69 -0
  73. package/composables/useUserRealtime.js +17 -0
  74. package/composables/useUsers.js +111 -0
  75. package/css/themes/autumn.css +401 -0
  76. package/css/themes/bubblegum.css +408 -0
  77. package/css/themes/cashmere.css +412 -0
  78. package/css/themes/harvest.css +416 -0
  79. package/css/themes/moon.css +140 -0
  80. package/css/themes/ocean.css +273 -0
  81. package/css/themes/olive.css +413 -0
  82. package/css/themes/retro.css +431 -0
  83. package/css/themes/theme.css +725 -0
  84. package/error.vue +78 -0
  85. package/middleware/01.detect-subdomain.global.ts +43 -0
  86. package/middleware/02.validate-tenant.global.ts +67 -0
  87. package/middleware/03.apps.global.ts +88 -0
  88. package/middleware/auth.ts +9 -0
  89. package/middleware/guest.ts +9 -0
  90. package/nuxt.config.ts +42 -0
  91. package/package.json +60 -0
  92. package/pages/tenant-error.vue +50 -0
  93. package/plugins/api-auth.ts +12 -0
  94. package/plugins/api-tenant.client.ts +21 -0
  95. package/plugins/appearance.ts +8 -0
  96. package/plugins/auth-init.ts +34 -0
  97. package/plugins/dark-state.client.ts +29 -0
  98. package/plugins/dockedPreviewsSync.client.js +17 -0
  99. package/plugins/preline.client.ts +68 -0
  100. package/plugins/theme.client.ts +7 -0
  101. package/plugins/vue-query.ts +29 -0
  102. package/public/init-theme.js +15 -0
  103. package/spark.css +721 -0
  104. package/stores/auth.js +130 -0
  105. package/stores/dockedPreviews.js +34 -0
  106. package/stores/notifications.js +24 -0
  107. package/stores/tenant.js +54 -0
  108. package/stores/toast.js +129 -0
@@ -0,0 +1,433 @@
1
+ <script setup>
2
+ defineProps({
3
+ title: { type: String, default: "No hay contenido" },
4
+ description: { type: String, default: "Aún no hay datos que mostrar." },
5
+ actionText: { type: String, default: "" },
6
+ actionLink: { type: String, default: "" },
7
+ showAction: { type: Boolean, default: false },
8
+ shape: {
9
+ type: String,
10
+ default: "square",
11
+ validator: (value) => ["square", "circle"].includes(value),
12
+ },
13
+ });
14
+ </script>
15
+ <template>
16
+ <div>
17
+ <slot v-if="shape == 'square'" name="icon">
18
+ <!-- Ícono por defecto (tu SVG) -->
19
+ <svg
20
+ class="w-48 mx-auto mb-4"
21
+ width="178"
22
+ height="90"
23
+ viewBox="0 0 178 90"
24
+ fill="none"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ >
27
+ <rect
28
+ x="27"
29
+ y="50.5"
30
+ width="124"
31
+ height="39"
32
+ rx="7.5"
33
+ fill="currentColor"
34
+ class="fill-white dark:fill-slate-800"
35
+ />
36
+ <rect
37
+ x="27"
38
+ y="50.5"
39
+ width="124"
40
+ height="39"
41
+ rx="7.5"
42
+ stroke="currentColor"
43
+ class="stroke-slate-50 dark:stroke-slate-700/10"
44
+ />
45
+ <rect
46
+ x="34.5"
47
+ y="58"
48
+ width="24"
49
+ height="24"
50
+ rx="4"
51
+ fill="currentColor"
52
+ class="fill-slate-50 dark:fill-slate-700/30"
53
+ />
54
+ <rect
55
+ x="66.5"
56
+ y="61"
57
+ width="60"
58
+ height="6"
59
+ rx="3"
60
+ fill="currentColor"
61
+ class="fill-slate-50 dark:fill-slate-700/30"
62
+ />
63
+ <rect
64
+ x="66.5"
65
+ y="73"
66
+ width="77"
67
+ height="6"
68
+ rx="3"
69
+ fill="currentColor"
70
+ class="fill-slate-50 dark:fill-slate-700/30"
71
+ />
72
+ <rect
73
+ x="19.5"
74
+ y="28.5"
75
+ width="139"
76
+ height="39"
77
+ rx="7.5"
78
+ fill="currentColor"
79
+ class="fill-white dark:fill-slate-800"
80
+ />
81
+ <rect
82
+ x="19.5"
83
+ y="28.5"
84
+ width="139"
85
+ height="39"
86
+ rx="7.5"
87
+ stroke="currentColor"
88
+ class="stroke-slate-100 dark:stroke-slate-700/30"
89
+ />
90
+ <rect
91
+ x="27"
92
+ y="36"
93
+ width="24"
94
+ height="24"
95
+ rx="4"
96
+ fill="currentColor"
97
+ class="fill-slate-100 dark:fill-slate-700/70"
98
+ />
99
+ <rect
100
+ x="59"
101
+ y="39"
102
+ width="60"
103
+ height="6"
104
+ rx="3"
105
+ fill="currentColor"
106
+ class="fill-slate-100 dark:fill-slate-700/70"
107
+ />
108
+ <rect
109
+ x="59"
110
+ y="51"
111
+ width="92"
112
+ height="6"
113
+ rx="3"
114
+ fill="currentColor"
115
+ class="fill-slate-100 dark:fill-slate-700/70"
116
+ />
117
+ <g filter="url(#filter15)">
118
+ <rect
119
+ x="12"
120
+ y="6"
121
+ width="154"
122
+ height="40"
123
+ rx="8"
124
+ fill="currentColor"
125
+ class="fill-white dark:fill-slate-800"
126
+ shape-rendering="crispEdges"
127
+ />
128
+ <rect
129
+ x="12.5"
130
+ y="6.5"
131
+ width="153"
132
+ height="39"
133
+ rx="7.5"
134
+ stroke="currentColor"
135
+ class="stroke-slate-100 dark:stroke-slate-700/60"
136
+ shape-rendering="crispEdges"
137
+ />
138
+ <rect
139
+ x="20"
140
+ y="14"
141
+ width="24"
142
+ height="24"
143
+ rx="4"
144
+ fill="currentColor"
145
+ class="fill-slate-200 dark:fill-slate-700"
146
+ />
147
+ <rect
148
+ x="52"
149
+ y="17"
150
+ width="60"
151
+ height="6"
152
+ rx="3"
153
+ fill="currentColor"
154
+ class="fill-slate-200 dark:fill-slate-700"
155
+ />
156
+ <rect
157
+ x="52"
158
+ y="29"
159
+ width="106"
160
+ height="6"
161
+ rx="3"
162
+ fill="currentColor"
163
+ class="fill-slate-200 dark:fill-slate-700"
164
+ />
165
+ </g>
166
+ <defs>
167
+ <filter
168
+ id="filter15"
169
+ x="0"
170
+ y="0"
171
+ width="178"
172
+ height="64"
173
+ filterUnits="userSpaceOnUse"
174
+ color-interpolation-filters="sRGB"
175
+ >
176
+ <feFlood flood-opacity="0" result="BackgroundImageFix" />
177
+ <feColorMatrix
178
+ in="SourceAlpha"
179
+ type="matrix"
180
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
181
+ result="hardAlpha"
182
+ />
183
+ <feOffset dy="6" />
184
+ <feGaussianBlur stdDeviation="6" />
185
+ <feComposite in2="hardAlpha" operator="out" />
186
+ <feColorMatrix
187
+ type="matrix"
188
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0"
189
+ />
190
+ <feBlend
191
+ mode="normal"
192
+ in2="BackgroundImageFix"
193
+ result="effect1_dropShadow_1187_14810"
194
+ />
195
+ <feBlend
196
+ mode="normal"
197
+ in="SourceGraphic"
198
+ in2="effect1_dropShadow_1187_14810"
199
+ result="shape"
200
+ />
201
+ </filter>
202
+ </defs>
203
+ </svg>
204
+ </slot>
205
+ <slot v-else name="icon">
206
+ <svg
207
+ class="w-48 mx-auto mb-4"
208
+ width="178"
209
+ height="90"
210
+ viewBox="0 0 178 90"
211
+ fill="none"
212
+ xmlns="http://www.w3.org/2000/svg"
213
+ >
214
+ <rect
215
+ x="27"
216
+ y="50.5"
217
+ width="124"
218
+ height="39"
219
+ rx="7.5"
220
+ fill="currentColor"
221
+ class="fill-white dark:fill-slate-800"
222
+ />
223
+ <rect
224
+ x="27"
225
+ y="50.5"
226
+ width="124"
227
+ height="39"
228
+ rx="7.5"
229
+ stroke="currentColor"
230
+ class="stroke-slate-50 dark:stroke-slate-700/10"
231
+ />
232
+ <rect
233
+ x="34.5"
234
+ y="58"
235
+ width="24"
236
+ height="24"
237
+ rx="12"
238
+ fill="currentColor"
239
+ class="fill-slate-50 dark:fill-slate-700/30"
240
+ />
241
+ <rect
242
+ x="66.5"
243
+ y="61"
244
+ width="60"
245
+ height="6"
246
+ rx="3"
247
+ fill="currentColor"
248
+ class="fill-slate-50 dark:fill-slate-700/30"
249
+ />
250
+ <rect
251
+ x="66.5"
252
+ y="73"
253
+ width="77"
254
+ height="6"
255
+ rx="3"
256
+ fill="currentColor"
257
+ class="fill-slate-50 dark:fill-slate-700/30"
258
+ />
259
+ <rect
260
+ x="19.5"
261
+ y="28.5"
262
+ width="139"
263
+ height="39"
264
+ rx="7.5"
265
+ fill="currentColor"
266
+ class="fill-white dark:fill-slate-800"
267
+ />
268
+ <rect
269
+ x="19.5"
270
+ y="28.5"
271
+ width="139"
272
+ height="39"
273
+ rx="7.5"
274
+ stroke="currentColor"
275
+ class="stroke-slate-100 dark:stroke-slate-700/30"
276
+ />
277
+ <rect
278
+ x="27"
279
+ y="36"
280
+ width="24"
281
+ height="24"
282
+ rx="12"
283
+ fill="currentColor"
284
+ class="fill-slate-100 dark:fill-slate-700/70"
285
+ />
286
+ <rect
287
+ x="59"
288
+ y="39"
289
+ width="60"
290
+ height="6"
291
+ rx="3"
292
+ fill="currentColor"
293
+ class="fill-slate-100 dark:fill-slate-700/70"
294
+ />
295
+ <rect
296
+ x="59"
297
+ y="51"
298
+ width="92"
299
+ height="6"
300
+ rx="3"
301
+ fill="currentColor"
302
+ class="fill-slate-100 dark:fill-slate-700/70"
303
+ />
304
+ <g filter="url(#filter4)">
305
+ <rect
306
+ x="12"
307
+ y="6"
308
+ width="154"
309
+ height="40"
310
+ rx="8"
311
+ class="fill-white dark:fill-slate-800"
312
+ shape-rendering="crispEdges"
313
+ />
314
+ <rect
315
+ x="12.5"
316
+ y="6.5"
317
+ width="153"
318
+ height="39"
319
+ rx="7.5"
320
+ stroke="currentColor"
321
+ class="stroke-slate-100 dark:stroke-slate-700/60"
322
+ shape-rendering="crispEdges"
323
+ />
324
+ <rect
325
+ x="20"
326
+ y="14"
327
+ width="24"
328
+ height="24"
329
+ rx="12"
330
+ fill="currentColor"
331
+ class="fill-slate-200 dark:fill-slate-700"
332
+ />
333
+ <rect
334
+ x="52"
335
+ y="17"
336
+ width="60"
337
+ height="6"
338
+ rx="3"
339
+ fill="currentColor"
340
+ class="fill-slate-200 dark:fill-slate-700"
341
+ />
342
+ <rect
343
+ x="52"
344
+ y="29"
345
+ width="106"
346
+ height="6"
347
+ rx="3"
348
+ fill="currentColor"
349
+ class="fill-slate-200 dark:fill-slate-700"
350
+ />
351
+ </g>
352
+ <defs>
353
+ <filter
354
+ id="filter4"
355
+ x="0"
356
+ y="0"
357
+ width="178"
358
+ height="64"
359
+ filterUnits="userSpaceOnUse"
360
+ color-interpolation-filters="sRGB"
361
+ >
362
+ <feFlood flood-opacity="0" result="BackgroundImageFix" />
363
+ <feColorMatrix
364
+ in="SourceAlpha"
365
+ type="matrix"
366
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
367
+ result="hardAlpha"
368
+ />
369
+ <feOffset dy="6" />
370
+ <feGaussianBlur stdDeviation="6" />
371
+ <feComposite in2="hardAlpha" operator="out" />
372
+ <feColorMatrix
373
+ type="matrix"
374
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0"
375
+ />
376
+ <feBlend
377
+ mode="normal"
378
+ in2="BackgroundImageFix"
379
+ result="effect1_dropShadow_1187_14810"
380
+ />
381
+ <feBlend
382
+ mode="normal"
383
+ in="SourceGraphic"
384
+ in2="effect1_dropShadow_1187_14810"
385
+ result="shape"
386
+ />
387
+ </filter>
388
+ </defs>
389
+ </svg>
390
+ </slot>
391
+
392
+ <div class="max-w-sm mx-auto">
393
+ <p class="mt-2 font-medium text-foreground">
394
+ {{ title }}
395
+ </p>
396
+ <p class="mb-5 text-sm text-muted-foreground text-wrap">
397
+ {{ description }}
398
+ </p>
399
+ </div>
400
+
401
+ <a
402
+ v-if="showAction"
403
+ :href="actionLink"
404
+ class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-card-line bg-card text-foreground shadow-2xs hover:bg-muted-hover focus:outline-hidden focus:bg-muted-hover"
405
+ >
406
+ {{ actionText }}
407
+ </a>
408
+
409
+ <template name="footer">
410
+ <button
411
+ type="button"
412
+ class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:ring-2 focus:ring-blue-500"
413
+ data-hs-overlay="#hs-pro-empty"
414
+ >
415
+ <svg
416
+ class="hidden sm:block shrink-0 size-4"
417
+ xmlns="http://www.w3.org/2000/svg"
418
+ width="24"
419
+ height="24"
420
+ viewBox="0 0 24 24"
421
+ fill="none"
422
+ stroke="currentColor"
423
+ stroke-width="2"
424
+ stroke-linecap="round"
425
+ stroke-linejoin="round"
426
+ >
427
+ <path d="M5 12h14" />
428
+ <path d="M12 5v14" /></svg
429
+ >Add user
430
+ </button>
431
+ </template>
432
+ </div>
433
+ </template>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div
3
+ class="flex flex-col items-center justify-center py-10 px-4 space-y-6 w-full h-full text-center"
4
+ >
5
+ <!-- Ícono/loader -->
6
+ <svg
7
+ class="animate-spin w-10 h-10 text-muted-foreground-2"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ stroke-width="2"
13
+ stroke-linecap="round"
14
+ stroke-linejoin="round"
15
+ >
16
+ <circle cx="12" cy="12" r="10" opacity=".25" />
17
+ <path d="M22 12a10 10 0 0 1-10 10" />
18
+ </svg>
19
+
20
+ <!-- Skeleton líneas -->
21
+ <div class="w-full max-w-xs space-y-3">
22
+ <div
23
+ class="h-4 bg-surface-1 rounded animate-pulse"
24
+ ></div>
25
+ <div
26
+ class="h-4 bg-surface-1 rounded animate-pulse"
27
+ ></div>
28
+ <div
29
+ v-if="showHint"
30
+ class="h-4 w-1/2 bg-surface-1 rounded animate-pulse mx-auto"
31
+ ></div>
32
+ </div>
33
+ </div>
34
+ </template>
35
+
36
+ <script setup>
37
+ defineProps({
38
+ showHint: { type: Boolean, default: false },
39
+ });
40
+ </script>
@@ -0,0 +1,118 @@
1
+ <script setup>
2
+ const isLoading = ref(false);
3
+ let hideTimer;
4
+ let cleanup = [];
5
+
6
+ const showLoader = () => {
7
+ clearTimeout(hideTimer);
8
+ isLoading.value = true;
9
+ };
10
+
11
+ const hideLoader = () => {
12
+ clearTimeout(hideTimer);
13
+ hideTimer = setTimeout(() => {
14
+ isLoading.value = false;
15
+ }, 120);
16
+ };
17
+
18
+ onMounted(() => {
19
+ const nuxtApp = useNuxtApp();
20
+ const router = useRouter();
21
+
22
+ cleanup = [
23
+ nuxtApp.hooks.hook("page:start", showLoader),
24
+ nuxtApp.hooks.hook("page:finish", hideLoader),
25
+ nuxtApp.hooks.hook("app:error", hideLoader),
26
+ router.beforeEach((to, from) => {
27
+ if (to.fullPath !== from.fullPath) {
28
+ showLoader();
29
+ }
30
+ }),
31
+ router.afterEach(hideLoader),
32
+ router.onError(hideLoader),
33
+ ].filter(Boolean);
34
+ });
35
+
36
+ onBeforeUnmount(() => {
37
+ clearTimeout(hideTimer);
38
+ cleanup.forEach((unregister) => unregister());
39
+ });
40
+ </script>
41
+
42
+ <template>
43
+ <ClientOnly>
44
+ <Transition name="page-loading-spinner">
45
+ <div
46
+ v-if="isLoading"
47
+ class="page-loading-spinner"
48
+ aria-label="Cargando pagina"
49
+ role="status"
50
+ >
51
+ <span class="page-loading-spinner__ring"></span>
52
+ </div>
53
+ </Transition>
54
+ </ClientOnly>
55
+ </template>
56
+
57
+ <style scoped>
58
+ .page-loading-spinner {
59
+ position: fixed;
60
+ top: 0.625rem;
61
+ right: 0.5rem;
62
+ z-index: 2147483647;
63
+ display: flex;
64
+ width: 2.5rem;
65
+ height: 2.5rem;
66
+ align-items: center;
67
+ justify-content: center;
68
+ border: 1px solid rgb(226 232 240 / 0.9);
69
+ border-radius: 9999px;
70
+ background: rgb(255 255 255 / 0.88);
71
+ box-shadow: 0 10px 30px rgb(15 23 42 / 0.16);
72
+ backdrop-filter: blur(10px);
73
+ pointer-events: none;
74
+ }
75
+
76
+ .page-loading-spinner__ring {
77
+ width: 1.25rem;
78
+ height: 1.25rem;
79
+ border: 2px solid rgb(148 163 184 / 0.38);
80
+ border-top-color: rgb(20 184 166);
81
+ border-radius: 9999px;
82
+ animation: page-loading-spinner-rotate 0.75s linear infinite;
83
+ }
84
+
85
+ .page-loading-spinner-enter-active,
86
+ .page-loading-spinner-leave-active {
87
+ transition: opacity 0.15s ease, transform 0.15s ease;
88
+ }
89
+
90
+ .page-loading-spinner-enter-from,
91
+ .page-loading-spinner-leave-to {
92
+ opacity: 0;
93
+ transform: scale(0.92);
94
+ }
95
+
96
+ @media (min-width: 640px) {
97
+ .page-loading-spinner {
98
+ right: 1.25rem;
99
+ }
100
+ }
101
+
102
+ :global(.dark) .page-loading-spinner {
103
+ border-color: rgb(51 65 85 / 0.85);
104
+ background: rgb(15 23 42 / 0.78);
105
+ box-shadow: 0 10px 30px rgb(0 0 0 / 0.3);
106
+ }
107
+
108
+ :global(.dark) .page-loading-spinner__ring {
109
+ border-color: rgb(100 116 139 / 0.42);
110
+ border-top-color: rgb(45 212 191);
111
+ }
112
+
113
+ @keyframes page-loading-spinner-rotate {
114
+ to {
115
+ transform: rotate(360deg);
116
+ }
117
+ }
118
+ </style>
@@ -0,0 +1,64 @@
1
+ <script setup>
2
+ import { IconX } from '@tabler/icons-vue'
3
+
4
+ const { docked, undock, expandDock, activeDockId } = useDockedPreviews()
5
+ const router = useRouter()
6
+ const route = useRoute()
7
+
8
+ async function open(item, event) {
9
+ // Misma ruta → la tabla está montada, mostrar float encima del tab
10
+ if (route.path === item.route) {
11
+ const rect = event.currentTarget.getBoundingClientRect()
12
+ expandDock(item.id, rect)
13
+ return
14
+ }
15
+ // Ruta diferente → navegar y restaurar como preview completo
16
+ await router.push(item.route)
17
+ await nextTick()
18
+ useNuxtApp().hooks.callHook('preview:restore', item)
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <Transition
24
+ enter-active-class="transition ease-out duration-200"
25
+ enter-from-class="opacity-0 translate-y-4"
26
+ enter-to-class="opacity-100 translate-y-0"
27
+ leave-active-class="transition ease-in duration-150"
28
+ leave-from-class="opacity-100 translate-y-0"
29
+ leave-to-class="opacity-0 translate-y-4"
30
+ >
31
+ <div
32
+ v-if="docked.length"
33
+ class="fixed bottom-0 left-0 right-0 z-50 flex items-center gap-2 px-4 py-2 bg-card/95 backdrop-blur-md border-t border-card-line shadow-lg"
34
+ >
35
+ <span class="text-xs text-muted-foreground shrink-0 mr-1">Minimizados</span>
36
+
37
+ <div class="flex items-center gap-2 flex-1 overflow-x-auto">
38
+ <button
39
+ v-for="item in docked"
40
+ :key="item.id"
41
+ type="button"
42
+ class="group inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-sm transition-all shrink-0"
43
+ :class="activeDockId === item.id
44
+ ? 'border-primary/50 bg-primary/10 text-primary shadow-sm'
45
+ : 'border-card-line bg-surface hover:bg-muted-hover text-foreground'"
46
+ @click="open(item, $event)"
47
+ >
48
+ <span class="size-5 rounded-full bg-primary flex items-center justify-center text-[10px] font-bold text-primary-foreground shrink-0">
49
+ {{ (item.label?.[0] ?? '?').toUpperCase() }}
50
+ </span>
51
+ <span class="font-medium max-w-32 truncate">{{ item.label }}</span>
52
+ <span v-if="item.subtitle" class="text-muted-foreground text-xs max-w-28 truncate hidden sm:inline">{{ item.subtitle }}</span>
53
+
54
+ <span
55
+ class="size-4 inline-flex items-center justify-center rounded hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 transition-colors ml-0.5"
56
+ @click.stop="undock(item.id)"
57
+ >
58
+ <IconX class="size-3" />
59
+ </span>
60
+ </button>
61
+ </div>
62
+ </div>
63
+ </Transition>
64
+ </template>
@@ -0,0 +1,51 @@
1
+ <script setup>
2
+ const isDark = ref(false)
3
+
4
+ onMounted(() => {
5
+ isDark.value = document.documentElement.classList.contains('dark')
6
+ })
7
+
8
+ async function setTheme(value) {
9
+ const dark = value === 'dark'
10
+ isDark.value = dark
11
+ document.documentElement.classList.toggle('dark', dark)
12
+ localStorage.setItem('hs_theme', value)
13
+ document.cookie = `hs_theme=${value};path=/;max-age=${60 * 60 * 24 * 365};SameSite=Lax`
14
+
15
+ try {
16
+ const api = useApi()
17
+ await api.put('auth/me/preferences/appearance', { value })
18
+ } catch { /* best-effort */ }
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <button
24
+ v-if="isDark"
25
+ type="button"
26
+ class="font-medium text-slate-800 rounded-full hover:bg-surface focus:outline-hidden focus:bg-surface dark:text-foreground dark:hover:bg-card dark:focus:bg-card"
27
+ @click="setTheme('light')"
28
+ >
29
+ <span class="group inline-flex shrink-0 justify-center items-center size-9">
30
+ <svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+ <circle cx="12" cy="12" r="4"></circle>
32
+ <path d="M12 2v2"></path><path d="M12 20v2"></path>
33
+ <path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path>
34
+ <path d="M2 12h2"></path><path d="M20 12h2"></path>
35
+ <path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path>
36
+ </svg>
37
+ </span>
38
+ </button>
39
+ <button
40
+ v-else
41
+ type="button"
42
+ class="font-medium text-slate-400 rounded-full hover:bg-surface focus:outline-hidden focus:bg-surface dark:text-foreground dark:hover:bg-card dark:focus:bg-card"
43
+ @click="setTheme('dark')"
44
+ >
45
+ <span class="group inline-flex shrink-0 justify-center items-center size-9">
46
+ <svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
48
+ </svg>
49
+ </span>
50
+ </button>
51
+ </template>