feat(10.1-02): render header and form partials through an allowlisted node renderer

- partialNodes rebuilds the server node tree with h() under the server's tag, attribute and URL lists
- PartialHost owns the skeleton, empty and failure states and keeps nodes visible on refetch
- type: partial is a valueless group-labelled field rendered on create and update
- ListView shows headerPartial above the list card and refetches it after bulk delete
- summer-partial and summer-stats style kit in main.css, documented in the cabana README; dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-29 02:10:18 +02:00
parent 107d820109
commit 9df9fae930
20 changed files with 870 additions and 22 deletions

View File

@@ -4,12 +4,13 @@
// is registered like any control but holds no form value: it edits its
// relation through its own endpoints and renders only on an existing record.
// Phase 10.1 adds the plugin extension types: a widget mounts a plugin custom
// element (D-04, D-09). It holds no form value either, but it renders on
// create and update.
// element (D-04, D-09) and a partial renders a server template (D-17). They
// hold no form value either, but they render on create and update.
import type { Component } from 'vue'
import CheckboxField from './fields/CheckboxField.vue'
import DropdownField from './fields/DropdownField.vue'
import NumberField from './fields/NumberField.vue'
import PartialField from './fields/PartialField.vue'
import RelationManager from '../relation/RelationManager.vue'
import RelationField from './fields/RelationField.vue'
import SwitchField from './fields/SwitchField.vue'
@@ -37,6 +38,7 @@ const renderers = new Map<string, Component>([
['relation', RelationField],
[RELATION_MANAGER, RelationManager],
['widget', WidgetField],
['partial', PartialField],
])
/** Types whose control shows the label itself (toggle cards, relation manager). */
@@ -49,13 +51,13 @@ const selfLabelled = new Set<string>(['switch', 'checkbox', RELATION_MANAGER])
const recordBound = new Set<string>([RELATION_MANAGER])
/** Types that hold no form value: never part of the save body (D-09). */
const valueless = new Set<string>([RELATION_MANAGER, 'widget'])
const valueless = new Set<string>([RELATION_MANAGER, 'widget', 'partial'])
/**
* Types whose control is a group rather than one focusable element: the
* visible label is a span the group points at, not a label for an input.
*/
const groupLabelledTypes = new Set<string>(['widget'])
const groupLabelledTypes = new Set<string>(['widget', 'partial'])
export function rendererFor(type: string): Component {
return renderers.get(type) ?? UnsupportedField
@@ -75,7 +77,10 @@ export function ownsLabel(type: string): boolean {
return selfLabelled.has(type)
}
/** Whether a field type's label labels a group (widget) instead of a control. */
/**
* Whether a field type's label names a group (widget, partial) instead of a
* control. Such a row shows no label row when the field declares no label.
*/
export function groupLabelled(type: string): boolean {
return groupLabelledTypes.has(type)
}