@cosmicdrift/kumiko-renderer 0.205.0 → 0.207.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.205.0",
3
+ "version": "0.207.0",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -15,8 +15,8 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.205.0",
19
- "@cosmicdrift/kumiko-headless": "0.205.0",
18
+ "@cosmicdrift/kumiko-framework": "0.207.0",
19
+ "@cosmicdrift/kumiko-headless": "0.207.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2",
22
22
  "zod": "^4.4.3"
@@ -6,7 +6,7 @@
6
6
  import { describe, expect, test } from "bun:test";
7
7
  import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
8
8
  import type { FeatureSchema } from "../feature-schema";
9
- import { formatPath, parsePath, resolveTarget } from "../nav";
9
+ import { formatPath, hasDetailScreen, parsePath, resolveTarget } from "../nav";
10
10
 
11
11
  describe("parsePath — ohne Workspaces", () => {
12
12
  test("/<screenId>", () => {
@@ -126,10 +126,14 @@ describe("resolveTarget", () => {
126
126
  expect(resolveTarget([], target)).toBe(target);
127
127
  });
128
128
 
129
+ // Short screenId, not the qualified "property:screen:lease-detail" —
130
+ // ScreenTarget.screenId round-trips through formatPath/parsePath as a
131
+ // short id; RoutedScreen re-qualifies it itself (fw#2164 fix, previously
132
+ // double-qualified and silently fell back to the app's default screen).
129
133
  test("ObjectTarget resolves via the matching custom detailFor screen", () => {
130
134
  const schema = schemaWith("property", [leaseDetailScreen]);
131
135
  expect(resolveTarget([schema], { entity: "lease", id: "l-1" })).toEqual({
132
- screenId: "property:screen:lease-detail",
136
+ screenId: "lease-detail",
133
137
  entityId: "l-1",
134
138
  });
135
139
  });
@@ -146,7 +150,7 @@ describe("resolveTarget", () => {
146
150
  leaseDetailScreen,
147
151
  ]);
148
152
  expect(resolveTarget([schema], { entity: "lease", id: "l-1" })).toEqual({
149
- screenId: "property:screen:lease-detail",
153
+ screenId: "lease-detail",
150
154
  entityId: "l-1",
151
155
  });
152
156
  });
@@ -156,7 +160,7 @@ describe("resolveTarget", () => {
156
160
  const resolved = resolveTarget([schema], { entity: "lease", id: "l-1", workspaceId: "admin" });
157
161
  expect(resolved).toEqual({
158
162
  workspaceId: "admin",
159
- screenId: "property:screen:lease-detail",
163
+ screenId: "lease-detail",
160
164
  entityId: "l-1",
161
165
  });
162
166
  });
@@ -172,3 +176,23 @@ describe("resolveTarget", () => {
172
176
  expect(() => resolveTarget([schema], { entity: "boat", id: "b-1" })).toThrow(/"boat"/);
173
177
  });
174
178
  });
179
+
180
+ // hasDetailScreen: non-throwing existence check create-app's row-click
181
+ // default (fw#2164) uses to pick between a detailFor screen and its other
182
+ // fallbacks — resolveTarget's throw isn't usable there since not resolving
183
+ // is the expected, non-error case for most entities.
184
+ describe("hasDetailScreen", () => {
185
+ test("true when a screen declares detailFor for the entity", () => {
186
+ const schema = schemaWith("property", [leaseDetailScreen]);
187
+ expect(hasDetailScreen([schema], "lease")).toBe(true);
188
+ });
189
+
190
+ test("false when no screen declares detailFor for the entity", () => {
191
+ const schema = schemaWith("property", [leaseListScreen, leaseEditScreen]);
192
+ expect(hasDetailScreen([schema], "lease")).toBe(false);
193
+ });
194
+
195
+ test("false for an empty feature list", () => {
196
+ expect(hasDetailScreen([], "lease")).toBe(false);
197
+ });
198
+ });
package/src/app/nav.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import { createContext, type ReactNode, useContext } from "react";
2
2
  import type { FeatureSchema } from "./feature-schema";
3
- import { qualifyScreenId } from "./qualify-screen-id";
4
3
 
5
4
  // Navigation-Contract, plattform-neutral. Types + Context + Hook leben
6
5
  // hier; die konkrete Implementation (window.history im Web,
@@ -118,21 +117,45 @@ export function formatPath(target: ScreenTarget): string {
118
117
  return `/${segments.join("/")}`;
119
118
  }
120
119
 
120
+ // Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
121
+ // branch and hasDetailScreen (create-app's row-click default, fw#2164) both
122
+ // need "is there a detail screen for this entity", one to resolve it, the
123
+ // other to just check before falling back to a different default.
124
+ function findDetailForScreen(
125
+ features: readonly FeatureSchema[],
126
+ entity: string,
127
+ ): { readonly featureName: string; readonly screenId: string } | undefined {
128
+ for (const feature of features) {
129
+ for (const screen of feature.screens) {
130
+ if (screen.detailFor === entity)
131
+ return { featureName: feature.featureName, screenId: screen.id };
132
+ }
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ /** Non-throwing existence check for findDetailForScreen — lets a caller pick
138
+ * a different default (fw#2164's row-click fallback) instead of resolving. */
139
+ export function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean {
140
+ return findDetailForScreen(features, entity) !== undefined;
141
+ }
142
+
121
143
  // Resolves a NavTarget to the ScreenTarget form navigate/replace/hrefFor
122
144
  // operate on. Pure — no context, no I/O — so callers can use it outside
123
145
  // React too (see resolve-at-NavApi-build alternative in renderer-web).
124
146
  export function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget {
125
147
  if ("screenId" in target) return target;
126
148
 
127
- for (const feature of features) {
128
- for (const screen of feature.screens) {
129
- if (screen.detailFor !== target.entity) continue;
130
- return {
131
- ...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
132
- screenId: qualifyScreenId(feature.featureName, screen.id),
133
- entityId: target.id,
134
- };
135
- }
149
+ const found = findDetailForScreen(features, target.entity);
150
+ if (found !== undefined) {
151
+ // Short form (globally unique per validateScreenShortIdCollisions)
152
+ // ScreenTarget.screenId round-trips through formatPath/parsePath as a
153
+ // short id; qualifying it here double-qualifies at RoutedScreen.
154
+ return {
155
+ ...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
156
+ screenId: found.screenId,
157
+ entityId: target.id,
158
+ };
136
159
  }
137
160
 
138
161
  throw new Error(
@@ -1,18 +1,18 @@
1
- // Framework-Default-Bundle. Strings die die Renderer-Components hart
2
- // brauchen (Save/Cancel/Delete-Buttons, Empty-States, Search-Placeholder,
3
- // Nav-Toggle-aria-Labels, Validation-Reasons). createKumikoApp hängt das
4
- // als ALLERLETZTEN Fallback in den LocaleProvider — Apps können
5
- // einzelne Keys via clientFeatures.translations überschreiben.
1
+ // Framework default bundle. Strings the renderer components hard-require
2
+ // (save/cancel/delete buttons, empty states, search placeholder,
3
+ // nav-toggle aria-labels, validation reasons). createKumikoApp appends
4
+ // this as the very last fallback into the LocaleProvider — apps can
5
+ // override individual keys via clientFeatures.translations.
6
6
  //
7
- // Convention: alle Keys mit `kumiko.`-Prefix damit sie nicht mit
8
- // App-Keys kollidieren. Sub-Pfade gruppieren nach Bereich (actions /
9
- // list / nav / form / validation).
7
+ // Convention: all keys use the `kumiko.` prefix so they don't collide
8
+ // with app keys. Sub-paths group by area (actions / list / nav / form /
9
+ // validation).
10
10
 
11
11
  import type { TranslationsByLocale } from "./i18n";
12
12
 
13
13
  export const kumikoDefaultTranslations: TranslationsByLocale = {
14
14
  de: {
15
- // Actions — Buttons in RenderEdit, RenderList, Confirm-Dialogen.
15
+ // Actions — buttons in RenderEdit, RenderList, confirm dialogs.
16
16
  "kumiko.actions.save": "Speichern",
17
17
  "kumiko.actions.cancel": "Abbrechen",
18
18
  "kumiko.actions.delete": "Löschen",
@@ -36,7 +36,7 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
36
36
  // Toast — Self-service Docs-Link-Default (useToast docsLinkLabel).
37
37
  "kumiko.toast.learn-more": "Mehr erfahren",
38
38
 
39
- // Field — aria-Labels der Date/Timestamp-Primitives.
39
+ // Field — aria-labels for the Date/Timestamp primitives.
40
40
  "kumiko.field.open-calendar": "Kalender öffnen",
41
41
  "kumiko.field.dateField.placeholderYear": "J",
42
42
  "kumiko.field.dateField.placeholderMonth": "M",
@@ -66,7 +66,7 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
66
66
  "kumiko.list.no-entries": "Keine Einträge.",
67
67
  "kumiko.list.end-of-list": "— Ende der Liste —",
68
68
 
69
- // Pager — Status-Zeile + Prev/Next/Page-aria-Labels (DataTable pagination="pages").
69
+ // Pager — status line + prev/next/page aria-labels (DataTable pagination="pages").
70
70
  "kumiko.pager.status": "{from}–{to} von {total}",
71
71
  "kumiko.pager.previousPage": "Vorherige Seite",
72
72
  "kumiko.pager.nextPage": "Nächste Seite",
@@ -77,8 +77,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
77
77
  "kumiko.combobox.empty": "Keine Treffer.",
78
78
  "kumiko.combobox.loading": "Lade…",
79
79
 
80
- // Dashboard — Default-Label für den "(alle)"-Eintrag im Screen-Filter,
81
- // wenn DashboardFilterDefinition.allLabel nicht gesetzt ist.
80
+ // Dashboard — default label for the "(all)" entry in the screen filter,
81
+ // when DashboardFilterDefinition.allLabel isn't set.
82
82
  "kumiko.dashboard.filter.all": "Alle",
83
83
  "kumiko.combobox.placeholder": "—",
84
84
 
@@ -86,7 +86,7 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
86
86
  "kumiko.widget.loading": "Lade…",
87
87
  "kumiko.widget.error.title": "Konnte nicht geladen werden.",
88
88
 
89
- // Widgets — UploadZone Status-Zeile pro Datei.
89
+ // Widgets — UploadZone status line per file.
90
90
  "kumiko.widget.upload.uploading": "Wird hochgeladen…",
91
91
  "kumiko.widget.upload.done": "Hochgeladen",
92
92
  "kumiko.widget.upload.error": "Fehlgeschlagen",
@@ -104,6 +104,7 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
104
104
  "kumiko.nav.expand": "Aufklappen",
105
105
  "kumiko.nav.collapse": "Zuklappen",
106
106
  "kumiko.nav.search": "Navigation durchsuchen…",
107
+ "kumiko.nav.language": "Sprache",
107
108
 
108
109
  // Workspace — Switcher-Trigger aria-Label (renderer-web).
109
110
  "kumiko.workspace.switch": "Workspace wechseln",
@@ -128,10 +129,10 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
128
129
  "kumiko.aiText.style.expand": "Ausführlicher",
129
130
  "kumiko.aiText.capExceeded": "Monatliches AI-Limit erreicht.",
130
131
 
131
- // Row-Actions — Fehler-Toast wenn ein Action-Write fehlschlägt.
132
+ // Row-Actions — error toast when an action write fails.
132
133
  "kumiko.rowAction.failed": "Aktion fehlgeschlagen",
133
134
 
134
- // ContentEditor — Variablen-Chip-Leiste (VariableChips).
135
+ // ContentEditor — variable chip bar (VariableChips).
135
136
  "kumiko.contentEditor.insertVariable": "{name} einfügen",
136
137
  "kumiko.contentEditor.preview": "Vorschau",
137
138
  "kumiko.contentEditor.editMode": "Bearbeiten",
@@ -154,8 +155,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
154
155
  "kumiko.config.cascade.activeMarker": "aktiv",
155
156
  "kumiko.config.cascade.resetTo": "Überschreibung zurücksetzen ({scope})",
156
157
 
157
- // Form — Standard-Errors (App-Code kann eigene zod-Reasons nutzen,
158
- // diese sind die letzte Sicherheitsschicht).
158
+ // Form — standard errors (app code can supply its own zod reasons,
159
+ // these are the last safety layer).
159
160
  "kumiko.form.error.generic": "Etwas ist schiefgegangen.",
160
161
  "kumiko.form.error.version-conflict":
161
162
  "Datensatz wurde zwischenzeitlich geändert. Lade neu und versuche es erneut.",
@@ -166,18 +167,17 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
166
167
  "Ein offener Entwurf für dieses Formular gefunden. Möchtest du ihn fortsetzen?",
167
168
  "kumiko.form.draft.start-new": "Neu beginnen",
168
169
 
169
- // Validation — Default-Reason-Codes aus dem Framework. App-Code
170
- // kann eigene Codes via Validation-Hooks reinwerfen; die hier sind
171
- // die generischen.
170
+ // Validation — default reason codes from the framework. App code can
171
+ // inject its own via validation hooks; these are the generic ones.
172
172
  "kumiko.validation.required": "Pflichtfeld.",
173
173
  "kumiko.validation.invalid": "Ungültiger Wert.",
174
174
  "kumiko.validation.too-short": "Zu kurz (mindestens {min} Zeichen).",
175
175
  "kumiko.validation.too-long": "Zu lang (höchstens {max} Zeichen).",
176
176
  "kumiko.validation.out-of-range": "Wert außerhalb des erlaubten Bereichs.",
177
177
 
178
- // errors.validation.* — der kanonische Key-Namespace den Server
179
- // (ValidationError) und Client (zod-bridge) für Field-Issues
180
- // erzeugen. Codes = Zod-4-Issue-Codes + Framework-eigene.
178
+ // errors.validation.* — the canonical key namespace that server
179
+ // (ValidationError) and client (zod-bridge) produce for field issues.
180
+ // Codes = Zod 4 issue codes + framework-specific ones.
181
181
  "errors.validation.invalid_type": "Ungültiger Wert.",
182
182
  "errors.validation.too_small": "Zu klein oder zu kurz (Minimum: {minimum}).",
183
183
  "errors.validation.too_big": "Zu groß oder zu lang (Maximum: {maximum}).",
@@ -194,10 +194,10 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
194
194
  "errors.validation.invalid_option": "Ungültige Auswahl.",
195
195
  "errors.validation.failed": "Validierung fehlgeschlagen.",
196
196
 
197
- // errors.* — Top-Level-Error-Codes (eine pro httpStatus-Klasse). Letzter
198
- // Fallback wenn eine App keine eigene Übersetzung liefert; bewusst
199
- // generisch (keine technischen Entity-/Feature-/Key-Namen an End-User
200
- // die stecken in `details` für Devs). Apps überschreiben pro Key.
197
+ // errors.* — top-level error codes (one per httpStatus class). Last
198
+ // fallback when an app doesn't supply its own translation; deliberately
199
+ // generic (no technical entity/feature/key names shown to end users
200
+ // those live in `details` for devs). Apps override per key.
201
201
  "errors.feature.disabled": "Diese Funktion ist derzeit nicht verfügbar.",
202
202
  "errors.access.denied": "Dazu hast du keine Berechtigung.",
203
203
  "errors.notFound": "Nicht gefunden.",
@@ -295,6 +295,7 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
295
295
  "kumiko.nav.expand": "Expand",
296
296
  "kumiko.nav.collapse": "Collapse",
297
297
  "kumiko.nav.search": "Search navigation…",
298
+ "kumiko.nav.language": "Language",
298
299
 
299
300
  "kumiko.workspace.switch": "Switch workspace",
300
301
  // Workspace-Switcher fallback label when activeId points at no visible workspace.
@@ -388,4 +389,208 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
388
389
  "dispatcher.errors.network": "Network error. Please check your connection and try again.",
389
390
  "dispatcher.errors.aborted": "Request was cancelled.",
390
391
  },
392
+ es: {
393
+ // Actions — buttons in RenderEdit, RenderList, confirm dialogs.
394
+ "kumiko.actions.save": "Guardar",
395
+ "kumiko.actions.cancel": "Cancelar",
396
+ "kumiko.actions.delete": "Eliminar",
397
+ "kumiko.actions.delete-confirm": "¿Seguro que quieres eliminar?",
398
+ "kumiko.actions.reload": "Recargar",
399
+ "kumiko.actions.create": "Nuevo",
400
+ "kumiko.actions.edit": "Editar",
401
+ "kumiko.actions.copyLink": "Copiar enlace",
402
+ "kumiko.actions.copyLinkCopied": "¡Copiado!",
403
+ "kumiko.actions.next": "Siguiente",
404
+ "kumiko.actions.back": "Atrás",
405
+ "kumiko.actions.finish": "Finalizar",
406
+
407
+ // Wizard step chrome (RenderEdit layout.mode="wizard").
408
+ "kumiko.wizard.step": "Paso {current} de {total}",
409
+ "kumiko.wizard.step-with-title": "Paso {current} de {total} · {title}",
410
+
411
+ // Version — Update-Awareness-Banner (UpdateChecker).
412
+ "kumiko.version.update-available": "Hay una nueva versión disponible.",
413
+
414
+ // Toast — Self-service Docs-Link-Default (useToast docsLinkLabel).
415
+ "kumiko.toast.learn-more": "Más información",
416
+
417
+ // Field — aria-labels for the Date/Timestamp primitives.
418
+ "kumiko.field.open-calendar": "Abrir calendario",
419
+ "kumiko.field.dateField.placeholderYear": "A",
420
+ "kumiko.field.dateField.placeholderMonth": "M",
421
+ "kumiko.field.dateField.placeholderDay": "D",
422
+ "kumiko.field.time": "Hora",
423
+ "kumiko.field.timezone": "Zona horaria",
424
+ "kumiko.field.locatedTzHint": "Hora local del lugar indicado",
425
+ "kumiko.field.reference-created-no-id":
426
+ "El registro se creó pero no se pudo seleccionar automáticamente. Selecciónalo manualmente.",
427
+ "kumiko.field.unsupported": "Este tipo de campo todavía no se puede editar aquí.",
428
+ "kumiko.field.embedded-list.add-row": "Añadir fila",
429
+ "kumiko.field.embedded-list.remove-row": "Quitar fila",
430
+ "kumiko.field.embedded-list.duplicate-row": "Duplicar fila",
431
+ "kumiko.field.embedded-list.move-up": "Mover arriba",
432
+ "kumiko.field.embedded-list.move-down": "Mover abajo",
433
+ "kumiko.field.embedded-list.empty": "Todavía no hay filas.",
434
+ "kumiko.field.embedded-list.empty-cta": "Añadir la primera fila",
435
+ "kumiko.field.embedded-list.paste-rows-truncated":
436
+ "Se descartaron {count} fila(s) pegadas (se alcanzó el número máximo de filas).",
437
+ "kumiko.field.embedded-list.paste-cells-unmatched":
438
+ "{count} celda(s) no tenían una opción coincidente y no se modificaron.",
439
+
440
+ // List — DataTable Toolbar, Empty-State, Search.
441
+ "kumiko.list.search-placeholder": "Buscar…",
442
+ "kumiko.list.empty.title": "Todavía no hay entradas.",
443
+ "kumiko.list.empty.hint": "Crea la primera para empezar.",
444
+ "kumiko.list.no-entries": "Sin entradas.",
445
+ "kumiko.list.end-of-list": "— Fin de la lista —",
446
+
447
+ // Pager — status line + prev/next/page aria-labels (DataTable pagination="pages").
448
+ "kumiko.pager.status": "{from}–{to} de {total}",
449
+ "kumiko.pager.previousPage": "Página anterior",
450
+ "kumiko.pager.nextPage": "Página siguiente",
451
+ "kumiko.pager.page": "Página {entry}",
452
+
453
+ // Combobox — Tier 2.1c Searchable-Select.
454
+ "kumiko.combobox.search-placeholder": "Buscar…",
455
+ "kumiko.combobox.empty": "Sin resultados.",
456
+ "kumiko.combobox.loading": "Cargando…",
457
+
458
+ // Dashboard — default label for the "(all)" entry in the screen filter,
459
+ // when DashboardFilterDefinition.allLabel isn't set.
460
+ "kumiko.dashboard.filter.all": "Todos",
461
+ "kumiko.combobox.placeholder": "—",
462
+
463
+ // Widgets — Query-States (QueryTable, LoadingState, ErrorState).
464
+ "kumiko.widget.loading": "Cargando…",
465
+ "kumiko.widget.error.title": "No se pudo cargar.",
466
+
467
+ // Widgets — UploadZone status line per file.
468
+ "kumiko.widget.upload.uploading": "Subiendo…",
469
+ "kumiko.widget.upload.done": "Subido",
470
+ "kumiko.widget.upload.error": "Fallido",
471
+ "kumiko.widget.upload.rejected-type": "Tipo de archivo no permitido",
472
+
473
+ // Widgets — StepBar screen-reader text for completed steps whose number is visually replaced by a checkmark.
474
+ "kumiko.widget.step-bar.done": "Hecho",
475
+
476
+ // Widgets — Drawer resize handle + maximize toggle aria-labels.
477
+ "kumiko.widget.drawer.restore": "Restablecer ancho del panel",
478
+ "kumiko.widget.drawer.maximize": "Maximizar panel",
479
+ "kumiko.widget.drawer.resize": "Cambiar tamaño del panel",
480
+
481
+ // Nav — Sidebar Tree (Toggle-aria-Labels).
482
+ "kumiko.nav.expand": "Expandir",
483
+ "kumiko.nav.collapse": "Contraer",
484
+ "kumiko.nav.search": "Buscar en la navegación…",
485
+ "kumiko.nav.language": "Idioma",
486
+
487
+ // Workspace — Switcher-Trigger aria-Label (renderer-web).
488
+ "kumiko.workspace.switch": "Cambiar de espacio de trabajo",
489
+ "kumiko.workspace.select": "Selecciona un espacio de trabajo",
490
+
491
+ // Dialog — Confirm-Buttons + Close-aria-Label.
492
+ "kumiko.dialog.confirm": "Confirmar",
493
+ "kumiko.dialog.cancel": "Cancelar",
494
+ "kumiko.dialog.close": "Cerrar",
495
+
496
+ // AiTextField/AiTextArea — Ghost-Text-Hint, Toolbar-Aria-Labels, Diff-Dialog.
497
+ "kumiko.aiText.acceptHint": "Tab = aceptar, Esc = descartar",
498
+ "kumiko.aiText.correct": "Corregir",
499
+ "kumiko.aiText.translate": "Traducir",
500
+ "kumiko.aiText.rewrite": "Reescribir",
501
+ "kumiko.aiText.diff.before": "Antes",
502
+ "kumiko.aiText.diff.after": "Después",
503
+ "kumiko.aiText.diff.generating": "Generando…",
504
+ "kumiko.aiText.style.formal": "Formal",
505
+ "kumiko.aiText.style.casual": "Informal",
506
+ "kumiko.aiText.style.concise": "Conciso",
507
+ "kumiko.aiText.style.expand": "Ampliar",
508
+ "kumiko.aiText.capExceeded": "Se alcanzó el límite mensual de IA.",
509
+
510
+ // Row-Actions — error toast when an action write fails.
511
+ "kumiko.rowAction.failed": "La acción falló",
512
+
513
+ // ContentEditor — variable chip bar (VariableChips).
514
+ "kumiko.contentEditor.insertVariable": "Insertar {name}",
515
+ "kumiko.contentEditor.preview": "Vista previa",
516
+ "kumiko.contentEditor.editMode": "Editar",
517
+ "kumiko.contentEditor.bold": "Negrita",
518
+ "kumiko.contentEditor.italic": "Cursiva",
519
+ "kumiko.contentEditor.heading1": "Encabezado 1",
520
+ "kumiko.contentEditor.heading2": "Encabezado 2",
521
+ "kumiko.contentEditor.bulletList": "Lista con viñetas",
522
+ "kumiko.contentEditor.orderedList": "Lista numerada",
523
+
524
+ // Config-Cascade — Source-Badges + Cascade-Panel (ConfigCascadeView).
525
+ "kumiko.config.source.user": "Mi valor",
526
+ "kumiko.config.source.tenant": "Tenant",
527
+ "kumiko.config.source.system": "Sistema",
528
+ "kumiko.config.source.appOverride": "Anulación de la app",
529
+ "kumiko.config.source.computed": "Calculado",
530
+ "kumiko.config.source.default": "Predeterminado",
531
+ "kumiko.config.source.missing": "Falta",
532
+ "kumiko.config.cascade.noValue": "Sin valor establecido",
533
+ "kumiko.config.cascade.activeMarker": "activo",
534
+ "kumiko.config.cascade.resetTo": "Restablecer anulación ({scope})",
535
+
536
+ // Form — standard errors (app code can supply its own zod reasons,
537
+ // these are the last safety layer).
538
+ "kumiko.form.error.generic": "Algo salió mal.",
539
+ "kumiko.form.error.version-conflict":
540
+ "El registro se modificó mientras tanto. Recarga e inténtalo de nuevo.",
541
+ "kumiko.form.extension.save-failed": "No se pudo guardar un campo adicional.",
542
+ "kumiko.form.draft.resume-multiple":
543
+ "Se encontraron varios borradores abiertos para este formulario. ¿Cuál quieres continuar?",
544
+ "kumiko.form.draft.resume-single":
545
+ "Se encontró un borrador abierto para este formulario. ¿Quieres continuarlo?",
546
+ "kumiko.form.draft.start-new": "Empezar de nuevo",
547
+
548
+ // Validation — default reason codes from the framework. App code can
549
+ // inject its own via validation hooks; these are the generic ones.
550
+ "kumiko.validation.required": "Campo obligatorio.",
551
+ "kumiko.validation.invalid": "Valor no válido.",
552
+ "kumiko.validation.too-short": "Demasiado corto (mínimo {min} caracteres).",
553
+ "kumiko.validation.too-long": "Demasiado largo (máximo {max} caracteres).",
554
+ "kumiko.validation.out-of-range": "Valor fuera del rango permitido.",
555
+
556
+ // errors.validation.* — the canonical key namespace that server
557
+ // (ValidationError) and client (zod-bridge) produce for field issues.
558
+ // Codes = Zod 4 issue codes + framework-specific ones.
559
+ "errors.validation.invalid_type": "Valor no válido.",
560
+ "errors.validation.too_small": "Demasiado pequeño o demasiado corto (mínimo: {minimum}).",
561
+ "errors.validation.too_big": "Demasiado grande o demasiado largo (máximo: {maximum}).",
562
+ "errors.validation.invalid_format": "Formato no válido.",
563
+ "errors.validation.not_multiple_of": "Debe ser un múltiplo de {divisor}.",
564
+ "errors.validation.unrecognized_keys": "Campos desconocidos.",
565
+ "errors.validation.invalid_union": "Valor no válido.",
566
+ "errors.validation.invalid_key": "Clave no válida.",
567
+ "errors.validation.invalid_element": "Entrada no válida.",
568
+ "errors.validation.invalid_value": "Selección no válida.",
569
+ "errors.validation.custom": "Valor no válido.",
570
+ "errors.validation.unexpected_field": "Campo desconocido.",
571
+ "errors.validation.out_of_bounds": "Valor fuera del rango permitido.",
572
+ "errors.validation.invalid_option": "Selección no válida.",
573
+ "errors.validation.failed": "Validación fallida.",
574
+
575
+ // errors.* — top-level error codes (one per httpStatus class). Last
576
+ // fallback when an app doesn't supply its own translation; deliberately
577
+ // generic (no technical entity/feature/key names shown to end users —
578
+ // those live in `details` for devs). Apps override per key.
579
+ "errors.feature.disabled": "Esta función no está disponible actualmente.",
580
+ "errors.access.denied": "No tienes permiso para hacer esto.",
581
+ "errors.notFound": "No encontrado.",
582
+ "errors.conflict": "Conflicto — no se pudo completar la operación.",
583
+ "errors.versionConflict":
584
+ "El registro se modificó mientras tanto. Recarga e inténtalo de nuevo.",
585
+ "errors.uniqueViolation": "Esta entrada ya existe.",
586
+ "errors.unprocessable": "No se pudo procesar la solicitud.",
587
+ "errors.unconfigured": "Esta función todavía no está configurada.",
588
+ "errors.internal": "Algo salió mal. Inténtalo de nuevo más tarde.",
589
+ "errors.rate_limited": "Demasiadas solicitudes. Inténtalo de nuevo en breve.",
590
+ "errors.cap.exceeded": "Límite alcanzado. Mejora tu plan o espera al siguiente período.",
591
+ "errors.download.urlMissing": "Descarga no disponible — inténtalo de nuevo.",
592
+ "auth.errors.originNotAllowed": "No se permiten solicitudes desde este origen.",
593
+ "dispatcher.errors.network": "Error de red. Comprueba tu conexión e inténtalo de nuevo.",
594
+ "dispatcher.errors.aborted": "Solicitud cancelada.",
595
+ },
391
596
  };
package/src/index.ts CHANGED
@@ -72,7 +72,14 @@ export type {
72
72
  ObjectTarget,
73
73
  ScreenTarget,
74
74
  } from "./app/nav";
75
- export { formatPath, NavProvider, parsePath, resolveTarget, useNav } from "./app/nav";
75
+ export {
76
+ formatPath,
77
+ hasDetailScreen,
78
+ NavProvider,
79
+ parsePath,
80
+ resolveTarget,
81
+ useNav,
82
+ } from "./app/nav";
76
83
  export { lastSegment } from "./app/qn";
77
84
  export type { VariableChipsProps } from "./app/variable-chips";
78
85
  export { VariableChips } from "./app/variable-chips";