Merge branch 'main' into renovate/renovate-renovate-44.x
This commit was merged in pull request #91.
This commit is contained in:
@@ -47,7 +47,7 @@ folder under `plugins/` goes live after a restart. Create `plugins/hello/plugin.
|
|||||||
import { definePlugin } from "@plainpages/plugin-api";
|
import { definePlugin } from "@plainpages/plugin-api";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0",
|
apiVersion: "0.3.0",
|
||||||
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
||||||
routes: [
|
routes: [
|
||||||
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
||||||
@@ -349,7 +349,7 @@ import { definePlugin } from "@plainpages/plugin-api";
|
|||||||
import { listThings, createThings } from "./handlers.ts";
|
import { listThings, createThings } from "./handlers.ts";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0", // semver string of the host contract this plugin was built against (see Versioning)
|
apiVersion: "0.3.0", // semver string of the host contract this plugin was built against (see Versioning)
|
||||||
|
|
||||||
// Nav fragment, merged into the global menu and permission-filtered per user.
|
// Nav fragment, merged into the global menu and permission-filtered per user.
|
||||||
// `icon` is a Lucide icon by its sprite id (src/ui/icons.ts).
|
// `icon` is a Lucide icon by its sprite id (src/ui/icons.ts).
|
||||||
@@ -470,7 +470,7 @@ import { definePlugin } from "@plainpages/plugin-api";
|
|||||||
import { landing, board } from "./pages.ts";
|
import { landing, board } from "./pages.ts";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0",
|
apiVersion: "0.3.0",
|
||||||
home: landing, // owns "/" — the public front page
|
home: landing, // owns "/" — the public front page
|
||||||
dashboard: board, // owns "/dashboard" — the post-login app home
|
dashboard: board, // owns "/dashboard" — the post-login app home
|
||||||
});
|
});
|
||||||
@@ -747,7 +747,7 @@ camel humps both becoming underscores — so `upstream` on the `scheduling` plug
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0",
|
apiVersion: "0.3.0",
|
||||||
settings: [
|
settings: [
|
||||||
{ key: "upstream", type: "url", required: true, description: "Base URL of the backend" },
|
{ key: "upstream", type: "url", required: true, description: "Base URL of the backend" },
|
||||||
{ key: "pageSize", type: "number", default: 25 },
|
{ key: "pageSize", type: "number", default: 25 },
|
||||||
@@ -801,7 +801,7 @@ import { definePlugin } from "@plainpages/plugin-api";
|
|||||||
let sql: ReturnType<typeof postgres>;
|
let sql: ReturnType<typeof postgres>;
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0",
|
apiVersion: "0.3.0",
|
||||||
storage: true,
|
storage: true,
|
||||||
hooks: {
|
hooks: {
|
||||||
onBoot: async (boot) => {
|
onBoot: async (boot) => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const clients = on("oauth2-clients");
|
|||||||
const pluginSettings = on("plugin-settings");
|
const pluginSettings = on("plugin-settings");
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
apiVersion: "0.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
||||||
|
|
||||||
nav: [ADMIN_NAV],
|
nav: [ADMIN_NAV],
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let upstreamUrl = "";
|
|||||||
const upstream = createUpstream(() => upstreamUrl);
|
const upstream = createUpstream(() => upstreamUrl);
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
apiVersion: "0.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
||||||
|
|
||||||
// onBoot runs after discovery, before the server listens — where a plugin receives its resolved
|
// onBoot runs after discovery, before the server listens — where a plugin receives its resolved
|
||||||
// settings. A malformed URL already failed the boot by then; the host validated the declared type.
|
// settings. A malformed URL already failed the boot by then; the host validated the declared type.
|
||||||
|
|||||||
Generated
+4
-4
@@ -8,7 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@larvit/log": "2.3.0",
|
"@larvit/log": "2.3.0",
|
||||||
"ejs": "6.0.1",
|
"ejs": "6.0.1",
|
||||||
"lucide-static": "1.33.0",
|
"lucide-static": "1.34.0",
|
||||||
"postgres": "3.4.9"
|
"postgres": "3.4.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -399,9 +399,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-static": {
|
"node_modules/lucide-static": {
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.34.0.tgz",
|
||||||
"integrity": "sha512-jNGgvTNcLUfVRX4N9PH9pVVTJzoph/BmYmgU838bYBQodkUJL4nAThkuymFz1x3OUYMhJxPndC7rdg1sxOPYKg==",
|
"integrity": "sha512-pSUvFhfhvDnhXN1fXerZEMgKLQQ3DneKwFYlqB5ji8OEAN0iPi/qgwQvCkcL519QgMeR66IpS/pT342VyT/g4g==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/postgres": {
|
"node_modules/postgres": {
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@larvit/log": "2.3.0",
|
"@larvit/log": "2.3.0",
|
||||||
"ejs": "6.0.1",
|
"ejs": "6.0.1",
|
||||||
"lucide-static": "1.33.0",
|
"lucide-static": "1.34.0",
|
||||||
"postgres": "3.4.9"
|
"postgres": "3.4.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -316,6 +316,11 @@ span.nav-self { cursor: default; } /* static / non-clickable */
|
|||||||
.btn-primary:hover { filter: brightness(1.06); }
|
.btn-primary:hover { filter: brightness(1.06); }
|
||||||
.btn-ghost { background: transparent; border-color: transparent; }
|
.btn-ghost { background: transparent; border-color: transparent; }
|
||||||
.btn-ghost:hover { background: var(--surface-2); }
|
.btn-ghost:hover { background: var(--surface-2); }
|
||||||
|
.btn-menu::after {
|
||||||
|
content: ""; width: 7px; height: 7px; margin: -2px 1px 0 1px;
|
||||||
|
border-right: 1.5px solid var(--text-faint); border-bottom: 1.5px solid var(--text-faint);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 30px; height: 30px; padding: 0; justify-content: center;
|
width: 30px; height: 30px; padding: 0; justify-content: center;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -494,7 +499,8 @@ span.nav-self { cursor: default; } /* static / non-clickable */
|
|||||||
position-anchor: auto;
|
position-anchor: auto;
|
||||||
position-try-fallbacks: flip-block, flip-inline;
|
position-try-fallbacks: flip-block, flip-inline;
|
||||||
top: anchor(bottom); right: anchor(right);
|
top: anchor(bottom); right: anchor(right);
|
||||||
min-width: 210px; padding: 6px;
|
min-width: 210px; max-width: min(320px, 92vw); padding: 6px;
|
||||||
|
max-height: 60vh; overflow-y: auto; overflow-wrap: anywhere;
|
||||||
background: var(--surface); color: var(--text);
|
background: var(--surface); color: var(--text);
|
||||||
border: 1px solid var(--border-2); border-radius: var(--radius);
|
border: 1px solid var(--border-2); border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 28px rgba(0,0,0,.16);
|
box-shadow: 0 8px 28px rgba(0,0,0,.16);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ test("readHostApiVersion pulls the constant out of the real source, and returns
|
|||||||
test("bumping HOST_API_VERSION is a deliberate act, so pin the shipped value", () => {
|
test("bumping HOST_API_VERSION is a deliberate act, so pin the shipped value", () => {
|
||||||
// Not a substitute for the release gate — this test cannot see a tag. It is the tripwire that
|
// Not a substitute for the release gate — this test cannot see a tag. It is the tripwire that
|
||||||
// makes an accidental edit fail here rather than at release time.
|
// makes an accidental edit fail here rather than at release time.
|
||||||
assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.2.0");
|
assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.3.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("every author-facing apiVersion sample matches the shipped contract", () => {
|
test("every author-facing apiVersion sample matches the shipped contract", () => {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ into the app. Create `plugins/hello/plugin.ts`:
|
|||||||
import { definePlugin } from "@plainpages/plugin-api";
|
import { definePlugin } from "@plainpages/plugin-api";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.2.0",
|
apiVersion: "0.3.0",
|
||||||
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
||||||
routes: [
|
routes: [
|
||||||
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const messages = {
|
|||||||
"filter.remove": "Remove {{label}} filter",
|
"filter.remove": "Remove {{label}} filter",
|
||||||
"filter.reset": "Reset",
|
"filter.reset": "Reset",
|
||||||
"filter.search": "Search",
|
"filter.search": "Search",
|
||||||
|
"filter.selected": "{{label}}, {{count}} selected",
|
||||||
"filter.to": "To",
|
"filter.to": "To",
|
||||||
"filter.toSeparator": "to",
|
"filter.toSeparator": "to",
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const messages: CoreMessages = {
|
|||||||
"filter.remove": "Ta bort filtret {{label}}",
|
"filter.remove": "Ta bort filtret {{label}}",
|
||||||
"filter.reset": "Återställ",
|
"filter.reset": "Återställ",
|
||||||
"filter.search": "Sök",
|
"filter.search": "Sök",
|
||||||
|
"filter.selected": "{{label}}, {{count}} valda",
|
||||||
"filter.to": "Till",
|
"filter.to": "Till",
|
||||||
"filter.toSeparator": "till",
|
"filter.toSeparator": "till",
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { envName, type SettingDecl, type SettingsOf } from "./settings.ts";
|
|||||||
import type { StorageCredentials } from "./storage.ts";
|
import type { StorageCredentials } from "./storage.ts";
|
||||||
|
|
||||||
// The Plainpages release this contract ships in — see README → Contract versioning.
|
// The Plainpages release this contract ships in — see README → Contract versioning.
|
||||||
export const HOST_API_VERSION = "0.2.0";
|
export const HOST_API_VERSION = "0.3.0";
|
||||||
|
|
||||||
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ const config = {
|
|||||||
options: [{ value: "engineering", label: "Engineering" }, { value: "design", label: "Design" }, { value: "oncall", label: "On-call" }],
|
options: [{ value: "engineering", label: "Engineering" }, { value: "design", label: "Design" }, { value: "oncall", label: "On-call" }],
|
||||||
},
|
},
|
||||||
{ type: "daterange", legend: "Joined", from: { name: "joined_from", value: "2026-01-01", label: "Joined from" }, to: { name: "joined_to", value: "2026-06-14", label: "Joined to" } },
|
{ type: "daterange", legend: "Joined", from: { name: "joined_from", value: "2026-01-01", label: "Joined from" }, to: { name: "joined_to", value: "2026-06-14", label: "Joined to" } },
|
||||||
|
{
|
||||||
|
type: "multiselect",
|
||||||
|
name: "owner",
|
||||||
|
legend: "Owner",
|
||||||
|
note: "30 of 45",
|
||||||
|
value: ["ann"],
|
||||||
|
options: [{ value: "ann", label: "Ann Berg" }, { value: "bo", label: "Bo Falk" }],
|
||||||
|
},
|
||||||
|
{ type: "multiselect", name: "room", legend: "Room", options: [{ value: "lab", label: "Lab" }] },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
pills: [{ label: "Team", value: "Engineering", remove: "?tag=oncall" }],
|
pills: [{ label: "Team", value: "Engineering", remove: "?tag=oncall" }],
|
||||||
@@ -64,6 +73,14 @@ test("filter-bar renders a GET form with every control type, reflecting current
|
|||||||
assert.match(html, /<input type="checkbox" name="tag" value="oncall" checked>On-call/);
|
assert.match(html, /<input type="checkbox" name="tag" value="oncall" checked>On-call/);
|
||||||
assert.match(html, /<input type="checkbox" name="tag" value="design">Design/);
|
assert.match(html, /<input type="checkbox" name="tag" value="design">Design/);
|
||||||
|
|
||||||
|
// multiselect — a disclosure: the trigger names the filter and counts what is chosen, the options
|
||||||
|
// are checkboxes in a fieldset the popover holds, so a long list costs one line of the bar.
|
||||||
|
assert.match(html, /<div class="menu"><button class="btn btn-menu" type="button" popovertarget="f-owner-menu" aria-label="Owner, 1 selected">Owner<span class="badge">1<\/span><\/button><div id="f-owner-menu" class="menu-pop left" popover>/);
|
||||||
|
assert.match(html, /popover><div class="menu-head">30 of 45<\/div><fieldset class="menu-field"><legend class="menu-head">Owner<\/legend><label class="menu-check"><input type="checkbox" name="owner" value="ann" checked>Ann Berg<\/label><label class="menu-check"><input type="checkbox" name="owner" value="bo">Bo Falk<\/label><\/fieldset>/);
|
||||||
|
|
||||||
|
// Nothing chosen — no badge and no count in the accessible name.
|
||||||
|
assert.match(html, /<button class="btn btn-menu" type="button" popovertarget="f-room-menu">Room<\/button>/);
|
||||||
|
|
||||||
// daterange — calendar icon + two date inputs with values.
|
// daterange — calendar icon + two date inputs with values.
|
||||||
assert.match(html, /<div class="daterange"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-cal"\s*\/?><\/svg>.*?<input type="date" id="f-joined_from" name="joined_from" value="2026-01-01">.*?<span class="to" aria-hidden="true">to<\/span>.*?<input type="date" id="f-joined_to" name="joined_to" value="2026-06-14">/);
|
assert.match(html, /<div class="daterange"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-cal"\s*\/?><\/svg>.*?<input type="date" id="f-joined_from" name="joined_from" value="2026-01-01">.*?<span class="to" aria-hidden="true">to<\/span>.*?<input type="date" id="f-joined_to" name="joined_to" value="2026-06-14">/);
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ const flat = (s: string): string => s.replace(/>\s+</g, "><").replace(/\s+/g, "
|
|||||||
test("menu renders trigger, positioning, the item matrix and check groups", async () => {
|
test("menu renders trigger, positioning, the item matrix and check groups", async () => {
|
||||||
const html = flat(await render({
|
const html = flat(await render({
|
||||||
id: "cols-menu",
|
id: "cols-menu",
|
||||||
trigger: { icon: "i-cols", text: "Columns", label: "Column settings" },
|
trigger: { count: 3, icon: "i-cols", text: "Columns", label: "Column settings" },
|
||||||
align: "left", up: true, width: 240,
|
align: "left", up: true, width: 240,
|
||||||
items: [
|
items: [
|
||||||
{ head: "Actions" },
|
{ head: "Actions" },
|
||||||
@@ -30,7 +30,7 @@ test("menu renders trigger, positioning, the item matrix and check groups", asyn
|
|||||||
|
|
||||||
// Trigger: icon + text + aria-label, wired to the panel by id; popover carries align/up + width.
|
// Trigger: icon + text + aria-label, wired to the panel by id; popover carries align/up + width.
|
||||||
// The panel is the trigger's next sibling inside the wrapper — the CSS open state reads that.
|
// The panel is the trigger's next sibling inside the wrapper — the CSS open state reads that.
|
||||||
assert.match(html, /<div class="menu"><button class="btn" type="button" popovertarget="cols-menu" aria-label="Column settings"><svg class="ico ico-sm"><use href="#i-cols"\s*\/?><\/svg>Columns<\/button><div id="cols-menu" class="menu-pop left up" popover style="min-width:240px">/);
|
assert.match(html, /<div class="menu"><button class="btn" type="button" popovertarget="cols-menu" aria-label="Column settings"><svg class="ico ico-sm"><use href="#i-cols"\s*\/?><\/svg>Columns<span class="badge">3<\/span><\/button><div id="cols-menu" class="menu-pop left up" popover style="min-width:240px">/);
|
||||||
|
|
||||||
// Item matrix: head, button-with-icon, link, separator, danger button.
|
// Item matrix: head, button-with-icon, link, separator, danger button.
|
||||||
assert.match(html, /<div class="menu-head">Actions<\/div>/);
|
assert.match(html, /<div class="menu-head">Actions<\/div>/);
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
<p>${t("dashboard.starter.intro")}</p>
|
<p>${t("dashboard.starter.intro")}</p>
|
||||||
<p>${t("dashboard.starter.replace")}</p>
|
<p>${t("dashboard.starter.replace")}</p>
|
||||||
<pre class="code-block"><code>export default definePlugin({
|
<pre class="code-block"><code>export default definePlugin({
|
||||||
apiVersion: "0.2.0",
|
apiVersion: "0.3.0",
|
||||||
// view names plugins/<id>/views/<view>.ejs, rendered in this same shell
|
// view names plugins/<id>/views/<view>.ejs, rendered in this same shell
|
||||||
dashboard: (ctx) => ({ view: "dashboard", data: { /* … */ } }),
|
dashboard: (ctx) => ({ view: "dashboard", data: { /* … */ } }),
|
||||||
});</code></pre>
|
});</code></pre>
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
Filter bar: a real GET form so filtering is server-side and zero-JS. Config:
|
Filter bar: a real GET form so filtering is server-side and zero-JS. Config:
|
||||||
rows: Control[][] rows of controls, laid out left→right
|
rows: Control[][] rows of controls, laid out left→right
|
||||||
pills, clearHref, label, action, applyLabel
|
pills, clearHref, label, action, applyLabel
|
||||||
Control.type ∈ search | segmented | select | chips | daterange | spacer.
|
Control.type ∈ search | segmented | select | chips | multiselect | daterange | spacer.
|
||||||
search { name, placeholder?, value?, label? }
|
search { name, placeholder?, value?, label? }
|
||||||
segmented { name, legend?, value?, options:{value,label,count?}[] } (radios)
|
segmented { name, legend?, value?, options:{value,label,count?}[] } (radios)
|
||||||
select { name, label, value?, options:{value,label}[] }
|
select { name, label, value?, options:{value,label}[] }
|
||||||
chips { name, legend?, value?:string[], options:{value,label}[] } (checkboxes)
|
chips { name, legend?, value?:string[], options:{value,label}[] } (checkboxes)
|
||||||
daterange { legend?, from:{name,value?,label?}, to:{name,value?,label?} }
|
multiselect { name, legend?, note?, value?:string[], options:{value,label}[] } (checkboxes in a popover)
|
||||||
|
daterange { legend?, from:{name,value?,label?}, to:{name,value?,label?} }
|
||||||
|
chips and multiselect are the same checkboxes on the same parameter: on the bar, or behind a
|
||||||
|
button once the list is too long to lay there.
|
||||||
The form is a GET, which replaces the whole query string — so the visitor's chosen language rides
|
The form is a GET, which replaces the whole query string — so the visitor's chosen language rides
|
||||||
along as a hidden input, and every href here (pills, clear) is run through localeHref.
|
along as a hidden input, and every href here (pills, clear) is run through localeHref.
|
||||||
%><%
|
%><%
|
||||||
@@ -34,6 +37,16 @@
|
|||||||
<span class="filter"><label class="sr-only" for="f-<%= c.name %>"><%= c.label %></label><span class="select"><select id="f-<%= c.name %>" name="<%= c.name %>"><% c.options.forEach((o) => { %><option value="<%= o.value %>"<% if (eq(c.value, o.value)) { %> selected<% } %>><%= o.label %></option><% }) %></select></span></span>
|
<span class="filter"><label class="sr-only" for="f-<%= c.name %>"><%= c.label %></label><span class="select"><select id="f-<%= c.name %>" name="<%= c.name %>"><% c.options.forEach((o) => { %><option value="<%= o.value %>"<% if (eq(c.value, o.value)) { %> selected<% } %>><%= o.label %></option><% }) %></select></span></span>
|
||||||
<% } else if (c.type === "chips") { -%>
|
<% } else if (c.type === "chips") { -%>
|
||||||
<fieldset class="filter-field"><legend class="sr-only"><%= c.legend || c.name %></legend><span class="filter-legend" aria-hidden="true"><%= c.legend || c.name %></span><div class="chips"><% (c.options).forEach((o) => { const on = (c.value || []).map(String).includes(String(o.value)); %><label class="chip"><span class="chip-dot" aria-hidden="true"></span><input type="checkbox" name="<%= c.name %>" value="<%= o.value %>"<% if (on) { %> checked<% } %>><%= o.label %></label><% }) %></div></fieldset>
|
<fieldset class="filter-field"><legend class="sr-only"><%= c.legend || c.name %></legend><span class="filter-legend" aria-hidden="true"><%= c.legend || c.name %></span><div class="chips"><% (c.options).forEach((o) => { const on = (c.value || []).map(String).includes(String(o.value)); %><label class="chip"><span class="chip-dot" aria-hidden="true"></span><input type="checkbox" name="<%= c.name %>" value="<%= o.value %>"<% if (on) { %> checked<% } %>><%= o.label %></label><% }) %></div></fieldset>
|
||||||
|
<% } else if (c.type === "multiselect") { const legend = c.legend || c.name; const on = (c.value || []).map(String); -%>
|
||||||
|
<%- include("menu", {
|
||||||
|
id: "f-" + c.name + "-menu",
|
||||||
|
align: "left", kebab: false, up: false, width: null, // explicit: EJS would otherwise inherit the page's own
|
||||||
|
trigger: { class: "btn btn-menu", count: on.length || null, label: on.length ? t("filter.selected", { count: on.length, label: legend }) : null, text: legend },
|
||||||
|
items: [
|
||||||
|
...(c.note ? [{ head: c.note }] : []),
|
||||||
|
{ group: { legend, name: c.name, options: c.options.map((o) => ({ checked: on.includes(String(o.value)), label: o.label, value: o.value })) } },
|
||||||
|
],
|
||||||
|
}) %>
|
||||||
<% } else if (c.type === "daterange") { -%>
|
<% } else if (c.type === "daterange") { -%>
|
||||||
<fieldset class="filter-field"><legend class="sr-only"><%= c.legend || t("filter.dateRange") %></legend><span class="filter-legend" aria-hidden="true"><%= c.legend || t("filter.dateRange") %></span><div class="daterange"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-cal"/></svg><label class="sr-only" for="f-<%= c.from.name %>"><%= c.from.label || t("filter.from") %></label><input type="date" id="f-<%= c.from.name %>" name="<%= c.from.name %>"<% if (c.from.value) { %> value="<%= c.from.value %>"<% } %>><span class="to" aria-hidden="true"><%= t("filter.toSeparator") %></span><label class="sr-only" for="f-<%= c.to.name %>"><%= c.to.label || t("filter.to") %></label><input type="date" id="f-<%= c.to.name %>" name="<%= c.to.name %>"<% if (c.to.value) { %> value="<%= c.to.value %>"<% } %>></div></fieldset>
|
<fieldset class="filter-field"><legend class="sr-only"><%= c.legend || t("filter.dateRange") %></legend><span class="filter-legend" aria-hidden="true"><%= c.legend || t("filter.dateRange") %></span><div class="daterange"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-cal"/></svg><label class="sr-only" for="f-<%= c.from.name %>"><%= c.from.label || t("filter.from") %></label><input type="date" id="f-<%= c.from.name %>" name="<%= c.from.name %>"<% if (c.from.value) { %> value="<%= c.from.value %>"<% } %>><span class="to" aria-hidden="true"><%= t("filter.toSeparator") %></span><label class="sr-only" for="f-<%= c.to.name %>"><%= c.to.label || t("filter.to") %></label><input type="date" id="f-<%= c.to.name %>" name="<%= c.to.name %>"<% if (c.to.value) { %> value="<%= c.to.value %>"<% } %>></div></fieldset>
|
||||||
<% } else if (c.type === "spacer") { -%>
|
<% } else if (c.type === "spacer") { -%>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
Config:
|
Config:
|
||||||
id string REQUIRED — the panel's id and the trigger's popovertarget. Name it for what
|
id string REQUIRED — the panel's id and the trigger's popovertarget. Name it for what
|
||||||
the menu is (`locale-menu`); it must be unique on the page.
|
the menu is (`locale-menu`); it must be unique on the page.
|
||||||
trigger { class?(="btn", "" ⇒ none) · label?(aria-label) · icon? · text? · html?(raw inner, wins) }
|
trigger { class?(="btn", "" ⇒ none) · label?(aria-label) · icon? · text? · count?(badge) · html?(raw inner, wins) }
|
||||||
align? "left" left-align the popover (default right)
|
align? "left" left-align the popover (default right)
|
||||||
up? boolean open upward (footer menus)
|
up? boolean open upward (footer menus)
|
||||||
kebab? boolean bare kebab trigger (adds .kebab)
|
kebab? boolean bare kebab trigger (adds .kebab)
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
const popCls = "menu-pop" + (locals.align === "left" ? " left" : "") + (locals.up ? " up" : "");
|
const popCls = "menu-pop" + (locals.align === "left" ? " left" : "") + (locals.up ? " up" : "");
|
||||||
const width = locals.width;
|
const width = locals.width;
|
||||||
-%>
|
-%>
|
||||||
<div class="menu"><button<% if (btnCls) { %> class="<%= btnCls %>"<% } %> type="button" popovertarget="<%= locals.id %>"<% if (trigger.label) { %> aria-label="<%= trigger.label %>"<% } %>><% if (trigger.html != null) { %><%- trigger.html %><% } else { if (trigger.icon) { %><svg class="ico ico-sm"><use href="#<%= trigger.icon %>"/></svg><% } if (trigger.text) { %><%= trigger.text %><% } } %></button><div id="<%= locals.id %>" class="<%= popCls %>" popover<% if (width != null) { %> style="min-width:<%= typeof width === "number" ? width + "px" : width %>"<% } %>>
|
<div class="menu"><button<% if (btnCls) { %> class="<%= btnCls %>"<% } %> type="button" popovertarget="<%= locals.id %>"<% if (trigger.label) { %> aria-label="<%= trigger.label %>"<% } %>><% if (trigger.html != null) { %><%- trigger.html %><% } else { if (trigger.icon) { %><svg class="ico ico-sm"><use href="#<%= trigger.icon %>"/></svg><% } if (trigger.text) { %><%= trigger.text %><% } if (trigger.count != null) { %><span class="badge"><%= trigger.count %></span><% } } %></button><div id="<%= locals.id %>" class="<%= popCls %>" popover<% if (width != null) { %> style="min-width:<%= typeof width === "number" ? width + "px" : width %>"<% } %>>
|
||||||
<% items.forEach((it) => { -%>
|
<% items.forEach((it) => { -%>
|
||||||
<% if (it.head != null) { -%>
|
<% if (it.head != null) { -%>
|
||||||
<div class="menu-head"><%= it.head %></div>
|
<div class="menu-head"><%= it.head %></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user