release: WRNexusJS 0.5.10
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -28,18 +28,18 @@
|
|||||||
"wrnexus": "./dist/index.js"
|
"wrnexus": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1",
|
"@wrnexus/core": "^0.5.10",
|
||||||
"@wrnexus/router": "^0.5.1",
|
"@wrnexus/router": "^0.5.10",
|
||||||
"@wrnexus/csr": "^0.5.1",
|
"@wrnexus/csr": "^0.5.10",
|
||||||
"@wrnexus/compiler": "^0.5.1",
|
"@wrnexus/compiler": "^0.5.10",
|
||||||
"@wrnexus/styles": "^0.5.1",
|
"@wrnexus/styles": "^0.5.10",
|
||||||
"@wrnexus/dev-server": "^0.5.1",
|
"@wrnexus/dev-server": "^0.5.10",
|
||||||
"@wrnexus/ui": "^0.5.1",
|
"@wrnexus/ui": "^0.5.10",
|
||||||
"@wrnexus/validation": "^0.5.1",
|
"@wrnexus/validation": "^0.5.10",
|
||||||
"@wrnexus/i18n": "^0.5.1",
|
"@wrnexus/i18n": "^0.5.10",
|
||||||
"@wrnexus/db": "^0.5.1",
|
"@wrnexus/db": "^0.5.10",
|
||||||
"@wrnexus/plugin": "^0.5.1",
|
"@wrnexus/plugin": "^0.5.10",
|
||||||
"@wrnexus/syntax": "^0.5.1"
|
"@wrnexus/syntax": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "^0.5.1"
|
"@wrnexus/syntax": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -25,20 +25,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1",
|
"@wrnexus/core": "^0.5.10",
|
||||||
"@wrnexus/dev-toolbar": "^0.5.1",
|
"@wrnexus/dev-toolbar": "^0.5.10",
|
||||||
"@wrnexus/router": "^0.5.1",
|
"@wrnexus/router": "^0.5.10",
|
||||||
"@wrnexus/ssr": "^0.5.1",
|
"@wrnexus/ssr": "^0.5.10",
|
||||||
"@wrnexus/csr": "^0.5.1",
|
"@wrnexus/csr": "^0.5.10",
|
||||||
"@wrnexus/compiler": "^0.5.1",
|
"@wrnexus/compiler": "^0.5.10",
|
||||||
"@wrnexus/styles": "^0.5.1",
|
"@wrnexus/styles": "^0.5.10",
|
||||||
"@wrnexus/ui": "^0.5.1",
|
"@wrnexus/ui": "^0.5.10",
|
||||||
"@wrnexus/validation": "^0.5.1",
|
"@wrnexus/validation": "^0.5.10",
|
||||||
"@wrnexus/i18n": "^0.5.1",
|
"@wrnexus/i18n": "^0.5.10",
|
||||||
"@wrnexus/db": "^0.5.1",
|
"@wrnexus/db": "^0.5.10",
|
||||||
"@wrnexus/pubsub": "^0.5.1",
|
"@wrnexus/pubsub": "^0.5.10",
|
||||||
"@wrnexus/uploader": "^0.5.1",
|
"@wrnexus/uploader": "^0.5.10",
|
||||||
"@wrnexus/plugin": "^0.5.1"
|
"@wrnexus/plugin": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
|
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/native": "^0.5.1"
|
"@wrnexus/native": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/plugin",
|
"name": "@wrnexus/plugin",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/plugin — part of the WrNexus framework.",
|
"description": "@wrnexus/plugin — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "^0.5.1"
|
"@wrnexus/syntax": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "^0.5.1",
|
"@wrnexus/compiler": "^0.5.10",
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/uploader": "^0.5.1",
|
"@wrnexus/uploader": "^0.5.10",
|
||||||
"@wrnexus/core": "^0.5.1",
|
"@wrnexus/core": "^0.5.10",
|
||||||
"@wrnexus/plugin": "^0.5.1"
|
"@wrnexus/plugin": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/syntax",
|
"name": "@wrnexus/syntax",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/syntax — part of the WrNexus framework.",
|
"description": "@wrnexus/syntax — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+57
-10
@@ -1,6 +1,6 @@
|
|||||||
# WRNexus UI component reference
|
# WRNexus UI component reference
|
||||||
|
|
||||||
This reference is generated from the 85 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
This reference is generated from the 90 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
||||||
|
|
||||||
## Advanced-forms
|
## Advanced-forms
|
||||||
|
|
||||||
@@ -330,15 +330,35 @@ Theme-aware, responsive tree view component.
|
|||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Events: `select`, `toggle`, `expand`, `collapse`
|
- Events: `select`, `toggle`, `expand`, `collapse`
|
||||||
|
|
||||||
|
## Blocks
|
||||||
|
|
||||||
|
### PortalDashboard
|
||||||
|
|
||||||
|
Responsive portal dashboard with metrics, actions, tasks, and updates.
|
||||||
|
|
||||||
|
- Mount: `data-component="PortalDashboard"`
|
||||||
|
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Overview"`, `eyebrowKey: string = ""`, `title: string = "Dashboard"`, `titleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `userName: string = ""`, `metrics: string = []`, `actions: string = []`, `updates: string = []`, `tasks: string = []`, `class: string = ""`
|
||||||
|
- Slots: `hero-action`
|
||||||
|
- Events: `action`, `navigate`
|
||||||
|
|
||||||
## Forms
|
## Forms
|
||||||
|
|
||||||
|
### AuthForm
|
||||||
|
|
||||||
|
Reusable authentication form for sign-in, registration, recovery, reset, and MFA.
|
||||||
|
|
||||||
|
- Mount: `data-component="AuthForm"`
|
||||||
|
- Props: `size: string = "default"`, `color: string = "primary"`, `mode: string = "sign-in"`, `action: string = "/api/auth/login"`, `method: string = "post"`, `title: string = "Sign in"`, `description: string = ""`, `returnTo: string = ""`, `schema: string = ""`, `showRemember: boolean = true`, `showName: boolean = true`, `submitLabel: string = "Continue"`, `class: string = ""`
|
||||||
|
- Slots: `default`
|
||||||
|
- Events: `submit`, `change`, `input`, `focus`, `blur`
|
||||||
|
|
||||||
### Checkbox
|
### Checkbox
|
||||||
|
|
||||||
Theme-aware, responsive checkbox component.
|
Theme-aware, responsive checkbox component.
|
||||||
|
|
||||||
- Mount: `data-component="Checkbox"`
|
- Mount: `data-component="Checkbox"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Checkbox"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `values: string = []`, `options: string = []`, `checked: boolean = false`, `indeterminate: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Checkbox"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `values: string = []`, `options: string = []`, `checked: boolean = false`, `indeterminate: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
||||||
- Slots: None
|
- Slots: `default`
|
||||||
- Events: `input`, `change`, `focus`, `blur`, `invalid`
|
- Events: `input`, `change`, `focus`, `blur`, `invalid`
|
||||||
|
|
||||||
### ColorPicker
|
### ColorPicker
|
||||||
@@ -543,6 +563,15 @@ Theme-aware, responsive wysiwyg editor component.
|
|||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
### AuthSplitLayout
|
||||||
|
|
||||||
|
Responsive 50/50 authentication layout with content and form regions.
|
||||||
|
|
||||||
|
- Mount: `data-component="AuthSplitLayout"`
|
||||||
|
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Secure identity"`, `title: string = "Welcome back"`, `description: string = ""`, `brand: string = "Police Management System"`, `features: string = []`, `class: string = ""`
|
||||||
|
- Slots: `aside-extra`, `form`
|
||||||
|
- Events: None
|
||||||
|
|
||||||
### Columns
|
### Columns
|
||||||
|
|
||||||
Theme-aware, responsive columns component.
|
Theme-aware, responsive columns component.
|
||||||
@@ -644,6 +673,15 @@ Theme-aware, responsive breadcrumb component.
|
|||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Events: `navigate`, `click`
|
- Events: `navigate`, `click`
|
||||||
|
|
||||||
|
### Footer
|
||||||
|
|
||||||
|
Responsive application footer with structured links and pre/post content slots.
|
||||||
|
|
||||||
|
- Mount: `data-component="Footer"`
|
||||||
|
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: string = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
|
||||||
|
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
|
||||||
|
- Events: `select`, `action`
|
||||||
|
|
||||||
### MegaMenu
|
### MegaMenu
|
||||||
|
|
||||||
Theme-aware, responsive mega menu component.
|
Theme-aware, responsive mega menu component.
|
||||||
@@ -667,9 +705,9 @@ Theme-aware, responsive nav component.
|
|||||||
Theme-aware, responsive navbar component.
|
Theme-aware, responsive navbar component.
|
||||||
|
|
||||||
- Mount: `data-component="Navbar"`
|
- Mount: `data-component="Navbar"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Navbar"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: string = {}`, `items: string = []`, `actions: string = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `topbar`, `actions`
|
||||||
- Events: `toggle`, `open`, `close`
|
- Events: `toggle`, `open`, `close`, `select`, `action`
|
||||||
|
|
||||||
### Pagination
|
### Pagination
|
||||||
|
|
||||||
@@ -680,6 +718,15 @@ Theme-aware, responsive pagination component.
|
|||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Events: `change`, `previous`, `next`
|
- Events: `change`, `previous`, `next`
|
||||||
|
|
||||||
|
### PreferenceSwitcher
|
||||||
|
|
||||||
|
Accessible theme, accent color, and language preference controls.
|
||||||
|
|
||||||
|
- Mount: `data-component="PreferenceSwitcher"`
|
||||||
|
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
|
||||||
|
- Slots: None
|
||||||
|
- Events: `theme`, `color`, `language`
|
||||||
|
|
||||||
### Scrollspy
|
### Scrollspy
|
||||||
|
|
||||||
Theme-aware, responsive scrollspy component.
|
Theme-aware, responsive scrollspy component.
|
||||||
@@ -694,9 +741,9 @@ Theme-aware, responsive scrollspy component.
|
|||||||
Theme-aware, responsive sidebar component.
|
Theme-aware, responsive sidebar component.
|
||||||
|
|
||||||
- Mount: `data-component="Sidebar"`
|
- Mount: `data-component="Sidebar"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mobileLabel: string = "Open navigation"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Events: `toggle`, `open`, `close`
|
- Events: `toggle`, `open`, `close`, `select`
|
||||||
|
|
||||||
### Stepper
|
### Stepper
|
||||||
|
|
||||||
@@ -741,9 +788,9 @@ Theme-aware, responsive drawer component.
|
|||||||
Theme-aware, responsive dropdown component.
|
Theme-aware, responsive dropdown component.
|
||||||
|
|
||||||
- Mount: `data-component="Dropdown"`
|
- Mount: `data-component="Dropdown"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Dropdown"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Open menu"`, `items: string = []`, `placement: string = "end"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `trigger`, `header`, `footer`
|
||||||
- Events: `open`, `close`, `select`, `change`
|
- Events: `toggle`, `open`, `close`, `select`
|
||||||
|
|
||||||
### Modal
|
### Modal
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,11 @@
|
|||||||
"category": "navigation",
|
"category": "navigation",
|
||||||
"purpose": "Theme-aware, responsive navbar component."
|
"purpose": "Theme-aware, responsive navbar component."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Footer",
|
||||||
|
"category": "navigation",
|
||||||
|
"purpose": "Responsive application footer with structured links and pre/post content slots."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "MegaMenu",
|
"name": "MegaMenu",
|
||||||
"category": "navigation",
|
"category": "navigation",
|
||||||
@@ -429,6 +434,26 @@
|
|||||||
"name": "WysiwygEditor",
|
"name": "WysiwygEditor",
|
||||||
"category": "integrations",
|
"category": "integrations",
|
||||||
"purpose": "Theme-aware, responsive wysiwyg editor component."
|
"purpose": "Theme-aware, responsive wysiwyg editor component."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PreferenceSwitcher",
|
||||||
|
"category": "navigation",
|
||||||
|
"purpose": "Accessible theme, accent color, and language preference controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PortalDashboard",
|
||||||
|
"category": "blocks",
|
||||||
|
"purpose": "Responsive portal dashboard with metrics, actions, tasks, and updates."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AuthSplitLayout",
|
||||||
|
"category": "layout",
|
||||||
|
"purpose": "Responsive 50/50 authentication layout with content and form regions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AuthForm",
|
||||||
|
"category": "forms",
|
||||||
|
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"generatedFrom": "packages/ui/components/*.wrn",
|
"generatedFrom": "packages/ui/components/*.wrn",
|
||||||
"count": 85,
|
"count": 90,
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "Accordion",
|
"name": "Accordion",
|
||||||
@@ -675,6 +675,154 @@
|
|||||||
"events": ["dismiss", "action"],
|
"events": ["dismiss", "action"],
|
||||||
"source": "components/alert.wrn"
|
"source": "components/alert.wrn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "AuthForm",
|
||||||
|
"mount": "AuthForm",
|
||||||
|
"category": "forms",
|
||||||
|
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA.",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"default\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"primary\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mode",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"sign-in\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "action",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"/api/auth/login\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "method",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"post\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Sign in\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "returnTo",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "schema",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "showRemember",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "showName",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "submitLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Continue\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"slots": ["default"],
|
||||||
|
"events": ["submit", "change", "input", "focus", "blur"],
|
||||||
|
"source": "components/AuthForm.wrn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AuthSplitLayout",
|
||||||
|
"mount": "AuthSplitLayout",
|
||||||
|
"category": "layout",
|
||||||
|
"purpose": "Responsive 50/50 authentication layout with content and form regions.",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"default\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"primary\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eyebrow",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Secure identity\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Welcome back\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brand",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Police Management System\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "features",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"slots": ["aside-extra", "form"],
|
||||||
|
"events": [],
|
||||||
|
"source": "components/AuthSplitLayout.wrn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Avatar",
|
"name": "Avatar",
|
||||||
"mount": "Avatar",
|
"mount": "Avatar",
|
||||||
@@ -2062,7 +2210,7 @@
|
|||||||
"default": "\"\""
|
"default": "\"\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": [],
|
"slots": ["default"],
|
||||||
"events": ["input", "change", "focus", "blur", "invalid"],
|
"events": ["input", "change", "focus", "blur", "invalid"],
|
||||||
"source": "components/Checkbox.wrn"
|
"source": "components/Checkbox.wrn"
|
||||||
},
|
},
|
||||||
@@ -3436,34 +3584,22 @@
|
|||||||
"default": "\"primary\""
|
"default": "\"primary\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "title",
|
"name": "label",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": false,
|
"required": false,
|
||||||
"default": "\"Dropdown\""
|
"default": "\"Open menu\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "description",
|
"name": "items",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": false,
|
"required": false,
|
||||||
"default": "\"\""
|
"default": "[]"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "open",
|
|
||||||
"type": "boolean",
|
|
||||||
"required": false,
|
|
||||||
"default": "false"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "placement",
|
"name": "placement",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": false,
|
"required": false,
|
||||||
"default": "\"bottom\""
|
"default": "\"end\""
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "closeLabel",
|
|
||||||
"type": "string",
|
|
||||||
"required": false,
|
|
||||||
"default": "\"Close\""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "class",
|
"name": "class",
|
||||||
@@ -3472,8 +3608,8 @@
|
|||||||
"default": "\"\""
|
"default": "\"\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["trigger", "header", "footer"],
|
||||||
"events": ["open", "close", "select", "change"],
|
"events": ["toggle", "open", "close", "select"],
|
||||||
"source": "components/Dropdown.wrn"
|
"source": "components/Dropdown.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3755,6 +3891,65 @@
|
|||||||
"events": ["cancel", "retry", "complete"],
|
"events": ["cancel", "retry", "complete"],
|
||||||
"source": "components/FileUploadProgress.wrn"
|
"source": "components/FileUploadProgress.wrn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Footer",
|
||||||
|
"mount": "Footer",
|
||||||
|
"category": "navigation",
|
||||||
|
"purpose": "Responsive application footer with structured links and pre/post content slots.",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"default\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"primary\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "label",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Footer navigation\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "items",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "columns",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"default": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxWidth",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"compact\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "copyright",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"slots": ["pre-footer", "post-footer", "copyright-left", "copyright-right"],
|
||||||
|
"events": ["select", "action"],
|
||||||
|
"source": "components/Footer.wrn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Grid",
|
"name": "Grid",
|
||||||
"mount": "Grid",
|
"mount": "Grid",
|
||||||
@@ -5039,7 +5234,19 @@
|
|||||||
"name": "label",
|
"name": "label",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": false,
|
"required": false,
|
||||||
"default": "\"Navbar\""
|
"default": "\"Primary navigation\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "topbarLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Utility navigation\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brand",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "{}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "items",
|
"name": "items",
|
||||||
@@ -5047,6 +5254,12 @@
|
|||||||
"required": false,
|
"required": false,
|
||||||
"default": "[]"
|
"default": "[]"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "actions",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "active",
|
"name": "active",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5054,10 +5267,28 @@
|
|||||||
"default": "\"\""
|
"default": "\"\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "orientation",
|
"name": "sticky",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "openOnHover",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxWidth",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": false,
|
"required": false,
|
||||||
"default": "\"horizontal\""
|
"default": "\"full\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mobileLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Toggle navigation\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "class",
|
"name": "class",
|
||||||
@@ -5066,8 +5297,8 @@
|
|||||||
"default": "\"\""
|
"default": "\"\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["topbar", "actions"],
|
||||||
"events": ["toggle", "open", "close"],
|
"events": ["toggle", "open", "close", "select", "action"],
|
||||||
"source": "components/Navbar.wrn"
|
"source": "components/Navbar.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -5349,6 +5580,166 @@
|
|||||||
"events": ["open", "close"],
|
"events": ["open", "close"],
|
||||||
"source": "components/Popover.wrn"
|
"source": "components/Popover.wrn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "PortalDashboard",
|
||||||
|
"mount": "PortalDashboard",
|
||||||
|
"category": "blocks",
|
||||||
|
"purpose": "Responsive portal dashboard with metrics, actions, tasks, and updates.",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"default\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"primary\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eyebrow",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Overview\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eyebrowKey",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Dashboard\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "titleKey",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "descriptionKey",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userName",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metrics",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "actions",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updates",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tasks",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"slots": ["hero-action"],
|
||||||
|
"events": ["action", "navigate"],
|
||||||
|
"source": "components/PortalDashboard.wrn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PreferenceSwitcher",
|
||||||
|
"mount": "PreferenceSwitcher",
|
||||||
|
"category": "navigation",
|
||||||
|
"purpose": "Accessible theme, accent color, and language preference controls.",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"default\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"primary\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "themeLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Theme\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "colorLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Accent color\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "languageLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Language\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "languages",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "["
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "colors",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "["
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "compact",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"slots": [],
|
||||||
|
"events": ["theme", "color", "language"],
|
||||||
|
"source": "components/PreferenceSwitcher.wrn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Progress",
|
"name": "Progress",
|
||||||
"mount": "Progress",
|
"mount": "Progress",
|
||||||
@@ -6110,6 +6501,12 @@
|
|||||||
"required": false,
|
"required": false,
|
||||||
"default": "\"horizontal\""
|
"default": "\"horizontal\""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mobileLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"Open navigation\""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "class",
|
"name": "class",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -6118,7 +6515,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["default"],
|
||||||
"events": ["toggle", "open", "close"],
|
"events": ["toggle", "open", "close", "select"],
|
||||||
"source": "components/Sidebar.wrn"
|
"source": "components/Sidebar.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ component AvatarGroup {
|
|||||||
role="group"
|
role="group"
|
||||||
aria-label="Avatar group"
|
aria-label="Avatar group"
|
||||||
>
|
>
|
||||||
<div class="wire-next__avatar-group-members">
|
<div
|
||||||
|
class="wire-next__avatar-group-members"
|
||||||
|
>
|
||||||
{#each visibleMembers() as item}
|
{#each visibleMembers() as item}
|
||||||
<span
|
<span
|
||||||
class="wire-next__avatar-group-member"
|
class="wire-next__avatar-group-member"
|
||||||
@@ -64,18 +66,35 @@ component AvatarGroup {
|
|||||||
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
|
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
|
||||||
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
|
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
|
||||||
>
|
>
|
||||||
<span class="wire-next__avatar-group-avatar">
|
<span
|
||||||
|
class="wire-next__avatar-group-avatar"
|
||||||
|
>
|
||||||
{#if item.src}
|
{#if item.src}
|
||||||
<img src="{item.src}" alt="{item.alt || item.name || ''}" loading="lazy" />
|
<img
|
||||||
|
src="{item.src}"
|
||||||
|
alt="{item.alt || item.name || ''}"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
{:else if item.initials}
|
{:else if item.initials}
|
||||||
<span aria-hidden="true">{item.initials}</span>
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{item.initials}
|
||||||
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="icon-[lucide--user-round]" aria-hidden="true"></span>
|
<span
|
||||||
|
class="icon-[lucide--user-round]"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#if showTooltips && (item.tooltip || item.name)}
|
{#if showTooltips && (item.tooltip || item.name)}
|
||||||
<span class="wire-next__avatar-group-tooltip" role="tooltip">
|
<span
|
||||||
|
class="wire-next__avatar-group-tooltip"
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
{item.tooltip || item.name}
|
{item.tooltip || item.name}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -83,7 +102,9 @@ component AvatarGroup {
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if hiddenMembers().length > 0}
|
{#if hiddenMembers().length > 0}
|
||||||
<span class="wire-next__avatar-group-overflow">
|
<span
|
||||||
|
class="wire-next__avatar-group-overflow"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="wire-next__avatar-group-overflow-button"
|
class="wire-next__avatar-group-overflow-button"
|
||||||
@@ -101,10 +122,19 @@ component AvatarGroup {
|
|||||||
aria-hidden="{overflowOpen ? 'false' : 'true'}"
|
aria-hidden="{overflowOpen ? 'false' : 'true'}"
|
||||||
>
|
>
|
||||||
{#each hiddenMembers() as item}
|
{#each hiddenMembers() as item}
|
||||||
<span class="wire-next__avatar-group-menu-item" role="menuitem">
|
<span
|
||||||
<span class="wire-next__avatar-group-menu-avatar">
|
class="wire-next__avatar-group-menu-item"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="wire-next__avatar-group-menu-avatar"
|
||||||
|
>
|
||||||
{#if item.src}
|
{#if item.src}
|
||||||
<img src="{item.src}" alt="" loading="lazy" />
|
<img
|
||||||
|
src="{item.src}"
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<span>{item.initials || '?'}</span>
|
<span>{item.initials || '?'}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,23 +1,53 @@
|
|||||||
component Dropdown {
|
component Dropdown {
|
||||||
props {
|
props {
|
||||||
|
@event toggle = function
|
||||||
@event open = function
|
@event open = function
|
||||||
@event close = function
|
@event close = function
|
||||||
@event select = function
|
@event select = function
|
||||||
@event change = function
|
|
||||||
size = "default"
|
size = "default"
|
||||||
color = "primary"
|
color = "primary"
|
||||||
title = "Dropdown"
|
label = "Open menu"
|
||||||
description = ""
|
items = []
|
||||||
open = false
|
placement = "end"
|
||||||
placement = "bottom"
|
|
||||||
closeLabel = "Close"
|
|
||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
functions {
|
||||||
|
function toggleMenu(event) {
|
||||||
|
$emit("toggle", { open: event.currentTarget.open })
|
||||||
|
$emit(event.currentTarget.open ? "open" : "close", { source: "trigger" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(item) {
|
||||||
|
$emit("select", { item: item, value: item.value || "" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view {
|
view {
|
||||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--dropdown wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
<details class="wire-dropdown wire-dropdown--{placement} wire-next--color-{color} wire-next--size-{size} {class}" data-wrn-dropdown @toggle="toggleMenu($event)">
|
||||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
<summary aria-label="{label}">
|
||||||
{#if description}<p>{description}</p>{/if}
|
<slot name="trigger" />
|
||||||
<slot />
|
<span class="wire-dropdown__chevron icon-[lucide--chevron-down]" aria-hidden="true"></span>
|
||||||
</div>
|
</summary>
|
||||||
|
<div class="wire-dropdown__panel" role="menu" aria-label="{label}">
|
||||||
|
<slot name="header" />
|
||||||
|
{#each items as item}
|
||||||
|
{#if item.type === "divider"}
|
||||||
|
<hr />
|
||||||
|
{:else}
|
||||||
|
{#if item.type === "header"}
|
||||||
|
<span class="wire-dropdown__heading">{item.label}</span>
|
||||||
|
{:else}
|
||||||
|
<a class="wire-dropdown__item {item.danger ? 'wire-dropdown__item--danger' : ''}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" role="menuitem" @click="selectItem(item)">
|
||||||
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
|
||||||
|
{#if item.external}<span class="icon-[lucide--arrow-up-right]" aria-hidden="true"></span>{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,138 @@ component Navbar {
|
|||||||
@event toggle = function
|
@event toggle = function
|
||||||
@event open = function
|
@event open = function
|
||||||
@event close = function
|
@event close = function
|
||||||
|
@event select = function
|
||||||
|
@event action = function
|
||||||
size = "default"
|
size = "default"
|
||||||
color = "primary"
|
color = "primary"
|
||||||
label = "Navbar"
|
label = "Primary navigation"
|
||||||
|
topbarLabel = "Utility navigation"
|
||||||
|
brand = {}
|
||||||
items = []
|
items = []
|
||||||
|
actions = []
|
||||||
active = ""
|
active = ""
|
||||||
orientation = "horizontal"
|
sticky = false
|
||||||
|
openOnHover = false
|
||||||
|
maxWidth = "full"
|
||||||
|
mobileLabel = "Toggle navigation"
|
||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state mobileOpen = false
|
||||||
|
|
||||||
|
functions {
|
||||||
|
function toggleNavigation() {
|
||||||
|
mobileOpen = !mobileOpen
|
||||||
|
$emit("toggle", { open: mobileOpen })
|
||||||
|
$emit(mobileOpen ? "open" : "close", { source: "mobile" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(item, level) {
|
||||||
|
mobileOpen = false
|
||||||
|
$emit("select", { item: item, value: item.value || "", level: level })
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAction(item) {
|
||||||
|
mobileOpen = false
|
||||||
|
$emit("action", { item: item, value: item.value || "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function isItemActive(item) {
|
||||||
|
if (!item) return false
|
||||||
|
if (item.value && item.value === active) return true
|
||||||
|
if (!item.children || !item.children.length) return false
|
||||||
|
return item.children.some(function (child) {
|
||||||
|
return isItemActive(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDropdown(event, item) {
|
||||||
|
$emit(event.currentTarget.open ? "open" : "close", {
|
||||||
|
source: "menu",
|
||||||
|
item: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view {
|
view {
|
||||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--navbar wire-next--{orientation} {class}" aria-label="{label}">
|
<header class="wire-navbar wire-navbar--width-{maxWidth} wire-next--color-{color} wire-next--size-{size} {sticky ? 'wire-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
|
||||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
<div class="wire-navbar__topbar" aria-label="{topbarLabel}">
|
||||||
<slot />
|
<slot name="topbar" />
|
||||||
</nav>
|
</div>
|
||||||
|
|
||||||
|
<div class="wire-navbar__main">
|
||||||
|
<a class="wire-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
|
||||||
|
{#if brand.logo}
|
||||||
|
<img class="wire-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
|
||||||
|
{/if}
|
||||||
|
{#if brand.icon}
|
||||||
|
<span class="wire-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
|
||||||
|
{/if}
|
||||||
|
{#if brand.label || brand.description}
|
||||||
|
<span class="wire-navbar__brand-copy">
|
||||||
|
{#if brand.label}<strong>{brand.label}</strong>{/if}
|
||||||
|
{#if brand.description}<small>{brand.description}</small>{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button type="button" class="wire-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
|
||||||
|
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
||||||
|
<nav class="wire-navbar__menus" aria-label="{label}">
|
||||||
|
{#each items as item}
|
||||||
|
{#if item.children && item.children.length}
|
||||||
|
<details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown($event, item)">
|
||||||
|
<summary aria-current="{isItemActive(item) ? 'page' : ''}">
|
||||||
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<span class="wire-navbar__chevron" aria-hidden="true"></span>
|
||||||
|
</summary>
|
||||||
|
<div class="wire-navbar__panel wire-navbar__panel--columns-{item.columns || 1}">
|
||||||
|
{#if item.description}<p class="wire-navbar__panel-intro">{item.description}</p>{/if}
|
||||||
|
{#each item.children as child}
|
||||||
|
<div class="wire-navbar__group">
|
||||||
|
{#if child.children && child.children.length}
|
||||||
|
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
|
||||||
|
{#if child.description}<small>{child.description}</small>{/if}
|
||||||
|
{#each child.children as nested}
|
||||||
|
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : ''}" @click="selectItem(nested, 3)">
|
||||||
|
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
|
||||||
|
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{:else}
|
||||||
|
<a class="wire-navbar__menu-link" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
|
||||||
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="wire-navbar__actions">
|
||||||
|
{#each actions as item}
|
||||||
|
<a class="wire-navbar__action wire-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
|
||||||
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,77 @@ component Sidebar {
|
|||||||
@event toggle = function
|
@event toggle = function
|
||||||
@event open = function
|
@event open = function
|
||||||
@event close = function
|
@event close = function
|
||||||
|
@event select = function
|
||||||
size = "default"
|
size = "default"
|
||||||
color = "primary"
|
color = "primary"
|
||||||
label = "Sidebar"
|
label = "Sidebar"
|
||||||
items = []
|
items = []
|
||||||
active = ""
|
active = ""
|
||||||
orientation = "horizontal"
|
orientation = "horizontal"
|
||||||
|
mobileLabel = "Open navigation"
|
||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state mobileOpen = false
|
||||||
|
|
||||||
|
functions {
|
||||||
|
function toggleSidebar() {
|
||||||
|
mobileOpen = !mobileOpen
|
||||||
|
$emit("toggle", { open: mobileOpen })
|
||||||
|
$emit(mobileOpen ? "open" : "close", { source: "mobile" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSidebar() {
|
||||||
|
mobileOpen = false
|
||||||
|
$emit("close", { source: "mobile" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(item, level) {
|
||||||
|
mobileOpen = false
|
||||||
|
$emit("select", { item: item, value: item.value || "", level: level })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view {
|
view {
|
||||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--sidebar wire-next--{orientation} {class}" aria-label="{label}">
|
<div class="wire-sidebar-shell wire-next--color-{color} wire-next--size-{size} {mobileOpen ? 'is-open' : ''} {class}">
|
||||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
<button type="button" class="wire-sidebar-toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleSidebar()">
|
||||||
<slot />
|
<span class="icon-[lucide--panel-left-open]" aria-hidden="true"></span>
|
||||||
</nav>
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="wire-sidebar-backdrop" aria-label="Close navigation" @click="closeSidebar()"></button>
|
||||||
|
<nav class="wire-next wire-next--sidebar wire-next--{orientation} wire-sidebar-panel" aria-label="{label}">
|
||||||
|
<div class="wire-sidebar-panel__head">
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<button type="button" aria-label="Close navigation" @click="closeSidebar()"><span class="icon-[lucide--x]" aria-hidden="true"></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="wire-sidebar-items">
|
||||||
|
{#each items as item}
|
||||||
|
{#if item.children && item.children.length}
|
||||||
|
<details class="wire-sidebar-group" name="wire-sidebar-group">
|
||||||
|
<summary>
|
||||||
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<span class="icon-[lucide--chevron-down]" aria-hidden="true"></span>
|
||||||
|
</summary>
|
||||||
|
<div class="wire-sidebar-group__children">
|
||||||
|
{#each item.children as child}
|
||||||
|
<a href="{child.href || '#'}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
|
||||||
|
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span>{child.label}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{:else}
|
||||||
|
<a href="{item.href || '#'}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
|
||||||
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,25 +38,136 @@ component Checkbox {
|
|||||||
function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
|
function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
|
||||||
}
|
}
|
||||||
view {
|
view {
|
||||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--choice-field wire-next--checkbox-field {class}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-orientation="{orientation}" data-card="{card}" data-list="{list}" data-right-aligned="{rightAligned}">
|
<div
|
||||||
<div class="wire-next__field-heading">{#if options.length || cornerHint}<span class="{hiddenLabel ? 'wire-next__sr-only' : ''}">{label}</span>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}{/if}</div>
|
{...attrs}
|
||||||
|
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--choice-field wire-next--checkbox-field {class}"
|
||||||
|
data-inline="{inline}"
|
||||||
|
data-invalid="{error ? 'true' : 'false'}"
|
||||||
|
data-orientation="{orientation}"
|
||||||
|
data-card="{card}"
|
||||||
|
data-list="{list}"
|
||||||
|
data-right-aligned="{rightAligned}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="wire-next__field-heading"
|
||||||
|
>
|
||||||
|
{#if options.length || cornerHint}
|
||||||
|
<span
|
||||||
|
class="{hiddenLabel ? 'wire-next__sr-only' : ''}"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{#if cornerHint}
|
||||||
|
<span
|
||||||
|
class="wire-next__field-hint"
|
||||||
|
>
|
||||||
|
{cornerHint}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if options.length}
|
{#if options.length}
|
||||||
<div class="wire-next__checkbox-group" role="group" aria-label="{hiddenLabel ? label : ''}" aria-invalid="{error ? 'true' : 'false'}">
|
<div
|
||||||
|
class="wire-next__checkbox-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="{hiddenLabel ? label : ''}"
|
||||||
|
aria-invalid="{error ? 'true' : 'false'}"
|
||||||
|
>
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
<label class="wire-next__choice wire-next--checkbox" for="{id || name}-{option.value}" data-variant="{variant}" data-disabled="{disabled || option.disabled}">
|
<label
|
||||||
<input id="{id || name}-{option.value}" type="checkbox" name="{name}" value="{option.value}" checked="{values.includes(option.value) || option.checked}" disabled="{disabled || option.disabled}" required="{required}" readonly="{readonly}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" />
|
class="wire-next__choice wire-next--checkbox"
|
||||||
<span class="wire-next__choice-copy"><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
|
for="{id || name}-{option.value}"
|
||||||
|
data-variant="{variant}"
|
||||||
|
data-disabled="{disabled || option.disabled}"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="{id || name}-{option.value}"
|
||||||
|
type="checkbox"
|
||||||
|
name="{name}"
|
||||||
|
value="{option.value}"
|
||||||
|
checked="{values.includes(option.value) || option.checked}"
|
||||||
|
disabled="{disabled || option.disabled}"
|
||||||
|
required="{required}"
|
||||||
|
readonly="{readonly}"
|
||||||
|
@click="preventReadonly(event)"
|
||||||
|
@input="emitField('input', event)"
|
||||||
|
@change="emitField('change', event)"
|
||||||
|
@focus="emitField('focus', event)"
|
||||||
|
@blur="emitField('blur', event)"
|
||||||
|
@invalid="emitField('invalid', event)"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="wire-next__choice-copy"
|
||||||
|
>
|
||||||
|
<strong>{option.label}</strong>
|
||||||
|
{#if option.description}
|
||||||
|
<small>{option.description}</small>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<label class="wire-next__choice wire-next--checkbox" for="{id || name}" data-variant="{variant}" data-icon-position="{iconPosition}">
|
<div
|
||||||
<input id="{id || name}" type="checkbox" name="{name}" value="{value}" checked="{checked}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-checked="{indeterminate ? 'mixed' : (checked ? 'true' : 'false')}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" />
|
class="wire-next__checkbox-content"
|
||||||
{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}
|
>
|
||||||
<span class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}">{label}</span>
|
<label
|
||||||
</label>
|
class="wire-next__choice wire-next--checkbox"
|
||||||
|
for="{id || name}"
|
||||||
|
data-variant="{variant}"
|
||||||
|
data-icon-position="{iconPosition}"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="{id || name}"
|
||||||
|
type="checkbox"
|
||||||
|
name="{name}"
|
||||||
|
value="{value}"
|
||||||
|
checked="{checked}"
|
||||||
|
disabled="{disabled}"
|
||||||
|
required="{required}"
|
||||||
|
readonly="{readonly}"
|
||||||
|
aria-checked="{indeterminate ? 'mixed' : (checked ? 'true' : 'false')}"
|
||||||
|
aria-invalid="{error ? 'true' : 'false'}"
|
||||||
|
@click="preventReadonly(event)"
|
||||||
|
@input="emitField('input', event)"
|
||||||
|
@change="emitField('change', event)"
|
||||||
|
@focus="emitField('focus', event)"
|
||||||
|
@blur="emitField('blur', event)"
|
||||||
|
@invalid="emitField('invalid', event)"
|
||||||
|
/>
|
||||||
|
{#if icon}
|
||||||
|
<span
|
||||||
|
class="{icon}"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span
|
||||||
|
class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="wire-next__checkbox-slot"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
{#if helperText}
|
||||||
|
<small
|
||||||
|
class="wire-next__field-help"
|
||||||
|
>
|
||||||
|
{helperText}
|
||||||
|
</small>
|
||||||
|
{/if}
|
||||||
|
<small
|
||||||
|
class="wire-next__field-error"
|
||||||
|
data-error="{name}"
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"./ui.css": "./ui.css"
|
"./ui.css": "./ui.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
+1552
-14
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.5.1"
|
"@wrnexus/core": "^0.5.10"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -83,11 +83,11 @@
|
|||||||
},
|
},
|
||||||
"packages/ai": {
|
"packages/ai": {
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.5.0-alpha.1",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -98,6 +98,7 @@
|
|||||||
"@wrnexus/oauth": "workspace:*",
|
"@wrnexus/oauth": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
"@wrnexus/queue": "workspace:*",
|
"@wrnexus/queue": "workspace:*",
|
||||||
|
"@wrnexus/ui": "workspace:*",
|
||||||
"@wrnexus/validation": "workspace:*",
|
"@wrnexus/validation": "workspace:*",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -108,11 +109,11 @@
|
|||||||
},
|
},
|
||||||
"packages/authz": {
|
"packages/authz": {
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/captcha": {
|
"packages/captcha": {
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -147,29 +148,29 @@
|
|||||||
},
|
},
|
||||||
"packages/compiler": {
|
"packages/compiler": {
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/csr": {
|
"packages/csr": {
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
@@ -189,7 +190,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-toolbar": {
|
"packages/dev-toolbar": {
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
@@ -197,63 +198,63 @@
|
|||||||
},
|
},
|
||||||
"packages/encryption": {
|
"packages/encryption": {
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/helpers": {
|
"packages/helpers": {
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/i18n": {
|
"packages/i18n": {
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/jwt": {
|
"packages/jwt": {
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/mobile": {
|
"packages/mobile": {
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/native": "workspace:*",
|
"@wrnexus/native": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/native": {
|
"packages/native": {
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/oauth": {
|
"packages/oauth": {
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@wrnexus/plugin",
|
"name": "@wrnexus/plugin",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/pubsub": {
|
"packages/pubsub": {
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/queue": {
|
"packages/queue": {
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/reactive": {
|
"packages/reactive": {
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/router": {
|
"packages/router": {
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
@@ -261,14 +262,14 @@
|
|||||||
},
|
},
|
||||||
"packages/ssr": {
|
"packages/ssr": {
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/styles": {
|
"packages/styles": {
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -277,37 +278,37 @@
|
|||||||
},
|
},
|
||||||
"packages/syntax": {
|
"packages/syntax": {
|
||||||
"name": "@wrnexus/syntax",
|
"name": "@wrnexus/syntax",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/test": {
|
"packages/test": {
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/tracking": {
|
"packages/tracking": {
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/uploader": {
|
"packages/uploader": {
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/validation": {
|
"packages/validation": {
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.4.0",
|
"version": "0.5.10",
|
||||||
},
|
},
|
||||||
"services/managed-captcha": {
|
"services/managed-captcha": {
|
||||||
"name": "@wrnexus/managed-captcha-service",
|
"name": "@wrnexus/managed-captcha-service",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
- Updated compiler-backed diagnostics for native array and object state values.
|
||||||
|
- Added support for unquoted component prop expressions such as `items={items}`,
|
||||||
|
inline arrays, and inline objects.
|
||||||
|
- Added readable, idempotent formatting for native JSON state values and direct
|
||||||
|
structured component props.
|
||||||
|
- Added extension release validation to prevent the bundled compiler from
|
||||||
|
drifting behind the framework parser.
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
- Added language support for `runtime`, `hydrate`, `computed`, `effect`, `security`,
|
- Added language support for `runtime`, `hydrate`, `computed`, `effect`, `security`,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"displayName": "WRNexus Language Support",
|
"displayName": "WRNexus Language Support",
|
||||||
"description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.",
|
"description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"publisher": "wrnexus",
|
"publisher": "wrnexus",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
|||||||
+251
-23
@@ -3,6 +3,142 @@
|
|||||||
const __nodeRequire = require;
|
const __nodeRequire = require;
|
||||||
const __path = __nodeRequire("node:path");
|
const __path = __nodeRequire("node:path");
|
||||||
const __modules = {
|
const __modules = {
|
||||||
|
"packages/compiler/src/cache.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.DependencyGraph = void 0;
|
||||||
|
exports.compilationKey = compilationKey;
|
||||||
|
exports.createCompilationCache = createCompilationCache;
|
||||||
|
const node_crypto_1 = require("node:crypto");
|
||||||
|
const syntax_1 = require("@wrnexus/syntax");
|
||||||
|
const codegen_ts_1 = require("./codegen.js");
|
||||||
|
function compileSource(source, filePath) {
|
||||||
|
const richDiagnostics = (0, syntax_1.diagnose)(source, { file: filePath, accessibility: true });
|
||||||
|
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new syntax_1.ParseError(errors.map((diagnostic) => diagnostic.message).join("\n"), errors[0].code);
|
||||||
|
}
|
||||||
|
const ast = (0, syntax_1.parse)(source);
|
||||||
|
return {
|
||||||
|
code: `// compiled from .wrn\n${(0, codegen_ts_1.generate)(ast)}`,
|
||||||
|
ast,
|
||||||
|
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
|
||||||
|
richDiagnostics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function compilationKey(source, file = "<inline .wrn>", salt = "") {
|
||||||
|
return (0, node_crypto_1.createHash)("sha256")
|
||||||
|
.update(file)
|
||||||
|
.update("\0")
|
||||||
|
.update(salt)
|
||||||
|
.update("\0")
|
||||||
|
.update(source)
|
||||||
|
.digest("hex");
|
||||||
|
}
|
||||||
|
function createCompilationCache(options = {}) {
|
||||||
|
const maxEntries = options.maxEntries ?? 500;
|
||||||
|
if (!Number.isInteger(maxEntries) || maxEntries < 1)
|
||||||
|
throw new RangeError("maxEntries must be positive");
|
||||||
|
const now = options.now ?? Date.now;
|
||||||
|
const entries = new Map();
|
||||||
|
let hits = 0;
|
||||||
|
let misses = 0;
|
||||||
|
function touch(key, value) {
|
||||||
|
entries.delete(key);
|
||||||
|
entries.set(key, value);
|
||||||
|
while (entries.size > maxEntries)
|
||||||
|
entries.delete(entries.keys().next().value);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
compile(source, file = "<inline .wrn>", salt = "") {
|
||||||
|
const key = compilationKey(source, file, salt);
|
||||||
|
const existing = entries.get(key);
|
||||||
|
if (existing) {
|
||||||
|
hits++;
|
||||||
|
touch(key, existing);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
misses++;
|
||||||
|
const result = compileSource(source, file);
|
||||||
|
const entry = {
|
||||||
|
...result,
|
||||||
|
key,
|
||||||
|
file,
|
||||||
|
sourceHash: (0, node_crypto_1.createHash)("sha256").update(source).digest("hex"),
|
||||||
|
createdAt: now(),
|
||||||
|
};
|
||||||
|
touch(key, entry);
|
||||||
|
return entry;
|
||||||
|
},
|
||||||
|
get(key) {
|
||||||
|
const entry = entries.get(key);
|
||||||
|
if (entry)
|
||||||
|
touch(key, entry);
|
||||||
|
return entry;
|
||||||
|
},
|
||||||
|
invalidate(file) {
|
||||||
|
let removed = 0;
|
||||||
|
for (const [key, entry] of entries) {
|
||||||
|
if (!file || entry.file === file) {
|
||||||
|
entries.delete(key);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
entries.clear();
|
||||||
|
},
|
||||||
|
size: () => entries.size,
|
||||||
|
stats: () => ({ hits, misses, entries: entries.size }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
class DependencyGraph {
|
||||||
|
#dependencies = new Map();
|
||||||
|
#dependents = new Map();
|
||||||
|
set(file, dependencies) {
|
||||||
|
this.remove(file);
|
||||||
|
const values = new Set(dependencies);
|
||||||
|
this.#dependencies.set(file, values);
|
||||||
|
for (const dependency of values) {
|
||||||
|
const set = this.#dependents.get(dependency) ?? new Set();
|
||||||
|
set.add(file);
|
||||||
|
this.#dependents.set(dependency, set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove(file) {
|
||||||
|
for (const dependency of this.#dependencies.get(file) ?? []) {
|
||||||
|
const set = this.#dependents.get(dependency);
|
||||||
|
set?.delete(file);
|
||||||
|
if (set?.size === 0)
|
||||||
|
this.#dependents.delete(dependency);
|
||||||
|
}
|
||||||
|
this.#dependencies.delete(file);
|
||||||
|
}
|
||||||
|
dependencies(file) {
|
||||||
|
return [...(this.#dependencies.get(file) ?? [])].sort();
|
||||||
|
}
|
||||||
|
dependents(file) {
|
||||||
|
return [...(this.#dependents.get(file) ?? [])].sort();
|
||||||
|
}
|
||||||
|
affected(file) {
|
||||||
|
const found = new Set();
|
||||||
|
const queue = [file];
|
||||||
|
while (queue.length) {
|
||||||
|
const current = queue.shift();
|
||||||
|
for (const dependent of this.#dependents.get(current) ?? []) {
|
||||||
|
if (found.has(dependent))
|
||||||
|
continue;
|
||||||
|
found.add(dependent);
|
||||||
|
queue.push(dependent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...found].sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.DependencyGraph = DependencyGraph;
|
||||||
|
|
||||||
|
},
|
||||||
"packages/compiler/src/codegen.ts": function (module, exports, require, __filename, __dirname) {
|
"packages/compiler/src/codegen.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
"use strict";
|
"use strict";
|
||||||
/**
|
/**
|
||||||
@@ -937,6 +1073,9 @@ function exprRefsState(expr, stateNames) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
function exprRefsComponentReactiveValue(expr, ctx) {
|
||||||
|
return exprRefsState(expr, ctx.stateNames) || exprRefsState(expr, ctx.functionNames);
|
||||||
|
}
|
||||||
function viewHasEvents(nodes) {
|
function viewHasEvents(nodes) {
|
||||||
return nodes.some((node) => {
|
return nodes.some((node) => {
|
||||||
if (node.type === "text")
|
if (node.type === "text")
|
||||||
@@ -999,7 +1138,7 @@ function compileText(raw, ctx) {
|
|||||||
// list renderer fills it per item; it has no server-side value.
|
// list renderer fills it per item; it has no server-side value.
|
||||||
out += escLit(`{${expr}}`);
|
out += escLit(`{${expr}}`);
|
||||||
}
|
}
|
||||||
else if (exprRefsState(expr, ctx.stateNames)) {
|
else if (exprRefsComponentReactiveValue(expr, ctx)) {
|
||||||
// State interpolation: bake the initial value AND keep it reactive via a
|
// State interpolation: bake the initial value AND keep it reactive via a
|
||||||
// data-text span, so no-JS clients see the real value and hydration
|
// data-text span, so no-JS clients see the real value and hydration
|
||||||
// updates it in place. `count` → `<span data-text="count">0</span>`.
|
// updates it in place. `count` → `<span data-text="count">0</span>`.
|
||||||
@@ -1156,6 +1295,7 @@ function renderComponentNode(node, ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
|
||||||
const attrs = node.attrs
|
const attrs = node.attrs
|
||||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
@@ -1172,7 +1312,7 @@ function renderComponentNode(node, ctx) {
|
|||||||
if (isHtmlBooleanAttribute(a.name)) {
|
if (isHtmlBooleanAttribute(a.name)) {
|
||||||
const expression = wholeAttributeExpression(a.value);
|
const expression = wholeAttributeExpression(a.value);
|
||||||
if (expression) {
|
if (expression) {
|
||||||
const referencesState = exprRefsState(a.value, ctx.stateNames);
|
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
||||||
const referencesLoopVariable = elementContext.loopVars
|
const referencesLoopVariable = elementContext.loopVars
|
||||||
? exprRefsState(a.value, elementContext.loopVars)
|
? exprRefsState(a.value, elementContext.loopVars)
|
||||||
: false;
|
: false;
|
||||||
@@ -1189,8 +1329,12 @@ function renderComponentNode(node, ctx) {
|
|||||||
if (a.value === "true" || a.value === "")
|
if (a.value === "true" || a.value === "")
|
||||||
return ` ${a.name}`;
|
return ` ${a.name}`;
|
||||||
}
|
}
|
||||||
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
|
const wholeExpression = wholeAttributeExpression(a.value);
|
||||||
const referencesState = exprRefsState(a.value, ctx.stateNames);
|
const compiledValue = isExplicitComponentMount && wholeExpression
|
||||||
|
? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||||
|
: compileAttrValue(a.value, elementContext);
|
||||||
|
const rendered = ` ${a.name}="${compiledValue}"`;
|
||||||
|
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
||||||
const referencesLoopVariable = elementContext.loopVars
|
const referencesLoopVariable = elementContext.loopVars
|
||||||
? exprRefsState(a.value, elementContext.loopVars)
|
? exprRefsState(a.value, elementContext.loopVars)
|
||||||
: false;
|
: false;
|
||||||
@@ -1219,7 +1363,7 @@ function renderComponentNode(node, ctx) {
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
const staticClassValue = staticClasses.join(" ");
|
const staticClassValue = staticClasses.join(" ");
|
||||||
const classReferencesState = exprRefsState(staticClassValue, ctx.stateNames);
|
const classReferencesState = exprRefsComponentReactiveValue(staticClassValue, ctx);
|
||||||
const classReferencesLoopVariable = elementContext.loopVars
|
const classReferencesLoopVariable = elementContext.loopVars
|
||||||
? exprRefsState(staticClassValue, elementContext.loopVars)
|
? exprRefsState(staticClassValue, elementContext.loopVars)
|
||||||
: false;
|
: false;
|
||||||
@@ -1243,9 +1387,7 @@ function renderComponentNode(node, ctx) {
|
|||||||
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
|
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
|
||||||
const allAttrs = `${loopLocalsAttribute}` +
|
const allAttrs = `${loopLocalsAttribute}` +
|
||||||
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
||||||
`${ctx.forwardRestAttrs && ctx.eventNames?.length
|
`${ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""}` +
|
||||||
? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"`
|
|
||||||
: ""}` +
|
|
||||||
`${classAttribute}` +
|
`${classAttribute}` +
|
||||||
`${classReactiveBinding}` +
|
`${classReactiveBinding}` +
|
||||||
`${classBindings}` +
|
`${classBindings}` +
|
||||||
@@ -1261,9 +1403,6 @@ function generateComponent(ast) {
|
|||||||
if (ast.imports.length > 0)
|
if (ast.imports.length > 0)
|
||||||
out.push(ast.imports.join("\n"));
|
out.push(ast.imports.join("\n"));
|
||||||
const hasServerEach = viewHasServerEach(ast.view);
|
const hasServerEach = viewHasServerEach(ast.view);
|
||||||
if (hasServerEach) {
|
|
||||||
out.push(`import { Buffer } from "node:buffer";`);
|
|
||||||
}
|
|
||||||
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content")
|
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content")
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -1300,6 +1439,9 @@ function generateComponent(ast) {
|
|||||||
};
|
};
|
||||||
const ctx = {
|
const ctx = {
|
||||||
stateNames,
|
stateNames,
|
||||||
|
functionNames: new Set(ast.functions.flatMap((body) => {
|
||||||
|
return Array.from(body.matchAll(/(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g), (match) => match[1]);
|
||||||
|
})),
|
||||||
resolveExpr,
|
resolveExpr,
|
||||||
eventNames: ast.events.map((event) => event.name),
|
eventNames: ast.events.map((event) => event.name),
|
||||||
};
|
};
|
||||||
@@ -1330,6 +1472,9 @@ function generateComponent(ast) {
|
|||||||
ast.computed.length > 0 ||
|
ast.computed.length > 0 ||
|
||||||
viewHasEvents(ast.view) ||
|
viewHasEvents(ast.view) ||
|
||||||
behavior !== null);
|
behavior !== null);
|
||||||
|
if (hasServerEach || needsScope) {
|
||||||
|
out.push(`import { Buffer as __WrnexusBuffer } from "node:buffer";`);
|
||||||
|
}
|
||||||
const scopeKeys = [
|
const scopeKeys = [
|
||||||
...effectiveProps.map((prop) => prop.name),
|
...effectiveProps.map((prop) => prop.name),
|
||||||
...ast.states.map((state) => state.name),
|
...ast.states.map((state) => state.name),
|
||||||
@@ -1354,16 +1499,16 @@ function generateComponent(ast) {
|
|||||||
const returnExpr = needsScope
|
const returnExpr = needsScope
|
||||||
? "`" +
|
? "`" +
|
||||||
styleTag +
|
styleTag +
|
||||||
`<div data-scope="\${__scope}"${behaviorAttr}${hydrationAttribute(ast)}>` +
|
`<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}"${behaviorAttr}${hydrationAttribute(ast)}>` +
|
||||||
viewCode +
|
viewCode +
|
||||||
"</div>`"
|
"</div>`"
|
||||||
: "`" + styleTag + viewCode + "`";
|
: "`" + styleTag + viewCode + "`";
|
||||||
const scopeLine = needsScope && scopeKeys.length > 0
|
const scopeLine = needsScope && scopeKeys.length > 0
|
||||||
? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys
|
? ` const __scopeState = { ${scopeKeys
|
||||||
.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`)
|
.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`)
|
||||||
.join(", ")} });\n`
|
.join(", ")} };\n const __scope = __wrnexusScopeDecl(__scopeState);\n const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");\n`
|
||||||
: needsScope
|
: needsScope
|
||||||
? ` const __scope = "";\n`
|
? ` const __scopeState = {};\n const __scope = "";\n const __scopePayload = __WrnexusBuffer.from("{}", "utf8").toString("base64");\n`
|
||||||
: "";
|
: "";
|
||||||
if (ast.kind === "layout") {
|
if (ast.kind === "layout") {
|
||||||
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
|
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
|
||||||
@@ -1553,7 +1698,7 @@ function __wireRaw(v: any): string {
|
|||||||
}`);
|
}`);
|
||||||
if (hasServerEach) {
|
if (hasServerEach) {
|
||||||
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
|
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
|
||||||
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
@@ -1661,7 +1806,7 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
|
|||||||
* `@wrnexus/syntax` package. This package owns platform-specific codegen.
|
* `@wrnexus/syntax` package. This package owns platform-specific codegen.
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = void 0;
|
exports.DependencyGraph = exports.createCompilationCache = exports.compilationKey = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = void 0;
|
||||||
exports.compileNativeWireFile = compileNativeWireFile;
|
exports.compileNativeWireFile = compileNativeWireFile;
|
||||||
exports.compileWireFile = compileWireFile;
|
exports.compileWireFile = compileWireFile;
|
||||||
exports.compile = compile;
|
exports.compile = compile;
|
||||||
@@ -1725,6 +1870,10 @@ function compile(source, filePath = "<inline .wrn>") {
|
|||||||
richDiagnostics,
|
richDiagnostics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var cache_ts_1 = require("./cache.js");
|
||||||
|
Object.defineProperty(exports, "compilationKey", { enumerable: true, get: function () { return cache_ts_1.compilationKey; } });
|
||||||
|
Object.defineProperty(exports, "createCompilationCache", { enumerable: true, get: function () { return cache_ts_1.createCompilationCache; } });
|
||||||
|
Object.defineProperty(exports, "DependencyGraph", { enumerable: true, get: function () { return cache_ts_1.DependencyGraph; } });
|
||||||
|
|
||||||
},
|
},
|
||||||
"packages/compiler/src/native-codegen.ts": function (module, exports, require, __filename, __dirname) {
|
"packages/compiler/src/native-codegen.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
@@ -2211,7 +2360,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|||||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.LexError = exports.Lexer = void 0;
|
exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.LexError = exports.Lexer = void 0;
|
||||||
var tokenizer_ts_1 = require("./tokenizer.js");
|
var tokenizer_ts_1 = require("./tokenizer.js");
|
||||||
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
|
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
|
||||||
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
|
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
|
||||||
@@ -2235,6 +2384,13 @@ Object.defineProperty(exports, "isHydrationStrategy", { enumerable: true, get: f
|
|||||||
Object.defineProperty(exports, "isRuntimeTarget", { enumerable: true, get: function () { return diagnostics_ts_1.isRuntimeTarget; } });
|
Object.defineProperty(exports, "isRuntimeTarget", { enumerable: true, get: function () { return diagnostics_ts_1.isRuntimeTarget; } });
|
||||||
Object.defineProperty(exports, "positionAt", { enumerable: true, get: function () { return diagnostics_ts_1.positionAt; } });
|
Object.defineProperty(exports, "positionAt", { enumerable: true, get: function () { return diagnostics_ts_1.positionAt; } });
|
||||||
__exportStar(require("./spec.js"), exports);
|
__exportStar(require("./spec.js"), exports);
|
||||||
|
var versioning_ts_1 = require("./versioning.js");
|
||||||
|
Object.defineProperty(exports, "WRN_SYNTAX_VERSION", { enumerable: true, get: function () { return versioning_ts_1.WRN_SYNTAX_VERSION; } });
|
||||||
|
Object.defineProperty(exports, "WRN_SYNTAX_FEATURES", { enumerable: true, get: function () { return versioning_ts_1.WRN_SYNTAX_FEATURES; } });
|
||||||
|
Object.defineProperty(exports, "createSourceRange", { enumerable: true, get: function () { return versioning_ts_1.createSourceRange; } });
|
||||||
|
Object.defineProperty(exports, "sliceSource", { enumerable: true, get: function () { return versioning_ts_1.sliceSource; } });
|
||||||
|
Object.defineProperty(exports, "diagnosticSummary", { enumerable: true, get: function () { return versioning_ts_1.diagnosticSummary; } });
|
||||||
|
Object.defineProperty(exports, "supportsSyntaxFeature", { enumerable: true, get: function () { return versioning_ts_1.supportsSyntaxFeature; } });
|
||||||
|
|
||||||
},
|
},
|
||||||
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
|
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
@@ -2475,7 +2631,11 @@ function parse(source) {
|
|||||||
else {
|
else {
|
||||||
expect("eq");
|
expect("eq");
|
||||||
}
|
}
|
||||||
states.push({ name: sName, valueType, expr: lx.readToLineEnd() });
|
// State values may be multiline structured expressions. Use the same
|
||||||
|
// balanced initializer reader as props so formatted arrays/objects
|
||||||
|
// remain one declaration instead of exposing their inner braces as
|
||||||
|
// component members on the next line.
|
||||||
|
states.push({ name: sName, valueType, expr: lx.readPropInitializer() });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "computed": {
|
case "computed": {
|
||||||
@@ -2755,14 +2915,29 @@ function parseHtmlView(src, pos) {
|
|||||||
while (i < src.length && isWs(src[i]))
|
while (i < src.length && isWs(src[i]))
|
||||||
i++;
|
i++;
|
||||||
};
|
};
|
||||||
/** Read a `{...}` interpolation (brace-balanced), braces included. */
|
/** Read a `{...}` interpolation (brace-balanced and quote-aware), braces included. */
|
||||||
const readInterpolation = () => {
|
const readInterpolation = () => {
|
||||||
const start = i;
|
const start = i;
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
let quote = null;
|
||||||
for (; i < src.length; i++) {
|
for (; i < src.length; i++) {
|
||||||
if (src[i] === "{")
|
const char = src[i];
|
||||||
|
if (quote) {
|
||||||
|
if (char === "\\" && i + 1 < src.length) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === quote)
|
||||||
|
quote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'" || char === "`") {
|
||||||
|
quote = char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "{")
|
||||||
depth++;
|
depth++;
|
||||||
else if (src[i] === "}" && --depth === 0) {
|
else if (char === "}" && --depth === 0) {
|
||||||
i++;
|
i++;
|
||||||
return src.slice(start, i);
|
return src.slice(start, i);
|
||||||
}
|
}
|
||||||
@@ -2852,7 +3027,12 @@ function parseHtmlView(src, pos) {
|
|||||||
if (src[i] === "=") {
|
if (src[i] === "=") {
|
||||||
i++;
|
i++;
|
||||||
skipWs();
|
skipWs();
|
||||||
attrs.push({ name, value: readQuoted(), event: false });
|
const value = src[i] === '"' || src[i] === "'"
|
||||||
|
? readQuoted()
|
||||||
|
: src[i] === "{"
|
||||||
|
? readInterpolation()
|
||||||
|
: fail(`Expected a quoted value or {...} expression after '${name}='`);
|
||||||
|
attrs.push({ name, value, event: false });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
attrs.push({ name, value: "", event: false, boolean: true });
|
attrs.push({ name, value: "", event: false, boolean: true });
|
||||||
@@ -3447,6 +3627,54 @@ function eraseFunctionTypes(source) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"packages/syntax/src/versioning.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = void 0;
|
||||||
|
exports.createSourceRange = createSourceRange;
|
||||||
|
exports.sliceSource = sliceSource;
|
||||||
|
exports.diagnosticSummary = diagnosticSummary;
|
||||||
|
exports.supportsSyntaxFeature = supportsSyntaxFeature;
|
||||||
|
/** Current stable syntax contract. Bump only when parsers/codegen need migration. */
|
||||||
|
exports.WRN_SYNTAX_VERSION = "0.4";
|
||||||
|
exports.WRN_SYNTAX_FEATURES = Object.freeze({
|
||||||
|
"typed-declarations": true,
|
||||||
|
layouts: true,
|
||||||
|
"server-client-blocks": true,
|
||||||
|
effects: true,
|
||||||
|
watch: true,
|
||||||
|
lifecycle: true,
|
||||||
|
"embedded-api": true,
|
||||||
|
realtime: true,
|
||||||
|
"runtime-markers": true,
|
||||||
|
});
|
||||||
|
function createSourceRange(start, end) {
|
||||||
|
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
|
||||||
|
throw new RangeError(`Invalid source range ${start}..${end}`);
|
||||||
|
}
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
function sliceSource(source, range) {
|
||||||
|
return source.slice(range.start, range.end);
|
||||||
|
}
|
||||||
|
function diagnosticSummary(diagnostics) {
|
||||||
|
const summary = { errors: 0, warnings: 0, info: 0, codes: {} };
|
||||||
|
for (const diagnostic of diagnostics) {
|
||||||
|
if (diagnostic.severity === "error")
|
||||||
|
summary.errors++;
|
||||||
|
else if (diagnostic.severity === "warning")
|
||||||
|
summary.warnings++;
|
||||||
|
else
|
||||||
|
summary.info++;
|
||||||
|
summary.codes[diagnostic.code] = (summary.codes[diagnostic.code] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
function supportsSyntaxFeature(feature) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(exports.WRN_SYNTAX_FEATURES, feature);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const __aliases = {
|
const __aliases = {
|
||||||
|
|||||||
@@ -1155,18 +1155,21 @@ function validateRequiredView(document, source, rootKind, rootMatch) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateLayoutUsage(document, source, rootKind) {
|
function validateLayoutUsage(document, source, rootKind, rootMatch) {
|
||||||
const diagnostics = [];
|
const diagnostics = [];
|
||||||
|
|
||||||
if (rootKind !== "page" && /^\s*layout\s*=/m.test(source)) {
|
if (rootKind !== "page") {
|
||||||
const match = /^\s*layout\s*=/m.exec(source);
|
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||||
|
const layoutMember = findRootMembers(source, bodyRange.start, bodyRange.end).find(
|
||||||
|
(member) => member.name === "layout",
|
||||||
|
);
|
||||||
|
|
||||||
if (match) {
|
if (layoutMember) {
|
||||||
diagnostics.push(
|
diagnostics.push(
|
||||||
createDiagnostic(
|
createDiagnostic(
|
||||||
document,
|
document,
|
||||||
match.index,
|
layoutMember.start,
|
||||||
match.index + match[0].length,
|
layoutMember.end,
|
||||||
'`layout = "..."` is only valid inside a page.',
|
'`layout = "..."` is only valid inside a page.',
|
||||||
vscode.DiagnosticSeverity.Error,
|
vscode.DiagnosticSeverity.Error,
|
||||||
"wrn-invalid-layout-member",
|
"wrn-invalid-layout-member",
|
||||||
@@ -1229,7 +1232,7 @@ function validateDocument(document) {
|
|||||||
|
|
||||||
diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match));
|
diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match));
|
||||||
|
|
||||||
diagnostics.push(...validateLayoutUsage(document, source, declaration.kind));
|
diagnostics.push(...validateLayoutUsage(document, source, declaration.kind, declaration.match));
|
||||||
|
|
||||||
return diagnostics;
|
return diagnostics;
|
||||||
}
|
}
|
||||||
@@ -1325,6 +1328,7 @@ module.exports = {
|
|||||||
validateDocument,
|
validateDocument,
|
||||||
validateHtmlTags,
|
validateHtmlTags,
|
||||||
validateLifecycleBlocks,
|
validateLifecycleBlocks,
|
||||||
|
validateLayoutUsage,
|
||||||
validateRootMembers,
|
validateRootMembers,
|
||||||
validateWatchBlocks,
|
validateWatchBlocks,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,18 +153,95 @@ function findOpeningTagEnd(value) {
|
|||||||
|
|
||||||
function parseAttributes(value) {
|
function parseAttributes(value) {
|
||||||
const attributes = [];
|
const attributes = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s"'=<>`]+))?/g;
|
while (index < value.length) {
|
||||||
|
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||||
|
if (index >= value.length) break;
|
||||||
|
|
||||||
let match;
|
const start = index;
|
||||||
|
while (index < value.length && !/[\s=]/.test(value[index])) index += 1;
|
||||||
|
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||||
|
|
||||||
while ((match = pattern.exec(value)) !== null) {
|
if (value[index] === "=") {
|
||||||
attributes.push(match[0].trim());
|
index += 1;
|
||||||
|
while (index < value.length && /\s/.test(value[index])) index += 1;
|
||||||
|
|
||||||
|
const quote = value[index];
|
||||||
|
if (quote === '"' || quote === "'") {
|
||||||
|
index += 1;
|
||||||
|
let escaped = false;
|
||||||
|
while (index < value.length) {
|
||||||
|
const character = value[index++];
|
||||||
|
if (escaped) escaped = false;
|
||||||
|
else if (character === "\\") escaped = true;
|
||||||
|
else if (character === quote) break;
|
||||||
|
}
|
||||||
|
} else if (value[index] === "{") {
|
||||||
|
let depth = 0;
|
||||||
|
let expressionQuote = null;
|
||||||
|
let escaped = false;
|
||||||
|
while (index < value.length) {
|
||||||
|
const character = value[index++];
|
||||||
|
if (expressionQuote !== null) {
|
||||||
|
if (escaped) escaped = false;
|
||||||
|
else if (character === "\\") escaped = true;
|
||||||
|
else if (character === expressionQuote) expressionQuote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === '"' || character === "'" || character === "`") {
|
||||||
|
expressionQuote = character;
|
||||||
|
} else if (character === "{") {
|
||||||
|
depth += 1;
|
||||||
|
} else if (character === "}" && --depth === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (index < value.length && !/\s/.test(value[index])) index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute = value.slice(start, index).trim();
|
||||||
|
if (attribute) attributes.push(attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseStructuredAttribute(attribute) {
|
||||||
|
const match = /^([^\s=]+)\s*=\s*\{([\s\S]*)\}$/.exec(attribute);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const expression = match[2].trim();
|
||||||
|
if (!expression.startsWith("[") && !expression.startsWith("{")) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
name: match[1],
|
||||||
|
value: JSON.parse(expression),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAttribute(attribute, indentation, unit) {
|
||||||
|
const structured = parseStructuredAttribute(attribute);
|
||||||
|
if (!structured) return [`${indentation}${attribute}`];
|
||||||
|
|
||||||
|
const jsonLines = JSON.stringify(structured.value, null, unit).split("\n");
|
||||||
|
if (jsonLines.length === 1) {
|
||||||
|
return [`${indentation}${structured.name}={${jsonLines[0]}}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`${indentation}${structured.name}={${jsonLines[0]}`,
|
||||||
|
...jsonLines.slice(1, -1).map((line) => `${indentation}${line}`),
|
||||||
|
`${indentation}${jsonLines.at(-1)}}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function escapeRegExp(value) {
|
function escapeRegExp(value) {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
@@ -257,7 +334,7 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
|
|||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`${baseIndent}<${parsed.tagName}`,
|
`${baseIndent}<${parsed.tagName}`,
|
||||||
...parsed.attributes.map((attribute) => `${childIndent}${attribute}`),
|
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (parsed.selfClosing) {
|
if (parsed.selfClosing) {
|
||||||
@@ -328,7 +405,7 @@ function countLeadingClosingBraces(value) {
|
|||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value[index] !== "}") {
|
if (value[index] !== "}" && value[index] !== "]") {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +476,10 @@ function countStructuralBraces(value) {
|
|||||||
openings += 1;
|
openings += 1;
|
||||||
} else if (character === "}") {
|
} else if (character === "}") {
|
||||||
closings += 1;
|
closings += 1;
|
||||||
|
} else if (character === "[") {
|
||||||
|
openings += 1;
|
||||||
|
} else if (character === "]") {
|
||||||
|
closings += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +538,27 @@ function expandInlineControlBlocks(lines) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expandStructuredStateDeclarations(lines, unit) {
|
||||||
|
return lines.flatMap((line) => {
|
||||||
|
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
|
||||||
|
if (!match) return [line];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(match[2].trim());
|
||||||
|
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
|
||||||
|
if (jsonLines.length === 1) return [`${match[1]}${jsonLines[0]}`];
|
||||||
|
|
||||||
|
const leading = match[1].match(/^\s*/)?.[0] ?? "";
|
||||||
|
return [
|
||||||
|
`${match[1]}${jsonLines[0]}`,
|
||||||
|
...jsonLines.slice(1).map((jsonLine) => `${leading}${jsonLine}`),
|
||||||
|
];
|
||||||
|
} catch {
|
||||||
|
return [line];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatWrn(source, options = {}) {
|
function formatWrn(source, options = {}) {
|
||||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||||
|
|
||||||
@@ -469,7 +571,10 @@ function formatWrn(source, options = {}) {
|
|||||||
let controlDepth = 0;
|
let controlDepth = 0;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
const inputLines = expandInlineControlBlocks(source.replace(/\r\n/g, "\n").split("\n"));
|
const sourceLines = source.replace(/\r\n/g, "\n").split("\n");
|
||||||
|
const inputLines = expandInlineControlBlocks(
|
||||||
|
expandStructuredStateDeclarations(sourceLines, unit),
|
||||||
|
);
|
||||||
|
|
||||||
const output = [];
|
const output = [];
|
||||||
|
|
||||||
@@ -587,6 +692,7 @@ function formatWrn(source, options = {}) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
countStructuralBraces,
|
countStructuralBraces,
|
||||||
formatOpeningTag,
|
formatOpeningTag,
|
||||||
|
formatAttribute,
|
||||||
formatWrn,
|
formatWrn,
|
||||||
parseAttributes,
|
parseAttributes,
|
||||||
parseOpeningTag,
|
parseOpeningTag,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const {
|
|||||||
maskLeadingTrivia,
|
maskLeadingTrivia,
|
||||||
validateBalancedCharacters,
|
validateBalancedCharacters,
|
||||||
validateHtmlTags,
|
validateHtmlTags,
|
||||||
|
validateLayoutUsage,
|
||||||
validateRootMembers,
|
validateRootMembers,
|
||||||
} = require("../src/diagnostics");
|
} = require("../src/diagnostics");
|
||||||
Module._load = originalLoad;
|
Module._load = originalLoad;
|
||||||
@@ -136,6 +137,41 @@ test("ignores member-like words inside component line and block comments", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("allows a component prop named layout but rejects a root layout member", () => {
|
||||||
|
const component = `component Card {
|
||||||
|
props {
|
||||||
|
layout = "vertical"
|
||||||
|
}
|
||||||
|
view { <article>{layout}</article> }
|
||||||
|
}`;
|
||||||
|
const componentDeclaration = findTopLevelDeclaration({}, component);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
validateLayoutUsage(
|
||||||
|
mockDocument(component),
|
||||||
|
component,
|
||||||
|
componentDeclaration.kind,
|
||||||
|
componentDeclaration.match,
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalid = `component Card {
|
||||||
|
layout = "dashboard"
|
||||||
|
view { <article></article> }
|
||||||
|
}`;
|
||||||
|
const invalidDeclaration = findTopLevelDeclaration({}, invalid);
|
||||||
|
const diagnostics = validateLayoutUsage(
|
||||||
|
mockDocument(invalid),
|
||||||
|
invalid,
|
||||||
|
invalidDeclaration.kind,
|
||||||
|
invalidDeclaration.match,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 1);
|
||||||
|
assert.equal(diagnostics[0].code, "wrn-invalid-layout-member");
|
||||||
|
});
|
||||||
|
|
||||||
test("ignores HTML-like tags inside WRN comments", () => {
|
test("ignores HTML-like tags inside WRN comments", () => {
|
||||||
const source = `// The <section> below listens for child events.
|
const source = `// The <section> below listens for child events.
|
||||||
// <ComboBox> is only documentation in this comment.
|
// <ComboBox> is only documentation in this comment.
|
||||||
|
|||||||
@@ -20,6 +20,69 @@ test("preserves nested prop defaults while formatting", () => {
|
|||||||
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
|
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("formats native JSON state values with readable indentation", () => {
|
||||||
|
const source = `component Footer {
|
||||||
|
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
|
||||||
|
view { <footer></footer> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const expected = `component Footer {
|
||||||
|
state footerItems = [
|
||||||
|
{
|
||||||
|
"label": "Accessibility",
|
||||||
|
"href": "/accessibility",
|
||||||
|
"value": "accessibility"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Privacy",
|
||||||
|
"href": "/privacy",
|
||||||
|
"value": "privacy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
view { <footer></footer> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const options = { insertSpaces: true, tabSize: 2 };
|
||||||
|
|
||||||
|
assert.equal(formatWrn(source, options), expected);
|
||||||
|
assert.equal(formatWrn(expected, options), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formats direct structured component props and preserves expression props", () => {
|
||||||
|
const source = `component Demo {
|
||||||
|
view {
|
||||||
|
<Footer items={footerItems} options={{"dense":true,"theme":"public"}} links={[{"label":"Privacy","href":"/privacy"},{"label":"Contact","href":"/contact"}]} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const expected = `component Demo {
|
||||||
|
view {
|
||||||
|
<Footer
|
||||||
|
items={footerItems}
|
||||||
|
options={{
|
||||||
|
"dense": true,
|
||||||
|
"theme": "public"
|
||||||
|
}}
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
"label": "Privacy",
|
||||||
|
"href": "/privacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Contact",
|
||||||
|
"href": "/contact"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const options = { insertSpaces: true, tabSize: 2, multilineAttributes: true };
|
||||||
|
|
||||||
|
assert.equal(formatWrn(source, options), expected);
|
||||||
|
assert.equal(formatWrn(expected, options), expected);
|
||||||
|
});
|
||||||
|
|
||||||
test("formats public event declarations as individual props members", () => {
|
test("formats public event declarations as individual props members", () => {
|
||||||
const source = `component Search {\nprops { value = "" @event search = function @event clear = function }\nview { <input/> }\n}\n`;
|
const source = `component Search {\nprops { value = "" @event search = function @event clear = function }\nview { <input/> }\n}\n`;
|
||||||
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2 });
|
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2 });
|
||||||
|
|||||||
@@ -102,6 +102,25 @@ try {
|
|||||||
compiler.compileWireFile(good);
|
compiler.compileWireFile(good);
|
||||||
ok("compiler accepts valid .wrn");
|
ok("compiler accepts valid .wrn");
|
||||||
|
|
||||||
|
const structuredProps = `page FooterExample {
|
||||||
|
state footerItems = [
|
||||||
|
{
|
||||||
|
"label": "Accessibility",
|
||||||
|
"href": "/accessibility",
|
||||||
|
"value": "accessibility"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
view {
|
||||||
|
<Footer
|
||||||
|
items={footerItems}
|
||||||
|
options={{"dense":true}}
|
||||||
|
links={[{"label":"Privacy","href":"/privacy"}]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
compiler.compileWireFile(structuredProps);
|
||||||
|
ok("compiler accepts native structured state and unquoted prop expressions");
|
||||||
|
|
||||||
const badSrc = `page Home {\n view { <h1>Hi</h2> }\n}`;
|
const badSrc = `page Home {\n view { <h1>Hi</h2> }\n}`;
|
||||||
let threw = false;
|
let threw = false;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"version": "0.5.1",
|
"version": "0.5.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
|
"description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier . --write",
|
"format": "prettier . --write",
|
||||||
"format:check": "prettier . --check",
|
"format:check": "prettier . --check",
|
||||||
"check": "bun run typecheck \u0026\u0026 bun run lint \u0026\u0026 bun run test \u0026\u0026 bun run format:check",
|
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check",
|
||||||
"verify:0.3": "node scripts/verify-0.3.mjs",
|
"verify:0.3": "node scripts/verify-0.3.mjs",
|
||||||
"verify:0.4": "node scripts/verify-0.4.mjs",
|
"verify:0.4": "node scripts/verify-0.4.mjs",
|
||||||
"validate:0.4": "node scripts/validate-0.4.mjs",
|
"validate:0.4": "node scripts/validate-0.4.mjs",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"typescript-eslint": "latest"
|
"typescript-eslint": "latest"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": "\u003e=1.3.0"
|
"bun": ">=1.3.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"esbuild": "0.28.1"
|
"esbuild": "0.28.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||||
|
|||||||
@@ -36,13 +36,9 @@ component AuthenticatorSetup {
|
|||||||
novalidate
|
novalidate
|
||||||
class='mt-5 space-y-4 {credentialId == "" ? "" : "hidden"}'
|
class='mt-5 space-y-4 {credentialId == "" ? "" : "hidden"}'
|
||||||
>
|
>
|
||||||
<div class="space-y-1.5">
|
<Input id="auth-authenticator-label" name="label" label="Authenticator name" value="Authenticator" autocomplete="off" icon="icon-[lucide--smartphone]" color="{color}" size="{size}" />
|
||||||
<label for="auth-authenticator-label" class="block text-sm font-medium">Authenticator name</label>
|
|
||||||
<input id="auth-authenticator-label" name="label" value="Authenticator" autocomplete="off" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" />
|
|
||||||
<p data-error="label" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:opacity-60">{setupLabel}</button>
|
<Button type="submit" label="{setupLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div data-auth-authenticator-details class='{credentialId == "" ? "hidden" : ""}'>
|
<div data-auth-authenticator-details class='{credentialId == "" ? "hidden" : ""}'>
|
||||||
@@ -53,15 +49,11 @@ component AuthenticatorSetup {
|
|||||||
</div>
|
</div>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' data-auth-authenticator-confirm novalidate class="mt-5 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' data-auth-authenticator-confirm novalidate class="mt-5 space-y-4">
|
||||||
<input type="hidden" name="credentialId" value='{credentialId}' />
|
<input type="hidden" name="credentialId" value='{credentialId}' />
|
||||||
<div class="space-y-1.5">
|
<PinInput label="Authenticator code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||||
<label for="auth-authenticator-code" class="block text-sm font-medium">Authenticator code</label>
|
|
||||||
<input id="auth-authenticator-code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em]" />
|
|
||||||
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-error="credentialId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="credentialId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ component ForgotPassword {
|
|||||||
title = "Recover your account"
|
title = "Recover your account"
|
||||||
description = "Enter your email, phone, or username."
|
description = "Enter your email, phone, or username."
|
||||||
identifierLabel = "Account identifier"
|
identifierLabel = "Account identifier"
|
||||||
|
identifierPlaceholder = "Enter your registered email, mobile number, or username"
|
||||||
submitLabel = "Send recovery link"
|
submitLabel = "Send recovery link"
|
||||||
successMessage = "If an account matches, a recovery link has been sent."
|
successMessage = "If an account matches, a recovery link has been sent."
|
||||||
signInHref = "/sign-in"
|
signInHref = "/sign-in"
|
||||||
@@ -17,14 +18,11 @@ component ForgotPassword {
|
|||||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||||
<div class="space-y-1.5">
|
<Input id="auth-recovery-identifier" name="identifier" label="{identifierLabel}" autocomplete="username" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||||
<label for="auth-recovery-identifier" class="block text-sm font-medium">{identifierLabel}</label>
|
<slot></slot>
|
||||||
<input id="auth-recovery-identifier" name="identifier" autocomplete="username" aria-describedby="auth-recovery-identifier-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" />
|
|
||||||
<p id="auth-recovery-identifier-error" data-error="identifier" role="alert" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Back to sign in</a>
|
<a href='{signInHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Back to sign in</a>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ component InvitationAccept {
|
|||||||
<div class="mt-5 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] p-4 text-sm"><p class="m-0 font-semibold">{organization}</p><p class="mb-0 mt-1 text-[var(--wire-color-muted)]">Invited by {inviter}</p></div>
|
<div class="mt-5 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] p-4 text-sm"><p class="m-0 font-semibold">{organization}</p><p class="mb-0 mt-1 text-[var(--wire-color-muted)]">Invited by {inviter}</p></div>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-5 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-5 space-y-4">
|
||||||
<input type="hidden" name="token" value='{token}' />
|
<input type="hidden" name="token" value='{token}' />
|
||||||
<div class="space-y-1.5"><label for="auth-invitation-name" class="block text-sm font-medium">Display name</label><input id="auth-invitation-name" name="displayName" autocomplete="name" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="displayName" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
<Input id="auth-invitation-name" name="displayName" label="Display name" autocomplete="name" placeholder="Enter your display name" icon="icon-[lucide--user]" color="{color}" size="{size}" />
|
||||||
<div class="space-y-1.5"><label for="auth-invitation-password" class="block text-sm font-medium">Create password</label><input id="auth-invitation-password" name="password" type="password" autocomplete="new-password" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
<StrongPassword label="Create password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
|
||||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ component MagicLinkSignIn {
|
|||||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||||
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||||
<div class="space-y-1.5"><label for="auth-magic-link-identifier" class="block text-sm font-medium">{emailLabel}</label><input id="auth-magic-link-identifier" name="identifier" type="email" autocomplete="email" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" /><p data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
<Input id="auth-magic-link-identifier" name="identifier" type="email" label="{emailLabel}" autocomplete="email" placeholder="Enter your email address" icon="icon-[lucide--mail]" color="{color}" size="{size}" />
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm font-medium text-[var(--wire-color-primary)] hover:underline">Return to password sign-in</a>
|
<a href='{signInHref}' class="mt-4 inline-flex text-sm font-medium text-[var(--wire-color-primary)] hover:underline">Return to password sign-in</a>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -41,15 +41,11 @@ component OtpSignIn {
|
|||||||
class='mt-6 space-y-4 {challengeId == "" ? "" : "hidden"}'
|
class='mt-6 space-y-4 {challengeId == "" ? "" : "hidden"}'
|
||||||
>
|
>
|
||||||
<input type="hidden" name="method" value='{method}' />
|
<input type="hidden" name="method" value='{method}' />
|
||||||
<div class="space-y-1.5">
|
<Input id="auth-otp-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username" placeholder="Enter your email address or phone number" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||||
<label for="auth-otp-identifier" class="block text-sm font-medium">{identifierLabel}</label>
|
|
||||||
<input id="auth-otp-identifier" name="identifier" type="text" autocomplete="username" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" />
|
|
||||||
<p data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{requestLabel}</button>
|
<Button type="submit" label="{requestLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -62,15 +58,11 @@ component OtpSignIn {
|
|||||||
>
|
>
|
||||||
<input type="hidden" name="challengeId" value='{challengeId}' />
|
<input type="hidden" name="challengeId" value='{challengeId}' />
|
||||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||||
<div class="space-y-1.5">
|
<PinInput label="One-time code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||||
<label for="auth-otp-code" class="block text-sm font-medium">One-time code</label>
|
|
||||||
<input id="auth-otp-code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em] outline-none focus:border-[var(--wire-color-primary)]" />
|
|
||||||
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{verifyLabel}</button>
|
<Button type="submit" label="{verifyLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
<button type="button" data-auth-otp-back class="w-full text-sm text-[var(--wire-color-primary)] hover:underline">Use a different account</button>
|
<Button type="button" data-auth-otp-back label="Use a different account" variant="ghost" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,11 @@ component ResetPassword {
|
|||||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-6 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-6 space-y-4">
|
||||||
<input type="hidden" name="token" value='{token}' />
|
<input type="hidden" name="token" value='{token}' />
|
||||||
<div class="space-y-1.5">
|
<StrongPassword label="New password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
|
||||||
<label for="auth-reset-password" class="block text-sm font-medium">New password</label>
|
|
||||||
<input id="auth-reset-password" name="password" type="password" autocomplete="new-password" aria-describedby="auth-reset-password-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
|
||||||
<p class="m-0 text-xs text-[var(--wire-color-muted)]">At least 12 characters with uppercase, lowercase, and a number.</p>
|
|
||||||
<p id="auth-reset-password-error" data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ component SignIn {
|
|||||||
title = "Welcome back"
|
title = "Welcome back"
|
||||||
description = "Sign in to continue to your account."
|
description = "Sign in to continue to your account."
|
||||||
identifierLabel = "Email, phone, or username"
|
identifierLabel = "Email, phone, or username"
|
||||||
|
identifierPlaceholder = "Enter your email, mobile number, or username"
|
||||||
passwordLabel = "Password"
|
passwordLabel = "Password"
|
||||||
|
passwordPlaceholder = "Enter your password"
|
||||||
submitLabel = "Sign in"
|
submitLabel = "Sign in"
|
||||||
forgotHref = "/recover"
|
forgotHref = "/recover"
|
||||||
signUpHref = "/sign-up"
|
signUpHref = "/sign-up"
|
||||||
@@ -29,22 +31,25 @@ component SignIn {
|
|||||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||||
<input type="hidden" name="deviceFingerprint" value="" />
|
<input type="hidden" name="deviceFingerprint" value="" />
|
||||||
<input type="hidden" name="deviceName" value="" />
|
<input type="hidden" name="deviceName" value="" />
|
||||||
|
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="auth-sign-in-identifier" class="block text-sm font-medium">{identifierLabel}</label>
|
<div class="flex justify-end"><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
|
||||||
<input id="auth-sign-in-identifier" name="identifier" type="text" autocomplete="username webauthn" aria-describedby="auth-sign-in-identifier-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)] focus:ring-2 focus:ring-[color-mix(in_srgb,var(--wire-color-primary)_25%,transparent)]" />
|
<TogglePassword
|
||||||
<p id="auth-sign-in-identifier-error" data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
label="{passwordLabel}"
|
||||||
</div>
|
name="password"
|
||||||
<div class="space-y-1.5">
|
placeholder="{passwordPlaceholder}"
|
||||||
<div class="flex items-center justify-between gap-3 text-sm font-medium"><label for="auth-sign-in-password">{passwordLabel}</label><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
|
autocomplete="current-password"
|
||||||
<input id="auth-sign-in-password" name="password" type="password" autocomplete="current-password" aria-describedby="auth-sign-in-password-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)] focus:ring-2 focus:ring-[color-mix(in_srgb,var(--wire-color-primary)_25%,transparent)]" />
|
pattern=""
|
||||||
<p id="auth-sign-in-password-error" data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if showRemember}
|
{#if showRemember}
|
||||||
<label class="flex items-center gap-2 text-sm text-[var(--wire-color-muted)]"><input name="rememberDevice" type="checkbox" class="size-4 rounded border-[var(--wire-color-border)]" />Trust this device</label>
|
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
|
||||||
{/if}
|
{/if}
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<button type="submit" class="inline-flex h-11 w-full items-center justify-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
{#if showPasskey}
|
{#if showPasskey}
|
||||||
<div class="my-4 flex items-center gap-3 text-xs text-[var(--wire-color-muted)]"><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span><span>or</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
|
<div class="my-4 flex items-center gap-3 text-xs text-[var(--wire-color-muted)]"><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span><span>or</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ component SignUp {
|
|||||||
description = "Use a strong password and verify your contact details."
|
description = "Use a strong password and verify your contact details."
|
||||||
submitLabel = "Create account"
|
submitLabel = "Create account"
|
||||||
signInHref = "/sign-in"
|
signInHref = "/sign-in"
|
||||||
|
termsHref = "/legal/terms"
|
||||||
|
privacyHref = "/legal/privacy-policy"
|
||||||
|
namePlaceholder = "Enter your full name"
|
||||||
|
emailPlaceholder = "Enter your email address"
|
||||||
|
phonePlaceholder = "Enter your mobile number"
|
||||||
|
usernamePlaceholder = "Choose a username"
|
||||||
|
passwordPlaceholder = "Create a strong password"
|
||||||
showPhone = true
|
showPhone = true
|
||||||
showUsername = true
|
showUsername = true
|
||||||
requireConsent = true
|
requireConsent = true
|
||||||
@@ -15,10 +22,24 @@ component SignUp {
|
|||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
view {
|
view {
|
||||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
<section
|
||||||
<header class="mb-6 space-y-1.5">
|
{...attrs}
|
||||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
data-wrnexus-runtime="auth"
|
||||||
<p class="m-0 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="mb-6 space-y-1.5"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="m-0 text-2xl font-semibold"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="m-0 text-sm text-[var(--wire-color-muted)]"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -29,62 +50,146 @@ component SignUp {
|
|||||||
novalidate
|
novalidate
|
||||||
class="grid gap-4 sm:grid-cols-2"
|
class="grid gap-4 sm:grid-cols-2"
|
||||||
>
|
>
|
||||||
<div class="space-y-1.5 sm:col-span-2">
|
<Input
|
||||||
<label for="auth-sign-up-name" class="text-sm font-medium">Full name</label>
|
id="auth-sign-up-name"
|
||||||
<input id="auth-sign-up-name" name="displayName" autocomplete="name" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
name="displayName"
|
||||||
<p data-error="displayName" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
label="Full name"
|
||||||
</div>
|
autocomplete="name"
|
||||||
|
placeholder="{namePlaceholder}"
|
||||||
|
icon="icon-[lucide--user]"
|
||||||
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
class="sm:col-span-2"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<Input
|
||||||
<label for="auth-sign-up-email" class="text-sm font-medium">Email</label>
|
id="auth-sign-up-email"
|
||||||
<input id="auth-sign-up-email" name="email" type="email" autocomplete="email" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
name="email"
|
||||||
<p data-error="email" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
type="email"
|
||||||
</div>
|
label="Email"
|
||||||
|
autocomplete="email"
|
||||||
|
placeholder="{emailPlaceholder}"
|
||||||
|
icon="icon-[lucide--mail]"
|
||||||
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
/>
|
||||||
|
|
||||||
{#if showPhone}
|
{#if showPhone}
|
||||||
<div class="space-y-1.5">
|
<Input
|
||||||
<label for="auth-sign-up-phone" class="text-sm font-medium">Phone</label>
|
id="auth-sign-up-phone"
|
||||||
<input id="auth-sign-up-phone" name="phone" type="tel" autocomplete="tel" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
name="phone"
|
||||||
<p data-error="phone" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
type="tel"
|
||||||
</div>
|
label="Phone"
|
||||||
|
autocomplete="tel"
|
||||||
|
placeholder="{phonePlaceholder}"
|
||||||
|
icon="icon-[lucide--phone]"
|
||||||
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showUsername}
|
{#if showUsername}
|
||||||
<div class="space-y-1.5 sm:col-span-2">
|
<Input
|
||||||
<label for="auth-sign-up-username" class="text-sm font-medium">Username</label>
|
id="auth-sign-up-username"
|
||||||
<input id="auth-sign-up-username" name="username" autocomplete="username" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
name="username"
|
||||||
<p data-error="username" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
label="Username"
|
||||||
</div>
|
autocomplete="username"
|
||||||
|
placeholder="{usernamePlaceholder}"
|
||||||
|
icon="icon-[lucide--circle-user-round]"
|
||||||
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
class="sm:col-span-2"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="space-y-1.5 sm:col-span-2">
|
<div
|
||||||
<label for="auth-sign-up-password" class="text-sm font-medium">Password</label>
|
class="space-y-1.5 sm:col-span-2"
|
||||||
<input id="auth-sign-up-password" name="password" type="password" autocomplete="new-password" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
>
|
||||||
<p class="m-0 text-xs text-[var(--wire-color-muted)]">At least 12 characters with uppercase, lowercase, and a number.</p>
|
<StrongPassword
|
||||||
<p data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
placeholder="{passwordPlaceholder}"
|
||||||
|
autocomplete="new-password"
|
||||||
|
minLength="12"
|
||||||
|
requireLowercase="true"
|
||||||
|
requireUppercase="true"
|
||||||
|
requireNumber="true"
|
||||||
|
requireSpecialCharacter="true"
|
||||||
|
presentation="inline"
|
||||||
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if requireConsent}
|
{#if requireConsent}
|
||||||
<div class="space-y-1 sm:col-span-2">
|
<Checkbox
|
||||||
<label class="flex items-start gap-2 text-sm text-[var(--wire-color-muted)]">
|
id="auth-sign-up-consent"
|
||||||
<input name="consent" type="checkbox" class="mt-0.5 size-4" />
|
name="consent"
|
||||||
<span>I agree to the terms and privacy policy.</span>
|
label="I agree to the"
|
||||||
</label>
|
color="{color}"
|
||||||
<p data-error="consent" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
size="{size}"
|
||||||
</div>
|
class="sm:col-span-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{termsHref}"
|
||||||
|
>
|
||||||
|
terms
|
||||||
|
</a>
|
||||||
|
and
|
||||||
|
<a
|
||||||
|
href="{privacyHref}"
|
||||||
|
>
|
||||||
|
privacy policy
|
||||||
|
</a>.
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
{:else}
|
{:else}
|
||||||
<input type="hidden" name="consent" value="true" />
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="consent"
|
||||||
|
value="true"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="sm:col-span-2"><slot></slot></div>
|
<div
|
||||||
|
class="sm:col-span-2"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"></p>
|
<p
|
||||||
<p data-success="Account created successfully." role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"></p>
|
data-error="_form"
|
||||||
|
role="alert"
|
||||||
|
class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
data-success="Account created successfully."
|
||||||
|
role="status"
|
||||||
|
hidden
|
||||||
|
class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
<button type="submit" class="h-11 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white sm:col-span-2 disabled:cursor-not-allowed disabled:opacity-60">{submitLabel}</button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
label="{submitLabel}"
|
||||||
|
variant="solid"
|
||||||
|
color="{color}"
|
||||||
|
size="{size}"
|
||||||
|
fullWidth="true"
|
||||||
|
class="sm:col-span-2"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]">Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a></p>
|
<p
|
||||||
|
class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]"
|
||||||
|
>
|
||||||
|
Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a>
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ component TwoFactorChallenge {
|
|||||||
size = "md"
|
size = "md"
|
||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
|
state methodOptions = [
|
||||||
|
{"label":"Authenticator app","value":"totp"},
|
||||||
|
{"label":"Recovery code","value":"recovery-code"},
|
||||||
|
{"label":"Email code","value":"email-otp"},
|
||||||
|
{"label":"SMS code","value":"sms-otp"}
|
||||||
|
]
|
||||||
view {
|
view {
|
||||||
<section
|
<section
|
||||||
{...attrs}
|
{...attrs}
|
||||||
@@ -31,26 +37,13 @@ component TwoFactorChallenge {
|
|||||||
<input type="hidden" name="mfaToken" value='{mfaToken}' />
|
<input type="hidden" name="mfaToken" value='{mfaToken}' />
|
||||||
<input type="hidden" name="challengeId" value='{challengeId}' />
|
<input type="hidden" name="challengeId" value='{challengeId}' />
|
||||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||||
<div class="space-y-1.5">
|
<Select id="auth-mfa-method" name="method" data-auth-mfa-method label="Verification method" value="{method}" options="{methodOptions}" color="{color}" size="{size}" />
|
||||||
<label for="auth-mfa-method" class="block text-sm font-medium">Verification method</label>
|
<PinInput label="Verification code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||||
<select id="auth-mfa-method" name="method" data-auth-mfa-method class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3">
|
|
||||||
<option value="totp">Authenticator app</option>
|
|
||||||
<option value="recovery-code">Recovery code</option>
|
|
||||||
<option value="email-otp">Email code</option>
|
|
||||||
<option value="sms-otp">SMS code</option>
|
|
||||||
</select>
|
|
||||||
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<label for="auth-mfa-code" class="block text-sm font-medium">Verification code</label>
|
|
||||||
<input id="auth-mfa-code" name="code" inputmode="numeric" autocomplete="one-time-code" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em]" />
|
|
||||||
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
|
||||||
</div>
|
|
||||||
<p data-auth-mfa-status role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-primary)_10%,transparent)] p-3 text-sm text-[var(--wire-color-primary)]"></p>
|
<p data-auth-mfa-status role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-primary)_10%,transparent)] p-3 text-sm text-[var(--wire-color-primary)]"></p>
|
||||||
<p data-error="mfaToken" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="mfaToken" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
<a href='{recoveryHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Account recovery options</a>
|
<a href='{recoveryHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Account recovery options</a>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ component VerifyEmail {
|
|||||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||||
<div class="space-y-1.5"><label for="auth-email-token" class="block text-sm font-medium">Verification token</label><input id="auth-email-token" name="token" value='{token}' autocomplete="one-time-code" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
<Input id="auth-email-token" name="token" label="Verification token" value="{token}" autocomplete="one-time-code" placeholder="Enter your verification token" icon="icon-[lucide--badge-check]" color="{color}" size="{size}" />
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
||||||
<input type="hidden" name="type" value='{kind}' />
|
<input type="hidden" name="type" value='{kind}' />
|
||||||
<input type="hidden" name="identifier" value='{identifier}' />
|
<input type="hidden" name="identifier" value='{identifier}' />
|
||||||
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<button type="submit" class="text-sm text-[var(--wire-color-primary)] hover:underline disabled:opacity-60">Send a new code</button>
|
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ component VerifyPhone {
|
|||||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||||
<div class="space-y-1.5"><label for="auth-phone-token" class="block text-sm font-medium">Verification token</label><input id="auth-phone-token" name="token" value='{token}' autocomplete="one-time-code" inputmode="numeric" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
<PinInput label="Verification token" name="token" value="{token}" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
||||||
<input type="hidden" name="type" value='{kind}' />
|
<input type="hidden" name="type" value='{kind}' />
|
||||||
<input type="hidden" name="identifier" value='{identifier}' />
|
<input type="hidden" name="identifier" value='{identifier}' />
|
||||||
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||||
<button type="submit" class="text-sm text-[var(--wire-color-primary)] hover:underline disabled:opacity-60">Send a new code</button>
|
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"check": "bun run typecheck \u0026\u0026 bun run test"
|
"check": "bun run typecheck && bun run test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"@wrnexus/oauth": "workspace:*",
|
"@wrnexus/oauth": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
"@wrnexus/queue": "workspace:*",
|
"@wrnexus/queue": "workspace:*",
|
||||||
|
"@wrnexus/ui": "workspace:*",
|
||||||
"@wrnexus/validation": "workspace:*",
|
"@wrnexus/validation": "workspace:*",
|
||||||
"@wrnexus/db": "workspace:*"
|
"@wrnexus/db": "workspace:*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
getAuthUser,
|
getAuthUser,
|
||||||
} from "../middleware.ts";
|
} from "../middleware.ts";
|
||||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
||||||
import type { AuthSignedInHandler, AuthSignedOutHandler } from "../types.ts";
|
import type {
|
||||||
|
AuthSessionVerificationHandler,
|
||||||
|
AuthSignedInHandler,
|
||||||
|
AuthSignedOutHandler,
|
||||||
|
} from "../types.ts";
|
||||||
|
|
||||||
function text(value: unknown): string {
|
function text(value: unknown): string {
|
||||||
return typeof value === "string" ? value : value == null ? "" : String(value);
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
||||||
@@ -53,6 +57,7 @@ export interface AuthHttpOptions {
|
|||||||
onSignedIn?: AuthSignedInHandler;
|
onSignedIn?: AuthSignedInHandler;
|
||||||
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
||||||
onSignedOut?: AuthSignedOutHandler;
|
onSignedOut?: AuthSignedOutHandler;
|
||||||
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
||||||
@@ -83,6 +88,21 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
async verifySession(ctx: Context): Promise<Response> {
|
||||||
|
const user = getAuthUser(ctx);
|
||||||
|
if (options.onSessionVerification) {
|
||||||
|
return options.onSessionVerification(ctx, user);
|
||||||
|
}
|
||||||
|
if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
|
||||||
|
if ((ctx.req.headers.get("accept") ?? "").includes("application/json")) {
|
||||||
|
return json({ ok: true, user });
|
||||||
|
}
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: { "cache-control": "no-store" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async register(ctx: Context): Promise<Response> {
|
async register(ctx: Context): Promise<Response> {
|
||||||
const validation = await parseBody(schemas.register, ctx.req);
|
const validation = await parseBody(schemas.register, ctx.req);
|
||||||
if (!validation.ok) return validation.response;
|
if (!validation.ok) return validation.response;
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ function wantsJson(ctx: Context): boolean {
|
|||||||
return accept.includes("application/json") && !accept.includes("text/html");
|
return accept.includes("application/json") && !accept.includes("text/html");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authSession(engine: AuthEngine): Middleware {
|
export interface AuthSessionOptions {
|
||||||
|
/** Optional shared SSO cookie containing an AuthEngine session id. */
|
||||||
|
cookieName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authSession(engine: AuthEngine, options: AuthSessionOptions = {}): Middleware {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
const sessionId = ctx.session.get<string>(AUTH_SESSION_KEY);
|
const sessionId =
|
||||||
|
(options.cookieName ? ctx.cookies.get(options.cookieName) : undefined) ??
|
||||||
|
ctx.session.get<string>(AUTH_SESSION_KEY);
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
ctx.user = null;
|
ctx.user = null;
|
||||||
ctx.locals.authUser = null;
|
ctx.locals.authUser = null;
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||||
import type { AuthEngine } from "./engine.ts";
|
import type { AuthEngine } from "./engine.ts";
|
||||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||||
import type { AuthSignedInHandler, AuthSignedOutHandler } from "./types.ts";
|
import type {
|
||||||
|
AuthSessionVerificationHandler,
|
||||||
|
AuthSignedInHandler,
|
||||||
|
AuthSignedOutHandler,
|
||||||
|
} from "./types.ts";
|
||||||
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
|
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
|
||||||
import {
|
import {
|
||||||
clearDefaultAuthEngine,
|
clearDefaultAuthEngine,
|
||||||
@@ -52,6 +56,10 @@ export interface AuthConfig {
|
|||||||
onSignedIn?: AuthSignedInHandler;
|
onSignedIn?: AuthSignedInHandler;
|
||||||
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
||||||
onSignedOut?: AuthSignedOutHandler;
|
onSignedOut?: AuthSignedOutHandler;
|
||||||
|
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
||||||
|
sessionCookieName?: string;
|
||||||
|
/** Customize the package forward-auth verification response. */
|
||||||
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
||||||
@@ -88,14 +96,22 @@ interface ResolvedAuthConfig {
|
|||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
onSignedIn?: AuthSignedInHandler;
|
onSignedIn?: AuthSignedInHandler;
|
||||||
onSignedOut?: AuthSignedOutHandler;
|
onSignedOut?: AuthSignedOutHandler;
|
||||||
|
sessionCookieName?: string;
|
||||||
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const packageRoot = dirname(moduleRoot);
|
||||||
|
const compiledPackage = moduleRoot === join(packageRoot, "dist");
|
||||||
const clientRuntime = join(packageRoot, "assets", "client", "auth.js");
|
const clientRuntime = join(packageRoot, "assets", "client", "auth.js");
|
||||||
const migrationsFile = join(packageRoot, "migrations", "001_auth.sql");
|
const migrationsFile = join(packageRoot, "migrations", "001_auth.sql");
|
||||||
const otpPurposeMigrationFile = join(packageRoot, "migrations", "002_auth_otp_purpose.sql");
|
const otpPurposeMigrationFile = join(packageRoot, "migrations", "002_auth_otp_purpose.sql");
|
||||||
const apiRoutesDir = join(packageRoot, "src", "routes", "api");
|
const apiRoutesDir = compiledPackage
|
||||||
const middlewareFile = join(packageRoot, "src", "routes", "middleware.ts");
|
? join(moduleRoot, "routes", "api")
|
||||||
|
: join(packageRoot, "src", "routes", "api");
|
||||||
|
const middlewareFile = compiledPackage
|
||||||
|
? join(moduleRoot, "routes", "middleware.js")
|
||||||
|
: join(packageRoot, "src", "routes", "middleware.ts");
|
||||||
const resolvedConfigKey = "@wrnexus/auth:resolved-config";
|
const resolvedConfigKey = "@wrnexus/auth:resolved-config";
|
||||||
|
|
||||||
export function authComponentsDir(): string {
|
export function authComponentsDir(): string {
|
||||||
@@ -104,7 +120,7 @@ export function authComponentsDir(): string {
|
|||||||
|
|
||||||
function authApiRouteEntry(path: string): string {
|
function authApiRouteEntry(path: string): string {
|
||||||
const routeName = path.replace(/^\/api\/auth\/?/, "").replace(/\//g, "-") || "index";
|
const routeName = path.replace(/^\/api\/auth\/?/, "").replace(/\//g, "-") || "index";
|
||||||
return join(apiRoutesDir, routeName + ".ts");
|
return join(apiRoutesDir, routeName + (compiledPackage ? ".js" : ".ts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveConfig(
|
function resolveConfig(
|
||||||
@@ -142,6 +158,8 @@ function resolveConfig(
|
|||||||
passkey: raw.passkey,
|
passkey: raw.passkey,
|
||||||
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
|
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
|
||||||
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
|
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
|
||||||
|
sessionCookieName: raw.sessionCookieName,
|
||||||
|
onSessionVerification: raw.onSessionVerification,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +395,10 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
|||||||
onSignedIn: value.onSignedIn,
|
onSignedIn: value.onSignedIn,
|
||||||
|
|
||||||
onSignedOut: value.onSignedOut,
|
onSignedOut: value.onSignedOut,
|
||||||
|
|
||||||
|
sessionCookieName: value.sessionCookieName,
|
||||||
|
|
||||||
|
onSessionVerification: value.onSessionVerification,
|
||||||
});
|
});
|
||||||
|
|
||||||
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
|
|||||||
passkey: routeOptions.passkey,
|
passkey: routeOptions.passkey,
|
||||||
onSignedIn: routeOptions.onSignedIn,
|
onSignedIn: routeOptions.onSignedIn,
|
||||||
onSignedOut: routeOptions.onSignedOut,
|
onSignedOut: routeOptions.onSignedOut,
|
||||||
|
onSessionVerification: routeOptions.onSessionVerification,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("verifySession", ctx);
|
||||||
|
}
|
||||||
@@ -23,6 +23,12 @@ export interface AuthRouteDefinition {
|
|||||||
|
|
||||||
/** Single source of truth for package-contributed auth endpoints. */
|
/** Single source of truth for package-contributed auth endpoints. */
|
||||||
export const AUTH_ROUTE_DEFINITIONS = [
|
export const AUTH_ROUTE_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
path: "/api/auth/session/verify",
|
||||||
|
group: "sessions",
|
||||||
|
handler: "verifySession",
|
||||||
|
methods: ["GET"],
|
||||||
|
},
|
||||||
{ path: "/api/auth/register", group: "registration", handler: "register", methods: ["POST"] },
|
{ path: "/api/auth/register", group: "registration", handler: "register", methods: ["POST"] },
|
||||||
{ path: "/api/auth/login", group: "login", handler: "login", methods: ["POST"] },
|
{ path: "/api/auth/login", group: "login", handler: "login", methods: ["POST"] },
|
||||||
{ path: "/api/auth/logout", group: "login", handler: "logout", methods: ["POST"] },
|
{ path: "/api/auth/logout", group: "login", handler: "logout", methods: ["POST"] },
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import type { Middleware } from "@wrnexus/core";
|
import type { Middleware } from "@wrnexus/core";
|
||||||
import { authSession } from "../middleware.ts";
|
import { authSession } from "../middleware.ts";
|
||||||
import { getDefaultAuthEngine, hasDefaultAuthEngine } from "../runtime.ts";
|
import {
|
||||||
|
getDefaultAuthEngine,
|
||||||
|
getDefaultAuthRouteOptions,
|
||||||
|
hasDefaultAuthEngine,
|
||||||
|
} from "../runtime.ts";
|
||||||
|
|
||||||
/** Package middleware: hydrates auth state when a default engine is configured. */
|
/** Package middleware: hydrates auth state when a default engine is configured. */
|
||||||
const middleware: Middleware = async (ctx, next) => {
|
const middleware: Middleware = async (ctx, next) => {
|
||||||
if (!hasDefaultAuthEngine()) return next();
|
if (!hasDefaultAuthEngine()) return next();
|
||||||
return authSession(getDefaultAuthEngine())(ctx, next);
|
return authSession(getDefaultAuthEngine(), {
|
||||||
|
cookieName: getDefaultAuthRouteOptions().sessionCookieName,
|
||||||
|
})(ctx, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default middleware;
|
export default middleware;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Context } from "@wrnexus/core";
|
import type { Context } from "@wrnexus/core";
|
||||||
import type { AuthEngine } from "./engine.ts";
|
import type { AuthEngine } from "./engine.ts";
|
||||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||||
|
import type { AuthSessionVerificationHandler } from "./types.ts";
|
||||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
|
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
|
||||||
|
|
||||||
export interface DefaultAuthRouteOptions {
|
export interface DefaultAuthRouteOptions {
|
||||||
@@ -11,6 +12,8 @@ export interface DefaultAuthRouteOptions {
|
|||||||
onSignedIn?: (ctx: Context, returnTo?: string) => Response | Promise<Response>;
|
onSignedIn?: (ctx: Context, returnTo?: string) => Response | Promise<Response>;
|
||||||
|
|
||||||
onSignedOut?: (ctx: Context) => Response | Promise<Response>;
|
onSignedOut?: (ctx: Context) => Response | Promise<Response>;
|
||||||
|
sessionCookieName?: string;
|
||||||
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthRuntimeState {
|
interface AuthRuntimeState {
|
||||||
|
|||||||
@@ -241,6 +241,12 @@ export type AuthSignedInHandler = (ctx: Context, returnTo?: string) => Response
|
|||||||
/** HTTP response hook invoked after the auth engine clears a signed-in session. */
|
/** HTTP response hook invoked after the auth engine clears a signed-in session. */
|
||||||
export type AuthSignedOutHandler = (ctx: Context) => Response | Promise<Response>;
|
export type AuthSignedOutHandler = (ctx: Context) => Response | Promise<Response>;
|
||||||
|
|
||||||
|
/** Forward-auth response hook. A null user means the request is unauthenticated. */
|
||||||
|
export type AuthSessionVerificationHandler = (
|
||||||
|
ctx: Context,
|
||||||
|
user: AuthPublicUser | null,
|
||||||
|
) => Response | Promise<Response>;
|
||||||
|
|
||||||
export interface AuthSuccessfulSignUpAction {
|
export interface AuthSuccessfulSignUpAction {
|
||||||
/**
|
/**
|
||||||
* Run the normal login policy with the newly registered credentials and
|
* Run the normal login policy with the newly registered credentials and
|
||||||
|
|||||||
@@ -51,6 +51,48 @@ test("passwordless and passkey components preserve MFA continuation metadata", (
|
|||||||
expect(signIn).toContain('name="deviceName"');
|
expect(signIn).toContain('name="deviceName"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("recovery forms expose a form slot for CAPTCHA and policy controls", () => {
|
||||||
|
const source = readFileSync(join(directory, "ForgotPassword.wrn"), "utf8");
|
||||||
|
const formStart = source.indexOf("<form");
|
||||||
|
const formEnd = source.indexOf("</form>");
|
||||||
|
const slot = source.indexOf("<slot></slot>");
|
||||||
|
|
||||||
|
expect(formStart).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(slot).toBeGreaterThan(formStart);
|
||||||
|
expect(slot).toBeLessThan(formEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("account forms use the dedicated password components", () => {
|
||||||
|
const signIn = readFileSync(join(directory, "SignIn.wrn"), "utf8");
|
||||||
|
const signUp = readFileSync(join(directory, "SignUp.wrn"), "utf8");
|
||||||
|
|
||||||
|
expect(signIn).toContain("<TogglePassword");
|
||||||
|
expect(signIn).not.toContain('id="auth-sign-in-password"');
|
||||||
|
expect(signUp).toContain("<StrongPassword");
|
||||||
|
expect(signUp).not.toContain('id="auth-sign-up-password"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authentication journeys use themed UI form controls", () => {
|
||||||
|
const expected = {
|
||||||
|
"SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"],
|
||||||
|
"SignUp.wrn": ["<Input", "<StrongPassword", "<Checkbox", "<Button"],
|
||||||
|
"ForgotPassword.wrn": ["<Input", "<Button"],
|
||||||
|
"ResetPassword.wrn": ["<StrongPassword", "<Button"],
|
||||||
|
"MagicLinkSignIn.wrn": ["<Input", "<Button"],
|
||||||
|
"InvitationAccept.wrn": ["<Input", "<StrongPassword", "<Button"],
|
||||||
|
"OtpSignIn.wrn": ["<Input", "<PinInput", "<Button"],
|
||||||
|
"TwoFactorChallenge.wrn": ["<Select", "<PinInput", "<Button"],
|
||||||
|
"AuthenticatorSetup.wrn": ["<Input", "<PinInput", "<Button"],
|
||||||
|
"VerifyEmail.wrn": ["<Input", "<Button"],
|
||||||
|
"VerifyPhone.wrn": ["<PinInput", "<Button"],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [file, controls] of Object.entries(expected)) {
|
||||||
|
const source = readFileSync(join(directory, file), "utf8");
|
||||||
|
for (const control of controls) expect(source).toContain(control);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("verification components can resend without application-owned schema files", () => {
|
test("verification components can resend without application-owned schema files", () => {
|
||||||
const email = readFileSync(join(directory, "VerifyEmail.wrn"), "utf8");
|
const email = readFileSync(join(directory, "VerifyEmail.wrn"), "utf8");
|
||||||
const phone = readFileSync(join(directory, "VerifyPhone.wrn"), "utf8");
|
const phone = readFileSync(join(directory, "VerifyPhone.wrn"), "utf8");
|
||||||
|
|||||||
@@ -178,6 +178,26 @@ test("config.auth registers package routes", async () => {
|
|||||||
|
|
||||||
expect(contributions.migrations).toHaveLength(0);
|
expect(contributions.migrations).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
test("config.auth contributes a reusable forward-auth verification route", async () => {
|
||||||
|
const runner = createPluginRunner(authPlugin(), {
|
||||||
|
root: process.cwd(),
|
||||||
|
mode: "development",
|
||||||
|
command: "dev",
|
||||||
|
metadata: new Map<string, unknown>(),
|
||||||
|
warn() {},
|
||||||
|
});
|
||||||
|
await runner.configure({
|
||||||
|
auth: {
|
||||||
|
engine: {} as never,
|
||||||
|
routes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const contributions = await runner.contributions();
|
||||||
|
expect(contributions.routes.some((route) => route.path === "/api/auth/session/verify")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("config.auth can disable route groups", async () => {
|
test("config.auth can disable route groups", async () => {
|
||||||
const metadata = new Map<string, unknown>();
|
const metadata = new Map<string, unknown>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -415,7 +415,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleItem(state, itemId, button) {
|
function toggleItem(state, itemId, button) {
|
||||||
if (state.config.disabled || state.status !== "ready") return;
|
if (state.config.disabled || (state.status !== "ready" && state.status !== "incorrect")) return;
|
||||||
|
|
||||||
|
// An incorrect answer is still the same usable challenge until the server
|
||||||
|
// expires it or exhausts its attempts. Let the user amend the selection
|
||||||
|
// immediately and clear the stale error/locked control state.
|
||||||
|
if (state.status === "incorrect") setStatus(state, "ready", "");
|
||||||
|
|
||||||
var index = state.selections.indexOf(itemId);
|
var index = state.selections.indexOf(itemId);
|
||||||
var selected = index >= 0;
|
var selected = index >= 0;
|
||||||
@@ -867,10 +872,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFormSubmit(state, event) {
|
function passesSubmitGuard(state) {
|
||||||
if (!state.config.required || state.responseToken) return;
|
if (!state.config.required || state.responseToken) return true;
|
||||||
event.preventDefault();
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
setStatus(state, "incorrect", state.config.requiredMessage);
|
setStatus(state, "incorrect", state.config.requiredMessage);
|
||||||
emit(state, "failure", { code: "missing-input" });
|
emit(state, "failure", { code: "missing-input" });
|
||||||
var target =
|
var target =
|
||||||
@@ -878,6 +881,15 @@
|
|||||||
state.root.querySelector("[data-captcha-not-robot-button]") ||
|
state.root.querySelector("[data-captcha-not-robot-button]") ||
|
||||||
state.root.querySelector("[data-captcha-verify]");
|
state.root.querySelector("[data-captcha-verify]");
|
||||||
if (target && typeof target.focus === "function") target.focus();
|
if (target && typeof target.focus === "function") target.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormSubmit(state, event) {
|
||||||
|
// Schema-backed forms run this guard after their ordinary fields validate.
|
||||||
|
if (state.form && state.form.__wireValidateBound) return;
|
||||||
|
if (passesSubmitGuard(state)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialize(root) {
|
function initialize(root) {
|
||||||
@@ -963,6 +975,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (state.form) {
|
if (state.form) {
|
||||||
|
state.submitGuard = function () {
|
||||||
|
return passesSubmitGuard(state);
|
||||||
|
};
|
||||||
|
state.form.__wireSubmitGuards = state.form.__wireSubmitGuards || [];
|
||||||
|
state.form.__wireSubmitGuards.push(state.submitGuard);
|
||||||
state.formSubmitListener = function (event) {
|
state.formSubmitListener = function (event) {
|
||||||
onFormSubmit(state, event);
|
onFormSubmit(state, event);
|
||||||
};
|
};
|
||||||
@@ -1018,6 +1035,11 @@
|
|||||||
if (state.form && state.formSubmitListener) {
|
if (state.form && state.formSubmitListener) {
|
||||||
state.form.removeEventListener("submit", state.formSubmitListener, true);
|
state.form.removeEventListener("submit", state.formSubmitListener, true);
|
||||||
}
|
}
|
||||||
|
if (state.form && state.form.__wireSubmitGuards && state.submitGuard) {
|
||||||
|
state.form.__wireSubmitGuards = state.form.__wireSubmitGuards.filter(function (guard) {
|
||||||
|
return guard !== state.submitGuard;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (state.form && state.formSuccessListener) {
|
if (state.form && state.formSuccessListener) {
|
||||||
state.form.removeEventListener("wire:success", state.formSuccessListener);
|
state.form.removeEventListener("wire:success", state.formSuccessListener);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ component Captcha {
|
|||||||
required = true
|
required = true
|
||||||
resetOnError = true
|
resetOnError = true
|
||||||
compact = false
|
compact = false
|
||||||
|
layout = "vertical"
|
||||||
|
|
||||||
@event ready = function
|
@event ready = function
|
||||||
@event challenge = function
|
@event challenge = function
|
||||||
@@ -90,6 +91,7 @@ component Captcha {
|
|||||||
data-captcha-required='{required}'
|
data-captcha-required='{required}'
|
||||||
data-captcha-reset-on-error='{resetOnError}'
|
data-captcha-reset-on-error='{resetOnError}'
|
||||||
data-captcha-compact='{compact}'
|
data-captcha-compact='{compact}'
|
||||||
|
data-captcha-layout='{layout}'
|
||||||
data-captcha-required-message='{requiredMessage}'
|
data-captcha-required-message='{requiredMessage}'
|
||||||
data-captcha-incorrect-message='{incorrectMessage}'
|
data-captcha-incorrect-message='{incorrectMessage}'
|
||||||
data-captcha-expired-message='{expiredMessage}'
|
data-captcha-expired-message='{expiredMessage}'
|
||||||
@@ -97,9 +99,9 @@ component Captcha {
|
|||||||
data-captcha-status="idle"
|
data-captcha-status="idle"
|
||||||
data-server-verification-required="true"
|
data-server-verification-required="true"
|
||||||
aria-busy="false"
|
aria-busy="false"
|
||||||
class='group/captcha relative flex w-full max-w-lg flex-col gap-3 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-4 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] [--captcha-accent:var(--wire-color-primary)] data-[captcha-color=secondary]:[--captcha-accent:var(--wire-color-secondary)] data-[captcha-color=success]:[--captcha-accent:var(--wire-color-success)] data-[captcha-color=warning]:[--captcha-accent:var(--wire-color-warning)] data-[captcha-color=danger]:[--captcha-accent:var(--wire-color-danger)] data-[captcha-color=info]:[--captcha-accent:var(--wire-color-info)] data-[captcha-size=compact]:max-w-xs data-[captcha-size=compact]:gap-1.5 data-[captcha-size=compact]:p-2 data-[captcha-size=big]:max-w-2xl data-[captcha-size=big]:gap-4 data-[captcha-size=big]:p-5 data-[captcha-compact=true]:max-w-xs data-[captcha-compact=true]:gap-1.5 data-[captcha-compact=true]:p-2 data-[captcha-type=not-robot]:max-w-sm data-[captcha-type=not-robot]:gap-0 data-[captcha-type=not-robot]:p-0 data-[captcha-disabled=true]:pointer-events-none data-[captcha-disabled=true]:opacity-60 data-[captcha-status=verified]:border-[var(--wire-color-success)] data-[captcha-status=incorrect]:border-[var(--wire-color-danger)] data-[captcha-status=expired]:border-[var(--wire-color-danger)] data-[captcha-status=network-error]:border-[var(--wire-color-danger)] data-[captcha-status=provider-error]:border-[var(--wire-color-danger)] {class}'
|
class='group/captcha relative flex w-full max-w-lg flex-col gap-3 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-4 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] [--captcha-accent:var(--wire-color-primary)] data-[captcha-color=secondary]:[--captcha-accent:var(--wire-color-secondary)] data-[captcha-color=success]:[--captcha-accent:var(--wire-color-success)] data-[captcha-color=warning]:[--captcha-accent:var(--wire-color-warning)] data-[captcha-color=danger]:[--captcha-accent:var(--wire-color-danger)] data-[captcha-color=info]:[--captcha-accent:var(--wire-color-info)] data-[captcha-size=compact]:max-w-xs data-[captcha-size=compact]:gap-1.5 data-[captcha-size=compact]:p-2 data-[captcha-size=big]:max-w-2xl data-[captcha-size=big]:gap-4 data-[captcha-size=big]:p-5 data-[captcha-compact=true]:max-w-xs data-[captcha-compact=true]:gap-1.5 data-[captcha-compact=true]:p-2 data-[captcha-layout=horizontal]:max-w-2xl data-[captcha-layout=horizontal]:grid data-[captcha-layout=horizontal]:grid-cols-1 sm:data-[captcha-layout=horizontal]:grid-cols-[minmax(0,1fr)_minmax(12rem,0.8fr)] sm:data-[captcha-layout=horizontal]:items-start data-[captcha-type=not-robot]:max-w-sm data-[captcha-type=not-robot]:gap-0 data-[captcha-type=not-robot]:p-0 data-[captcha-disabled=true]:pointer-events-none data-[captcha-disabled=true]:opacity-60 data-[captcha-status=verified]:border-[var(--wire-color-success)] data-[captcha-status=incorrect]:border-[var(--wire-color-danger)] data-[captcha-status=expired]:border-[var(--wire-color-danger)] data-[captcha-status=network-error]:border-[var(--wire-color-danger)] data-[captcha-status=provider-error]:border-[var(--wire-color-danger)] {class}'
|
||||||
>
|
>
|
||||||
<header data-captcha-header class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
<header data-captcha-header class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-layout=horizontal]/captcha:col-span-full group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||||
<div class='min-w-0'>
|
<div class='min-w-0'>
|
||||||
<div class='flex items-center gap-2 group-data-[captcha-size=compact]/captcha:gap-1.5'>
|
<div class='flex items-center gap-2 group-data-[captcha-size=compact]/captcha:gap-1.5'>
|
||||||
<span
|
<span
|
||||||
@@ -140,7 +142,7 @@ component Captcha {
|
|||||||
Loading challenge…
|
Loading challenge…
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-captcha-challenge hidden class='flex flex-col gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=big]/captcha:gap-4 group-data-[captcha-type=not-robot]/captcha:p-3'>
|
<div data-captcha-challenge hidden class='flex min-w-0 flex-col gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=big]/captcha:gap-4 group-data-[captcha-type=not-robot]/captcha:p-3'>
|
||||||
<div data-captcha-prompt-row class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-2'>
|
<div data-captcha-prompt-row class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-2'>
|
||||||
<p data-captcha-prompt class='m-0 text-sm font-medium text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4'></p>
|
<p data-captcha-prompt class='m-0 text-sm font-medium text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4'></p>
|
||||||
<span data-captcha-countdown hidden class='shrink-0 text-xs tabular-nums text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px]'></span>
|
<span data-captcha-countdown hidden class='shrink-0 text-xs tabular-nums text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px]'></span>
|
||||||
@@ -219,20 +221,6 @@ component Captcha {
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<label data-captcha-answer-wrap hidden class='flex flex-col gap-1.5 group-data-[captcha-size=compact]/captcha:gap-1'>
|
|
||||||
<span class='text-xs font-semibold text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[10px]'>Your answer</span>
|
|
||||||
<input
|
|
||||||
data-captcha-answer
|
|
||||||
type="text"
|
|
||||||
inputmode="text"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="characters"
|
|
||||||
spellcheck="false"
|
|
||||||
class='h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-3 text-base font-semibold tracking-[0.18em] text-[var(--wire-color-text)] outline-none transition-[border-color,box-shadow] placeholder:tracking-normal focus:border-[var(--captcha-accent)] focus:ring-2 focus:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:bg-[var(--wire-color-surface-2)] aria-[invalid=true]:border-[var(--wire-color-danger)] aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-[var(--wire-color-danger)] group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:px-2 group-data-[captcha-size=compact]/captcha:text-xs group-data-[captcha-size=compact]/captcha:tracking-[0.12em] group-data-[captcha-size=big]/captcha:h-12 group-data-[captcha-size=big]/captcha:px-4 group-data-[captcha-size=big]/captcha:text-lg'
|
|
||||||
placeholder="Enter the answer"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
data-captcha-honeypot
|
data-captcha-honeypot
|
||||||
type="text"
|
type="text"
|
||||||
@@ -245,6 +233,21 @@ component Captcha {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div data-captcha-response-panel class='flex min-w-0 flex-col gap-2 group-data-[captcha-layout=horizontal]/captcha:self-stretch'>
|
||||||
|
<label data-captcha-answer-wrap hidden class='flex min-w-0 flex-col gap-1.5 group-data-[captcha-size=compact]/captcha:gap-1'>
|
||||||
|
<span class='text-xs font-semibold text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[10px]'>Your answer</span>
|
||||||
|
<input
|
||||||
|
data-captcha-answer
|
||||||
|
type="text"
|
||||||
|
inputmode="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="characters"
|
||||||
|
spellcheck="false"
|
||||||
|
class='h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-3 text-base font-semibold tracking-[0.18em] text-[var(--wire-color-text)] outline-none transition-[border-color,box-shadow] placeholder:tracking-normal focus:border-[var(--captcha-accent)] focus:ring-2 focus:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:bg-[var(--wire-color-surface-2)] aria-[invalid=true]:border-[var(--wire-color-danger)] aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-[var(--wire-color-danger)] group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:px-2 group-data-[captcha-size=compact]/captcha:text-xs group-data-[captcha-size=compact]/captcha:tracking-[0.12em] group-data-[captcha-size=big]/captcha:h-12 group-data-[captcha-size=big]/captcha:px-4 group-data-[captcha-size=big]/captcha:text-lg'
|
||||||
|
placeholder="Enter the answer"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div data-captcha-success hidden role="status" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
<div data-captcha-success hidden role="status" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||||
<span aria-hidden="true" class='icon-[lucide--circle-check] mt-0.5 size-3.5 shrink-0'></span>
|
<span aria-hidden="true" class='icon-[lucide--circle-check] mt-0.5 size-3.5 shrink-0'></span>
|
||||||
<span data-captcha-success-message>Verification completed.</span>
|
<span data-captcha-success-message>Verification completed.</span>
|
||||||
@@ -312,6 +315,7 @@ component Captcha {
|
|||||||
<p data-captcha-credit class='m-0 text-[11px] leading-4 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-type=not-robot]/captcha:hidden'>
|
<p data-captcha-credit class='m-0 text-[11px] leading-4 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||||
Protected by WRNexus CAPTCHA. Server-side verification is required.
|
Protected by WRNexus CAPTCHA. Server-side verification is required.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"check": "bun run typecheck \u0026\u0026 bun run test"
|
"check": "bun run typecheck && bun run test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ const DEFAULT_TOKEN_TTL_MS = 300_000;
|
|||||||
const DEFAULT_MAX_ATTEMPTS = 3;
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
const DEFAULT_MIN_COMPLETION_MS = 800;
|
const DEFAULT_MIN_COMPLETION_MS = 800;
|
||||||
const DEFAULT_RESPONSE_FIELD = "wrn-captcha-response";
|
const DEFAULT_RESPONSE_FIELD = "wrn-captcha-response";
|
||||||
|
const RANDOM_CHALLENGE_TYPES: CaptchaChallengeType[] = [
|
||||||
|
"number",
|
||||||
|
"alpha",
|
||||||
|
"alphanumeric",
|
||||||
|
"calculation",
|
||||||
|
"image",
|
||||||
|
];
|
||||||
|
|
||||||
function failure(
|
function failure(
|
||||||
action: string,
|
action: string,
|
||||||
@@ -150,13 +157,22 @@ export class DefaultCaptchaEngine implements CaptchaEngine {
|
|||||||
this.randomBytes = options.randomBytes ?? defaultRandomBytes;
|
this.randomBytes = options.randomBytes ?? defaultRandomBytes;
|
||||||
for (const generator of options.generators ?? defaultCaptchaGenerators())
|
for (const generator of options.generators ?? defaultCaptchaGenerators())
|
||||||
this.generators.set(generator.type, generator);
|
this.generators.set(generator.type, generator);
|
||||||
if (!this.generators.has(this.defaultType))
|
if (this.defaultType !== "random" && !this.generators.has(this.defaultType))
|
||||||
throw new Error(`No CAPTCHA generator registered for ${this.defaultType}`);
|
throw new Error(`No CAPTCHA generator registered for ${this.defaultType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(options: CreateCaptchaOptions): Promise<CaptchaChallenge> {
|
async create(options: CreateCaptchaOptions): Promise<CaptchaChallenge> {
|
||||||
const action = assertAction(options.action);
|
const action = assertAction(options.action);
|
||||||
const requestedType = options.type ?? this.defaultType;
|
const configuredType = options.type ?? this.defaultType;
|
||||||
|
const randomInt = (min: number, max: number): number =>
|
||||||
|
randomInteger(this.randomBytes, min, max);
|
||||||
|
const randomTypes = RANDOM_CHALLENGE_TYPES.filter((type) => this.generators.has(type));
|
||||||
|
if (configuredType === "random" && randomTypes.length === 0)
|
||||||
|
throw new Error("No interactive CAPTCHA generators are registered");
|
||||||
|
const requestedType =
|
||||||
|
configuredType === "random"
|
||||||
|
? randomTypes[randomInt(0, randomTypes.length - 1)]
|
||||||
|
: configuredType;
|
||||||
const requestedPresentation =
|
const requestedPresentation =
|
||||||
options.presentation ??
|
options.presentation ??
|
||||||
(requestedType === "honeypot" || requestedType === "timing" || requestedType === "not-robot"
|
(requestedType === "honeypot" || requestedType === "timing" || requestedType === "not-robot"
|
||||||
@@ -169,8 +185,6 @@ export class DefaultCaptchaEngine implements CaptchaEngine {
|
|||||||
const now = this.now();
|
const now = this.now();
|
||||||
const difficulty = options.difficulty ?? this.defaultDifficulty;
|
const difficulty = options.difficulty ?? this.defaultDifficulty;
|
||||||
const disturbance = normalizeDisturbance(options.disturbance, difficulty);
|
const disturbance = normalizeDisturbance(options.disturbance, difficulty);
|
||||||
const randomInt = (min: number, max: number): number =>
|
|
||||||
randomInteger(this.randomBytes, min, max);
|
|
||||||
const imageStyle = resolveCaptchaImageStyle({
|
const imageStyle = resolveCaptchaImageStyle({
|
||||||
imageStyle: options.imageStyle,
|
imageStyle: options.imageStyle,
|
||||||
allowedStyles: options.allowedStyles,
|
allowedStyles: options.allowedStyles,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type CaptchaProviderName =
|
|||||||
"self-hosted" | "wrnexus-managed" | "turnstile" | "recaptcha" | "hcaptcha" | (string & {});
|
"self-hosted" | "wrnexus-managed" | "turnstile" | "recaptcha" | "hcaptcha" | (string & {});
|
||||||
|
|
||||||
export type CaptchaChallengeType =
|
export type CaptchaChallengeType =
|
||||||
|
| "random"
|
||||||
| "number"
|
| "number"
|
||||||
| "alpha"
|
| "alpha"
|
||||||
| "alphanumeric"
|
| "alphanumeric"
|
||||||
|
|||||||
@@ -23,9 +23,14 @@ test("Captcha browser runtime is valid JavaScript and exposes the expected lifec
|
|||||||
expect(source).toContain("normalizeSize");
|
expect(source).toContain("normalizeSize");
|
||||||
expect(source).toContain("verifyNotRobot");
|
expect(source).toContain("verifyNotRobot");
|
||||||
expect(source).toContain("stopImmediatePropagation");
|
expect(source).toContain("stopImmediatePropagation");
|
||||||
|
expect(source).toContain("__wireSubmitGuards");
|
||||||
|
expect(source).toContain("__wireValidateBound");
|
||||||
|
expect(source).toContain("passesSubmitGuard");
|
||||||
expect(source).toContain('addEventListener("submit"');
|
expect(source).toContain('addEventListener("submit"');
|
||||||
expect(source).toContain('addEventListener("wire:error"');
|
expect(source).toContain('addEventListener("wire:error"');
|
||||||
expect(source).toContain("state.config.resetOnError || captchaFailed");
|
expect(source).toContain("state.config.resetOnError || captchaFailed");
|
||||||
|
expect(source).toContain('state.status !== "ready" && state.status !== "incorrect"');
|
||||||
|
expect(source).toContain('state.status === "incorrect") setStatus(state, "ready", "")');
|
||||||
expect(source).toContain("Verified response tokens are single-use");
|
expect(source).toContain("Verified response tokens are single-use");
|
||||||
expect(source).toContain("MutationObserver");
|
expect(source).toContain("MutationObserver");
|
||||||
expect(source).toContain("new CustomEvent(name");
|
expect(source).toContain("new CustomEvent(name");
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ function fixture() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("self-hosted CAPTCHA engine", () => {
|
describe("self-hosted CAPTCHA engine", () => {
|
||||||
|
test("random challenges resolve to an interactive challenge type", async () => {
|
||||||
|
const engine = createCaptchaEngine({
|
||||||
|
secret: "captcha-test-secret-with-at-least-thirty-two-characters",
|
||||||
|
store: new MemoryCaptchaStore(),
|
||||||
|
defaultType: "random",
|
||||||
|
minCompletionMs: 0,
|
||||||
|
});
|
||||||
|
const supported = new Set(["number", "alpha", "alphanumeric", "calculation", "image"]);
|
||||||
|
|
||||||
|
for (let index = 0; index < 12; index += 1) {
|
||||||
|
const challenge = await engine.create({ action: "random-check" });
|
||||||
|
expect(supported.has(challenge.type)).toBe(true);
|
||||||
|
expect(challenge.type).not.toBe("random");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("creates, solves, and consumes a challenge and response token", async () => {
|
test("creates, solves, and consumes a challenge and response token", async () => {
|
||||||
const { engine } = fixture();
|
const { engine } = fixture();
|
||||||
const challenge = await engine.create({
|
const challenge = await engine.create({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ await createProductionServer(
|
|||||||
security: ${JSON.stringify(config.security ?? {})},
|
security: ${JSON.stringify(config.security ?? {})},
|
||||||
observability: ${JSON.stringify(config.observability ?? {})},
|
observability: ${JSON.stringify(config.observability ?? {})},
|
||||||
tenancy: ${JSON.stringify(config.tenancy ?? {})},
|
tenancy: ${JSON.stringify(config.tenancy ?? {})},
|
||||||
|
navigation: ${JSON.stringify(config.navigation ?? {})},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -263,16 +263,14 @@ const config: AppConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fonts — the framework emits optimized <head> markup (preconnect, subsetted
|
// Fonts — optimized preconnect, subsetted weights, font-display, and CSP.
|
||||||
// Google Fonts with font-display, self-hosted @font-face with preload) and
|
fonts: {
|
||||||
// auto-extends the CSP for Google Fonts. Uncomment to use a custom font:
|
sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
google: [{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700] }],
|
||||||
|
},
|
||||||
//
|
//
|
||||||
// fonts: {
|
|
||||||
// sans: '"Inter", ui-sans-serif, system-ui, sans-serif',
|
|
||||||
// google: [{ family: "Inter", weights: [400, 500, 600, 700] }],
|
|
||||||
// // Or self-host (fastest, no third party) — drop files in public/fonts/:
|
// // Or self-host (fastest, no third party) — drop files in public/fonts/:
|
||||||
// // local: [{ family: "Inter", src: "/fonts/inter.woff2", weight: "100 900", preload: true }],
|
// // local: [{ family: "Inter", src: "/fonts/inter.woff2", weight: "100 900", preload: true }],
|
||||||
// },
|
|
||||||
|
|
||||||
// security: {
|
// security: {
|
||||||
// cors: {
|
// cors: {
|
||||||
@@ -304,13 +302,7 @@ Allow: /
|
|||||||
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family: var(--wire-font-sans, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif);
|
||||||
ui-sans-serif,
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
"Segoe UI",
|
|
||||||
Roboto,
|
|
||||||
sans-serif;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
|
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
|
||||||
|
|||||||
@@ -1103,6 +1103,32 @@ const MIGRATIONS: Migration[] = [
|
|||||||
// Syntax and compiler support are package-managed; existing applications remain compatible.
|
// Syntax and compiler support are package-managed; existing applications remain compatible.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: "0.5.2",
|
||||||
|
id: "multiline-wrn-json-and-stable-gateway-runtime",
|
||||||
|
description:
|
||||||
|
"Supports formatted multiline array and object state initializers and keeps gateway children on the matching framework runtime.",
|
||||||
|
apply() {
|
||||||
|
// Syntax, compiler, and gateway fixes are package-managed; no application rewrite is required.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.5.3",
|
||||||
|
id: "public-workspace-origins-and-schema-backed-auth-forms",
|
||||||
|
description:
|
||||||
|
"Keeps cross-app redirects on configured public origins and enables schema-backed auth forms with active navigation states.",
|
||||||
|
apply() {
|
||||||
|
// Gateway, helper, validation, and UI improvements are package-managed.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.5.10",
|
||||||
|
id: "latest-release-after-upgrade",
|
||||||
|
description: "Latest New Packages and release ready packages",
|
||||||
|
apply() {
|
||||||
|
// Gateway, helper, validation, and UI improvements are package-managed.
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||||
|
|||||||
@@ -69,9 +69,13 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", ()
|
|||||||
expect(pkg.devDependencies["@wrnexus/cli"]).toBe(version);
|
expect(pkg.devDependencies["@wrnexus/cli"]).toBe(version);
|
||||||
expect(pkg.devDependencies["@iconify/tailwind4"]).toBe("^1.2.3");
|
expect(pkg.devDependencies["@iconify/tailwind4"]).toBe("^1.2.3");
|
||||||
expect(pkg.devDependencies["@iconify-json/lucide"]).toBe("^1.2.118");
|
expect(pkg.devDependencies["@iconify-json/lucide"]).toBe("^1.2.118");
|
||||||
expect(readFileSync(join(root, "app", "styles", "global.css"), "utf8")).toContain(
|
const globalCss = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
||||||
'@plugin "@iconify/tailwind4";',
|
expect(globalCss).toContain('@plugin "@iconify/tailwind4";');
|
||||||
);
|
expect(globalCss).toContain('"Plus Jakarta Sans"');
|
||||||
|
expect(globalCss).toContain("--wire-font-sans");
|
||||||
|
const appConfig = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||||
|
expect(appConfig).toContain('family: "Plus Jakarta Sans"');
|
||||||
|
expect(appConfig).toContain("weights: [400, 500, 600, 700]");
|
||||||
expect(existsSync(join(root, "public", "llms.txt"))).toBe(true);
|
expect(existsSync(join(root, "public", "llms.txt"))).toBe(true);
|
||||||
expect(existsSync(join(root, "llms.txt"))).toBe(false);
|
expect(existsSync(join(root, "llms.txt"))).toBe(false);
|
||||||
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md");
|
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1437,6 +1437,10 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExplicitComponentMount = node.attrs.some(
|
||||||
|
(attribute) => attribute.name === "data-component",
|
||||||
|
);
|
||||||
|
|
||||||
const attrs = node.attrs
|
const attrs = node.attrs
|
||||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
@@ -1476,7 +1480,12 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
|||||||
if (a.value === "true" || a.value === "") return ` ${a.name}`;
|
if (a.value === "true" || a.value === "") return ` ${a.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
|
const wholeExpression = wholeAttributeExpression(a.value);
|
||||||
|
const compiledValue =
|
||||||
|
isExplicitComponentMount && wholeExpression
|
||||||
|
? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||||
|
: compileAttrValue(a.value, elementContext);
|
||||||
|
const rendered = ` ${a.name}="${compiledValue}"`;
|
||||||
|
|
||||||
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,20 @@ test("component event declarations compile to public root metadata", async () =>
|
|||||||
expect(render({})).toContain('data-wrn-events="complete"');
|
expect(render({})).toContain('data-wrn-events="complete"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("explicit nested component mounts serialize structured expression props", () => {
|
||||||
|
const output = compileWireFile(`component Shell {
|
||||||
|
state brand = { "label": "WRNexus" }
|
||||||
|
state items = [{ "label": "Home", "href": "/" }]
|
||||||
|
view {
|
||||||
|
<div data-component="Navbar" brand={brand} items={items}></div>
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
expect(output).toContain('data-component="Navbar"');
|
||||||
|
expect(output).toContain('brand="${__wireProp(brand)}"');
|
||||||
|
expect(output).toContain('items="${__wireProp(items)}"');
|
||||||
|
});
|
||||||
|
|
||||||
test("typed props, required props, state, custom types, and function parameters compile", () => {
|
test("typed props, required props, state, custom types, and function parameters compile", () => {
|
||||||
const source = `component TypedPicker {
|
const source = `component TypedPicker {
|
||||||
types {
|
types {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -2011,10 +2011,234 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureBehaviorObserver();
|
ensureBehaviorObserver();
|
||||||
|
hydrateNavbarControllers(host);
|
||||||
|
hydratePreferenceControllers(host);
|
||||||
|
hydrateSidebarControllers(host);
|
||||||
|
hydrateDropdownControllers(host);
|
||||||
hydrateSelectControllers(host);
|
hydrateSelectControllers(host);
|
||||||
hydratePinInputControllers(host);
|
hydratePinInputControllers(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var navbarOutsideClickBound = false;
|
||||||
|
var preferenceOutsideClickBound = false;
|
||||||
|
var dropdownOutsideClickBound = false;
|
||||||
|
|
||||||
|
function hydrateSidebarControllers(root) {
|
||||||
|
var host = root || document;
|
||||||
|
var sidebars = [];
|
||||||
|
if (host.nodeType === 1 && host.matches && host.matches(".wire-sidebar-shell")) {
|
||||||
|
sidebars.push(host);
|
||||||
|
}
|
||||||
|
if (host.querySelectorAll) {
|
||||||
|
Array.prototype.push.apply(sidebars, host.querySelectorAll(".wire-sidebar-shell"));
|
||||||
|
}
|
||||||
|
sidebars.forEach(function (sidebar) {
|
||||||
|
var current = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||||
|
var best = null;
|
||||||
|
var bestLength = -1;
|
||||||
|
sidebar.querySelectorAll(".wire-sidebar-items a[href]").forEach(function (link) {
|
||||||
|
var url;
|
||||||
|
try { url = new URL(link.getAttribute("href"), window.location.origin); } catch (_) { return; }
|
||||||
|
var path = url.pathname.replace(/\/+$/, "") || "/";
|
||||||
|
var matches = path === current || (path !== "/" && current.indexOf(path + "/") === 0);
|
||||||
|
link.classList.remove("is-active");
|
||||||
|
if (!matches || path.length <= bestLength) return;
|
||||||
|
best = link;
|
||||||
|
bestLength = path.length;
|
||||||
|
});
|
||||||
|
sidebar.querySelectorAll(".wire-sidebar-items a[aria-current='page']").forEach(function (link) {
|
||||||
|
if (link !== best) link.removeAttribute("aria-current");
|
||||||
|
});
|
||||||
|
sidebar.querySelectorAll(".wire-sidebar-group.has-active").forEach(function (group) {
|
||||||
|
group.classList.remove("has-active");
|
||||||
|
});
|
||||||
|
if (best) {
|
||||||
|
best.classList.add("is-active");
|
||||||
|
best.setAttribute("aria-current", "page");
|
||||||
|
var group = best.closest(".wire-sidebar-group");
|
||||||
|
if (group) {
|
||||||
|
group.setAttribute("open", "");
|
||||||
|
group.classList.add("has-active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateDropdownControllers(root) {
|
||||||
|
var host = root || document;
|
||||||
|
var dropdowns = [];
|
||||||
|
if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-dropdown]")) {
|
||||||
|
dropdowns.push(host);
|
||||||
|
}
|
||||||
|
if (host.querySelectorAll) {
|
||||||
|
Array.prototype.push.apply(dropdowns, host.querySelectorAll("[data-wrn-dropdown]"));
|
||||||
|
}
|
||||||
|
dropdowns.forEach(function (dropdown) {
|
||||||
|
if (dropdown.dataset.wrnDropdownBound === "true") return;
|
||||||
|
dropdown.dataset.wrnDropdownBound = "true";
|
||||||
|
dropdown.addEventListener("toggle", function () {
|
||||||
|
if (!dropdown.open) return;
|
||||||
|
document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (candidate) {
|
||||||
|
if (candidate !== dropdown) candidate.removeAttribute("open");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!dropdownOutsideClickBound) {
|
||||||
|
dropdownOutsideClickBound = true;
|
||||||
|
document.addEventListener("pointerdown", function (event) {
|
||||||
|
document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (dropdown) {
|
||||||
|
if (!dropdown.contains(event.target)) dropdown.removeAttribute("open");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key !== "Escape") return;
|
||||||
|
document.querySelectorAll("[data-wrn-dropdown][open]").forEach(function (dropdown) {
|
||||||
|
dropdown.removeAttribute("open");
|
||||||
|
var summary = dropdown.querySelector(":scope > summary");
|
||||||
|
if (summary) summary.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydratePreferenceControllers(root) {
|
||||||
|
var host = root || document;
|
||||||
|
var switchers = [];
|
||||||
|
if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-preferences]")) {
|
||||||
|
switchers.push(host);
|
||||||
|
}
|
||||||
|
if (host.querySelectorAll) {
|
||||||
|
Array.prototype.push.apply(
|
||||||
|
switchers,
|
||||||
|
host.querySelectorAll("[data-wrn-preferences]"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
switchers.forEach(function (switcher) {
|
||||||
|
if (switcher.dataset.wrnPreferencesBound === "true") return;
|
||||||
|
switcher.dataset.wrnPreferencesBound = "true";
|
||||||
|
switcher.addEventListener("toggle", function (event) {
|
||||||
|
var opened = event.target;
|
||||||
|
if (!opened || opened.tagName !== "DETAILS" || !opened.open) return;
|
||||||
|
switcher.querySelectorAll(".wire-preferences__menu[open]").forEach(function (menu) {
|
||||||
|
if (menu !== opened) menu.removeAttribute("open");
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
if (!preferenceOutsideClickBound) {
|
||||||
|
preferenceOutsideClickBound = true;
|
||||||
|
document.addEventListener("pointerdown", function (event) {
|
||||||
|
document.querySelectorAll("[data-wrn-preferences]").forEach(function (switcher) {
|
||||||
|
if (switcher.contains(event.target)) return;
|
||||||
|
switcher.querySelectorAll(".wire-preferences__menu[open]").forEach(function (menu) {
|
||||||
|
menu.removeAttribute("open");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key !== "Escape") return;
|
||||||
|
document.querySelectorAll("[data-wrn-preferences] .wire-preferences__menu[open]").forEach(function (menu) {
|
||||||
|
menu.removeAttribute("open");
|
||||||
|
var summary = menu.querySelector(":scope > summary");
|
||||||
|
if (summary) summary.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateNavbarControllers(root) {
|
||||||
|
var host = root || document;
|
||||||
|
var navbars = [];
|
||||||
|
if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-navbar]")) {
|
||||||
|
navbars.push(host);
|
||||||
|
}
|
||||||
|
if (host.querySelectorAll) {
|
||||||
|
Array.prototype.push.apply(
|
||||||
|
navbars,
|
||||||
|
host.querySelectorAll("[data-wrn-navbar]"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
navbars.forEach(function (navbar) {
|
||||||
|
var current = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||||
|
var best = null;
|
||||||
|
var bestLength = -1;
|
||||||
|
navbar.querySelectorAll(".wire-navbar__menus a[href]").forEach(function (link) {
|
||||||
|
var url;
|
||||||
|
try { url = new URL(link.getAttribute("href"), window.location.origin); } catch (_) { return; }
|
||||||
|
if (url.origin !== window.location.origin) return;
|
||||||
|
var path = url.pathname.replace(/\/+$/, "") || "/";
|
||||||
|
var matches = path === current || (path !== "/" && current.indexOf(path + "/") === 0);
|
||||||
|
if (!matches || path.length <= bestLength) return;
|
||||||
|
best = link;
|
||||||
|
bestLength = path.length;
|
||||||
|
});
|
||||||
|
navbar.querySelectorAll(".wire-navbar__menus [aria-current='page']").forEach(function (item) {
|
||||||
|
item.removeAttribute("aria-current");
|
||||||
|
});
|
||||||
|
if (best) {
|
||||||
|
best.setAttribute("aria-current", "page");
|
||||||
|
var parentMenu = best.closest(".wire-navbar__dropdown");
|
||||||
|
var parentSummary = parentMenu && parentMenu.querySelector(":scope > summary");
|
||||||
|
if (parentSummary) parentSummary.setAttribute("aria-current", "page");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navbar.dataset.wrnNavbarBound === "true") return;
|
||||||
|
navbar.dataset.wrnNavbarBound = "true";
|
||||||
|
var hoverEnabled = navbar.getAttribute("data-open-on-hover") === "true";
|
||||||
|
var hoverCapable =
|
||||||
|
typeof window.matchMedia !== "function" ||
|
||||||
|
window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
||||||
|
if (hoverEnabled && hoverCapable) {
|
||||||
|
navbar.querySelectorAll(".wire-navbar__dropdown").forEach(function (menu) {
|
||||||
|
menu.addEventListener("pointerenter", function () {
|
||||||
|
if (menu.__wrnNavbarCloseTimer) {
|
||||||
|
clearTimeout(menu.__wrnNavbarCloseTimer);
|
||||||
|
menu.__wrnNavbarCloseTimer = null;
|
||||||
|
}
|
||||||
|
navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (candidate) {
|
||||||
|
if (candidate !== menu) candidate.removeAttribute("open");
|
||||||
|
});
|
||||||
|
menu.setAttribute("open", "");
|
||||||
|
});
|
||||||
|
menu.addEventListener("pointerleave", function () {
|
||||||
|
if (menu.__wrnNavbarCloseTimer) clearTimeout(menu.__wrnNavbarCloseTimer);
|
||||||
|
menu.__wrnNavbarCloseTimer = setTimeout(function () {
|
||||||
|
menu.removeAttribute("open");
|
||||||
|
menu.__wrnNavbarCloseTimer = null;
|
||||||
|
}, 240);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
navbar.addEventListener("toggle", function (event) {
|
||||||
|
var opened = event.target;
|
||||||
|
if (!opened || opened.tagName !== "DETAILS" || !opened.open) return;
|
||||||
|
navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (menu) {
|
||||||
|
if (menu !== opened) menu.removeAttribute("open");
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!navbarOutsideClickBound) {
|
||||||
|
navbarOutsideClickBound = true;
|
||||||
|
document.addEventListener("pointerdown", function (event) {
|
||||||
|
document.querySelectorAll("[data-wrn-navbar]").forEach(function (navbar) {
|
||||||
|
if (navbar.contains(event.target)) return;
|
||||||
|
navbar.querySelectorAll(".wire-navbar__dropdown[open]").forEach(function (menu) {
|
||||||
|
menu.removeAttribute("open");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key !== "Escape") return;
|
||||||
|
document.querySelectorAll("[data-wrn-navbar] .wire-navbar__dropdown[open]").forEach(function (menu) {
|
||||||
|
menu.removeAttribute("open");
|
||||||
|
var summary = menu.querySelector(":scope > summary");
|
||||||
|
if (summary) summary.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var selectOutsideClickBound = false;
|
var selectOutsideClickBound = false;
|
||||||
|
|
||||||
function selectScopeApi(root) {
|
function selectScopeApi(root) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -23,14 +23,21 @@ interface ExecMeta {
|
|||||||
insertId?: number | bigint;
|
insertId?: number | bigint;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runnerFor(client: BunSqlClient): TxHandle {
|
/** Convert the framework's portable positional placeholders for PostgreSQL. */
|
||||||
|
export function sqlForDialect(sql: string, dialect: Dialect): string {
|
||||||
|
if (dialect !== "postgres" || !sql.includes("?")) return sql;
|
||||||
|
let index = 0;
|
||||||
|
return sql.replace(/\?/g, () => `$${++index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runnerFor(client: BunSqlClient, dialect: Dialect): TxHandle {
|
||||||
return {
|
return {
|
||||||
async query(sql, params = []): Promise<Row[]> {
|
async query(sql, params = []): Promise<Row[]> {
|
||||||
const rows = (await client.unsafe(sql, params)) as Iterable<Row>;
|
const rows = (await client.unsafe(sqlForDialect(sql, dialect), params)) as Iterable<Row>;
|
||||||
return Array.from(rows);
|
return Array.from(rows);
|
||||||
},
|
},
|
||||||
async exec(sql, params = []) {
|
async exec(sql, params = []) {
|
||||||
const meta = (await client.unsafe(sql, params)) as ExecMeta;
|
const meta = (await client.unsafe(sqlForDialect(sql, dialect), params)) as ExecMeta;
|
||||||
const id = meta.lastInsertRowid ?? meta.insertId;
|
const id = meta.lastInsertRowid ?? meta.insertId;
|
||||||
return {
|
return {
|
||||||
changes: Number(meta.affectedRows ?? meta.count ?? 0),
|
changes: Number(meta.affectedRows ?? meta.count ?? 0),
|
||||||
@@ -44,13 +51,13 @@ function runnerFor(client: BunSqlClient): TxHandle {
|
|||||||
export function bunSql(url: string, dialect: Dialect): Driver {
|
export function bunSql(url: string, dialect: Dialect): Driver {
|
||||||
const Ctor = (Bun as unknown as { SQL: new (u: string) => BunSqlClient }).SQL;
|
const Ctor = (Bun as unknown as { SQL: new (u: string) => BunSqlClient }).SQL;
|
||||||
const client = new Ctor(url);
|
const client = new Ctor(url);
|
||||||
const runner = runnerFor(client);
|
const runner = runnerFor(client, dialect);
|
||||||
return {
|
return {
|
||||||
dialect,
|
dialect,
|
||||||
query: runner.query,
|
query: runner.query,
|
||||||
exec: runner.exec,
|
exec: runner.exec,
|
||||||
transaction(fn) {
|
transaction(fn) {
|
||||||
return client.begin((tx) => fn(runnerFor(tx)));
|
return client.begin((tx) => fn(runnerFor(tx, dialect)));
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
return client.close();
|
return client.close();
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
import type { Db } from "./driver.ts";
|
import type { Db } from "./driver.ts";
|
||||||
|
|
||||||
const DEFAULT = "default";
|
const DEFAULT = "default";
|
||||||
const registry = new Map<string, Db>();
|
type DbFactory = () => Db;
|
||||||
|
type RegistryEntry = { db?: Db; factory?: DbFactory };
|
||||||
|
const registry = new Map<string, RegistryEntry>();
|
||||||
|
|
||||||
/** Set the default database (called by the runtime at startup). */
|
/** Set the default database (called by the runtime at startup). */
|
||||||
export function setDb(db: Db): Db;
|
export function setDb(db: Db): Db;
|
||||||
@@ -21,7 +23,7 @@ export function setDb(name: string, db: Db): Db;
|
|||||||
export function setDb(a: string | Db, b?: Db): Db {
|
export function setDb(a: string | Db, b?: Db): Db {
|
||||||
const name = typeof a === "string" ? a : DEFAULT;
|
const name = typeof a === "string" ? a : DEFAULT;
|
||||||
const db = typeof a === "string" ? b! : a;
|
const db = typeof a === "string" ? b! : a;
|
||||||
registry.set(name, db);
|
registry.set(name, { db });
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +32,18 @@ export function registerDb(name: string, db: Db): Db {
|
|||||||
return setDb(name, db);
|
return setDb(name, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a named database without opening its connection pool. The first
|
||||||
|
* `getDb(name)` call creates and caches the connection.
|
||||||
|
*/
|
||||||
|
export function registerLazyDb(name: string, factory: DbFactory): void {
|
||||||
|
registry.set(name, { factory });
|
||||||
|
}
|
||||||
|
|
||||||
/** The default database, or a named one. Throws if it isn't configured. */
|
/** The default database, or a named one. Throws if it isn't configured. */
|
||||||
export function getDb(name = DEFAULT): Db {
|
export function getDb(name = DEFAULT): Db {
|
||||||
const db = registry.get(name);
|
const entry = registry.get(name);
|
||||||
if (!db) {
|
if (!entry) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
name === DEFAULT
|
name === DEFAULT
|
||||||
? "No database configured. Add `db: { driver, url }` to wrnexus.config.ts."
|
? "No database configured. Add `db: { driver, url }` to wrnexus.config.ts."
|
||||||
@@ -41,7 +51,11 @@ export function getDb(name = DEFAULT): Db {
|
|||||||
`(e.g. databases: { ${name}: { driver, url } }).`,
|
`(e.g. databases: { ${name}: { driver, url } }).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return db;
|
if (!entry.db) {
|
||||||
|
if (!entry.factory) throw new Error(`Database '${name}' has no connection factory.`);
|
||||||
|
entry.db = entry.factory();
|
||||||
|
}
|
||||||
|
return entry.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the default (or a named) database has been configured. */
|
/** Whether the default (or a named) database has been configured. */
|
||||||
@@ -56,6 +70,8 @@ export function databaseNames(): string[] {
|
|||||||
|
|
||||||
/** Close every configured database and clear the registry. */
|
/** Close every configured database and clear the registry. */
|
||||||
export async function closeDatabases(): Promise<void> {
|
export async function closeDatabases(): Promise<void> {
|
||||||
for (const db of registry.values()) await db.close();
|
for (const entry of registry.values()) {
|
||||||
|
if (entry.db) await entry.db.close();
|
||||||
|
}
|
||||||
registry.clear();
|
registry.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ export { v, table, Column } from "./schema.ts";
|
|||||||
export type { Model, Columns, ColumnDef, BaseType } from "./schema.ts";
|
export type { Model, Columns, ColumnDef, BaseType } from "./schema.ts";
|
||||||
export { createDb } from "./driver.ts";
|
export { createDb } from "./driver.ts";
|
||||||
export type { Db, Driver, Row, ExecResult, TxHandle } from "./driver.ts";
|
export type { Db, Driver, Row, ExecResult, TxHandle } from "./driver.ts";
|
||||||
export { setDb, getDb, hasDb, registerDb, databaseNames, closeDatabases } from "./client.ts";
|
export {
|
||||||
|
setDb,
|
||||||
|
getDb,
|
||||||
|
hasDb,
|
||||||
|
registerDb,
|
||||||
|
registerLazyDb,
|
||||||
|
databaseNames,
|
||||||
|
closeDatabases,
|
||||||
|
} from "./client.ts";
|
||||||
export { createTableSql } from "./sql.ts";
|
export { createTableSql } from "./sql.ts";
|
||||||
export type { Dialect } from "./sql.ts";
|
export type { Dialect } from "./sql.ts";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export async function appliedMigrations(db: Db): Promise<string[]> {
|
|||||||
|
|
||||||
/** Apply an ordered migration list (each in a transaction). Returns applied names. */
|
/** Apply an ordered migration list (each in a transaction). Returns applied names. */
|
||||||
export async function applyMigrations(db: Db, migrations: readonly Migration[]): Promise<string[]> {
|
export async function applyMigrations(db: Db, migrations: readonly Migration[]): Promise<string[]> {
|
||||||
|
if (migrations.length === 0) return [];
|
||||||
const applied = new Set(await appliedMigrations(db));
|
const applied = new Set(await appliedMigrations(db));
|
||||||
const pending = migrations.filter((migration) => !applied.has(migration.name));
|
const pending = migrations.filter((migration) => !applied.has(migration.name));
|
||||||
const done: string[] = [];
|
const done: string[] = [];
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { sqlForDialect } from "../src/adapters/bunsql.ts";
|
||||||
|
|
||||||
|
test("PostgreSQL converts portable placeholders to numbered parameters", () => {
|
||||||
|
expect(sqlForDialect("SELECT * FROM users WHERE id = ? AND status = ?", "postgres")).toBe(
|
||||||
|
"SELECT * FROM users WHERE id = $1 AND status = $2",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("MySQL and parameter-free PostgreSQL statements remain unchanged", () => {
|
||||||
|
expect(sqlForDialect("SELECT * FROM users WHERE id = ?", "mysql")).toBe(
|
||||||
|
"SELECT * FROM users WHERE id = ?",
|
||||||
|
);
|
||||||
|
expect(sqlForDialect("CREATE TABLE users (id TEXT)", "postgres")).toBe(
|
||||||
|
"CREATE TABLE users (id TEXT)",
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -8,12 +8,14 @@ import {
|
|||||||
createDb,
|
createDb,
|
||||||
createTableSql,
|
createTableSql,
|
||||||
parseMigration,
|
parseMigration,
|
||||||
|
applyMigrations,
|
||||||
migrate,
|
migrate,
|
||||||
status,
|
status,
|
||||||
rollback,
|
rollback,
|
||||||
parseQueries,
|
parseQueries,
|
||||||
generateQueriesFile,
|
generateQueriesFile,
|
||||||
} from "../src/index.ts";
|
} from "../src/index.ts";
|
||||||
|
import type { Db } from "../src/index.ts";
|
||||||
import { sqlite } from "../src/adapters/sqlite.ts";
|
import { sqlite } from "../src/adapters/sqlite.ts";
|
||||||
import { bunSql } from "../src/adapters/bunsql.ts";
|
import { bunSql } from "../src/adapters/bunsql.ts";
|
||||||
|
|
||||||
@@ -95,6 +97,20 @@ test("migration runner: parse, migrate, status, rollback", async () => {
|
|||||||
db.close();
|
db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("an empty migration set does not touch the database", async () => {
|
||||||
|
const unreachable = async () => {
|
||||||
|
throw new Error("database should remain lazy");
|
||||||
|
};
|
||||||
|
const db = {
|
||||||
|
all: unreachable,
|
||||||
|
one: unreachable,
|
||||||
|
exec: unreachable,
|
||||||
|
tx: unreachable,
|
||||||
|
} as unknown as Db;
|
||||||
|
|
||||||
|
expect(await applyMigrations(db, [])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("query generator infers params and result types", () => {
|
test("query generator infers params and result types", () => {
|
||||||
const q = parseQueries(
|
const q = parseQueries(
|
||||||
"-- name: GetByEmail :one\nSELECT * FROM users WHERE email = :email;\n" +
|
"-- name: GetByEmail :one\nSELECT * FROM users WHERE email = :email;\n" +
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getDb,
|
getDb,
|
||||||
hasDb,
|
hasDb,
|
||||||
registerDb,
|
registerDb,
|
||||||
|
registerLazyDb,
|
||||||
databaseNames,
|
databaseNames,
|
||||||
closeDatabases,
|
closeDatabases,
|
||||||
} from "../src/index.ts";
|
} from "../src/index.ts";
|
||||||
@@ -45,3 +46,35 @@ test("getDb throws a helpful error for an unknown named database", async () => {
|
|||||||
expect(() => getDb("nope")).toThrow(/No database named 'nope'/);
|
expect(() => getDb("nope")).toThrow(/No database named 'nope'/);
|
||||||
await closeDatabases();
|
await closeDatabases();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("named databases can be registered without opening a connection", async () => {
|
||||||
|
await closeDatabases();
|
||||||
|
let calls = 0;
|
||||||
|
const analytics = createDb(sqlite(":memory:"));
|
||||||
|
|
||||||
|
registerLazyDb("analytics", () => {
|
||||||
|
calls++;
|
||||||
|
return analytics;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hasDb("analytics")).toBe(true);
|
||||||
|
expect(databaseNames()).toEqual(["analytics"]);
|
||||||
|
expect(calls).toBe(0);
|
||||||
|
expect(getDb("analytics")).toBe(analytics);
|
||||||
|
expect(getDb("analytics")).toBe(analytics);
|
||||||
|
expect(calls).toBe(1);
|
||||||
|
|
||||||
|
await closeDatabases();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("closing the registry does not instantiate unused lazy databases", async () => {
|
||||||
|
await closeDatabases();
|
||||||
|
let calls = 0;
|
||||||
|
registerLazyDb("unused", () => {
|
||||||
|
calls++;
|
||||||
|
return createDb(sqlite(":memory:"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await closeDatabases();
|
||||||
|
expect(calls).toBe(0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.5.1",
|
"version": "0.5.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user