diff --git a/.publish/ai/package.json b/.publish/ai/package.json index d44e29c7..daf31261 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/README.md b/.publish/authz/README.md index f0102ea5..4247ad62 100644 --- a/.publish/authz/README.md +++ b/.publish/authz/README.md @@ -149,3 +149,156 @@ app.put( - **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported. - Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`. - Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise` (e.g. for a database ownership check). + +## Declaring permissions + +The RBAC/PBAC/ABAC surface above is the low-level toolkit. On top of it sits a +declarative **registry + catalog + store + engine**: permissions, roles, and +policies are declared once in code, merged into a frozen catalog at boot, and +resolved per-request against a pluggable `PermissionStore` that holds who has +what. + +Put declarations in `app/authz/.ts`; they are discovered automatically +and merged (conflicting declarations of the same permission/role/policy across +files fail the boot loudly, naming both source files). + +```ts +import { defineAuthz, owner } from "@wrnexus/authz"; + +export default defineAuthz({ + permissions: { + "post:read": { title: "View posts", public: true }, + "post:delete": { title: "Delete posts", risk: "high" }, + }, + // "post:*" is a namespace wildcard grant, valid inside a role's list — it is + // not itself a registered permission, so it can only ever grant permissions + // that ARE declared above (e.g. "post:read", "post:delete"). + roles: { editor: ["post:*"], admin: ["role:editor"] }, + policies: { ownsPost: owner("id", "authorId") }, + bindings: { "post:delete": ["ownsPost"] }, +}); +``` + +`public: true` means anonymous callers may hold the permission — but any +policy bound to it still runs, and can still veto the anonymous caller (e.g. a +`notBanned` policy on a public `post:preview` permission). + +## Checking permissions + +Register `authzMiddleware` once, in `app/middleware/`, with the merged +catalog and a `PermissionStore`. Like every other `app/middleware/*.ts` file, +the registration is an eager, module-scope call — the same shape as +`authzMiddleware({ catalog, store })` requires — so it must run after the +catalog has been populated. Both the dev server and `wrnexus build`'s +generated production entry guarantee `getAuthzCatalog()` is populated before +any app middleware module evaluates. Name the file so it sorts after whatever +middleware sets `ctx.user` (middleware runs in alphabetical filename order — +`authz.ts` after `auth.ts`, for instance). + +```ts +// app/middleware/authz.ts +import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz"; +import { dbPermissionStore } from "@wrnexus/authz/db"; +import { getDb } from "@wrnexus/db"; + +export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) }); +``` + +> **`subject.id` must be a non-empty string.** The engine denies (and logs to +> stderr) whenever `ctx.user.id` is present but not a non-empty string — this +> includes the common case of an integer primary key. Coerce it before it +> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for +> that user denies with "Invalid subject" instead of resolving normally. +> `owner()` (the built-in ownership policy) compares subject and resource ids +> with `Object.is`, so both sides must be the same type too — `owner()` on a +> numeric `resource.authorId` against a stringified `subject.id` never +> matches even when they represent "the same" id. + +There is no per-route `middleware` export — `app/middleware/*.ts` is the only +place middleware is registered. To gate part of the app, branch on the +request the same way any other conditional middleware does (compare +`app/middleware/captcha-login.ts` in the auth showcase, which branches on +method + path the same way): + +```ts +// app/middleware/protect-posts.ts +import type { Context, Next } from "@wrnexus/core"; +import { guardPermission } from "@wrnexus/authz"; + +const guardPostWrite = guardPermission("post:write"); + +export default function protectPosts(ctx: Context, next: Next) { + return ctx.url.pathname.startsWith("/api/posts") && ctx.req.method !== "GET" + ? guardPostWrite(ctx, next) + : next(); +} +``` + +Or check inline inside a route handler with the free function `can()`: + +```ts +// app/api/posts/[id].ts +import type { Context } from "@wrnexus/core"; +import { can } from "@wrnexus/authz"; + +export const DELETE = async (ctx: Context) => { + const post = { id: "1", authorId: "alice" }; // load your own resource here + if (!(await can(ctx, "post:delete", post))) { + return Response.json({ ok: false, error: "Forbidden" }, { status: 403 }); + } + return Response.json({ ok: true }); +}; +``` + +`can()` is a free function taking `ctx`, not `ctx.can` — `@wrnexus/core` must +not depend on `@wrnexus/authz`, so the per-request resolver lives in +`ctx.locals` instead, reached through `can()` / `decideFor()` / +`guardPermission()` / `filterCan()`. Calling any of them before +`authzMiddleware` has run for that request throws a `WRN-AUTHZ-SETUP` error +naming the missing registration, rather than silently denying. + +See `examples/auth-showcase/app/authz/showcase.ts` and +`examples/auth-showcase/app/middleware/authz.ts` for a complete, runnable +version of this wiring. + +## Precedence + +1. An explicit deny wins over everything, including `*` — and honours the + same namespace-wildcard matching as grants (denying `post:*` blocks + `post:comment:delete`, not just `post:*` itself). +2. A bound policy can veto a permission a role grants, and runs even for a + `public: true` permission — including for an anonymous caller. +3. Otherwise the permission must be held via a role or an explicit grant. +4. Default deny. + +Every failure — an unknown permission (outside strict/dev mode), a store +outage, a thrown policy — denies rather than throwing through to the caller. + +`permissionsFor()` (on the resolver returned by `createAuthzResolver`) is a +coarse hint for hiding UI (e.g. a menu section), **never authoritative**. A +`Set` cannot represent "granted `post:*` except `post:delete`", so a +narrow deny beneath a broad grant is invisible to it — the set still contains +`post:*` while `can()` / `decide()` correctly refuse `post:delete`. Gate real +actions with `can()`, `decideFor()`, or `filterCan()`; never by matching +against `permissionsFor()`'s result. + +## CLI + +```bash +wrnexus authz list # every registered permission, role, and policy +wrnexus authz generate # app/authz/permissions.gen.ts type unions +wrnexus authz init # scaffold the assignment-table migration +``` + +`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal +union — `can()`, `guardPermission()`, and `decideFor()` all take a bare +`string` and nothing reads this file automatically, so import it to type your +own helpers/constants against the registered catalog, e.g.: + +```ts +import type { Permission } from "app/authz/permissions.gen.ts"; + +function guard(permission: Permission) { + return guardPermission(permission); +} +``` diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 4bf8e00d..dc8f9f44 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", @@ -34,8 +34,16 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./db": { + "types": "./dist/db.d.ts", + "import": "./dist/db.js" } }, + "dependencies": { + "@wrnexus/core": "^0.8.5", + "@wrnexus/db": "^0.8.5" + }, "files": [ "dist", "README.md" diff --git a/.publish/cli/package.json b/.publish/cli/package.json index b1038c88..aadcb11f 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -44,22 +44,23 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.8.4", - "@wrnexus/router": "^0.8.4", - "@wrnexus/csr": "^0.8.4", - "@wrnexus/compiler": "^0.8.4", - "@wrnexus/styles": "^0.8.4", - "@wrnexus/dev-server": "^0.8.4", - "@wrnexus/ui": "^0.8.4", - "@wrnexus/validation": "^0.8.4", - "@wrnexus/i18n": "^0.8.4", - "@wrnexus/mcp": "^0.8.4", - "@wrnexus/playground": "^0.8.4", - "@wrnexus/db": "^0.8.4", - "@wrnexus/plugin": "^0.8.4", - "@wrnexus/syntax": "^0.8.4", - "@wrnexus/typecheck": "^0.8.4", - "@wrnexus/security": "^0.8.4", + "@wrnexus/core": "^0.8.5", + "@wrnexus/router": "^0.8.5", + "@wrnexus/csr": "^0.8.5", + "@wrnexus/compiler": "^0.8.5", + "@wrnexus/styles": "^0.8.5", + "@wrnexus/dev-server": "^0.8.5", + "@wrnexus/ui": "^0.8.5", + "@wrnexus/validation": "^0.8.5", + "@wrnexus/i18n": "^0.8.5", + "@wrnexus/mcp": "^0.8.5", + "@wrnexus/playground": "^0.8.5", + "@wrnexus/db": "^0.8.5", + "@wrnexus/authz": "^0.8.5", + "@wrnexus/plugin": "^0.8.5", + "@wrnexus/syntax": "^0.8.5", + "@wrnexus/typecheck": "^0.8.5", + "@wrnexus/security": "^0.8.5", "selfsigned": "^5.5.0" }, "files": [ diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 0051dc95..4c094a17 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", @@ -37,10 +37,10 @@ } }, "dependencies": { - "@wrnexus/csr": "^0.8.4", - "@wrnexus/syntax": "^0.8.4", - "@wrnexus/store": "^0.8.4", - "@wrnexus/validation": "^0.8.4" + "@wrnexus/csr": "^0.8.5", + "@wrnexus/syntax": "^0.8.5", + "@wrnexus/store": "^0.8.5", + "@wrnexus/validation": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/core/package.json b/.publish/core/package.json index 497d1d54..fb5e9bb2 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 86d40d03..9b86effd 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4" + "@wrnexus/core": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/db/package.json b/.publish/db/package.json index c1f51571..6fb6505a 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 8c4d4374..681f649d 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -41,25 +41,27 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4", - "@wrnexus/dev-toolbar": "^0.8.4", - "@wrnexus/router": "^0.8.4", - "@wrnexus/ssr": "^0.8.4", - "@wrnexus/csr": "^0.8.4", - "@wrnexus/compiler": "^0.8.4", - "@wrnexus/styles": "^0.8.4", - "@wrnexus/ui": "^0.8.4", - "@wrnexus/validation": "^0.8.4", - "@wrnexus/i18n": "^0.8.4", - "@wrnexus/db": "^0.8.4", - "@wrnexus/pubsub": "^0.8.4", - "@wrnexus/uploader": "^0.8.4", - "@wrnexus/plugin": "^0.8.4", - "@wrnexus/store": "^0.8.4", - "@wrnexus/security": "^0.8.4", - "@wrnexus/observability": "^0.8.4", - "@wrnexus/cache": "^0.8.4", - "@wrnexus/pwa": "^0.8.4" + "@wrnexus/authz": "^0.8.5", + "@wrnexus/rpc": "^0.8.5", + "@wrnexus/core": "^0.8.5", + "@wrnexus/dev-toolbar": "^0.8.5", + "@wrnexus/router": "^0.8.5", + "@wrnexus/ssr": "^0.8.5", + "@wrnexus/csr": "^0.8.5", + "@wrnexus/compiler": "^0.8.5", + "@wrnexus/styles": "^0.8.5", + "@wrnexus/ui": "^0.8.5", + "@wrnexus/validation": "^0.8.5", + "@wrnexus/i18n": "^0.8.5", + "@wrnexus/db": "^0.8.5", + "@wrnexus/pubsub": "^0.8.5", + "@wrnexus/uploader": "^0.8.5", + "@wrnexus/plugin": "^0.8.5", + "@wrnexus/store": "^0.8.5", + "@wrnexus/security": "^0.8.5", + "@wrnexus/observability": "^0.8.5", + "@wrnexus/cache": "^0.8.5", + "@wrnexus/pwa": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/dev-toolbar/package.json b/.publish/dev-toolbar/package.json index babace46..df5dbe4f 100644 --- a/.publish/dev-toolbar/package.json +++ b/.publish/dev-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-toolbar", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 03c619cb..4d11d955 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4" + "@wrnexus/core": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 527d37d7..00cdc7b0 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4" + "@wrnexus/core": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index ff7a6fe8..77c532d0 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.", "license": "MIT", @@ -42,9 +42,9 @@ "./components/*": "./components/*" }, "dependencies": { - "@wrnexus/core": "^0.8.4", - "@wrnexus/plugin": "^0.8.4", - "@wrnexus/ui": "^0.8.4" + "@wrnexus/core": "^0.8.5", + "@wrnexus/plugin": "^0.8.5", + "@wrnexus/ui": "^0.8.5" }, "wrnexus": { "plugin": { diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 98c824ad..8705737c 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4" + "@wrnexus/core": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 3eec6352..68c2c27b 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.8.4" + "@wrnexus/native": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/native/package.json b/.publish/native/package.json index ecae31cf..05a7a94c 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 7800062a..1b7757c6 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/jwt": "^0.8.4" + "@wrnexus/jwt": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/plugin/package.json b/.publish/plugin/package.json index d6222afc..29df8a9a 100644 --- a/.publish/plugin/package.json +++ b/.publish/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/plugin", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/plugin — part of the WrNexus framework.", "license": "MIT", @@ -49,7 +49,7 @@ } }, "dependencies": { - "@wrnexus/syntax": "^0.8.4" + "@wrnexus/syntax": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index b5d7bd92..0270f874 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index b00444b6..db862087 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4" + "@wrnexus/core": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index c919bcb8..b867e156 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 85b2e4c0..3751cd76 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -37,8 +37,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.8.4", - "@wrnexus/core": "^0.8.4" + "@wrnexus/compiler": "^0.8.5", + "@wrnexus/core": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 072db827..af9df974 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -45,9 +45,9 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.4", - "@wrnexus/store": "^0.8.4", - "@wrnexus/security": "^0.8.4" + "@wrnexus/core": "^0.8.5", + "@wrnexus/store": "^0.8.5", + "@wrnexus/security": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/styles/package.json b/.publish/styles/package.json index f535d396..09ff8f05 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -37,9 +37,9 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.8.4", - "@wrnexus/core": "^0.8.4", - "@wrnexus/plugin": "^0.8.4" + "@wrnexus/uploader": "^0.8.5", + "@wrnexus/core": "^0.8.5", + "@wrnexus/plugin": "^0.8.5" }, "files": [ "dist", diff --git a/.publish/syntax/package.json b/.publish/syntax/package.json index ff45af91..ed60bf5d 100644 --- a/.publish/syntax/package.json +++ b/.publish/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/syntax — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/test/package.json b/.publish/test/package.json index 9e8a1442..894122af 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index f9ed7611..54ea9c20 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.8.4", + "version": "0.8.5", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/COMPONENTS.md b/.publish/ui/COMPONENTS.md index a4f489bd..a0ab568d 100644 --- a/.publish/ui/COMPONENTS.md +++ b/.publish/ui/COMPONENTS.md @@ -359,6 +359,15 @@ Reusable preference switcher component. - Slots: None - Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])` +### Toaster + +Reusable toaster component. + +- Mount: `data-component="Toaster"` +- Props: `color: string = "info"`, `size: string = "default"`, `position: string = "bottom-right"`, `duration: number = 4500`, `max: number = 4`, `pauseOnHover: boolean = true`, `showIcon: boolean = true`, `successIcon: string = ""`, `dangerIcon: string = ""`, `warningIcon: string = ""`, `infoIcon: string = ""`, `closable: boolean = true`, `showProgress: boolean = true`, `closeLabel: string = "Dismiss notification"`, `class: string = ""` +- Slots: None +- Outputs: `show({ id: number; message: string; tone: string })`, `dismiss({ id: number; reason: string })`, `action({ id: number; sourceEvent: Event })` + ## Data ### MetricCard @@ -554,15 +563,6 @@ Theme-aware, responsive data map component. - Slots: `default` - Outputs: `select({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)` -### DataTable - -Theme-aware, responsive data table component. - -- Mount: `data-component="DataTable"` -- Props: `size: string = "default"`, `color: string = "primary"`, `caption: string = "Data Table"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `striped: boolean = true`, `class: string = ""` -- Slots: `default` -- Outputs: `sort({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `rowClick({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `pageChange({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)` - ### DragAndDrop Theme-aware, responsive drag and drop component. @@ -945,7 +945,7 @@ Open an accessible keyboard-aware action menu from pointer or keyboard context i Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events. - Mount: `data-component="Drawer"` -- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""` +- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `duration: number = 260`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""` - Slots: `trigger`, `header`, `default`, `footer` - Outputs: `open({ placement: string; sourceEvent: Event })`, `close({ reason: string; placement: string; sourceEvent: Event })`, `cancel({ placement: string; sourceEvent: Event })` @@ -963,7 +963,7 @@ Open an accessible anchored menu with keyboard navigation, item selection, actio Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing. - Mount: `data-component="Modal"` -- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `class: string = ""` +- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `scrollBehavior: string = "inside"`, `class: string = ""` - Slots: `trigger`, `header`, `default`, `footer` - Outputs: `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `cancel({ sourceEvent: Event })`, `confirm({ sourceEvent: Event })` @@ -987,11 +987,11 @@ Show concise accessible contextual help on hover, focus, click, or controlled op ## Tables -### Table +### DataTable -Theme-aware, responsive table component. +Sortable, filterable, paginated data table with row selection. -- Mount: `data-component="Table"` -- Props: `size: string = "default"`, `color: string = "primary"`, `caption: string = "Table"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `striped: boolean = true`, `class: string = ""` +- Mount: `data-component="DataTable"` +- Props: `color: string = "primary"`, `size: string = "default"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `rowKey: string = "id"`, `remote: boolean = false`, `loadingLabel: string = "Loading"`, `errorLabel: string = "Could not load this data"`, `retryLabel: string = "Try again"`, `caption: string = ""`, `description: string = ""`, `searchable: boolean = true`, `searchPlaceholder: string = "Search"`, `paginated: boolean = true`, `pageSize: number = 10`, `paginationStyle: string = "compact"`, `pageSizes: number[] = [10, 25, 50]`, `selectable: boolean = false`, `actions: unknown[] = []`, `striped: boolean = true`, `bordered: boolean = true`, `gridlines: string = "rows"`, `density: string = "default"`, `emptyLabel: string = "No records to show"`, `noResultsLabel: string = "No records match your search"`, `clearSearchLabel: string = "Clear search"`, `stickyFirstColumn: boolean = false`, `layout: string = "rows"`, `class: string = ""` - Slots: `default` -- Outputs: `sort({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `rowClick({ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)` +- Outputs: `sort({ key: string; direction: string })`, `search({ query: string })`, `pageChange({ page: number; pageSize: number })`, `select({ selected: Array; all: boolean })`, `change({ page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string })`, `rowClick({ row: object; sourceEvent: Event })`, `action({ id: string; selected: Array; rows: object[]; sourceEvent: Event })`, `request({ instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string })` diff --git a/.publish/ui/component-catalog.json b/.publish/ui/component-catalog.json index 007b14ae..c4478a65 100644 --- a/.publish/ui/component-catalog.json +++ b/.publish/ui/component-catalog.json @@ -168,8 +168,8 @@ }, { "name": "DataTable", - "category": "integrations", - "purpose": "Theme-aware, responsive data table component." + "category": "tables", + "purpose": "Sortable, filterable, paginated data table with row selection." }, { "name": "DatePicker", @@ -471,11 +471,6 @@ "category": "forms", "purpose": "Theme-aware, responsive switch component." }, - { - "name": "Table", - "category": "tables", - "purpose": "Theme-aware, responsive table component." - }, { "name": "Tabs", "category": "navigation", @@ -511,6 +506,11 @@ "category": "integrations", "purpose": "Theme-aware, responsive toast notifications component." }, + { + "name": "Toaster", + "category": "core", + "purpose": "Reusable toaster component." + }, { "name": "ToggleCount", "category": "advanced-forms", diff --git a/.publish/ui/component-reference.json b/.publish/ui/component-reference.json index 258a50b0..80cf8373 100644 --- a/.publish/ui/component-reference.json +++ b/.publish/ui/component-reference.json @@ -4569,16 +4569,9 @@ { "name": "DataTable", "mount": "DataTable", - "category": "integrations", - "purpose": "Theme-aware, responsive data table component.", + "category": "tables", + "purpose": "Sortable, filterable, paginated data table with row selection.", "props": [ - { - "name": "size", - "type": "string", - "required": false, - "default": "\"default\"", - "options": [] - }, { "name": "color", "type": "string", @@ -4587,10 +4580,10 @@ "options": [] }, { - "name": "caption", + "name": "size", "type": "string", "required": false, - "default": "\"Data Table\"", + "default": "\"default\"", "options": [] }, { @@ -4607,6 +4600,111 @@ "default": "[]", "options": [] }, + { + "name": "rowKey", + "type": "string", + "required": false, + "default": "\"id\"", + "options": [] + }, + { + "name": "remote", + "type": "boolean", + "required": false, + "default": "false", + "options": [] + }, + { + "name": "loadingLabel", + "type": "string", + "required": false, + "default": "\"Loading\"", + "options": [] + }, + { + "name": "errorLabel", + "type": "string", + "required": false, + "default": "\"Could not load this data\"", + "options": [] + }, + { + "name": "retryLabel", + "type": "string", + "required": false, + "default": "\"Try again\"", + "options": [] + }, + { + "name": "caption", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, + { + "name": "description", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, + { + "name": "searchable", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "searchPlaceholder", + "type": "string", + "required": false, + "default": "\"Search\"", + "options": [] + }, + { + "name": "paginated", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "pageSize", + "type": "number", + "required": false, + "default": "10", + "options": [] + }, + { + "name": "paginationStyle", + "type": "string", + "required": false, + "default": "\"compact\"", + "options": [] + }, + { + "name": "pageSizes", + "type": "number[]", + "required": false, + "default": "[10, 25, 50]", + "options": [] + }, + { + "name": "selectable", + "type": "boolean", + "required": false, + "default": "false", + "options": [] + }, + { + "name": "actions", + "type": "unknown[]", + "required": false, + "default": "[]", + "options": [] + }, { "name": "striped", "type": "boolean", @@ -4614,6 +4712,62 @@ "default": "true", "options": [] }, + { + "name": "bordered", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "gridlines", + "type": "string", + "required": false, + "default": "\"rows\"", + "options": [] + }, + { + "name": "density", + "type": "string", + "required": false, + "default": "\"default\"", + "options": [] + }, + { + "name": "emptyLabel", + "type": "string", + "required": false, + "default": "\"No records to show\"", + "options": [] + }, + { + "name": "noResultsLabel", + "type": "string", + "required": false, + "default": "\"No records match your search\"", + "options": [] + }, + { + "name": "clearSearchLabel", + "type": "string", + "required": false, + "default": "\"Clear search\"", + "options": [] + }, + { + "name": "stickyFirstColumn", + "type": "boolean", + "required": false, + "default": "false", + "options": [] + }, + { + "name": "layout", + "type": "string", + "required": false, + "default": "\"rows\"", + "options": [] + }, { "name": "class", "type": "string", @@ -4626,26 +4780,47 @@ "outputs": [ { "name": "sort", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" + "payloadType": "{ key: string; direction: string }" }, { - "name": "select", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" - }, - { - "name": "change", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" - }, - { - "name": "rowClick", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" + "name": "search", + "payloadType": "{ query: string }" }, { "name": "pageChange", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" + "payloadType": "{ page: number; pageSize: number }" + }, + { + "name": "select", + "payloadType": "{ selected: Array; all: boolean }" + }, + { + "name": "change", + "payloadType": "{ page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string }" + }, + { + "name": "rowClick", + "payloadType": "{ row: object; sourceEvent: Event }" + }, + { + "name": "action", + "payloadType": "{ id: string; selected: Array; rows: object[]; sourceEvent: Event }" + }, + { + "name": "request", + "payloadType": "{ instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string }" } ], - "events": ["sort", "select", "change", "rowClick", "pageChange"], + "events": [ + "sort", + "search", + "pageChange", + "select", + "change", + "rowClick", + "action", + "request" + ], "source": "components/DataTable.wrn" }, { @@ -5220,6 +5395,13 @@ "default": "true", "options": [] }, + { + "name": "duration", + "type": "number", + "required": false, + "default": "260", + "options": [] + }, { "name": "overlay", "type": "boolean", @@ -8736,6 +8918,13 @@ "default": "true", "options": [] }, + { + "name": "scrollBehavior", + "type": "string", + "required": false, + "default": "\"inside\"", + "options": [] + }, { "name": "class", "type": "string", @@ -12009,80 +12198,6 @@ "events": ["input", "change", "focus", "blur"], "source": "components/Switch.wrn" }, - { - "name": "Table", - "mount": "Table", - "category": "tables", - "purpose": "Theme-aware, responsive table component.", - "props": [ - { - "name": "size", - "type": "string", - "required": false, - "default": "\"default\"", - "options": [] - }, - { - "name": "color", - "type": "string", - "required": false, - "default": "\"primary\"", - "options": [] - }, - { - "name": "caption", - "type": "string", - "required": false, - "default": "\"Table\"", - "options": [] - }, - { - "name": "columns", - "type": "unknown[]", - "required": false, - "default": "[]", - "options": [] - }, - { - "name": "rows", - "type": "unknown[]", - "required": false, - "default": "[]", - "options": [] - }, - { - "name": "striped", - "type": "boolean", - "required": false, - "default": "true", - "options": [] - }, - { - "name": "class", - "type": "string", - "required": false, - "default": "\"\"", - "options": [] - } - ], - "slots": ["default"], - "outputs": [ - { - "name": "sort", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" - }, - { - "name": "select", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" - }, - { - "name": "rowClick", - "payloadType": "{ value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" - } - ], - "events": ["sort", "select", "rowClick"], - "source": "components/table.wrn" - }, { "name": "Tabs", "mount": "Tabs", @@ -12893,6 +13008,136 @@ "events": ["add", "dismiss", "clear", "action"], "source": "components/ToastNotifications.wrn" }, + { + "name": "Toaster", + "mount": "Toaster", + "category": "core", + "purpose": "Reusable toaster component.", + "props": [ + { + "name": "color", + "type": "string", + "required": false, + "default": "\"info\"", + "options": [] + }, + { + "name": "size", + "type": "string", + "required": false, + "default": "\"default\"", + "options": [] + }, + { + "name": "position", + "type": "string", + "required": false, + "default": "\"bottom-right\"", + "options": [] + }, + { + "name": "duration", + "type": "number", + "required": false, + "default": "4500", + "options": [] + }, + { + "name": "max", + "type": "number", + "required": false, + "default": "4", + "options": [] + }, + { + "name": "pauseOnHover", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "showIcon", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "successIcon", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, + { + "name": "dangerIcon", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, + { + "name": "warningIcon", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, + { + "name": "infoIcon", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, + { + "name": "closable", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "showProgress", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "closeLabel", + "type": "string", + "required": false, + "default": "\"Dismiss notification\"", + "options": [] + }, + { + "name": "class", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + } + ], + "slots": [], + "outputs": [ + { + "name": "show", + "payloadType": "{ id: number; message: string; tone: string }" + }, + { + "name": "dismiss", + "payloadType": "{ id: number; reason: string }" + }, + { + "name": "action", + "payloadType": "{ id: number; sourceEvent: Event }" + } + ], + "events": ["show", "dismiss", "action"], + "source": "components/Toaster.wrn" + }, { "name": "ToggleCount", "mount": "ToggleCount", diff --git a/.publish/ui/components/ContextMenu.wrn b/.publish/ui/components/ContextMenu.wrn index fa691b75..78e3f4b7 100644 --- a/.publish/ui/components/ContextMenu.wrn +++ b/.publish/ui/components/ContextMenu.wrn @@ -45,8 +45,12 @@ component ContextMenu { sourceEvent.preventDefault() } if (placement === "pointer" && sourceEvent) { - positionX = Math.max(12, Math.min(sourceEvent.clientX || 12, window.innerWidth - 340)) - positionY = Math.max(12, Math.min(sourceEvent.clientY || 12, window.innerHeight - 420)) + // Place the menu at the pointer and let the anchored clamp in the + // runtime pull it back on screen once it has been laid out and can + // actually be measured. Subtracting a guessed 340x420 here instead + // pushed every menu that was not that size away from the pointer. + positionX = Math.max(12, sourceEvent.clientX || 12) + positionY = Math.max(12, sourceEvent.clientY || 12) } visible = true output.open({ @@ -161,6 +165,7 @@ component ContextMenu {