@entifix/react-controls 0.5.0 → 0.6.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 (41) hide show
  1. package/dist/i18n/catalog/es.d.ts +1 -1
  2. package/dist/i18n/catalog/es.js +1 -1
  3. package/dist/i18n/catalog/es.js.map +1 -1
  4. package/dist/preferences/use-viewport-mode.d.ts +0 -1
  5. package/dist/preferences/use-viewport-mode.d.ts.map +1 -1
  6. package/dist/preferences/use-viewport-mode.js +0 -1
  7. package/dist/preferences/use-viewport-mode.js.map +1 -1
  8. package/dist/ui/actions/action-surfaces.d.ts +3 -3
  9. package/dist/ui/actions/action-surfaces.js +4 -4
  10. package/dist/ui/actions/action-surfaces.js.map +1 -1
  11. package/dist/ui/atoms/field-control/field-control.d.ts +1 -1
  12. package/dist/ui/atoms/field-control/field-control.js +1 -1
  13. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  14. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts +2 -2
  15. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
  16. package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
  17. package/dist/ui/molecules/loading-boundary/loading-boundary.d.ts +1 -1
  18. package/dist/ui/molecules/loading-boundary/loading-boundary.js +1 -1
  19. package/dist/ui/molecules/loading-boundary/loading-boundary.js.map +1 -1
  20. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts +2 -2
  21. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +2 -2
  22. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  23. package/dist/ui/organisms/entity-form/entity-form.js +4 -4
  24. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  25. package/dist/ui/organisms/entity-form/entity-form.types.d.ts +2 -2
  26. package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
  27. package/dist/ui/organisms/entity-table/entity-table.js +2 -2
  28. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  29. package/dist/ui/organisms/wizard/wizard.d.ts +1 -2
  30. package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
  31. package/dist/ui/organisms/wizard/wizard.js +2 -3
  32. package/dist/ui/organisms/wizard/wizard.js.map +1 -1
  33. package/dist/ui/organisms/wizard/wizard.types.d.ts +2 -2
  34. package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
  35. package/dist/ui/utils/active-section/active-section.d.ts +1 -1
  36. package/dist/ui/utils/active-section/active-section.js +1 -1
  37. package/dist/ui/utils/active-section/active-section.js.map +1 -1
  38. package/dist/ui/utils/keep-section/keep-section.d.ts +1 -1
  39. package/dist/ui/utils/keep-section/keep-section.js +1 -1
  40. package/dist/ui/utils/keep-section/keep-section.js.map +1 -1
  41. package/package.json +5 -5
@@ -250,7 +250,7 @@ export declare const controls: {
250
250
  finish: string;
251
251
  /**
252
252
  * Un envío que **no** resuelve. El asistente entrega el trabajo y vuelve
253
- * antes de que la escritura termine (ADR 0043), así que dice "enviando" y
253
+ * antes de que la escritura termine, así que dice "enviando" y
254
254
  * nunca "guardado": lo segundo sería una afirmación que el control no puede
255
255
  * comprobar.
256
256
  */
@@ -238,7 +238,7 @@
238
238
  finish: 'Finalizar',
239
239
  /**
240
240
  * Un envío que **no** resuelve. El asistente entrega el trabajo y vuelve
241
- * antes de que la escritura termine (ADR 0043), así que dice "enviando" y
241
+ * antes de que la escritura termine, así que dice "enviando" y
242
242
  * nunca "guardado": lo segundo sería una afirmación que el control no puede
243
243
  * comprobar.
244
244
  */ submitting: 'Enviando…',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/catalog/es.ts"],"sourcesContent":["/**\n * Copy owned by `@entifix/react-controls` — the agnostic entity UI. It lives\n * here rather than next to the components because the typed-key augmentation has\n * to see every namespace from one module, and `@entifix/tooling` (T0) may not\n * import a T3 package.\n */\nexport const controls = {\n table: {\n open: 'Abrir',\n select: 'Seleccionar',\n actions: 'Acciones',\n new: 'Nuevo',\n filters: 'Filtros',\n sorting: 'Orden',\n loading: 'Cargando…',\n empty: 'Sin registros',\n error: 'No se pudieron cargar los registros',\n /**\n * La columna de selección. `selectRow` nombra la fila porque una casilla\n * sin etiqueta visible sólo se anuncia por su `aria-label`, y repetir el\n * encabezado en cada fila no diría cuál es.\n */\n selection: 'Selección',\n selectRow: 'Seleccionar {{record}}',\n selectAllOnPage: 'Seleccionar todo en esta página',\n /**\n * La segunda selección — la que el servidor evalúa — lleva el número\n * siempre: es la diferencia entre actuar sobre 25 filas y sobre 3.200.\n */\n selectAllMatching_one: 'Seleccionar {{count}} coincidencia',\n selectAllMatching_other: 'Seleccionar las {{count}} coincidencias',\n selectedCount_one: '{{count}} seleccionado',\n selectedCount_other: '{{count}} seleccionados',\n clearSelection: 'Limpiar selección',\n },\n /**\n * Una acción sobre una selección. El resultado es por fila, nunca un aviso\n * único: 40 seleccionadas y 3 fallidas no es ni un éxito ni un error.\n */\n bulk: {\n running: 'Ejecutando…',\n succeeded_one: '{{count}} registro actualizado',\n succeeded_other: '{{count}} registros actualizados',\n failed_one: '{{count}} registro falló',\n failed_other: '{{count}} registros fallaron',\n retryFailed: 'Reintentar los fallidos',\n dismiss: 'Cerrar resultado',\n resultLabel: 'Resultado de la acción masiva',\n barLabel: 'Acciones sobre la selección',\n },\n form: {\n details: 'Detalles',\n new: 'Nuevo',\n view: 'Ver',\n edit: 'Editar',\n save: 'Guardar',\n saving: 'Guardando…',\n delete: 'Eliminar',\n deleting: 'Eliminando…',\n clone: 'Duplicar',\n moreActions: 'Más acciones',\n back: 'Volver',\n loading: 'Cargando…',\n },\n pagination: {\n summary: 'Página {{current}} de {{total}} · {{items}} registros',\n rows: 'Filas',\n previous: 'Anterior',\n next: 'Siguiente',\n },\n columns: {\n title: 'Columnas',\n subtitle: 'Visibilidad y orden',\n reset: 'Restablecer',\n moveUp: 'Subir {{column}}',\n moveDown: 'Bajar {{column}}',\n },\n filter: {\n match: 'Coincidir',\n all: 'todos',\n any: 'cualquiera',\n add: 'Añadir filtro',\n apply: 'Aplicar',\n clear: 'Limpiar',\n none: 'Esta entidad no tiene miembros filtrables.',\n listPlaceholder: 'valores, separados, por comas',\n matchAria: 'Coincidir todos o cualquiera de los filtros',\n member: 'Miembro del filtro',\n operator: 'Operador del filtro',\n value: 'Valor del filtro',\n rangeEnd: 'Fin del rango del filtro',\n remove: 'Quitar filtro',\n applyAria: 'Aplicar filtros',\n clearAria: 'Limpiar filtros',\n },\n sort: {\n by: 'Ordenar por',\n then: 'después por',\n ascending: 'ascendente',\n descending: 'descendente',\n add: 'Añadir orden',\n apply: 'Aplicar',\n clear: 'Limpiar',\n none: 'Esta entidad no tiene miembros ordenables.',\n member: 'Miembro de orden',\n direction: 'Dirección de orden',\n raisePriority: 'Subir prioridad de orden',\n lowerPriority: 'Bajar prioridad de orden',\n remove: 'Quitar orden',\n applyAria: 'Aplicar orden',\n clearAria: 'Limpiar orden',\n },\n tabs: {\n close: 'Cerrar {{label}}',\n new: 'Abrir una pestaña nueva',\n },\n /**\n * El editor de una relación. `search` es la búsqueda rápida y `browse` abre el\n * catálogo completo; ambos botones son iconos, así que sus etiquetas `aria` son\n * lo único que un lector de pantalla anuncia.\n */\n link: {\n search: 'Buscar {{field}}',\n suggestAria: 'Ver sugerencias de {{field}}',\n browseAria: 'Examinar {{field}}',\n browseTitle: 'Seleccionar {{field}}',\n clear: 'Quitar {{field}}',\n empty: '— sin asignar —',\n loading: 'Cargando…',\n noResults: 'Sin coincidencias',\n error: 'No se pudieron cargar las opciones',\n close: 'Cerrar',\n },\n /**\n * La rejilla de detalle: las filas que un registro **posee** — las líneas de\n * un pedido, los renglones de una factura. `errorSummary` es el único\n * `role=\"alert\"` del control: un aviso por celda anunciaría la rejilla entera\n * en cada pulsación, y un mensaje que sólo vive en la celda es inalcanzable\n * cuando la fila que falla está fuera de pantalla.\n */\n detail: {\n addRow: 'Añadir fila',\n removeRow: 'Quitar fila {{row}}',\n rowActions: 'Acciones de fila',\n empty: 'Todavía no hay filas.',\n errorSummary_one: '{{count}} celda con errores',\n errorSummary_other: '{{count}} celdas con errores',\n },\n value: {\n none: '— ninguno —',\n yes: 'Sí',\n no: 'No',\n /**\n * Una `composition` en una tabla: sus filas se editan en la pantalla del\n * maestro, no en la celda, así que la celda dice cuántas hay.\n */\n rowCount_one: '{{count}} fila',\n rowCount_other: '{{count}} filas',\n },\n /**\n * `required`…`option` son las reglas derivadas de la metadata. El resto es el\n * vocabulario en el que un Standard Schema de una entidad escribe sus\n * mensajes: el esquema lleva la *clave*, nunca una frase, para que la regla\n * siga siendo traducible.\n *\n * `field` es el único parámetro disponible — un issue de Standard Schema\n * expone un mensaje y una ruta, nada más — así que estos mensajes no indican\n * el límite. Una regla que deba enunciarlo escribe su propia clave en el\n * catálogo de su dominio (`entity:product-specification.validation.codeLength`).\n */\n validation: {\n required: '{{field}} es obligatorio',\n number: '{{field}} debe ser un número',\n date: '{{field}} debe ser una fecha',\n option: '{{field}} no es una opción válida',\n minLength: '{{field}} es demasiado corto',\n maxLength: '{{field}} es demasiado largo',\n min: '{{field}} es demasiado pequeño',\n max: '{{field}} es demasiado grande',\n pattern: '{{field}} tiene un formato inválido',\n minItems_one: '{{field}} necesita al menos {{count}} elemento',\n minItems_other: '{{field}} necesita al menos {{count}} elementos',\n maxItems_one: '{{field}} admite como máximo {{count}} elemento',\n maxItems_other: '{{field}} admite como máximo {{count}} elementos',\n notBefore: '{{field}} no puede ser anterior a {{other}}',\n locale: '{{field}} no tiene texto en {{locale}}',\n },\n /**\n * The filter vocabulary shown to a user. Deliberately distinct from\n * `RSQL_TOKENS` in `@entifix/core` — those are wire tokens (`==`, `=gt=`)\n * and must never be translated.\n */\n operator: {\n eq: 'es',\n ne: 'no es',\n gt: 'mayor que',\n gte: 'mayor o igual',\n lt: 'menor que',\n lte: 'menor o igual',\n in: 'es uno de',\n nin: 'no es ninguno de',\n between: 'entre',\n nbetween: 'fuera de',\n like: 'contiene',\n nlike: 'no contiene',\n isNull: 'está vacío',\n isNotNull: 'no está vacío',\n },\n theme: {\n label: 'Tema',\n },\n /**\n * Captions for the theme presets shipped by `@entifix/style`, plus the\n * per-app brand sets. They name design-system tokens, not app copy, so they\n * live beside the switcher that renders them and stay reachable from\n * Storybook, where no shell is mounted.\n */\n themes: {\n aurora: 'Aurora',\n sunset: 'Atardecer',\n midnight: 'Medianoche',\n ocean: 'Océano (en runtime)',\n marketplace: 'Marketplace',\n marketplaceDark: 'Marketplace oscuro',\n auth: 'Identidad',\n authDark: 'Identidad oscura',\n },\n confirm: {\n confirm: 'Confirmar',\n cancel: 'Cancelar',\n title: 'Confirmar',\n },\n breadcrumbs: {\n label: 'Ruta de navegación',\n },\n /**\n * El asistente. Las etiquetas de cada paso las traduce quien declara el\n * asistente, no el control: un paso es del dominio que lo definió, y su copy\n * vive en `shell:` junto al resto de sus nombres.\n */\n wizard: {\n steps: 'Pasos',\n previous: 'Atrás',\n /**\n * \"Continuar\" y no \"Siguiente\": una tabla dentro de un paso trae su propio\n * paginador, cuyo botón **es** \"Siguiente\". Dos controles con el mismo\n * nombre en la misma pantalla se anuncian igual y hacen cosas distintas.\n */\n next: 'Continuar',\n finish: 'Finalizar',\n /**\n * Un envío que **no** resuelve. El asistente entrega el trabajo y vuelve\n * antes de que la escritura termine (ADR 0043), así que dice \"enviando\" y\n * nunca \"guardado\": lo segundo sería una afirmación que el control no puede\n * comprobar.\n */\n submitting: 'Enviando…',\n stepOf: 'Paso {{current}} de {{total}}',\n goToStep: 'Volver a {{label}}',\n /**\n * El estado de cada paso, anunciado además de mostrarse. El color por sí\n * solo no lo dice, y un lector de pantalla recorre la lista sin verlo.\n */\n status: {\n pending: 'pendiente',\n active: 'en curso',\n complete: 'completado',\n error: 'con errores',\n },\n /**\n * El recordatorio al retomar. La persistencia da el \"continuar\" gratis; lo\n * que no da es acordarse de lo ya decidido, que es la falla conocida de los\n * asistentes largos.\n */\n resume: {\n title: 'Retomando donde lo dejaste',\n dismiss: 'Entendido',\n },\n },\n};\n"],"names":["controls","table","open","select","actions","new","filters","sorting","loading","empty","error","selection","selectRow","selectAllOnPage","selectAllMatching_one","selectAllMatching_other","selectedCount_one","selectedCount_other","clearSelection","bulk","running","succeeded_one","succeeded_other","failed_one","failed_other","retryFailed","dismiss","resultLabel","barLabel","form","details","view","edit","save","saving","delete","deleting","clone","moreActions","back","pagination","summary","rows","previous","next","columns","title","subtitle","reset","moveUp","moveDown","filter","match","all","any","add","apply","clear","none","listPlaceholder","matchAria","member","operator","value","rangeEnd","remove","applyAria","clearAria","sort","by","then","ascending","descending","direction","raisePriority","lowerPriority","tabs","close","link","search","suggestAria","browseAria","browseTitle","noResults","detail","addRow","removeRow","rowActions","errorSummary_one","errorSummary_other","yes","no","rowCount_one","rowCount_other","validation","required","number","date","option","minLength","maxLength","min","max","pattern","minItems_one","minItems_other","maxItems_one","maxItems_other","notBefore","locale","eq","ne","gt","gte","lt","lte","in","nin","between","nbetween","like","nlike","isNull","isNotNull","theme","label","themes","aurora","sunset","midnight","ocean","marketplace","marketplaceDark","auth","authDark","confirm","cancel","breadcrumbs","wizard","steps","finish","submitting","stepOf","goToStep","status","pending","active","complete","resume"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/i18n/catalog/es.ts"],"sourcesContent":["/**\n * Copy owned by `@entifix/react-controls` — the agnostic entity UI. It lives\n * here rather than next to the components because the typed-key augmentation has\n * to see every namespace from one module, and `@entifix/tooling` (T0) may not\n * import a T3 package.\n */\nexport const controls = {\n table: {\n open: 'Abrir',\n select: 'Seleccionar',\n actions: 'Acciones',\n new: 'Nuevo',\n filters: 'Filtros',\n sorting: 'Orden',\n loading: 'Cargando…',\n empty: 'Sin registros',\n error: 'No se pudieron cargar los registros',\n /**\n * La columna de selección. `selectRow` nombra la fila porque una casilla\n * sin etiqueta visible sólo se anuncia por su `aria-label`, y repetir el\n * encabezado en cada fila no diría cuál es.\n */\n selection: 'Selección',\n selectRow: 'Seleccionar {{record}}',\n selectAllOnPage: 'Seleccionar todo en esta página',\n /**\n * La segunda selección — la que el servidor evalúa — lleva el número\n * siempre: es la diferencia entre actuar sobre 25 filas y sobre 3.200.\n */\n selectAllMatching_one: 'Seleccionar {{count}} coincidencia',\n selectAllMatching_other: 'Seleccionar las {{count}} coincidencias',\n selectedCount_one: '{{count}} seleccionado',\n selectedCount_other: '{{count}} seleccionados',\n clearSelection: 'Limpiar selección',\n },\n /**\n * Una acción sobre una selección. El resultado es por fila, nunca un aviso\n * único: 40 seleccionadas y 3 fallidas no es ni un éxito ni un error.\n */\n bulk: {\n running: 'Ejecutando…',\n succeeded_one: '{{count}} registro actualizado',\n succeeded_other: '{{count}} registros actualizados',\n failed_one: '{{count}} registro falló',\n failed_other: '{{count}} registros fallaron',\n retryFailed: 'Reintentar los fallidos',\n dismiss: 'Cerrar resultado',\n resultLabel: 'Resultado de la acción masiva',\n barLabel: 'Acciones sobre la selección',\n },\n form: {\n details: 'Detalles',\n new: 'Nuevo',\n view: 'Ver',\n edit: 'Editar',\n save: 'Guardar',\n saving: 'Guardando…',\n delete: 'Eliminar',\n deleting: 'Eliminando…',\n clone: 'Duplicar',\n moreActions: 'Más acciones',\n back: 'Volver',\n loading: 'Cargando…',\n },\n pagination: {\n summary: 'Página {{current}} de {{total}} · {{items}} registros',\n rows: 'Filas',\n previous: 'Anterior',\n next: 'Siguiente',\n },\n columns: {\n title: 'Columnas',\n subtitle: 'Visibilidad y orden',\n reset: 'Restablecer',\n moveUp: 'Subir {{column}}',\n moveDown: 'Bajar {{column}}',\n },\n filter: {\n match: 'Coincidir',\n all: 'todos',\n any: 'cualquiera',\n add: 'Añadir filtro',\n apply: 'Aplicar',\n clear: 'Limpiar',\n none: 'Esta entidad no tiene miembros filtrables.',\n listPlaceholder: 'valores, separados, por comas',\n matchAria: 'Coincidir todos o cualquiera de los filtros',\n member: 'Miembro del filtro',\n operator: 'Operador del filtro',\n value: 'Valor del filtro',\n rangeEnd: 'Fin del rango del filtro',\n remove: 'Quitar filtro',\n applyAria: 'Aplicar filtros',\n clearAria: 'Limpiar filtros',\n },\n sort: {\n by: 'Ordenar por',\n then: 'después por',\n ascending: 'ascendente',\n descending: 'descendente',\n add: 'Añadir orden',\n apply: 'Aplicar',\n clear: 'Limpiar',\n none: 'Esta entidad no tiene miembros ordenables.',\n member: 'Miembro de orden',\n direction: 'Dirección de orden',\n raisePriority: 'Subir prioridad de orden',\n lowerPriority: 'Bajar prioridad de orden',\n remove: 'Quitar orden',\n applyAria: 'Aplicar orden',\n clearAria: 'Limpiar orden',\n },\n tabs: {\n close: 'Cerrar {{label}}',\n new: 'Abrir una pestaña nueva',\n },\n /**\n * El editor de una relación. `search` es la búsqueda rápida y `browse` abre el\n * catálogo completo; ambos botones son iconos, así que sus etiquetas `aria` son\n * lo único que un lector de pantalla anuncia.\n */\n link: {\n search: 'Buscar {{field}}',\n suggestAria: 'Ver sugerencias de {{field}}',\n browseAria: 'Examinar {{field}}',\n browseTitle: 'Seleccionar {{field}}',\n clear: 'Quitar {{field}}',\n empty: '— sin asignar —',\n loading: 'Cargando…',\n noResults: 'Sin coincidencias',\n error: 'No se pudieron cargar las opciones',\n close: 'Cerrar',\n },\n /**\n * La rejilla de detalle: las filas que un registro **posee** — las líneas de\n * un pedido, los renglones de una factura. `errorSummary` es el único\n * `role=\"alert\"` del control: un aviso por celda anunciaría la rejilla entera\n * en cada pulsación, y un mensaje que sólo vive en la celda es inalcanzable\n * cuando la fila que falla está fuera de pantalla.\n */\n detail: {\n addRow: 'Añadir fila',\n removeRow: 'Quitar fila {{row}}',\n rowActions: 'Acciones de fila',\n empty: 'Todavía no hay filas.',\n errorSummary_one: '{{count}} celda con errores',\n errorSummary_other: '{{count}} celdas con errores',\n },\n value: {\n none: '— ninguno —',\n yes: 'Sí',\n no: 'No',\n /**\n * Una `composition` en una tabla: sus filas se editan en la pantalla del\n * maestro, no en la celda, así que la celda dice cuántas hay.\n */\n rowCount_one: '{{count}} fila',\n rowCount_other: '{{count}} filas',\n },\n /**\n * `required`…`option` son las reglas derivadas de la metadata. El resto es el\n * vocabulario en el que un Standard Schema de una entidad escribe sus\n * mensajes: el esquema lleva la *clave*, nunca una frase, para que la regla\n * siga siendo traducible.\n *\n * `field` es el único parámetro disponible — un issue de Standard Schema\n * expone un mensaje y una ruta, nada más — así que estos mensajes no indican\n * el límite. Una regla que deba enunciarlo escribe su propia clave en el\n * catálogo de su dominio (`entity:product-specification.validation.codeLength`).\n */\n validation: {\n required: '{{field}} es obligatorio',\n number: '{{field}} debe ser un número',\n date: '{{field}} debe ser una fecha',\n option: '{{field}} no es una opción válida',\n minLength: '{{field}} es demasiado corto',\n maxLength: '{{field}} es demasiado largo',\n min: '{{field}} es demasiado pequeño',\n max: '{{field}} es demasiado grande',\n pattern: '{{field}} tiene un formato inválido',\n minItems_one: '{{field}} necesita al menos {{count}} elemento',\n minItems_other: '{{field}} necesita al menos {{count}} elementos',\n maxItems_one: '{{field}} admite como máximo {{count}} elemento',\n maxItems_other: '{{field}} admite como máximo {{count}} elementos',\n notBefore: '{{field}} no puede ser anterior a {{other}}',\n locale: '{{field}} no tiene texto en {{locale}}',\n },\n /**\n * The filter vocabulary shown to a user. Deliberately distinct from\n * `RSQL_TOKENS` in `@entifix/core` — those are wire tokens (`==`, `=gt=`)\n * and must never be translated.\n */\n operator: {\n eq: 'es',\n ne: 'no es',\n gt: 'mayor que',\n gte: 'mayor o igual',\n lt: 'menor que',\n lte: 'menor o igual',\n in: 'es uno de',\n nin: 'no es ninguno de',\n between: 'entre',\n nbetween: 'fuera de',\n like: 'contiene',\n nlike: 'no contiene',\n isNull: 'está vacío',\n isNotNull: 'no está vacío',\n },\n theme: {\n label: 'Tema',\n },\n /**\n * Captions for the theme presets shipped by `@entifix/style`, plus the\n * per-app brand sets. They name design-system tokens, not app copy, so they\n * live beside the switcher that renders them and stay reachable from\n * Storybook, where no shell is mounted.\n */\n themes: {\n aurora: 'Aurora',\n sunset: 'Atardecer',\n midnight: 'Medianoche',\n ocean: 'Océano (en runtime)',\n marketplace: 'Marketplace',\n marketplaceDark: 'Marketplace oscuro',\n auth: 'Identidad',\n authDark: 'Identidad oscura',\n },\n confirm: {\n confirm: 'Confirmar',\n cancel: 'Cancelar',\n title: 'Confirmar',\n },\n breadcrumbs: {\n label: 'Ruta de navegación',\n },\n /**\n * El asistente. Las etiquetas de cada paso las traduce quien declara el\n * asistente, no el control: un paso es del dominio que lo definió, y su copy\n * vive en `shell:` junto al resto de sus nombres.\n */\n wizard: {\n steps: 'Pasos',\n previous: 'Atrás',\n /**\n * \"Continuar\" y no \"Siguiente\": una tabla dentro de un paso trae su propio\n * paginador, cuyo botón **es** \"Siguiente\". Dos controles con el mismo\n * nombre en la misma pantalla se anuncian igual y hacen cosas distintas.\n */\n next: 'Continuar',\n finish: 'Finalizar',\n /**\n * Un envío que **no** resuelve. El asistente entrega el trabajo y vuelve\n * antes de que la escritura termine, así que dice \"enviando\" y\n * nunca \"guardado\": lo segundo sería una afirmación que el control no puede\n * comprobar.\n */\n submitting: 'Enviando…',\n stepOf: 'Paso {{current}} de {{total}}',\n goToStep: 'Volver a {{label}}',\n /**\n * El estado de cada paso, anunciado además de mostrarse. El color por sí\n * solo no lo dice, y un lector de pantalla recorre la lista sin verlo.\n */\n status: {\n pending: 'pendiente',\n active: 'en curso',\n complete: 'completado',\n error: 'con errores',\n },\n /**\n * El recordatorio al retomar. La persistencia da el \"continuar\" gratis; lo\n * que no da es acordarse de lo ya decidido, que es la falla conocida de los\n * asistentes largos.\n */\n resume: {\n title: 'Retomando donde lo dejaste',\n dismiss: 'Entendido',\n },\n },\n};\n"],"names":["controls","table","open","select","actions","new","filters","sorting","loading","empty","error","selection","selectRow","selectAllOnPage","selectAllMatching_one","selectAllMatching_other","selectedCount_one","selectedCount_other","clearSelection","bulk","running","succeeded_one","succeeded_other","failed_one","failed_other","retryFailed","dismiss","resultLabel","barLabel","form","details","view","edit","save","saving","delete","deleting","clone","moreActions","back","pagination","summary","rows","previous","next","columns","title","subtitle","reset","moveUp","moveDown","filter","match","all","any","add","apply","clear","none","listPlaceholder","matchAria","member","operator","value","rangeEnd","remove","applyAria","clearAria","sort","by","then","ascending","descending","direction","raisePriority","lowerPriority","tabs","close","link","search","suggestAria","browseAria","browseTitle","noResults","detail","addRow","removeRow","rowActions","errorSummary_one","errorSummary_other","yes","no","rowCount_one","rowCount_other","validation","required","number","date","option","minLength","maxLength","min","max","pattern","minItems_one","minItems_other","maxItems_one","maxItems_other","notBefore","locale","eq","ne","gt","gte","lt","lte","in","nin","between","nbetween","like","nlike","isNull","isNotNull","theme","label","themes","aurora","sunset","midnight","ocean","marketplace","marketplaceDark","auth","authDark","confirm","cancel","breadcrumbs","wizard","steps","finish","submitting","stepOf","goToStep","status","pending","active","complete","resume"],"mappings":"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"}
@@ -16,7 +16,6 @@ export type ViewportMode = 'compact' | 'rail' | 'wide';
16
16
  * with `flex-wrap`/`flex-basis`/`gap`, and `Sidebar` still does. Choosing
17
17
  * between a drawer and a persistent rail is not laying out a box — it is picking
18
18
  * a navigation *mode*, and no amount of intrinsic sizing produces a focus trap.
19
- * See ADR 0041.
20
19
  *
21
20
  * It lives beside the UI preferences rather than in `ui/` because it belongs to
22
21
  * the same decision: what the viewport allows and what the person chose are the
@@ -1 +1 @@
1
- {"version":3,"file":"use-viewport-mode.d.ts","sourceRoot":"","sources":["../../src/preferences/use-viewport-mode.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAUvD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,IAAI,YAAY,CA+B9C;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,UA3DJ,MAAM,KAAG,YA2DK,CAAC"}
1
+ {"version":3,"file":"use-viewport-mode.d.ts","sourceRoot":"","sources":["../../src/preferences/use-viewport-mode.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAUvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,IAAI,YAAY,CA+B9C;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,UA1DJ,MAAM,KAAG,YA0DK,CAAC"}
@@ -15,7 +15,6 @@ const modeFor = (width)=>{
15
15
  * with `flex-wrap`/`flex-basis`/`gap`, and `Sidebar` still does. Choosing
16
16
  * between a drawer and a persistent rail is not laying out a box — it is picking
17
17
  * a navigation *mode*, and no amount of intrinsic sizing produces a focus trap.
18
- * See ADR 0041.
19
18
  *
20
19
  * It lives beside the UI preferences rather than in `ui/` because it belongs to
21
20
  * the same decision: what the viewport allows and what the person chose are the
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/preferences/use-viewport-mode.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\n/**\n * How much room the viewport has for chrome.\n *\n * - `compact` — under 768px. Navigation belongs behind a drawer; there is no\n * room for a persistent rail beside the content.\n * - `rail` — 768px to 1024px. Enough for icons, not for labels.\n * - `wide` — 1024px and up. The stored preference decides.\n */\nexport type ViewportMode = 'compact' | 'rail' | 'wide';\n\nconst RAIL_MIN = 768;\nconst WIDE_MIN = 1024;\n\nconst modeFor = (width: number): ViewportMode => {\n if (width < RAIL_MIN) return 'compact';\n return width < WIDE_MIN ? 'rail' : 'wide';\n};\n\n/**\n * The viewport mode, as a media query the shell may branch on.\n *\n * ⚠️ This is the one place in the design system that asks about the viewport,\n * and it is deliberate. `docs/FRONTEND.md`'s no-media-query rule governs the\n * **layout primitives** in `ui/layout/`: they lay themselves out intrinsically\n * with `flex-wrap`/`flex-basis`/`gap`, and `Sidebar` still does. Choosing\n * between a drawer and a persistent rail is not laying out a box — it is picking\n * a navigation *mode*, and no amount of intrinsic sizing produces a focus trap.\n * See ADR 0041.\n *\n * It lives beside the UI preferences rather than in `ui/` because it belongs to\n * the same decision: what the viewport allows and what the person chose are the\n * two inputs to one answer, and only the second is a preference.\n *\n * Resolved in an effect, not during render, and defaulting to `wide`. The server\n * has no viewport, so any other default would render markup the first client\n * pass immediately contradicts — the same reason `useUiPreference` reads after\n * mount rather than during it.\n */\nexport function useViewportMode(): ViewportMode {\n const [mode, setMode] = useState<ViewportMode>('wide');\n\n useEffect(() => {\n // `typeof`, not `'matchMedia' in window`: jsdom declares the property and\n // leaves it uncallable, so the `in` check passes and the call then throws\n // inside a passive effect — which surfaces as a broken shell, not a missing\n // media query.\n if (\n typeof window === 'undefined' ||\n typeof window.matchMedia !== 'function'\n )\n return;\n\n const compact = window.matchMedia(`(max-width: ${RAIL_MIN - 1}px)`);\n const wide = window.matchMedia(`(min-width: ${WIDE_MIN}px)`);\n\n const sync = () => {\n setMode(compact.matches ? 'compact' : wide.matches ? 'wide' : 'rail');\n };\n\n sync();\n compact.addEventListener('change', sync);\n wide.addEventListener('change', sync);\n return () => {\n compact.removeEventListener('change', sync);\n wide.removeEventListener('change', sync);\n };\n }, []);\n\n return mode;\n}\n\n/** Exported for tests and for callers that already hold a width. */\nexport const viewportModeFor = modeFor;\n"],"names":["useEffect","useState","RAIL_MIN","WIDE_MIN","modeFor","width","useViewportMode","mode","setMode","window","matchMedia","compact","wide","sync","matches","addEventListener","removeEventListener","viewportModeFor"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAY5C,MAAMC,WAAW;AACjB,MAAMC,WAAW;AAEjB,MAAMC,UAAU,CAACC;IACf,IAAIA,QAAQH,UAAU,OAAO;IAC7B,OAAOG,QAAQF,WAAW,SAAS;AACrC;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG;IACd,MAAM,CAACC,MAAMC,QAAQ,GAAGP,SAAuB;IAE/CD,UAAU;QACR,0EAA0E;QAC1E,0EAA0E;QAC1E,4EAA4E;QAC5E,eAAe;QACf,IACE,OAAOS,WAAW,eAClB,OAAOA,OAAOC,UAAU,KAAK,YAE7B;QAEF,MAAMC,UAAUF,OAAOC,UAAU,CAAC,CAAC,YAAY,EAAER,WAAW,EAAE,GAAG,CAAC;QAClE,MAAMU,OAAOH,OAAOC,UAAU,CAAC,CAAC,YAAY,EAAEP,SAAS,GAAG,CAAC;QAE3D,MAAMU,OAAO;YACXL,QAAQG,QAAQG,OAAO,GAAG,YAAYF,KAAKE,OAAO,GAAG,SAAS;QAChE;QAEAD;QACAF,QAAQI,gBAAgB,CAAC,UAAUF;QACnCD,KAAKG,gBAAgB,CAAC,UAAUF;QAChC,OAAO;YACLF,QAAQK,mBAAmB,CAAC,UAAUH;YACtCD,KAAKI,mBAAmB,CAAC,UAAUH;QACrC;IACF,GAAG,EAAE;IAEL,OAAON;AACT;AAEA,kEAAkE,GAClE,OAAO,MAAMU,kBAAkBb,QAAQ"}
1
+ {"version":3,"sources":["../../src/preferences/use-viewport-mode.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\n/**\n * How much room the viewport has for chrome.\n *\n * - `compact` — under 768px. Navigation belongs behind a drawer; there is no\n * room for a persistent rail beside the content.\n * - `rail` — 768px to 1024px. Enough for icons, not for labels.\n * - `wide` — 1024px and up. The stored preference decides.\n */\nexport type ViewportMode = 'compact' | 'rail' | 'wide';\n\nconst RAIL_MIN = 768;\nconst WIDE_MIN = 1024;\n\nconst modeFor = (width: number): ViewportMode => {\n if (width < RAIL_MIN) return 'compact';\n return width < WIDE_MIN ? 'rail' : 'wide';\n};\n\n/**\n * The viewport mode, as a media query the shell may branch on.\n *\n * ⚠️ This is the one place in the design system that asks about the viewport,\n * and it is deliberate. `docs/FRONTEND.md`'s no-media-query rule governs the\n * **layout primitives** in `ui/layout/`: they lay themselves out intrinsically\n * with `flex-wrap`/`flex-basis`/`gap`, and `Sidebar` still does. Choosing\n * between a drawer and a persistent rail is not laying out a box — it is picking\n * a navigation *mode*, and no amount of intrinsic sizing produces a focus trap.\n *\n * It lives beside the UI preferences rather than in `ui/` because it belongs to\n * the same decision: what the viewport allows and what the person chose are the\n * two inputs to one answer, and only the second is a preference.\n *\n * Resolved in an effect, not during render, and defaulting to `wide`. The server\n * has no viewport, so any other default would render markup the first client\n * pass immediately contradicts — the same reason `useUiPreference` reads after\n * mount rather than during it.\n */\nexport function useViewportMode(): ViewportMode {\n const [mode, setMode] = useState<ViewportMode>('wide');\n\n useEffect(() => {\n // `typeof`, not `'matchMedia' in window`: jsdom declares the property and\n // leaves it uncallable, so the `in` check passes and the call then throws\n // inside a passive effect — which surfaces as a broken shell, not a missing\n // media query.\n if (\n typeof window === 'undefined' ||\n typeof window.matchMedia !== 'function'\n )\n return;\n\n const compact = window.matchMedia(`(max-width: ${RAIL_MIN - 1}px)`);\n const wide = window.matchMedia(`(min-width: ${WIDE_MIN}px)`);\n\n const sync = () => {\n setMode(compact.matches ? 'compact' : wide.matches ? 'wide' : 'rail');\n };\n\n sync();\n compact.addEventListener('change', sync);\n wide.addEventListener('change', sync);\n return () => {\n compact.removeEventListener('change', sync);\n wide.removeEventListener('change', sync);\n };\n }, []);\n\n return mode;\n}\n\n/** Exported for tests and for callers that already hold a width. */\nexport const viewportModeFor = modeFor;\n"],"names":["useEffect","useState","RAIL_MIN","WIDE_MIN","modeFor","width","useViewportMode","mode","setMode","window","matchMedia","compact","wide","sync","matches","addEventListener","removeEventListener","viewportModeFor"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAY5C,MAAMC,WAAW;AACjB,MAAMC,WAAW;AAEjB,MAAMC,UAAU,CAACC;IACf,IAAIA,QAAQH,UAAU,OAAO;IAC7B,OAAOG,QAAQF,WAAW,SAAS;AACrC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASG;IACd,MAAM,CAACC,MAAMC,QAAQ,GAAGP,SAAuB;IAE/CD,UAAU;QACR,0EAA0E;QAC1E,0EAA0E;QAC1E,4EAA4E;QAC5E,eAAe;QACf,IACE,OAAOS,WAAW,eAClB,OAAOA,OAAOC,UAAU,KAAK,YAE7B;QAEF,MAAMC,UAAUF,OAAOC,UAAU,CAAC,CAAC,YAAY,EAAER,WAAW,EAAE,GAAG,CAAC;QAClE,MAAMU,OAAOH,OAAOC,UAAU,CAAC,CAAC,YAAY,EAAEP,SAAS,GAAG,CAAC;QAE3D,MAAMU,OAAO;YACXL,QAAQG,QAAQG,OAAO,GAAG,YAAYF,KAAKE,OAAO,GAAG,SAAS;QAChE;QAEAD;QACAF,QAAQI,gBAAgB,CAAC,UAAUF;QACnCD,KAAKG,gBAAgB,CAAC,UAAUF;QAChC,OAAO;YACLF,QAAQK,mBAAmB,CAAC,UAAUH;YACtCD,KAAKI,mBAAmB,CAAC,UAAUH;QACrC;IACF,GAAG,EAAE;IAEL,OAAON;AACT;AAEA,kEAAkE,GAClE,OAAO,MAAMU,kBAAkBb,QAAQ"}
@@ -2,13 +2,13 @@ import { type UseCaseDescriptor } from '@entifix/core';
2
2
  /**
3
3
  * Where a declared verb is rendered.
4
4
  *
5
- * ADR 0026 gave a use case two independent axes and then built only one
5
+ * The use-case descriptor has two independent axes, and at first only one
6
6
  * surface, so four of the nine cells had nowhere to go: `EntityForm` filtered
7
7
  * `binding === 'entity' && placement !== 'context-dependent'` and everything
8
8
  * else was dropped without a word. This is the map that closes it —
9
9
  * **placement decides the surface, binding decides the payload** — and it lives
10
10
  * in one module so no surface re-derives it and the two axes cannot drift
11
- * apart again ([ADR 0035](../../../../../../docs/adr/0035-entity-actions-selection-and-bulk.md)).
11
+ * apart again.
12
12
  */
13
13
  export declare const ACTION_SURFACES: readonly ["form-header", "form-footer", "row-menu", "bulk-bar", "table-toolbar", "command-palette"];
14
14
  export type ActionSurface = (typeof ACTION_SURFACES)[number];
@@ -27,7 +27,7 @@ export declare const OVERFLOW_GLYPH = "\u22EF";
27
27
  *
28
28
  * ⚠️ **A verb may name more than one cell**, and that is what makes publishing
29
29
  * reachable from a form header, a row menu and a selection without becoming
30
- * three permissions (#216). The primary `binding` + `placement` come first, then
30
+ * three permissions. The primary `binding` + `placement` come first, then
31
31
  * whatever `alsoAt` adds, and each is validated the same way — an illegal cell
32
32
  * in `alsoAt` throws exactly as one in the primary pair does.
33
33
  *
@@ -2,13 +2,13 @@ import { EntifixLogicError } from '@entifix/core';
2
2
  /**
3
3
  * Where a declared verb is rendered.
4
4
  *
5
- * ADR 0026 gave a use case two independent axes and then built only one
5
+ * The use-case descriptor has two independent axes, and at first only one
6
6
  * surface, so four of the nine cells had nowhere to go: `EntityForm` filtered
7
7
  * `binding === 'entity' && placement !== 'context-dependent'` and everything
8
8
  * else was dropped without a word. This is the map that closes it —
9
9
  * **placement decides the surface, binding decides the payload** — and it lives
10
10
  * in one module so no surface re-derives it and the two axes cannot drift
11
- * apart again ([ADR 0035](../../../../../../docs/adr/0035-entity-actions-selection-and-bulk.md)).
11
+ * apart again.
12
12
  */ export const ACTION_SURFACES = [
13
13
  'form-header',
14
14
  'form-footer',
@@ -25,7 +25,7 @@ import { EntifixLogicError } from '@entifix/core';
25
25
  * page* — it is the footer that ends an object page — and a list screen has no
26
26
  * such thing to finalize, which is Fiori's own rule. Dropping it silently is
27
27
  * the fault this repo already writes an assertion against elsewhere: the verb
28
- * would be declared, granted, exported, pass every `@r10c/slices` invariant,
28
+ * would be declared, granted, exported, pass every check a register runs,
29
29
  * and simply never appear, which reads to its author as "the permission is
30
30
  * wrong" and sends them looking in the wrong place entirely.
31
31
  */ const SURFACE_BY_CELL = {
@@ -63,7 +63,7 @@ import { EntifixLogicError } from '@entifix/core';
63
63
  *
64
64
  * ⚠️ **A verb may name more than one cell**, and that is what makes publishing
65
65
  * reachable from a form header, a row menu and a selection without becoming
66
- * three permissions (#216). The primary `binding` + `placement` come first, then
66
+ * three permissions. The primary `binding` + `placement` come first, then
67
67
  * whatever `alsoAt` adds, and each is validated the same way — an illegal cell
68
68
  * in `alsoAt` throws exactly as one in the primary pair does.
69
69
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/actions/action-surfaces.ts"],"sourcesContent":["import {\n EntifixLogicError,\n type UseCaseBinding,\n type UseCaseDescriptor,\n type UseCasePlacement,\n} from '@entifix/core';\n\n/**\n * Where a declared verb is rendered.\n *\n * ADR 0026 gave a use case two independent axes and then built only one\n * surface, so four of the nine cells had nowhere to go: `EntityForm` filtered\n * `binding === 'entity' && placement !== 'context-dependent'` and everything\n * else was dropped without a word. This is the map that closes it —\n * **placement decides the surface, binding decides the payload** — and it lives\n * in one module so no surface re-derives it and the two axes cannot drift\n * apart again ([ADR 0035](../../../../../../docs/adr/0035-entity-actions-selection-and-bulk.md)).\n */\nexport const ACTION_SURFACES = [\n 'form-header',\n 'form-footer',\n 'row-menu',\n 'bulk-bar',\n 'table-toolbar',\n 'command-palette',\n] as const;\nexport type ActionSurface = (typeof ACTION_SURFACES)[number];\n\n/**\n * The nine cells, exhaustively.\n *\n * `collection` + `determining` is deliberately absent, and asking for it\n * throws rather than returning `undefined`. A determining action *finalizes a\n * page* — it is the footer that ends an object page — and a list screen has no\n * such thing to finalize, which is Fiori's own rule. Dropping it silently is\n * the fault this repo already writes an assertion against elsewhere: the verb\n * would be declared, granted, exported, pass every `@r10c/slices` invariant,\n * and simply never appear, which reads to its author as \"the permission is\n * wrong\" and sends them looking in the wrong place entirely.\n */\nconst SURFACE_BY_CELL: Record<string, ActionSurface> = {\n 'entity:context-independent': 'form-header',\n 'entity:determining': 'form-footer',\n 'entity:context-dependent': 'row-menu',\n 'collection:context-dependent': 'bulk-bar',\n 'collection:context-independent': 'table-toolbar',\n 'unbound:context-dependent': 'command-palette',\n 'unbound:context-independent': 'command-palette',\n 'unbound:determining': 'command-palette',\n};\n\n/**\n * The overflow trigger's glyph.\n *\n * A constant rather than a literal in JSX, because `react/jsx-no-literals`\n * fails the build on written-in copy and is right to: it cannot tell a\n * typographic glyph from a forgotten Spanish string. The trigger's *name* is a\n * catalog key (`form.moreActions`, `table.actions`) — this is only the mark,\n * and it is never read aloud.\n */\nexport const OVERFLOW_GLYPH = '\\u22ef';\n\n/** Which surface renders one cell. Throws on a cell no surface owns. */\nfunction surfaceForCell(\n key: string,\n binding: UseCaseBinding,\n placement: UseCasePlacement,\n): ActionSurface {\n const surface = SURFACE_BY_CELL[`${binding}:${placement}`];\n\n if (surface === undefined) {\n throw new EntifixLogicError(\n `The use case \"${key}\" is declared ${binding} + ${placement}, which no surface renders. ` +\n 'A `determining` action finalizes a page — it is an object page’s footer — and a list ' +\n 'screen has no page to finalize, so a collection-bound verb cannot be determining. ' +\n 'Declare it `context-dependent` to reach the bulk bar, or `context-independent` to sit ' +\n 'in the table toolbar.',\n undefined,\n { key, binding, placement },\n );\n }\n\n return surface;\n}\n\n/**\n * Every surface this descriptor renders on.\n *\n * ⚠️ **A verb may name more than one cell**, and that is what makes publishing\n * reachable from a form header, a row menu and a selection without becoming\n * three permissions (#216). The primary `binding` + `placement` come first, then\n * whatever `alsoAt` adds, and each is validated the same way — an illegal cell\n * in `alsoAt` throws exactly as one in the primary pair does.\n *\n * Duplicates are collapsed. A descriptor that named the same cell twice would\n * otherwise render the verb twice in one menu, which reads as a bug in the\n * screen rather than in the declaration.\n */\nexport function surfacesFor(\n descriptor: UseCaseDescriptor,\n): readonly ActionSurface[] {\n const cells = [\n { binding: descriptor.binding, placement: descriptor.placement },\n ...(descriptor.alsoAt ?? []),\n ];\n\n return [\n ...new Set(\n cells.map(cell =>\n surfaceForCell(descriptor.key, cell.binding, cell.placement),\n ),\n ),\n ];\n}\n\n/**\n * The single surface this descriptor renders on.\n *\n * Kept for the callers that genuinely have one — the command palette asks \"is\n * this mine?\" of an `unbound` verb, which can only ever be in one place. Any\n * caller that renders a *list* of verbs wants {@link surfacesFor}.\n */\nexport function surfaceFor(descriptor: UseCaseDescriptor): ActionSurface {\n return surfaceForCell(\n descriptor.key,\n descriptor.binding,\n descriptor.placement,\n );\n}\n\n/**\n * The descriptors one surface should render, in declaration order.\n *\n * Every descriptor is passed through {@link surfacesFor}, so an unrenderable\n * cell throws on the **first render of any surface** rather than only on the\n * one that would have shown it — the same reason\n * `assertLinkSourcesAreEditable` runs as one pass over the registry instead of\n * per row: a check that only fires on the screen that was already looking for\n * the verb is a check that fires after the bug is reported. A cell declared in\n * `alsoAt` is validated on that same first render, not on the surface it names.\n */\nexport function useCasesForSurface(\n surface: ActionSurface,\n useCases: readonly UseCaseDescriptor[] | undefined,\n): UseCaseDescriptor[] {\n return (useCases ?? []).filter(descriptor =>\n surfacesFor(descriptor).includes(surface),\n );\n}\n"],"names":["EntifixLogicError","ACTION_SURFACES","SURFACE_BY_CELL","OVERFLOW_GLYPH","surfaceForCell","key","binding","placement","surface","undefined","surfacesFor","descriptor","cells","alsoAt","Set","map","cell","surfaceFor","useCasesForSurface","useCases","filter","includes"],"mappings":"AAAA,SACEA,iBAAiB,QAIZ,gBAAgB;AAEvB;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,kBAAkB;IAC7B;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAGX;;;;;;;;;;;CAWC,GACD,MAAMC,kBAAiD;IACrD,8BAA8B;IAC9B,sBAAsB;IACtB,4BAA4B;IAC5B,gCAAgC;IAChC,kCAAkC;IAClC,6BAA6B;IAC7B,+BAA+B;IAC/B,uBAAuB;AACzB;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMC,iBAAiB,SAAS;AAEvC,sEAAsE,GACtE,SAASC,eACPC,GAAW,EACXC,OAAuB,EACvBC,SAA2B;IAE3B,MAAMC,UAAUN,eAAe,CAAC,GAAGI,QAAQ,CAAC,EAAEC,WAAW,CAAC;IAE1D,IAAIC,YAAYC,WAAW;QACzB,MAAM,IAAIT,kBACR,CAAC,cAAc,EAAEK,IAAI,cAAc,EAAEC,QAAQ,GAAG,EAAEC,UAAU,4BAA4B,CAAC,GACvF,0FACA,uFACA,2FACA,yBACFE,WACA;YAAEJ;YAAKC;YAASC;QAAU;IAE9B;IAEA,OAAOC;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASE,YACdC,UAA6B;QAIvBA;IAFN,MAAMC,QAAQ;QACZ;YAAEN,SAASK,WAAWL,OAAO;YAAEC,WAAWI,WAAWJ,SAAS;QAAC;YAC3DI,qBAAAA,WAAWE,MAAM,YAAjBF,qBAAqB,EAAE;KAC5B;IAED,OAAO;WACF,IAAIG,IACLF,MAAMG,GAAG,CAACC,CAAAA,OACRZ,eAAeO,WAAWN,GAAG,EAAEW,KAAKV,OAAO,EAAEU,KAAKT,SAAS;KAGhE;AACH;AAEA;;;;;;CAMC,GACD,OAAO,SAASU,WAAWN,UAA6B;IACtD,OAAOP,eACLO,WAAWN,GAAG,EACdM,WAAWL,OAAO,EAClBK,WAAWJ,SAAS;AAExB;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASW,mBACdV,OAAsB,EACtBW,QAAkD;IAElD,OAAO,CAACA,mBAAAA,WAAY,EAAE,EAAEC,MAAM,CAACT,CAAAA,aAC7BD,YAAYC,YAAYU,QAAQ,CAACb;AAErC"}
1
+ {"version":3,"sources":["../../../src/ui/actions/action-surfaces.ts"],"sourcesContent":["import {\n EntifixLogicError,\n type UseCaseBinding,\n type UseCaseDescriptor,\n type UseCasePlacement,\n} from '@entifix/core';\n\n/**\n * Where a declared verb is rendered.\n *\n * The use-case descriptor has two independent axes, and at first only one\n * surface, so four of the nine cells had nowhere to go: `EntityForm` filtered\n * `binding === 'entity' && placement !== 'context-dependent'` and everything\n * else was dropped without a word. This is the map that closes it —\n * **placement decides the surface, binding decides the payload** — and it lives\n * in one module so no surface re-derives it and the two axes cannot drift\n * apart again.\n */\nexport const ACTION_SURFACES = [\n 'form-header',\n 'form-footer',\n 'row-menu',\n 'bulk-bar',\n 'table-toolbar',\n 'command-palette',\n] as const;\nexport type ActionSurface = (typeof ACTION_SURFACES)[number];\n\n/**\n * The nine cells, exhaustively.\n *\n * `collection` + `determining` is deliberately absent, and asking for it\n * throws rather than returning `undefined`. A determining action *finalizes a\n * page* — it is the footer that ends an object page — and a list screen has no\n * such thing to finalize, which is Fiori's own rule. Dropping it silently is\n * the fault this repo already writes an assertion against elsewhere: the verb\n * would be declared, granted, exported, pass every check a register runs,\n * and simply never appear, which reads to its author as \"the permission is\n * wrong\" and sends them looking in the wrong place entirely.\n */\nconst SURFACE_BY_CELL: Record<string, ActionSurface> = {\n 'entity:context-independent': 'form-header',\n 'entity:determining': 'form-footer',\n 'entity:context-dependent': 'row-menu',\n 'collection:context-dependent': 'bulk-bar',\n 'collection:context-independent': 'table-toolbar',\n 'unbound:context-dependent': 'command-palette',\n 'unbound:context-independent': 'command-palette',\n 'unbound:determining': 'command-palette',\n};\n\n/**\n * The overflow trigger's glyph.\n *\n * A constant rather than a literal in JSX, because `react/jsx-no-literals`\n * fails the build on written-in copy and is right to: it cannot tell a\n * typographic glyph from a forgotten Spanish string. The trigger's *name* is a\n * catalog key (`form.moreActions`, `table.actions`) — this is only the mark,\n * and it is never read aloud.\n */\nexport const OVERFLOW_GLYPH = '\\u22ef';\n\n/** Which surface renders one cell. Throws on a cell no surface owns. */\nfunction surfaceForCell(\n key: string,\n binding: UseCaseBinding,\n placement: UseCasePlacement,\n): ActionSurface {\n const surface = SURFACE_BY_CELL[`${binding}:${placement}`];\n\n if (surface === undefined) {\n throw new EntifixLogicError(\n `The use case \"${key}\" is declared ${binding} + ${placement}, which no surface renders. ` +\n 'A `determining` action finalizes a page — it is an object page’s footer — and a list ' +\n 'screen has no page to finalize, so a collection-bound verb cannot be determining. ' +\n 'Declare it `context-dependent` to reach the bulk bar, or `context-independent` to sit ' +\n 'in the table toolbar.',\n undefined,\n { key, binding, placement },\n );\n }\n\n return surface;\n}\n\n/**\n * Every surface this descriptor renders on.\n *\n * ⚠️ **A verb may name more than one cell**, and that is what makes publishing\n * reachable from a form header, a row menu and a selection without becoming\n * three permissions. The primary `binding` + `placement` come first, then\n * whatever `alsoAt` adds, and each is validated the same way — an illegal cell\n * in `alsoAt` throws exactly as one in the primary pair does.\n *\n * Duplicates are collapsed. A descriptor that named the same cell twice would\n * otherwise render the verb twice in one menu, which reads as a bug in the\n * screen rather than in the declaration.\n */\nexport function surfacesFor(\n descriptor: UseCaseDescriptor,\n): readonly ActionSurface[] {\n const cells = [\n { binding: descriptor.binding, placement: descriptor.placement },\n ...(descriptor.alsoAt ?? []),\n ];\n\n return [\n ...new Set(\n cells.map(cell =>\n surfaceForCell(descriptor.key, cell.binding, cell.placement),\n ),\n ),\n ];\n}\n\n/**\n * The single surface this descriptor renders on.\n *\n * Kept for the callers that genuinely have one — the command palette asks \"is\n * this mine?\" of an `unbound` verb, which can only ever be in one place. Any\n * caller that renders a *list* of verbs wants {@link surfacesFor}.\n */\nexport function surfaceFor(descriptor: UseCaseDescriptor): ActionSurface {\n return surfaceForCell(\n descriptor.key,\n descriptor.binding,\n descriptor.placement,\n );\n}\n\n/**\n * The descriptors one surface should render, in declaration order.\n *\n * Every descriptor is passed through {@link surfacesFor}, so an unrenderable\n * cell throws on the **first render of any surface** rather than only on the\n * one that would have shown it — the same reason\n * `assertLinkSourcesAreEditable` runs as one pass over the registry instead of\n * per row: a check that only fires on the screen that was already looking for\n * the verb is a check that fires after the bug is reported. A cell declared in\n * `alsoAt` is validated on that same first render, not on the surface it names.\n */\nexport function useCasesForSurface(\n surface: ActionSurface,\n useCases: readonly UseCaseDescriptor[] | undefined,\n): UseCaseDescriptor[] {\n return (useCases ?? []).filter(descriptor =>\n surfacesFor(descriptor).includes(surface),\n );\n}\n"],"names":["EntifixLogicError","ACTION_SURFACES","SURFACE_BY_CELL","OVERFLOW_GLYPH","surfaceForCell","key","binding","placement","surface","undefined","surfacesFor","descriptor","cells","alsoAt","Set","map","cell","surfaceFor","useCasesForSurface","useCases","filter","includes"],"mappings":"AAAA,SACEA,iBAAiB,QAIZ,gBAAgB;AAEvB;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,kBAAkB;IAC7B;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAGX;;;;;;;;;;;CAWC,GACD,MAAMC,kBAAiD;IACrD,8BAA8B;IAC9B,sBAAsB;IACtB,4BAA4B;IAC5B,gCAAgC;IAChC,kCAAkC;IAClC,6BAA6B;IAC7B,+BAA+B;IAC/B,uBAAuB;AACzB;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMC,iBAAiB,SAAS;AAEvC,sEAAsE,GACtE,SAASC,eACPC,GAAW,EACXC,OAAuB,EACvBC,SAA2B;IAE3B,MAAMC,UAAUN,eAAe,CAAC,GAAGI,QAAQ,CAAC,EAAEC,WAAW,CAAC;IAE1D,IAAIC,YAAYC,WAAW;QACzB,MAAM,IAAIT,kBACR,CAAC,cAAc,EAAEK,IAAI,cAAc,EAAEC,QAAQ,GAAG,EAAEC,UAAU,4BAA4B,CAAC,GACvF,0FACA,uFACA,2FACA,yBACFE,WACA;YAAEJ;YAAKC;YAASC;QAAU;IAE9B;IAEA,OAAOC;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASE,YACdC,UAA6B;QAIvBA;IAFN,MAAMC,QAAQ;QACZ;YAAEN,SAASK,WAAWL,OAAO;YAAEC,WAAWI,WAAWJ,SAAS;QAAC;YAC3DI,qBAAAA,WAAWE,MAAM,YAAjBF,qBAAqB,EAAE;KAC5B;IAED,OAAO;WACF,IAAIG,IACLF,MAAMG,GAAG,CAACC,CAAAA,OACRZ,eAAeO,WAAWN,GAAG,EAAEW,KAAKV,OAAO,EAAEU,KAAKT,SAAS;KAGhE;AACH;AAEA;;;;;;CAMC,GACD,OAAO,SAASU,WAAWN,UAA6B;IACtD,OAAOP,eACLO,WAAWN,GAAG,EACdM,WAAWL,OAAO,EAClBK,WAAWJ,SAAS;AAExB;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASW,mBACdV,OAAsB,EACtBW,QAAkD;IAElD,OAAO,CAACA,mBAAAA,WAAY,EAAE,EAAEC,MAAM,CAACT,CAAAA,aAC7BD,YAAYC,YAAYU,QAAQ,CAACb;AAErC"}
@@ -38,7 +38,7 @@ export interface FieldControlProps {
38
38
  * picker in one level up from its `linkSources`, so the member does get an
39
39
  * editor, just not from here. A `linkCollection` has no editor anywhere yet
40
40
  * (#26), and read-only is all it gets. A `composition` is the same: its rows
41
- * need a detail grid (#122), and a text box holding `[object Object]` would be
41
+ * need a detail grid, and a text box holding `[object Object]` would be
42
42
  * strictly worse than an honest disabled field.
43
43
  *
44
44
  * A `scalarCollection` is the exception among the collections and falls through
@@ -22,7 +22,7 @@ import { Checkbox, Select, TextInput } from '../field/index.js';
22
22
  * picker in one level up from its `linkSources`, so the member does get an
23
23
  * editor, just not from here. A `linkCollection` has no editor anywhere yet
24
24
  * (#26), and read-only is all it gets. A `composition` is the same: its rows
25
- * need a detail grid (#122), and a text box holding `[object Object]` would be
25
+ * need a detail grid, and a text box holding `[object Object]` would be
26
26
  * strictly worse than an honest disabled field.
27
27
  *
28
28
  * A `scalarCollection` is the exception among the collections and falls through
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid (#122), and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","Checkbox","Select","TextInput","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","aria","enumLabel","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,QAAQ,yBAAyB;AACtD,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AA4BhE,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAMT;QANS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EAEgB,OADfC;;;;;;QA6COR;IA3CV,MAAMS,YAAYb;IAClB,MAAMc,WACJN,WAAWO,QAAQ,IACnBP,WAAWQ,IAAI,KAAK,UACpBR,WAAWQ,IAAI,KAAK,oBACpBR,WAAWQ,IAAI,KAAK;IAEtB,IAAIR,WAAWQ,IAAI,KAAK,WAAW;QACjC,qBACE,KAACf;YACCU,IAAIA;WACAC;YACJK,OAAOT,WAAWS,KAAK;YACvBC,SAAST,UAAU;YACnBK,UAAUA;YACVJ,UAAUS,CAAAA,QAAST,SAASU,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIV,WAAWQ,IAAI,KAAK,QAAQ;YAUxBR;QATN,qBACE,MAACN;YACCS,IAAIA;WACAC;YACJH,OAAOA;YACPK,UAAUA;YACVJ,UAAUS,CAAAA,QAAST,SAASS,MAAME,aAAa,CAACZ,KAAK;;8BAErD,KAACa;oBAAOb,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWe,UAAU,YAArBf,yBAAyB,EAAE,EAAEgB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBb,OAAOa;kCACzBT,UAAUL,YAAYc;uBADZA;;;IAMrB;IAEA,qBACE,KAACnB;QACCQ,IAAIA;OACAC;QACJI,IAAI,GAAEZ,8BAAAA,UAAU,CAACI,WAAWQ,IAAI,CAAC,YAA3BZ,8BAA+B;QACrCK,OAAOA;QACPK,UAAUA;QACVJ,UAAUS,CAAAA,QAAST,SAASS,MAAME,aAAa,CAACZ,KAAK;;AAG3D"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","Checkbox","Select","TextInput","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","aria","enumLabel","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,QAAQ,yBAAyB;AACtD,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AA4BhE,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAMT;QANS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EAEgB,OADfC;;;;;;QA6COR;IA3CV,MAAMS,YAAYb;IAClB,MAAMc,WACJN,WAAWO,QAAQ,IACnBP,WAAWQ,IAAI,KAAK,UACpBR,WAAWQ,IAAI,KAAK,oBACpBR,WAAWQ,IAAI,KAAK;IAEtB,IAAIR,WAAWQ,IAAI,KAAK,WAAW;QACjC,qBACE,KAACf;YACCU,IAAIA;WACAC;YACJK,OAAOT,WAAWS,KAAK;YACvBC,SAAST,UAAU;YACnBK,UAAUA;YACVJ,UAAUS,CAAAA,QAAST,SAASU,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIV,WAAWQ,IAAI,KAAK,QAAQ;YAUxBR;QATN,qBACE,MAACN;YACCS,IAAIA;WACAC;YACJH,OAAOA;YACPK,UAAUA;YACVJ,UAAUS,CAAAA,QAAST,SAASS,MAAME,aAAa,CAACZ,KAAK;;8BAErD,KAACa;oBAAOb,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWe,UAAU,YAArBf,yBAAyB,EAAE,EAAEgB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBb,OAAOa;kCACzBT,UAAUL,YAAYc;uBADZA;;;IAMrB;IAEA,qBACE,KAACnB;QACCQ,IAAIA;OACAC;QACJI,IAAI,GAAEZ,8BAAAA,UAAU,CAACI,WAAWQ,IAAI,CAAC,YAA3BZ,8BAA+B;QACrCK,OAAOA;QACPK,UAAUA;QACVJ,UAAUS,CAAAA,QAAST,SAASS,MAAME,aAAa,CAACZ,KAAK;;AAG3D"}
@@ -6,7 +6,7 @@ export interface BulkActionBarProps {
6
6
  * The collection-bound verbs this caller may run, already filtered against
7
7
  * the verified principal by the service. The bar renders what it is given and
8
8
  * re-checks nothing — hiding an action protects nothing, the route guard is
9
- * the enforcement (ADR 0026).
9
+ * the enforcement.
10
10
  */
11
11
  useCases: readonly UseCaseDescriptor[];
12
12
  /**
@@ -26,7 +26,7 @@ export interface BulkActionBarProps {
26
26
  * The caller has to know whether the verb asked to be confirmed, and the bar
27
27
  * is already holding the answer. Passing the key alone forced a lookup back
28
28
  * into the same list — a branch for "not found" that cannot happen, since
29
- * every key here came from this array (#216).
29
+ * every key here came from this array.
30
30
  */
31
31
  onUseCase: (useCase: UseCaseDescriptor) => void;
32
32
  /** Disables every verb while one is running. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/bulk-action-bar/bulk-action-bar.tsx"],"sourcesContent":["'use client';\n\nimport type { UseCaseDescriptor } from '@entifix/core';\n\nimport { useT, useTranslateKey } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface BulkActionBarProps {\n /** How many rows the action would touch. */\n count: number;\n /**\n * The collection-bound verbs this caller may run, already filtered against\n * the verified principal by the service. The bar renders what it is given and\n * re-checks nothing — hiding an action protects nothing, the route guard is\n * the enforcement (ADR 0026).\n */\n useCases: readonly UseCaseDescriptor[];\n /**\n * Offered only when the selection is a page's worth of ids *and* more rows\n * match than are shown. Absent means there is no larger set to escalate to.\n *\n * It is a **separate affordance carrying the count**, never a widening of the\n * header checkbox: \"select all on this page\" and \"select all 3.200 matching\"\n * are different actions and the second has to be chosen deliberately.\n */\n matchingTotal?: number;\n onSelectAllMatching?: () => void;\n onClear: () => void;\n /**\n * Hands back the descriptor it rendered, not just its key.\n *\n * The caller has to know whether the verb asked to be confirmed, and the bar\n * is already holding the answer. Passing the key alone forced a lookup back\n * into the same list — a branch for \"not found\" that cannot happen, since\n * every key here came from this array (#216).\n */\n onUseCase: (useCase: UseCaseDescriptor) => void;\n /** Disables every verb while one is running. */\n busy?: boolean;\n className?: string;\n}\n\n/**\n * The bar that appears once rows are selected: what is selected, what can be\n * done to it, and the way out.\n *\n * It is `aria-live=\"polite\"` on the count rather than on the whole region — a\n * region that re-announces its buttons every time one more row is ticked is\n * noise, and the count is the only part that changed. `role=\"region\"` with a\n * name is what lets a screen-reader user jump to it at all, since it appears\n * without the focus moving.\n */\nexport function BulkActionBar({\n count,\n useCases,\n matchingTotal,\n onSelectAllMatching,\n onClear,\n onUseCase,\n busy = false,\n className,\n}: BulkActionBarProps) {\n const t = useT();\n const translateKey = useTranslateKey();\n\n return (\n <div\n role=\"region\"\n aria-label={t('bulk.barLabel')}\n data-testid=\"bulk-action-bar\"\n className={cn(\n 'flex flex-wrap items-center gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n <span aria-live=\"polite\" className=\"text-step-sm text-content\">\n {t('table.selectedCount', { count })}\n </span>\n\n {matchingTotal !== undefined && onSelectAllMatching && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onSelectAllMatching}\n >\n {t('table.selectAllMatching', { count: matchingTotal })}\n </Button>\n )}\n\n {useCases.map(descriptor => (\n <Button\n key={descriptor.key}\n type=\"button\"\n variant={\n descriptor.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'secondary'\n }\n size=\"sm\"\n disabled={busy}\n onClick={() => onUseCase(descriptor)}\n >\n {/* A descriptor's label is a *runtime* catalog key — the type system\n cannot see a typo here, `@r10c/i18n-check` is what does. */}\n {translateKey(descriptor.labelKey)}\n </Button>\n ))}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onClear}\n >\n {t('table.clearSelection')}\n </Button>\n </div>\n );\n}\n"],"names":["useT","useTranslateKey","Button","cn","BulkActionBar","count","useCases","matchingTotal","onSelectAllMatching","onClear","onUseCase","busy","className","t","translateKey","div","role","aria-label","data-testid","span","aria-live","undefined","type","variant","size","onClick","map","descriptor","confirm","tone","disabled","labelKey","key"],"mappings":"AAAA;;AAIA,SAASA,IAAI,EAAEC,eAAe,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAqCvC;;;;;;;;;CASC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,QAAQ,EACRC,aAAa,EACbC,mBAAmB,EACnBC,OAAO,EACPC,SAAS,EACTC,OAAO,KAAK,EACZC,SAAS,EACU;IACnB,MAAMC,IAAIb;IACV,MAAMc,eAAeb;IAErB,qBACE,MAACc;QACCC,MAAK;QACLC,cAAYJ,EAAE;QACdK,eAAY;QACZN,WAAWT,GACT,uGACAS;;0BAGF,KAACO;gBAAKC,aAAU;gBAASR,WAAU;0BAChCC,EAAE,uBAAuB;oBAAER;gBAAM;;YAGnCE,kBAAkBc,aAAab,qCAC9B,KAACN;gBACCoB,MAAK;gBACLC,SAAQ;gBACRC,MAAK;gBACLC,SAASjB;0BAERK,EAAE,2BAA2B;oBAAER,OAAOE;gBAAc;;YAIxDD,SAASoB,GAAG,CAACC,CAAAA;oBAKRA;qCAJJ,KAACzB;oBAECoB,MAAK;oBACLC,SACEI,EAAAA,sBAAAA,WAAWC,OAAO,qBAAlBD,oBAAoBE,IAAI,MAAK,gBACzB,gBACA;oBAENL,MAAK;oBACLM,UAAUnB;oBACVc,SAAS,IAAMf,UAAUiB;8BAIxBb,aAAaa,WAAWI,QAAQ;mBAb5BJ,WAAWK,GAAG;;0BAiBvB,KAAC9B;gBACCoB,MAAK;gBACLC,SAAQ;gBACRC,MAAK;gBACLM,UAAUnB;gBACVc,SAAShB;0BAERI,EAAE;;;;AAIX"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/bulk-action-bar/bulk-action-bar.tsx"],"sourcesContent":["'use client';\n\nimport type { UseCaseDescriptor } from '@entifix/core';\n\nimport { useT, useTranslateKey } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface BulkActionBarProps {\n /** How many rows the action would touch. */\n count: number;\n /**\n * The collection-bound verbs this caller may run, already filtered against\n * the verified principal by the service. The bar renders what it is given and\n * re-checks nothing — hiding an action protects nothing, the route guard is\n * the enforcement.\n */\n useCases: readonly UseCaseDescriptor[];\n /**\n * Offered only when the selection is a page's worth of ids *and* more rows\n * match than are shown. Absent means there is no larger set to escalate to.\n *\n * It is a **separate affordance carrying the count**, never a widening of the\n * header checkbox: \"select all on this page\" and \"select all 3.200 matching\"\n * are different actions and the second has to be chosen deliberately.\n */\n matchingTotal?: number;\n onSelectAllMatching?: () => void;\n onClear: () => void;\n /**\n * Hands back the descriptor it rendered, not just its key.\n *\n * The caller has to know whether the verb asked to be confirmed, and the bar\n * is already holding the answer. Passing the key alone forced a lookup back\n * into the same list — a branch for \"not found\" that cannot happen, since\n * every key here came from this array.\n */\n onUseCase: (useCase: UseCaseDescriptor) => void;\n /** Disables every verb while one is running. */\n busy?: boolean;\n className?: string;\n}\n\n/**\n * The bar that appears once rows are selected: what is selected, what can be\n * done to it, and the way out.\n *\n * It is `aria-live=\"polite\"` on the count rather than on the whole region — a\n * region that re-announces its buttons every time one more row is ticked is\n * noise, and the count is the only part that changed. `role=\"region\"` with a\n * name is what lets a screen-reader user jump to it at all, since it appears\n * without the focus moving.\n */\nexport function BulkActionBar({\n count,\n useCases,\n matchingTotal,\n onSelectAllMatching,\n onClear,\n onUseCase,\n busy = false,\n className,\n}: BulkActionBarProps) {\n const t = useT();\n const translateKey = useTranslateKey();\n\n return (\n <div\n role=\"region\"\n aria-label={t('bulk.barLabel')}\n data-testid=\"bulk-action-bar\"\n className={cn(\n 'flex flex-wrap items-center gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n <span aria-live=\"polite\" className=\"text-step-sm text-content\">\n {t('table.selectedCount', { count })}\n </span>\n\n {matchingTotal !== undefined && onSelectAllMatching && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onSelectAllMatching}\n >\n {t('table.selectAllMatching', { count: matchingTotal })}\n </Button>\n )}\n\n {useCases.map(descriptor => (\n <Button\n key={descriptor.key}\n type=\"button\"\n variant={\n descriptor.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'secondary'\n }\n size=\"sm\"\n disabled={busy}\n onClick={() => onUseCase(descriptor)}\n >\n {/* A descriptor's label is a *runtime* catalog key — the type system\n cannot see a typo here, a host's catalog check is what does. */}\n {translateKey(descriptor.labelKey)}\n </Button>\n ))}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onClear}\n >\n {t('table.clearSelection')}\n </Button>\n </div>\n );\n}\n"],"names":["useT","useTranslateKey","Button","cn","BulkActionBar","count","useCases","matchingTotal","onSelectAllMatching","onClear","onUseCase","busy","className","t","translateKey","div","role","aria-label","data-testid","span","aria-live","undefined","type","variant","size","onClick","map","descriptor","confirm","tone","disabled","labelKey","key"],"mappings":"AAAA;;AAIA,SAASA,IAAI,EAAEC,eAAe,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAqCvC;;;;;;;;;CASC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,QAAQ,EACRC,aAAa,EACbC,mBAAmB,EACnBC,OAAO,EACPC,SAAS,EACTC,OAAO,KAAK,EACZC,SAAS,EACU;IACnB,MAAMC,IAAIb;IACV,MAAMc,eAAeb;IAErB,qBACE,MAACc;QACCC,MAAK;QACLC,cAAYJ,EAAE;QACdK,eAAY;QACZN,WAAWT,GACT,uGACAS;;0BAGF,KAACO;gBAAKC,aAAU;gBAASR,WAAU;0BAChCC,EAAE,uBAAuB;oBAAER;gBAAM;;YAGnCE,kBAAkBc,aAAab,qCAC9B,KAACN;gBACCoB,MAAK;gBACLC,SAAQ;gBACRC,MAAK;gBACLC,SAASjB;0BAERK,EAAE,2BAA2B;oBAAER,OAAOE;gBAAc;;YAIxDD,SAASoB,GAAG,CAACC,CAAAA;oBAKRA;qCAJJ,KAACzB;oBAECoB,MAAK;oBACLC,SACEI,EAAAA,sBAAAA,WAAWC,OAAO,qBAAlBD,oBAAoBE,IAAI,MAAK,gBACzB,gBACA;oBAENL,MAAK;oBACLM,UAAUnB;oBACVc,SAAS,IAAMf,UAAUiB;8BAIxBb,aAAaa,WAAWI,QAAQ;mBAb5BJ,WAAWK,GAAG;;0BAiBvB,KAAC9B;gBACCoB,MAAK;gBACLC,SAAQ;gBACRC,MAAK;gBACLM,UAAUnB;gBACVc,SAAShB;0BAERI,EAAE;;;;AAIX"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/bulk-result/bulk-result.tsx"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n type EntityId,\n failedIds,\n succeededCount,\n} from '@entifix/core';\n\nimport { useT, useTranslateKey } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface BulkResultProps {\n outcomes: readonly BulkOutcome[];\n /**\n * How a failed row is named to the user. An id is not an answer to \"which\n * ones failed\" — the caller holds the rows and can render the same label the\n * table shows. Falls back to the id when the row is no longer on the page.\n */\n labelFor?: (id: EntityId) => string | undefined;\n /** Re-runs the failures **only**. Absent hides the affordance. */\n onRetryFailed?: (ids: EntityId[]) => void;\n onDismiss: () => void;\n busy?: boolean;\n className?: string;\n}\n\n/**\n * What a bulk action actually did, per row.\n *\n * The requirement this exists for: 40 selected, 3 fail. A single notice lies\n * whichever way it is written — as a failure it hides the 37 rows that were\n * written, and as a success it hides the 3 that were not, which is worse\n * because the user walks away believing the work is done. So both counts are\n * always stated, and every failure is named with its own reason.\n *\n * `role=\"status\"`, not `role=\"alert\"`: a partial failure is an outcome to read,\n * not an interruption, and `alert` on a list this long is hostile. The region\n * is `tabIndex={-1}` so the caller can move focus to it once the run ends,\n * which is the only reliable way a keyboard user learns the result arrived.\n */\nexport function BulkResult({\n outcomes,\n labelFor,\n onRetryFailed,\n onDismiss,\n busy = false,\n className,\n}: BulkResultProps) {\n const t = useT();\n const translateKey = useTranslateKey();\n\n const failed = outcomes.filter(outcome => !outcome.ok);\n const succeeded = succeededCount(outcomes);\n\n return (\n <div\n role=\"status\"\n tabIndex={-1}\n aria-label={t('bulk.resultLabel')}\n data-testid=\"bulk-result\"\n className={cn(\n 'flex flex-col gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <span className=\"text-step-sm text-content\">\n {t('bulk.succeeded', { count: succeeded })}\n </span>\n {failed.length > 0 && (\n <span className=\"text-step-sm text-danger\">\n {t('bulk.failed', { count: failed.length })}\n </span>\n )}\n {failed.length > 0 && onRetryFailed && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={busy}\n onClick={() => onRetryFailed(failedIds(outcomes))}\n >\n {t('bulk.retryFailed')}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onDismiss}\n >\n {t('bulk.dismiss')}\n </Button>\n </div>\n\n {failed.length > 0 && (\n <ul className=\"flex flex-col gap-3xs\">\n {failed.map(outcome => (\n <li\n key={String(outcome.id)}\n className=\"text-step-sm text-content-muted\"\n >\n <span className=\"text-content\">\n {labelFor?.(outcome.id) ?? String(outcome.id)}\n </span>\n {' — '}\n {/* The reason is a *code*, resolved through the shared `errors`\n catalog — the same vocabulary a service's `{ error, code }`\n body is rendered from, so a new failure reason is\n translatable rather than English, and `@r10c/i18n-check`\n fails the build on one the catalog lacks. `unexpected` is the\n browser-side fallback the catalog already carries. */}\n {translateKey(`errors:${outcome.code ?? 'unexpected'}`)}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"names":["failedIds","succeededCount","useT","useTranslateKey","Button","cn","BulkResult","outcomes","labelFor","onRetryFailed","onDismiss","busy","className","t","translateKey","failed","filter","outcome","ok","succeeded","div","role","tabIndex","aria-label","data-testid","span","count","length","type","variant","size","disabled","onClick","ul","map","li","id","String","code"],"mappings":"AAAA;;AAEA,SAGEA,SAAS,EACTC,cAAc,QACT,gBAAgB;AAEvB,SAASC,IAAI,EAAEC,eAAe,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAiBvC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,WAAW,EACzBC,QAAQ,EACRC,QAAQ,EACRC,aAAa,EACbC,SAAS,EACTC,OAAO,KAAK,EACZC,SAAS,EACO;IAChB,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IAErB,MAAMY,SAASR,SAASS,MAAM,CAACC,CAAAA,UAAW,CAACA,QAAQC,EAAE;IACrD,MAAMC,YAAYlB,eAAeM;IAEjC,qBACE,MAACa;QACCC,MAAK;QACLC,UAAU,CAAC;QACXC,cAAYV,EAAE;QACdW,eAAY;QACZZ,WAAWP,GACT,yFACAO;;0BAGF,MAACQ;gBAAIR,WAAU;;kCACb,KAACa;wBAAKb,WAAU;kCACbC,EAAE,kBAAkB;4BAAEa,OAAOP;wBAAU;;oBAEzCJ,OAAOY,MAAM,GAAG,mBACf,KAACF;wBAAKb,WAAU;kCACbC,EAAE,eAAe;4BAAEa,OAAOX,OAAOY,MAAM;wBAAC;;oBAG5CZ,OAAOY,MAAM,GAAG,KAAKlB,+BACpB,KAACL;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAS,IAAMvB,cAAcT,UAAUO;kCAEtCM,EAAE;;kCAGP,KAACT;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAStB;kCAERG,EAAE;;;;YAINE,OAAOY,MAAM,GAAG,mBACf,KAACM;gBAAGrB,WAAU;0BACXG,OAAOmB,GAAG,CAACjB,CAAAA;8BAegBA;yCAd1B,MAACkB;wBAECvB,WAAU;;0CAEV,KAACa;gCAAKb,WAAU;kDACbJ,4BAAAA,SAAWS,QAAQmB,EAAE,oBAAKC,OAAOpB,QAAQmB,EAAE;;4BAE7C;4BAOAtB,aAAa,CAAC,OAAO,GAAEG,gBAAAA,QAAQqB,IAAI,YAAZrB,gBAAgB,cAAc;;uBAbjDoB,OAAOpB,QAAQmB,EAAE;;;;;AAoBpC"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/bulk-result/bulk-result.tsx"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n type EntityId,\n failedIds,\n succeededCount,\n} from '@entifix/core';\n\nimport { useT, useTranslateKey } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface BulkResultProps {\n outcomes: readonly BulkOutcome[];\n /**\n * How a failed row is named to the user. An id is not an answer to \"which\n * ones failed\" — the caller holds the rows and can render the same label the\n * table shows. Falls back to the id when the row is no longer on the page.\n */\n labelFor?: (id: EntityId) => string | undefined;\n /** Re-runs the failures **only**. Absent hides the affordance. */\n onRetryFailed?: (ids: EntityId[]) => void;\n onDismiss: () => void;\n busy?: boolean;\n className?: string;\n}\n\n/**\n * What a bulk action actually did, per row.\n *\n * The requirement this exists for: 40 selected, 3 fail. A single notice lies\n * whichever way it is written — as a failure it hides the 37 rows that were\n * written, and as a success it hides the 3 that were not, which is worse\n * because the user walks away believing the work is done. So both counts are\n * always stated, and every failure is named with its own reason.\n *\n * `role=\"status\"`, not `role=\"alert\"`: a partial failure is an outcome to read,\n * not an interruption, and `alert` on a list this long is hostile. The region\n * is `tabIndex={-1}` so the caller can move focus to it once the run ends,\n * which is the only reliable way a keyboard user learns the result arrived.\n */\nexport function BulkResult({\n outcomes,\n labelFor,\n onRetryFailed,\n onDismiss,\n busy = false,\n className,\n}: BulkResultProps) {\n const t = useT();\n const translateKey = useTranslateKey();\n\n const failed = outcomes.filter(outcome => !outcome.ok);\n const succeeded = succeededCount(outcomes);\n\n return (\n <div\n role=\"status\"\n tabIndex={-1}\n aria-label={t('bulk.resultLabel')}\n data-testid=\"bulk-result\"\n className={cn(\n 'flex flex-col gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <span className=\"text-step-sm text-content\">\n {t('bulk.succeeded', { count: succeeded })}\n </span>\n {failed.length > 0 && (\n <span className=\"text-step-sm text-danger\">\n {t('bulk.failed', { count: failed.length })}\n </span>\n )}\n {failed.length > 0 && onRetryFailed && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={busy}\n onClick={() => onRetryFailed(failedIds(outcomes))}\n >\n {t('bulk.retryFailed')}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onDismiss}\n >\n {t('bulk.dismiss')}\n </Button>\n </div>\n\n {failed.length > 0 && (\n <ul className=\"flex flex-col gap-3xs\">\n {failed.map(outcome => (\n <li\n key={String(outcome.id)}\n className=\"text-step-sm text-content-muted\"\n >\n <span className=\"text-content\">\n {labelFor?.(outcome.id) ?? String(outcome.id)}\n </span>\n {' — '}\n {/* The reason is a *code*, resolved through the shared `errors`\n catalog — the same vocabulary a service's `{ error, code }`\n body is rendered from, so a new failure reason is\n translatable rather than English, and a host's catalog\n check fails the build on one the catalog lacks. `unexpected` is the\n browser-side fallback the catalog already carries. */}\n {translateKey(`errors:${outcome.code ?? 'unexpected'}`)}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"names":["failedIds","succeededCount","useT","useTranslateKey","Button","cn","BulkResult","outcomes","labelFor","onRetryFailed","onDismiss","busy","className","t","translateKey","failed","filter","outcome","ok","succeeded","div","role","tabIndex","aria-label","data-testid","span","count","length","type","variant","size","disabled","onClick","ul","map","li","id","String","code"],"mappings":"AAAA;;AAEA,SAGEA,SAAS,EACTC,cAAc,QACT,gBAAgB;AAEvB,SAASC,IAAI,EAAEC,eAAe,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAiBvC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,WAAW,EACzBC,QAAQ,EACRC,QAAQ,EACRC,aAAa,EACbC,SAAS,EACTC,OAAO,KAAK,EACZC,SAAS,EACO;IAChB,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IAErB,MAAMY,SAASR,SAASS,MAAM,CAACC,CAAAA,UAAW,CAACA,QAAQC,EAAE;IACrD,MAAMC,YAAYlB,eAAeM;IAEjC,qBACE,MAACa;QACCC,MAAK;QACLC,UAAU,CAAC;QACXC,cAAYV,EAAE;QACdW,eAAY;QACZZ,WAAWP,GACT,yFACAO;;0BAGF,MAACQ;gBAAIR,WAAU;;kCACb,KAACa;wBAAKb,WAAU;kCACbC,EAAE,kBAAkB;4BAAEa,OAAOP;wBAAU;;oBAEzCJ,OAAOY,MAAM,GAAG,mBACf,KAACF;wBAAKb,WAAU;kCACbC,EAAE,eAAe;4BAAEa,OAAOX,OAAOY,MAAM;wBAAC;;oBAG5CZ,OAAOY,MAAM,GAAG,KAAKlB,+BACpB,KAACL;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAS,IAAMvB,cAAcT,UAAUO;kCAEtCM,EAAE;;kCAGP,KAACT;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAStB;kCAERG,EAAE;;;;YAINE,OAAOY,MAAM,GAAG,mBACf,KAACM;gBAAGrB,WAAU;0BACXG,OAAOmB,GAAG,CAACjB,CAAAA;8BAegBA;yCAd1B,MAACkB;wBAECvB,WAAU;;0CAEV,KAACa;gCAAKb,WAAU;kDACbJ,4BAAAA,SAAWS,QAAQmB,EAAE,oBAAKC,OAAOpB,QAAQmB,EAAE;;4BAE7C;4BAOAtB,aAAa,CAAC,OAAO,GAAEG,gBAAAA,QAAQqB,IAAI,YAAZrB,gBAAgB,cAAc;;uBAbjDoB,OAAOpB,QAAQmB,EAAE;;;;;AAoBpC"}
@@ -20,7 +20,7 @@ export interface LoadingBoundaryProps {
20
20
  * a non-sighted caller is told nothing at all while a region is pending.
21
21
  *
22
22
  * It is not an error boundary and does not catch anything. Rendering actions
23
- * became asynchronous when affordances moved behind `$metadata` (ADR 0026);
23
+ * became asynchronous when affordances moved behind `$metadata`;
24
24
  * this is where that wait is shown.
25
25
  */
26
26
  export declare function LoadingBoundary({ isLoading, children, fallback, lines, className, label, }: LoadingBoundaryProps): import("react").JSX.Element;
@@ -11,7 +11,7 @@ import { cn } from '../../utils/cn.js';
11
11
  * a non-sighted caller is told nothing at all while a region is pending.
12
12
  *
13
13
  * It is not an error boundary and does not catch anything. Rendering actions
14
- * became asynchronous when affordances moved behind `$metadata` (ADR 0026);
14
+ * became asynchronous when affordances moved behind `$metadata`;
15
15
  * this is where that wait is shown.
16
16
  */ export function LoadingBoundary({ isLoading, children, fallback, lines = 3, className, label }) {
17
17
  if (!isLoading) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/loading-boundary/loading-boundary.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { Skeleton, SkeletonText } from '../../atoms/skeleton/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface LoadingBoundaryProps {\n isLoading: boolean;\n children: ReactNode;\n /** What to show while loading. Defaults to a shape-matched skeleton. */\n fallback?: ReactNode;\n /** Lines for the default text fallback. `0` renders a single block instead. */\n lines?: number;\n className?: string;\n /** Announced to assistive tech; a catalog key resolved by the caller. */\n label?: string;\n}\n\n/**\n * Holds a region's shape while its content is still arriving.\n *\n * Presentational and copy-free: it renders a skeleton, not the word \"Loading\",\n * because a placeholder that matches the real geometry causes no layout shift\n * when the content lands and needs no translation. The optional {@link label} is\n * for screen readers only — the visual skeleton is `aria-hidden`, so without it\n * a non-sighted caller is told nothing at all while a region is pending.\n *\n * It is not an error boundary and does not catch anything. Rendering actions\n * became asynchronous when affordances moved behind `$metadata` (ADR 0026);\n * this is where that wait is shown.\n */\nexport function LoadingBoundary({\n isLoading,\n children,\n fallback,\n lines = 3,\n className,\n label,\n}: LoadingBoundaryProps) {\n if (!isLoading) {\n return <>{children}</>;\n }\n\n return (\n <div\n data-testid=\"loading-boundary\"\n className={cn(className)}\n role=\"status\"\n aria-busy=\"true\"\n aria-live=\"polite\"\n >\n {label && <span className=\"sr-only\">{label}</span>}\n {fallback ??\n (lines === 0 ? (\n <Skeleton className=\"h-8 w-full\" />\n ) : (\n <SkeletonText lines={lines} />\n ))}\n </div>\n );\n}\n"],"names":["Skeleton","SkeletonText","cn","LoadingBoundary","isLoading","children","fallback","lines","className","label","div","data-testid","role","aria-busy","aria-live","span"],"mappings":";AAEA,SAASA,QAAQ,EAAEC,YAAY,QAAQ,gCAAgC;AACvE,SAASC,EAAE,QAAQ,oBAAoB;AAcvC;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBAAgB,EAC9BC,SAAS,EACTC,QAAQ,EACRC,QAAQ,EACRC,QAAQ,CAAC,EACTC,SAAS,EACTC,KAAK,EACgB;IACrB,IAAI,CAACL,WAAW;QACd,qBAAO;sBAAGC;;IACZ;IAEA,qBACE,MAACK;QACCC,eAAY;QACZH,WAAWN,GAAGM;QACdI,MAAK;QACLC,aAAU;QACVC,aAAU;;YAETL,uBAAS,KAACM;gBAAKP,WAAU;0BAAWC;;YACpCH,mBAAAA,WACEC,UAAU,kBACT,KAACP;gBAASQ,WAAU;+BAEpB,KAACP;gBAAaM,OAAOA;;;;AAI/B"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/loading-boundary/loading-boundary.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { Skeleton, SkeletonText } from '../../atoms/skeleton/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface LoadingBoundaryProps {\n isLoading: boolean;\n children: ReactNode;\n /** What to show while loading. Defaults to a shape-matched skeleton. */\n fallback?: ReactNode;\n /** Lines for the default text fallback. `0` renders a single block instead. */\n lines?: number;\n className?: string;\n /** Announced to assistive tech; a catalog key resolved by the caller. */\n label?: string;\n}\n\n/**\n * Holds a region's shape while its content is still arriving.\n *\n * Presentational and copy-free: it renders a skeleton, not the word \"Loading\",\n * because a placeholder that matches the real geometry causes no layout shift\n * when the content lands and needs no translation. The optional {@link label} is\n * for screen readers only — the visual skeleton is `aria-hidden`, so without it\n * a non-sighted caller is told nothing at all while a region is pending.\n *\n * It is not an error boundary and does not catch anything. Rendering actions\n * became asynchronous when affordances moved behind `$metadata`;\n * this is where that wait is shown.\n */\nexport function LoadingBoundary({\n isLoading,\n children,\n fallback,\n lines = 3,\n className,\n label,\n}: LoadingBoundaryProps) {\n if (!isLoading) {\n return <>{children}</>;\n }\n\n return (\n <div\n data-testid=\"loading-boundary\"\n className={cn(className)}\n role=\"status\"\n aria-busy=\"true\"\n aria-live=\"polite\"\n >\n {label && <span className=\"sr-only\">{label}</span>}\n {fallback ??\n (lines === 0 ? (\n <Skeleton className=\"h-8 w-full\" />\n ) : (\n <SkeletonText lines={lines} />\n ))}\n </div>\n );\n}\n"],"names":["Skeleton","SkeletonText","cn","LoadingBoundary","isLoading","children","fallback","lines","className","label","div","data-testid","role","aria-busy","aria-live","span"],"mappings":";AAEA,SAASA,QAAQ,EAAEC,YAAY,QAAQ,gCAAgC;AACvE,SAASC,EAAE,QAAQ,oBAAoB;AAcvC;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBAAgB,EAC9BC,SAAS,EACTC,QAAQ,EACRC,QAAQ,EACRC,QAAQ,CAAC,EACTC,SAAS,EACTC,KAAK,EACgB;IACrB,IAAI,CAACL,WAAW;QACd,qBAAO;sBAAGC;;IACZ;IAEA,qBACE,MAACK;QACCC,eAAY;QACZH,WAAWN,GAAGM;QACdI,MAAK;QACLC,aAAU;QACVC,aAAU;;YAETL,uBAAS,KAACM;gBAAKP,WAAU;0BAAWC;;YACpCH,mBAAAA,WACEC,UAAU,kBACT,KAACP;gBAASQ,WAAU;+BAEpB,KAACP;gBAAaM,OAAOA;;;;AAI/B"}
@@ -4,7 +4,7 @@ import type { EntityDetailGridProps } from './entity-detail-grid.types.js';
4
4
  *
5
5
  * A `composition` — an order and its lines — where the rows have no life outside
6
6
  * the master and go out in the same write
7
- * ([ADR 0034](../../../../../../../docs/adr/0034-composition-metadata.md)). Not
7
+ *. Not
8
8
  * a `linkCollection`, which is association to records that exist on their own
9
9
  * and save separately; that editor is still #26.
10
10
  *
@@ -37,7 +37,7 @@ import type { EntityDetailGridProps } from './entity-detail-grid.types.js';
37
37
  * **one** summary above it, which is the only `role="alert"` in the component.
38
38
  * A live region per cell would announce the whole grid on every keystroke, and
39
39
  * a message rendered only in the cell is unreachable when the failing row is
40
- * scrolled out — which is the case #110 names.
40
+ * scrolled out — the case a long grid always meets.
41
41
  */
42
42
  export declare function EntityDetailGrid({ descriptor, rows, onRowsChange, errors, editing, isLoading, skeleton, footer, className, }: EntityDetailGridProps): import("react").JSX.Element;
43
43
  //# sourceMappingURL=entity-detail-grid.d.ts.map
@@ -16,7 +16,7 @@ import { cn } from '../../utils/cn.js';
16
16
  *
17
17
  * A `composition` — an order and its lines — where the rows have no life outside
18
18
  * the master and go out in the same write
19
- * ([ADR 0034](../../../../../../../docs/adr/0034-composition-metadata.md)). Not
19
+ *. Not
20
20
  * a `linkCollection`, which is association to records that exist on their own
21
21
  * and save separately; that editor is still #26.
22
22
  *
@@ -49,7 +49,7 @@ import { cn } from '../../utils/cn.js';
49
49
  * **one** summary above it, which is the only `role="alert"` in the component.
50
50
  * A live region per cell would announce the whole grid on every keystroke, and
51
51
  * a message rendered only in the cell is unreachable when the failing row is
52
- * scrolled out — which is the case #110 names.
52
+ * scrolled out — the case a long grid always meets.
53
53
  */ export function EntityDetailGrid({ descriptor, rows, onRowsChange, errors, editing = false, isLoading = false, skeleton = true, footer, className }) {
54
54
  var _columns_;
55
55
  const t = useT();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"sourcesContent":["'use client';\n\nimport {\n describeChildColumns,\n editableChildColumns,\n type EntityFieldDescriptor,\n type EntityRowDraft,\n newRowKey,\n parseRowFieldPath,\n ROW_KEY,\n rowFieldPath,\n} from '@entifix/core';\nimport { type KeyboardEvent, useId, useMemo } from 'react';\n\nimport { useLocalizedDescriptors, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { EntityDetailGridProps } from './entity-detail-grid.types.js';\n\n/** Rows the placeholder stands in for. Enough to read as a grid, not a page. */\nconst SKELETON_ROW_COUNT = 3;\n\n/**\n * The rows a record **owns**, edited in place.\n *\n * A `composition` — an order and its lines — where the rows have no life outside\n * the master and go out in the same write\n * ([ADR 0034](../../../../../../../docs/adr/0034-composition-metadata.md)). Not\n * a `linkCollection`, which is association to records that exist on their own\n * and save separately; that editor is still #26.\n *\n * ## Why this is not `EntityTable`\n *\n * `EntityTable` is a *server* listing: pages, RSQL filtering and sorting,\n * column personalization, a selection model and a bulk bar. A detail grid has\n * none of those and cannot have them — its rows are local, unsaved, and\n * unqueryable by construction, since a collection member is never `filterable`.\n * Sharing the organism would mean every one of those features growing a\n * \"detail\" branch. It shares the `Table` **atoms** instead, which is the level\n * the two actually have in common.\n *\n * ## Inline cells, and why Enter is free\n *\n * Each cell is a {@link FieldControl}, the same descriptor→input map the form\n * uses, so a child's `enum` gets a select and its `date` a date input with no\n * code here. `Tab` walks the row natively and crosses into the next; `Enter` on\n * the last row appends and focuses the new row's first cell.\n *\n * That last binding is available only because `EntityForm` renders a `<Card>`\n * with a `<button onClick>` Save rather than a `<form>` — inside a real form,\n * `Enter` in a text input submits, and appending a row would fight the browser\n * for it. Wrapping the form in a `<form>` element later would break this, which\n * is the one thing worth knowing before doing it.\n *\n * ## Errors\n *\n * A cell carries `aria-invalid` and points at its own message; the grid carries\n * **one** summary above it, which is the only `role=\"alert\"` in the component.\n * A live region per cell would announce the whole grid on every keystroke, and\n * a message rendered only in the cell is unreachable when the failing row is\n * scrolled out — which is the case #110 names.\n */\nexport function EntityDetailGrid({\n descriptor,\n rows,\n onRowsChange,\n errors,\n editing = false,\n isLoading = false,\n skeleton = true,\n footer,\n className,\n}: EntityDetailGridProps) {\n const t = useT();\n const gridId = useId();\n\n const described = useMemo(\n () =>\n descriptor.childType === undefined\n ? []\n : editableChildColumns(describeChildColumns(descriptor.childType)),\n [descriptor.childType],\n );\n const columns = useLocalizedDescriptors(described);\n\n // Only the messages that name *this* member. The form hands over its whole\n // error map so a record with two owned collections needs no bookkeeping at\n // the call site, and a grid that filtered nothing would show the other's.\n const rowErrors = useMemo(\n () => readRowErrors(descriptor.name, errors),\n [descriptor.name, errors],\n );\n\n const showSkeleton = isLoading && skeleton !== false;\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n // One more than the columns: every row carries its remove control.\n const columnCount = columns.length + (editing ? 1 : 0);\n\n const setRow = (index: number, member: string, value: string) => {\n onRowsChange(\n rows.map((row, position) =>\n position === index ? { ...row, [member]: value } : row,\n ),\n );\n };\n\n const firstColumn = columns[0]?.name;\n\n const appendRow = () => {\n onRowsChange([...rows, seedEmptyRow(columns)]);\n focusCell(gridId, rows.length, firstColumn);\n };\n\n const removeRow = (index: number) => {\n onRowsChange(rows.filter((_row, position) => position !== index));\n // Focus the row that slid into this position, or the one above it when the\n // removed row was last — never nothing, which drops the user to the page.\n focusCell(gridId, Math.min(index, rows.length - 2), firstColumn);\n };\n\n /**\n * `Enter` anywhere in the **last row** appends the next one.\n *\n * Bound to the row rather than to its final cell, which is what it looked\n * like it should be: the last *column* is whatever the child declared last,\n * and a read-only member there renders a disabled input that never receives a\n * key at all — so the binding would exist and be unreachable, on exactly the\n * entities that have a computed column. The row is the unit the user is\n * finishing anyway.\n *\n * Only from a text input: a `<select>` opens on Enter and a checkbox toggles\n * on Space, so taking the key from those would break a control's own\n * behaviour to add a row the user did not ask for. A read-only column needs\n * no test of its own — its input is `disabled`, so it never takes focus and\n * never raises a key here at all.\n */\n const onLastRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n if (event.key !== 'Enter') return;\n if (!(event.target instanceof HTMLInputElement)) return;\n event.preventDefault();\n appendRow();\n };\n\n return (\n <Stack gap=\"2xs\" className={className}>\n <div className=\"flex items-center justify-between gap-s\">\n <span className=\"text-step-sm font-medium text-content-muted\">\n {descriptor.label}\n </span>\n {editing && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={appendRow}\n disabled={isLoading}\n >\n {t('detail.addRow')}\n </Button>\n )}\n </div>\n\n {editing && rowErrors.size > 0 && (\n <div role=\"alert\" className=\"text-step-sm text-danger\">\n {t('detail.errorSummary', { count: rowErrors.size })}\n </div>\n )}\n\n <Table\n aria-label={descriptor.label}\n aria-busy={isLoading || undefined}\n aria-rowcount={rows.length}\n >\n <TableHead>\n <TableRow>\n {columns.map(column => (\n <TableHeaderCell\n key={column.name}\n id={`${gridId}-${column.name}`}\n >\n {column.label}\n </TableHeaderCell>\n ))}\n {editing && (\n <TableHeaderCell align=\"end\">\n <span className=\"sr-only\">{t('detail.rowActions')}</span>\n </TableHeaderCell>\n )}\n </TableRow>\n </TableHead>\n\n <TableBody>\n {showSkeleton\n ? (customSkeleton ?? (\n <SkeletonRows\n columns={columns.length}\n extra={editing ? 1 : 0}\n />\n ))\n : rows.map((row, index) => (\n <TableRow\n key={row[ROW_KEY]}\n onKeyDown={\n editing && index === rows.length - 1\n ? onLastRowKeyDown\n : undefined\n }\n >\n {columns.map(column => {\n const error = rowErrors.get(\n rowFieldPath(descriptor.name, index, column.name),\n );\n const cellId = `${gridId}-${index}-${column.name}`;\n return (\n <TableCell key={column.name}>\n {editing ? (\n <FieldControl\n descriptor={column}\n value={row[column.name] ?? ''}\n id={cellId}\n aria-labelledby={`${gridId}-${column.name}`}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={\n error === undefined\n ? undefined\n : `${cellId}-error`\n }\n onChange={next => setRow(index, column.name, next)}\n />\n ) : (\n <span>{row[column.name] ?? ''}</span>\n )}\n {editing && error !== undefined && (\n <span\n id={`${cellId}-error`}\n className=\"mt-3xs block text-step-sm text-danger\"\n >\n {error}\n </span>\n )}\n </TableCell>\n );\n })}\n {editing && (\n <TableCell align=\"end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeRow(index)}\n >\n {t('detail.removeRow', { row: index + 1 })}\n </Button>\n </TableCell>\n )}\n </TableRow>\n ))}\n\n {!showSkeleton && rows.length === 0 && (\n <TableMessageRow colSpan={Math.max(columnCount, 1)}>\n {t('detail.empty')}\n </TableMessageRow>\n )}\n </TableBody>\n </Table>\n\n {footer !== undefined && !showSkeleton && (\n <div className={cn('flex justify-end text-step-sm text-content')}>\n {footer(rows)}\n </div>\n )}\n </Stack>\n );\n}\n\n/** The placeholder, shaped like the grid it stands in for. */\nfunction SkeletonRows({ columns, extra }: { columns: number; extra: number }) {\n return (\n <>\n {Array.from({ length: SKELETON_ROW_COUNT }, (_row, index) => (\n <TableRow key={index}>\n {Array.from({ length: columns + extra }, (_cell, cellIndex) => (\n <TableCell key={cellIndex}>\n <Skeleton className=\"h-8 w-full\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n </>\n );\n}\n\n/**\n * A blank row, seeded with every column the grid edits.\n *\n * Every member is present and empty rather than absent, for the reason a form's\n * seed carries every key: a control handed `undefined` flips from controlled to\n * uncontrolled the moment the user types into it.\n */\nfunction seedEmptyRow(\n columns: readonly EntityFieldDescriptor[],\n): EntityRowDraft {\n const row: EntityRowDraft = { [ROW_KEY]: newRowKey() };\n for (const column of columns) row[column.name] = '';\n return row;\n}\n\n/**\n * Moves focus into a cell after the list changed.\n *\n * By element id rather than through a ref into the `<tbody>`, because the row\n * being focused does not exist yet when this is called — it is the row the\n * caller has just asked React to render. Deferring to a microtask lets that\n * render commit first; `getElementById` then finds the input the same way the\n * cell's own `aria-describedby` does.\n *\n * A row index that names nothing simply focuses nothing, which is the right\n * outcome for removing the only row: there is no cell to land in, and stealing\n * focus somewhere arbitrary is worse than leaving it where the user put it.\n */\nfunction focusCell(gridId: string, index: number, column?: string): void {\n if (column === undefined || index < 0) return;\n\n queueMicrotask(() => {\n document.getElementById(`${gridId}-${index}-${column}`)?.focus();\n });\n}\n\n/** The messages naming a row of `member`, keyed by their full path. */\nfunction readRowErrors(\n member: string,\n errors: Record<string, string> | undefined,\n): Map<string, string> {\n const found = new Map<string, string>();\n if (errors === undefined) return found;\n\n for (const [path, message] of Object.entries(errors)) {\n if (parseRowFieldPath(path)?.member === member) found.set(path, message);\n }\n return found;\n}\n"],"names":["describeChildColumns","editableChildColumns","newRowKey","parseRowFieldPath","ROW_KEY","rowFieldPath","useId","useMemo","useLocalizedDescriptors","useT","Button","FieldControl","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","Stack","cn","SKELETON_ROW_COUNT","EntityDetailGrid","descriptor","rows","onRowsChange","errors","editing","isLoading","skeleton","footer","className","columns","t","gridId","described","childType","undefined","rowErrors","readRowErrors","name","showSkeleton","customSkeleton","columnCount","length","setRow","index","member","value","map","row","position","firstColumn","appendRow","seedEmptyRow","focusCell","removeRow","filter","_row","Math","min","onLastRowKeyDown","event","key","target","HTMLInputElement","preventDefault","gap","div","span","label","type","variant","size","onClick","disabled","role","count","aria-label","aria-busy","aria-rowcount","column","id","align","SkeletonRows","extra","onKeyDown","error","get","cellId","aria-labelledby","aria-invalid","aria-describedby","onChange","next","colSpan","max","Array","from","_cell","cellIndex","queueMicrotask","document","getElementById","focus","found","Map","path","message","Object","entries","set"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"sourcesContent":["'use client';\n\nimport {\n describeChildColumns,\n editableChildColumns,\n type EntityFieldDescriptor,\n type EntityRowDraft,\n newRowKey,\n parseRowFieldPath,\n ROW_KEY,\n rowFieldPath,\n} from '@entifix/core';\nimport { type KeyboardEvent, useId, useMemo } from 'react';\n\nimport { useLocalizedDescriptors, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { EntityDetailGridProps } from './entity-detail-grid.types.js';\n\n/** Rows the placeholder stands in for. Enough to read as a grid, not a page. */\nconst SKELETON_ROW_COUNT = 3;\n\n/**\n * The rows a record **owns**, edited in place.\n *\n * A `composition` — an order and its lines — where the rows have no life outside\n * the master and go out in the same write\n *. Not\n * a `linkCollection`, which is association to records that exist on their own\n * and save separately; that editor is still #26.\n *\n * ## Why this is not `EntityTable`\n *\n * `EntityTable` is a *server* listing: pages, RSQL filtering and sorting,\n * column personalization, a selection model and a bulk bar. A detail grid has\n * none of those and cannot have them — its rows are local, unsaved, and\n * unqueryable by construction, since a collection member is never `filterable`.\n * Sharing the organism would mean every one of those features growing a\n * \"detail\" branch. It shares the `Table` **atoms** instead, which is the level\n * the two actually have in common.\n *\n * ## Inline cells, and why Enter is free\n *\n * Each cell is a {@link FieldControl}, the same descriptor→input map the form\n * uses, so a child's `enum` gets a select and its `date` a date input with no\n * code here. `Tab` walks the row natively and crosses into the next; `Enter` on\n * the last row appends and focuses the new row's first cell.\n *\n * That last binding is available only because `EntityForm` renders a `<Card>`\n * with a `<button onClick>` Save rather than a `<form>` — inside a real form,\n * `Enter` in a text input submits, and appending a row would fight the browser\n * for it. Wrapping the form in a `<form>` element later would break this, which\n * is the one thing worth knowing before doing it.\n *\n * ## Errors\n *\n * A cell carries `aria-invalid` and points at its own message; the grid carries\n * **one** summary above it, which is the only `role=\"alert\"` in the component.\n * A live region per cell would announce the whole grid on every keystroke, and\n * a message rendered only in the cell is unreachable when the failing row is\n * scrolled out — the case a long grid always meets.\n */\nexport function EntityDetailGrid({\n descriptor,\n rows,\n onRowsChange,\n errors,\n editing = false,\n isLoading = false,\n skeleton = true,\n footer,\n className,\n}: EntityDetailGridProps) {\n const t = useT();\n const gridId = useId();\n\n const described = useMemo(\n () =>\n descriptor.childType === undefined\n ? []\n : editableChildColumns(describeChildColumns(descriptor.childType)),\n [descriptor.childType],\n );\n const columns = useLocalizedDescriptors(described);\n\n // Only the messages that name *this* member. The form hands over its whole\n // error map so a record with two owned collections needs no bookkeeping at\n // the call site, and a grid that filtered nothing would show the other's.\n const rowErrors = useMemo(\n () => readRowErrors(descriptor.name, errors),\n [descriptor.name, errors],\n );\n\n const showSkeleton = isLoading && skeleton !== false;\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n // One more than the columns: every row carries its remove control.\n const columnCount = columns.length + (editing ? 1 : 0);\n\n const setRow = (index: number, member: string, value: string) => {\n onRowsChange(\n rows.map((row, position) =>\n position === index ? { ...row, [member]: value } : row,\n ),\n );\n };\n\n const firstColumn = columns[0]?.name;\n\n const appendRow = () => {\n onRowsChange([...rows, seedEmptyRow(columns)]);\n focusCell(gridId, rows.length, firstColumn);\n };\n\n const removeRow = (index: number) => {\n onRowsChange(rows.filter((_row, position) => position !== index));\n // Focus the row that slid into this position, or the one above it when the\n // removed row was last — never nothing, which drops the user to the page.\n focusCell(gridId, Math.min(index, rows.length - 2), firstColumn);\n };\n\n /**\n * `Enter` anywhere in the **last row** appends the next one.\n *\n * Bound to the row rather than to its final cell, which is what it looked\n * like it should be: the last *column* is whatever the child declared last,\n * and a read-only member there renders a disabled input that never receives a\n * key at all — so the binding would exist and be unreachable, on exactly the\n * entities that have a computed column. The row is the unit the user is\n * finishing anyway.\n *\n * Only from a text input: a `<select>` opens on Enter and a checkbox toggles\n * on Space, so taking the key from those would break a control's own\n * behaviour to add a row the user did not ask for. A read-only column needs\n * no test of its own — its input is `disabled`, so it never takes focus and\n * never raises a key here at all.\n */\n const onLastRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n if (event.key !== 'Enter') return;\n if (!(event.target instanceof HTMLInputElement)) return;\n event.preventDefault();\n appendRow();\n };\n\n return (\n <Stack gap=\"2xs\" className={className}>\n <div className=\"flex items-center justify-between gap-s\">\n <span className=\"text-step-sm font-medium text-content-muted\">\n {descriptor.label}\n </span>\n {editing && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={appendRow}\n disabled={isLoading}\n >\n {t('detail.addRow')}\n </Button>\n )}\n </div>\n\n {editing && rowErrors.size > 0 && (\n <div role=\"alert\" className=\"text-step-sm text-danger\">\n {t('detail.errorSummary', { count: rowErrors.size })}\n </div>\n )}\n\n <Table\n aria-label={descriptor.label}\n aria-busy={isLoading || undefined}\n aria-rowcount={rows.length}\n >\n <TableHead>\n <TableRow>\n {columns.map(column => (\n <TableHeaderCell\n key={column.name}\n id={`${gridId}-${column.name}`}\n >\n {column.label}\n </TableHeaderCell>\n ))}\n {editing && (\n <TableHeaderCell align=\"end\">\n <span className=\"sr-only\">{t('detail.rowActions')}</span>\n </TableHeaderCell>\n )}\n </TableRow>\n </TableHead>\n\n <TableBody>\n {showSkeleton\n ? (customSkeleton ?? (\n <SkeletonRows\n columns={columns.length}\n extra={editing ? 1 : 0}\n />\n ))\n : rows.map((row, index) => (\n <TableRow\n key={row[ROW_KEY]}\n onKeyDown={\n editing && index === rows.length - 1\n ? onLastRowKeyDown\n : undefined\n }\n >\n {columns.map(column => {\n const error = rowErrors.get(\n rowFieldPath(descriptor.name, index, column.name),\n );\n const cellId = `${gridId}-${index}-${column.name}`;\n return (\n <TableCell key={column.name}>\n {editing ? (\n <FieldControl\n descriptor={column}\n value={row[column.name] ?? ''}\n id={cellId}\n aria-labelledby={`${gridId}-${column.name}`}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={\n error === undefined\n ? undefined\n : `${cellId}-error`\n }\n onChange={next => setRow(index, column.name, next)}\n />\n ) : (\n <span>{row[column.name] ?? ''}</span>\n )}\n {editing && error !== undefined && (\n <span\n id={`${cellId}-error`}\n className=\"mt-3xs block text-step-sm text-danger\"\n >\n {error}\n </span>\n )}\n </TableCell>\n );\n })}\n {editing && (\n <TableCell align=\"end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeRow(index)}\n >\n {t('detail.removeRow', { row: index + 1 })}\n </Button>\n </TableCell>\n )}\n </TableRow>\n ))}\n\n {!showSkeleton && rows.length === 0 && (\n <TableMessageRow colSpan={Math.max(columnCount, 1)}>\n {t('detail.empty')}\n </TableMessageRow>\n )}\n </TableBody>\n </Table>\n\n {footer !== undefined && !showSkeleton && (\n <div className={cn('flex justify-end text-step-sm text-content')}>\n {footer(rows)}\n </div>\n )}\n </Stack>\n );\n}\n\n/** The placeholder, shaped like the grid it stands in for. */\nfunction SkeletonRows({ columns, extra }: { columns: number; extra: number }) {\n return (\n <>\n {Array.from({ length: SKELETON_ROW_COUNT }, (_row, index) => (\n <TableRow key={index}>\n {Array.from({ length: columns + extra }, (_cell, cellIndex) => (\n <TableCell key={cellIndex}>\n <Skeleton className=\"h-8 w-full\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n </>\n );\n}\n\n/**\n * A blank row, seeded with every column the grid edits.\n *\n * Every member is present and empty rather than absent, for the reason a form's\n * seed carries every key: a control handed `undefined` flips from controlled to\n * uncontrolled the moment the user types into it.\n */\nfunction seedEmptyRow(\n columns: readonly EntityFieldDescriptor[],\n): EntityRowDraft {\n const row: EntityRowDraft = { [ROW_KEY]: newRowKey() };\n for (const column of columns) row[column.name] = '';\n return row;\n}\n\n/**\n * Moves focus into a cell after the list changed.\n *\n * By element id rather than through a ref into the `<tbody>`, because the row\n * being focused does not exist yet when this is called — it is the row the\n * caller has just asked React to render. Deferring to a microtask lets that\n * render commit first; `getElementById` then finds the input the same way the\n * cell's own `aria-describedby` does.\n *\n * A row index that names nothing simply focuses nothing, which is the right\n * outcome for removing the only row: there is no cell to land in, and stealing\n * focus somewhere arbitrary is worse than leaving it where the user put it.\n */\nfunction focusCell(gridId: string, index: number, column?: string): void {\n if (column === undefined || index < 0) return;\n\n queueMicrotask(() => {\n document.getElementById(`${gridId}-${index}-${column}`)?.focus();\n });\n}\n\n/** The messages naming a row of `member`, keyed by their full path. */\nfunction readRowErrors(\n member: string,\n errors: Record<string, string> | undefined,\n): Map<string, string> {\n const found = new Map<string, string>();\n if (errors === undefined) return found;\n\n for (const [path, message] of Object.entries(errors)) {\n if (parseRowFieldPath(path)?.member === member) found.set(path, message);\n }\n return found;\n}\n"],"names":["describeChildColumns","editableChildColumns","newRowKey","parseRowFieldPath","ROW_KEY","rowFieldPath","useId","useMemo","useLocalizedDescriptors","useT","Button","FieldControl","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","Stack","cn","SKELETON_ROW_COUNT","EntityDetailGrid","descriptor","rows","onRowsChange","errors","editing","isLoading","skeleton","footer","className","columns","t","gridId","described","childType","undefined","rowErrors","readRowErrors","name","showSkeleton","customSkeleton","columnCount","length","setRow","index","member","value","map","row","position","firstColumn","appendRow","seedEmptyRow","focusCell","removeRow","filter","_row","Math","min","onLastRowKeyDown","event","key","target","HTMLInputElement","preventDefault","gap","div","span","label","type","variant","size","onClick","disabled","role","count","aria-label","aria-busy","aria-rowcount","column","id","align","SkeletonRows","extra","onKeyDown","error","get","cellId","aria-labelledby","aria-invalid","aria-describedby","onChange","next","colSpan","max","Array","from","_cell","cellIndex","queueMicrotask","document","getElementById","focus","found","Map","path","message","Object","entries","set"],"mappings":"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"}