@vasakgroup/vue-libvasak 0.3.0 → 0.3.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.
- package/dist/types/cards/DeviceCard.vue.d.ts +32 -0
- package/dist/types/cards/ListCard.vue.d.ts +24 -0
- package/dist/types/controls/ActionButton.vue.d.ts +35 -0
- package/dist/types/controls/ToggleControl.vue.d.ts +23 -0
- package/dist/types/forms/FormGroup.vue.d.ts +23 -0
- package/dist/types/forms/SliderControl.vue.d.ts +28 -0
- package/dist/types/forms/SwitchToggle.vue.d.ts +21 -0
- package/{src/index.ts → dist/types/index.d.ts} +4 -40
- package/dist/types/layout/ConfigSection.vue.d.ts +21 -0
- package/dist/types/sidebar/SideBar.vue.d.ts +51 -0
- package/dist/types/sidebar/SideButton.vue.d.ts +21 -0
- package/dist/types/sidebar/SideGroup.vue.d.ts +21 -0
- package/dist/types/sidebar/tipos.d.ts +15 -0
- package/dist/types/tray/TrayIconButton.vue.d.ts +37 -0
- package/dist/types/window/TopBar.vue.d.ts +9 -0
- package/dist/types/window/WindowFrame.vue.d.ts +20 -0
- package/package.json +7 -7
- package/src/cards/DeviceCard.vue +0 -82
- package/src/cards/ListCard.vue +0 -40
- package/src/controls/ActionButton.vue +0 -85
- package/src/controls/ToggleControl.vue +0 -55
- package/src/forms/FormGroup.vue +0 -27
- package/src/forms/SliderControl.vue +0 -100
- package/src/forms/SwitchToggle.vue +0 -49
- package/src/layout/ConfigSection.vue +0 -28
- package/src/shims-vue.d.ts +0 -15
- package/src/sidebar/SideBar.vue +0 -183
- package/src/sidebar/SideButton.vue +0 -115
- package/src/sidebar/SideGroup.vue +0 -46
- package/src/sidebar/tipos.ts +0 -16
- package/src/tray/TrayIconButton.vue +0 -80
- package/src/types/vue-libvasak.d.ts +0 -191
- package/src/window/TopBar.vue +0 -46
- package/src/window/WindowFrame.vue +0 -21
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
interface Props {
|
|
3
|
-
label: string;
|
|
4
|
-
disabled?: boolean;
|
|
5
|
-
variant?: 'primary' | 'secondary' | 'danger';
|
|
6
|
-
loading?: boolean;
|
|
7
|
-
customClass?: string | Record<string, boolean>;
|
|
8
|
-
size?: 'sm' | 'md' | 'lg';
|
|
9
|
-
fullWidth?: boolean;
|
|
10
|
-
iconSrc?: string;
|
|
11
|
-
iconAlt?: string;
|
|
12
|
-
iconRight?: boolean;
|
|
13
|
-
type?: 'button' | 'submit' | 'reset';
|
|
14
|
-
stopPropagation?: boolean;
|
|
15
|
-
preventDefault?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
19
|
-
disabled: false,
|
|
20
|
-
variant: 'primary',
|
|
21
|
-
loading: false,
|
|
22
|
-
customClass: () => ({}),
|
|
23
|
-
size: 'md',
|
|
24
|
-
fullWidth: false,
|
|
25
|
-
iconSrc: '',
|
|
26
|
-
iconAlt: '',
|
|
27
|
-
iconRight: false,
|
|
28
|
-
type: 'button',
|
|
29
|
-
stopPropagation: false,
|
|
30
|
-
preventDefault: false,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const emit = defineEmits<{
|
|
34
|
-
click: [];
|
|
35
|
-
}>();
|
|
36
|
-
|
|
37
|
-
const variantClasses: Record<string, string> = {
|
|
38
|
-
primary: 'bg-primary dark:bg-primary-dark text-tx-on-primary dark:text-tx-on-primary-dark hover:bg-primary/90 dark:hover:bg-primary-dark/90',
|
|
39
|
-
secondary: 'bg-secondary dark:bg-secondary-dark text-tx-on-primary dark:text-tx-on-primary-dark hover:bg-secondary/80 dark:hover:bg-secondary-dark/80',
|
|
40
|
-
danger: 'bg-status-error dark:bg-status-error-dark text-tx-on-primary dark:text-tx-on-primary-dark hover:bg-status-error/90 dark:hover:bg-status-error-dark/90',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const sizeClasses: Record<'sm' | 'md' | 'lg', string> = {
|
|
44
|
-
sm: 'px-2 py-1 text-xs',
|
|
45
|
-
md: 'px-3 py-1 text-sm',
|
|
46
|
-
lg: 'px-4 py-2 text-base',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const handleClick = (event: Event) => {
|
|
50
|
-
if (props.stopPropagation) event.stopPropagation();
|
|
51
|
-
if (props.preventDefault) event.preventDefault();
|
|
52
|
-
if (!props.disabled && !props.loading) {
|
|
53
|
-
emit('click');
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
</script>
|
|
57
|
-
|
|
58
|
-
<template>
|
|
59
|
-
<button
|
|
60
|
-
:type="props.type"
|
|
61
|
-
@click="handleClick"
|
|
62
|
-
class="rounded-corner transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
63
|
-
:class="[
|
|
64
|
-
variantClasses[props.variant],
|
|
65
|
-
sizeClasses[props.size],
|
|
66
|
-
props.fullWidth ? 'w-full' : '',
|
|
67
|
-
props.iconSrc && !props.label ? 'px-2 py-2' : '',
|
|
68
|
-
customClass,
|
|
69
|
-
]"
|
|
70
|
-
:disabled="props.disabled || props.loading"
|
|
71
|
-
>
|
|
72
|
-
<span v-if="loading" class="w-4 h-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
|
|
73
|
-
<template v-if="props.iconSrc && !props.iconRight">
|
|
74
|
-
<img :src="props.iconSrc" :alt="props.iconAlt || props.label" class="w-4 h-4" />
|
|
75
|
-
</template>
|
|
76
|
-
<span v-if="props.label">{{ props.label }}</span>
|
|
77
|
-
<template v-if="props.iconSrc && props.iconRight">
|
|
78
|
-
<img :src="props.iconSrc" :alt="props.iconAlt || props.label" class="w-4 h-4" />
|
|
79
|
-
</template>
|
|
80
|
-
</button>
|
|
81
|
-
</template>
|
|
82
|
-
|
|
83
|
-
<style scoped>
|
|
84
|
-
/* Ningún estilo adicional requerido */
|
|
85
|
-
</style>
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<button
|
|
3
|
-
@click="handleClick"
|
|
4
|
-
class="p-2 rounded-corner background hover:opacity-50 transition-all duration-300 h-17.5 w-17.5 group relative overflow-hidden hover:scale-105 hover:shadow-lg active:scale-95"
|
|
5
|
-
:class="{
|
|
6
|
-
'animate-pulse': isLoading,
|
|
7
|
-
'ring-2 ring-primary dark:ring-primary-dark': isActive,
|
|
8
|
-
'opacity-60': !isActive,
|
|
9
|
-
...customClass
|
|
10
|
-
}"
|
|
11
|
-
:disabled="isLoading"
|
|
12
|
-
>
|
|
13
|
-
<img
|
|
14
|
-
:src="icon"
|
|
15
|
-
:alt="alt"
|
|
16
|
-
:title="tooltip"
|
|
17
|
-
class="m-auto w-12.5 h-12.5 transition-all duration-300 group-hover:scale-110 relative z-10"
|
|
18
|
-
:class="{
|
|
19
|
-
'animate-spin': isLoading,
|
|
20
|
-
'filter brightness-75': !isActive,
|
|
21
|
-
'drop-shadow-lg': isActive,
|
|
22
|
-
...iconClass
|
|
23
|
-
}"
|
|
24
|
-
/>
|
|
25
|
-
</button>
|
|
26
|
-
</template>
|
|
27
|
-
|
|
28
|
-
<script setup lang="ts">
|
|
29
|
-
interface Props {
|
|
30
|
-
icon: string;
|
|
31
|
-
alt?: string;
|
|
32
|
-
tooltip?: string;
|
|
33
|
-
isActive?: boolean;
|
|
34
|
-
isLoading?: boolean;
|
|
35
|
-
iconClass?: Record<string, boolean>;
|
|
36
|
-
customClass?: Record<string, boolean>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
withDefaults(defineProps<Props>(), {
|
|
40
|
-
alt: '',
|
|
41
|
-
tooltip: '',
|
|
42
|
-
isActive: false,
|
|
43
|
-
isLoading: false,
|
|
44
|
-
iconClass: () => ({}),
|
|
45
|
-
customClass: () => ({}),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const emit = defineEmits<{
|
|
49
|
-
click: [];
|
|
50
|
-
}>();
|
|
51
|
-
|
|
52
|
-
const handleClick = () => {
|
|
53
|
-
emit('click');
|
|
54
|
-
};
|
|
55
|
-
</script>
|
package/src/forms/FormGroup.vue
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
interface Props {
|
|
3
|
-
label: string;
|
|
4
|
-
htmlFor?: string;
|
|
5
|
-
customClass?: string | Record<string, boolean>;
|
|
6
|
-
labelClass?: string | Record<string, boolean>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
withDefaults(defineProps<Props>(), {
|
|
10
|
-
htmlFor: '',
|
|
11
|
-
customClass: () => ({}),
|
|
12
|
-
labelClass: () => ({}),
|
|
13
|
-
});
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<div class="flex flex-col gap-2" :class="customClass">
|
|
18
|
-
<label v-if="label" :for="htmlFor" class="text-sm font-medium text-primary dark:text-primary-dark" :class="labelClass">
|
|
19
|
-
{{ label }}
|
|
20
|
-
</label>
|
|
21
|
-
<slot />
|
|
22
|
-
</div>
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<style scoped>
|
|
26
|
-
/* Ningún estilo adicional requerido */
|
|
27
|
-
</style>
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="background rounded-corner flex flex-row items-center gap-2 justify-between w-full h-auto p-4 transition-all duration-200 hover:bg-ui-surface/80 dark:hover:bg-ui-surface-dark/80"
|
|
4
|
-
>
|
|
5
|
-
<button
|
|
6
|
-
v-if="showButton"
|
|
7
|
-
@click="handleButtonClick"
|
|
8
|
-
class="w-8 h-8 flex items-center justify-center rounded-corner transition-all duration-200 hover:bg-ui-surface/80 dark:hover:bg-ui-surface-dark/80 hover:scale-110 active:scale-95"
|
|
9
|
-
>
|
|
10
|
-
<img
|
|
11
|
-
:src="icon"
|
|
12
|
-
:alt="alt"
|
|
13
|
-
:title="tooltip"
|
|
14
|
-
class="w-6 h-6 transition-all duration-200"
|
|
15
|
-
:class="iconClass"
|
|
16
|
-
/>
|
|
17
|
-
</button>
|
|
18
|
-
|
|
19
|
-
<div
|
|
20
|
-
v-else
|
|
21
|
-
class="w-8 h-8 flex items-center justify-center"
|
|
22
|
-
>
|
|
23
|
-
<img
|
|
24
|
-
:src="icon"
|
|
25
|
-
:alt="alt"
|
|
26
|
-
class="w-6 h-6 transition-all duration-200"
|
|
27
|
-
/>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<input
|
|
31
|
-
type="range"
|
|
32
|
-
:min="min"
|
|
33
|
-
:max="max"
|
|
34
|
-
:value="modelValue"
|
|
35
|
-
@input="handleInput"
|
|
36
|
-
class="flex-1 transition-all duration-200 hover:scale-105"
|
|
37
|
-
/>
|
|
38
|
-
|
|
39
|
-
<span
|
|
40
|
-
class="w-12 text-right transition-all duration-200 font-medium"
|
|
41
|
-
:class="percentageClass"
|
|
42
|
-
>
|
|
43
|
-
{{ percentage }}%
|
|
44
|
-
</span>
|
|
45
|
-
</div>
|
|
46
|
-
</template>
|
|
47
|
-
|
|
48
|
-
<script setup lang="ts">
|
|
49
|
-
import { computed } from 'vue';
|
|
50
|
-
|
|
51
|
-
interface Props {
|
|
52
|
-
icon: string;
|
|
53
|
-
alt?: string;
|
|
54
|
-
tooltip?: string;
|
|
55
|
-
modelValue: number;
|
|
56
|
-
min?: number;
|
|
57
|
-
max?: number;
|
|
58
|
-
showButton?: boolean;
|
|
59
|
-
iconClass?: string | Record<string, boolean>;
|
|
60
|
-
getPercentageClass?: (percentage: number) => string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
64
|
-
alt: '',
|
|
65
|
-
tooltip: '',
|
|
66
|
-
min: 0,
|
|
67
|
-
max: 100,
|
|
68
|
-
showButton: false,
|
|
69
|
-
iconClass: () => ({}),
|
|
70
|
-
getPercentageClass: () => '',
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const emit = defineEmits<{
|
|
74
|
-
'update:modelValue': [value: number];
|
|
75
|
-
'buttonClick': [];
|
|
76
|
-
}>();
|
|
77
|
-
|
|
78
|
-
const percentage = computed(() => {
|
|
79
|
-
if (props.max <= props.min) return 0;
|
|
80
|
-
const range = props.max - props.min;
|
|
81
|
-
const value = props.modelValue - props.min;
|
|
82
|
-
return Math.round((value / range) * 100);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const percentageClass = computed(() => {
|
|
86
|
-
if (props.getPercentageClass) {
|
|
87
|
-
return props.getPercentageClass(percentage.value);
|
|
88
|
-
}
|
|
89
|
-
return '';
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const handleInput = (event: Event) => {
|
|
93
|
-
const target = event.target as HTMLInputElement;
|
|
94
|
-
emit('update:modelValue', Number(target.value));
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const handleButtonClick = () => {
|
|
98
|
-
emit('buttonClick');
|
|
99
|
-
};
|
|
100
|
-
</script>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<button
|
|
3
|
-
type="button"
|
|
4
|
-
@click="handleClick"
|
|
5
|
-
:disabled="disabled"
|
|
6
|
-
:class="[
|
|
7
|
-
'relative inline-flex items-center rounded-full transition-colors',
|
|
8
|
-
size === 'small' ? 'h-6 w-11' : 'h-7 w-12',
|
|
9
|
-
isOn ? activeClass : inactiveClass,
|
|
10
|
-
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
|
11
|
-
customClass
|
|
12
|
-
]"
|
|
13
|
-
>
|
|
14
|
-
<span
|
|
15
|
-
:class="[
|
|
16
|
-
'inline-block transform rounded-full bg-white shadow transition-transform',
|
|
17
|
-
size === 'small' ? 'h-4 w-4' : 'h-6 w-6',
|
|
18
|
-
isOn ? (size === 'small' ? 'translate-x-6' : 'translate-x-5') : 'translate-x-1'
|
|
19
|
-
]"
|
|
20
|
-
></span>
|
|
21
|
-
</button>
|
|
22
|
-
</template>
|
|
23
|
-
|
|
24
|
-
<script setup lang="ts">
|
|
25
|
-
interface Props {
|
|
26
|
-
isOn: boolean;
|
|
27
|
-
disabled?: boolean;
|
|
28
|
-
size?: 'small' | 'medium';
|
|
29
|
-
activeClass?: string;
|
|
30
|
-
inactiveClass?: string;
|
|
31
|
-
customClass?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
35
|
-
disabled: false,
|
|
36
|
-
size: 'small',
|
|
37
|
-
activeClass: 'bg-primary dark:bg-primary-dark',
|
|
38
|
-
inactiveClass: 'background',
|
|
39
|
-
customClass: '',
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const emit = defineEmits<{
|
|
43
|
-
toggle: [value: boolean];
|
|
44
|
-
}>();
|
|
45
|
-
|
|
46
|
-
const handleClick = () => {
|
|
47
|
-
emit('toggle', !props.isOn);
|
|
48
|
-
};
|
|
49
|
-
</script>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
interface Props {
|
|
3
|
-
title: string;
|
|
4
|
-
icon?: string;
|
|
5
|
-
customClass?: string | Record<string, boolean>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
withDefaults(defineProps<Props>(), {
|
|
9
|
-
icon: '',
|
|
10
|
-
customClass: () => ({}),
|
|
11
|
-
});
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<template>
|
|
15
|
-
<div
|
|
16
|
-
class="flex flex-col gap-4 p-4 background rounded-corner"
|
|
17
|
-
:class="customClass"
|
|
18
|
-
>
|
|
19
|
-
<h3 class="text-base font-semibold m-0 text-primary dark:text-primary-dark">
|
|
20
|
-
{{ icon ? `${icon} ${title}` : title }}
|
|
21
|
-
</h3>
|
|
22
|
-
<slot />
|
|
23
|
-
</div>
|
|
24
|
-
</template>
|
|
25
|
-
|
|
26
|
-
<style scoped>
|
|
27
|
-
/* Ningún estilo adicional requerido */
|
|
28
|
-
</style>
|
package/src/shims-vue.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Que TypeScript entienda los `.vue`.
|
|
3
|
-
*
|
|
4
|
-
* Acá había un `declare module '*'`, que no es un shim: es apagar el chequeo de
|
|
5
|
-
* **todos** los imports del proyecto y dejarlos en `any`. Con eso, los tipos que
|
|
6
|
-
* esta librería publica no los comprobaba nadie —ni siquiera contra sus propios
|
|
7
|
-
* componentes—, que para un paquete cuya razón de ser es que otras seis
|
|
8
|
-
* aplicaciones lo usen es justo lo que no puede pasar.
|
|
9
|
-
*/
|
|
10
|
-
declare module '*.vue' {
|
|
11
|
-
import type { DefineComponent } from 'vue';
|
|
12
|
-
|
|
13
|
-
const componente: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
|
14
|
-
export default componente;
|
|
15
|
-
}
|
package/src/sidebar/SideBar.vue
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
/**
|
|
3
|
-
* La barra lateral de las aplicaciones de VasakOS.
|
|
4
|
-
*
|
|
5
|
-
* Es la de Configuración, que es la que todas las demás venían copiando: mismo
|
|
6
|
-
* `aside` con borde y esquina redondeada, mismo plegado a 84 píxeles, mismos
|
|
7
|
-
* grupos con título. Que estén acá y no copiadas en cada repositorio es lo que
|
|
8
|
-
* hace que las ventanas se lean como partes del mismo escritorio en vez de
|
|
9
|
-
* parecerse por casualidad.
|
|
10
|
-
*
|
|
11
|
-
* # Los dos caminos
|
|
12
|
-
*
|
|
13
|
-
* Con `categories` se arma sola: cada categoría es un grupo plegable y cada
|
|
14
|
-
* elemento un botón, con el activo marcado contra `modelValue`. Es el camino de
|
|
15
|
-
* Configuración y del monitor, donde la barra **es** la navegación.
|
|
16
|
-
*
|
|
17
|
-
* Con la ranura por omisión se pone cualquier cosa adentro —discos, pasos de un
|
|
18
|
-
* asistente, una línea de tiempo— y la barra aporta nada más que el marco y el
|
|
19
|
-
* plegado. La ranura recibe `collapsed`, porque lo que se dibuja adentro casi
|
|
20
|
-
* siempre tiene que saberlo.
|
|
21
|
-
*
|
|
22
|
-
* Los dos caminos conviven: lo declarativo va primero y la ranura después.
|
|
23
|
-
*
|
|
24
|
-
* # Por qué no hay traducciones acá
|
|
25
|
-
*
|
|
26
|
-
* Una librería de componentes que traduce obliga a todas las aplicaciones a
|
|
27
|
-
* compartir sus claves. Los textos entran por propiedades; el `aria-label` del
|
|
28
|
-
* botón de plegar también, que es el único que no se ve pero se oye.
|
|
29
|
-
*/
|
|
30
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
31
|
-
import SideGroup from './SideGroup.vue';
|
|
32
|
-
import SideButton from './SideButton.vue';
|
|
33
|
-
import type { SidebarCategory } from './tipos';
|
|
34
|
-
|
|
35
|
-
const props = withDefaults(
|
|
36
|
-
defineProps<{
|
|
37
|
-
/** El nombre de la ventana. Sin él y sin `subtitle` no hay área de título. */
|
|
38
|
-
title?: string;
|
|
39
|
-
subtitle?: string;
|
|
40
|
-
categories?: SidebarCategory[];
|
|
41
|
-
/** El identificador del elemento activo. */
|
|
42
|
-
modelValue?: string;
|
|
43
|
-
/** Plegada desde afuera. Sin esto, la barra se maneja sola. */
|
|
44
|
-
collapsed?: boolean;
|
|
45
|
-
/** Lo que oye un lector de pantalla en el botón de plegar. */
|
|
46
|
-
collapseLabel?: string;
|
|
47
|
-
expandLabel?: string;
|
|
48
|
-
}>(),
|
|
49
|
-
{
|
|
50
|
-
title: '',
|
|
51
|
-
subtitle: '',
|
|
52
|
-
categories: () => [],
|
|
53
|
-
modelValue: '',
|
|
54
|
-
collapsed: undefined,
|
|
55
|
-
collapseLabel: 'Collapse',
|
|
56
|
-
expandLabel: 'Expand',
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const emit = defineEmits<{
|
|
61
|
-
'update:modelValue': [value: string];
|
|
62
|
-
'update:collapsed': [value: boolean];
|
|
63
|
-
change: [value: string];
|
|
64
|
-
}>();
|
|
65
|
-
|
|
66
|
-
const plegadaAMano = ref(props.collapsed ?? false);
|
|
67
|
-
const esAngosta = ref(false);
|
|
68
|
-
let consulta: MediaQueryList | null = null;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Plegada por decisión o por ancho.
|
|
72
|
-
*
|
|
73
|
-
* Por debajo de 768 píxeles no hay lugar para el texto de los botones, así que
|
|
74
|
-
* la barra se pliega sola y el botón de plegar no se muestra: ofrecer
|
|
75
|
-
* desplegarla ahí sería ofrecer algo que no entra.
|
|
76
|
-
*/
|
|
77
|
-
const plegada = computed(() => esAngosta.value || plegadaAMano.value);
|
|
78
|
-
const hayTitulo = computed(() => Boolean(props.title || props.subtitle));
|
|
79
|
-
const hayCategorias = computed(() => props.categories.length > 0);
|
|
80
|
-
|
|
81
|
-
function revisar() {
|
|
82
|
-
esAngosta.value = consulta?.matches ?? false;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function alternar() {
|
|
86
|
-
plegadaAMano.value = !plegadaAMano.value;
|
|
87
|
-
emit('update:collapsed', plegadaAMano.value);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function elegir(id: string) {
|
|
91
|
-
emit('update:modelValue', id);
|
|
92
|
-
emit('change', id);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Controlada desde afuera cuando la aplicación pasa `collapsed`: así dos
|
|
96
|
-
// ventanas de la misma aplicación pueden recordar cómo la dejó la persona.
|
|
97
|
-
watch(
|
|
98
|
-
() => props.collapsed,
|
|
99
|
-
(valor) => {
|
|
100
|
-
if (valor !== undefined) {
|
|
101
|
-
plegadaAMano.value = valor;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
onMounted(() => {
|
|
107
|
-
consulta = window.matchMedia('(max-width: 767px)');
|
|
108
|
-
revisar();
|
|
109
|
-
consulta.addEventListener('change', revisar);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
onBeforeUnmount(() => consulta?.removeEventListener('change', revisar));
|
|
113
|
-
|
|
114
|
-
defineExpose({ collapsed: plegada });
|
|
115
|
-
</script>
|
|
116
|
-
|
|
117
|
-
<template>
|
|
118
|
-
<aside
|
|
119
|
-
class="relative z-30 flex h-full shrink-0 flex-col rounded-corner border border-ui-border bg-ui-bg/80 transition-all duration-300"
|
|
120
|
-
:class="['w-[84px]', plegada ? 'md:w-[84px]' : 'md:w-72']">
|
|
121
|
-
<header
|
|
122
|
-
v-if="hayTitulo || $slots.header"
|
|
123
|
-
class="flex flex-col gap-2 border-ui-border border-b p-2">
|
|
124
|
-
<div class="flex items-center gap-2">
|
|
125
|
-
<button
|
|
126
|
-
type="button"
|
|
127
|
-
class="hidden h-10 w-10 items-center justify-center rounded-corner border border-ui-border bg-ui-surface/70 font-semibold text-sm md:inline-flex"
|
|
128
|
-
:aria-label="plegada ? expandLabel : collapseLabel"
|
|
129
|
-
:aria-expanded="!plegada"
|
|
130
|
-
@click="alternar">
|
|
131
|
-
{{ plegada ? '>' : '<' }}
|
|
132
|
-
</button>
|
|
133
|
-
<!-- El área de título es opcional: hay ventanas donde el nombre ya está
|
|
134
|
-
en la barra superior y repetirlo acá gasta la mitad del alto. -->
|
|
135
|
-
<div v-if="hayTitulo && !plegada" class="min-w-0 flex-1">
|
|
136
|
-
<p v-if="title" class="truncate font-semibold text-sm">{{ title }}</p>
|
|
137
|
-
<p v-if="subtitle" class="truncate text-tx-muted text-xs">{{ subtitle }}</p>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<!-- Lo que va antes que cualquier categoría: la búsqueda de la tienda,
|
|
142
|
-
por ejemplo. Plegada no entra un campo de texto —84 píxeles es el
|
|
143
|
-
ancho del icono— así que se esconde en vez de quedar ilegible. -->
|
|
144
|
-
<div v-if="$slots.header && !plegada">
|
|
145
|
-
<slot name="header" :collapsed="plegada" />
|
|
146
|
-
</div>
|
|
147
|
-
</header>
|
|
148
|
-
|
|
149
|
-
<!-- Sin área de título el botón de plegar necesita su propio lugar, o la
|
|
150
|
-
barra deja de poder plegarse. -->
|
|
151
|
-
<div v-else class="flex justify-center border-ui-border border-b p-2">
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
class="hidden h-10 w-10 items-center justify-center rounded-corner border border-ui-border bg-ui-surface/70 font-semibold text-sm md:inline-flex"
|
|
155
|
-
:aria-label="plegada ? expandLabel : collapseLabel"
|
|
156
|
-
:aria-expanded="!plegada"
|
|
157
|
-
@click="alternar">
|
|
158
|
-
{{ plegada ? '>' : '<' }}
|
|
159
|
-
</button>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
<div class="flex-1 space-y-3 overflow-y-auto p-2">
|
|
163
|
-
<SideGroup
|
|
164
|
-
v-for="category in categories"
|
|
165
|
-
:key="category.id"
|
|
166
|
-
:title="category.title"
|
|
167
|
-
:collapsed="plegada">
|
|
168
|
-
<SideButton
|
|
169
|
-
v-for="item in category.items"
|
|
170
|
-
:key="item.id"
|
|
171
|
-
:label="item.label"
|
|
172
|
-
:icon="item.icon"
|
|
173
|
-
:badge="item.badge"
|
|
174
|
-
:disabled="item.disabled"
|
|
175
|
-
:collapsed="plegada"
|
|
176
|
-
:active="modelValue === item.id"
|
|
177
|
-
@click="elegir(item.id)" />
|
|
178
|
-
</SideGroup>
|
|
179
|
-
|
|
180
|
-
<slot :collapsed="plegada" />
|
|
181
|
-
</div>
|
|
182
|
-
</aside>
|
|
183
|
-
</template>
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
/**
|
|
3
|
-
* Un elemento de la barra lateral.
|
|
4
|
-
*
|
|
5
|
-
* El icono sale del tema del escritorio y se vuelve a resolver cuando la
|
|
6
|
-
* persona cambia de tema: por eso no se recibe una ruta sino un nombre. Plegado
|
|
7
|
-
* queda sólo el icono, y el nombre pasa al `title` para que el globo lo diga —
|
|
8
|
-
* sin eso, una barra plegada es una columna de dibujos sin explicación.
|
|
9
|
-
*/
|
|
10
|
-
import { getIconSource } from '@vasakgroup/plugin-vicons';
|
|
11
|
-
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
|
12
|
-
import { onMounted, onUnmounted, ref, toRef, watch } from 'vue';
|
|
13
|
-
|
|
14
|
-
const props = withDefaults(
|
|
15
|
-
defineProps<{
|
|
16
|
-
label: string;
|
|
17
|
-
icon?: string;
|
|
18
|
-
active?: boolean;
|
|
19
|
-
collapsed?: boolean;
|
|
20
|
-
disabled?: boolean;
|
|
21
|
-
badge?: string | number;
|
|
22
|
-
}>(),
|
|
23
|
-
{ icon: '', active: false, collapsed: false, disabled: false, badge: '' }
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
defineEmits<{ click: [] }>();
|
|
27
|
-
|
|
28
|
-
const fuente = ref('');
|
|
29
|
-
const icono = toRef(props, 'icon');
|
|
30
|
-
let soltar: UnlistenFn | null = null;
|
|
31
|
-
let desmontado = false;
|
|
32
|
-
|
|
33
|
-
/** Cuántas resoluciones se pidieron. De las que estén en vuelo, sólo vale la última. */
|
|
34
|
-
let ultimoPedido = 0;
|
|
35
|
-
|
|
36
|
-
async function resolver() {
|
|
37
|
-
const mio = ++ultimoPedido;
|
|
38
|
-
const nombre = icono.value;
|
|
39
|
-
if (!nombre) {
|
|
40
|
-
fuente.value = '';
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const resuelto = await getIconSource(nombre);
|
|
44
|
-
// Cambiar de icono y cambiar de tema resuelven en paralelo, y el tema tarda
|
|
45
|
-
// lo que tarde el backend. Sin el testigo, la respuesta vieja llega última y
|
|
46
|
-
// deja puesto el icono anterior — que es el mismo síntoma que se venía a
|
|
47
|
-
// evitar, pero intermitente y según cuál tarde más.
|
|
48
|
-
if (mio === ultimoPedido) {
|
|
49
|
-
fuente.value = resuelto;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
onMounted(async () => {
|
|
54
|
-
// El oyente **antes** de la primera resolución. El tema de iconos cambia en
|
|
55
|
-
// caliente, y resolver el primero tarda: un cambio de tema durante esa
|
|
56
|
-
// espera no lo escuchaba nadie, y el botón se quedaba con el icono del tema
|
|
57
|
-
// anterior hasta el cambio siguiente.
|
|
58
|
-
const dejarDeEscuchar = await listen('vicons:theme-changed', resolver);
|
|
59
|
-
// Registrarse tarda, y en una lista que se desplaza un botón puede irse
|
|
60
|
-
// antes de que termine. Ahí `onUnmounted` ya pasó y no vio nada que soltar:
|
|
61
|
-
// el oyente quedaba registrado para siempre sobre un componente muerto.
|
|
62
|
-
if (desmontado) {
|
|
63
|
-
dejarDeEscuchar();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
soltar = dejarDeEscuchar;
|
|
67
|
-
|
|
68
|
-
// Las dos resoluciones pueden cruzarse —ésta y la que dispare un cambio de
|
|
69
|
-
// tema—, y de eso se encarga el testigo de `resolver`.
|
|
70
|
-
await resolver();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
onUnmounted(() => {
|
|
74
|
-
desmontado = true;
|
|
75
|
-
soltar?.();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
watch(icono, resolver);
|
|
79
|
-
</script>
|
|
80
|
-
|
|
81
|
-
<template>
|
|
82
|
-
<button
|
|
83
|
-
type="button"
|
|
84
|
-
:title="collapsed ? label : undefined"
|
|
85
|
-
:aria-label="collapsed ? label : undefined"
|
|
86
|
-
:disabled="disabled"
|
|
87
|
-
:aria-current="active ? 'page' : undefined"
|
|
88
|
-
class="group relative flex w-full items-center gap-3 rounded-corner border px-3 py-2 text-left text-sm transition-all duration-200"
|
|
89
|
-
:class="[
|
|
90
|
-
active
|
|
91
|
-
? 'border-secondary bg-primary/15 text-tx-main shadow-sm'
|
|
92
|
-
: 'border-transparent bg-ui-bg/30 hover:border-ui-border hover:bg-ui-surface/70',
|
|
93
|
-
disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
|
|
94
|
-
collapsed ? 'justify-center px-2' : '',
|
|
95
|
-
]"
|
|
96
|
-
@click="$emit('click')">
|
|
97
|
-
<span
|
|
98
|
-
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-corner font-semibold text-xs uppercase tracking-wide"
|
|
99
|
-
:class="active ? 'border-secondary bg-primary/20' : ''"
|
|
100
|
-
aria-hidden="true">
|
|
101
|
-
<img v-if="fuente" :src="fuente" alt="" class="h-8 w-8 object-contain">
|
|
102
|
-
<!-- Sin icono, la inicial: un hueco vacío del mismo tamaño deja la fila
|
|
103
|
-
desalineada contra las que sí lo tienen. -->
|
|
104
|
-
<span v-else>{{ label.charAt(0).toUpperCase() }}</span>
|
|
105
|
-
</span>
|
|
106
|
-
|
|
107
|
-
<span v-if="!collapsed" class="min-w-0 flex-1 truncate font-medium">{{ label }}</span>
|
|
108
|
-
|
|
109
|
-
<span
|
|
110
|
-
v-if="!collapsed && badge !== ''"
|
|
111
|
-
class="rounded-corner bg-ui-surface px-2 py-0.5 font-semibold text-tx-muted text-xs">
|
|
112
|
-
{{ badge }}
|
|
113
|
-
</span>
|
|
114
|
-
</button>
|
|
115
|
-
</template>
|