Right-slide panel that renders a resource's edit form. Driven by the URL: ?new=1 opens a fresh form, ?edit=<id> hydrates with the current item. POSTs back to the same URL with _action (save | delete | <action key>); the route handler in step 7 dispatches. - FieldRenderer.astro: dispatches on field.kind, wraps each field with label + helper text + error state. - fields/*.astro: one component per kind — Text, Textarea, Markdown (with Write/Preview toggle), Select, SelectAsync, MultiSelectAsync, MultiText (with add/remove), Date, Datetime, Number, Readonly. - ResourceEditPanel.astro: header (title + close X), scrollable body, sticky footer (save + per-resource secondary actions + destructive delete when ops.delete is defined and item exists). Scrim closes on click, Esc, or the close link. Confirm-before-submit honours action.confirmText. Embedded sub-form sections render a placeholder until step 8 wires the pulse renderer. - admin.css: panel chrome + scrim + slide-in keyframes, full field styling for every kind, mobile full-screen modal collapse. - preview.astro: exercises every field kind so the panel can be eyeballed in a logged-in session. Try /admin/preview?new=1 and /admin/preview?edit=<id>. Autosave deferred to Phase 2 per the approved deltas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47 lines
1.4 KiB
Text
47 lines
1.4 KiB
Text
---
|
|
/* ---------------------------------------------------------------------------
|
|
* MultiSelectAsyncField — checkbox grid for picking multiple options.
|
|
*
|
|
* Submits as repeated form values under field.key[] (browser default for
|
|
* multiple checkboxes with the same name). The route handler in step 7
|
|
* normalises the array form via getAll().
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
import type { MultiSelectAsyncField } from '../../resource-types';
|
|
|
|
interface Props {
|
|
field: MultiSelectAsyncField;
|
|
value: unknown;
|
|
}
|
|
|
|
const { field, value } = Astro.props;
|
|
const options = await field.loadOptions();
|
|
|
|
const selected = new Set<string>();
|
|
if (Array.isArray(value)) {
|
|
for (const v of value) selected.add(String(v));
|
|
}
|
|
---
|
|
|
|
<fieldset class="bs-multiselect" disabled={field.readOnly}>
|
|
<legend class="bs-visually-hidden">{field.label}</legend>
|
|
{options.length === 0 && (
|
|
<p class="bs-multiselect-empty">No options available.</p>
|
|
)}
|
|
{options.map(opt => {
|
|
const id = `f-${field.key}-${opt.value}`;
|
|
const isChecked = selected.has(String(opt.value));
|
|
return (
|
|
<label class="bs-multiselect-row" for={id}>
|
|
<input
|
|
type="checkbox"
|
|
id={id}
|
|
name={field.key}
|
|
value={String(opt.value)}
|
|
checked={isChecked}
|
|
/>
|
|
<span>{opt.label}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</fieldset>
|