adicoonado compoente de dataehora
This commit is contained in:
parent
eca01fca75
commit
fd5c49071c
15 changed files with 1430 additions and 233 deletions
27
IA.md
27
IA.md
|
|
@ -61,7 +61,7 @@ createApp(App)
|
||||||
### 2) Importação direta (quando não quiser plugin)
|
### 2) Importação direta (quando não quiser plugin)
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { EliBotao, EliInput, EliBadge, EliCartao } from "eli-vue";
|
import { EliBotao, EliInput, EliBadge, EliCartao, EliDataHora } from "eli-vue";
|
||||||
```
|
```
|
||||||
|
|
||||||
> Observação: ainda pode ser necessário importar o CSS do pacote:
|
> Observação: ainda pode ser necessário importar o CSS do pacote:
|
||||||
|
|
@ -105,6 +105,30 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Data e hora (entrada) com suporte a UTC/Z
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- Valor chega do backend em ISO 8601 (UTC/offset), e o componente exibe em horário local -->
|
||||||
|
<EliDataHora v-model="dataHora" rotulo="Agendamento" />
|
||||||
|
|
||||||
|
<!-- Somente data -->
|
||||||
|
<EliDataHora v-model="data" modo="data" rotulo="Nascimento" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const dataHora = ref<string | null>("2026-01-09T16:15:00Z");
|
||||||
|
const data = ref<string | null>("2026-01-09T00:00:00-03:00");
|
||||||
|
return { dataHora, data };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting (para IAs)
|
## Troubleshooting (para IAs)
|
||||||
|
|
@ -150,3 +174,4 @@ Atualize este `IA.md` quando houver mudanças em qualquer um destes pontos:
|
||||||
- lista de exports públicos (novos componentes, renomes, remoções)
|
- lista de exports públicos (novos componentes, renomes, remoções)
|
||||||
- mudanças de comportamento relevantes para consumo
|
- mudanças de comportamento relevantes para consumo
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
dist/eli-vue.css
vendored
2
dist/eli-vue.css
vendored
|
|
@ -1 +1 @@
|
||||||
[data-v-de2fbf2f] .v-badge__badge,[data-v-de2fbf2f] .v-badge__content{border-radius:var(--eli-badge-radius)!important}.eli-input[data-v-2f57f5c8]{width:100%}.checkbox-group[data-v-2f57f5c8]{display:flex;gap:8px;flex-wrap:wrap}.cursor-pointer[data-v-2f57f5c8]{cursor:pointer}.eli-cartao[data-v-6c492bd9]{border-radius:12px}.eli-cartao__titulo[data-v-6c492bd9]{display:flex;align-items:center;justify-content:space-between;gap:12px}.eli-cartao__titulo-texto[data-v-6c492bd9]{min-width:0}.eli-cartao__conteudo[data-v-6c492bd9]{padding-top:8px}.eli-cartao__acoes[data-v-6c492bd9]{padding-top:0}.eli-cartao--cancelado[data-v-6c492bd9]{opacity:.85}
|
[data-v-de2fbf2f] .v-badge__badge,[data-v-de2fbf2f] .v-badge__content{border-radius:var(--eli-badge-radius)!important}.eli-input[data-v-2f57f5c8]{width:100%}.checkbox-group[data-v-2f57f5c8]{display:flex;gap:8px;flex-wrap:wrap}.cursor-pointer[data-v-2f57f5c8]{cursor:pointer}.eli-cartao[data-v-6c492bd9]{border-radius:12px}.eli-cartao__titulo[data-v-6c492bd9]{display:flex;align-items:center;justify-content:space-between;gap:12px}.eli-cartao__titulo-texto[data-v-6c492bd9]{min-width:0}.eli-cartao__conteudo[data-v-6c492bd9]{padding-top:8px}.eli-cartao__acoes[data-v-6c492bd9]{padding-top:0}.eli-cartao--cancelado[data-v-6c492bd9]{opacity:.85}.eli-data-hora[data-v-523063f3]{width:100%}
|
||||||
|
|
|
||||||
894
dist/eli-vue.es.js
vendored
894
dist/eli-vue.es.js
vendored
File diff suppressed because it is too large
Load diff
2
dist/eli-vue.umd.js
vendored
2
dist/eli-vue.umd.js
vendored
File diff suppressed because one or more lines are too long
221
dist/types/componentes/data_hora/EliDataHora.vue.d.ts
vendored
Normal file
221
dist/types/componentes/data_hora/EliDataHora.vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import type { CampoDensidade, CampoVariante } from "../../tipos";
|
||||||
|
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||||
|
/**
|
||||||
|
* Valor em ISO 8601:
|
||||||
|
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
|
||||||
|
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: PropType<string | null>;
|
||||||
|
default: null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Define o tipo de entrada.
|
||||||
|
* - `dataHora`: usa `datetime-local`
|
||||||
|
* - `data`: usa `date`
|
||||||
|
*/
|
||||||
|
modo: {
|
||||||
|
type: PropType<"data" | "dataHora">;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Rótulo exibido no v-text-field (Vuetify). */
|
||||||
|
rotulo: {
|
||||||
|
type: StringConstructor;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Placeholder do input. */
|
||||||
|
placeholder: {
|
||||||
|
type: StringConstructor;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Desabilita a interação. */
|
||||||
|
desabilitado: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
|
||||||
|
limpavel: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Estado de erro (visual). */
|
||||||
|
erro: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Mensagens de erro. */
|
||||||
|
mensagensErro: {
|
||||||
|
type: PropType<string | string[]>;
|
||||||
|
default: () => never[];
|
||||||
|
};
|
||||||
|
/** Texto de apoio. */
|
||||||
|
dica: {
|
||||||
|
type: StringConstructor;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Mantém a dica sempre visível. */
|
||||||
|
dicaPersistente: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Densidade do campo (Vuetify). */
|
||||||
|
densidade: {
|
||||||
|
type: PropType<CampoDensidade>;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Variante do v-text-field (Vuetify). */
|
||||||
|
variante: {
|
||||||
|
type: PropType<CampoVariante>;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Valor mínimo permitido.
|
||||||
|
* ISO 8601 (offset ou `Z`).
|
||||||
|
*/
|
||||||
|
min: {
|
||||||
|
type: PropType<string | undefined>;
|
||||||
|
default: undefined;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Valor máximo permitido.
|
||||||
|
* ISO 8601 (offset ou `Z`).
|
||||||
|
*/
|
||||||
|
max: {
|
||||||
|
type: PropType<string | undefined>;
|
||||||
|
default: undefined;
|
||||||
|
};
|
||||||
|
}>, {
|
||||||
|
attrs: {
|
||||||
|
[x: string]: unknown;
|
||||||
|
};
|
||||||
|
valor: import("vue").WritableComputedRef<string, string>;
|
||||||
|
emit: ((event: "update:modelValue", _valor: string | null) => void) & ((event: "alterar", _valor: string | null) => void) & ((event: "foco") => void) & ((event: "desfoco") => void);
|
||||||
|
minLocal: import("vue").ComputedRef<string | undefined>;
|
||||||
|
maxLocal: import("vue").ComputedRef<string | undefined>;
|
||||||
|
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
|
||||||
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||||
|
/** v-model padrão. */
|
||||||
|
"update:modelValue": (_valor: string | null) => true;
|
||||||
|
/** Alias para consumidores que querem um evento semântico. */
|
||||||
|
alterar: (_valor: string | null) => true;
|
||||||
|
foco: () => true;
|
||||||
|
desfoco: () => true;
|
||||||
|
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||||
|
/**
|
||||||
|
* Valor em ISO 8601:
|
||||||
|
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
|
||||||
|
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: PropType<string | null>;
|
||||||
|
default: null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Define o tipo de entrada.
|
||||||
|
* - `dataHora`: usa `datetime-local`
|
||||||
|
* - `data`: usa `date`
|
||||||
|
*/
|
||||||
|
modo: {
|
||||||
|
type: PropType<"data" | "dataHora">;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Rótulo exibido no v-text-field (Vuetify). */
|
||||||
|
rotulo: {
|
||||||
|
type: StringConstructor;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Placeholder do input. */
|
||||||
|
placeholder: {
|
||||||
|
type: StringConstructor;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Desabilita a interação. */
|
||||||
|
desabilitado: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
|
||||||
|
limpavel: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Estado de erro (visual). */
|
||||||
|
erro: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Mensagens de erro. */
|
||||||
|
mensagensErro: {
|
||||||
|
type: PropType<string | string[]>;
|
||||||
|
default: () => never[];
|
||||||
|
};
|
||||||
|
/** Texto de apoio. */
|
||||||
|
dica: {
|
||||||
|
type: StringConstructor;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Mantém a dica sempre visível. */
|
||||||
|
dicaPersistente: {
|
||||||
|
type: BooleanConstructor;
|
||||||
|
default: boolean;
|
||||||
|
};
|
||||||
|
/** Densidade do campo (Vuetify). */
|
||||||
|
densidade: {
|
||||||
|
type: PropType<CampoDensidade>;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/** Variante do v-text-field (Vuetify). */
|
||||||
|
variante: {
|
||||||
|
type: PropType<CampoVariante>;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Valor mínimo permitido.
|
||||||
|
* ISO 8601 (offset ou `Z`).
|
||||||
|
*/
|
||||||
|
min: {
|
||||||
|
type: PropType<string | undefined>;
|
||||||
|
default: undefined;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Valor máximo permitido.
|
||||||
|
* ISO 8601 (offset ou `Z`).
|
||||||
|
*/
|
||||||
|
max: {
|
||||||
|
type: PropType<string | undefined>;
|
||||||
|
default: undefined;
|
||||||
|
};
|
||||||
|
}>> & Readonly<{
|
||||||
|
"onUpdate:modelValue"?: ((_valor: string | null) => any) | undefined;
|
||||||
|
onAlterar?: ((_valor: string | null) => any) | undefined;
|
||||||
|
onFoco?: (() => any) | undefined;
|
||||||
|
onDesfoco?: (() => any) | undefined;
|
||||||
|
}>, {
|
||||||
|
placeholder: string;
|
||||||
|
modelValue: string | null;
|
||||||
|
modo: "data" | "dataHora";
|
||||||
|
rotulo: string;
|
||||||
|
desabilitado: boolean;
|
||||||
|
limpavel: boolean;
|
||||||
|
erro: boolean;
|
||||||
|
mensagensErro: string | string[];
|
||||||
|
dica: string;
|
||||||
|
dicaPersistente: boolean;
|
||||||
|
densidade: CampoDensidade;
|
||||||
|
variante: CampoVariante;
|
||||||
|
min: string | undefined;
|
||||||
|
max: string | undefined;
|
||||||
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||||
|
/**
|
||||||
|
* EliDataHora
|
||||||
|
*
|
||||||
|
* Campo para entrada de data + hora.
|
||||||
|
*
|
||||||
|
* Modelo:
|
||||||
|
* - O componente **recebe** `modelValue` em ISO 8601 (UTC `Z` ou com offset).
|
||||||
|
* - Converte para horário local para exibir no `datetime-local`.
|
||||||
|
* - Ao editar, **emite** ISO 8601 com o **offset local**.
|
||||||
|
*/
|
||||||
|
declare const _default: typeof __VLS_export;
|
||||||
|
export default _default;
|
||||||
1
dist/types/componentes/data_hora/index.d.ts
vendored
Normal file
1
dist/types/componentes/data_hora/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as EliDataHora } from "./EliDataHora.vue";
|
||||||
2
dist/types/index.d.ts
vendored
2
dist/types/index.d.ts
vendored
|
|
@ -4,10 +4,12 @@ import { EliBotao } from "./componentes/botao";
|
||||||
import { EliBadge } from "./componentes/indicador";
|
import { EliBadge } from "./componentes/indicador";
|
||||||
import { EliInput } from "./componentes/campo";
|
import { EliInput } from "./componentes/campo";
|
||||||
import { EliCartao } from "./componentes/cartao";
|
import { EliCartao } from "./componentes/cartao";
|
||||||
|
import { EliDataHora } from "./componentes/data_hora";
|
||||||
export { EliOlaMundo };
|
export { EliOlaMundo };
|
||||||
export { EliBotao };
|
export { EliBotao };
|
||||||
export { EliBadge };
|
export { EliBadge };
|
||||||
export { EliInput };
|
export { EliInput };
|
||||||
export { EliCartao };
|
export { EliCartao };
|
||||||
|
export { EliDataHora };
|
||||||
declare const EliVue: Plugin;
|
declare const EliVue: Plugin;
|
||||||
export default EliVue;
|
export default EliVue;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "eli-vue",
|
"name": "eli-vue",
|
||||||
"version": "0.1.6",
|
"version": "0.1.13",
|
||||||
"private": false,
|
"private": false,
|
||||||
"main": "./dist/eli-vue.umd.js",
|
"main": "./dist/eli-vue.umd.js",
|
||||||
"module": "./dist/eli-vue.es.js",
|
"module": "./dist/eli-vue.es.js",
|
||||||
|
|
@ -33,5 +33,8 @@
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-tsc": "^3.1.6",
|
"vue-tsc": "^3.1.6",
|
||||||
"vuetify": "^3.11.2"
|
"vuetify": "^3.11.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
|
@ -7,6 +7,10 @@ settings:
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
dayjs:
|
||||||
|
specifier: ^1.11.19
|
||||||
|
version: 1.11.19
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@mdi/font':
|
'@mdi/font':
|
||||||
specifier: ^7.4.47
|
specifier: ^7.4.47
|
||||||
|
|
@ -488,6 +492,9 @@ packages:
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
dayjs@1.11.19:
|
||||||
|
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
@ -1037,6 +1044,8 @@ snapshots:
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
dayjs@1.11.19: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
|
||||||
231
src/componentes/data_hora/EliDataHora.vue
Normal file
231
src/componentes/data_hora/EliDataHora.vue
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
<template>
|
||||||
|
<div class="eli-data-hora">
|
||||||
|
<!--
|
||||||
|
Implementação propositalmente “simples” e estável:
|
||||||
|
- Usa o input nativo `datetime-local` dentro do v-text-field.
|
||||||
|
- Evita depender de componentes experimentais (labs) do Vuetify.
|
||||||
|
- Mantém v-model como string ISO local: `YYYY-MM-DDTHH:mm`.
|
||||||
|
|
||||||
|
Observação importante:
|
||||||
|
- `datetime-local` NÃO armazena timezone.
|
||||||
|
- Se o projeto precisar persistir em UTC, converta no consumidor.
|
||||||
|
-->
|
||||||
|
<v-text-field
|
||||||
|
v-model="valor"
|
||||||
|
:type="tipoInput"
|
||||||
|
:label="rotulo"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="desabilitado"
|
||||||
|
:clearable="limpavel"
|
||||||
|
:error="erro"
|
||||||
|
:error-messages="mensagensErro"
|
||||||
|
:hint="dica"
|
||||||
|
:persistent-hint="dicaPersistente"
|
||||||
|
:density="densidade"
|
||||||
|
:variant="variante"
|
||||||
|
:min="minLocal"
|
||||||
|
:max="maxLocal"
|
||||||
|
v-bind="attrs"
|
||||||
|
@focus="emit('foco')"
|
||||||
|
@blur="emit('desfoco')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from "vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import type { CampoDensidade, CampoVariante } from "../../tipos";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EliDataHora
|
||||||
|
*
|
||||||
|
* Campo para entrada de data + hora.
|
||||||
|
*
|
||||||
|
* Modelo:
|
||||||
|
* - O componente **recebe** `modelValue` em ISO 8601 (UTC `Z` ou com offset).
|
||||||
|
* - Converte para horário local para exibir no `datetime-local`.
|
||||||
|
* - Ao editar, **emite** ISO 8601 com o **offset local**.
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliDataHora",
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Valor em ISO 8601:
|
||||||
|
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
|
||||||
|
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define o tipo de entrada.
|
||||||
|
* - `dataHora`: usa `datetime-local`
|
||||||
|
* - `data`: usa `date`
|
||||||
|
*/
|
||||||
|
modo: {
|
||||||
|
type: String as PropType<"data" | "dataHora">,
|
||||||
|
default: "dataHora",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Rótulo exibido no v-text-field (Vuetify). */
|
||||||
|
rotulo: {
|
||||||
|
type: String,
|
||||||
|
default: "Data e hora",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Placeholder do input. */
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Desabilita a interação. */
|
||||||
|
desabilitado: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
|
||||||
|
limpavel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Estado de erro (visual). */
|
||||||
|
erro: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Mensagens de erro. */
|
||||||
|
mensagensErro: {
|
||||||
|
type: [String, Array] as PropType<string | string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Texto de apoio. */
|
||||||
|
dica: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Mantém a dica sempre visível. */
|
||||||
|
dicaPersistente: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Densidade do campo (Vuetify). */
|
||||||
|
densidade: {
|
||||||
|
type: String as PropType<CampoDensidade>,
|
||||||
|
default: "comfortable",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Variante do v-text-field (Vuetify). */
|
||||||
|
variante: {
|
||||||
|
type: String as PropType<CampoVariante>,
|
||||||
|
default: "outlined",
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valor mínimo permitido.
|
||||||
|
* ISO 8601 (offset ou `Z`).
|
||||||
|
*/
|
||||||
|
min: {
|
||||||
|
// ISO 8601 (offset ou Z)
|
||||||
|
type: String as PropType<string | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valor máximo permitido.
|
||||||
|
* ISO 8601 (offset ou `Z`).
|
||||||
|
*/
|
||||||
|
max: {
|
||||||
|
// ISO 8601 (offset ou Z)
|
||||||
|
type: String as PropType<string | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: {
|
||||||
|
/** v-model padrão. */
|
||||||
|
"update:modelValue": (_valor: string | null) => true,
|
||||||
|
/** Alias para consumidores que querem um evento semântico. */
|
||||||
|
alterar: (_valor: string | null) => true,
|
||||||
|
foco: () => true,
|
||||||
|
desfoco: () => true,
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props, { emit, attrs }) {
|
||||||
|
const tipoInput = computed<"date" | "datetime-local">(() =>
|
||||||
|
props.modo === "data" ? "date" : "datetime-local"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Converte ISO (Z/offset) para o formato que o `datetime-local` aceita.
|
||||||
|
function isoParaInputDatetime(valorIso: string): string {
|
||||||
|
// `dayjs(valorIso)` interpreta ISO com timezone e converte para o local do usuário.
|
||||||
|
if (props.modo === "data") {
|
||||||
|
return dayjs(valorIso).format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(valorIso).format("YYYY-MM-DDTHH:mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converte o valor do input (`YYYY-MM-DDTHH:mm`) para ISO 8601 com offset local.
|
||||||
|
function inputDatetimeParaIsoLocal(valorInput: string): string {
|
||||||
|
// `format()` retorna ISO 8601 com offset local.
|
||||||
|
// Em modo `data`, normalizamos para o começo do dia (00:00:00) no fuso local.
|
||||||
|
if (props.modo === "data") {
|
||||||
|
return dayjs(`${valorInput}T00:00`).format();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(valorInput).format();
|
||||||
|
}
|
||||||
|
|
||||||
|
const valor = computed<string>({
|
||||||
|
get: () => {
|
||||||
|
if (!props.modelValue) return "";
|
||||||
|
return isoParaInputDatetime(props.modelValue);
|
||||||
|
},
|
||||||
|
set: (v) => {
|
||||||
|
// O `datetime-local` entrega string ou "" quando limpo.
|
||||||
|
const normalizado = v && v.length > 0 ? v : null;
|
||||||
|
if (!normalizado) {
|
||||||
|
emit("update:modelValue", null);
|
||||||
|
emit("alterar", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valorEmitido = inputDatetimeParaIsoLocal(normalizado);
|
||||||
|
emit("update:modelValue", valorEmitido);
|
||||||
|
emit("alterar", valorEmitido);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const minLocal = computed<string | undefined>(() => {
|
||||||
|
if (!props.min) return undefined;
|
||||||
|
return isoParaInputDatetime(props.min);
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxLocal = computed<string | undefined>(() => {
|
||||||
|
if (!props.max) return undefined;
|
||||||
|
return isoParaInputDatetime(props.max);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { attrs, valor, emit, minLocal, maxLocal, tipoInput };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.eli-data-hora {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
108
src/componentes/data_hora/README.md
Normal file
108
src/componentes/data_hora/README.md
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
# EliDataHora
|
||||||
|
|
||||||
|
O `EliDataHora` é um componente de **entrada de data e hora** baseado em `v-text-field` (Vuetify), usando o tipo nativo do HTML `datetime-local`.
|
||||||
|
|
||||||
|
Ele foi criado para oferecer uma solução **estável e leve** sem depender de componentes experimentais do Vuetify.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
- Permitir o usuário selecionar **data + hora** com UX nativa do navegador.
|
||||||
|
- Padronizar a API em português (props/eventos) no Design System.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Tipo | Padrão | Descrição |
|
||||||
|
|------|------|--------|-----------|
|
||||||
|
| `modelValue` | `string \| null` | `null` | **Sempre em ISO 8601**, aceitando UTC absoluto (`Z`) ou com offset (ex.: `2026-01-09T16:15:00Z`, `2026-01-09T13:15:00-03:00`). O componente converte para horário **local** antes de exibir. |
|
||||||
|
| `modo` | `"data" \| "dataHora"` | `"dataHora"` | Define se o campo permite selecionar apenas data (`date`) ou data+hora (`datetime-local`). |
|
||||||
|
| `rotulo` | `string` | `"Data e hora"` | Label do campo. |
|
||||||
|
| `placeholder` | `string` | `""` | Placeholder do campo. |
|
||||||
|
| `desabilitado` | `boolean` | `false` | Desabilita o campo. |
|
||||||
|
| `limpavel` | `boolean` | `false` | Habilita botão de limpar (Vuetify `clearable`). |
|
||||||
|
| `erro` | `boolean` | `false` | Estado de erro visual. |
|
||||||
|
| `mensagensErro` | `string \| string[]` | `[]` | Mensagens de erro. |
|
||||||
|
| `dica` | `string` | `""` | Hint/ajuda abaixo do campo. |
|
||||||
|
| `dicaPersistente` | `boolean` | `false` | Mantém dica sempre visível. |
|
||||||
|
| `densidade` | `CampoDensidade` | `"comfortable"` | Densidade (Vuetify). |
|
||||||
|
| `variante` | `CampoVariante` | `"outlined"` | Variante (Vuetify). |
|
||||||
|
| `min` | `string \| undefined` | `undefined` | Mínimo permitido em ISO 8601 (offset ou `Z`). |
|
||||||
|
| `max` | `string \| undefined` | `undefined` | Máximo permitido em ISO 8601 (offset ou `Z`). |
|
||||||
|
|
||||||
|
> Observação: o atributo HTML `datetime-local` **não inclui timezone**.
|
||||||
|
> Este componente resolve isso convertendo:
|
||||||
|
>
|
||||||
|
> - **entrada**: ISO 8601 (UTC/offset) → **exibição** em horário local
|
||||||
|
> - **saída**: valor selecionado → ISO 8601 com **offset local**
|
||||||
|
|
||||||
|
### Emits
|
||||||
|
|
||||||
|
| Evento | Payload | Quando dispara |
|
||||||
|
|--------|---------|---------------|
|
||||||
|
| `update:modelValue` | `string \| null` | Sempre que o valor muda (padrão do v-model). O payload é ISO 8601 com **offset local**. |
|
||||||
|
| `alterar` | `string \| null` | Alias semântico para mudanças de valor (mesmo payload do v-model). |
|
||||||
|
| `foco` | `void` | Ao focar o campo. |
|
||||||
|
| `desfoco` | `void` | Ao sair do foco. |
|
||||||
|
|
||||||
|
### Slots
|
||||||
|
|
||||||
|
Este componente não define slots próprios. Você pode usar slots do `v-text-field` via `v-bind="$attrs"` caso precise (ver exemplos abaixo).
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
### 1) Uso básico com v-model
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<EliDataHora v-model="dataHora" />
|
||||||
|
<div class="text-caption">Valor: {{ dataHora }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { EliDataHora } from "eli-vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { EliDataHora },
|
||||||
|
setup() {
|
||||||
|
const dataHora = ref<string | null>("2026-01-09T16:15:00Z");
|
||||||
|
return { dataHora };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Com limites (min/max) e validação visual
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<EliDataHora
|
||||||
|
v-model="dataHora"
|
||||||
|
rotulo="Agendar"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:erro="!dataHora"
|
||||||
|
:mensagensErro="!dataHora ? ['Obrigatório'] : []"
|
||||||
|
limpavel
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Casos de borda / comportamento esperado
|
||||||
|
|
||||||
|
- Ao limpar o campo, o componente emite `null` (não string vazia).
|
||||||
|
- O navegador pode variar a UI do seletor (isso é esperado do `datetime-local`).
|
||||||
|
- `min/max` devem ser strings em ISO 8601 (offset ou `Z`).
|
||||||
|
- Em `modo="data"`, o componente emite ISO no **início do dia** (`00:00:00`) no fuso local.
|
||||||
|
|
||||||
|
## Acessibilidade
|
||||||
|
|
||||||
|
- O `v-text-field` do Vuetify já oferece base de acessibilidade.
|
||||||
|
- Sempre prefira passar `rotulo` significativo.
|
||||||
|
|
||||||
|
## Decisões de implementação
|
||||||
|
|
||||||
|
- Usamos `datetime-local` por ser amplamente suportado e não depender de APIs experimentais.
|
||||||
|
- O componente usa `dayjs` para converter entradas UTC/offset para local antes de exibir e para emitir ISO 8601 com offset local.
|
||||||
|
- Mantemos o valor como `string | null` para evitar conversões implícitas e permitir que cada projeto decida como persistir (UTC/local).
|
||||||
1
src/componentes/data_hora/index.ts
Normal file
1
src/componentes/data_hora/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as EliDataHora } from "./EliDataHora.vue";
|
||||||
|
|
@ -4,12 +4,14 @@ import { EliBotao } from "./componentes/botao";
|
||||||
import { EliBadge } from "./componentes/indicador";
|
import { EliBadge } from "./componentes/indicador";
|
||||||
import { EliInput } from "./componentes/campo";
|
import { EliInput } from "./componentes/campo";
|
||||||
import { EliCartao } from "./componentes/cartao";
|
import { EliCartao } from "./componentes/cartao";
|
||||||
|
import { EliDataHora } from "./componentes/data_hora";
|
||||||
|
|
||||||
export { EliOlaMundo };
|
export { EliOlaMundo };
|
||||||
export { EliBotao };
|
export { EliBotao };
|
||||||
export { EliBadge };
|
export { EliBadge };
|
||||||
export { EliInput };
|
export { EliInput };
|
||||||
export { EliCartao };
|
export { EliCartao };
|
||||||
|
export { EliDataHora };
|
||||||
|
|
||||||
const EliVue: Plugin = {
|
const EliVue: Plugin = {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
|
|
@ -18,6 +20,7 @@ const EliVue: Plugin = {
|
||||||
app.component("EliBadge", EliBadge);
|
app.component("EliBadge", EliBadge);
|
||||||
app.component("EliInput", EliInput);
|
app.component("EliInput", EliInput);
|
||||||
app.component("EliCartao", EliCartao);
|
app.component("EliCartao", EliCartao);
|
||||||
|
app.component("EliDataHora", EliDataHora);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<v-tab value="indicador">Indicador</v-tab>
|
<v-tab value="indicador">Indicador</v-tab>
|
||||||
<v-tab value="cartao">Cartão</v-tab>
|
<v-tab value="cartao">Cartão</v-tab>
|
||||||
<v-tab value="campo">Campo</v-tab>
|
<v-tab value="campo">Campo</v-tab>
|
||||||
|
<v-tab value="data_hora">Data e hora</v-tab>
|
||||||
<v-tab value="ola_mundo">Demo</v-tab>
|
<v-tab value="ola_mundo">Demo</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
||||||
<CartaoPlayground v-else-if="aba === 'cartao'" />
|
<CartaoPlayground v-else-if="aba === 'cartao'" />
|
||||||
<CampoPlayground v-else-if="aba === 'campo'" />
|
<CampoPlayground v-else-if="aba === 'campo'" />
|
||||||
|
<DataHoraPlayground v-else-if="aba === 'data_hora'" />
|
||||||
<OlaMundoPlayground v-else />
|
<OlaMundoPlayground v-else />
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -26,6 +28,7 @@ import BotaoPlayground from "./botao.playground.vue";
|
||||||
import IndicadorPlayground from "./indicador.playground.vue";
|
import IndicadorPlayground from "./indicador.playground.vue";
|
||||||
import CartaoPlayground from "./cartao.playground.vue";
|
import CartaoPlayground from "./cartao.playground.vue";
|
||||||
import CampoPlayground from "./campo.playground.vue";
|
import CampoPlayground from "./campo.playground.vue";
|
||||||
|
import DataHoraPlayground from "./data_hora.playground.vue";
|
||||||
import OlaMundoPlayground from "./ola_mundo.playground.vue";
|
import OlaMundoPlayground from "./ola_mundo.playground.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -35,11 +38,18 @@ export default defineComponent({
|
||||||
IndicadorPlayground,
|
IndicadorPlayground,
|
||||||
CartaoPlayground,
|
CartaoPlayground,
|
||||||
CampoPlayground,
|
CampoPlayground,
|
||||||
|
DataHoraPlayground,
|
||||||
OlaMundoPlayground,
|
OlaMundoPlayground,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
aba: "botao" as "botao" | "indicador" | "cartao" | "campo" | "ola_mundo",
|
aba: "botao" as
|
||||||
|
| "botao"
|
||||||
|
| "indicador"
|
||||||
|
| "cartao"
|
||||||
|
| "campo"
|
||||||
|
| "data_hora"
|
||||||
|
| "ola_mundo",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
145
src/playground/data_hora.playground.vue
Normal file
145
src/playground/data_hora.playground.vue
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<template>
|
||||||
|
<section class="stack">
|
||||||
|
<h2>EliDataHora (entrada de data e hora)</h2>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<!-- variação 1: padrão -->
|
||||||
|
<EliDataHora v-model="dataHora" />
|
||||||
|
|
||||||
|
<!-- variação 1a: somente data -->
|
||||||
|
<EliDataHora
|
||||||
|
v-model="somenteData"
|
||||||
|
modo="data"
|
||||||
|
rotulo="Somente data"
|
||||||
|
dica="Emite ISO com offset local às 00:00:00"
|
||||||
|
dica-persistente
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- variação 1b: recebendo UTC (Z) e exibindo local -->
|
||||||
|
<EliDataHora
|
||||||
|
v-model="dataHoraUtcEntrada"
|
||||||
|
rotulo="Entrada vinda do backend (UTC/Z)"
|
||||||
|
dica="O valor chega em UTC, mas o campo mostra em horário local"
|
||||||
|
dica-persistente
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- variação 2: com limites min/max -->
|
||||||
|
<EliDataHora
|
||||||
|
v-model="dataHoraComLimite"
|
||||||
|
rotulo="Agendamento (com limite)"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
dica="Escolha um horário dentro do intervalo"
|
||||||
|
dica-persistente
|
||||||
|
limpavel
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- variação 3: estado de erro + desabilitado -->
|
||||||
|
<EliDataHora
|
||||||
|
v-model="dataHoraObrigatoria"
|
||||||
|
rotulo="Obrigatório"
|
||||||
|
:erro="!dataHoraObrigatoria"
|
||||||
|
:mensagens-erro="!dataHoraObrigatoria ? ['Obrigatório'] : []"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliDataHora
|
||||||
|
v-model="dataHoraDesabilitado"
|
||||||
|
rotulo="Desabilitado"
|
||||||
|
desabilitado
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- variação 4: saída sempre em ISO com offset local -->
|
||||||
|
<EliDataHora
|
||||||
|
v-model="dataHoraUtcEmitida"
|
||||||
|
rotulo="Emitindo ISO com offset local"
|
||||||
|
dica="Ao alterar, o v-model vira ISO 8601 com offset do seu fuso"
|
||||||
|
dica-persistente
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre class="debug">{{ debug }}</pre>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from "vue";
|
||||||
|
import { EliDataHora } from "@/componentes/data_hora";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "DataHoraPlayground",
|
||||||
|
components: { EliDataHora },
|
||||||
|
setup() {
|
||||||
|
const dataHora = ref<string | null>("2026-01-09T13:15:00-03:00");
|
||||||
|
|
||||||
|
const somenteData = ref<string | null>("2026-01-09T00:00:00-03:00");
|
||||||
|
|
||||||
|
// Valor vindo do backend em UTC/Z (exibir local, manter como UTC no estado)
|
||||||
|
const dataHoraUtcEntrada = ref<string | null>("2026-01-09T16:15:00Z");
|
||||||
|
|
||||||
|
// Exemplo com min/max
|
||||||
|
const min = ref("2026-01-09T08:00:00-03:00");
|
||||||
|
const max = ref("2026-01-09T18:00:00-03:00");
|
||||||
|
const dataHoraComLimite = ref<string | null>("2026-01-09T09:00:00-03:00");
|
||||||
|
|
||||||
|
// Obrigatório (mostra erro quando null)
|
||||||
|
const dataHoraObrigatoria = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Desabilitado
|
||||||
|
const dataHoraDesabilitado = ref<string | null>("2026-01-09T10:30:00-03:00");
|
||||||
|
|
||||||
|
// Saída: sempre ISO com offset local
|
||||||
|
const dataHoraUtcEmitida = ref<string | null>("2026-01-09T16:15:00Z");
|
||||||
|
|
||||||
|
const debug = computed(() =>
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
dataHora: dataHora.value,
|
||||||
|
somenteData: somenteData.value,
|
||||||
|
dataHoraUtcEntrada: dataHoraUtcEntrada.value,
|
||||||
|
dataHoraComLimite: dataHoraComLimite.value,
|
||||||
|
dataHoraObrigatoria: dataHoraObrigatoria.value,
|
||||||
|
dataHoraDesabilitado: dataHoraDesabilitado.value,
|
||||||
|
dataHoraUtcEmitida: dataHoraUtcEmitida.value,
|
||||||
|
min: min.value,
|
||||||
|
max: max.value,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataHora,
|
||||||
|
somenteData,
|
||||||
|
dataHoraUtcEntrada,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
dataHoraComLimite,
|
||||||
|
dataHoraObrigatoria,
|
||||||
|
dataHoraDesabilitado,
|
||||||
|
dataHoraUtcEmitida,
|
||||||
|
debug,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug {
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue