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

@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { CircleAlert } from '@lucide/vue'
import { t, tc } from '../../app/i18n'
import { keyField } from './formState'
// 422 banner (design screen 4): "Nie udało się zapisać. Popraw N pola…".
// Messages of keys that are not form fields are listed here, so no server
@@ -14,9 +15,11 @@ const props = withDefaults(
)
const count = computed(() => Object.keys(props.errors).length)
// A repeater row path (`content.3.answer`) belongs to its field: it renders
// under the nested control, not here.
const orphans = computed(() =>
Object.entries(props.errors)
.filter(([name]) => !props.fieldNames.includes(name))
.filter(([name]) => !props.fieldNames.includes(name) && !props.fieldNames.includes(keyField(name)))
.flatMap(([, messages]) => messages),
)
</script>