diff --git a/')) b/')) deleted file mode 100644 index e69de29b..00000000 diff --git a/.gitignore b/.gitignore index 2dedf281..bb140a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ dist/ *.db-wal .DS_Store bun.lockb + +.publish/ diff --git a/.publish/ai/package.json b/.publish/ai/package.json index fec6674b..a016b1b4 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index d07e99bb..2ca0954c 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 59d8a582..af26cd6f 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,18 +28,18 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.3.6", - "@wrnexus/router": "^0.3.6", - "@wrnexus/csr": "^0.3.6", - "@wrnexus/compiler": "^0.3.6", - "@wrnexus/styles": "^0.3.6", - "@wrnexus/dev-server": "^0.3.6", - "@wrnexus/ui": "^0.3.6", - "@wrnexus/validation": "^0.3.6", - "@wrnexus/i18n": "^0.3.6", - "@wrnexus/db": "^0.3.6", - "@wrnexus/plugin": "^0.3.6", - "@wrnexus/syntax": "^0.3.6" + "@wrnexus/core": "^0.4.0", + "@wrnexus/router": "^0.4.0", + "@wrnexus/csr": "^0.4.0", + "@wrnexus/compiler": "^0.4.0", + "@wrnexus/styles": "^0.4.0", + "@wrnexus/dev-server": "^0.4.0", + "@wrnexus/ui": "^0.4.0", + "@wrnexus/validation": "^0.4.0", + "@wrnexus/i18n": "^0.4.0", + "@wrnexus/db": "^0.4.0", + "@wrnexus/plugin": "^0.4.0", + "@wrnexus/syntax": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 79ffb6bb..fd90ea93 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/syntax": "^0.3.6" + "@wrnexus/syntax": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/core/package.json b/.publish/core/package.json index f2b2a288..f08406c7 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 84f982a3..b7d2f731 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.3.6" + "@wrnexus/core": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index c47ffd6f..68c35266 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 6582a715..94e99a33 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,20 +25,20 @@ } }, "dependencies": { - "@wrnexus/core": "^0.3.6", - "@wrnexus/dev-toolbar": "^0.3.6", - "@wrnexus/router": "^0.3.6", - "@wrnexus/ssr": "^0.3.6", - "@wrnexus/csr": "^0.3.6", - "@wrnexus/compiler": "^0.3.6", - "@wrnexus/styles": "^0.3.6", - "@wrnexus/ui": "^0.3.6", - "@wrnexus/validation": "^0.3.6", - "@wrnexus/i18n": "^0.3.6", - "@wrnexus/db": "^0.3.6", - "@wrnexus/pubsub": "^0.3.6", - "@wrnexus/uploader": "^0.3.6", - "@wrnexus/plugin": "^0.3.6" + "@wrnexus/core": "^0.4.0", + "@wrnexus/dev-toolbar": "^0.4.0", + "@wrnexus/router": "^0.4.0", + "@wrnexus/ssr": "^0.4.0", + "@wrnexus/csr": "^0.4.0", + "@wrnexus/compiler": "^0.4.0", + "@wrnexus/styles": "^0.4.0", + "@wrnexus/ui": "^0.4.0", + "@wrnexus/validation": "^0.4.0", + "@wrnexus/i18n": "^0.4.0", + "@wrnexus/db": "^0.4.0", + "@wrnexus/pubsub": "^0.4.0", + "@wrnexus/uploader": "^0.4.0", + "@wrnexus/plugin": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/dev-toolbar/package.json b/.publish/dev-toolbar/package.json index 26cc64f0..6d52fcbc 100644 --- a/.publish/dev-toolbar/package.json +++ b/.publish/dev-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-toolbar", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 22e21e35..16ab1dc1 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 98cc99f6..a2b29257 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.3.6" + "@wrnexus/core": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index ac451f53..fb46d7ff 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.3.6" + "@wrnexus/core": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index edfba941..a84a0268 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 406b8c2e..25cf6d9f 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.3.6" + "@wrnexus/native": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 095e65cb..4b3ce4fe 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 0e87a10f..23fa12d5 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/plugin/package.json b/.publish/plugin/package.json index 66eae4c5..e2e278a6 100644 --- a/.publish/plugin/package.json +++ b/.publish/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/plugin", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/plugin — part of the WrNexus framework.", "license": "MIT", @@ -18,10 +18,22 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./types": { + "types": "./dist/types.d.ts", + "import": "./dist/types.js" + }, + "./manifest": { + "types": "./dist/manifest.d.ts", + "import": "./dist/manifest.js" + }, + "./discovery": { + "types": "./dist/discovery.d.ts", + "import": "./dist/discovery.js" } }, "dependencies": { - "@wrnexus/syntax": "^0.3.6" + "@wrnexus/syntax": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index 95a26c52..d1a64b77 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 9923c596..c58c89ca 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 26c097d5..a02d588b 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 6a50eaef..42f954d3 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.3.6", - "@wrnexus/core": "^0.3.6" + "@wrnexus/compiler": "^0.4.0", + "@wrnexus/core": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index d4baba54..3f36289a 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.3.6" + "@wrnexus/core": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 07b71553..279f9293 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,9 +21,9 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.3.6", - "@wrnexus/core": "^0.3.6", - "@wrnexus/plugin": "^0.3.6" + "@wrnexus/uploader": "^0.4.0", + "@wrnexus/core": "^0.4.0", + "@wrnexus/plugin": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/syntax/package.json b/.publish/syntax/package.json index 2148cb70..75b8d195 100644 --- a/.publish/syntax/package.json +++ b/.publish/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/syntax — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/test/package.json b/.publish/test/package.json index d67727c6..3ba16fdd 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index fb2e8357..a10c65c7 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/COMPONENTS.md b/.publish/ui/COMPONENTS.md index c45638be..a3163320 100644 --- a/.publish/ui/COMPONENTS.md +++ b/.publish/ui/COMPONENTS.md @@ -36,9 +36,9 @@ Theme-aware, responsive copy markup component. Theme-aware, responsive input number component. - Mount: `data-component="InputNumber"` -- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Input Number"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "number"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""` +- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`, `id: string = ""`, `name: string = "quantity"`, `value: number = 0`, `min: string = ""`, `max: string = ""`, `step: number = 1`, `precision: string = "auto"`, `label: string = ""`, `description: string = ""`, `helpText: string = ""`, `error: string = ""`, `invalid: boolean = false`, `prefix: string = ""`, `suffix: string = ""`, `placeholder: string = ""`, `autocomplete: string = "off"`, `inputMode: string = "decimal"`, `ariaLabel: string = ""`, `required: boolean = false`, `disabled: boolean = false`, `inputDisabled: boolean = false`, `buttonsDisabled: boolean = false`, `readonly: boolean = false`, `allowInput: boolean = true`, `keyboard: boolean = true`, `wheel: boolean = false`, `clamp: boolean = true`, `fullWidth: boolean = false`, `showButtons: boolean = true`, `showValidationMessage: boolean = true`, `decrementLabel: string = "Decrease value"`, `incrementLabel: string = "Increase value"`, `controlsLabel: string = "Quantity controls"`, `requiredMessage: string = "A value is required."`, `minMessage: string = "Value is below the minimum."`, `maxMessage: string = "Value is above the maximum."` - Slots: None -- Events: None +- Events: `input`, `change`, `increment`, `decrement` ### PinInput diff --git a/.publish/ui/README.md b/.publish/ui/README.md index 017f1837..14e79db6 100644 --- a/.publish/ui/README.md +++ b/.publish/ui/README.md @@ -15,7 +15,7 @@ driven by `var(--wire-*)` theme tokens, so components restyle instantly when the theme changes. The tiny JS surface (`src/index.ts`) exists only so the toolchain (CLI build + dev server) can locate the component directory and stylesheet. -The complete PDF-aligned catalog currently contains **891 components**. The +The complete PDF-aligned catalog currently contains **85 components**. The generated `COMPONENTS.md` and `component-reference.json` files document every mount name, prop, inferred type, default/required status, slot, event, category, and source file directly from the packaged `.wrn` source. @@ -35,10 +35,11 @@ In practice you rarely install this directly: `@wrnexus/cli` and ## Components -Components live as `.wrn` files under `packages/ui/components/`. The mount name -is the **lowercase file basename** (e.g. `button.wrn` → `data-component="button"`). -Each accepts a `class` prop (appended to its root element) and most render their -body from either a named prop or the default slot. +Components live as `.wrn` files under `packages/ui/components/`. The canonical mount +name comes from the component declaration (for example, `component Button` mounts as +`data-component="Button"`). Component lookup is case-insensitive, so existing lowercase +mounts continue to work. Each component accepts a `class` prop (appended to its root +element), and most render their body from either a named prop or the default slot. ### Layout @@ -91,16 +92,17 @@ equivalent is exported as `@wrnexus/ui/component-reference.json`. ## API -The JS module (`@wrnexus/ui`) exposes four helpers used by the build tooling to +The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to locate the component assets. There is no component code to import — the components are `.wrn` files rendered server-side. -| Export | Signature | Returns | -| ------------------ | ---------------- | ------------------------------------------------------------------------------------------ | -| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). | -| `uiCssPath` | `() => string` | Absolute path to `ui.css`. | -| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). | -| `uiComponentNames` | `() => string[]` | Sorted list of built-in component names (e.g. for `wrnexus eject` listing). | +| Export | Signature | Returns | +| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ | +| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). | +| `uiCssPath` | `() => string` | Absolute path to `ui.css`. | +| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). | +| `uiComponentNames` | `() => string[]` | Sorted list of declared built-in component names. | +| `uiComponentPath` | `(name: string) => string` | Absolute source path for a declared component name or case-insensitive alias. | ### `./ui.css` asset export diff --git a/.publish/ui/component-reference.json b/.publish/ui/component-reference.json index 86bbad7c..6c8ed5ff 100644 --- a/.publish/ui/component-reference.json +++ b/.publish/ui/component-reference.json @@ -517,7 +517,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Alert.wrn" + "source": "components/alert.wrn" }, { "name": "Avatar", @@ -570,7 +570,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Avatar.wrn" + "source": "components/avatar.wrn" }, { "name": "AvatarGroup", @@ -676,7 +676,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Badge.wrn" + "source": "components/badge.wrn" }, { "name": "Blockquote", @@ -943,7 +943,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Button.wrn" + "source": "components/button.wrn" }, { "name": "ButtonGroup", @@ -1049,7 +1049,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Card.wrn" + "source": "components/card.wrn" }, { "name": "Carousel", @@ -1267,7 +1267,7 @@ ], "slots": [], "events": [], - "source": "components/Checkbox.wrn" + "source": "components/checkbox.wrn" }, { "name": "Clipboard", @@ -1754,7 +1754,7 @@ ], "slots": [], "events": ["search", "select", "change", "clear", "open", "close", "load", "error"], - "source": "components/ComboBox.wrn" + "source": "components/Combobox.wrn" }, { "name": "Confetti", @@ -1854,7 +1854,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Container.wrn" + "source": "components/container.wrn" }, { "name": "ContextMenu", @@ -2338,7 +2338,7 @@ ], "slots": [], "events": [], - "source": "components/Divider.wrn" + "source": "components/divider.wrn" }, { "name": "DragAndDrop", @@ -2751,7 +2751,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Grid.wrn" + "source": "components/grid.wrn" }, { "name": "Image", @@ -2905,7 +2905,7 @@ ], "slots": [], "events": [], - "source": "components/Input.wrn" + "source": "components/input.wrn" }, { "name": "InputGroup", @@ -3015,34 +3015,34 @@ "default": "\"primary\"" }, { - "name": "label", + "name": "variant", "type": "string", "required": false, - "default": "\"Input Number\"" + "default": "\"default\"" + }, + { + "name": "class", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "id", + "type": "string", + "required": false, + "default": "\"\"" }, { "name": "name", "type": "string", "required": false, - "default": "\"\"" + "default": "\"quantity\"" }, { "name": "value", - "type": "string", + "type": "number", "required": false, - "default": "\"\"" - }, - { - "name": "placeholder", - "type": "string", - "required": false, - "default": "\"\"" - }, - { - "name": "type", - "type": "string", - "required": false, - "default": "\"number\"" + "default": "0" }, { "name": "min", @@ -3058,16 +3058,82 @@ }, { "name": "step", + "type": "number", + "required": false, + "default": "1" + }, + { + "name": "precision", + "type": "string", + "required": false, + "default": "\"auto\"" + }, + { + "name": "label", "type": "string", "required": false, "default": "\"\"" }, { - "name": "disabled", + "name": "description", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "helpText", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "error", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "invalid", "type": "boolean", "required": false, "default": "false" }, + { + "name": "prefix", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "suffix", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "placeholder", + "type": "string", + "required": false, + "default": "\"\"" + }, + { + "name": "autocomplete", + "type": "string", + "required": false, + "default": "\"off\"" + }, + { + "name": "inputMode", + "type": "string", + "required": false, + "default": "\"decimal\"" + }, + { + "name": "ariaLabel", + "type": "string", + "required": false, + "default": "\"\"" + }, { "name": "required", "type": "boolean", @@ -3075,14 +3141,110 @@ "default": "false" }, { - "name": "class", + "name": "disabled", + "type": "boolean", + "required": false, + "default": "false" + }, + { + "name": "inputDisabled", + "type": "boolean", + "required": false, + "default": "false" + }, + { + "name": "buttonsDisabled", + "type": "boolean", + "required": false, + "default": "false" + }, + { + "name": "readonly", + "type": "boolean", + "required": false, + "default": "false" + }, + { + "name": "allowInput", + "type": "boolean", + "required": false, + "default": "true" + }, + { + "name": "keyboard", + "type": "boolean", + "required": false, + "default": "true" + }, + { + "name": "wheel", + "type": "boolean", + "required": false, + "default": "false" + }, + { + "name": "clamp", + "type": "boolean", + "required": false, + "default": "true" + }, + { + "name": "fullWidth", + "type": "boolean", + "required": false, + "default": "false" + }, + { + "name": "showButtons", + "type": "boolean", + "required": false, + "default": "true" + }, + { + "name": "showValidationMessage", + "type": "boolean", + "required": false, + "default": "true" + }, + { + "name": "decrementLabel", "type": "string", "required": false, - "default": "\"\"" + "default": "\"Decrease value\"" + }, + { + "name": "incrementLabel", + "type": "string", + "required": false, + "default": "\"Increase value\"" + }, + { + "name": "controlsLabel", + "type": "string", + "required": false, + "default": "\"Quantity controls\"" + }, + { + "name": "requiredMessage", + "type": "string", + "required": false, + "default": "\"A value is required.\"" + }, + { + "name": "minMessage", + "type": "string", + "required": false, + "default": "\"Value is below the minimum.\"" + }, + { + "name": "maxMessage", + "type": "string", + "required": false, + "default": "\"Value is above the maximum.\"" } ], "slots": [], - "events": [], + "events": ["input", "change", "increment", "decrement"], "source": "components/InputNumber.wrn" }, { @@ -4039,7 +4201,7 @@ ], "slots": [], "events": [], - "source": "components/Progress.wrn" + "source": "components/progress.wrn" }, { "name": "Radio", @@ -4098,7 +4260,7 @@ ], "slots": [], "events": [], - "source": "components/Radio.wrn" + "source": "components/radio.wrn" }, { "name": "RangeSlider", @@ -4453,7 +4615,7 @@ ], "slots": [], "events": [], - "source": "components/Select.wrn" + "source": "components/select.wrn" }, { "name": "Sidebar", @@ -4547,7 +4709,7 @@ ], "slots": [], "events": [], - "source": "components/Skeleton.wrn" + "source": "components/skeleton.wrn" }, { "name": "Spinner", @@ -4588,7 +4750,7 @@ ], "slots": [], "events": [], - "source": "components/Spinner.wrn" + "source": "components/spinner.wrn" }, { "name": "Stepper", @@ -4926,7 +5088,7 @@ ], "slots": [], "events": [], - "source": "components/Switch.wrn" + "source": "components/switch.wrn" }, { "name": "Table", @@ -4979,7 +5141,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Table.wrn" + "source": "components/table.wrn" }, { "name": "Tabs", @@ -5103,7 +5265,7 @@ ], "slots": [], "events": [], - "source": "components/Textarea.wrn" + "source": "components/textarea.wrn" }, { "name": "Timeline", @@ -5654,7 +5816,7 @@ ], "slots": ["default"], "events": [], - "source": "components/Tooltip.wrn" + "source": "components/tooltip.wrn" }, { "name": "TreeView", diff --git a/.publish/ui/components/InputNumber.wrn b/.publish/ui/components/InputNumber.wrn index 4b205de1..226922fd 100644 --- a/.publish/ui/components/InputNumber.wrn +++ b/.publish/ui/components/InputNumber.wrn @@ -1,20 +1,623 @@ component InputNumber { - props { - size = "default" - color = "primary" - label = "Input Number" - name = "" - value = "" - placeholder = "" - type = "number" - min = "" - max = "" - step = "" - disabled = false - required = false - class = "" - } - view { - - } + props { + size = "default" + color = "primary" + variant = "default" + class = "" + + id = "" + name = "quantity" + value = 0 + min = "" + max = "" + step = 1 + precision = "auto" + + label = "" + description = "" + helpText = "" + error = "" + invalid = false + + prefix = "" + suffix = "" + placeholder = "" + autocomplete = "off" + inputMode = "decimal" + ariaLabel = "" + + required = false + disabled = false + inputDisabled = false + buttonsDisabled = false + readonly = false + allowInput = true + keyboard = true + wheel = false + clamp = true + fullWidth = false + showButtons = true + showValidationMessage = true + + decrementLabel = "Decrease value" + incrementLabel = "Increase value" + controlsLabel = "Quantity controls" + requiredMessage = "A value is required." + minMessage = "Value is below the minimum." + maxMessage = "Value is above the maximum." + + @event input = function + @event change = function + @event increment = function + @event decrement = function + } + + state currentValue = value + state committedValue = value + + functions { + function inputId() { + if (id !== "") { + return id + } + + if (name !== "") { + return name + } + + return "input-number" + } + + function descriptionId() { + return inputId() + "-description" + } + + function messageId() { + return inputId() + "-message" + } + + function componentColor() { + if (color === "secondary") { + return "var(--wire-color-secondary)" + } + + if (color === "success") { + return "var(--wire-color-success)" + } + + if (color === "warning") { + return "var(--wire-color-warning)" + } + + if (color === "danger") { + return "var(--wire-color-danger)" + } + + if (color === "info") { + return "var(--wire-color-info)" + } + + return "var(--wire-color-primary)" + } + + function hasMin() { + return min !== "" && min !== null && min !== undefined + } + + function hasMax() { + return max !== "" && max !== null && max !== undefined + } + + function isBlank() { + return currentValue === "" || currentValue === null || currentValue === undefined + } + + function normalizedStep() { + return Number(step) > 0 ? Number(step) : 1 + } + + function inferredPrecision() { + if (precision !== "auto" && precision !== "") { + return Math.max(0, Number(precision) || 0) + } + + if (String(normalizedStep()).includes(".")) { + return String(normalizedStep()).split(".")[1].length + } + + return 0 + } + + function precisionFactor() { + return Math.pow(10, inferredPrecision()) + } + + function roundValue(nextValue) { + return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor() + } + + function clampValue(nextValue) { + nextValue = Number(nextValue) + + if (hasMin() && nextValue < Number(min)) { + nextValue = Number(min) + } + + if (hasMax() && nextValue > Number(max)) { + nextValue = Number(max) + } + + return roundValue(nextValue) + } + + function isBelowMin() { + return !isBlank() && hasMin() && Number(currentValue) < Number(min) + } + + function isAboveMax() { + return !isBlank() && hasMax() && Number(currentValue) > Number(max) + } + + function isInvalid() { + return ( + invalid || + error !== "" || + (required && isBlank()) || + isBelowMin() || + isAboveMax() + ) + } + + function validationMessage() { + if (error !== "") { + return error + } + + if (required && isBlank()) { + return requiredMessage + } + + if (isBelowMin()) { + return minMessage + } + + if (isAboveMax()) { + return maxMessage + } + + return "" + } + + function hasMessage() { + return ( + helpText !== "" || + (showValidationMessage && isInvalid() && validationMessage() !== "") + ) + } + + function describedBy() { + if (description !== "" && hasMessage()) { + return descriptionId() + " " + messageId() + } + + if (description !== "") { + return descriptionId() + } + + if (hasMessage()) { + return messageId() + } + + return "" + } + + function decrementDisabled() { + return ( + disabled || + readonly || + buttonsDisabled || + (hasMin() && !isBlank() && Number(currentValue) <= Number(min)) + ) + } + + function incrementDisabled() { + return ( + disabled || + readonly || + buttonsDisabled || + (hasMax() && !isBlank() && Number(currentValue) >= Number(max)) + ) + } + + function dispatchInputNumberEvent( + sourceEvent, + eventName, + action, + previousValue, + root, + customEvent + ) { + root = sourceEvent.currentTarget.closest("[data-wrn-input-number]") + + if (!root && sourceEvent.target) { + root = sourceEvent.target.closest("[data-wrn-input-number]") + } + + if (!root) { + return + } + + customEvent = document.createEvent("CustomEvent") + customEvent.initCustomEvent(eventName, true, false, { + component: "InputNumber", + name: name, + value: currentValue, + previousValue: previousValue, + action: action, + min: hasMin() ? Number(min) : null, + max: hasMax() ? Number(max) : null, + step: normalizedStep(), + valid: !isInvalid() + }) + root.dispatchEvent(customEvent) + } + + function applyControlValue(nextValue, action, sourceEvent, previousValue) { + previousValue = currentValue + currentValue = clampValue(nextValue) + committedValue = currentValue + + dispatchInputNumberEvent( + sourceEvent, + "input", + action, + previousValue + ) + dispatchInputNumberEvent( + sourceEvent, + "change", + action, + previousValue + ) + dispatchInputNumberEvent( + sourceEvent, + action, + action, + previousValue + ) + } + + function incrementValue(sourceEvent, nextValue) { + if (incrementDisabled()) { + return + } + + if (isBlank()) { + nextValue = hasMin() ? Number(min) : normalizedStep() + } else { + nextValue = + Number(currentValue) + + normalizedStep() * (sourceEvent.shiftKey ? 10 : 1) + } + + applyControlValue(nextValue, "increment", sourceEvent) + } + + function decrementValue(sourceEvent, nextValue) { + if (decrementDisabled()) { + return + } + + if (isBlank()) { + nextValue = hasMax() ? Number(max) : -normalizedStep() + } else { + nextValue = + Number(currentValue) - + normalizedStep() * (sourceEvent.shiftKey ? 10 : 1) + } + + applyControlValue(nextValue, "decrement", sourceEvent) + } + + function handleInput(sourceEvent, previousValue, nextValue) { + sourceEvent.stopPropagation() + previousValue = currentValue + nextValue = sourceEvent.target.value + + if (nextValue === "") { + currentValue = "" + } else if (!Number.isNaN(Number(nextValue))) { + currentValue = roundValue(Number(nextValue)) + } + + dispatchInputNumberEvent( + sourceEvent, + "input", + "input", + previousValue + ) + } + + function handleChange(sourceEvent, previousValue) { + sourceEvent.stopPropagation() + previousValue = committedValue + + if (!isBlank()) { + currentValue = clamp + ? clampValue(currentValue) + : roundValue(currentValue) + } + + committedValue = currentValue + + dispatchInputNumberEvent( + sourceEvent, + "change", + "change", + previousValue + ) + } + + function handleKeydown(sourceEvent) { + if ( + !keyboard || + disabled || + readonly || + inputDisabled || + !allowInput + ) { + return + } + + if (sourceEvent.key === "ArrowUp") { + sourceEvent.preventDefault() + incrementValue(sourceEvent) + } else if (sourceEvent.key === "ArrowDown") { + sourceEvent.preventDefault() + decrementValue(sourceEvent) + } else if (sourceEvent.key === "Home" && hasMin()) { + sourceEvent.preventDefault() + applyControlValue(Number(min), "decrement", sourceEvent) + } else if (sourceEvent.key === "End" && hasMax()) { + sourceEvent.preventDefault() + applyControlValue(Number(max), "increment", sourceEvent) + } + } + + function handleWheel(sourceEvent) { + if ( + !wheel || + disabled || + readonly || + inputDisabled || + !allowInput + ) { + return + } + + sourceEvent.preventDefault() + + if (sourceEvent.deltaY < 0) { + incrementValue(sourceEvent) + } else if (sourceEvent.deltaY > 0) { + decrementValue(sourceEvent) + } + } + } + + view { +
+ {#if label !== "" && variant !== "labeled" && variant !== "seat"} + + {/if} + + {#if description !== "" && variant !== "labeled" && variant !== "seat"} +

+ {description} +

+ {/if} + +
+ {#if variant === "horizontal" && showButtons} + + {/if} + +
+ {#if variant === "labeled" || variant === "seat"} +
+ {#if label !== ""} + + {/if} + + {#if description !== ""} + + {description} + + {/if} +
+ {/if} + +
+ {#if prefix !== ""} + + {/if} + + + + {#if suffix !== ""} + + {/if} +
+
+ + {#if variant === "horizontal" && showButtons} + + {:else} + {#if showButtons} +
+ + + +
+ {/if} + {/if} +
+ + {#if showValidationMessage && isInvalid() && validationMessage() !== ""} + + {/if} + + {#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""} +

+ {helpText} +

+ {/if} +
+ } } diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 2a29a2c7..c7b21836 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -26,7 +26,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.3.6" + "@wrnexus/core": "^0.4.0" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 87574325..3f3da297 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.3.6" + "@wrnexus/core": "^0.4.0" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 066ce231..79d68fc0 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.3.6", + "version": "0.4.0", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/AUDIO-PLAYBACK-FIX.md b/AUDIO-PLAYBACK-FIX.md deleted file mode 100644 index d13f3d7f..00000000 --- a/AUDIO-PLAYBACK-FIX.md +++ /dev/null @@ -1,51 +0,0 @@ -# WRNexusJS CAPTCHA audio playback fix - -This patch fixes audio CAPTCHA playback in production and bundled server builds. - -## Root cause - -`AssetAudioRenderer` originally located WAV files only from `import.meta.url`. After -WRNexusJS bundles the server into `dist/server.js`, that URL points to the app build -folder instead of `packages/captcha`, so `/api/captcha/audio/...` fails while trying -to read the WAV clips. - -## Changes - -- Resolves audio assets from the installed `@wrnexus/captcha/audio` package entry. -- Supports `WRNEXUS_CAPTCHA_AUDIO_DIR` for custom deployments. -- Searches workspace and `node_modules` layouts as safe fallbacks. -- Keeps the browser `Audio` instance alive while it plays. -- Stops old audio when a new challenge is loaded. -- Returns a clear HTTP 503 and server log if audio rendering fails. -- Adds a real WAV rendering test. - -## Apply - -Extract this ZIP into the WRNexusJS repository root and replace the included files. -Then run: - -```bash -bun run typecheck -bun test packages/captcha -bun run --cwd examples/captcha-showcase check -``` - -Restart the server. If running the production build, rebuild first: - -```bash -bun run --cwd examples/captcha-showcase build -bun examples/captcha-showcase/dist/server.js -``` - -## Verify - -1. Open the browser Network panel. -2. Click **Listen**. -3. The `/api/captcha/audio/?key=...` request must return HTTP 200. -4. Its `content-type` must be `audio/wav`. - -For a nonstandard package layout, set an absolute path before starting the server: - -```powershell -$env:WRNEXUS_CAPTCHA_AUDIO_DIR="E:\WireJS\packages\captcha\assets\audio" -``` diff --git a/CAPTCHA-FIX8-README.md b/CAPTCHA-FIX8-README.md deleted file mode 100644 index 00172b5e..00000000 --- a/CAPTCHA-FIX8-README.md +++ /dev/null @@ -1,40 +0,0 @@ -# WRNexusJS CAPTCHA Fixed 8 - -Extract this ZIP directly into the WRNexusJS repository root and allow the included files to replace the current CAPTCHA files. - -## Added - -- 18 concrete generated CAPTCHA image styles -- `imageStyle="random"` -- `allowedStyles` and `excludedStyles` -- `randomizeStyle` -- Resolved style metadata and browser event details -- Easy, normal, hard, controlled-pool, and full renderer showcase at `/styles` -- Tailwind Iconify/Lucide showcase dependencies -- Faster CRC32 PNG encoding -- Safe floating-point line rendering for web, darts, distortion, wave, and scribble effects -- Tests for every renderer and style-pool rule - -## Supported styles - -`classic`, `collision`, `snow`, `corrosion`, `spiderweb`, `cross-shadow`, `split`, `split2`, `cut`, `darts`, `distortion`, `stitch`, `striped`, `wave`, `grid-noise`, `scribble`, `pixel`, `broken-lines`, and `random`. - -## After extraction - -```bash -bun install -bun run scripts/generate-ui-component-reference.mjs -bun run typecheck -bun test packages/captcha -bun test services/managed-captcha -bun run --cwd examples/captcha-showcase check -bun run --cwd examples/captcha-showcase dev -``` - -Open: - -```text -http://localhost:3000/styles -``` - -Use a hard browser refresh after restarting the development server. diff --git a/CAPTCHA-FIXED7-INSTRUCTIONS.md b/CAPTCHA-FIXED7-INSTRUCTIONS.md deleted file mode 100644 index 37d58bac..00000000 --- a/CAPTCHA-FIXED7-INSTRUCTIONS.md +++ /dev/null @@ -1,51 +0,0 @@ -# WRNexusJS CAPTCHA Fixed 7 - -Extract this patch directly into the WRNexusJS repository root and allow files to be replaced. - -## Added - -- `size="compact"`, `size="normal"`, and `size="big"` -- Backward-compatible `small`/`sm`, `large`/`lg`, and `compact="true"` aliases -- `showListen="true|false"` for the Listen button -- Existing `showAudio="true|false"` remains the broader audio-alternative switch -- Self-hosted `type="not-robot"` checkbox challenge -- Showcase examples and tests for all new behavior -- Managed API/OpenAPI support for `not-robot` - -## Usage - -```wrn - -``` - -```wrn - -``` - -The not-robot checkbox is server verified. It uses a one-time challenge, minimum completion time, honeypot, expiry, attempt limits, action binding, optional hostname/session/IP binding, and a single-use response token. It is a low-friction mode; high-risk traffic should escalate to a visual challenge or an external provider. - -## Regenerate and test - -```bash -bun run scripts/generate-ui-component-reference.mjs -bun run typecheck -bun test packages/captcha -bun run --cwd examples/captcha-showcase check -bun run --cwd examples/captcha-showcase dev -``` - -Hard-refresh the browser after restarting the development server. diff --git a/CHANGED-FILES.txt b/CHANGED-FILES.txt deleted file mode 100644 index c42421cf..00000000 --- a/CHANGED-FILES.txt +++ /dev/null @@ -1,9 +0,0 @@ -tsconfig.json -packages/styles/src/config.ts -packages/dev-server/package.json -packages/dev-server/src/hmr.ts -packages/dev-server/src/watch.ts -packages/dev-server/src/runtime.ts -packages/dev-server/src/index.ts -packages/dev-server/src/serve-entry.ts -packages/dev-toolbar/src/client/runtime.ts diff --git a/CHANGELOG-0.4.0.md b/CHANGELOG-0.4.0.md new file mode 100644 index 00000000..ef1d81e1 --- /dev/null +++ b/CHANGELOG-0.4.0.md @@ -0,0 +1,37 @@ +# WRNexusJS 0.4.0 + +## Package platform + +- Added automatic package plugin discovery from application dependencies and workspaces. +- Added package-owned components, client runtimes, assets, styles, routes, middleware, database migrations, and DevToolbar panels. +- Added duplicate/collision validation and strict discovery in development, build, doctor, and inspect flows. +- Added production content-hashed client runtimes, content-addressed package asset storage with stable public URLs, and page-scoped runtime injection. +- Added development bundling for TypeScript package runtimes and package-source HMR. +- Added CSR package runtime mount/unmount lifecycle. + +## CLI and diagnostics + +- Added `wrnexus inspect packages|plugins|routes|assets|runtimes|styles|migrations|bundle`. +- Added `wrnexus generate system `. +- Expanded production build reports with all route types, plugins, runtimes, assets, component directories, and package migrations. +- Added the 0.4 application migration and release verification scripts. +- Fixed duplicate historical migration IDs. + +## CAPTCHA + +- Removed manual `captcha.js` copying and script inclusion. +- Removed duplicated CAPTCHA components from `@wrnexus/ui` and the showcase application. +- CAPTCHA is now discovered from `@wrnexus/captcha` and injected only on pages that render it. +- Preserved all challenge modes, audio, protected-page flow, validated form, compact/normal/big modes, Listen controls, not-robot mode, disturbance controls, and image-style renderers. + +## Existing packages + +Every existing package received an exported advanced capability layer. See `docs/PACKAGE-UPGRADES-0.4.md` for the package-by-package inventory. + +## Fixes + +- Package runtime and asset path collisions now fail before serving. +- Production style processing fails closed by default. +- Page ETags now include structured script metadata. +- Package `.wrn` changes invalidate Tailwind output during HMR. +- Package migrations use the same ordered migration contract as application migrations. diff --git a/MANIFEST-0.4.0.md b/MANIFEST-0.4.0.md new file mode 100644 index 00000000..266e69cb --- /dev/null +++ b/MANIFEST-0.4.0.md @@ -0,0 +1,109 @@ +# WRNexusJS 0.4.0 upgrade manifest + +This replacement upgrades the uploaded WRNexusJS repository to the 0.4.0 platform baseline and preserves existing APIs where practical. + +Compared with the uploaded baseline archive, the final tree contains 45 added files, 214 modified files, and 15 removed obsolete or generated files. + +## Main platform additions + +- Automatic installed/workspace package discovery through `package.json` `wrnexus.plugin` +- Package-owned component directories +- Page-scoped client runtimes +- Virtual development assets with secure content types +- Hashed production runtime assets +- Package CSS entries and Tailwind scan sources +- Package page/API/realtime routes and middleware +- Package database migrations +- Package DevToolbar panels +- Dependency ordering, duplicate detection, cycle detection, and safe contribution validation +- Runtime mount/unmount lifecycle during CSR navigation +- Package-aware HMR and build reporting +- `wrnexus inspect` commands and `wrnexus generate system` +- 0.4 migration and compatibility tooling + +## CAPTCHA integration + +`@wrnexus/captcha` is now self-contained and automatically discovered. The old duplicate copies are removed from `@wrnexus/ui` and the showcase. The package contributes its own component, runtime, Tailwind scan source, and DevToolbar audit. + +Developers use: + +```bash +bun add @wrnexus/captcha +``` + +```wrn + +``` + +They do not copy `captcha.js`, add a script tag, or manually register `captchaPlugin()` for normal use. The runtime is injected only on pages that render a CAPTCHA. + +## Existing packages upgraded + +| Package | 0.4.0 addition or hardening | +| ---------------------- | ------------------------------------------------------------------------------------ | +| `@wrnexus/syntax` | syntax contract, feature/version metadata, ranges and diagnostics | +| `@wrnexus/compiler` | compilation cache, dependency graph, cycle-safe cache implementation | +| `@wrnexus/reactive` | watchers, resources, abort/supersession handling and cleanup scopes | +| `@wrnexus/core` | service container, lifecycle, health, Problem Details, request IDs and idempotency | +| `@wrnexus/router` | named routes, manifests, reverse URLs and app-over-package precedence | +| `@wrnexus/ssr` | structured scripts, deduplication, CSP/SRI attributes and streaming output | +| `@wrnexus/csr` | lazy package runtime loading and navigation mount/unmount lifecycle | +| `@wrnexus/plugin` | discovery and package components/runtimes/assets/styles/routes/middleware/migrations | +| `@wrnexus/dev-server` | virtual assets, TS runtime bundling, package HMR and migration integration | +| `@wrnexus/cli` | package-aware builds, inspect commands, generator and 0.4 migration | +| `@wrnexus/dev-toolbar` | package panels and platform snapshots | +| `@wrnexus/styles` | package CSS/source support, package-only styles, token and contrast audits | +| `@wrnexus/test` | request/context utilities, JSON/problem assertions, cookies and deferred helpers | +| `@wrnexus/db` | migration ordering, cursor pagination, optimistic updates and tenant helpers | +| `@wrnexus/queue` | durable stores, claims, priority, retry, dead-letter and processing safety | +| `@wrnexus/pubsub` | resilient delivery and expiring defensive presence channels | +| `@wrnexus/uploader` | safe object keys, inspection, policy enforcement and signed file tokens | +| `@wrnexus/encryption` | versioned payloads and validated rotatable keyrings | +| `@wrnexus/jwt` | key-ID signing, validated keyrings and safe decoding | +| `@wrnexus/oauth` | state/PKCE storage, refresh, OIDC discovery and safe return URLs | +| `@wrnexus/authz` | explainable decisions, owner policies, composition and filtering | +| `@wrnexus/captcha` | automatic component/runtime, providers, audio, image styles, gates and guards | +| `@wrnexus/validation` | async/cross-field refinements and OpenAPI conversion | +| `@wrnexus/i18n` | fallbacks, coverage, formatters and plural messages | +| `@wrnexus/ui` | component metadata and reference audits | +| `@wrnexus/helpers` | retry, backoff, abortable sleep, timeouts and stable serialization | +| `@wrnexus/tracking` | sampled/batched telemetry and reliable sink handling | +| `@wrnexus/ai` | provider-neutral client, fallback providers and usage metadata | +| `@wrnexus/mobile` | deep links, offline tasks and environment detection | +| `@wrnexus/native` | capability manifests, permissions and target inspection | + +## Upgrade and release tooling + +- Root/package/service versions: 0.4.0 +- VS Code extension version: 0.4.0 +- Workspace lock versions updated +- `update.ts` includes a 0.4.0 migration +- Legacy CAPTCHA scripts are archived by migration rather than silently destroyed +- Obsolete generated/release artifacts removed +- New commands: + +```bash +bun run verify:0.4 +bun run validate:0.4 +wrnexus doctor +wrnexus inspect packages . +wrnexus inspect plugins . +wrnexus inspect routes . +wrnexus inspect runtimes . +wrnexus inspect assets . +wrnexus generate system +``` + +## Documentation included + +- `README.md` +- `CHANGELOG-0.4.0.md` +- `docs/ARCHITECTURE-0.4.md` +- `docs/PACKAGE-RUNTIMES-0.4.md` +- `docs/PACKAGE-UPGRADES-0.4.md` +- `docs/UPGRADE-0.4.md` +- `docs/TEST-CHECKLIST-0.4.md` +- `VALIDATION-0.4.0.md` +- `MANIFEST-0.4.0.md` + +See `VALIDATION-0.4.0.md` for the exact checks completed and the required final Bun validation commands. diff --git a/MANIFEST.md b/MANIFEST.md deleted file mode 100644 index 5515daec..00000000 --- a/MANIFEST.md +++ /dev/null @@ -1,38 +0,0 @@ -# Bundle manifest - -## Package - -- Engine, challenge generators, optimized PNG renderer, audio renderer -- Memory, SQLite-compatible, and Redis-compatible stores -- Self-hosted, managed, Turnstile, reCAPTCHA, hCaptcha, and custom providers -- HTTP handlers, guards, page gates, adaptive policy, validation integration -- Plugin and DevToolbar audit panel -- Tailwind-only `Captcha.wrn` with compact/normal/big sizing, listen visibility, not-robot mode, 25–75 disturbance control, and generated image renderer props -- English WAV audio assets -- Unit tests for all 18 PNG renderers, style resolution, security behavior, and documentation - -## Example - -- All self-hosted challenge types, including the not-robot checkbox -- Audio and invisible examples -- Shared `@wrnexus/validation` contact schema with client and API validation -- Protected form API using `captchaGuard` -- Working once-per-session protected-page grant and safe return redirect -- Compact, normal, and big component examples -- Listen-visible and listen-hidden examples -- Easy, normal, and hard image-disturbance examples -- Dedicated `/styles` gallery with 18 concrete renderers, random selection, allowed pools, and excluded pools -- External-provider placeholders - -## Managed service - -- Project keys, allowed hostnames, quotas, key rotation -- Public create/solve and secret-authenticated verification routes -- Memory reference store, SQL migration, OpenAPI specification - -## Integration - -- Repeat-safe installer -- TypeScript path aliases -- Current component-discovery compatibility copy -- Generated 18-style preview gallery and renderer guidance diff --git a/README.md b/README.md index 228ab710..b1590475 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,21 @@ -# WRNexusJS CAPTCHA complete bundle +# WRNexusJS 0.4.0 lint fix -This bundle targets WRNexusJS `main-80` / package version `0.3.6` and contains: +Extract this archive into the WRNexusJS repository root and replace the included files. -- `packages/captcha`: publishable `@wrnexus/captcha` package -- `packages/captcha/components/Captcha.wrn`: Tailwind-only UI component -- `examples/captcha-showcase`: runnable challenge, disturbance, validated-form, and protected-page showcase -- `services/managed-captcha`: managed API starter with projects, keys, hostname restrictions, quotas, and rotation -- `scripts/install-captcha.mjs`: repeat-safe repository installer -- `integration`: integration notes and generated path changes -- `docs/IMAGE-STYLES.md`: renderer catalog with a generated preview gallery +Fixes: -## Install into a WRNexusJS checkout +- Removes the `no-useless-assignment` failure in the CAPTCHA page-grant endpoint. +- Restores the Button showcase's specialized examples so `buttonUses` is used. +- Declares browser globals for the package-owned CAPTCHA runtime. +- Replaces empty catch blocks with documented best-effort cleanup. +- Fixes `prefer-const` in package discovery. +- Removes unused plugin, style-audit, and UI-test bindings. +- Uses the UI-reference verifier in the release workflow. +- Declares Node globals correctly in installer and validation scripts. + +Run: ```bash -bun scripts/install-captcha.mjs /path/to/WRNexusJS \ - --with-showcase \ - --with-managed-service \ - --check +bun run lint +bun run validate:0.4 ``` - -The installer copies the package, places `Captcha.wrn` in `packages/ui/components` for current component discovery, adds TypeScript aliases, optionally installs the example and managed service, and can run validation commands. - -Read `packages/captcha/README.md`, `packages/captcha/SECURITY.md`, and `services/managed-captcha/README.md` before production deployment. - -## Fixed 9 compact-size update - -- Compact width is now capped at `max-w-xs` instead of `max-w-sm`. -- Compact padding, gaps, image height, input height, buttons, icons, and text are smaller. -- The subtitle and provider-credit line are hidden in compact mode. -- Normal and big rendering remain unchanged. - -## Fixed 8 integration updates - -- Added 18 generated CAPTCHA image renderers plus `imageStyle="random"`. -- Added allowed and excluded renderer pools, forced randomization, and resolved style metadata. -- Added the `/styles` showcase page with every renderer and easy/normal/hard random examples. -- Optimized PNG line drawing and CRC generation for fast challenge creation. -- Retains compact/normal/big sizing, Listen controls, not-robot mode, page-gate, 25–75 disturbance, validated form, audio, and runtime compatibility fixes. diff --git a/REPLACE-FILES.md b/REPLACE-FILES.md deleted file mode 100644 index a85fa269..00000000 --- a/REPLACE-FILES.md +++ /dev/null @@ -1,81 +0,0 @@ -# WRNexusJS DevToolbar Integration - -Extract this ZIP directly over the WRNexusJS repository root and allow these files to be replaced. - -## Replace files one by one - -1. `tsconfig.json` - - Adds workspace path aliases for `@wrnexus/dev-toolbar` exports. - -2. `packages/styles/src/config.ts` - - Adds `DevToolbarConfig` and `devToolbar` to `AppConfig`. - -3. `packages/dev-server/package.json` - - Confirms the workspace dependency on `@wrnexus/dev-toolbar`. - -4. `packages/dev-server/src/hmr.ts` - - Adds generic JSON broadcasting for toolbar events. - -5. `packages/dev-server/src/watch.ts` - - Clears stale source issues and triggers rescans for CSS changes. - -6. `packages/dev-server/src/runtime.ts` - - Serves toolbar routes. - - Injects the toolbar only in development HTML. - - Collects route, component, layout, and request errors. - - Sends HMR/browser events used by the toolbar. - -7. `packages/dev-server/src/index.ts` - - Resolves toolbar configuration. - - Creates the server issue collector. - - Connects collector updates to HMR. - - Passes the toolbar into runtime and watcher dependencies. - -8. `packages/dev-server/src/serve-entry.ts` - - Passes `config.devToolbar` from `wrnexus.config.ts` into the server. - -9. `packages/dev-toolbar/src/client/runtime.ts` - - Loads server-side issues. - - Receives live toolbar issue/scan messages. - - Rescans after HMR and client navigation. - -## Configuration - -The toolbar is enabled automatically in development. Optional configuration: - -```ts -export default { - devToolbar: { - enabled: true, - position: "bottom-center", - defaultOpen: false, - scanOnNavigation: true, - scanOnHmr: true, - openEditor: true, - editor: "code", - }, -}; -``` - -Disable it for an app: - -```ts -export default { - devToolbar: false, -}; -``` - -## After replacement - -Run from the repository root: - -```bash -bun install -bun run format -bun run typecheck -bun run lint -bun test -bun run check -``` - -The toolbar is development-only and is not injected when the server mode is `production`. diff --git a/RUNTIME-COMPATIBILITY.md b/RUNTIME-COMPATIBILITY.md deleted file mode 100644 index 37d95174..00000000 --- a/RUNTIME-COMPATIBILITY.md +++ /dev/null @@ -1,7 +0,0 @@ -# Reactive runtime compatibility correction - -The original Captcha component attempted to execute full native JavaScript inside WRNexusJS behavior blocks. WRNexusJS 0.3.6 behavior blocks use a deliberately limited eval-free interpreter and do not execute arbitrary JavaScript syntax. - -The corrected design keeps the `.wrn` component declarative and delegates network, audio, timers, provider SDKs, DOM updates, and form guards to a packaged same-origin browser runtime. - -This removes the `Unexpected token '?'` mount failure and prevents the later failures that would have occurred for `async`, `await`, `try`, callback functions, constructors, and member assignments. diff --git a/VALIDATION-0.4.0.md b/VALIDATION-0.4.0.md new file mode 100644 index 00000000..05b78ae2 --- /dev/null +++ b/VALIDATION-0.4.0.md @@ -0,0 +1,145 @@ +# WRNexusJS 0.4.0 validation report + +Validation date: 2026-07-25 + +Baseline archive: + +- File: `WRNexusJS-main-current.zip` +- SHA-256: `7860543f1b0d8bb227857bb01f70c6a3719d51215e24990cc0b3e45a18211c50` + +## Result + +All validation that can be performed in this build environment passed. The repository is structurally aligned to 0.4.0, TypeScript source is syntactically and semantically valid under the project configuration used for validation, automatic package discovery works, CAPTCHA is loaded automatically without a copied `captcha.js`, editor checks pass, generated component documentation succeeds, and targeted runtime smoke checks pass. + +Bun is not installed in this environment and external package installation is unavailable. Therefore the Bun unit-test suite, ESLint, Prettier, and real Bun production builds were not executed here. Run the final commands in the last section on the target Windows/Bun environment before publishing. + +## Passed checks + +### Repository structure + +Command: + +```bash +node scripts/verify-0.4.mjs +``` + +Passed: + +- 30 `@wrnexus/*` packages aligned to version 0.4.0 +- Managed CAPTCHA service aligned to version 0.4.0 +- 73 unique updater migrations +- Package/runtime/asset/migration discovery markers +- CAPTCHA has no copied UI component or public JavaScript requirement +- Bun workspace lock structure +- VS Code extension package and lock versions +- Required 0.4 architecture, migration, runtime, testing, and documentation files + +### TypeScript + +Passed: + +- 346 TypeScript files transpiled with zero syntax diagnostics +- Strict semantic TypeScript check completed with zero diagnostics +- Compiler cache runtime-cycle smoke check passed + +The semantic check used the repository `tsconfig.json` plus temporary environment-only type roots for Node/Bun declarations. The temporary config is not included in the delivered archives. + +### WRN source + +Passed: + +- 211 `.wrn` files parsed successfully +- CAPTCHA component parses without a manual ` - -
-
-
- -

- {label} -

-
-

- {description} -

-
- - -
- - - - - - - - - - - -

- {helpText} -

- -
-
- - - - - -
- - -
- - - -

- Protected by WRNexus CAPTCHA. Server-side verification is required. -

- - } -} diff --git a/examples/captcha-showcase/app/lib/captcha.ts b/examples/captcha-showcase/app/lib/captcha.ts index 1dc8a6c4..4445134d 100644 --- a/examples/captcha-showcase/app/lib/captcha.ts +++ b/examples/captcha-showcase/app/lib/captcha.ts @@ -1,4 +1,8 @@ -import { createCaptchaEngine, createCaptchaHttpHandlers, MemoryCaptchaStore } from "@wrnexus/captcha/server"; +import { + createCaptchaEngine, + createCaptchaHttpHandlers, + MemoryCaptchaStore, +} from "@wrnexus/captcha/server"; export const captchaEngine = createCaptchaEngine({ secret: process.env.CAPTCHA_SECRET ?? "development-only-change-this-captcha-secret-123456", diff --git a/examples/captcha-showcase/app/styles/global.css b/examples/captcha-showcase/app/styles/global.css index 4e4269cf..82bb37a0 100644 --- a/examples/captcha-showcase/app/styles/global.css +++ b/examples/captcha-showcase/app/styles/global.css @@ -1,7 +1,13 @@ @import "tailwindcss"; @plugin "@iconify/tailwind4"; @source "../**/*.wrn"; -@source "../../../packages/captcha/components/*.wrn"; -html { color-scheme: light dark; } -body { margin: 0; background: var(--wire-color-bg); color: var(--wire-color-text); font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif; } +html { + color-scheme: light dark; +} +body { + margin: 0; + background: var(--wire-color-bg); + color: var(--wire-color-text); + font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif; +} diff --git a/examples/captcha-showcase/public/assets/wrnexus/captcha.js b/examples/captcha-showcase/public/assets/wrnexus/captcha.js deleted file mode 100644 index dc4b2c9c..00000000 --- a/examples/captcha-showcase/public/assets/wrnexus/captcha.js +++ /dev/null @@ -1,878 +0,0 @@ -(function () { - "use strict"; - - var RUNTIME_KEY = "__wrnexusCaptchaRuntime"; - var existingRuntime = window[RUNTIME_KEY]; - - if (existingRuntime && typeof existingRuntime.scan === "function") { - existingRuntime.scan(document); - return; - } - - var states = new WeakMap(); - var scriptPromises = new Map(); - var instanceCounter = 0; - - function bool(value, fallback) { - if (value === undefined || value === null || value === "") return fallback; - return value === true || value === "true" || value === "1"; - } - - function numberInRange(value, fallback, minimum, maximum) { - var parsed = Number(value); - if (!Number.isFinite(parsed)) return fallback; - return Math.max(minimum, Math.min(maximum, Math.round(parsed))); - } - - function normalizeSize(value, compact) { - if (compact) return "compact"; - var normalized = String(value || "normal").trim().toLowerCase(); - if (normalized === "compact" || normalized === "small" || normalized === "sm") return "compact"; - if (normalized === "big" || normalized === "large" || normalized === "lg") return "big"; - return "normal"; - } - - function commaList(value) { - return String(value || "") - .split(",") - .map(function (item) { return item.trim().toLowerCase(); }) - .filter(Boolean); - } - - function text(root, selector, value) { - var element = root.querySelector(selector); - if (element) element.textContent = value == null ? "" : String(value); - } - - function show(element, visible) { - if (!element) return; - element.hidden = !visible; - } - - function setBusy(root, busy) { - root.setAttribute("aria-busy", busy ? "true" : "false"); - } - - function config(root) { - var data = root.dataset; - var responseField = data.captchaResponseField || data.captchaName || "wrn-captcha-response"; - var compact = bool(data.captchaCompact, false); - var size = normalizeSize(data.captchaSize, compact); - - return { - provider: data.captchaProvider || "self-hosted", - siteKey: data.captchaSiteKey || "", - type: data.captchaType || "alphanumeric", - action: data.captchaAction || "form-submit", - presentation: data.captchaPresentation || "visual", - difficulty: data.captchaDifficulty || "normal", - disturbance: numberInRange(data.captchaDisturbance, 50, 25, 75), - imageStyle: data.captchaImageStyle || "random", - allowedStyles: commaList(data.captchaAllowedStyles), - excludedStyles: commaList(data.captchaExcludedStyles), - randomizeStyle: bool(data.captchaRandomizeStyle, false), - locale: data.captchaLocale || "en", - size: size, - endpoint: data.captchaEndpoint || "/__wrnexus/captcha/challenge", - verifyEndpoint: data.captchaVerifyEndpoint || "/__wrnexus/captcha/verify", - responseField: responseField, - autoLoad: bool(data.captchaAutoLoad, true), - autoVerify: bool(data.captchaAutoVerify, false), - showVerify: bool(data.captchaShowVerify, true), - showRefresh: bool(data.captchaShowRefresh, true), - showAudio: bool(data.captchaShowAudio, true), - showListen: bool(data.captchaShowListen, true), - showStatus: bool(data.captchaShowStatus, true), - disabled: bool(data.captchaDisabled, false), - required: bool(data.captchaRequired, true), - compact: compact, - requiredMessage: data.captchaRequiredMessage || "Please complete the security check.", - incorrectMessage: data.captchaIncorrectMessage || "That answer was not correct. Try again.", - expiredMessage: data.captchaExpiredMessage || "This challenge expired. Load a new one.", - networkMessage: data.captchaNetworkMessage || "The verification service is unavailable. Try again.", - }; - } - - function eventDetail(state, extra) { - var challenge = state.challenge; - return { - component: "Captcha", - provider: state.config.provider, - type: challenge && challenge.type ? challenge.type : state.config.type, - action: state.config.action, - disturbance: state.config.disturbance, - imageStyle: challenge && challenge.metadata && challenge.metadata.imageStyle - ? challenge.metadata.imageStyle - : state.config.imageStyle, - requestedImageStyle: challenge && challenge.metadata && challenge.metadata.requestedImageStyle - ? challenge.metadata.requestedImageStyle - : state.config.imageStyle, - imageStylePool: challenge && challenge.metadata && challenge.metadata.imageStylePool - ? challenge.metadata.imageStylePool - : state.config.allowedStyles, - size: state.config.size, - status: state.status, - challengeId: challenge && challenge.id ? challenge.id : "", - responseToken: state.responseToken, - expiresAt: challenge && challenge.expiresAt ? challenge.expiresAt : null, - extra: extra || null, - }; - } - - function emit(state, name, extra) { - state.root.dispatchEvent( - new CustomEvent(name, { - bubbles: true, - detail: eventDetail(state, extra), - }), - ); - } - - function setResponseToken(state, token) { - state.responseToken = token || ""; - var input = state.root.querySelector("[data-captcha-response]"); - if (input) { - input.name = state.config.responseField; - input.value = state.responseToken; - } - } - - function isNotRobot(state) { - return Boolean( - (state.challenge && state.challenge.type === "not-robot") || - (!state.challenge && state.config.type === "not-robot"), - ); - } - - function updateNotRobotState(state) { - var panel = state.root.querySelector("[data-captcha-not-robot]"); - var button = state.root.querySelector("[data-captcha-not-robot-button]"); - var empty = state.root.querySelector("[data-captcha-not-robot-empty]"); - var spinner = state.root.querySelector("[data-captcha-not-robot-spinner]"); - var check = state.root.querySelector("[data-captcha-not-robot-check]"); - var control = state.root.querySelector("[data-captcha-not-robot-control]"); - var label = state.root.querySelector("[data-captcha-not-robot-label]"); - var active = isNotRobot(state); - var pending = state.notRobotPending || state.status === "verifying"; - var verified = state.status === "verified"; - - show(panel, active && state.status !== "loading" && state.status !== "idle"); - if (!active) return; - - if (button) { - button.disabled = state.config.disabled || pending || verified || state.status === "expired"; - button.setAttribute("aria-pressed", verified ? "true" : "false"); - } - show(empty, !pending && !verified); - show(spinner, pending && !verified); - show(check, verified); - - if (control) control.dataset.verified = verified ? "true" : "false"; - if (label) { - label.textContent = verified ? "Verified" : pending ? "Checking…" : "I'm not a robot"; - } - } - - function setStatus(state, status, message) { - state.status = status; - state.root.dataset.captchaStatus = status; - setBusy(state.root, status === "loading" || status === "verifying"); - - var loading = state.root.querySelector("[data-captcha-loading]"); - var challenge = state.root.querySelector("[data-captcha-challenge]"); - var success = state.root.querySelector("[data-captcha-success]"); - var error = state.root.querySelector("[data-captcha-error]"); - var badge = state.root.querySelector("[data-captcha-verified-badge]"); - var verifyButton = state.root.querySelector("[data-captcha-verify]"); - var verifyIcon = state.root.querySelector("[data-captcha-verify-icon]"); - var verifySpinner = state.root.querySelector("[data-captcha-verify-spinner]"); - var verifyLabel = state.root.querySelector("[data-captcha-verify-label]"); - var answer = state.root.querySelector("[data-captcha-answer]"); - - show(loading, status === "loading"); - show(challenge, status !== "loading" && status !== "idle" && state.config.provider !== "turnstile" && state.config.provider !== "recaptcha" && state.config.provider !== "hcaptcha"); - show(badge, status === "verified"); - - if (state.config.showStatus) { - show(success, status === "verified"); - show(error, Boolean(message) && status !== "verified"); - } else { - show(success, false); - show(error, false); - } - - if (message && status === "verified") text(state.root, "[data-captcha-success-message]", message); - if (message && status !== "verified") text(state.root, "[data-captcha-error-message]", message); - - if (answer) { - answer.disabled = state.config.disabled || status === "verified" || status === "expired" || status === "loading" || status === "verifying"; - answer.setAttribute("aria-invalid", status === "incorrect" ? "true" : "false"); - } - - if (verifyButton) { - verifyButton.disabled = state.config.disabled || status === "loading" || status === "verifying" || status === "verified" || status === "expired"; - } - - show(verifyIcon, status !== "verifying"); - show(verifySpinner, status === "verifying"); - if (verifyLabel) verifyLabel.textContent = status === "verifying" ? "Verifying…" : "Verify"; - - updateNotRobotState(state); - updateControls(state); - } - - function clearTimer(state) { - if (state.timer) { - window.clearInterval(state.timer); - state.timer = null; - } - } - - function updateCountdown(state) { - var countdown = state.root.querySelector("[data-captcha-countdown]"); - if (!countdown || !state.challenge || !state.challenge.expiresAt) { - show(countdown, false); - return; - } - - var remaining = Math.max(0, Math.ceil((Number(state.challenge.expiresAt) - Date.now()) / 1000)); - countdown.textContent = remaining + "s"; - show(countdown, remaining > 0 && state.status !== "verified"); - - if (remaining <= 0 && state.status !== "verified" && state.status !== "expired") { - clearTimer(state); - setResponseToken(state, ""); - setStatus(state, "expired", state.config.expiredMessage); - emit(state, "expired"); - } - } - - function startTimer(state) { - clearTimer(state); - if (!state.challenge || !state.challenge.expiresAt) return; - updateCountdown(state); - state.timer = window.setInterval(function () { - updateCountdown(state); - }, 1000); - } - - function resetUi(state) { - if (state.audioPlayer) { - state.audioPlayer.pause(); - state.audioPlayer.removeAttribute("src"); - state.audioPlayer.load(); - state.audioPlayer = null; - } - if (state.notRobotTimer) { - window.clearTimeout(state.notRobotTimer); - state.notRobotTimer = null; - } - - state.notRobotPending = false; - state.challengeLoadedAt = 0; - state.answer = ""; - state.selections = []; - state.challenge = null; - setResponseToken(state, ""); - - var answer = state.root.querySelector("[data-captcha-answer]"); - var items = state.root.querySelector("[data-captcha-items]"); - var imageWrap = state.root.querySelector("[data-captcha-image-wrap]"); - var providerMount = state.root.querySelector("[data-captcha-provider-mount]"); - var honeypot = state.root.querySelector("[data-captcha-honeypot]"); - var notRobot = state.root.querySelector("[data-captcha-not-robot]"); - - if (answer) answer.value = ""; - if (items) items.replaceChildren(); - show(items, false); - show(imageWrap, false); - show(notRobot, false); - if (honeypot) { - honeypot.value = ""; - honeypot.name = ""; - } - if (providerMount && state.config.provider !== "turnstile" && state.config.provider !== "recaptcha" && state.config.provider !== "hcaptcha") { - providerMount.replaceChildren(); - show(providerMount, false); - } - } - - function updateControls(state) { - var challenge = state.challenge; - var audio = state.root.querySelector("[data-captcha-audio]"); - var audioAlternative = state.root.querySelector("[data-captcha-audio-alternative]"); - var refresh = state.root.querySelector("[data-captcha-refresh]"); - var verify = state.root.querySelector("[data-captcha-verify]"); - var footer = state.root.querySelector("[data-captcha-footer]"); - var isExternal = state.config.provider === "turnstile" || state.config.provider === "recaptcha" || state.config.provider === "hcaptcha"; - var notRobot = isNotRobot(state); - var locked = state.config.disabled || state.status === "loading" || state.status === "verifying" || state.notRobotPending; - - show(footer, !notRobot); - show(audio, !notRobot && state.config.showAudio && state.config.showListen && Boolean(challenge && challenge.audioUrl) && state.status !== "verified"); - show(audioAlternative, !notRobot && state.config.showAudio && Boolean(challenge && challenge.type === "image") && state.status !== "verified"); - show(refresh, !notRobot && state.config.showRefresh && state.status !== "verified"); - show(verify, !notRobot && state.config.showVerify && !isExternal); - - if (audio) audio.disabled = locked; - if (audioAlternative) audioAlternative.disabled = locked; - if (refresh) refresh.disabled = locked; - if (verify) verify.disabled = locked || state.status === "verified" || state.status === "expired"; - } - - function renderItems(state, challenge) { - var container = state.root.querySelector("[data-captcha-items]"); - var template = state.root.querySelector("[data-captcha-item-template]"); - if (!container || !template || !Array.isArray(challenge.items) || challenge.items.length === 0) { - show(container, false); - return; - } - - container.replaceChildren(); - container.setAttribute("aria-label", challenge.prompt || "Select matching images"); - - challenge.items.forEach(function (item, index) { - var fragment = template.content.cloneNode(true); - var button = fragment.querySelector("button"); - var image = fragment.querySelector("[data-captcha-item-image]"); - if (!button || !image) return; - - button.dataset.captchaItemId = item.id; - button.setAttribute("aria-label", item.alt || "Challenge tile " + (index + 1)); - image.src = item.image; - image.alt = ""; - button.addEventListener("click", function () { - toggleItem(state, item.id, button); - }); - container.appendChild(fragment); - }); - - show(container, true); - } - - function toggleItem(state, itemId, button) { - if (state.config.disabled || state.status !== "ready") return; - - var index = state.selections.indexOf(itemId); - var selected = index >= 0; - - if (selected) { - state.selections.splice(index, 1); - } else { - var maximum = Number(state.challenge && state.challenge.maxSelections ? state.challenge.maxSelections : 0); - if (maximum > 0 && state.selections.length >= maximum) { - var removed = state.selections.shift(); - var previousButton = state.root.querySelector('[data-captcha-item-id="' + CSS.escape(removed) + '"]'); - if (previousButton) { - previousButton.setAttribute("aria-pressed", "false"); - show(previousButton.querySelector("[data-captcha-item-check]"), false); - } - } - state.selections.push(itemId); - } - - var nowSelected = state.selections.indexOf(itemId) >= 0; - button.setAttribute("aria-pressed", nowSelected ? "true" : "false"); - show(button.querySelector("[data-captcha-item-check]"), nowSelected); - emit(state, "input", { selections: state.selections.slice() }); - - var minimum = Number(state.challenge && state.challenge.minSelections ? state.challenge.minSelections : 0); - if (state.config.autoVerify && minimum > 0 && state.selections.length >= minimum) verify(state); - } - - function renderChallenge(state, challenge) { - state.challenge = challenge; - state.challengeLoadedAt = Date.now(); - state.notRobotPending = false; - state.answer = ""; - state.selections = []; - - var notRobot = challenge.type === "not-robot"; - state.root.dataset.captchaType = challenge.type || state.config.type; - state.root.dataset.captchaResolvedImageStyle = challenge.metadata && challenge.metadata.imageStyle - ? String(challenge.metadata.imageStyle) - : state.config.imageStyle; - text(state.root, "[data-captcha-prompt]", challenge.prompt || "Complete the security challenge."); - - var promptRow = state.root.querySelector("[data-captcha-prompt-row]"); - var notRobotPanel = state.root.querySelector("[data-captcha-not-robot]"); - show(promptRow, !notRobot); - show(notRobotPanel, notRobot); - - var imageWrap = state.root.querySelector("[data-captcha-image-wrap]"); - var image = state.root.querySelector("[data-captcha-image]"); - if (!notRobot && challenge.image && image) { - image.src = challenge.image; - show(imageWrap, true); - } else { - if (image) image.removeAttribute("src"); - show(imageWrap, false); - } - - if (notRobot) { - var items = state.root.querySelector("[data-captcha-items]"); - if (items) items.replaceChildren(); - show(items, false); - } else { - renderItems(state, challenge); - } - - var answerWrap = state.root.querySelector("[data-captcha-answer-wrap]"); - var answer = state.root.querySelector("[data-captcha-answer]"); - var needsAnswer = !notRobot && !challenge.items && challenge.inputMode !== "none"; - show(answerWrap, needsAnswer); - if (answer) { - answer.value = ""; - answer.inputMode = challenge.inputMode === "numeric" ? "numeric" : "text"; - answer.setAttribute("aria-invalid", "false"); - } - - var honeypot = state.root.querySelector("[data-captcha-honeypot]"); - if (honeypot) honeypot.name = challenge.honeypotField || ""; - - setStatus(state, "ready", ""); - startTimer(state); - emit(state, "challenge", challenge); - emit(state, "ready"); - - if (state.config.autoVerify && !notRobot && challenge.inputMode === "none" && !challenge.items) { - window.setTimeout(function () { - verify(state); - }, 0); - } - } - - async function createChallenge(state, requestedPresentation) { - if (state.config.disabled) return; - - if (state.config.provider === "turnstile" || state.config.provider === "recaptcha" || state.config.provider === "hcaptcha") { - await mountExternal(state); - return; - } - - clearTimer(state); - resetUi(state); - setStatus(state, "loading", ""); - - try { - var response = await fetch(state.config.endpoint, { - method: "POST", - credentials: "same-origin", - headers: { - "content-type": "application/json", - accept: "application/json", - }, - body: JSON.stringify({ - siteKey: state.config.siteKey, - action: state.config.action, - type: state.config.type, - presentation: requestedPresentation || state.config.presentation, - difficulty: state.config.difficulty, - disturbance: state.config.disturbance, - imageStyle: state.config.imageStyle, - allowedStyles: state.config.allowedStyles, - excludedStyles: state.config.excludedStyles, - randomizeStyle: state.config.randomizeStyle, - locale: state.config.locale, - responseField: state.config.responseField, - }), - }); - - var result = await response.json().catch(function () { - return null; - }); - - if (!response.ok || !result || !result.id) { - throw new Error(result && result.message ? result.message : "Challenge request failed"); - } - - renderChallenge(state, result); - } catch (error) { - setStatus(state, "network-error", state.config.networkMessage); - emit(state, "error", { error: error instanceof Error ? error.message : String(error) }); - } - } - - async function verify(state) { - state.notRobotPending = false; - if (state.config.disabled || state.status === "loading" || state.status === "verifying" || state.status === "expired" || state.status === "verified") { - updateNotRobotState(state); - updateControls(state); - return; - } - - if (!state.challenge) { - await createChallenge(state); - return; - } - - var answerInput = state.root.querySelector("[data-captcha-answer]"); - var honeypotInput = state.root.querySelector("[data-captcha-honeypot]"); - state.answer = answerInput ? answerInput.value : ""; - - setStatus(state, "verifying", ""); - emit(state, "verify"); - - try { - var response = await fetch(state.challenge.verifyUrl || state.config.verifyEndpoint, { - method: "POST", - credentials: "same-origin", - headers: { - "content-type": "application/json", - accept: "application/json", - }, - body: JSON.stringify({ - challengeId: state.challenge.id, - action: state.config.action, - answer: state.answer, - selections: state.selections, - honeypot: honeypotInput ? honeypotInput.value : "", - timingToken: state.challenge.timingToken || "", - }), - }); - - var result = await response.json().catch(function () { - return null; - }); - - if (!response.ok || !result || !result.success) { - setResponseToken(state, ""); - var expired = Boolean(result && result.code === "expired"); - setStatus( - state, - expired ? "expired" : "incorrect", - expired - ? state.config.expiredMessage - : result && result.message - ? result.message - : state.config.incorrectMessage, - ); - emit(state, "failure", result || null); - return; - } - - clearTimer(state); - setResponseToken(state, result.responseToken || ""); - setStatus(state, "verified", "Verification completed."); - emit(state, "success", result); - } catch (error) { - setStatus(state, "network-error", state.config.networkMessage); - emit(state, "error", { error: error instanceof Error ? error.message : String(error) }); - } - } - - function verifyNotRobot(state) { - if (state.config.disabled || state.notRobotPending || state.status !== "ready") return; - if (!state.challenge || state.challenge.type !== "not-robot") { - createChallenge(state); - return; - } - - var configuredMinimum = Number( - state.challenge.metadata && state.challenge.metadata.minCompletionMs !== undefined - ? state.challenge.metadata.minCompletionMs - : 800, - ); - var minimum = Number.isFinite(configuredMinimum) ? Math.max(0, configuredMinimum) : 800; - var elapsed = Math.max(0, Date.now() - state.challengeLoadedAt); - var remaining = Math.max(0, minimum - elapsed); - - state.notRobotPending = true; - updateNotRobotState(state); - updateControls(state); - emit(state, "input", { checked: true }); - - var complete = function () { - state.notRobotTimer = null; - state.notRobotPending = false; - verify(state); - }; - - if (remaining > 0) state.notRobotTimer = window.setTimeout(complete, remaining); - else complete(); - } - - function providerDefinition(provider) { - if (provider === "turnstile") { - return { - url: "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit", - globalName: "turnstile", - }; - } - if (provider === "recaptcha") { - return { - url: "https://www.google.com/recaptcha/api.js?render=explicit", - globalName: "grecaptcha", - }; - } - if (provider === "hcaptcha") { - return { - url: "https://js.hcaptcha.com/1/api.js?render=explicit", - globalName: "hcaptcha", - }; - } - return null; - } - - function loadProviderScript(definition) { - if (window[definition.globalName]) return Promise.resolve(window[definition.globalName]); - if (scriptPromises.has(definition.url)) return scriptPromises.get(definition.url); - - var promise = new Promise(function (resolve, reject) { - var script = document.querySelector('script[src="' + definition.url + '"]'); - if (!script) { - script = document.createElement("script"); - script.src = definition.url; - script.async = true; - script.defer = true; - document.head.appendChild(script); - } - - script.addEventListener( - "load", - function () { - if (window[definition.globalName]) resolve(window[definition.globalName]); - else reject(new Error("CAPTCHA provider did not initialize")); - }, - { once: true }, - ); - script.addEventListener("error", function () { reject(new Error("CAPTCHA provider script failed")); }, { once: true }); - }); - - scriptPromises.set(definition.url, promise); - return promise; - } - - async function mountExternal(state) { - var definition = providerDefinition(state.config.provider); - if (!definition) return; - - if (!state.config.siteKey) { - setStatus(state, "provider-error", "A public site key is required."); - return; - } - - var mount = state.root.querySelector("[data-captcha-provider-mount]"); - if (!mount) return; - - show(mount, true); - setStatus(state, "loading", ""); - - try { - var api = await loadProviderScript(definition); - - if (state.externalWidgetId !== null && typeof api.reset === "function") { - setResponseToken(state, ""); - api.reset(state.externalWidgetId); - setStatus(state, "ready", ""); - emit(state, "ready"); - return; - } - - mount.replaceChildren(); - state.externalApi = api; - state.externalWidgetId = api.render(mount, { - sitekey: state.config.siteKey, - theme: "auto", - size: state.config.size === "compact" ? "compact" : state.config.provider === "turnstile" ? "flexible" : "normal", - action: state.config.action, - callback: function (token) { - setResponseToken(state, token); - setStatus(state, "verified", "Verification completed."); - emit(state, "success"); - }, - "expired-callback": function () { - setResponseToken(state, ""); - setStatus(state, "expired", state.config.expiredMessage); - emit(state, "expired"); - }, - "error-callback": function (providerError) { - setResponseToken(state, ""); - setStatus(state, "provider-error", state.config.networkMessage); - emit(state, "error", { error: providerError || "provider-error" }); - }, - }); - - setStatus(state, "ready", ""); - emit(state, "ready"); - } catch (error) { - setStatus(state, "provider-error", state.config.networkMessage); - emit(state, "error", { error: error instanceof Error ? error.message : String(error) }); - } - } - - function playAudio(state) { - if (!state.challenge || !state.challenge.audioUrl || state.config.disabled) return; - - if (state.audioPlayer) { - state.audioPlayer.pause(); - state.audioPlayer.currentTime = 0; - } - - var audio = new Audio(); - state.audioPlayer = audio; - audio.preload = "auto"; - audio.volume = 1; - audio.src = new URL(state.challenge.audioUrl, window.location.href).href; - - audio.addEventListener("ended", function () { - if (state.audioPlayer === audio) state.audioPlayer = null; - emit(state, "audioEnd"); - emit(state, "audio-end"); - }, { once: true }); - - audio.addEventListener("error", function () { - if (state.audioPlayer === audio) state.audioPlayer = null; - var mediaError = audio.error; - setStatus(state, "network-error", "The audio challenge could not be played. Load a new challenge and try again."); - emit(state, "error", { - code: "audio-playback-failed", - audioUrl: audio.src, - mediaErrorCode: mediaError ? mediaError.code : null, - }); - }, { once: true }); - - audio.load(); - audio.play().then(function () { - emit(state, "audioStart"); - emit(state, "audio-start"); - }).catch(function (error) { - if (state.audioPlayer === audio) state.audioPlayer = null; - setStatus(state, "network-error", "The audio challenge could not be played. Check the audio endpoint and try again."); - emit(state, "error", { - code: "audio-playback-rejected", - audioUrl: audio.src, - error: error instanceof Error ? error.message : String(error), - }); - }); - } - - function onFormSubmit(state, event) { - if (!state.config.required || state.responseToken) return; - event.preventDefault(); - event.stopImmediatePropagation(); - setStatus(state, "incorrect", state.config.requiredMessage); - emit(state, "failure", { code: "missing-input" }); - var target = state.root.querySelector("[data-captcha-answer]") || state.root.querySelector("[data-captcha-not-robot-button]") || state.root.querySelector("[data-captcha-verify]"); - if (target && typeof target.focus === "function") target.focus(); - } - - function initialize(root) { - if (!(root instanceof HTMLElement) || states.has(root)) return; - - if (!root.id) { - instanceCounter += 1; - root.id = "wrn-captcha-" + instanceCounter; - } - - var state = { - root: root, - config: config(root), - challenge: null, - answer: "", - selections: [], - responseToken: "", - status: "idle", - timer: null, - notRobotTimer: null, - notRobotPending: false, - challengeLoadedAt: 0, - externalApi: null, - externalWidgetId: null, - audioPlayer: null, - form: root.closest("form"), - }; - states.set(root, state); - root.dataset.captchaSize = state.config.size; - - var response = root.querySelector("[data-captcha-response]"); - if (response) response.name = state.config.responseField; - - var answer = root.querySelector("[data-captcha-answer]"); - var verifyButton = root.querySelector("[data-captcha-verify]"); - var refreshButton = root.querySelector("[data-captcha-refresh]"); - var audioButton = root.querySelector("[data-captcha-audio]"); - var audioAlternative = root.querySelector("[data-captcha-audio-alternative]"); - var notRobotButton = root.querySelector("[data-captcha-not-robot-button]"); - - if (answer) { - answer.addEventListener("input", function () { - state.answer = answer.value; - emit(state, "input", { answerLength: state.answer.length }); - }); - answer.addEventListener("keydown", function (event) { - if (event.key === "Enter") { - event.preventDefault(); - verify(state); - } - }); - } - - if (verifyButton) verifyButton.addEventListener("click", function () { verify(state); }); - if (notRobotButton) notRobotButton.addEventListener("click", function () { verifyNotRobot(state); }); - if (refreshButton) refreshButton.addEventListener("click", function () { - emit(state, "refresh"); - createChallenge(state, state.config.presentation); - }); - if (audioButton) audioButton.addEventListener("click", function () { playAudio(state); }); - if (audioAlternative) audioAlternative.addEventListener("click", function () { - emit(state, "refresh", { presentation: "audio" }); - createChallenge(state, "audio"); - }); - - root.addEventListener("captcha-reset", function () { - emit(state, "refresh"); - createChallenge(state, state.config.presentation); - }); - - if (state.form) { - state.form.addEventListener("submit", function (event) { - onFormSubmit(state, event); - }, true); - state.form.addEventListener("wire:success", function () { - createChallenge(state, state.config.presentation); - }); - } - - updateNotRobotState(state); - updateControls(state); - if (state.config.disabled) setStatus(state, "disabled", ""); - else if (state.config.autoLoad) createChallenge(state, state.config.presentation); - } - - function scan(scope) { - var host = scope || document; - if (host instanceof Element && host.matches("[data-wrn-captcha]")) initialize(host); - host.querySelectorAll("[data-wrn-captcha]").forEach(initialize); - } - - var runtime = { - scan: scan, - reset: function (element) { - var root = typeof element === "string" ? document.querySelector(element) : element; - var state = root ? states.get(root) : null; - if (state) createChallenge(state, state.config.presentation); - }, - verify: function (element) { - var root = typeof element === "string" ? document.querySelector(element) : element; - var state = root ? states.get(root) : null; - return state ? verify(state) : Promise.resolve(); - }, - }; - - window[RUNTIME_KEY] = runtime; - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", function () { scan(document); }, { once: true }); - } else { - scan(document); - } - - new MutationObserver(function (records) { - records.forEach(function (record) { - record.addedNodes.forEach(function (node) { - if (node.nodeType === 1) scan(node); - }); - }); - }).observe(document.documentElement, { childList: true, subtree: true }); -})(); diff --git a/examples/captcha-showcase/test/showcase.test.ts b/examples/captcha-showcase/test/showcase.test.ts index 0cbb1f0d..f27eed0f 100644 --- a/examples/captcha-showcase/test/showcase.test.ts +++ b/examples/captcha-showcase/test/showcase.test.ts @@ -3,6 +3,14 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; const showcaseRoot = join(import.meta.dir, ".."); +const repositoryRoot = join(showcaseRoot, "..", ".."); +const packageComponentPath = join( + repositoryRoot, + "packages", + "captcha", + "components", + "Captcha.wrn", +); function path(relativePath: string): string { return join(showcaseRoot, relativePath); @@ -19,7 +27,6 @@ describe("CAPTCHA showcase structure", () => { "app/pages/captcha.wrn", "app/pages/protected.wrn", "app/pages/styles.wrn", - "app/components/Captcha.wrn", "app/api/contact.ts", "app/api/page-grant.ts", "app/schemas/contact.ts", @@ -41,7 +48,8 @@ describe("CAPTCHA showcase structure", () => { expect(packageJson).toContain('"@iconify-json/lucide"'); expect(packageJson).toContain('"@iconify/tailwind4"'); expect(styles).toContain('@plugin "@iconify/tailwind4";'); - expect(styles).toContain('@source "../../../packages/captcha/components/*.wrn";'); + expect(styles).not.toContain("packages/captcha/components"); + expect(read("wrnexus.config.ts")).not.toContain("includePackageSources: false"); }); test("documents every bundled challenge mode", () => { @@ -72,7 +80,6 @@ describe("CAPTCHA showcase structure", () => { expect(source).toContain('showListen="false"'); }); - test("documents every generated image renderer and random pool controls", () => { const source = read("app/pages/styles.wrn"); const styles = [ @@ -170,15 +177,23 @@ describe("CAPTCHA showcase integration", () => { expect(grantRoute).toContain("Response.redirect"); }); - test("registers the CAPTCHA plugin and DevToolbar audit", () => { - const source = read("wrnexus.config.ts"); + test("auto-discovers the CAPTCHA package without manual plugin or script wiring", () => { + const config = read("wrnexus.config.ts"); + const packageJson = read("package.json"); + const component = readFileSync(packageComponentPath, "utf8"); - expect(source).toContain("captchaPlugin"); - expect(source).toContain("enableDevToolbar: true"); + expect(packageJson).toContain('"@wrnexus/captcha": "workspace:*"'); + expect(existsSync(path("app/components/Captcha.wrn"))).toBe(false); + expect(existsSync(packageComponentPath)).toBe(true); + expect(config).not.toContain("captchaPlugin"); + expect(config).not.toContain("plugins:"); + expect(component).toContain('data-wrnexus-runtime="captcha"'); + expect(component).not.toContain(" { - const source = read("app/components/Captcha.wrn"); + const source = readFileSync(packageComponentPath, "utf8"); const expectedEvents = [ "ready", "challenge", @@ -195,14 +210,14 @@ describe("CAPTCHA showcase integration", () => { expect(source).toContain('name = "wrn-captcha-response"'); expect(source).toContain('responseField = "wrn-captcha-response"'); - expect(source).toContain('disturbance = 50'); + expect(source).toContain("disturbance = 50"); expect(source).toContain('size = "normal"'); - expect(source).toContain('showListen = true'); + expect(source).toContain("showListen = true"); expect(source).toContain('imageStyle = "random"'); expect(source).toContain('allowedStyles = ""'); expect(source).toContain('excludedStyles = ""'); - expect(source).toContain('randomizeStyle = false'); - expect(source).toContain('data-captcha-not-robot-button'); + expect(source).toContain("randomizeStyle = false"); + expect(source).toContain("data-captcha-not-robot-button"); for (const eventName of expectedEvents) { expect(source).toContain(`@event ${eventName} = function`); diff --git a/examples/captcha-showcase/wrnexus.config.ts b/examples/captcha-showcase/wrnexus.config.ts index f3aa5b4c..0f168671 100644 --- a/examples/captcha-showcase/wrnexus.config.ts +++ b/examples/captcha-showcase/wrnexus.config.ts @@ -1,10 +1,10 @@ import type { AppConfig } from "@wrnexus/styles"; -import { captchaPlugin } from "@wrnexus/captcha/plugin"; -const config: AppConfig & { plugins?: unknown[] } = { +const config: AppConfig = { seo: { title: "WRNexus CAPTCHA Showcase", - description: "Number, text, calculation, image, audio, invisible, managed, and third-party CAPTCHA examples.", + description: + "Number, text, calculation, image, audio, invisible, managed, and third-party CAPTCHA examples.", canonicalBase: "http://localhost:3000", robots: "noindex,nofollow", }, @@ -12,12 +12,21 @@ const config: AppConfig & { plugins?: unknown[] } = { styles: { entry: "app/styles/global.css", process: async ({ entryPath, appRoot, mode }) => { - const args = ["@tailwindcss/cli", "-i", entryPath!]; + if (!entryPath) throw new Error("CAPTCHA showcase stylesheet entry was not resolved."); + const args = ["@tailwindcss/cli", "-i", entryPath]; if (mode === "production") args.push("--minify"); - return await Bun.$.cwd(appRoot)`bunx ${args}`.text(); + const result = await Bun.$.cwd(appRoot).nothrow()`bunx ${args}`; + if (result.exitCode !== 0) { + throw new Error( + result.stderr.toString().trim() || + result.stdout.toString().trim() || + "Tailwind CSS processing failed.", + ); + } + return result.stdout.toString(); }, + failureMode: "throw", }, - plugins: [captchaPlugin({ enableDevToolbar: true })], }; export default config; diff --git a/examples/component-showcase/app/pages/advanced-forms.wrn b/examples/component-showcase/app/pages/advanced-forms.wrn index 21c4ccf5..498b7659 100644 --- a/examples/component-showcase/app/pages/advanced-forms.wrn +++ b/examples/component-showcase/app/pages/advanced-forms.wrn @@ -46,11 +46,11 @@ page AdvancedFormsShowcase {
diff --git a/examples/component-showcase/app/pages/base.wrn b/examples/component-showcase/app/pages/base.wrn index d92bcd6c..eba4c5d6 100644 --- a/examples/component-showcase/app/pages/base.wrn +++ b/examples/component-showcase/app/pages/base.wrn @@ -10,7 +10,7 @@ page BaseShowcase { Component category

Base

27 unique, responsive, theme-aware components. Open a component to inspect multiple live configurations and its complete props API.

-
27 componentsMultiple use cases81 live configurations
+
27 componentsMultiple use cases86 live configurations
diff --git a/examples/component-showcase/app/pages/components/button.wrn b/examples/component-showcase/app/pages/components/button.wrn index d0998ca6..4bd4244a 100644 --- a/examples/component-showcase/app/pages/components/button.wrn +++ b/examples/component-showcase/app/pages/components/button.wrn @@ -70,7 +70,7 @@ page ButtonDetail {
Live examples

Designed for real product surfaces

Compare configurations and resize the browser to check responsive behavior.

-
Recommended

Production default

Balanced spacing, hierarchy, and content for the most common product workflow.

+
Default

Primary action

The standard high-emphasis action for forms and product workflows.

01
@@ -78,7 +78,7 @@ page ButtonDetail {
Live preview
-
+
Component usage.wrn
<Button
   label="Save & Submit"
@@ -90,7 +90,7 @@ page ButtonDetail {
       
-
Dense UI

Compact application

A tighter variation for dashboards, side panels, tables, and operational interfaces.

+
Outline · Small

Secondary navigation

A compact link button with an end icon and restrained emphasis.

02
@@ -98,7 +98,7 @@ page ButtonDetail {
Live preview
-
+
Component usage.wrn
<Button
   label="Learn more"
@@ -114,7 +114,7 @@ page ButtonDetail {
       
-
Extended

Rich configuration

A more expressive variation using additional data, stronger emphasis, and optional states.

+
Secondary · Large · Pill

Prominent rounded action

A large secondary action using the optional pill shape.

03
@@ -122,7 +122,7 @@ page ButtonDetail {
Live preview
-
+
Component usage.wrn
<Button
   label="Upgrade plan"
@@ -130,6 +130,113 @@ page ButtonDetail {
   variant="secondary"
   size="lg"
   pill="true"
+/>
+
+
+
+
+
+
Ghost · Icon

Accessible icon action

An icon-only control with its accessible name supplied through props.

+ 04 +
+
+
+
Live preview
+
+
+
+
Component usage.wrn
+
<Button
+  label="Open settings"
+  as="button"
+  variant="ghost"
+  size="icon"
+  icon="icon-[lucide--settings]"
+  ariaLabel="Open settings"
+/>
+
+
+
+
+
+
Destructive

Dangerous action

A destructive treatment for irreversible or high-risk operations.

+ 05 +
+
+
+
Live preview
+
+
+
+
Component usage.wrn
+
<Button
+  label="Delete project"
+  as="button"
+  variant="destructive"
+  icon="icon-[lucide--trash-2]"
+/>
+
+
+
+
+
+
Link

Inline action

A low-chrome action that behaves and reads like an inline link.

+ 06 +
+
+
+
Live preview
+
+
+
+
Component usage.wrn
+
<Button
+  label="Read documentation"
+  as="a"
+  href="#button-demo"
+  variant="link"
+/>
+
+
+
+
+
+
Loading

Pending action

The button owns its busy state, loading label, spinner, and disabled behavior.

+ 07 +
+
+
+
Live preview
+
+
+
+
Component usage.wrn
+
<Button
+  label="Publish"
+  loadingLabel="Publishing…"
+  as="button"
+  loading="true"
+/>
+
+
+
+
+
+
Disabled

Unavailable action

A visibly and semantically disabled control.

+ 08 +
+
+
+
Live preview
+
+
+
+
Component usage.wrn
+
<Button
+  label="Unavailable action"
+  as="button"
+  variant="outline"
+  disabled="true"
 />
@@ -138,7 +245,7 @@ page ButtonDetail {
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
labelstring"Button"No
loadingLabelstring"Loading…"No
descriptionstring""No
asstring""No
hrefstring""No
targetstring""No
relstring""No
typestring"button"No
variantstring"default"No
colorstring"primary"No
sizestring"default"No
disabledbooleanfalseNo
loadingbooleanfalseNo
pillbooleanfalseNo
fullWidthbooleanfalseNo
iconstring""No
iconPositionstring"start"No
ariaLabelstring""No
ariaPressedstring""No
ariaExpandedstring""No
ariaControlsstring""No
titlestring""No
autofocusbooleanfalseNo
controlClassstring""No
classstring""No
-
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
sizestring"default"No
colorstring"primary"No
labelstring"Input Number"No
namestring""No
valuestring""No
placeholderstring""No
typestring"number"No
minstring""No
maxstring""No
stepstring""No
disabledbooleanfalseNo
requiredbooleanfalseNo
classstring""No
+
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
sizestring"default"No
colorstring"primary"No
variantstring"default"No
classstring""No
idstring""No
namestring"quantity"No
valuenumber0No
minstring""No
maxstring""No
stepnumber1No
precisionstring"auto"No
labelstring""No
descriptionstring""No
helpTextstring""No
errorstring""No
invalidbooleanfalseNo
prefixstring""No
suffixstring""No
placeholderstring""No
autocompletestring"off"No
inputModestring"decimal"No
ariaLabelstring""No
requiredbooleanfalseNo
disabledbooleanfalseNo
inputDisabledbooleanfalseNo
buttonsDisabledbooleanfalseNo
readonlybooleanfalseNo
allowInputbooleantrueNo
keyboardbooleantrueNo
wheelbooleanfalseNo
clampbooleantrueNo
fullWidthbooleanfalseNo
showButtonsbooleantrueNo
showValidationMessagebooleantrueNo
decrementLabelstring"Decrease value"No
incrementLabelstring"Increase value"No
controlsLabelstring"Quantity controls"No
requiredMessagestring"A value is required."No
minMessagestring"Value is below the minimum."No
maxMessagestring"Value is above the maximum."No