Compare commits
13 Commits
v0.3.0
...
25239cd978
| Author | SHA1 | Date | |
|---|---|---|---|
| 25239cd978 | |||
| 8128ec2f42 | |||
| f959404c6f | |||
| 33065afd18 | |||
| 09d86eddad | |||
| 9fbda833d5 | |||
| 098359e298 | |||
| 4ae9326741 | |||
| 11ba843ab1 | |||
| 706abf1204 | |||
| f5c3d93837 | |||
| ea320f6a28 | |||
| d3151f222a |
@@ -21,7 +21,7 @@ jobs:
|
|||||||
-e RENOVATE_PLATFORM=gitea \
|
-e RENOVATE_PLATFORM=gitea \
|
||||||
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
|
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
|
||||||
-e RENOVATE_TOKEN \
|
-e RENOVATE_TOKEN \
|
||||||
renovate/renovate:44.59.3
|
renovate/renovate:44.61.6
|
||||||
|
|
||||||
# After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the
|
# After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the
|
||||||
# last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the
|
# last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ branch, create a PR and merge it when the CI/CD turns green.
|
|||||||
## Project priorities (do not erode)
|
## Project priorities (do not erode)
|
||||||
|
|
||||||
1. **Simplicity** — prefer the solution that is easiest to understand, smallest, and most readable.
|
1. **Simplicity** — prefer the solution that is easiest to understand, smallest, and most readable.
|
||||||
|
**A page is a document**: it scrolls, and the chrome scrolls with it. Nothing may bound the
|
||||||
|
viewport to hold content still — no `height: 100dvh` frame, no `overflow: hidden` on `body`, no
|
||||||
|
`position: sticky` header. Each such box buys an app-like look with CSS the next reader has to
|
||||||
|
reverse-engineer, and is one more thing to undo before the content under it can be reached.
|
||||||
|
Overlays are not this: the skip link, the mobile off-canvas nav and its scrim sit *above* the
|
||||||
|
document rather than holding it still, and have no other spelling — the document keeps scrolling
|
||||||
|
behind the open nav, accepted rather than overlooked. Only the document scroller gets keyboard
|
||||||
|
paging unconditionally and back/forward scroll restoration, and a page a box clips fails silently:
|
||||||
|
nothing in a test or a console says content is unreachable below the fold.
|
||||||
2. **Few dependencies** — runtime deps stay minimal (today `ejs`, `lucide-static`, `@larvit/log`,
|
2. **Few dependencies** — runtime deps stay minimal (today `ejs`, `lucide-static`, `@larvit/log`,
|
||||||
`postgres`). Prefer the Node standard library; justify any new dependency; do not add frameworks.
|
`postgres`). Prefer the Node standard library; justify any new dependency; do not add frameworks.
|
||||||
The **host is stateless — it owns no schema and stores nothing of its own**; a plugin may own a
|
The **host is stateless — it owns no schema and stores nothing of its own**; a plugin may own a
|
||||||
@@ -278,6 +287,10 @@ Revisit only if the stated reason stops holding.
|
|||||||
it means disclosure rather than popup: the nav tree. `shell.ejs` hand-rolls the same block for the
|
it means disclosure rather than popup: the nav tree. `shell.ejs` hand-rolls the same block for the
|
||||||
profile menu (its trigger composes escaped user values and its one item is a CSRF POST form) — keep
|
profile menu (its trigger composes escaped user values and its one item is a CSRF POST form) — keep
|
||||||
the two in step.
|
the two in step.
|
||||||
|
- **One scroller, the document** (priority 1). `.app` is `min-height: 100dvh`. `.nav`'s
|
||||||
|
`overflow-y: auto` and `.side-footer`'s `flex: 0 0 auto` are not leftovers of a bounded frame:
|
||||||
|
they are what makes the off-canvas panel usable with a long tree. `#nav-toggle` is `position: fixed` —
|
||||||
|
a label click focuses it, and a browser scrolls a focused element into view.
|
||||||
- **`ICON_NAMES` (`src/ui/icons.ts`) is a host-owned registry, not a frozen plugin contract**, so it
|
- **`ICON_NAMES` (`src/ui/icons.ts`) is a host-owned registry, not a frozen plugin contract**, so it
|
||||||
is deliberately not re-exported from `@plainpages/plugin-api`. The palette may narrow when the last reference
|
is deliberately not re-exported from `@plainpages/plugin-api`. The palette may narrow when the last reference
|
||||||
to an id goes, and a plugin needing one gets it re-registered in the same change. Accepted cost: an
|
to an id goes, and a plugin needing one gets it re-registered in the same change. Accepted cost: an
|
||||||
|
|||||||
@@ -4,6 +4,38 @@ The release version **is** the plugin contract version (`HOST_API_VERSION`), so
|
|||||||
contract break: a plugin's `apiVersion` must match the host's `major.minor` or discovery refuses it
|
contract break: a plugin's `apiVersion` must match the host's `major.minor` or discovery refuses it
|
||||||
at boot. Entries start at 0.3.0.
|
at boot. Entries start at 0.3.0.
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
**Breaking.** Set `apiVersion: "0.4.0"`. The app shell no longer bounds the content column, so a page
|
||||||
|
that relied on filling it scrolls the document instead.
|
||||||
|
|
||||||
|
### The document scrolls, and the chrome scrolls with it
|
||||||
|
|
||||||
|
`.app` was a `100dvh` box with `overflow: hidden`, so a page was only reachable below the fold if its
|
||||||
|
own wrapper was a flex child with `overflow-y: auto`. `.table-wrap` and `.shell-auth` were; nothing
|
||||||
|
else was, and a long page in `.form-page` clipped everything past the window in every engine.
|
||||||
|
|
||||||
|
Now the shell is `min-height: 100dvh` and nothing bounds the viewport. The sidebar and topbar scroll
|
||||||
|
with the page, and keyboard paging, back/forward scroll restoration and find-in-page work without a
|
||||||
|
page doing anything.
|
||||||
|
|
||||||
|
The sticky `thead` on `data-table` goes with it: a header only sticks to a scrollport that moves, and
|
||||||
|
there is no longer one. A plugin that wants a full-height pane owns that in its own stylesheet; the
|
||||||
|
shell offers no opt-out, per the simplicity priority in `AGENTS.md`.
|
||||||
|
|
||||||
|
### Upgrading a plugin
|
||||||
|
|
||||||
|
1. Set `apiVersion: "0.4.0"`.
|
||||||
|
2. A page that scrolled the whole window needs no change — it now scrolls the document.
|
||||||
|
3. A page holding a region that filled the content column (`flex: 1 1 auto; min-height: 0` with its
|
||||||
|
own `overflow`) no longer gets a bounded column to fill, so that region grows and the page scrolls.
|
||||||
|
Either let it, or give the region its own height in the plugin's stylesheet.
|
||||||
|
4. A `data-table` no longer scrolls its rows in a bounded region: the page scrolls, and the header
|
||||||
|
scrolls with it.
|
||||||
|
|
||||||
|
The sidebar stretches the whole document, so on a long page its footer — theme, language, profile and
|
||||||
|
**Sign out** — sits at the end of that page rather than the bottom of the screen.
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
**Breaking.** Set `apiVersion: "0.3.0"`, and name a gate on every route and nav node.
|
**Breaking.** Set `apiVersion: "0.3.0"`, and name a gate on every route and nav node.
|
||||||
|
|||||||
@@ -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.3.0",
|
apiVersion: "0.4.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>" }) },
|
||||||
@@ -350,7 +350,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.3.0", // semver string of the host contract this plugin was built against (see Versioning)
|
apiVersion: "0.4.0", // semver string of the host contract this plugin was built against (see Versioning)
|
||||||
|
|
||||||
// Nav fragment, merged into the global menu and gate-filtered per user.
|
// Nav fragment, merged into the global menu and gate-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).
|
||||||
@@ -471,7 +471,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.3.0",
|
apiVersion: "0.4.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
|
||||||
});
|
});
|
||||||
@@ -757,7 +757,7 @@ camel humps both becoming underscores — so `upstream` on the `scheduling` plug
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.3.0",
|
apiVersion: "0.4.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 },
|
||||||
@@ -811,7 +811,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.3.0",
|
apiVersion: "0.4.0",
|
||||||
storage: true,
|
storage: true,
|
||||||
hooks: {
|
hooks: {
|
||||||
onBoot: async (boot) => {
|
onBoot: async (boot) => {
|
||||||
@@ -933,6 +933,11 @@ recovery / front pages — so it looks identical signed in or out and just shows
|
|||||||
anonymous visitor. The sidebar collapses to a burger on a narrow screen; a page wanting a
|
anonymous visitor. The sidebar collapses to a burger on a narrow screen; a page wanting a
|
||||||
chrome-free layout opts out with the shell's `menu: false`.
|
chrome-free layout opts out with the shell's `menu: false`.
|
||||||
|
|
||||||
|
**The document scrolls, and the chrome scrolls with it.** Nothing bounds the viewport, so a page is
|
||||||
|
reachable below the fold without adding a scroll region of its own, and browser paging, scroll
|
||||||
|
restoration and find-in-page work without a page doing anything. A plugin that wants a full-height
|
||||||
|
pane owns that in its own stylesheet.
|
||||||
|
|
||||||
## Building blocks
|
## Building blocks
|
||||||
|
|
||||||
Plainpages is a **component library, not a page generator** — reusable EJS partials + TS helpers,
|
Plainpages is a **component library, not a page generator** — reusable EJS partials + TS helpers,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ services:
|
|||||||
# Dev mail catcher — Kratos recovery/verification emails land here (web UI on 8025).
|
# Dev mail catcher — Kratos recovery/verification emails land here (web UI on 8025).
|
||||||
# kratos.yml points the courier at smtp://mailpit:1025; prod uses a real SMTP via env.
|
# kratos.yml points the courier at smtp://mailpit:1025; prod uses a real SMTP via env.
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit:v1.31.0
|
image: axllent/mailpit:v1.31.1
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e-tests/.
|
# Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e-tests/.
|
||||||
# Built/run via e2e-tests/compose.visual.yml; targets the `web` service over the network.
|
# Built/run via e2e-tests/compose.visual.yml; targets the `web` service over the network.
|
||||||
FROM mcr.microsoft.com/playwright:v1.62.1-noble
|
FROM mcr.microsoft.com/playwright:v1.63.0-noble
|
||||||
|
|
||||||
WORKDIR /e2e-tests
|
WORKDIR /e2e-tests
|
||||||
|
|
||||||
|
|||||||
Generated
+12
-30
@@ -6,17 +6,17 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "plainpages-e2e",
|
"name": "plainpages-e2e",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.62.1"
|
"@playwright/test": "1.63.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.62.1",
|
"version": "1.63.0",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
|
||||||
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
"integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.62.1"
|
"playwright": "1.63.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -25,44 +25,26 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.62.1",
|
"version": "1.63.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
|
||||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.62.1"
|
"playwright-core": "1.63.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.62.1",
|
"version": "1.63.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
|
||||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"test": "playwright test"
|
"test": "playwright test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.62.1"
|
"@playwright/test": "1.63.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,45 @@ test.beforeEach(async ({ context }) => {
|
|||||||
await context.addCookies([{ name: SESSION_COOKIE, url: BASE_URL, value: devSession() }]);
|
await context.addCookies([{ name: SESSION_COOKIE, url: BASE_URL, value: devSession() }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A key press, not scrollIntoView (a script can scroll a box no reader can) and not the wheel
|
||||||
|
// (Firefox's synthetic event never reaches the document).
|
||||||
|
for (const [name, path, tail] of [
|
||||||
|
["the starter dashboard", "/dashboard", ".form-actions .btn"],
|
||||||
|
["the public landing", "/", ".landing-actions .btn"],
|
||||||
|
] as const) {
|
||||||
|
for (const width of [1280, 390]) {
|
||||||
|
test(`${name} scrolls to its end at ${width}px wide`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width, height: 200 });
|
||||||
|
await page.goto(path);
|
||||||
|
|
||||||
|
const overflows = await page.evaluate(() => document.documentElement.scrollHeight > window.innerHeight);
|
||||||
|
expect(overflows, "the page must overflow, or it proves nothing").toBe(true);
|
||||||
|
await page.keyboard.press("End");
|
||||||
|
await expect(page.locator(tail).last()).toBeInViewport({ ratio: 1 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Green only while #nav-toggle is position: fixed — a label tap focuses it, and focus scrolls into view.
|
||||||
|
test("closing the mobile drawer leaves the reader where the scrim found them", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 200 });
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
|
||||||
|
await page.locator(".hamburger").click();
|
||||||
|
await expect(page.locator("#nav-toggle")).toBeChecked();
|
||||||
|
// Scripted, because a key press with focus on the toggle does not scroll in every engine — and
|
||||||
|
// what is under test is closing the drawer, not how the reader got down the page.
|
||||||
|
await page.evaluate(() => window.scrollTo(0, 120));
|
||||||
|
const at = await page.evaluate(() => window.scrollY);
|
||||||
|
expect(at, "the page must have somewhere to scroll behind the scrim").toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// The exposed strip beside the 264px panel: the scrim spans the viewport, so its centre is under
|
||||||
|
// the drawer and a centre click lands on the panel instead.
|
||||||
|
await page.locator(".scrim").click({ position: { x: 340, y: 100 } });
|
||||||
|
await expect(page.locator("#nav-toggle")).not.toBeChecked();
|
||||||
|
expect(await page.evaluate(() => window.scrollY), "closing the drawer must not move the page").toBe(at);
|
||||||
|
});
|
||||||
|
|
||||||
test("captures the live pages for review", async ({ page }) => {
|
test("captures the live pages for review", async ({ page }) => {
|
||||||
await page.goto("/dashboard");
|
await page.goto("/dashboard");
|
||||||
await expect(page.locator(".sidebar")).toBeVisible();
|
await expect(page.locator(".sidebar")).toBeVisible();
|
||||||
|
|||||||
@@ -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.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
apiVersion: "0.4.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.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
apiVersion: "0.4.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.
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ html:has(#theme-light:checked) {
|
|||||||
|
|
||||||
/* ---------- 2. RESET ---------------------------------------- */
|
/* ---------- 2. RESET ---------------------------------------- */
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
html, body { height: 100%; }
|
|
||||||
body { margin: 0; background: var(--bg); color: var(--text);
|
body { margin: 0; background: var(--bg); color: var(--text);
|
||||||
-webkit-font-smoothing: antialiased; }
|
-webkit-font-smoothing: antialiased; }
|
||||||
button { font: inherit; color: inherit; }
|
button { font: inherit; color: inherit; }
|
||||||
@@ -151,8 +150,7 @@ summary { list-style: none; cursor: pointer; }
|
|||||||
.app {
|
.app {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--nav-w) minmax(0, 1fr);
|
grid-template-columns: var(--nav-w) minmax(0, 1fr);
|
||||||
height: 100dvh;
|
min-height: 100dvh;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 4. SIDEBAR -------------------------------------- */
|
/* ---------- 4. SIDEBAR -------------------------------------- */
|
||||||
@@ -160,7 +158,6 @@ summary { list-style: none; cursor: pointer; }
|
|||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -331,7 +328,7 @@ span.nav-self { cursor: default; } /* static / non-clickable */
|
|||||||
.content {
|
.content {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column;
|
||||||
min-width: 0; min-height: 0;
|
min-width: 0;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,13 +556,12 @@ span.nav-self { cursor: default; } /* static / non-clickable */
|
|||||||
.pill-clear:hover { text-decoration: underline; }
|
.pill-clear:hover { text-decoration: underline; }
|
||||||
|
|
||||||
/* ---------- 9. TABLE --------------------------------------- */
|
/* ---------- 9. TABLE --------------------------------------- */
|
||||||
.table-wrap { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
.table-wrap { overflow-x: auto; }
|
||||||
table.table {
|
table.table {
|
||||||
width: 100%; border-collapse: separate; border-spacing: 0;
|
width: 100%; border-collapse: separate; border-spacing: 0;
|
||||||
font-size: var(--fz); font-variant-numeric: tabular-nums;
|
font-size: var(--fz); font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.table thead th {
|
.table thead th {
|
||||||
position: sticky; top: 0; z-index: 10;
|
|
||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
border-bottom: 1px solid var(--border-2);
|
border-bottom: 1px solid var(--border-2);
|
||||||
color: var(--text-muted); font-weight: 600; font-size: var(--fz-xs);
|
color: var(--text-muted); font-weight: 600; font-size: var(--fz-xs);
|
||||||
@@ -697,7 +693,7 @@ th[aria-sort="descending"] .sort-ico { transform: rotate(180deg); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* the nav-toggle checkbox itself is visually hidden but focusable */
|
/* the nav-toggle checkbox itself is visually hidden but focusable */
|
||||||
#nav-toggle { position: absolute; opacity: 0; pointer-events: none; }
|
#nav-toggle { position: fixed; top: 0; left: 0; opacity: 0; pointer-events: none; }
|
||||||
|
|
||||||
/* admin forms: create/edit user, account actions */
|
/* admin forms: create/edit user, account actions */
|
||||||
.form-page { padding: 16px; display: flex; flex-direction: column; gap: 14px; max-width: 560px; }
|
.form-page { padding: 16px; display: flex; flex-direction: column; gap: 14px; max-width: 560px; }
|
||||||
@@ -722,5 +718,6 @@ th[aria-sort="descending"] .sort-ico { transform: rotate(180deg); }
|
|||||||
/* Chromeless shell: a page may drop the sidebar for a focused single column. */
|
/* Chromeless shell: a page may drop the sidebar for a focused single column. */
|
||||||
.app-bare { grid-template-columns: minmax(0, 1fr); }
|
.app-bare { grid-template-columns: minmax(0, 1fr); }
|
||||||
.app-bare .content { grid-column: 1; }
|
.app-bare .content { grid-column: 1; }
|
||||||
|
|
||||||
/* Auth/landing rendered inside the app shell: a roomy, centered column in the content area. */
|
/* Auth/landing rendered inside the app shell: a roomy, centered column in the content area. */
|
||||||
.shell-auth { flex: 1 1 auto; overflow-y: auto; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px 80px; }
|
.shell-auth { flex: 1 1 auto; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px 80px; }
|
||||||
|
|||||||
@@ -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.3.0");
|
assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.4.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("every author-facing apiVersion sample matches the shipped contract", () => {
|
test("every author-facing apiVersion sample matches the shipped contract", () => {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ services:
|
|||||||
|
|
||||||
# Catches Kratos' recovery/verification emails — UI on http://localhost:8025
|
# Catches Kratos' recovery/verification emails — UI on http://localhost:8025
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit:v1.31.0
|
image: axllent/mailpit:v1.31.1
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -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.3.0",
|
apiVersion: "0.4.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>" }) },
|
||||||
|
|||||||
@@ -11,7 +11,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.3.0";
|
export const HOST_API_VERSION = "0.4.0";
|
||||||
|
|
||||||
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
||||||
|
|
||||||
|
|||||||
+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.3.0",
|
apiVersion: "0.4.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>
|
||||||
|
|||||||
Reference in New Issue
Block a user