@tinya-code/gas-admin-ui 0.1.0-alpha.5 → 0.2.0-alpha.0

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.
Files changed (40) hide show
  1. package/README.md +538 -117
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs → tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs} +4 -4
  3. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map → tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs.map} +1 -1
  4. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs → tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs} +2 -2
  5. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs.map} +1 -1
  6. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs → tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs} +2 -3
  7. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map → tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs → tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs.map} +1 -1
  10. package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs → tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs} +3 -3
  11. package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map → tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs.map} +1 -1
  12. package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs → tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs} +9 -10
  13. package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map → tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs → tinya-code-gas-admin-ui-image-BlDRZe-F.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map → tinya-code-gas-admin-ui-image-BlDRZe-F.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs → tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs} +4 -4
  17. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map → tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs.map} +1 -1
  18. package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs → tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs} +2 -2
  19. package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map → tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs → tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs} +3 -3
  21. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map → tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs → tinya-code-gas-admin-ui-relation-BSqbQB28.mjs} +3 -3
  23. package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map → tinya-code-gas-admin-ui-relation-BSqbQB28.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs → tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs} +10 -10
  25. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map → tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs.map} +1 -1
  26. package/fesm2022/{tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs → tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs} +161 -7
  27. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs.map +1 -0
  28. package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs → tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs} +2 -2
  29. package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map → tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs.map} +1 -1
  30. package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs → tinya-code-gas-admin-ui-select-BUbb36-T.mjs} +3 -3
  31. package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map → tinya-code-gas-admin-ui-select-BUbb36-T.mjs.map} +1 -1
  32. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs → tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs} +2 -2
  33. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map → tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs.map} +1 -1
  34. package/fesm2022/tinya-code-gas-admin-ui.mjs +65 -11
  35. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
  36. package/package.json +8 -2
  37. package/types/tinya-code-gas-admin-ui.d.ts +69 -3
  38. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +0 -1
  39. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +0 -131
  40. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { n as get } from "./tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs";
1
+ import { n as get } from "./tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs";
2
2
  const COL_SPANS = {
3
3
  1: "sm:col-span-1",
4
4
  2: "sm:col-span-2",
@@ -77,4 +77,4 @@ function isRecord(value) {
77
77
  }
78
78
  export { colSpanClass as a, seedFields as i, mergeRecord as n, seedFieldValue as r, isRecord as t };
79
79
 
80
- //# sourceMappingURL=tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map
80
+ //# sourceMappingURL=tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-seed-CNbQtREA.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/shared/utils/col-span.ts","../../../projects/gas-admin-ui/src/lib/shared/utils/seed.ts"],"sourcesContent":["/**\n * Anchos de columna del layout (base.md §4: `width` 1–12).\n *\n * `sm:col-span-*` como literales: Tailwind v4 solo genera las clases que ve\n * en el código, así que no pueden construirse por concatenación.\n */\nconst COL_SPANS: Record<number, string> = {\n 1: 'sm:col-span-1',\n 2: 'sm:col-span-2',\n 3: 'sm:col-span-3',\n 4: 'sm:col-span-4',\n 5: 'sm:col-span-5',\n 6: 'sm:col-span-6',\n 7: 'sm:col-span-7',\n 8: 'sm:col-span-8',\n 9: 'sm:col-span-9',\n 10: 'sm:col-span-10',\n 11: 'sm:col-span-11',\n 12: 'sm:col-span-12',\n};\n\n/** Clase `sm:col-span-*` para el ancho del campo (por defecto: 12). */\nexport function colSpanClass(width: number | undefined): string {\n return COL_SPANS[width ?? 12];\n}\n","import type { FieldSchema, GroupField } from '../../core/models/schema.model';\nimport { get } from '../../core/registry/render-registry';\n\n/**\n * Forma del valor inicial que siembra un campo (D32 · R3.8).\n *\n * Son **todos** los valores posibles del dominio: un campo de formulario\n * nuevo nace como cadena, número, booleano, lista o como contenedor que\n * siembra sus hijos. Si un tipo llegara a necesitar otra forma, ampliar este\n * `type` obliga a compilar el `switch` de `seedValue` — que es exactamente\n * el fail-loud que la regla 4 pide.\n */\nexport type SeedKind = 'string' | 'number' | 'boolean' | 'array' | 'recursive';\n\n/**\n * Siembra del modelo del formulario: un valor por defecto para CADA campo\n * declarado en el schema (base.md §10).\n *\n * Compartido entre `form-model` (crear/recargar) y los componentes de lista\n * (crear ítems nuevos con la forma de sus `itemFields`).\n */\n\n/**\n * Valor inicial cuando el schema no declara `default`.\n *\n * **D32 · ya no despacha por tipo.** El `switch` de 20 ramas que obligaba a\n * editar este fichero al añadir un campo — y que §0 contaba como tocar el\n * motor — pasa a resolver el nombre que declara el propio descriptor\n * (`RenderDescriptor.seed`). «Añadir un tipo» queda en carpeta + manifiesto\n * + unión: este archivo no se entera.\n *\n * Dos salidas distintas, ninguna silenciosa:\n *\n * · **tipo fuera del catálogo** ⇒ `undefined`, la misma degradación\n * controlada que el render (R1.10): el campo no se pinta, así que su\n * valor no existe;\n * · **descriptor sin `seed`** ⇒ lanza. Es un error de programación que\n * `core/registry/contract.spec.ts` detecta en CI antes de llegar acá.\n */\nexport function seedValue(field: FieldSchema): unknown {\n const descriptor = get('field', field.type);\n\n if (!descriptor) {\n return undefined;\n }\n if (!descriptor.seed) {\n throw new Error(`[seed] el descriptor de \"${field.type}\" no declara \\`seed\\`.`);\n }\n\n switch (descriptor.seed) {\n case 'string':\n return '';\n case 'number':\n return null;\n case 'boolean':\n return false;\n case 'array':\n return [];\n case 'recursive':\n return seedHijos(field, descriptor.seed);\n }\n}\n\n/**\n * Siembra recursiva de un contenedor.\n *\n * El único `seed` recursivo del catálogo es `group`, pero el registro no lo\n * sabe — sólo lleva un nombre. El estrechado se comprueba aquí y **falla\n * ruidoso** si algún día un tipo declara `'recursive'` sin traer subcampos.\n */\nfunction seedHijos(field: FieldSchema, seed: 'recursive'): unknown {\n if (seed !== 'recursive' || !esGrupo(field)) {\n throw new Error(`[seed] \"${field.type}\" declara \\`seed: 'recursive'\\` pero no trae subcampos.`);\n }\n return seedFields(field.fields);\n}\n\n/** Estrechado de forma, no de despacho: no decide qué hacer, sólo accede. */\nfunction esGrupo(field: FieldSchema): field is GroupField {\n return field.type === 'group';\n}\n\n/** Valor sembrado de un campo, fusionando `default` con la forma del schema. */\nexport function seedFieldValue(field: FieldSchema): unknown {\n if (field.default === undefined) {\n return seedValue(field);\n }\n if (field.type === 'group' && isRecord(field.default)) {\n // Un `default` de grupo podría traer claves incompletas: los hijos se\n // acceden por árbol, así que la forma completa es obligatoria.\n return { ...seedFields(field.fields), ...field.default };\n }\n if (field.type === 'list' && Array.isArray(field.default)) {\n return field.default.map((item) =>\n isRecord(item) ? { ...seedFields(field.itemFields), ...item } : item,\n );\n }\n return field.default;\n}\n\n/**\n * Siembra recursiva: los `group` anidan sus propios campos. Exportada porque\n * la lista crea sus ítems nuevos con la misma forma (`base.md §6`).\n */\nexport function seedFields(fields: readonly FieldSchema[]): Record<string, unknown> {\n const model: Record<string, unknown> = {};\n for (const field of fields) {\n model[field.key] = seedFieldValue(field);\n }\n return model;\n}\n\n/**\n * Fusiona el registro leído con la forma sembrada del schema.\n *\n * El backend puede omitir claves (p. ej. un ítem guardado antes de que el\n * schema agregara un campo): sin esta fusión el árbol no tendría nodo para\n * ese campo y `childTree` lanzaría al renderizar. Lo ausente toma su\n * `default` y los extras del registro (p. ej. `position` de los ítems) se\n * conservan.\n */\nexport function mergeRecord(\n fields: readonly FieldSchema[],\n record: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {};\n for (const field of fields) {\n const incoming = record[field.key];\n if (field.type === 'group') {\n merged[field.key] = isRecord(incoming)\n ? mergeRecord(field.fields, incoming)\n : seedFieldValue(field);\n continue;\n }\n if (field.type === 'list') {\n const itemSeed = seedFields(field.itemFields);\n merged[field.key] = Array.isArray(incoming)\n ? incoming.map((item) => (isRecord(item) ? { ...itemSeed, ...item } : item))\n : seedFieldValue(field);\n continue;\n }\n merged[field.key] = incoming !== undefined ? incoming : seedFieldValue(field);\n }\n return merged;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"],"mappings":";AAMA,MAAM,YAAoC;CACxC,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,IAAI;CACJ,IAAI;CACJ,IAAI;AACL;AAGD,SAAgB,aAAa,OAAyB;CACpD,OAAO,UAAU,SAAS;AAC5B;ACeA,SAAgB,UAAU,OAAkB;CAC1C,MAAM,aAAa,IAAI,SAAS,MAAM,IAAI;CAE1C,IAAI,CAAC,YACH;CAEF,IAAI,CAAC,WAAW,MACd,MAAM,IAAI,MAAM,4BAA4B,MAAM,KAAI,uBAAwB;CAGhF,QAAQ,WAAW,MAAnB;EACE,KAAK,UACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,SACH,OAAO,CAAA;EACT,KAAK,aACH,OAAO,UAAU,OAAO,WAAW,IAAI;CAC3C;AACF;AASA,SAAS,UAAU,OAAoB,MAAiB;CACtD,IAAI,SAAS,eAAe,CAAC,QAAQ,KAAK,GACxC,MAAM,IAAI,MAAM,WAAW,MAAM,KAAI,wDAAyD;CAEhG,OAAO,WAAW,MAAM,MAAM;AAChC;AAGA,SAAS,QAAQ,OAAkB;CACjC,OAAO,MAAM,SAAS;AACxB;AAGA,SAAgB,eAAe,OAAkB;CAC/C,IAAI,MAAM,YAAY,QACpB,OAAO,UAAU,KAAK;CAExB,IAAI,MAAM,SAAS,WAAW,SAAS,MAAM,OAAO,GAGlD,OAAO;EAAE,GAAG,WAAW,MAAM,MAAM;EAAG,GAAG,MAAM;CAAO;CAExD,IAAI,MAAM,SAAS,UAAU,MAAM,QAAQ,MAAM,OAAO,GACtD,OAAO,MAAM,QAAQ,KAAK,SACxB,SAAS,IAAI,IAAI;EAAE,GAAG,WAAW,MAAM,UAAU;EAAG,GAAG;CAAI,IAAK,IAAI;CAGxE,OAAO,MAAM;AACf;AAMA,SAAgB,WAAW,QAA8B;CACvD,MAAM,QAAiC,CAAA;CACvC,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,OAAO,eAAe,KAAK;CAEzC,OAAO;AACT;AAWA,SAAgB,YACd,QACA,QAA+B;CAE/B,MAAM,SAAkC,CAAA;CACxC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,OAAO,MAAM;EAC9B,IAAI,MAAM,SAAS,SAAS;GAC1B,OAAO,MAAM,OAAO,SAAS,QAAQ,IACjC,YAAY,MAAM,QAAQ,QAAQ,IAClC,eAAe,KAAK;GACxB;EACF;EACA,IAAI,MAAM,SAAS,QAAQ;GACzB,MAAM,WAAW,WAAW,MAAM,UAAU;GAC5C,OAAO,MAAM,OAAO,MAAM,QAAQ,QAAQ,IACtC,SAAS,KAAK,SAAU,SAAS,IAAI,IAAI;IAAE,GAAG;IAAU,GAAG;GAAI,IAAK,IAAK,IACzE,eAAe,KAAK;GACxB;EACF;EACA,OAAO,MAAM,OAAO,aAAa,SAAY,WAAW,eAAe,KAAK;CAC9E;CACA,OAAO;AACT;AAEA,SAAgB,SAAS,OAAc;CACrC,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/shared/utils/col-span.ts","../../../projects/gas-admin-ui/src/lib/shared/utils/seed.ts"],"sourcesContent":["/**\n * Anchos de columna del layout (base.md §4: `width` 1–12).\n *\n * `sm:col-span-*` como literales: Tailwind v4 solo genera las clases que ve\n * en el código, así que no pueden construirse por concatenación.\n */\nconst COL_SPANS: Record<number, string> = {\n 1: 'sm:col-span-1',\n 2: 'sm:col-span-2',\n 3: 'sm:col-span-3',\n 4: 'sm:col-span-4',\n 5: 'sm:col-span-5',\n 6: 'sm:col-span-6',\n 7: 'sm:col-span-7',\n 8: 'sm:col-span-8',\n 9: 'sm:col-span-9',\n 10: 'sm:col-span-10',\n 11: 'sm:col-span-11',\n 12: 'sm:col-span-12',\n};\n\n/** Clase `sm:col-span-*` para el ancho del campo (por defecto: 12). */\nexport function colSpanClass(width: number | undefined): string {\n return COL_SPANS[width ?? 12];\n}\n","import type { FieldSchema, GroupField } from '../../core/models/schema.model';\nimport { get } from '../../core/registry/render-registry';\n\n/**\n * Forma del valor inicial que siembra un campo (D32 · R3.8).\n *\n * Son **todos** los valores posibles del dominio: un campo de formulario\n * nuevo nace como cadena, número, booleano, lista o como contenedor que\n * siembra sus hijos. Si un tipo llegara a necesitar otra forma, ampliar este\n * `type` obliga a compilar el `switch` de `seedValue` — que es exactamente\n * el fail-loud que la regla 4 pide.\n */\nexport type SeedKind = 'string' | 'number' | 'boolean' | 'array' | 'recursive';\n\n/**\n * Siembra del modelo del formulario: un valor por defecto para CADA campo\n * declarado en el schema (base.md §10).\n *\n * Compartido entre `form-model` (crear/recargar) y los componentes de lista\n * (crear ítems nuevos con la forma de sus `itemFields`).\n */\n\n/**\n * Valor inicial cuando el schema no declara `default`.\n *\n * **D32 · ya no despacha por tipo.** El `switch` de 20 ramas que obligaba a\n * editar este fichero al añadir un campo — y que §0 contaba como tocar el\n * motor — pasa a resolver el nombre que declara el propio descriptor\n * (`RenderDescriptor.seed`). «Añadir un tipo» queda en carpeta + manifiesto\n * + unión: este archivo no se entera.\n *\n * Dos salidas distintas, ninguna silenciosa:\n *\n * · **tipo fuera del catálogo** ⇒ `undefined`, la misma degradación\n * controlada que el render (R1.10): el campo no se pinta, así que su\n * valor no existe;\n * · **descriptor sin `seed`** ⇒ lanza. Es un error de programación que\n * `core/registry/contract.spec.ts` detecta en CI antes de llegar acá.\n */\nexport function seedValue(field: FieldSchema): unknown {\n const descriptor = get('field', field.type);\n\n if (!descriptor) {\n return undefined;\n }\n if (!descriptor.seed) {\n throw new Error(`[seed] el descriptor de \"${field.type}\" no declara \\`seed\\`.`);\n }\n\n switch (descriptor.seed) {\n case 'string':\n return '';\n case 'number':\n return null;\n case 'boolean':\n return false;\n case 'array':\n return [];\n case 'recursive':\n return seedHijos(field, descriptor.seed);\n }\n}\n\n/**\n * Siembra recursiva de un contenedor.\n *\n * El único `seed` recursivo del catálogo es `group`, pero el registro no lo\n * sabe — sólo lleva un nombre. El estrechado se comprueba aquí y **falla\n * ruidoso** si algún día un tipo declara `'recursive'` sin traer subcampos.\n */\nfunction seedHijos(field: FieldSchema, seed: 'recursive'): unknown {\n if (seed !== 'recursive' || !esGrupo(field)) {\n throw new Error(`[seed] \"${field.type}\" declara \\`seed: 'recursive'\\` pero no trae subcampos.`);\n }\n return seedFields(field.fields);\n}\n\n/** Estrechado de forma, no de despacho: no decide qué hacer, sólo accede. */\nfunction esGrupo(field: FieldSchema): field is GroupField {\n return field.type === 'group';\n}\n\n/** Valor sembrado de un campo, fusionando `default` con la forma del schema. */\nexport function seedFieldValue(field: FieldSchema): unknown {\n if (field.default === undefined) {\n return seedValue(field);\n }\n if (field.type === 'group' && isRecord(field.default)) {\n // Un `default` de grupo podría traer claves incompletas: los hijos se\n // acceden por árbol, así que la forma completa es obligatoria.\n return { ...seedFields(field.fields), ...field.default };\n }\n if (field.type === 'list' && Array.isArray(field.default)) {\n return field.default.map((item) =>\n isRecord(item) ? { ...seedFields(field.itemFields), ...item } : item,\n );\n }\n return field.default;\n}\n\n/**\n * Siembra recursiva: los `group` anidan sus propios campos. Exportada porque\n * la lista crea sus ítems nuevos con la misma forma (`base.md §6`).\n */\nexport function seedFields(fields: readonly FieldSchema[]): Record<string, unknown> {\n const model: Record<string, unknown> = {};\n for (const field of fields) {\n model[field.key] = seedFieldValue(field);\n }\n return model;\n}\n\n/**\n * Fusiona el registro leído con la forma sembrada del schema.\n *\n * El backend puede omitir claves (p. ej. un ítem guardado antes de que el\n * schema agregara un campo): sin esta fusión el árbol no tendría nodo para\n * ese campo y `childTree` lanzaría al renderizar. Lo ausente toma su\n * `default` y los extras del registro (p. ej. `position` de los ítems) se\n * conservan.\n */\nexport function mergeRecord(\n fields: readonly FieldSchema[],\n record: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {};\n for (const field of fields) {\n const incoming = record[field.key];\n if (field.type === 'group') {\n merged[field.key] = isRecord(incoming)\n ? mergeRecord(field.fields, incoming)\n : seedFieldValue(field);\n continue;\n }\n if (field.type === 'list') {\n const itemSeed = seedFields(field.itemFields);\n merged[field.key] = Array.isArray(incoming)\n ? incoming.map((item) => (isRecord(item) ? { ...itemSeed, ...item } : item))\n : seedFieldValue(field);\n continue;\n }\n merged[field.key] = incoming !== undefined ? incoming : seedFieldValue(field);\n }\n return merged;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"],"mappings":";AAMA,MAAM,YAAoC;CACxC,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,IAAI;CACJ,IAAI;CACJ,IAAI;AACL;AAGD,SAAgB,aAAa,OAAyB;CACpD,OAAO,UAAU,SAAS;AAC5B;ACeA,SAAgB,UAAU,OAAkB;CAC1C,MAAM,aAAa,IAAI,SAAS,MAAM,IAAI;CAE1C,IAAI,CAAC,YACH;CAEF,IAAI,CAAC,WAAW,MACd,MAAM,IAAI,MAAM,4BAA4B,MAAM,KAAI,uBAAwB;CAGhF,QAAQ,WAAW,MAAnB;EACE,KAAK,UACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,SACH,OAAO,CAAA;EACT,KAAK,aACH,OAAO,UAAU,OAAO,WAAW,IAAI;CAC3C;AACF;AASA,SAAS,UAAU,OAAoB,MAAiB;CACtD,IAAI,SAAS,eAAe,CAAC,QAAQ,KAAK,GACxC,MAAM,IAAI,MAAM,WAAW,MAAM,KAAI,wDAAyD;CAEhG,OAAO,WAAW,MAAM,MAAM;AAChC;AAGA,SAAS,QAAQ,OAAkB;CACjC,OAAO,MAAM,SAAS;AACxB;AAGA,SAAgB,eAAe,OAAkB;CAC/C,IAAI,MAAM,YAAY,QACpB,OAAO,UAAU,KAAK;CAExB,IAAI,MAAM,SAAS,WAAW,SAAS,MAAM,OAAO,GAGlD,OAAO;EAAE,GAAG,WAAW,MAAM,MAAM;EAAG,GAAG,MAAM;CAAO;CAExD,IAAI,MAAM,SAAS,UAAU,MAAM,QAAQ,MAAM,OAAO,GACtD,OAAO,MAAM,QAAQ,KAAK,SACxB,SAAS,IAAI,IAAI;EAAE,GAAG,WAAW,MAAM,UAAU;EAAG,GAAG;CAAI,IAAK,IAAI;CAGxE,OAAO,MAAM;AACf;AAMA,SAAgB,WAAW,QAA8B;CACvD,MAAM,QAAiC,CAAA;CACvC,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,OAAO,eAAe,KAAK;CAEzC,OAAO;AACT;AAWA,SAAgB,YACd,QACA,QAA+B;CAE/B,MAAM,SAAkC,CAAA;CACxC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,OAAO,MAAM;EAC9B,IAAI,MAAM,SAAS,SAAS;GAC1B,OAAO,MAAM,OAAO,SAAS,QAAQ,IACjC,YAAY,MAAM,QAAQ,QAAQ,IAClC,eAAe,KAAK;GACxB;EACF;EACA,IAAI,MAAM,SAAS,QAAQ;GACzB,MAAM,WAAW,WAAW,MAAM,UAAU;GAC5C,OAAO,MAAM,OAAO,MAAM,QAAQ,QAAQ,IACtC,SAAS,KAAK,SAAU,SAAS,IAAI,IAAI;IAAE,GAAG;IAAU,GAAG;GAAI,IAAK,IAAK,IACzE,eAAe,KAAK;GACxB;EACF;EACA,OAAO,MAAM,OAAO,aAAa,SAAY,WAAW,eAAe,KAAK;CAC9E;CACA,OAAO;AACT;AAEA,SAAgB,SAAS,OAAc;CACrC,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E"}
@@ -1,5 +1,5 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
2
- import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs";
2
+ import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs";
3
3
  import { n as FieldAria } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
4
4
  import { t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
5
5
  import { t as optionsFromRows } from "./tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs";
@@ -289,4 +289,4 @@ i0.ɵɵngDeclareClassMetadata({
289
289
  });
290
290
  export { FieldSelect };
291
291
 
292
- //# sourceMappingURL=tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map
292
+ //# sourceMappingURL=tinya-code-gas-admin-ui-select-BUbb36-T.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-select-DzaRfHXO.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/select/select.ts"],"sourcesContent":["import { Component, computed, inject, input, resource, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\n\nimport type { SelectField, SelectOption } from '../../core/models/schema.model';\nimport { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';\nimport { optionsFromRows } from '../../core/utils/source-options';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { FieldAria } from '../field-aria';\nimport { childTree, type RootTree } from '../field-node';\nimport { esGrupoRadio } from './radio-mode';\n\n/**\n * Desplegable de un solo valor (base.md §5.1).\n *\n * Sus opciones se declaran de dos formas y **exactamente una** (XOR, D62):\n * `options` estáticas en el schema o `source` — una fuente declarada que\n * pide `DataSourceService` con la misma caché que usan las relaciones y los\n * widgets (F8 · R6.5). Mientras la vuelta no llega se anuncia; sin anuncio\n * un desplegable vacío se confundiría con «esto no tiene opciones».\n */\n@Component({\n selector: 'app-field-select',\n imports: [FieldAria, FormField],\n template: `\n @if (fallo()) {\n <p class=\"mb-1 text-xs text-danger\" role=\"alert\">No se pudieron cargar las opciones.</p>\n } @else if (cargando()) {\n <p class=\"mb-1 text-xs text-neutral\" role=\"status\">Cargando opciones…</p>\n }\n @if (useRadio()) {\n <!-- Guía de control: 2–4 opciones estáticas → radio buttons, no un\n desplegable. Cada opción es un label implícito (texto y control\n juntos) y el grupo se nombra con aria-label: el rótulo del host\n se dibuja como texto porque un grupo no es un único control con\n el que emparejar un for. formField va en CADA input: Signal\n Forms soporta radio (checked = value === element.value) y setea\n name/disabled del grupo por estado de campo. -->\n <div\n class=\"mt-1 flex flex-col gap-1\"\n role=\"radiogroup\"\n [attr.aria-label]=\"field().label\"\n data-testid=\"select-radios\"\n >\n @if (!field().required) {\n <label class=\"flex w-fit items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n class=\"accent-primary\"\n [id]=\"state().name() + '-__vacio'\"\n value=\"\"\n [formField]=\"node()\"\n />\n <span class=\"text-neutral\">{{ field().placeholder ?? '— Seleccionar —' }}</span>\n </label>\n }\n @for (option of options(); track option.value) {\n <label class=\"flex w-fit items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n class=\"accent-primary\"\n [id]=\"state().name() + '-' + option.value\"\n [value]=\"option.value\"\n [formField]=\"node()\"\n />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n } @else if (useSearch()) {\n <!-- Guía §1: más de 7 opciones → dropdown con búsqueda. Se filtra el\n select nativo en vez de construir un combobox: el teclado, el\n anuncio de la opción activa y el valor elegido siguen siendo los\n del navegador. -->\n <input\n type=\"search\"\n class=\"mb-1 w-full rounded-lg border border-neutral/30 bg-surface px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n placeholder=\"Buscar…\"\n aria-label=\"Buscar entre las opciones\"\n [attr.aria-controls]=\"state().name()\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n />\n }\n <select class=\"field-input\" [id]=\"state().name()\" [formField]=\"node()\">\n @if (!field().required) {\n <option value=\"\">{{ field().placeholder ?? '— Seleccionar —' }}</option>\n }\n @for (option of visibleOptions(); track option.value) {\n <option [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">\n {{ option.label }}\n </option>\n }\n </select>\n @if (noMatches()) {\n <p class=\"mt-1 text-xs text-neutral\" role=\"status\">Sin resultados para «{{ query() }}».</p>\n }\n `,\n})\nexport class FieldSelect {\n readonly field = input.required<SelectField>();\n readonly tree = input.required<RootTree>();\n\n private readonly ds = inject(DataSourceService);\n\n protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));\n protected readonly state = computed(() => this.node()());\n\n /** Texto escrito en el buscador (sólo se usa si supera el umbral). */\n protected readonly query = signal('');\n\n /**\n * Registro del formulario, sólo para resolver `{campo}` en `source.params`.\n * Sin parámetros declarados no se lee: el campo no depende del formulario.\n */\n private readonly registro = computed<Record<string, unknown> | undefined>(() => {\n const source = this.field().source;\n if (source === undefined || source.params === undefined) return undefined;\n return this.tree()().value();\n });\n\n /** `undefined` sin fuente ⇒ `resource` queda idle y jamás llama al loader. */\n private readonly claveFuente = computed(() => {\n const source = this.field().source;\n return source === undefined ? undefined : dataSourceKey(source, this.registro());\n });\n\n private readonly fuenteResource = resource({\n params: () => this.claveFuente(),\n loader: async () => {\n const source = this.field().source;\n if (source === undefined) return [];\n return this.ds.rows(source, this.registro());\n },\n });\n\n /**\n * Opciones efectivas: las estáticas del schema o las filas de la fuente\n * declarada, mapeadas con la misma regla que una relación (F8 · R6.5).\n */\n protected readonly options = computed<SelectOption[]>(() => {\n const field = this.field();\n if (field.source === undefined) return field.options ?? [];\n // `value()` del resource TIRA en estado error (contract de Angular):\n // se mira el estado antes de leerlo, o el propio render rompe en vez\n // de pintar el aviso de fallo.\n if (this.fuenteResource.status() === 'error') return [];\n return optionsFromRows(this.fuenteResource.value() ?? [], {\n source: field.source,\n target: getSchema(field.source.resource),\n });\n });\n\n /** Sólo aplica con `source`: una lista estática no tiene vuelta que esperar. */\n protected readonly cargando = computed(\n () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'loading',\n );\n\n protected readonly fallo = computed(\n () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'error',\n );\n\n /** Umbral de la guía §1: 5–7 opciones dropdown simple, más de 7 con búsqueda. */\n protected readonly useSearch = computed(() => this.options().length > 7);\n\n /** Guía §1: 2–4 opciones estáticas → radio buttons (ver `radio-mode.ts`). */\n protected readonly useRadio = computed(() => esGrupoRadio(this.field()));\n\n protected onQuery(event: Event): void {\n this.query.set((event.target as HTMLInputElement).value);\n }\n\n /**\n * Opciones que coinciden con el término, SIN conservar la elegida: es la\n * base del mensaje «Sin resultados».\n */\n protected readonly matches = computed<SelectOption[]>(() => {\n const all = this.options();\n const term = this.query().trim().toLowerCase();\n if (term === '') {\n return all;\n }\n return all.filter((option) => option.label.toLowerCase().includes(term));\n });\n\n /** El buscador está activo y no encontró nada nuevo (el elegido no cuenta). */\n protected readonly noMatches = computed(\n () => this.useSearch() && this.query().trim() !== '' && this.matches().length === 0,\n );\n\n /**\n * Opciones que se dibujan. Con búsqueda activa se acotan por etiqueta, pero\n * la opción elegida se conserva siempre aunque no coincida con el término:\n * si desapareciera del DOM, el select dejaría de mostrar el valor actual\n * (guía §1: «valor seleccionado siempre visible»).\n */\n protected readonly visibleOptions = computed<SelectOption[]>(() => {\n const filtered = this.matches();\n if (this.query().trim() === '') {\n return filtered;\n }\n const selected = String(this.state().value() ?? '');\n if (filtered.some((option) => option.value === selected)) {\n return filtered;\n }\n const kept = this.options().find((option) => option.value === selected);\n return kept === undefined ? filtered : [...filtered, kept];\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-select-BUbb36-T.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/select/select.ts"],"sourcesContent":["import { Component, computed, inject, input, resource, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\n\nimport type { SelectField, SelectOption } from '../../core/models/schema.model';\nimport { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';\nimport { optionsFromRows } from '../../core/utils/source-options';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { FieldAria } from '../field-aria';\nimport { childTree, type RootTree } from '../field-node';\nimport { esGrupoRadio } from './radio-mode';\n\n/**\n * Desplegable de un solo valor (base.md §5.1).\n *\n * Sus opciones se declaran de dos formas y **exactamente una** (XOR, D62):\n * `options` estáticas en el schema o `source` — una fuente declarada que\n * pide `DataSourceService` con la misma caché que usan las relaciones y los\n * widgets (F8 · R6.5). Mientras la vuelta no llega se anuncia; sin anuncio\n * un desplegable vacío se confundiría con «esto no tiene opciones».\n */\n@Component({\n selector: 'app-field-select',\n imports: [FieldAria, FormField],\n template: `\n @if (fallo()) {\n <p class=\"mb-1 text-xs text-danger\" role=\"alert\">No se pudieron cargar las opciones.</p>\n } @else if (cargando()) {\n <p class=\"mb-1 text-xs text-neutral\" role=\"status\">Cargando opciones…</p>\n }\n @if (useRadio()) {\n <!-- Guía de control: 2–4 opciones estáticas → radio buttons, no un\n desplegable. Cada opción es un label implícito (texto y control\n juntos) y el grupo se nombra con aria-label: el rótulo del host\n se dibuja como texto porque un grupo no es un único control con\n el que emparejar un for. formField va en CADA input: Signal\n Forms soporta radio (checked = value === element.value) y setea\n name/disabled del grupo por estado de campo. -->\n <div\n class=\"mt-1 flex flex-col gap-1\"\n role=\"radiogroup\"\n [attr.aria-label]=\"field().label\"\n data-testid=\"select-radios\"\n >\n @if (!field().required) {\n <label class=\"flex w-fit items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n class=\"accent-primary\"\n [id]=\"state().name() + '-__vacio'\"\n value=\"\"\n [formField]=\"node()\"\n />\n <span class=\"text-neutral\">{{ field().placeholder ?? '— Seleccionar —' }}</span>\n </label>\n }\n @for (option of options(); track option.value) {\n <label class=\"flex w-fit items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n class=\"accent-primary\"\n [id]=\"state().name() + '-' + option.value\"\n [value]=\"option.value\"\n [formField]=\"node()\"\n />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n } @else if (useSearch()) {\n <!-- Guía §1: más de 7 opciones → dropdown con búsqueda. Se filtra el\n select nativo en vez de construir un combobox: el teclado, el\n anuncio de la opción activa y el valor elegido siguen siendo los\n del navegador. -->\n <input\n type=\"search\"\n class=\"mb-1 w-full rounded-lg border border-neutral/30 bg-surface px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n placeholder=\"Buscar…\"\n aria-label=\"Buscar entre las opciones\"\n [attr.aria-controls]=\"state().name()\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n />\n }\n <select class=\"field-input\" [id]=\"state().name()\" [formField]=\"node()\">\n @if (!field().required) {\n <option value=\"\">{{ field().placeholder ?? '— Seleccionar —' }}</option>\n }\n @for (option of visibleOptions(); track option.value) {\n <option [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">\n {{ option.label }}\n </option>\n }\n </select>\n @if (noMatches()) {\n <p class=\"mt-1 text-xs text-neutral\" role=\"status\">Sin resultados para «{{ query() }}».</p>\n }\n `,\n})\nexport class FieldSelect {\n readonly field = input.required<SelectField>();\n readonly tree = input.required<RootTree>();\n\n private readonly ds = inject(DataSourceService);\n\n protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));\n protected readonly state = computed(() => this.node()());\n\n /** Texto escrito en el buscador (sólo se usa si supera el umbral). */\n protected readonly query = signal('');\n\n /**\n * Registro del formulario, sólo para resolver `{campo}` en `source.params`.\n * Sin parámetros declarados no se lee: el campo no depende del formulario.\n */\n private readonly registro = computed<Record<string, unknown> | undefined>(() => {\n const source = this.field().source;\n if (source === undefined || source.params === undefined) return undefined;\n return this.tree()().value();\n });\n\n /** `undefined` sin fuente ⇒ `resource` queda idle y jamás llama al loader. */\n private readonly claveFuente = computed(() => {\n const source = this.field().source;\n return source === undefined ? undefined : dataSourceKey(source, this.registro());\n });\n\n private readonly fuenteResource = resource({\n params: () => this.claveFuente(),\n loader: async () => {\n const source = this.field().source;\n if (source === undefined) return [];\n return this.ds.rows(source, this.registro());\n },\n });\n\n /**\n * Opciones efectivas: las estáticas del schema o las filas de la fuente\n * declarada, mapeadas con la misma regla que una relación (F8 · R6.5).\n */\n protected readonly options = computed<SelectOption[]>(() => {\n const field = this.field();\n if (field.source === undefined) return field.options ?? [];\n // `value()` del resource TIRA en estado error (contract de Angular):\n // se mira el estado antes de leerlo, o el propio render rompe en vez\n // de pintar el aviso de fallo.\n if (this.fuenteResource.status() === 'error') return [];\n return optionsFromRows(this.fuenteResource.value() ?? [], {\n source: field.source,\n target: getSchema(field.source.resource),\n });\n });\n\n /** Sólo aplica con `source`: una lista estática no tiene vuelta que esperar. */\n protected readonly cargando = computed(\n () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'loading',\n );\n\n protected readonly fallo = computed(\n () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'error',\n );\n\n /** Umbral de la guía §1: 5–7 opciones dropdown simple, más de 7 con búsqueda. */\n protected readonly useSearch = computed(() => this.options().length > 7);\n\n /** Guía §1: 2–4 opciones estáticas → radio buttons (ver `radio-mode.ts`). */\n protected readonly useRadio = computed(() => esGrupoRadio(this.field()));\n\n protected onQuery(event: Event): void {\n this.query.set((event.target as HTMLInputElement).value);\n }\n\n /**\n * Opciones que coinciden con el término, SIN conservar la elegida: es la\n * base del mensaje «Sin resultados».\n */\n protected readonly matches = computed<SelectOption[]>(() => {\n const all = this.options();\n const term = this.query().trim().toLowerCase();\n if (term === '') {\n return all;\n }\n return all.filter((option) => option.label.toLowerCase().includes(term));\n });\n\n /** El buscador está activo y no encontró nada nuevo (el elegido no cuenta). */\n protected readonly noMatches = computed(\n () => this.useSearch() && this.query().trim() !== '' && this.matches().length === 0,\n );\n\n /**\n * Opciones que se dibujan. Con búsqueda activa se acotan por etiqueta, pero\n * la opción elegida se conserva siempre aunque no coincida con el término:\n * si desapareciera del DOM, el select dejaría de mostrar el valor actual\n * (guía §1: «valor seleccionado siempre visible»).\n */\n protected readonly visibleOptions = computed<SelectOption[]>(() => {\n const filtered = this.matches();\n if (this.query().trim() === '') {\n return filtered;\n }\n const selected = String(this.state().value() ?? '');\n if (filtered.some((option) => option.value === selected)) {\n return filtered;\n }\n const kept = this.options().find((option) => option.value === selected);\n return kept === undefined ? filtered : [...filtered, kept];\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
1
+ import { l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs";
2
2
  import * as i0 from "@angular/core";
3
3
  import { Service, inject } from "@angular/core";
4
4
  import { HttpClient, HttpEventType, HttpResponse } from "@angular/common/http";
@@ -57,4 +57,4 @@ i0.ɵɵngDeclareClassMetadata({
57
57
  });
58
58
  export { UploadService as t };
59
59
 
60
- //# sourceMappingURL=tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map
60
+ //# sourceMappingURL=tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/upload.service.ts"],"sourcesContent":["import { HttpClient, HttpEvent, HttpEventType, HttpResponse } from '@angular/common/http';\nimport { inject, Service } from '@angular/core';\nimport { map, Observable, switchMap } from 'rxjs';\n\nimport { ApiService } from './api.service';\n\n/** Firma que emite `/admin/upload-signature` (baseapi §11.3). NUNCA `api_secret`. */\ninterface UploadSignature {\n cloud_name: string;\n api_key: string;\n timestamp: number;\n signature: string;\n folder: string;\n allowed_formats: string;\n upload_url: string;\n}\n\n/** Respuesta de Cloudinary en un upload normal (sólo interesa `secure_url`). */\ninterface CloudinaryResponse {\n secure_url?: unknown;\n}\n\n/**\n * Subida de imágenes en DOS PASOS (baseapi §11.2, opción B):\n * 1. Firma firmada por el backend → `POST /admin/upload-signature`\n * con `{ resource }` (mismo envelope text/plain que todo `/admin/*`).\n * 2. El navegador sube directo a Cloudinary (FormData a `upload_url`) con\n * los parámetros que fijó y firmó el backend; el API secret jamás sale\n * de acá (§11.1–§11.3).\n *\n * Se guarda SOLO `secure_url` (base.md §7: la URL pública es el valor del\n * campo; la imagen anterior no se borra al reemplazar — §11.6).\n *\n * Los errores (firma caducada, rate-limit 429, red) llegan por el canal de\n * error; el componente conserva la imagen anterior (§11.2 paso 6).\n */\n@Service()\nexport class UploadService {\n private readonly http = inject(HttpClient);\n private readonly api = inject(ApiService);\n\n /**\n * Misma subida con eventos de progreso (base.md §7, estado «Subiendo»):\n * emite `UploadProgress` durante la subida a Cloudinary y termina con\n * `Response` llevando la URL en el body. El campo sólo cambia cuando\n * llega la respuesta.\n */\n uploadWithProgress(file: File, resource: string, field?: string): Observable<HttpEvent<string>> {\n // `field` es opcional (R9.5.3): si el campo declara `formats`, esos son\n // los formatos que el backend firma. Sin campo firma la lista global.\n const payload: { resource: string; field?: string } = field\n ? { resource, field }\n : { resource };\n return this.api.request<UploadSignature>('POST', '/admin/upload-signature', payload).pipe(\n switchMap((signature) => {\n const form = new FormData();\n form.append('file', file, file.name);\n form.append('api_key', signature.api_key);\n form.append('timestamp', String(signature.timestamp));\n form.append('signature', signature.signature);\n form.append('folder', signature.folder);\n form.append('allowed_formats', signature.allowed_formats);\n return this.http.post<CloudinaryResponse>(signature.upload_url, form, {\n reportProgress: true,\n observe: 'events',\n });\n }),\n map((event) => {\n if (event.type !== HttpEventType.Response) {\n return event;\n }\n const url = typeof event.body?.secure_url === 'string' ? event.body.secure_url : '';\n return new HttpResponse({\n body: url,\n status: event.status,\n statusText: event.statusText,\n });\n }),\n );\n }\n}\n"],"mappings":";;;;;AAqCA,IAAa,gBAAb,MAAa,cAAa;CACP,OAAO,OAAO,UAAU;CACxB,MAAM,OAAO,UAAU;CAQxC,mBAAmB,MAAY,UAAkB,OAAc;EAG7D,MAAM,UAAgD,QAClD;GAAE;GAAU;EAAK,IACjB,EAAE,SAAQ;EACd,OAAO,KAAK,IAAI,QAAyB,QAAQ,2BAA2B,OAAO,CAAC,CAAC,KACnF,WAAW,cAAa;GACtB,MAAM,OAAO,IAAI,SAAQ;GACzB,KAAK,OAAO,QAAQ,MAAM,KAAK,IAAI;GACnC,KAAK,OAAO,WAAW,UAAU,OAAO;GACxC,KAAK,OAAO,aAAa,OAAO,UAAU,SAAS,CAAC;GACpD,KAAK,OAAO,aAAa,UAAU,SAAS;GAC5C,KAAK,OAAO,UAAU,UAAU,MAAM;GACtC,KAAK,OAAO,mBAAmB,UAAU,eAAe;GACxD,OAAO,KAAK,KAAK,KAAyB,UAAU,YAAY,MAAM;IACpE,gBAAgB;IAChB,SAAS;GACV,CAAA;EACH,CAAC,GACD,KAAK,UAAS;GACZ,IAAI,MAAM,SAAS,cAAc,UAC/B,OAAO;GAET,MAAM,MAAM,OAAO,MAAM,MAAM,eAAe,WAAW,MAAM,KAAK,aAAa;GACjF,OAAO,IAAI,aAAa;IACtB,MAAM;IACN,QAAQ,MAAM;IACd,YAAY,MAAM;GACnB,CAAA;EACH,CAAC,CAAC;CAEN;CA1CW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAa,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAb,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAa,CAAA;;AAAb,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAa,YAAA,CAAA,EADzB,MAAA,QAAA,CAAA"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/upload.service.ts"],"sourcesContent":["import { HttpClient, HttpEvent, HttpEventType, HttpResponse } from '@angular/common/http';\nimport { inject, Service } from '@angular/core';\nimport { map, Observable, switchMap } from 'rxjs';\n\nimport { ApiService } from './api.service';\n\n/** Firma que emite `/admin/upload-signature` (baseapi §11.3). NUNCA `api_secret`. */\ninterface UploadSignature {\n cloud_name: string;\n api_key: string;\n timestamp: number;\n signature: string;\n folder: string;\n allowed_formats: string;\n upload_url: string;\n}\n\n/** Respuesta de Cloudinary en un upload normal (sólo interesa `secure_url`). */\ninterface CloudinaryResponse {\n secure_url?: unknown;\n}\n\n/**\n * Subida de imágenes en DOS PASOS (baseapi §11.2, opción B):\n * 1. Firma firmada por el backend → `POST /admin/upload-signature`\n * con `{ resource }` (mismo envelope text/plain que todo `/admin/*`).\n * 2. El navegador sube directo a Cloudinary (FormData a `upload_url`) con\n * los parámetros que fijó y firmó el backend; el API secret jamás sale\n * de acá (§11.1–§11.3).\n *\n * Se guarda SOLO `secure_url` (base.md §7: la URL pública es el valor del\n * campo; la imagen anterior no se borra al reemplazar — §11.6).\n *\n * Los errores (firma caducada, rate-limit 429, red) llegan por el canal de\n * error; el componente conserva la imagen anterior (§11.2 paso 6).\n */\n@Service()\nexport class UploadService {\n private readonly http = inject(HttpClient);\n private readonly api = inject(ApiService);\n\n /**\n * Misma subida con eventos de progreso (base.md §7, estado «Subiendo»):\n * emite `UploadProgress` durante la subida a Cloudinary y termina con\n * `Response` llevando la URL en el body. El campo sólo cambia cuando\n * llega la respuesta.\n */\n uploadWithProgress(file: File, resource: string, field?: string): Observable<HttpEvent<string>> {\n // `field` es opcional (R9.5.3): si el campo declara `formats`, esos son\n // los formatos que el backend firma. Sin campo firma la lista global.\n const payload: { resource: string; field?: string } = field\n ? { resource, field }\n : { resource };\n return this.api.request<UploadSignature>('POST', '/admin/upload-signature', payload).pipe(\n switchMap((signature) => {\n const form = new FormData();\n form.append('file', file, file.name);\n form.append('api_key', signature.api_key);\n form.append('timestamp', String(signature.timestamp));\n form.append('signature', signature.signature);\n form.append('folder', signature.folder);\n form.append('allowed_formats', signature.allowed_formats);\n return this.http.post<CloudinaryResponse>(signature.upload_url, form, {\n reportProgress: true,\n observe: 'events',\n });\n }),\n map((event) => {\n if (event.type !== HttpEventType.Response) {\n return event;\n }\n const url = typeof event.body?.secure_url === 'string' ? event.body.secure_url : '';\n return new HttpResponse({\n body: url,\n status: event.status,\n statusText: event.statusText,\n });\n }),\n );\n }\n}\n"],"mappings":";;;;;AAqCA,IAAa,gBAAb,MAAa,cAAa;CACP,OAAO,OAAO,UAAU;CACxB,MAAM,OAAO,UAAU;CAQxC,mBAAmB,MAAY,UAAkB,OAAc;EAG7D,MAAM,UAAgD,QAClD;GAAE;GAAU;EAAK,IACjB,EAAE,SAAQ;EACd,OAAO,KAAK,IAAI,QAAyB,QAAQ,2BAA2B,OAAO,CAAC,CAAC,KACnF,WAAW,cAAa;GACtB,MAAM,OAAO,IAAI,SAAQ;GACzB,KAAK,OAAO,QAAQ,MAAM,KAAK,IAAI;GACnC,KAAK,OAAO,WAAW,UAAU,OAAO;GACxC,KAAK,OAAO,aAAa,OAAO,UAAU,SAAS,CAAC;GACpD,KAAK,OAAO,aAAa,UAAU,SAAS;GAC5C,KAAK,OAAO,UAAU,UAAU,MAAM;GACtC,KAAK,OAAO,mBAAmB,UAAU,eAAe;GACxD,OAAO,KAAK,KAAK,KAAyB,UAAU,YAAY,MAAM;IACpE,gBAAgB;IAChB,SAAS;GACV,CAAA;EACH,CAAC,GACD,KAAK,UAAS;GACZ,IAAI,MAAM,SAAS,cAAc,UAC/B,OAAO;GAET,MAAM,MAAM,OAAO,MAAM,MAAM,eAAe,WAAW,MAAM,KAAK,aAAa;GACjF,OAAO,IAAI,aAAa;IACtB,MAAM;IACN,QAAQ,MAAM;IACd,YAAY,MAAM;GACnB,CAAA;EACH,CAAC,CAAC;CAEN;CA1CW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAa,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAb,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAa,CAAA;;AAAb,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAa,YAAA,CAAA,EADzB,MAAA,QAAA,CAAA"}
@@ -1,17 +1,16 @@
1
1
  import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
- import { a as SESSION_USER_KEY, c as renewedTokenOf, i as SESSION_TOKEN_KEY, n as AuthService, o as API_URL, r as LOGIN_PATH, s as ApiError, t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
3
- import { a as provideGasAdmin, c as apiErrorInterceptor, i as getSchema, n as allSchemas, o as setSchemaCatalog, r as getAuth, s as ConfigService, t as GAS_SCHEMAS } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
4
- import { a as register, c as unregister, d as SUPPORTED_FIELD_TYPE_LIST, f as fieldDescriptors, i as provideRenderDescriptors, l as viewDescriptors, m as columnFormatDescriptors, n as get, o as renderKinds, p as COLUMN_FORMAT_TYPES, r as isSupported, u as layoutDescriptors } from "./tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs";
5
- import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs";
2
+ import { _ as ApiError, a as provideFirebaseAdapter, c as ConfigService, d as AuthService, f as LOGIN_PATH, g as FIREBASE_AUTH_ADAPTER, h as API_URL, i as getSchema, l as ApiService, m as SESSION_USER_KEY, n as allSchemas, o as provideGasAdmin, p as SESSION_TOKEN_KEY, r as getAuth, s as setSchemaCatalog, t as GAS_SCHEMAS, u as apiErrorInterceptor, v as renewedTokenOf } from "./tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs";
3
+ import { a as register, c as unregister, d as SUPPORTED_FIELD_TYPE_LIST, f as fieldDescriptors, i as provideRenderDescriptors, l as viewDescriptors, m as columnFormatDescriptors, n as get, o as renderKinds, p as COLUMN_FORMAT_TYPES, r as isSupported, u as layoutDescriptors } from "./tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs";
4
+ import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs";
6
5
  import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
7
6
  import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
8
7
  import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
9
- import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs";
10
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs";
11
- import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs";
8
+ import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs";
9
+ import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs";
10
+ import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs";
12
11
  import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
13
12
  import { a as stringItemTree, i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
14
- import { _ as resolveShare, a as DrawerForm, c as buildFormSchema, d as actionTypes, f as actionsFor, g as isTriggerAction, h as isShareAction, i as CopyShare, l as FieldHost, m as registerAction, o as buildPayload, p as getAction, r as Modal, s as seedModel, t as FormView, u as actionType, v as RenderHost } from "./tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs";
13
+ import { _ as resolveShare, a as DrawerForm, c as buildFormSchema, d as actionTypes, f as actionsFor, g as isTriggerAction, h as isShareAction, i as CopyShare, l as FieldHost, m as registerAction, o as buildPayload, p as getAction, r as Modal, s as seedModel, t as FormView, u as actionType, v as RenderHost } from "./tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs";
15
14
  import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
16
15
  import { t as getValue } from "./tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs";
17
16
  import * as i0 from "@angular/core";
@@ -793,8 +792,10 @@ var Login = class Login {
793
792
  auth = inject(AuthService);
794
793
  router = inject(Router);
795
794
  schema = getAuth();
795
+ providers = this.schema.providers;
796
796
  error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
797
797
  loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
798
+ providerLoading = signal(null, ...ngDevMode ? [{ debugName: "providerLoading" }] : /* istanbul ignore next */ []);
798
799
  values = signal({}, ...ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []);
799
800
  label(key) {
800
801
  return LABELS[key] ?? key;
@@ -808,6 +809,21 @@ var Login = class Login {
808
809
  [key]: next
809
810
  }));
810
811
  }
812
+ loginWithGoogle() {
813
+ if (this.providerLoading()) return;
814
+ this.providerLoading.set("google");
815
+ this.error.set(null);
816
+ this.auth.loginWithGoogle().subscribe({
817
+ next: () => {
818
+ this.providerLoading.set(null);
819
+ this.goAfterLogin();
820
+ },
821
+ error: (err) => {
822
+ this.providerLoading.set(null);
823
+ this.error.set(err instanceof ApiError && err.message !== "" ? err.message : err instanceof Error && err.message !== "" ? err.message : "No se pudo iniciar sesión con Google.");
824
+ }
825
+ });
826
+ }
811
827
  submit(event) {
812
828
  event.preventDefault();
813
829
  if (this.loading()) return;
@@ -817,6 +833,21 @@ var Login = class Login {
817
833
  this.error.set("Completa el correo y la contraseña.");
818
834
  return;
819
835
  }
836
+ if (this.schema.firebase && this.schema.providers.includes("password")) {
837
+ this.loading.set(true);
838
+ this.error.set(null);
839
+ this.auth.loginWithFirebasePassword(email, password).subscribe({
840
+ next: () => {
841
+ this.loading.set(false);
842
+ this.goAfterLogin();
843
+ },
844
+ error: (err) => {
845
+ this.loading.set(false);
846
+ this.error.set(err instanceof ApiError && err.message !== "" ? err.message : err instanceof Error && err.message !== "" ? err.message : "No se pudo iniciar sesión.");
847
+ }
848
+ });
849
+ return;
850
+ }
820
851
  this.loading.set(true);
821
852
  this.error.set(null);
822
853
  this.auth.login(email, password).subscribe({
@@ -850,7 +881,7 @@ var Login = class Login {
850
881
  isStandalone: true,
851
882
  selector: "app-login",
852
883
  ngImport: i0,
853
- template: "\n <main class=\"flex min-h-[100svh] items-center justify-center bg-surface px-4\">\n <div\n class=\"w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm\"\n >\n <h1 class=\"font-display text-lg font-semibold\">Admin</h1>\n <p class=\"mt-1 text-sm text-neutral\">Inicia sesión para continuar</p>\n\n <form class=\"mt-6 space-y-4\" (submit)=\"submit($event)\" novalidate>\n @for (field of schema.loginFields; track field.key) {\n <div>\n <label [attr.for]=\"field.key\" class=\"block text-sm font-medium text-neutral\">\n {{ label(field.key) }}\n </label>\n\n @switch (field.type) {\n @case ('email') {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"email\"\n autocomplete=\"email\"\n inputmode=\"email\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n @default {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"password\"\n autocomplete=\"current-password\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n }\n </div>\n }\n\n @if (error(); as message) {\n <p role=\"alert\" class=\"rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger\">\n {{ message }}\n </p>\n }\n\n <button app-button type=\"submit\" [loading]=\"loading()\" class=\"w-full\">\n Iniciar sesión\n </button>\n </form>\n </div>\n </main>\n ",
884
+ template: "\n <main class=\"flex min-h-[100svh] items-center justify-center bg-surface px-4\">\n <div\n class=\"w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm\"\n >\n <h1 class=\"font-display text-lg font-semibold\">Admin</h1>\n <p class=\"mt-1 text-sm text-neutral\">Inicia sesión para continuar</p>\n\n @if (providers.includes('google')) {\n <button\n type=\"button\"\n id=\"btn-login-google\"\n app-button\n variant=\"outline\"\n [loading]=\"providerLoading() === 'google'\"\n (click)=\"loginWithGoogle()\"\n class=\"mt-6 w-full\"\n >\n Continuar con Google\n </button>\n <div class=\"my-4 flex items-center gap-3\">\n <hr class=\"flex-1 border-neutral/20\" />\n <span class=\"text-xs text-neutral/50\">o</span>\n <hr class=\"flex-1 border-neutral/20\" />\n </div>\n }\n\n <form\n [class]=\"providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'\"\n (submit)=\"submit($event)\"\n novalidate\n >\n @for (field of schema.loginFields; track field.key) {\n <div>\n <label [attr.for]=\"field.key\" class=\"block text-sm font-medium text-neutral\">\n {{ label(field.key) }}\n </label>\n\n @switch (field.type) {\n @case ('email') {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"email\"\n autocomplete=\"email\"\n inputmode=\"email\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n @default {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"password\"\n autocomplete=\"current-password\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n }\n </div>\n }\n\n @if (error(); as message) {\n <p role=\"alert\" class=\"rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger\">\n {{ message }}\n </p>\n }\n\n <button app-button type=\"submit\" [loading]=\"loading()\" class=\"w-full\">\n Iniciar sesión\n </button>\n </form>\n </div>\n </main>\n ",
854
885
  isInline: true,
855
886
  dependencies: [{
856
887
  kind: "component",
@@ -884,7 +915,30 @@ i0.ɵɵngDeclareClassMetadata({
884
915
  <h1 class="font-display text-lg font-semibold">Admin</h1>
885
916
  <p class="mt-1 text-sm text-neutral">Inicia sesión para continuar</p>
886
917
 
887
- <form class="mt-6 space-y-4" (submit)="submit($event)" novalidate>
918
+ @if (providers.includes('google')) {
919
+ <button
920
+ type="button"
921
+ id="btn-login-google"
922
+ app-button
923
+ variant="outline"
924
+ [loading]="providerLoading() === 'google'"
925
+ (click)="loginWithGoogle()"
926
+ class="mt-6 w-full"
927
+ >
928
+ Continuar con Google
929
+ </button>
930
+ <div class="my-4 flex items-center gap-3">
931
+ <hr class="flex-1 border-neutral/20" />
932
+ <span class="text-xs text-neutral/50">o</span>
933
+ <hr class="flex-1 border-neutral/20" />
934
+ </div>
935
+ }
936
+
937
+ <form
938
+ [class]="providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'"
939
+ (submit)="submit($event)"
940
+ novalidate
941
+ >
888
942
  @for (field of schema.loginFields; track field.key) {
889
943
  <div>
890
944
  <label [attr.for]="field.key" class="block text-sm font-medium text-neutral">
@@ -4398,6 +4452,6 @@ i0.ɵɵngDeclareClassMetadata({
4398
4452
  }]
4399
4453
  }
4400
4454
  });
4401
- export { API_URL, ApiError, ApiService, AuthService, COLUMN_FORMAT_TYPES, ComposerHost, ConfirmDialog, Dashboard, DataSourceService, ErrorMapperService, FieldHost, FormView, GAS_SCHEMAS, ListView, Login, MAX_DEPTH, NotificationService, ResourcePage, SESSION_TOKEN_KEY, SESSION_USER_KEY, SUPPORTED_FIELD_TYPE_LIST, SchemaCompositionError, Sidebar, Toast, UploadService, actionType, actionTypes, actionsFor, allSchemas, apiErrorInterceptor, authGuard, buildFormSchema, buildPayload, childTree, columnFormatDescriptors, fieldDescriptors, getAction, getAuth, get as getRenderDescriptor, getSchema, groupTree, interpolateTemplate, isSupported as isRenderSupported, isSupported, isShareAction, isTriggerAction, itemTree, layoutDescriptors, listTree, resource_page_exports as n, provideGasAdmin, provideRenderDescriptors, registerAction, register as registerRenderDescriptor, renderKinds, renewedTokenOf, resolveShare, resourceRoutes, seedModel, setSchemaCatalog, shellRoutes, stringItemTree, composer_host_exports as t, unregister as unregisterRenderDescriptor, validateComposition, viewDescriptors };
4455
+ export { API_URL, ApiError, ApiService, AuthService, COLUMN_FORMAT_TYPES, ComposerHost, ConfirmDialog, Dashboard, DataSourceService, ErrorMapperService, FIREBASE_AUTH_ADAPTER, FieldHost, FormView, GAS_SCHEMAS, ListView, Login, MAX_DEPTH, NotificationService, ResourcePage, SESSION_TOKEN_KEY, SESSION_USER_KEY, SUPPORTED_FIELD_TYPE_LIST, SchemaCompositionError, Sidebar, Toast, UploadService, actionType, actionTypes, actionsFor, allSchemas, apiErrorInterceptor, authGuard, buildFormSchema, buildPayload, childTree, columnFormatDescriptors, fieldDescriptors, getAction, getAuth, get as getRenderDescriptor, getSchema, groupTree, interpolateTemplate, isSupported as isRenderSupported, isSupported, isShareAction, isTriggerAction, itemTree, layoutDescriptors, listTree, resource_page_exports as n, provideFirebaseAdapter, provideGasAdmin, provideRenderDescriptors, registerAction, register as registerRenderDescriptor, renderKinds, renewedTokenOf, resolveShare, resourceRoutes, seedModel, setSchemaCatalog, shellRoutes, stringItemTree, composer_host_exports as t, unregister as unregisterRenderDescriptor, validateComposition, viewDescriptors };
4402
4456
 
4403
4457
  //# sourceMappingURL=tinya-code-gas-admin-ui.mjs.map