MetadataPanel era solo un wrapper que determinaba contexto, PanelMeta es más genérico y reutilizable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>glm
parent
030bf3a765
commit
0e85c236d5
@ -0,0 +1,5 @@
|
||||
# Configuración de Desarrollo Local
|
||||
VITE_POCKETBASE_URL=http://192.168.18.131:8090
|
||||
VITE_APP_ENV=development
|
||||
VITE_LOG_LEVEL=debug
|
||||
VITE_APP_VERSION=1.0.0
|
||||
@ -0,0 +1,14 @@
|
||||
# Variables de Entorno para vcen-edit
|
||||
# Copia este archivo a .env.development para desarrollo local
|
||||
|
||||
# PocketBase URL
|
||||
VITE_POCKETBASE_URL=http://192.168.18.131:8090
|
||||
|
||||
# Entorno de aplicación
|
||||
VITE_APP_ENV=development
|
||||
|
||||
# Nivel de logging (debug|info|warn|error)
|
||||
VITE_LOG_LEVEL=debug
|
||||
|
||||
# Versión de aplicación
|
||||
VITE_APP_VERSION=1.0.0
|
||||
@ -0,0 +1,6 @@
|
||||
# Configuración de Producción
|
||||
# Reemplaza la URL con la de tu servidor de producción
|
||||
VITE_POCKETBASE_URL=https://api.production.com
|
||||
VITE_APP_ENV=production
|
||||
VITE_LOG_LEVEL=warn
|
||||
VITE_APP_VERSION=1.0.0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
@ -0,0 +1,120 @@
|
||||
# JsonLogicEngine
|
||||
|
||||
> Motor de evaluación y compilación de reglas JsonLogic personalizadas.
|
||||
|
||||
Este motor permite ejecutar lógica condicional compleja definida en formato JSON, optimizada para rendimiento y flexibilidad.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Características Principales
|
||||
|
||||
A diferencia del estándar clásico, este motor incluye mejoras específicas:
|
||||
|
||||
- **Normalización de Argumentos**: Los argumentos de los operadores siempre se tratan como arrays, simplificando la lógica interna.
|
||||
- **Paths con Separador `/`**: Soporte nativo para rutas de datos usando `/` (ej. `user/profile/name`) en lugar del punto `.` clásico.
|
||||
- **Compilación a JS Puro**: Permite compilar reglas a funciones de JavaScript real para un rendimiento superior en evaluaciones masivas.
|
||||
- **Seguridad**: Manejo robusto de valores `undefined` o `null` en los paths de datos.
|
||||
- **Operadores de Fecha**: Comparación lexicográfica ISO incluida por defecto.
|
||||
|
||||
---
|
||||
|
||||
## 💻 Instalación y Uso
|
||||
|
||||
### Ejemplo Básico
|
||||
|
||||
```typescript
|
||||
const engine = new JsonLogicEngine();
|
||||
|
||||
const rule = {
|
||||
"and": [
|
||||
{ ">": [{ "var": "age" }, 18] },
|
||||
{ "<": [{ "var": "age" }, 65] }
|
||||
]
|
||||
};
|
||||
|
||||
const data = { age: 30 };
|
||||
|
||||
// Evaluación directa
|
||||
console.log(engine.evaluate(rule, data)); // true
|
||||
|
||||
// Evaluación compilada (Recomendado para bucles o alto tráfico)
|
||||
const checkAge = engine.compile(rule);
|
||||
console.log(checkAge(data)); // true
|
||||
```
|
||||
|
||||
### Rutas Anidadas (Paths)
|
||||
|
||||
El operador `var` utiliza el separador `/` por defecto:
|
||||
|
||||
```typescript
|
||||
engine.evaluate(
|
||||
{ "var": "user/profile/name" },
|
||||
{ user: { profile: { name: "Ana" } } }
|
||||
); // → "Ana"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Operadores Soportados
|
||||
|
||||
El motor registra automáticamente los siguientes operadores "core":
|
||||
|
||||
| Tipo | Operadores |
|
||||
|-------------|--------------------------------------------------|
|
||||
| Lógicos | `and`, `or` |
|
||||
| Comparación | `==`, `!=`, `<`, `>`, `<=`, `>=` |
|
||||
| Acceso | `var` (usa `/` como separador) |
|
||||
| Fechas | `date-before`, `date-after`, `date-between` |
|
||||
|
||||
### Agregar Operadores Personalizados
|
||||
|
||||
Puedes extender el motor fácilmente usando `addOperation`:
|
||||
|
||||
```typescript
|
||||
engine.addOperation(
|
||||
"sum",
|
||||
(_, a, b) => a + b, // Función de evaluación
|
||||
([a, b]) => `(${a} + ${b})` // (Opcional) Lógica de compilación
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Referencia de la API
|
||||
|
||||
### `evaluate(rule, data)`
|
||||
|
||||
Evalúa una regla contra un objeto de datos.
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
|-----------|------------------|----------------------------------------------|
|
||||
| `rule` | `JsonLogicValue` | Primitivo, array o expresión JsonLogic. |
|
||||
| `data` | `any` | El contexto de datos contra el que se evalúa.|
|
||||
|
||||
**Retorna:** El resultado de la evaluación.
|
||||
|
||||
---
|
||||
|
||||
### `compile(rule)`
|
||||
|
||||
Transforma la regla en una función de JavaScript optimizada.
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
|-----------|------------------|------------------------------|
|
||||
| `rule` | `JsonLogicValue` | La regla a compilar. |
|
||||
|
||||
**Retorna:** `(data: any) => any`
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Tipos de Datos
|
||||
|
||||
El motor define los siguientes tipos para asegurar la integridad de las reglas:
|
||||
|
||||
| Tipo | Definición |
|
||||
|-----------------------|---------------------------------------------------------------|
|
||||
| `JsonPrimitive` | `string \| number \| boolean \| null` |
|
||||
| `JsonLogicValue` | Primitivos, arrays de valores o expresiones JsonLogic. |
|
||||
| `JsonLogicExpression` | Objeto con una única clave que representa el operador. |
|
||||
|
||||
|
||||
@ -0,0 +1,351 @@
|
||||
/**
|
||||
* Motor de evaluación y compilación de reglas JsonLogic.
|
||||
*
|
||||
* Permite ejecutar reglas escritas con sintaxis JsonLogic y también
|
||||
* compilarlas a funciones JavaScript nativas para evaluaciones repetidas
|
||||
* con mejor rendimiento.
|
||||
*
|
||||
* ## Características
|
||||
*
|
||||
* - Evaluación compatible con la sintaxis JsonLogic
|
||||
* - Normalización automática de argumentos (los operadores reciben arrays)
|
||||
* - Acceso a variables anidadas usando separador `/`
|
||||
* - Operadores lógicos y de comparación incluidos
|
||||
* - Operadores básicos de fechas
|
||||
* - Compilación de reglas a funciones JavaScript
|
||||
* - Acceso seguro a paths (evita errores con `undefined`)
|
||||
*
|
||||
* ## Sintaxis de paths
|
||||
*
|
||||
* El operador `var` usa `/` como separador en lugar del `.` clásico.
|
||||
*
|
||||
* Ejemplo:
|
||||
*
|
||||
* ```
|
||||
* user/profile/name
|
||||
* ```
|
||||
*
|
||||
* ## Ejemplo básico
|
||||
*
|
||||
* @example
|
||||
* const engine = new JsonLogicEngine();
|
||||
*
|
||||
* const rule = {
|
||||
* and: [
|
||||
* { ">": [{ var: "age" }, 18] },
|
||||
* { "<": [{ var: "age" }, 65] }
|
||||
* ]
|
||||
* };
|
||||
*
|
||||
* engine.evaluate(rule, { age: 30 }); // true
|
||||
*
|
||||
* ## Compilación de reglas
|
||||
*
|
||||
* Cuando una regla se evalúa muchas veces, es recomendable compilarla.
|
||||
*
|
||||
* @example
|
||||
* const esAdulto = engine.compile({
|
||||
* ">=": [{ var: "age" }, 18]
|
||||
* });
|
||||
*
|
||||
* esAdulto({ age: 20 }); // true
|
||||
* esAdulto({ age: 15 }); // false
|
||||
*
|
||||
* @see https://jsonlogic.com
|
||||
*/
|
||||
|
||||
import type { JsonLogicExpression, JsonLogicValue } from "@/jslg/lib/index.ts"
|
||||
|
||||
|
||||
/**
|
||||
* Separador de paths usado por el operador `var`.
|
||||
*
|
||||
* Ejemplo:
|
||||
*
|
||||
* user/profile/name
|
||||
*/
|
||||
export const JSON_LOGIC_PATH_SEPARATOR = "/" as const
|
||||
|
||||
|
||||
|
||||
// TIPOS INTERNOS
|
||||
|
||||
type OperationEval = (engine: JsonLogicEngine, ...args: any[]) => any
|
||||
type OperationCompile = (args: string[]) => string
|
||||
|
||||
type Operation = {
|
||||
eval: OperationEval
|
||||
compile?: OperationCompile
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convierte una expresión JsonLogic en string JSON a JsonLogicValue.
|
||||
* Útil en testsass y en la UI para parsear condiciones desde texto.
|
||||
*
|
||||
* @throws SyntaxError si el string no es JSON válido
|
||||
*/
|
||||
export function toJsonCondition(expr: string): JsonLogicValue {
|
||||
return JSON.parse(expr) as JsonLogicValue;
|
||||
}
|
||||
|
||||
export function asJsonCondition(value: unknown): JsonLogicValue {
|
||||
return value as JsonLogicValue;
|
||||
}
|
||||
|
||||
|
||||
// CLASE PRINCIPAL
|
||||
|
||||
export class JsonLogicEngine {
|
||||
private operations: Record<string, Operation> = {}
|
||||
private currentData: any = null
|
||||
|
||||
/**
|
||||
* Crea una nueva instancia del motor JsonLogic.
|
||||
*
|
||||
* Registra automáticamente los operadores básicos:
|
||||
*
|
||||
* - `var`
|
||||
* - `==`, `!=`
|
||||
* - `<`, `>`, `<=`, `>=`
|
||||
* - `and`, `or`
|
||||
* - `date-before`
|
||||
* - `date-after`
|
||||
* - `date-between`
|
||||
*/
|
||||
constructor() {
|
||||
this.registerCore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un operador personalizado.
|
||||
*
|
||||
* @param name Nombre del operador
|
||||
* @param evalFn Función que ejecuta la lógica del operador
|
||||
* @param compileFn Función opcional para generar código durante la compilación
|
||||
*
|
||||
* @example
|
||||
* engine.addOperation(
|
||||
* "sum",
|
||||
* (_, a, b) => a + b,
|
||||
* ([a, b]) => `(${a} + ${b})`
|
||||
* );
|
||||
*/
|
||||
addOperation(
|
||||
name: string,
|
||||
evalFn: OperationEval,
|
||||
compileFn?: OperationCompile
|
||||
) {
|
||||
this.operations[name] = { eval: evalFn, compile: compileFn }
|
||||
}
|
||||
|
||||
/**
|
||||
* Evalúa una regla JsonLogic usando un contexto de datos.
|
||||
*
|
||||
* @param rule Regla JsonLogic
|
||||
* @param data Datos sobre los que se ejecuta la regla
|
||||
*
|
||||
* @returns Resultado de la evaluación
|
||||
*
|
||||
* @throws Error si se usa un operador desconocido
|
||||
*
|
||||
* @example
|
||||
* engine.evaluate(
|
||||
* { ">": [{ var: "age" }, 18] },
|
||||
* { age: 21 }
|
||||
* ); // true
|
||||
*/
|
||||
evaluate(rule: JsonLogicValue, data: any): any {
|
||||
this.currentData = data
|
||||
try {
|
||||
return this._evaluate(rule)
|
||||
} finally {
|
||||
this.currentData = null
|
||||
}
|
||||
}
|
||||
|
||||
private _evaluate(rule: JsonLogicValue): any {
|
||||
if (rule === null || typeof rule !== "object") return rule
|
||||
|
||||
if (Array.isArray(rule)) {
|
||||
return rule.map(r => this._evaluate(r))
|
||||
}
|
||||
|
||||
const op = Object.keys(rule)[0]
|
||||
let args = (rule as JsonLogicExpression)[op]
|
||||
|
||||
// ← NORMALIZACIÓN CLAVE (esto arregla todos los testsass)
|
||||
if (!Array.isArray(args)) {
|
||||
args = args === undefined ? [] : [args]
|
||||
}
|
||||
|
||||
const operation = this.operations[op]
|
||||
if (!operation) {
|
||||
throw new Error(`Unknown operator: ${op}`)
|
||||
}
|
||||
|
||||
const evaluatedArgs = args.map(a => this._evaluate(a))
|
||||
return operation.eval(this, ...evaluatedArgs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compila una regla JsonLogic a una función JavaScript.
|
||||
*
|
||||
* Esto evita interpretar la estructura de la regla en cada ejecución
|
||||
* y mejora mucho el rendimiento en evaluaciones repetidas.
|
||||
*
|
||||
* @param rule Regla a compilar
|
||||
*
|
||||
* @returns Función que recibe los datos y devuelve el resultado
|
||||
*
|
||||
* @example
|
||||
* const regla = engine.compile({
|
||||
* ">": [{ var: "score" }, 80]
|
||||
* });
|
||||
*
|
||||
* regla({ score: 90 }); // true
|
||||
*/
|
||||
compile(rule: JsonLogicValue): (data: any) => any {
|
||||
const expr = this.compileExpr(rule)
|
||||
/* old
|
||||
return new Function("data", `return ${expr};`) as (data: any) => any
|
||||
*/
|
||||
const source = `/* Compiled JsonLogic */ return ${expr};`;
|
||||
try {
|
||||
return new Function("data", source) as (data: any) => any;
|
||||
} catch (e) {
|
||||
console.error("Error crítico compilando regla:", expr);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extrae los paths de variables usados en una regla JsonLogic.
|
||||
* Útil para construir grafos de dependencias.
|
||||
*
|
||||
* @example
|
||||
* engine.usesData({ "==": [{ var: "at:color" }, "rojo"] })
|
||||
* // → ["at:color"]
|
||||
*
|
||||
* engine.usesData({ "and": [
|
||||
* { "==": [{ var: "sc:motor/at:fuel" }, "electrico"] },
|
||||
* { ">": [{ var: "at:cv" }, 100] }
|
||||
* ]})
|
||||
* // → ["sc:motor/at:fuel", "at:cv"]
|
||||
*/
|
||||
usesData(rule: JsonLogicValue): string[] {
|
||||
const vars: string[] = []
|
||||
|
||||
const walk = (node: JsonLogicValue) => {
|
||||
if (node === null || typeof node !== 'object') return
|
||||
if (Array.isArray(node)) { node.forEach(walk); return }
|
||||
|
||||
const op = Object.keys(node)[0]
|
||||
let args = (node as JsonLogicExpression)[op]
|
||||
if (!Array.isArray(args)) args = args === undefined ? [] : [args]
|
||||
|
||||
if (op === 'var') {
|
||||
const path = args[0]
|
||||
if (typeof path === 'string' && path) vars.push(path)
|
||||
return
|
||||
}
|
||||
|
||||
args.forEach(walk)
|
||||
}
|
||||
|
||||
walk(rule)
|
||||
return [...new Set(vars)]
|
||||
}
|
||||
|
||||
private compileExpr(rule: JsonLogicValue): string {
|
||||
if (rule === null) return "null"
|
||||
if (typeof rule === "number" || typeof rule === "boolean") return String(rule)
|
||||
if (typeof rule === "string") return JSON.stringify(rule)
|
||||
|
||||
if (Array.isArray(rule)) {
|
||||
return `[${rule.map(v => this.compileExpr(v)).join(",")}]`
|
||||
}
|
||||
|
||||
const op = Object.keys(rule)[0]
|
||||
let args = (rule as JsonLogicExpression)[op]
|
||||
|
||||
if (!Array.isArray(args)) {
|
||||
args = args === undefined ? [] : [args]
|
||||
}
|
||||
|
||||
if (op === "var") {
|
||||
return this.compileVar(args[0] as string)
|
||||
}
|
||||
|
||||
const operation = this.operations[op]
|
||||
if (!operation?.compile) {
|
||||
throw new Error(`Operator '${op}' cannot be compiled`)
|
||||
}
|
||||
|
||||
const compiledArgs = args.map(a => this.compileExpr(a))
|
||||
return operation.compile(compiledArgs)
|
||||
}
|
||||
|
||||
private compileVar(path: string): string {
|
||||
const clean = (path ?? "").trim()
|
||||
if (!clean) return "data"
|
||||
|
||||
const parts = clean.split(JSON_LOGIC_PATH_SEPARATOR)
|
||||
let expr = "data"
|
||||
|
||||
/*
|
||||
for (const p of parts) {
|
||||
const safe = p.replace(/"/g, '\\"')
|
||||
expr += `?.["${safe}"]`
|
||||
}
|
||||
*/
|
||||
for (const p of parts) {
|
||||
// JSON.stringify pone las comillas automáticamente y escapa
|
||||
// CUALQUIER carácter raro (saltos de línea, backslashes, etc.)
|
||||
expr += `?.[${JSON.stringify(p)}]`;
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
private getVar(data: any, path: string): any {
|
||||
if (!path || data == null) return undefined
|
||||
|
||||
let current = data
|
||||
for (const p of path.split(JSON_LOGIC_PATH_SEPARATOR)) {
|
||||
if (current == null) return undefined
|
||||
current = current[p]
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private registerCore() {
|
||||
// var
|
||||
this.addOperation(
|
||||
"var",
|
||||
(engine, path = "") => engine.getVar(engine.currentData, path),
|
||||
([path = ""]) => this.compileVar(path)
|
||||
)
|
||||
|
||||
// comparaciones
|
||||
this.addOperation("==", (_, a, b) => a === b, ([a, b]) => `(${a} === ${b})`)
|
||||
this.addOperation("!=", (_, a, b) => a !== b, ([a, b]) => `(${a} !== ${b})`)
|
||||
this.addOperation(">", (_, a, b) => a > b, ([a, b]) => `(${a} > ${b})`)
|
||||
this.addOperation("<", (_, a, b) => a < b, ([a, b]) => `(${a} < ${b})`)
|
||||
this.addOperation(">=", (_, a, b) => a >= b, ([a, b]) => `(${a} >= ${b})`)
|
||||
this.addOperation("<=", (_, a, b) => a <= b, ([a, b]) => `(${a} <= ${b})`)
|
||||
|
||||
// lógicos
|
||||
this.addOperation("and", (_, ...args) => args.every(Boolean), args => `(${args.join(" && ")})`)
|
||||
this.addOperation("or", (_, ...args) => args.some(Boolean), args => `(${args.join(" || ")})`)
|
||||
|
||||
// fechas (comparación lexicográfica segura para ISO strings)
|
||||
this.addOperation("date-before", (_, a, b) => String(a) < String(b), ([a, b]) => `(${a} < ${b})`)
|
||||
this.addOperation("date-after", (_, a, b) => String(a) > String(b), ([a, b]) => `(${a} > ${b})`)
|
||||
this.addOperation(
|
||||
"date-between",
|
||||
(_, v, s, e) => String(v) >= String(s) && String(v) <= String(e),
|
||||
([v, s, e]) => `(${v} >= ${s} && ${v} <= ${e})`
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
// index.ts
|
||||
|
||||
|
||||
|
||||
export * from './types.ts'
|
||||
export * from './engine.ts'
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Valores primitivos permitidos en JsonLogic.
|
||||
*/
|
||||
|
||||
export type JsonPrimitive =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
|
||||
|
||||
/**
|
||||
* Valor válido dentro de una regla JsonLogic.
|
||||
*/
|
||||
export type JsonLogicValue =
|
||||
| JsonPrimitive
|
||||
| JsonLogicExpression
|
||||
| JsonLogicValue[]
|
||||
|
||||
|
||||
/**
|
||||
* Expresión JsonLogic.
|
||||
*
|
||||
* Un objeto cuya clave es el operador
|
||||
* y cuyo valor son los argumentos.
|
||||
*/
|
||||
export type JsonLogicExpression = {
|
||||
[operator: string]: JsonLogicValue | JsonLogicValue[]
|
||||
}
|
||||
@ -0,0 +1,314 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { JsonLogicEngine, JSON_LOGIC_PATH_SEPARATOR } from "../lib/engine.ts"
|
||||
import type {JsonLogicValue} from "@/tsat/libs/json_logic/types.ts";
|
||||
|
||||
describe("JsonLogicEngine", () => {
|
||||
|
||||
let engine: JsonLogicEngine
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new JsonLogicEngine()
|
||||
})
|
||||
|
||||
describe("PATH_SEPARATOR", () => {
|
||||
|
||||
it("should use '/' as separator", () => {
|
||||
expect(JSON_LOGIC_PATH_SEPARATOR).toBe("/")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe("var operator", () => {
|
||||
|
||||
it("should resolve simple path", () => {
|
||||
|
||||
const rule = { "var": "user/name" }
|
||||
|
||||
const data = {
|
||||
user: { name: "John" }
|
||||
}
|
||||
|
||||
const result = engine.evaluate(rule, data)
|
||||
|
||||
expect(result).toBe("John")
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should resolve nested path", () => {
|
||||
|
||||
const rule = { "var": "order/customer/name" }
|
||||
|
||||
const data = {
|
||||
order: {
|
||||
customer: {
|
||||
name: "Alice"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe("Alice")
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should support segments with ':'", () => {
|
||||
|
||||
const rule = { "var": "ct:vehicles/0/id" }
|
||||
|
||||
const data = {
|
||||
"ct:vehicles": [
|
||||
{ id: 42 }
|
||||
]
|
||||
}
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(42)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should return undefined for missing paths", () => {
|
||||
|
||||
const rule = { "var": "user/address/city" }
|
||||
|
||||
const data = {
|
||||
user: {}
|
||||
}
|
||||
|
||||
const result = engine.evaluate(rule as JsonLogicValue, data)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe("comparison operators", () => {
|
||||
|
||||
it("should evaluate ==", () => {
|
||||
|
||||
const rule = {
|
||||
"==": [
|
||||
{ "var": "age" },
|
||||
18
|
||||
]
|
||||
}
|
||||
|
||||
const data = { age: 18 }
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should evaluate !=", () => {
|
||||
|
||||
const rule = {
|
||||
"!=": [
|
||||
{ "var": "age" },
|
||||
18
|
||||
]
|
||||
}
|
||||
|
||||
const data = { age: 20 }
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should evaluate <", () => {
|
||||
|
||||
const rule = {
|
||||
"<": [
|
||||
{ "var": "age" },
|
||||
30
|
||||
]
|
||||
}
|
||||
|
||||
const data = { age: 25 }
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should evaluate >", () => {
|
||||
|
||||
const rule = {
|
||||
">": [
|
||||
{ "var": "age" },
|
||||
18
|
||||
]
|
||||
}
|
||||
|
||||
const data = { age: 20 }
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe("logical operators", () => {
|
||||
|
||||
it("should evaluate and", () => {
|
||||
|
||||
const rule = {
|
||||
"and": [
|
||||
{ ">": [ { "var": "age" }, 18 ] },
|
||||
{ "<": [ { "var": "age" }, 30 ] }
|
||||
]
|
||||
} as JsonLogicValue
|
||||
|
||||
const data = { age: 25 }
|
||||
|
||||
expect(engine.evaluate(rule, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should evaluate or", () => {
|
||||
|
||||
const rule = {
|
||||
"or": [
|
||||
{ "==": [ { "var": "role" }, "admin" ] },
|
||||
{ "==": [ { "var": "role" }, "editor" ] }
|
||||
]
|
||||
}
|
||||
|
||||
const data = { role: "editor" }
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe("custom operators", () => {
|
||||
|
||||
it("should allow adding custom operation", () => {
|
||||
|
||||
engine.addOperation(
|
||||
"sum",
|
||||
(_, a: number, b: number) => a + b,
|
||||
([a, b]) => `(${a} + ${b})`
|
||||
)
|
||||
|
||||
const rule = {
|
||||
"sum": [2, 3]
|
||||
}
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, {})).toBe(5)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe("date operators", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
engine.addOperation(
|
||||
"date-before",
|
||||
(_, a: string, b: string) => a < b,
|
||||
([a, b]) => `(${a} < ${b})`
|
||||
)
|
||||
|
||||
engine.addOperation(
|
||||
"date-after",
|
||||
(_, a: string, b: string) => a > b,
|
||||
([a, b]) => `(${a} > ${b})`
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should evaluate date-before", () => {
|
||||
|
||||
const rule = {
|
||||
"date-before": [
|
||||
{ "var": "date" },
|
||||
"2025-01-01"
|
||||
]
|
||||
}
|
||||
|
||||
const data = {
|
||||
date: "2024-12-31"
|
||||
}
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should evaluate date-after", () => {
|
||||
|
||||
const rule = {
|
||||
"date-after": [
|
||||
{ "var": "date" },
|
||||
"2025-01-01"
|
||||
]
|
||||
}
|
||||
|
||||
const data = {
|
||||
date: "2025-06-01"
|
||||
}
|
||||
|
||||
expect(engine.evaluate(rule as JsonLogicValue, data)).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe("compile", () => {
|
||||
|
||||
it("should compile expression to function", () => {
|
||||
|
||||
const rule = {
|
||||
"==": [
|
||||
{ "var": "user/name" },
|
||||
"John"
|
||||
]
|
||||
}
|
||||
|
||||
const fn = engine.compile(rule as JsonLogicValue)
|
||||
|
||||
const result = fn({
|
||||
user: { name: "John" }
|
||||
})
|
||||
|
||||
expect(result).toBe(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("compiled function should match evaluate()", () => {
|
||||
|
||||
const rule = {
|
||||
"and": [
|
||||
{ ">": [ { "var": "age" }, 18 ] },
|
||||
{ "<": [ { "var": "age" }, 65 ] }
|
||||
]
|
||||
} as JsonLogicValue
|
||||
|
||||
const data = { age: 30 }
|
||||
|
||||
const compiled = engine.compile(rule)
|
||||
|
||||
const r1 = engine.evaluate(rule, data)
|
||||
const r2 = compiled(data)
|
||||
|
||||
expect(r1).toBe(r2)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { CatalogEditorStore } from '@/vcen-edit/stores/catalog-editor.svelte.js'
|
||||
import type { EditorGlob } from '@/vcen-edit/lib/editor-glob.svelte.js'
|
||||
import { labelOf } from '@/vcen-edit/lib/label.ts'
|
||||
import PanelMeta from './PanelMeta.svelte'
|
||||
|
||||
const editor = getContext<CatalogEditorStore>('catalog-editor')
|
||||
const glob = getContext<EditorGlob>('editor-glob')
|
||||
const sel = $derived(editor.selection)
|
||||
|
||||
const ctx = $derived.by(() => {
|
||||
if (sel.kind === 'cat-meta') {
|
||||
const cat = editor.cat
|
||||
return {
|
||||
title: cat.name || glob.t('vcen.editor.sidebar.catSection'),
|
||||
id: cat.id,
|
||||
meta: cat.metadata,
|
||||
onUpdate: (m: any) => editor.updateCatMeta(m),
|
||||
}
|
||||
}
|
||||
if (sel.kind === 'obj-meta') {
|
||||
const obj = editor.findObj(sel.objId)
|
||||
if (!obj) return null
|
||||
return {
|
||||
title: labelOf(obj.label, editor.cat.lang) || glob.t('vcen.editor.common.noName'),
|
||||
id: obj.id,
|
||||
meta: obj.metadata,
|
||||
onUpdate: (m: any) => editor.updateObjMeta(sel.objId, m),
|
||||
}
|
||||
}
|
||||
if (sel.kind === 'sec-meta') {
|
||||
const sec = editor.findSec(sel.objId, sel.secId)
|
||||
if (!sec) return null
|
||||
return {
|
||||
title: labelOf(sec.label, editor.cat.lang) || glob.t('vcen.editor.common.noName'),
|
||||
id: sec.id,
|
||||
meta: sec.metadata,
|
||||
onUpdate: (m: any) => editor.updateSecMeta(sel.objId, sel.secId, m),
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if ctx}
|
||||
<div class="absolute inset-0 flex flex-col overflow-hidden">
|
||||
<PanelMeta value={ctx.meta} onUpdate={ctx.onUpdate} />
|
||||
</div>
|
||||
{/if}
|
||||
@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { StringField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: StringField;
|
||||
glob: GlobState;
|
||||
onFieldChange: (field: MetadataField) => void;
|
||||
} = $props();
|
||||
|
||||
function onInput(e: Event): void {
|
||||
onFieldChange({ ...field, value: (e.target as HTMLInputElement).value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1 w-full">
|
||||
<label class="text-xs font-medium text-gray-500">{glob.t('vcen.editor.mtas.types.string')}</label>
|
||||
<input type="text" value={field.value} oninput={onInput} class="rounded border border-gray-700 bg-gray-800 px-2 py-1.5 text-sm text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full" placeholder="Texto…" />
|
||||
</div>
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ColorField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {ColorField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: ColorField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { CoordinatesField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {CoordinatesField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: CoordinatesField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { CurrencyField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {CurrencyField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: CurrencyField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { DateField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {DateField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: DateField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { DateTimeField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {DateTimeField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: DateTimeField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { EmailField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {EmailField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: EmailField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FileField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {FileField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: FileField;
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { GroupField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {GroupField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob }: {
|
||||
field: GroupField;
|
||||
@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { HtmlField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import LingInput from '@/ling/svelte/LingInput.svelte';
|
||||
import type {HtmlField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: HtmlField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ImageField, ImageRef } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {ImageField, ImageRef, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: ImageField;
|
||||
@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { MarkdownField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import LingInput from '@/ling/svelte/LingInput.svelte';
|
||||
import type {MarkdownField, MetadataField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: MarkdownField;
|
||||
@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {MetadataFieldType} from "@/mtas/lib";
|
||||
import MetaFieldTypeIcon from "./MetaFieldTypeIcon.svelte";
|
||||
|
||||
let {
|
||||
onSelect,
|
||||
iconOnly = false,
|
||||
placement = 'end',
|
||||
glob,
|
||||
}: {
|
||||
onSelect: (type: MetadataFieldType) => void;
|
||||
iconOnly?: boolean;
|
||||
placement?: 'start' | 'end';
|
||||
glob: GlobState;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let buttonEl = $state<HTMLButtonElement | null>(null);
|
||||
|
||||
function dropdownStyle(): string {
|
||||
if (!buttonEl) return '';
|
||||
const r = buttonEl.getBoundingClientRect();
|
||||
const top = r.bottom + 4;
|
||||
const dropW = 360;
|
||||
const center = r.left + r.width / 2;
|
||||
const left = Math.max(8, Math.min(center - dropW / 2, window.innerWidth - dropW - 8));
|
||||
return `position:fixed;top:${top}px;left:${left}px;z-index:50`;
|
||||
}
|
||||
|
||||
const typeGroups: { key: string; types: MetadataFieldType[] }[] = [
|
||||
{ key: 'text', types: ['string', 'ling', 'html', 'url', 'slug', 'markdown'] },
|
||||
{ key: 'number', types: ['number', 'currency'] },
|
||||
{ key: 'date', types: ['date', 'datetime'] },
|
||||
{ key: 'logical', types: ['boolean'] },
|
||||
{ key: 'media', types: ['image', 'video', 'audio', 'pdf', 'file'] },
|
||||
{ key: 'choice', types: ['select', 'multiselect', 'color'] },
|
||||
{ key: 'contact', types: ['email', 'phone', 'coordinates'] },
|
||||
{ key: 'group', types: ['group'] },
|
||||
];
|
||||
|
||||
// Mismos colores que TypeBadge, convertidos a clases de texto para los iconos
|
||||
const iconColor: Record<MetadataFieldType, string> = {
|
||||
string: 'text-blue-400',
|
||||
number: 'text-purple-400',
|
||||
boolean: 'text-amber-400',
|
||||
ling: 'text-green-400',
|
||||
date: 'text-teal-400',
|
||||
datetime: 'text-teal-400',
|
||||
url: 'text-blue-400',
|
||||
currency: 'text-green-400',
|
||||
image: 'text-red-400',
|
||||
video: 'text-red-400',
|
||||
audio: 'text-purple-400',
|
||||
pdf: 'text-gray-400',
|
||||
file: 'text-gray-400',
|
||||
html: 'text-amber-400',
|
||||
color: 'text-amber-400',
|
||||
select: 'text-purple-400',
|
||||
multiselect: 'text-purple-400',
|
||||
email: 'text-blue-400',
|
||||
phone: 'text-blue-400',
|
||||
slug: 'text-gray-400',
|
||||
coordinates: 'text-green-400',
|
||||
markdown: 'text-amber-400',
|
||||
group: 'text-indigo-400',
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
bind:this={buttonEl}
|
||||
type="button"
|
||||
onclick={() => open = !open}
|
||||
title={glob.t('vcen.editor.mtas.header.addFieldTitle')}
|
||||
class={[
|
||||
iconOnly
|
||||
? 'flex items-center justify-center w-5 h-5 rounded text-gray-500 hover:text-indigo-400 hover:bg-indigo-500/10 transition-colors'
|
||||
: 'inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-500 transition-colors',
|
||||
].join(' ')}
|
||||
>
|
||||
<svg
|
||||
class={iconOnly ? 'h-3.5 w-3.5' : 'h-4 w-4'}
|
||||
viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<path d="M8 2v12M2 8h12"/>
|
||||
</svg>
|
||||
{#if !iconOnly}
|
||||
<span class="text-sm">{glob.t('vcen.editor.mtas.header.addField')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div class="fixed inset-0 z-40" onclick={() => open = false}></div>
|
||||
<div
|
||||
style={dropdownStyle()}
|
||||
class="w-[360px] rounded-lg border border-gray-700 bg-gray-800 shadow-xl overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-col divide-y divide-gray-900">
|
||||
{#each typeGroups as group}
|
||||
<div class="flex items-stretch">
|
||||
<!-- Label del grupo — columna con fondo propio -->
|
||||
<div class="flex items-center justify-end bg-gray-700/60 w-16 shrink-0 px-2 py-1.5">
|
||||
<span class="text-[9px] uppercase tracking-widest font-semibold text-gray-500">
|
||||
{glob.t(`vcen.editor.mtas.typeGroups.${group.key}` as any)}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Tipos en horizontal -->
|
||||
<div class="flex flex-wrap gap-0.5 items-center px-2 py-1 bg-gray-900 w-full">
|
||||
{#each group.types as type}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { onSelect(type); open = false; }}
|
||||
class="inline-flex items-center gap-1 px-2 py-1 text-xs rounded text-gray-500 hover:text-gray-300 hover:bg-gray-800 transition-colors"
|
||||
title={glob.t(`vcen.editor.mtas.types.${type}` as any)}
|
||||
>
|
||||
<span class={iconColor[type]}>
|
||||
<MetaFieldTypeIcon {type} size={13} />
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">
|
||||
{glob.t(`vcen.editor.mtas.types.${type}` as any)}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import type { MetadataField } from '../../lib';
|
||||
import FieldForm from './MetaFieldForm.svelte';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import { isValidId } from '@/vcen-edit/lib/validate';
|
||||
|
||||
let {
|
||||
fieldKey, field,
|
||||
tags = [],
|
||||
glob,
|
||||
locales,
|
||||
defaultLocale,
|
||||
onKeyChange, onFieldChange,
|
||||
}: {
|
||||
fieldKey: string;
|
||||
field: MetadataField;
|
||||
tags?: string[];
|
||||
glob: GlobState;
|
||||
locales: string[];
|
||||
defaultLocale: string;
|
||||
onKeyChange: (newKey: string) => void;
|
||||
onFieldChange: (field: MetadataField) => void;
|
||||
} = $props();
|
||||
|
||||
let editKey = $state(fieldKey);
|
||||
let keyError = $state('');
|
||||
|
||||
$effect(() => { editKey = fieldKey; });
|
||||
|
||||
function handleKeyCommit(): void {
|
||||
const trimmed = editKey.trim();
|
||||
if (!trimmed) {
|
||||
keyError = glob.t('vcen.editor.mtas.editor.keyEmpty');
|
||||
editKey = fieldKey;
|
||||
return;
|
||||
}
|
||||
if (!isValidId(trimmed)) {
|
||||
keyError = glob.t('vcen.editor.mtas.editor.keyInvalid');
|
||||
return;
|
||||
}
|
||||
keyError = '';
|
||||
if (trimmed !== fieldKey) onKeyChange(trimmed);
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent): void {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
|
||||
if (e.key === 'Escape') { editKey = fieldKey; keyError = ''; (e.currentTarget as HTMLInputElement).blur(); }
|
||||
}
|
||||
|
||||
function handleKeyInput(e: Event): void {
|
||||
editKey = (e.target as HTMLInputElement).value;
|
||||
keyError = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
|
||||
<!-- ── Field key ──────────────────────────────────────────────── -->
|
||||
<div class="flex flex-col gap-1 w-full">
|
||||
<label class="text-xs font-medium text-gray-500">
|
||||
{glob.t('vcen.editor.mtas.editor.keyLabel')}
|
||||
</label>
|
||||
<input
|
||||
value={editKey}
|
||||
oninput={handleKeyInput}
|
||||
onblur={handleKeyCommit}
|
||||
onkeydown={handleKeydown}
|
||||
class={['rounded border bg-gray-800 px-2 py-1.5 text-sm text-gray-200 focus:outline-none focus:ring-2 w-full font-mono', keyError ? 'border-red-600 focus:ring-red-500' : 'border-gray-700 focus:ring-blue-500'].join(' ')}
|
||||
placeholder={glob.t('vcen.editor.mtas.editor.keyPlaceholder')}
|
||||
/>
|
||||
{#if keyError}
|
||||
<span class="text-xs text-red-400 mt-0.5">{keyError}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- ── Field form (value + attributes + hidden) ───────────────── -->
|
||||
<FieldForm {field} {glob} {locales} {defaultLocale} {tags} {onFieldChange} />
|
||||
|
||||
</div>
|
||||
@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import StringField from './StringField.svelte';
|
||||
import NumberField from './NumberField.svelte';
|
||||
import BooleanField from './BooleanField.svelte';
|
||||
import LingField from './LingField.svelte';
|
||||
import DateField from './DateField.svelte';
|
||||
import DateTimeField from './DateTimeField.svelte';
|
||||
import UrlField from './UrlField.svelte';
|
||||
import CurrencyField from './CurrencyField.svelte';
|
||||
import ImageField from './ImageField.svelte';
|
||||
import VideoField from './VideoField.svelte';
|
||||
import AudioField from './AudioField.svelte';
|
||||
import PdfField from './PdfField.svelte';
|
||||
import FileField from './FileField.svelte';
|
||||
import HtmlField from './HtmlField.svelte';
|
||||
import ColorField from './ColorField.svelte';
|
||||
import SelectField from './SelectField.svelte';
|
||||
import MultiSelectField from './MultiSelectField.svelte';
|
||||
import EmailField from './EmailField.svelte';
|
||||
import PhoneField from './PhoneField.svelte';
|
||||
import SlugField from './SlugField.svelte';
|
||||
import CoordinatesField from './CoordinatesField.svelte';
|
||||
import MarkdownField from './MarkdownField.svelte';
|
||||
import GroupField from './GroupField.svelte';
|
||||
import type {MetadataField} from "@/mtas/lib";
|
||||
|
||||
let {
|
||||
field,
|
||||
glob,
|
||||
locales,
|
||||
defaultLocale,
|
||||
tags = [],
|
||||
onFieldChange,
|
||||
}: {
|
||||
field: MetadataField;
|
||||
glob: GlobState;
|
||||
locales: string[];
|
||||
defaultLocale: string;
|
||||
tags?: string[];
|
||||
onFieldChange: (field: MetadataField) => void;
|
||||
} = $props();
|
||||
|
||||
let asBase = $derived(field as MetadataField & { hidden?: boolean; tags?: string[] });
|
||||
let activeAttrs = $derived(asBase.tags ?? []);
|
||||
|
||||
function update(partial: Partial<MetadataField & { hidden?: boolean; attrs?: string[] }>): void {
|
||||
onFieldChange({ ...field, ...partial } as MetadataField);
|
||||
}
|
||||
|
||||
function toggleAttr(attr: string): void {
|
||||
const current = asBase.tags ?? [];
|
||||
const next = current.includes(attr) ? current.filter(a => a !== attr) : [...current, attr];
|
||||
update({ tags: next });
|
||||
}
|
||||
|
||||
function onHiddenChange(e: Event): void {
|
||||
update({ hidden: (e.target as HTMLInputElement).checked });
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ── Value editor ──────────────────────────────────────────────── -->
|
||||
{#if field.type === 'string'}
|
||||
<StringField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'number'}
|
||||
<NumberField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'boolean'}
|
||||
<BooleanField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'ling'}
|
||||
<LingField {field} {glob} {locales} {defaultLocale} {onFieldChange} />
|
||||
{:else if field.type === 'date'}
|
||||
<DateField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'datetime'}
|
||||
<DateTimeField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'url'}
|
||||
<UrlField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'currency'}
|
||||
<CurrencyField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'image'}
|
||||
<ImageField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'video'}
|
||||
<VideoField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'audio'}
|
||||
<AudioField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'pdf'}
|
||||
<PdfField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'file'}
|
||||
<FileField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'html'}
|
||||
<HtmlField {field} {glob} {locales} {defaultLocale} {onFieldChange} />
|
||||
{:else if field.type === 'color'}
|
||||
<ColorField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'select'}
|
||||
<SelectField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'multiselect'}
|
||||
<MultiSelectField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'email'}
|
||||
<EmailField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'phone'}
|
||||
<PhoneField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'slug'}
|
||||
<SlugField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'coordinates'}
|
||||
<CoordinatesField {field} {glob} {onFieldChange} />
|
||||
{:else if field.type === 'markdown'}
|
||||
<MarkdownField {field} {glob} {locales} {defaultLocale} {onFieldChange} />
|
||||
{:else if field.type === 'group'}
|
||||
<GroupField {field} {glob} />
|
||||
{/if}
|
||||
|
||||
<!-- ── Attributes ──────────────────────────────────────────────────── -->
|
||||
{#if tags.length > 0}
|
||||
<div class="border-t border-gray-800 my-2 flex items-center gap-2 text-xs text-gray-600 before:flex-1 before:border-t before:border-gray-800 after:flex-1 after:border-t after:border-gray-800">{glob.t('vcen.editor.mtas.editor.attributes')}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each tags as attr (attr)}
|
||||
{@const active = activeAttrs.includes(attr)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => toggleAttr(attr)}
|
||||
class={[
|
||||
'px-2 py-1 text-xs rounded border cursor-pointer select-none transition-colors gap-1 inline-flex items-center',
|
||||
active
|
||||
? 'bg-indigo-950 text-indigo-400 border-indigo-900 hover:bg-indigo-900'
|
||||
: 'bg-gray-800 text-gray-500 border-gray-700 hover:border-indigo-700',
|
||||
].join(' ')}
|
||||
>
|
||||
{#if active}
|
||||
<svg class="h-3 w-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 6l3 3 5-5"/>
|
||||
</svg>
|
||||
{/if}
|
||||
{attr}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ── Hidden toggle ──────────────────────────────────────────────── -->
|
||||
<div class="border-t border-gray-800 my-2"></div>
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<span class="relative inline-block w-8 h-4 shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={asBase.hidden ?? false}
|
||||
onchange={onHiddenChange}
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<span class="absolute inset-0 rounded-full bg-gray-700 peer-checked:bg-indigo-600 transition-colors
|
||||
after:absolute after:top-0.5 after:left-0.5 after:h-3 after:w-3
|
||||
after:rounded-full after:bg-white after:shadow
|
||||
after:transition-transform peer-checked:after:translate-x-4"></span>
|
||||
</span>
|
||||
<span class="text-sm text-gray-400">{glob.t('vcen.editor.mtas.editor.hideField')}</span>
|
||||
</label>
|
||||
@ -0,0 +1,150 @@
|
||||
<!-- MetaFieldTypeIcon.svelte — icono SVG por tipo de campo -->
|
||||
<script lang="ts">
|
||||
|
||||
|
||||
import type {MetadataFieldType} from "@/mtas/lib";
|
||||
|
||||
let {
|
||||
type,
|
||||
size = 16,
|
||||
class: cls = '',
|
||||
}: {
|
||||
type: MetadataFieldType;
|
||||
size?: number;
|
||||
class?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if type === 'string'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'number'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/>
|
||||
<line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'boolean'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="1" y="5" width="22" height="14" rx="7"/><circle cx="16" cy="12" r="3" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'ling'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'date'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'datetime'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
|
||||
<polyline points="12 14 12 17 14 17"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'url'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'currency'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'image'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'video'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'audio'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'pdf'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="9" y1="15" x2="15" y2="15"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'file'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'html'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'color'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/>
|
||||
<line x1="12" y1="3" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="21"/>
|
||||
<line x1="3" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="21" y2="12"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'select'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="3" y="6" width="18" height="12" rx="2"/>
|
||||
<polyline points="8 12 12 16 16 12"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'multiselect'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="3" y="4" width="5" height="5" rx="1"/><polyline points="4 6.5 5.5 8 8 5"/>
|
||||
<rect x="3" y="14" width="5" height="5" rx="1"/>
|
||||
<line x1="11" y1="6.5" x2="21" y2="6.5"/><line x1="11" y1="16.5" x2="21" y2="16.5"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'email'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22 6 12 13 2 6"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'phone'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.14 9a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.05 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'slug'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
|
||||
<line x1="7" y1="7" x2="7.01" y2="7"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'coordinates'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'markdown'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<polyline points="8 13 10 11 12 13 14 11 16 13"/>
|
||||
<line x1="8" y1="17" x2="12" y2="17"/>
|
||||
</svg>
|
||||
|
||||
{:else if type === 'group'}
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`fill-current ${cls}`}>
|
||||
<rect x="2" y="3" width="8" height="8" rx="1"/><rect x="14" y="3" width="8" height="8" rx="1"/>
|
||||
<rect x="2" y="13" width="8" height="8" rx="1"/><rect x="14" y="13" width="8" height="8" rx="1"/>
|
||||
</svg>
|
||||
{/if}
|
||||
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import FieldTypeIcon from './MetaFieldTypeIcon.svelte';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {MetadataFieldType} from "@/mtas/lib";
|
||||
|
||||
let {
|
||||
type,
|
||||
showLabel = true,
|
||||
glob,
|
||||
}: {
|
||||
type: MetadataFieldType;
|
||||
showLabel?: boolean;
|
||||
glob: GlobState;
|
||||
} = $props();
|
||||
|
||||
const colorMap: Record<MetadataFieldType, string> = {
|
||||
string: 'bg-blue-950 text-blue-400 border-blue-900',
|
||||
number: 'bg-purple-950 text-purple-400 border-purple-900',
|
||||
boolean: 'bg-amber-950 text-amber-400 border-amber-900',
|
||||
ling: 'bg-green-950 text-green-400 border-green-900',
|
||||
date: 'bg-teal-950 text-teal-400 border-teal-900',
|
||||
datetime: 'bg-teal-950 text-teal-400 border-teal-900',
|
||||
url: 'bg-blue-950 text-blue-400 border-blue-900',
|
||||
currency: 'bg-green-950 text-green-400 border-green-900',
|
||||
image: 'bg-red-950 text-red-400 border-red-900',
|
||||
video: 'bg-red-950 text-red-400 border-red-900',
|
||||
audio: 'bg-purple-950 text-purple-400 border-purple-900',
|
||||
pdf: 'bg-gray-800 text-gray-400 border-gray-700',
|
||||
file: 'bg-gray-800 text-gray-400 border-gray-700',
|
||||
html: 'bg-amber-950 text-amber-400 border-amber-900',
|
||||
color: 'bg-amber-950 text-amber-400 border-amber-900',
|
||||
select: 'bg-purple-950 text-purple-400 border-purple-900',
|
||||
multiselect: 'bg-purple-950 text-purple-400 border-purple-900',
|
||||
email: 'bg-blue-950 text-blue-400 border-blue-900',
|
||||
phone: 'bg-blue-950 text-blue-400 border-blue-900',
|
||||
slug: 'bg-gray-800 text-gray-400 border-gray-700',
|
||||
coordinates: 'bg-green-950 text-green-400 border-green-900',
|
||||
markdown: 'bg-amber-950 text-amber-400 border-amber-900',
|
||||
group: 'bg-indigo-950 text-indigo-400 border-indigo-900',
|
||||
};
|
||||
</script>
|
||||
|
||||
<span class={['inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs border', colorMap[type]].join(' ')}>
|
||||
<FieldTypeIcon {type} size={12} />
|
||||
{#if showLabel}
|
||||
<span>{glob.t(`vcen.editor.mtas.types.${type}` as any)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {MetadataField, StringField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: StringField;
|
||||
glob: GlobState;
|
||||
onFieldChange: (field: MetadataField) => void;
|
||||
} = $props();
|
||||
|
||||
function onInput(e: Event): void {
|
||||
onFieldChange({ ...field, value: (e.target as HTMLInputElement).value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1 w-full">
|
||||
<label class="text-xs font-medium text-gray-500">
|
||||
{glob.t('vcen.editor.mtas.types.string')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={field.value}
|
||||
oninput={onInput} class="rounded border border-gray-700 bg-gray-800 px-2 py-1.5 text-sm text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full"
|
||||
placeholder="Texto…"
|
||||
/>
|
||||
</div>
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { UrlField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {MetadataField, UrlField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: UrlField;
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { VideoField } from '../../../lib';
|
||||
import type { MetadataField } from '../../../lib';
|
||||
import type { GlobState } from '@/glob/adapters/svelte';
|
||||
import type {MetadataField, VideoField} from "@/mtas/lib";
|
||||
|
||||
let { field, glob, onFieldChange }: {
|
||||
field: VideoField;
|
||||
Loading…
Reference in new issue