create-lexy 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/r/accordion.json +1 -1
- package/assets/r/alert-dialog.json +1 -1
- package/assets/r/app-accordion.json +1 -1
- package/assets/r/app-dialog.json +1 -1
- package/assets/r/app-header-bar.json +1 -1
- package/assets/r/app-sidebar.json +1 -1
- package/assets/r/avatar.json +1 -1
- package/assets/r/badge.json +1 -1
- package/assets/r/brand-background.json +135 -0
- package/assets/r/breadcrumb.json +1 -1
- package/assets/r/button-group.json +1 -1
- package/assets/r/button.json +1 -1
- package/assets/r/calendar.json +1 -1
- package/assets/r/card.json +1 -1
- package/assets/r/chart.json +1 -1
- package/assets/r/checkbox.json +1 -1
- package/assets/r/combobox.json +1 -1
- package/assets/r/command.json +1 -1
- package/assets/r/confirmacion.json +2 -2
- package/assets/r/counter-badge.json +1 -1
- package/assets/r/crm-app-layout.json +2 -2
- package/assets/r/crm-desk.json +2 -2
- package/assets/r/crm-detalle-caso.json +2 -2
- package/assets/r/date-picker.json +1 -1
- package/assets/r/dialog.json +1 -1
- package/assets/r/dropdown-menu.json +1 -1
- package/assets/r/empty.json +1 -1
- package/assets/r/feature-card.json +1 -1
- package/assets/r/form.json +1 -1
- package/assets/r/header-bar.json +1 -1
- package/assets/r/input.json +1 -1
- package/assets/r/intake-wizard.json +2 -2
- package/assets/r/label.json +1 -1
- package/assets/r/login.json +2 -2
- package/assets/r/logo.json +1 -1
- package/assets/r/menubar.json +1 -1
- package/assets/r/navigation-menu.json +1 -1
- package/assets/r/pagination.json +1 -1
- package/assets/r/popover.json +1 -1
- package/assets/r/profile-card.json +1 -1
- package/assets/r/progress.json +1 -1
- package/assets/r/radio-group.json +1 -1
- package/assets/r/registry.json +116 -0
- package/assets/r/scroll-area.json +1 -1
- package/assets/r/searchbox.json +1 -1
- package/assets/r/select.json +1 -1
- package/assets/r/separator.json +1 -1
- package/assets/r/sheet.json +1 -1
- package/assets/r/sidebar.json +2 -2
- package/assets/r/skeleton.json +1 -1
- package/assets/r/slider.json +1 -1
- package/assets/r/snippet.json +1 -1
- package/assets/r/spinner.json +1 -1
- package/assets/r/status-dot.json +1 -1
- package/assets/r/switch.json +1 -1
- package/assets/r/table.json +1 -1
- package/assets/r/tabs.json +1 -1
- package/assets/r/tag.json +1 -1
- package/assets/r/textarea.json +1 -1
- package/assets/r/toaster.json +1 -1
- package/assets/r/tooltip.json +1 -1
- package/assets/r/tree.json +1 -1
- package/assets/registry-version +1 -1
- package/assets/theme/lexy-theme.css +15 -4
- package/dist/index.js +2421 -70
- package/package.json +16 -5
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync as
|
|
3
|
-
`)};import q from"fs/promises";import
|
|
4
|
-
`);if(process.stdout.isTTY&&process.stdin.isTTY){let
|
|
5
|
-
${
|
|
6
|
-
\xBFContinuar?`});if(
|
|
7
|
-
`)+
|
|
8
|
-
Usa --overwrite para sobrescribirlos.`)),process.exitCode=1;return}}for(let[
|
|
2
|
+
import{readFileSync as Po}from"fs";import tt from"path";import{fileURLToPath as jo}from"url";import{Command as Co}from"commander";import Io from"picocolors";import{spawn as ct}from"child_process";import Q from"fs/promises";import G from"path";import{confirm as dt,isCancel as pt,spinner as Te}from"@clack/prompts";import h from"picocolors";import te from"path";var ot=(e,t)=>t.architecture!=="feature"?e:e.replace(/from ["']@\/lib\/utils\/cn["']/g,'from "@/shared/lib/utils/cn"').replace(/from ["']@\/components\/base\//g,'from "@/shared/components/base/'),at=[["components/base/",e=>e.paths.components],["views/",e=>e.paths.views],["hooks/",e=>e.paths.hooks],["lib/",e=>e.paths.lib]],T=(e,t)=>e.files.filter(o=>typeof o.content=="string").map(o=>{let a=at.find(([r])=>o.path.startsWith(r)),n=a?te.posix.join(a[1](t),o.path.slice(a[0].length)):te.posix.join(t.paths.components,te.posix.basename(o.path));return o.encoding==="base64"?{relPath:n,content:o.content,encoding:"base64"}:{relPath:n,content:ot(o.content,t)}});import we from"fs/promises";import nt from"path";var Se=e=>nt.join(e,".lexy"),O=async e=>{let t;try{t=await we.readFile(Se(e),"utf-8")}catch{throw new Error("No se encontr\xF3 el archivo .lexy. \xBFEst\xE1s en la ra\xEDz de un proyecto Lexy? (cr\xE9alo con `npx create-lexy create`)")}let o=JSON.parse(t);if(!o.paths?.components)throw new Error("El .lexy no tiene paths.components \u2014 archivo corrupto o de una versi\xF3n incompatible.");return o.installed??={},o},ke=async(e,t)=>{await we.writeFile(Se(e),JSON.stringify(t,null,2)+`
|
|
3
|
+
`)};import q from"fs/promises";import rt from"os";import R from"path";import{fileURLToPath as it}from"url";var Ee="@lexydesign/registry",F=`https://cdn.jsdelivr.net/npm/${Ee}`,st=8e3,J=R.resolve(R.dirname(it(import.meta.url)),"../assets"),lt=R.join(rt.homedir(),".cache","create-lexy","registry"),B=async e=>{let t=await fetch(e,{signal:AbortSignal.timeout(st)});if(!t.ok){let o=new Error(`HTTP ${t.status} al pedir ${e}`);throw o.status=t.status,o}return await t.json()},Pe=async e=>{try{return JSON.parse(await q.readFile(R.join(J,"r",e),"utf-8"))}catch{return null}},oe=async()=>{try{return(await q.readFile(R.join(J,"registry-version"),"utf-8")).trim()}catch{return"bundle"}},$=null,W=async()=>{if($)return $;try{$=(await B(`https://registry.npmjs.org/${Ee}/latest`)).version}catch{try{$=(await B(`${F}@latest/package.json`)).version}catch{$=await oe()}}return $},k=async(e,t)=>{let o=e.replace(/^@lexy\//,""),a=t??await W(),n=R.join(lt,a,`${o}.json`);try{return{item:JSON.parse(await q.readFile(n,"utf-8")),version:a,source:"cache"}}catch{}try{let r=await B(`${F}@${a}/r/${o}.json`);return await q.mkdir(R.dirname(n),{recursive:!0}),await q.writeFile(n,JSON.stringify(r)),{item:r,version:a,source:"cdn"}}catch(r){if(r.status===404)throw new Error(`El componente "${o}" no existe en el registry (versi\xF3n ${a}). Mira el cat\xE1logo con \`create-lexy view --list\`.`);let i=await Pe(`${o}.json`);if(i)return{item:i,version:await oe(),source:"bundle"};throw new Error(`No se pudo alcanzar el registry (\xBFsin red?) y "${o}" tampoco est\xE1 en el bundle local del CLI.`)}},je=async()=>{try{let e=await W();return{index:await B(`${F}@${e}/r/registry.json`),version:e,source:"cdn"}}catch{let e=await Pe("registry.json");if(e)return{index:e,version:await oe(),source:"bundle"};throw new Error("No se pudo alcanzar el registry ni leer el bundle local del CLI.")}},Ce=R.join(J,"theme/lexy-theme.css"),Ie=R.join(J,"fonts");var mt=(e,t)=>new Promise((o,a)=>{ct("pnpm",["add",...e],{cwd:t,stdio:"ignore",shell:!0}).on("close",r=>r===0?o():a(new Error(`pnpm add fall\xF3 con c\xF3digo ${r}`)))}),ut=async(e,t="utf-8")=>{try{return await Q.readFile(e,t)}catch{return null}},Re=async(e,t)=>{let o=process.cwd(),a=await O(o),n=Te();n.start("Resolviendo componentes en el registry...");let r=new Map,i=async p=>{let f=p.toLowerCase().replace(/^@lexy\//,"");if(r.has(f))return;let c=await k(f);r.set(f,c);for(let l of c.item.registryDependencies??[])await i(l)};try{for(let p of e)await i(p)}catch(p){throw n.stop(h.red("No se pudo resolver el pedido.")),p}let d=new Map;for(let[p,{item:f}]of r){let c=[];for(let l of T(f,a)){let g=await ut(G.join(o,l.relPath),l.encoding??"utf-8");c.push({...l,status:g===null?"new":g===l.content?"same":"differs"})}d.set(p,c)}let s=Array.from(d.values()).flat().filter(p=>p.status==="differs");if(n.stop(`Resueltos: ${Array.from(r.keys()).join(", ")}`),s.length>0&&!t.overwrite){let p=s.map(f=>` ${f.relPath}`).join(`
|
|
4
|
+
`);if(process.stdout.isTTY&&process.stdin.isTTY){let f=await dt({message:`Estos archivos existen con cambios locales y ser\xE1n sobrescritos:
|
|
5
|
+
${p}
|
|
6
|
+
\xBFContinuar?`});if(pt(f)||f!==!0){console.log(h.yellow("Operaci\xF3n cancelada \u2014 nada fue modificado.")),process.exitCode=1;return}}else{console.error(h.red(`Estos archivos existen con cambios locales:
|
|
7
|
+
`)+p+h.dim(`
|
|
8
|
+
Usa --overwrite para sobrescribirlos.`)),process.exitCode=1;return}}for(let[p,f]of d){let{item:c,version:l,source:g}=r.get(p);for(let x of f){if(x.status==="same")continue;let A=G.join(o,x.relPath);await Q.mkdir(G.dirname(A),{recursive:!0}),await Q.writeFile(A,x.content,x.encoding??"utf-8")}a.installed[c.name]=l;let P=e.map(x=>x.toLowerCase()).includes(p)?"":h.dim(" (dependencia)"),U=g==="bundle"?h.yellow(" [bundle local \u2014 sin red]"):"";console.log(h.green(`${h.bold(c.title??c.name)} instalado`)+P+h.cyan(` \u2192 ${a.paths.components}/`)+U);for(let x of f){let A=x.status==="same"?h.dim(" (sin cambios)"):"";console.log(h.dim(` ${x.relPath}${A}`))}}await ke(o,a);let u=new Set;for(let{item:p}of r.values())for(let f of p.dependencies??[])u.add(f);let m=JSON.parse(await Q.readFile(G.join(o,"package.json"),"utf-8")),y={...m.dependencies,...m.devDependencies},z=p=>{let f=p.lastIndexOf("@");return f>0?p.slice(0,f):p},M=Array.from(u).filter(p=>!y[z(p)]);if(M.length>0)if(t.install===!1)console.log(h.yellow(`Dependencias pendientes (inst\xE1lalas t\xFA): ${M.join(", ")}`));else{let p=Te();p.start(`Instalando dependencias: ${M.join(", ")}...`),await mt(M,o),p.stop("Dependencias instaladas.")}};import{spawn as po}from"child_process";import{note as mo,outro as uo,spinner as yo}from"@clack/prompts";import j from"picocolors";import{cancel as yt,intro as ft,isCancel as gt,select as Oe,text as xt}from"@clack/prompts";import _e from"picocolors";var ae=[{value:"screen",label:"Una pantalla o flujo corto",hint:"landing, formulario, demo \u2014 sin navegaci\xF3n entre secciones",architecture:"layer"},{value:"app",label:"Una app con secciones y navegaci\xF3n",hint:"dashboard, listados, detalle \u2014 varias vistas con router",architecture:"feature"}],Ne=e=>ae.find(t=>t.value===e).architecture;var vt=`
|
|
9
9
|
###### ######
|
|
10
10
|
##::+#### ####+::##
|
|
11
11
|
#-..:+################+:..:#
|
|
@@ -20,9 +20,2292 @@ Usa --overwrite para sobrescribirlos.`)),process.exitCode=1;return}}for(let[a,p]
|
|
|
20
20
|
...........:====:...........
|
|
21
21
|
......................
|
|
22
22
|
..............
|
|
23
|
-
`,
|
|
24
|
-
${
|
|
25
|
-
|
|
23
|
+
`,ne=e=>{gt(e)&&(yt("Operaci\xF3n cancelada por el usuario."),process.exit(0))},Ae=async e=>{console.clear(),ft(`${_e.cyan(vt)}
|
|
24
|
+
${_e.bold(" Bienvenido al generador de proyectos Lexy")}`);let t=e;if(!t){let n=await xt({message:"\xBFCu\xE1l es el nombre de tu proyecto?",placeholder:"lexy-app",validate:r=>{if(!r||r.trim()==="")return"El nombre es obligatorio.";if(!/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(r))return"El nombre debe estar en min\xFAsculas, sin espacios ni caracteres especiales."}});ne(n),t=n}let o=await Oe({message:"\xBFPara qui\xE9n es este proyecto?",options:[{label:"Clientes de Lexy",value:"cliente",hint:"Producto de cara al cliente final"},{label:"Equipo interno / CRM",value:"crm",hint:"Herramienta densa para el equipo Lexy"},{label:"Ambos o a\xFAn no lo s\xE9",value:"mixto",hint:"Se decide por pantalla m\xE1s adelante"}],initialValue:"mixto"});ne(o);let a=await Oe({message:"\xBFC\xF3mo es lo que vas a construir?",options:ae.map(n=>({label:n.label,value:n.value,hint:n.hint})),initialValue:"app"});return ne(a),{projectName:t,architecture:Ne(a),world:o}};import b from"fs/promises";import w from"path";import{format as qt,resolveConfig as Vt}from"prettier";import Le from"fs/promises";import De from"path";var re="src/prototype/data-contract/prototype-data-contract.ts",ht="src/prototype/data-contract/data-contract-schema.ts",bt="scripts/check-data-contract.ts",wt=String.raw`import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
const camelCaseIdSchema = z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^[a-z][a-zA-Z0-9]*$/, "Debe usar camelCase y comenzar con una letra minúscula.");
|
|
29
|
+
|
|
30
|
+
const nonEmptyTextSchema = z.string().trim().min(1, "No puede quedar vacío.");
|
|
31
|
+
const experienceLabelsSchema = z
|
|
32
|
+
.array(nonEmptyTextSchema)
|
|
33
|
+
.min(1, "Debe indicar al menos un uso dentro de la experiencia.");
|
|
34
|
+
|
|
35
|
+
export const dataOriginSchema = z.enum([
|
|
36
|
+
"lexyConfirmed",
|
|
37
|
+
"productAssumption",
|
|
38
|
+
"generatedByUsability",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const technicalValidationStatusSchema = z.enum(["validated", "pendingTi", "rejected"]);
|
|
42
|
+
|
|
43
|
+
export const dataClassificationSchema = z.enum(["public", "internal", "sensitive", "restricted"]);
|
|
44
|
+
|
|
45
|
+
export const technicalValidationSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
status: technicalValidationStatusSchema,
|
|
48
|
+
note: nonEmptyTextSchema.optional(),
|
|
49
|
+
validatedBy: nonEmptyTextSchema.optional(),
|
|
50
|
+
validatedAt: z.string().datetime({ offset: true }).optional(),
|
|
51
|
+
evidence: nonEmptyTextSchema.optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.superRefine((validation, context) => {
|
|
55
|
+
if (validation.status === "pendingTi" && !validation.note) {
|
|
56
|
+
context.addIssue({
|
|
57
|
+
code: "custom",
|
|
58
|
+
path: ["note"],
|
|
59
|
+
message: "Un pendiente de TI debe explicar qué necesita validarse.",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (validation.status === "validated") {
|
|
64
|
+
if (!validation.validatedBy) {
|
|
65
|
+
context.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
path: ["validatedBy"],
|
|
68
|
+
message: "Un dato validado debe indicar quién lo validó.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!validation.validatedAt) {
|
|
72
|
+
context.addIssue({
|
|
73
|
+
code: "custom",
|
|
74
|
+
path: ["validatedAt"],
|
|
75
|
+
message: "Un dato validado debe indicar cuándo fue validado.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const backendReferenceSchema = z
|
|
82
|
+
.string()
|
|
83
|
+
.regex(
|
|
84
|
+
/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/,
|
|
85
|
+
"Las referencias de Lexy deben usar snake_case, por ejemplo cliente.rut_cliente.",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const sourceReferenceSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
kind: z.enum(["lexyEntity", "lexyProjection", "api", "derived", "unknown"]),
|
|
91
|
+
reference: nonEmptyTextSchema.optional(),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
.superRefine((source, context) => {
|
|
95
|
+
if ((source.kind === "lexyEntity" || source.kind === "lexyProjection") && !source.reference) {
|
|
96
|
+
context.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
path: ["reference"],
|
|
99
|
+
message: "La fuente Lexy debe indicar su referencia en snake_case.",
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (source.reference && (source.kind === "lexyEntity" || source.kind === "lexyProjection")) {
|
|
105
|
+
const result = backendReferenceSchema.safeParse(source.reference);
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
context.addIssue({
|
|
108
|
+
code: "custom",
|
|
109
|
+
path: ["reference"],
|
|
110
|
+
message: result.error.issues[0]?.message ?? "Referencia backend inválida.",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const usageSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
visible: z.boolean(),
|
|
119
|
+
editable: z.boolean(),
|
|
120
|
+
calculated: z.boolean(),
|
|
121
|
+
technical: z.boolean(),
|
|
122
|
+
filterable: z.boolean().optional(),
|
|
123
|
+
sortable: z.boolean().optional(),
|
|
124
|
+
})
|
|
125
|
+
.strict();
|
|
126
|
+
|
|
127
|
+
const metadataSchema = z
|
|
128
|
+
.object({
|
|
129
|
+
origin: dataOriginSchema,
|
|
130
|
+
source: sourceReferenceSchema,
|
|
131
|
+
dataClassification: dataClassificationSchema,
|
|
132
|
+
technicalValidation: technicalValidationSchema,
|
|
133
|
+
})
|
|
134
|
+
.strict();
|
|
135
|
+
|
|
136
|
+
const validateUsabilityEvidence = (
|
|
137
|
+
value: {
|
|
138
|
+
origin: z.infer<typeof dataOriginSchema>;
|
|
139
|
+
technicalValidation: z.infer<typeof technicalValidationSchema>;
|
|
140
|
+
},
|
|
141
|
+
context: z.RefinementCtx,
|
|
142
|
+
) => {
|
|
143
|
+
if (
|
|
144
|
+
value.origin === "generatedByUsability" &&
|
|
145
|
+
value.technicalValidation.status === "validated" &&
|
|
146
|
+
!value.technicalValidation.evidence
|
|
147
|
+
) {
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: ["technicalValidation", "evidence"],
|
|
151
|
+
message: "Un dato generado por usabilidad solo puede validarse con evidencia explícita.",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const fieldSpecSchema = z
|
|
157
|
+
.object({
|
|
158
|
+
id: camelCaseIdSchema,
|
|
159
|
+
productDescription: nonEmptyTextSchema,
|
|
160
|
+
dataType: z.enum(["string", "number", "boolean", "date", "datetime", "enum", "identifier"]),
|
|
161
|
+
required: z.boolean(),
|
|
162
|
+
usage: usageSchema,
|
|
163
|
+
usedIn: experienceLabelsSchema,
|
|
164
|
+
enumValues: z.array(nonEmptyTextSchema).min(1).optional(),
|
|
165
|
+
derivation: nonEmptyTextSchema.optional(),
|
|
166
|
+
})
|
|
167
|
+
.extend(metadataSchema.shape)
|
|
168
|
+
.strict()
|
|
169
|
+
.superRefine((field, context) => {
|
|
170
|
+
validateUsabilityEvidence(field, context);
|
|
171
|
+
|
|
172
|
+
if (field.dataType === "enum" && !field.enumValues) {
|
|
173
|
+
context.addIssue({
|
|
174
|
+
code: "custom",
|
|
175
|
+
path: ["enumValues"],
|
|
176
|
+
message: "Un campo enum debe declarar al menos un valor.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (field.dataType !== "enum" && field.enumValues) {
|
|
180
|
+
context.addIssue({
|
|
181
|
+
code: "custom",
|
|
182
|
+
path: ["enumValues"],
|
|
183
|
+
message: "Solo los campos enum pueden declarar enumValues.",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if ((field.usage.calculated || field.source.kind === "derived") && !field.derivation) {
|
|
187
|
+
context.addIssue({
|
|
188
|
+
code: "custom",
|
|
189
|
+
path: ["derivation"],
|
|
190
|
+
message: "Un dato calculado debe explicar su regla de derivación.",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const entityStateSpecSchema = z
|
|
196
|
+
.object({
|
|
197
|
+
id: camelCaseIdSchema,
|
|
198
|
+
productDescription: nonEmptyTextSchema,
|
|
199
|
+
activationCondition: nonEmptyTextSchema,
|
|
200
|
+
visualImpact: nonEmptyTextSchema,
|
|
201
|
+
usedIn: experienceLabelsSchema,
|
|
202
|
+
})
|
|
203
|
+
.extend(metadataSchema.shape)
|
|
204
|
+
.strict()
|
|
205
|
+
.superRefine(validateUsabilityEvidence);
|
|
206
|
+
|
|
207
|
+
export const entitySpecSchema = z
|
|
208
|
+
.object({
|
|
209
|
+
id: camelCaseIdSchema,
|
|
210
|
+
productDescription: nonEmptyTextSchema,
|
|
211
|
+
roleInExperience: nonEmptyTextSchema,
|
|
212
|
+
usedIn: experienceLabelsSchema,
|
|
213
|
+
fields: z.record(camelCaseIdSchema, fieldSpecSchema),
|
|
214
|
+
states: z.array(entityStateSpecSchema).optional(),
|
|
215
|
+
})
|
|
216
|
+
.extend(metadataSchema.shape)
|
|
217
|
+
.strict()
|
|
218
|
+
.superRefine(validateUsabilityEvidence);
|
|
219
|
+
|
|
220
|
+
export const relationSpecSchema = z
|
|
221
|
+
.object({
|
|
222
|
+
id: camelCaseIdSchema,
|
|
223
|
+
fromEntity: camelCaseIdSchema,
|
|
224
|
+
toEntity: camelCaseIdSchema,
|
|
225
|
+
cardinality: z.enum(["oneToOne", "oneToMany", "manyToMany"]),
|
|
226
|
+
productDescription: nonEmptyTextSchema,
|
|
227
|
+
requiredBy: experienceLabelsSchema,
|
|
228
|
+
resolution: z.enum(["embedded", "queried", "derived", "unknown"]),
|
|
229
|
+
derivation: nonEmptyTextSchema.optional(),
|
|
230
|
+
})
|
|
231
|
+
.extend(metadataSchema.shape)
|
|
232
|
+
.strict()
|
|
233
|
+
.superRefine((relation, context) => {
|
|
234
|
+
validateUsabilityEvidence(relation, context);
|
|
235
|
+
if (
|
|
236
|
+
(relation.resolution === "derived" || relation.source.kind === "derived") &&
|
|
237
|
+
!relation.derivation
|
|
238
|
+
) {
|
|
239
|
+
context.addIssue({
|
|
240
|
+
code: "custom",
|
|
241
|
+
path: ["derivation"],
|
|
242
|
+
message: "Una relación derivada debe explicar su regla de derivación.",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export const projectionFieldSpecSchema = z
|
|
248
|
+
.object({
|
|
249
|
+
id: camelCaseIdSchema,
|
|
250
|
+
source: z
|
|
251
|
+
.string()
|
|
252
|
+
.regex(
|
|
253
|
+
/^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/,
|
|
254
|
+
"Debe referenciar un campo como entidad.campo usando camelCase.",
|
|
255
|
+
),
|
|
256
|
+
required: z.boolean(),
|
|
257
|
+
})
|
|
258
|
+
.strict();
|
|
259
|
+
|
|
260
|
+
export const projectionSpecSchema = z
|
|
261
|
+
.object({
|
|
262
|
+
id: camelCaseIdSchema,
|
|
263
|
+
productDescription: nonEmptyTextSchema,
|
|
264
|
+
sourceEntities: experienceLabelsSchema,
|
|
265
|
+
consumedBy: experienceLabelsSchema,
|
|
266
|
+
fields: z.array(projectionFieldSpecSchema),
|
|
267
|
+
derivation: nonEmptyTextSchema.optional(),
|
|
268
|
+
})
|
|
269
|
+
.extend(metadataSchema.shape)
|
|
270
|
+
.strict()
|
|
271
|
+
.superRefine((projection, context) => {
|
|
272
|
+
validateUsabilityEvidence(projection, context);
|
|
273
|
+
if (projection.source.kind === "derived" && !projection.derivation) {
|
|
274
|
+
context.addIssue({
|
|
275
|
+
code: "custom",
|
|
276
|
+
path: ["derivation"],
|
|
277
|
+
message: "Una proyección derivada debe explicar su regla de derivación.",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const projectSchema = z
|
|
283
|
+
.object({
|
|
284
|
+
name: nonEmptyTextSchema,
|
|
285
|
+
description: z.string().trim(),
|
|
286
|
+
})
|
|
287
|
+
.strict();
|
|
288
|
+
|
|
289
|
+
export const prototypeDataContractSchema = z
|
|
290
|
+
.object({
|
|
291
|
+
contractVersion: z.literal("1"),
|
|
292
|
+
project: projectSchema,
|
|
293
|
+
entities: z.record(camelCaseIdSchema, entitySpecSchema),
|
|
294
|
+
relations: z.array(relationSpecSchema),
|
|
295
|
+
projections: z.record(camelCaseIdSchema, projectionSpecSchema),
|
|
296
|
+
})
|
|
297
|
+
.strict()
|
|
298
|
+
.superRefine((contract, context) => {
|
|
299
|
+
for (const [entityKey, entity] of Object.entries(contract.entities)) {
|
|
300
|
+
if (entityKey !== entity.id) {
|
|
301
|
+
context.addIssue({
|
|
302
|
+
code: "custom",
|
|
303
|
+
path: ["entities", entityKey, "id"],
|
|
304
|
+
message: 'El id "' + entity.id + '" debe coincidir con la key "' + entityKey + '".',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const [fieldKey, field] of Object.entries(entity.fields)) {
|
|
309
|
+
if (fieldKey !== field.id) {
|
|
310
|
+
context.addIssue({
|
|
311
|
+
code: "custom",
|
|
312
|
+
path: ["entities", entityKey, "fields", fieldKey, "id"],
|
|
313
|
+
message: 'El id "' + field.id + '" debe coincidir con la key "' + fieldKey + '".',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const stateIds = new Set<string>();
|
|
319
|
+
for (const [stateIndex, state] of (entity.states ?? []).entries()) {
|
|
320
|
+
if (stateIds.has(state.id)) {
|
|
321
|
+
context.addIssue({
|
|
322
|
+
code: "custom",
|
|
323
|
+
path: ["entities", entityKey, "states", stateIndex, "id"],
|
|
324
|
+
message: 'El estado "' + state.id + '" está declarado más de una vez.',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
stateIds.add(state.id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const relationIds = new Set<string>();
|
|
332
|
+
for (const [relationIndex, relation] of contract.relations.entries()) {
|
|
333
|
+
if (relationIds.has(relation.id)) {
|
|
334
|
+
context.addIssue({
|
|
335
|
+
code: "custom",
|
|
336
|
+
path: ["relations", relationIndex, "id"],
|
|
337
|
+
message: 'La relación "' + relation.id + '" está declarada más de una vez.',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
relationIds.add(relation.id);
|
|
341
|
+
|
|
342
|
+
if (!contract.entities[relation.fromEntity]) {
|
|
343
|
+
context.addIssue({
|
|
344
|
+
code: "custom",
|
|
345
|
+
path: ["relations", relationIndex, "fromEntity"],
|
|
346
|
+
message: 'La entidad "' + relation.fromEntity + '" no existe.',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (!contract.entities[relation.toEntity]) {
|
|
350
|
+
context.addIssue({
|
|
351
|
+
code: "custom",
|
|
352
|
+
path: ["relations", relationIndex, "toEntity"],
|
|
353
|
+
message: 'La entidad "' + relation.toEntity + '" no existe.',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [projectionKey, projection] of Object.entries(contract.projections)) {
|
|
359
|
+
if (projectionKey !== projection.id) {
|
|
360
|
+
context.addIssue({
|
|
361
|
+
code: "custom",
|
|
362
|
+
path: ["projections", projectionKey, "id"],
|
|
363
|
+
message:
|
|
364
|
+
'El id "' + projection.id + '" debe coincidir con la key "' + projectionKey + '".',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const [sourceIndex, entityId] of projection.sourceEntities.entries()) {
|
|
369
|
+
if (!contract.entities[entityId]) {
|
|
370
|
+
context.addIssue({
|
|
371
|
+
code: "custom",
|
|
372
|
+
path: ["projections", projectionKey, "sourceEntities", sourceIndex],
|
|
373
|
+
message: 'La entidad "' + entityId + '" no existe.',
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const projectionFieldIds = new Set<string>();
|
|
379
|
+
for (const [fieldIndex, field] of projection.fields.entries()) {
|
|
380
|
+
if (projectionFieldIds.has(field.id)) {
|
|
381
|
+
context.addIssue({
|
|
382
|
+
code: "custom",
|
|
383
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "id"],
|
|
384
|
+
message: 'El campo proyectado "' + field.id + '" está declarado más de una vez.',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
projectionFieldIds.add(field.id);
|
|
388
|
+
|
|
389
|
+
const [entityId, fieldId] = field.source.split(".");
|
|
390
|
+
const entity = contract.entities[entityId];
|
|
391
|
+
if (!entity) {
|
|
392
|
+
context.addIssue({
|
|
393
|
+
code: "custom",
|
|
394
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "source"],
|
|
395
|
+
message: 'La referencia "' + field.source + '" apunta a una entidad que no existe.',
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!entity.fields[fieldId]) {
|
|
400
|
+
context.addIssue({
|
|
401
|
+
code: "custom",
|
|
402
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "source"],
|
|
403
|
+
message: 'La referencia "' + field.source + '" apunta a un campo que no existe.',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (!projection.sourceEntities.includes(entityId)) {
|
|
407
|
+
context.addIssue({
|
|
408
|
+
code: "custom",
|
|
409
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "source"],
|
|
410
|
+
message: 'La entidad "' + entityId + '" debe declararse también en sourceEntities.',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
export type DataOrigin = z.infer<typeof dataOriginSchema>;
|
|
418
|
+
export type TechnicalValidationStatus = z.infer<typeof technicalValidationStatusSchema>;
|
|
419
|
+
export type TechnicalValidation = z.infer<typeof technicalValidationSchema>;
|
|
420
|
+
export type SourceReference = z.infer<typeof sourceReferenceSchema>;
|
|
421
|
+
export type FieldSpec = z.infer<typeof fieldSpecSchema>;
|
|
422
|
+
export type EntityStateSpec = z.infer<typeof entityStateSpecSchema>;
|
|
423
|
+
export type EntitySpec = z.infer<typeof entitySpecSchema>;
|
|
424
|
+
export type RelationSpec = z.infer<typeof relationSpecSchema>;
|
|
425
|
+
export type ProjectionSpec = z.infer<typeof projectionSpecSchema>;
|
|
426
|
+
export type PrototypeDataContract = z.infer<typeof prototypeDataContractSchema>;
|
|
427
|
+
|
|
428
|
+
export function definePrototypeDataContract(
|
|
429
|
+
contract: PrototypeDataContract,
|
|
430
|
+
): PrototypeDataContract {
|
|
431
|
+
return prototypeDataContractSchema.parse(contract);
|
|
432
|
+
}
|
|
433
|
+
`,St=String.raw`import { ZodError } from "zod";
|
|
434
|
+
|
|
435
|
+
import { prototypeDataContractSchema } from "../src/prototype/data-contract/data-contract-schema";
|
|
436
|
+
|
|
437
|
+
const formatPath = (path: PropertyKey[]) =>
|
|
438
|
+
path.length === 0 ? "contrato" : path.map(String).join(".");
|
|
439
|
+
|
|
440
|
+
const printIssues = (error: ZodError) => {
|
|
441
|
+
console.error("✗ Contrato de datos inválido");
|
|
442
|
+
for (const issue of error.issues) {
|
|
443
|
+
console.error(" " + formatPath(issue.path));
|
|
444
|
+
console.error(" " + issue.message);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const countPendingValidations = (value: unknown): number => {
|
|
449
|
+
if (Array.isArray(value)) {
|
|
450
|
+
return value.reduce((total, item) => total + countPendingValidations(item), 0);
|
|
451
|
+
}
|
|
452
|
+
if (!value || typeof value !== "object") return 0;
|
|
453
|
+
|
|
454
|
+
const record = value as Record<string, unknown>;
|
|
455
|
+
const ownPending =
|
|
456
|
+
record.technicalValidation &&
|
|
457
|
+
typeof record.technicalValidation === "object" &&
|
|
458
|
+
(record.technicalValidation as Record<string, unknown>).status === "pendingTi"
|
|
459
|
+
? 1
|
|
460
|
+
: 0;
|
|
461
|
+
|
|
462
|
+
return Object.values(record).reduce(
|
|
463
|
+
(total, item) => total + countPendingValidations(item),
|
|
464
|
+
ownPending,
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const module = await import("../src/prototype/data-contract/prototype-data-contract");
|
|
470
|
+
const result = prototypeDataContractSchema.safeParse(module.prototypeDataContract);
|
|
471
|
+
|
|
472
|
+
if (!result.success) {
|
|
473
|
+
printIssues(result.error);
|
|
474
|
+
process.exitCode = 1;
|
|
475
|
+
} else {
|
|
476
|
+
const contract = result.data;
|
|
477
|
+
console.log("✓ Contrato de datos válido");
|
|
478
|
+
console.log(" " + Object.keys(contract.entities).length + " entidades");
|
|
479
|
+
console.log(" " + contract.relations.length + " relaciones");
|
|
480
|
+
console.log(" " + Object.keys(contract.projections).length + " proyecciones");
|
|
481
|
+
console.log(
|
|
482
|
+
" " + countPendingValidations(contract) + " elementos pendientes de validación con TI",
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error instanceof ZodError) {
|
|
487
|
+
printIssues(error);
|
|
488
|
+
} else {
|
|
489
|
+
console.error("✗ No se pudo cargar el contrato de datos");
|
|
490
|
+
console.error(" " + (error instanceof Error ? error.message : String(error)));
|
|
491
|
+
}
|
|
492
|
+
process.exitCode = 1;
|
|
493
|
+
}
|
|
494
|
+
`,kt=e=>`import { definePrototypeDataContract } from "./data-contract-schema";
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Fuente de verdad de los datos usados por el prototipo.
|
|
498
|
+
*
|
|
499
|
+
* El designer describe el producto en lenguaje natural; el agente mantiene este
|
|
500
|
+
* contrato y ejecuta \`pnpm check:data-contract\` antes de implementar la UI.
|
|
501
|
+
*/
|
|
502
|
+
export const prototypeDataContract = definePrototypeDataContract({
|
|
503
|
+
contractVersion: "1",
|
|
504
|
+
project: {
|
|
505
|
+
name: ${JSON.stringify(e)},
|
|
506
|
+
description: "",
|
|
507
|
+
},
|
|
508
|
+
entities: {},
|
|
509
|
+
relations: [],
|
|
510
|
+
projections: {},
|
|
511
|
+
});
|
|
512
|
+
`,ze=async(e,t)=>{let o=async(a,n)=>{let r=De.join(e,a);await Le.mkdir(De.dirname(r),{recursive:!0}),await Le.writeFile(r,n)};await o(ht,wt),await o(re,kt(t)),await o(bt,St)};import Me from"fs/promises";import ie from"path";var se="src/prototype/designer-panel/designer-activity.ts",le="src/prototype/designer-panel/PrototypeDesignerPanel.tsx",Et=String.raw`export type DesignerActivityRecordInput = {
|
|
513
|
+
kind: "load" | "event";
|
|
514
|
+
label: string;
|
|
515
|
+
technicalId?: string;
|
|
516
|
+
triggerId?: string;
|
|
517
|
+
input?: unknown;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export type DesignerActivityRecord = {
|
|
521
|
+
id: string;
|
|
522
|
+
occurredAt: string;
|
|
523
|
+
kind: "load" | "event";
|
|
524
|
+
label: string;
|
|
525
|
+
technicalId?: string;
|
|
526
|
+
triggerId?: string;
|
|
527
|
+
inputPreview?: string;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export type DesignerActivityRecorder = {
|
|
531
|
+
record(input: DesignerActivityRecordInput): void;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
export type DesignerActivityStore = DesignerActivityRecorder & {
|
|
535
|
+
getSnapshot(): readonly DesignerActivityRecord[];
|
|
536
|
+
subscribe(listener: () => void): () => void;
|
|
537
|
+
clear(): void;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const preview = (value: unknown): string | undefined => {
|
|
541
|
+
if (value === undefined) return undefined;
|
|
542
|
+
try {
|
|
543
|
+
return JSON.stringify(value, null, 2);
|
|
544
|
+
} catch {
|
|
545
|
+
return "[no serializable]";
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const tabId = Math.random().toString(36).slice(2, 8);
|
|
550
|
+
|
|
551
|
+
type ChannelMessage =
|
|
552
|
+
| { type: "record"; record: DesignerActivityRecord }
|
|
553
|
+
| { type: "clear" }
|
|
554
|
+
| { type: "syncRequest" }
|
|
555
|
+
| { type: "sync"; records: DesignerActivityRecord[] };
|
|
556
|
+
|
|
557
|
+
export const createDesignerActivityStore = (maxItems = 80): DesignerActivityStore => {
|
|
558
|
+
const listeners = new Set<() => void>();
|
|
559
|
+
let records: DesignerActivityRecord[] = [];
|
|
560
|
+
let sequence = 0;
|
|
561
|
+
let synced = false;
|
|
562
|
+
|
|
563
|
+
const channel =
|
|
564
|
+
typeof BroadcastChannel !== "undefined"
|
|
565
|
+
? new BroadcastChannel("lexy-designer-activity")
|
|
566
|
+
: null;
|
|
567
|
+
|
|
568
|
+
const notify = () => {
|
|
569
|
+
for (const listener of listeners) listener();
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const addRecord = (record: DesignerActivityRecord) => {
|
|
573
|
+
records = [record, ...records].slice(0, maxItems);
|
|
574
|
+
notify();
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
if (channel) {
|
|
578
|
+
channel.onmessage = (event: MessageEvent<ChannelMessage>) => {
|
|
579
|
+
const message = event.data;
|
|
580
|
+
if (message.type === "record") addRecord(message.record);
|
|
581
|
+
else if (message.type === "clear") {
|
|
582
|
+
records = [];
|
|
583
|
+
notify();
|
|
584
|
+
} else if (message.type === "syncRequest") {
|
|
585
|
+
channel.postMessage({ type: "sync", records });
|
|
586
|
+
} else if (message.type === "sync" && !synced && records.length === 0) {
|
|
587
|
+
synced = true;
|
|
588
|
+
records = message.records.slice(0, maxItems);
|
|
589
|
+
notify();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
channel.postMessage({ type: "syncRequest" });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
getSnapshot: () => records,
|
|
597
|
+
subscribe: (listener) => {
|
|
598
|
+
listeners.add(listener);
|
|
599
|
+
return () => listeners.delete(listener);
|
|
600
|
+
},
|
|
601
|
+
record: (input) => {
|
|
602
|
+
sequence += 1;
|
|
603
|
+
const record: DesignerActivityRecord = {
|
|
604
|
+
id: tabId + "_" + sequence,
|
|
605
|
+
occurredAt: new Date().toISOString(),
|
|
606
|
+
kind: input.kind,
|
|
607
|
+
label: input.label,
|
|
608
|
+
...(input.technicalId ? { technicalId: input.technicalId } : {}),
|
|
609
|
+
...(input.triggerId ? { triggerId: input.triggerId } : {}),
|
|
610
|
+
...(preview(input.input) ? { inputPreview: preview(input.input) } : {}),
|
|
611
|
+
};
|
|
612
|
+
addRecord(record);
|
|
613
|
+
channel?.postMessage({ type: "record", record });
|
|
614
|
+
},
|
|
615
|
+
clear: () => {
|
|
616
|
+
records = [];
|
|
617
|
+
notify();
|
|
618
|
+
channel?.postMessage({ type: "clear" });
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
export const designerActivity = createDesignerActivityStore();
|
|
624
|
+
`,Pt=`
|
|
625
|
+
.lexy-dp {
|
|
626
|
+
--dp-bg: #ffffff;
|
|
627
|
+
--dp-fg: #242424;
|
|
628
|
+
--dp-card: #fafafa;
|
|
629
|
+
--dp-popover: #ffffff;
|
|
630
|
+
--dp-primary: #4648d4;
|
|
631
|
+
--dp-primary-fg: #ffffff;
|
|
632
|
+
--dp-muted: #f5f5f5;
|
|
633
|
+
--dp-muted-fg: #616161;
|
|
634
|
+
--dp-accent: #ebf3fc;
|
|
635
|
+
--dp-accent-fg: #4648d4;
|
|
636
|
+
--dp-border: #d1d1d1;
|
|
637
|
+
--dp-ring: #4648d4;
|
|
638
|
+
--dp-destructive: #d13438;
|
|
639
|
+
--dp-success: #107c10;
|
|
640
|
+
--dp-success-fg: #ffffff;
|
|
641
|
+
--dp-warning: #f7630c;
|
|
642
|
+
--dp-warning-fg: #ffffff;
|
|
643
|
+
--dp-info: #0f6cbd;
|
|
644
|
+
--dp-radius: 0.5rem;
|
|
645
|
+
--dp-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
646
|
+
--dp-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
647
|
+
color: var(--dp-fg);
|
|
648
|
+
font-family: var(--dp-sans);
|
|
649
|
+
font-size: 14px;
|
|
650
|
+
line-height: 1.4;
|
|
651
|
+
}
|
|
652
|
+
.lexy-dp *, .lexy-dp *::before, .lexy-dp *::after { box-sizing: border-box; }
|
|
653
|
+
.lexy-dp p, .lexy-dp h1, .lexy-dp pre, .lexy-dp ul { margin: 0; }
|
|
654
|
+
.lexy-dp ul { list-style: none; padding: 0; }
|
|
655
|
+
.lexy-dp button { font-family: inherit; color: inherit; }
|
|
656
|
+
.lexy-dp code { font-family: var(--dp-mono); }
|
|
657
|
+
|
|
658
|
+
/* \u2500\u2500 P\xE1gina completa \u2500\u2500 */
|
|
659
|
+
.lexy-dp-page {
|
|
660
|
+
display: flex;
|
|
661
|
+
flex-direction: column;
|
|
662
|
+
height: 100vh;
|
|
663
|
+
background: var(--dp-bg);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* \u2500\u2500 Tabs \u2500\u2500 */
|
|
667
|
+
.lexy-dp-tablist {
|
|
668
|
+
display: flex;
|
|
669
|
+
align-items: stretch;
|
|
670
|
+
border-bottom: 1px solid var(--dp-border);
|
|
671
|
+
padding: 0 12px;
|
|
672
|
+
background: var(--dp-bg);
|
|
673
|
+
}
|
|
674
|
+
.lexy-dp-tab {
|
|
675
|
+
display: inline-flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
gap: 6px;
|
|
678
|
+
padding: 8px 12px;
|
|
679
|
+
font-size: 12px;
|
|
680
|
+
border: none;
|
|
681
|
+
border-bottom: 2px solid transparent;
|
|
682
|
+
margin-bottom: -1px;
|
|
683
|
+
background: transparent;
|
|
684
|
+
color: var(--dp-muted-fg);
|
|
685
|
+
border-radius: 0;
|
|
686
|
+
cursor: pointer;
|
|
687
|
+
}
|
|
688
|
+
.lexy-dp-tab:hover { color: var(--dp-fg); }
|
|
689
|
+
.lexy-dp-tab.is-active { color: var(--dp-primary); font-weight: 500; border-bottom-color: var(--dp-primary); }
|
|
690
|
+
.lexy-dp-tab svg { width: 14px; height: 14px; }
|
|
691
|
+
.lexy-dp-tabbadge {
|
|
692
|
+
margin-left: 4px;
|
|
693
|
+
border-radius: 9999px;
|
|
694
|
+
background: var(--dp-warning);
|
|
695
|
+
color: var(--dp-warning-fg);
|
|
696
|
+
padding: 0 6px;
|
|
697
|
+
font-size: 10px;
|
|
698
|
+
font-weight: 500;
|
|
699
|
+
}
|
|
700
|
+
.lexy-dp-body { flex: 1; min-height: 0; }
|
|
701
|
+
.lexy-dp-panel { height: 100%; }
|
|
702
|
+
|
|
703
|
+
/* \u2500\u2500 Scroll y padding \u2500\u2500 */
|
|
704
|
+
.lexy-dp-scroll { height: 100%; overflow-y: auto; }
|
|
705
|
+
.lexy-dp-pad { padding: 20px 24px; }
|
|
706
|
+
|
|
707
|
+
/* \u2500\u2500 Columna y toolbar \u2500\u2500 */
|
|
708
|
+
.lexy-dp-col { display: flex; flex-direction: column; height: 100%; }
|
|
709
|
+
.lexy-dp-toolbar {
|
|
710
|
+
display: flex;
|
|
711
|
+
align-items: center;
|
|
712
|
+
gap: 4px;
|
|
713
|
+
border-bottom: 1px solid var(--dp-border);
|
|
714
|
+
background: var(--dp-bg);
|
|
715
|
+
padding: 4px 8px;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* \u2500\u2500 Botones \u2500\u2500 */
|
|
719
|
+
.lexy-dp-btn {
|
|
720
|
+
display: inline-flex;
|
|
721
|
+
align-items: center;
|
|
722
|
+
justify-content: center;
|
|
723
|
+
gap: 6px;
|
|
724
|
+
padding: 6px 12px;
|
|
725
|
+
font-size: 13px;
|
|
726
|
+
border-radius: var(--dp-radius);
|
|
727
|
+
border: 1px solid transparent;
|
|
728
|
+
background: var(--dp-primary);
|
|
729
|
+
color: var(--dp-primary-fg);
|
|
730
|
+
cursor: pointer;
|
|
731
|
+
}
|
|
732
|
+
.lexy-dp-btn:hover { opacity: 0.92; }
|
|
733
|
+
.lexy-dp-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
734
|
+
.lexy-dp-btn--outline { background: transparent; border-color: var(--dp-border); color: var(--dp-fg); }
|
|
735
|
+
.lexy-dp-btn--outline:hover { background: var(--dp-muted); opacity: 1; }
|
|
736
|
+
.lexy-dp-btn--outline:disabled:hover { background: transparent; }
|
|
737
|
+
.lexy-dp-btn--sm { padding: 4px 10px; font-size: 12px; }
|
|
738
|
+
.lexy-dp-btn svg { width: 14px; height: 14px; }
|
|
739
|
+
|
|
740
|
+
/* \u2500\u2500 Badges \u2500\u2500 */
|
|
741
|
+
.lexy-dp-badge {
|
|
742
|
+
display: inline-flex;
|
|
743
|
+
align-items: center;
|
|
744
|
+
border-radius: 9999px;
|
|
745
|
+
padding: 1px 8px;
|
|
746
|
+
font-size: 11px;
|
|
747
|
+
font-weight: 500;
|
|
748
|
+
border: 1px solid transparent;
|
|
749
|
+
background: var(--dp-primary);
|
|
750
|
+
color: var(--dp-primary-fg);
|
|
751
|
+
white-space: nowrap;
|
|
752
|
+
}
|
|
753
|
+
.lexy-dp-badge--outline { background: var(--dp-muted); color: var(--dp-muted-fg); border-color: var(--dp-border); }
|
|
754
|
+
.lexy-dp-badge--success { background: var(--dp-success); color: var(--dp-success-fg); }
|
|
755
|
+
.lexy-dp-badge--warning { background: var(--dp-warning); color: var(--dp-warning-fg); }
|
|
756
|
+
|
|
757
|
+
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
758
|
+
.lexy-dp-card {
|
|
759
|
+
border: 1px solid var(--dp-border);
|
|
760
|
+
background: var(--dp-card);
|
|
761
|
+
border-radius: var(--dp-radius);
|
|
762
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
763
|
+
padding: 12px 14px;
|
|
764
|
+
}
|
|
765
|
+
.lexy-dp-card__title { font-size: 14px; font-weight: 600; }
|
|
766
|
+
.lexy-dp-card__desc { font-size: 13px; color: var(--dp-muted-fg); margin-top: 2px; }
|
|
767
|
+
.lexy-dp-card__content { margin-top: 10px; }
|
|
768
|
+
.lexy-dp-card__head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
769
|
+
|
|
770
|
+
/* \u2500\u2500 Searchbox \u2500\u2500 */
|
|
771
|
+
.lexy-dp-search { position: relative; display: flex; align-items: center; flex: 1; }
|
|
772
|
+
.lexy-dp-search input {
|
|
773
|
+
width: 100%;
|
|
774
|
+
padding: 3px 26px 3px 8px;
|
|
775
|
+
font-size: 12px;
|
|
776
|
+
font-family: inherit;
|
|
777
|
+
border: 1px solid var(--dp-border);
|
|
778
|
+
border-radius: 3px;
|
|
779
|
+
background: var(--dp-bg);
|
|
780
|
+
color: var(--dp-fg);
|
|
781
|
+
}
|
|
782
|
+
.lexy-dp-search input:focus { outline: 2px solid var(--dp-ring); outline-offset: -1px; }
|
|
783
|
+
.lexy-dp-search__clear {
|
|
784
|
+
position: absolute;
|
|
785
|
+
right: 6px;
|
|
786
|
+
border: none;
|
|
787
|
+
background: transparent;
|
|
788
|
+
color: var(--dp-muted-fg);
|
|
789
|
+
cursor: pointer;
|
|
790
|
+
padding: 2px;
|
|
791
|
+
display: inline-flex;
|
|
792
|
+
}
|
|
793
|
+
.lexy-dp-search__clear svg { width: 14px; height: 14px; }
|
|
794
|
+
|
|
795
|
+
/* \u2500\u2500 Snippet / separador / etiquetas \u2500\u2500 */
|
|
796
|
+
.lexy-dp-snippet {
|
|
797
|
+
font-family: var(--dp-mono);
|
|
798
|
+
font-size: 12px;
|
|
799
|
+
background: var(--dp-muted);
|
|
800
|
+
border: 1px solid var(--dp-border);
|
|
801
|
+
border-radius: var(--dp-radius);
|
|
802
|
+
padding: 10px 12px;
|
|
803
|
+
overflow: auto;
|
|
804
|
+
white-space: pre;
|
|
805
|
+
color: var(--dp-fg);
|
|
806
|
+
}
|
|
807
|
+
.lexy-dp-sep { border: none; border-top: 1px solid var(--dp-border); height: 1px; }
|
|
808
|
+
.lexy-dp-section-label {
|
|
809
|
+
font-size: 12px;
|
|
810
|
+
font-weight: 500;
|
|
811
|
+
text-transform: uppercase;
|
|
812
|
+
letter-spacing: 0.04em;
|
|
813
|
+
color: var(--dp-muted-fg);
|
|
814
|
+
}
|
|
815
|
+
.lexy-dp-muted { color: var(--dp-muted-fg); }
|
|
816
|
+
.lexy-dp-mono { font-family: var(--dp-mono); }
|
|
817
|
+
.lexy-dp-empty { padding: 16px 12px; color: var(--dp-muted-fg); font-size: 13px; }
|
|
818
|
+
|
|
819
|
+
.lexy-dp-log__event { color: var(--dp-fg); }
|
|
820
|
+
|
|
821
|
+
/* \u2500\u2500 Stacks y grids \u2500\u2500 */
|
|
822
|
+
.lexy-dp-stack { display: flex; flex-direction: column; gap: 2px; }
|
|
823
|
+
.lexy-dp-stack-sm { display: flex; flex-direction: column; gap: 6px; }
|
|
824
|
+
.lexy-dp-stack-lg { display: flex; flex-direction: column; gap: 12px; }
|
|
825
|
+
.lexy-dp-substack { display: flex; flex-direction: column; gap: 2px; padding-left: 16px; color: var(--dp-muted-fg); }
|
|
826
|
+
.lexy-dp-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
827
|
+
|
|
828
|
+
/* \u2500\u2500 \xC1rbol JSON \u2500\u2500 */
|
|
829
|
+
.lexy-dp-json-str { color: var(--dp-fg); }
|
|
830
|
+
.lexy-dp-json-num { color: var(--dp-info); }
|
|
831
|
+
.lexy-dp-json-row { display: flex; align-items: flex-start; gap: 4px; padding: 1px 0; }
|
|
832
|
+
.lexy-dp-json-spacer { width: 14px; height: 14px; flex-shrink: 0; }
|
|
833
|
+
.lexy-dp-json-leaf { min-width: 0; word-break: break-word; }
|
|
834
|
+
.lexy-dp-json-toggle {
|
|
835
|
+
display: flex;
|
|
836
|
+
width: 100%;
|
|
837
|
+
align-items: flex-start;
|
|
838
|
+
gap: 4px;
|
|
839
|
+
padding: 1px 0;
|
|
840
|
+
text-align: left;
|
|
841
|
+
background: transparent;
|
|
842
|
+
border: none;
|
|
843
|
+
cursor: pointer;
|
|
844
|
+
}
|
|
845
|
+
.lexy-dp-json-toggle:hover { background: var(--dp-muted); }
|
|
846
|
+
.lexy-dp-chevron { width: 14px; height: 14px; flex-shrink: 0; color: var(--dp-muted-fg); margin-top: 1px; }
|
|
847
|
+
.lexy-dp-chevron--sm { width: 12px; height: 12px; margin-top: 0; }
|
|
848
|
+
.lexy-dp-chevron.is-open { transform: rotate(90deg); }
|
|
849
|
+
|
|
850
|
+
/* \u2500\u2500 Log de actividad \u2500\u2500 */
|
|
851
|
+
.lexy-dp-activitylog {
|
|
852
|
+
min-height: 100%;
|
|
853
|
+
background: var(--dp-bg);
|
|
854
|
+
padding: 4px 0;
|
|
855
|
+
font-family: var(--dp-mono);
|
|
856
|
+
font-size: 12px;
|
|
857
|
+
line-height: 1.5;
|
|
858
|
+
color: var(--dp-fg);
|
|
859
|
+
}
|
|
860
|
+
.lexy-dp-activitylog--scroll { flex: 1; min-height: 0; overflow-y: auto; }
|
|
861
|
+
.lexy-dp-log { border-bottom: 1px solid var(--dp-border); }
|
|
862
|
+
.lexy-dp-log__head {
|
|
863
|
+
display: flex;
|
|
864
|
+
width: 100%;
|
|
865
|
+
align-items: center;
|
|
866
|
+
gap: 6px;
|
|
867
|
+
padding: 4px 8px;
|
|
868
|
+
text-align: left;
|
|
869
|
+
background: transparent;
|
|
870
|
+
border: none;
|
|
871
|
+
cursor: pointer;
|
|
872
|
+
}
|
|
873
|
+
.lexy-dp-log__head:hover { background: var(--dp-muted); }
|
|
874
|
+
.lexy-dp-log__time { margin-left: auto; flex-shrink: 0; color: var(--dp-muted-fg); }
|
|
875
|
+
.lexy-dp-log__body { padding-bottom: 6px; }
|
|
876
|
+
|
|
877
|
+
/* \u2500\u2500 Detalle (event/entity detail) \u2500\u2500 */
|
|
878
|
+
.lexy-dp-eventdetail { display: flex; flex-direction: column; font-size: 12px; line-height: 1.5; }
|
|
879
|
+
.lexy-dp-detail-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; }
|
|
880
|
+
.lexy-dp-detail-field { display: flex; gap: 4px; font-family: var(--dp-mono); font-size: 12px; }
|
|
881
|
+
.lexy-dp-detail-label { color: var(--dp-muted-fg); flex-shrink: 0; }
|
|
882
|
+
.lexy-dp-detail-label::after { content: ":"; }
|
|
883
|
+
|
|
884
|
+
/* \u2500\u2500 Triggers list \u2500\u2500 */
|
|
885
|
+
.lexy-dp-trigger-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
886
|
+
.lexy-dp-trigger-item { display: flex; gap: 8px; align-items: baseline; }
|
|
887
|
+
.lexy-dp-bullet { color: var(--dp-primary); font-weight: 700; flex-shrink: 0; }
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
/* \u2500\u2500 Split (lista + detalle) \u2500\u2500 */
|
|
891
|
+
.lexy-dp-split { display: flex; height: 100%; }
|
|
892
|
+
.lexy-dp-split__list {
|
|
893
|
+
width: 288px;
|
|
894
|
+
flex-shrink: 0;
|
|
895
|
+
overflow-y: auto;
|
|
896
|
+
border-right: 1px solid var(--dp-border);
|
|
897
|
+
padding: 8px 0;
|
|
898
|
+
display: flex;
|
|
899
|
+
flex-direction: column;
|
|
900
|
+
gap: 1px;
|
|
901
|
+
}
|
|
902
|
+
.lexy-dp-list-section {
|
|
903
|
+
display: flex;
|
|
904
|
+
align-items: center;
|
|
905
|
+
justify-content: space-between;
|
|
906
|
+
width: 100%;
|
|
907
|
+
padding: 5px 10px;
|
|
908
|
+
font-size: 12px;
|
|
909
|
+
font-weight: 500;
|
|
910
|
+
color: var(--dp-fg);
|
|
911
|
+
background: var(--dp-muted);
|
|
912
|
+
border: none;
|
|
913
|
+
border-top: 1px solid var(--dp-border);
|
|
914
|
+
border-bottom: 1px solid var(--dp-border);
|
|
915
|
+
cursor: pointer;
|
|
916
|
+
font-family: inherit;
|
|
917
|
+
text-align: left;
|
|
918
|
+
}
|
|
919
|
+
.lexy-dp-list-section:hover { background: var(--dp-border); }
|
|
920
|
+
.lexy-dp-listitem {
|
|
921
|
+
width: 100%;
|
|
922
|
+
text-align: left;
|
|
923
|
+
border: none;
|
|
924
|
+
background: transparent;
|
|
925
|
+
border-radius: 0;
|
|
926
|
+
padding: 5px 12px;
|
|
927
|
+
font-family: var(--dp-mono);
|
|
928
|
+
font-size: 12px;
|
|
929
|
+
color: var(--dp-fg);
|
|
930
|
+
cursor: pointer;
|
|
931
|
+
white-space: nowrap;
|
|
932
|
+
overflow: hidden;
|
|
933
|
+
text-overflow: ellipsis;
|
|
934
|
+
}
|
|
935
|
+
.lexy-dp-listitem:hover { background: var(--dp-muted); }
|
|
936
|
+
.lexy-dp-listitem.is-active { background: var(--dp-accent); color: var(--dp-accent-fg); font-weight: 500; }
|
|
937
|
+
.lexy-dp-split__detail { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: 0; }
|
|
938
|
+
|
|
939
|
+
/* \u2500\u2500 Tabla de campos (devtools) \u2500\u2500 */
|
|
940
|
+
.lexy-dp-field-table { width: 100%; border-collapse: collapse; font-family: var(--dp-mono); font-size: 11px; table-layout: fixed; }
|
|
941
|
+
.lexy-dp-field-table th { text-align: left; font-weight: 500; color: var(--dp-fg); padding: 3px 8px; background: var(--dp-muted); border-bottom: 1px solid var(--dp-border); border-right: 1px solid var(--dp-border); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
942
|
+
.lexy-dp-field-table th:last-child { border-right: none; }
|
|
943
|
+
.lexy-dp-field-table td { padding: 2px 8px; border-bottom: 1px solid var(--dp-border); border-right: 1px solid var(--dp-border); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: top; }
|
|
944
|
+
.lexy-dp-field-table td:last-child { border-right: none; }
|
|
945
|
+
.lexy-dp-field-table tbody tr { cursor: pointer; }
|
|
946
|
+
.lexy-dp-field-table tbody tr:hover { background: var(--dp-muted); }
|
|
947
|
+
.lexy-dp-field-table tbody tr.is-selected { background: var(--dp-accent); color: var(--dp-accent-fg); }
|
|
948
|
+
.lexy-dp-field-table tbody tr.is-selected td { color: var(--dp-accent-fg); }
|
|
949
|
+
.lexy-dp-field-table tbody tr:last-child td { border-bottom: none; }
|
|
950
|
+
|
|
951
|
+
/* \u2500\u2500 Launcher flotante \u2500\u2500 */
|
|
952
|
+
.lexy-dp-launcher {
|
|
953
|
+
position: fixed;
|
|
954
|
+
bottom: 16px;
|
|
955
|
+
right: 16px;
|
|
956
|
+
z-index: 40;
|
|
957
|
+
display: flex;
|
|
958
|
+
align-items: center;
|
|
959
|
+
justify-content: center;
|
|
960
|
+
height: 48px;
|
|
961
|
+
width: 48px;
|
|
962
|
+
gap: 0;
|
|
963
|
+
overflow: hidden;
|
|
964
|
+
border: none;
|
|
965
|
+
border-radius: 9999px;
|
|
966
|
+
background: var(--dp-fg);
|
|
967
|
+
color: var(--dp-primary-fg);
|
|
968
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
|
969
|
+
cursor: pointer;
|
|
970
|
+
padding: 0 12px;
|
|
971
|
+
}
|
|
972
|
+
.lexy-dp-launcher:hover,
|
|
973
|
+
.lexy-dp-launcher:focus-visible,
|
|
974
|
+
.lexy-dp-launcher.is-open { width: 176px; gap: 8px; }
|
|
975
|
+
.lexy-dp-launcher:focus-visible { outline: 2px solid var(--dp-ring); outline-offset: 2px; }
|
|
976
|
+
.lexy-dp-launcher__icon { width: 20px; height: 20px; flex-shrink: 0; }
|
|
977
|
+
.lexy-dp-launcher__label {
|
|
978
|
+
overflow: hidden;
|
|
979
|
+
white-space: nowrap;
|
|
980
|
+
font-size: 13px;
|
|
981
|
+
font-weight: 500;
|
|
982
|
+
max-width: 0;
|
|
983
|
+
opacity: 0;
|
|
984
|
+
}
|
|
985
|
+
.lexy-dp-launcher:hover .lexy-dp-launcher__label,
|
|
986
|
+
.lexy-dp-launcher:focus-visible .lexy-dp-launcher__label,
|
|
987
|
+
.lexy-dp-launcher.is-open .lexy-dp-launcher__label { max-width: 160px; opacity: 1; }
|
|
988
|
+
|
|
989
|
+
/* \u2500\u2500 Sheet flotante \u2500\u2500 */
|
|
990
|
+
.lexy-dp-sheet {
|
|
991
|
+
position: fixed;
|
|
992
|
+
bottom: 80px;
|
|
993
|
+
right: 16px;
|
|
994
|
+
z-index: 40;
|
|
995
|
+
display: flex;
|
|
996
|
+
flex-direction: column;
|
|
997
|
+
height: 70vh;
|
|
998
|
+
width: min(100vw - 2rem, 400px);
|
|
999
|
+
overflow: hidden;
|
|
1000
|
+
border: 1px solid var(--dp-border);
|
|
1001
|
+
border-radius: 12px;
|
|
1002
|
+
background: var(--dp-popover);
|
|
1003
|
+
color: var(--dp-fg);
|
|
1004
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
|
|
1005
|
+
}
|
|
1006
|
+
.lexy-dp-sheet__header {
|
|
1007
|
+
display: flex;
|
|
1008
|
+
align-items: center;
|
|
1009
|
+
justify-content: space-between;
|
|
1010
|
+
gap: 12px;
|
|
1011
|
+
border-bottom: 1px solid var(--dp-border);
|
|
1012
|
+
background: var(--dp-fg);
|
|
1013
|
+
padding: 12px 16px;
|
|
1014
|
+
color: var(--dp-primary-fg);
|
|
1015
|
+
}
|
|
1016
|
+
.lexy-dp-sheet__title { font-size: 14px; font-weight: 500; }
|
|
1017
|
+
.lexy-dp-sheet__actions { display: flex; align-items: center; gap: 4px; }
|
|
1018
|
+
.lexy-dp-iconbtn {
|
|
1019
|
+
display: flex;
|
|
1020
|
+
height: 28px;
|
|
1021
|
+
width: 28px;
|
|
1022
|
+
align-items: center;
|
|
1023
|
+
justify-content: center;
|
|
1024
|
+
border: none;
|
|
1025
|
+
background: transparent;
|
|
1026
|
+
border-radius: 6px;
|
|
1027
|
+
opacity: 0.7;
|
|
1028
|
+
cursor: pointer;
|
|
1029
|
+
}
|
|
1030
|
+
.lexy-dp-iconbtn:hover { opacity: 1; }
|
|
1031
|
+
.lexy-dp-iconbtn svg { width: 16px; height: 16px; }
|
|
1032
|
+
.lexy-dp-sheet__body { flex: 1; min-height: 0; overflow-y: auto; background: var(--dp-bg); }
|
|
1033
|
+
`,jt=()=>String.raw`import {
|
|
1034
|
+
Activity,
|
|
1035
|
+
ArrowUpRight,
|
|
1036
|
+
ChevronRight,
|
|
1037
|
+
Maximize2,
|
|
1038
|
+
Settings,
|
|
1039
|
+
Trash2,
|
|
1040
|
+
X,
|
|
1041
|
+
Zap,
|
|
1042
|
+
} from "lucide-react";
|
|
1043
|
+
import { useState, useSyncExternalStore } from "react";
|
|
1044
|
+
|
|
1045
|
+
import { prototypeDataContract } from "../data-contract/prototype-data-contract";
|
|
1046
|
+
import {
|
|
1047
|
+
catalog,
|
|
1048
|
+
type CatalogEvent,
|
|
1049
|
+
type CatalogLoad,
|
|
1050
|
+
} from "../ports/catalog";
|
|
1051
|
+
import type { DesignerActivityRecord } from "./designer-activity";
|
|
1052
|
+
import { designerActivity } from "./designer-activity";
|
|
1053
|
+
|
|
1054
|
+
// Panel autocontenido: trae su propio CSS scopeado (.lexy-dp) y no depende del
|
|
1055
|
+
// tema Tailwind del proyecto host ni de componentes del registry. Así se puede
|
|
1056
|
+
// previsualizar e iterar el diseño en cualquier entorno.
|
|
1057
|
+
const PANEL_STYLES = ${JSON.stringify(Pt)};
|
|
1058
|
+
|
|
1059
|
+
function PanelStyles() {
|
|
1060
|
+
return <style>{PANEL_STYLES}</style>;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const formatTime = (value: string) =>
|
|
1064
|
+
new Intl.DateTimeFormat("es-CL", {
|
|
1065
|
+
hour: "2-digit",
|
|
1066
|
+
minute: "2-digit",
|
|
1067
|
+
second: "2-digit",
|
|
1068
|
+
}).format(new Date(value));
|
|
1069
|
+
|
|
1070
|
+
const safeParse = (value: string | undefined): unknown => {
|
|
1071
|
+
if (value === undefined) return undefined;
|
|
1072
|
+
try {
|
|
1073
|
+
return JSON.parse(value);
|
|
1074
|
+
} catch {
|
|
1075
|
+
return value;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
const TREE_INDENT = 14;
|
|
1080
|
+
const TREE_BASE = 8;
|
|
1081
|
+
|
|
1082
|
+
const renderPrimitive = (value: unknown) => {
|
|
1083
|
+
if (typeof value === "string") return <span className="lexy-dp-json-str">"{value}"</span>;
|
|
1084
|
+
if (typeof value === "number") return <span className="lexy-dp-json-num">{value}</span>;
|
|
1085
|
+
if (typeof value === "boolean") return <span className="lexy-dp-json-num">{String(value)}</span>;
|
|
1086
|
+
if (value === null) return <span className="lexy-dp-muted">null</span>;
|
|
1087
|
+
return <span className="lexy-dp-json-str">{String(value)}</span>;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
function JsonNode({
|
|
1091
|
+
label,
|
|
1092
|
+
value,
|
|
1093
|
+
depth = 0,
|
|
1094
|
+
defaultOpen = false,
|
|
1095
|
+
}: {
|
|
1096
|
+
label: string;
|
|
1097
|
+
value: unknown;
|
|
1098
|
+
depth?: number;
|
|
1099
|
+
defaultOpen?: boolean;
|
|
1100
|
+
}) {
|
|
1101
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
1102
|
+
const isBranch = value !== null && typeof value === "object";
|
|
1103
|
+
const indent = { paddingLeft: (depth * TREE_INDENT + TREE_BASE) + "px" };
|
|
1104
|
+
|
|
1105
|
+
if (!isBranch) {
|
|
1106
|
+
return (
|
|
1107
|
+
<div style={indent} className="lexy-dp-json-row">
|
|
1108
|
+
<span className="lexy-dp-json-spacer" aria-hidden />
|
|
1109
|
+
<span className="lexy-dp-json-leaf">
|
|
1110
|
+
<span className="lexy-dp-muted">{label}:</span> {renderPrimitive(value)}
|
|
1111
|
+
</span>
|
|
1112
|
+
</div>
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
1117
|
+
|
|
1118
|
+
return (
|
|
1119
|
+
<div>
|
|
1120
|
+
<button
|
|
1121
|
+
type="button"
|
|
1122
|
+
onClick={() => setOpen((v) => !v)}
|
|
1123
|
+
style={indent}
|
|
1124
|
+
className="lexy-dp-json-toggle"
|
|
1125
|
+
>
|
|
1126
|
+
<ChevronRight className={"lexy-dp-chevron" + (open ? " is-open" : "")} />
|
|
1127
|
+
<span className="lexy-dp-json-leaf">
|
|
1128
|
+
<span className="lexy-dp-muted">{label}:</span>
|
|
1129
|
+
{!open ? (
|
|
1130
|
+
<span className="lexy-dp-muted">
|
|
1131
|
+
{" "}
|
|
1132
|
+
{Array.isArray(value) ? "Array(" + entries.length + ")" : "{…}"}
|
|
1133
|
+
</span>
|
|
1134
|
+
) : null}
|
|
1135
|
+
</span>
|
|
1136
|
+
</button>
|
|
1137
|
+
{open
|
|
1138
|
+
? entries.map(([key, child]) => (
|
|
1139
|
+
<JsonNode key={key} label={key} value={child} depth={depth + 1} defaultOpen />
|
|
1140
|
+
))
|
|
1141
|
+
: null}
|
|
1142
|
+
</div>
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function LogEntry({ item }: { item: DesignerActivityRecord }) {
|
|
1147
|
+
const [open, setOpen] = useState(false);
|
|
1148
|
+
const trigger = item.triggerId ?? "desconocido";
|
|
1149
|
+
const operationName = item.technicalId ?? item.label;
|
|
1150
|
+
const input = safeParse(item.inputPreview);
|
|
1151
|
+
const inputLabel = item.kind === "load" ? "params" : "payload";
|
|
1152
|
+
|
|
1153
|
+
return (
|
|
1154
|
+
<div className="lexy-dp-log">
|
|
1155
|
+
<button type="button" onClick={() => setOpen((v) => !v)} className="lexy-dp-log__head">
|
|
1156
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (open ? " is-open" : "")} />
|
|
1157
|
+
<span className="lexy-dp-log__event">{operationName}</span>
|
|
1158
|
+
<span className="lexy-dp-log__time">{formatTime(item.occurredAt)}</span>
|
|
1159
|
+
</button>
|
|
1160
|
+
{open ? (
|
|
1161
|
+
<div className="lexy-dp-log__body">
|
|
1162
|
+
<div style={{ paddingLeft: TREE_BASE + "px" }} className="lexy-dp-json-row">
|
|
1163
|
+
<span className="lexy-dp-json-spacer" aria-hidden />
|
|
1164
|
+
<span className="lexy-dp-json-leaf">
|
|
1165
|
+
<span className="lexy-dp-muted">fuente:</span>{" "}
|
|
1166
|
+
<span className="lexy-dp-json-str">{trigger}</span>
|
|
1167
|
+
</span>
|
|
1168
|
+
</div>
|
|
1169
|
+
{input !== undefined ? <JsonNode label={inputLabel} value={input} defaultOpen /> : null}
|
|
1170
|
+
</div>
|
|
1171
|
+
) : null}
|
|
1172
|
+
</div>
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const DESIGNER_PANEL_ROUTE = "/designer-panel";
|
|
1177
|
+
|
|
1178
|
+
function ActivityLog() {
|
|
1179
|
+
const activity = useSyncExternalStore(
|
|
1180
|
+
designerActivity.subscribe,
|
|
1181
|
+
designerActivity.getSnapshot,
|
|
1182
|
+
designerActivity.getSnapshot,
|
|
1183
|
+
);
|
|
1184
|
+
return (
|
|
1185
|
+
<div className="lexy-dp-activitylog">
|
|
1186
|
+
{activity.length === 0 ? (
|
|
1187
|
+
<p className="lexy-dp-empty">Sin actividad. Ejecuta una carga o publica un evento.</p>
|
|
1188
|
+
) : null}
|
|
1189
|
+
{activity.map((item) => (
|
|
1190
|
+
<LogEntry key={item.id} item={item} />
|
|
1191
|
+
))}
|
|
1192
|
+
</div>
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const matchesQuery = (item: DesignerActivityRecord, query: string): boolean => {
|
|
1197
|
+
const haystack = [
|
|
1198
|
+
item.technicalId,
|
|
1199
|
+
item.label,
|
|
1200
|
+
item.triggerId,
|
|
1201
|
+
item.inputPreview,
|
|
1202
|
+
]
|
|
1203
|
+
.filter(Boolean)
|
|
1204
|
+
.join(" ")
|
|
1205
|
+
.toLowerCase();
|
|
1206
|
+
return haystack.includes(query);
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
function ActivityTab() {
|
|
1210
|
+
const [query, setQuery] = useState("");
|
|
1211
|
+
const activity = useSyncExternalStore(
|
|
1212
|
+
designerActivity.subscribe,
|
|
1213
|
+
designerActivity.getSnapshot,
|
|
1214
|
+
designerActivity.getSnapshot,
|
|
1215
|
+
);
|
|
1216
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1217
|
+
const filtered = normalizedQuery
|
|
1218
|
+
? activity.filter((item) => matchesQuery(item, normalizedQuery))
|
|
1219
|
+
: activity;
|
|
1220
|
+
|
|
1221
|
+
return (
|
|
1222
|
+
<div className="lexy-dp-col">
|
|
1223
|
+
<div className="lexy-dp-toolbar">
|
|
1224
|
+
<button
|
|
1225
|
+
type="button"
|
|
1226
|
+
aria-label="Limpiar actividad"
|
|
1227
|
+
title="Limpiar actividad"
|
|
1228
|
+
className="lexy-dp-iconbtn"
|
|
1229
|
+
onClick={() => designerActivity.clear()}
|
|
1230
|
+
>
|
|
1231
|
+
<Trash2 />
|
|
1232
|
+
</button>
|
|
1233
|
+
<div className="lexy-dp-search">
|
|
1234
|
+
<input
|
|
1235
|
+
value={query}
|
|
1236
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
1237
|
+
placeholder="Buscar en los logs…"
|
|
1238
|
+
/>
|
|
1239
|
+
{query ? (
|
|
1240
|
+
<button
|
|
1241
|
+
type="button"
|
|
1242
|
+
aria-label="Limpiar búsqueda"
|
|
1243
|
+
className="lexy-dp-search__clear"
|
|
1244
|
+
onClick={() => setQuery("")}
|
|
1245
|
+
>
|
|
1246
|
+
<X />
|
|
1247
|
+
</button>
|
|
1248
|
+
) : null}
|
|
1249
|
+
</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
<div className="lexy-dp-activitylog lexy-dp-activitylog--scroll">
|
|
1252
|
+
{filtered.length === 0 ? (
|
|
1253
|
+
<p className="lexy-dp-empty">
|
|
1254
|
+
{activity.length === 0
|
|
1255
|
+
? "Sin actividad. Ejecuta una carga o publica un evento."
|
|
1256
|
+
: "Sin coincidencias para la búsqueda."}
|
|
1257
|
+
</p>
|
|
1258
|
+
) : null}
|
|
1259
|
+
{filtered.map((item) => (
|
|
1260
|
+
<LogEntry key={item.id} item={item} />
|
|
1261
|
+
))}
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const inferType = (value: unknown): string => {
|
|
1268
|
+
if (value === null) return "null";
|
|
1269
|
+
if (Array.isArray(value)) return "array";
|
|
1270
|
+
return typeof value;
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
1274
|
+
string: "var(--dp-success)",
|
|
1275
|
+
number: "var(--dp-info)",
|
|
1276
|
+
boolean: "var(--dp-warning)",
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const TypeLabel = ({ value }: { value: unknown }) => (
|
|
1280
|
+
<span className="lexy-dp-muted" style={{ marginLeft: "8px" }}>{inferType(value)}</span>
|
|
1281
|
+
);
|
|
1282
|
+
|
|
1283
|
+
function PayloadKeyNode({
|
|
1284
|
+
label,
|
|
1285
|
+
value,
|
|
1286
|
+
depth = 0,
|
|
1287
|
+
}: {
|
|
1288
|
+
label: string;
|
|
1289
|
+
value: unknown;
|
|
1290
|
+
depth?: number;
|
|
1291
|
+
}) {
|
|
1292
|
+
const [open, setOpen] = useState(true);
|
|
1293
|
+
const indent = { paddingLeft: depth * TREE_INDENT + TREE_BASE + "px" };
|
|
1294
|
+
|
|
1295
|
+
if (Array.isArray(value)) {
|
|
1296
|
+
const firstObj = value.find(
|
|
1297
|
+
(item) => item !== null && typeof item === "object" && !Array.isArray(item),
|
|
1298
|
+
);
|
|
1299
|
+
const children = firstObj ? Object.entries(firstObj as Record<string, unknown>) : [];
|
|
1300
|
+
if (children.length === 0) {
|
|
1301
|
+
return (
|
|
1302
|
+
<div style={indent} className="lexy-dp-json-row">
|
|
1303
|
+
<span className="lexy-dp-json-spacer" aria-hidden />
|
|
1304
|
+
<span className="lexy-dp-json-leaf" style={{ color: TYPE_COLORS[inferType(value)] ?? "var(--dp-fg)" }}>{label}<TypeLabel value={value} /></span>
|
|
1305
|
+
</div>
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
return (
|
|
1309
|
+
<div>
|
|
1310
|
+
<button type="button" onClick={() => setOpen((v) => !v)} style={indent} className="lexy-dp-json-toggle">
|
|
1311
|
+
<ChevronRight className={"lexy-dp-chevron" + (open ? " is-open" : "")} />
|
|
1312
|
+
<span className="lexy-dp-json-leaf" style={{ color: TYPE_COLORS[inferType(value)] ?? "var(--dp-fg)" }}>{label}<TypeLabel value={value} /></span>
|
|
1313
|
+
</button>
|
|
1314
|
+
{open
|
|
1315
|
+
? children.map(([key, child]) => (
|
|
1316
|
+
<PayloadKeyNode key={key} label={key} value={child} depth={depth + 1} />
|
|
1317
|
+
))
|
|
1318
|
+
: null}
|
|
1319
|
+
</div>
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (value !== null && typeof value === "object") {
|
|
1324
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
1325
|
+
return (
|
|
1326
|
+
<div>
|
|
1327
|
+
<button type="button" onClick={() => setOpen((v) => !v)} style={indent} className="lexy-dp-json-toggle">
|
|
1328
|
+
<ChevronRight className={"lexy-dp-chevron" + (open ? " is-open" : "")} />
|
|
1329
|
+
<span className="lexy-dp-json-leaf" style={{ color: TYPE_COLORS[inferType(value)] ?? "var(--dp-fg)" }}>{label}<TypeLabel value={value} /></span>
|
|
1330
|
+
</button>
|
|
1331
|
+
{open
|
|
1332
|
+
? entries.map(([key, child]) => (
|
|
1333
|
+
<PayloadKeyNode key={key} label={key} value={child} depth={depth + 1} />
|
|
1334
|
+
))
|
|
1335
|
+
: null}
|
|
1336
|
+
</div>
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return (
|
|
1341
|
+
<div style={indent} className="lexy-dp-json-row">
|
|
1342
|
+
<span className="lexy-dp-json-spacer" aria-hidden />
|
|
1343
|
+
<span className="lexy-dp-json-leaf" style={{ color: TYPE_COLORS[inferType(value)] ?? "var(--dp-fg)" }}>{label}<TypeLabel value={value} /></span>
|
|
1344
|
+
</div>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function EventDetail({ event }: { event: CatalogEvent }) {
|
|
1349
|
+
const [triggersOpen, setTriggersOpen] = useState(true);
|
|
1350
|
+
const [payloadOpen, setPayloadOpen] = useState(true);
|
|
1351
|
+
|
|
1352
|
+
const activity = useSyncExternalStore(
|
|
1353
|
+
designerActivity.subscribe,
|
|
1354
|
+
designerActivity.getSnapshot,
|
|
1355
|
+
designerActivity.getSnapshot,
|
|
1356
|
+
);
|
|
1357
|
+
const observedTriggers = Array.from(
|
|
1358
|
+
new Set(
|
|
1359
|
+
activity
|
|
1360
|
+
.filter((record) => record.technicalId === event.technicalId && record.triggerId)
|
|
1361
|
+
.map((record) => record.triggerId as string),
|
|
1362
|
+
),
|
|
1363
|
+
);
|
|
1364
|
+
const payload = event.lastPayload;
|
|
1365
|
+
|
|
1366
|
+
return (
|
|
1367
|
+
<div className="lexy-dp-eventdetail">
|
|
1368
|
+
<div className="lexy-dp-detail-body">
|
|
1369
|
+
<div className="lexy-dp-detail-field">
|
|
1370
|
+
<span className="lexy-dp-detail-label">Nombre</span>
|
|
1371
|
+
<span>{event.technicalId}</span>
|
|
1372
|
+
</div>
|
|
1373
|
+
{event.description ? (
|
|
1374
|
+
<div className="lexy-dp-detail-field">
|
|
1375
|
+
<span className="lexy-dp-detail-label">Descripción</span>
|
|
1376
|
+
<span>{event.description}</span>
|
|
1377
|
+
</div>
|
|
1378
|
+
) : null}
|
|
1379
|
+
{event.lastReceipt ? (
|
|
1380
|
+
<div className="lexy-dp-detail-field">
|
|
1381
|
+
<span className="lexy-dp-detail-label">Último receipt</span>
|
|
1382
|
+
<span className="lexy-dp-mono">
|
|
1383
|
+
{event.lastReceipt.eventId} · {event.lastReceipt.status}
|
|
1384
|
+
</span>
|
|
1385
|
+
</div>
|
|
1386
|
+
) : null}
|
|
1387
|
+
</div>
|
|
1388
|
+
|
|
1389
|
+
<button type="button" className="lexy-dp-list-section" onClick={() => setTriggersOpen((v) => !v)}>
|
|
1390
|
+
Detonantes
|
|
1391
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (triggersOpen ? " is-open" : "")} />
|
|
1392
|
+
</button>
|
|
1393
|
+
{triggersOpen ? (
|
|
1394
|
+
<div className="lexy-dp-detail-body">
|
|
1395
|
+
{observedTriggers.length > 0 ? (
|
|
1396
|
+
<ul className="lexy-dp-trigger-list">
|
|
1397
|
+
{observedTriggers.map((trigger) => (
|
|
1398
|
+
<li key={trigger} className="lexy-dp-trigger-item">
|
|
1399
|
+
<span className="lexy-dp-bullet">·</span>
|
|
1400
|
+
<span className="lexy-dp-mono">{trigger}</span>
|
|
1401
|
+
</li>
|
|
1402
|
+
))}
|
|
1403
|
+
</ul>
|
|
1404
|
+
) : event.trigger ? (
|
|
1405
|
+
<span className="lexy-dp-muted">{event.trigger} · sin publicar aún</span>
|
|
1406
|
+
) : (
|
|
1407
|
+
<span className="lexy-dp-muted">Sin publicar aún.</span>
|
|
1408
|
+
)}
|
|
1409
|
+
</div>
|
|
1410
|
+
) : null}
|
|
1411
|
+
|
|
1412
|
+
<button type="button" className="lexy-dp-list-section" onClick={() => setPayloadOpen((v) => !v)}>
|
|
1413
|
+
Payload
|
|
1414
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (payloadOpen ? " is-open" : "")} />
|
|
1415
|
+
</button>
|
|
1416
|
+
{payloadOpen ? (
|
|
1417
|
+
<div className="lexy-dp-detail-body">
|
|
1418
|
+
{payload !== undefined && payload !== null ? (
|
|
1419
|
+
typeof payload === "object" ? (
|
|
1420
|
+
<div className="lexy-dp-mono" style={{ fontSize: "12px" }}>
|
|
1421
|
+
{Object.entries(payload as Record<string, unknown>).map(([key, value]) => (
|
|
1422
|
+
<PayloadKeyNode key={key} label={key} value={value} />
|
|
1423
|
+
))}
|
|
1424
|
+
</div>
|
|
1425
|
+
) : (
|
|
1426
|
+
<span className="lexy-dp-mono lexy-dp-muted">{typeof payload}</span>
|
|
1427
|
+
)
|
|
1428
|
+
) : (
|
|
1429
|
+
<span className="lexy-dp-muted">Sin datos aún — publica el evento para ver el payload.</span>
|
|
1430
|
+
)}
|
|
1431
|
+
</div>
|
|
1432
|
+
) : null}
|
|
1433
|
+
</div>
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function LoadDetail({ load }: { load: CatalogLoad }) {
|
|
1438
|
+
const [paramsOpen, setParamsOpen] = useState(true);
|
|
1439
|
+
const params = load.lastParams;
|
|
1440
|
+
|
|
1441
|
+
return (
|
|
1442
|
+
<div className="lexy-dp-eventdetail">
|
|
1443
|
+
<div className="lexy-dp-detail-body">
|
|
1444
|
+
<div className="lexy-dp-detail-field">
|
|
1445
|
+
<span className="lexy-dp-detail-label">Nombre</span>
|
|
1446
|
+
<span>{load.technicalId}</span>
|
|
1447
|
+
</div>
|
|
1448
|
+
{load.description ? (
|
|
1449
|
+
<div className="lexy-dp-detail-field">
|
|
1450
|
+
<span className="lexy-dp-detail-label">Descripción</span>
|
|
1451
|
+
<span>{load.description}</span>
|
|
1452
|
+
</div>
|
|
1453
|
+
) : null}
|
|
1454
|
+
{load.reads.entities.length > 0 ? (
|
|
1455
|
+
<div className="lexy-dp-detail-field">
|
|
1456
|
+
<span className="lexy-dp-detail-label">Lee</span>
|
|
1457
|
+
<span className="lexy-dp-mono">{load.reads.entities.join(", ")}</span>
|
|
1458
|
+
</div>
|
|
1459
|
+
) : null}
|
|
1460
|
+
</div>
|
|
1461
|
+
|
|
1462
|
+
<button type="button" className="lexy-dp-list-section" onClick={() => setParamsOpen((v) => !v)}>
|
|
1463
|
+
Parámetros
|
|
1464
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (paramsOpen ? " is-open" : "")} />
|
|
1465
|
+
</button>
|
|
1466
|
+
{paramsOpen ? (
|
|
1467
|
+
<div className="lexy-dp-detail-body">
|
|
1468
|
+
{params !== undefined && params !== null && typeof params === "object" ? (
|
|
1469
|
+
<div className="lexy-dp-mono" style={{ fontSize: "12px" }}>
|
|
1470
|
+
{Object.entries(params as Record<string, unknown>).map(([key, value]) => (
|
|
1471
|
+
<PayloadKeyNode key={key} label={key} value={value} />
|
|
1472
|
+
))}
|
|
1473
|
+
</div>
|
|
1474
|
+
) : (
|
|
1475
|
+
<span className="lexy-dp-muted">Sin parámetros — esta carga no recibe filtros.</span>
|
|
1476
|
+
)}
|
|
1477
|
+
</div>
|
|
1478
|
+
) : null}
|
|
1479
|
+
</div>
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
type SelectedContract =
|
|
1484
|
+
| { kind: "event"; id: string }
|
|
1485
|
+
| { kind: "load"; id: string }
|
|
1486
|
+
| { kind: "entity"; id: string };
|
|
1487
|
+
|
|
1488
|
+
type EntitySelectedItem =
|
|
1489
|
+
| { kind: "field"; id: string }
|
|
1490
|
+
| { kind: "relation"; id: string; reverse: boolean };
|
|
1491
|
+
|
|
1492
|
+
function EntityDetail({ entityId }: { entityId: string }) {
|
|
1493
|
+
const [fieldsOpen, setFieldsOpen] = useState(true);
|
|
1494
|
+
const [selected, setSelected] = useState<EntitySelectedItem | null>(null);
|
|
1495
|
+
const [itemDetailOpen, setItemDetailOpen] = useState(true);
|
|
1496
|
+
|
|
1497
|
+
const entries = useSyncExternalStore(
|
|
1498
|
+
catalog.subscribe,
|
|
1499
|
+
catalog.getSnapshot,
|
|
1500
|
+
catalog.getSnapshot,
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
const entity = prototypeDataContract.entities[entityId];
|
|
1504
|
+
if (!entity) return <p className="lexy-dp-empty">Entidad no encontrada.</p>;
|
|
1505
|
+
|
|
1506
|
+
const entityRelations = [
|
|
1507
|
+
...prototypeDataContract.relations
|
|
1508
|
+
.filter((r) => r.fromEntity === entityId)
|
|
1509
|
+
.map((r) => ({ ...r, targetEntity: r.toEntity, reverse: false })),
|
|
1510
|
+
...prototypeDataContract.relations
|
|
1511
|
+
.filter((r) => r.toEntity === entityId)
|
|
1512
|
+
.map((r) => ({ ...r, targetEntity: r.fromEntity, reverse: true })),
|
|
1513
|
+
];
|
|
1514
|
+
|
|
1515
|
+
const hasRows = Object.keys(entity.fields).length > 0 || entityRelations.length > 0;
|
|
1516
|
+
|
|
1517
|
+
const selectedField = selected?.kind === "field" ? entity.fields[selected.id] : null;
|
|
1518
|
+
const selectedRelation =
|
|
1519
|
+
selected?.kind === "relation"
|
|
1520
|
+
? entityRelations.find((r) => r.id === selected.id && r.reverse === selected.reverse) ?? null
|
|
1521
|
+
: null;
|
|
1522
|
+
|
|
1523
|
+
const fieldEvents =
|
|
1524
|
+
selected?.kind === "field"
|
|
1525
|
+
? entries.filter(
|
|
1526
|
+
(entry): entry is CatalogEvent =>
|
|
1527
|
+
entry.kind === "event" &&
|
|
1528
|
+
entry.writes.fields.includes(entityId + "." + selected.id),
|
|
1529
|
+
)
|
|
1530
|
+
: [];
|
|
1531
|
+
|
|
1532
|
+
const handleClick = (item: EntitySelectedItem) => {
|
|
1533
|
+
setSelected((prev) =>
|
|
1534
|
+
prev?.kind === item.kind && prev.id === item.id ? null : item,
|
|
1535
|
+
);
|
|
1536
|
+
setItemDetailOpen(true);
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
const isSelected = (item: EntitySelectedItem) =>
|
|
1540
|
+
selected?.kind === item.kind &&
|
|
1541
|
+
selected.id === item.id &&
|
|
1542
|
+
(item.kind !== "relation" || (selected as { reverse: boolean }).reverse === item.reverse);
|
|
1543
|
+
|
|
1544
|
+
return (
|
|
1545
|
+
<div className="lexy-dp-eventdetail">
|
|
1546
|
+
<div className="lexy-dp-detail-body">
|
|
1547
|
+
<div className="lexy-dp-detail-field">
|
|
1548
|
+
<span className="lexy-dp-detail-label">Nombre</span>
|
|
1549
|
+
<span>{entityId}</span>
|
|
1550
|
+
</div>
|
|
1551
|
+
<div className="lexy-dp-detail-field">
|
|
1552
|
+
<span className="lexy-dp-detail-label">Descripción</span>
|
|
1553
|
+
<span>{entity.productDescription}</span>
|
|
1554
|
+
</div>
|
|
1555
|
+
</div>
|
|
1556
|
+
|
|
1557
|
+
<button type="button" className="lexy-dp-list-section" onClick={() => setFieldsOpen((v) => !v)}>
|
|
1558
|
+
Campos
|
|
1559
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (fieldsOpen ? " is-open" : "")} />
|
|
1560
|
+
</button>
|
|
1561
|
+
{fieldsOpen ? (
|
|
1562
|
+
!hasRows ? (
|
|
1563
|
+
<div className="lexy-dp-detail-body">
|
|
1564
|
+
<span className="lexy-dp-muted">Sin campos declarados.</span>
|
|
1565
|
+
</div>
|
|
1566
|
+
) : (
|
|
1567
|
+
<div style={{ overflowX: "auto" }}>
|
|
1568
|
+
<table className="lexy-dp-field-table">
|
|
1569
|
+
<thead>
|
|
1570
|
+
<tr>
|
|
1571
|
+
<th style={{ color: "var(--dp-fg)" }}>Campo</th>
|
|
1572
|
+
<th style={{ width: "100px", color: "var(--dp-muted-fg)" }}>Tipo</th>
|
|
1573
|
+
</tr>
|
|
1574
|
+
</thead>
|
|
1575
|
+
<tbody>
|
|
1576
|
+
{Object.entries(entity.fields).map(([fieldId, field]) => {
|
|
1577
|
+
const sel = isSelected({ kind: "field", id: fieldId });
|
|
1578
|
+
return (
|
|
1579
|
+
<tr
|
|
1580
|
+
key={fieldId}
|
|
1581
|
+
className={sel ? "is-selected" : ""}
|
|
1582
|
+
onClick={() => handleClick({ kind: "field", id: fieldId })}
|
|
1583
|
+
>
|
|
1584
|
+
<td style={sel ? undefined : { color: TYPE_COLORS[field.dataType] ?? "var(--dp-fg)" }}>
|
|
1585
|
+
{fieldId}
|
|
1586
|
+
</td>
|
|
1587
|
+
<td className={sel ? undefined : "lexy-dp-muted"}>{field.dataType}</td>
|
|
1588
|
+
</tr>
|
|
1589
|
+
);
|
|
1590
|
+
})}
|
|
1591
|
+
{entityRelations.map((rel) => {
|
|
1592
|
+
const sel = isSelected({ kind: "relation", id: rel.id, reverse: rel.reverse });
|
|
1593
|
+
return (
|
|
1594
|
+
<tr
|
|
1595
|
+
key={rel.id + (rel.reverse ? "_r" : "")}
|
|
1596
|
+
className={sel ? "is-selected" : ""}
|
|
1597
|
+
onClick={() => handleClick({ kind: "relation", id: rel.id, reverse: rel.reverse })}
|
|
1598
|
+
>
|
|
1599
|
+
<td style={sel ? undefined : { color: "var(--dp-primary)" }}>
|
|
1600
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: "3px" }}>
|
|
1601
|
+
<ArrowUpRight style={{ width: "11px", height: "11px", flexShrink: 0 }} />
|
|
1602
|
+
{rel.id}
|
|
1603
|
+
</span>
|
|
1604
|
+
</td>
|
|
1605
|
+
<td style={sel ? undefined : { color: "var(--dp-primary)", opacity: 0.8 }}>
|
|
1606
|
+
{rel.targetEntity}
|
|
1607
|
+
</td>
|
|
1608
|
+
</tr>
|
|
1609
|
+
);
|
|
1610
|
+
})}
|
|
1611
|
+
</tbody>
|
|
1612
|
+
</table>
|
|
1613
|
+
</div>
|
|
1614
|
+
)
|
|
1615
|
+
) : null}
|
|
1616
|
+
|
|
1617
|
+
{selected ? (
|
|
1618
|
+
<>
|
|
1619
|
+
<button type="button" className="lexy-dp-list-section" onClick={() => setItemDetailOpen((v) => !v)}>
|
|
1620
|
+
<span>Detalle · <span className="lexy-dp-mono">{selected.id}</span></span>
|
|
1621
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (itemDetailOpen ? " is-open" : "")} />
|
|
1622
|
+
</button>
|
|
1623
|
+
{itemDetailOpen ? (
|
|
1624
|
+
<div className="lexy-dp-detail-body">
|
|
1625
|
+
{selectedField ? (
|
|
1626
|
+
<>
|
|
1627
|
+
<div className="lexy-dp-detail-field">
|
|
1628
|
+
<span className="lexy-dp-detail-label">Eventos que modifican</span>
|
|
1629
|
+
</div>
|
|
1630
|
+
{fieldEvents.length === 0 ? (
|
|
1631
|
+
<span className="lexy-dp-muted">Ningún evento modifica este campo.</span>
|
|
1632
|
+
) : (
|
|
1633
|
+
<ul className="lexy-dp-trigger-list">
|
|
1634
|
+
{fieldEvents.map((event) => (
|
|
1635
|
+
<li key={event.technicalId} className="lexy-dp-trigger-item">
|
|
1636
|
+
<span className="lexy-dp-bullet">·</span>
|
|
1637
|
+
<span className="lexy-dp-mono">{event.technicalId}</span>
|
|
1638
|
+
</li>
|
|
1639
|
+
))}
|
|
1640
|
+
</ul>
|
|
1641
|
+
)}
|
|
1642
|
+
</>
|
|
1643
|
+
) : selectedRelation ? (
|
|
1644
|
+
<>
|
|
1645
|
+
<div className="lexy-dp-detail-field">
|
|
1646
|
+
<span className="lexy-dp-detail-label">Descripción</span>
|
|
1647
|
+
<span>{selectedRelation.productDescription}</span>
|
|
1648
|
+
</div>
|
|
1649
|
+
<div className="lexy-dp-detail-field">
|
|
1650
|
+
<span className="lexy-dp-detail-label">Cardinalidad</span>
|
|
1651
|
+
<span>{selectedRelation.cardinality}</span>
|
|
1652
|
+
</div>
|
|
1653
|
+
<div className="lexy-dp-detail-field">
|
|
1654
|
+
<span className="lexy-dp-detail-label">Resolución</span>
|
|
1655
|
+
<span>{selectedRelation.resolution}</span>
|
|
1656
|
+
</div>
|
|
1657
|
+
</>
|
|
1658
|
+
) : null}
|
|
1659
|
+
</div>
|
|
1660
|
+
) : null}
|
|
1661
|
+
</>
|
|
1662
|
+
) : null}
|
|
1663
|
+
</div>
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function EventsTab() {
|
|
1668
|
+
const entries = useSyncExternalStore(
|
|
1669
|
+
catalog.subscribe,
|
|
1670
|
+
catalog.getSnapshot,
|
|
1671
|
+
catalog.getSnapshot,
|
|
1672
|
+
);
|
|
1673
|
+
const events = entries.filter((e): e is CatalogEvent => e.kind === "event");
|
|
1674
|
+
const loads = entries.filter((e): e is CatalogLoad => e.kind === "load");
|
|
1675
|
+
const entities = Object.entries(prototypeDataContract.entities);
|
|
1676
|
+
|
|
1677
|
+
const defaultSelected: SelectedContract | null =
|
|
1678
|
+
entities[0] ? { kind: "entity", id: entities[0][0] } : null;
|
|
1679
|
+
|
|
1680
|
+
const [selected, setSelected] = useState<SelectedContract | null>(defaultSelected);
|
|
1681
|
+
const [eventsOpen, setEventsOpen] = useState(true);
|
|
1682
|
+
const [loadsOpen, setLoadsOpen] = useState(true);
|
|
1683
|
+
const [entitiesOpen, setEntitiesOpen] = useState(true);
|
|
1684
|
+
|
|
1685
|
+
if (events.length === 0 && loads.length === 0 && entities.length === 0) {
|
|
1686
|
+
return (
|
|
1687
|
+
<p className="lexy-dp-empty">
|
|
1688
|
+
Publica un evento, ejecuta una carga o declara entidades para ver contenido aquí.
|
|
1689
|
+
</p>
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const selectedEvent =
|
|
1694
|
+
selected?.kind === "event"
|
|
1695
|
+
? (events.find((e) => e.technicalId === selected.id) ?? null)
|
|
1696
|
+
: null;
|
|
1697
|
+
const selectedLoad =
|
|
1698
|
+
selected?.kind === "load"
|
|
1699
|
+
? (loads.find((l) => l.technicalId === selected.id) ?? null)
|
|
1700
|
+
: null;
|
|
1701
|
+
|
|
1702
|
+
const renderGroup = (
|
|
1703
|
+
label: string,
|
|
1704
|
+
open: boolean,
|
|
1705
|
+
toggle: () => void,
|
|
1706
|
+
ids: string[],
|
|
1707
|
+
kind: "event" | "load" | "entity",
|
|
1708
|
+
) =>
|
|
1709
|
+
ids.length > 0 ? (
|
|
1710
|
+
<>
|
|
1711
|
+
<li>
|
|
1712
|
+
<button type="button" className="lexy-dp-list-section" onClick={toggle}>
|
|
1713
|
+
{label}
|
|
1714
|
+
<ChevronRight className={"lexy-dp-chevron lexy-dp-chevron--sm" + (open ? " is-open" : "")} />
|
|
1715
|
+
</button>
|
|
1716
|
+
</li>
|
|
1717
|
+
{open
|
|
1718
|
+
? ids.map((id) => (
|
|
1719
|
+
<li key={kind + ":" + id}>
|
|
1720
|
+
<button
|
|
1721
|
+
type="button"
|
|
1722
|
+
onClick={() => setSelected({ kind, id })}
|
|
1723
|
+
title={id}
|
|
1724
|
+
className={
|
|
1725
|
+
"lexy-dp-listitem" +
|
|
1726
|
+
(selected?.kind === kind && selected.id === id ? " is-active" : "")
|
|
1727
|
+
}
|
|
1728
|
+
>
|
|
1729
|
+
{id}
|
|
1730
|
+
</button>
|
|
1731
|
+
</li>
|
|
1732
|
+
))
|
|
1733
|
+
: null}
|
|
1734
|
+
</>
|
|
1735
|
+
) : null;
|
|
1736
|
+
|
|
1737
|
+
return (
|
|
1738
|
+
<div className="lexy-dp-split">
|
|
1739
|
+
<ul className="lexy-dp-split__list">
|
|
1740
|
+
{renderGroup("Eventos", eventsOpen, () => setEventsOpen((v) => !v), events.map((e) => e.technicalId), "event")}
|
|
1741
|
+
{renderGroup("Cargas de datos", loadsOpen, () => setLoadsOpen((v) => !v), loads.map((l) => l.technicalId), "load")}
|
|
1742
|
+
{renderGroup("Entidades", entitiesOpen, () => setEntitiesOpen((v) => !v), entities.map(([id]) => id), "entity")}
|
|
1743
|
+
</ul>
|
|
1744
|
+
<div className="lexy-dp-split__detail">
|
|
1745
|
+
{selected?.kind === "event" && selectedEvent ? (
|
|
1746
|
+
<EventDetail event={selectedEvent} />
|
|
1747
|
+
) : selected?.kind === "load" && selectedLoad ? (
|
|
1748
|
+
<LoadDetail load={selectedLoad} />
|
|
1749
|
+
) : selected?.kind === "entity" ? (
|
|
1750
|
+
<EntityDetail entityId={selected.id} />
|
|
1751
|
+
) : null}
|
|
1752
|
+
</div>
|
|
1753
|
+
</div>
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
type DesignerTab = "activity" | "events";
|
|
1758
|
+
|
|
1759
|
+
export function DesignerPanelPage() {
|
|
1760
|
+
const [tab, setTab] = useState<DesignerTab>("activity");
|
|
1761
|
+
|
|
1762
|
+
return (
|
|
1763
|
+
<div className="lexy-dp lexy-dp-page">
|
|
1764
|
+
<PanelStyles />
|
|
1765
|
+
<div className="lexy-dp-tablist" role="tablist">
|
|
1766
|
+
<button
|
|
1767
|
+
type="button"
|
|
1768
|
+
role="tab"
|
|
1769
|
+
aria-selected={tab === "activity"}
|
|
1770
|
+
className={"lexy-dp-tab" + (tab === "activity" ? " is-active" : "")}
|
|
1771
|
+
onClick={() => setTab("activity")}
|
|
1772
|
+
>
|
|
1773
|
+
<Activity />
|
|
1774
|
+
Actividad
|
|
1775
|
+
</button>
|
|
1776
|
+
<button
|
|
1777
|
+
type="button"
|
|
1778
|
+
role="tab"
|
|
1779
|
+
aria-selected={tab === "events"}
|
|
1780
|
+
className={"lexy-dp-tab" + (tab === "events" ? " is-active" : "")}
|
|
1781
|
+
onClick={() => setTab("events")}
|
|
1782
|
+
>
|
|
1783
|
+
<Zap />
|
|
1784
|
+
Eventos y Datos
|
|
1785
|
+
</button>
|
|
1786
|
+
</div>
|
|
1787
|
+
<div className="lexy-dp-body">
|
|
1788
|
+
{tab === "activity" ? (
|
|
1789
|
+
<div className="lexy-dp-panel">
|
|
1790
|
+
<ActivityTab />
|
|
1791
|
+
</div>
|
|
1792
|
+
) : null}
|
|
1793
|
+
{tab === "events" ? (
|
|
1794
|
+
<div className="lexy-dp-panel">
|
|
1795
|
+
<EventsTab />
|
|
1796
|
+
</div>
|
|
1797
|
+
) : null}
|
|
1798
|
+
</div>
|
|
1799
|
+
</div>
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
export function PrototypeDesignerPanel() {
|
|
1804
|
+
const [open, setOpen] = useState(false);
|
|
1805
|
+
return (
|
|
1806
|
+
<div className="lexy-dp">
|
|
1807
|
+
<PanelStyles />
|
|
1808
|
+
{open ? (
|
|
1809
|
+
<div role="dialog" aria-label="Actividad del prototipo" className="lexy-dp-sheet">
|
|
1810
|
+
<div className="lexy-dp-sheet__header">
|
|
1811
|
+
<p className="lexy-dp-sheet__title">Panel de Actividad</p>
|
|
1812
|
+
<div className="lexy-dp-sheet__actions">
|
|
1813
|
+
<button
|
|
1814
|
+
type="button"
|
|
1815
|
+
aria-label="Abrir panel completo"
|
|
1816
|
+
title="Abrir panel completo"
|
|
1817
|
+
className="lexy-dp-iconbtn"
|
|
1818
|
+
onClick={() => window.open(DESIGNER_PANEL_ROUTE, "_blank", "noopener,noreferrer")}
|
|
1819
|
+
>
|
|
1820
|
+
<Maximize2 />
|
|
1821
|
+
</button>
|
|
1822
|
+
<button
|
|
1823
|
+
type="button"
|
|
1824
|
+
aria-label="Cerrar"
|
|
1825
|
+
className="lexy-dp-iconbtn"
|
|
1826
|
+
onClick={() => setOpen(false)}
|
|
1827
|
+
>
|
|
1828
|
+
<X />
|
|
1829
|
+
</button>
|
|
1830
|
+
</div>
|
|
1831
|
+
</div>
|
|
1832
|
+
<div className="lexy-dp-sheet__body">
|
|
1833
|
+
<ActivityLog />
|
|
1834
|
+
</div>
|
|
1835
|
+
</div>
|
|
1836
|
+
) : null}
|
|
1837
|
+
|
|
1838
|
+
<button
|
|
1839
|
+
type="button"
|
|
1840
|
+
aria-label="Designer Assist"
|
|
1841
|
+
aria-expanded={open}
|
|
1842
|
+
onClick={() => setOpen((value) => !value)}
|
|
1843
|
+
className={"lexy-dp-launcher" + (open ? " is-open" : "")}
|
|
1844
|
+
>
|
|
1845
|
+
<Settings className="lexy-dp-launcher__icon" />
|
|
1846
|
+
<span className="lexy-dp-launcher__label">Designer Assist</span>
|
|
1847
|
+
</button>
|
|
1848
|
+
</div>
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
`,$e=async(e,t)=>{let o=async(a,n)=>{await Me.mkdir(ie.dirname(ie.join(e,a)),{recursive:!0}),await Me.writeFile(ie.join(e,a),n)};await o(se,Et),await o(le,jt())};import Fe from"fs/promises";import qe from"path";var ce="src/prototype/mock-store/mock-store.ts",de="src/prototype/mock-store/fixtures.ts",Ct="src/prototype/ports/types.ts",It="src/prototype/ports/catalog.ts",Tt="src/prototype/ports/mock-read.ts",Rt="src/prototype/ports/mock-write.ts",Nt="src/prototype/ports/production.ts",pe="src/prototype/ports/index.ts",Ot=String.raw`import { fixtures } from "./fixtures";
|
|
1852
|
+
|
|
1853
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
1854
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
1855
|
+
export type MockEntityRecord = Record<string, JsonValue>;
|
|
1856
|
+
|
|
1857
|
+
export type PrototypeMockState = {
|
|
1858
|
+
datasetVersion: number;
|
|
1859
|
+
entities: Record<string, MockEntityRecord[]>;
|
|
1860
|
+
entitySequences: Record<string, number>;
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
export type MockStorage = {
|
|
1864
|
+
read(): string | null;
|
|
1865
|
+
write(value: string): void;
|
|
1866
|
+
remove(): void;
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
export type PrototypeMockStore = {
|
|
1870
|
+
getSnapshot(): PrototypeMockState;
|
|
1871
|
+
subscribe(listener: () => void): () => void;
|
|
1872
|
+
transact(operation: (draft: PrototypeMockState) => void): PrototypeMockState;
|
|
1873
|
+
clearPersistence(): void;
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
const STORAGE_KEY = "lexy:prototype:mock-store:v1";
|
|
1877
|
+
|
|
1878
|
+
const clone = <T>(value: T): T => {
|
|
1879
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
1880
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
const buildSequences = (entities: Record<string, MockEntityRecord[]>) => {
|
|
1884
|
+
const sequences: Record<string, number> = {};
|
|
1885
|
+
for (const [entityId, records] of Object.entries(entities)) {
|
|
1886
|
+
sequences[entityId] = records.length + 1;
|
|
1887
|
+
}
|
|
1888
|
+
return sequences;
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
const stateFromFixtures = (): PrototypeMockState => {
|
|
1892
|
+
const entities = clone(fixtures.entities);
|
|
1893
|
+
return {
|
|
1894
|
+
datasetVersion: fixtures.datasetVersion,
|
|
1895
|
+
entities,
|
|
1896
|
+
entitySequences: buildSequences(entities),
|
|
1897
|
+
};
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
const isCompatible = (value: unknown): value is PrototypeMockState => {
|
|
1901
|
+
if (!value || typeof value !== "object") return false;
|
|
1902
|
+
const state = value as Partial<PrototypeMockState>;
|
|
1903
|
+
return (
|
|
1904
|
+
state.datasetVersion === fixtures.datasetVersion &&
|
|
1905
|
+
Boolean(state.entities && typeof state.entities === "object") &&
|
|
1906
|
+
Boolean(state.entitySequences && typeof state.entitySequences === "object")
|
|
1907
|
+
);
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
const createMemoryStorage = (): MockStorage => {
|
|
1911
|
+
let current: string | null = null;
|
|
1912
|
+
return {
|
|
1913
|
+
read: () => current,
|
|
1914
|
+
write: (value) => {
|
|
1915
|
+
current = value;
|
|
1916
|
+
},
|
|
1917
|
+
remove: () => {
|
|
1918
|
+
current = null;
|
|
1919
|
+
},
|
|
1920
|
+
};
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
const createLocalStorageStorage = (key: string): MockStorage => {
|
|
1924
|
+
const memory = createMemoryStorage();
|
|
1925
|
+
const getLocalStorage = () => {
|
|
1926
|
+
try {
|
|
1927
|
+
return globalThis.localStorage;
|
|
1928
|
+
} catch {
|
|
1929
|
+
return undefined;
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
return {
|
|
1933
|
+
read: () => {
|
|
1934
|
+
try {
|
|
1935
|
+
return getLocalStorage()?.getItem(key) ?? memory.read();
|
|
1936
|
+
} catch {
|
|
1937
|
+
return memory.read();
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
write: (value) => {
|
|
1941
|
+
memory.write(value);
|
|
1942
|
+
try {
|
|
1943
|
+
getLocalStorage()?.setItem(key, value);
|
|
1944
|
+
} catch {
|
|
1945
|
+
// fallback en memoria
|
|
1946
|
+
}
|
|
1947
|
+
},
|
|
1948
|
+
remove: () => {
|
|
1949
|
+
memory.remove();
|
|
1950
|
+
try {
|
|
1951
|
+
getLocalStorage()?.removeItem(key);
|
|
1952
|
+
} catch {
|
|
1953
|
+
// fallback en memoria
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
};
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
export const createPrototypeMockStore = (
|
|
1960
|
+
storage: MockStorage = createLocalStorageStorage(STORAGE_KEY),
|
|
1961
|
+
): PrototypeMockStore => {
|
|
1962
|
+
const listeners = new Set<() => void>();
|
|
1963
|
+
|
|
1964
|
+
const hydrate = (): PrototypeMockState => {
|
|
1965
|
+
const persisted = storage.read();
|
|
1966
|
+
if (!persisted) return stateFromFixtures();
|
|
1967
|
+
try {
|
|
1968
|
+
const parsed = JSON.parse(persisted) as unknown;
|
|
1969
|
+
// Al cambiar fixtures se sube datasetVersion: el estado viejo se descarta.
|
|
1970
|
+
if (isCompatible(parsed)) return parsed;
|
|
1971
|
+
} catch {
|
|
1972
|
+
// estado corrupto: reinicia desde fixtures
|
|
1973
|
+
}
|
|
1974
|
+
return stateFromFixtures();
|
|
1975
|
+
};
|
|
1976
|
+
|
|
1977
|
+
let snapshot = hydrate();
|
|
1978
|
+
|
|
1979
|
+
const notify = () => {
|
|
1980
|
+
for (const listener of listeners) listener();
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
return {
|
|
1984
|
+
getSnapshot: () => snapshot,
|
|
1985
|
+
subscribe: (listener) => {
|
|
1986
|
+
listeners.add(listener);
|
|
1987
|
+
return () => {
|
|
1988
|
+
listeners.delete(listener);
|
|
1989
|
+
};
|
|
1990
|
+
},
|
|
1991
|
+
transact: (operation) => {
|
|
1992
|
+
const draft = clone(snapshot);
|
|
1993
|
+
operation(draft);
|
|
1994
|
+
snapshot = draft;
|
|
1995
|
+
storage.write(JSON.stringify(snapshot));
|
|
1996
|
+
notify();
|
|
1997
|
+
return snapshot;
|
|
1998
|
+
},
|
|
1999
|
+
clearPersistence: () => storage.remove(),
|
|
2000
|
+
};
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
/** Instancia compartida que ven el adapter de lectura y el de escritura. */
|
|
2004
|
+
export const mockStore = createPrototypeMockStore();
|
|
2005
|
+
|
|
2006
|
+
export const findRecordById = (
|
|
2007
|
+
records: readonly MockEntityRecord[],
|
|
2008
|
+
fieldId: string,
|
|
2009
|
+
id: JsonPrimitive,
|
|
2010
|
+
) => records.find((record) => record[fieldId] === id);
|
|
2011
|
+
`,_t=String.raw`import type { MockEntityRecord } from "./mock-store";
|
|
2012
|
+
|
|
2013
|
+
export type Fixtures = {
|
|
2014
|
+
datasetVersion: number;
|
|
2015
|
+
entities: Record<string, MockEntityRecord[]>;
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Datos sintéticos, deterministas y revisables en PR. Sin Math.random(),
|
|
2020
|
+
* Date.now() ni new Date(). Usa contexto chileno (es-CL, RUT con K mayúscula,
|
|
2021
|
+
* teléfonos +56, fechas ISO, CLP entero, correos example.com).
|
|
2022
|
+
*/
|
|
2023
|
+
export const fixtures: Fixtures = {
|
|
2024
|
+
datasetVersion: 1,
|
|
2025
|
+
entities: {},
|
|
2026
|
+
};
|
|
2027
|
+
`,At=String.raw`import { z } from "zod";
|
|
2028
|
+
|
|
2029
|
+
/** Metadata de una carga de datos (GET): describe qué lee y qué la detona. */
|
|
2030
|
+
export const readMetaSchema = z
|
|
2031
|
+
.object({
|
|
2032
|
+
description: z.string().optional(),
|
|
2033
|
+
trigger: z.string().optional(),
|
|
2034
|
+
reads: z
|
|
2035
|
+
.object({
|
|
2036
|
+
entities: z.array(z.string()).optional(),
|
|
2037
|
+
projections: z.array(z.string()).optional(),
|
|
2038
|
+
})
|
|
2039
|
+
.strict()
|
|
2040
|
+
.optional(),
|
|
2041
|
+
})
|
|
2042
|
+
.strict();
|
|
2043
|
+
|
|
2044
|
+
export type ReadMeta = z.infer<typeof readMetaSchema>;
|
|
2045
|
+
|
|
2046
|
+
/** Metadata de un evento publicado (POST): describe qué escribe y qué lo detona. */
|
|
2047
|
+
export const writeMetaSchema = z
|
|
2048
|
+
.object({
|
|
2049
|
+
description: z.string().optional(),
|
|
2050
|
+
trigger: z.string().optional(),
|
|
2051
|
+
writes: z
|
|
2052
|
+
.object({
|
|
2053
|
+
entities: z.array(z.string()).optional(),
|
|
2054
|
+
fields: z.array(z.string()).optional(),
|
|
2055
|
+
})
|
|
2056
|
+
.strict()
|
|
2057
|
+
.optional(),
|
|
2058
|
+
})
|
|
2059
|
+
.strict();
|
|
2060
|
+
|
|
2061
|
+
export type WriteMeta = z.infer<typeof writeMetaSchema>;
|
|
2062
|
+
|
|
2063
|
+
/** Lo que devuelve publish(): el id del hecho creado y el código HTTP. */
|
|
2064
|
+
export const eventReceiptSchema = z.object({
|
|
2065
|
+
eventId: z.string(),
|
|
2066
|
+
status: z.number().int().min(100).max(599),
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
export type EventReceipt = z.infer<typeof eventReceiptSchema>;
|
|
2070
|
+
|
|
2071
|
+
/** Lectura (GET): resuelve y devuelve datos; un error se lanza como excepción. */
|
|
2072
|
+
export interface LexyReadPort {
|
|
2073
|
+
load<T = unknown>(loadId: string, params?: unknown, meta?: ReadMeta): Promise<T>;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/** Escritura (POST): registra un hecho y devuelve su receipt. */
|
|
2077
|
+
export interface LexyWritePort {
|
|
2078
|
+
publish(eventId: string, payload: unknown, meta?: WriteMeta): Promise<EventReceipt>;
|
|
2079
|
+
}
|
|
2080
|
+
`,Lt=String.raw`import type { EventReceipt, ReadMeta, WriteMeta } from "./types";
|
|
2081
|
+
|
|
2082
|
+
export type CatalogEvent = {
|
|
2083
|
+
kind: "event";
|
|
2084
|
+
technicalId: string;
|
|
2085
|
+
description: string;
|
|
2086
|
+
trigger: string;
|
|
2087
|
+
writes: { entities: string[]; fields: string[] };
|
|
2088
|
+
lastPayload: unknown;
|
|
2089
|
+
lastReceipt: EventReceipt | null;
|
|
2090
|
+
};
|
|
2091
|
+
|
|
2092
|
+
export type CatalogLoad = {
|
|
2093
|
+
kind: "load";
|
|
2094
|
+
technicalId: string;
|
|
2095
|
+
description: string;
|
|
2096
|
+
trigger: string;
|
|
2097
|
+
reads: { entities: string[]; projections: string[] };
|
|
2098
|
+
lastParams: unknown;
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
export type CatalogEntry = CatalogEvent | CatalogLoad;
|
|
2102
|
+
|
|
2103
|
+
type CatalogMessage =
|
|
2104
|
+
| { type: "entry"; entry: CatalogEntry }
|
|
2105
|
+
| { type: "syncRequest" }
|
|
2106
|
+
| { type: "sync"; entries: CatalogEntry[] };
|
|
2107
|
+
|
|
2108
|
+
const createCatalog = () => {
|
|
2109
|
+
const store = new Map<string, CatalogEntry>();
|
|
2110
|
+
const listeners = new Set<() => void>();
|
|
2111
|
+
let snapshot: CatalogEntry[] = [];
|
|
2112
|
+
let synced = false;
|
|
2113
|
+
|
|
2114
|
+
const channel =
|
|
2115
|
+
typeof BroadcastChannel !== "undefined" ? new BroadcastChannel("lexy-ports-catalog") : null;
|
|
2116
|
+
|
|
2117
|
+
const key = (entry: CatalogEntry) => entry.kind + ":" + entry.technicalId;
|
|
2118
|
+
|
|
2119
|
+
const notify = () => {
|
|
2120
|
+
snapshot = Array.from(store.values());
|
|
2121
|
+
for (const l of listeners) l();
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
const apply = (entry: CatalogEntry) => {
|
|
2125
|
+
store.set(key(entry), entry);
|
|
2126
|
+
notify();
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
if (channel) {
|
|
2130
|
+
channel.onmessage = (ev: MessageEvent<CatalogMessage>) => {
|
|
2131
|
+
const msg = ev.data;
|
|
2132
|
+
if (msg.type === "entry") apply(msg.entry);
|
|
2133
|
+
else if (msg.type === "syncRequest")
|
|
2134
|
+
channel.postMessage({ type: "sync", entries: Array.from(store.values()) });
|
|
2135
|
+
else if (msg.type === "sync" && !synced && store.size === 0) {
|
|
2136
|
+
synced = true;
|
|
2137
|
+
for (const entry of msg.entries) store.set(key(entry), entry);
|
|
2138
|
+
notify();
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
channel.postMessage({ type: "syncRequest" });
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
const publishEntry = (entry: CatalogEntry) => {
|
|
2145
|
+
store.set(key(entry), entry);
|
|
2146
|
+
notify();
|
|
2147
|
+
channel?.postMessage({ type: "entry", entry });
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
return {
|
|
2151
|
+
recordEvent(technicalId: string, payload: unknown, meta: WriteMeta, receipt: EventReceipt) {
|
|
2152
|
+
const existing = store.get("event:" + technicalId);
|
|
2153
|
+
const prev = existing?.kind === "event" ? existing : undefined;
|
|
2154
|
+
publishEntry({
|
|
2155
|
+
kind: "event",
|
|
2156
|
+
technicalId,
|
|
2157
|
+
description: meta.description ?? prev?.description ?? "",
|
|
2158
|
+
trigger: meta.trigger ?? prev?.trigger ?? "",
|
|
2159
|
+
writes: {
|
|
2160
|
+
entities: meta.writes?.entities ?? prev?.writes.entities ?? [],
|
|
2161
|
+
fields: meta.writes?.fields ?? prev?.writes.fields ?? [],
|
|
2162
|
+
},
|
|
2163
|
+
lastPayload: payload,
|
|
2164
|
+
lastReceipt: receipt,
|
|
2165
|
+
});
|
|
2166
|
+
},
|
|
2167
|
+
recordLoad(technicalId: string, params: unknown, meta: ReadMeta) {
|
|
2168
|
+
const existing = store.get("load:" + technicalId);
|
|
2169
|
+
const prev = existing?.kind === "load" ? existing : undefined;
|
|
2170
|
+
publishEntry({
|
|
2171
|
+
kind: "load",
|
|
2172
|
+
technicalId,
|
|
2173
|
+
description: meta.description ?? prev?.description ?? "",
|
|
2174
|
+
trigger: meta.trigger ?? prev?.trigger ?? "",
|
|
2175
|
+
reads: {
|
|
2176
|
+
entities: meta.reads?.entities ?? prev?.reads.entities ?? [],
|
|
2177
|
+
projections: meta.reads?.projections ?? prev?.reads.projections ?? [],
|
|
2178
|
+
},
|
|
2179
|
+
lastParams: params,
|
|
2180
|
+
});
|
|
2181
|
+
},
|
|
2182
|
+
subscribe: (l: () => void) => {
|
|
2183
|
+
listeners.add(l);
|
|
2184
|
+
return () => listeners.delete(l);
|
|
2185
|
+
},
|
|
2186
|
+
getSnapshot: () => snapshot,
|
|
2187
|
+
};
|
|
2188
|
+
};
|
|
2189
|
+
|
|
2190
|
+
export const catalog = createCatalog();
|
|
2191
|
+
`,Dt=String.raw`import { designerActivity } from "../designer-panel/designer-activity";
|
|
2192
|
+
import { type MockEntityRecord, mockStore } from "../mock-store/mock-store";
|
|
2193
|
+
import { catalog } from "./catalog";
|
|
2194
|
+
import { type LexyReadPort, type ReadMeta, readMetaSchema } from "./types";
|
|
2195
|
+
|
|
2196
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
2197
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2198
|
+
|
|
2199
|
+
export const mockRead: LexyReadPort = {
|
|
2200
|
+
async load<T = unknown>(loadId: string, params?: unknown, rawMeta: ReadMeta = {}): Promise<T> {
|
|
2201
|
+
const parsed = readMetaSchema.safeParse(rawMeta);
|
|
2202
|
+
if (!parsed.success) {
|
|
2203
|
+
console.warn(
|
|
2204
|
+
'[lexy] Metadata de carga inválida para "' + loadId + '":',
|
|
2205
|
+
parsed.error.flatten().fieldErrors,
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
const meta: ReadMeta = parsed.success ? parsed.data : {};
|
|
2209
|
+
catalog.recordLoad(loadId, params, meta);
|
|
2210
|
+
|
|
2211
|
+
const entityId = meta.reads?.entities?.[0];
|
|
2212
|
+
const all = entityId ? (mockStore.getSnapshot().entities[entityId] ?? []) : [];
|
|
2213
|
+
// Filtro genérico: si params es un objeto, exige coincidencia exacta de sus campos.
|
|
2214
|
+
const data: MockEntityRecord[] = isPlainObject(params)
|
|
2215
|
+
? all.filter((record) =>
|
|
2216
|
+
Object.entries(params).every(([field, value]) => record[field] === value),
|
|
2217
|
+
)
|
|
2218
|
+
: all;
|
|
2219
|
+
designerActivity.record({
|
|
2220
|
+
kind: "load",
|
|
2221
|
+
label: meta.description ?? loadId,
|
|
2222
|
+
technicalId: loadId,
|
|
2223
|
+
triggerId: meta.trigger,
|
|
2224
|
+
input: params,
|
|
2225
|
+
});
|
|
2226
|
+
return data as T;
|
|
2227
|
+
},
|
|
2228
|
+
};
|
|
2229
|
+
`,zt=String.raw`import { designerActivity } from "../designer-panel/designer-activity";
|
|
2230
|
+
import { mockStore } from "../mock-store/mock-store";
|
|
2231
|
+
import { catalog } from "./catalog";
|
|
2232
|
+
import { type EventReceipt, type LexyWritePort, type WriteMeta, writeMetaSchema } from "./types";
|
|
2233
|
+
|
|
2234
|
+
let publicationSequence = 0;
|
|
2235
|
+
|
|
2236
|
+
export const mockWrite: LexyWritePort = {
|
|
2237
|
+
async publish(eventId: string, payload: unknown, rawMeta: WriteMeta = {}): Promise<EventReceipt> {
|
|
2238
|
+
const parsed = writeMetaSchema.safeParse(rawMeta);
|
|
2239
|
+
if (!parsed.success) {
|
|
2240
|
+
console.warn(
|
|
2241
|
+
'[lexy] Metadata de evento inválida para "' + eventId + '":',
|
|
2242
|
+
parsed.error.flatten().fieldErrors,
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
const meta: WriteMeta = parsed.success ? parsed.data : {};
|
|
2246
|
+
|
|
2247
|
+
const entityId = meta.writes?.entities?.[0];
|
|
2248
|
+
if (entityId) {
|
|
2249
|
+
mockStore.transact((draft) => {
|
|
2250
|
+
const records = (draft.entities[entityId] ??= []);
|
|
2251
|
+
const nextId = draft.entitySequences[entityId] ?? records.length + 1;
|
|
2252
|
+
records.push({ id: String(nextId), ...(payload as Record<string, unknown>) });
|
|
2253
|
+
draft.entitySequences[entityId] = nextId + 1;
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
publicationSequence += 1;
|
|
2258
|
+
const receipt: EventReceipt = {
|
|
2259
|
+
eventId: eventId + "_" + publicationSequence,
|
|
2260
|
+
status: 201,
|
|
2261
|
+
};
|
|
2262
|
+
catalog.recordEvent(eventId, payload, meta, receipt);
|
|
2263
|
+
designerActivity.record({
|
|
2264
|
+
kind: "event",
|
|
2265
|
+
label: meta.description ?? eventId,
|
|
2266
|
+
technicalId: eventId,
|
|
2267
|
+
triggerId: meta.trigger,
|
|
2268
|
+
input: payload,
|
|
2269
|
+
});
|
|
2270
|
+
return receipt;
|
|
2271
|
+
},
|
|
2272
|
+
};
|
|
2273
|
+
`,Mt=String.raw`import type { LexyReadPort, LexyWritePort } from "./types";
|
|
2274
|
+
|
|
2275
|
+
export const productionRead: LexyReadPort = {
|
|
2276
|
+
async load<T = unknown>(loadId: string): Promise<T> {
|
|
2277
|
+
throw new Error(
|
|
2278
|
+
'[lexy] productionRead.load("' + loadId + '") sin implementar: conéctalo al GET de tu API.',
|
|
2279
|
+
);
|
|
2280
|
+
},
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
export const productionWrite: LexyWritePort = {
|
|
2284
|
+
async publish(eventId: string) {
|
|
2285
|
+
throw new Error(
|
|
2286
|
+
'[lexy] productionWrite.publish("' + eventId + '") sin implementar: encólalo en el worker.',
|
|
2287
|
+
);
|
|
2288
|
+
},
|
|
2289
|
+
};
|
|
2290
|
+
`,$t=String.raw`import { mockRead } from "./mock-read";
|
|
2291
|
+
import { mockWrite } from "./mock-write";
|
|
2292
|
+
import { productionRead, productionWrite } from "./production";
|
|
2293
|
+
import type { LexyReadPort, LexyWritePort } from "./types";
|
|
2294
|
+
|
|
2295
|
+
const USE_MOCK = import.meta.env.DEV;
|
|
2296
|
+
|
|
2297
|
+
export const read: LexyReadPort = USE_MOCK ? mockRead : productionRead;
|
|
2298
|
+
export const write: LexyWritePort = USE_MOCK ? mockWrite : productionWrite;
|
|
2299
|
+
|
|
2300
|
+
export type {
|
|
2301
|
+
EventReceipt,
|
|
2302
|
+
LexyReadPort,
|
|
2303
|
+
LexyWritePort,
|
|
2304
|
+
ReadMeta,
|
|
2305
|
+
WriteMeta,
|
|
2306
|
+
} from "./types";
|
|
2307
|
+
`,Ft=[[ce,Ot],[de,_t],[Ct,At],[It,Lt],[Tt,Dt],[Rt,zt],[Nt,Mt],[pe,$t]],Ve=async e=>{for(let[t,o]of Ft){let a=qe.join(e,t);await Fe.mkdir(qe.dirname(a),{recursive:!0}),await Fe.writeFile(a,o)}};var Kt={feature:["src/app","src/features","src/shared/assets","src/shared/components/base","src/shared/hooks","src/shared/services","src/shared/lib","src/shared/types"],layer:["src/assets","src/components/base","src/hooks","src/services","src/lib","src/types","src/views","src/stores"]},Ut="10.33.3",Ht=e=>{let t=e.architecture==="feature"?{"react-router":"^7.9.0"}:{};return JSON.stringify({name:e.projectName,private:!0,version:"0.0.0",type:"module",packageManager:`pnpm@${Ut}`,scripts:{dev:"vite","check:data-contract":"tsx scripts/check-data-contract.ts","check:prototype":"pnpm check:data-contract","build:app":"tsc -b && vite build",build:"pnpm check:prototype && pnpm build:app",preview:"vite preview",lint:"eslint .",format:"prettier --write ."},dependencies:{clsx:"^2.1.1","lucide-react":"^0.460.0",react:"^19.2.6","react-dom":"^19.2.6",...t,"tailwind-merge":"^3.3.1",zod:"^4.1.0"},devDependencies:{"@babel/core":"^7.29.7","@eslint/js":"^10.0.1","@rolldown/plugin-babel":"^0.2.3","@tailwindcss/vite":"^4.3.0","@types/babel__core":"^7.20.5","@types/node":"^24.12.3","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^6.0.1","babel-plugin-react-compiler":"^1.0.0",eslint:"^10.4.1","eslint-plugin-jsx-a11y":"^6.10.2","eslint-plugin-react-hooks":"^7.1.1","eslint-plugin-simple-import-sort":"^13.0.0",globals:"^17.6.0",prettier:"^3.8.4",tailwindcss:"^4.3.0",terser:"^5.48.0",tsx:"^4.19.0",typescript:"~5.9.3","typescript-eslint":"^8.61.0",vite:"^8.0.12"}},null,2)+`
|
|
2308
|
+
`},Bt=`import path from "node:path";
|
|
26
2309
|
|
|
27
2310
|
import babel from "@rolldown/plugin-babel";
|
|
28
2311
|
import tailwindcss from "@tailwindcss/vite";
|
|
@@ -50,9 +2333,30 @@ export default defineConfig({
|
|
|
50
2333
|
terserOptions: {
|
|
51
2334
|
compress: { drop_console: true, drop_debugger: true },
|
|
52
2335
|
},
|
|
2336
|
+
// Code splitting por grupos de vendor (auditor\xEDa de desarrollo, jun 2026):
|
|
2337
|
+
// node_modules se separa en bundles tem\xE1ticos para carga diferida y mejor
|
|
2338
|
+
// cach\xE9 entre deploys. Los grupos sin match simplemente no emiten chunk.
|
|
2339
|
+
rolldownOptions: {
|
|
2340
|
+
output: {
|
|
2341
|
+
codeSplitting: {
|
|
2342
|
+
groups: [
|
|
2343
|
+
{ name: "react-core", test: /node_modules[\\\\/](react|react-dom|scheduler)[\\\\/]/ },
|
|
2344
|
+
{ name: "router", test: /node_modules[\\\\/]react-router[\\\\/]/ },
|
|
2345
|
+
{ name: "query", test: /node_modules[\\\\/]@tanstack[\\\\/]/ },
|
|
2346
|
+
{ name: "state", test: /node_modules[\\\\/]jotai[\\\\/]/ },
|
|
2347
|
+
{ name: "utils", test: /node_modules[\\\\/](clsx|tailwind-merge|class-variance-authority|zod)[\\\\/]/ },
|
|
2348
|
+
{ name: "ui", test: /node_modules[\\\\/](tailwind-animations|@tailwindcss)[\\\\/]/ },
|
|
2349
|
+
{ name: "radix", test: /node_modules[\\\\/]@radix-ui[\\\\/]/ },
|
|
2350
|
+
{ name: "icons", test: /node_modules[\\\\/]lucide-react[\\\\/]/ },
|
|
2351
|
+
{ name: "vendor", test: /node_modules[\\\\/]/ },
|
|
2352
|
+
],
|
|
2353
|
+
minSize: 20000,
|
|
2354
|
+
},
|
|
2355
|
+
},
|
|
2356
|
+
},
|
|
53
2357
|
},
|
|
54
2358
|
});
|
|
55
|
-
`,
|
|
2359
|
+
`,Jt=`import js from "@eslint/js";
|
|
56
2360
|
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
57
2361
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
58
2362
|
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
|
@@ -81,14 +2385,14 @@ export default tseslint.config(
|
|
|
81
2385
|
},
|
|
82
2386
|
},
|
|
83
2387
|
);
|
|
84
|
-
`,
|
|
2388
|
+
`,Wt=`{
|
|
85
2389
|
"printWidth": 100
|
|
86
2390
|
}
|
|
87
|
-
`,
|
|
2391
|
+
`,Gt=`pnpm-lock.yaml
|
|
88
2392
|
src/lexy-theme.css
|
|
89
2393
|
public/fonts/
|
|
90
2394
|
dist/
|
|
91
|
-
`,
|
|
2395
|
+
`,Qt=`name: CI
|
|
92
2396
|
|
|
93
2397
|
on:
|
|
94
2398
|
push:
|
|
@@ -108,9 +2412,10 @@ jobs:
|
|
|
108
2412
|
- run: pnpm install --frozen-lockfile
|
|
109
2413
|
- run: pnpm lint
|
|
110
2414
|
- run: pnpm exec prettier --check .
|
|
111
|
-
- run: pnpm
|
|
2415
|
+
- run: pnpm check:data-contract
|
|
2416
|
+
- run: pnpm build:app
|
|
112
2417
|
- run: pnpm audit --audit-level high
|
|
113
|
-
`,
|
|
2418
|
+
`,Yt=`{
|
|
114
2419
|
"files": [],
|
|
115
2420
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
|
116
2421
|
"compilerOptions": {
|
|
@@ -118,7 +2423,7 @@ jobs:
|
|
|
118
2423
|
"paths": { "@/*": ["./src/*"] }
|
|
119
2424
|
}
|
|
120
2425
|
}
|
|
121
|
-
`,
|
|
2426
|
+
`,Xt=`{
|
|
122
2427
|
"compilerOptions": {
|
|
123
2428
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
124
2429
|
"target": "ES2022",
|
|
@@ -142,7 +2447,7 @@ jobs:
|
|
|
142
2447
|
},
|
|
143
2448
|
"include": ["src"]
|
|
144
2449
|
}
|
|
145
|
-
`,
|
|
2450
|
+
`,Zt=`{
|
|
146
2451
|
"compilerOptions": {
|
|
147
2452
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
148
2453
|
"target": "ES2023",
|
|
@@ -159,7 +2464,7 @@ jobs:
|
|
|
159
2464
|
},
|
|
160
2465
|
"include": ["vite.config.ts"]
|
|
161
2466
|
}
|
|
162
|
-
`,
|
|
2467
|
+
`,eo=e=>`<!doctype html>
|
|
163
2468
|
<html lang="es">
|
|
164
2469
|
<head>
|
|
165
2470
|
<meta charset="UTF-8" />
|
|
@@ -171,9 +2476,9 @@ jobs:
|
|
|
171
2476
|
<script type="module" src="/src/main.tsx"></script>
|
|
172
2477
|
</body>
|
|
173
2478
|
</html>
|
|
174
|
-
`,
|
|
2479
|
+
`,to=`@import "tailwindcss";
|
|
175
2480
|
@import "./lexy-theme.css";
|
|
176
|
-
`,
|
|
2481
|
+
`,oo=e=>e.architecture==="feature"?`import "./index.css";
|
|
177
2482
|
|
|
178
2483
|
import { StrictMode } from "react";
|
|
179
2484
|
import { createRoot } from "react-dom/client";
|
|
@@ -200,76 +2505,91 @@ createRoot(document.getElementById("root")!).render(
|
|
|
200
2505
|
<App />
|
|
201
2506
|
</StrictMode>,
|
|
202
2507
|
);
|
|
203
|
-
`,
|
|
2508
|
+
`,Ke=` <main className="flex min-h-screen items-center justify-center bg-background text-foreground">
|
|
204
2509
|
<div className="text-center">
|
|
205
2510
|
<h1 className="text-2xl font-semibold">Proyecto Lexy listo</h1>
|
|
206
2511
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
207
2512
|
Trae tu primer componente: <code>npx create-lexy add button</code>
|
|
208
2513
|
</p>
|
|
209
2514
|
</div>
|
|
210
|
-
</main>`,
|
|
2515
|
+
</main>`,ao=e=>e.architecture==="feature"?`import { Route, Routes } from "react-router";
|
|
2516
|
+
|
|
2517
|
+
import { PrototypeDesignerPanel } from "../prototype/designer-panel/PrototypeDesignerPanel";
|
|
211
2518
|
|
|
212
2519
|
const Home = () => {
|
|
213
2520
|
return (
|
|
214
|
-
${
|
|
2521
|
+
${Ke}
|
|
215
2522
|
);
|
|
216
2523
|
};
|
|
217
2524
|
|
|
2525
|
+
const showPrototypeDesignerPanel = import.meta.env.DEV;
|
|
2526
|
+
|
|
218
2527
|
export const App = () => {
|
|
219
2528
|
return (
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
2529
|
+
<>
|
|
2530
|
+
<Routes>
|
|
2531
|
+
<Route path="/" element={<Home />} />
|
|
2532
|
+
</Routes>
|
|
2533
|
+
{showPrototypeDesignerPanel ? <PrototypeDesignerPanel /> : null}
|
|
2534
|
+
</>
|
|
223
2535
|
);
|
|
224
2536
|
};
|
|
225
|
-
`:`
|
|
2537
|
+
`:`import { PrototypeDesignerPanel } from "../prototype/designer-panel/PrototypeDesignerPanel";
|
|
2538
|
+
|
|
2539
|
+
const showPrototypeDesignerPanel = import.meta.env.DEV;
|
|
2540
|
+
|
|
2541
|
+
export const App = () => {
|
|
226
2542
|
return (
|
|
227
|
-
|
|
2543
|
+
<>
|
|
2544
|
+
${Ke}
|
|
2545
|
+
{showPrototypeDesignerPanel ? <PrototypeDesignerPanel /> : null}
|
|
2546
|
+
</>
|
|
228
2547
|
);
|
|
229
2548
|
};
|
|
230
|
-
`,
|
|
2549
|
+
`,no=`import { type ClassValue, clsx } from "clsx";
|
|
231
2550
|
import { twMerge } from "tailwind-merge";
|
|
232
2551
|
|
|
233
2552
|
export function cn(...inputs: ClassValue[]) {
|
|
234
2553
|
return twMerge(clsx(inputs));
|
|
235
2554
|
}
|
|
236
|
-
`,
|
|
2555
|
+
`,ro=`node_modules/
|
|
237
2556
|
dist/
|
|
238
2557
|
*.log
|
|
239
2558
|
.env
|
|
240
|
-
`,
|
|
2559
|
+
`,io=e=>`# ${e.projectName}
|
|
241
2560
|
|
|
242
2561
|
Proyecto Lexy (React + Vite + TS + theme Lexy). Los componentes viven en tu proyecto: m\xEDralos con \`view\`, inst\xE1lalos con \`add\`, ed\xEDtalos con libertad.
|
|
243
2562
|
|
|
244
2563
|
\`\`\`bash
|
|
245
2564
|
pnpm dev # desarrollo
|
|
2565
|
+
pnpm check:prototype # validar el contrato de datos del prototipo
|
|
246
2566
|
npx create-lexy view button # ver un componente antes de instalarlo
|
|
247
2567
|
npx create-lexy add button # instalarlo (local y editable)
|
|
248
2568
|
npx create-lexy doctor # divergencia con el registry
|
|
249
2569
|
\`\`\`
|
|
250
|
-
`,
|
|
251
|
-
`},
|
|
252
|
-
`),await n("components.json",
|
|
2570
|
+
`,so=e=>{let o=e.architecture==="feature"?"@/shared":"@";return JSON.stringify({$schema:"https://ui.shadcn.com/schema.json",style:"new-york",rsc:!1,tsx:!0,tailwind:{config:"",css:"src/index.css",baseColor:"neutral",cssVariables:!0},aliases:{components:`${o}/components`,ui:`${o}/components/base`,utils:`${o}/lib/utils/cn`,lib:`${o}/lib`,hooks:`${o}/hooks`},registries:{"@lexy":`${F}@latest/r/{name}.json`}},null,2)+`
|
|
2571
|
+
`},lo=e=>{let t=e.architecture==="feature";return{version:"2.0.0",generatedAt:new Date().toISOString(),architecture:e.architecture,world:e.world,registry:{package:"@lexydesign/registry",cdn:F},componentImportPattern:t?"@/shared/components/base/{Component}":"@/components/base/{Component}",paths:{components:t?"src/shared/components/base":"src/components/base",hooks:t?"src/shared/hooks":"src/hooks",services:t?"src/shared/services":"src/services",lib:t?"src/shared/lib":"src/lib",views:t?"src/features":"src/views"},prototype:{dataContractVersion:"1",dataContractPath:re,runtimeVersion:"1",mockStorePath:ce,fixturesPath:de,portsPath:pe,designerPanelVersion:"1",designerPanelPath:le,designerActivityStorePath:se},installed:{}}},Ue=async(e,t)=>{let o=["package.json","src","index.html",".lexy"],a=[];for(let s of o)try{await b.access(w.join(t,s)),a.push(s)}catch{}if(a.length>0)throw new Error(`El directorio actual ya contiene archivos de un proyecto (${a.join(", ")}). Ejecuta el comando dentro de una carpeta vac\xEDa.`);for(let s of Kt[e.architecture])await b.mkdir(w.join(t,s),{recursive:!0});let n=(s,u)=>b.writeFile(w.join(t,s),u);await n("package.json",Ht(e)),await n("vite.config.ts",Bt),await n("tsconfig.json",Yt),await n("tsconfig.app.json",Xt),await n("tsconfig.node.json",Zt),await n("eslint.config.js",Jt),await n(".prettierrc",Wt),await n(".prettierignore",Gt),await b.mkdir(w.join(t,".github/workflows"),{recursive:!0}),await n(".github/workflows/ci.yml",Qt),await n("index.html",eo(e)),await n(".gitignore",ro),await n("README.md",io(e)),await n("src/index.css",to),await n("src/main.tsx",oo(e)),await ze(t,e.projectName),await Ve(t);let r=lo(e);await $e(t,r);let i=e.architecture==="feature"?"src/app":"src/views";await n(`${i}/App.tsx`,ao(e)),await n(".lexy",JSON.stringify(r,null,2)+`
|
|
2572
|
+
`),await n("components.json",so(e)),await b.copyFile(Ce,w.join(t,"src/lexy-theme.css")),await b.mkdir(w.join(t,"public/fonts"),{recursive:!0}),await b.cp(Ie,w.join(t,"public/fonts"),{recursive:!0});let d=w.join(t,r.paths.lib,"utils");await b.mkdir(d,{recursive:!0}),await b.writeFile(w.join(d,"cn.ts"),no),await co(t)},co=async e=>{let t=[],o=async a=>{let n;try{n=await b.readdir(a,{withFileTypes:!0})}catch{return}for(let r of n){let i=w.join(a,r.name);r.isDirectory()?await o(i):/\.(ts|tsx)$/.test(r.name)&&t.push(i)}};await o(w.join(e,"src")),await o(w.join(e,"scripts"));for(let a of t){let n=await b.readFile(a,"utf-8"),r=await Vt(a)??{},i=await qt(n,{...r,filepath:a});i!==n&&await b.writeFile(a,i)}};var fo=["feature","layer"],go=["cliente","crm","mixto"],xo=e=>new Promise((t,o)=>{po("pnpm",["install"],{cwd:e,stdio:"ignore",shell:!0}).on("close",n=>n===0?t():o(new Error(`pnpm install fall\xF3 con c\xF3digo ${n}`)))}),He=async(e,t)=>{let o;!!(e&&t.type)?(fo.includes(t.type)||(console.error(j.red(`Error: la arquitectura "${t.type}" no es v\xE1lida. Usa "feature" o "layer".`)),process.exit(1)),t.world&&!go.includes(t.world)&&(console.error(j.red(`Error: el mundo "${t.world}" no es v\xE1lido. Usa "cliente", "crm" o "mixto".`)),process.exit(1)),t.world||console.warn(j.yellow('Aviso: sin --world; se asume "mixto". ')+j.dim("Pasa --world cliente|crm para fijar densidad y voz desde el inicio.")),o={projectName:e,architecture:t.type,world:t.world??"mixto"},console.log(j.blue("Modo automatizado detectado. Saltando interfaz interactiva..."))):o=await Ae(e),mo(`Nombre: ${o.projectName}
|
|
253
2573
|
Arquitectura: ${o.architecture}
|
|
254
|
-
Mundo: ${o.world}`,"Configuraci\xF3n de Proyecto");let n=
|
|
255
|
-
|
|
256
|
-
`)+
|
|
257
|
-
`)+
|
|
258
|
-
`)+
|
|
259
|
-
`),
|
|
260
|
-
`),n=o.length,
|
|
261
|
-
`)};var
|
|
262
|
-
${n.title??
|
|
263
|
-
`));let
|
|
2574
|
+
Mundo: ${o.world}`,"Configuraci\xF3n de Proyecto");let n=yo();n.start("Construyendo tu proyecto...");let r=process.cwd();try{await Ue(o,r)}catch(i){throw n.stop(j.red("No se pudo crear el proyecto.")),i}t.install!==!1&&(n.message("Instalando dependencias base..."),await xo(r)),n.stop("\xA1Proyecto construido con \xE9xito!"),uo(j.green(`Todo listo en el directorio actual
|
|
2575
|
+
|
|
2576
|
+
`)+j.dim(`Siguientes pasos:
|
|
2577
|
+
`)+j.cyan(` pnpm dev
|
|
2578
|
+
`)+j.cyan(" npx create-lexy add button"))};import vo from"fs/promises";import ho from"path";import S from"picocolors";import me from"picocolors";var V=(e,t)=>{let o=e.split(`
|
|
2579
|
+
`),a=t.split(`
|
|
2580
|
+
`),n=o.length,r=a.length,i=Array.from({length:n+1},()=>new Array(r+1).fill(0));for(let m=n-1;m>=0;m--)for(let y=r-1;y>=0;y--)i[m][y]=o[m]===a[y]?i[m+1][y+1]+1:Math.max(i[m+1][y],i[m][y+1]);let d=[],s=0,u=0;for(;s<n&&u<r;)o[s]===a[u]?(d.push({type:"same",text:o[s]}),s++,u++):i[s+1][u]>=i[s][u+1]?d.push({type:"del",text:o[s++]}):d.push({type:"add",text:a[u++]});for(;s<n;)d.push({type:"del",text:o[s++]});for(;u<r;)d.push({type:"add",text:a[u++]});return d},Y=e=>e.some(t=>t.type!=="same"),Be=(e,t=3)=>{if(!Y(e))return null;let o=new Array(e.length).fill(!1);e.forEach((r,i)=>{if(r.type!=="same")for(let d=Math.max(0,i-t);d<=Math.min(e.length-1,i+t);d++)o[d]=!0});let a=[],n=!1;for(let r=0;r<e.length;r++){if(!o[r]){n||(a.push(me.dim(" \u22EF")),n=!0);continue}n=!1;let i=e[r];i.type==="same"?a.push(` ${i.text}`):i.type==="del"?a.push(me.red(`- ${i.text}`)):a.push(me.green(`+ ${i.text}`))}return a.join(`
|
|
2581
|
+
`)};var Je=async e=>{let t=process.cwd(),o=await O(t),a=e.toLowerCase(),{item:n,version:r}=await k(a),i=o.installed[a];console.log(S.bold(`
|
|
2582
|
+
${n.title??a}`)+S.dim(` \xB7 instalado con registry@${i??"?"} \xB7 vigente registry@${r}
|
|
2583
|
+
`));let d=!1;for(let s of T(n,o)){let u=await vo.readFile(ho.join(t,s.relPath),s.encoding??"utf-8").catch(()=>null);if(u===null){console.log(S.red(`\u2717 ${s.relPath} no existe localmente`)+S.dim(` \u2014 cr\xE9alo con \`create-lexy add ${a}\``)),d=!0;continue}if(s.encoding==="base64"){u===s.content?console.log(S.green(`\u2713 ${s.relPath}`)+S.dim(" sin diferencias")):(d=!0,console.log(S.yellow(`\xB1 ${s.relPath}`)+S.dim(" (binario difiere del registry)")));continue}let m=Be(V(u,s.content));m===null?console.log(S.green(`\u2713 ${s.relPath}`)+S.dim(" sin diferencias")):(d=!0,console.log(S.yellow(`\xB1 ${s.relPath}`)+S.dim(" (\u2212 local \xB7 + registry)")),console.log(m))}d&&(process.exitCode=1)};import _ from"fs/promises";import v from"path";import C from"picocolors";var We=async e=>{let t=process.cwd(),o=0,a=c=>console.log(C.green(" \u2713 ")+c),n=c=>console.log(C.yellow(" \u2022 ")+c),r=(c,l)=>{o=1,console.log(C.red(" \u2717 ")+c),l&&console.log(C.dim(` \u2192 ${l}`))};console.log(C.bold(`
|
|
264
2584
|
Diagn\xF3stico del proyecto Lexy
|
|
265
|
-
`));let i;try{i=await
|
|
266
|
-
Sin componentes instalados a\xFAn (create-lexy add <nombre>).`));else{let
|
|
267
|
-
Componentes instalados (registry vigente: ${
|
|
268
|
-
`));for(let[
|
|
2585
|
+
`));let i;try{i=await O(t),a(`.lexy v\xE1lido (arquitectura ${i.architecture}${i.world?`, mundo ${i.world}`:""})`)}catch(c){return r(c.message),1}await _.access(v.join(t,"src/lexy-theme.css")).then(()=>!0,()=>!1)?a("theme presente (src/lexy-theme.css)"):r("falta src/lexy-theme.css","re-cr\xE9alo desde un proyecto nuevo o copia el theme del registry");let s=v.join(i.paths.lib,"utils/cn.ts");await _.access(v.join(t,s)).then(()=>!0,()=>!1)?a(`helper cn presente (${s})`):r(`falta el helper cn (${s})`);let m=await _.readFile(v.join(t,"package.json"),"utf-8").then(c=>JSON.parse(c)).catch(()=>null),y=async(c,l)=>{await _.access(v.join(t,c)).then(()=>!0,()=>!1)?a(`${l} presente (${c})`):r(`falta ${l} (${c})`)},z=(c,l)=>{if(!m){r("package.json no se pudo leer para validar scripts del prototipo");return}let g=m.scripts?.[c];if(!g){r(`falta el script ${c} en package.json`,l?`agrega \`${l}\``:void 0);return}if(l&&!g.includes(l)){r(`el script ${c} no ejecuta ${l}`);return}a(`script ${c} configurado`)};if(!i.prototype)n("prototipo funcional no habilitado \u2014 proyecto anterior al runtime de datos y ports");else{let c=i.prototype.dataContractPath;if(!c)n("contrato de datos no configurado \u2014 proyecto anterior a la Spec 1");else{await y(c,"contrato de datos");let l=v.join(v.dirname(c),"data-contract-schema.ts");await y(l,"schema del contrato"),await y("scripts/check-data-contract.ts","validador del contrato"),z("check:data-contract","tsx scripts/check-data-contract.ts")}i.prototype.runtimeVersion?(i.prototype.mockStorePath?(await y(i.prototype.mockStorePath,"mock-store del prototipo"),await y(v.join(v.dirname(i.prototype.mockStorePath),"fixtures.ts"),"fixtures del prototipo")):r("mockStorePath no est\xE1 configurado en .lexy"),i.prototype.portsPath?await y(i.prototype.portsPath,"ports del prototipo"):r("portsPath no est\xE1 configurado en .lexy")):n("runtime del prototipo no habilitado \u2014 proyecto anterior a la Spec 2"),i.prototype.designerPanelVersion?(i.prototype.designerPanelPath?await y(i.prototype.designerPanelPath,"panel de asistencia al Designer"):r("designerPanelPath no est\xE1 configurado en .lexy"),i.prototype.designerActivityStorePath?await y(i.prototype.designerActivityStorePath,"store de actividad del Designer"):r("designerActivityStorePath no est\xE1 configurado en .lexy")):n("panel de asistencia al Designer no habilitado \u2014 proyecto anterior a la Spec 4")}let M=v.join(t,"ai/lexy-ai-manifest.json"),p=await _.readFile(M,"utf-8").catch(()=>null);if(p===null)console.log(C.dim(" \xB7 capa de IA no instalada (npx @lexydesign/ai install) \u2014 opcional"));else try{let c=JSON.parse(p),l=c.files??[],g=[];for(let P of l)await _.access(v.join(t,P)).then(()=>!0,()=>!1)||g.push(P);g.length>0?r(`capa de IA ${c.aiVersion??"?"} incompleta \u2014 faltan ${g.length} archivo(s) (${g.slice(0,3).join(", ")}${g.length>3?", \u2026":""})`,"npx @lexydesign/ai install"):a(`capa de IA ${c.aiVersion??"?"} completa (${l.length} archivos)`)}catch{r("ai/lexy-ai-manifest.json no es JSON v\xE1lido","regenera con `npx @lexydesign/ai install`")}let f=Object.entries(i.installed);if(f.length===0)console.log(C.dim(`
|
|
2586
|
+
Sin componentes instalados a\xFAn (create-lexy add <nombre>).`));else{let c=await W();console.log(C.bold(`
|
|
2587
|
+
Componentes instalados (registry vigente: ${c})
|
|
2588
|
+
`));for(let[l,g]of f){let P;try{P=await k(l)}catch(L){r(`${l}: ${L.message}`);continue}let U=T(P.item,i),x=[],A=!1;for(let L of U){let H=await _.readFile(v.join(t,L.relPath),"utf-8").catch(()=>null);H===null?x.push(L.relPath):Y(V(H,L.content))&&(A=!0)}if(x.length>0){r(`${l}: archivos faltantes (${x.join(", ")})`,`create-lexy add ${l}`);continue}if(!A){a(`${l} al d\xEDa (${P.version})`);continue}let ee=!0;if(g&&g!==P.version)try{let L=await k(l,g),H=T(L.item,i);ee=!1;for(let he of H){let be=await _.readFile(v.join(t,he.relPath),"utf-8").catch(()=>null);(be===null||Y(V(be,he.content)))&&(ee=!0)}}catch{}ee?e.strict?r(`${l}: editado localmente (difiere del registry vigente)`,`create-lexy diff ${l}`):n(`${l}: editado localmente \u2014 divergencia deliberada (m\xEDrala con \`create-lexy diff ${l}\`)`):r(`${l}: desactualizado (instalado con ${g}, vigente ${P.version})`,`create-lexy diff ${l} \xB7 create-lexy add ${l} --overwrite`)}}return console.log(o===0?C.green(`
|
|
269
2589
|
Todo en orden.
|
|
270
|
-
`):
|
|
2590
|
+
`):C.red(`
|
|
271
2591
|
Hay problemas que atender (exit 1).
|
|
272
|
-
`)),o};import
|
|
2592
|
+
`)),o};import ue from"fs/promises";import Ge from"path";import{note as bo,outro as wo}from"@clack/prompts";import ye from"picocolors";var X="LEXY-KICKOFF.md",So=`# Kickoff de proyecto Lexy \u2014 protocolo de entrevista
|
|
273
2593
|
|
|
274
2594
|
> **Para el agente (Claude Code):** este archivo es tu guion. La persona que
|
|
275
2595
|
> tienes enfrente es **dise\xF1adora, no programadora**: no le preguntes por
|
|
@@ -291,8 +2611,20 @@ hazlas de a poco, repregunta y resume lo que entendiste. Cubre:
|
|
|
291
2611
|
dashboard, listados, vistas de detalle)?
|
|
292
2612
|
- **Pantallas y flujos clave** \u2014 \xBFqu\xE9 vistas importan y qu\xE9 hace la persona en
|
|
293
2613
|
cada una? \xBFC\xF3mo se mueve entre ellas?
|
|
294
|
-
- **Datos
|
|
295
|
-
|
|
2614
|
+
- **Datos de la experiencia** \u2014 \xBFqu\xE9 objetos principales existen? \xBFQu\xE9 necesita
|
|
2615
|
+
ver o modificar la persona? \xBFQu\xE9 relaciones necesita entender? \xBFQu\xE9 estados
|
|
2616
|
+
cambian lo que aparece en pantalla? \xBFQu\xE9 supuestos todav\xEDa debe validar TI?
|
|
2617
|
+
- **Cargas de datos** \u2014 \xBFqu\xE9 informaci\xF3n debe llegar desde backend para cada
|
|
2618
|
+
pantalla? \xBFQu\xE9 significa que una carga venga vac\xEDa? \xBFQu\xE9 errores o latencias
|
|
2619
|
+
vale la pena poder probar?
|
|
2620
|
+
- **Eventos publicados** \u2014 \xBFqu\xE9 escrituras hace la interfaz? \xBFQu\xE9 hecho ocurri\xF3
|
|
2621
|
+
cuando se publican? \xBFQu\xE9 entidad cambia visualmente despu\xE9s de ver "Evento
|
|
2622
|
+
publicado"? \xBFQu\xE9 debe mantenerse intacto si falla?
|
|
2623
|
+
- **Datos mock y escenarios** \u2014 \xBFqu\xE9 situaciones reales debe representar el
|
|
2624
|
+
prototipo? \xBFQu\xE9 casos l\xEDmite importan? \xBFQu\xE9 datos deben verse chilenos? \xBFQu\xE9
|
|
2625
|
+
campos son sensibles aunque sean sint\xE9ticos?
|
|
2626
|
+
- **Referencias** \u2014 \xBFhay un Figma, capturas o un producto de referencia que
|
|
2627
|
+
mande sobre composiciones gen\xE9ricas?
|
|
296
2628
|
|
|
297
2629
|
## 2. Traducir las respuestas a flags (no se lo muestres a la persona)
|
|
298
2630
|
|
|
@@ -318,26 +2650,45 @@ npx @lexydesign/ai install
|
|
|
318
2650
|
|
|
319
2651
|
## 4. Volcar la entrevista en el brief vivo
|
|
320
2652
|
|
|
321
|
-
Abre \`ai/PROJECT-CONTEXT.md\` (lo cre\xF3 \`@lexydesign/ai install\`) y
|
|
322
|
-
con lo que recopilaste: **qu\xE9 se construye**, **para qui\xE9n**,
|
|
323
|
-
flujos clave**,
|
|
324
|
-
|
|
2653
|
+
1. Abre \`ai/PROJECT-CONTEXT.md\` (lo cre\xF3 \`@lexydesign/ai install\`) y
|
|
2654
|
+
compl\xE9talo con lo que recopilaste: **qu\xE9 se construye**, **para qui\xE9n**,
|
|
2655
|
+
**pantallas y flujos clave**, un resumen de **datos principales**,
|
|
2656
|
+
**referencias** y **decisiones/restricciones** ya tomadas.
|
|
2657
|
+
2. Traduce los objetos, datos, relaciones, estados y supuestos conversados a
|
|
2658
|
+
\`src/prototype/data-contract/prototype-data-contract.ts\`. No inventes
|
|
2659
|
+
backend: lo que nazca desde usabilidad queda como
|
|
2660
|
+
\`origin: "generatedByUsability"\` y
|
|
2661
|
+
\`technicalValidation.status: "pendingTi"\`.
|
|
2662
|
+
3. Mant\xE9n los datos mock en
|
|
2663
|
+
\`src/prototype/mock-store/fixtures.ts\`, nunca dentro de componentes. Las
|
|
2664
|
+
pantallas leen con \`read.load(loadId, params, meta)\` y publican hechos con
|
|
2665
|
+
\`write.publish(eventId, payload, meta)\`, importados desde
|
|
2666
|
+
\`src/prototype/ports\`. La metadata inline (\`reads\`/\`writes\`,
|
|
2667
|
+
\`description\`, \`trigger\`) alimenta el panel del Designer. No registres
|
|
2668
|
+
interacciones puramente locales.
|
|
2669
|
+
4. Ejecuta \`pnpm check:prototype\` y corrige cualquier inconsistencia antes de
|
|
2670
|
+
empezar a implementar pantallas.
|
|
2671
|
+
|
|
2672
|
+
\`PROJECT-CONTEXT.md\` es el resumen humano. El contrato de datos TypeScript es la
|
|
2673
|
+
fuente estructurada para las pr\xF3ximas sesiones. El mock-store mantiene datos
|
|
2674
|
+
coherentes y persistentes; los ports separan el adapter mock del adapter de
|
|
2675
|
+
producci\xF3n; el panel registra cargas y publicaciones para inspecci\xF3n.
|
|
325
2676
|
|
|
326
2677
|
## 5. Cerrar
|
|
327
2678
|
|
|
328
|
-
Borra este archivo (\`${
|
|
2679
|
+
Borra este archivo (\`${X}\`): su trabajo termin\xF3. Conf\xEDrmale a la
|
|
329
2680
|
persona que su proyecto est\xE1 listo y prop\xF3n el primer paso concreto
|
|
330
2681
|
(p. ej. "\xBFDise\xF1amos la pantalla de inicio?").
|
|
331
|
-
`,
|
|
332
|
-
|
|
333
|
-
Abre la carpeta con Claude Code y di: `+
|
|
334
|
-
El agente te entrevistar\xE1 y montar\xE1 el proyecto por ti.`,"Kickoff listo"),
|
|
335
|
-
Trayendo ${
|
|
336
|
-
`));let i=`${
|
|
337
|
-
`)),console.log(` Abre ${
|
|
338
|
-
`))};import
|
|
2682
|
+
`,Qe=async e=>{let t=process.cwd();for(let n of["package.json",".lexy"])if(await ue.access(Ge.join(t,n)).then(()=>!0,()=>!1))throw new Error(`El directorio actual ya contiene un proyecto (${n}). \`kickoff\` es para empezar de cero en una carpeta vac\xEDa.`);let o=Ge.join(t,X);if(await ue.access(o).then(()=>!0,()=>!1)&&!e.force)throw new Error(`${X} ya existe. Usa --force para regenerarlo.`);await ue.writeFile(o,So),bo(`Se cre\xF3 ${ye.cyan(X)} en esta carpeta.
|
|
2683
|
+
|
|
2684
|
+
Abre la carpeta con Claude Code y di: `+ye.bold("\xABay\xFAdame a arrancar mi proyecto Lexy\xBB")+`.
|
|
2685
|
+
El agente te entrevistar\xE1 y montar\xE1 el proyecto por ti.`,"Kickoff listo"),wo(ye.green("Sin flags, sin prompts t\xE9cnicos \u2014 solo conversa con el agente."))};import fe from"fs/promises";import Z from"path";import I from"picocolors";var xe="@lexydesign/kit",Ye=`https://cdn.jsdelivr.net/npm/${xe}`,Xe=8e3,ge=async e=>{let t=await fetch(e,{signal:AbortSignal.timeout(Xe)});if(!t.ok){let o=new Error(`HTTP ${t.status} al pedir ${e}`);throw o.status=t.status,o}return Buffer.from(await t.arrayBuffer())},ko=async()=>{try{let t=await fetch(`https://registry.npmjs.org/${xe}/latest`,{signal:AbortSignal.timeout(Xe)});if(t.ok)return(await t.json()).version}catch{}return JSON.parse((await ge(`${Ye}@latest/package.json`)).toString("utf-8")).version},Ze=async e=>{let t=process.cwd(),o=Z.resolve(t,e.dir??"lexy-kit"),a=Z.relative(t,o)||".";if(await fe.access(o).then(()=>!0).catch(()=>!1)&&!e.force)throw new Error(`Ya existe ${I.bold(a)}. Usa ${I.cyan("--force")} para sobrescribir o ${I.cyan("--dir <ruta>")} para otra carpeta.`);let r=await ko();console.log(I.dim(`
|
|
2686
|
+
Trayendo ${xe}@${r} \u2192 ${a}/
|
|
2687
|
+
`));let i=`${Ye}@${r}/kit`,d=JSON.parse((await ge(`${i}/manifest.json`)).toString("utf-8")),s=["index.html","kit.css","kit.js","manifest.json","README.md","AGENTS.md","llms.txt","llms-full.txt","fonts/noto-sans-latin.woff2","fonts/OFL-NotoSans.txt",...d.guides?.pautas??[],...d.components.map(m=>m.file),...d.blocks.map(m=>m.file)],u=0;for(let m of s)try{let y=await ge(`${i}/${m}`),z=Z.join(o,m);await fe.mkdir(Z.dirname(z),{recursive:!0}),await fe.writeFile(z,y),u+=1}catch(y){console.warn(I.yellow(`\u26A0 no se pudo traer ${m}`)+I.dim(` (${y.message})`))}console.log(I.green("\u2713 Kit listo")+I.dim(` \u2014 ${u} archivo(s), ${d.components.length} componente(s) + ${d.blocks.length} block(s)
|
|
2688
|
+
`)),console.log(` Abre ${I.bold(`${a}/index.html`)} en el navegador.`),console.log(I.dim(` El cat\xE1logo m\xE1quina-legible est\xE1 en ${a}/manifest.json.
|
|
2689
|
+
`))};import Eo from"fs/promises";import ve from"path";import E from"picocolors";var K=e=>console.log(E.bold(E.cyan(`
|
|
339
2690
|
\u2500\u2500 ${e} \u2500\u2500
|
|
340
|
-
`))),
|
|
341
|
-
${i.items.length} componente(s). Instala con: create-lexy add <nombre>`));return}let o=e.toLowerCase();if(t.installed){let i=process.cwd(),
|
|
342
|
-
`),console.log(
|
|
343
|
-
Error: `)+t.message),process.exit(1)}};
|
|
2691
|
+
`))),et=async(e,t)=>{if(t.list||!e){let{index:i,version:d,source:s}=await je();K(`Cat\xE1logo Lexy \xB7 @lexydesign/registry@${d}${s==="bundle"?" (bundle local)":""}`);for(let u of i.items)console.log(` ${E.bold(u.name.padEnd(20))} ${E.dim(u.description??"")}`);console.log(E.dim(`
|
|
2692
|
+
${i.items.length} componente(s). Instala con: create-lexy add <nombre>`));return}let o=e.toLowerCase();if(t.installed){let i=process.cwd(),d=await O(i),s=d.installed[o];if(!s){console.error(E.red(`"${o}" no figura como instalado en .lexy. Inst\xE1lalo con \`create-lexy add ${o}\`.`)),process.exitCode=1;return}let{item:u}=await k(o,s).catch(()=>k(o));K(`${u.title??o} \xB7 copia local (instalado con registry@${s})`);for(let m of T(u,d)){let y=await Eo.readFile(ve.join(i,m.relPath),"utf-8").catch(()=>null);K(y===null?`${m.relPath} (FALTANTE)`:m.relPath),y!==null&&console.log(y)}return}let{item:a,version:n,source:r}=await k(o);K(`${a.title??a.name} \xB7 @lexydesign/registry@${n}${r==="bundle"?" (bundle local)":""}`),a.description&&console.log(a.description+`
|
|
2693
|
+
`),console.log(E.dim("tipo: ")+a.type),console.log(E.dim("deps npm: ")+((a.dependencies??[]).join(", ")||"\u2014")),console.log(E.dim("deps registry: ")+((a.registryDependencies??[]).join(", ")||"\u2014")),console.log(E.dim("archivos: ")+a.files.map(i=>ve.posix.basename(i.path)).join(", ")),console.log(E.dim("instalar: ")+`create-lexy add ${a.name}`);for(let i of a.files)typeof i.content=="string"&&(K(ve.posix.basename(i.path)),console.log(i.content))};var To=tt.dirname(jo(import.meta.url)),Ro=JSON.parse(Po(tt.join(To,"../package.json"),"utf-8")),N=new Co,D=e=>async()=>{try{await e()}catch(t){console.error(Io.red(`
|
|
2694
|
+
Error: `)+t.message),process.exit(1)}};N.name("create-lexy").description("CLI del Lexy Design System: crea proyectos y trae componentes a demanda.\nLos componentes viven en tu proyecto \u2014 m\xEDralos con `view`, inst\xE1lalos con `add`, ed\xEDtalos con libertad.").version(Ro.version);N.command("create",{isDefault:!0}).alias("init").description("Crea un proyecto Lexy vac\xEDo (React + Vite + TS + theme); los componentes llegan con `add`").argument("[project-name]","Nombre del proyecto").option("-t, --type <architecture>","Arquitectura (feature o layer) \u2014 activa el modo no interactivo").option("-w, --world <world>","Mundo de dise\xF1o (cliente, crm o mixto)").option("--no-install","No correr pnpm install al final").action((e,t)=>D(()=>He(e,t))());N.command("kickoff").description("Deja un .md de entrevista para que el agente conduzca el arranque de un dise\xF1ador (carpeta vac\xEDa)").option("--force","Regenerar el .md de kickoff si ya existe").action(e=>D(()=>Qe(e))());N.command("add").description("Instala componentes del registry en tu proyecto (local y editable), con sus dependencias").argument("<components...>","Nombres en el registry (ej: button card)").option("--overwrite","Sobrescribir copias locales con cambios sin preguntar").option("--no-install","No instalar dependencias npm").action((e,t)=>D(()=>Re(e,t))());N.command("view").description("Muestra c\xF3digo + doc + metadata de un componente antes de instalarlo").argument("[component]","Nombre en el registry (sin nombre: lista el cat\xE1logo)").option("--installed","Mostrar la copia local instalada en vez de la del registry").option("--list","Listar el cat\xE1logo completo").action((e,t)=>D(()=>et(e,t))());N.command("kit").description("Trae el kit HTML/low-fi (mismos componentes y blocks en HTML plano) a una carpeta").option("--dir <path>","Carpeta destino (por defecto: lexy-kit)").option("--force","Sobrescribir la carpeta si ya existe").action(e=>D(()=>Ze(e))());N.command("diff").description("Copia local vs versi\xF3n vigente del registry (exit 1 si hay diferencias)").argument("<component>","Nombre en el registry").action(e=>D(()=>Je(e))());N.command("doctor").description("Drift de todo lo instalado contra el registry (solo lectura; exit 1 con problemas)").option("--strict","Las ediciones locales tambi\xE9n hacen fallar").action(e=>D(async()=>{process.exitCode=await We(e)})());N.parse(process.argv);
|