feat(17-02): repeater field and dependent dropdown states

- RepeaterField: rows with drag handle, index, titleFrom title, error chip,
  move up/down (buttons, ArrowUp/ArrowDown on the handle, native drag and
  drop), expand/collapse, remove (asks only for a row holding a value),
  add with nested defaults, maxItems counter, empty state, collapse all;
  every change edits form state only
- 422 row paths (content.3.answer) open the row, mark it and focus the
  nested control; the banner no longer repeats row messages
- DropdownField dependsOn: waiting, loading, not used, empty, loaded with
  depth indent, failed with retry; cached per sibling value, last request
  wins, a stale value is cleared after a sibling change and announced
- FormView provides form feedback (errors and notices by row path, the
  polite live region, its confirm dialog) and the options cache; FormGrid
  provides the sibling scope for repeater rows
- repeater.* and form.dependent_* phrase keys (en, pl), smoke specs, docs
This commit is contained in:
2026-10-12 16:08:23 +02:00
parent 4a5e896429
commit d3bf5d8f86
17 changed files with 1432 additions and 60 deletions

View File

@@ -252,6 +252,27 @@
"backend::lang.form.deleting": {
"other": "Usuwanie…"
},
"backend::lang.form.dependent_cleared": {
"other": "Wyczyszczono pole „:field”, bo zmieniły się jego opcje."
},
"backend::lang.form.dependent_empty": {
"other": "Brak rekordów typu „:type”. Najpierw dodaj rekord albo wybierz inny typ."
},
"backend::lang.form.dependent_failed": {
"other": "Nie udało się wczytać opcji."
},
"backend::lang.form.dependent_invalid": {
"other": "Wybierz jedną z dostępnych opcji."
},
"backend::lang.form.dependent_not_used": {
"other": "Nie dotyczy tego wyboru"
},
"backend::lang.form.dependent_retry": {
"other": "Ponów wczytywanie opcji"
},
"backend::lang.form.dependent_waiting": {
"other": "Najpierw wybierz pole „:field”"
},
"backend::lang.form.discard": {
"other": "Porzuć zmiany"
},
@@ -609,6 +630,60 @@
"backend::lang.relation.unlink": {
"other": "Odłącz"
},
"backend::lang.repeater.add": {
"other": "Dodaj pozycję"
},
"backend::lang.repeater.collapse": {
"other": "Zwiń: :title"
},
"backend::lang.repeater.collapse_all": {
"other": "Zwiń wszystkie"
},
"backend::lang.repeater.count": {
"other": "Pozycje: :current z :max"
},
"backend::lang.repeater.empty": {
"other": "Brak pozycji"
},
"backend::lang.repeater.empty_hint": {
"other": "Użyj przycisku „:prompt” poniżej, aby dodać pierwszą."
},
"backend::lang.repeater.expand": {
"other": "Rozwiń: :title"
},
"backend::lang.repeater.expand_all": {
"other": "Rozwiń wszystkie"
},
"backend::lang.repeater.max_items": {
"other": "Usuń pozycje, aby zostało ich najwyżej :max."
},
"backend::lang.repeater.min_items": {
"other": "Dodaj pozycje, aby było ich co najmniej :min."
},
"backend::lang.repeater.move": {
"other": "Przenieś: :title"
},
"backend::lang.repeater.move_down": {
"other": "Przenieś niżej: :title"
},
"backend::lang.repeater.move_up": {
"other": "Przenieś wyżej: :title"
},
"backend::lang.repeater.moved": {
"other": "Przeniesiono :title na pozycję :position z :total."
},
"backend::lang.repeater.remove": {
"other": "Usuń pozycję"
},
"backend::lang.repeater.remove_confirm": {
"other": "Usunąć „:title”? Zmiana zostanie zapisana razem z rekordem."
},
"backend::lang.repeater.remove_named": {
"other": "Usuń: :title"
},
"backend::lang.repeater.untitled": {
"other": "Pozycja :n"
},
"backend::lang.richeditor.bold": {
"other": "Pogrubienie"
},