feat: support WRN imports and dynamic public shell

This commit is contained in:
2026-07-21 11:15:06 +05:30
parent 0013c0771d
commit 2ca2d02b22
82 changed files with 959 additions and 174 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.73", "version": "0.2.74",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.73", "version": "0.2.74",
"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,16 +28,16 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.73", "@wrnexus/core": "^0.2.74",
"@wrnexus/router": "^0.2.73", "@wrnexus/router": "^0.2.74",
"@wrnexus/csr": "^0.2.73", "@wrnexus/csr": "^0.2.74",
"@wrnexus/compiler": "^0.2.73", "@wrnexus/compiler": "^0.2.74",
"@wrnexus/styles": "^0.2.73", "@wrnexus/styles": "^0.2.74",
"@wrnexus/dev-server": "^0.2.73", "@wrnexus/dev-server": "^0.2.74",
"@wrnexus/ui": "^0.2.73", "@wrnexus/ui": "^0.2.74",
"@wrnexus/validation": "^0.2.73", "@wrnexus/validation": "^0.2.74",
"@wrnexus/i18n": "^0.2.73", "@wrnexus/i18n": "^0.2.74",
"@wrnexus/db": "^0.2.73" "@wrnexus/db": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+26 -13
View File
@@ -8,6 +8,19 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page. `@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation ## Installation
```bash ```bash
@@ -76,19 +89,19 @@ class Lexer {
Exported type-only symbols describing the parsed tree: Exported type-only symbols describing the parsed tree:
| Type | Description | | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node including `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. | | `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. | | `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. | | `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. | | `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. | | `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. | | `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. | | `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. | | `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `DataMode` | `"ssr" \| "client"`. | | `DataMode` | `"ssr" \| "client"`. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. | | `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. | | `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
## Usage ## Usage
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.73", "version": "0.2.74",
"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.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+13 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.73", "version": "0.2.74",
"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,18 +25,18 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.73", "@wrnexus/core": "^0.2.74",
"@wrnexus/router": "^0.2.73", "@wrnexus/router": "^0.2.74",
"@wrnexus/ssr": "^0.2.73", "@wrnexus/ssr": "^0.2.74",
"@wrnexus/csr": "^0.2.73", "@wrnexus/csr": "^0.2.74",
"@wrnexus/compiler": "^0.2.73", "@wrnexus/compiler": "^0.2.74",
"@wrnexus/styles": "^0.2.73", "@wrnexus/styles": "^0.2.74",
"@wrnexus/ui": "^0.2.73", "@wrnexus/ui": "^0.2.74",
"@wrnexus/validation": "^0.2.73", "@wrnexus/validation": "^0.2.74",
"@wrnexus/i18n": "^0.2.73", "@wrnexus/i18n": "^0.2.74",
"@wrnexus/db": "^0.2.73", "@wrnexus/db": "^0.2.74",
"@wrnexus/pubsub": "^0.2.73", "@wrnexus/pubsub": "^0.2.74",
"@wrnexus/uploader": "^0.2.73" "@wrnexus/uploader": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.73", "version": "0.2.74",
"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.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.73", "version": "0.2.74",
"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.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.73", "version": "0.2.74",
"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.2.73" "@wrnexus/native": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.73", "version": "0.2.74",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.73", "version": "0.2.74",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.73", "version": "0.2.74",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.73", "version": "0.2.74",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.73", "version": "0.2.74",
"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.2.73", "@wrnexus/compiler": "^0.2.74",
"@wrnexus/core": "^0.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.73", "version": "0.2.74",
"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.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.73", "version": "0.2.74",
"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,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.2.73" "@wrnexus/uploader": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.73", "version": "0.2.74",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -3253,7 +3253,7 @@ Reusable progress bar component.
Reusable public footer component. Reusable public footer component.
- Mount: `data-component="PublicFooter"` - Mount: `data-component="PublicFooter"`
- Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `newsletterEyebrow: string = "WrNexus updates"`, `newsletterTitle: string = "Identity insights delivered to your inbox"`, `newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources."`, `newsletterAction: string = "/api/newsletter/subscribe"`, `newsletterButtonLabel: string = "Subscribe"`, `newsletterPlaceholder: string = "Enter your work email"`, `newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription."`, `copyrightText: string = "© 2026 WrNexus. All rights reserved."`, `attributionText: string = "Built by WorkRoot Workspace."`, `showNewsletter: boolean = true`, `showSocialLinks: boolean = true`, `showThemeToggle: boolean = true` - Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `brandIcon: string = "icon-[lucide--blocks]"`, `brandDescription: string = "Secure authentication, user management, organizations, authorization, and enterprise identity for modern applications."`, `statusLabel: string = "All systems operational"`, `statusHref: string = "/status"`, `newsletterEyebrow: string = "WrNexus updates"`, `newsletterTitle: string = "Identity insights delivered to your inbox"`, `newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources."`, `newsletterAction: string = "/api/newsletter/subscribe"`, `newsletterButtonLabel: string = "Subscribe"`, `newsletterPlaceholder: string = "Enter your work email"`, `newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription."`, `newsletterPrivacyLabel: string = "privacy policy"`, `newsletterPrivacyHref: string = "/privacy"`, `newsletterFinePrintPrefix: string = "No spam. Unsubscribe at any time. Read our"`, `newsletterFinePrintSuffix: string = "."`, `newsletterEmailLabel: string = "Work email address"`, `newsletterEmailName: string = "email"`, `copyrightText: string = "© 2026 WrNexus. All rights reserved."`, `attributionText: string = "Built by WorkRoot Workspace."`, `showNewsletter: boolean = true`, `showSocialLinks: boolean = true`, `showThemeToggle: boolean = true`, `showStatus: boolean = true`, `showCookiePreferences: boolean = true`, `cookiePreferencesLabel: string = "Cookie preferences"`, `themeLabel: string = "Theme"`, `themeToggleLabel: string = "Toggle color theme"`, `legalTitle: string = "Legal"`, `socialLinks: PublicFooterLink[] = []`, `navigationColumns: PublicFooterColumn[] = []`, `legalLinks: PublicFooterLink[] = []`, `navigationAriaLabel: string = "Footer navigation"`
- Slots: `navigation` - Slots: `navigation`
- Events: `submit` - Events: `submit`
@@ -7203,7 +7203,7 @@ Reusable product page shell component.
Reusable public header component. Reusable public header component.
- Mount: `data-component="PublicHeader"` - Mount: `data-component="PublicHeader"`
- Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `brandIcon: string = "icon-[lucide--blocks]"`, `pricingLabel: string = "Pricing"`, `pricingHref: string = "/pricing"`, `statusLabel: string = "All systems operational"`, `statusHref: string = "/status"`, `signInLabel: string = "Sign in"`, `signInHref: string = "/sign-in"`, `primaryLabel: string = "Start free"`, `primaryHref: string = "/sign-up"`, `showStatus: boolean = true`, `showThemeToggle: boolean = true`, `showSignIn: boolean = true`, `showPrimaryAction: boolean = true`, `productLabel: string = "Product"`, `solutionsLabel: string = "Solutions"`, `developersLabel: string = "Developers"`, `resourcesLabel: string = "Resources"` - Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `brandIcon: string = "icon-[lucide--blocks]"`, `pricingLabel: string = "Pricing"`, `pricingHref: string = "/pricing"`, `statusLabel: string = "All systems operational"`, `statusHref: string = "/status"`, `signInLabel: string = "Sign in"`, `signInHref: string = "/sign-in"`, `primaryLabel: string = "Start free"`, `primaryHref: string = "/sign-up"`, `showStatus: boolean = true`, `showThemeToggle: boolean = true`, `showSignIn: boolean = true`, `showPrimaryAction: boolean = true`, `productLabel: string = "Product"`, `solutionsLabel: string = "Solutions"`, `developersLabel: string = "Developers"`, `resourcesLabel: string = "Resources"`, `navigationAriaLabel: string = "Main navigation"`, `themeToggleLabel: string = "Toggle color theme"`, `mobileMenuOpenLabel: string = "Open navigation menu"`, `mobileMenuCloseLabel: string = "Close navigation menu"`, `navigationItems: PublicHeaderNavigationItem[] = []`, `actionItems: PublicHeaderActionItem[] = []`, `showNavigation: boolean = true`, `showMobileThemeToggle: boolean = true`
- Slots: `navigation`, `actions` - Slots: `navigation`, `actions`
- Events: `click` - Events: `click`
+168
View File
@@ -33954,6 +33954,30 @@
"required": false, "required": false,
"default": "\"WrNexus home\"" "default": "\"WrNexus home\""
}, },
{
"name": "brandIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--blocks]\""
},
{
"name": "brandDescription",
"type": "string",
"required": false,
"default": "\"Secure authentication, user management, organizations, authorization, and enterprise identity for modern applications.\""
},
{
"name": "statusLabel",
"type": "string",
"required": false,
"default": "\"All systems operational\""
},
{
"name": "statusHref",
"type": "string",
"required": false,
"default": "\"/status\""
},
{ {
"name": "newsletterEyebrow", "name": "newsletterEyebrow",
"type": "string", "type": "string",
@@ -33996,6 +34020,42 @@
"required": false, "required": false,
"default": "\"Thanks. Please check your inbox to confirm your subscription.\"" "default": "\"Thanks. Please check your inbox to confirm your subscription.\""
}, },
{
"name": "newsletterPrivacyLabel",
"type": "string",
"required": false,
"default": "\"privacy policy\""
},
{
"name": "newsletterPrivacyHref",
"type": "string",
"required": false,
"default": "\"/privacy\""
},
{
"name": "newsletterFinePrintPrefix",
"type": "string",
"required": false,
"default": "\"No spam. Unsubscribe at any time. Read our\""
},
{
"name": "newsletterFinePrintSuffix",
"type": "string",
"required": false,
"default": "\".\""
},
{
"name": "newsletterEmailLabel",
"type": "string",
"required": false,
"default": "\"Work email address\""
},
{
"name": "newsletterEmailName",
"type": "string",
"required": false,
"default": "\"email\""
},
{ {
"name": "copyrightText", "name": "copyrightText",
"type": "string", "type": "string",
@@ -34025,6 +34085,66 @@
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": "true" "default": "true"
},
{
"name": "showStatus",
"type": "boolean",
"required": false,
"default": "true"
},
{
"name": "showCookiePreferences",
"type": "boolean",
"required": false,
"default": "true"
},
{
"name": "cookiePreferencesLabel",
"type": "string",
"required": false,
"default": "\"Cookie preferences\""
},
{
"name": "themeLabel",
"type": "string",
"required": false,
"default": "\"Theme\""
},
{
"name": "themeToggleLabel",
"type": "string",
"required": false,
"default": "\"Toggle color theme\""
},
{
"name": "legalTitle",
"type": "string",
"required": false,
"default": "\"Legal\""
},
{
"name": "socialLinks",
"type": "PublicFooterLink[]",
"required": false,
"default": "[]"
},
{
"name": "navigationColumns",
"type": "PublicFooterColumn[]",
"required": false,
"default": "[]"
},
{
"name": "legalLinks",
"type": "PublicFooterLink[]",
"required": false,
"default": "[]"
},
{
"name": "navigationAriaLabel",
"type": "string",
"required": false,
"default": "\"Footer navigation\""
} }
], ],
"slots": ["navigation"], "slots": ["navigation"],
@@ -34168,6 +34288,54 @@
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"Resources\"" "default": "\"Resources\""
},
{
"name": "navigationAriaLabel",
"type": "string",
"required": false,
"default": "\"Main navigation\""
},
{
"name": "themeToggleLabel",
"type": "string",
"required": false,
"default": "\"Toggle color theme\""
},
{
"name": "mobileMenuOpenLabel",
"type": "string",
"required": false,
"default": "\"Open navigation menu\""
},
{
"name": "mobileMenuCloseLabel",
"type": "string",
"required": false,
"default": "\"Close navigation menu\""
},
{
"name": "navigationItems",
"type": "PublicHeaderNavigationItem[]",
"required": false,
"default": "[]"
},
{
"name": "actionItems",
"type": "PublicHeaderActionItem[]",
"required": false,
"default": "[]"
},
{
"name": "showNavigation",
"type": "boolean",
"required": false,
"default": "true"
},
{
"name": "showMobileThemeToggle",
"type": "boolean",
"required": false,
"default": "true"
} }
], ],
"slots": ["navigation", "actions"], "slots": ["navigation", "actions"],
+68 -18
View File
@@ -1,10 +1,27 @@
component PublicFooter { component PublicFooter {
types {
interface PublicFooterLink {
label: string
href: string
ariaLabel?: string
target?: string
icon?: string
}
interface PublicFooterColumn {
title: string
links: PublicFooterLink[]
}
}
props { props {
class: string = "" class: string = ""
homeHref: string = "/" homeHref: string = "/"
brandName: string = "WrNexus" brandName: string = "WrNexus"
brandTagline: string = "Identity Cloud" brandTagline: string = "Identity Cloud"
brandAriaLabel: string = "WrNexus home" brandAriaLabel: string = "WrNexus home"
brandIcon: string = "icon-[lucide--blocks]"
brandDescription: string = "Secure authentication, user management, organizations, authorization, and enterprise identity for modern applications."
statusLabel: string = "All systems operational"
statusHref: string = "/status"
newsletterEyebrow: string = "WrNexus updates" newsletterEyebrow: string = "WrNexus updates"
newsletterTitle: string = "Identity insights delivered to your inbox" newsletterTitle: string = "Identity insights delivered to your inbox"
newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources." newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources."
@@ -12,11 +29,27 @@ component PublicFooter {
newsletterButtonLabel: string = "Subscribe" newsletterButtonLabel: string = "Subscribe"
newsletterPlaceholder: string = "Enter your work email" newsletterPlaceholder: string = "Enter your work email"
newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription." newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription."
newsletterPrivacyLabel: string = "privacy policy"
newsletterPrivacyHref: string = "/privacy"
newsletterFinePrintPrefix: string = "No spam. Unsubscribe at any time. Read our"
newsletterFinePrintSuffix: string = "."
newsletterEmailLabel: string = "Work email address"
newsletterEmailName: string = "email"
copyrightText: string = "© 2026 WrNexus. All rights reserved." copyrightText: string = "© 2026 WrNexus. All rights reserved."
attributionText: string = "Built by WorkRoot Workspace." attributionText: string = "Built by WorkRoot Workspace."
showNewsletter: boolean = true showNewsletter: boolean = true
showSocialLinks: boolean = true showSocialLinks: boolean = true
showThemeToggle: boolean = true showThemeToggle: boolean = true
showStatus: boolean = true
showCookiePreferences: boolean = true
cookiePreferencesLabel: string = "Cookie preferences"
themeLabel: string = "Theme"
themeToggleLabel: string = "Toggle color theme"
legalTitle: string = "Legal"
socialLinks: PublicFooterLink[] = []
navigationColumns: PublicFooterColumn[] = []
legalLinks: PublicFooterLink[] = []
navigationAriaLabel: string = "Footer navigation"
} }
state email = "" state email = ""
state newsletterSubmitted = false state newsletterSubmitted = false
@@ -81,7 +114,7 @@ component PublicFooter {
class="sr-only" class="sr-only"
for="footer-newsletter-email" for="footer-newsletter-email"
> >
Work email address {newsletterEmailLabel}
</label> </label>
<div class="relative min-w-0 flex-1"> <div class="relative min-w-0 flex-1">
@@ -89,7 +122,7 @@ component PublicFooter {
<input <input
id="footer-newsletter-email" id="footer-newsletter-email"
name="email" name="{newsletterEmailName}"
type="email" type="email"
autocomplete="email" autocomplete="email"
required required
@@ -113,13 +146,13 @@ component PublicFooter {
<p class="mt-3 flex items-start gap-2 text-xs leading-5 text-slate-500 dark:text-slate-400"> <p class="mt-3 flex items-start gap-2 text-xs leading-5 text-slate-500 dark:text-slate-400">
<span class="icon-[lucide--shield-check] mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" ></span> <span class="icon-[lucide--shield-check] mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" ></span>
No spam. Unsubscribe at any time. Read our {newsletterFinePrintPrefix}
<a <a
href="/privacy" href="{newsletterPrivacyHref}"
class="font-semibold text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300" class="font-semibold text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
> >
privacy policy {newsletterPrivacyLabel}
</a>. </a>{newsletterFinePrintSuffix}
</p> </p>
<div <div
@@ -148,7 +181,7 @@ component PublicFooter {
<span <span
class="grid h-11 w-11 place-items-center rounded-2xl bg-indigo-600 text-white shadow-lg shadow-indigo-600/20 transition group-hover:-translate-y-0.5 group-hover:bg-indigo-500" class="grid h-11 w-11 place-items-center rounded-2xl bg-indigo-600 text-white shadow-lg shadow-indigo-600/20 transition group-hover:-translate-y-0.5 group-hover:bg-indigo-500"
> >
<span class="icon-[lucide--blocks] h-5 w-5" aria-hidden="true" ></span> <span class="{brandIcon} h-5 w-5" aria-hidden="true" ></span>
</span> </span>
<span> <span>
@@ -165,13 +198,12 @@ component PublicFooter {
</a> </a>
<p class="mt-5 max-w-sm text-sm leading-6 text-slate-600 dark:text-slate-400"> <p class="mt-5 max-w-sm text-sm leading-6 text-slate-600 dark:text-slate-400">
Secure authentication, user management, organizations, {brandDescription}
authorization, and enterprise identity for modern applications.
</p> </p>
<div class="mt-6 flex flex-wrap items-center gap-2"> <div data-show="showStatus" class="mt-6 flex flex-wrap items-center gap-2">
<a <a
href="/status" href="{statusHref}"
class="inline-flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-800 transition hover:border-emerald-300 hover:bg-emerald-100 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:border-emerald-500/30" class="inline-flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-800 transition hover:border-emerald-300 hover:bg-emerald-100 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:border-emerald-500/30"
> >
<span class="relative flex h-2 w-2"> <span class="relative flex h-2 w-2">
@@ -179,12 +211,17 @@ component PublicFooter {
<span class="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"></span> <span class="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"></span>
</span> </span>
All systems operational {statusLabel}
</a> </a>
</div> </div>
<!-- Social links --> <!-- Social links -->
<div data-show="showSocialLinks" class="mt-6 flex items-center gap-2"> <div data-show="showSocialLinks" class="mt-6 flex items-center gap-2">
{#if socialLinks.length > 0}
{#each socialLinks as item}
<a href="{item.href}" target="{item.target || '_blank'}" rel="noopener noreferrer" aria-label="{item.ariaLabel || item.label}" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-500 transition hover:border-indigo-300 hover:text-indigo-700 dark:border-white/10 dark:bg-white/5 dark:text-slate-400 dark:hover:border-indigo-500/30 dark:hover:text-indigo-300"><span class="{item.icon || 'icon-[lucide--external-link]'} h-4.5 w-4.5" aria-hidden="true"></span><span class="sr-only">{item.label}</span></a>
{/each}
{:else}
<a <a
href="https://github.com/wrnexus" href="https://github.com/wrnexus"
target="_blank" target="_blank"
@@ -224,15 +261,21 @@ component PublicFooter {
> >
<span class="icon-[lucide--youtube] h-4.5 w-4.5" aria-hidden="true" ></span> <span class="icon-[lucide--youtube] h-4.5 w-4.5" aria-hidden="true" ></span>
</a> </a>
{/if}
</div> </div>
</div> </div>
<!-- Navigation columns --> <!-- Navigation columns -->
<nav <nav
aria-label="Footer navigation" aria-label="{navigationAriaLabel}"
class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 xl:grid-cols-5" class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 xl:grid-cols-5"
> >
<slot name="navigation"> <slot name="navigation">
{#if navigationColumns.length > 0}
{#each navigationColumns as column}
<div><h3 class="text-sm font-bold text-slate-950 dark:text-white">{column.title}</h3><ul class="mt-4 space-y-3">{#each column.links as item}<li><a href="{item.href}" target="{item.target || '_self'}" aria-label="{item.ariaLabel || item.label}" class="footer-link">{item.label}</a></li>{/each}</ul></div>
{/each}
{:else}
<!-- Product --> <!-- Product -->
<div> <div>
<h3 class="text-sm font-bold text-slate-950 dark:text-white"> <h3 class="text-sm font-bold text-slate-950 dark:text-white">
@@ -547,6 +590,7 @@ component PublicFooter {
</li> </li>
</ul> </ul>
</div> </div>
{/if}
</slot> </slot>
</nav> </nav>
</div> </div>
@@ -570,6 +614,9 @@ component PublicFooter {
</div> </div>
<div class="flex flex-wrap items-center gap-x-5 gap-y-3"> <div class="flex flex-wrap items-center gap-x-5 gap-y-3">
{#if legalLinks.length > 0}
{#each legalLinks as item}<a href="{item.href}" target="{item.target || '_self'}" class="footer-bottom-link">{item.label}</a>{/each}
{:else}
<a <a
href="/privacy" href="/privacy"
class="footer-bottom-link" class="footer-bottom-link"
@@ -595,27 +642,30 @@ component PublicFooter {
href="/legal" href="/legal"
class="footer-bottom-link" class="footer-bottom-link"
> >
Legal {legalTitle}
</a> </a>
{/if}
<button data-show="showThemeToggle" <button data-show="showCookiePreferences"
type="button" type="button"
data-open-cookie-preferences data-open-cookie-preferences
class="footer-bottom-link" class="footer-bottom-link"
> >
Cookie preferences {cookiePreferencesLabel}
</button> </button>
<button <button data-show="showThemeToggle"
type="button" type="button"
data-wire-theme-toggle data-wire-theme-toggle
aria-label="{themeToggleLabel}"
title="{themeToggleLabel}"
class="inline-flex items-center gap-2 rounded-lg text-xs font-semibold text-slate-500 transition hover:text-indigo-700 dark:text-slate-400 dark:hover:text-indigo-300" class="inline-flex items-center gap-2 rounded-lg text-xs font-semibold text-slate-500 transition hover:text-indigo-700 dark:text-slate-400 dark:hover:text-indigo-300"
> >
<span class="icon-[lucide--moon] h-3.5 w-3.5 dark:hidden" aria-hidden="true" ></span> <span class="icon-[lucide--moon] h-3.5 w-3.5 dark:hidden" aria-hidden="true" ></span>
<span class="icon-[lucide--sun] hidden h-3.5 w-3.5 dark:block" aria-hidden="true" ></span> <span class="icon-[lucide--sun] hidden h-3.5 w-3.5 dark:block" aria-hidden="true" ></span>
Theme {themeLabel}
</button> </button>
</div> </div>
</div> </div>
+50 -9
View File
@@ -1,4 +1,15 @@
component PublicHeader { component PublicHeader {
types {
interface PublicHeaderNavigationItem {
label: string
href: string
ariaLabel?: string
target?: string
}
interface PublicHeaderActionItem extends PublicHeaderNavigationItem {
variant?: "primary" | "secondary" | "ghost"
}
}
props { props {
class: string = "" class: string = ""
homeHref: string = "/" homeHref: string = "/"
@@ -22,6 +33,14 @@ component PublicHeader {
solutionsLabel: string = "Solutions" solutionsLabel: string = "Solutions"
developersLabel: string = "Developers" developersLabel: string = "Developers"
resourcesLabel: string = "Resources" resourcesLabel: string = "Resources"
navigationAriaLabel: string = "Main navigation"
themeToggleLabel: string = "Toggle color theme"
mobileMenuOpenLabel: string = "Open navigation menu"
mobileMenuCloseLabel: string = "Close navigation menu"
navigationItems: PublicHeaderNavigationItem[] = []
actionItems: PublicHeaderActionItem[] = []
showNavigation: boolean = true
showMobileThemeToggle: boolean = true
} }
state activeMenu = "" state activeMenu = ""
state mobileMenuOpen = false state mobileMenuOpen = false
@@ -71,10 +90,16 @@ component PublicHeader {
<!-- Desktop navigation --> <!-- Desktop navigation -->
<nav <nav
aria-label="Main navigation" aria-label="{navigationAriaLabel}"
data-show="showNavigation"
class="hidden items-center gap-1 lg:flex" class="hidden items-center gap-1 lg:flex"
> >
<slot name="navigation"> <slot name="navigation">
{#if navigationItems.length > 0}
{#each navigationItems as item}
<a href="{item.href}" aria-label="{item.ariaLabel || item.label}" target="{item.target || '_self'}" class="inline-flex h-10 items-center rounded-xl px-3.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-white">{item.label}</a>
{/each}
{:else}
<!-- Product --> <!-- Product -->
<button <button
type="button" type="button"
@@ -171,12 +196,18 @@ component PublicHeader {
> >
{pricingLabel} {pricingLabel}
</a> </a>
{/if}
</slot> </slot>
</nav> </nav>
<!-- Desktop actions --> <!-- Desktop actions -->
<div class="hidden items-center gap-2 lg:flex"> <div class="hidden items-center gap-2 lg:flex">
<slot name="actions"> <slot name="actions">
{#if actionItems.length > 0}
{#each actionItems as item}
<a href="{item.href}" aria-label="{item.ariaLabel || item.label}" target="{item.target || '_self'}" class="wire-public-header-action wire-public-header-action--{item.variant || 'secondary'}">{item.label}{#if item.variant === 'primary'}<span aria-hidden="true">→</span>{/if}</a>
{/each}
{:else}
<a data-show="showStatus" <a data-show="showStatus"
href="{statusHref}" href="{statusHref}"
class="mr-1 inline-flex items-center gap-2 rounded-xl px-3 py-2 text-xs font-semibold text-slate-500 transition hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-white/5 dark:hover:text-white" class="mr-1 inline-flex items-center gap-2 rounded-xl px-3 py-2 text-xs font-semibold text-slate-500 transition hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-white/5 dark:hover:text-white"
@@ -192,8 +223,8 @@ component PublicHeader {
<button data-show="showThemeToggle" <button data-show="showThemeToggle"
type="button" type="button"
data-wire-theme-toggle data-wire-theme-toggle
aria-label="Toggle color theme" aria-label="{themeToggleLabel}"
title="Toggle color theme" title="{themeToggleLabel}"
class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-white/20 dark:hover:bg-white/10 dark:hover:text-white" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-white/20 dark:hover:bg-white/10 dark:hover:text-white"
> >
<span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span> <span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span>
@@ -201,7 +232,7 @@ component PublicHeader {
<span class="icon-[lucide--sun] hidden h-4 w-4 dark:block" aria-hidden="true" ></span> <span class="icon-[lucide--sun] hidden h-4 w-4 dark:block" aria-hidden="true" ></span>
<span class="sr-only"> <span class="sr-only">
Toggle color theme {themeToggleLabel}
</span> </span>
</button> </button>
@@ -220,16 +251,17 @@ component PublicHeader {
<span class="icon-[lucide--arrow-right] h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span> <span class="icon-[lucide--arrow-right] h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span>
</a> </a>
{/if}
</slot> </slot>
</div> </div>
<!-- Mobile actions --> <!-- Mobile actions -->
<div class="flex items-center gap-2 lg:hidden"> <div class="flex items-center gap-2 lg:hidden">
<button <button data-show="showThemeToggle && showMobileThemeToggle"
type="button" type="button"
data-wire-theme-toggle data-wire-theme-toggle
aria-label="Toggle color theme" aria-label="{themeToggleLabel}"
title="Toggle color theme" title="{themeToggleLabel}"
class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
> >
<span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span> <span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span>
@@ -240,7 +272,7 @@ component PublicHeader {
<button <button
type="button" type="button"
@click="mobileMenuOpen = !mobileMenuOpen" @click="mobileMenuOpen = !mobileMenuOpen"
aria-label="{mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}" aria-label="{mobileMenuOpen ? mobileMenuCloseLabel : mobileMenuOpenLabel}"
aria-expanded="{mobileMenuOpen}" aria-expanded="{mobileMenuOpen}"
aria-controls="public-mobile-navigation" aria-controls="public-mobile-navigation"
class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-700 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:hover:bg-white/10 dark:hover:text-white" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-700 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:hover:bg-white/10 dark:hover:text-white"
@@ -258,7 +290,7 @@ component PublicHeader {
</div> </div>
<!-- Outside-click backdrop --> <!-- Outside-click backdrop -->
<button type="button" data-show="activeMenu !== ''" @click="activeMenu = ''" aria-label="Close navigation menu" class="fixed inset-0 z-40 hidden cursor-default bg-transparent lg:block" class:hidden="activeMenu === ''" class:block="activeMenu !== ''" ></button> <button type="button" data-show="activeMenu !== ''" @click="activeMenu = ''" aria-label="{mobileMenuCloseLabel}" class="fixed inset-0 z-40 hidden cursor-default bg-transparent lg:block" class:hidden="activeMenu === ''" class:block="activeMenu !== ''" ></button>
<!-- Desktop mega menus --> <!-- Desktop mega menus -->
<div <div
@@ -302,7 +334,16 @@ component PublicHeader {
class:hidden="!mobileMenuOpen" class:hidden="!mobileMenuOpen"
class:block="mobileMenuOpen" class:block="mobileMenuOpen"
> >
{#if navigationItems.length > 0 || actionItems.length > 0}
<nav aria-label="{navigationAriaLabel}" class="border-t border-slate-200 bg-white px-5 py-5 shadow-xl dark:border-white/10 dark:bg-slate-950">
<div class="grid gap-2">
{#each navigationItems as item}<a href="{item.href}" target="{item.target || '_self'}" class="rounded-xl px-4 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-white/5">{item.label}</a>{/each}
{#each actionItems as item}<a href="{item.href}" target="{item.target || '_self'}" class="wire-public-header-action wire-public-header-action--{item.variant || 'secondary'}">{item.label}</a>{/each}
</div>
</nav>
{:else}
<PublicMobileNavigation /> <PublicMobileNavigation />
{/if}
</div> </div>
</header> </header>
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -25,7 +25,7 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist", "dist",
+38
View File
@@ -1186,6 +1186,44 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.wire-public-header-action {
display: inline-flex;
min-height: 2.5rem;
align-items: center;
justify-content: center;
gap: 0.5rem;
border: 1px solid transparent;
border-radius: 0.75rem;
padding: 0.625rem 1rem;
color: var(--wire-color-text);
font-size: 0.875rem;
font-weight: 700;
text-decoration: none;
transition:
background var(--wire-motion-base),
border-color var(--wire-motion-base),
color var(--wire-motion-base),
transform var(--wire-motion-base) var(--wire-ease-emphasized);
}
.wire-public-header-action--primary {
color: var(--wire-color-primary-contrast);
background: var(--wire-color-primary);
box-shadow: 0 0.75rem 1.5rem color-mix(in srgb, var(--wire-color-primary) 20%, transparent);
}
.wire-public-header-action--primary:hover {
background: var(--wire-color-primary-hover);
transform: translateY(-1px);
}
.wire-public-header-action--secondary {
border-color: var(--wire-color-border);
background: var(--wire-color-surface);
}
.wire-public-header-action--secondary:hover,
.wire-public-header-action--ghost:hover {
color: var(--wire-color-primary);
background: var(--wire-color-surface-2);
}
.wire-auth-field__input { .wire-auth-field__input {
width: 100%; width: 100%;
min-height: 3.5rem; min-height: 3.5rem;
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.73", "version": "0.2.74",
"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.2.73" "@wrnexus/core": "^0.2.74"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.", "description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.2.14
- Added top-level ES module import highlighting, completion, snippets, formatting,
and diagnostics support for `.wrn` files.
- Imported helpers can now be used in server-rendered component prop expressions.
## 0.2.13 ## 0.2.13
- Added explicit prop and state types, local `types` blocks, and typed function - Added explicit prop and state types, local `types` blocks, and typed function
+13
View File
@@ -27,6 +27,19 @@ component UserCard {
The extension shows declared prop types and reports missing required props and incompatible values. Typed prop and state expressions passed into nested components retain their declared type. The extension shows declared prop types and reports missing required props and incompatible values. Typed prop and state expressions passed into nested components retain their declared type.
Top-level imports are supported before `page`, `component`, or `layout`. Imported
helpers can be used in server-rendered prop expressions:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation ## Installation
Search for **WRNexus Language Support** in the VS Code Extensions view or run: Search for **WRNexus Language Support** in the VS Code Extensions view or run:
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "wrnexus", "name": "wrnexus",
"version": "0.2.13", "version": "0.2.14",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wrnexus", "name": "wrnexus",
"version": "0.2.13", "version": "0.2.14",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"devDependencies": { "devDependencies": {
"@vscode/vsce": "^3.9.2" "@vscode/vsce": "^3.9.2"
+2 -2
View File
@@ -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.2.13", "version": "0.2.14",
"publisher": "wrnexus", "publisher": "wrnexus",
"private": true, "private": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
@@ -68,7 +68,7 @@
"extensions": [ "extensions": [
".wrn" ".wrn"
], ],
"firstLine": "^\\s*(?:page|component|layout)\\s+[A-Za-z_$][A-Za-z0-9_$]*\\s*\\{", "firstLine": "^\\s*(?:(?:import\\b[^;\\n]*(?:;|\\n)\\s*)*)(?:page|component|layout)\\s+[A-Za-z_$][A-Za-z0-9_$]*\\s*\\{",
"configuration": "./language-configuration.json", "configuration": "./language-configuration.json",
"icon": { "icon": {
"light": "./icons/wrn.png", "light": "./icons/wrn.png",
+5
View File
@@ -1,4 +1,9 @@
{ {
"WRN import": {
"prefix": ["wrn-import", "import"],
"description": "Import a server-side helper into a WRN file",
"body": ["import { ${1:appUrl} } from \"${2:@wrnexus/helpers}\";", "", "$0"]
},
"WRN page": { "WRN page": {
"prefix": ["wrn-page", "page"], "prefix": ["wrn-page", "page"],
"description": "Create a WRN page", "description": "Create a WRN page",
+27
View File
@@ -382,6 +382,24 @@ function unescapeSeoValue(value) {
} }
function parse(source) { function parse(source) {
const lx = new Lexer(source); const lx = new Lexer(source);
const imports = [];
const importPattern = /import\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'][^"'\r\n]+["']\s*;?/y;
while (true) {
while (/\s/u.test(source[lx.pos] ?? ""))
lx.pos++;
if (source.startsWith("//", lx.pos)) {
while (lx.pos < source.length && source[lx.pos] !== `
`)
lx.pos++;
continue;
}
importPattern.lastIndex = lx.pos;
const statement = importPattern.exec(source);
if (!statement)
break;
imports.push(statement[0].trim());
lx.pos = importPattern.lastIndex;
}
const expect = (type) => { const expect = (type) => {
const t = lx.next(); const t = lx.next();
if (t.type !== type) { if (t.type !== type) {
@@ -627,6 +645,7 @@ function parse(source) {
} }
return { return {
type: "page", type: "page",
imports,
kind, kind,
name, name,
layout, layout,
@@ -1304,6 +1323,9 @@ function generate(ast) {
return generateComponent(ast); return generateComponent(ast);
} }
const out = []; const out = [];
if (ast.imports.length > 0)
out.push(ast.imports.join(`
`));
const ssrBindings = []; const ssrBindings = [];
const csrBindings = []; const csrBindings = [];
const helpers = ast.functions.map((body2) => body2.trim()).filter(Boolean).join(` const helpers = ast.functions.map((body2) => body2.trim()).filter(Boolean).join(`
@@ -1722,6 +1744,9 @@ function renderComponentNode(node, ctx) {
} }
function generateComponent(ast) { function generateComponent(ast) {
const out = []; const out = [];
if (ast.imports.length > 0)
out.push(ast.imports.join(`
`));
const hasServerEach = viewHasServerEach(ast.view); const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) { if (hasServerEach) {
out.push(`import { Buffer } from "node:buffer";`); out.push(`import { Buffer } from "node:buffer";`);
@@ -2192,6 +2217,8 @@ function generateNative(ast) {
import React, { useState } from "react"; import React, { useState } from "react";
import { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native"; import { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
${ast.imports.join(`
`)}
${typeSource} ${typeSource}
+7
View File
@@ -3,6 +3,13 @@
const vscode = require("vscode"); const vscode = require("vscode");
const BLOCK_COMPLETIONS = [ const BLOCK_COMPLETIONS = [
{
label: "import",
detail: "WRN server module import",
documentation:
"Import a package or module before the page, component, or layout declaration for server rendering.",
snippet: 'import { ${1:appUrl} } from "${2:@wrnexus/helpers}";\n\n$0',
},
{ {
label: "page", label: "page",
detail: "WRN page", detail: "WRN page",
+12
View File
@@ -89,6 +89,7 @@ function stripComments(source) {
function maskLeadingTrivia(source) { function maskLeadingTrivia(source) {
const masked = [...source]; const masked = [...source];
let offset = 0; let offset = 0;
const importPattern = /import\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'][^"'\r\n]+["']\s*;?/y;
while (offset < source.length) { while (offset < source.length) {
if (/\s/u.test(source[offset])) { if (/\s/u.test(source[offset])) {
@@ -96,6 +97,17 @@ function maskLeadingTrivia(source) {
continue; continue;
} }
importPattern.lastIndex = offset;
const importStatement = importPattern.exec(source);
if (importStatement) {
const end = importPattern.lastIndex;
while (offset < end) {
if (source[offset] !== "\n" && source[offset] !== "\r") masked[offset] = " ";
offset += 1;
}
continue;
}
if (!source.startsWith("//", offset)) break; if (!source.startsWith("//", offset)) break;
while (offset < source.length && source[offset] !== "\n") { while (offset < source.length && source[offset] !== "\n") {
+14
View File
@@ -326,6 +326,20 @@ function formatWrn(source, options = {}) {
previousWasBlank = false; previousWasBlank = false;
if (/^import\b/.test(value)) {
const importLines = [value];
while (
!/(?:\bfrom\s+)?["'][^"']+["']\s*;?$/.test(importLines[importLines.length - 1]) &&
index + 1 < inputLines.length
) {
index += 1;
importLines.push(inputLines[index].trim());
}
output.push(importLines[0], ...importLines.slice(1).map((line) => `${unit}${line}`));
index += 1;
continue;
}
if (isMultilineOpeningTagStart(value)) { if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index); const collected = collectOpeningTag(inputLines, index);
@@ -6,6 +6,9 @@
{ {
"include": "#comments" "include": "#comments"
}, },
{
"include": "#imports"
},
{ {
"include": "#declaration" "include": "#declaration"
}, },
@@ -14,6 +17,21 @@
} }
], ],
"repository": { "repository": {
"imports": {
"begin": "^\\s*(import)\\b",
"beginCaptures": {
"1": {
"name": "keyword.control.import.ts"
}
},
"end": ";|$",
"name": "meta.import.wrn",
"patterns": [
{
"include": "source.ts"
}
]
},
"comments": { "comments": {
"patterns": [ "patterns": [
{ {
+13
View File
@@ -56,6 +56,19 @@ component ThemeToggle {
assert.equal(declaration.diagnostic, undefined); assert.equal(declaration.diagnostic, undefined);
}); });
test("recognizes a WRN declaration after top-level imports", () => {
const source = `import { appUrl } from "@wrnexus/helpers";
import type { Context } from "@wrnexus/core";
page Home {
view { <PublicHeader signInHref="{appUrl('sso', '/sign-in')}" /> }
}`;
const declaration = findTopLevelDeclaration({}, source);
assert.equal(declaration.kind, "page");
assert.equal(declaration.name, "Home");
assert.equal(declaration.diagnostic, undefined);
});
test("masks only leading trivia and preserves source offsets", () => { test("masks only leading trivia and preserves source offsets", () => {
const source = "\uFEFF// Summary\r\npage Home {\n // member comment\n}"; const source = "\uFEFF// Summary\r\npage Home {\n // member comment\n}";
const masked = maskLeadingTrivia(source); const masked = maskLeadingTrivia(source);
+7
View File
@@ -127,6 +127,13 @@ try {
formatWrn(formatted, { insertSpaces: true, tabSize: 2 }) === formatted formatWrn(formatted, { insertSpaces: true, tabSize: 2 }) === formatted
? ok("formatter is idempotent") ? ok("formatter is idempotent")
: bad("formatter is idempotent"); : bad("formatter is idempotent");
const imported = formatWrn(
`import {\nappUrl,\nother\n} from "@wrnexus/helpers";\n\nlayout Public {\nview { <p>Hi</p> }\n}\n`,
{ insertSpaces: true, tabSize: 2 },
);
imported.includes(`\nlayout Public {`) && formatWrn(imported, { tabSize: 2 }) === imported
? ok("formatter preserves top-level imports")
: bad("formatter preserves top-level imports");
} catch (e) { } catch (e) {
bad("formatter loads", e.message); bad("formatter loads", e.message);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.73", "version": "0.2.74",
"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.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+9
View File
@@ -734,6 +734,15 @@ const MIGRATIONS: Migration[] = [
// Shared UI visuals improve automatically after updating and rebuilding. // Shared UI visuals improve automatically after updating and rebuilding.
}, },
}, },
{
version: "0.2.74",
id: "wrn-imports-and-dynamic-public-shell",
description:
"Adds top-level WRN imports and data-driven public header and footer navigation, actions, links, and labels.",
apply() {
// Language and shared UI behavior improve automatically after updating.
},
},
]; ];
/** Release tooling uses this to require an explicit migration entry per version. */ /** Release tooling uses this to require an explicit migration entry per version. */
+26 -13
View File
@@ -8,6 +8,19 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page. `@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation ## Installation
```bash ```bash
@@ -76,19 +89,19 @@ class Lexer {
Exported type-only symbols describing the parsed tree: Exported type-only symbols describing the parsed tree:
| Type | Description | | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node including `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. | | `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. | | `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. | | `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. | | `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. | | `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. | | `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. | | `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. | | `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `DataMode` | `"ssr" \| "client"`. | | `DataMode` | `"ssr" \| "client"`. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. | | `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. | | `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
## Usage ## Usage
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+2
View File
@@ -660,6 +660,7 @@ export function generate(ast: PageAst): string {
} }
const out: string[] = []; const out: string[] = [];
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
const ssrBindings: SsrBinding[] = []; const ssrBindings: SsrBinding[] = [];
const csrBindings: CsrBinding[] = []; const csrBindings: CsrBinding[] = [];
const helpers = ast.functions const helpers = ast.functions
@@ -1292,6 +1293,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
function generateComponent(ast: PageAst): string { function generateComponent(ast: PageAst): string {
const out: string[] = []; const out: string[] = [];
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
const hasServerEach = viewHasServerEach(ast.view); const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) { if (hasServerEach) {
+1 -1
View File
@@ -233,5 +233,5 @@ export function generateNative(ast: PageAst): string {
.map((block) => block.trim()) .map((block) => block.trim())
.filter(Boolean) .filter(Boolean)
.join("\n\n"); .join("\n\n");
return `// generated from .wrn for Expo/React Native\nimport React, { useState } from "react";\nimport { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";\nimport { useRouter } from "expo-router";\n\n${typeSource}\n\nexport default function ${ast.name}() {\n const router = useRouter();\n${hooks}\n return <>${body}</>;\n}\n\n${nativeStyles(ast.styles)}\n`; return `// generated from .wrn for Expo/React Native\nimport React, { useState } from "react";\nimport { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";\nimport { useRouter } from "expo-router";\n${ast.imports.join("\n")}\n\n${typeSource}\n\nexport default function ${ast.name}() {\n const router = useRouter();\n${hooks}\n return <>${body}</>;\n}\n\n${nativeStyles(ast.styles)}\n`;
} }
+18
View File
@@ -146,6 +146,8 @@ export interface PropDecl {
export interface PageAst { export interface PageAst {
type: "page"; type: "page";
/** Static ES module imports declared before the WRN root declaration. */
imports: string[];
/** /**
* `page` is a route, `component` is a reusable fragment, * `page` is a route, `component` is a reusable fragment,
* and `layout` is a reusable page wrapper. * and `layout` is a reusable page wrapper.
@@ -197,6 +199,21 @@ function unescapeSeoValue(value: string): string {
export function parse(source: string): PageAst { export function parse(source: string): PageAst {
const lx = new Lexer(source); const lx = new Lexer(source);
const imports: string[] = [];
const importPattern = /import\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'][^"'\r\n]+["']\s*;?/y;
while (true) {
while (/\s/u.test(source[lx.pos] ?? "")) lx.pos++;
if (source.startsWith("//", lx.pos)) {
while (lx.pos < source.length && source[lx.pos] !== "\n") lx.pos++;
continue;
}
importPattern.lastIndex = lx.pos;
const statement = importPattern.exec(source);
if (!statement) break;
imports.push(statement[0].trim());
lx.pos = importPattern.lastIndex;
}
const expect = (type: Token["type"]): Token => { const expect = (type: Token["type"]): Token => {
const t = lx.next(); const t = lx.next();
if (t.type !== type) { if (t.type !== type) {
@@ -474,6 +491,7 @@ export function parse(source: string): PageAst {
return { return {
type: "page", type: "page",
imports,
kind, kind,
name, name,
layout, layout,
+14
View File
@@ -57,6 +57,20 @@ test("parses a page with a layout member", () => {
expect(ast.layout).toBe("public"); expect(ast.layout).toBe("public");
}); });
test("top-level imports are preserved and available to SSR view expressions", async () => {
const source = `import { posix } from "node:path";
page Home {
view { <PublicHeader signInHref="{posix.join('/sso', '/sign-in')}" /> }
}`;
const ast = parse(source);
expect(ast.imports).toEqual(['import { posix } from "node:path";']);
const mod = await compileAndImport(source);
const render = mod.default as (ctx: unknown) => string;
expect(String(await render({}))).toContain('signInHref="/sso/sign-in"');
});
test("parses a component with props and state", () => { test("parses a component with props and state", () => {
const ast = parse( const ast = parse(
`component C {\n props {\n n = 0\n }\n state count = n\n view { <b>{count}</b> }\n}`, `component C {\n props {\n n = 0\n }\n state count = n\n view { <b>{count}</b> }\n}`,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"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.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.73", "version": "0.2.74",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+2 -2
View File
@@ -3253,7 +3253,7 @@ Reusable progress bar component.
Reusable public footer component. Reusable public footer component.
- Mount: `data-component="PublicFooter"` - Mount: `data-component="PublicFooter"`
- Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `newsletterEyebrow: string = "WrNexus updates"`, `newsletterTitle: string = "Identity insights delivered to your inbox"`, `newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources."`, `newsletterAction: string = "/api/newsletter/subscribe"`, `newsletterButtonLabel: string = "Subscribe"`, `newsletterPlaceholder: string = "Enter your work email"`, `newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription."`, `copyrightText: string = "© 2026 WrNexus. All rights reserved."`, `attributionText: string = "Built by WorkRoot Workspace."`, `showNewsletter: boolean = true`, `showSocialLinks: boolean = true`, `showThemeToggle: boolean = true` - Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `brandIcon: string = "icon-[lucide--blocks]"`, `brandDescription: string = "Secure authentication, user management, organizations, authorization, and enterprise identity for modern applications."`, `statusLabel: string = "All systems operational"`, `statusHref: string = "/status"`, `newsletterEyebrow: string = "WrNexus updates"`, `newsletterTitle: string = "Identity insights delivered to your inbox"`, `newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources."`, `newsletterAction: string = "/api/newsletter/subscribe"`, `newsletterButtonLabel: string = "Subscribe"`, `newsletterPlaceholder: string = "Enter your work email"`, `newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription."`, `newsletterPrivacyLabel: string = "privacy policy"`, `newsletterPrivacyHref: string = "/privacy"`, `newsletterFinePrintPrefix: string = "No spam. Unsubscribe at any time. Read our"`, `newsletterFinePrintSuffix: string = "."`, `newsletterEmailLabel: string = "Work email address"`, `newsletterEmailName: string = "email"`, `copyrightText: string = "© 2026 WrNexus. All rights reserved."`, `attributionText: string = "Built by WorkRoot Workspace."`, `showNewsletter: boolean = true`, `showSocialLinks: boolean = true`, `showThemeToggle: boolean = true`, `showStatus: boolean = true`, `showCookiePreferences: boolean = true`, `cookiePreferencesLabel: string = "Cookie preferences"`, `themeLabel: string = "Theme"`, `themeToggleLabel: string = "Toggle color theme"`, `legalTitle: string = "Legal"`, `socialLinks: PublicFooterLink[] = []`, `navigationColumns: PublicFooterColumn[] = []`, `legalLinks: PublicFooterLink[] = []`, `navigationAriaLabel: string = "Footer navigation"`
- Slots: `navigation` - Slots: `navigation`
- Events: `submit` - Events: `submit`
@@ -7203,7 +7203,7 @@ Reusable product page shell component.
Reusable public header component. Reusable public header component.
- Mount: `data-component="PublicHeader"` - Mount: `data-component="PublicHeader"`
- Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `brandIcon: string = "icon-[lucide--blocks]"`, `pricingLabel: string = "Pricing"`, `pricingHref: string = "/pricing"`, `statusLabel: string = "All systems operational"`, `statusHref: string = "/status"`, `signInLabel: string = "Sign in"`, `signInHref: string = "/sign-in"`, `primaryLabel: string = "Start free"`, `primaryHref: string = "/sign-up"`, `showStatus: boolean = true`, `showThemeToggle: boolean = true`, `showSignIn: boolean = true`, `showPrimaryAction: boolean = true`, `productLabel: string = "Product"`, `solutionsLabel: string = "Solutions"`, `developersLabel: string = "Developers"`, `resourcesLabel: string = "Resources"` - Props: `class: string = ""`, `homeHref: string = "/"`, `brandName: string = "WrNexus"`, `brandTagline: string = "Identity Cloud"`, `brandAriaLabel: string = "WrNexus home"`, `brandIcon: string = "icon-[lucide--blocks]"`, `pricingLabel: string = "Pricing"`, `pricingHref: string = "/pricing"`, `statusLabel: string = "All systems operational"`, `statusHref: string = "/status"`, `signInLabel: string = "Sign in"`, `signInHref: string = "/sign-in"`, `primaryLabel: string = "Start free"`, `primaryHref: string = "/sign-up"`, `showStatus: boolean = true`, `showThemeToggle: boolean = true`, `showSignIn: boolean = true`, `showPrimaryAction: boolean = true`, `productLabel: string = "Product"`, `solutionsLabel: string = "Solutions"`, `developersLabel: string = "Developers"`, `resourcesLabel: string = "Resources"`, `navigationAriaLabel: string = "Main navigation"`, `themeToggleLabel: string = "Toggle color theme"`, `mobileMenuOpenLabel: string = "Open navigation menu"`, `mobileMenuCloseLabel: string = "Close navigation menu"`, `navigationItems: PublicHeaderNavigationItem[] = []`, `actionItems: PublicHeaderActionItem[] = []`, `showNavigation: boolean = true`, `showMobileThemeToggle: boolean = true`
- Slots: `navigation`, `actions` - Slots: `navigation`, `actions`
- Events: `click` - Events: `click`
+168
View File
@@ -33954,6 +33954,30 @@
"required": false, "required": false,
"default": "\"WrNexus home\"" "default": "\"WrNexus home\""
}, },
{
"name": "brandIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--blocks]\""
},
{
"name": "brandDescription",
"type": "string",
"required": false,
"default": "\"Secure authentication, user management, organizations, authorization, and enterprise identity for modern applications.\""
},
{
"name": "statusLabel",
"type": "string",
"required": false,
"default": "\"All systems operational\""
},
{
"name": "statusHref",
"type": "string",
"required": false,
"default": "\"/status\""
},
{ {
"name": "newsletterEyebrow", "name": "newsletterEyebrow",
"type": "string", "type": "string",
@@ -33996,6 +34020,42 @@
"required": false, "required": false,
"default": "\"Thanks. Please check your inbox to confirm your subscription.\"" "default": "\"Thanks. Please check your inbox to confirm your subscription.\""
}, },
{
"name": "newsletterPrivacyLabel",
"type": "string",
"required": false,
"default": "\"privacy policy\""
},
{
"name": "newsletterPrivacyHref",
"type": "string",
"required": false,
"default": "\"/privacy\""
},
{
"name": "newsletterFinePrintPrefix",
"type": "string",
"required": false,
"default": "\"No spam. Unsubscribe at any time. Read our\""
},
{
"name": "newsletterFinePrintSuffix",
"type": "string",
"required": false,
"default": "\".\""
},
{
"name": "newsletterEmailLabel",
"type": "string",
"required": false,
"default": "\"Work email address\""
},
{
"name": "newsletterEmailName",
"type": "string",
"required": false,
"default": "\"email\""
},
{ {
"name": "copyrightText", "name": "copyrightText",
"type": "string", "type": "string",
@@ -34025,6 +34085,66 @@
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": "true" "default": "true"
},
{
"name": "showStatus",
"type": "boolean",
"required": false,
"default": "true"
},
{
"name": "showCookiePreferences",
"type": "boolean",
"required": false,
"default": "true"
},
{
"name": "cookiePreferencesLabel",
"type": "string",
"required": false,
"default": "\"Cookie preferences\""
},
{
"name": "themeLabel",
"type": "string",
"required": false,
"default": "\"Theme\""
},
{
"name": "themeToggleLabel",
"type": "string",
"required": false,
"default": "\"Toggle color theme\""
},
{
"name": "legalTitle",
"type": "string",
"required": false,
"default": "\"Legal\""
},
{
"name": "socialLinks",
"type": "PublicFooterLink[]",
"required": false,
"default": "[]"
},
{
"name": "navigationColumns",
"type": "PublicFooterColumn[]",
"required": false,
"default": "[]"
},
{
"name": "legalLinks",
"type": "PublicFooterLink[]",
"required": false,
"default": "[]"
},
{
"name": "navigationAriaLabel",
"type": "string",
"required": false,
"default": "\"Footer navigation\""
} }
], ],
"slots": ["navigation"], "slots": ["navigation"],
@@ -34168,6 +34288,54 @@
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"Resources\"" "default": "\"Resources\""
},
{
"name": "navigationAriaLabel",
"type": "string",
"required": false,
"default": "\"Main navigation\""
},
{
"name": "themeToggleLabel",
"type": "string",
"required": false,
"default": "\"Toggle color theme\""
},
{
"name": "mobileMenuOpenLabel",
"type": "string",
"required": false,
"default": "\"Open navigation menu\""
},
{
"name": "mobileMenuCloseLabel",
"type": "string",
"required": false,
"default": "\"Close navigation menu\""
},
{
"name": "navigationItems",
"type": "PublicHeaderNavigationItem[]",
"required": false,
"default": "[]"
},
{
"name": "actionItems",
"type": "PublicHeaderActionItem[]",
"required": false,
"default": "[]"
},
{
"name": "showNavigation",
"type": "boolean",
"required": false,
"default": "true"
},
{
"name": "showMobileThemeToggle",
"type": "boolean",
"required": false,
"default": "true"
} }
], ],
"slots": ["navigation", "actions"], "slots": ["navigation", "actions"],
+68 -18
View File
@@ -1,10 +1,27 @@
component PublicFooter { component PublicFooter {
types {
interface PublicFooterLink {
label: string
href: string
ariaLabel?: string
target?: string
icon?: string
}
interface PublicFooterColumn {
title: string
links: PublicFooterLink[]
}
}
props { props {
class: string = "" class: string = ""
homeHref: string = "/" homeHref: string = "/"
brandName: string = "WrNexus" brandName: string = "WrNexus"
brandTagline: string = "Identity Cloud" brandTagline: string = "Identity Cloud"
brandAriaLabel: string = "WrNexus home" brandAriaLabel: string = "WrNexus home"
brandIcon: string = "icon-[lucide--blocks]"
brandDescription: string = "Secure authentication, user management, organizations, authorization, and enterprise identity for modern applications."
statusLabel: string = "All systems operational"
statusHref: string = "/status"
newsletterEyebrow: string = "WrNexus updates" newsletterEyebrow: string = "WrNexus updates"
newsletterTitle: string = "Identity insights delivered to your inbox" newsletterTitle: string = "Identity insights delivered to your inbox"
newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources." newsletterDescription: string = "Get product updates, security guidance, implementation strategies, and practical identity architecture resources."
@@ -12,11 +29,27 @@ component PublicFooter {
newsletterButtonLabel: string = "Subscribe" newsletterButtonLabel: string = "Subscribe"
newsletterPlaceholder: string = "Enter your work email" newsletterPlaceholder: string = "Enter your work email"
newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription." newsletterSuccessMessage: string = "Thanks. Please check your inbox to confirm your subscription."
newsletterPrivacyLabel: string = "privacy policy"
newsletterPrivacyHref: string = "/privacy"
newsletterFinePrintPrefix: string = "No spam. Unsubscribe at any time. Read our"
newsletterFinePrintSuffix: string = "."
newsletterEmailLabel: string = "Work email address"
newsletterEmailName: string = "email"
copyrightText: string = "© 2026 WrNexus. All rights reserved." copyrightText: string = "© 2026 WrNexus. All rights reserved."
attributionText: string = "Built by WorkRoot Workspace." attributionText: string = "Built by WorkRoot Workspace."
showNewsletter: boolean = true showNewsletter: boolean = true
showSocialLinks: boolean = true showSocialLinks: boolean = true
showThemeToggle: boolean = true showThemeToggle: boolean = true
showStatus: boolean = true
showCookiePreferences: boolean = true
cookiePreferencesLabel: string = "Cookie preferences"
themeLabel: string = "Theme"
themeToggleLabel: string = "Toggle color theme"
legalTitle: string = "Legal"
socialLinks: PublicFooterLink[] = []
navigationColumns: PublicFooterColumn[] = []
legalLinks: PublicFooterLink[] = []
navigationAriaLabel: string = "Footer navigation"
} }
state email = "" state email = ""
state newsletterSubmitted = false state newsletterSubmitted = false
@@ -81,7 +114,7 @@ component PublicFooter {
class="sr-only" class="sr-only"
for="footer-newsletter-email" for="footer-newsletter-email"
> >
Work email address {newsletterEmailLabel}
</label> </label>
<div class="relative min-w-0 flex-1"> <div class="relative min-w-0 flex-1">
@@ -89,7 +122,7 @@ component PublicFooter {
<input <input
id="footer-newsletter-email" id="footer-newsletter-email"
name="email" name="{newsletterEmailName}"
type="email" type="email"
autocomplete="email" autocomplete="email"
required required
@@ -113,13 +146,13 @@ component PublicFooter {
<p class="mt-3 flex items-start gap-2 text-xs leading-5 text-slate-500 dark:text-slate-400"> <p class="mt-3 flex items-start gap-2 text-xs leading-5 text-slate-500 dark:text-slate-400">
<span class="icon-[lucide--shield-check] mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" ></span> <span class="icon-[lucide--shield-check] mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" ></span>
No spam. Unsubscribe at any time. Read our {newsletterFinePrintPrefix}
<a <a
href="/privacy" href="{newsletterPrivacyHref}"
class="font-semibold text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300" class="font-semibold text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
> >
privacy policy {newsletterPrivacyLabel}
</a>. </a>{newsletterFinePrintSuffix}
</p> </p>
<div <div
@@ -148,7 +181,7 @@ component PublicFooter {
<span <span
class="grid h-11 w-11 place-items-center rounded-2xl bg-indigo-600 text-white shadow-lg shadow-indigo-600/20 transition group-hover:-translate-y-0.5 group-hover:bg-indigo-500" class="grid h-11 w-11 place-items-center rounded-2xl bg-indigo-600 text-white shadow-lg shadow-indigo-600/20 transition group-hover:-translate-y-0.5 group-hover:bg-indigo-500"
> >
<span class="icon-[lucide--blocks] h-5 w-5" aria-hidden="true" ></span> <span class="{brandIcon} h-5 w-5" aria-hidden="true" ></span>
</span> </span>
<span> <span>
@@ -165,13 +198,12 @@ component PublicFooter {
</a> </a>
<p class="mt-5 max-w-sm text-sm leading-6 text-slate-600 dark:text-slate-400"> <p class="mt-5 max-w-sm text-sm leading-6 text-slate-600 dark:text-slate-400">
Secure authentication, user management, organizations, {brandDescription}
authorization, and enterprise identity for modern applications.
</p> </p>
<div class="mt-6 flex flex-wrap items-center gap-2"> <div data-show="showStatus" class="mt-6 flex flex-wrap items-center gap-2">
<a <a
href="/status" href="{statusHref}"
class="inline-flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-800 transition hover:border-emerald-300 hover:bg-emerald-100 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:border-emerald-500/30" class="inline-flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-800 transition hover:border-emerald-300 hover:bg-emerald-100 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:border-emerald-500/30"
> >
<span class="relative flex h-2 w-2"> <span class="relative flex h-2 w-2">
@@ -179,12 +211,17 @@ component PublicFooter {
<span class="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"></span> <span class="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"></span>
</span> </span>
All systems operational {statusLabel}
</a> </a>
</div> </div>
<!-- Social links --> <!-- Social links -->
<div data-show="showSocialLinks" class="mt-6 flex items-center gap-2"> <div data-show="showSocialLinks" class="mt-6 flex items-center gap-2">
{#if socialLinks.length > 0}
{#each socialLinks as item}
<a href="{item.href}" target="{item.target || '_blank'}" rel="noopener noreferrer" aria-label="{item.ariaLabel || item.label}" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-500 transition hover:border-indigo-300 hover:text-indigo-700 dark:border-white/10 dark:bg-white/5 dark:text-slate-400 dark:hover:border-indigo-500/30 dark:hover:text-indigo-300"><span class="{item.icon || 'icon-[lucide--external-link]'} h-4.5 w-4.5" aria-hidden="true"></span><span class="sr-only">{item.label}</span></a>
{/each}
{:else}
<a <a
href="https://github.com/wrnexus" href="https://github.com/wrnexus"
target="_blank" target="_blank"
@@ -224,15 +261,21 @@ component PublicFooter {
> >
<span class="icon-[lucide--youtube] h-4.5 w-4.5" aria-hidden="true" ></span> <span class="icon-[lucide--youtube] h-4.5 w-4.5" aria-hidden="true" ></span>
</a> </a>
{/if}
</div> </div>
</div> </div>
<!-- Navigation columns --> <!-- Navigation columns -->
<nav <nav
aria-label="Footer navigation" aria-label="{navigationAriaLabel}"
class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 xl:grid-cols-5" class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 xl:grid-cols-5"
> >
<slot name="navigation"> <slot name="navigation">
{#if navigationColumns.length > 0}
{#each navigationColumns as column}
<div><h3 class="text-sm font-bold text-slate-950 dark:text-white">{column.title}</h3><ul class="mt-4 space-y-3">{#each column.links as item}<li><a href="{item.href}" target="{item.target || '_self'}" aria-label="{item.ariaLabel || item.label}" class="footer-link">{item.label}</a></li>{/each}</ul></div>
{/each}
{:else}
<!-- Product --> <!-- Product -->
<div> <div>
<h3 class="text-sm font-bold text-slate-950 dark:text-white"> <h3 class="text-sm font-bold text-slate-950 dark:text-white">
@@ -547,6 +590,7 @@ component PublicFooter {
</li> </li>
</ul> </ul>
</div> </div>
{/if}
</slot> </slot>
</nav> </nav>
</div> </div>
@@ -570,6 +614,9 @@ component PublicFooter {
</div> </div>
<div class="flex flex-wrap items-center gap-x-5 gap-y-3"> <div class="flex flex-wrap items-center gap-x-5 gap-y-3">
{#if legalLinks.length > 0}
{#each legalLinks as item}<a href="{item.href}" target="{item.target || '_self'}" class="footer-bottom-link">{item.label}</a>{/each}
{:else}
<a <a
href="/privacy" href="/privacy"
class="footer-bottom-link" class="footer-bottom-link"
@@ -595,27 +642,30 @@ component PublicFooter {
href="/legal" href="/legal"
class="footer-bottom-link" class="footer-bottom-link"
> >
Legal {legalTitle}
</a> </a>
{/if}
<button data-show="showThemeToggle" <button data-show="showCookiePreferences"
type="button" type="button"
data-open-cookie-preferences data-open-cookie-preferences
class="footer-bottom-link" class="footer-bottom-link"
> >
Cookie preferences {cookiePreferencesLabel}
</button> </button>
<button <button data-show="showThemeToggle"
type="button" type="button"
data-wire-theme-toggle data-wire-theme-toggle
aria-label="{themeToggleLabel}"
title="{themeToggleLabel}"
class="inline-flex items-center gap-2 rounded-lg text-xs font-semibold text-slate-500 transition hover:text-indigo-700 dark:text-slate-400 dark:hover:text-indigo-300" class="inline-flex items-center gap-2 rounded-lg text-xs font-semibold text-slate-500 transition hover:text-indigo-700 dark:text-slate-400 dark:hover:text-indigo-300"
> >
<span class="icon-[lucide--moon] h-3.5 w-3.5 dark:hidden" aria-hidden="true" ></span> <span class="icon-[lucide--moon] h-3.5 w-3.5 dark:hidden" aria-hidden="true" ></span>
<span class="icon-[lucide--sun] hidden h-3.5 w-3.5 dark:block" aria-hidden="true" ></span> <span class="icon-[lucide--sun] hidden h-3.5 w-3.5 dark:block" aria-hidden="true" ></span>
Theme {themeLabel}
</button> </button>
</div> </div>
</div> </div>
+50 -9
View File
@@ -1,4 +1,15 @@
component PublicHeader { component PublicHeader {
types {
interface PublicHeaderNavigationItem {
label: string
href: string
ariaLabel?: string
target?: string
}
interface PublicHeaderActionItem extends PublicHeaderNavigationItem {
variant?: "primary" | "secondary" | "ghost"
}
}
props { props {
class: string = "" class: string = ""
homeHref: string = "/" homeHref: string = "/"
@@ -22,6 +33,14 @@ component PublicHeader {
solutionsLabel: string = "Solutions" solutionsLabel: string = "Solutions"
developersLabel: string = "Developers" developersLabel: string = "Developers"
resourcesLabel: string = "Resources" resourcesLabel: string = "Resources"
navigationAriaLabel: string = "Main navigation"
themeToggleLabel: string = "Toggle color theme"
mobileMenuOpenLabel: string = "Open navigation menu"
mobileMenuCloseLabel: string = "Close navigation menu"
navigationItems: PublicHeaderNavigationItem[] = []
actionItems: PublicHeaderActionItem[] = []
showNavigation: boolean = true
showMobileThemeToggle: boolean = true
} }
state activeMenu = "" state activeMenu = ""
state mobileMenuOpen = false state mobileMenuOpen = false
@@ -71,10 +90,16 @@ component PublicHeader {
<!-- Desktop navigation --> <!-- Desktop navigation -->
<nav <nav
aria-label="Main navigation" aria-label="{navigationAriaLabel}"
data-show="showNavigation"
class="hidden items-center gap-1 lg:flex" class="hidden items-center gap-1 lg:flex"
> >
<slot name="navigation"> <slot name="navigation">
{#if navigationItems.length > 0}
{#each navigationItems as item}
<a href="{item.href}" aria-label="{item.ariaLabel || item.label}" target="{item.target || '_self'}" class="inline-flex h-10 items-center rounded-xl px-3.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-white">{item.label}</a>
{/each}
{:else}
<!-- Product --> <!-- Product -->
<button <button
type="button" type="button"
@@ -171,12 +196,18 @@ component PublicHeader {
> >
{pricingLabel} {pricingLabel}
</a> </a>
{/if}
</slot> </slot>
</nav> </nav>
<!-- Desktop actions --> <!-- Desktop actions -->
<div class="hidden items-center gap-2 lg:flex"> <div class="hidden items-center gap-2 lg:flex">
<slot name="actions"> <slot name="actions">
{#if actionItems.length > 0}
{#each actionItems as item}
<a href="{item.href}" aria-label="{item.ariaLabel || item.label}" target="{item.target || '_self'}" class="wire-public-header-action wire-public-header-action--{item.variant || 'secondary'}">{item.label}{#if item.variant === 'primary'}<span aria-hidden="true">→</span>{/if}</a>
{/each}
{:else}
<a data-show="showStatus" <a data-show="showStatus"
href="{statusHref}" href="{statusHref}"
class="mr-1 inline-flex items-center gap-2 rounded-xl px-3 py-2 text-xs font-semibold text-slate-500 transition hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-white/5 dark:hover:text-white" class="mr-1 inline-flex items-center gap-2 rounded-xl px-3 py-2 text-xs font-semibold text-slate-500 transition hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-white/5 dark:hover:text-white"
@@ -192,8 +223,8 @@ component PublicHeader {
<button data-show="showThemeToggle" <button data-show="showThemeToggle"
type="button" type="button"
data-wire-theme-toggle data-wire-theme-toggle
aria-label="Toggle color theme" aria-label="{themeToggleLabel}"
title="Toggle color theme" title="{themeToggleLabel}"
class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-white/20 dark:hover:bg-white/10 dark:hover:text-white" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-white/20 dark:hover:bg-white/10 dark:hover:text-white"
> >
<span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span> <span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span>
@@ -201,7 +232,7 @@ component PublicHeader {
<span class="icon-[lucide--sun] hidden h-4 w-4 dark:block" aria-hidden="true" ></span> <span class="icon-[lucide--sun] hidden h-4 w-4 dark:block" aria-hidden="true" ></span>
<span class="sr-only"> <span class="sr-only">
Toggle color theme {themeToggleLabel}
</span> </span>
</button> </button>
@@ -220,16 +251,17 @@ component PublicHeader {
<span class="icon-[lucide--arrow-right] h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span> <span class="icon-[lucide--arrow-right] h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span>
</a> </a>
{/if}
</slot> </slot>
</div> </div>
<!-- Mobile actions --> <!-- Mobile actions -->
<div class="flex items-center gap-2 lg:hidden"> <div class="flex items-center gap-2 lg:hidden">
<button <button data-show="showThemeToggle && showMobileThemeToggle"
type="button" type="button"
data-wire-theme-toggle data-wire-theme-toggle
aria-label="Toggle color theme" aria-label="{themeToggleLabel}"
title="Toggle color theme" title="{themeToggleLabel}"
class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
> >
<span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span> <span class="icon-[lucide--moon] h-4 w-4 dark:hidden" aria-hidden="true" ></span>
@@ -240,7 +272,7 @@ component PublicHeader {
<button <button
type="button" type="button"
@click="mobileMenuOpen = !mobileMenuOpen" @click="mobileMenuOpen = !mobileMenuOpen"
aria-label="{mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}" aria-label="{mobileMenuOpen ? mobileMenuCloseLabel : mobileMenuOpenLabel}"
aria-expanded="{mobileMenuOpen}" aria-expanded="{mobileMenuOpen}"
aria-controls="public-mobile-navigation" aria-controls="public-mobile-navigation"
class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-700 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:hover:bg-white/10 dark:hover:text-white" class="grid h-10 w-10 place-items-center rounded-xl border border-slate-200 bg-white text-slate-700 shadow-sm transition hover:bg-slate-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:hover:bg-white/10 dark:hover:text-white"
@@ -258,7 +290,7 @@ component PublicHeader {
</div> </div>
<!-- Outside-click backdrop --> <!-- Outside-click backdrop -->
<button type="button" data-show="activeMenu !== ''" @click="activeMenu = ''" aria-label="Close navigation menu" class="fixed inset-0 z-40 hidden cursor-default bg-transparent lg:block" class:hidden="activeMenu === ''" class:block="activeMenu !== ''" ></button> <button type="button" data-show="activeMenu !== ''" @click="activeMenu = ''" aria-label="{mobileMenuCloseLabel}" class="fixed inset-0 z-40 hidden cursor-default bg-transparent lg:block" class:hidden="activeMenu === ''" class:block="activeMenu !== ''" ></button>
<!-- Desktop mega menus --> <!-- Desktop mega menus -->
<div <div
@@ -302,7 +334,16 @@ component PublicHeader {
class:hidden="!mobileMenuOpen" class:hidden="!mobileMenuOpen"
class:block="mobileMenuOpen" class:block="mobileMenuOpen"
> >
{#if navigationItems.length > 0 || actionItems.length > 0}
<nav aria-label="{navigationAriaLabel}" class="border-t border-slate-200 bg-white px-5 py-5 shadow-xl dark:border-white/10 dark:bg-slate-950">
<div class="grid gap-2">
{#each navigationItems as item}<a href="{item.href}" target="{item.target || '_self'}" class="rounded-xl px-4 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-white/5">{item.label}</a>{/each}
{#each actionItems as item}<a href="{item.href}" target="{item.target || '_self'}" class="wire-public-header-action wire-public-header-action--{item.variant || 'secondary'}">{item.label}</a>{/each}
</div>
</nav>
{:else}
<PublicMobileNavigation /> <PublicMobileNavigation />
{/if}
</div> </div>
</header> </header>
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+38
View File
@@ -1186,6 +1186,44 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.wire-public-header-action {
display: inline-flex;
min-height: 2.5rem;
align-items: center;
justify-content: center;
gap: 0.5rem;
border: 1px solid transparent;
border-radius: 0.75rem;
padding: 0.625rem 1rem;
color: var(--wire-color-text);
font-size: 0.875rem;
font-weight: 700;
text-decoration: none;
transition:
background var(--wire-motion-base),
border-color var(--wire-motion-base),
color var(--wire-motion-base),
transform var(--wire-motion-base) var(--wire-ease-emphasized);
}
.wire-public-header-action--primary {
color: var(--wire-color-primary-contrast);
background: var(--wire-color-primary);
box-shadow: 0 0.75rem 1.5rem color-mix(in srgb, var(--wire-color-primary) 20%, transparent);
}
.wire-public-header-action--primary:hover {
background: var(--wire-color-primary-hover);
transform: translateY(-1px);
}
.wire-public-header-action--secondary {
border-color: var(--wire-color-border);
background: var(--wire-color-surface);
}
.wire-public-header-action--secondary:hover,
.wire-public-header-action--ghost:hover {
color: var(--wire-color-primary);
background: var(--wire-color-surface-2);
}
.wire-auth-field__input { .wire-auth-field__input {
width: 100%; width: 100%;
min-height: 3.5rem; min-height: 3.5rem;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.73", "version": "0.2.74",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
const version = "0.2.73"; const version = "0.2.74";
const dependencyGroups = [ const dependencyGroups = [
"dependencies", "dependencies",