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

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 (39) hide show
  1. package/README.md +186 -2
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs → tinya-code-gas-admin-ui-calendar-view-tzp8wDv8.mjs} +12 -7
  3. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-tzp8wDv8.mjs.map +1 -0
  4. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs → tinya-code-gas-admin-ui-condition-evaluator-D6iWD7x6.mjs} +2 -2
  5. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-D6iWD7x6.mjs.map} +1 -1
  6. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs → tinya-code-gas-admin-ui-data-source.service-D6UUFaRh.mjs} +2 -2
  7. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs.map → tinya-code-gas-admin-ui-data-source.service-D6UUFaRh.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs → tinya-code-gas-admin-ui-error-mapper.service-DbbRUos2.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-DbbRUos2.mjs.map} +1 -1
  10. package/fesm2022/{tinya-code-gas-admin-ui-file-DhLi3eO0.mjs → tinya-code-gas-admin-ui-file-BHjCoDqI.mjs} +3 -3
  11. package/fesm2022/{tinya-code-gas-admin-ui-file-DhLi3eO0.mjs.map → tinya-code-gas-admin-ui-file-BHjCoDqI.mjs.map} +1 -1
  12. package/fesm2022/{tinya-code-gas-admin-ui-form-view-CjC-GOQl.mjs → tinya-code-gas-admin-ui-form-view-CCuSWleN.mjs} +9 -9
  13. package/fesm2022/{tinya-code-gas-admin-ui-form-view-CjC-GOQl.mjs.map → tinya-code-gas-admin-ui-form-view-CCuSWleN.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-image-D9-mpBhA.mjs → tinya-code-gas-admin-ui-image-D3wT1DT8.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-image-D9-mpBhA.mjs.map → tinya-code-gas-admin-ui-image-D3wT1DT8.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B4u2ibPb.mjs → tinya-code-gas-admin-ui-kanban-view-Db4tmlEt.mjs} +4 -4
  17. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B4u2ibPb.mjs.map → tinya-code-gas-admin-ui-kanban-view-Db4tmlEt.mjs.map} +1 -1
  18. package/fesm2022/{tinya-code-gas-admin-ui-list-CQpL6vyE.mjs → tinya-code-gas-admin-ui-list-B9_KFhLF.mjs} +2 -2
  19. package/fesm2022/{tinya-code-gas-admin-ui-list-CQpL6vyE.mjs.map → tinya-code-gas-admin-ui-list-B9_KFhLF.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CTQPGBOK.mjs → tinya-code-gas-admin-ui-multiselect-D4fmOIdl.mjs} +3 -3
  21. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CTQPGBOK.mjs.map → tinya-code-gas-admin-ui-multiselect-D4fmOIdl.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-relation-lr_XG6jt.mjs → tinya-code-gas-admin-ui-relation-ByFiFNoG.mjs} +3 -3
  23. package/fesm2022/{tinya-code-gas-admin-ui-relation-lr_XG6jt.mjs.map → tinya-code-gas-admin-ui-relation-ByFiFNoG.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CcIgBsSl.mjs → tinya-code-gas-admin-ui-render-registry-FiaFFQgq.mjs} +10 -10
  25. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CcIgBsSl.mjs.map → tinya-code-gas-admin-ui-render-registry-FiaFFQgq.mjs.map} +1 -1
  26. package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs → tinya-code-gas-admin-ui-schema-catalog-C20qT5mn.mjs} +191 -62
  27. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-C20qT5mn.mjs.map +1 -0
  28. package/fesm2022/{tinya-code-gas-admin-ui-seed-Bn6tl-yM.mjs → tinya-code-gas-admin-ui-seed-BXlVLSx1.mjs} +2 -2
  29. package/fesm2022/{tinya-code-gas-admin-ui-seed-Bn6tl-yM.mjs.map → tinya-code-gas-admin-ui-seed-BXlVLSx1.mjs.map} +1 -1
  30. package/fesm2022/{tinya-code-gas-admin-ui-select-BwefyMVx.mjs → tinya-code-gas-admin-ui-select-09unvYOK.mjs} +3 -3
  31. package/fesm2022/{tinya-code-gas-admin-ui-select-BwefyMVx.mjs.map → tinya-code-gas-admin-ui-select-09unvYOK.mjs.map} +1 -1
  32. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs → tinya-code-gas-admin-ui-upload.service-Buw8VetB.mjs} +2 -2
  33. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs.map → tinya-code-gas-admin-ui-upload.service-Buw8VetB.mjs.map} +1 -1
  34. package/fesm2022/tinya-code-gas-admin-ui.mjs +51 -53
  35. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/types/tinya-code-gas-admin-ui.d.ts +190 -12
  38. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs.map +0 -1
  39. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
2
- import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-C20qT5mn.mjs";
2
+ import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-D6UUFaRh.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-BwefyMVx.mjs.map
292
+ //# sourceMappingURL=tinya-code-gas-admin-ui-select-09unvYOK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-select-BwefyMVx.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-09unvYOK.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 { l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
1
+ import { l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-C20qT5mn.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-DRmKFrSH.mjs.map
60
+ //# sourceMappingURL=tinya-code-gas-admin-ui-upload.service-Buw8VetB.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-upload.service-DRmKFrSH.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-Buw8VetB.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,23 +1,23 @@
1
1
  import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
- import { _ as FIREBASE_AUTH_ADAPTER, a as provideFirebaseAdapter, b as renewedTokenOf, c as ConfigService, d as AuthService, f as LOGIN_PATH, g as API_URL, h as API_DRIVER, 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 VALUE_SOURCES, y as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.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-CcIgBsSl.mjs";
4
- import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs";
2
+ import { _ as DEFAULT_RESPONSE_UNWRAP, a as provideFirebaseAdapter, b as ApiError, c as ConfigService, d as AuthService, f as LOGIN_PATH, g as API_URL, h as API_DRIVER, 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 FIREBASE_AUTH_ADAPTER, x as renewedTokenOf, y as VALUE_SOURCES } from "./tinya-code-gas-admin-ui-schema-catalog-C20qT5mn.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-FiaFFQgq.mjs";
4
+ import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-D6iWD7x6.mjs";
5
5
  import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
6
6
  import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
7
7
  import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
8
- import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs";
9
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs";
10
- import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs";
8
+ import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-D6UUFaRh.mjs";
9
+ import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-DbbRUos2.mjs";
10
+ import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-Buw8VetB.mjs";
11
11
  import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
12
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";
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-CjC-GOQl.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-CCuSWleN.mjs";
14
14
  import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
15
15
  import { t as getValue } from "./tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs";
16
16
  import * as i0 from "@angular/core";
17
17
  import { ChangeDetectionStrategy, Component, EnvironmentInjector, Injector, Service, ViewContainerRef, computed, effect, inject, input, model, output, resource, signal, viewChild } from "@angular/core";
18
18
  import { catchError, combineLatest, distinctUntilChanged, firstValueFrom, map, of, switchMap } from "rxjs";
19
19
  import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet, convertToParamMap } from "@angular/router";
20
- import { LucideArrowLeft, LucideChevronDown, LucideChevronRight, LucideChevronUp, LucideCircleCheck, LucideCircleX, LucideDynamicIcon, LucideExternalLink, LucideEye, LucideGripVertical, LucideInfo, LucideMonitor, LucideMoon, LucidePencil, LucidePlus, LucideRotateCw, LucideSun, LucideTrash2, LucideX, LucideZap } from "@lucide/angular";
20
+ import { LucideArrowLeft, LucideChevronDown, LucideChevronRight, LucideChevronUp, LucideCircleCheck, LucideCircleX, LucideDynamicIcon, LucideExternalLink, LucideEye, LucideGripVertical, LucideInfo, LucideLogOut, LucideMonitor, LucideMoon, LucidePencil, LucidePlus, LucideRotateCw, LucideSun, LucideTrash2, LucideX, LucideZap } from "@lucide/angular";
21
21
  import { takeUntilDestroyed, toObservable, toSignal } from "@angular/core/rxjs-interop";
22
22
  import { CdkDrag, CdkDragHandle, CdkDropList } from "@angular/cdk/drag-drop";
23
23
  const authGuard = () => {
@@ -366,30 +366,11 @@ var Sidebar = class Sidebar {
366
366
  auth = inject(AuthService);
367
367
  notifications = inject(NotificationService);
368
368
  isAuthenticated = this.auth.isAuthenticated;
369
- closingAllSessions = signal(false, ...ngDevMode ? [{ debugName: "closingAllSessions" }] : /* istanbul ignore next */ []);
370
369
  loggingOut = signal(false, ...ngDevMode ? [{ debugName: "loggingOut" }] : /* istanbul ignore next */ []);
371
370
  menuItems = computed(() => allSchemas().filter((schema) => isVisible(schema) && this.auth.canAccess(schema.access)).map((schema) => ({
372
371
  id: schema.id,
373
372
  label: schema.labelPlural
374
373
  })), ...ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []);
375
- async closeAllSessions() {
376
- if (!await this.notifications.confirm({
377
- title: "Cerrar todas las sesiones",
378
- message: "Se cerrarán las sesiones abiertas en todos tus dispositivos. ¿Continuar?",
379
- confirmLabel: "Cerrar sesiones"
380
- })) return;
381
- this.closingAllSessions.set(true);
382
- this.auth.logoutAll().subscribe({
383
- next: ({ effectiveWithinSeconds }) => {
384
- this.notifications.success(`Sesiones revocadas. El cambio se aplica en un máximo de ${effectiveWithinSeconds} segundos.`);
385
- },
386
- error: () => {
387
- this.closingAllSessions.set(false);
388
- this.notifications.error("No se pudieron cerrar todas las sesiones. Inténtalo de nuevo.");
389
- },
390
- complete: () => this.closingAllSessions.set(false)
391
- });
392
- }
393
374
  async closeSession() {
394
375
  if (!await this.notifications.confirm({
395
376
  title: "Cerrar sesión",
@@ -465,14 +446,6 @@ var Sidebar = class Sidebar {
465
446
  <svg lucideLogOut size="16" />
466
447
  Cerrar sesión
467
448
  </button>
468
- <button
469
- type="button"
470
- class="w-full rounded-lg px-3 py-2 text-left text-sm text-neutral hover:bg-neutral/10 disabled:opacity-50 mt-2"
471
- [disabled]="closingAllSessions()"
472
- (click)="closeAllSessions()"
473
- >
474
- {{ closingAllSessions() ? 'Cerrando sesiones…' : 'Cerrar todas las sesiones' }}
475
- </button>
476
449
  </div>
477
450
  }
478
451
  </aside>
@@ -526,6 +499,11 @@ var Sidebar = class Sidebar {
526
499
  "disabled",
527
500
  "loading"
528
501
  ]
502
+ },
503
+ {
504
+ kind: "component",
505
+ type: LucideLogOut,
506
+ selector: "svg[lucideLogOut]"
529
507
  }
530
508
  ]
531
509
  });
@@ -543,7 +521,8 @@ i0.ɵɵngDeclareClassMetadata({
543
521
  RouterLink,
544
522
  RouterLinkActive,
545
523
  ThemeToggle,
546
- Button
524
+ Button,
525
+ LucideLogOut
547
526
  ],
548
527
  template: `
549
528
  <!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
@@ -588,14 +567,6 @@ i0.ɵɵngDeclareClassMetadata({
588
567
  <svg lucideLogOut size="16" />
589
568
  Cerrar sesión
590
569
  </button>
591
- <button
592
- type="button"
593
- class="w-full rounded-lg px-3 py-2 text-left text-sm text-neutral hover:bg-neutral/10 disabled:opacity-50 mt-2"
594
- [disabled]="closingAllSessions()"
595
- (click)="closeAllSessions()"
596
- >
597
- {{ closingAllSessions() ? 'Cerrando sesiones…' : 'Cerrar todas las sesiones' }}
598
- </button>
599
570
  </div>
600
571
  }
601
572
  </aside>
@@ -1038,19 +1009,46 @@ i0.ɵɵngDeclareClassMetadata({
1038
1009
  }]
1039
1010
  }]
1040
1011
  });
1012
+ const MOBILE_SPANS = {
1013
+ 1: "col-span-1",
1014
+ 2: "col-span-2",
1015
+ 3: "col-span-3",
1016
+ 4: "col-span-4",
1017
+ 5: "col-span-5",
1018
+ 6: "col-span-6",
1019
+ 7: "col-span-7",
1020
+ 8: "col-span-8",
1021
+ 9: "col-span-9",
1022
+ 10: "col-span-10",
1023
+ 11: "col-span-11",
1024
+ 12: "col-span-12"
1025
+ };
1026
+ const RESPONSIVE_SPANS = {
1027
+ 12: "",
1028
+ 11: "lg:col-span-11",
1029
+ 10: "lg:col-span-10",
1030
+ 9: "lg:col-span-9",
1031
+ 8: "lg:col-span-8",
1032
+ 7: "sm:col-span-6 lg:col-span-7",
1033
+ 6: "sm:col-span-6",
1034
+ 5: "sm:col-span-6 lg:col-span-5",
1035
+ 4: "sm:col-span-6 lg:col-span-4",
1036
+ 3: "sm:col-span-6 lg:col-span-3",
1037
+ 2: "sm:col-span-6 lg:col-span-2",
1038
+ 1: "sm:col-span-6 lg:col-span-1"
1039
+ };
1041
1040
  var WidgetHost = class WidgetHost {
1042
1041
  widget = input.required(...ngDevMode ? [{ debugName: "widget" }] : /* istanbul ignore next */ []);
1043
1042
  data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
1044
1043
  loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
1045
1044
  gridSpanClass = computed(() => {
1046
- switch (this.widget().width) {
1047
- case 12: return "col-span-12 block";
1048
- case 8: return "col-span-12 lg:col-span-8 block";
1049
- case 6: return "col-span-12 sm:col-span-6 block";
1050
- case 4: return "col-span-12 sm:col-span-6 lg:col-span-4 block";
1051
- case 3: return "col-span-12 sm:col-span-6 lg:col-span-3 block";
1052
- default: return "col-span-12 sm:col-span-6 lg:col-span-4 block";
1053
- }
1045
+ const w = this.widget().width ?? 4;
1046
+ const mw = this.widget().mobileWidth ?? 12;
1047
+ return [
1048
+ MOBILE_SPANS[mw] ?? "col-span-12",
1049
+ RESPONSIVE_SPANS[w] ?? "sm:col-span-6 lg:col-span-4",
1050
+ "block"
1051
+ ].filter(Boolean).join(" ");
1054
1052
  }, ...ngDevMode ? [{ debugName: "gridSpanClass" }] : /* istanbul ignore next */ []);
1055
1053
  static ɵfac = i0.ɵɵngDeclareFactory({
1056
1054
  minVersion: "12.0.0",
@@ -4499,6 +4497,6 @@ i0.ɵɵngDeclareClassMetadata({
4499
4497
  }]
4500
4498
  }
4501
4499
  });
4502
- export { API_DRIVER, 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, VALUE_SOURCES, 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 };
4500
+ export { API_DRIVER, API_URL, ApiError, ApiService, AuthService, COLUMN_FORMAT_TYPES, ComposerHost, ConfirmDialog, DEFAULT_RESPONSE_UNWRAP, 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, VALUE_SOURCES, 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 };
4503
4501
 
4504
4502
  //# sourceMappingURL=tinya-code-gas-admin-ui.mjs.map