@innertia-solutions/innertia-nuxt 0.1.10 → 0.1.12

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 (37) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/app.config.ts +10 -10
  3. package/components/Auth/LoginForm.vue +1 -1
  4. package/components/DataPreview.vue +323 -0
  5. package/components/DataTable.vue +12 -12
  6. package/components/Forms/DatePicker.vue +64 -39
  7. package/components/Forms/Input.vue +8 -2
  8. package/components/Forms/Select.vue +56 -20
  9. package/components/Forms/SelectServer.vue +39 -34
  10. package/components/MobileBlocker.vue +5 -5
  11. package/components/MobileLoginPicker.vue +13 -13
  12. package/components/Organization/Picker.vue +3 -3
  13. package/components/PermissionsTree.vue +4 -4
  14. package/components/Table/Database.vue +5 -5
  15. package/components/Table/DownloadDropdown.vue +8 -8
  16. package/components/Table/Enterprise.vue +9 -9
  17. package/components/Table/FilterDropdown.vue +3 -3
  18. package/components/Table/Grid.vue +4 -4
  19. package/components/Table/Kanban.vue +3 -3
  20. package/components/Table/List.vue +6 -6
  21. package/components/Table/Standard.vue +69 -324
  22. package/components/Table/index.vue +13 -13
  23. package/components/TableExportable.vue +6 -6
  24. package/components/TableFilter.vue +4 -4
  25. package/components/Tree/Node.vue +187 -84
  26. package/components/Tree/Standard.vue +345 -57
  27. package/composables/useAuth.js +1 -1
  28. package/composables/useContext.js +53 -5
  29. package/composables/useMobileGuard.ts +21 -21
  30. package/composables/useOrganization.ts +3 -3
  31. package/css/tokens.css +90 -0
  32. package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
  33. package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
  34. package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
  35. package/package.json +1 -1
  36. package/theme.css +2 -0
  37. package/composables/useApp.ts +0 -46
@@ -180,76 +180,101 @@ const clear = () => {
180
180
  <template>
181
181
  <div class="relative w-full">
182
182
  <div class="relative group">
183
- <div class="absolute inset-y-0 start-0 flex items-center pointer-events-none z-20 ps-3.5">
184
- <IconCalendar class="size-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
185
- </div>
186
-
187
183
  <input
188
184
  ref="inputEl"
189
185
  type="text"
190
186
  :value="internalValue"
191
187
  :disabled="disabled"
192
188
  :placeholder="placeholder"
193
- class="w-full py-2 ps-10 pe-10 border border-gray-300 rounded-lg text-sm dark:bg-card dark:border-card-line dark:text-white disabled:opacity-50 disabled:pointer-events-none cursor-pointer focus:border-blue-500 focus:ring-blue-500/20 outline-none transition-all block"
189
+ class="innertia-field pe-10 cursor-pointer"
194
190
  readonly
195
191
  />
196
192
 
197
- <!-- Clear button -->
193
+ <!-- Clear button (cuando hay valor) o ícono calendario (cuando está vacío) — ambos a la derecha -->
198
194
  <button
199
195
  v-if="clearable && internalValue && !disabled"
200
196
  @click.stop="clear"
201
197
  type="button"
202
- class="absolute inset-y-0 end-0 flex items-center z-20 pe-3 text-gray-400 hover:text-red-500 transition-colors focus:outline-none"
198
+ class="absolute inset-y-0 end-0 flex items-center z-20 pe-3 text-muted-foreground hover:text-destructive transition-colors focus:outline-none"
203
199
  >
204
200
  <IconX class="size-4" />
205
201
  </button>
202
+ <div
203
+ v-else
204
+ class="absolute inset-y-0 end-0 flex items-center pointer-events-none z-20 pe-3"
205
+ >
206
+ <IconCalendar class="size-4 text-muted-foreground-2 group-focus-within:text-primary transition-colors" />
207
+ </div>
206
208
  </div>
207
209
  </div>
208
210
  </template>
209
211
 
210
212
  <style>
211
213
  /*
212
- Vanilla Calendar v3 compiles Tailwind classes directly into its themes.
213
- We override the selected date and hover backgrounds for Light and Dark modes.
214
- */
215
-
216
- /* LIGHT MODE OVERRIDES */
217
- [data-vc-theme=light] .vc-months__month[data-vc-months-month-selected],
218
- [data-vc-theme=light] .vc-years__year[data-vc-years-year-selected],
219
- [data-vc-theme=light] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn,
220
- [data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn {
221
- background-color: #2563eb !important; /* blue-600 */
222
- color: #ffffff !important;
214
+ * Vanilla Calendar v3 compila Tailwind directo en sus themes. Overridemos
215
+ * solo los acentos del brand color para que sigan los tokens del DS.
216
+ *
217
+ * Light y dark usan las mismas variables — `--color-primary` ya cambia
218
+ * automáticamente entre claro/oscuro a través de la cascada del tema.
219
+ *
220
+ * Las clases de selected / today / hover aplican tanto en
221
+ * `[data-vc-theme=light]` como `[data-vc-theme=dark]`.
222
+ */
223
+
224
+ /* Día seleccionado / mes seleccionado / año seleccionado */
225
+ .vc-months__month[data-vc-months-month-selected],
226
+ .vc-years__year[data-vc-years-year-selected],
227
+ .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn,
228
+ .vc-date[data-vc-date-selected] .vc-date__btn {
229
+ background-color: var(--color-primary) !important;
230
+ color: var(--color-primary-foreground) !important;
231
+ border-radius: var(--radius-control) !important;
223
232
  }
224
233
 
225
- [data-vc-theme=light] .vc-months__month[data-vc-months-month-selected]:hover,
226
- [data-vc-theme=light] .vc-years__year[data-vc-years-year-selected]:hover,
227
- [data-vc-theme=light] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn:hover,
228
- [data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn:hover {
229
- background-color: #1d4ed8 !important; /* blue-700 */
234
+ .vc-months__month[data-vc-months-month-selected]:hover,
235
+ .vc-years__year[data-vc-years-year-selected]:hover,
236
+ .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn:hover,
237
+ .vc-date[data-vc-date-selected] .vc-date__btn:hover {
238
+ background-color: var(--color-primary-hover) !important;
230
239
  }
231
240
 
232
- [data-vc-theme=light] .vc-date[data-vc-date-today] .vc-date__btn {
233
- color: #2563eb !important;
241
+ /* Hoy (sin seleccionar): color del texto en brand */
242
+ .vc-date[data-vc-date-today] .vc-date__btn {
243
+ color: var(--color-primary) !important;
244
+ font-weight: 600;
234
245
  }
235
246
 
236
- /* DARK MODE OVERRIDES */
237
- [data-vc-theme=dark] .vc-months__month[data-vc-months-month-selected],
238
- [data-vc-theme=dark] .vc-years__year[data-vc-years-year-selected],
239
- [data-vc-theme=dark] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn,
240
- [data-vc-theme=dark] .vc-date[data-vc-date-selected] .vc-date__btn {
241
- background-color: #3b82f6 !important; /* blue-500 */
242
- color: #ffffff !important;
247
+ /* Hover de cualquier día (no seleccionado) */
248
+ .vc-date:not([data-vc-date-selected]) .vc-date__btn:hover,
249
+ .vc-months__month:not([data-vc-months-month-selected]):hover,
250
+ .vc-years__year:not([data-vc-years-year-selected]):hover {
251
+ background-color: var(--color-muted-hover) !important;
252
+ border-radius: var(--radius-control) !important;
253
+ }
254
+
255
+ /* Días del mes anterior / siguiente — más opacos */
256
+ .vc-date[data-vc-date-month=prev] .vc-date__btn,
257
+ .vc-date[data-vc-date-month=next] .vc-date__btn {
258
+ color: var(--color-muted-foreground-2) !important;
259
+ }
260
+
261
+ /* Botones de navegación (mes anterior / siguiente) */
262
+ .vc-arrow {
263
+ color: var(--color-foreground) !important;
264
+ }
265
+ .vc-arrow:hover {
266
+ background-color: var(--color-muted-hover) !important;
267
+ border-radius: var(--radius-control) !important;
243
268
  }
244
269
 
245
- [data-vc-theme=dark] .vc-months__month[data-vc-months-month-selected]:hover,
246
- [data-vc-theme=dark] .vc-years__year[data-vc-years-year-selected]:hover,
247
- [data-vc-theme=dark] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn:hover,
248
- [data-vc-theme=dark] .vc-date[data-vc-date-selected] .vc-date__btn:hover {
249
- background-color: #2563eb !important; /* blue-600 */
270
+ /* Header del mes/año en el centro */
271
+ .vc-month,
272
+ .vc-year {
273
+ color: var(--color-foreground) !important;
250
274
  }
251
275
 
252
- [data-vc-theme=dark] .vc-date[data-vc-date-today] .vc-date__btn {
253
- color: #3b82f6 !important;
276
+ /* Nombres de los días (Lun, Mar, …) */
277
+ .vc-week__day {
278
+ color: var(--color-muted-foreground) !important;
254
279
  }
255
280
  </style>
@@ -10,6 +10,7 @@ const props = defineProps<{
10
10
  label?: string
11
11
  hint?: string
12
12
  iconLeft?: object | Function | null
13
+ iconRight?: object | Function | null
13
14
  autocomplete?: string
14
15
  size?: 'sm' | 'md'
15
16
  }>()
@@ -24,7 +25,7 @@ const inputType = computed(() => {
24
25
  })
25
26
 
26
27
  const baseClasses = computed(() =>
27
- `${props.size === 'sm' ? 'py-1.5' : 'py-2'} px-3 block w-full rounded-lg text-sm text-slate-800 border border-card-line focus:ring-0 focus:border-gray-400 focus:outline-none disabled:opacity-50 dark:bg-transparent dark:text-muted-foreground-1 transition-colors placeholder:text-muted-foreground dark:placeholder:text-muted-foreground`
28
+ `innertia-field${props.size === 'sm' ? ' innertia-field-sm' : ''}`
28
29
  )
29
30
  </script>
30
31
 
@@ -49,11 +50,16 @@ const baseClasses = computed(() =>
49
50
  :class="[
50
51
  baseClasses,
51
52
  iconLeft ? 'ps-9' : '',
52
- type === 'password' ? 'pe-10' : '',
53
+ (iconRight || type === 'password') ? 'pe-10' : '',
53
54
  error ? '!border-red-400 dark:!border-red-500' : '',
54
55
  ]"
55
56
  />
56
57
 
58
+ <!-- Ícono derecho (no se renderiza si type=password — ahí va el toggle) -->
59
+ <div v-if="iconRight && type !== 'password'" class="absolute inset-y-0 end-0 flex items-center pe-3 pointer-events-none text-muted-foreground">
60
+ <component :is="iconRight" class="size-4" />
61
+ </div>
62
+
57
63
  <!-- Toggle contraseña -->
58
64
  <button
59
65
  v-if="type === 'password'"
@@ -16,24 +16,43 @@ const emit = defineEmits<{
16
16
 
17
17
  const selectRef = ref<HTMLSelectElement | null>(null)
18
18
 
19
- const reinitHsSelect = async () => {
19
+ /**
20
+ * Init Preline HSSelect una sola vez después del mount.
21
+ * No destruimos ni re-inicializamos en cambios de options — para forzar
22
+ * un re-mount limpio, el consumer puede cambiar el `:key` del componente.
23
+ *
24
+ * Si Preline aún no está disponible (plugin carga async), esperamos.
25
+ */
26
+ const initHsSelect = async () => {
20
27
  await nextTick()
21
28
  const el = selectRef.value
22
29
  if (!el) return
23
30
 
24
- const instance = (window as any).HSSelect?.getInstance?.(el)
25
- if (instance?.destroy) instance.destroy()
31
+ const tryInit = (attempts = 0) => {
32
+ const HSSelect = (window as any).HSSelect
33
+ if (!HSSelect) {
34
+ if (attempts < 20) {
35
+ // Re-intentar hasta 20 frames (~330ms) esperando que cargue el plugin
36
+ requestAnimationFrame(() => tryInit(attempts + 1))
37
+ }
38
+ return
39
+ }
26
40
 
27
- new (window as any).HSSelect(el)
28
- }
41
+ // Evitar doble init si ya hay una instancia registrada
42
+ if (HSSelect.getInstance?.(el)) return
43
+
44
+ try {
45
+ new HSSelect(el)
46
+ } catch (e) {
47
+ console.warn('[FormsSelect] HSSelect init falló:', e)
48
+ }
49
+ }
29
50
 
30
- // Re-initialize when options change (async load)
31
- watch(() => props.options, async () => {
32
- await reinitHsSelect()
33
- }, { deep: true })
51
+ tryInit()
52
+ }
34
53
 
35
54
  onMounted(() => {
36
- reinitHsSelect()
55
+ initHsSelect()
37
56
  })
38
57
 
39
58
  const handleChange = (e: Event) => {
@@ -53,7 +72,7 @@ const handleChange = (e: Event) => {
53
72
  <!-- Select (HSSelect) -->
54
73
  <ClientOnly>
55
74
  <template #fallback>
56
- <div class="h-[38px] bg-surface animate-pulse rounded-lg" />
75
+ <div class="innertia-field bg-surface animate-pulse" />
57
76
  </template>
58
77
 
59
78
  <div :class="['relative', error ? 'select-error' : '']">
@@ -66,9 +85,9 @@ const handleChange = (e: Event) => {
66
85
  data-hs-select='{
67
86
  "placeholder": "Seleccionar...",
68
87
  "toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
69
- "toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 ps-4 pe-9 flex gap-x-2 text-nowrap w-full cursor-pointer bg-card border border-card-line rounded-lg text-start text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 dark:text-muted-foreground-1 dark:focus:outline-hidden dark:focus:ring-1 dark:focus:ring-blue-600",
70
- "dropdownClasses": "mt-1 z-50 w-full max-h-72 p-1 space-y-0.5 bg-dropdown border border-dropdown-line rounded-lg overflow-hidden overflow-y-auto shadow-lg",
71
- "optionClasses": "py-2 px-4 w-full text-sm text-foreground cursor-pointer hover:bg-muted-hover rounded-lg focus:outline-hidden focus:bg-muted-hover dark:bg-card",
88
+ "toggleClasses": "innertia-field hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 aria-expanded:!rounded-b-none aria-expanded:!border-b-transparent relative pe-9 flex gap-x-2 items-center text-nowrap cursor-pointer text-start",
89
+ "dropdownClasses": "-mt-px z-50 w-full max-h-72 py-1 bg-[color:var(--field-dropdown-bg)] border border-[color:var(--field-border)] rounded-b-control overflow-hidden overflow-y-auto shadow-lg",
90
+ "optionClasses": "py-2 px-3 w-full text-sm text-foreground cursor-pointer bg-transparent hover:bg-muted-hover focus:outline-hidden focus:bg-muted-hover",
72
91
  "optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" 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\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
73
92
  }'
74
93
  >
@@ -84,17 +103,34 @@ const handleChange = (e: Event) => {
84
103
  </div>
85
104
  </ClientOnly>
86
105
 
87
- <!-- Error -->
88
- <p v-if="error" class="text-xs text-red-500 dark:text-red-400">{{ error }}</p>
106
+ <!-- Error message -->
107
+ <p v-if="error" class="text-xs text-red-500">{{ error }}</p>
89
108
 
90
109
  <!-- Hint -->
91
110
  <p v-else-if="hint" class="text-xs text-muted-foreground">{{ hint }}</p>
92
111
  </div>
93
112
  </template>
94
113
 
95
- <style scoped>
96
- /* Apply error border to the HSSelect toggle button when in error state */
97
- .select-error :deep(button[aria-expanded]) {
98
- border-color: rgb(248 113 113) !important; /* red-400 */
114
+ <style>
115
+ /*
116
+ * Preline HSSelect: cuando el dropdown está abierto, el trigger pierde
117
+ * sus esquinas inferiores y el borde inferior se vuelve invisible para
118
+ * fusionarse visualmente con el dropdown panel.
119
+ *
120
+ * Estilos globales (sin scoped) porque el HSSelect inyecta DOM fuera
121
+ * del componente Vue (button generado, no del template).
122
+ */
123
+ .hs-select [aria-expanded="true"].innertia-field {
124
+ border-bottom-left-radius: 0 !important;
125
+ border-bottom-right-radius: 0 !important;
126
+ border-bottom-color: transparent !important;
127
+ }
128
+
129
+ /* Pegar el dropdown al trigger eliminando cualquier offset/margen que
130
+ * Preline pueda inyectar. Target amplio: cualquier div con la clase
131
+ * `rounded-b-control` dentro del wrapper `.hs-select`. */
132
+ .hs-select div[class*="rounded-b-control"] {
133
+ top: 100% !important;
134
+ margin-top: -1px !important;
99
135
  }
100
136
  </style>
@@ -20,6 +20,8 @@ const props = defineProps({
20
20
  multiple: { type: Boolean, default: false },
21
21
  searchable: { type: Boolean, default: true },
22
22
  disabled: { type: Boolean, default: false },
23
+ loading: { type: Boolean, default: false },
24
+ name: { type: String, default: "" },
23
25
  clearable: { type: Boolean, default: false },
24
26
  searchPlaceholder: { type: String, default: "Buscar..." },
25
27
  minSearchLength: { type: Number, default: 0 },
@@ -336,23 +338,22 @@ const validationClasses = computed(() => {
336
338
 
337
339
  // Combined select classes
338
340
  const selectClasses = computed(() => {
339
- let base, focus, disabled;
340
-
341
- base =
342
- "relative w-full rounded-lg border bg-card transition-colors cursor-pointer text-foreground";
343
-
344
- if (props.multiple && props.tagsMode) {
345
- base += " px-3 pe-8 min-h-[2.375rem] flex items-center flex-wrap text-nowrap";
346
- focus = "focus:outline-none focus:border-gray-400";
347
- } else {
348
- focus = "focus:outline-none focus:ring-0 focus:border-gray-400";
349
- }
350
-
351
- disabled = props.disabled || props.loading ? "opacity-50 cursor-not-allowed" : "";
352
-
353
- const sizeClass = props.multiple && props.tagsMode ? "" : sizeClasses.value;
354
-
355
- return `${base} ${sizeClass} ${validationClasses.value} ${focus} ${disabled} ${props.class}`;
341
+ // Apariencia unificada con el resto de los fields del DS.
342
+ // En modo tags-multiple desactivamos la altura fija porque el control crece
343
+ // según la cantidad de chips seleccionados.
344
+ const base = props.multiple && props.tagsMode
345
+ ? "innertia-field relative cursor-pointer text-start !ps-3.5 !pe-9 flex items-center flex-wrap gap-2 text-nowrap !h-auto min-h-[var(--field-height)] py-2.5"
346
+ : "innertia-field relative cursor-pointer text-start pe-9 flex items-center gap-x-2";
347
+
348
+ // Cuando el dropdown está abierto, el trigger se "funde" visualmente con
349
+ // el panel: deja de redondear sus esquinas inferiores y queda como una sola pieza.
350
+ const openStyles = isOpen.value
351
+ ? "!rounded-b-none border-b-transparent"
352
+ : "";
353
+
354
+ const disabled = props.disabled || props.loading ? "opacity-50 cursor-not-allowed" : "";
355
+
356
+ return `${base} ${openStyles} ${validationClasses.value} ${disabled} ${props.class ?? ""}`;
356
357
  });
357
358
 
358
359
  // Filtered options — backend does the filtering, serverOptions IS the filteredOptions
@@ -403,8 +404,9 @@ const selectOption = (option) => {
403
404
  if (option.disabled) return;
404
405
 
405
406
  if (props.multiple) {
407
+ const target = getOptionValue(option);
406
408
  const index = selectedOptions.value.findIndex(
407
- (opt) => opt.id === option.id
409
+ (opt) => getOptionValue(opt) === target
408
410
  );
409
411
  if (index > -1) {
410
412
  selectedOptions.value.splice(index, 1);
@@ -428,7 +430,8 @@ const selectOption = (option) => {
428
430
 
429
431
  const removeTag = (option) => {
430
432
  if (props.disabled) return;
431
- const index = selectedOptions.value.findIndex((opt) => opt.id === option.id);
433
+ const target = getOptionValue(option);
434
+ const index = selectedOptions.value.findIndex((opt) => getOptionValue(opt) === target);
432
435
  if (index > -1) {
433
436
  selectedOptions.value.splice(index, 1);
434
437
  localValue.value = selectedOptions.value.map((opt) => getOptionValue(opt));
@@ -443,7 +446,8 @@ const clearSelection = () => {
443
446
  };
444
447
 
445
448
  const isOptionSelected = (option) => {
446
- return selectedOptions.value.some((opt) => opt.id === option.id);
449
+ const target = getOptionValue(option);
450
+ return selectedOptions.value.some((opt) => getOptionValue(opt) === target);
447
451
  };
448
452
 
449
453
  // Keyboard navigation
@@ -524,27 +528,28 @@ onMounted(() => {
524
528
  <!-- Selected values display -->
525
529
  <div v-else class="flex items-center justify-between w-full">
526
530
  <!-- Tags mode for multiple selection -->
527
- <div v-if="multiple && tagsMode && selectedOptions.length" class="flex flex-wrap items-center gap-1 flex-1">
531
+ <div v-if="multiple && tagsMode && selectedOptions.length" class="flex flex-wrap items-center gap-1.5 flex-1">
528
532
  <div v-for="option in selectedOptions" :key="option.id"
529
- class="flex flex-nowrap items-center relative z-10 bg-card border border-card-line rounded-full p-1 m-1">
533
+ class="inline-flex items-center gap-1.5 bg-muted border border-[color:var(--field-border)] rounded-badge ps-2.5 pe-1 py-1 text-xs leading-none">
530
534
  <!-- Avatar/Icon -->
531
- <div v-if="option.avatar || option.icon" class="size-6 me-1">
535
+ <div v-if="option.avatar || option.icon" class="size-4">
532
536
  <img v-if="option.avatar" :src="option.avatar" :alt="getOptionLabel(option)"
533
- class="inline-block rounded-full size-6" />
534
- <div v-else-if="option.icon" v-html="option.icon" class="size-6"></div>
537
+ class="inline-block rounded-avatar size-4" />
538
+ <div v-else-if="option.icon" v-html="option.icon" class="size-4"></div>
535
539
  </div>
536
540
 
537
541
  <!-- Label -->
538
- <div class="whitespace-nowrap text-foreground text-sm pl-1.5">
542
+ <span class="whitespace-nowrap text-foreground">
539
543
  <slot name="tag" :option="option">
540
544
  {{ getOptionLabel(option) }}
541
545
  </slot>
542
- </div>
546
+ </span>
543
547
 
544
548
  <!-- Remove button -->
545
549
  <button type="button" v-if="!disabled" @click.stop="removeTag(option)"
546
- class="inline-flex shrink-0 justify-center items-center size-5 ms-2 rounded-full text-foreground bg-surface-1 hover:bg-surface-1 focus:outline-none focus:ring-2 focus:ring-gray-400 text-sm cursor-pointer">
547
- <svg class="shrink-0 size-3" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
550
+ class="inline-flex shrink-0 justify-center items-center size-4 rounded-badge text-muted-foreground hover:text-foreground focus:outline-none focus-visible:outline-none cursor-pointer"
551
+ tabindex="-1">
552
+ <svg class="shrink-0 size-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
548
553
  fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
549
554
  <path d="M18 6 6 18" />
550
555
  <path d="m6 6 12 12" />
@@ -592,12 +597,12 @@ onMounted(() => {
592
597
  enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
593
598
  leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
594
599
  <div v-show="isOpen" ref="optionsListRef" @scroll="handleScroll"
595
- class="absolute z-50 w-full mt-1 bg-dropdown border border-dropdown-line rounded-xl shadow-xl max-h-60 overflow-auto">
600
+ class="absolute z-50 w-full -mt-px bg-[color:var(--field-dropdown-bg)] border border-[color:var(--field-border)] rounded-b-control shadow-lg max-h-60 overflow-auto">
596
601
  <!-- Search input -->
597
602
  <div v-if="searchable" class="p-2 border-b border-card-line relative">
598
603
  <input type="text" :value="searchQuery" :placeholder="searchPlaceholder" @input="handleSearchInput"
599
604
  @keydown.enter.prevent
600
- class="w-full px-3 py-2.5 pr-8 border border-card-line rounded-lg focus:outline-none focus:ring-0 focus:border-gray-400 dark:bg-surface dark:text-white text-sm transition-colors" />
605
+ class="innertia-field innertia-field-sm pe-8" />
601
606
  <button type="button" v-if="searchQuery" @click.prevent="clearSearch"
602
607
  class="absolute top-1/2 right-4 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
603
608
  <svg class="size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -635,7 +640,7 @@ onMounted(() => {
635
640
  <!-- Multiple selection checkbox -->
636
641
  <div v-if="multiple" class="mr-2">
637
642
  <div :class="[
638
- 'w-4 h-4 rounded-lg border-2 flex items-center justify-center',
643
+ 'w-4 h-4 rounded-control border-2 flex items-center justify-center',
639
644
  isOptionSelected(option)
640
645
  ? 'bg-slate-600 border-slate-600 text-white'
641
646
  : 'border-card-line',
@@ -652,7 +657,7 @@ onMounted(() => {
652
657
  <div class="flex items-center flex-1">
653
658
  <!-- Avatar/Icon -->
654
659
  <div v-if="option.avatar || option.icon" class="mr-3">
655
- <img v-if="option.avatar" :src="option.avatar" :alt="option.label" class="w-6 h-6 rounded-full" />
660
+ <img v-if="option.avatar" :src="option.avatar" :alt="option.label" class="w-6 h-6 rounded-avatar" />
656
661
  <component v-else-if="option.icon" :is="option.icon" class="w-5 h-5 text-gray-500" />
657
662
  </div>
658
663
 
@@ -702,7 +707,7 @@ onMounted(() => {
702
707
  <!-- No data message -->
703
708
  <div v-else-if="!serverOptions.length && !isFetching" class="px-3 py-8 text-center">
704
709
  <div class="flex flex-col items-center gap-y-2">
705
- <div class="size-10 rounded-full bg-muted flex items-center justify-center">
710
+ <div class="size-10 rounded-avatar bg-muted flex items-center justify-center">
706
711
  <svg class="size-5 text-muted-foreground-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
707
712
  stroke-width="1.5" stroke="currentColor">
708
713
  <path stroke-linecap="round" stroke-linejoin="round"
@@ -1,12 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { IconDeviceDesktop, IconArrowRight, IconCopy, IconCheck } from '@tabler/icons-vue'
3
- import type { AppDefinition } from '~/configs/apps'
3
+ import type { ContextDefinition } from '~/configs/apps'
4
4
 
5
5
  const props = defineProps<{
6
- /** App al que se intentó acceder (para el copy contextual). */
7
- blockedApp?: AppDefinition | null
8
- /** App mobile-friendly al que el usuario puede continuar (opcional). */
9
- fallbackApp?: AppDefinition | null
6
+ /** Context al que se intentó acceder (para el copy contextual). */
7
+ blockedApp?: ContextDefinition | null
8
+ /** Context mobile-friendly al que el usuario puede continuar (opcional). */
9
+ fallbackApp?: ContextDefinition | null
10
10
  }>()
11
11
 
12
12
  const route = useRoute()
@@ -1,18 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import * as TablerIcons from '@tabler/icons-vue'
3
3
  import { IconArrowRight } from '@tabler/icons-vue'
4
- import type { AppDefinition } from '~/configs/apps'
4
+ import type { ContextDefinition } from '~/configs/apps'
5
5
 
6
6
  const props = defineProps<{
7
- /** Apps a mostrar como opciones de login. */
8
- options: AppDefinition[]
7
+ /** Contexts a mostrar como opciones de login. */
8
+ options: ContextDefinition[]
9
9
  /** Título personalizable. */
10
10
  title?: string
11
11
  }>()
12
12
 
13
13
  const emit = defineEmits<{
14
- /** Emitido cuando el usuario elige un app — el padre se encarga de navegar y recordar la elección. */
15
- pick: [app: AppDefinition]
14
+ /** Emitido cuando el usuario elige un context — el padre se encarga de navegar y recordar la elección. */
15
+ pick: [context: ContextDefinition]
16
16
  }>()
17
17
 
18
18
  const heading = computed(() => props.title ?? '¿Cómo quieres ingresar?')
@@ -46,25 +46,25 @@ function resolveIcon(name?: string) {
46
46
  <!-- Opciones -->
47
47
  <div class="space-y-3">
48
48
  <button
49
- v-for="app in options"
50
- :key="app.path"
49
+ v-for="context in options"
50
+ :key="context.path"
51
51
  type="button"
52
52
  class="group w-full flex items-center gap-4 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-4 py-4 text-left hover:border-violet-300 dark:hover:border-violet-700 hover:bg-violet-50/50 dark:hover:bg-violet-900/10 transition-colors"
53
- @click="emit('pick', app)"
53
+ @click="emit('pick', context)"
54
54
  >
55
55
  <div class="shrink-0 size-11 rounded-xl bg-violet-100 dark:bg-violet-900/30 flex items-center justify-center group-hover:bg-violet-200 dark:group-hover:bg-violet-900/50 transition-colors">
56
56
  <component
57
- v-if="resolveIcon(app.icon)"
58
- :is="resolveIcon(app.icon)"
57
+ v-if="resolveIcon(context.icon)"
58
+ :is="resolveIcon(context.icon)"
59
59
  class="size-5 text-violet-600 dark:text-violet-400"
60
60
  />
61
61
  </div>
62
62
  <div class="flex-1 min-w-0">
63
63
  <div class="text-base font-semibold text-slate-900 dark:text-white">
64
- {{ app.label }}
64
+ {{ context.label }}
65
65
  </div>
66
- <div v-if="app.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
67
- {{ app.description }}
66
+ <div v-if="context.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
67
+ {{ context.description }}
68
68
  </div>
69
69
  </div>
70
70
  <IconArrowRight class="shrink-0 size-5 text-slate-400 group-hover:text-violet-600 dark:group-hover:text-violet-400 transition-colors" />
@@ -10,12 +10,12 @@
10
10
  */
11
11
  import { IconBuildingSkyscraper, IconArrowRight } from '@tabler/icons-vue'
12
12
 
13
- const { current: currentApp } = useApp()
13
+ const { current: currentContext } = useContext()
14
14
  const { available, switchTo } = useOrganization()
15
15
 
16
16
  const title = computed(() =>
17
- currentApp.value
18
- ? `Elige una organización para continuar en ${currentApp.value.label}`
17
+ currentContext.value
18
+ ? `Elige una organización para continuar en ${currentContext.value.label}`
19
19
  : 'Elige una organización'
20
20
  )
21
21
 
@@ -133,17 +133,17 @@ function groupSelectedCount(group) {
133
133
 
134
134
  <!-- Search -->
135
135
  <div class="relative">
136
- <IconSearch class="absolute left-2.5 top-1/2 -translate-y-1/2 size-3 text-slate-400 pointer-events-none" />
136
+ <IconSearch class="absolute right-2.5 top-1/2 -translate-y-1/2 size-3 text-muted-foreground pointer-events-none" />
137
137
  <input
138
138
  v-model="search"
139
- type="search"
139
+ type="text"
140
140
  placeholder="Buscar permiso…"
141
- class="w-full pl-7 pr-3 py-1.5 text-xs bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md text-slate-700 dark:text-slate-300 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-blue-500/40 focus:border-blue-400"
141
+ class="innertia-field innertia-field-sm pe-7"
142
142
  />
143
143
  </div>
144
144
 
145
145
  <!-- Tree -->
146
- <div class="rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 overflow-hidden text-sm">
146
+ <div class="rounded-card border border-card-line bg-card overflow-hidden text-sm">
147
147
 
148
148
  <div v-if="visibleApps.length === 0" class="px-3 py-6 text-center text-xs text-slate-400 dark:text-slate-500">
149
149
  {{ search ? 'Sin resultados.' : readonly ? 'Sin permisos asignados.' : 'Sin permisos disponibles.' }}
@@ -82,19 +82,19 @@ defineExpose({ reload })
82
82
  <template>
83
83
  <div class="flex flex-col gap-3">
84
84
  <div v-if="showSearch" class="relative max-w-sm">
85
- <div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
86
- <IconSearch class="size-4 text-slate-400" stroke="1.5" />
85
+ <div class="absolute inset-y-0 end-0 flex items-center pe-3 pointer-events-none">
86
+ <IconSearch class="size-4 text-muted-foreground" stroke="1.5" />
87
87
  </div>
88
88
  <input
89
89
  v-model="search"
90
- type="search"
90
+ type="text"
91
91
  :placeholder="searchPlaceholder"
92
- class="block w-full rounded-lg border border-card-line bg-card text-foreground py-1.5 ps-9 pe-4 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
92
+ class="innertia-field innertia-field-sm pe-9"
93
93
  />
94
94
  </div>
95
95
 
96
96
  <!-- Dense table wrapper — override Table's default padding with compact styles -->
97
- <div class="overflow-x-auto border border-card-line rounded-xl">
97
+ <div class="overflow-x-auto border border-card-line rounded-card">
98
98
  <Table
99
99
  ref="tableRef"
100
100
  :endpoint="endpoint"