feat(10-03): tabs, toggles, relation fields, form lifecycle and settings pages
- FormTabs groups fields by tab (untabbed fields in the default tab) as a
segmented tablist; a tab holding invalid fields after a 422 shows a
count badge and the form switches to the first invalid field
- switch, checkbox and relation join the renderer registry: toggle cards
(role=switch, 20px checkbox) keep a numeric value numeric; relation
fields are read-only labels from meta.labels, a searchable single
select over fields/{field}/options with emptyOption first, or ordered
removable chips with an appending search (300 ms debounce, 20 per
page, more on scroll or the more action)
- FormView gets the back button, record title and update subtitle, a
sticky footer (Usuń with deleteConfirm then DELETE, Anuluj, Zapisz i
zamknij to the mapped redirectClose, Zapisz), and asks before leaving a
dirty form on any route change plus a beforeunload guard
- Settings: the rail pins Ustawienia to the bottom when /settings is
non-empty; /settings lists pages by category; /settings/:code renders
the settings schema through FormGrid and the registry, PUTs the values
and maps a 422 like the record form
- New backend::lang keys (form.more_options, tab_default, discard,
settings.back); form and settings smoke tests; boardwalk/dist rebuilt
This commit is contained in:
47
admin/src/components/form/fields/SwitchField.vue
Normal file
47
admin/src/components/form/fields/SwitchField.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { toggleOn, toggleValue, type FieldControlProps } from '../registry'
|
||||
|
||||
// Toggle card (design screen 4): the label as title, the comment as helper
|
||||
// text and a 44x26 role="switch" control; the off track uses borderStrong.
|
||||
const props = defineProps<FieldControlProps>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean | number] }>()
|
||||
|
||||
const on = computed(() => toggleOn(props.modelValue))
|
||||
|
||||
function toggle(): void {
|
||||
emit('update:modelValue', toggleValue(props.modelValue, !on.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="invalid ? 'border-danger' : 'border-border'"
|
||||
class="flex items-center justify-between gap-4 rounded-inner border px-4 py-3.5"
|
||||
data-toggle-card
|
||||
>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<label :for="controlId" class="font-semibold">
|
||||
{{ field.label || field.name }}<span v-if="field.required" class="text-danger" aria-hidden="true"> *</span>
|
||||
</label>
|
||||
<p v-if="field.comment" :id="`${controlId}-comment`" class="text-[13px] text-muted">{{ field.comment }}</p>
|
||||
</div>
|
||||
<button
|
||||
:id="controlId"
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="on ? 'true' : 'false'"
|
||||
:aria-invalid="invalid ? 'true' : undefined"
|
||||
:aria-describedby="describedBy || undefined"
|
||||
:class="on ? 'bg-primary' : 'bg-border-strong'"
|
||||
class="relative h-[26px] w-11 shrink-0 rounded-pill transition-colors duration-150 ease-out"
|
||||
@click="toggle"
|
||||
>
|
||||
<span
|
||||
:class="on ? 'translate-x-[18px] bg-on-primary' : 'translate-x-0 bg-white'"
|
||||
class="absolute top-[3px] left-[3px] size-5 rounded-full shadow-tab transition-transform duration-150 ease-out"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user