bkp
This commit is contained in:
parent
de7c19be24
commit
6aedf2469f
20 changed files with 2032 additions and 1541 deletions
220
src/componentes/EliEntrada/EliEntradaDataHora.vue
Normal file
220
src/componentes/EliEntrada/EliEntradaDataHora.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<div class="eli-data-hora">
|
||||
<v-text-field
|
||||
v-model="valor"
|
||||
:type="tipoInput"
|
||||
:label="opcoesEfetivas.rotulo"
|
||||
:placeholder="opcoesEfetivas.placeholder"
|
||||
:disabled="desabilitadoEfetivo"
|
||||
:clearable="Boolean(opcoesEfetivas.limpavel)"
|
||||
:error="Boolean(opcoesEfetivas.erro)"
|
||||
:error-messages="opcoesEfetivas.mensagensErro"
|
||||
:hint="opcoesEfetivas.dica"
|
||||
:persistent-hint="Boolean(opcoesEfetivas.dicaPersistente)"
|
||||
:density="opcoesEfetivas.densidade ?? 'comfortable'"
|
||||
:variant="opcoesEfetivas.variante ?? 'outlined'"
|
||||
:min="minLocal"
|
||||
:max="maxLocal"
|
||||
v-bind="attrs"
|
||||
@focus="emitCompatFocus"
|
||||
@blur="emitCompatBlur"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import type { CampoDensidade, CampoVariante } from "../../tipos";
|
||||
import type { PadroesEntradas } from "./tiposEntradas";
|
||||
|
||||
type EntradaDataHora = PadroesEntradas["dataHora"];
|
||||
|
||||
type PropsAntigas = {
|
||||
modo?: "data" | "dataHora";
|
||||
rotulo?: string;
|
||||
placeholder?: string;
|
||||
desabilitado?: boolean;
|
||||
limpavel?: boolean;
|
||||
erro?: boolean;
|
||||
mensagensErro?: string | string[];
|
||||
dica?: string;
|
||||
dicaPersistente?: boolean;
|
||||
densidade?: CampoDensidade;
|
||||
variante?: CampoVariante;
|
||||
min?: string;
|
||||
max?: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliEntradaDataHora",
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
// --- Novo padrão EliEntrada ---
|
||||
value: {
|
||||
type: String as PropType<EntradaDataHora["value"]>,
|
||||
default: undefined,
|
||||
},
|
||||
opcoes: {
|
||||
type: Object as PropType<EntradaDataHora["opcoes"]>,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
// --- Compatibilidade com componente antigo EliDataHora ---
|
||||
modelValue: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
modo: { type: String as PropType<PropsAntigas["modo"]>, default: undefined },
|
||||
rotulo: { type: String, default: undefined },
|
||||
placeholder: { type: String, default: undefined },
|
||||
desabilitado: { type: Boolean, default: undefined },
|
||||
limpavel: { type: Boolean, default: undefined },
|
||||
erro: { type: Boolean, default: undefined },
|
||||
mensagensErro: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: undefined,
|
||||
},
|
||||
dica: { type: String, default: undefined },
|
||||
dicaPersistente: { type: Boolean, default: undefined },
|
||||
densidade: { type: String as PropType<CampoDensidade>, default: undefined },
|
||||
variante: { type: String as PropType<CampoVariante>, default: undefined },
|
||||
min: { type: String as PropType<string | undefined>, default: undefined },
|
||||
max: { type: String as PropType<string | undefined>, default: undefined },
|
||||
},
|
||||
emits: {
|
||||
// Novo padrão
|
||||
"update:value": (_v: string | null) => true,
|
||||
input: (_v: string | null) => true, // compat Vue2
|
||||
change: (_v: string | null) => true,
|
||||
|
||||
// Compat antigo
|
||||
"update:modelValue": (_v: string | null) => true,
|
||||
alterar: (_v: string | null) => true,
|
||||
foco: () => true,
|
||||
desfoco: () => true,
|
||||
focus: () => true,
|
||||
blur: () => true,
|
||||
},
|
||||
setup(props, { emit, attrs }) {
|
||||
const opcoesEfetivas = computed<EntradaDataHora["opcoes"]>(() => {
|
||||
// 1) se veio `opcoes` (novo), usa
|
||||
if (props.opcoes) return props.opcoes;
|
||||
|
||||
// 2) fallback: constrói a partir das props antigas
|
||||
return {
|
||||
rotulo: props.rotulo ?? "Data e hora",
|
||||
placeholder: props.placeholder ?? "",
|
||||
modo: props.modo ?? "dataHora",
|
||||
limpavel: props.limpavel,
|
||||
erro: props.erro,
|
||||
mensagensErro: props.mensagensErro,
|
||||
dica: props.dica,
|
||||
dicaPersistente: props.dicaPersistente,
|
||||
densidade: props.densidade,
|
||||
variante: props.variante,
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
};
|
||||
});
|
||||
|
||||
const modoEfetivo = computed<"data" | "dataHora">(
|
||||
() => opcoesEfetivas.value.modo ?? "dataHora"
|
||||
);
|
||||
|
||||
const desabilitadoEfetivo = computed<boolean>(() => Boolean(props.desabilitado));
|
||||
|
||||
const tipoInput = computed<"date" | "datetime-local">(() =>
|
||||
modoEfetivo.value === "data" ? "date" : "datetime-local"
|
||||
);
|
||||
|
||||
function isoParaInputDatetime(valorIso: string): string {
|
||||
if (modoEfetivo.value === "data") {
|
||||
return dayjs(valorIso).format("YYYY-MM-DD");
|
||||
}
|
||||
return dayjs(valorIso).format("YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
function inputDatetimeParaIsoLocal(valorInput: string): string {
|
||||
if (modoEfetivo.value === "data") {
|
||||
return dayjs(`${valorInput}T00:00`).format();
|
||||
}
|
||||
return dayjs(valorInput).format();
|
||||
}
|
||||
|
||||
const effectiveModelValue = computed<string | null>(() => {
|
||||
// Prioridade: value (novo) se vier definido; senão usa modelValue (antigo)
|
||||
return props.value !== undefined ? (props.value ?? null) : props.modelValue;
|
||||
});
|
||||
|
||||
const valor = computed<string>({
|
||||
get: () => {
|
||||
if (!effectiveModelValue.value) return "";
|
||||
return isoParaInputDatetime(effectiveModelValue.value);
|
||||
},
|
||||
set: (v) => {
|
||||
const normalizado = v && v.length > 0 ? v : null;
|
||||
if (!normalizado) {
|
||||
emit("update:value", null);
|
||||
emit("input", null);
|
||||
emit("change", null);
|
||||
|
||||
emit("update:modelValue", null);
|
||||
emit("alterar", null);
|
||||
return;
|
||||
}
|
||||
|
||||
const valorEmitido = inputDatetimeParaIsoLocal(normalizado);
|
||||
|
||||
emit("update:value", valorEmitido);
|
||||
emit("input", valorEmitido);
|
||||
emit("change", valorEmitido);
|
||||
|
||||
emit("update:modelValue", valorEmitido);
|
||||
emit("alterar", valorEmitido);
|
||||
},
|
||||
});
|
||||
|
||||
const minLocal = computed<string | undefined>(() => {
|
||||
const min = opcoesEfetivas.value.min;
|
||||
if (!min) return undefined;
|
||||
return isoParaInputDatetime(min);
|
||||
});
|
||||
|
||||
const maxLocal = computed<string | undefined>(() => {
|
||||
const max = opcoesEfetivas.value.max;
|
||||
if (!max) return undefined;
|
||||
return isoParaInputDatetime(max);
|
||||
});
|
||||
|
||||
function emitCompatFocus() {
|
||||
emit("foco");
|
||||
emit("focus");
|
||||
}
|
||||
|
||||
function emitCompatBlur() {
|
||||
emit("desfoco");
|
||||
emit("blur");
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
valor,
|
||||
tipoInput,
|
||||
minLocal,
|
||||
maxLocal,
|
||||
opcoesEfetivas,
|
||||
desabilitadoEfetivo,
|
||||
emitCompatFocus,
|
||||
emitCompatBlur,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.eli-data-hora {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
176
src/componentes/EliEntrada/README.md
Normal file
176
src/componentes/EliEntrada/README.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# EliEntrada (Padrão de Entradas)
|
||||
|
||||
Esta pasta define o **padrão EliEntrada**: um conjunto de componentes de entrada (inputs) com uma **API uniforme**.
|
||||
|
||||
> TL;DR
|
||||
> - Toda entrada recebe **`value`** (estado) e **`opcoes`** (configuração).
|
||||
> - O padrão de uso é **`v-model:value`**.
|
||||
> - Mantemos compatibilidade com Vue 2 via evento **`input`**.
|
||||
|
||||
---
|
||||
|
||||
## Para humanos (uso no dia-a-dia)
|
||||
|
||||
### Conceito
|
||||
|
||||
Um componente **EliEntrada** recebe **duas propriedades**:
|
||||
|
||||
- `value`: o valor atual do campo (entrada e saída)
|
||||
- `opcoes`: um objeto que configura o componente (rótulo, placeholder e opções específicas do tipo)
|
||||
|
||||
Essa padronização facilita:
|
||||
- gerar formulários dinamicamente
|
||||
- trocar tipos de entrada com o mínimo de refactor
|
||||
- documentar e tipar de forma previsível
|
||||
|
||||
### Tipos e contratos
|
||||
|
||||
Os contratos ficam em: [`tiposEntradas.ts`](./tiposEntradas.ts)
|
||||
|
||||
- `PadroesEntradas`: mapa de tipos suportados (ex.: `texto`, `numero`, `dataHora`)
|
||||
- `TipoEntrada`: união das chaves do mapa (ex.: `"texto" | "numero" | "dataHora"`)
|
||||
|
||||
### Componentes disponíveis
|
||||
|
||||
#### 1) `EliEntradaTexto`
|
||||
|
||||
**Value**: `string | null | undefined`
|
||||
|
||||
**Opções** (além de `rotulo`/`placeholder`):
|
||||
- `limiteCaracteres?: number`
|
||||
|
||||
Exemplo:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<EliEntradaTexto
|
||||
v-model:value="nome"
|
||||
:opcoes="{ rotulo: 'Nome', placeholder: 'Digite seu nome', limiteCaracteres: 50 }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { EliEntradaTexto } from '@/index'
|
||||
|
||||
const nome = ref<string | null>(null)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2) `EliEntradaNumero`
|
||||
|
||||
**Value**: `number | null | undefined`
|
||||
|
||||
**Opções**:
|
||||
- `precisao?: number`
|
||||
- `1` => inteiro
|
||||
- `0.1` => 1 casa decimal
|
||||
- `0.01` => 2 casas decimais
|
||||
- `prefixo?: string` (ex.: `"R$"`)
|
||||
- `sufixo?: string` (ex.: `"kg"`)
|
||||
|
||||
Comportamento:
|
||||
- Quando `precisao < 1` o componente entra em modo **fixed-point**: você digita números continuamente e ele insere a vírgula automaticamente.
|
||||
- O que é exibido sempre corresponde ao `value` emitido.
|
||||
|
||||
Exemplos:
|
||||
|
||||
```vue
|
||||
<EliEntradaNumero
|
||||
v-model:value="quantidade"
|
||||
:opcoes="{ rotulo: 'Quantidade', placeholder: 'Ex: 10', precisao: 1, sufixo: 'kg' }"
|
||||
/>
|
||||
|
||||
<EliEntradaNumero
|
||||
v-model:value="preco"
|
||||
:opcoes="{ rotulo: 'Preço', placeholder: 'Digite', precisao: 0.01, prefixo: 'R$' }"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3) `EliEntradaDataHora`
|
||||
|
||||
**Value**: `string | null | undefined` (ISO 8601 com offset ou `Z`)
|
||||
|
||||
**Opções**:
|
||||
- `modo?: "data" | "dataHora"` (default: `dataHora`)
|
||||
- `min?: string` (ISO)
|
||||
- `max?: string` (ISO)
|
||||
- `limpavel?: boolean`
|
||||
- `erro?: boolean`
|
||||
- `mensagensErro?: string | string[]`
|
||||
- `dica?: string`
|
||||
- `dicaPersistente?: boolean`
|
||||
- `densidade?: CampoDensidade`
|
||||
- `variante?: CampoVariante`
|
||||
|
||||
Importante:
|
||||
- O input nativo `datetime-local` não carrega timezone.
|
||||
- O componente converte ISO (Z/offset) para **local** para exibir.
|
||||
- Ao alterar, emite ISO 8601 com o **offset local**.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```vue
|
||||
<EliEntradaDataHora
|
||||
v-model:value="agendamento"
|
||||
:opcoes="{ rotulo: 'Agendar', modo: 'dataHora', min, max, limpavel: true }"
|
||||
/>
|
||||
```
|
||||
|
||||
### Compatibilidade Vue 2 / Vue 3
|
||||
|
||||
Padrão recomendado (Vue 3):
|
||||
- `v-model:value`
|
||||
|
||||
Compat Vue 2:
|
||||
- todos os EliEntradas também emitem `input`.
|
||||
- isso permite consumir com o padrão `value + input` quando necessário.
|
||||
|
||||
### Playground
|
||||
|
||||
- Entradas: `src/playground/entradas.playground.vue`
|
||||
- Data/hora: `src/playground/data_hora.playground.vue`
|
||||
|
||||
---
|
||||
|
||||
## Para IA (contratos, invariantes e padrões de evolução)
|
||||
|
||||
### Contratos (não quebrar)
|
||||
|
||||
1) **Todo EliEntrada tem**:
|
||||
- prop `value`
|
||||
- prop `opcoes`
|
||||
- evento `update:value`
|
||||
|
||||
2) **Compatibilidade**:
|
||||
- emitir `input` (compat Vue 2) é obrigatório
|
||||
|
||||
3) **Tipagem**:
|
||||
- `PadroesEntradas` é a fonte única do contrato (value/opcoes)
|
||||
- `TipoEntrada = keyof PadroesEntradas`
|
||||
|
||||
4) **Sanitização/Normalização**:
|
||||
- `EliEntradaNumero` deve bloquear caracteres inválidos e manter display coerente com `value`
|
||||
- `EliEntradaDataHora` deve receber/emitir ISO e converter para local apenas para exibição
|
||||
|
||||
### Como adicionar uma nova entrada (checklist)
|
||||
|
||||
1) Adicionar chave em `PadroesEntradas` em `tiposEntradas.ts`
|
||||
2) Criar `EliEntradaX.vue` seguindo o padrão:
|
||||
- `value` + `opcoes`
|
||||
- emite `update:value`, `input`, `change`
|
||||
3) Exportar no `src/componentes/EliEntrada/index.ts`
|
||||
4) Registrar no `src/componentes/EliEntrada/registryEliEntradas.ts`
|
||||
5) Criar/atualizar playground (`src/playground/*.playground.vue`)
|
||||
6) Validar `pnpm -s run build:types` e `pnpm -s run build`
|
||||
|
||||
### Padrões de mudança (refactors seguros)
|
||||
|
||||
- Se precisar mudar o contrato, faça **migração incremental**:
|
||||
- manter props/eventos antigos como fallback temporário
|
||||
- atualizar playground e exemplos
|
||||
- rodar `build:types` para garantir geração de `.d.ts`
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import EliEntradaTexto from "./EliEntradaTexto.vue";
|
||||
import EliEntradaNumero from "./EliEntradaNumero.vue";
|
||||
import EliEntradaDataHora from "./EliEntradaDataHora.vue";
|
||||
|
||||
export { EliEntradaTexto, EliEntradaNumero };
|
||||
export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora };
|
||||
export type { PadroesEntradas, TipoEntrada } from "./tiposEntradas";
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import type { Component } from "vue";
|
|||
|
||||
import EliEntradaTexto from "./EliEntradaTexto.vue";
|
||||
import EliEntradaNumero from "./EliEntradaNumero.vue";
|
||||
import EliEntradaDataHora from "./EliEntradaDataHora.vue";
|
||||
|
||||
import type { TipoEntrada } from "./tiposEntradas";
|
||||
|
||||
export const registryTabelaCelulas = {
|
||||
texto: EliEntradaTexto,
|
||||
numero: EliEntradaNumero,
|
||||
dataHora: EliEntradaDataHora,
|
||||
} as const satisfies Record<TipoEntrada, Component>;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,41 @@ export type PadroesEntradas = {
|
|||
precisao?: number
|
||||
}
|
||||
>
|
||||
|
||||
dataHora: tipoPadraoEntrada<
|
||||
string | null | undefined,
|
||||
{
|
||||
/** Define o tipo de entrada. - `dataHora`: datetime-local - `data`: date */
|
||||
modo?: "data" | "dataHora"
|
||||
|
||||
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
|
||||
limpavel?: boolean
|
||||
|
||||
/** Estado de erro (visual). */
|
||||
erro?: boolean
|
||||
|
||||
/** Mensagens de erro. */
|
||||
mensagensErro?: string | string[]
|
||||
|
||||
/** Texto de apoio. */
|
||||
dica?: string
|
||||
|
||||
/** Mantém a dica sempre visível. */
|
||||
dicaPersistente?: boolean
|
||||
|
||||
/** Valor mínimo permitido (ISO 8601 - offset ou Z). */
|
||||
min?: string
|
||||
|
||||
/** Valor máximo permitido (ISO 8601 - offset ou Z). */
|
||||
max?: string
|
||||
|
||||
/** Densidade do campo (Vuetify). */
|
||||
densidade?: import("../../tipos").CampoDensidade
|
||||
|
||||
/** Variante do v-text-field (Vuetify). */
|
||||
variante?: import("../../tipos").CampoVariante
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue